Checkpoint

This commit is contained in:
2026-03-09 16:43:24 +01:00
parent aed73e824e
commit 19bb022290
35 changed files with 1093 additions and 58 deletions

View File

@@ -1,3 +1,8 @@
#!/bin/sh
# Husky pre-commit hook: run lint-staged
# Generated by automation
npx --no-install lint-staged
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"

30
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,30 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v10.0.3
hooks:
- id: eslint
files: "\\.(js|jsx|ts|tsx)$"
exclude: node_modules/
# Use stylelint (local) instead of the deprecated scss-lint Ruby gem which
# cannot parse modern Sass `@use` and module syntax. This invokes the
# project's installed `stylelint` via `npx` so the devDependency is used.
- repo: local
hooks:
- id: stylelint
name: stylelint
entry: npx stylelint --fix
language: system
files: "\\.(scss|sass|css)$"

View File

@@ -19,6 +19,9 @@
"width", "width",
"height" "height"
], ],
"max-nesting-depth": 3 "max-nesting-depth": 3,
"scss/no-global-function-names": null,
"declaration-block-single-line-max-declarations": null,
"selector-class-pattern": "^[a-z0-9]+(?:-+[a-z0-9]+)*$"
} }
} }

View File

@@ -34,6 +34,11 @@ are stored in that base folder, more specific Components that are only used by o
get their own subfolder. Components have short, but descriptive names often mathing the type of tag, get their own subfolder. Components have short, but descriptive names often mathing the type of tag,
container or view they represent. container or view they represent.
**React/Typescript convention:**
- Use imports, multi imports from the same package are split over multiple lines so it generates clean diffs
- Imports are ordered: first React and React plugin imports, then third party imports, then local code
imports and the last imports are stylesheets. Global stylesheets are imported before local imports.
## Testing ## Testing
Run the specific npm test commands for when making changes: Run the specific npm test commands for when making changes:

View File

@@ -7,7 +7,7 @@
<title>cyberduck</title> <title>cyberduck</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;600;700&family=DotGothic16&family=Rajdhani:wght@400;600;700&family=Orbitron:wght@400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&family=Rajdhani:wght@400;600;700&family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

61
package-lock.json generated
View File

