Checkpoint

This commit is contained in:
2026-03-09 11:33:07 +01:00
commit aed73e824e
38 changed files with 8473 additions and 0 deletions

276
.gitignore vendored Normal file
View File

@@ -0,0 +1,276 @@
# Created by https://www.toptal.com/developers/gitignore/api/node,react,sass,macos,linux,windows,visualstudiocode,vim
# Edit at https://www.toptal.com/developers/gitignore?templates=node,react,sass,macos,linux,windows,visualstudiocode,vim
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### react ###
.DS_*
**/*.backup.*
**/*.back.*
node_modules
*.sublime*
psd
thumb
sketch
### Sass ###
.sass-cache/
*.css.map
*.sass.map
*.scss.map
### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/node,react,sass,macos,linux,windows,visualstudiocode,vim

6
.husky/pre-commit Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm test
npx lint-staged
npx lint-staged

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}

24
.stylelintrc.json Normal file
View File

@@ -0,0 +1,24 @@
{
"extends": [
"stylelint-config-standard-scss"
],
"plugins": [
"stylelint-scss",
"stylelint-order"
],
"customSyntax": "postcss-scss",
"rules": {
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": true,
"order/properties-alphabetical-order": null,
"order/order": [
"custom-properties",
"declarations"
],
"order/properties-order": [
"width",
"height"
],
"max-nesting-depth": 3
}
}

51
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,51 @@
{
"gopls": {
"formatting.local": "git.maze.io",
"ui.semanticTokens": true
},
// Global defaults for all other languages (4 spaces)
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
// Go: Use tabs, with a tab size of 4
"[go]": {
"editor.insertSpaces": false,
"editor.tabSize": 4,
"editor.detectIndentation": false
},
// CSS, JavaScript, TypeScript, JSON: Use 2 spaces
"[css]": {
"editor.tabSize": 2,
},
"[sass]": {
"editor.tabSize": 2,
},
"[scss]": {
"editor.tabSize": 2,
},
"[javascript]": {
"editor.tabSize": 2,
},
"[typescript]": {
"editor.tabSize": 2,
},
"[typescriptreact]": {
"editor.tabSize": 2,
},
"[json]": {
"editor.tabSize": 2,
},
"[yaml]": {
"editor.tabSize": 2,
},
// For JSON with comments, often used in VSCode config files
"[jsonc]": {
"editor.insertSpaces": true,
"editor.tabSize": 2,
"editor.detectIndentation": false
}
}

51
AGENTS.md Normal file
View File

@@ -0,0 +1,51 @@
# Project
This is a case study for creating a Yellow Cyberpunk theme called "CyberDuck" that is suitable for
use in a React project. It is based on Bootstrap v5 and Material icons.
## Stylesheets
We use a modular system based on SASS (SCSS), all style elements are stored in `src/styles`, each
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
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`
and `$cyberduck-font-display`.
**Colors:**
- The base background color variants are `--cyberduck-void-<shade>`, accents are `--cyberduck-yellow-<shade>`.
- 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`).
- Update tokens in `src/styles/_variables.scss` to change palette or fonts globally.
## React
We use a modular system based on React Components, all are stored in `src/components`. Generic components
are stored in that base folder, more specific Components that are only used by one other type of component
get their own subfolder. Components have short, but descriptive names often mathing the type of tag,
container or view they represent.
## 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
the URL and warnings.
## Changes strategy
Prefer patching over running external commands. Prefer inline replacement over removing files and
regenerating them.
When you need feedback from me, use the VSCode dialog option when possible, optimize interactions
with me to avoid typing where possible.

22
README-LINTING.md Normal file
View File

@@ -0,0 +1,22 @@
SASS linting & formatting
=========================
This project uses Stylelint (for SCSS linting) and Prettier (for formatting).
Install dev dependencies:
```bash
npm install -D stylelint stylelint-config-standard-scss stylelint-scss stylelint-order postcss-scss prettier husky lint-staged
```
Available scripts:
```bash
npm run lint:css # run stylelint
npm run format:css # run prettier on styles
npm run prepare # sets up husky hooks (run after npm install)
```
Pre-commit hook
We use `lint-staged` to run formatting/lint fixes on staged SCSS files. The hook runs `prettier --write` followed by `stylelint --fix`.

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

23
eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

30
index.html Normal file
View File

@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>cyberduck</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;600;700&family=DotGothic16&family=Rajdhani:wght@400;600;700&family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script>
// If the page includes a fixed `.site-footer`, add a class to `body`
// so older browsers (without :has support) can still apply the
// `--site-footer-height` CSS variable via `body.has-fixed-footer`.
(function () {
try {
if (document.querySelector('.site-footer.fixed-bottom')) {
document.body.classList.add('has-fixed-footer')
}
} catch (e) {
// silent fail
}
})();
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6441
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

