diff --git a/.changeset/hex-background-package.md b/.changeset/hex-background-package.md new file mode 100644 index 0000000..adbc983 --- /dev/null +++ b/.changeset/hex-background-package.md @@ -0,0 +1,5 @@ +--- +"@dangerousthings/hex-background": minor +--- + +New package: 3D hexagon grid background using Three.js + React Three Fiber. Includes HexGridBackground (full-viewport web component), HexGrid (instanced mesh with animated heights), and HexCamera (orbiting camera). Supports both web and React Native via separate entry points. diff --git a/.changeset/react-web-package.md b/.changeset/react-web-package.md new file mode 100644 index 0000000..a6d2d88 --- /dev/null +++ b/.changeset/react-web-package.md @@ -0,0 +1,6 @@ +--- +"@dangerousthings/react": minor +"@dangerousthings/tokens": patch +--- + +New package: @dangerousthings/react — React web components wrapping @dangerousthings/web CSS. 22 components matching the React Native package API (DTCard, DTButton, DTLabel, DTChip, DTModal, DTDrawer, DTAccordion, DTMenu, DTStaggerContainer, DTFeatureLegend, DTMobileFilterOverlay, DTGallery, DTHexagon, form primitives). Includes DTWebThemeProvider, animation hooks (useScaleIn, usePulse), and variant utility functions. DTVariant type moved to @dangerousthings/tokens as shared canonical source. diff --git a/.changeset/storefront-mode-tokens.md b/.changeset/storefront-mode-tokens.md new file mode 100644 index 0000000..106ec8b --- /dev/null +++ b/.changeset/storefront-mode-tokens.md @@ -0,0 +1,5 @@ +--- +"@dangerousthings/tokens": minor +--- + +Add mode CSS custom properties (--mode-normal, --mode-emphasis, --mode-warning, --mode-success, --mode-other) with RGB and selected variants for per-component color overrides diff --git a/.changeset/storefront-rn-features.md b/.changeset/storefront-rn-features.md new file mode 100644 index 0000000..5d8e719 --- /dev/null +++ b/.changeset/storefront-rn-features.md @@ -0,0 +1,5 @@ +--- +"@dangerousthings/react-native": minor +--- + +Add storefront component features: DTCard selected state and progress bar, DTBadgeOverlay positioning wrapper, DTStaggerContainer with staggered scale-in animation, DTFeatureLegend product feature grid, DTMobileFilterOverlay slide-up filter panel, useScaleIn and usePulse animation hooks. Rewrite DTButton with interactive bevel pattern (outlined rectangle, bevels on hover/select). diff --git a/.changeset/storefront-web-features.md b/.changeset/storefront-web-features.md new file mode 100644 index 0000000..5e94d84 --- /dev/null +++ b/.changeset/storefront-web-features.md @@ -0,0 +1,5 @@ +--- +"@dangerousthings/web": minor +--- + +Add storefront component features: animations library (entrance, interactive, stagger container), scrollbar styling, card color modes with selected/hover states, card progress bar, badge positioning, interactive bevel buttons, menu items, filter headers/overlays, feature legend grid. Fix link hover glow to respect per-element --dt-glow-color. diff --git a/README.md b/README.md index 7f55f56..6fca671 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,19 @@ Shared design tokens, web CSS, and React Native components for the Dangerous Thi ## Packages -| Package | Description | -|---------|-------------| -| [`@dangerousthings/tokens`](packages/tokens) | Canonical design tokens (colors, typography, shape) defined in TypeScript and exported as CSS custom properties | -| [`@dangerousthings/web`](packages/web) | Web CSS themes — bevels, glows, elevation, and form styles | -| [`@dangerousthings/react-native`](packages/react-native) | React Native components built on React Native Paper | +| Package | Description | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| [`@dangerousthings/tokens`](packages/tokens) | Canonical design tokens (colors, typography, shape) defined in TypeScript and exported as CSS custom properties | +| [`@dangerousthings/web`](packages/web) | Web CSS themes — bevels, glows, animations, forms, and filter styles | +| [`@dangerousthings/react-native`](packages/react-native) | React Native components built on React Native Paper | +| [`@dangerousthings/hex-background`](packages/hex-background) | 3D hexagon grid background using Three.js + React Three Fiber (web and React Native) | ## Brand Themes -Three brand themes ship out of the box: +Two brand themes ship out of the box: - **DT** — neon cyberpunk aesthetic with beveled corners and glow effects (Tektur font) - **Classic** — dark navy palette with magenta accents and standard border radius -- **Supra** — Material Design 3–inspired with blue accent and rounded corners ## Getting Started @@ -80,12 +80,12 @@ console.log(dt.color.brand.primary); // "#00ffff" ### Scripts -| Command | Description | -|---------|-------------| -| `npm run build` | Build all packages (Turbo) | -| `npm run clean` | Remove all `dist/` directories | -| `npm run typecheck` | Run TypeScript type checking | -| `npm run lint` | Lint all packages | +| Command | Description | +| ------------------- | ------------------------------ | +| `npm run build` | Build all packages (Turbo) | +| `npm run clean` | Remove all `dist/` directories | +| `npm run typecheck` | Run TypeScript type checking | +| `npm run lint` | Lint all packages | ### Release @@ -99,14 +99,18 @@ npm run release # build + publish to npm ## React Native Components -DTAccordion, DTButton, DTCard, DTCheckbox, DTChip, DTDrawer, DTGallery, DTHexagon, DTLabel, DTMediaFrame, DTMenu, DTModal, DTProgressBar, DTQuantityStepper, DTRadioGroup, DTSearchInput, DTSwitch, DTTextInput +DTAccordion, DTBadgeOverlay, DTButton, DTCard, DTCheckbox, DTChip, DTDrawer, DTFeatureLegend, DTGallery, DTHexagon, DTLabel, DTMediaFrame, DTMenu, DTMobileFilterOverlay, DTModal, DTProgressBar, DTQuantityStepper, DTRadioGroup, DTSearchInput, DTStaggerContainer, DTSwitch, DTTextInput + +**Animation hooks:** `useScaleIn`, `usePulse` ## Web CSS Modules -- `bevels.css` — angular clip-path patterns for the DT brand -- `glows.css` — neon drop-shadow effects for clipped elements -- `forms-dt.css` — text inputs, checkboxes, radio buttons, switches -- `elevation.css` — shadow elevation for Supra / MD3 brand +- `bevels.css` — angular clip-path bevels, card modes, selected states, progress bars, badge overlays, interactive bevel buttons +- `glows.css` — neon drop-shadow and text-shadow effects (mode-aware) +- `forms-dt.css` — text inputs, checkboxes, radio buttons, switches, menu items, filter headers +- `animations.css` — entrance animations, stagger containers, transition utilities +- `scrollbar.css` — thin neon scrollbar styling +- `feature-legend.css` — product feature grid with rotated labels ## Project Structure @@ -116,7 +120,8 @@ dt-design-system/ │ ├── tokens/ # Design token definitions + CSS generation │ ├── web/ # CSS themes and components │ ├── react-native/ # React Native Paper components -│ └── showcase/ # Demo app (placeholder) +│ ├── hex-background/ # 3D hexagon grid (Three.js) +│ └── showcase/ # Demo apps (desktop + mobile) ├── turbo.json # Turbo pipeline config └── package.json # Workspace root ``` diff --git a/package-lock.json b/package-lock.json index 11a096b..8b1e75a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ ], "devDependencies": { "@changesets/cli": "^2.30.0", + "puppeteer": "^24.38.0", "turbo": "^2.4.0", "typescript": "^5.7.0" }, @@ -35,6 +36,19 @@ } } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -1817,6 +1831,14 @@ "prettier": "^2.7.1" } }, + "node_modules/@dangerousthings/hex-background": { + "resolved": "packages/hex-background", + "link": true + }, + "node_modules/@dangerousthings/react": { + "resolved": "packages/react", + "link": true + }, "node_modules/@dangerousthings/react-native": { "resolved": "packages/react-native", "link": true @@ -1829,6 +1851,10 @@ "resolved": "packages/showcase/mobile", "link": true }, + "node_modules/@dangerousthings/tailwind-preset": { + "resolved": "packages/tailwind-preset", + "link": true + }, "node_modules/@dangerousthings/tokens": { "resolved": "packages/tokens", "link": true @@ -5027,6 +5053,28 @@ "node": ">=14" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.84.1.tgz", @@ -5441,6 +5489,13 @@ "nanoid": "^3.3.11" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -5858,6 +5913,13 @@ "node": ">= 10" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -6017,6 +6079,13 @@ "xmlbuilder": ">=11.0.1" } }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -6052,6 +6121,16 @@ "react-native": "*" } }, + "node_modules/@types/react-reconciler": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz", + "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -6068,6 +6147,26 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.160.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.160.0.tgz", + "integrity": "sha512-jWlbUBovicUKaOYxzgkLlhkiEQJkhCVvg4W2IYD2trqD2om3VK4DGLpHH5zQHNr7RweZK/5re/4IVhbhvxbV9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.6.10", + "meshoptimizer": "~0.18.1" + } + }, "node_modules/@types/verror": { "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", @@ -6076,6 +6175,13 @@ "license": "MIT", "optional": true }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -6131,6 +6237,37 @@ "@urql/core": "^5.0.0" } }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -6635,6 +6772,19 @@ "node": ">=0.8" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -6686,6 +6836,43 @@ "node": ">= 4.0.0" } }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/axios": { "version": "1.13.6", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", @@ -6698,6 +6885,21 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -6947,6 +7149,99 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", + "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz", + "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", + "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.21.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6979,6 +7274,16 @@ "node": ">=6.0.0" } }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/better-opn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", @@ -7030,6 +7335,19 @@ "node": ">=0.6" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -7448,6 +7766,16 @@ "node": ">= 0.4" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -7457,6 +7785,16 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001776", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", @@ -7500,6 +7838,31 @@ "dev": true, "license": "MIT" }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -7528,6 +7891,20 @@ "node": ">=12.13.0" } }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/chromium-edge-launcher": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", @@ -8022,6 +8399,53 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -8126,6 +8550,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -8133,6 +8570,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -8275,6 +8722,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8338,6 +8800,20 @@ "license": "MIT", "optional": true }, + "node_modules/devtools-protocol": { + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -8378,6 +8854,13 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/dmg-builder": { "version": "25.1.8", "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz", @@ -8950,6 +9433,23 @@ "dev": true, "license": "MIT" }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-ex/node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/error-stack-parser": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -9085,6 +9585,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -9098,6 +9631,26 @@ "node": ">=4" } }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -9116,6 +9669,16 @@ "node": ">=6" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/expo": { "version": "54.0.33", "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz", @@ -9342,6 +9905,13 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -9407,6 +9977,13 @@ "pend": "~1.2.0" } }, + "node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "dev": true, + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -9610,6 +10187,20 @@ "node": ">= 0.6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/freeport-async": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", @@ -9796,6 +10387,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/getenv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", @@ -10290,6 +10896,33 @@ "node": ">=16.x" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -10364,6 +10997,19 @@ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-ci": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", @@ -10574,6 +11220,29 @@ "semver": "bin/semver.js" } }, + "node_modules/its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -10784,6 +11453,16 @@ "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", "license": "MIT" }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/joi": { "version": "18.0.2", "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", @@ -10847,6 +11526,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -11252,6 +11938,19 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -11541,6 +12240,13 @@ "node": ">= 8" } }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "dev": true, + "license": "MIT" + }, "node_modules/metro": { "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.5.tgz", @@ -12047,6 +12753,13 @@ "dev": true, "license": "ISC" }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -12119,6 +12832,16 @@ "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", "license": "MIT" }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-abi": { "version": "3.87.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", @@ -12324,6 +13047,16 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -12497,6 +13230,55 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -12514,6 +13296,38 @@ "quansync": "^0.2.7" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-png": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz", @@ -12722,6 +13536,140 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -12845,6 +13793,51 @@ "node": ">= 6" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-agent/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -12872,6 +13865,69 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "24.38.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.38.0.tgz", + "integrity": "sha512-abnJOBVoL9PQTLKSbYGm9mjNFyIPaTVj77J/6cS370dIQtcZMpx8wyZoAuBzR71Aoon6yvI71NEVFUsl3JU82g==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1581282", + "puppeteer-core": "24.38.0", + "typed-query-selector": "^2.12.1" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.38.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.38.0.tgz", + "integrity": "sha512-zB3S/tksIhgi2gZRndUe07AudBz5SXOB7hqG0kEa9/YXWrGwlVlYm3tZtwKgfRftBzbmLQl5iwHkQQl04n/mWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/qrcode-terminal": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", @@ -13014,6 +14070,15 @@ "react": ">=17.0.0" } }, + "node_modules/react-icons": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -13172,6 +14237,26 @@ "read-binary-file-arch": "cli.js" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-yaml-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", @@ -13239,6 +14324,19 @@ "node": ">=10" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -14108,6 +15206,18 @@ "node": ">= 0.10.0" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -14289,6 +15399,67 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -14308,6 +15479,34 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -14343,6 +15542,16 @@ "dev": true, "license": "ISC" }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/temp-file": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", @@ -14459,6 +15668,16 @@ "node": ">=8" } }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -14480,6 +15699,13 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.160.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz", + "integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==", + "dev": true, + "license": "MIT" + }, "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", @@ -14731,6 +15957,13 @@ "node": ">=8" } }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -15154,6 +16387,13 @@ "defaults": "^1.0.3" } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", @@ -15453,6 +16693,219 @@ "node": ">= 10" } }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "packages/hex-background": { + "name": "@dangerousthings/hex-background", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@react-three/fiber": "^8.18.0", + "@types/react": "^18.2.0", + "@types/three": "^0.160.0", + "react": "^18.0.0", + "three": "^0.160.0", + "typescript": "^5.7.0" + }, + "peerDependencies": { + "@react-three/fiber": ">=8", + "react": ">=18", + "three": ">=0.150" + }, + "peerDependenciesMeta": { + "expo-gl": { + "optional": true + } + } + }, + "packages/hex-background/node_modules/@react-three/fiber": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz", + "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.26.7", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^1.0.6", + "react-reconciler": "^0.27.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.21.0", + "suspend-react": "^0.1.3", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=18 <19", + "react-dom": ">=18 <19", + "react-native": ">=0.64", + "three": ">=0.133" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "packages/hex-background/node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "packages/hex-background/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "packages/hex-background/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "packages/hex-background/node_modules/react-reconciler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", + "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.21.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "packages/hex-background/node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "packages/hex-background/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "packages/react": { + "name": "@dangerousthings/react", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@dangerousthings/tokens": "*", + "@dangerousthings/web": "*", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "typescript": "^5.8.0" + }, + "peerDependencies": { + "@dangerousthings/tokens": "*", + "@dangerousthings/web": "*", + "react": ">=18", + "react-dom": ">=18" + } + }, "packages/react-native": { "name": "@dangerousthings/react-native", "version": "0.2.0", @@ -15481,22 +16934,146 @@ } } }, + "packages/react/node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "packages/react/node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "packages/react/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "packages/react/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "packages/react/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "packages/showcase/desktop": { "name": "@dangerousthings/showcase-desktop", "version": "0.0.0", "dependencies": { + "@dangerousthings/react": "*", "@dangerousthings/tokens": "*", - "@dangerousthings/web": "*" + "@dangerousthings/web": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-icons": "^5.6.0" }, "devDependencies": { + "@dangerousthings/tailwind-preset": "*", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.0", "concurrently": "^9.0.0", "electron": "33.4.11", "electron-builder": "^25.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", "typescript": "^5.8.3", "vite": "^6.0.0", "wait-on": "^8.0.0" } }, + "packages/showcase/desktop/node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "packages/showcase/desktop/node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "packages/showcase/desktop/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "packages/showcase/desktop/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "packages/showcase/desktop/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "packages/showcase/mobile": { "name": "@dangerousthings/showcase-mobile", "version": "0.0.0", @@ -15800,6 +17377,18 @@ "async-limiter": "~1.0.0" } }, + "packages/tailwind-preset": { + "name": "@dangerousthings/tailwind-preset", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "tailwindcss": "^3.4.0", + "typescript": "^5.8.0" + }, + "peerDependencies": { + "tailwindcss": "^3.4.0" + } + }, "packages/tokens": { "name": "@dangerousthings/tokens", "version": "0.2.0", diff --git a/package.json b/package.json index 8da07af..01dcbe6 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "devDependencies": { "@changesets/cli": "^2.30.0", + "puppeteer": "^24.38.0", "turbo": "^2.4.0", "typescript": "^5.7.0" }, diff --git a/packages/hex-background/package.json b/packages/hex-background/package.json new file mode 100644 index 0000000..7996523 --- /dev/null +++ b/packages/hex-background/package.json @@ -0,0 +1,51 @@ +{ + "name": "@dangerousthings/hex-background", + "version": "0.1.0", + "description": "3D hexagon grid background for the Dangerous Things design system", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./native": { + "types": "./dist/native.d.ts", + "import": "./dist/native.js", + "default": "./dist/native.js" + } + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "react-native": "dist/native.js", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "react": ">=18", + "@react-three/fiber": ">=8", + "three": ">=0.150" + }, + "peerDependenciesMeta": { + "expo-gl": { + "optional": true + } + }, + "devDependencies": { + "@react-three/fiber": "^8.18.0", + "@types/react": "^18.2.0", + "@types/three": "^0.160.0", + "react": "^18.0.0", + "three": "^0.160.0", + "typescript": "^5.7.0" + } +} diff --git a/packages/hex-background/src/HexCamera.tsx b/packages/hex-background/src/HexCamera.tsx new file mode 100644 index 0000000..cecdd1a --- /dev/null +++ b/packages/hex-background/src/HexCamera.tsx @@ -0,0 +1,36 @@ +/** + * HexCamera — Orbiting camera for hexagon grid + * + * Continuously rotates around the grid center at a fixed radius and height. + */ + +import {useRef} from 'react'; +import {useFrame, useThree} from '@react-three/fiber'; + +export interface HexCameraProps { + /** Orbital rotation speed in radians per second @default 0.02 */ + speed?: number; + /** Orbital distance from center @default 8 */ + radius?: number; + /** Camera Y height @default 5 */ + height?: number; +} + +export function HexCamera({ + speed = 0.02, + radius = 8, + height = 5, +}: HexCameraProps) { + const angleRef = useRef(0); + const {camera} = useThree(); + + useFrame((_state, delta) => { + angleRef.current = (angleRef.current + speed * delta) % (Math.PI * 2); + const x = Math.cos(angleRef.current) * radius; + const z = Math.sin(angleRef.current) * radius; + camera.position.set(x, height, z); + camera.lookAt(0, 0, 0); + }); + + return null; +} diff --git a/packages/hex-background/src/HexGrid.tsx b/packages/hex-background/src/HexGrid.tsx new file mode 100644 index 0000000..bc0ea32 --- /dev/null +++ b/packages/hex-background/src/HexGrid.tsx @@ -0,0 +1,148 @@ +/** + * HexGrid — Core 3D hexagon grid renderer + * + * Renders an instanced mesh of animated hexagonal columns using Three.js. + * Source: dt-shopify-storefront HexagonGrid.tsx + */ + +import {useRef, useMemo, useEffect} from 'react'; +import {useFrame} from '@react-three/fiber'; +import { + InstancedMesh, + Object3D, + Shape, + ExtrudeGeometry, + MeshStandardMaterial, + MathUtils, +} from 'three'; + +export interface HexGridProps { + rows: number; + cols: number; + /** Hexagon radius in world units @default 0.5 */ + hexRadius?: number; + /** Extrude depth of hex geometry @default 1 */ + extrudeHeight?: number; + /** Spacing between hexagons @default 0.05 */ + margin?: number; + /** Maximum random height for animation @default 3 */ + maxHeight?: number; + /** Interval (ms) between height target changes @default 1500 */ + animationInterval?: number; +} + +function computeHexPositions( + rows: number, + cols: number, + hexWidth: number, + hexHeight: number, + margin: number, +) { + const positions: {x: number; z: number}[] = []; + const horizontalStep = hexWidth * 0.75 + margin; + const verticalStep = hexHeight * 0.866 + margin; + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const x = col * horizontalStep; + const z = row * verticalStep + (col % 2 ? verticalStep / 2 : 0); + const centerX = ((cols - 1) * horizontalStep) / 2; + const centerZ = ((rows - 1) * verticalStep) / 2; + positions.push({x: x - centerX, z: z - centerZ}); + } + } + return positions; +} + +export function HexGrid({ + rows, + cols, + hexRadius = 0.5, + extrudeHeight = 1, + margin = 0.05, + maxHeight = 3, + animationInterval = 1500, +}: HexGridProps) { + const meshRef = useRef(null); + + const positions = useMemo( + () => computeHexPositions(rows, cols, hexRadius * 2, extrudeHeight, margin), + [rows, cols, hexRadius, extrudeHeight, margin], + ); + + const targetHeightsRef = useRef( + positions.map(() => 1 + Math.random() * maxHeight), + ); + const currentHeightsRef = useRef( + positions.map(() => 1 + Math.random() * maxHeight), + ); + + // Reuse dummy object for matrix updates — 15-20% FPS improvement + const dummyRef = useRef(new Object3D()); + + const hexGeometry = useMemo(() => { + const shape = new Shape(); + for (let i = 0; i < 6; i++) { + const angle = (Math.PI / 3) * i; + const x = hexRadius * Math.cos(angle); + const z = hexRadius * Math.sin(angle); + i === 0 ? shape.moveTo(x, z) : shape.lineTo(x, z); + } + shape.closePath(); + const geometry = new ExtrudeGeometry(shape, { + depth: 1, + bevelEnabled: false, + }); + geometry.rotateX(Math.PI / 2); + geometry.computeVertexNormals(); + return geometry; + }, [hexRadius]); + + // Regenerate target heights at interval + useEffect(() => { + const interval = setInterval(() => { + targetHeightsRef.current = positions.map( + () => 0.5 + Math.random() * maxHeight, + ); + }, animationInterval); + return () => clearInterval(interval); + }, [positions, maxHeight, animationInterval]); + + // Animate per frame: lerp current heights toward targets + useFrame((_state, delta) => { + if (!meshRef.current) return; + const dummy = dummyRef.current; + + positions.forEach(({x, z}, i) => { + currentHeightsRef.current[i] = MathUtils.lerp( + currentHeightsRef.current[i], + targetHeightsRef.current[i], + delta * 0.5, + ); + const height = currentHeightsRef.current[i]; + + dummy.position.set(x, height / 2, z); + dummy.scale.set(1, height, 1); + dummy.rotation.set(0, 0, 0); + dummy.updateMatrix(); + meshRef.current!.setMatrixAt(i, dummy.matrix); + }); + + meshRef.current.instanceMatrix.needsUpdate = true; + }); + + return ( + + ); +} diff --git a/packages/hex-background/src/HexGridBackground.tsx b/packages/hex-background/src/HexGridBackground.tsx new file mode 100644 index 0000000..779605f --- /dev/null +++ b/packages/hex-background/src/HexGridBackground.tsx @@ -0,0 +1,141 @@ +/** + * HexGridBackground — Full-viewport hex grid background + * + * Drop-in component that renders a fixed, semi-transparent 3D hexagon grid + * behind page content. Automatically sizes grid to viewport and detects + * automated browsers to skip rendering. + */ + +import {useState, useEffect, useRef, Suspense} from 'react'; +import {Canvas} from '@react-three/fiber'; +import {HexGrid} from './HexGrid'; +import {HexCamera} from './HexCamera'; + +export interface HexGridBackgroundProps { + /** Canvas opacity @default 0.5 */ + opacity?: number; + /** Hexagon radius @default 0.5 */ + hexRadius?: number; + /** Spacing between hexagons @default 0.05 */ + margin?: number; + /** Maximum animation height @default 3 */ + maxHeight?: number; + /** Height target refresh interval in ms @default 1500 */ + animationInterval?: number; + /** Camera orbital speed @default 0.02 */ + cameraSpeed?: number; + /** Camera orbital radius @default 8 */ + cameraRadius?: number; + /** Camera field of view @default 40 */ + fov?: number; +} + +function calculateGridDimensions( + width: number, + height: number, + cameraDistance: number, + fov: number, + hexRadius: number, + margin: number, +): {rows: number; cols: number} { + const aspect = width / height; + const fovRadians = (fov * Math.PI) / 180; + const visibleHeight = 2 * cameraDistance * Math.tan(fovRadians / 2); + const visibleWidth = visibleHeight * aspect; + + const hexWidth = hexRadius * 2; + const horizontalStep = hexWidth * 0.75 + margin; + const verticalStep = hexWidth * 0.866 + margin; + + const cols = Math.ceil((visibleWidth / horizontalStep) * 1.5); + const rows = Math.ceil((visibleHeight / verticalStep) * 1.5); + + return { + rows: Math.min(Math.max(rows, 15), 35), + cols: Math.min(Math.max(cols, 15), 35), + }; +} + +export function HexGridBackground({ + opacity = 0.5, + hexRadius = 0.5, + margin = 0.05, + maxHeight = 3, + animationInterval = 1500, + cameraSpeed = 0.02, + cameraRadius = 8, + fov = 40, +}: HexGridBackgroundProps) { + const [mounted, setMounted] = useState(false); + const [isAutomated, setIsAutomated] = useState(false); + const [gridDimensions, setGridDimensions] = useState({rows: 20, cols: 20}); + const resizeTimeout = useRef>(); + + useEffect(() => { + setMounted(true); + + // Detect automated browsers (Selenium, Puppeteer, etc.) + if (typeof navigator !== 'undefined' && (navigator as any).webdriver) { + setIsAutomated(true); + return; + } + + const updateDimensions = () => { + const dims = calculateGridDimensions( + window.innerWidth, + window.innerHeight, + cameraRadius, + fov, + hexRadius, + margin, + ); + setGridDimensions(dims); + }; + + updateDimensions(); + + const handleResize = () => { + clearTimeout(resizeTimeout.current); + resizeTimeout.current = setTimeout(updateDimensions, 250); + }; + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + clearTimeout(resizeTimeout.current); + }; + }, [cameraRadius, fov, hexRadius, margin]); + + if (!mounted || isAutomated) return null; + + return ( + + + + + + + + + + ); +} diff --git a/packages/hex-background/src/index.ts b/packages/hex-background/src/index.ts new file mode 100644 index 0000000..9042c1c --- /dev/null +++ b/packages/hex-background/src/index.ts @@ -0,0 +1,23 @@ +/** + * @dangerousthings/hex-background + * + * 3D hexagon grid background for the Dangerous Things design system. + * Uses Three.js + React Three Fiber for instanced WebGL rendering. + * + * Web usage: + * import { HexGridBackground } from '@dangerousthings/hex-background'; + * + * + * React Native usage: + * import { HexGridBackground } from '@dangerousthings/hex-background/native'; + * + */ + +export { HexGridBackground } from './HexGridBackground'; +export type { HexGridBackgroundProps } from './HexGridBackground'; + +export { HexGrid } from './HexGrid'; +export type { HexGridProps } from './HexGrid'; + +export { HexCamera } from './HexCamera'; +export type { HexCameraProps } from './HexCamera'; diff --git a/packages/hex-background/src/native.ts b/packages/hex-background/src/native.ts new file mode 100644 index 0000000..eee52a6 --- /dev/null +++ b/packages/hex-background/src/native.ts @@ -0,0 +1,25 @@ +/** + * React Native entry point for @dangerousthings/hex-background + * + * Uses @react-three/fiber/native renderer with expo-gl. + * Consumers must install: expo-gl, @react-three/fiber, three + * + * Note: The HexGridBackground component uses web-specific APIs (window, CSS units). + * For React Native, use HexGrid and HexCamera directly within a native Canvas: + * + * import { Canvas } from '@react-three/fiber/native'; + * import { HexGrid, HexCamera } from '@dangerousthings/hex-background/native'; + * + * + * + * + * + * + * + */ + +export { HexGrid } from './HexGrid'; +export type { HexGridProps } from './HexGrid'; + +export { HexCamera } from './HexCamera'; +export type { HexCameraProps } from './HexCamera'; diff --git a/packages/hex-background/tsconfig.json b/packages/hex-background/tsconfig.json new file mode 100644 index 0000000..f93df5f --- /dev/null +++ b/packages/hex-background/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 5336a59..242f00c 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -1,6 +1,6 @@ # @dangerousthings/react-native -React Native components for the Dangerous Things design system — 18 themed components built on React Native Paper with cyberpunk beveled aesthetics. +React Native components for the Dangerous Things design system — 22 themed components built on React Native Paper with cyberpunk beveled aesthetics. ## Install @@ -74,6 +74,9 @@ Options: | `DTMediaFrame` | Diagonal beveled frame for images | | `DTAccordion` | Collapsible sections with accent border | | `DTHexagon` | Decorative hexagon SVG shape | +| `DTBadgeOverlay` | Absolute-positioned badge wrapper (top-left, top-right, bottom-left, bottom-right) | +| `DTStaggerContainer` | Staggered scale-in entrance animation for child elements | +| `DTFeatureLegend` | Product feature grid with icons and rotated labels | ### Interactive @@ -83,6 +86,14 @@ Options: | `DTDrawer` | Side drawer with edge bevels | | `DTGallery` | Image gallery | | `DTMenu` | Dropdown menu with item variants | +| `DTMobileFilterOverlay` | Full-screen slide-up filter overlay with backdrop | + +### Animation Hooks + +| Hook | Description | +|------|-------------| +| `useScaleIn` | Scale 0→1 entrance animation | +| `usePulse` | Looping opacity pulse animation | ## Usage Examples @@ -99,6 +110,11 @@ import { DTButton, DTCard, DTTextInput } from "@dangerousthings/react-native"; Chip detected +// Card with selected state and progress bar + + Selected with 60% progress + + // Text input with error state ; +} + +export function DTBadgeOverlay({ + children, + position = 'bottom-right', + offset = 8, + style, +}: DTBadgeOverlayProps) { + const positionStyle: ViewStyle = { + position: 'absolute', + zIndex: 4, + ...(position.includes('top') ? {top: offset} : {bottom: offset}), + ...(position.includes('right') ? {right: offset} : {left: offset}), + }; + + return {children}; +} diff --git a/packages/react-native/src/components/DTButton.tsx b/packages/react-native/src/components/DTButton.tsx index 08897ba..ed9665f 100644 --- a/packages/react-native/src/components/DTButton.tsx +++ b/packages/react-native/src/components/DTButton.tsx @@ -1,18 +1,20 @@ /** * DT Button Component * - * A themed button following the Dangerous Things design language - * with SVG-based beveled corners. + * Interactive bevel button matching the storefront .menu-item-clipped pattern: + * - Default: outlined rectangle, no bevel, colored border with thick top + * - Pressed/hover: bottom-right bevel appears, fills with mode color, text goes dark + * - Selected: bevel persists, fills with 70% opacity mode color */ -import {StyleSheet, View, ViewStyle, StyleProp, Pressable} from 'react-native'; +import {useRef, useEffect} from 'react'; +import {StyleSheet, View, ViewStyle, StyleProp, Pressable, Animated} from 'react-native'; import {Text} from 'react-native-paper'; -import Svg, {Path} from 'react-native-svg'; +import Svg, {Path, Rect} from 'react-native-svg'; import {useDTTheme} from '../theme/DTThemeProvider'; import {type DTVariant, getVariantColor} from '../utils/variantColors'; -import {buildButtonBevelPath} from '../utils/bevelPaths'; +import {buildBeveledRectPath} from '../utils/bevelPaths'; import {useComponentLayout} from '../utils/useComponentLayout'; -import {useState} from 'react'; interface DTButtonProps { /** @@ -26,11 +28,16 @@ interface DTButtonProps { variant?: DTVariant; /** * Button display mode - * - 'outlined': Border with transparent background (default) - * - 'contained': Filled background + * - 'outlined': Border with transparent background, bevel on press (default) + * - 'contained': Filled background with static bevel * @default 'outlined' */ mode?: 'outlined' | 'contained'; + /** + * Whether the button is in a persistent selected state + * (bevel visible, filled with 70% opacity mode color) + */ + selected?: boolean; /** * Custom color (overrides variant) */ @@ -53,91 +60,169 @@ interface DTButtonProps { */ borderWidth?: number; /** - * Bevel size in pixels - * @default 8 + * Bevel size in pixels (bottom-right corner) + * @default 16 */ bevelSize?: number; } -/** - * DT-styled Button component with SVG beveled corners - * - * @example - * - * Scan NFC Tag - * - * - * @example - * - * View Results - * - */ export function DTButton({ children, variant = 'normal', mode = 'outlined', + selected = false, color, onPress, disabled = false, style, borderWidth = 2, - bevelSize = 8, + bevelSize = 16, }: DTButtonProps) { const theme = useDTTheme(); const {dimensions, onLayout, hasDimensions} = useComponentLayout(); - const [pressed, setPressed] = useState(false); + const pulseAnim = useRef(new Animated.Value(1)).current; + const pressed = useRef(false); const accentColor = getVariantColor(theme, variant, color); + const selectedColor = accentColor + 'B3'; // ~70% opacity const isContained = mode === 'contained'; - const bgColor = isContained ? accentColor : 'transparent'; - const textColor = isContained ? theme.colors.onPrimary : accentColor; + const useBevels = theme.custom.bevelMd > 0; const {width, height} = dimensions; - const opacity = disabled ? 0.5 : pressed ? 0.7 : 1; - const useBevels = theme.custom.bevelMd > 0; + + // Build SVG paths: rectangle (default) and beveled (active) + const rectPath = hasDimensions + ? buildBeveledRectPath(width, height, {corners: {}, strokeWidth: borderWidth}) + : ''; + const bevelPath = useBevels && hasDimensions + ? buildBeveledRectPath(width, height, { + corners: {bottomRight: bevelSize}, + strokeWidth: borderWidth, + }) + : ''; + + // Pulse animation for pressed/selected hover + useEffect(() => { + if (selected) { + // No pulse when just selected (matches storefront: animation: none on .selected) + pulseAnim.setValue(1); + } + return () => { + pulseAnim.stopAnimation(); + }; + }, [selected, pulseAnim]); + + const startPulse = () => { + Animated.loop( + Animated.sequence([ + Animated.timing(pulseAnim, { + toValue: 0.5, + duration: 1000, + useNativeDriver: true, + }), + Animated.timing(pulseAnim, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }), + ]), + ).start(); + }; + + const stopPulse = () => { + pulseAnim.stopAnimation(); + pulseAnim.setValue(1); + }; + + const handlePressIn = () => { + pressed.current = true; + startPulse(); + }; + + const handlePressOut = () => { + pressed.current = false; + if (!selected) { + stopPulse(); + } + }; + + // Determine visual state + const showBevel = isContained || selected; + + const getBgColor = (isPressed: boolean) => { + if (isContained) return accentColor; + if (selected) return selectedColor; + if (isPressed) return accentColor; + return 'transparent'; + }; + + const getTextColor = (isPressed: boolean) => { + if (isContained || selected || isPressed) return theme.colors.onPrimary; + return accentColor; + }; return ( setPressed(true)} - onPressOut={() => setPressed(false)} - style={[{opacity}, style]}> - - {useBevels && hasDimensions && ( - - + {({pressed: isPressedState}) => ( + + {/* Non-beveled mode (classic brand) */} + {!useBevels && ( + - - )} - - {typeof children === 'string' ? ( - - {children} - - ) : ( - children )} - - + {/* Beveled mode (DT brand) */} + {useBevels && hasDimensions && ( + + + {/* Thick top accent — rendered inside SVG for exact alignment */} + + + )} + + {typeof children === 'string' ? ( + + {children} + + ) : ( + children + )} + + + )} ); } diff --git a/packages/react-native/src/components/DTCard.tsx b/packages/react-native/src/components/DTCard.tsx index 24bb564..b189156 100644 --- a/packages/react-native/src/components/DTCard.tsx +++ b/packages/react-native/src/components/DTCard.tsx @@ -8,6 +8,10 @@ * - Bottom-right bevel: 2em (~32px) * - Bottom-left bevel: 1em (~16px) * - Border width: 0.2em (~3px) + * + * The progress bar is a structural element on the left edge (0 to bevelSizeSmall). + * It is always present. At 0 progress it shows surface color (empty bar). + * As progress increases, accent color fills from the bottom up. */ import {ReactNode} from 'react'; @@ -39,6 +43,12 @@ interface DTCardProps { * @default true when title is provided */ showHeader?: boolean; + /** + * Progress value (0–1) for left-edge vertical progress bar. + * Always present. At 0 the bar shows surface color (empty). + * @default 0 + */ + progress?: number; /** * Additional styles for the card container */ @@ -77,6 +87,114 @@ interface DTCardProps { onPress?: () => void; } +/** + * Build inner card path with left edge at bevelSizeSmall (for progress bar zone). + * 5-point polygon — no bottom-left bevel, straight vertical left edge. + */ +function buildInnerCardPath( + w: number, + h: number, + bevelBR: number, + bw: number, + bevelSizeSmall: number, +): string { + const right = w - bw; + const top = bw; + const bottom = h - bw; + const br = Math.min( + bevelBR - bw, + (right - bevelSizeSmall) / 3, + (bottom - top) / 3, + ); + if (right <= bevelSizeSmall || bottom <= top) return ''; + return [ + `M ${bevelSizeSmall} ${top}`, + `L ${right} ${top}`, + `L ${right} ${bottom - br}`, + `L ${right - br} ${bottom}`, + `L ${bevelSizeSmall} ${bottom}`, + 'Z', + ].join(' '); +} + +/** + * Build the full progress bar fill area path (inset 3px from frame on all sides). + * This is the region where the surface/accent gradient fills. + */ +function buildProgressAreaPath( + cardH: number, + bw: number, + bevelSizeSmall: number, +): string { + const left = bw; + const right = bevelSizeSmall - bw; + const top = bw; + const bevelStartY = cardH - bevelSizeSmall; + const bottomRight = cardH - bw * 2; // y at bottom-right of fill area (on bevel diagonal) + + if (right <= left || bevelStartY <= top) return ''; + + return [ + `M ${left} ${top}`, + `L ${right} ${top}`, + `L ${right} ${bottomRight}`, + `L ${left} ${bevelStartY}`, + 'Z', + ].join(' '); +} + +/** + * Build the accent-colored portion of the progress bar (fills from bottom). + * Returns empty string when progress is 0. + */ +function buildProgressFillPath( + cardH: number, + bw: number, + bevelSizeSmall: number, + progress: number, +): string { + const p = Math.max(0, Math.min(1, progress)); + if (p === 0) return ''; + + const left = bw; + const right = bevelSizeSmall - bw; + const top = bw; + const bevelStartY = cardH - bevelSizeSmall; + const bottomRight = cardH - bw * 2; + + if (right <= left || bevelStartY <= top) return ''; + + // Total fill area height (along the left edge, from top to bevelStartY) + const areaH = bevelStartY - top; + // How much of the area is filled from the bottom + const fillTop = top + areaH * (1 - p); + + if (fillTop >= bevelStartY) { + // Fill is entirely within the bevel zone + // The bevel diagonal goes from (left, bevelStartY) to (right, bottomRight) + // At y=fillTop, x on the diagonal: x = left + (fillTop - bevelStartY) * (right - left) / (bottomRight - bevelStartY) + const bevelH = bottomRight - bevelStartY; + if (bevelH <= 0) return ''; + const xAtFillTop = left + ((fillTop - bevelStartY) / bevelH) * (right - left); + if (xAtFillTop >= right) return ''; + return [ + `M ${xAtFillTop} ${fillTop}`, + `L ${right} ${fillTop}`, + `L ${right} ${bottomRight}`, + 'Z', + ].join(' '); + } + + // Fill extends above the bevel zone — rectangle + bevel triangle + return [ + `M ${left} ${fillTop}`, + `L ${right} ${fillTop}`, + `L ${right} ${bottomRight}`, + `L ${left} ${bevelStartY}`, + 'Z', + ].join(' '); +} + /** * DT-styled Card component with SVG beveled corners * @@ -96,6 +214,7 @@ export function DTCard({ borderColor, title, showHeader, + progress = 0, style, contentStyle, padding = 16, @@ -118,9 +237,21 @@ export function DTCard({ const outerPath = useBevels && hasDimensions ? buildCardBevelPath(width, height, bevelSize, bevelSizeSmall, 0) : ''; - // Inner bevel path at border inset (for background + frame cutout) + // Inner bevel path — left edge at bevelSizeSmall (progress bar zone) const innerPath = useBevels && hasDimensions - ? buildCardBevelPath(width, height, bevelSize - borderWidth, bevelSizeSmall - borderWidth, borderWidth) + ? buildInnerCardPath(width, height, bevelSize, borderWidth, bevelSizeSmall) + : ''; + + // Progress bar paths — computed once, reused in progress SVG and frame overlay + const progressAreaPath = useBevels && hasDimensions + ? buildProgressAreaPath(height, borderWidth, bevelSizeSmall) + : ''; + const progressFillPath = useBevels && hasDimensions && progress > 0 + ? buildProgressFillPath(height, borderWidth, bevelSizeSmall, progress) + : ''; + // Frame overlay: outer + inner + progressArea hole (evenodd punches window for progress bar) + const framePath = useBevels && hasDimensions + ? outerPath + ' ' + innerPath + (progressAreaPath ? ' ' + progressAreaPath : '') : ''; const content = ( @@ -147,7 +278,54 @@ export function DTCard({ )} - + {/* Progress bar fill area — beveled mode */} + {useBevels && hasDimensions && ( + + {/* Accent base — visible through the semi-transparent surface above. + Matches web where ::after sits over the accent-colored card bg. */} + + {/* Surface fill over accent base — 0.6 opacity lets accent bleed through. + Matches web --dt-progress-empty-opacity default. */} + + {/* Accent fill from bottom (progressed portion) */} + {progressFillPath !== '' && ( + + )} + + )} + {/* Progress bar — non-beveled (classic) mode */} + {!useBevels && ( + 0 ? Math.max(0, theme.custom.radius - borderWidth) : 0, + }]}> + + + )} + {shouldShowHeader && ( {title && ( @@ -161,7 +339,10 @@ export function DTCard({ )} {children} - {/* Frame overlay (above content) — beveled mode only */} + {/* Frame overlay (above content) — beveled mode only. + Uses evenodd with 3 sub-paths: outer + inner + progressArea. + The progress area path punches a hole in the frame so the + progress bar SVG underneath (zIndex:1) shows through. */} {useBevels && hasDimensions && ( @@ -221,7 +402,8 @@ const styles = StyleSheet.create({ overflow: 'hidden', }, header: { - paddingHorizontal: 16, + paddingLeft: 8, + paddingRight: 16, paddingVertical: 12, }, headerText: { @@ -232,4 +414,17 @@ const styles = StyleSheet.create({ frameOverlay: { zIndex: 2, }, + progressBar: { + position: 'absolute' as const, + left: 0, + top: 0, + bottom: 0, + backgroundColor: 'transparent', + zIndex: 3, + justifyContent: 'flex-end' as const, + overflow: 'hidden' as const, + }, + progressFill: { + width: '100%' as const, + }, }); diff --git a/packages/react-native/src/components/DTChip.tsx b/packages/react-native/src/components/DTChip.tsx index 76642de..9cc2c3c 100644 --- a/packages/react-native/src/components/DTChip.tsx +++ b/packages/react-native/src/components/DTChip.tsx @@ -61,6 +61,7 @@ export function DTChip({ = { + supported: 'normal', + disabled: 'emphasis', + unsupported: 'warning', +}; + +interface DTFeatureLegendProps { + features: DTFeatureItem[]; + /** + * Header title text + */ + title?: string; + /** + * Color variant for the header bar + * @default 'normal' + */ + variant?: DTVariant; + /** + * Number of columns in the grid + * @default 5 + */ + columns?: number; + /** + * Icon size in pixels + * @default 42 + */ + iconSize?: number; + style?: StyleProp; +} + +export function DTFeatureLegend({ + features, + title, + variant = 'normal', + columns = 5, + iconSize = 42, + style, +}: DTFeatureLegendProps) { + const theme = useDTTheme(); + const headerColor = getVariantColor(theme, variant); + const itemWidth = `${100 / columns}%` as const; + + return ( + + {title && ( + + + {title} + + + )} + + {features.map(feature => { + const featureColor = getVariantColor( + theme, + stateToVariant[feature.state], + ); + return ( + + + {feature.icon} + + + + {feature.name} + + + + ); + })} + + + ); +} + +const styles = StyleSheet.create({ + container: { + overflow: 'hidden', + }, + header: { + paddingHorizontal: 16, + paddingVertical: 8, + }, + headerText: { + fontWeight: '700', + letterSpacing: 0.5, + textTransform: 'uppercase', + }, + grid: { + flexDirection: 'row', + flexWrap: 'wrap', + }, + item: { + alignItems: 'center', + paddingVertical: 8, + paddingHorizontal: 4, + gap: 4, + }, + iconContainer: { + alignItems: 'center', + justifyContent: 'center', + }, + labelContainer: { + paddingTop: 4, + }, + label: { + fontSize: 11, + fontWeight: '600', + textAlign: 'center', + }, +}); diff --git a/packages/react-native/src/components/DTMobileFilterOverlay.tsx b/packages/react-native/src/components/DTMobileFilterOverlay.tsx new file mode 100644 index 0000000..73cec22 --- /dev/null +++ b/packages/react-native/src/components/DTMobileFilterOverlay.tsx @@ -0,0 +1,230 @@ +/** + * DT Mobile Filter Overlay + * + * Full-screen slide-up overlay for mobile filter menus. + * Source: dt-shopify-storefront MobileFilterMenu component. + */ + +import {ReactNode, useRef, useEffect} from 'react'; +import { + StyleSheet, + View, + ViewStyle, + StyleProp, + Pressable, + Animated, + Dimensions, + BackHandler, +} from 'react-native'; +import {Text, Portal} from 'react-native-paper'; +import {useDTTheme} from '../theme/DTThemeProvider'; +import {type DTVariant, getVariantColor} from '../utils/variantColors'; + +interface DTMobileFilterOverlayProps { + visible: boolean; + onDismiss: () => void; + /** + * Header title + */ + heading?: string; + /** + * Number of active filters (shown as badge in header) + */ + activeFilterCount?: number; + /** + * Callback to clear all filters + */ + onClearAll?: () => void; + /** + * Color variant for the header + * @default 'normal' + */ + variant?: DTVariant; + children: ReactNode; + style?: StyleProp; +} + +export function DTMobileFilterOverlay({ + visible, + onDismiss, + heading = 'Filters', + activeFilterCount, + onClearAll, + variant = 'normal', + children, + style, +}: DTMobileFilterOverlayProps) { + const theme = useDTTheme(); + const accentColor = getVariantColor(theme, variant); + const slideAnim = useRef(new Animated.Value(0)).current; + const backdropAnim = useRef(new Animated.Value(0)).current; + const screenHeight = Dimensions.get('window').height; + + useEffect(() => { + if (visible) { + Animated.parallel([ + Animated.timing(backdropAnim, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(slideAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + ]).start(); + } else { + Animated.parallel([ + Animated.timing(backdropAnim, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(slideAnim, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]).start(); + } + }, [visible, backdropAnim, slideAnim]); + + // Android back button + useEffect(() => { + if (!visible) return; + const handler = BackHandler.addEventListener('hardwareBackPress', () => { + onDismiss(); + return true; + }); + return () => handler.remove(); + }, [visible, onDismiss]); + + if (!visible) return null; + + const translateY = slideAnim.interpolate({ + inputRange: [0, 1], + outputRange: [screenHeight, 0], + }); + + return ( + + + {/* Backdrop */} + + + + + {/* Content */} + + {/* Header */} + + + + {heading} + + {activeFilterCount !== undefined && activeFilterCount > 0 && ( + + + {activeFilterCount} + + + )} + + + + CLOSE + + + + + {/* Clear all button */} + {onClearAll && activeFilterCount !== undefined && activeFilterCount > 0 && ( + + + Clear All Filters + + + )} + + {/* Filter content */} + {children} + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + ...StyleSheet.absoluteFillObject, + zIndex: 1000, + justifyContent: 'flex-end', + }, + backdrop: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + content: { + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + overflow: 'hidden', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 12, + }, + headerLeft: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + headerText: { + fontWeight: '700', + letterSpacing: 0.5, + }, + countBadge: { + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 10, + minWidth: 24, + alignItems: 'center', + }, + countText: { + fontSize: 12, + fontWeight: '700', + }, + closeText: { + fontSize: 14, + fontWeight: '600', + letterSpacing: 1, + }, + clearAll: { + paddingHorizontal: 16, + paddingVertical: 10, + borderBottomWidth: 1, + }, + clearAllText: { + fontSize: 14, + fontWeight: '600', + }, + body: { + flexGrow: 1, + }, +}); diff --git a/packages/react-native/src/components/DTStaggerContainer.tsx b/packages/react-native/src/components/DTStaggerContainer.tsx new file mode 100644 index 0000000..680e12e --- /dev/null +++ b/packages/react-native/src/components/DTStaggerContainer.tsx @@ -0,0 +1,66 @@ +/** + * DT Stagger Container + * + * Animates children with staggered scale-in entrances. + * Source: dt-shopify-storefront framer-motion staggered card entrance pattern. + */ + +import {ReactNode, Children, useRef, useEffect} from 'react'; +import {Animated, ViewStyle, StyleProp} from 'react-native'; + +interface DTStaggerContainerProps { + children: ReactNode; + /** + * Duration of each child's scale-in animation in ms + * @default 330 + */ + duration?: number; + /** + * Delay between each child's animation start in ms + * @default 75 + */ + interval?: number; + /** + * Additional styles for the container + */ + style?: StyleProp; +} + +export function DTStaggerContainer({ + children, + duration = 330, + interval = 75, + style, +}: DTStaggerContainerProps) { + const childArray = Children.toArray(children); + const animations = useRef(childArray.map(() => new Animated.Value(0))).current; + + useEffect(() => { + // Reset animations if child count changes + while (animations.length < childArray.length) { + animations.push(new Animated.Value(0)); + } + + const staggered = childArray.map((_, i) => + Animated.timing(animations[i], { + toValue: 1, + duration, + delay: i * interval, + useNativeDriver: true, + }), + ); + Animated.parallel(staggered).start(); + }, [childArray.length, duration, interval]); + + return ( + + {childArray.map((child, i) => ( + + {child} + + ))} + + ); +} diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 9144227..5c70ac9 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -30,6 +30,7 @@ export type { DTVariant } from './utils/variantColors'; export { getVariantColor } from './utils/variantColors'; export { buildBeveledRectPath } from './utils/bevelPaths'; export { useComponentLayout } from './utils/useComponentLayout'; +export { useScaleIn, usePulse } from './utils/animations'; // Component exports — existing export { DTCard, DTCardClipPath } from './components/DTCard'; @@ -50,6 +51,10 @@ export { DTMediaFrame } from './components/DTMediaFrame'; export { DTAccordion } from './components/DTAccordion'; export type { DTAccordionSection } from './components/DTAccordion'; +// Component exports — positioning & animation containers +export { DTBadgeOverlay } from './components/DTBadgeOverlay'; +export { DTStaggerContainer } from './components/DTStaggerContainer'; + // Component exports — complex interactive export { DTModal } from './components/DTModal'; export { DTDrawer } from './components/DTDrawer'; @@ -57,6 +62,11 @@ export { DTGallery } from './components/DTGallery'; export type { DTGalleryItem } from './components/DTGallery'; export { DTSearchInput } from './components/DTSearchInput'; +// Component exports — filter & feature +export { DTFeatureLegend } from './components/DTFeatureLegend'; +export type { DTFeatureItem } from './components/DTFeatureLegend'; +export { DTMobileFilterOverlay } from './components/DTMobileFilterOverlay'; + // Component exports — navigation & decorative export { DTMenu, DTMenuDropdown } from './components/DTMenu'; export type { DTMenuItem } from './components/DTMenu'; diff --git a/packages/react-native/src/utils/animations.ts b/packages/react-native/src/utils/animations.ts new file mode 100644 index 0000000..0760750 --- /dev/null +++ b/packages/react-native/src/utils/animations.ts @@ -0,0 +1,73 @@ +/** + * Reusable animation hooks for DT components + * + * Provides common animation patterns (scale-in, pulse) as hooks + * that consumers and new components can use consistently. + */ + +import {useRef, useEffect} from 'react'; +import {Animated} from 'react-native'; + +/** + * Scale-in entrance animation. + * Returns an Animated.Value (0→1) to use as a scale transform. + */ +export function useScaleIn(options?: { + enabled?: boolean; + duration?: number; + delay?: number; +}): Animated.Value { + const {enabled = true, duration = 330, delay = 0} = options ?? {}; + const anim = useRef(new Animated.Value(enabled ? 0 : 1)).current; + + useEffect(() => { + if (enabled) { + anim.setValue(0); + Animated.timing(anim, { + toValue: 1, + duration, + delay, + useNativeDriver: true, + }).start(); + } else { + anim.setValue(1); + } + }, [enabled, duration, delay, anim]); + + return anim; +} + +/** + * Pulse (ping) opacity animation loop. + * Returns an Animated.Value that oscillates between 1 and 0.3. + */ +export function usePulse(enabled = true): Animated.Value { + const anim = useRef(new Animated.Value(1)).current; + + useEffect(() => { + if (enabled) { + const loop = Animated.loop( + Animated.sequence([ + Animated.timing(anim, { + toValue: 0.3, + duration: 1000, + useNativeDriver: true, + }), + Animated.timing(anim, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }), + ]), + ); + loop.start(); + return () => { + loop.stop(); + anim.setValue(1); + }; + } + anim.setValue(1); + }, [enabled, anim]); + + return anim; +} diff --git a/packages/react-native/src/utils/variantColors.ts b/packages/react-native/src/utils/variantColors.ts index 2a09375..d27843b 100644 --- a/packages/react-native/src/utils/variantColors.ts +++ b/packages/react-native/src/utils/variantColors.ts @@ -6,8 +6,10 @@ */ import type {DTExtendedTheme} from '../theme/paperTheme'; +import type {DTVariant} from '@dangerousthings/tokens'; -export type DTVariant = 'normal' | 'emphasis' | 'warning' | 'success' | 'other'; +// Re-export DTVariant from the canonical source (tokens package) +export type {DTVariant} from '@dangerousthings/tokens'; /** String-typed keys in DTExtendedTheme['custom'] that map to mode colors */ type ModeColorKey = 'modeNormal' | 'modeEmphasis' | 'modeWarning' | 'modeSuccess' | 'modeOther'; diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 0000000..a815aed --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,42 @@ +{ + "name": "@dangerousthings/react", + "version": "0.1.0", + "description": "React web components for the Dangerous Things design system", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18", + "@dangerousthings/tokens": "*", + "@dangerousthings/web": "*" + }, + "devDependencies": { + "@dangerousthings/tokens": "*", + "@dangerousthings/web": "*", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "typescript": "^5.8.0" + } +} diff --git a/packages/react/src/components/DTAccordion.tsx b/packages/react/src/components/DTAccordion.tsx new file mode 100644 index 0000000..a83dc1c --- /dev/null +++ b/packages/react/src/components/DTAccordion.tsx @@ -0,0 +1,83 @@ +/** + * DTAccordion — Expandable sections with animated height and chevron. + * + * CSS reference: forms-dt.css .dt-accordion, .dt-accordion-header, + * .dt-accordion-chevron, .dt-accordion-content + */ + +import { useState, useCallback, type ReactNode, type CSSProperties } from 'react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; + +export interface DTAccordionSection { + key: string; + title: string; + children: ReactNode; +} + +interface DTAccordionProps { + sections: DTAccordionSection[]; + /** Header variant when closed @default 'normal' */ + variant?: DTVariant; + /** Header variant when open @default 'emphasis' */ + activeVariant?: DTVariant; + /** Allow multiple sections open @default false */ + allowMultiple?: boolean; + initialOpenKeys?: string[]; + onSectionToggle?: (key: string, isOpen: boolean) => void; + className?: string; + style?: CSSProperties; +} + +export function DTAccordion({ + sections, + allowMultiple = false, + initialOpenKeys, + onSectionToggle, + className, + style, +}: DTAccordionProps) { + const [openKeys, setOpenKeys] = useState>( + new Set(initialOpenKeys), + ); + + const toggle = useCallback( + (key: string) => { + setOpenKeys(prev => { + const next = new Set(allowMultiple ? prev : []); + const isOpening = !prev.has(key); + if (isOpening) next.add(key); + else next.delete(key); + onSectionToggle?.(key, isOpening); + return next; + }); + }, + [allowMultiple, onSectionToggle], + ); + + return ( +
+ {sections.map(section => { + const isOpen = openKeys.has(section.key); + return ( +
+ +
+ {section.children} +
+
+ ); + })} +
+ ); +} diff --git a/packages/react/src/components/DTBadgeOverlay.tsx b/packages/react/src/components/DTBadgeOverlay.tsx new file mode 100644 index 0000000..1a39111 --- /dev/null +++ b/packages/react/src/components/DTBadgeOverlay.tsx @@ -0,0 +1,50 @@ +/** + * DTBadgeOverlay — Absolute-positioned badge container. + * + * CSS reference: bevels.css .dt-badge-bottom-right, .dt-badge-top-right, etc. + */ + +import type { ReactNode, CSSProperties } from 'react'; +import { cx } from '../utils/cx'; + +type BadgePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; + +interface DTBadgeOverlayProps { + children: ReactNode; + /** Badge position @default 'bottom-right' */ + position?: BadgePosition; + /** Offset from corner in px */ + offset?: number; + className?: string; + style?: CSSProperties; +} + +const positionClassMap: Record = { + 'top-left': 'dt-badge-top-left', + 'top-right': 'dt-badge-top-right', + 'bottom-left': 'dt-badge-overlay', + 'bottom-right': 'dt-badge-bottom-right', +}; + +export function DTBadgeOverlay({ + children, + position = 'bottom-right', + offset, + className, + style, +}: DTBadgeOverlayProps) { + const offsetStyle: CSSProperties = offset + ? { + ...(position.includes('top') ? { top: `${offset}px` } : { bottom: `${offset}px` }), + ...(position.includes('left') ? { left: `${offset}px` } : { right: `${offset}px` }), + } + : {}; + + return ( +
+ {children} +
+ ); +} diff --git a/packages/react/src/components/DTButton.tsx b/packages/react/src/components/DTButton.tsx new file mode 100644 index 0000000..90ee916 --- /dev/null +++ b/packages/react/src/components/DTButton.tsx @@ -0,0 +1,66 @@ +/** + * DTButton — Interactive bevel button with mode colors. + * + * CSS reference: bevels.css .dt-btn, .btn-primary, .mode-*, .selected + */ + +import type { ReactNode, CSSProperties, MouseEvent } from 'react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass } from '../utils/variantClasses'; + +interface DTButtonProps { + children: ReactNode; + /** Color variant @default 'normal' */ + variant?: DTVariant; + /** Display mode @default 'outlined' */ + mode?: 'outlined' | 'contained'; + /** Persistent selected state (bevel + 70% fill) */ + selected?: boolean; + onClick?: (e: MouseEvent) => void; + disabled?: boolean; + type?: 'button' | 'submit' | 'reset'; + className?: string; + style?: CSSProperties; +} + +export function DTButton({ + children, + variant = 'normal', + mode = 'outlined', + selected = false, + onClick, + disabled = false, + type = 'button', + className, + style, +}: DTButtonProps) { + if (mode === 'contained') { + return ( + + ); + } + + return ( + + ); +} diff --git a/packages/react/src/components/DTCard.tsx b/packages/react/src/components/DTCard.tsx new file mode 100644 index 0000000..cdc029e --- /dev/null +++ b/packages/react/src/components/DTCard.tsx @@ -0,0 +1,66 @@ +/** + * DTCard — Beveled card with header, progress bar, and mode colors. + * + * CSS reference: bevels.css .card / .dt-bevel-card, .card-title, + * ::after progress bar, .mode-* + * + * The progress bar is a structural element on the left edge (0 to bevel-sm). + * It is always present. At 0 progress it shows surface color (empty bar). + * As progress increases, accent color fills from the bottom up. + */ + +import type { ReactNode, CSSProperties } from 'react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass } from '../utils/variantClasses'; + +interface DTCardProps { + children: ReactNode; + /** Color variant @default 'normal' */ + variant?: DTVariant; + /** Card title (displayed in header bar) */ + title?: string; + /** Whether to show the accent header bar @default true when title is provided */ + showHeader?: boolean; + /** Progress value (0–100) for left-edge vertical progress bar @default 0 */ + progress?: number; + className?: string; + style?: CSSProperties; + onClick?: () => void; +} + +export function DTCard({ + children, + variant = 'normal', + title, + showHeader, + progress = 0, + className, + style, + onClick, +}: DTCardProps) { + const shouldShowHeader = showHeader ?? !!title; + const Tag = onClick ? 'button' : 'div'; + + return ( + + {shouldShowHeader && ( +
+ {title} +
+ )} + {children} +
+ ); +} diff --git a/packages/react/src/components/DTCheckbox.tsx b/packages/react/src/components/DTCheckbox.tsx new file mode 100644 index 0000000..edce60e --- /dev/null +++ b/packages/react/src/components/DTCheckbox.tsx @@ -0,0 +1,46 @@ +/** + * DTCheckbox — Beveled diamond checkbox. + * + * CSS reference: forms-dt.css input[type="checkbox"] + */ + +import type { CSSProperties } from 'react'; +import { cx } from '../utils/cx'; + +interface DTCheckboxProps { + checked: boolean; + onChange: (checked: boolean) => void; + disabled?: boolean; + label?: string; + className?: string; + style?: CSSProperties; +} + +export function DTCheckbox({ + checked, + onChange, + disabled = false, + label, + className, + style, +}: DTCheckboxProps) { + return ( + + ); +} diff --git a/packages/react/src/components/DTChip.tsx b/packages/react/src/components/DTChip.tsx new file mode 100644 index 0000000..50b673c --- /dev/null +++ b/packages/react/src/components/DTChip.tsx @@ -0,0 +1,47 @@ +/** + * DTChip — Small badge/chip with variant-based status colors. + * + * CSS reference: bevels.css .badge, .badge-success, .badge-error, etc. + */ + +import type { ReactNode, CSSProperties } from 'react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass, variantToBadgeClass } from '../utils/variantClasses'; + +interface DTChipProps { + children: ReactNode; + /** Color variant @default 'normal' */ + variant?: DTVariant; + /** Use mode-colored fill instead of status border color */ + filled?: boolean; + onClick?: () => void; + className?: string; + style?: CSSProperties; +} + +export function DTChip({ + children, + variant = 'normal', + filled = false, + onClick, + className, + style, +}: DTChipProps) { + const Tag = onClick ? 'button' : 'span'; + + return ( + + {children} + + ); +} diff --git a/packages/react/src/components/DTDrawer.tsx b/packages/react/src/components/DTDrawer.tsx new file mode 100644 index 0000000..8cde678 --- /dev/null +++ b/packages/react/src/components/DTDrawer.tsx @@ -0,0 +1,144 @@ +/** + * DTDrawer — Sliding side panel with beveled edges. + * + * CSS reference: bevels.css .dt-bevel-drawer-right, .dt-bevel-drawer-left + */ + +import { useEffect, useCallback, type ReactNode, type CSSProperties } from 'react'; +import { createPortal } from 'react-dom'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass } from '../utils/variantClasses'; + +interface DTDrawerProps { + visible: boolean; + onDismiss: () => void; + heading: string; + /** Color variant for heading bar @default 'emphasis' */ + headingVariant?: DTVariant; + /** Slide direction @default 'right' */ + position?: 'right' | 'left'; + /** Panel width @default '400px' */ + width?: string | number; + children: ReactNode; + className?: string; + style?: CSSProperties; +} + +export function DTDrawer({ + visible, + onDismiss, + heading, + headingVariant = 'emphasis', + position = 'right', + width = 400, + children, + className, + style, +}: DTDrawerProps) { + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') onDismiss(); + }, + [onDismiss], + ); + + useEffect(() => { + if (visible) { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + } + }, [visible, handleKeyDown]); + + if (!visible) return null; + + const bevelClass = position === 'right' ? 'dt-bevel-drawer-right' : 'dt-bevel-drawer-left'; + const widthValue = typeof width === 'number' ? `${width}px` : width; + + return createPortal( +
+ {/* Backdrop */} +
+ {/* Panel */} +
+ {/* Header */} +
+ {heading} + +
+ {/* Content */} +
+ {children} +
+
+
, + document.body, + ); +} + +function getVariantCSSVar(variant: DTVariant): string { + const map: Record = { + normal: '--mode-normal', + emphasis: '--mode-emphasis', + warning: '--mode-warning', + success: '--mode-success', + other: '--mode-other', + }; + return map[variant]; +} diff --git a/packages/react/src/components/DTFeatureLegend.tsx b/packages/react/src/components/DTFeatureLegend.tsx new file mode 100644 index 0000000..b9a9303 --- /dev/null +++ b/packages/react/src/components/DTFeatureLegend.tsx @@ -0,0 +1,77 @@ +/** + * DTFeatureLegend — Grid of product feature icons with rotated labels. + * + * CSS reference: feature-legend.css .dt-feature-legend, .dt-feature-legend-header, + * .dt-feature-legend-grid, .dt-feature-legend-item, .dt-feature-supported, etc. + */ + +import type { ReactNode, CSSProperties } from 'react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass, featureStateToVariant } from '../utils/variantClasses'; + +export interface DTFeatureItem { + key: string; + name: string; + icon: ReactNode; + state: 'supported' | 'disabled' | 'unsupported'; +} + +const stateClassMap: Record = { + supported: 'dt-feature-supported', + disabled: 'dt-feature-disabled', + unsupported: 'dt-feature-unsupported', +}; + +interface DTFeatureLegendProps { + features: DTFeatureItem[]; + title?: string; + /** Header variant @default 'normal' */ + variant?: DTVariant; + /** Grid columns @default 5 */ + columns?: number; + /** Icon size in px @default 42 */ + iconSize?: number; + className?: string; + style?: CSSProperties; +} + +export function DTFeatureLegend({ + features, + title, + variant = 'normal', + columns = 5, + iconSize = 42, + className, + style, +}: DTFeatureLegendProps) { + // Suppress unused import warning — featureStateToVariant is available for consumers + void featureStateToVariant; + + return ( +
+ {title && ( +
{title}
+ )} +
+ {features.map(feature => ( +
+
+ {feature.icon} +
+
{feature.name}
+
+ ))} +
+
+ ); +} diff --git a/packages/react/src/components/DTGallery.tsx b/packages/react/src/components/DTGallery.tsx new file mode 100644 index 0000000..d2b9afa --- /dev/null +++ b/packages/react/src/components/DTGallery.tsx @@ -0,0 +1,91 @@ +/** + * DTGallery — Image gallery with beveled media frame and thumbnails. + * + * CSS reference: bevels.css .dt-bevel-media + */ + +import { useState, type CSSProperties } from 'react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass } from '../utils/variantClasses'; + +export interface DTGalleryItem { + key: string; + src: string; + alt?: string; + thumbnail?: string; +} + +interface DTGalleryProps { + items: DTGalleryItem[]; + /** Color variant @default 'normal' */ + variant?: DTVariant; + className?: string; + style?: CSSProperties; +} + +export function DTGallery({ + items, + variant = 'normal', + className, + style, +}: DTGalleryProps) { + const [activeIndex, setActiveIndex] = useState(0); + + if (items.length === 0) return null; + + const current = items[activeIndex]; + + return ( +
+ {/* Main image */} +
+ {current.alt +
+ {/* Thumbnails */} + {items.length > 1 && ( +
+ {items.map((item, i) => ( + + ))} +
+ )} +
+ ); +} diff --git a/packages/react/src/components/DTHexagon.tsx b/packages/react/src/components/DTHexagon.tsx new file mode 100644 index 0000000..11ec4ca --- /dev/null +++ b/packages/react/src/components/DTHexagon.tsx @@ -0,0 +1,109 @@ +/** + * DTHexagon — Decorative SVG hexagon with optional animations. + * + * Uses inline SVG (not CSS classes) — same approach as the RN version. + * Animation classes from animations.css applied to wrapper div. + */ + +import type { ReactNode, CSSProperties } from 'react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass } from '../utils/variantClasses'; + +interface DTHexagonProps { + /** Diameter in pixels */ + size: number; + /** Color variant @default 'normal' */ + variant?: DTVariant; + /** Filled or outline @default true */ + filled?: boolean; + /** Enable animation @default false */ + animated?: boolean; + /** Animation type @default 'rotate' */ + animationType?: 'rotate' | 'pulse' | 'none'; + /** Animation duration in ms @default 2000 */ + animationDuration?: number; + /** Outline stroke width @default 2 */ + borderWidth?: number; + /** Custom color override */ + color?: string; + /** Opacity @default 1 */ + opacity?: number; + /** Centered content inside hexagon */ + children?: ReactNode; + className?: string; + style?: CSSProperties; +} + +/** Flat-top hexagon points for given size */ +function hexPoints(size: number): string { + const r = size / 2; + const cx = r; + const cy = r; + const points: [number, number][] = []; + for (let i = 0; i < 6; i++) { + const angle = (Math.PI / 3) * i - Math.PI / 6; + points.push([cx + r * Math.cos(angle), cy + r * Math.sin(angle)]); + } + return points.map(([x, y]) => `${x},${y}`).join(' '); +} + +export function DTHexagon({ + size, + variant = 'normal', + filled = true, + animated = false, + animationType = 'rotate', + animationDuration = 2000, + borderWidth = 2, + color, + opacity = 1, + children, + className, + style, +}: DTHexagonProps) { + const animClass = animated + ? animationType === 'rotate' + ? 'dt-animate-spin' + : animationType === 'pulse' + ? 'dt-animate-pulse' + : undefined + : undefined; + + return ( +
+ + + + {children && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/packages/react/src/components/DTLabel.tsx b/packages/react/src/components/DTLabel.tsx new file mode 100644 index 0000000..dea3b62 --- /dev/null +++ b/packages/react/src/components/DTLabel.tsx @@ -0,0 +1,33 @@ +/** + * DTLabel — Filled badge/label with top-right bevel and mode color. + * + * CSS reference: bevels.css .badge, .badge-mode, .mode-* + */ + +import type { ReactNode, CSSProperties } from 'react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass } from '../utils/variantClasses'; + +interface DTLabelProps { + children: ReactNode; + /** Color variant @default 'normal' */ + variant?: DTVariant; + className?: string; + style?: CSSProperties; +} + +export function DTLabel({ + children, + variant = 'normal', + className, + style, +}: DTLabelProps) { + return ( + + {children} + + ); +} diff --git a/packages/react/src/components/DTMediaFrame.tsx b/packages/react/src/components/DTMediaFrame.tsx new file mode 100644 index 0000000..94f7c43 --- /dev/null +++ b/packages/react/src/components/DTMediaFrame.tsx @@ -0,0 +1,26 @@ +/** + * DTMediaFrame — Diagonal beveled frame (top-left + bottom-right). + * + * CSS reference: bevels.css .dt-bevel-media + */ + +import type { ReactNode, CSSProperties } from 'react'; +import { cx } from '../utils/cx'; + +interface DTMediaFrameProps { + children: ReactNode; + className?: string; + style?: CSSProperties; +} + +export function DTMediaFrame({ + children, + className, + style, +}: DTMediaFrameProps) { + return ( +
+ {children} +
+ ); +} diff --git a/packages/react/src/components/DTMenu.tsx b/packages/react/src/components/DTMenu.tsx new file mode 100644 index 0000000..66d4df9 --- /dev/null +++ b/packages/react/src/components/DTMenu.tsx @@ -0,0 +1,108 @@ +/** + * DTMenu — Hierarchical menu with beveled items. + * + * CSS reference: forms-dt.css .dt-menu-item, .active, .selected + */ + +import { useState, useCallback, type ReactNode, type CSSProperties } from 'react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass } from '../utils/variantClasses'; + +export interface DTMenuItem { + id: string; + title: string; + icon?: ReactNode; + items?: DTMenuItem[]; + isActive?: boolean; + isSelected?: boolean; +} + +interface DTMenuProps { + items: DTMenuItem[]; + /** Color variant @default 'normal' */ + variant?: DTVariant; + onItemClick?: (id: string) => void; + className?: string; + style?: CSSProperties; +} + +export function DTMenu({ + items, + variant = 'normal', + onItemClick, + className, + style, +}: DTMenuProps) { + return ( +
+ {items.map(item => ( + + ))} +
+ ); +} + +function MenuItem({ + item, + level, + onItemClick, +}: { + item: DTMenuItem; + level: number; + onItemClick?: (id: string) => void; +}) { + const [expanded, setExpanded] = useState(false); + const hasChildren = item.items && item.items.length > 0; + + const handleClick = useCallback(() => { + if (hasChildren) { + setExpanded(prev => !prev); + } + onItemClick?.(item.id); + }, [hasChildren, item.id, onItemClick]); + + return ( + <> + + {hasChildren && expanded && item.items!.map(child => ( + + ))} + + ); +} diff --git a/packages/react/src/components/DTMobileFilterOverlay.tsx b/packages/react/src/components/DTMobileFilterOverlay.tsx new file mode 100644 index 0000000..13fc007 --- /dev/null +++ b/packages/react/src/components/DTMobileFilterOverlay.tsx @@ -0,0 +1,120 @@ +/** + * DTMobileFilterOverlay — Full-screen slide-up filter panel. + * + * CSS reference: forms-dt.css .dt-filter-overlay, .dt-filter-overlay-backdrop, + * .dt-filter-overlay-content + */ + +import { useEffect, useCallback, type ReactNode, type CSSProperties } from 'react'; +import { createPortal } from 'react-dom'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass } from '../utils/variantClasses'; + +interface DTMobileFilterOverlayProps { + visible: boolean; + onDismiss: () => void; + /** Heading text @default 'Filters' */ + heading?: string; + /** Number of active filters (shown as badge) */ + activeFilterCount?: number; + /** Called when "Clear All" is pressed */ + onClearAll?: () => void; + /** Color variant @default 'normal' */ + variant?: DTVariant; + children: ReactNode; + className?: string; + style?: CSSProperties; +} + +export function DTMobileFilterOverlay({ + visible, + onDismiss, + heading = 'Filters', + activeFilterCount, + onClearAll, + variant = 'normal', + children, + className, + style, +}: DTMobileFilterOverlayProps) { + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') onDismiss(); + }, + [onDismiss], + ); + + useEffect(() => { + if (visible) { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + } + }, [visible, handleKeyDown]); + + if (!visible) return null; + + return createPortal( +
+
+
+ {/* Header */} +
+ + {heading} + {activeFilterCount !== undefined && activeFilterCount > 0 && ( + + {activeFilterCount} + + )} + +
+ {onClearAll && activeFilterCount !== undefined && activeFilterCount > 0 && ( + + )} + +
+
+ {/* Content */} +
+ {children} +
+
+
, + document.body, + ); +} diff --git a/packages/react/src/components/DTModal.tsx b/packages/react/src/components/DTModal.tsx new file mode 100644 index 0000000..c1c9d43 --- /dev/null +++ b/packages/react/src/components/DTModal.tsx @@ -0,0 +1,96 @@ +/** + * DTModal — Portal-based modal with beveled card content. + * + * CSS reference: bevels.css .dt-bevel-modal, .card, .mode-* + */ + +import { useEffect, useCallback, type ReactNode, type CSSProperties } from 'react'; +import { createPortal } from 'react-dom'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; +import { getVariantClass } from '../utils/variantClasses'; + +interface DTModalProps { + visible: boolean; + onDismiss: () => void; + title?: string; + /** Color variant @default 'normal' */ + variant?: DTVariant; + children: ReactNode; + /** Whether clicking backdrop dismisses @default true */ + dismissable?: boolean; + className?: string; + style?: CSSProperties; +} + +export function DTModal({ + visible, + onDismiss, + title, + variant = 'normal', + children, + dismissable = true, + className, + style, +}: DTModalProps) { + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape' && dismissable) onDismiss(); + }, + [onDismiss, dismissable], + ); + + useEffect(() => { + if (visible) { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + } + }, [visible, handleKeyDown]); + + if (!visible) return null; + + return createPortal( +
+
+
+ {title &&
{title}
} +
{children}
+
+
, + document.body, + ); +} diff --git a/packages/react/src/components/DTProgressBar.tsx b/packages/react/src/components/DTProgressBar.tsx new file mode 100644 index 0000000..9aae065 --- /dev/null +++ b/packages/react/src/components/DTProgressBar.tsx @@ -0,0 +1,45 @@ +/** + * DTProgressBar — Angular progress bar (horizontal or vertical). + * + * CSS reference: forms-dt.css .dt-progress, .dt-progress-fill, .dt-progress-label + */ + +import type { CSSProperties } from 'react'; +import { cx } from '../utils/cx'; + +interface DTProgressBarProps { + /** Progress value (0–1) */ + value: number; + /** Text label below/beside the bar */ + label?: string; + /** Layout direction @default 'horizontal' */ + direction?: 'horizontal' | 'vertical'; + className?: string; + style?: CSSProperties; +} + +export function DTProgressBar({ + value, + label, + direction = 'horizontal', + className, + style, +}: DTProgressBarProps) { + const pct = Math.round(Math.max(0, Math.min(1, value)) * 100); + + return ( +
+
+
+
+ {label && {label}} +
+ ); +} diff --git a/packages/react/src/components/DTQuantityStepper.tsx b/packages/react/src/components/DTQuantityStepper.tsx new file mode 100644 index 0000000..a5e2252 --- /dev/null +++ b/packages/react/src/components/DTQuantityStepper.tsx @@ -0,0 +1,50 @@ +/** + * DTQuantityStepper — Beveled +/- stepper. + * + * CSS reference: forms-dt.css .dt-stepper, .dt-stepper-btn, .dt-stepper-value + */ + +import type { CSSProperties } from 'react'; +import { cx } from '../utils/cx'; + +interface DTQuantityStepperProps { + value: number; + onChange: (value: number) => void; + min?: number; + max?: number; + disabled?: boolean; + className?: string; + style?: CSSProperties; +} + +export function DTQuantityStepper({ + value, + onChange, + min = 0, + max = 99, + disabled = false, + className, + style, +}: DTQuantityStepperProps) { + return ( +
+ + {value} + +
+ ); +} diff --git a/packages/react/src/components/DTRadioGroup.tsx b/packages/react/src/components/DTRadioGroup.tsx new file mode 100644 index 0000000..2f47896 --- /dev/null +++ b/packages/react/src/components/DTRadioGroup.tsx @@ -0,0 +1,58 @@ +/** + * DTRadioGroup — Hexagonal radio buttons. + * + * CSS reference: forms-dt.css input[type="radio"], .dt-radio-option + */ + +import { useId, type CSSProperties } from 'react'; +import { cx } from '../utils/cx'; + +interface DTRadioOption { + value: string; + label: string; +} + +interface DTRadioGroupProps { + options: DTRadioOption[]; + value: string; + onChange: (value: string) => void; + name?: string; + className?: string; + style?: CSSProperties; +} + +export { type DTRadioOption }; + +export function DTRadioGroup({ + options, + value, + onChange, + name, + className, + style, +}: DTRadioGroupProps) { + const groupId = useId(); + const groupName = name || `dt-radio-${groupId}`; + + return ( +
+ {options.map(option => ( + + ))} +
+ ); +} diff --git a/packages/react/src/components/DTSearchInput.tsx b/packages/react/src/components/DTSearchInput.tsx new file mode 100644 index 0000000..7861710 --- /dev/null +++ b/packages/react/src/components/DTSearchInput.tsx @@ -0,0 +1,70 @@ +/** + * DTSearchInput — Search field with icon. + * + * CSS reference: forms-dt.css .input + */ + +import { useCallback, type CSSProperties, type KeyboardEvent } from 'react'; +import { cx } from '../utils/cx'; + +interface DTSearchInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + onSearch?: (value: string) => void; + className?: string; + style?: CSSProperties; +} + +export function DTSearchInput({ + value, + onChange, + placeholder = 'Search...', + onSearch, + className, + style, +}: DTSearchInputProps) { + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter' && onSearch) { + onSearch(value); + } + }, + [onSearch, value], + ); + + return ( +
+ onChange(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + style={{ width: '100%', padding: '8px 12px 8px 36px' }} + /> + + + + +
+ ); +} diff --git a/packages/react/src/components/DTStaggerContainer.tsx b/packages/react/src/components/DTStaggerContainer.tsx new file mode 100644 index 0000000..7c3f224 --- /dev/null +++ b/packages/react/src/components/DTStaggerContainer.tsx @@ -0,0 +1,39 @@ +/** + * DTStaggerContainer — Staggered scale-in animation for children. + * + * CSS reference: animations.css .dt-stagger-container + * CSS handles all stagger timing via nth-child — no JS animation needed. + */ + +import type { ReactNode, CSSProperties } from 'react'; +import { cx } from '../utils/cx'; + +interface DTStaggerContainerProps { + children: ReactNode; + /** Duration per child animation @default '0.33s' */ + duration?: string; + /** Delay between each child @default '75ms' */ + interval?: string; + className?: string; + style?: CSSProperties; +} + +export function DTStaggerContainer({ + children, + duration, + interval, + className, + style, +}: DTStaggerContainerProps) { + const cssVars: Record = {}; + if (duration) cssVars['--dt-stagger-duration'] = duration; + if (interval) cssVars['--dt-stagger-interval'] = interval; + + return ( +
+ {children} +
+ ); +} diff --git a/packages/react/src/components/DTSwitch.tsx b/packages/react/src/components/DTSwitch.tsx new file mode 100644 index 0000000..5d204a8 --- /dev/null +++ b/packages/react/src/components/DTSwitch.tsx @@ -0,0 +1,44 @@ +/** + * DTSwitch — Angular toggle switch. + * + * CSS reference: forms-dt.css .dt-switch, .dt-switch-track, .dt-switch-thumb + */ + +import type { CSSProperties } from 'react'; +import { cx } from '../utils/cx'; + +interface DTSwitchProps { + checked: boolean; + onChange: (checked: boolean) => void; + disabled?: boolean; + label?: string; + className?: string; + style?: CSSProperties; +} + +export function DTSwitch({ + checked, + onChange, + disabled = false, + label, + className, + style, +}: DTSwitchProps) { + return ( +
+ + {label && {label}} +
+ ); +} diff --git a/packages/react/src/components/DTTextInput.tsx b/packages/react/src/components/DTTextInput.tsx new file mode 100644 index 0000000..ed406b8 --- /dev/null +++ b/packages/react/src/components/DTTextInput.tsx @@ -0,0 +1,53 @@ +/** + * DTTextInput — Styled text input with focus glow. + * + * CSS reference: forms-dt.css input[type="text"], .input, .error + */ + +import type { CSSProperties, InputHTMLAttributes } from 'react'; +import { cx } from '../utils/cx'; + +interface DTTextInputProps extends Omit, 'className' | 'style'> { + /** Show error state */ + error?: boolean; + /** Label text above input */ + label?: string; + className?: string; + style?: CSSProperties; +} + +export function DTTextInput({ + error = false, + label, + className, + style, + id, + ...inputProps +}: DTTextInputProps) { + const inputId = id || (label ? `dt-input-${label.replace(/\s+/g, '-').toLowerCase()}` : undefined); + + return ( +
+ {label && ( + + )} + +
+ ); +} diff --git a/packages/react/src/components/DTWebThemeProvider.tsx b/packages/react/src/components/DTWebThemeProvider.tsx new file mode 100644 index 0000000..13cc51d --- /dev/null +++ b/packages/react/src/components/DTWebThemeProvider.tsx @@ -0,0 +1,42 @@ +/** + * DTWebThemeProvider + * + * Sets data-brand and data-theme attributes on a wrapper div. + * Provides React context for child components to read the active brand/theme. + */ + +import { createContext, type ReactNode } from 'react'; +import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens'; +import { cx } from '../utils/cx'; + +interface DTWebThemeContextValue { + brand: ThemeBrand; + theme: ThemeMode; +} + +export const DTWebThemeContext = createContext(null); + +interface DTWebThemeProviderProps { + /** Brand to apply @default 'dt' */ + brand?: ThemeBrand; + /** Theme mode @default 'dark' */ + theme?: ThemeMode; + /** Additional className for the wrapper div */ + className?: string; + children: ReactNode; +} + +export function DTWebThemeProvider({ + brand = 'dt', + theme = 'dark', + className, + children, +}: DTWebThemeProviderProps) { + return ( + +
+ {children} +
+
+ ); +} diff --git a/packages/react/src/hooks/useDTWebTheme.ts b/packages/react/src/hooks/useDTWebTheme.ts new file mode 100644 index 0000000..d5fae2f --- /dev/null +++ b/packages/react/src/hooks/useDTWebTheme.ts @@ -0,0 +1,14 @@ +/** + * Hook to access the DTWebThemeProvider context. + */ +import { useContext } from 'react'; +import { DTWebThemeContext } from '../components/DTWebThemeProvider'; +import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens'; + +export function useDTWebTheme(): { brand: ThemeBrand; theme: ThemeMode } { + const ctx = useContext(DTWebThemeContext); + if (!ctx) { + return { brand: 'dt', theme: 'dark' }; + } + return ctx; +} diff --git a/packages/react/src/hooks/usePulse.ts b/packages/react/src/hooks/usePulse.ts new file mode 100644 index 0000000..fb4af64 --- /dev/null +++ b/packages/react/src/hooks/usePulse.ts @@ -0,0 +1,24 @@ +/** + * Hook: toggles dt-animate-pulse CSS class based on enabled flag. + */ +import { useEffect } from 'react'; + +export function usePulse( + ref: React.RefObject, + enabled = false, +): void { + useEffect(() => { + const el = ref.current; + if (!el) return; + + if (enabled) { + el.classList.add('dt-animate-pulse'); + } else { + el.classList.remove('dt-animate-pulse'); + } + + return () => { + el.classList.remove('dt-animate-pulse'); + }; + }, [ref, enabled]); +} diff --git a/packages/react/src/hooks/useScaleIn.ts b/packages/react/src/hooks/useScaleIn.ts new file mode 100644 index 0000000..a6345c1 --- /dev/null +++ b/packages/react/src/hooks/useScaleIn.ts @@ -0,0 +1,29 @@ +/** + * Hook: applies dt-animate-scale-in CSS class on mount. + */ +import { useEffect } from 'react'; + +export function useScaleIn( + ref: React.RefObject, + options?: { duration?: number; delay?: number }, +): void { + useEffect(() => { + const el = ref.current; + if (!el) return; + + if (options?.duration) { + el.style.animationDuration = `${options.duration}ms`; + } + if (options?.delay) { + el.style.animationDelay = `${options.delay}ms`; + } + + el.classList.add('dt-animate-scale-in'); + + return () => { + el.classList.remove('dt-animate-scale-in'); + el.style.animationDuration = ''; + el.style.animationDelay = ''; + }; + }, [ref, options?.duration, options?.delay]); +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts new file mode 100644 index 0000000..cece900 --- /dev/null +++ b/packages/react/src/index.ts @@ -0,0 +1,60 @@ +/** + * @dangerousthings/react + * + * React web components for the Dangerous Things design system. + * Wraps @dangerousthings/web CSS classes with React component APIs + * matching @dangerousthings/react-native for cross-platform parity. + */ + +// Theme +export { DTWebThemeProvider } from './components/DTWebThemeProvider'; +export { useDTWebTheme } from './hooks/useDTWebTheme'; + +// Types (re-exported from tokens for convenience) +export type { DTVariant, ThemeBrand, ThemeMode } from '@dangerousthings/tokens'; +export { variantToClassName, variantToCSSProperty } from '@dangerousthings/tokens'; + +// Utilities +export { cx } from './utils/cx'; +export { getVariantClass, featureStateToVariant, variantToBadgeClass } from './utils/variantClasses'; + +// Hooks +export { useScaleIn } from './hooks/useScaleIn'; +export { usePulse } from './hooks/usePulse'; + +// Components — bevel & layout +export { DTCard } from './components/DTCard'; +export { DTButton } from './components/DTButton'; +export { DTLabel } from './components/DTLabel'; +export { DTChip } from './components/DTChip'; +export { DTMediaFrame } from './components/DTMediaFrame'; +export { DTModal } from './components/DTModal'; +export { DTDrawer } from './components/DTDrawer'; + +// Components — forms +export { DTTextInput } from './components/DTTextInput'; +export { DTCheckbox } from './components/DTCheckbox'; +export { DTSwitch } from './components/DTSwitch'; +export { DTRadioGroup } from './components/DTRadioGroup'; +export type { DTRadioOption } from './components/DTRadioGroup'; +export { DTQuantityStepper } from './components/DTQuantityStepper'; +export { DTSearchInput } from './components/DTSearchInput'; + +// Components — layout & animation +export { DTProgressBar } from './components/DTProgressBar'; +export { DTAccordion } from './components/DTAccordion'; +export type { DTAccordionSection } from './components/DTAccordion'; +export { DTStaggerContainer } from './components/DTStaggerContainer'; +export { DTBadgeOverlay } from './components/DTBadgeOverlay'; + +// Components — filter & feature +export { DTMenu } from './components/DTMenu'; +export type { DTMenuItem } from './components/DTMenu'; +export { DTFeatureLegend } from './components/DTFeatureLegend'; +export type { DTFeatureItem } from './components/DTFeatureLegend'; +export { DTMobileFilterOverlay } from './components/DTMobileFilterOverlay'; + +// Components — media & decorative +export { DTGallery } from './components/DTGallery'; +export type { DTGalleryItem } from './components/DTGallery'; +export { DTHexagon } from './components/DTHexagon'; diff --git a/packages/react/src/utils/cx.ts b/packages/react/src/utils/cx.ts new file mode 100644 index 0000000..aa85c61 --- /dev/null +++ b/packages/react/src/utils/cx.ts @@ -0,0 +1,7 @@ +/** + * Lightweight className composition utility. + * Filters out falsy values and joins with spaces. + */ +export function cx(...args: (string | false | null | undefined)[]): string { + return args.filter(Boolean).join(' '); +} diff --git a/packages/react/src/utils/variantClasses.ts b/packages/react/src/utils/variantClasses.ts new file mode 100644 index 0000000..7824c14 --- /dev/null +++ b/packages/react/src/utils/variantClasses.ts @@ -0,0 +1,31 @@ +/** + * DTVariant → CSS class name mapping + */ +import { variantToClassName } from '@dangerousthings/tokens'; +import type { DTVariant } from '@dangerousthings/tokens'; + +export function getVariantClass(variant: DTVariant): string { + return variantToClassName[variant]; +} + +/** Map feature state to variant */ +export function featureStateToVariant(state: 'supported' | 'disabled' | 'unsupported'): DTVariant { + const map: Record = { + supported: 'normal', + disabled: 'emphasis', + unsupported: 'warning', + }; + return map[state]; +} + +/** Map variant to badge CSS class (for status badge colors) */ +export function variantToBadgeClass(variant: DTVariant): string { + const map: Record = { + normal: 'badge-info', + emphasis: 'badge-warning', + warning: 'badge-error', + success: 'badge-success', + other: 'badge-info', + }; + return map[variant]; +} diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 0000000..f93df5f --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/packages/showcase/desktop/package.json b/packages/showcase/desktop/package.json index bf8a6b1..7014ebc 100644 --- a/packages/showcase/desktop/package.json +++ b/packages/showcase/desktop/package.json @@ -24,13 +24,24 @@ "clean": "rm -rf dist release" }, "dependencies": { + "@dangerousthings/react": "*", "@dangerousthings/tokens": "*", - "@dangerousthings/web": "*" + "@dangerousthings/web": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-icons": "^5.6.0" }, "devDependencies": { + "@dangerousthings/tailwind-preset": "*", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.0", "concurrently": "^9.0.0", "electron": "33.4.11", "electron-builder": "^25.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", "typescript": "^5.8.3", "vite": "^6.0.0", "wait-on": "^8.0.0" diff --git a/packages/showcase/desktop/postcss.config.mjs b/packages/showcase/desktop/postcss.config.mjs new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/packages/showcase/desktop/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/showcase/desktop/src/renderer/App.tsx b/packages/showcase/desktop/src/renderer/App.tsx new file mode 100644 index 0000000..0bcc707 --- /dev/null +++ b/packages/showcase/desktop/src/renderer/App.tsx @@ -0,0 +1,100 @@ +import { useState, useEffect } from 'react'; +import { themes } from '@dangerousthings/tokens'; +import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens'; +import { HomePage } from './pages/HomePage'; +import { BevelsPage } from './pages/BevelsPage'; +import { GlowsPage } from './pages/GlowsPage'; +import { FormsPage } from './pages/FormsPage'; +import { AnimationsPage } from './pages/AnimationsPage'; +import { CardsAdvancedPage } from './pages/CardsAdvancedPage'; +import { TokensPage } from './pages/TokensPage'; + +const navPages = [ + { hash: '', label: 'Home' }, + { hash: 'bevels', label: 'Bevels' }, + { hash: 'glows', label: 'Glows' }, + { hash: 'forms', label: 'Forms' }, + { hash: 'animations', label: 'Animations' }, + { hash: 'cards-advanced', label: 'Advanced Cards' }, + { hash: 'tokens', label: 'Tokens' }, +]; + +export function App() { + const [brand, setBrand] = useState('dt'); + const [theme, setTheme] = useState('dark'); + const [currentHash, setCurrentHash] = useState(''); + + // Sync brand/theme to so CSS custom properties cascade everywhere + useEffect(() => { + document.documentElement.setAttribute('data-brand', brand); + document.documentElement.setAttribute('data-theme', theme); + }, [brand, theme]); + + // Hash-based routing + useEffect(() => { + function onHashChange() { + setCurrentHash(window.location.hash.replace('#/', '').replace('#', '')); + } + window.addEventListener('hashchange', onHashChange); + onHashChange(); + return () => window.removeEventListener('hashchange', onHashChange); + }, []); + + function renderPage() { + switch (currentHash) { + case 'bevels': return ; + case 'glows': return ; + case 'forms': return ; + case 'animations': return ; + case 'cards-advanced': return ; + case 'tokens': return ; + default: return ; + } + } + + return ( + <> + +
+ {renderPage()} +
+ + ); +} diff --git a/packages/showcase/desktop/src/renderer/brand-switcher.ts b/packages/showcase/desktop/src/renderer/brand-switcher.ts deleted file mode 100644 index a4e3820..0000000 --- a/packages/showcase/desktop/src/renderer/brand-switcher.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { themes } from '@dangerousthings/tokens'; -import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens'; - -let currentBrand: ThemeBrand = 'dt'; -let currentMode: ThemeMode = 'dark'; - -export function initBrandSwitcher(container: HTMLElement) { - // Brand buttons - for (const theme of themes) { - const btn = document.createElement('button'); - btn.textContent = theme.name; - btn.className = 'brand-btn'; - btn.dataset.brand = theme.id; - if (theme.id === currentBrand) btn.classList.add('active'); - btn.addEventListener('click', () => switchBrand(theme.id)); - container.appendChild(btn); - } - - // Mode switcher - const modeContainer = document.createElement('div'); - modeContainer.className = 'mode-switcher'; - - const modes: ThemeMode[] = ['dark', 'light', 'auto']; - for (const mode of modes) { - const btn = document.createElement('button'); - btn.textContent = mode; - btn.className = 'mode-btn'; - btn.dataset.mode = mode; - if (mode === currentMode) btn.classList.add('active'); - btn.addEventListener('click', () => switchMode(mode)); - modeContainer.appendChild(btn); - } - - container.appendChild(modeContainer); - - // Set initial state - applyTheme(); -} - -function switchBrand(id: ThemeBrand) { - currentBrand = id; - applyTheme(); - updateBrandButtons(); -} - -function switchMode(mode: ThemeMode) { - currentMode = mode; - applyTheme(); - updateModeButtons(); -} - -function applyTheme() { - document.documentElement.setAttribute('data-brand', currentBrand); - document.documentElement.setAttribute('data-theme', currentMode); -} - -function updateBrandButtons() { - document.querySelectorAll('.brand-btn').forEach((btn) => { - const el = btn as HTMLButtonElement; - el.classList.toggle('active', el.dataset.brand === currentBrand); - }); -} - -function updateModeButtons() { - document.querySelectorAll('.mode-btn').forEach((btn) => { - const el = btn as HTMLButtonElement; - el.classList.toggle('active', el.dataset.mode === currentMode); - }); -} - -export function getCurrentBrand(): ThemeBrand { - return currentBrand; -} - -export function getCurrentMode(): ThemeMode { - return currentMode; -} diff --git a/packages/showcase/desktop/src/renderer/components/Section.tsx b/packages/showcase/desktop/src/renderer/components/Section.tsx new file mode 100644 index 0000000..f257eb3 --- /dev/null +++ b/packages/showcase/desktop/src/renderer/components/Section.tsx @@ -0,0 +1,23 @@ +import type { ReactNode, CSSProperties } from 'react'; + +export function Section({ title, description, children }: { title: string; description: string; children: ReactNode }) { + return ( +
+