@@ -15,7 +15,9 @@
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"react": "^19.2.0", "react": "^19.2.0",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-dom": "^19.2.0" "react-dom": "^19.2.0",
"react-router": "^7.13.1",
"react-router-dom": "^7.13.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
@@ -3287,6 +3289,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -5193,6 +5208,44 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
"integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
"integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
"license": "MIT",
"dependencies": {
"react-router": "7.13.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-transition-group": { "node_modules/react-transition-group": {
"version": "4.4.5", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -5404,6 +5457,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -10,9 +10,9 @@
"lint:css": "stylelint \"src/**/*.{scss,css,sass}\"", "lint:css": "stylelint \"src/**/*.{scss,css,sass}\"",
"format:css": "prettier --write \"src/**/*.{scss,css,sass}\"", "format:css": "prettier --write \"src/**/*.{scss,css,sass}\"",
"prepare": "husky install", "prepare": "husky install",
"styles:build": "sass --no-source-map --style=compressed src:dist", "styles:build": "sass --no-source-map --style=compressed --load-path=node_modules src:dist",
"styles:check": "sass --no-source-map --update src:dist", "styles:check": "sass --no-source-map --update --load-path=node_modules src:dist",
"styles:dev": "sass --watch --no-source-map src:dist", "styles:dev": "sass --watch --no-source-map --load-path=node_modules src:dist",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
@@ -23,7 +23,9 @@
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"react": "^19.2.0", "react": "^19.2.0",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-dom": "^19.2.0" "react-dom": "^19.2.0",
"react-router": "^7.13.1",
"react-router-dom": "^7.13.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",

View File

@@ -1,6 +1,32 @@
import Showcase from './Showcase' import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
import Showcase from './pages/Showcase'
import Home from './pages/Home'
import LayoutShowcase from './pages/LayoutShowcase'
import LayoutHorizontal from './pages/LayoutHorizontal'
import LayoutVertical from './pages/LayoutVertical'
function App() { function App() {
return <Showcase /> return (
<BrowserRouter>
<header className="p-2 bg-light border-bottom">
<nav className="container d-flex gap-3">
<Link to="/">Home</Link>
<Link to="/showcase">Showcase</Link>
<Link to="/layout/showcase">Layout Showcase</Link>
<Link to="/layout/horizontal">Layout Horizontal</Link>
<Link to="/layout/vertical">Layout Vertical</Link>
</nav>
</header>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/showcase" element={<Showcase />} />
<Route path="/layout/showcase" element={<LayoutShowcase />} />
<Route path="/layout/horizontal" element={<LayoutHorizontal />} />
<Route path="/layout/vertical" element={<LayoutVertical />} />
</Routes>
</BrowserRouter>
)
} }
export default App export default App

View File

@@ -0,0 +1,11 @@
import React from 'react'
const Footer: React.FC = () => {
return (
<footer className="footer mt-auto py-3 bg-dark text-light">
<div className="container text-center">© CyberDuck</div>
</footer>
)
}
export default Footer

View File

@@ -0,0 +1,21 @@
import React from 'react'
import Navbar from './Navbar'
import Footer from './Footer'
interface LayoutProps {
children: React.ReactNode
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<div className="app-layout d-flex flex-column">
<Navbar />
<main className="app-main d-flex flex-fill">
{children}
</main>
<Footer />
</div>
)
}
export default Layout

View File

@@ -0,0 +1,13 @@
import React from 'react'
const Navbar: React.FC = () => {
return (
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
<div className="container-fluid">
<a className="navbar-brand" href="#">CyberDuck</a>
</div>
</nav>
)
}
export default Navbar

View File

@@ -0,0 +1,148 @@
import React from 'react'
export type AccentKey =
| 'primary'
| 'warning'
| 'info'
| 'success'
| 'danger'
| 'secondary'
| 'green'
| 'pink'
| 'yellow'
| 'light'
| 'dark'
export interface PanelProps extends React.HTMLAttributes<HTMLDivElement> {
size?: number
variant?: 'border' | 'solid'
accent?: AccentKey
children?: React.ReactNode
}
const Panel: React.FC<PanelProps> = ({ children }) => <>{children}</>
Panel.displayName = 'SplitPanel'
interface SplitProps {
direction?: 'horizontal' | 'vertical'
gutter?: string | number
className?: string
// allow any children but we will look for Panel elements
children?: React.ReactNode
variant?: 'border' | 'solid'
accent?: AccentKey
}
const Split: React.FC<SplitProps> & { Panel: React.FC<PanelProps> } = ({
direction = 'horizontal',
gutter = 20,
className = '',
children,
variant: splitVariant,
accent: splitAccent,
}) => {
const childrenArray = React.Children.toArray(children)
if (childrenArray.length < 1) {
throw new Error('Split requires at least one child Panel')
}
const gutterVal = typeof gutter === 'number' ? `${gutter}px` : gutter
const containerStyle: React.CSSProperties = { ['--split-gutter' as any]: gutterVal }
const dirClass = direction === 'vertical' ? 'split--vertical' : 'split--horizontal'
// detect Panel children and extract sizes and passthrough props
const sizes: (number | null)[] = childrenArray.map((c) => {
if (React.isValidElement(c) && (c.type as any).displayName === 'SplitPanel') {
const p = c as React.ReactElement<PanelProps>
return typeof p.props.size === 'number' ? p.props.size : 1
}
return null
})
const allPanels = sizes.every((s) => s != null)
// compute flex styles if panels provided sizes
return (
<div className={`split ${dirClass} ${className}`.trim()} style={containerStyle}>
{childrenArray.map((child, idx) => {
const style: React.CSSProperties = {}
let extraClass = ''
let extraStyle: React.CSSProperties | undefined
if (React.isValidElement(child) && (child.type as any).displayName === 'SplitPanel') {
const p = child as React.ReactElement<PanelProps>
extraClass = p.props.className || ''
extraStyle = p.props.style
// inherit variant/accent from parent if panel doesn't specify one
if (!p.props.variant && splitVariant) {
// handled below
}
if (!p.props.accent && splitAccent) {
// handled below
}
}
if (allPanels) {
const unit = (sizes[idx] || 0) as number
style.flex = `${unit} 1 0`
}
const mergedStyle = extraStyle ? { ...style, ...extraStyle } : style
const childElem = React.isValidElement(child) ? (child as React.ReactElement<PanelProps>) : null
const variant = childElem ? childElem.props.variant ?? splitVariant : splitVariant
// Determine accent: a panel with its own `accent` always uses it.
// Otherwise, if the parent `Split` has an `accent`, only the first
// panel (top-left decoration) and the last panel (bottom-right)
// receive the inherited accent. No other panels inherit the split
// accent.
const panelHasOwnAccent = !!(childElem && childElem.props.accent)
let accent: AccentKey | undefined = undefined
let accentTL = false
let accentBR = false
const lastIndex = childrenArray.length - 1
const isFirst = idx === 0
const isLast = idx === lastIndex
if (panelHasOwnAccent) {
// panel-specified accent: show both decorations
accent = childElem!.props.accent as AccentKey
accentTL = true
accentBR = true
} else if (splitAccent) {
// inherited accent from Split: only first gets top-left, last gets bottom-right
if (isFirst) {
accent = splitAccent
accentTL = true
}
if (isLast) {
accent = splitAccent
accentBR = true
}
}
let variantClass = ''
if (variant === 'border') variantClass = 'pane--border'
else if (variant === 'solid') variantClass = 'pane--solid'
const paneClass = `pane ${variantClass} ${extraClass} ${accentTL ? 'pane--accent--tl' : ''} ${accentBR ? 'pane--accent--br' : ''}`.trim()
// attach accent color as CSS var for the mixin to pick up
const finalStyle = { ...mergedStyle }
if (accent) {
;(finalStyle as any)['--pane-accent-color'] = `var(--color-variant-${accent})`
}
return (
<div className={paneClass} key={idx} style={finalStyle}>
{child}
</div>
)
})}
</div>
)
}
Split.Panel = Panel
export { Split }
export default Split

16
src/pages/Home.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { Link } from 'react-router-dom'
export default function Home() {
return (
<div className="container py-4">
<h1>Welcome</h1>
<p>Navigate to the demo pages:</p>
<ul>
<li><Link to="/showcase">Showcase</Link></li>
<li><Link to="/layout/showcase">Layout Showcase</Link></li>
<li><Link to="/layout/horizontal">Layout Horizontal</Link></li>
<li><Link to="/layout/vertical">Layout Vertical</Link></li>
</ul>
</div>
)
}

View File

@@ -0,0 +1,17 @@
import Layout from '../components/Layout/Layout'
import Split from '../components/Layout/Split'
export default function LayoutHorizontal() {
return (
<Layout>
<Split direction="horizontal" gutter={16} variant="solid">
<Split.Panel size={1} accent="green">
<div className="p-3">Left pane (1)</div>
</Split.Panel>
<Split.Panel size={2} variant="border">
<div className="p-3">Right pane (2) overridden border</div>
</Split.Panel>
</Split>
</Layout>
)
}

View File

@@ -0,0 +1,34 @@
import Layout from '../components/Layout/Layout'
import Split from '../components/Layout/Split'
export default function LayoutShowcase() {
return (
<Layout>
<div className="container-fluid py-4">
<h3>Horizontal Split</h3>
<div style={{ height: 240 }} className="mb-4">
<Split direction="horizontal" gutter={12} variant="solid">
<Split.Panel size={1} accent="yellow">
<div className="p-3">Left</div>
</Split.Panel>
<Split.Panel size={2}>
<div className="p-3">Right</div>
</Split.Panel>
</Split>
</div>
<h3>Vertical Split</h3>
<div style={{ height: 360 }} className="mb-4">
<Split direction="vertical" gutter={12} accent="pink">
<Split.Panel size={1}>
<div className="p-3">Top</div>
</Split.Panel>
<Split.Panel size={3}>
<div className="p-3">Bottom</div>
</Split.Panel>
</Split>
</div>
</div>
</Layout>
)
}

View File

@@ -0,0 +1,17 @@
import Layout from '../components/Layout/Layout'
import Split from '../components/Layout/Split'
export default function LayoutVertical() {
return (
<Layout>
<Split direction="vertical" gutter={12} variant="border">
<Split.Panel size={1} accent="yellow">
<div className="p-3">Top pane (1)</div>
</Split.Panel>
<Split.Panel size={3} variant="solid" accent="primary">
<div className="p-3">Bottom pane (3) solid override</div>
</Split.Panel>
</Split>
</Layout>
)
}

View File

@@ -192,6 +192,46 @@ export default function Showcase() {
</Form> </Form>
</Col> </Col>
</Row> </Row>
<Row className="g-3">
<Col xs={12}>
<div className="mt-3">
<h5>Buttons</h5>
<div className="d-flex flex-wrap gap-2">
{['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'].map((v) => (
<Button key={v} variant={v} className="me-1">
{v}
</Button>
))}
{['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'].map((v) => (
<Button key={`o-${v}`} variant={`outline-${v}`} className="me-1">
outline {v}
</Button>
))}
<Button variant="primary" active className="me-1">
Active
</Button>
<Button variant="secondary" disabled>
Disabled
</Button>
</div>
</div>
</Col>
</Row>
<Row className="g-3 mt-3">
<Col xs={12} md={6} lg={12}>
<h5>Shadow Card</h5>
<div className="card-shadow-wrap accent-yellow mb-0">
<Card>
<Card.Body>
<Card.Title>Shadow + Neon Rim</Card.Title>
<Card.Text>Uses the `card-shadow` wrapper with the `accent-yellow` utility.</Card.Text>
</Card.Body>
</Card>
</div>
</Col>
</Row>
</Col> </Col>
</Row> </Row>
</Col> </Col>
@@ -241,6 +281,9 @@ export default function Showcase() {
<Badge bg={variant} className="me-2"> <Badge bg={variant} className="me-2">
{variant} {variant}
</Badge> </Badge>
<Badge bg={variant} className="me-2" pill>
{variant}
</Badge>
</> </>
) )
})()} })()}