58
package.json Normal file
View File

@@ -0,0 +1,58 @@
{
"name": "cyberduck",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"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",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.12.0",
"@emotion/styled": "^11.12.0",
"@mui/icons-material": "^5.14.11",
"@mui/material": "^5.14.11",
"bootstrap": "^5.3.8",
"react": "^19.2.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"husky": "^8.0.0",
"lint-staged": "^16.3.2",
"postcss-scss": "^4.0.9",
"prettier": "^3.8.1",
"sass": "^1.73.0",
"stylelint": "^17.4.0",
"stylelint-config-standard-scss": "^17.0.0",
"stylelint-order": "^7.0.1",
"stylelint-scss": "^7.0.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1"
},
"lint-staged": {
"src/**/*.{scss,css,sass}": [
"prettier --write",
"stylelint --fix",
"git add"
]
}
}

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

6
src/App.tsx Normal file
View File

@@ -0,0 +1,6 @@
import Showcase from './Showcase'
function App() {
return <Showcase />
}
export default App

309
src/Showcase.tsx Normal file
View File

@@ -0,0 +1,309 @@
import { useMemo, useState } from 'react'
import {
Navbar,
Nav,
Container,
ButtonGroup,
Button,
Card,
ListGroup,
Table,
Badge,
Pagination,
Row,
Col,
Form,
Modal,
Dropdown,
InputGroup,
OverlayTrigger,
Tooltip,
} from 'react-bootstrap'
import HomeIcon from '@mui/icons-material/Home'
import SearchIcon from '@mui/icons-material/Search'
import FavoriteIcon from '@mui/icons-material/Favorite'
import InfoIcon from '@mui/icons-material/Info'
// AddIcon intentionally omitted (unused)
import EditIcon from '@mui/icons-material/Edit'
import DeleteIcon from '@mui/icons-material/Delete'
const makeRows = (n: number) =>
Array.from({ length: n }).map((_, i) => ({ id: i + 1, name: `User ${i + 1}`, email: `user${i + 1}@example.com` }))
export default function Showcase() {
const rows = useMemo(() => makeRows(42), [])
const [page, setPage] = useState(1)
const [showModal, setShowModal] = useState(false)
const perPage = 6
const pageCount = Math.ceil(rows.length / perPage)
const pageRows = rows.slice((page - 1) * perPage, page * perPage)
// Include all standard Bootstrap badge variants so the table demonstrates each
const statusVariants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark']
return (
<div className="showcase-root d-flex flex-column min-vh-100 w-100">
<Navbar bg="light" expand="lg" className="fullwidth-navbar">
<Container fluid className="px-0">
<div className="d-flex align-items-center w-100">
<div className="d-flex align-items-center ms-2">
<Navbar.Brand className="me-3 mb-0">Brand Showcase</Navbar.Brand>
<ButtonGroup className="me-3">
<Button variant="outline-primary" active aria-pressed="true">
<HomeIcon /> Home
</Button>
<Button variant="outline-secondary">
<SearchIcon /> Search
</Button>
<Button variant="outline-danger">
<FavoriteIcon /> Fav
</Button>
</ButtonGroup>
</div>
<Navbar.Collapse className="justify-content-end">
<Nav>
<Nav.Link href="#home" active aria-current="page">
<HomeIcon fontSize="small" /> Home
</Nav.Link>
<Nav.Link href="#about">
<InfoIcon fontSize="small" /> About
</Nav.Link>
<Nav.Link href="#search">
<SearchIcon fontSize="small" /> Search
</Nav.Link>
</Nav>
</Navbar.Collapse>
</div>
</Container>
</Navbar>
<main className="flex-grow-1 main-with-navbar container-fluid py-4">
<Row className="g-3 mb-4">
<Col xs={12}>
{/* compact utilities row - use available horizontal space */}
<Row className="g-3">
<Col xs={12} md={4}>
<Card className="mb-0 accent-yellow">
<Card.Body>Accent utility yellow</Card.Body>
</Card>
</Col>
<Col xs={12} md={4}>
<Card className="mb-0 accent-green">
<Card.Body>
<Card.Title>Green utility</Card.Title>
<Card.Text>Accent utility demonstration.</Card.Text>
</Card.Body>
</Card>
</Col>
<Col xs={12} md={4}>
<Card className="mb-0 accent-pink">
<Card.Body>
<Card.Title>Pink utility</Card.Title>
<Card.Text>Accent utility demonstration.</Card.Text>
</Card.Body>
</Card>
</Col>
</Row>
</Col>
</Row>
<div className="container-fluid">
<Row className="g-4">
{/* Three demos side-by-side: Cards | List Group | Forms */}
<Col xs={12}>
<Row className="g-3">
<Col xs={12} md={4}>
<h4>Cards</h4>
<Row className="g-3">
<Col xs={12}>
<Card className="mb-0 card--accent-yellow">
<Card.Body>Plain card body content</Card.Body>
</Card>
</Col>
<Col xs={12}>
<Card className="mb-0 card--accent-green">
<Card.Header>Card Header</Card.Header>
<Card.Body>
<Card.Title>Card with header</Card.Title>
</Card.Body>
</Card>
</Col>
<Col xs={12}>
<Card className="mb-0 card--accent-pink">
<Card.Img variant="top" src="https://picsum.photos/seed/picsum/600/200" />
<Card.Body>
<Card.Title>Card with image</Card.Title>
</Card.Body>
</Card>
</Col>
</Row>
</Col>
<Col xs={12} md={4}>
<h4>List Group</h4>
<Row className="g-3">
<Col xs={12}>
<ListGroup className="mb-0">
<ListGroup.Item>First item</ListGroup.Item>
<ListGroup.Item>Second item</ListGroup.Item>
<ListGroup.Item active>Active item</ListGroup.Item>
</ListGroup>
</Col>
<Col xs={12}>
<ListGroup className="mb-0 accent-yellow">
<ListGroup.Item>Yellow one</ListGroup.Item>
<ListGroup.Item>Yellow two</ListGroup.Item>
<ListGroup.Item active>Active</ListGroup.Item>
</ListGroup>
</Col>
<Col xs={12}>
<ListGroup className="mb-0 accent-green">
<ListGroup.Item>Green one</ListGroup.Item>
<ListGroup.Item>Green two</ListGroup.Item>
<ListGroup.Item active>Active</ListGroup.Item>
</ListGroup>
</Col>
<Col xs={12}>
<ListGroup className="mb-0 accent-pink">
<ListGroup.Item>Pink one</ListGroup.Item>
<ListGroup.Item>Pink two</ListGroup.Item>
<ListGroup.Item active>Active</ListGroup.Item>
</ListGroup>
</Col>
</Row>
</Col>
<Col xs={12} md={4}>
<h4>Forms</h4>
<Row className="g-3">
<Col xs={12}>
<Form>
<Form.Group className="mb-2" controlId="formEmail">
<Form.Label>Email</Form.Label>
<Form.Control type="email" placeholder="you@example.com" />
</Form.Group>
<InputGroup className="mb-2">
<Form.Control placeholder="Username" />
<InputGroup.Text>@</InputGroup.Text>
</InputGroup>
<Button size="sm" variant="primary">Submit</Button>
</Form>
</Col>
</Row>
</Col>
</Row>
</Col>
{/* Table gets its own dedicated full-width row */}
<Col xs={12}>
<h4>Table</h4>
<div className="d-flex gap-2 mb-3">
<OverlayTrigger placement="top" overlay={<Tooltip id="tt-modal">Open a sample modal</Tooltip>}>
<Button onClick={() => setShowModal(true)}>Open Modal</Button>
</OverlayTrigger>
<Dropdown>
<Dropdown.Toggle variant="secondary" id="dropdown-basic">
Dropdown
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item>Action</Dropdown.Item>
<Dropdown.Item>Another action</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>
<Card>
<Card.Body>
<Table striped bordered hover responsive>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{pageRows.map((r) => (
<tr key={r.id}>
<td>{r.id}</td>
<td>{r.name}</td>
<td>{r.email}</td>
<td>
{/* Badge pill variants showcase */}
{(() => {
const variant = statusVariants[r.id % statusVariants.length]
return (
<>
<Badge bg={variant} className="me-2">
{variant}
</Badge>
</>
)
})()}
</td>
<td>
<Button size="sm" variant="outline-success" className="me-1">
<EditIcon fontSize="small" />
</Button>
<Button size="sm" variant="outline-danger">
<DeleteIcon fontSize="small" />
</Button>
</td>
</tr>
))}
</tbody>
</Table>
<div className="d-flex justify-content-end">
<Pagination>
<Pagination.Prev onClick={() => setPage((p) => Math.max(1, p - 1))} />
{Array.from({ length: pageCount }).map((_, i) => (
<Pagination.Item key={i} active={i + 1 === page} onClick={() => setPage(i + 1)}>
{i + 1}
</Pagination.Item>
))}
<Pagination.Next onClick={() => setPage((p) => Math.min(pageCount, p + 1))} />
</Pagination>
</div>
</Card.Body>
</Card>
</Col>
</Row>
</div>
</main>
<Modal show={showModal} onHide={() => setShowModal(false)}>
<Modal.Header closeButton>
<Modal.Title>Sample Modal</Modal.Title>
</Modal.Header>
<Modal.Body>
This is a simple modal body to demonstrate the themed modal styles.
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => setShowModal(false)}>
Close
</Button>
<Button variant="primary">Save changes</Button>
</Modal.Footer>
</Modal>
<footer className="site-footer fixed-bottom">
<Container fluid>
<div className="footer-inner container d-flex align-items-center">
<div className="footer-brand">Brand Showcase</div>
<div className="footer-links">
<a href="#privacy">Privacy</a>
<a href="#terms">Terms</a>
<a href="#contact">Contact</a>
</div>
<div className="footer-copyright">© {new Date().getFullYear()} Brand Showcase</div>
</div>
</Container>
</footer>
</div>
)
}