{title}

+

{description}

+ {children} +
+ ); +} + +export function Row({ children, style }: { children: ReactNode; style?: CSSProperties }) { + return
{children}
; +} + +export function CodeLabel({ text }: { text: string }) { + return
{text}
; +} + +export function DemoLabel({ text }: { text: string }) { + return
{text}
; +} diff --git a/packages/showcase/desktop/src/renderer/index.html b/packages/showcase/desktop/src/renderer/index.html index 1005b30..cff8108 100644 --- a/packages/showcase/desktop/src/renderer/index.html +++ b/packages/showcase/desktop/src/renderer/index.html @@ -6,10 +6,7 @@ DT Design System — Desktop Showcase -
- -
-
- +
+ diff --git a/packages/showcase/desktop/src/renderer/main.ts b/packages/showcase/desktop/src/renderer/main.ts deleted file mode 100644 index 4c1e410..0000000 --- a/packages/showcase/desktop/src/renderer/main.ts +++ /dev/null @@ -1,56 +0,0 @@ -import '@dangerousthings/web/index.css'; -import './style.css'; - -import { initRouter } from './utils/router'; -import { initBrandSwitcher } from './brand-switcher'; -import { renderHomePage } from './pages/home'; -import { renderBevelsPage } from './pages/bevels'; -import { renderGlowsPage } from './pages/glows'; -import { renderFormsPage } from './pages/forms'; -import { renderTokensPage } from './pages/tokens'; - -const routes: Record void> = { - '': renderHomePage, - 'bevels': renderBevelsPage, - 'glows': renderGlowsPage, - 'forms': renderFormsPage, - 'tokens': renderTokensPage, -}; - -const sidebar = document.getElementById('sidebar')!; -const content = document.getElementById('content')!; - -// Build sidebar navigation -const navTitle = document.createElement('div'); -navTitle.className = 'nav-title'; -navTitle.textContent = 'DT DESIGN SYSTEM'; -sidebar.appendChild(navTitle); - -const brandContainer = document.createElement('div'); -brandContainer.className = 'brand-switcher-container'; -sidebar.appendChild(brandContainer); -initBrandSwitcher(brandContainer); - -const navLinks = document.createElement('div'); -navLinks.className = 'nav-links'; - -const pages = [ - { hash: '', label: 'Home' }, - { hash: 'bevels', label: 'Bevels' }, - { hash: 'glows', label: 'Glows' }, - { hash: 'forms', label: 'Forms' }, - { hash: 'tokens', label: 'Tokens' }, -]; - -for (const page of pages) { - const link = document.createElement('a'); - link.href = `#/${page.hash}`; - link.textContent = page.label; - link.className = 'nav-link'; - navLinks.appendChild(link); -} - -sidebar.appendChild(navLinks); - -// Initialize router -initRouter(routes, content); diff --git a/packages/showcase/desktop/src/renderer/main.tsx b/packages/showcase/desktop/src/renderer/main.tsx new file mode 100644 index 0000000..c9f8037 --- /dev/null +++ b/packages/showcase/desktop/src/renderer/main.tsx @@ -0,0 +1,6 @@ +import '@dangerousthings/web/index.css'; +import './style.css'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; + +createRoot(document.getElementById('app')!).render(); diff --git a/packages/showcase/desktop/src/renderer/pages/AnimationsPage.tsx b/packages/showcase/desktop/src/renderer/pages/AnimationsPage.tsx new file mode 100644 index 0000000..52d3c9b --- /dev/null +++ b/packages/showcase/desktop/src/renderer/pages/AnimationsPage.tsx @@ -0,0 +1,150 @@ +import { useState } from 'react'; +import { DTCard, DTStaggerContainer } from '@dangerousthings/react'; +import { Section, Row, CodeLabel } from '../components/Section'; + +function AnimBox({ className, label }: { className: string; label: string }) { + return ( +
+
+ {label} +
+
+ ); +} + +export function AnimationsPage() { + const [entranceKey, setEntranceKey] = useState(0); + const [staggerKey, setStaggerKey] = useState(0); + const [accordionExpanded, setAccordionExpanded] = useState(false); + + return ( + <> +

Animations

+

Entrance animations, interactive effects, stagger containers, and transition utilities.

+ +
+ + + + + + + +
+ +
+ + + + + + +
+ +
+ + {Array.from({ length: 12 }, (_, i) => ( + +
Item #{i + 1}
+
+ ))} +
+ + +
+ +
+
+ +
+
+ This content expands with a smooth max-height transition. The chevron rotates 180 degrees. +
+
+
+ +
+ +
+
+ {Array.from({ length: 20 }, (_, i) => ( +
+ Scrollbar line {i + 1} — thin neon scrollbar styled with --color-primary +
+ ))} +
+ +
+ + ); +} diff --git a/packages/showcase/desktop/src/renderer/pages/BevelsPage.tsx b/packages/showcase/desktop/src/renderer/pages/BevelsPage.tsx new file mode 100644 index 0000000..18901fa --- /dev/null +++ b/packages/showcase/desktop/src/renderer/pages/BevelsPage.tsx @@ -0,0 +1,103 @@ +import { DTCard, DTMediaFrame } from '@dangerousthings/react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { Section, Row, CodeLabel } from '../components/Section'; + +const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other']; + +export function BevelsPage() { + return ( + <> +

Bevels

+

Angular clip-path patterns from the DT design system. Active on the DT brand.

+ +
+ +
Dual bottom bevels — bottom-right (bevel-md) and bottom-left (bevel-sm). Uses the dual-element technique with ::before pseudo-element.
+
+ + +
Card without title — no header bar, just the beveled container.
+
+ +
+ +
+ + + + + + +
+ +
+ + Default + Success + Error + Warning + Info + + +
+ +
+ +
+ MEDIA FRAME +
+
+ +
+ +
+
+
+
Modal Content
+
Large dual bottom bevels
+
+
+ +
+ +
+ +
+ RIGHT +
+
+ LEFT +
+
+ +
+ +
+ +
+ SM +
+
+ +
+ +
+
+ Thick Top Border Accent +
+ +
+ +
+
+ {modes.map(mode => ( + +
mode-{mode}
+
+ ))} +
+ +
+ + ); +} diff --git a/packages/showcase/desktop/src/renderer/pages/CardsAdvancedPage.tsx b/packages/showcase/desktop/src/renderer/pages/CardsAdvancedPage.tsx new file mode 100644 index 0000000..1250c22 --- /dev/null +++ b/packages/showcase/desktop/src/renderer/pages/CardsAdvancedPage.tsx @@ -0,0 +1,210 @@ +import { createElement, useState, type CSSProperties } from 'react'; +import { + DTCard, + DTButton, + DTStaggerContainer, + DTFeatureLegend, +} from '@dangerousthings/react'; +import type { DTFeatureItem } from '@dangerousthings/react'; +import type { DTVariant } from '@dangerousthings/tokens'; +import { Section, Row, CodeLabel } from '../components/Section'; + +// Feature icons from dt-shopify-storefront +import { + MdOutlinePhonelinkRing, + MdOutlineVpnKey, + MdOutlineMobileScreenShare, + MdOutlineCreditCard, + MdOutlineCopyAll, + MdOutlineLightbulb, + MdOutlineThermostat, + MdOutlineSensors, + MdOutlineFitbit, + MdOutlineVibration, + MdOutlineExplore, + MdLightbulbOutline, + MdOutlineVisibility, +} from 'react-icons/md'; +import { FaUserShield } from 'react-icons/fa'; +import { LuBinary } from 'react-icons/lu'; + +const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other']; + +// Wrap react-icons to avoid IconType/ReactNode type mismatch +const ico = (C: any) => createElement(C, { style: { fontSize: '2rem' } }); + +// Full chip feature legend (9 features — from storefront UseCaseLegend) +const chipFeatures: DTFeatureItem[] = [ + { key: 'smartphone', name: 'Smartphone', icon: ico(MdOutlinePhonelinkRing), state: 'supported' }, + { key: 'access_control', name: 'Access Control', icon: ico(MdOutlineVpnKey), state: 'supported' }, + { key: 'digital_security', name: 'Digital Security', icon: ico(FaUserShield), state: 'supported' }, + { key: 'cryptography', name: 'Cryptography', icon: ico(LuBinary), state: 'unsupported' }, + { key: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported' }, + { key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'disabled' }, + { key: 'magic', name: 'UID Magic', icon: ico(MdOutlineCopyAll), state: 'supported' }, + { key: 'illumination', name: 'Illumination', icon: ico(MdOutlineLightbulb), state: 'unsupported' }, + { key: 'temperature', name: 'Temperature', icon: ico(MdOutlineThermostat), state: 'unsupported' }, +]; + +// Full biomagnet feature legend (4 features — from storefront MagnetUseCaseLegend) +const magnetFeatures: DTFeatureItem[] = [ + { key: 'sensing', name: 'Sensing', icon: ico(MdOutlineSensors), state: 'supported' }, + { key: 'lifting', name: 'Lifting', icon: ico(MdOutlineFitbit), state: 'supported' }, + { key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported' }, + { key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'unsupported' }, +]; + +// Full aesthetic feature legend (2 features — from storefront AestheticUseCaseLegend) +const aestheticFeatures: DTFeatureItem[] = [ + { key: 'illumination', name: 'Illumination', icon: ico(MdLightbulbOutline), state: 'supported' }, + { key: 'prominence', name: 'Prominence', icon: ico(MdOutlineVisibility), state: 'supported' }, +]; + +export function CardsAdvancedPage() { + const [staggerKey, setStaggerKey] = useState(0); + + return ( + <> +