View File

@@ -6,6 +6,25 @@
border: 1px solid var(--color-border-default); border: 1px solid var(--color-border-default);
border-radius: 0; border-radius: 0;
// Default chamfer both top-right and bottom-left
@include cd-chamfered-sides(true, true, $fillet);
position: relative;
overflow: hidden;
}
// Mixin: flexible chamfer clip-path allowing selective corners
@mixin cd-chamfered-sides($top-right: false, $bottom-left: false, $fillet: vars.$cyberduck-border-fillet) {
--cd-list-fillet: var(--cyberduck-border-fillet, #{$fillet});
border-radius: 0;
// Only build and apply a clip-path if either corner is requested
@if $top-right or $bottom-left {
border: 1px solid var(--color-border-default);
@if $top-right and $bottom-left {
clip-path: polygon( clip-path: polygon(
0 0, 0 0,
calc(100% - var(--cd-list-fillet)) 0, calc(100% - var(--cd-list-fillet)) 0,
@@ -14,8 +33,24 @@
var(--cd-list-fillet) 100%, var(--cd-list-fillet) 100%,
0 calc(100% - var(--cd-list-fillet)) 0 calc(100% - var(--cd-list-fillet))
); );
position: relative; } @else if $top-right {
overflow: hidden; clip-path: polygon(
0 0,
calc(100% - var(--cd-list-fillet)) 0,
100% var(--cd-list-fillet),
100% 100%,
0 100%
);
} @else if $bottom-left {
clip-path: polygon(
0 0,
100% 0,
100% 100%,
var(--cd-list-fillet) 100%,
0 calc(100% - var(--cd-list-fillet))
);
}
}
} }
// Mixin: add corner accent L-shape at top-left and bottom-right // Mixin: add corner accent L-shape at top-left and bottom-right
@@ -64,4 +99,44 @@
} }
} }
// Accent only at top-left
@mixin cd-accent-top-left($color, $size: vars.$cyberduck-accent-size, $length: vars.$cyberduck-accent-length) {
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: $length;
height: $length;
pointer-events: none;
background-image:
linear-gradient(to right, $color 0 $size, transparent $size),
linear-gradient(to bottom, $color 0 $size, transparent $size);
background-repeat: no-repeat;
background-position: left top, left top;
z-index: 1000;
}
}
// Accent only at bottom-right
@mixin cd-accent-bottom-right($color, $size: vars.$cyberduck-accent-size, $length: vars.$cyberduck-accent-length) {
position: relative;
&::after {
content: '';
position: absolute;
right: 0;
bottom: 0;
width: $length;
height: $length;
pointer-events: none;
background-image:
linear-gradient(to left, $color 0 $size, transparent $size),
linear-gradient(to top, $color 0 $size, transparent $size);
background-repeat: no-repeat;
background-position: right bottom, right bottom;
z-index: 1000;
}
}

View File