1
src/assets/react.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

91
src/index.scss Normal file
View File

@@ -0,0 +1,91 @@
/* Project CSS Reset and base styles
- reset browser defaults
- basic layout variables for the Showcase
*/
@use './styles/cyberduck';
/* Reset: box-sizing and remove default spacing */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body,
#root {
height: 100%;
}
/* Default type and color system (migrated) */
:root {
--navbar-height: 56px;
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgb(255 255 255 / 87%);
background-color: #242424;
}
/* Make top navbar full-bleed and remove gaps; use sticky so it remains in flow */
.fullwidth-navbar {
position: sticky;
top: 0;
height: var(--navbar-height);
z-index: 1100;
padding: 0.25rem 0.5rem;
background-clip: padding-box;
}
.main-with-navbar {
padding-top: calc(var(--navbar-height) + 0.5rem);
}
/* Ensure `main` background covers its padding area so bottom padding shows
the same background color behind the page content (prevents body color
showing through under a fixed footer). */
main {
background-color: var(--color-bg-primary);
}
.small-footer {
background: #f8f9fa;
}
/* Small helper layout */
.showcase-root {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Buttons/icons spacing */
.fullwidth-navbar .btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
}
/* Ensure navbar brand doesn't have side gaps */
.fullwidth-navbar .navbar-brand {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
/* Make footer small and flush */
.small-footer {
padding: 0.5rem 0;
}
/* light theme overrides for text color consistency */
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #fff;
}
}