Advanced Cards

+

Card color modes, progress bars, badge overlays, and interactive bevel buttons.

+ +
+
+ {modes.map(mode => ( + +
mode-{mode}
+
+ ))} +
+ +
+ + +
+
+ {[0, 25, 50, 75, 100].map((val, i) => ( + +
Progress: {val}%
+
+ ))} +
+ +
+ +
+
+ {[ + { label: 'LAB', mode: 'warning' as DTVariant, img: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=400&h=400&fit=crop' }, + { label: 'BUNDLE', mode: 'other' as DTVariant, img: 'https://images.unsplash.com/photo-1518770660439-4636190af475?w=400&h=400&fit=crop' }, + { label: 'NFC', mode: 'normal' as DTVariant, img: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=400&h=400&fit=crop' }, + ].map(badge => ( + +
+ + {badge.label} +
+
+ ))} +
+ + +

+ Also works on beveled media frames: +

+
+ {[ + { label: 'LAB', mode: 'warning' as DTVariant, img: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=400&h=400&fit=crop' }, + { label: 'BUNDLE', mode: 'other' as DTVariant, img: 'https://images.unsplash.com/photo-1518770660439-4636190af475?w=400&h=400&fit=crop' }, + { label: 'NFC', mode: 'normal' as DTVariant, img: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=400&h=400&fit=crop' }, + ].map(badge => ( +
+ + {badge.label} +
+ ))} +
+ + +

