diff --git a/.gitignore b/.gitignore
index fc6e165..a61cd08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -273,4 +273,4 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
-# End of https://www.toptal.com/developers/gitignore/api/node,react,sass,macos,linux,windows,visualstudiocode,vim
\ No newline at end of file
+# End of https://www.toptal.com/developers/gitignore/api/node,react,sass,macos,linux,windows,visualstudiocode,vim
diff --git a/.husky/pre-commit b/.husky/pre-commit
index f9a582d..64c2d63 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -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
. "$(dirname -- "$0")/_/husky.sh"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..2f8c7ac
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -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)$"
diff --git a/.stylelintrc.json b/.stylelintrc.json
index c99c1eb..798546f 100644
--- a/.stylelintrc.json
+++ b/.stylelintrc.json
@@ -19,6 +19,9 @@
"width",
"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]+)*$"
}
}
diff --git a/AGENTS.md b/AGENTS.md
index b8bd02b..299e588 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,13 +10,13 @@ Bootstrap core component gets its own file under `src/styles/components`.
**SCSS convention:**:
- Use modern `@use` and single-source SASS maps
-- Component code should prefer compile-time SASS colors when using Sass functions, and read emitted
+- Component code should prefer compile-time SASS colors when using Sass functions, and read emitted
CSS custom properties (`--color-variant-*`, `--cyberduck-void-*`, `--cyberduck-font-display`) for
runtime theming.
- Order `@use` statements to include sass-builtins first, then bootstrap includes, then local includes
**Fonts:**
-- Google fonts are included in `index.html`; the display font is referenced via `--cyberduck-font-display`
+- Google fonts are included in `index.html`; the display font is referenced via `--cyberduck-font-display`
and `$cyberduck-font-display`.
**Colors:**
@@ -24,7 +24,7 @@ Bootstrap core component gets its own file under `src/styles/components`.
- All other colors stick to the Cyberpunk theme and use vibrant, neon style color palettes.
**Where to change visuals:**
-- Edit partials under `src/styles/components` (e.g. `_navbar.scss`, `_card.scss`, `_footer.scss`).
+- Edit partials under `src/styles/components` (e.g. `_navbar.scss`, `_card.scss`, `_footer.scss`).
- Update tokens in `src/styles/_variables.scss` to change palette or fonts globally.
## React
@@ -34,12 +34,17 @@ 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,
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
Run the specific npm test commands for when making changes:
- For style updates run `npm run styles:check`
-Don't run `npm run dev` without permission; if you want visual QA, ask me to run it and I will report
+Don't run `npm run dev` without permission; if you want visual QA, ask me to run it and I will report
the URL and warnings.
## Changes strategy
diff --git a/index.html b/index.html
index 1a000a0..2c41ffc 100644
--- a/index.html
+++ b/index.html
@@ -7,7 +7,7 @@
cyberduck
-
+
diff --git a/package-lock.json b/package-lock.json
index f97900f..f90c0eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,9 @@
"bootstrap": "^5.3.8",
"react": "^19.2.0",
"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": {
"@eslint/js": "^9.39.1",
@@ -3287,6 +3289,19 @@
"dev": true,
"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": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -5193,6 +5208,44 @@
"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": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -5404,6 +5457,12 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/package.json b/package.json
index 094cf97..4e2a496 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,9 @@
"lint:css": "stylelint \"src/**/*.{scss,css,sass}\"",
"format:css": "prettier --write \"src/**/*.{scss,css,sass}\"",
"prepare": "husky install",
- "styles:build": "sass --no-source-map --style=compressed src:dist",
- "styles:check": "sass --no-source-map --update src:dist",
- "styles:dev": "sass --watch --no-source-map src:dist",
+ "styles:build": "sass --no-source-map --style=compressed --load-path=node_modules src:dist",
+ "styles:check": "sass --no-source-map --update --load-path=node_modules src:dist",
+ "styles:dev": "sass --watch --no-source-map --load-path=node_modules src:dist",
"preview": "vite preview"
},
"dependencies": {
@@ -23,7 +23,9 @@
"bootstrap": "^5.3.8",
"react": "^19.2.0",
"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": {
"@eslint/js": "^9.39.1",
diff --git a/public/vite.svg b/public/vite.svg
index e7b8dfb..ee9fada 100644
--- a/public/vite.svg
+++ b/public/vite.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/App.tsx b/src/App.tsx
index 2bf1ff4..73fb2fc 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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() {
- return
+ return (
+
+
+
+ Home
+ Showcase
+ Layout Showcase
+ Layout Horizontal
+ Layout Vertical
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ )
}
export default App
diff --git a/src/assets/react.svg b/src/assets/react.svg
index 6c87de9..8e0e0f1 100644
--- a/src/assets/react.svg
+++ b/src/assets/react.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/components/Layout/Footer.tsx b/src/components/Layout/Footer.tsx
new file mode 100644
index 0000000..64e6a8c
--- /dev/null
+++ b/src/components/Layout/Footer.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+
+const Footer: React.FC = () => {
+ return (
+
+ )
+}
+
+export default Footer
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
new file mode 100644
index 0000000..01875b2
--- /dev/null
+++ b/src/components/Layout/Layout.tsx
@@ -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 = ({ children }) => {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+export default Layout
diff --git a/src/components/Layout/Navbar.tsx b/src/components/Layout/Navbar.tsx
new file mode 100644
index 0000000..b818045
--- /dev/null
+++ b/src/components/Layout/Navbar.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+
+const Navbar: React.FC = () => {
+ return (
+
+
+
+ )
+}
+
+export default Navbar
diff --git a/src/components/Layout/Split.tsx b/src/components/Layout/Split.tsx
new file mode 100644
index 0000000..c8cd729
--- /dev/null
+++ b/src/components/Layout/Split.tsx
@@ -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 {
+ size?: number
+ variant?: 'border' | 'solid'
+ accent?: AccentKey
+ children?: React.ReactNode
+}
+
+const Panel: React.FC = ({ 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 & { Panel: React.FC } = ({
+ 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
+ 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 (
+
+ {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
+ 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) : 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 (
+
+ {child}
+
+ )
+ })}
+
+ )
+}
+
+Split.Panel = Panel
+export { Split }
+export default Split
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
new file mode 100644
index 0000000..55b7d87
--- /dev/null
+++ b/src/pages/Home.tsx
@@ -0,0 +1,16 @@
+import { Link } from 'react-router-dom'
+
+export default function Home() {
+ return (
+
+
Welcome
+
Navigate to the demo pages:
+
+ Showcase
+ Layout Showcase
+ Layout Horizontal
+ Layout Vertical
+
+
+ )
+}
diff --git a/src/pages/LayoutHorizontal.tsx b/src/pages/LayoutHorizontal.tsx
new file mode 100644
index 0000000..25d53dc
--- /dev/null
+++ b/src/pages/LayoutHorizontal.tsx
@@ -0,0 +1,17 @@
+import Layout from '../components/Layout/Layout'
+import Split from '../components/Layout/Split'
+
+export default function LayoutHorizontal() {
+ return (
+
+
+
+ Left pane (1)
+
+
+ Right pane (2) — overridden border
+
+
+
+ )
+}
diff --git a/src/pages/LayoutShowcase.tsx b/src/pages/LayoutShowcase.tsx
new file mode 100644
index 0000000..e41d44e
--- /dev/null
+++ b/src/pages/LayoutShowcase.tsx
@@ -0,0 +1,34 @@
+import Layout from '../components/Layout/Layout'
+import Split from '../components/Layout/Split'
+
+export default function LayoutShowcase() {
+ return (
+
+
+
Horizontal Split
+
+
+
+ Left
+
+
+ Right
+
+
+
+
+
Vertical Split
+
+
+
+ Top
+
+
+ Bottom
+
+
+
+
+
+ )
+}
diff --git a/src/pages/LayoutVertical.tsx b/src/pages/LayoutVertical.tsx
new file mode 100644
index 0000000..006acf2
--- /dev/null
+++ b/src/pages/LayoutVertical.tsx
@@ -0,0 +1,17 @@
+import Layout from '../components/Layout/Layout'
+import Split from '../components/Layout/Split'
+
+export default function LayoutVertical() {
+ return (
+
+
+
+ Top pane (1)
+
+
+ Bottom pane (3) — solid override
+
+
+
+ )
+}
diff --git a/src/Showcase.tsx b/src/pages/Showcase.tsx
similarity index 85%
rename from src/Showcase.tsx
rename to src/pages/Showcase.tsx
index cca0b2f..563f072 100644
--- a/src/Showcase.tsx
+++ b/src/pages/Showcase.tsx
@@ -192,6 +192,46 @@ export default function Showcase() {
+
+
+
+
Buttons
+
+ {['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'].map((v) => (
+
+ {v}
+
+ ))}
+
+ {['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'].map((v) => (
+
+ outline {v}
+
+ ))}
+
+
+ Active
+
+
+ Disabled
+
+
+
+
+
+
+
+ Shadow Card
+
+
+
+ Shadow + Neon Rim
+ Uses the `card-shadow` wrapper with the `accent-yellow` utility.
+
+
+
+
+
@@ -241,6 +281,9 @@ export default function Showcase() {
{variant}
+
+ {variant}
+
>
)
})()}
diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss
index aef6699..79ded1f 100644
--- a/src/styles/_mixins.scss
+++ b/src/styles/_mixins.scss
@@ -6,18 +6,53 @@
border: 1px solid var(--color-border-default);
border-radius: 0;
- clip-path: polygon(
- 0 0,
- calc(100% - var(--cd-list-fillet)) 0,
- 100% var(--cd-list-fillet),
- 100% 100%,
- var(--cd-list-fillet) 100%,
- 0 calc(100% - var(--cd-list-fillet))
- );
+
+ // 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(
+ 0 0,
+ calc(100% - var(--cd-list-fillet)) 0,
+ 100% var(--cd-list-fillet),
+ 100% 100%,
+ var(--cd-list-fillet) 100%,
+ 0 calc(100% - var(--cd-list-fillet))
+ );
+ } @else if $top-right {
+ 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
// Generalized name: cd-accent-container
@mixin cd-accent-container(
@@ -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;
+ }
+}
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
index 0ae3e81..6d4c69c 100644
--- a/src/styles/_variables.scss
+++ b/src/styles/_variables.scss
@@ -14,6 +14,17 @@ $cyberduck-yellow: (
800: #806000,
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;
@@ -45,7 +56,7 @@ $cyberduck-chrome: (
// Neon variant colors for Bootstrap semantic variants
$cyberduck-neon: (
- primary: map.get($cyberduck-yellow, 500),
+ primary: map.get($cyberduck-green, 500),
warning: map.get($cyberduck-yellow, 500),
info: #00f0ff,
success: #00ff7a,
@@ -60,6 +71,7 @@ $cyberduck-neon: (
);
// Border and accent sizes
+$cyberduck-border-color: map.get($cyberduck-void, 100);
$cyberduck-border-fillet: 10px;
$cyberduck-accent-size: 2px;
@@ -79,7 +91,7 @@ $cyberduck-font-body:
arial !default;
// `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
$cyberduck-font-display:
@@ -89,6 +101,17 @@ $cyberduck-font-display:
-apple-system,
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
:root {
// yellow shades
@@ -108,13 +131,13 @@ $cyberduck-font-display:
// neon variant tokens
@each $k, $v in $cyberduck-neon {
- --color-variant-#{$k}: #{$v};
+ --color-variant-#{"" + $k}: #{$v};
}
// 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-tertiary: var(--cyberduck-void-300);
+ --color-bg-tertiary: var(--cyberduck-void-200);
--color-bg-elevated: var(--cyberduck-void-200);
--color-text-primary: var(--cyberduck-chrome-100);
--color-text-secondary: var(--cyberduck-chrome-300);
@@ -127,6 +150,20 @@ $cyberduck-font-display:
// legacy single-family var (points to body) for backward compatibility
--cyberduck-font-family: var(--cyberduck-font-body);
--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
diff --git a/src/styles/components/_badge.scss b/src/styles/components/_badge.scss
index c5a97b8..8a6df96 100644
--- a/src/styles/components/_badge.scss
+++ b/src/styles/components/_badge.scss
@@ -6,12 +6,15 @@
.badge {
border-radius: 3px;
padding: 0.25em 0.5em;
- font-weight: 600;
- text-transform: none;
+ font-weight: 700;
+ 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)
- $badge-darken: 12%;
+$badge-darken: 42%;
$variants: primary secondary success danger warning info light dark;
@each $v in $variants {
@@ -27,13 +30,8 @@ $variants: primary secondary success danger warning info light dark;
@if $v == light {
color: vars.cd-color(vars.$cyberduck-chrome, 800) !important;
} @else {
- color: #fff !important;
+ color: $base !important;
}
}
}
-
- // Force small radius even when `rounded-pill` is present
- .badge.rounded-pill {
- border-radius: 3px !important;
- }
}
diff --git a/src/styles/components/_button-group.scss b/src/styles/components/_button-group.scss
index b5812c3..b7b5a03 100644
--- a/src/styles/components/_button-group.scss
+++ b/src/styles/components/_button-group.scss
@@ -11,3 +11,27 @@
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;
+ }
+}
diff --git a/src/styles/components/_buttons.scss b/src/styles/components/_buttons.scss
new file mode 100644
index 0000000..4c95cb8
--- /dev/null
+++ b/src/styles/components/_buttons.scss
@@ -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; }
+ }
+}
diff --git a/src/styles/components/_card.scss b/src/styles/components/_card.scss
index ef5f83c..0ca9a23 100644
--- a/src/styles/components/_card.scss
+++ b/src/styles/components/_card.scss
@@ -27,6 +27,47 @@
border-top: 1px solid var(--color-border-default);
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:
+.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
@@ -41,7 +82,7 @@
.card {
&--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
@@ -60,3 +101,23 @@
@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)};
+ }
+}
diff --git a/src/styles/components/_footer.scss b/src/styles/components/_footer.scss
index e2c4ccc..13ad53e 100644
--- a/src/styles/components/_footer.scss
+++ b/src/styles/components/_footer.scss
@@ -140,11 +140,11 @@
`body` for older browsers (see index.html script that sets the class). */
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 {
- --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
diff --git a/src/styles/components/_forms.scss b/src/styles/components/_forms.scss
index 2baecd3..b8a33e1 100644
--- a/src/styles/components/_forms.scss
+++ b/src/styles/components/_forms.scss
@@ -31,5 +31,61 @@ label {
// Apply chamfered container style to form-group wrapper only
.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);
+ }
}
diff --git a/src/styles/components/_layout.scss b/src/styles/components/_layout.scss
new file mode 100644
index 0000000..d0bcf7c
--- /dev/null
+++ b/src/styles/components/_layout.scss
@@ -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);
+}
diff --git a/src/styles/components/_list-group.scss b/src/styles/components/_list-group.scss
index 7538aa8..1773631 100644
--- a/src/styles/components/_list-group.scss
+++ b/src/styles/components/_list-group.scss
@@ -4,6 +4,8 @@
// ListGroup overrides scaffold
.list-group {
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 */
@include mixins.cd-chamfered-container;
@@ -22,7 +24,7 @@
// Accent variants for list-group container (mirror card accent variants)
&--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,
diff --git a/src/styles/components/_modal.scss b/src/styles/components/_modal.scss
index 52f255b..06f0a2b 100644
--- a/src/styles/components/_modal.scss
+++ b/src/styles/components/_modal.scss
@@ -1,22 +1,91 @@
@use '../variables' as vars;
+@use '../mixins' as cdmixins;
+@use 'sass:color' as color;
+@use 'sass:map' as map;
.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));
- border: 1px solid var(--color-border-default);
color: var(--color-text-primary);
}
.modal-header,
.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 {
background-color: rgb(2 6 10 / 60%);
backdrop-filter: blur(2px);
+ z-index: 2000;
}
.modal-title {
- font-family: vars.$cyberduck-font-display;
+ font-family: var(--cyberduck-font-display);
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-` 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);
}
diff --git a/src/styles/components/_navbar.scss b/src/styles/components/_navbar.scss
index 202cb0e..e01ff16 100644
--- a/src/styles/components/_navbar.scss
+++ b/src/styles/components/_navbar.scss
@@ -16,7 +16,7 @@
}
.navbar-brand {
- color: var(--color-variant-primary);
+ color: var(--color-variant-green);
font-family: vars.$cyberduck-font-display;
font-variant-caps: small-caps;
text-transform: uppercase;
@@ -27,7 +27,7 @@
/* Neon glow */
text-shadow:
0 0 6px rgb(0 0 0 / 12%),
- 0 0 8px var(--color-variant-primary);
+ 0 0 8px var(--color-variant-green);
&::before {
content: '//';
diff --git a/src/styles/components/_table.scss b/src/styles/components/_table.scss
index 8f3e492..a71726b 100644
--- a/src/styles/components/_table.scss
+++ b/src/styles/components/_table.scss
@@ -3,7 +3,7 @@
// Table overrides scaffold
table {
background: transparent;
- color: var(--color-text-primary);
+ font-family: vars.$cyberduck-font-mono;
th,
td {
@@ -30,4 +30,11 @@ table {
color: var(--color-variant-green) !important;
}
}
+
+ tbody {
+ td,
+ td * {
+ color: var(--color-text-primary);
+ }
+ }
}
diff --git a/src/styles/components/_tooltip.scss b/src/styles/components/_tooltip.scss
index 65d67ff..119ac3b 100644
--- a/src/styles/components/_tooltip.scss
+++ b/src/styles/components/_tooltip.scss
@@ -1,14 +1,46 @@
@use '../variables' as vars;
+@use '../mixins' as mixins;
.tooltip,
.popover {
- background: var(--cyberduck-void-400) !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%);
- 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 {
+ @include mixins.cd-chamfered-container;
+
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;
+ }
}
diff --git a/src/styles/cyberduck.scss b/src/styles/cyberduck.scss
index 0b33223..c3682b1 100644
--- a/src/styles/cyberduck.scss
+++ b/src/styles/cyberduck.scss
@@ -2,15 +2,27 @@
// Configure Bootstrap with our customisations
@use 'bootstrap/scss/bootstrap' with (
+ // Fonts
$font-family-sans-serif: vars.$cyberduck-font-body,
$font-family-monospace: vars.$cyberduck-font-mono,
$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
- $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),
$link-color: vars.cd-color(vars.$cyberduck-yellow, 300),
$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
$primary: vars.cd-color(vars.$cyberduck-neon, primary),
$warning: vars.cd-color(vars.$cyberduck-neon, warning),
@@ -19,10 +31,11 @@
$danger: vars.cd-color(vars.$cyberduck-neon, danger),
$secondary: vars.cd-color(vars.$cyberduck-neon, secondary),
$light: vars.cd-color(vars.$cyberduck-neon, light),
- $dark: vars.cd-color(vars.$cyberduck-neon, dark)
- // Buttons: rely on `$primary` variant for button colors (avoid non-!default vars)
+ $dark: vars.cd-color(vars.$cyberduck-neon, dark),
+ // Buttons: rely on `$primary` variant for button colors (avoid non-!default vars)
// 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/list-group' as listgroup;
@@ -31,12 +44,14 @@
@use 'components/nav-links' as navlinks;
@use 'components/button-group' as buttongroup;
@use 'components/badge' as badge;
+@use 'components/buttons' as buttons;
@use 'components/forms' as forms;
@use 'components/modal' as modal;
@use 'components/dropdown' as dropdown;
@use 'components/input-group' as inputgroup;
@use 'components/tooltip' as tooltip;
@use 'components/footer' as footer;
+@use 'components/layout' as layout;
// CyberDuck theme entrypoint — wires variables and per-component overrides
@@ -57,6 +72,7 @@ h6 {
code,
kbd,
pre,
-samp {
+samp,
+table {
font-family: var(--cyberduck-font-mono);
}