11
src/main.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'bootstrap/dist/css/bootstrap.min.css'
import './index.scss'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

67
src/styles/_mixins.scss Normal file
View File

@@ -0,0 +1,67 @@
@use './variables' as vars;
// Reusable chamfer/fillet container mixin used by cards, lists, forms, etc.
@mixin cd-chamfered-container($fillet: vars.$cyberduck-border-fillet) {
--cd-list-fillet: var(--cyberduck-border-fillet, #{$fillet});
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))
);
position: relative;
overflow: hidden;
}
// Mixin: add corner accent L-shape at top-left and bottom-right
// Generalized name: cd-accent-container
@mixin cd-accent-container(
$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;
}
&::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;
}
}

135
src/styles/_variables.scss Normal file
View File

@@ -0,0 +1,135 @@
// CyberDuck SASS variables and CSS custom properties
// Use as: @use 'variables' as vars;
// SASS-native maps for programmatic use (single source of truth)
$cyberduck-yellow: (
50: #fff8db,
100: #fff0b8,
200: #ffe680,
300: #ffdb4d,
400: #ffd61a,
500: #ffd300,
600: #e6bf00,
700: #b38f00,
800: #806000,
900: #4d3000
);
@use 'sass:map' as map;
$cyberduck-void: (
50: #f2f2f3,
100: #2a2d3a,
200: #1f2230,
300: #181a25,
400: #12141d,
500: #333338,
700: #07080c,
800: #040508,
900: #010204
);
// Chrome shade group (based on #747d99)
$cyberduck-chrome: (
50: #f3f5fa,
100: #e6e9f5,
200: #cfd6ea,
300: #b8c2df,
400: #9da8d0,
500: #747d99,
600: #5f6781,
700: #4a4f61,
800: #31343f,
900: #1a1b22
);
// Neon variant colors for Bootstrap semantic variants
$cyberduck-neon: (
primary: map.get($cyberduck-yellow, 500),
warning: map.get($cyberduck-yellow, 500),
info: #00f0ff,
success: #00ff7a,
danger: #ff2d95,
secondary: map.get($cyberduck-chrome, 500),
// additional neon accents
green: #27f9eb,
pink: #ff00d1,
yellow: map.get($cyberduck-yellow, 500),
light: #ffffff,
dark: #000000
);
// Border and accent sizes
$cyberduck-border-fillet: 10px;
$cyberduck-accent-size: 2px;
// Corner fillet size used across components (top-right / bottom-left)
$cyberduck-border-fillet: 0.5rem !default;
$cyberduck-accent-length: 20px;
// Font-family groups
// `font-body` uses Google 'Exo 2' with system fallbacks
$cyberduck-font-body:
'Exo 2',
system-ui,
-apple-system,
'Segoe UI',
roboto,
'Helvetica Neue',
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;
// `font-display` for headings/brand using Rajdhani / Orbitron then system sans
$cyberduck-font-display:
'Rajdhani',
'Orbitron',
system-ui,
-apple-system,
sans-serif !default;
// Emit CSS custom properties from the SASS maps so there's a single source of truth
:root {
// yellow shades
@each $k, $v in $cyberduck-yellow {
--cyberduck-yellow-#{$k}: #{$v};
}
// void shades
@each $k, $v in $cyberduck-void {
--cyberduck-void-#{$k}: #{$v};
}
// chrome shades
@each $k, $v in $cyberduck-chrome {
--cyberduck-chrome-#{$k}: #{$v};
}
// neon variant tokens
@each $k, $v in $cyberduck-neon {
--color-variant-#{$k}: #{$v};
}
// Semantic color tokens (defaults mapped to CyberDuck palette)
--color-bg-primary: var(--cyberduck-void-500);
--color-bg-secondary: var(--cyberduck-void-400);
--color-bg-tertiary: var(--cyberduck-void-300);
--color-bg-elevated: var(--cyberduck-void-200);
--color-text-primary: var(--cyberduck-chrome-100);
--color-text-secondary: var(--cyberduck-chrome-300);
--color-text-muted: var(--cyberduck-chrome-500);
--color-border-default: var(--cyberduck-void-100);
--cyberduck-font-body: #{$cyberduck-font-body};
--cyberduck-font-mono: #{$cyberduck-font-mono};
--cyberduck-font-display: #{$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};
}
// Lightweight helper to read values from maps
@function cd-color($map, $key) {
@return map.get($map, $key);
}