@@ -14,6 +14,17 @@ $cyberduck-yellow: (
800: #806000, 800: #806000,
900: #4d3000 900: #4d3000
); );
$cyberduck-green: (
100: #b3f8f2,
200: #80f4ea,
300: #4df0e2,
400: #27f9eb,
500: #00e6d2,
600: #00b3a5,
700: #008078,
800: #004d4b,
900: #00201e
);
@use 'sass:map' as map; @use 'sass:map' as map;
@@ -45,7 +56,7 @@ $cyberduck-chrome: (
// Neon variant colors for Bootstrap semantic variants // Neon variant colors for Bootstrap semantic variants
$cyberduck-neon: ( $cyberduck-neon: (
primary: map.get($cyberduck-yellow, 500), primary: map.get($cyberduck-green, 500),
warning: map.get($cyberduck-yellow, 500), warning: map.get($cyberduck-yellow, 500),
info: #00f0ff, info: #00f0ff,
success: #00ff7a, success: #00ff7a,
@@ -60,6 +71,7 @@ $cyberduck-neon: (
); );
// Border and accent sizes // Border and accent sizes
$cyberduck-border-color: map.get($cyberduck-void, 100);
$cyberduck-border-fillet: 10px; $cyberduck-border-fillet: 10px;
$cyberduck-accent-size: 2px; $cyberduck-accent-size: 2px;
@@ -79,7 +91,7 @@ $cyberduck-font-body:
arial !default; arial !default;
// `font-mono` uses DotGothic16 as primary (pixel/mono display), with common monospaced fallbacks // `font-mono` uses DotGothic16 as primary (pixel/mono display), with common monospaced fallbacks
$cyberduck-font-mono: 'DotGothic16', monaco, 'Menlo', 'Consolas', 'Liberation Mono', monospace !default; $cyberduck-font-mono: 'JetBrains Mono', monaco, 'Menlo', 'Consolas', 'Liberation Mono', monospace !default;
// `font-display` for headings/brand using Rajdhani / Orbitron then system sans // `font-display` for headings/brand using Rajdhani / Orbitron then system sans
$cyberduck-font-display: $cyberduck-font-display:
@@ -89,6 +101,17 @@ $cyberduck-font-display:
-apple-system, -apple-system,
sans-serif !default; sans-serif !default;
// Text scales
$cyberduck-text-xs: 0.64rem; // clamp(0.64rem, 0.59rem + 0.24vw, 0.75rem);
$cyberduck-text-sm: 0.80rem; // clamp(0.8rem, 0.74rem + 0.32vw, 0.94rem);
$cyberduck-text-base: 1.00rem; // clamp(1rem, 0.93rem + 0.37vw, 1.18rem);
$cyberduck-text-lg: 1.25rem; // clamp(1.25rem, 1.16rem + 0.47vw, 1.47rem);
$cyberduck-text-xl: 1.56rem; // clamp(1.56rem, 1.45rem + 0.59vw, 1.84rem);
$cyberduck-text-2xl: 1.95rem; // clamp(1.95rem, 1.81rem + 0.74vw, 2.3rem);
$cyberduck-text-3xl: 2.44rem; // clamp(2.44rem, 2.26rem + 0.92vw, 2.87rem);
$cyberduck-text-4xl: 3.05rem; // clamp(3.05rem, 2.83rem + 1.15vw, 3.58rem);
$cyberduck-text-5xl: 3.81rem; // clamp(3.81rem, 3.54rem + 1.44vw, 4.48rem);
// Emit CSS custom properties from the SASS maps so there's a single source of truth // Emit CSS custom properties from the SASS maps so there's a single source of truth
:root { :root {
// yellow shades // yellow shades
@@ -108,13 +131,13 @@ $cyberduck-font-display:
// neon variant tokens // neon variant tokens
@each $k, $v in $cyberduck-neon { @each $k, $v in $cyberduck-neon {
--color-variant-#{$k}: #{$v}; --color-variant-#{"" + $k}: #{$v};
} }
// Semantic color tokens (defaults mapped to CyberDuck palette) // Semantic color tokens (defaults mapped to CyberDuck palette)
--color-bg-primary: var(--cyberduck-void-500); --color-bg-primary: var(--cyberduck-void-800);
--color-bg-secondary: var(--cyberduck-void-400); --color-bg-secondary: var(--cyberduck-void-400);
--color-bg-tertiary: var(--cyberduck-void-300); --color-bg-tertiary: var(--cyberduck-void-200);
--color-bg-elevated: var(--cyberduck-void-200); --color-bg-elevated: var(--cyberduck-void-200);
--color-text-primary: var(--cyberduck-chrome-100); --color-text-primary: var(--cyberduck-chrome-100);
--color-text-secondary: var(--cyberduck-chrome-300); --color-text-secondary: var(--cyberduck-chrome-300);
@@ -127,6 +150,20 @@ $cyberduck-font-display:
// legacy single-family var (points to body) for backward compatibility // legacy single-family var (points to body) for backward compatibility
--cyberduck-font-family: var(--cyberduck-font-body); --cyberduck-font-family: var(--cyberduck-font-body);
--cyberduck-border-fillet: #{$cyberduck-border-fillet}; --cyberduck-border-fillet: #{$cyberduck-border-fillet};
/* Shadow radius used by card underglow (adjustable) */
--cd-shadow-radius: 5px;
// Type Scale
--text-xs: #{$cyberduck-text-xs};
--text-sm: #{$cyberduck-text-sm};
--text-base: #{$cyberduck-text-base};
--text-lg: #{$cyberduck-text-lg};
--text-xl: #{$cyberduck-text-xl};
--text-2xl: #{$cyberduck-text-2xl};
--text-3xl: #{$cyberduck-text-3xl};
--text-4xl: #{$cyberduck-text-4xl};
--text-5xl: #{$cyberduck-text-5xl};
} }
// Lightweight helper to read values from maps // Lightweight helper to read values from maps

View File

@@ -6,12 +6,15 @@
.badge { .badge {
border-radius: 3px; border-radius: 3px;
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
font-weight: 600; font-weight: 700;
text-transform: none; text-transform: uppercase;
font-family: vars.$cyberduck-font-display;
white-space: nowrap;
color: map.get(vars.$cyberduck-neon, primary) !important;
} }
// Darken factor for badge backgrounds (keeps hue but a bit deeper) // Darken factor for badge backgrounds (keeps hue but a bit deeper)
$badge-darken: 12%; $badge-darken: 42%;
$variants: primary secondary success danger warning info light dark; $variants: primary secondary success danger warning info light dark;
@each $v in $variants { @each $v in $variants {
@@ -27,13 +30,8 @@ $variants: primary secondary success danger warning info light dark;
@if $v == light { @if $v == light {
color: vars.cd-color(vars.$cyberduck-chrome, 800) !important; color: vars.cd-color(vars.$cyberduck-chrome, 800) !important;
} @else { } @else {
color: #fff !important; color: $base !important;
} }
} }
} }
// Force small radius even when `rounded-pill` is present
.badge.rounded-pill {
border-radius: 3px !important;
}
} }

