From 19bb02229019b39bbf37b96b82e273383f760aad Mon Sep 17 00:00:00 2001 From: maze Date: Mon, 9 Mar 2026 16:43:24 +0100 Subject: [PATCH] Checkpoint --- .gitignore | 2 +- .husky/pre-commit | 5 + .pre-commit-config.yaml | 30 +++++ .stylelintrc.json | 5 +- AGENTS.md | 13 +- index.html | 2 +- package-lock.json | 61 +++++++++- package.json | 10 +- public/vite.svg | 2 +- src/App.tsx | 30 ++++- src/assets/react.svg | 2 +- src/components/Layout/Footer.tsx | 11 ++ src/components/Layout/Layout.tsx | 21 ++++ src/components/Layout/Navbar.tsx | 13 ++ src/components/Layout/Split.tsx | 148 +++++++++++++++++++++++ src/pages/Home.tsx | 16 +++ src/pages/LayoutHorizontal.tsx | 17 +++ src/pages/LayoutShowcase.tsx | 34 ++++++ src/pages/LayoutVertical.tsx | 17 +++ src/{ => pages}/Showcase.tsx | 43 +++++++ src/styles/_mixins.scss | 91 ++++++++++++-- src/styles/_variables.scss | 47 ++++++- src/styles/components/_badge.scss | 16 ++- src/styles/components/_button-group.scss | 24 ++++ src/styles/components/_buttons.scss | 115 ++++++++++++++++++ src/styles/components/_card.scss | 63 +++++++++- src/styles/components/_footer.scss | 4 +- src/styles/components/_forms.scss | 58 ++++++++- src/styles/components/_layout.scss | 93 ++++++++++++++ src/styles/components/_list-group.scss | 4 +- src/styles/components/_modal.scss | 75 +++++++++++- src/styles/components/_navbar.scss | 4 +- src/styles/components/_table.scss | 9 +- src/styles/components/_tooltip.scss | 38 +++++- src/styles/cyberduck.scss | 28 ++++- 35 files changed, 1093 insertions(+), 58 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 src/components/Layout/Footer.tsx create mode 100644 src/components/Layout/Layout.tsx create mode 100644 src/components/Layout/Navbar.tsx create mode 100644 src/components/Layout/Split.tsx create mode 100644 src/pages/Home.tsx create mode 100644 src/pages/LayoutHorizontal.tsx create mode 100644 src/pages/LayoutShowcase.tsx create mode 100644 src/pages/LayoutVertical.tsx rename src/{ => pages}/Showcase.tsx (85%) create mode 100644 src/styles/components/_buttons.scss create mode 100644 src/styles/components/_layout.scss 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 ( + +
+ +
+ + + } /> + } /> + } /> + } /> + } /> + +
+ ) } 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 ( +
+
© CyberDuck
+
+ ) +} + +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) => ( + + ))} + + {['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'].map((v) => ( + + ))} + + + +
+
+ +
+ + +
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); }