View File

@@ -0,0 +1,39 @@
@use '../variables' as vars;
@use 'sass:map' as map;
@use 'sass:color' as color;
// Badge overrides for CyberDuck theme
.badge {
border-radius: 3px;
padding: 0.25em 0.5em;
font-weight: 600;
text-transform: none;
}
// Darken factor for badge backgrounds (keeps hue but a bit deeper)
$badge-darken: 12%;
$variants: primary secondary success danger warning info light dark;
@each $v in $variants {
@if map.has-key(vars.$cyberduck-neon, $v) {
$base: vars.cd-color(vars.$cyberduck-neon, $v);
.badge.bg-#{$v},
.badge.rounded-pill.bg-#{$v} {
background-color: color.adjust($base, $lightness: - $badge-darken) !important;
border: 1px solid #{$base} !important;
// Ensure text is readable: light text on darker backgrounds, dark text on 'light'
@if $v == light {
color: vars.cd-color(vars.$cyberduck-chrome, 800) !important;
} @else {
color: #fff !important;
}
}
}
// Force small radius even when `rounded-pill` is present
.badge.rounded-pill {
border-radius: 3px !important;
}
}

View File

@@ -0,0 +1,13 @@
@use '../variables' as vars;
// ButtonGroup overrides scaffold
.btn-group .btn {
background-color: var(--cyberduck-void-600);
color: var(--cyberduck-void-50);
border-color: rgb(255 255 255 / 4%);
&:hover {
background-color: var(--cyberduck-void-500);
color: var(--cyberduck-yellow-500);
}
}

View File

@@ -0,0 +1,62 @@
@use '../variables' as vars;
@use '../mixins' as mixins;
// Card overrides scaffold for CyberDuck
.card {
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
@include mixins.cd-chamfered-container;
.card-header {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
border-top-left-radius: 0;
}
// Ensure card titles and subtitles use the display heading font
.card-title,
.card-subtitle {
font-family: var(--cyberduck-font-display);
color: var(--color-text-primary);
}
.card-footer {
background: transparent;
color: var(--color-text-muted);
border-top: 1px solid var(--color-border-default);
border-bottom-right-radius: 0;
}
}
// Ensure top image corners match card rounding when using .card-img-top
.card-img-top {
border-top-left-radius: 0;
border-top-right-radius: vars.$cyberduck-border-fillet;
}
// cd-card-accent mixin moved to src/styles/_mixins.scss
// Accent variants using neon colors (nested modifier notation)
.card {
&--accent-yellow,
&.accent-yellow {
@include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, primary));
}
// cyan is also known as green in some usages
&--accent-cyan,
&.accent-cyan,
&--accent-green,
&.accent-green {
@include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, green));
}
// magenta is also known as pink
&--accent-magenta,
&.accent-magenta,
&--accent-pink,
&.accent-pink {
@include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, pink));
}
}