View File

@@ -11,3 +11,27 @@
color: var(--cyberduck-yellow-500); color: var(--cyberduck-yellow-500);
} }
} }
// When buttons are grouped, only round the bottom-left corner of the
// first child and the top-right corner of the last child to match
// the CyberDuck chamfered motif.
.btn-group {
display: inline-flex;
.btn {
border-radius: 0;
}
.btn:first-child {
border-radius: 0 0 0 var(--cyberduck-border-fillet);
}
.btn:last-child {
border-radius: 0 var(--cyberduck-border-fillet) 0 0;
}
// Middle buttons should have no rounding
.btn:not(:first-child, :last-child) {
border-radius: 0;
}
}

View File

@@ -0,0 +1,115 @@
@use '../variables' as vars;
@use 'sass:map';
// Button overrides for CyberDuck theme
.btn {
border-radius: 0;
border-top-right-radius: var(--cyberduck-border-fillet);
border-bottom-left-radius: var(--cyberduck-border-fillet);
padding-left: 0.75rem;
padding-right: 0.75rem;
font-weight: 700;
text-transform: uppercase;
font-family: vars.$cyberduck-font-display;
transition: filter 120ms ease, box-shadow 160ms ease, transform 80ms ease;
&:focus {
outline: none;
box-shadow: 0 0 10px rgb(0 0 0 / 22%);
}
&.active,
&:active {
transform: translateY(1px);
box-shadow: inset 0 2px 6px rgba(#000, 0.15);
}
&:disabled,
&.disabled {
opacity: 0.6;
pointer-events: none;
filter: none;
box-shadow: none;
}
}
// Explicit variant rules to avoid interpolation/type issues
$text-on-dark: map.get(vars.$cyberduck-chrome, 100);
$text-on-light: map.get(vars.$cyberduck-void, 900);
// Primary (override to black)
.btn-primary {
$c: map.get(vars.$cyberduck-void, 400);
background-color: $c;
border-color: darken($c, 6%);
color: $text-on-dark;
&:hover,
&:focus {
filter: brightness(0.94);
box-shadow: 0 0 14px rgba($c, 0.16);
}
&.active,
&:active {
background-color: darken($c, 6%);
border-color: darken($c, 10%);
}
&:disabled,
&.disabled { opacity: 0.6; pointer-events: none; }
}
.btn-outline-primary {
$c: map.get(vars.$cyberduck-void, 400);
color: var(--color-text-primary);
background-color: transparent;
border-color: $c;
&:hover,
&:focus {
background-color: rgba($c, 0.10);
color: $text-on-dark;
}
&.active,
&:active {
background-color: $c;
color: $text-on-dark;
border-color: darken($c, 6%);
}
&:disabled,
&.disabled { opacity: 0.6; pointer-events: none; }
}
// Map non-primary variants to reduce duplication
$nonprimary-variants: (secondary success danger warning info light dark);
@each $v in $nonprimary-variants {
$c: map.get(vars.$cyberduck-neon, $v);
.btn-#{$v} {
background-color: rgba($c, 0.75);
border-color: darken($c, 6%);
color: if($v == light, $text-on-light, $text-on-dark);
&:hover,
&:focus { filter: brightness(0.94); box-shadow: 0 0 14px rgba($c, 0.16); }
&.active, &:active { background-color: var(--color-text-primary); border-color: darken($c, 10%); }
&:disabled, &.disabled { opacity: 0.6; pointer-events: none; }
}
.btn-outline-#{$v} {
color: $c;
background-color: rgba($c, 0.05);
border-color: $c;
&:hover,
&:focus { background-color: rgba($c, 0.25); color: $text-on-dark; }
&.active, &:active { background-color: $c; color: $text-on-dark; border-color: darken($c, 6%); }
&:disabled, &.disabled { opacity: 0.6; pointer-events: none; }
}
}

View File

@@ -27,6 +27,47 @@
border-top: 1px solid var(--color-border-default); border-top: 1px solid var(--color-border-default);
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
}
// Shadow variant: adds a subtle drop shadow and lift on hover
.card-shadow {
/* default colored underglow using primary neon token */
--cd-shadow-color: #{rgba(vars.cd-color(vars.$cyberduck-neon, primary), 0.28)};
/* base dark drop shadow for depth plus colored underglow at right/bottom */
box-shadow: 0 6px 18px rgb(0 0 0 / 60%);
/* Right/bottom underglow with blur radius controlled by variable */
filter: drop-shadow(6px 8px var(--cd-shadow-radius) rgb(0 0 0 / 60%)) drop-shadow(8px 12px var(--cd-shadow-radius) var(--cd-shadow-color));
transition: filter 160ms ease, filter 160ms ease, transform 160ms ease, box-shadow 160ms ease;
will-change: filter, transform;
&:hover {
box-shadow: 0 18px 48px rgb(0 0 0 / 72%);
/* keep offsets larger on hover but use adjustable blur radius */
/* keep offsets larger on hover but keep blur controlled by variable */
filter: drop-shadow(10px 18px var(--cd-shadow-radius) rgb(0 0 0 / 72%)) drop-shadow(12px 24px var(--cd-shadow-radius) var(--cd-shadow-color));
/* keep visual lift applied to the inner card; wrapper handles the glow */
transform: none;
}
}
// Wrapper that holds the visual underglow so the glow isn't clipped
// by the card's chamfer clip-path. Use this as an outer wrapper around a
// `.card` element in the markup: <div class="card-shadow-wrap accent-yellow"><div class="card">...</div></div>
.card-shadow-wrap {
--cd-shadow-color: #{rgba(vars.cd-color(vars.$cyberduck-neon, primary), 0.28)};
display: inline-block;
/* right/bottom neon underglow using adjustable blur radius */
/* right/bottom neon underglow with blur controlled by variable */
filter: drop-shadow(8px 12px var(--cd-shadow-radius) var(--cd-shadow-color));
} }
// Ensure top image corners match card rounding when using .card-img-top // Ensure top image corners match card rounding when using .card-img-top
@@ -41,7 +82,7 @@
.card { .card {
&--accent-yellow, &--accent-yellow,
&.accent-yellow { &.accent-yellow {
@include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, primary)); @include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, yellow));
} }
// cyan is also known as green in some usages // cyan is also known as green in some usages
@@ -60,3 +101,23 @@
@include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, pink)); @include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, pink));
} }
} }
// Allow combining shadow with accent utilities (use same neon accents)
@each $k, $v in vars.$cyberduck-neon {
// stringify key to avoid color-name interpolation warnings
$name: "" + $k;
.card-shadow.card--accent-#{$name},
.card-shadow.accent-#{$name},
.card-shadow.card--accent-#{$name} {
/* set the underglow color for accented shadowed cards */
--cd-shadow-color: #{rgba($v, 0.32)};
@include mixins.cd-accent-container($v);
}
// Also support wrapper variant so the glow can live outside the clipped card
.card-shadow-wrap.card--accent-#{$name},
.card-shadow-wrap.accent-#{$name} {
--cd-shadow-color: #{rgba($v, 0.36)};
}
}