+ Missing image placeholder (no img element): +

+
+ {(['warning', 'other', 'normal'] as DTVariant[]).map(mode => ( +
+ ))} +
+ +
+ +
+ + {modes.map(mode => ( + {mode.toUpperCase()} + ))} + + + + {modes.map(mode => ( + {mode.toUpperCase()} SEL + ))} + + +
+ +
+
+ {['All Products', 'NFC Implants', 'RFID Tags', 'Accessories', 'Lab Products'].map((item, i) => ( + + ))} +
+ +
+ +
+ +
+ +
+ +
+ + + +
+ Supported + Disabled + Unsupported +
+
+ +
+ + {Array.from({ length: 10 }, (_, i) => ( + + {null} + + ))} + + + +
+ + ); +} diff --git a/packages/showcase/desktop/src/renderer/pages/FormsPage.tsx b/packages/showcase/desktop/src/renderer/pages/FormsPage.tsx new file mode 100644 index 0000000..76fdc84 --- /dev/null +++ b/packages/showcase/desktop/src/renderer/pages/FormsPage.tsx @@ -0,0 +1,122 @@ +import { useState } from 'react'; +import { + DTTextInput, + DTCheckbox, + DTSwitch, + DTRadioGroup, + DTProgressBar, + DTAccordion, + DTQuantityStepper, +} from '@dangerousthings/react'; +import type { DTAccordionSection } from '@dangerousthings/react'; +import { Section, Row, CodeLabel, DemoLabel } from '../components/Section'; + +export function FormsPage() { + const [checks, setChecks] = useState({ a: false, b: true, c: true, d: false }); + const [switches, setSwitches] = useState({ a: false, b: true, c: true, d: false }); + const [radioValue, setRadioValue] = useState('nfc'); + const [stepperValue, setStepperValue] = useState(1); + + const accordionSections: DTAccordionSection[] = [ + { key: 'size', title: 'Size', children:
Small, Medium, Large options available.
}, + { key: 'chip', title: 'Chip Type', children:
NTAG, DESFire, MIFARE Classic.
}, + { key: 'freq', title: 'Frequency', children:
13.56 MHz (HF), 125 kHz (LF).
}, + ]; + + return ( + <> +

Forms

+

DT-branded form components using @dangerousthings/react.

+ +
+ + +
+ + +
+