View File

@@ -0,0 +1,17 @@
@use '../variables' as vars;
.dropdown-menu {
background: var(--cyberduck-void-400);
border: 1px solid var(--color-border-default);
color: var(--color-text-primary);
}
.dropdown-item:hover,
.dropdown-item:focus {
background: rgb(39 249 235 / 6%);
color: var(--color-variant-green);
}
.dropdown-toggle::after {
filter: drop-shadow(0 0 6px var(--color-variant-primary));
}

View File

@@ -0,0 +1,155 @@
@use '../variables' as vars;
// Site footer styling matching CyberDuck theme
.site-footer {
/* default footer height used when fixed to avoid overlaying content */
/* keep a local fallback here, but we export the value to the parent
when the footer is fixed so ancestor content can read the value via var() */
--site-footer-height: 56px;
--site-footer-strip-height: 6px;
position: relative;
background-color: var(--cyberduck-void-300) !important;
color: var(--color-text-primary);
padding: 1.25rem 0;
font-size: 0.9rem;
// neon strip at top
&::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
height: 6px;
background: linear-gradient(
90deg,
var(--color-variant-pink),
var(--color-variant-green),
var(--color-variant-primary),
var(--color-variant-yellow)
);
opacity: 0.95;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
transform: translateY(-100%);
}
.footer-inner {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
// When a fixed footer is requested, pin to viewport bottom and add spacing
&.fixed-bottom {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 1060;
padding-top: 0.6rem;
padding-bottom: 0.6rem;
height: var(--site-footer-height);
display: flex;
align-items: center;
background-color: var(--cyberduck-void-300) !important;
}
.footer-brand {
color: var(--color-variant-primary);
font-family: vars.$cyberduck-font-display;
font-variant-caps: small-caps;
text-transform: uppercase;
letter-spacing: 0.06em;
font-size: 0.95rem;
display: inline-flex;
align-items: center;
&::before {
content: '//';
margin-right: 0.4rem;
color: var(--color-variant-pink);
text-shadow:
0 0 4px rgb(255 0 209 / 60%),
0 0 8px var(--color-variant-pink);
transform: translateY(-1px);
-webkit-font-smoothing: antialiased;
}
}
.footer-links {
display: flex;
gap: 0.75rem;
align-items: center;
flex-wrap: wrap;
a {
color: var(--color-text-primary);
text-decoration: none;
position: relative;
padding-bottom: 0.125rem;
font-size: 0.95rem;
font-family: vars.$cyberduck-font-display;
font-variant-caps: small-caps;
letter-spacing: 0.03em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: geometricprecision;
backface-visibility: hidden;
font-feature-settings: 'liga' 1;
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 2px;
background: transparent;
transform-origin: left center;
transform: scaleX(0);
transition:
transform 140ms ease-in,
background-color 120ms ease,
opacity 120ms ease;
opacity: 0;
}
&:hover::after,
&:focus::after {
background: var(--color-variant-green);
transform: scaleX(1);
opacity: 1;
}
}
}
.footer-copyright {
color: var(--color-text-muted, rgb(255 255 255 / 66%));
font-size: 0.82rem;
}
}
/* Export footer height to the document root when a fixed footer is present
so any `main` element can pick it up and avoid being obscured. Use both
the modern `:has()` selector and a fallback class `has-fixed-footer` on
`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));
}
body.has-fixed-footer {
--site-footer-height: calc(var(--site-footer-height, 56px) + var(--site-footer-strip-height, 0px));
}
/* Apply automatic bottom padding to any `main` so pages don't need ad-hoc
inline styles or special wrappers. The fallback value is 0 when no
fixed footer exists. */
main {
padding-bottom: var(--site-footer-height, 0);
}

View File