View File

@@ -140,11 +140,11 @@
`body` for older browsers (see index.html script that sets the class). */ `body` for older browsers (see index.html script that sets the class). */
body:has(.site-footer.fixed-bottom) { body:has(.site-footer.fixed-bottom) {
--site-footer-height: calc(var(--site-footer-height, 56px) + var(--site-footer-strip-height, 0px)); --site-footer-height: calc(var(--site-footer-height, 60px) + var(--site-footer-strip-height, 0px));
} }
body.has-fixed-footer { body.has-fixed-footer {
--site-footer-height: calc(var(--site-footer-height, 56px) + var(--site-footer-strip-height, 0px)); --site-footer-height: calc(var(--site-footer-height, 60px) + var(--site-footer-strip-height, 0px));
} }
/* Apply automatic bottom padding to any `main` so pages don't need ad-hoc /* Apply automatic bottom padding to any `main` so pages don't need ad-hoc

View File

@@ -31,5 +31,61 @@ label {
// Apply chamfered container style to form-group wrapper only // Apply chamfered container style to form-group wrapper only
.form-group { .form-group {
@include mixins.cd-chamfered-container; // keep existing simple group styling for generic use
}
// Treat a label combined with an input/select as a single chamfered card
.form-group:has(> label + .form-control),
.form-group:has(> label + .form-select) {
@include mixins.cd-chamfered-container;
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
> label {
margin: 0;
background: transparent;
padding: 0 0.25rem;
color: var(--color-text-secondary);
}
> .form-control,
> .form-select {
border: none;
background-color: transparent;
box-shadow: none;
margin: 0;
padding: 0.5rem;
border-radius: 0;
}
}
// Treat input groups as a single chamfered card
.input-group {
@include mixins.cd-chamfered-container;
display: flex;
align-items: center;
gap: 0;
padding: 0.125rem;
> .form-control,
> .form-select {
border: none;
background-color: transparent;
box-shadow: none;
margin: 0;
padding: 0.5rem 0.75rem;
border-radius: 0;
flex: 1 1 auto;
}
> .input-group-text {
background: transparent;
border: none;
padding: 0 0.5rem;
color: var(--color-text-primary);
}
} }

View File

@@ -0,0 +1,93 @@
/* Layout and Split styles */
@use '../mixins' as mixins;
@use '../variables' as vars;
/* App layout: main area fills viewport between navbar and footer */
.app-layout { height: 100vh; display: flex; flex-direction: column; }
.app-main { flex: 1 1 auto; overflow: hidden; width: 100%; min-height: 0; }
.app-navbar { z-index: 10; }
.app-footer { flex: 0 0 auto; }
/* Split container: use negative margins and per-pane half-margin so gutters
appear around every pane without doubling between panes. */
.split {
--split-gutter: 20px;
display: flex;
width: 100%;
height: 100%;
box-sizing: border-box;
gap: var(--split-gutter);
padding: var(--split-gutter);
}
.split--horizontal { flex-direction: row; }
.split--vertical { flex-direction: column; }
.split > .pane {
box-sizing: border-box;
overflow: auto;
margin: 0;
min-width: 0;
min-height: 0;
flex: 1 1 0;
}
/* Panel variants */
.pane--border {
border: 1px solid rgb(255 255 255 / 6%);
}
.pane--solid {
border: 1px solid rgb(255 255 255 / 6%);
background-color: rgb(255 255 255 / 2%);
}
/* Accent handling: pane can declare an accent color via --pane-accent-color
which is used by the accent mixins. We support separate classes for top-left
and bottom-right decorations so they can be applied independently. */
/* Both TL and BR present (panel-specified accent): render full container accents */
.pane--accent--tl.pane--accent--br {
@include mixins.cd-accent-container(var(--pane-accent-color, var(--color-variant-primary)));
}
/* Single-side accents: top-left */
.pane--accent--tl {
@include mixins.cd-accent-top-left(var(--pane-accent-color, var(--color-variant-primary)));
}
/* Single-side accents: bottom-right */
.pane--accent--br {
@include mixins.cd-accent-bottom-right(var(--pane-accent-color, var(--color-variant-primary)));
}
/* First and last pane accents when inheriting from Split: first gets TL, last gets BR */
.split > .pane:first-child.pane--accent--tl {
@include mixins.cd-accent-top-left(var(--pane-accent-color, var(--color-variant-primary)));
}
.split > .pane:last-child.pane--accent--br {
@include mixins.cd-accent-bottom-right(var(--pane-accent-color, var(--color-variant-primary)));
}
/* Apply chamfer styles to first and last panes: first pane gets bottom-left
chamfered, last pane gets top-right chamfered. Other panes remain straight. */
.split--horizontal > .pane:first-child {
@include mixins.cd-chamfered-sides(false, true, vars.$cyberduck-border-fillet);
}
.split--horizontal > .pane:last-child {
@include mixins.cd-chamfered-sides(true, false, vars.$cyberduck-border-fillet);
}
/* For vertical splits the chamfer orientation flips: topmost gets a top
chamfer, bottommost gets a bottom chamfer. */
.split--vertical > .pane:first-child {
@include mixins.cd-chamfered-sides(true, false, vars.$cyberduck-border-fillet);
}
.split--vertical > .pane:last-child {
@include mixins.cd-chamfered-sides(false, true, vars.$cyberduck-border-fillet);
}