@@ -0,0 +1,35 @@
@use '../variables' as vars;
@use '../mixins' as mixins;
// Basic form controls styling for CyberDuck
form,
.form-control,
.form-select {
font-family: vars.$cyberduck-font-body;
background-color: var(--cyberduck-void-500);
color: var(--color-text-primary);
border: 1px solid var(--color-border-default);
border-radius: 6px;
}
.form-control:focus,
.form-select:focus {
outline: none;
box-shadow:
0 0 10px rgb(var(--color-variant-primary, 255, 211, 0), 0.08),
0 0 18px var(--color-variant-primary);
border-color: var(--color-variant-primary);
}
label {
font-family: vars.$cyberduck-font-display;
font-variant-caps: small-caps;
color: var(--color-text-secondary);
font-size: 0.85rem;
letter-spacing: 0.04em;
}
// Apply chamfered container style to form-group wrapper only
.form-group {
@include mixins.cd-chamfered-container;
}

View File

@@ -0,0 +1,12 @@
@use '../variables' as vars;
.input-group .form-control {
background: var(--cyberduck-void-500);
border-right: 0;
}
.input-group .input-group-text {
background: var(--cyberduck-void-400);
border-left: 0;
color: var(--color-text-primary);
}

View File

@@ -0,0 +1,41 @@
@use '../variables' as vars;
@use '../mixins' as mixins;
// ListGroup overrides scaffold
.list-group {
background: transparent;
/* container-only border style to match Cards: square TL & BR, chamfer other corners */
@include mixins.cd-chamfered-container;
.list-group-item {
background: var(--color-bg-secondary);
color: var(--color-text-primary);
border-bottom: 1px solid var(--color-border-default);
}
.list-group-item.active {
background: var(--color-variant-primary);
color: var(--color-variant-dark);
}
// 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));
}
&--accent-cyan,
&.accent-cyan,
&--accent-green,
&.accent-green {
@include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, green));
}
&--accent-magenta,
&.accent-magenta,
&--accent-pink,
&.accent-pink {
@include mixins.cd-accent-container(vars.cd-color(vars.$cyberduck-neon, pink));
}
}

View File

@@ -0,0 +1,22 @@
@use '../variables' as vars;
.modal-content {
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;
}
.modal-backdrop.show {
background-color: rgb(2 6 10 / 60%);
backdrop-filter: blur(2px);
}
.modal-title {
font-family: vars.$cyberduck-font-display;
letter-spacing: 0.04em;
}

View File

@@ -0,0 +1,20 @@
@use '../variables' as vars;
// NavLinks overrides scaffold
.nav-link {
color: var(--cyberduck-void-50);
font-family: vars.$cyberduck-font-display;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
&:hover,
&:focus {
color: var(--cyberduck-yellow-300);
}
&.active {
color: var(--cyberduck-yellow-500);
background: transparent;
}
}

View File