View File

@@ -4,6 +4,8 @@
// ListGroup overrides scaffold // ListGroup overrides scaffold
.list-group { .list-group {
background: transparent; background: transparent;
font-weight: 500;
font-family: var(vars.$cyberduck-font-mono);
/* container-only border style to match Cards: square TL & BR, chamfer other corners */ /* container-only border style to match Cards: square TL & BR, chamfer other corners */
@include mixins.cd-chamfered-container; @include mixins.cd-chamfered-container;
@@ -22,7 +24,7 @@
// Accent variants for list-group container (mirror card accent variants) // Accent variants for list-group container (mirror card accent variants)
&--accent-yellow, &--accent-yellow,
&.accent-yellow { &.accent-yellow {
@include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, primary)); @include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, yellow));
} }
&--accent-cyan, &--accent-cyan,

View File

@@ -1,22 +1,91 @@
@use '../variables' as vars; @use '../variables' as vars;
@use '../mixins' as cdmixins;
@use 'sass:color' as color;
@use 'sass:map' as map;
.modal-content { .modal-content {
// Chamfered container (top-right & bottom-left) used across CyberDuck
@include cdmixins.cd-chamfered-container;
background: linear-gradient(180deg, var(--cyberduck-void-400), var(--cyberduck-void-300)); background: linear-gradient(180deg, var(--cyberduck-void-400), var(--cyberduck-void-300));
border: 1px solid var(--color-border-default);
color: var(--color-text-primary); color: var(--color-text-primary);
} }
.modal-header, .modal-header,
.modal-footer { .modal-footer {
border-color: transparent; border-color: var(--color-border-default);
background-color: var(--cyberduck-void-200);
}
.modal-header {
border-bottom: 1px solid var(--color-border-default);
}
.modal-footer {
border-top: 1px solid var(--color-border-default);
} }
.modal-backdrop.show { .modal-backdrop.show {
background-color: rgb(2 6 10 / 60%); background-color: rgb(2 6 10 / 60%);
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
z-index: 2000;
} }
.modal-title { .modal-title {
font-family: vars.$cyberduck-font-display; font-family: var(--cyberduck-font-display);
letter-spacing: 0.04em; letter-spacing: 0.04em;
text-transform: uppercase;
}
// Ensure modal appears above other fixed elements (footer/nav)
.modal {
z-index: 2005;
}
// Accent variants for modals using CyberDuck neon tokens.
// Usage: add `.modal-accent-<variant>` to the `.modal` element (e.g. `.modal-accent-danger`).
@each $k, $v in vars.$cyberduck-neon {
.modal-accent-#{'#{ $k }'} {
.modal-content {
@include cdmixins.cd-accent-container($v);
}
// Subtle header/footer tint for better contrast on accent variants
.modal-header,
.modal-footer {
background-color: rgba($v, 0.04);
border-color: rgba($v, 0.12);
}
}
}
// Style the bootstrap close button inside modals as a squared box
// with a border using the CyberDuck neon danger token.
.modal .btn-close {
width: 2rem;
height: 2rem;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
background-image: none !important;
background-color: transparent;
border: 1px solid var(--color-border-default);
color: var(--color-chrome-50);
border-radius: 0.25rem;
box-shadow: none;
}
.modal .btn-close::after {
content: '';
font-size: 1rem;
line-height: 1;
color: inherit;
}
.modal .btn-close:focus,
.modal .btn-close:hover {
background-color: color.adjust(map.get(vars.$cyberduck-neon, danger), $lightness: -20%);
border-color: var(--color-variant-danger);
color: var(--color-variant-danger);
} }

View File