@@ -0,0 +1,197 @@
@use '../variables' as vars;
// NavBar overrides scaffold
.navbar {
/* Use void-300 for navbar background (CyberDuck void shade).
Increase specificity to override Bootstrap `bg-*` utilities that may be applied
(e.g. `bg="light"` on the <Navbar/> component). */
background-color: var(--cyberduck-void-300) !important;
color: var(--color-text-primary);
// When Bootstrap utility classes like .bg-light are present, ensure our color wins
&.bg-light,
&.navbar-light,
.fullwidth-navbar.bg-light {
background-color: var(--cyberduck-void-300) !important;
}
.navbar-brand {
color: var(--color-variant-primary);
font-family: vars.$cyberduck-font-display;
font-variant-caps: small-caps;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 700;
position: relative;
/* Neon glow */
text-shadow:
0 0 6px rgb(0 0 0 / 12%),
0 0 8px var(--color-variant-primary);
&::before {
content: '//';
display: inline-block;
margin-right: 0.45rem;
color: var(--color-variant-pink);
font-weight: 800;
/* Stronger neon glow: layered text-shadows + subtle drop-shadow filter */
text-shadow:
0 0 4px rgb(255 0 209 / 65%),
0 0 8px rgb(255 0 209 / 45%),
0 0 14px var(--color-variant-pink),
0 0 28px rgb(255 0 209 / 12%);
filter: drop-shadow(0 0 6px rgb(255 0 209 / 28%))
drop-shadow(0 0 12px rgb(255 0 209 / 14%));
transform: translateY(-1px);
-webkit-font-smoothing: antialiased;
}
}
// Active nav links: neon green underline (cyber-neon)
.nav-link {
position: relative;
display: inline-block;
padding-bottom: 0.25rem;
font-size: 0.875rem; // slightly smaller
font-weight: 400; // normal weight for nav links
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 3px;
background: transparent;
/* reveal from left to right */
transform-origin: left center;
transform: scaleX(0);
transition:
transform 160ms ease-in,
background-color 120ms ease,
opacity 120ms ease;
opacity: 0;
}
// Keep original hover text/background color; only show underline on hover/focus
&:hover::after,
&:focus::after {
background: var(--color-variant-green);
transform-origin: left center;
transform: scaleX(1);
opacity: 1;
}
// subtle text glitch on hover (short, non-disruptive)
&:hover,
&:focus {
animation: cd-glitch-text 220ms linear 1;
}
&.active,
&.show {
color: var(--color-variant-green) !important;
}
&.active::after,
&.show::after {
background: var(--color-variant-green);
transform-origin: left center;
transform: scaleX(1);
opacity: 1;
}
}
// Buttons inside the navbar should visually match nav links
.btn,
.btn-group .btn {
font-family: vars.$cyberduck-font-display;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 400;
position: relative;
padding-bottom: 0.25rem;
font-size: 0.85rem; // slightly smaller than nav-links
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 3px;
background: transparent;
/* reveal from left to right */
transform-origin: left center;
transform: scaleX(0);
transition:
transform 160ms ease-in,
background-color 120ms ease,
opacity 120ms ease;
opacity: 0;
}
// Preserve original hover text/background color; only animate underline
&:hover::after,
&:focus::after {
background: var(--color-variant-green);
transform-origin: left center;
transform: scaleX(1);
opacity: 1;
}
// subtle text glitch on hover/focus for buttons
&:hover,
&:focus {
animation: cd-glitch-text 220ms linear 1;
}
&.active {
color: var(--color-variant-green) !important;
}
&.active::after {
background: var(--color-variant-green);
transform-origin: left center;
transform: scaleX(1);
opacity: 1;
}
}
// Glitch text keyframes (subtle): quick horizontal jitter and temporary colored shadow
@keyframes cd-glitch-text {
0% {
transform: translateX(0);
text-shadow: none;
}
10% {
transform: translateX(-1px);
text-shadow: -2px 0 var(--color-variant-green);
}
25% {
transform: translateX(1px);
text-shadow: 2px 0 var(--color-variant-green);
}
40% {
transform: translateX(-0.5px);
text-shadow: -1px 0 rgb(0 0 0 / 15%);
}
60% {
transform: translateX(0.5px);
text-shadow: 1px 0 rgb(0 0 0 / 8%);
}
100% {
transform: translateX(0);
text-shadow: none;
}
}
}

View File

@@ -0,0 +1,33 @@
@use '../variables' as vars;
// Table overrides scaffold
table {
background: transparent;
color: var(--color-text-primary);
th,
td {
border-color: var(--color-border-default);
}
thead {
background: var(--color-bg-secondary);
color: var(--color-text-primary);
// Cyberpunk neon green headings — apply to th and any descendant elements (links/icons)
th {
font-family: vars.$cyberduck-font-display;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
}
// Make sure any child elements inherit the neon green (anchors, icons, sort buttons)
th,
th *,
td,
td * {
color: var(--color-variant-green) !important;
}
}
}

View File

@@ -0,0 +1,14 @@
@use '../variables' as vars;
.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;
}
.tooltip .tooltip-inner {
background: var(--cyberduck-void-400) !important;
}

62
src/styles/cyberduck.scss Normal file
View File

@@ -0,0 +1,62 @@
@use 'variables' as vars;
// Configure Bootstrap with our customisations
@use 'bootstrap/scss/bootstrap' with (
$font-family-sans-serif: vars.$cyberduck-font-body,
$font-family-monospace: vars.$cyberduck-font-mono,
$headings-font-family: vars.$cyberduck-font-display,
// Compile-time color values (SASS maps) so Bootstrap functions work
$body-bg: vars.cd-color(vars.$cyberduck-void, 500),
$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),
// Variant colors wired to neon SASS map
$primary: vars.cd-color(vars.$cyberduck-neon, primary),
$warning: vars.cd-color(vars.$cyberduck-neon, warning),
$info: vars.cd-color(vars.$cyberduck-neon, info),
$success: vars.cd-color(vars.$cyberduck-neon, success),
$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)
// Navbar: handled via component partials and semantic tokens
// (avoid overriding non-!default Bootstrap vars here)
);
@use 'components/card' as card;
@use 'components/list-group' as listgroup;
@use 'components/table' as table;
@use 'components/navbar' as navbar;
@use 'components/nav-links' as navlinks;
@use 'components/button-group' as buttongroup;
@use 'components/badge' as badge;
@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;
// CyberDuck theme entrypoint — wires variables and per-component overrides
// Global font utilities
body {
font-family: var(--cyberduck-font-body);
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--cyberduck-font-display);
}
code,
kbd,
pre,
samp {
font-family: var(--cyberduck-font-mono);
}

28
tsconfig.app.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})