@@ -16,7 +16,7 @@
} }
.navbar-brand { .navbar-brand {
color: var(--color-variant-primary); color: var(--color-variant-green);
font-family: vars.$cyberduck-font-display; font-family: vars.$cyberduck-font-display;
font-variant-caps: small-caps; font-variant-caps: small-caps;
text-transform: uppercase; text-transform: uppercase;
@@ -27,7 +27,7 @@
/* Neon glow */ /* Neon glow */
text-shadow: text-shadow:
0 0 6px rgb(0 0 0 / 12%), 0 0 6px rgb(0 0 0 / 12%),
0 0 8px var(--color-variant-primary); 0 0 8px var(--color-variant-green);
&::before { &::before {
content: '//'; content: '//';

View File

@@ -3,7 +3,7 @@
// Table overrides scaffold // Table overrides scaffold
table { table {
background: transparent; background: transparent;
color: var(--color-text-primary); font-family: vars.$cyberduck-font-mono;
th, th,
td { td {
@@ -30,4 +30,11 @@ table {
color: var(--color-variant-green) !important; color: var(--color-variant-green) !important;
} }
} }
tbody {
td,
td * {
color: var(--color-text-primary);
}
}
} }

View File

@@ -1,14 +1,46 @@
@use '../variables' as vars; @use '../variables' as vars;
@use '../mixins' as mixins;
.tooltip, .tooltip,
.popover { .popover {
background: var(--cyberduck-void-400) !important;
color: var(--color-text-primary) !important; color: var(--color-text-primary) !important;
border: 1px solid var(--color-border-default) !important;
box-shadow: 0 6px 18px rgb(0 0 0 / 45%); box-shadow: 0 6px 18px rgb(0 0 0 / 45%);
font-family: vars.$cyberduck-font-body; font-family: var(--cyberduck-font-display);
} }
// Tooltip: apply chamfered top-right & bottom-left to the inner panel
.tooltip .tooltip-inner { .tooltip .tooltip-inner {
@include mixins.cd-chamfered-container;
background: var(--cyberduck-void-400) !important; background: var(--cyberduck-void-400) !important;
// make sure our border color wins over Bootstrap
border: 1px solid var(--color-border-default) !important;
padding: 0.5rem 0.75rem !important;
}
.tooltip .arrow {
display: none !important;
}
// Popover: chamfer the overall popover container
.popover {
@include mixins.cd-chamfered-container;
background: var(--cyberduck-void-400) !important;
// override bootstrap arrow to avoid visual mismatch
.popover-arrow {
display: none !important;
}
.popover-header {
border-bottom: 1px solid var(--color-border-default);
background: transparent;
padding: 0.5rem 0.75rem;
}
.popover-body {
padding: 0.5rem 0.75rem;
}
} }

View File

@@ -2,15 +2,27 @@
// Configure Bootstrap with our customisations // Configure Bootstrap with our customisations
@use 'bootstrap/scss/bootstrap' with ( @use 'bootstrap/scss/bootstrap' with (
// Fonts
$font-family-sans-serif: vars.$cyberduck-font-body, $font-family-sans-serif: vars.$cyberduck-font-body,
$font-family-monospace: vars.$cyberduck-font-mono, $font-family-monospace: vars.$cyberduck-font-mono,
$headings-font-family: vars.$cyberduck-font-display, $headings-font-family: vars.$cyberduck-font-display,
// Text sizes
$font-size-base: vars.$cyberduck-text-base,
$font-size-sm: vars.$cyberduck-text-sm,
$font-size-lg: vars.$cyberduck-text-lg,
$h1-font-size: vars.$cyberduck-text-5xl,
$h2-font-size: vars.$cyberduck-text-4xl,
$h3-font-size: vars.$cyberduck-text-3xl,
$h4-font-size: vars.$cyberduck-text-2xl,
$h5-font-size: vars.$cyberduck-text-xl,
$h6-font-size: vars.$cyberduck-text-lg,
// Compile-time color values (SASS maps) so Bootstrap functions work // Compile-time color values (SASS maps) so Bootstrap functions work
$body-bg: vars.cd-color(vars.$cyberduck-void, 500), $body-bg: vars.cd-color(vars.$cyberduck-void, 800),
$body-color: vars.cd-color(vars.$cyberduck-chrome, 100), $body-color: vars.cd-color(vars.$cyberduck-chrome, 100),
$link-color: vars.cd-color(vars.$cyberduck-yellow, 300), $link-color: vars.cd-color(vars.$cyberduck-yellow, 300),
$border-color: vars.cd-color(vars.$cyberduck-void, 100), $border-color: vars.cd-color(vars.$cyberduck-void, 100),
$headings-color: vars.cd-color(vars.$cyberduck-yellow, 500), $headings-color: vars.cd-color(vars.$cyberduck-green, 500),
$tooltip-color: vars.cd-color(vars.$cyberduck-chrome, 500),
// Variant colors wired to neon SASS map // Variant colors wired to neon SASS map
$primary: vars.cd-color(vars.$cyberduck-neon, primary), $primary: vars.cd-color(vars.$cyberduck-neon, primary),
$warning: vars.cd-color(vars.$cyberduck-neon, warning), $warning: vars.cd-color(vars.$cyberduck-neon, warning),
@@ -19,10 +31,11 @@
$danger: vars.cd-color(vars.$cyberduck-neon, danger), $danger: vars.cd-color(vars.$cyberduck-neon, danger),
$secondary: vars.cd-color(vars.$cyberduck-neon, secondary), $secondary: vars.cd-color(vars.$cyberduck-neon, secondary),
$light: vars.cd-color(vars.$cyberduck-neon, light), $light: vars.cd-color(vars.$cyberduck-neon, light),
$dark: vars.cd-color(vars.$cyberduck-neon, dark) $dark: vars.cd-color(vars.$cyberduck-neon, dark),
// Buttons: rely on `$primary` variant for button colors (avoid non-!default vars) // Buttons: rely on `$primary` variant for button colors (avoid non-!default vars)
// Navbar: handled via component partials and semantic tokens // Navbar: handled via component partials and semantic tokens
// (avoid overriding non-!default Bootstrap vars here) $table-color: vars.cd-color(vars.$cyberduck-chrome, 100), // Override default table text color
$table-striped-color: vars.cd-color(vars.$cyberduck-chrome, 100), // Ensure striped rows also use primary text color
); );
@use 'components/card' as card; @use 'components/card' as card;
@use 'components/list-group' as listgroup; @use 'components/list-group' as listgroup;
@@ -31,12 +44,14 @@
@use 'components/nav-links' as navlinks; @use 'components/nav-links' as navlinks;
@use 'components/button-group' as buttongroup; @use 'components/button-group' as buttongroup;
@use 'components/badge' as badge; @use 'components/badge' as badge;
@use 'components/buttons' as buttons;
@use 'components/forms' as forms; @use 'components/forms' as forms;
@use 'components/modal' as modal; @use 'components/modal' as modal;
@use 'components/dropdown' as dropdown; @use 'components/dropdown' as dropdown;
@use 'components/input-group' as inputgroup; @use 'components/input-group' as inputgroup;
@use 'components/tooltip' as tooltip; @use 'components/tooltip' as tooltip;
@use 'components/footer' as footer; @use 'components/footer' as footer;
@use 'components/layout' as layout;
// CyberDuck theme entrypoint — wires variables and per-component overrides // CyberDuck theme entrypoint — wires variables and per-component overrides
@@ -57,6 +72,7 @@ h6 {
code, code,
kbd, kbd,
pre, pre,
samp { samp,
table {
font-family: var(--cyberduck-font-mono); font-family: var(--cyberduck-font-mono);
} }