Compare commits

..

35 Commits

Author SHA1 Message Date
michael
a3ab5d892c pre-gitea addition 2026-07-13 21:40:03 -07:00
michael
1951cf98da fix(web): media placeholder reads LOADING MEDIA, not MISSING MEDIA
The ::after placeholder text on .dt-bevel-media and empty card bodies is
mostly seen while lazy media loads, where MISSING MEDIA reads as 'the
store is broken'. Neutral loading wording instead. Bump to 0.5.1.

Revert with: git revert <this-sha>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:55:18 -07:00
michael
2d49471ebb fix(web): .badge displays as inline-block so wrapped labels keep their shape
As a plain inline span, a badge that wraps to two lines fragments into
per-line boxes: the clip-path and ::before fill cover the wrong region,
so overflow text renders bare outside the beveled shape (storefront
DTLabel ribbons at <=375px viewports). inline-block keeps the badge one
box; the bevel and fill wrap all lines.

Revert with: git revert <this-sha>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 16:03:29 -07:00
michael
e33bf70896 fix(react): a11y attrs on DTMobileFilterOverlay
Add the screen-reader semantics that were missing:
- role="dialog" + aria-modal="true" on the content panel
- aria-labelledby pointing at the heading span (id via useId)
- aria-label="Close {heading}" on the ✕ button (was just text)
- aria-hidden="true" on the click-target backdrop

Without these, screen readers can't identify the mobile filter panel
as a dialog, can't announce its label, and the close button reads as
just "X". useId so multiple overlay instances on a page don't collide
on heading IDs.

Surfaced during dt-shopify pre-launch §10 verification — two
MobileFilterMenu.test.tsx assertions were dropped to make the suite
green pending this fix; storefront can put them back now.

Revert: git revert <THIS_COMMIT_SHA>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 16:38:38 -07:00
michael
a459b643e8 feat(web): dt-scale-in invisible-via-opacity during delay so children can self-measure
Prior keyframes held descendants at transform: scale(0) during the
animation-delay phase (with fill-mode: both). Any DTStaggerContainer
child performing getBoundingClientRect()-based self-measurement on
mount — most notably R3F's <Canvas>, but also chart libs, video
players, and virtualized lists — read a near-zero rect at the
transform-multiplied size and never recovered, because transform
changes don't fire ResizeObserver when they end.

New keyframes:
  0%     opacity 0, transform none    (during delay: invisible but
                                       full layout size, no transform —
                                       children measure correctly)
  0.001% opacity 0, transform scale(0) (snap to scaled-down at start)
  100%   opacity 1, transform scale(1) (animate scale + fade in)

Behavior change for consumers: entrance gains a subtle fade-in
alongside the scale-in. Functionally equivalent for the existing
DTStaggerContainer use cases. Fixes the SuggestedPlacements 3D
viewer "blank teal block until scroll" bug on the Hydrogen
storefront product pages.

Bumps @dangerousthings/web to 0.5.0 (MINOR — visual behavior change).

To revert: git revert <this-commit>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:38:42 -07:00
michael
1aeceb8f24 Main package.json fix 2026-05-05 14:20:26 -07:00
michael
e664cdaf3d Showcase fix 2026-05-05 14:18:53 -07:00
michael
0cfc945960 Fixed type in label 2026-05-05 14:16:32 -07:00
michael
77c6a5a3bf feat(DTGallery): add optional activeIndex prop for external control
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:42:10 -07:00
michael
27824ae189 chore(hex-background): bump 0.2.1, changelog for animation fix 2026-04-13 16:28:59 -07:00
michael
1f67ebf48a feat(hex-background): pass animationDuration through HexGridBackground 2026-04-13 16:26:17 -07:00
michael
5112e5c057 fix(hex-background): replace lerp with duration-based easing
Hexagons now interpolate via smoothstep over a fixed duration instead of
frame-rate-dependent lerp. Prevents height convergence to mean over time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:23:28 -07:00
michael
1be0070abf chore: version packages (web 0.4.1, react 2.0.1, react-native 0.4.1)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:26:57 -07:00
michael
c8c92d187d chore: version packages (web 0.4.0, react 2.0.0, react-native 0.4.0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:23:14 -07:00
michael
195c47602d feat: video letterboxing, card badge styling, component updates
- Add object-fit: contain + black background for video in DTMediaFrame
- Style .dt-card-badge with mode-colored background, padding, and
  bevel-aware positioning (negative right offset for clean diagonal clip)
- Restore DTStaggerContainer in react-native (was incorrectly deleted)
- Update DTGallery, DTModal, DTMobileFilterOverlay, DTMediaFrame components
- Refresh showcase pages for desktop and mobile

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:18:49 -07:00
michael
d1e2bf1964 fix: tune card badge position to 25% of bevel-md for responsive corner clipping 2026-03-13 05:03:46 -07:00
michael
926a282535 refactor: update showcase to use DTCard badge prop 2026-03-13 04:44:24 -07:00
michael
2dcf86c346 feat: add badge prop to DTCard for bevel-clipped badge slot 2026-03-12 19:21:11 -07:00
michael
8a91b21a11 feat: add .dt-card-badge positioning rule for card-level badge slot 2026-03-12 17:22:38 -07:00
michael
766266b799 Apply inner bevel clip-path to model-viewer and iframe elements
Extend the .dt-bevel-media inner clip-path selector to cover
model-viewer and iframe children, matching the existing treatment
for img and video. Without this, 3D viewers and embedded video
iframes filled edge-to-edge and covered the neon bevel border.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 13:26:48 -07:00
michael
36420868f5 Fix card-body-flush child bevel clip-path alignment
The child clip-path used bevel-md - 3px from child corner, but should
use bevel-md to align with the ::before inner surface diagonal. This
caused the bottom-right bevel border to appear too thin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:51:56 -07:00
michael
c8afd90527 Refactor DTStaggerContainer: wrapper-div approach with delay/interval props
Replace CSS nth-child stagger rules with JS-driven wrapper divs that set
animationDelay inline. Adds delay (base offset) and interval (ms between
children) props. Removes --dt-stagger-interval CSS variable and 24
nth-child rules in favor of unlimited children support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:18:12 -07:00
michael
34be1d0b9e DTCard: make progress optional, fill top-to-bottom
Progress bar now fills top-down (was bottom-up). When progress prop is
omitted the bar stays empty and the card renders at full size. Only cards
that explicitly pass progress={0–100} show the animated bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:04:15 -07:00
michael
6816ef4825 Add documentLevel mode to DTWebThemeProvider
When documentLevel is true, sets data-brand and data-theme on
document.documentElement instead of rendering a wrapper div.
This is needed for apps where CSS ancestor selectors must match
from the document root (e.g. [data-brand="dt"] .card).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:39:25 -07:00
michael
b319f15092 Fix feature legend data accuracy from storefront models
- NExT v2: fix access control (Legacy, not DESFire/iCLASS), digital
  security (unsupported), magic (supported via T5577), illumination
  (supported HF LED), data sharing (1 kB), payment (unsupported)
- Replace Titan with m0422a magnet: RP 22,860 G/g sensing, ~20 g
  lifting, Push/Pull haptics, Monopole polarity detection
- xLED HF: correct illumination colors and Low prominence
- All data sourced from dt-shopify-storefront model definitions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:12:03 -07:00
michael
68234e66f4 Add repository field to showcase desktop package.json
electron-builder requires a repository field to detect the
GitHub repo for publishing artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 12:41:03 -07:00
michael
2c9f7b8495 Fix showcase release: build all design system packages
Replace selective turbo filters with full build to ensure all
dependencies (tokens, web, tailwind-preset, hex-background, react)
are available for both desktop and mobile showcase builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 12:04:53 -07:00
michael
b137a99c78 Fix showcase release: add tailwind-preset to build filter
Desktop showcase depends on @dangerousthings/tailwind-preset
via its Tailwind config. Add it to the turbo build filter
alongside tokens and web.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 11:40:42 -07:00
michael
4f477a50d8 Add Windows and macOS builds to showcase release workflow
- Add desktop-windows job (NSIS installer + portable exe)
- Add desktop-macos job (dmg + zip, unsigned, x64 + arm64)
- Update create-release to collect artifacts from all platforms
- Update release notes with all artifact types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:59:20 -07:00
michael
741ee69079 Fix package-lock.json sync for CI
Regenerate lock file without --legacy-peer-deps to include
all resolved peer dependencies (react, react-native, etc.)
that npm ci requires.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:22:05 -07:00
michael
89aaac9548 Merge branch 'main' of https://github.com/dangerous-tac0s/dt-design-system 2026-03-08 10:07:23 -07:00
michael
09143edab8 Add hex background to showcase with interactive controls
- Render HexGridBackground globally on every page
- Add dedicated Hex Background page with sliders for all 8 props
- Lift hex props state to App for real-time global updates
- Add three, @react-three/fiber, hex-background deps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:05:47 -07:00
michael
5d2b245f5a Update docs to reflect current 6-package architecture
- README: add all 6 packages, React/Tailwind usage sections, remove glows refs
- CLAUDE.md: rewrite for 6 packages, 4-layer parity, build.cjs, storefront migration as implemented reference
- Package READMEs: fix monorepo links, remove glows references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:04:41 -07:00
michael
0cfa83259e Remove glows, refine feature legend, forms, and showcase pages
- Delete glows.css and GlowsPage (unused)
- Enhance DTFeatureLegend and feature-legend.css with improved layout
- Update DTRadioGroup styling
- Refine bevels.css, forms-dt.css
- Update AnimationsPage, CardsAdvancedPage, FormsPage, HomePage
- Add type:module to web package.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:03:48 -07:00
dangerous-tac0s
3d9ec57523 Merge pull request #4 from dangerous-tac0s/changeset-release/main
chore: version packages
2026-03-07 11:25:43 -08:00
59 changed files with 1578 additions and 1283 deletions

View File

@@ -39,7 +39,7 @@ jobs:
run: npm ci run: npm ci
- name: Build design system packages - name: Build design system packages
run: npx turbo run build --filter='@dangerousthings/tokens' --filter='@dangerousthings/web' run: npx turbo run build
- name: Build desktop showcase - name: Build desktop showcase
working-directory: packages/showcase/desktop working-directory: packages/showcase/desktop
@@ -60,6 +60,81 @@ jobs:
packages/showcase/desktop/release/*.deb packages/showcase/desktop/release/*.deb
retention-days: 30 retention-days: 30
# ─── Desktop Windows Build ──────────────────────────────────────
desktop-windows:
if: inputs.build_desktop
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build design system packages
run: npx turbo run build
- name: Build desktop showcase
working-directory: packages/showcase/desktop
run: npm run build
- name: Package desktop (Windows)
working-directory: packages/showcase/desktop
run: npx electron-builder --win
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload desktop artifacts
uses: actions/upload-artifact@v4
with:
name: desktop-windows
path: |
packages/showcase/desktop/release/*.exe
retention-days: 30
# ─── Desktop macOS Build ────────────────────────────────────────
desktop-macos:
if: inputs.build_desktop
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build design system packages
run: npx turbo run build
- name: Build desktop showcase
working-directory: packages/showcase/desktop
run: npm run build
- name: Package desktop (macOS)
working-directory: packages/showcase/desktop
run: npx electron-builder --mac --x64 --arm64
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# No CSC_LINK / CSC_KEY_PASSWORD = unsigned build
# Users will need to right-click > Open to bypass Gatekeeper
- name: Upload desktop artifacts
uses: actions/upload-artifact@v4
with:
name: desktop-macos
path: |
packages/showcase/desktop/release/*.dmg
packages/showcase/desktop/release/*.zip
retention-days: 30
# ─── Mobile Android Build ────────────────────────────────────── # ─── Mobile Android Build ──────────────────────────────────────
mobile-android: mobile-android:
if: inputs.build_mobile if: inputs.build_mobile
@@ -84,7 +159,7 @@ jobs:
run: npm ci run: npm ci
- name: Build design system packages - name: Build design system packages
run: npx turbo run build --filter='@dangerousthings/tokens' run: npx turbo run build
- name: Expo prebuild - name: Expo prebuild
working-directory: packages/showcase/mobile working-directory: packages/showcase/mobile
@@ -112,8 +187,8 @@ jobs:
# ─── Create GitHub Release ───────────────────────────────────── # ─── Create GitHub Release ─────────────────────────────────────
create-release: create-release:
needs: [desktop-linux, mobile-android] needs: [desktop-linux, desktop-windows, desktop-macos, mobile-android]
if: always() && (needs.desktop-linux.result == 'success' || needs.mobile-android.result == 'success') if: always() && (needs.desktop-linux.result == 'success' || needs.desktop-windows.result == 'success' || needs.desktop-macos.result == 'success' || needs.mobile-android.result == 'success')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -131,7 +206,7 @@ jobs:
run: | run: |
mkdir -p final-artifacts mkdir -p final-artifacts
# Desktop artifacts — replace 0.0.0 with release version # Desktop Linux artifacts
if [ -d release-artifacts/desktop-linux ]; then if [ -d release-artifacts/desktop-linux ]; then
for f in release-artifacts/desktop-linux/*; do for f in release-artifacts/desktop-linux/*; do
filename=$(basename "$f") filename=$(basename "$f")
@@ -140,7 +215,25 @@ jobs:
done done
fi fi
# Mobile APK — give it a descriptive name # Desktop Windows artifacts
if [ -d release-artifacts/desktop-windows ]; then
for f in release-artifacts/desktop-windows/*; do
filename=$(basename "$f")
newname=$(echo "$filename" | sed "s/0\.0\.0/${RELEASE_VERSION}/g")
cp "$f" "final-artifacts/$newname"
done
fi
# Desktop macOS artifacts
if [ -d release-artifacts/desktop-macos ]; then
for f in release-artifacts/desktop-macos/*; do
filename=$(basename "$f")
newname=$(echo "$filename" | sed "s/0\.0\.0/${RELEASE_VERSION}/g")
cp "$f" "final-artifacts/$newname"
done
fi
# Mobile APK
if [ -f release-artifacts/mobile-android/app-release.apk ]; then if [ -f release-artifacts/mobile-android/app-release.apk ]; then
cp release-artifacts/mobile-android/app-release.apk \ cp release-artifacts/mobile-android/app-release.apk \
"final-artifacts/dt-showcase-mobile-${RELEASE_VERSION}.apk" "final-artifacts/dt-showcase-mobile-${RELEASE_VERSION}.apk"
@@ -160,6 +253,9 @@ jobs:
### Desktop (Electron) ### Desktop (Electron)
- **AppImage** (Linux, portable) — download and `chmod +x` to run - **AppImage** (Linux, portable) — download and `chmod +x` to run
- **deb** (Debian/Ubuntu) — install with `sudo dpkg -i <file>.deb` - **deb** (Debian/Ubuntu) — install with `sudo dpkg -i <file>.deb`
- **exe** (Windows) — NSIS installer + portable exe
- **dmg** (macOS) — unsigned, right-click > Open to bypass Gatekeeper
- **zip** (macOS) — portable archive
### Mobile (Android) ### Mobile (Android)
- **APK** — sideload on any Android device (Settings > Install unknown apps) - **APK** — sideload on any Android device (Settings > Install unknown apps)

276
CLAUDE.md
View File

@@ -2,11 +2,14 @@
## Project Overview ## Project Overview
Monorepo for the Dangerous Things design system. Three npm packages publish under `@dangerousthings/*`: Monorepo for the Dangerous Things design system. Six npm packages publish under `@dangerousthings/*`:
- **tokens** — canonical design tokens in TypeScript, compiled to JS and auto-generated as CSS custom properties - **tokens** — canonical design tokens in TypeScript, compiled to JS and auto-generated as CSS custom properties
- **web** — CSS themes (bevels, glows, forms) consumed by web apps - **web** — CSS themes (bevels, forms, animations) consumed by web apps
- **react** — React web components wrapping web CSS
- **react-native** — React Native components built on React Native Paper - **react-native** — React Native components built on React Native Paper
- **tailwind-preset** — Tailwind CSS v3 preset mapping DT tokens
- **hex-background** — 3D hexagon grid background (Three.js)
Two brand themes: **dt** (neon cyberpunk / beveled), **classic** (dark navy / magenta). Two brand themes: **dt** (neon cyberpunk / beveled), **classic** (dark navy / magenta).
@@ -16,7 +19,7 @@ Two brand themes: **dt** (neon cyberpunk / beveled), **classic** (dark navy / ma
- Build everything: `npm run build` - Build everything: `npm run build`
- Typecheck everything: `npm run typecheck` - Typecheck everything: `npm run typecheck`
- Tokens build: `tsc` then `node dist/scripts/generate-css.js` - Tokens build: `tsc` then `node dist/scripts/generate-css.js`
- Web build: `node scripts/build.js` (copies CSS, assembles brand token CSS, copies fonts) - Web build: `node scripts/build.cjs` (copies CSS, assembles brand token CSS, copies fonts)
- React Native build: `tsc` - React Native build: `tsc`
- Versioning: Changesets (`npx changeset`, `npm run version-packages`, `npm run release`) - Versioning: Changesets (`npx changeset`, `npm run version-packages`, `npm run release`)
@@ -28,20 +31,22 @@ Two brand themes: **dt** (neon cyberpunk / beveled), **classic** (dark navy / ma
## Feature Parity ## Feature Parity
When adding or modifying a design token, component, or visual behavior, keep all three layers in sync: When adding or modifying a design token, component, or visual behavior, keep all four layers in sync:
1. **Tokens** — add/update the value in every brand file (`packages/tokens/src/brands/{dt,classic}.ts`) and in the shared type (`packages/tokens/src/types.ts`). 1. **Tokens** — add/update the value in every brand file (`packages/tokens/src/brands/{dt,classic}.ts`) and in the shared type (`packages/tokens/src/types.ts`).
2. **Web** — if the token maps to a CSS component, update the relevant CSS file under `packages/web/src/components/`. Ensure the CSS custom property name matches the token path (e.g., `--dt-color-brand-primary`). 2. **Web** — if the token maps to a CSS component, update the relevant CSS file under `packages/web/src/components/`. Ensure the CSS custom property name matches the token path (e.g., `--dt-color-brand-primary`).
3. **React Native** — update the corresponding component(s) under `packages/react-native/src/components/` and ensure the theme provider maps the new token. 3. **React** — if there is a React web component wrapper, update or create the corresponding component in `packages/react/src/components/`.
4. **React Native** — update the corresponding component(s) under `packages/react-native/src/components/` and ensure the theme provider maps the new token.
### Parity Checklist (use before closing any PR) ### Parity Checklist (use before closing any PR)
- [ ] New tokens exist in all three brand files with correct values - [ ] New tokens exist in all brand files with correct values
- [ ] Token types are updated if the shape of the token object changed - [ ] Token types are updated if the shape of the token object changed
- [ ] CSS generation script produces the expected custom properties (`npm run build:tokens` then inspect `dist/css/`) - [ ] CSS generation script produces the expected custom properties (`npm run build:tokens` then inspect `dist/css/`)
- [ ] Web CSS files reference the new/changed custom properties - [ ] Web CSS files reference the new/changed custom properties
- [ ] React web components are updated if applicable
- [ ] React Native components consume the new tokens via the theme - [ ] React Native components consume the new tokens via the theme
- [ ] All three packages build cleanly (`npm run build`) - [ ] All packages build cleanly (`npm run build`)
- [ ] TypeScript typechecks pass (`npm run typecheck`) - [ ] TypeScript typechecks pass (`npm run typecheck`)
## Testing ## Testing
@@ -65,10 +70,10 @@ When adding or modifying a design token, component, or visual behavior, keep all
### Adding a New Component ### Adding a New Component
1. Create the React Native component in `packages/react-native/src/components/DTFoo.tsx`. 1. If it needs new tokens, add them to all brand files and types first.
2. Re-export it from `packages/react-native/src/index.ts`. 2. If there is a web equivalent, add a CSS file in `packages/web/src/components/`.
3. If it needs new tokens, add them to all brand files and types first. 3. Create a React web component wrapper in `packages/react/src/components/DTFoo.tsx` and re-export from `packages/react/src/index.ts`.
4. If there is a web equivalent (CSS-only), add a CSS file in `packages/web/src/components/`. 4. Create the React Native component in `packages/react-native/src/components/DTFoo.tsx` and re-export from `packages/react-native/src/index.ts`.
5. Create a changeset: `npx changeset` — select affected packages and describe the change. 5. Create a changeset: `npx changeset` — select affected packages and describe the change.
### Adding a New Brand ### Adding a New Brand
@@ -99,19 +104,28 @@ GitHub Actions runs on push to `main` and on PRs:
- React Native components are prefixed `DT` (e.g., `DTButton`). - React Native components are prefixed `DT` (e.g., `DTButton`).
- Keep files focused — one component per file, one brand per file. - Keep files focused — one component per file, one brand per file.
## Storefront Component Migration — Features to Extend ## Storefront Component Migration — Implementation Reference
The DT Shopify Storefront (`dt-shopify-storefront`) is the genesis project for this design system. Several The DT Shopify Storefront (`dt-shopify-storefront`) is the genesis project for this design system.
storefront features are not yet represented in the design system and need to be baked in. This section The storefront features documented below have been migrated into the design system. This section
documents everything that must be supported before the storefront can fully adopt design system components. serves as a reference for the implemented components and their locations.
### Card Color Modes (per-card `--mode`) ### Implemented Features
The storefront assigns each card a color mode that controls border, header background, glow, and accent colors. | Feature | CSS (web) | React Component | Notes |
The design system's current `.card` component only uses `--color-primary` (always cyan). Cards must support |---|---|---|---|
per-instance mode overrides. | Card Color Modes | `.mode-normal`, `.mode-emphasis`, `.mode-warning`, `.mode-success`, `.mode-other` in `bevels.css` | `DTCard` | Five modes with `*-selected` variants at 70% opacity |
| Card Progress Bar | `.dt-card-progress` in `bevels.css` | `DTCard` (`progress` prop) | Vertical left-edge bar, 0-100%, fills on hover |
| Card Chip Badges | `.dt-badge-overlay` in `bevels.css` | `DTBadgeOverlay` | Positioned bottom-right, mode-color-aware |
| Card Hover/Selected | `.card.selected` and hover states in `bevels.css` | `DTCard` | Fills with `var(--dt-card-color-selected)` |
| Staggered Container | `.dt-stagger-container` in `animations.css` | `DTStaggerContainer` | CSS `nth-child` staggered `animation-delay` |
| Animation Library | `animations.css` | -- | `dt-scale-in`, `dt-fade-in`, `dt-slide-up`, `dt-pulse`, `dt-ping`, `dt-spin`, transition utilities |
| Filter System UI | `.dt-menu-item`, `.dt-filter-header`, `.dt-filter-overlay` in `forms-dt.css` | `DTMenu`, `DTMobileFilterOverlay` | Nested menus, filter accordions, mobile overlay |
| Feature Legend | `feature-legend.css` | `DTFeatureLegend` | Icon grid with rotated labels, mode-colored header, feature states |
| Theme Branching | `[data-brand="dt"]` / `[data-brand="classic"]` CSS scoping | -- | No JS conditionals needed; CSS handles visual differences |
| Scrollbar Styling | `scrollbar.css` (`.dt-scrollbar`) | -- | Thin scrollbar with mode-normal color |
**Five modes (tokens already exist, need component support):** **Card color modes reference:**
| Mode | Token | Default (DT dark) | Use Case | | Mode | Token | Default (DT dark) | Use Case |
|---|---|---|---| |---|---|---|---|
@@ -121,144 +135,20 @@ per-instance mode overrides.
| `mode-success` | `--color-accent` | `#00FF00` green | Positive/success states | | `mode-success` | `--color-accent` | `#00FF00` green | Positive/success states |
| `mode-other` | `--color-other` | `#FF00FF` magenta | Bundles, alternative states | | `mode-other` | `--color-other` | `#FF00FF` magenta | Bundles, alternative states |
Each mode also has a `*-selected` variant at 70% opacity for hover/active states. **Theme branching differences (handled by CSS, not JS):**
**Implementation pattern in storefront:** | Aspect | Cyberpunk (`data-brand="dt"`) | Clean (`data-brand="classic"`) |
```css |---|---|---|
.mode-normal { | Clip-paths | Heavy bevels | `border-radius: 0.75rem` |
--mode: var(--mode-normal); | Colors | Neon (#00FFFF, #FF00FF) | B&W or muted tones |
--accent-mode: var(--mode-emphasis); | Hover effects | Pulse glow animations | None |
--selected: var(--mode-normal-selected); | Typography | Tektur | system-ui sans-serif |
} | Text shadows | `0 0 8px var(--mode)` | None |
.selected { | Borders | Thick neon (5px top) | Subtle 1px |
background-color: var(--selected) !important;
color: black !important;
}
```
**Recommendation:** Add a `--dt-card-color` custom property that defaults to `--color-primary` and can be
overridden per-card. All card sub-components (header bg, border, glow, progress bar) should reference this
variable instead of `--color-primary` directly.
### Card Progress Bar (thick left edge)
Product cards have a vertical progress bar on the left edge that fills based on a percentage value.
This does not exist in the design system.
**Storefront implementation:**
- CSS variable: `--card-thick-edge-width: 0.75em`
- Class: `.progress` — left-side vertical bar inheriting the card's `--mode` color
- Fill: Dynamic height (0-100%) controlled by `progress` prop
- Hover: Fills to 100% on hover
- States: `.progress-indicator` (semi-transparent), `.progress-indicator-1` (darker with bevel clip)
**Recommendation:** Add an optional `.dt-card-progress` child element to the card component, driven by
a `--dt-card-progress` custom property (0-100).
### Card Chip Badges
Product cards display contextual badges (e.g., "LAB", "BUNDLE") positioned absolute bottom-right of the
image area, with background color matching the card's mode.
**Recommendation:** The existing `.badge` component in `bevels.css` could be extended with positioning
utilities (`.badge-overlay`, `.badge-bottom-right`) and mode-color-aware variants.
### Card Hover / Selected State
On hover, the storefront card fills with the mode's selected color and the progress bar fills to 100%.
The entire card gets a `.selected` class. The design system's `.card:hover` only applies a `drop-shadow`
but doesn't change fill color.
**Recommendation:** Add `.card.selected` and `.card:hover` states that fill with `var(--dt-card-color-selected)`
(defaulting to `rgba(var(--color-primary-rgb), 0.7)`).
### Staggered Card Container (Sequential Loading Animation)
The storefront animates product cards with staggered scale-in entrances — each card scales from 0 to 1 with
an incrementing delay. This creates a sequential "loading" effect across the grid.
**Current storefront pattern (per-card, using framer-motion):**
```tsx
<motion.div
initial={{scale: 0}}
animate={{scale: 1}}
transition={{ ease: 'easeInOut', duration: 0.33, delay: index * 0.075 }}
/>
```
**Recommendation:** Create a `dt-stagger-container` component that automatically assigns staggered
`animation-delay` values to its children. This should work with pure CSS (using `nth-child` or CSS
custom properties) so consumers don't need a JS animation library. Example:
```css
.dt-stagger-container {
--dt-stagger-duration: 0.33s;
--dt-stagger-interval: 75ms;
}
.dt-stagger-container > * {
animation: dt-scale-in var(--dt-stagger-duration) ease-in-out both;
}
.dt-stagger-container > :nth-child(1) { animation-delay: calc(0 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(2) { animation-delay: calc(1 * var(--dt-stagger-interval)); }
/* ... up to a reasonable limit, or use @property for dynamic calculation */
@keyframes dt-scale-in {
from { transform: scale(0); }
to { transform: scale(1); }
}
```
### Animation Library
The storefront uses several animation patterns that should be standardized in the design system:
**Entrance animations:**
- `dt-scale-in` — scale 0→1 (card entrance, staggered)
- `dt-fade-in` — opacity 0→1
- `dt-slide-up` — translateY(100%)→0 (mobile menus, modals)
**Interactive animations:**
- `dt-pulse` — opacity oscillation for hover glow (`2s cubic-bezier(0.4, 0, 0.6, 1) infinite`)
- `dt-ping` — scale + fade-out ping effect (used on active filter indicators)
- `dt-spin` — 360deg rotation (loading spinners)
**Transition utilities:**
- `dt-accordion-expand` — max-height 0→auto with 250ms ease-in-out
- `dt-chevron-rotate` — 180deg rotation for expand/collapse indicators
- `dt-progress-fill` — height transition for progress bars
**Recommendation:** Add `packages/web/src/components/animations.css` with `@keyframes` definitions
and utility classes. These should be brand-independent (no color references in animations).
### Filter System UI
The storefront's filter system includes several components that are not in the design system:
**NestedMenu:** Hierarchical filter navigation with recursive submenus.
- Level 0: Large icons (4xl) with vertical rotated labels below
- Level 1+: `.menu-item-clipped` buttons with bevel clip-path and thick top border
- Submenus: Portal-rendered, absolute positioned, hover-activated with 120ms close delay
- Selected state: `mode-emphasis` color, `menu-item-clipped-selected` class
**FilterAccordion:** Expandable filter panel with nested levels.
- Thick top border (5px in cyberpunk, 1px in clean)
- Active state: border and text color switch to `--mode-emphasis`
- Indent per level: `paddingLeft: (level + 1) * 16px`
- Chevron: MdExpandMore with 180deg rotation on expand
**MobileFilterMenu:** Full-screen overlay with spring animation.
- Backdrop: `rgba(0,0,0,0.5)` with blur(4px)
- Slide-up animation: `y: 100%``y: 0`
- Active filter count badge in header
- "Clear All Filters" button when filters are active
**Recommendation:** These are complex interactive components. Start with CSS patterns
(`.dt-menu-item`, `.dt-filter-header`) and leave JS behavior to consumers.
### Icon Set — Product Features & Filtering ### Icon Set — Product Features & Filtering
The storefront uses a specific set of icons from `react-icons` for product features and UI controls. Canonical icon set for the DT design system, sourced from `react-icons`.
These should be documented as the canonical icon set for the DT design system.
**Product Feature Icons (react-icons/md — Material Design):** **Product Feature Icons (react-icons/md — Material Design):**
@@ -271,7 +161,7 @@ These should be documented as the canonical icon set for the DT design system.
| Payment/NFC | `MdOutlineCreditCard` | react-icons/md | | Payment/NFC | `MdOutlineCreditCard` | react-icons/md |
| Smartphone Compat | `MdOutlinePhonelinkRing` | react-icons/md | | Smartphone Compat | `MdOutlinePhonelinkRing` | react-icons/md |
| Access Control | `MdOutlineVpnKey` | react-icons/md | | Access Control | `MdOutlineVpnKey` | react-icons/md |
| Temperature | `MdOutlineThermostat` | react-icons/md | | Sensors | `MdOutlineThermostat` | react-icons/md |
| NFC Sensing | `MdOutlineSensors` | react-icons/md | | NFC Sensing | `MdOutlineSensors` | react-icons/md |
| Fitness/Bio | `MdOutlineFitbit` | react-icons/md | | Fitness/Bio | `MdOutlineFitbit` | react-icons/md |
| Vibration | `MdOutlineVibration` | react-icons/md | | Vibration | `MdOutlineVibration` | react-icons/md |
@@ -306,80 +196,6 @@ These should be documented as the canonical icon set for the DT design system.
**Icon display standard:** Feature icons render at 42px in the UseCaseLegend grid. UI icons at 1.5em. **Icon display standard:** Feature icons render at 42px in the UseCaseLegend grid. UI icons at 1.5em.
Icons inherit color from their parent's `--mode` variable. Icons inherit color from their parent's `--mode` variable.
**Recommendation:** The design system should document this icon set as the canonical reference.
Consider bundling an icon sprite or re-exporting the specific react-icons subset as
`@dangerousthings/icons` for consistency across projects.
### Feature Legend Component
The storefront has a specialized `UseCaseLegend` component that displays product features in a
grid with icons, rotated labels, and a hover-driven detail header. This is a key product page
component that should be represented in the design system.
**Structure:**
- **Header bar:** Full-width, mode-colored background, shows chip name + hovered feature details
- **Icon grid:** 5-column flex layout with icons + vertical rotated labels
- **Labels:** `writing-mode: vertical-rl` + `transform: rotate(180deg)`, 0.85rem, font-weight 600
- **Feature states:** Supported (mode-normal/cyan), disabled (mode-other/magenta), unsupported (mode-warning/red)
**Color assignment in the legend uses hardcoded hex values** in `app/lib/cyberTheme.ts` — these
need to be migrated to use CSS variables. This is a known pre-existing issue in the storefront.
### Theme Branching Pattern
Many storefront components have two rendering paths: cyberpunk and clean. The design system should
support this via the two-axis theme system (`data-brand` + `data-theme`) so that components
automatically adapt without conditional JS logic.
**Key differences between branches:**
| Aspect | Cyberpunk (DT brand) | Clean |
|---|---|---|
| Clip-paths | Heavy bevels | `border-radius: 0.75rem` |
| Colors | Neon (#00FFFF, #FF00FF) | B&W or muted tones |
| Hover effects | Pulse glow animations | None |
| Typography | Tektur | system-ui sans-serif |
| Text shadows | `0 0 8px var(--mode)` | None |
| Borders | Thick neon (5px top) | Subtle 1px |
**Recommendation:** The clean theme should be a brand variant (e.g., `data-brand="classic"`) that
nullifies bevels, glows, and animations via CSS. Components should not need JS `if (isClean)` branches
if the design system CSS handles the visual differences.
### Scrollbar Styling
The storefront styles scrollbars in cyberpunk mode:
```css
scrollbar-width: thin;
scrollbar-color: var(--mode-normal) transparent;
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-thumb { background: var(--mode-normal); border-radius: 3px; }
```
**Recommendation:** Add to design system as a utility: `.dt-scrollbar` or scope under `[data-brand="dt"]`.
## Known Issues ## Known Issues
### Link hover glow ignores per-element mode colors (glows.css) (none currently)
`packages/web/dist/components/glows.css` line 37-38 applies a blanket link hover glow:
```css
[data-brand="dt"] a:hover {
text-shadow: 0 0 8px var(--color-primary);
}
```
`--color-primary` is always the brand's primary color (cyan for DT). This means **every** link glows cyan on hover, even inside elements that define their own mode color (e.g., a product card with `--mode: var(--mode-warning)` still gets a cyan glow instead of red).
**Workaround (dt-shopify-storefront):** The storefront overrides this with a higher-specificity rule:
```css
[data-brand="dt"] .card-container a:hover {
text-shadow: 0 0 8px var(--mode);
}
```
**Proper fix:** The link glow rule should respect a local color variable. Options:
1. Use `currentColor` instead of `var(--color-primary)` — glows whatever color the text already is
2. Introduce a `--glow-color` custom property defaulting to `--color-primary`, which components can override locally
3. Scope the rule more narrowly so it doesn't apply inside component containers that define their own color context

View File

@@ -7,15 +7,17 @@ Shared design tokens, web CSS, and React Native components for the Dangerous Thi
| Package | Description | | Package | Description |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| [`@dangerousthings/tokens`](packages/tokens) | Canonical design tokens (colors, typography, shape) defined in TypeScript and exported as CSS custom properties | | [`@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/web`](packages/web) | Web CSS themes — bevels, animations, forms, and component styles |
| [`@dangerousthings/react`](packages/react) | React web components wrapping @dangerousthings/web CSS |
| [`@dangerousthings/react-native`](packages/react-native) | React Native components built on React Native Paper | | [`@dangerousthings/react-native`](packages/react-native) | React Native components built on React Native Paper |
| [`@dangerousthings/tailwind-preset`](packages/tailwind-preset) | Tailwind CSS v3 preset mapping DT tokens to Tailwind theme |
| [`@dangerousthings/hex-background`](packages/hex-background) | 3D hexagon grid background using Three.js + React Three Fiber (web and React Native) | | [`@dangerousthings/hex-background`](packages/hex-background) | 3D hexagon grid background using Three.js + React Three Fiber (web and React Native) |
## Brand Themes ## Brand Themes
Two 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) - **DT** — neon cyberpunk aesthetic with beveled corners and baked in animations (Tektur font)
- **Classic** — dark navy palette with magenta accents and standard border radius - **Classic** — dark navy palette with magenta accents and standard border radius
## Getting Started ## Getting Started
@@ -50,6 +52,25 @@ npm run build:react-native
@import "@dangerousthings/web/components/bevels.css"; @import "@dangerousthings/web/components/bevels.css";
``` ```
### React (Web)
```tsx
import "@dangerousthings/web";
import { DTWebThemeProvider, DTButton, DTCard } from "@dangerousthings/react";
export default function App() {
return (
<DTWebThemeProvider brand="dt" theme="dark">
<DTCard title="PRODUCT" variant="emphasis" progress={60}>
<DTButton variant="normal" onClick={handleClick}>
Scan NFC
</DTButton>
</DTCard>
</DTWebThemeProvider>
);
}
```
### React Native ### React Native
```tsx ```tsx
@@ -76,6 +97,17 @@ import { dt } from "@dangerousthings/tokens/brands/dt";
console.log(dt.color.brand.primary); // "#00ffff" console.log(dt.color.brand.primary); // "#00ffff"
``` ```
### Tailwind
```js
// tailwind.config.js
import dtPreset from "@dangerousthings/tailwind-preset";
export default {
presets: [dtPreset],
content: ["./src/**/*.{tsx,ts,html}"],
};
```
## Development ## Development
### Scripts ### Scripts
@@ -85,7 +117,6 @@ console.log(dt.color.brand.primary); // "#00ffff"
| `npm run build` | Build all packages (Turbo) | | `npm run build` | Build all packages (Turbo) |
| `npm run clean` | Remove all `dist/` directories | | `npm run clean` | Remove all `dist/` directories |
| `npm run typecheck` | Run TypeScript type checking | | `npm run typecheck` | Run TypeScript type checking |
| `npm run lint` | Lint all packages |
### Release ### Release
@@ -97,6 +128,12 @@ npm run version-packages # bump versions from pending changesets
npm run release # build + publish to npm npm run release # build + publish to npm
``` ```
## React Web Components
The `@dangerousthings/react` package provides React web components that wrap the CSS from `@dangerousthings/web`. The component API mirrors the React Native package for consistency:
DTAccordion, DTBadgeOverlay, DTButton, DTCard, DTCheckbox, DTChip, DTDrawer, DTFeatureLegend, DTGallery, DTHexagon, DTLabel, DTMediaFrame, DTMenu, DTMobileFilterOverlay, DTModal, DTProgressBar, DTQuantityStepper, DTRadioGroup, DTSearchInput, DTStaggerContainer, DTSwitch, DTTextInput
## React Native Components ## React Native Components
DTAccordion, DTBadgeOverlay, DTButton, DTCard, DTCheckbox, DTChip, DTDrawer, DTFeatureLegend, DTGallery, DTHexagon, DTLabel, DTMediaFrame, DTMenu, DTMobileFilterOverlay, DTModal, DTProgressBar, DTQuantityStepper, DTRadioGroup, DTSearchInput, DTStaggerContainer, DTSwitch, DTTextInput DTAccordion, DTBadgeOverlay, DTButton, DTCard, DTCheckbox, DTChip, DTDrawer, DTFeatureLegend, DTGallery, DTHexagon, DTLabel, DTMediaFrame, DTMenu, DTMobileFilterOverlay, DTModal, DTProgressBar, DTQuantityStepper, DTRadioGroup, DTSearchInput, DTStaggerContainer, DTSwitch, DTTextInput
@@ -106,7 +143,6 @@ DTAccordion, DTBadgeOverlay, DTButton, DTCard, DTCheckbox, DTChip, DTDrawer, DTF
## Web CSS Modules ## Web CSS Modules
- `bevels.css` — angular clip-path bevels, card modes, selected states, progress bars, badge overlays, interactive bevel buttons - `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 - `forms-dt.css` — text inputs, checkboxes, radio buttons, switches, menu items, filter headers
- `animations.css` — entrance animations, stagger containers, transition utilities - `animations.css` — entrance animations, stagger containers, transition utilities
- `scrollbar.css` — thin neon scrollbar styling - `scrollbar.css` — thin neon scrollbar styling
@@ -119,7 +155,9 @@ dt-design-system/
├── packages/ ├── packages/
│ ├── tokens/ # Design token definitions + CSS generation │ ├── tokens/ # Design token definitions + CSS generation
│ ├── web/ # CSS themes and components │ ├── web/ # CSS themes and components
│ ├── react/ # React web components
│ ├── react-native/ # React Native Paper components │ ├── react-native/ # React Native Paper components
│ ├── tailwind-preset/ # Tailwind CSS v3 preset
│ ├── hex-background/ # 3D hexagon grid (Three.js) │ ├── hex-background/ # 3D hexagon grid (Three.js)
│ └── showcase/ # Demo apps (desktop + mobile) │ └── showcase/ # Demo apps (desktop + mobile)
├── turbo.json # Turbo pipeline config ├── turbo.json # Turbo pipeline config

594
package-lock.json generated
View File

@@ -5489,6 +5489,128 @@
"nanoid": "^3.3.11" "nanoid": "^3.3.11"
} }
}, },
"node_modules/@react-three/fiber": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz",
"integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/webxr": "*",
"base64-js": "^1.5.1",
"buffer": "^6.0.3",
"its-fine": "^2.0.0",
"react-use-measure": "^2.1.7",
"scheduler": "^0.27.0",
"suspend-react": "^0.1.3",
"use-sync-external-store": "^1.4.0",
"zustand": "^5.0.3"
},
"peerDependencies": {
"expo": ">=43.0",
"expo-asset": ">=8.4",
"expo-file-system": ">=11.0",
"expo-gl": ">=11.0",
"react": ">=19 <19.3",
"react-dom": ">=19 <19.3",
"react-native": ">=0.78",
"three": ">=0.156"
},
"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
}
}
},
"node_modules/@react-three/fiber/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==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@react-three/fiber/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"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"
}
},
"node_modules/@react-three/fiber/node_modules/its-fine": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
"integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
"license": "MIT",
"dependencies": {
"@types/react-reconciler": "^0.28.9"
},
"peerDependencies": {
"react": "^19.0.0"
}
},
"node_modules/@react-three/fiber/node_modules/zustand": {
"version": "5.0.11",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz",
"integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27", "version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -6090,7 +6212,6 @@
"version": "19.2.14", "version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
@@ -6179,7 +6300,6 @@
"version": "0.5.24", "version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
@@ -6636,85 +6756,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/archiver": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"archiver-utils": "^2.1.0",
"async": "^3.2.4",
"buffer-crc32": "^0.2.1",
"readable-stream": "^3.6.0",
"readdir-glob": "^1.1.2",
"tar-stream": "^2.2.0",
"zip-stream": "^4.1.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/archiver-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"glob": "^7.1.4",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0",
"readable-stream": "^2.0.0"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/archiver-utils/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/archiver-utils/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/archiver-utils/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/are-we-there-yet": { "node_modules/are-we-there-yet": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
@@ -8125,23 +8166,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/compress-commons": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"buffer-crc32": "^0.2.13",
"crc32-stream": "^4.0.2",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/compressible": { "node_modules/compressible": {
"version": "2.0.18", "version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -8397,7 +8421,8 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true
}, },
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "9.0.1", "version": "9.0.1",
@@ -8457,35 +8482,6 @@
"buffer": "^5.1.0" "buffer": "^5.1.0"
} }
}, },
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/crc32-stream": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"crc-32": "^1.2.0",
"readable-stream": "^3.4.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -8567,7 +8563,6 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/data-uri-to-buffer": { "node_modules/data-uri-to-buffer": {
@@ -9148,61 +9143,6 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/electron-builder-squirrel-windows": {
"version": "25.1.8",
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz",
"integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"app-builder-lib": "25.1.8",
"archiver": "^5.3.1",
"builder-util": "25.1.7",
"fs-extra": "^10.1.0"
}
},
"node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/electron-builder-squirrel-windows/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/electron-builder/node_modules/fs-extra": { "node_modules/electron-builder/node_modules/fs-extra": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -10219,14 +10159,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/fs-extra": { "node_modules/fs-extra": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
@@ -11159,14 +11091,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/isbinaryfile": { "node_modules/isbinaryfile": {
"version": "5.0.7", "version": "5.0.7",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz",
@@ -11605,56 +11529,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"readable-stream": "^2.0.5"
},
"engines": {
"node": ">= 0.6.3"
}
},
"node_modules/lazystream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/lazystream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lazystream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/leven": { "node_modules/leven": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -11982,38 +11856,6 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.startcase": { "node_modules/lodash.startcase": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz",
@@ -12027,14 +11869,6 @@
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/log-symbols": { "node_modules/log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -13733,14 +13567,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/progress": { "node_modules/progress": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -14224,6 +14050,21 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"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==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.13",
"react-dom": ">=16.13"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/read-binary-file-arch": { "node_modules/read-binary-file-arch": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
@@ -14288,42 +14129,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/readdir-glob": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"minimatch": "^5.1.0"
}
},
"node_modules/readdir-glob/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/readdir-glob/node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -14725,8 +14530,7 @@
"version": "0.27.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.4", "version": "7.7.4",
@@ -15403,7 +15207,6 @@
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
"integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": ">=17.0" "react": ">=17.0"
@@ -15507,24 +15310,6 @@
"streamx": "^2.15.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",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tar/node_modules/minipass": { "node_modules/tar/node_modules/minipass": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
@@ -15703,7 +15488,6 @@
"version": "0.160.1", "version": "0.160.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz", "resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz",
"integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==", "integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/throat": { "node_modules/throat": {
@@ -16654,45 +16438,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/zip-stream": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"archiver-utils": "^3.0.4",
"compress-commons": "^4.1.2",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/zip-stream/node_modules/archiver-utils": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"glob": "^7.2.3",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/zod": { "node_modules/zod": {
"version": "3.25.76", "version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
@@ -16723,7 +16468,7 @@
}, },
"packages/hex-background": { "packages/hex-background": {
"name": "@dangerousthings/hex-background", "name": "@dangerousthings/hex-background",
"version": "0.1.0", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@react-three/fiber": "^8.18.0", "@react-three/fiber": "^8.18.0",
@@ -16733,6 +16478,9 @@
"three": "^0.160.0", "three": "^0.160.0",
"typescript": "^5.7.0" "typescript": "^5.7.0"
}, },
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": { "peerDependencies": {
"@react-three/fiber": ">=8", "@react-three/fiber": ">=8",
"react": ">=18", "react": ">=18",
@@ -16860,22 +16608,6 @@
"react": "^18.0.0" "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": { "packages/hex-background/node_modules/scheduler": {
"version": "0.21.0", "version": "0.21.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
@@ -16888,7 +16620,7 @@
}, },
"packages/react": { "packages/react": {
"name": "@dangerousthings/react", "name": "@dangerousthings/react",
"version": "0.1.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@dangerousthings/tokens": "*", "@dangerousthings/tokens": "*",
@@ -16899,6 +16631,9 @@
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"typescript": "^5.8.0" "typescript": "^5.8.0"
}, },
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": { "peerDependencies": {
"@dangerousthings/tokens": "*", "@dangerousthings/tokens": "*",
"@dangerousthings/web": "*", "@dangerousthings/web": "*",
@@ -16908,7 +16643,7 @@
}, },
"packages/react-native": { "packages/react-native": {
"name": "@dangerousthings/react-native", "name": "@dangerousthings/react-native",
"version": "0.2.0", "version": "0.3.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@dangerousthings/tokens": "*" "@dangerousthings/tokens": "*"
@@ -16921,6 +16656,9 @@
"react-native-svg": "^15.15.3", "react-native-svg": "^15.15.3",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": { "peerDependencies": {
"react": ">=18.0.0", "react": ">=18.0.0",
"react-native": ">=0.72.0", "react-native": ">=0.72.0",
@@ -16995,13 +16733,17 @@
"packages/showcase/desktop": { "packages/showcase/desktop": {
"name": "@dangerousthings/showcase-desktop", "name": "@dangerousthings/showcase-desktop",
"version": "0.0.0", "version": "0.0.0",
"license": "MIT",
"dependencies": { "dependencies": {
"@dangerousthings/hex-background": "^0.2.0",
"@dangerousthings/react": "*", "@dangerousthings/react": "*",
"@dangerousthings/tokens": "*", "@dangerousthings/tokens": "*",
"@dangerousthings/web": "*", "@dangerousthings/web": "*",
"@react-three/fiber": "^9.5.0",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-icons": "^5.6.0" "react-icons": "^5.6.0",
"three": "^0.183.2"
}, },
"devDependencies": { "devDependencies": {
"@dangerousthings/tailwind-preset": "*", "@dangerousthings/tailwind-preset": "*",
@@ -17074,9 +16816,16 @@
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
} }
}, },
"packages/showcase/desktop/node_modules/three": {
"version": "0.183.2",
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
"license": "MIT"
},
"packages/showcase/mobile": { "packages/showcase/mobile": {
"name": "@dangerousthings/showcase-mobile", "name": "@dangerousthings/showcase-mobile",
"version": "0.0.0", "version": "0.0.0",
"license": "MIT",
"dependencies": { "dependencies": {
"@dangerousthings/react-native": "*", "@dangerousthings/react-native": "*",
"@dangerousthings/tokens": "*", "@dangerousthings/tokens": "*",
@@ -17379,31 +17128,40 @@
}, },
"packages/tailwind-preset": { "packages/tailwind-preset": {
"name": "@dangerousthings/tailwind-preset", "name": "@dangerousthings/tailwind-preset",
"version": "0.1.0", "version": "0.1.1",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"typescript": "^5.8.0" "typescript": "^5.8.0"
}, },
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": { "peerDependencies": {
"tailwindcss": "^3.4.0" "tailwindcss": "^3.4.0"
} }
}, },
"packages/tokens": { "packages/tokens": {
"name": "@dangerousthings/tokens", "name": "@dangerousthings/tokens",
"version": "0.2.0", "version": "0.3.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^25.3.3", "@types/node": "^25.3.3",
"typescript": "^5.7.0" "typescript": "^5.7.0"
},
"engines": {
"node": ">=18.0.0"
} }
}, },
"packages/web": { "packages/web": {
"name": "@dangerousthings/web", "name": "@dangerousthings/web",
"version": "0.2.0", "version": "0.3.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@dangerousthings/tokens": "*" "@dangerousthings/tokens": "*"
},
"engines": {
"node": ">=18.0.0"
} }
} }
} }

View File

@@ -1,7 +1,6 @@
{ {
"name": "dt-design-system", "name": "dt-design-system",
"version": "0.0.0", "version": "0.0.0",
"private": true,
"description": "Dangerous Things design system — shared tokens, web CSS, and React Native components", "description": "Dangerous Things design system — shared tokens, web CSS, and React Native components",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -1,5 +1,13 @@
# @dangerousthings/hex-background # @dangerousthings/hex-background
## 0.2.1
### Fixed
- Animation no longer degrades to noise over time — replaced frame-rate-dependent lerp with duration-based smoothstep easing
### Added
- `animationDuration` prop (default 1200ms) controls how long each hexagon takes to reach its target height
## 0.2.0 ## 0.2.0
### Minor Changes ### Minor Changes

View File

@@ -77,7 +77,7 @@ function Background() {
## Monorepo ## Monorepo
Part of the [DT Design System](https://github.com/nicholasgriffintn/dt-design-system) monorepo. Part of the [DT Design System](https://github.com/dangerous-tac0s/dt-design-system) monorepo.
## License ## License

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dangerousthings/hex-background", "name": "@dangerousthings/hex-background",
"version": "0.2.0", "version": "0.2.1",
"description": "3D hexagon grid background for the Dangerous Things design system", "description": "3D hexagon grid background for the Dangerous Things design system",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -13,7 +13,6 @@ import {
Shape, Shape,
ExtrudeGeometry, ExtrudeGeometry,
MeshStandardMaterial, MeshStandardMaterial,
MathUtils,
} from 'three'; } from 'three';
export interface HexGridProps { export interface HexGridProps {
@@ -29,6 +28,13 @@ export interface HexGridProps {
maxHeight?: number; maxHeight?: number;
/** Interval (ms) between height target changes @default 1500 */ /** Interval (ms) between height target changes @default 1500 */
animationInterval?: number; animationInterval?: number;
/** Duration (ms) for each hex to reach its target @default 1200 */
animationDuration?: number;
}
/** Hermite smoothstep: 3t² - 2t³ */
function smoothstep(t: number): number {
return t * t * (3 - 2 * t);
} }
function computeHexPositions( function computeHexPositions(
@@ -62,6 +68,7 @@ export function HexGrid({
margin = 0.05, margin = 0.05,
maxHeight = 3, maxHeight = 3,
animationInterval = 1500, animationInterval = 1500,
animationDuration = 1200,
}: HexGridProps) { }: HexGridProps) {
const meshRef = useRef<InstancedMesh>(null); const meshRef = useRef<InstancedMesh>(null);
@@ -70,11 +77,11 @@ export function HexGrid({
[rows, cols, hexRadius, extrudeHeight, margin], [rows, cols, hexRadius, extrudeHeight, margin],
); );
const targetHeightsRef = useRef( const animStateRef = useRef(
positions.map(() => 1 + Math.random() * maxHeight), positions.map(() => {
); const h = 1 + Math.random() * maxHeight;
const currentHeightsRef = useRef( return {startHeight: h, targetHeight: h, startTime: 0};
positions.map(() => 1 + Math.random() * maxHeight), }),
); );
// Reuse dummy object for matrix updates — 15-20% FPS improvement // Reuse dummy object for matrix updates — 15-20% FPS improvement
@@ -101,25 +108,41 @@ export function HexGrid({
// Regenerate target heights at interval // Regenerate target heights at interval
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
targetHeightsRef.current = positions.map( const now = performance.now();
() => 0.5 + Math.random() * maxHeight, animStateRef.current = animStateRef.current.map((state) => {
); // Compute where the hex currently is (same easing as useFrame)
const elapsed = Math.min(
(now - state.startTime) / animationDuration,
1,
);
const t = smoothstep(elapsed);
const currentHeight =
state.startHeight + (state.targetHeight - state.startHeight) * t;
return {
startHeight: currentHeight,
targetHeight: 0.5 + Math.random() * maxHeight,
startTime: now,
};
});
}, animationInterval); }, animationInterval);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [positions, maxHeight, animationInterval]); }, [positions.length, maxHeight, animationInterval, animationDuration]);
// Animate per frame: lerp current heights toward targets // Animate per frame: time-based smoothstep easing
useFrame((_state, delta) => { useFrame(() => {
if (!meshRef.current) return; if (!meshRef.current) return;
const dummy = dummyRef.current; const dummy = dummyRef.current;
const now = performance.now();
positions.forEach(({x, z}, i) => { positions.forEach(({x, z}, i) => {
currentHeightsRef.current[i] = MathUtils.lerp( const state = animStateRef.current[i];
currentHeightsRef.current[i], const elapsed = Math.min(
targetHeightsRef.current[i], (now - state.startTime) / animationDuration,
delta * 0.5, 1,
); );
const height = currentHeightsRef.current[i]; const t = smoothstep(elapsed);
const height =
state.startHeight + (state.targetHeight - state.startHeight) * t;
dummy.position.set(x, height / 2, z); dummy.position.set(x, height / 2, z);
dummy.scale.set(1, height, 1); dummy.scale.set(1, height, 1);

View File

@@ -22,6 +22,8 @@ export interface HexGridBackgroundProps {
maxHeight?: number; maxHeight?: number;
/** Height target refresh interval in ms @default 1500 */ /** Height target refresh interval in ms @default 1500 */
animationInterval?: number; animationInterval?: number;
/** Duration (ms) for each hex to ease to target @default 1200 */
animationDuration?: number;
/** Camera orbital speed @default 0.02 */ /** Camera orbital speed @default 0.02 */
cameraSpeed?: number; cameraSpeed?: number;
/** Camera orbital radius @default 8 */ /** Camera orbital radius @default 8 */
@@ -62,6 +64,7 @@ export function HexGridBackground({
margin = 0.05, margin = 0.05,
maxHeight = 3, maxHeight = 3,
animationInterval = 1500, animationInterval = 1500,
animationDuration = 1200,
cameraSpeed = 0.02, cameraSpeed = 0.02,
cameraRadius = 8, cameraRadius = 8,
fov = 40, fov = 40,
@@ -134,6 +137,7 @@ export function HexGridBackground({
margin={margin} margin={margin}
maxHeight={maxHeight} maxHeight={maxHeight}
animationInterval={animationInterval} animationInterval={animationInterval}
animationDuration={animationDuration}
/> />
</Suspense> </Suspense>
</Canvas> </Canvas>

View File

@@ -1,5 +1,17 @@
# @dangerousthings/react-native # @dangerousthings/react-native
## 0.4.1
### Patch Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer.
## 0.4.0
### Minor Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer in react-native, updated DTGallery/DTModal/DTMobileFilterOverlay components.
## 0.3.0 ## 0.3.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dangerousthings/react-native", "name": "@dangerousthings/react-native",
"version": "0.3.0", "version": "0.4.1",
"description": "React Native themed components for the Dangerous Things design system", "description": "React Native themed components for the Dangerous Things design system",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -251,7 +251,6 @@ const styles = StyleSheet.create({
title: { title: {
fontWeight: '600', fontWeight: '600',
letterSpacing: 0.5, letterSpacing: 0.5,
textTransform: 'uppercase',
fontSize: 14, fontSize: 14,
}, },
contentWrapper: {}, contentWrapper: {},

View File

@@ -243,6 +243,5 @@ const styles = StyleSheet.create({
label: { label: {
fontWeight: '600', fontWeight: '600',
letterSpacing: 1, letterSpacing: 1,
textTransform: 'uppercase',
}, },
}); });

View File

@@ -49,6 +49,11 @@ interface DTCardProps {
* @default 0 * @default 0
*/ */
progress?: number; progress?: number;
/**
* Which area expands when the card grows to fill space
* @default 'body'
*/
growArea?: 'body' | 'title';
/** /**
* Additional styles for the card container * Additional styles for the card container
*/ */
@@ -81,6 +86,10 @@ interface DTCardProps {
* Background color (defaults to theme background) * Background color (defaults to theme background)
*/ */
backgroundColor?: string; backgroundColor?: string;
/**
* Element rendered at the right edge of the header
*/
headerRight?: ReactNode;
/** /**
* Press handler * Press handler
*/ */
@@ -215,6 +224,7 @@ export function DTCard({
title, title,
showHeader, showHeader,
progress = 0, progress = 0,
growArea = 'body',
style, style,
contentStyle, contentStyle,
padding = 16, padding = 16,
@@ -222,6 +232,7 @@ export function DTCard({
bevelSize = 32, bevelSize = 32,
bevelSizeSmall = 16, bevelSizeSmall = 16,
backgroundColor, backgroundColor,
headerRight,
onPress, onPress,
}: DTCardProps) { }: DTCardProps) {
const theme = useDTTheme(); const theme = useDTTheme();
@@ -327,7 +338,7 @@ export function DTCard({
)} )}
<View style={[styles.innerContainer, useBevels && {paddingLeft: bevelSizeSmall - borderWidth}]}> <View style={[styles.innerContainer, useBevels && {paddingLeft: bevelSizeSmall - borderWidth}]}>
{shouldShowHeader && ( {shouldShowHeader && (
<View style={[styles.header, {backgroundColor: accentColor}]}> <View style={[styles.header, {backgroundColor: accentColor}, growArea === 'title' && {flex: 1}, !!(title && headerRight) && styles.headerRow]}>
{title && ( {title && (
<Text <Text
variant="titleMedium" variant="titleMedium"
@@ -335,9 +346,10 @@ export function DTCard({
{title} {title}
</Text> </Text>
)} )}
{headerRight}
</View> </View>
)} )}
<View style={[styles.content, {padding}, contentStyle]}>{children}</View> <View style={[styles.content, {padding}, growArea === 'body' && {flex: 1}, contentStyle]}>{children}</View>
</View> </View>
{/* Frame overlay (above content) — beveled mode only. {/* Frame overlay (above content) — beveled mode only.
Uses evenodd with 3 sub-paths: outer + inner + progressArea. Uses evenodd with 3 sub-paths: outer + inner + progressArea.
@@ -400,12 +412,18 @@ const styles = StyleSheet.create({
position: 'relative', position: 'relative',
zIndex: 1, zIndex: 1,
overflow: 'hidden', overflow: 'hidden',
flex: 1,
}, },
header: { header: {
paddingLeft: 8, paddingLeft: 8,
paddingRight: 16, paddingRight: 16,
paddingVertical: 12, paddingVertical: 12,
}, },
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
headerText: { headerText: {
fontWeight: '700', fontWeight: '700',
letterSpacing: 0.5, letterSpacing: 0.5,

View File

@@ -109,7 +109,6 @@ const styles = StyleSheet.create({
headerText: { headerText: {
fontWeight: '700', fontWeight: '700',
letterSpacing: 0.5, letterSpacing: 0.5,
textTransform: 'uppercase',
}, },
grid: { grid: {
flexDirection: 'row', flexDirection: 'row',

View File

@@ -79,6 +79,11 @@ interface DTGalleryProps {
* @default 3 * @default 3
*/ */
borderWidth?: number; borderWidth?: number;
/**
* Aspect ratio (width / height) for the display area
* @default 1
*/
aspectRatio?: number;
/** /**
* Additional styles * Additional styles
*/ */
@@ -108,6 +113,7 @@ export function DTGallery({
thumbnailSize = 64, thumbnailSize = 64,
bevelSize = 24, bevelSize = 24,
borderWidth = 3, borderWidth = 3,
aspectRatio = 1,
style, style,
}: DTGalleryProps) { }: DTGalleryProps) {
const theme = useDTTheme(); const theme = useDTTheme();
@@ -276,6 +282,7 @@ export function DTGallery({
<View <View
style={[ style={[
styles.mainImageContainer, styles.mainImageContainer,
{ aspectRatio },
!useBevels && { !useBevels && {
borderWidth, borderWidth,
borderColor: accentColor, borderColor: accentColor,
@@ -355,7 +362,6 @@ const styles = StyleSheet.create({
gap: 12, gap: 12,
}, },
mainImageContainer: { mainImageContainer: {
aspectRatio: 1,
position: 'relative', position: 'relative',
}, },
mainImageContent: { mainImageContent: {

View File

@@ -341,7 +341,6 @@ const styles = StyleSheet.create({
menuItemTitle: { menuItemTitle: {
fontWeight: '600', fontWeight: '600',
letterSpacing: 0.5, letterSpacing: 0.5,
textTransform: 'uppercase',
fontSize: 14, fontSize: 14,
}, },
chevron: { chevron: {

View File

@@ -9,8 +9,8 @@
* - Content in beveled card container * - Content in beveled card container
*/ */
import {StyleSheet, ViewStyle, StyleProp, KeyboardAvoidingView, Platform} from 'react-native'; import {StyleSheet, ViewStyle, StyleProp, KeyboardAvoidingView, Platform, Pressable} from 'react-native';
import {Portal, Modal} from 'react-native-paper'; import {Portal, Modal, Text} from 'react-native-paper';
import {useDTTheme} from '../theme/DTThemeProvider'; import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant} from '../utils/variantColors'; import {type DTVariant} from '../utils/variantColors';
import {DTCard} from './DTCard'; import {DTCard} from './DTCard';
@@ -97,6 +97,17 @@ export function DTModal({
mode={variant} mode={variant}
title={title} title={title}
showHeader={!!title} showHeader={!!title}
headerRight={dismissable ? (
<Pressable
onPress={onDismiss}
hitSlop={8}
accessibilityLabel="Close"
accessibilityRole="button">
<Text style={{ color: theme.colors.onPrimary, fontSize: 18, lineHeight: 18 }}>
{'\u2715'}
</Text>
</Pressable>
) : undefined}
contentStyle={contentStyle}> contentStyle={contentStyle}>
{children} {children}
</DTCard> </DTCard>

View File

@@ -1,5 +1,24 @@
# @dangerousthings/react # @dangerousthings/react
## 2.0.1
### Patch Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer.
- Updated dependencies
- @dangerousthings/web@0.4.1
## 2.0.0
### Minor Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer in react-native, updated DTGallery/DTModal/DTMobileFilterOverlay components.
### Patch Changes
- Updated dependencies
- @dangerousthings/web@0.4.0
## 1.0.0 ## 1.0.0
### Minor Changes ### Minor Changes

View File

@@ -124,7 +124,7 @@ variantToClassName("warning"); // token-level variant mapping
## Monorepo ## Monorepo
Part of the [DT Design System](https://github.com/nicholasgriffintn/dt-design-system) monorepo. Part of the [DT Design System](https://github.com/dangerous-tac0s/dt-design-system) monorepo.
## License ## License

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dangerousthings/react", "name": "@dangerousthings/react",
"version": "1.0.0", "version": "2.1.0",
"description": "React web components for the Dangerous Things design system", "description": "React web components for the Dangerous Things design system",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -6,7 +6,8 @@
* *
* The progress bar is a structural element on the left edge (0 to bevel-sm). * 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). * It is always present. At 0 progress it shows surface color (empty bar).
* As progress increases, accent color fills from the bottom up. * As progress increases, accent color fills from top to bottom.
* When progress is omitted, the bar stays empty (full card, no fill).
*/ */
import type { ReactNode, CSSProperties } from 'react'; import type { ReactNode, CSSProperties } from 'react';
@@ -22,10 +23,18 @@ interface DTCardProps {
title?: string; title?: string;
/** Whether to show the accent header bar @default true when title is provided */ /** Whether to show the accent header bar @default true when title is provided */
showHeader?: boolean; showHeader?: boolean;
/** Progress value (0100) for left-edge vertical progress bar @default 0 */ /** Dismiss callback — renders a close button in the header bar */
onDismiss?: () => void;
/** Progress value (0100) for left-edge vertical progress bar. Omit for empty bar. */
progress?: number; progress?: number;
/** Grow to fill available space in flex/grid containers @default false */
grow?: boolean;
/** Which area expands when grow is true @default 'body' */
growArea?: 'body' | 'title';
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
/** Badge content rendered at bottom-right, clipped by card bevel */
badge?: ReactNode;
onClick?: () => void; onClick?: () => void;
} }
@@ -34,12 +43,16 @@ export function DTCard({
variant = 'normal', variant = 'normal',
title, title,
showHeader, showHeader,
progress = 0, onDismiss,
progress,
grow = false,
growArea = 'body',
className, className,
style, style,
onClick, onClick,
badge,
}: DTCardProps) { }: DTCardProps) {
const shouldShowHeader = showHeader ?? !!title; const shouldShowHeader = showHeader ?? !!(title || onDismiss);
const Tag = onClick ? 'button' : 'div'; const Tag = onClick ? 'button' : 'div';
return ( return (
@@ -47,20 +60,40 @@ export function DTCard({
className={cx( className={cx(
'card', 'card',
getVariantClass(variant), getVariantClass(variant),
grow && 'dt-card-grow',
grow && growArea === 'title' && 'dt-card-grow-title',
className, className,
)} )}
style={{ style={{
...style, ...style,
'--dt-card-progress': progress, ...(progress != null && { '--dt-card-progress': progress }),
} as CSSProperties} } as CSSProperties}
onClick={onClick} onClick={onClick}
type={onClick ? 'button' : undefined}> type={onClick ? 'button' : undefined}>
{shouldShowHeader && ( {shouldShowHeader && (
<div className="card-title"> <div className="card-title" style={onDismiss ? { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } : undefined}>
{title} <span>{title}</span>
{onDismiss && (
<button
type="button"
onClick={(e) => { e.stopPropagation(); onDismiss(); }}
aria-label="Close"
style={{
background: 'none',
border: 'none',
color: 'inherit',
cursor: 'pointer',
padding: 0,
fontSize: '1.25rem',
lineHeight: 1,
}}>
&#x2715;
</button>
)}
</div> </div>
)} )}
{children} {children}
{badge && <div className="dt-card-badge">{badge}</div>}
</Tag> </Tag>
); );
} }

View File

@@ -160,7 +160,6 @@ export function DTDrawer({
fontWeight: 900, fontWeight: 900,
fontSize: '2em', fontSize: '2em',
letterSpacing: '0.05em', letterSpacing: '0.05em',
textTransform: 'uppercase',
borderBottom: '1px solid var(--color-bg)', borderBottom: '1px solid var(--color-bg)',
}}> }}>
<span>{heading}</span> <span>{heading}</span>

View File

@@ -1,20 +1,25 @@
/** /**
* DTFeatureLegend — Grid of product feature icons with rotated labels. * DTFeatureLegend — Interactive grid of product feature icons with rotated labels.
* *
* CSS reference: feature-legend.css .dt-feature-legend, .dt-feature-legend-header, * Matches dt-shopify-storefront UseCaseLegend: header bar with hover details,
* .dt-feature-legend-grid, .dt-feature-legend-item, .dt-feature-supported, etc. * rotated vertical labels (above for row 0, below for row 1), ? toggle,
* bordered grid with dark cell backgrounds, and pulse-on-hover icons.
*
* CSS reference: feature-legend.css
*/ */
import type { ReactNode, CSSProperties } from 'react'; import { useState, type ReactNode, type CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens'; import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
import { getVariantClass, featureStateToVariant } from '../utils/variantClasses'; import { getVariantClass } from '../utils/variantClasses';
export interface DTFeatureItem { export interface DTFeatureItem {
key: string; key: string;
name: string; name: string;
icon: ReactNode; icon: ReactNode;
state: 'supported' | 'disabled' | 'unsupported'; state: 'supported' | 'disabled' | 'unsupported';
/** Optional detail text shown in the header on hover */
detail?: string;
} }
const stateClassMap: Record<DTFeatureItem['state'], string> = { const stateClassMap: Record<DTFeatureItem['state'], string> = {
@@ -32,6 +37,8 @@ interface DTFeatureLegendProps {
columns?: number; columns?: number;
/** Icon size in px @default 42 */ /** Icon size in px @default 42 */
iconSize?: number; iconSize?: number;
/** Show rotated labels initially @default true */
showLabels?: boolean;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
} }
@@ -42,35 +49,93 @@ export function DTFeatureLegend({
variant = 'normal', variant = 'normal',
columns = 5, columns = 5,
iconSize = 42, iconSize = 42,
showLabels: initialShowLabels = true,
className, className,
style, style,
}: DTFeatureLegendProps) { }: DTFeatureLegendProps) {
// Suppress unused import warning — featureStateToVariant is available for consumers const [showLabels, setShowLabels] = useState(initialShowLabels);
void featureStateToVariant; const [hoveredFeature, setHoveredFeature] = useState<string | null>(null);
const hoveredItem = hoveredFeature
? features.find(f => f.key === hoveredFeature)
: null;
const rowCount = Math.ceil(features.length / columns);
const hasBottomRow = rowCount >= 2;
return ( return (
<div <div
className={cx('dt-feature-legend', getVariantClass(variant), className)} className={cx('dt-feature-legend', getVariantClass(variant), className)}
style={style}> style={style}>
{/* Header bar — title + hovered feature detail + ? toggle */}
{title && ( {title && (
<div className="dt-feature-legend-header">{title}</div> <div className="dt-feature-legend-header">
)} <div className="dt-feature-legend-header-content">
<div <div className="dt-feature-legend-title">{title}</div>
className="dt-feature-legend-grid" <div className="dt-feature-legend-detail">
style={{ '--dt-feature-columns': columns } as CSSProperties}> {hoveredItem ? (
{features.map(feature => ( <span className={stateClassMap[hoveredItem.state]}>
<div {hoveredItem.name}{hoveredItem.detail ? ': ' : ''}
key={feature.key} {hoveredItem.detail && <strong>{hoveredItem.detail}</strong>}
className={cx('dt-feature-legend-item', stateClassMap[feature.state])} </span>
style={{ width: `${100 / columns}%` }}> ) : (
<div <span className="dt-feature-legend-hint">
className="dt-feature-legend-icon" Hover a feature for details
style={{ width: `${iconSize}px`, height: `${iconSize}px` }}> </span>
{feature.icon} )}
</div> </div>
<div className="dt-feature-legend-label">{feature.name}</div>
</div> </div>
))} <button
className="dt-feature-legend-toggle"
onClick={() => setShowLabels(v => !v)}
aria-label={showLabels ? 'Hide feature labels' : 'Show feature labels'}
title={showLabels ? 'Hide labels' : 'Show labels'}
type="button">
?
</button>
</div>
)}
{/* Icon grid with border */}
<div className="dt-feature-legend-body">
<div
className={cx(
'dt-feature-legend-grid',
showLabels && 'with-labels',
showLabels && hasBottomRow && 'with-labels-bottom',
)}
style={{ '--dt-feature-columns': columns } as CSSProperties}>
{features.map((feature, index) => {
const row = Math.floor(index / columns);
const labelPosition = rowCount >= 2 ? (row === 0 ? 'above' : 'below') : 'above';
return (
<div
key={feature.key}
className={cx('dt-feature-legend-item', stateClassMap[feature.state])}
style={{ width: `${100 / columns}%` }}
onMouseEnter={() => setHoveredFeature(feature.key)}
onMouseLeave={() => setHoveredFeature(null)}>
{/* Rotated vertical label — always in DOM, visibility via CSS */}
<div className={cx(
'dt-feature-legend-rotated-label',
labelPosition === 'below' ? 'label-below' : 'label-above',
showLabels && 'label-visible',
)}>
<span>{feature.name}</span>
</div>
<div
className="dt-feature-legend-icon"
style={{ width: `${iconSize}px`, height: `${iconSize}px` }}>
{feature.icon}
</div>
</div>
);
})}
</div>
</div> </div>
</div> </div>
); );

View File

@@ -1,56 +1,179 @@
/** /**
* DTGallery — Image gallery with beveled media frame and thumbnails. * DTGallery — Media gallery with beveled frame and thumbnails.
* Supports images, 3D models (via <model-viewer>), videos, and external videos.
* *
* CSS reference: bevels.css .dt-bevel-media * CSS reference: bevels.css .dt-bevel-media
*/ */
import { useState, type CSSProperties } from 'react'; import { useState, useEffect, type CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens'; import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses'; import { getVariantClass } from '../utils/variantClasses';
export interface DTGalleryItem { /* ── TypeScript declaration for <model-viewer> web component ── */
key: string; declare global {
src: string; namespace JSX {
alt?: string; interface IntrinsicElements {
thumbnail?: string; 'model-viewer': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
> & {
src?: string;
alt?: string;
poster?: string;
'camera-controls'?: boolean | string;
'auto-rotate'?: boolean | string;
'interaction-prompt'?: string;
ar?: boolean | string;
loading?: 'auto' | 'lazy' | 'eager';
};
}
}
} }
/* ── Gallery item types ── */
export type DTGalleryItem =
| { type?: 'image'; key: string; src: string; alt?: string; thumbnail?: string }
| { type: 'model3d'; key: string; src: string; alt?: string; thumbnail?: string; poster?: string }
| { type: 'video'; key: string; src: string; alt?: string; thumbnail?: string; poster?: string }
| { type: 'external-video'; key: string; src: string; alt?: string; thumbnail?: string };
interface DTGalleryProps { interface DTGalleryProps {
items: DTGalleryItem[]; items: DTGalleryItem[];
/** Externally-controlled active index. When this value changes, the gallery
* jumps to the corresponding item. Thumbnail clicks still work normally. */
activeIndex?: number;
/** Color variant @default 'normal' */ /** Color variant @default 'normal' */
variant?: DTVariant; variant?: DTVariant;
/** Aspect ratio (width / height) for the display area @default 1 */
aspectRatio?: number;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
} }
/* ── Model-viewer script loader ── */
const MODEL_VIEWER_SRC =
'https://unpkg.com/@google/model-viewer@v1.12.1/dist/model-viewer.min.js';
let modelViewerLoaded = false;
function useModelViewerScript(needed: boolean) {
useEffect(() => {
if (!needed || modelViewerLoaded || typeof document === 'undefined') return;
if (customElements.get('model-viewer')) {
modelViewerLoaded = true;
return;
}
const script = document.createElement('script');
script.type = 'module';
script.src = MODEL_VIEWER_SRC;
document.head.appendChild(script);
modelViewerLoaded = true;
}, [needed]);
}
/* ── Shared styles ── */
const fillStyle: CSSProperties = {
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
display: 'block',
};
/* ── Component ── */
export function DTGallery({ export function DTGallery({
items, items,
activeIndex: activeIndexProp,
variant = 'normal', variant = 'normal',
aspectRatio = 1,
className, className,
style, style,
}: DTGalleryProps) { }: DTGalleryProps) {
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(activeIndexProp ?? 0);
// Sync external activeIndex prop to internal state when it changes
useEffect(() => {
if (activeIndexProp != null && activeIndexProp >= 0 && activeIndexProp < items.length) {
setActiveIndex(activeIndexProp);
}
}, [activeIndexProp, items.length]);
const hasModel = items.some((i) => i.type === 'model3d');
useModelViewerScript(hasModel);
if (items.length === 0) return null; if (items.length === 0) return null;
const current = items[activeIndex]; const current = items[activeIndex];
const currentType = current.type || 'image';
return ( return (
<div className={cx(getVariantClass(variant), className)} style={style}> <div className={cx(getVariantClass(variant), className)} style={style}>
{/* Main image */} {/* Main display */}
<div className="dt-bevel-media" style={{ overflow: 'hidden' }}> <div
<img className="dt-bevel-media"
src={current.src} style={{ overflow: 'hidden', aspectRatio, position: 'relative' }}
alt={current.alt || ''} >
style={{ {currentType === 'image' && (
width: '100%', <img
height: 'auto', src={current.src}
display: 'block', alt={current.alt || ''}
transition: 'opacity 300ms ease-in-out', style={{
}} ...fillStyle,
/> objectFit: 'contain',
transition: 'opacity 300ms ease-in-out',
}}
/>
)}
{currentType === 'model3d' && (
<model-viewer
src={current.src}
alt={current.alt || ''}
poster={'poster' in current ? current.poster : undefined}
camera-controls=""
auto-rotate=""
interaction-prompt="auto"
loading="lazy"
style={{
...fillStyle,
backgroundColor: 'var(--color-bg, #000)',
}}
/>
)}
{currentType === 'video' && (
<video
src={current.src}
poster={'poster' in current ? current.poster : undefined}
controls
playsInline
preload="metadata"
style={{
...fillStyle,
objectFit: 'contain',
}}
/>
)}
{currentType === 'external-video' && (
<iframe
src={current.src}
title={current.alt || 'Video'}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style={{
...fillStyle,
border: 'none',
}}
/>
)}
</div> </div>
{/* Thumbnails */} {/* Thumbnails */}
{items.length > 1 && ( {items.length > 1 && (
<div <div
@@ -59,31 +182,59 @@ export function DTGallery({
gap: '8px', gap: '8px',
marginTop: '8px', marginTop: '8px',
overflowX: 'auto', overflowX: 'auto',
}}> }}
{items.map((item, i) => ( >
<button {items.map((item, i) => {
key={item.key} const itemType = item.type || 'image';
onClick={() => setActiveIndex(i)} const thumbSrc = item.thumbnail || ('poster' in item ? item.poster : undefined) || item.src;
type="button" const isMedia = itemType === 'video' || itemType === 'external-video';
style={{
flexShrink: 0, return (
width: '64px', <button
height: '64px', key={item.key}
padding: 0, onClick={() => setActiveIndex(i)}
border: i === activeIndex type="button"
? '2px solid var(--dt-card-color, var(--color-primary))' style={{
: '2px solid transparent', flexShrink: 0,
background: 'none', width: '64px',
cursor: 'pointer', height: '64px',
overflow: 'hidden', padding: 0,
}}> border:
<img i === activeIndex
src={item.thumbnail || item.src} ? '2px solid var(--dt-card-color, var(--color-primary))'
alt={item.alt || ''} : '2px solid transparent',
style={{ width: '100%', height: '100%', objectFit: 'cover' }} background: 'none',
/> cursor: 'pointer',
</button> overflow: 'hidden',
))} position: 'relative',
}}
>
<img
src={thumbSrc}
alt={item.alt || ''}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
{/* Play icon overlay for video thumbnails */}
{isMedia && (
<svg
viewBox="0 0 24 24"
fill="white"
style={{
position: 'absolute',
inset: 0,
margin: 'auto',
width: '24px',
height: '24px',
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.6))',
pointerEvents: 'none',
}}
>
<path d="M8 5v14l11-7z" />
</svg>
)}
</button>
);
})}
</div> </div>
)} )}
</div> </div>

View File

@@ -25,7 +25,7 @@ export function DTLabel({
}: DTLabelProps) { }: DTLabelProps) {
return ( return (
<span <span
className={cx('badge', 'badge-mode', getVariantClass(variant), className)} className={cx('badge', 'p-3', 'badge-mode', getVariantClass(variant), className)}
style={style}> style={style}>
{children} {children}
</span> </span>

View File

@@ -2,25 +2,43 @@
* DTMediaFrame — Diagonal beveled frame (top-left + bottom-right). * DTMediaFrame — Diagonal beveled frame (top-left + bottom-right).
* *
* CSS reference: bevels.css .dt-bevel-media * CSS reference: bevels.css .dt-bevel-media
*
* The outer div provides the colored bevel border via clip-path.
* A ::before pseudo-element fills the inner surface.
* Direct img/video children are auto-clipped to match.
* A ::after pseudo-element shows "MISSING MEDIA" when empty.
*/ */
import type { ReactNode, CSSProperties } from 'react'; import type { ReactNode, CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
import { getVariantClass } from '../utils/variantClasses';
interface DTMediaFrameProps { interface DTMediaFrameProps {
children: ReactNode; children: ReactNode;
/** Color variant for the bevel border @default 'normal' */
variant?: DTVariant;
/** Custom placeholder shown when children is nullish (overrides CSS ::after) */
placeholder?: ReactNode;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
} }
export function DTMediaFrame({ export function DTMediaFrame({
children, children,
variant = 'normal',
placeholder,
className, className,
style, style,
}: DTMediaFrameProps) { }: DTMediaFrameProps) {
const isEmpty = children == null || children === false;
return ( return (
<div className={cx('dt-bevel-media', className)} style={style}> <div
{children} className={cx('dt-bevel-media', getVariantClass(variant), className)}
style={style}
>
{isEmpty && placeholder ? placeholder : children}
</div> </div>
); );
} }

View File

@@ -5,7 +5,7 @@
* .dt-filter-overlay-content * .dt-filter-overlay-content
*/ */
import { useEffect, useCallback, type ReactNode, type CSSProperties } from 'react'; import { useEffect, useCallback, useRef, useId, type ReactNode, type CSSProperties } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import type { DTVariant } from '@dangerousthings/tokens'; import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
@@ -38,6 +38,10 @@ export function DTMobileFilterOverlay({
className, className,
style, style,
}: DTMobileFilterOverlayProps) { }: DTMobileFilterOverlayProps) {
const panelRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLElement | null>(null);
const headingId = useId();
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
if (e.key === 'Escape') onDismiss(); if (e.key === 'Escape') onDismiss();
@@ -45,6 +49,7 @@ export function DTMobileFilterOverlay({
[onDismiss], [onDismiss],
); );
// Escape key
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
document.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown);
@@ -52,12 +57,67 @@ export function DTMobileFilterOverlay({
} }
}, [visible, handleKeyDown]); }, [visible, handleKeyDown]);
// Body scroll lock
useEffect(() => {
if (visible) {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}
}, [visible]);
// Focus trap
useEffect(() => {
if (!visible) return;
triggerRef.current = document.activeElement as HTMLElement;
const timer = setTimeout(() => {
const first = panelRef.current?.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
first?.focus();
}, 100);
const handleTab = (e: KeyboardEvent) => {
if (e.key !== 'Tab' || !panelRef.current) return;
const focusable = panelRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
};
document.addEventListener('keydown', handleTab);
return () => {
clearTimeout(timer);
document.removeEventListener('keydown', handleTab);
triggerRef.current?.focus();
};
}, [visible]);
if (!visible) return null; if (!visible) return null;
return createPortal( return createPortal(
<div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}> <div className={cx('dt-filter-overlay', getVariantClass(variant), className)} style={style}>
<div className="dt-filter-overlay-backdrop" onClick={onDismiss} /> <div
<div className="dt-filter-overlay-content"> className="dt-filter-overlay-backdrop"
onClick={onDismiss}
aria-hidden="true"
/>
<div
ref={panelRef}
className="dt-filter-overlay-content"
role="dialog"
aria-modal="true"
aria-labelledby={headingId}
>
{/* Header */} {/* Header */}
<div <div
style={{ style={{
@@ -67,7 +127,7 @@ export function DTMobileFilterOverlay({
padding: '16px', padding: '16px',
borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)', borderBottom: '1px solid rgba(var(--color-primary-rgb), 0.2)',
}}> }}>
<span style={{ fontWeight: 700, fontSize: '1.125rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}> <span id={headingId} style={{ fontWeight: 700, fontSize: '1.125rem', letterSpacing: '0.05em' }}>
{heading} {heading}
{activeFilterCount !== undefined && activeFilterCount > 0 && ( {activeFilterCount !== undefined && activeFilterCount > 0 && (
<span <span
@@ -88,7 +148,6 @@ export function DTMobileFilterOverlay({
color: 'var(--color-primary)', color: 'var(--color-primary)',
fontSize: '0.875rem', fontSize: '0.875rem',
cursor: 'pointer', cursor: 'pointer',
textTransform: 'uppercase',
}}> }}>
Clear All Clear All
</button> </button>
@@ -96,6 +155,7 @@ export function DTMobileFilterOverlay({
<button <button
onClick={onDismiss} onClick={onDismiss}
type="button" type="button"
aria-label={`Close ${heading}`}
style={{ style={{
background: 'none', background: 'none',
border: 'none', border: 'none',

View File

@@ -82,13 +82,35 @@ export function DTModal({
position: 'relative', position: 'relative',
maxWidth: '90vw', maxWidth: '90vw',
maxHeight: '85vh', maxHeight: '85vh',
overflow: 'auto', display: 'flex',
flexDirection: 'column',
...style, ...style,
}} }}
role="dialog" role="dialog"
aria-modal="true"> aria-modal="true">
{title && <div className="card-title">{title}</div>} {title && (
<div style={{ padding: 'var(--space-6, 24px)' }}>{children}</div> <div className="card-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0 }}>
<span>{title}</span>
{dismissable && (
<button
type="button"
onClick={onDismiss}
aria-label="Close"
style={{
background: 'none',
border: 'none',
color: 'inherit',
cursor: 'pointer',
padding: 0,
fontSize: '1.25rem',
lineHeight: 1,
}}>
&#x2715;
</button>
)}
</div>
)}
<div style={{ padding: 'var(--space-6, 24px)', overflow: 'auto', flex: '1 1 auto' }}>{children}</div>
</div> </div>
</div>, </div>,
document.body, document.body,

View File

@@ -1,7 +1,10 @@
/** /**
* DTRadioGroup — Hexagonal radio buttons. * DTRadioGroup — Hexagonal radio buttons.
* *
* CSS reference: forms-dt.css input[type="radio"], .dt-radio-option * Uses a custom span for the hexagon indicator since ::before pseudo-elements
* on <input> are unreliable in Chromium/Electron.
*
* CSS reference: forms-dt.css .dt-radio-hex, .dt-radio-option
*/ */
import { useId, type CSSProperties } from 'react'; import { useId, type CSSProperties } from 'react';
@@ -39,20 +42,27 @@ export function DTRadioGroup({
className={cx('dt-radio-group', className)} className={cx('dt-radio-group', className)}
style={{ display: 'flex', flexDirection: 'column', gap: '4px', ...style }} style={{ display: 'flex', flexDirection: 'column', gap: '4px', ...style }}
role="radiogroup"> role="radiogroup">
{options.map(option => ( {options.map(option => {
<label const checked = value === option.value;
key={option.value} return (
className={cx('dt-radio-option', value === option.value && 'selected')}> <label
<input key={option.value}
type="radio" className={cx('dt-radio-option', checked && 'selected')}>
name={groupName} <input
value={option.value} type="radio"
checked={value === option.value} name={groupName}
onChange={() => onChange(option.value)} value={option.value}
/> checked={checked}
<span>{option.label}</span> onChange={() => onChange(option.value)}
</label> style={{ position: 'absolute', opacity: 0, width: 0, height: 0 }}
))} />
<span className={cx('dt-radio-hex', checked && 'checked')}>
<span className="dt-radio-hex-inner" />
</span>
<span>{option.label}</span>
</label>
);
})}
</div> </div>
); );
} }

View File

@@ -2,18 +2,22 @@
* DTStaggerContainer — Staggered scale-in animation for children. * DTStaggerContainer — Staggered scale-in animation for children.
* *
* CSS reference: animations.css .dt-stagger-container * CSS reference: animations.css .dt-stagger-container
* CSS handles all stagger timing via nth-child — no JS animation needed. * Each child is wrapped in a div that carries the animationDelay.
* Wrapper divs inherit display:contents would break animation, so
* the CSS rule targets .dt-stagger-container > * directly.
*/ */
import type { ReactNode, CSSProperties } from 'react'; import { Children, type ReactNode, type CSSProperties } from 'react';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
interface DTStaggerContainerProps { interface DTStaggerContainerProps {
children: ReactNode; children: ReactNode;
/** Duration per child animation @default '0.33s' */ /** Duration per child animation @default '0.33s' */
duration?: string; duration?: string;
/** Delay between each child @default '75ms' */ /** Delay between each child in ms @default 150 */
interval?: string; interval?: number;
/** Base delay before the first child animates in ms @default 0 */
delay?: number;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
} }
@@ -21,19 +25,23 @@ interface DTStaggerContainerProps {
export function DTStaggerContainer({ export function DTStaggerContainer({
children, children,
duration, duration,
interval, interval = 150,
delay = 0,
className, className,
style, style,
}: DTStaggerContainerProps) { }: DTStaggerContainerProps) {
const cssVars: Record<string, string> = {}; const cssVars: Record<string, string> = {};
if (duration) cssVars['--dt-stagger-duration'] = duration; if (duration) cssVars['--dt-stagger-duration'] = duration;
if (interval) cssVars['--dt-stagger-interval'] = interval;
return ( return (
<div <div
className={cx('dt-stagger-container', className)} className={cx('dt-stagger-container', className)}
style={{ ...cssVars, ...style } as CSSProperties}> style={{ ...cssVars, ...style } as CSSProperties}>
{children} {Children.map(children, (child, i) => (
<div style={{ animationDelay: `${delay + i * interval}ms` }}>
{child}
</div>
))}
</div> </div>
); );
} }

View File

@@ -36,7 +36,6 @@ export function DTTextInput({
marginBottom: '4px', marginBottom: '4px',
fontSize: '0.875rem', fontSize: '0.875rem',
fontWeight: 600, fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em', letterSpacing: '0.05em',
}}> }}>
{label} {label}

View File

@@ -1,11 +1,12 @@
/** /**
* DTWebThemeProvider * DTWebThemeProvider
* *
* Sets data-brand and data-theme attributes on a wrapper div. * Sets data-brand and data-theme attributes on a wrapper div (default)
* or on document.documentElement (documentLevel mode).
* Provides React context for child components to read the active brand/theme. * Provides React context for child components to read the active brand/theme.
*/ */
import { createContext, type ReactNode } from 'react'; import { createContext, useEffect, type ReactNode } from 'react';
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens'; import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
import { cx } from '../utils/cx'; import { cx } from '../utils/cx';
@@ -21,7 +22,9 @@ interface DTWebThemeProviderProps {
brand?: ThemeBrand; brand?: ThemeBrand;
/** Theme mode @default 'dark' */ /** Theme mode @default 'dark' */
theme?: ThemeMode; theme?: ThemeMode;
/** Additional className for the wrapper div */ /** When true, sets data-brand/data-theme on document.documentElement instead of a wrapper div */
documentLevel?: boolean;
/** Additional className for the wrapper div (ignored when documentLevel is true) */
className?: string; className?: string;
children: ReactNode; children: ReactNode;
} }
@@ -29,14 +32,26 @@ interface DTWebThemeProviderProps {
export function DTWebThemeProvider({ export function DTWebThemeProvider({
brand = 'dt', brand = 'dt',
theme = 'dark', theme = 'dark',
documentLevel,
className, className,
children, children,
}: DTWebThemeProviderProps) { }: DTWebThemeProviderProps) {
useEffect(() => {
if (documentLevel) {
document.documentElement.dataset.brand = brand;
document.documentElement.dataset.theme = theme;
}
}, [documentLevel, brand, theme]);
return ( return (
<DTWebThemeContext.Provider value={{ brand, theme }}> <DTWebThemeContext.Provider value={{ brand, theme }}>
<div data-brand={brand} data-theme={theme} className={cx('dt-theme-root', className)}> {documentLevel ? (
{children} children
</div> ) : (
<div data-brand={brand} data-theme={theme} className={cx('dt-theme-root', className)}>
{children}
</div>
)}
</DTWebThemeContext.Provider> </DTWebThemeContext.Provider>
); );
} }

View File

@@ -44,7 +44,6 @@ export { DTSearchInput } from './components/DTSearchInput';
export { DTProgressBar } from './components/DTProgressBar'; export { DTProgressBar } from './components/DTProgressBar';
export { DTAccordion } from './components/DTAccordion'; export { DTAccordion } from './components/DTAccordion';
export type { DTAccordionSection } from './components/DTAccordion'; export type { DTAccordionSection } from './components/DTAccordion';
export { DTStaggerContainer } from './components/DTStaggerContainer';
export { DTBadgeOverlay } from './components/DTBadgeOverlay'; export { DTBadgeOverlay } from './components/DTBadgeOverlay';
// Components — filter & feature // Components — filter & feature
@@ -58,3 +57,6 @@ export { DTMobileFilterOverlay } from './components/DTMobileFilterOverlay';
export { DTGallery } from './components/DTGallery'; export { DTGallery } from './components/DTGallery';
export type { DTGalleryItem } from './components/DTGallery'; export type { DTGalleryItem } from './components/DTGallery';
export { DTHexagon } from './components/DTHexagon'; export { DTHexagon } from './components/DTHexagon';
// Components — animation
export { DTStaggerContainer } from './components/DTStaggerContainer';

View File

@@ -1,13 +1,17 @@
{ {
"name": "@dangerousthings/showcase-desktop", "name": "@dangerousthings/showcase-desktop",
"version": "0.0.0", "version": "0.0.0",
"private": true,
"license": "MIT", "license": "MIT",
"description": "Desktop showcase for the Dangerous Things design system", "description": "Desktop showcase for the Dangerous Things design system",
"author": { "author": {
"name": "Dangerous Things", "name": "Dangerous Things",
"email": "info@dangerousthings.com" "email": "info@dangerousthings.com"
}, },
"repository": {
"type": "git",
"url": "https://github.com/dangerous-tac0s/dt-design-system.git",
"directory": "packages/showcase/desktop"
},
"homepage": "https://github.com/dangerous-tac0s/dt-design-system", "homepage": "https://github.com/dangerous-tac0s/dt-design-system",
"main": "dist/main/main.js", "main": "dist/main/main.js",
"scripts": { "scripts": {
@@ -25,12 +29,15 @@
"clean": "rm -rf dist release" "clean": "rm -rf dist release"
}, },
"dependencies": { "dependencies": {
"@dangerousthings/hex-background": "^0.2.0",
"@dangerousthings/react": "*", "@dangerousthings/react": "*",
"@dangerousthings/tokens": "*", "@dangerousthings/tokens": "*",
"@dangerousthings/web": "*", "@dangerousthings/web": "*",
"@react-three/fiber": "^9.5.0",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-icons": "^5.6.0" "react-icons": "^5.6.0",
"three": "^0.183.2"
}, },
"devDependencies": { "devDependencies": {
"@dangerousthings/tailwind-preset": "*", "@dangerousthings/tailwind-preset": "*",

View File

@@ -1,21 +1,23 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { themes } from '@dangerousthings/tokens'; import { themes } from '@dangerousthings/tokens';
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens'; import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
import { HexGridBackground } from '@dangerousthings/hex-background';
import type { HexGridBackgroundProps } from '@dangerousthings/hex-background';
import { HomePage } from './pages/HomePage'; import { HomePage } from './pages/HomePage';
import { BevelsPage } from './pages/BevelsPage'; import { BevelsPage } from './pages/BevelsPage';
import { GlowsPage } from './pages/GlowsPage';
import { FormsPage } from './pages/FormsPage'; import { FormsPage } from './pages/FormsPage';
import { AnimationsPage } from './pages/AnimationsPage'; import { AnimationsPage } from './pages/AnimationsPage';
import { CardsAdvancedPage } from './pages/CardsAdvancedPage'; import { CardsAdvancedPage } from './pages/CardsAdvancedPage';
import { TokensPage } from './pages/TokensPage'; import { TokensPage } from './pages/TokensPage';
import { HexBackgroundPage } from './pages/HexBackgroundPage';
const navPages = [ const navPages = [
{ hash: '', label: 'Home' }, { hash: '', label: 'Home' },
{ hash: 'bevels', label: 'Bevels' }, { hash: 'bevels', label: 'Bevels' },
{ hash: 'glows', label: 'Glows' },
{ hash: 'forms', label: 'Forms' }, { hash: 'forms', label: 'Forms' },
{ hash: 'animations', label: 'Animations' }, { hash: 'animations', label: 'Animations' },
{ hash: 'cards-advanced', label: 'Advanced Cards' }, { hash: 'cards-advanced', label: 'Advanced Cards' },
{ hash: 'hex-background', label: 'Hex Background' },
{ hash: 'tokens', label: 'Tokens' }, { hash: 'tokens', label: 'Tokens' },
]; ];
@@ -23,6 +25,7 @@ export function App() {
const [brand, setBrand] = useState<ThemeBrand>('dt'); const [brand, setBrand] = useState<ThemeBrand>('dt');
const [theme, setTheme] = useState<ThemeMode>('dark'); const [theme, setTheme] = useState<ThemeMode>('dark');
const [currentHash, setCurrentHash] = useState(''); const [currentHash, setCurrentHash] = useState('');
const [hexProps, setHexProps] = useState<HexGridBackgroundProps>({});
// Sync brand/theme to <html> so CSS custom properties cascade everywhere // Sync brand/theme to <html> so CSS custom properties cascade everywhere
useEffect(() => { useEffect(() => {
@@ -43,10 +46,10 @@ export function App() {
function renderPage() { function renderPage() {
switch (currentHash) { switch (currentHash) {
case 'bevels': return <BevelsPage />; case 'bevels': return <BevelsPage />;
case 'glows': return <GlowsPage />;
case 'forms': return <FormsPage />; case 'forms': return <FormsPage />;
case 'animations': return <AnimationsPage />; case 'animations': return <AnimationsPage />;
case 'cards-advanced': return <CardsAdvancedPage />; case 'cards-advanced': return <CardsAdvancedPage />;
case 'hex-background': return <HexBackgroundPage hexProps={hexProps} onHexPropsChange={setHexProps} />;
case 'tokens': return <TokensPage brand={brand} />; case 'tokens': return <TokensPage brand={brand} />;
default: return <HomePage />; default: return <HomePage />;
} }
@@ -92,6 +95,7 @@ export function App() {
))} ))}
</div> </div>
</nav> </nav>
<HexGridBackground {...hexProps} />
<main id="content"> <main id="content">
{renderPage()} {renderPage()}
</main> </main>

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState, useEffect, useRef } from 'react';
import { DTCard, DTStaggerContainer } from '@dangerousthings/react'; import { DTCard } from '@dangerousthings/react';
import { Section, Row, CodeLabel } from '../components/Section'; import { Section, Row, CodeLabel } from '../components/Section';
function AnimBox({ className, label }: { className: string; label: string }) { function AnimBox({ className, label }: { className: string; label: string }) {
@@ -25,15 +25,77 @@ function AnimBox({ className, label }: { className: string; label: string }) {
); );
} }
const VARIANTS = ['normal', 'emphasis', 'warning', 'success', 'other'] as const;
function ProgressBarDemo() {
const [progress, setProgress] = useState<number[]>([0, 0, 0, 0, 0]);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [running, setRunning] = useState(false);
const animate = () => {
// Reset then animate up
setProgress([0, 0, 0, 0, 0]);
setRunning(true);
let tick = 0;
if (intervalRef.current) clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
tick++;
setProgress(prev =>
prev.map((_, i) => Math.min(100, Math.round(tick * (1.5 + i * 0.4))))
);
if (tick > 80) {
if (intervalRef.current) clearInterval(intervalRef.current);
setRunning(false);
}
}, 40);
};
useEffect(() => {
// Auto-run on mount
const t = setTimeout(animate, 500);
return () => {
clearTimeout(t);
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, []);
return (
<div>
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginBottom: 16 }}>
{VARIANTS.map((v, i) => (
<DTCard
key={v}
variant={v}
title={`${v} ${progress[i]}%`}
progress={progress[i]}
style={{ width: 160, transition: 'all 0.1s ease-out' }}
>
<div className="card-body" style={{ fontSize: '0.75rem', minHeight: 60, display: 'flex', alignItems: 'center' }}>
Progress fills the left edge from bottom to top
</div>
</DTCard>
))}
</div>
<button
className="btn-secondary"
onClick={animate}
disabled={running}
type="button"
>
{running ? 'ANIMATING...' : 'REPLAY PROGRESS'}
</button>
</div>
);
}
export function AnimationsPage() { export function AnimationsPage() {
const [entranceKey, setEntranceKey] = useState(0); const [entranceKey, setEntranceKey] = useState(0);
const [staggerKey, setStaggerKey] = useState(0);
const [accordionExpanded, setAccordionExpanded] = useState(false); const [accordionExpanded, setAccordionExpanded] = useState(false);
return ( return (
<> <>
<h1 className="page-title">Animations</h1> <h1 className="page-title">Animations</h1>
<p className="page-subtitle">Entrance animations, interactive effects, stagger containers, and transition utilities.</p> <p className="page-subtitle">Entrance animations, interactive effects, and transition utilities.</p>
<Section title="Entrance Animations" description="One-shot animations for elements entering the viewport."> <Section title="Entrance Animations" description="One-shot animations for elements entering the viewport.">
<Row key={entranceKey}> <Row key={entranceKey}>
@@ -56,28 +118,6 @@ export function AnimationsPage() {
<CodeLabel text=".dt-animate-pulse | .dt-animate-ping | .dt-animate-spin" /> <CodeLabel text=".dt-animate-pulse | .dt-animate-ping | .dt-animate-spin" />
</Section> </Section>
<Section title="Stagger Container" description="Automatic staggered scale-in animation for child elements. Customizable via --dt-stagger-duration and --dt-stagger-interval.">
<DTStaggerContainer
key={staggerKey}
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-dt-3"
>
{Array.from({ length: 12 }, (_, i) => (
<DTCard key={i} title={`CARD ${i + 1}`} style={{ textAlign: 'center' }}>
<div className="card-body" style={{ fontSize: '0.75rem' }}>Item #{i + 1}</div>
</DTCard>
))}
</DTStaggerContainer>
<button
className="btn-secondary"
style={{ marginTop: 'var(--space-4)' }}
onClick={() => setStaggerKey(k => k + 1)}
type="button"
>
REPLAY STAGGER
</button>
<CodeLabel text="<DTStaggerContainer> — nth-child delays up to 24 children" />
</Section>
<Section title="Transition Utilities" description="Accordion expand, chevron rotation, and progress bar transitions."> <Section title="Transition Utilities" description="Accordion expand, chevron rotation, and progress bar transitions.">
<div style={{ maxWidth: 400 }}> <div style={{ maxWidth: 400 }}>
<button <button
@@ -93,7 +133,6 @@ export function AnimationsPage() {
cursor: 'pointer', cursor: 'pointer',
color: 'var(--color-text)', color: 'var(--color-text)',
fontWeight: 600, fontWeight: 600,
textTransform: 'uppercase',
}} }}
aria-expanded={accordionExpanded} aria-expanded={accordionExpanded}
onClick={() => setAccordionExpanded(!accordionExpanded)} onClick={() => setAccordionExpanded(!accordionExpanded)}
@@ -126,6 +165,11 @@ export function AnimationsPage() {
<CodeLabel text=".dt-transition-accordion | .dt-transition-chevron | .dt-transition-progress" /> <CodeLabel text=".dt-transition-accordion | .dt-transition-chevron | .dt-transition-progress" />
</Section> </Section>
<Section title="Card Progress Bar" description="Cards have a vertical progress bar on the left edge. Animate it by transitioning --dt-card-progress from 0 to 100.">
<ProgressBarDemo />
<CodeLabel text="<DTCard progress={value} /> — CSS ::after gradient driven by --dt-card-progress" />
</Section>
<Section title="Scrollbar Styling" description="Thin neon scrollbar scoped under [data-brand='dt']."> <Section title="Scrollbar Styling" description="Thin neon scrollbar scoped under [data-brand='dt'].">
<div <div
className="dt-scrollbar" className="dt-scrollbar"

View File

@@ -53,12 +53,12 @@ export function BevelsPage() {
<Row> <Row>
{modes.map(mode => ( {modes.map(mode => (
<button key={mode} className={`dt-menu-item mode-${mode}`} type="button" onClick={() => setModalVariant(mode)}> <button key={mode} className={`dt-menu-item mode-${mode}`} type="button" onClick={() => setModalVariant(mode)}>
{mode.toUpperCase()} MODAL {mode} Modal
</button> </button>
))} ))}
</Row> </Row>
<CodeLabel text="<DTModal variant='normal' visible onDismiss title='...'>content</DTModal>" /> <CodeLabel text="<DTModal variant='normal' visible onDismiss title='...'>content</DTModal>" />
<DTModal visible={modalVariant !== null} onDismiss={() => setModalVariant(null)} variant={modalVariant ?? 'normal'} title={`${(modalVariant ?? 'normal').toUpperCase()} MODAL`}> <DTModal visible={modalVariant !== null} onDismiss={() => setModalVariant(null)} variant={modalVariant ?? 'normal'} title={`${modalVariant ?? 'normal'} Modal`}>
<p style={{ marginBottom: 'var(--space-4)' }}>This is a <strong>{modalVariant}</strong> modal with beveled card shape, backdrop blur, and scale-in animation.</p> <p style={{ marginBottom: 'var(--space-4)' }}>This is a <strong>{modalVariant}</strong> modal with beveled card shape, backdrop blur, and scale-in animation.</p>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop or press Escape to dismiss.</p> <p style={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop or press Escape to dismiss.</p>
</DTModal> </DTModal>
@@ -74,7 +74,7 @@ export function BevelsPage() {
</button> </button>
</Row> </Row>
<CodeLabel text="<DTDrawer position='right' heading='...' visible onDismiss>content</DTDrawer>" /> <CodeLabel text="<DTDrawer position='right' heading='...' visible onDismiss>content</DTDrawer>" />
<DTDrawer visible={drawerSide !== null} onDismiss={() => setDrawerSide(null)} position={drawerSide ?? 'right'} heading={`${(drawerSide ?? 'right').toUpperCase()} DRAWER`} headingVariant={drawerSide === 'left' ? 'other' : 'emphasis'}> <DTDrawer visible={drawerSide !== null} onDismiss={() => setDrawerSide(null)} position={drawerSide ?? 'right'} heading={`${drawerSide ?? 'right'} Drawer`} headingVariant={drawerSide === 'left' ? 'other' : 'emphasis'}>
<p style={{ marginBottom: 'var(--space-4)' }}>Sliding panel from the <strong>{drawerSide}</strong> edge with beveled corners and backdrop blur.</p> <p style={{ marginBottom: 'var(--space-4)' }}>Sliding panel from the <strong>{drawerSide}</strong> edge with beveled corners and backdrop blur.</p>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop, press Escape, or click to dismiss.</p> <p style={{ color: 'var(--color-text-muted)', fontSize: '0.85rem' }}>Click the backdrop, press Escape, or click to dismiss.</p>
</DTDrawer> </DTDrawer>
@@ -91,7 +91,7 @@ export function BevelsPage() {
<Section title="Accent Top" description="Used on accordion headers and menu items."> <Section title="Accent Top" description="Used on accordion headers and menu items.">
<div className="dt-accent-top" style={{ padding: 'var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}> <div className="dt-accent-top" style={{ padding: 'var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}>
<span style={{ fontWeight: 600, textTransform: 'uppercase' }}>Thick Top Border Accent</span> <span style={{ fontWeight: 600 }}>Thick Top Border Accent</span>
</div> </div>
<CodeLabel text=".dt-accent-top" /> <CodeLabel text=".dt-accent-top" />
</Section> </Section>
@@ -99,7 +99,7 @@ export function BevelsPage() {
<Section title="Card Color Modes" description="Per-card mode coloring — see Advanced Cards page for full demos."> <Section title="Card Color Modes" description="Per-card mode coloring — see Advanced Cards page for full demos.">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3">
{modes.map(mode => ( {modes.map(mode => (
<DTCard key={mode} variant={mode} title={mode.toUpperCase()}> <DTCard key={mode} variant={mode} title={mode}>
<div className="card-body" style={{ fontSize: '0.75rem' }}>mode-{mode}</div> <div className="card-body" style={{ fontSize: '0.75rem' }}>mode-{mode}</div>
</DTCard> </DTCard>
))} ))}

View File

@@ -1,7 +1,6 @@
import { createElement, useState, type CSSProperties } from 'react'; import { createElement, type CSSProperties } from 'react';
import { import {
DTCard, DTCard,
DTStaggerContainer,
DTFeatureLegend, DTFeatureLegend,
} from '@dangerousthings/react'; } from '@dangerousthings/react';
import type { DTFeatureItem } from '@dangerousthings/react'; import type { DTFeatureItem } from '@dangerousthings/react';
@@ -33,35 +32,35 @@ const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other']
const ico = (C: any) => createElement(C, { style: { fontSize: '2rem' } }); const ico = (C: any) => createElement(C, { style: { fontSize: '2rem' } });
// Full chip feature legend (9 features — from storefront UseCaseLegend) // Full chip feature legend (9 features — from storefront UseCaseLegend)
// NExT v2: NTAGI2C (HF) + T5577 (LF) with power-harvesting LED
const chipFeatures: DTFeatureItem[] = [ const chipFeatures: DTFeatureItem[] = [
{ key: 'smartphone', name: 'Smartphone', icon: ico(MdOutlinePhonelinkRing), state: 'supported' }, { key: 'smartphone', name: 'Smartphone', icon: ico(MdOutlinePhonelinkRing), state: 'supported', detail: 'Full NFC smartphone support' },
{ key: 'access_control', name: 'Access Control', icon: ico(MdOutlineVpnKey), state: 'supported' }, { key: 'access_control', name: 'Access Control', icon: ico(MdOutlineVpnKey), state: 'supported', detail: 'Legacy' },
{ key: 'digital_security', name: 'Digital Security', icon: ico(FaUserShield), state: 'supported' }, { key: 'digital_security', name: 'Digital Security', icon: ico(FaUserShield), state: 'unsupported', detail: 'Not Supported' },
{ key: 'cryptography', name: 'Cryptography', icon: ico(LuBinary), state: 'unsupported' }, { key: 'cryptography', name: 'Cryptography', icon: ico(LuBinary), state: 'unsupported', detail: 'Not Supported' },
{ key: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported' }, { key: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported', detail: '1 kB' },
{ key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'disabled' }, { key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'unsupported', detail: 'Not Supported' },
{ key: 'magic', name: 'UID Magic', icon: ico(MdOutlineCopyAll), state: 'supported' }, { key: 'magic', name: 'Magic', icon: ico(MdOutlineCopyAll), state: 'supported', detail: 'Yes' },
{ key: 'illumination', name: 'Illumination', icon: ico(MdOutlineLightbulb), state: 'unsupported' }, { key: 'Illumination', name: 'Illumination', icon: ico(MdOutlineLightbulb), state: 'supported', detail: 'HF: green, blue, white' },
{ key: 'temperature', name: 'Temperature', icon: ico(MdOutlineThermostat), state: 'unsupported' }, { key: 'temperature', name: 'Sensors', icon: ico(MdOutlineThermostat), state: 'unsupported', detail: 'None' },
]; ];
// Full biomagnet feature legend (4 features — from storefront MagnetUseCaseLegend) // Full biomagnet feature legend (4 features — from storefront MagnetUseCaseLegend)
// m0422a: axial neodymium disc, bioresin coated
const magnetFeatures: DTFeatureItem[] = [ const magnetFeatures: DTFeatureItem[] = [
{ key: 'sensing', name: 'Sensing', icon: ico(MdOutlineSensors), state: 'supported' }, { key: 'sensing', name: 'Sensing', icon: ico(MdOutlineSensors), state: 'supported', detail: 'RP 22,860 G/g' },
{ key: 'lifting', name: 'Lifting', icon: ico(MdOutlineFitbit), state: 'supported' }, { key: 'lifting', name: 'Lifting', icon: ico(MdOutlineFitbit), state: 'supported', detail: '~20 g' },
{ key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported' }, { key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported', detail: 'Push/Pull' },
{ key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'unsupported' }, { key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'supported', detail: 'Monopole' },
]; ];
// Full aesthetic feature legend (2 features — from storefront AestheticUseCaseLegend) // Full aesthetic feature legend (2 features — from storefront AestheticUseCaseLegend)
const aestheticFeatures: DTFeatureItem[] = [ const aestheticFeatures: DTFeatureItem[] = [
{ key: 'illumination', name: 'Illumination', icon: ico(MdLightbulbOutline), state: 'supported' }, { key: 'illumination', name: 'Illumination', icon: ico(MdLightbulbOutline), state: 'supported', detail: 'HF: red, green, blue, white' },
{ key: 'prominence', name: 'Prominence', icon: ico(MdOutlineVisibility), state: 'supported' }, { key: 'prominence', name: 'Prominence', icon: ico(MdOutlineVisibility), state: 'supported', detail: 'Low' },
]; ];
export function CardsAdvancedPage() { export function CardsAdvancedPage() {
const [staggerKey, setStaggerKey] = useState(0);
return ( return (
<> <>
<h1 className="page-title">Advanced Cards</h1> <h1 className="page-title">Advanced Cards</h1>
@@ -70,7 +69,7 @@ export function CardsAdvancedPage() {
<Section title="Card Color Modes" description="Per-card color via variant prop. Sets --dt-card-color, glow color, and accent."> <Section title="Card Color Modes" description="Per-card color via variant prop. Sets --dt-card-color, glow color, and accent.">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3">
{modes.map(mode => ( {modes.map(mode => (
<DTCard key={mode} variant={mode} title={mode.toUpperCase()}> <DTCard key={mode} variant={mode} title={mode}>
<div className="card-body" style={{ fontSize: '0.8rem' }}>mode-{mode}</div> <div className="card-body" style={{ fontSize: '0.8rem' }}>mode-{mode}</div>
</DTCard> </DTCard>
))} ))}
@@ -79,6 +78,23 @@ export function CardsAdvancedPage() {
</Section> </Section>
<Section title="Card Grow" description="Cards stretch to equal height in a row via grow prop. Compare the varying content heights.">
<div style={{ display: 'flex', gap: 'var(--space-3)' }}>
<DTCard variant="normal" title="SHORT" grow>
<div className="card-body" style={{ fontSize: '0.8rem' }}>One line</div>
</DTCard>
<DTCard variant="emphasis" title="TALL" grow>
<div className="card-body" style={{ fontSize: '0.8rem' }}>
Multiple lines of content to demonstrate that the shorter card stretches to match this card's height.
</div>
</DTCard>
<DTCard variant="other" title="MEDIUM" grow>
<div className="card-body" style={{ fontSize: '0.8rem' }}>Two lines of content here.</div>
</DTCard>
</div>
<CodeLabel text="<DTCard grow /> — flex-grow: 1 + height: 100% to fill container" />
</Section>
<Section title="Card Progress Bar" description="Vertical left-edge bar driven by progress prop (0-100)."> <Section title="Card Progress Bar" description="Vertical left-edge bar driven by progress prop (0-100).">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3">
{[0, 25, 50, 75, 100].map((val, i) => ( {[0, 25, 50, 75, 100].map((val, i) => (
@@ -97,41 +113,47 @@ export function CardsAdvancedPage() {
{ label: 'BUNDLE', mode: 'other' as DTVariant, img: 'https://images.unsplash.com/photo-1518770660439-4636190af475?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' }, { label: 'NFC', mode: 'normal' as DTVariant, img: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=400&h=400&fit=crop' },
].map(badge => ( ].map(badge => (
<DTCard key={badge.label} variant={badge.mode} title={badge.label + ' PRODUCT'}> <DTCard key={badge.label} variant={badge.mode} title={badge.label + ' PRODUCT'}
<div className="card-body-flush dt-badge-parent" style={{ aspectRatio: '1' }}> badge={<span>{badge.label}</span>}>
<div className="card-body-flush" style={{ aspectRatio: '1' }}>
<img src={badge.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} /> <img src={badge.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
<span className="dt-card-badge">{badge.label}</span>
</div> </div>
</DTCard> </DTCard>
))} ))}
</div> </div>
<CodeLabel text=".dt-badge-parent > .dt-card-badge — badge positioned on card image container" /> <CodeLabel text="<DTCard badge={<span>LAB</span>}> — badge rendered and positioned by card" />
</Section> </Section>
<Section title="Buttons" description="Beveled buttons with active/selected states, mode colors, and level indentation."> <Section title="Buttons" description="Beveled buttons with active/selected states and mode colors. Nested levels use inline paddingLeft.">
<div style={{ maxWidth: 300 }}> <div style={{ maxWidth: 300 }}>
{['All Products', 'NFC Implants', 'RFID Tags', 'Accessories', 'Lab Products'].map((item, i) => ( {[
{ name: 'All Products', cls: ' active', pad: 0 },
{ name: 'NFC Implants', cls: '', pad: 0 },
{ name: 'RFID Tags', cls: '', pad: 32 },
{ name: 'Accessories', cls: '', pad: 32 },
{ name: 'Lab Products', cls: ' mode-warning', pad: 0 },
].map(item => (
<button <button
key={item} key={item.name}
className={`dt-menu-item${i === 0 ? ' active' : ''}${i === 4 ? ' mode-warning' : ''}`} className={`dt-menu-item${item.cls}`}
style={i > 1 && i < 4 ? { '--dt-menu-level': '1' } as CSSProperties : undefined} style={item.pad ? { paddingLeft: item.pad } : undefined}
type="button" type="button"
> >
{item} {item.name}
</button> </button>
))} ))}
</div> </div>
<CodeLabel text=".dt-menu-item | .dt-menu-item.active | --dt-menu-level: 1" /> <CodeLabel text=".dt-menu-item | .active | paddingLeft for nesting" />
</Section> </Section>
<Section title="Feature Legends" description="Product feature grids from the storefront. Icons indicate feature capabilities; color indicates state."> <Section title="Feature Legends" description="Interactive product feature grids from the storefront. Hover icons for details. Toggle labels with the ? button.">
<DTFeatureLegend features={chipFeatures} title="NFC CHIP FEATURES" variant="normal" columns={5} /> <DTFeatureLegend features={chipFeatures} title="NExT v2 Features" variant="normal" columns={5} />
<div style={{ height: 'var(--space-6)' }} /> <div style={{ height: 'var(--space-6)' }} />
<DTFeatureLegend features={magnetFeatures} title="BIOMAGNET FEATURES" variant="emphasis" columns={4} /> <DTFeatureLegend features={magnetFeatures} title="m0422a Features" variant="emphasis" columns={4} />
<div style={{ height: 'var(--space-6)' }} /> <div style={{ height: 'var(--space-6)' }} />
<DTFeatureLegend features={aestheticFeatures} title="AESTHETIC FEATURES" variant="other" columns={2} /> <DTFeatureLegend features={aestheticFeatures} title="xLED HF Features" variant="other" columns={2} />
<div style={{ height: 'var(--space-4)' }} /> <div style={{ height: 'var(--space-4)' }} />
<CodeLabel text="<DTFeatureLegend features={chipFeatures} title='NFC CHIP FEATURES' variant='normal' />" /> <CodeLabel text="<DTFeatureLegend features={chipFeatures} title='NExT Features' variant='normal' />" />
<CodeLabel text="<DTFeatureLegend features={magnetFeatures} variant='emphasis' columns={4} />" /> <CodeLabel text="<DTFeatureLegend features={magnetFeatures} variant='emphasis' columns={4} />" />
<CodeLabel text="<DTFeatureLegend features={aestheticFeatures} variant='other' columns={2} />" /> <CodeLabel text="<DTFeatureLegend features={aestheticFeatures} variant='other' columns={2} />" />
<div style={{ marginTop: 'var(--space-3)', display: 'flex', gap: 'var(--space-4)', fontSize: '0.75rem' }}> <div style={{ marginTop: 'var(--space-3)', display: 'flex', gap: 'var(--space-4)', fontSize: '0.75rem' }}>
@@ -141,27 +163,6 @@ export function CardsAdvancedPage() {
</div> </div>
</Section> </Section>
<Section title="Stagger + Modes" description="Mode-colored cards in a stagger container.">
<DTStaggerContainer
key={staggerKey}
className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3"
>
{Array.from({ length: 10 }, (_, i) => (
<DTCard key={i} variant={modes[i % modes.length]} title={modes[i % modes.length].toUpperCase()} style={{ textAlign: 'center' }}>
{null}
</DTCard>
))}
</DTStaggerContainer>
<button
className="btn-secondary"
style={{ marginTop: 'var(--space-4)' }}
onClick={() => setStaggerKey(k => k + 1)}
type="button"
>
REPLAY
</button>
<CodeLabel text="<DTStaggerContainer> with <DTCard variant='...' />" />
</Section>
</> </>
); );
} }

View File

@@ -102,20 +102,26 @@ export function FormsPage() {
<CodeLabel text=".dt-filter-header | .dt-filter-header.active" /> <CodeLabel text=".dt-filter-header | .dt-filter-header.active" />
</Section> </Section>
<Section title="Buttons" description="Beveled buttons with active state, mode colors, and level indentation."> <Section title="Buttons" description="Beveled buttons with active state and mode colors. Nested levels use inline paddingLeft.">
<div style={{ maxWidth: 300 }}> <div style={{ maxWidth: 300 }}>
{['All Products', 'NFC Implants', 'RFID Tags', 'Accessories', 'Lab Products'].map((name, i) => ( {[
{ name: 'All Products', cls: ' active', pad: 0 },
{ name: 'NFC Implants', cls: '', pad: 0 },
{ name: 'RFID Tags', cls: '', pad: 32 },
{ name: 'Accessories', cls: '', pad: 32 },
{ name: 'Lab Products', cls: ' mode-warning', pad: 0 },
].map(item => (
<button <button
key={name} key={item.name}
className={`dt-menu-item${i === 0 ? ' active' : ''}${i === 4 ? ' mode-warning' : ''}`} className={`dt-menu-item${item.cls}`}
style={(i === 2 || i === 3) ? { '--dt-menu-level': '1' } as React.CSSProperties : undefined} style={item.pad ? { paddingLeft: item.pad } : undefined}
type="button" type="button"
> >
{name} {item.name}
</button> </button>
))} ))}
</div> </div>
<CodeLabel text=".dt-menu-item | .active | --dt-menu-level: 1" /> <CodeLabel text=".dt-menu-item | .active | paddingLeft for nesting" />
</Section> </Section>
</> </>
); );

View File

@@ -1,81 +0,0 @@
import { DTCard } from '@dangerousthings/react';
import { Section, Row, CodeLabel } from '../components/Section';
export function GlowsPage() {
return (
<>
<h1 className="page-title">Glows</h1>
<p className="page-subtitle">Neon drop-shadow and text-shadow effects. Active on the DT brand.</p>
<Section title="Button Glows" description="filter: drop-shadow() follows clip-path shape. Hover for enhanced glow.">
<Row>
<button className="btn-primary" type="button">PRIMARY GLOW</button>
<button className="btn-danger" type="button">DANGER GLOW</button>
</Row>
<CodeLabel text={'.btn-primary / .btn-danger — automatic on [data-brand="dt"]'} />
<br />
<Row>
<button className="btn-secondary" type="button">SECONDARY (HOVER)</button>
</Row>
<CodeLabel text=".btn-secondary — glow on hover" />
</Section>
<Section title="Link Glow" description="text-shadow on hover. Respects --dt-glow-color when set by a parent mode.">
<Row>
<a href="#" style={{ color: 'var(--color-primary)', fontWeight: 600, fontSize: '1.125rem' }}>
Hover this link for text glow
</a>
</Row>
<CodeLabel text="a:hover — uses var(--dt-glow-color, var(--color-primary))" />
</Section>
<Section title="Mode-Aware Link Glow" description="Links inside mode containers glow with that mode's color.">
<Row>
{(['normal', 'emphasis', 'warning', 'success', 'other'] as const).map(mode => (
<div key={mode} className={`mode-${mode}`} style={{ padding: 'var(--space-3)' }}>
<a href="#" style={{ fontWeight: 600, fontSize: '1rem' }}>{mode.toUpperCase()}</a>
</div>
))}
</Row>
<CodeLabel text=".mode-emphasis a:hover — glows yellow via --dt-glow-color" />
</Section>
<Section title="Terminal Inset Glow" description="Inset + outer box-shadow. No clip-path so box-shadow works directly.">
<div className="terminal dt-accent-top">
<code>{'$ dt-web-theme --install\n> Installing design tokens...\n> Applying cyberpunk aesthetic...\n> Done. Welcome to the future.'}</code>
</div>
<CodeLabel text={'.terminal — automatic on [data-brand="dt"]'} />
</Section>
<Section title="Card Hover Glow" description="filter: drop-shadow() respects the beveled clip-path.">
<DTCard title="HOVER ME" style={{ cursor: 'pointer' }}>
<div className="card-body">Cards get a drop-shadow glow on hover.</div>
</DTCard>
<CodeLabel text={'.card:hover — automatic on [data-brand="dt"]'} />
</Section>
<Section title="Input Focus Glow" description="box-shadow: 0 4px 0 1px — bright bar beneath the input.">
<input
type="text"
className="input"
placeholder="Click to focus — see the glow bar"
style={{ maxWidth: 400 }}
/>
<CodeLabel text={'.input:focus — automatic on [data-brand="dt"]'} />
</Section>
<Section title="Glow Utilities" description="Generic utility classes for applying glow effects to any element.">
<Row>
<div className="dt-glow" style={{ background: 'var(--color-primary)', padding: 'var(--space-4) var(--space-6)', display: 'inline-block', fontWeight: 700, color: 'var(--color-bg)' }}>.dt-glow</div>
<div className="dt-glow-strong" style={{ background: 'var(--color-primary)', padding: 'var(--space-4) var(--space-6)', display: 'inline-block', fontWeight: 700, color: 'var(--color-bg)' }}>.dt-glow-strong</div>
</Row>
<CodeLabel text=".dt-glow | .dt-glow-strong" />
<Row>
<div className="dt-glow-inset" style={{ padding: 'var(--space-4) var(--space-6)', display: 'inline-block', fontWeight: 700, border: '1px solid var(--color-border)' }}>.dt-glow-inset</div>
<div className="dt-text-glow" style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-primary)', display: 'inline-block' }}>.dt-text-glow</div>
</Row>
<CodeLabel text=".dt-glow-inset | .dt-text-glow" />
</Section>
</>
);
}

View File

@@ -0,0 +1,112 @@
import type { HexGridBackgroundProps } from '@dangerousthings/hex-background';
import { Section } from '../components/Section';
const DEFAULTS: Required<HexGridBackgroundProps> = {
opacity: 0.5,
hexRadius: 0.5,
margin: 0.05,
maxHeight: 3,
animationInterval: 1500,
cameraSpeed: 0.02,
cameraRadius: 8,
fov: 40,
};
interface SliderConfig {
key: keyof HexGridBackgroundProps;
label: string;
min: number;
max: number;
step: number;
}
const sliders: SliderConfig[] = [
{ key: 'opacity', label: 'Opacity', min: 0, max: 1, step: 0.05 },
{ key: 'hexRadius', label: 'Hex Radius', min: 0.1, max: 2, step: 0.1 },
{ key: 'margin', label: 'Margin', min: 0, max: 0.5, step: 0.01 },
{ key: 'maxHeight', label: 'Max Height', min: 0.5, max: 10, step: 0.5 },
{ key: 'animationInterval', label: 'Animation Interval (ms)', min: 200, max: 5000, step: 100 },
{ key: 'cameraSpeed', label: 'Camera Speed', min: 0, max: 0.2, step: 0.005 },
{ key: 'cameraRadius', label: 'Camera Radius', min: 3, max: 20, step: 0.5 },
{ key: 'fov', label: 'Field of View', min: 20, max: 90, step: 1 },
];
interface HexBackgroundPageProps {
hexProps: HexGridBackgroundProps;
onHexPropsChange: (props: HexGridBackgroundProps) => void;
}
export function HexBackgroundPage({ hexProps, onHexPropsChange }: HexBackgroundPageProps) {
const handleChange = (key: keyof HexGridBackgroundProps, value: number) => {
onHexPropsChange({ ...hexProps, [key]: value });
};
const handleReset = () => {
onHexPropsChange({ ...DEFAULTS });
};
return (
<>
<h1 className="page-title">Hex Background</h1>
<p className="page-subtitle">
3D hexagon grid background powered by Three.js + React Three Fiber.
Adjust parameters below changes apply to the global background in real time.
</p>
<Section title="Parameters" description="All HexGridBackground props exposed as controls.">
<div style={{ display: 'grid', gap: 'var(--space-4)' }}>
{sliders.map(({ key, label, min, max, step }) => (
<div key={key} style={{ display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center', gap: 'var(--space-4)' }}>
<label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<span style={{ fontSize: '0.8rem', color: 'var(--color-text-muted)', letterSpacing: '0.1em' }}>
{label}
</span>
<input
type="range"
min={min}
max={max}
step={step}
value={hexProps[key] ?? DEFAULTS[key]}
onChange={e => handleChange(key, parseFloat(e.target.value))}
style={{ width: '100%', accentColor: 'var(--color-primary)' }}
/>
</label>
<span style={{
fontFamily: 'monospace',
fontSize: '0.85rem',
color: 'var(--color-primary)',
minWidth: '4.5em',
textAlign: 'right',
}}>
{hexProps[key] ?? DEFAULTS[key]}
</span>
</div>
))}
</div>
<button
className="dt-btn mode-normal"
onClick={handleReset}
type="button"
style={{ marginTop: 'var(--space-6)' }}
>
Reset to Defaults
</button>
</Section>
<Section title="Usage" description="Drop-in component for any React app with Three.js.">
<div className="terminal dt-accent-top">
<code>{`import { HexGridBackground } from '@dangerousthings/hex-background';
// Full-viewport fixed background (renders behind content)
<HexGridBackground
opacity={${hexProps.opacity ?? DEFAULTS.opacity}}
hexRadius={${hexProps.hexRadius ?? DEFAULTS.hexRadius}}
maxHeight={${hexProps.maxHeight ?? DEFAULTS.maxHeight}}
cameraSpeed={${hexProps.cameraSpeed ?? DEFAULTS.cameraSpeed}}
/>`}</code>
</div>
</Section>
</>
);
}

View File

@@ -1,12 +1,11 @@
import { DTCard, DTStaggerContainer } from '@dangerousthings/react'; import { DTCard } from '@dangerousthings/react';
import type { DTVariant } from '@dangerousthings/tokens'; import type { DTVariant } from '@dangerousthings/tokens';
import { Section, CodeLabel } from '../components/Section'; import { Section, CodeLabel } from '../components/Section';
const categories: { hash: string; title: string; desc: string; mode: DTVariant; count: number }[] = [ const categories: { hash: string; title: string; desc: string; mode: DTVariant; count: number }[] = [
{ hash: 'bevels', title: 'Bevels', desc: 'Angular clip-path patterns — cards, buttons, badges, media frames, modals, drawers', mode: 'normal', count: 8 }, { hash: 'bevels', title: 'Bevels', desc: 'Angular clip-path patterns — cards, buttons, badges, media frames, modals, drawers', mode: 'normal', count: 8 },
{ hash: 'glows', title: 'Glows', desc: 'Neon drop-shadow and text-shadow effects for the DT brand', mode: 'emphasis', count: 6 },
{ hash: 'forms', title: 'Forms', desc: 'Text inputs, checkboxes, switches, radios, progress bars, accordions, steppers', mode: 'success', count: 7 }, { hash: 'forms', title: 'Forms', desc: 'Text inputs, checkboxes, switches, radios, progress bars, accordions, steppers', mode: 'success', count: 7 },
{ hash: 'animations', title: 'Animations', desc: 'Entrance animations, stagger containers, transition utilities, scrollbar styling', mode: 'other', count: 5 }, { hash: 'animations', title: 'Animations', desc: 'Entrance animations, transition utilities, scrollbar styling', mode: 'other', count: 5 },
{ hash: 'cards-advanced', title: 'Advanced Cards', desc: 'Card color modes, progress bars, badge overlays, interactive bevel buttons, feature legend', mode: 'warning', count: 6 }, { hash: 'cards-advanced', title: 'Advanced Cards', desc: 'Card color modes, progress bars, badge overlays, interactive bevel buttons, feature legend', mode: 'warning', count: 6 },
{ hash: 'tokens', title: 'Tokens', desc: 'Color palette, typography, spacing, and shape values for the active brand', mode: 'normal', count: 3 }, { hash: 'tokens', title: 'Tokens', desc: 'Color palette, typography, spacing, and shape values for the active brand', mode: 'normal', count: 3 },
]; ];
@@ -32,7 +31,6 @@ export function HomePage() {
fontWeight: 900, fontWeight: 900,
letterSpacing: '0.15em', letterSpacing: '0.15em',
marginBottom: '0.25rem', marginBottom: '0.25rem',
textTransform: 'uppercase',
}}> }}>
DANGEROUS THINGS DANGEROUS THINGS
</h1> </h1>
@@ -41,7 +39,6 @@ export function HomePage() {
fontSize: '1.25rem', fontSize: '1.25rem',
fontWeight: 700, fontWeight: 700,
letterSpacing: '0.2em', letterSpacing: '0.2em',
textTransform: 'uppercase',
marginTop: 0, marginTop: 0,
}}> }}>
DESIGN SYSTEM DESIGN SYSTEM
@@ -72,16 +69,15 @@ export function HomePage() {
color: 'var(--color-text-muted)', color: 'var(--color-text-muted)',
fontSize: '0.7rem', fontSize: '0.7rem',
letterSpacing: '0.15em', letterSpacing: '0.15em',
textTransform: 'uppercase',
marginBottom: 'var(--space-4)', marginBottom: 'var(--space-4)',
}}> }}>
COMPONENT CATALOG COMPONENT CATALOG
</p> </p>
<DTStaggerContainer className="grid grid-cols-1 md:grid-cols-2 gap-dt-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-dt-4">
{categories.map(cat => ( {categories.map(cat => (
<a key={cat.hash} href={`#/${cat.hash}`} style={{ textDecoration: 'none', color: 'inherit' }}> <a key={cat.hash} href={`#/${cat.hash}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<DTCard variant={cat.mode} title={cat.title.toUpperCase()}> <DTCard variant={cat.mode} title={cat.title}>
<div className="card-body">{cat.desc}</div> <div className="card-body">{cat.desc}</div>
{cat.count > 0 && ( {cat.count > 0 && (
<div style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem', marginTop: 'var(--space-2)' }}> <div style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem', marginTop: 'var(--space-2)' }}>
@@ -91,7 +87,7 @@ export function HomePage() {
</DTCard> </DTCard>
</a> </a>
))} ))}
</DTStaggerContainer> </div>
<Section title="Quick Start" description="Import the React components and CSS to get started."> <Section title="Quick Start" description="Import the React components and CSS to get started.">
<div className="terminal dt-accent-top"> <div className="terminal dt-accent-top">

View File

@@ -43,7 +43,6 @@ body {
font-size: 0.875rem; font-size: 0.875rem;
letter-spacing: 0.1em; letter-spacing: 0.1em;
color: var(--color-primary); color: var(--color-primary);
text-transform: uppercase;
padding-bottom: var(--space-4); padding-bottom: var(--space-4);
border-bottom: 2px solid var(--color-primary); border-bottom: 2px solid var(--color-primary);
} }
@@ -65,7 +64,6 @@ body {
font-size: 0.75rem; font-size: 0.75rem;
letter-spacing: 0.05em; letter-spacing: 0.05em;
cursor: pointer; cursor: pointer;
text-transform: uppercase;
transition: all var(--transition-fast); transition: all var(--transition-fast);
} }
@@ -93,7 +91,6 @@ body {
font-size: 0.625rem; font-size: 0.625rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
text-transform: uppercase;
transition: all var(--transition-fast); transition: all var(--transition-fast);
} }
@@ -117,7 +114,6 @@ body {
font-weight: 600; font-weight: 600;
font-size: 0.875rem; font-size: 0.875rem;
letter-spacing: 0.02em; letter-spacing: 0.02em;
text-transform: uppercase;
border-left: 3px solid transparent; border-left: 3px solid transparent;
transition: all var(--transition-fast); transition: all var(--transition-fast);
} }
@@ -151,7 +147,6 @@ body {
font-weight: 700; font-weight: 700;
font-size: 1.125rem; font-size: 1.125rem;
color: var(--color-primary); color: var(--color-primary);
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
padding-bottom: var(--space-2); padding-bottom: var(--space-2);
@@ -175,7 +170,6 @@ body {
.demo-label { .demo-label {
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.6875rem; font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
margin-top: var(--space-4); margin-top: var(--space-4);
@@ -200,7 +194,6 @@ body {
padding: var(--space-3) var(--space-6); padding: var(--space-3) var(--space-6);
font-weight: 700; font-weight: 700;
font-size: 0.875rem; font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-fast);
@@ -244,7 +237,6 @@ body {
.card-title { .card-title {
font-weight: 900; font-weight: 900;
font-size: 1rem; font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
@@ -260,7 +252,6 @@ body {
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
font-weight: 600; font-weight: 600;
font-size: 0.75rem; font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
@@ -300,7 +291,6 @@ input:focus {
font-weight: 900; font-weight: 900;
font-size: 1.75rem; font-size: 1.75rem;
color: var(--color-primary); color: var(--color-primary);
text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
} }

View File

@@ -1,7 +1,6 @@
{ {
"name": "@dangerousthings/showcase-mobile", "name": "@dangerousthings/showcase-mobile",
"version": "0.0.0", "version": "0.0.0",
"private": true,
"license": "MIT", "license": "MIT",
"main": "./index.js", "main": "./index.js",
"scripts": { "scripts": {

View File

@@ -1,15 +1,12 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper'; import { Text } from 'react-native-paper';
import { import {
DTCard, DTCard,
DTLabel, DTLabel,
DTStaggerContainer,
useDTTheme, useDTTheme,
useScaleIn, useScaleIn,
usePulse, usePulse,
} from '@dangerousthings/react-native'; } from '@dangerousthings/react-native';
import type { DTVariant } from '@dangerousthings/react-native';
import { ScreenContainer } from '../components/ScreenContainer'; import { ScreenContainer } from '../components/ScreenContainer';
import { DemoSection } from '../components/DemoSection'; import { DemoSection } from '../components/DemoSection';
import { CodeLabel } from '../components/CodeLabel'; import { CodeLabel } from '../components/CodeLabel';
@@ -20,44 +17,8 @@ export function AnimationsScreen() {
const scaleAnim = useScaleIn({ duration: 600 }); const scaleAnim = useScaleIn({ duration: 600 });
const pulseAnim = usePulse(true); const pulseAnim = usePulse(true);
const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other'];
return ( return (
<ScreenContainer> <ScreenContainer>
{/* Stagger Container */}
<DemoSection
title="DTStaggerContainer"
variant="normal"
description="Staggered scale-in entrance animation for child elements."
>
<DTStaggerContainer duration={330} interval={75}>
{modes.map((mode) => (
<DTCard key={mode} mode={mode} title={mode.toUpperCase()} style={{ marginBottom: 12 }}>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Staggered entrance with scale animation
</Text>
</DTCard>
))}
</DTStaggerContainer>
<CodeLabel text="<DTStaggerContainer duration={330} interval={75}>" />
</DemoSection>
{/* Stagger with Labels */}
<DemoSection
title="Staggered Labels"
variant="emphasis"
description="Labels with staggered entrance animation."
>
<DTStaggerContainer duration={400} interval={100}>
{modes.map((mode) => (
<View key={mode} style={{ marginBottom: 8 }}>
<DTLabel primaryText={mode.toUpperCase()} mode={mode} />
</View>
))}
</DTStaggerContainer>
<CodeLabel text="DTStaggerContainer > DTLabel — customizable timing" />
</DemoSection>
{/* Scale-In Hook */} {/* Scale-In Hook */}
<DemoSection <DemoSection
title="useScaleIn" title="useScaleIn"

View File

@@ -5,7 +5,6 @@ import {
DTCard, DTCard,
DTChip, DTChip,
DTBadgeOverlay, DTBadgeOverlay,
DTStaggerContainer,
useDTTheme, useDTTheme,
} from '@dangerousthings/react-native'; } from '@dangerousthings/react-native';
import type { DTVariant } from '@dangerousthings/react-native'; import type { DTVariant } from '@dangerousthings/react-native';
@@ -83,29 +82,6 @@ export function CardsAdvancedScreen() {
<CodeLabel text="<DTBadgeOverlay position='bottom-right'><DTChip variant='warning'>LAB</DTChip>" /> <CodeLabel text="<DTBadgeOverlay position='bottom-right'><DTChip variant='warning'>LAB</DTChip>" />
</DemoSection> </DemoSection>
{/* Stagger + Progress */}
<DemoSection
title="Staggered Cards with Progress"
variant="success"
description="Stagger container with progress bars across all modes."
>
<DTStaggerContainer>
{modes.map((mode) => (
<DTCard
key={mode}
mode={mode}
title={mode.toUpperCase()}
progress={Math.random()}
style={{ marginBottom: 12 }}
>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Staggered + progress
</Text>
</DTCard>
))}
</DTStaggerContainer>
<CodeLabel text="DTStaggerContainer > DTCard progress" />
</DemoSection>
</ScreenContainer> </ScreenContainer>
); );
} }

View File

@@ -58,14 +58,14 @@ const categories: {
}, },
{ {
title: 'ADVANCED CARDS', title: 'ADVANCED CARDS',
subtitle: 'Selected state, progress bar, badge overlays, stagger', subtitle: 'Selected state, progress bar, badge overlays',
mode: 'emphasis', mode: 'emphasis',
route: 'CardsAdvanced', route: 'CardsAdvanced',
count: 4, count: 4,
}, },
{ {
title: 'ANIMATIONS', title: 'ANIMATIONS',
subtitle: 'DTStaggerContainer, useScaleIn, usePulse', subtitle: 'useScaleIn, usePulse',
mode: 'success', mode: 'success',
route: 'Animations', route: 'Animations',
count: 3, count: 3,

View File

@@ -102,7 +102,7 @@ Because the preset uses `var()` references, switching brands at runtime requires
## Monorepo ## Monorepo
Part of the [DT Design System](https://github.com/nicholasgriffintn/dt-design-system) monorepo. Part of the [DT Design System](https://github.com/dangerous-tac0s/dt-design-system) monorepo.
## License ## License

View File

@@ -1,5 +1,23 @@
# @dangerousthings/web # @dangerousthings/web
## 0.5.0
### Minor Changes
- `dt-scale-in` keyframes now hold `transform: none, opacity: 0` during `animation-delay` (the period when `animation-fill-mode: both` shows the first keyframe), then snap to `scale(0)` at animation start before scaling/fading to 1. Visual difference vs prior is a subtle fade-in alongside the scale-in. **Why:** prior keyframes held descendants at `transform: scale(0)` during the delay, which caused any child performing `getBoundingClientRect()`-based self-measurement on mount (R3F `<Canvas>`, ResizeObserver-driven charts, virtualized lists, etc.) to read a near-zero rect and never recover — `transform` changes don't fire `ResizeObserver` when they end. Fixes the SuggestedPlacements 3D viewer "blank teal block until scroll" bug on the Hydrogen storefront product pages.
## 0.4.1
### Patch Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer.
## 0.4.0
### Minor Changes
- Video letterboxing for DTMediaFrame, card badge styling with bevel-aware positioning, restored DTStaggerContainer in react-native, updated DTGallery/DTModal/DTMobileFilterOverlay components.
## 0.3.0 ## 0.3.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
# @dangerousthings/web # @dangerousthings/web
Web CSS themes and components for the Dangerous Things design system — bevels, glows, and form styles powered by CSS custom properties. Web CSS themes and components for the Dangerous Things design system — bevels, animations, and form styles powered by CSS custom properties.
## Install ## Install
@@ -24,7 +24,6 @@ Or import individual pieces:
/* Individual components */ /* Individual components */
@import "@dangerousthings/web/components/bevels.css"; @import "@dangerousthings/web/components/bevels.css";
@import "@dangerousthings/web/components/glows.css";
@import "@dangerousthings/web/components/forms-dt.css"; @import "@dangerousthings/web/components/forms-dt.css";
``` ```
@@ -53,7 +52,6 @@ import { themes, brands } from "@dangerousthings/web/theme-registry";
| File | Description | | File | Description |
|------|-------------| |------|-------------|
| `bevels.css` | Angular clip-path bevels for cards, buttons, labels, modals, drawers. Card color modes, selected states, progress bars, badge overlays, interactive bevel buttons | | `bevels.css` | Angular clip-path bevels for cards, buttons, labels, modals, drawers. Card color modes, selected states, progress bars, badge overlays, interactive bevel buttons |
| `glows.css` | Neon drop-shadow and text-shadow effects — mode-aware via `--dt-glow-color` |
| `forms-dt.css` | Text inputs, checkboxes, switches, radio buttons, progress bars, accordions, steppers, menu items, filter headers, filter overlays | | `forms-dt.css` | Text inputs, checkboxes, switches, radio buttons, progress bars, accordions, steppers, menu items, filter headers, filter overlays |
| `animations.css` | Entrance animations (scale-in, fade-in, slide-up), interactive animations (pulse, ping, spin), stagger container, transition utilities | | `animations.css` | Entrance animations (scale-in, fade-in, slide-up), interactive animations (pulse, ping, spin), stagger container, transition utilities |
| `scrollbar.css` | Thin neon scrollbar styling scoped under `[data-brand="dt"]` | | `scrollbar.css` | Thin neon scrollbar styling scoped under `[data-brand="dt"]` |
@@ -67,8 +65,6 @@ import { themes, brands } from "@dangerousthings/web/theme-registry";
**Interactive Buttons**`.dt-btn` (outlined rectangle, bevels on hover/select) **Interactive Buttons**`.dt-btn` (outlined rectangle, bevels on hover/select)
**Glows**`.dt-glow`, `.dt-glow-strong`, `.dt-glow-inset`, `.dt-text-glow`
**Animations**`.dt-animate-scale-in`, `.dt-animate-fade-in`, `.dt-animate-slide-up`, `.dt-animate-pulse`, `.dt-animate-ping`, `.dt-animate-spin`, `.dt-stagger-container` **Animations**`.dt-animate-scale-in`, `.dt-animate-fade-in`, `.dt-animate-slide-up`, `.dt-animate-pulse`, `.dt-animate-ping`, `.dt-animate-spin`, `.dt-stagger-container`
**Scrollbar**`.dt-scrollbar`, `.dt-scrollbar-mode` **Scrollbar**`.dt-scrollbar`, `.dt-scrollbar-mode`

View File

@@ -1,6 +1,6 @@
{ {
"name": "@dangerousthings/web", "name": "@dangerousthings/web",
"version": "0.3.0", "version": "0.5.1",
"description": "Web CSS theme for the Dangerous Things design system — bevels, glows, form styles", "description": "Web CSS theme for the Dangerous Things design system — bevels, glows, form styles",
"license": "MIT", "license": "MIT",
"author": { "author": {
@@ -28,6 +28,7 @@
"main": "dist/index.css", "main": "dist/index.css",
"exports": { "exports": {
".": "./dist/index.css", ".": "./dist/index.css",
"./dist/index.css": "./dist/index.css",
"./tokens/*": "./dist/tokens/*", "./tokens/*": "./dist/tokens/*",
"./components/*": "./dist/components/*", "./components/*": "./dist/components/*",
"./fonts/*": "./dist/fonts/*", "./fonts/*": "./dist/fonts/*",

View File

@@ -7,8 +7,16 @@
============================================================================ */ ============================================================================ */
@keyframes dt-scale-in { @keyframes dt-scale-in {
from { transform: scale(0); } /* During animation-delay (with fill-mode: both), this 0% keyframe holds.
to { transform: scale(1); } transform: none lets descendants self-measure correctly on mount —
critical for ResizeObserver-based components (R3F Canvas, charts,
virtualized lists) whose initial getBoundingClientRect() would
otherwise be transform-multiplied to ~0 inside a scale(0) ancestor. */
0% { opacity: 0; transform: none; }
/* Snap to scale(0) at animation start so the entrance is still a
visible scale-in (with a brief fade-in alongside). */
0.001% { opacity: 0; transform: scale(0); }
100% { opacity: 1; transform: scale(1); }
} }
@keyframes dt-fade-in { @keyframes dt-fade-in {
@@ -82,49 +90,16 @@
/* ============================================================================ /* ============================================================================
Stagger Container Stagger Container
Source: storefront framer-motion staggered card entrance
Usage: <div class="dt-stagger-container">...</div>
Customize: --dt-stagger-duration, --dt-stagger-interval
============================================================================ */ ============================================================================ */
.dt-stagger-container { .dt-stagger-container {
--dt-stagger-duration: 0.33s; --dt-stagger-duration: 0.33s;
--dt-stagger-interval: 75ms;
} }
.dt-stagger-container > * { .dt-stagger-container > * {
animation: dt-scale-in var(--dt-stagger-duration) ease-in-out both; animation: dt-scale-in var(--dt-stagger-duration) ease-in-out both;
} }
.dt-stagger-container > :nth-child(1) { animation-delay: calc(0 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(2) { animation-delay: calc(1 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(3) { animation-delay: calc(2 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(4) { animation-delay: calc(3 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(5) { animation-delay: calc(4 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(6) { animation-delay: calc(5 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(7) { animation-delay: calc(6 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(8) { animation-delay: calc(7 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(9) { animation-delay: calc(8 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(10) { animation-delay: calc(9 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(11) { animation-delay: calc(10 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(12) { animation-delay: calc(11 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(13) { animation-delay: calc(12 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(14) { animation-delay: calc(13 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(15) { animation-delay: calc(14 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(16) { animation-delay: calc(15 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(17) { animation-delay: calc(16 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(18) { animation-delay: calc(17 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(19) { animation-delay: calc(18 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(20) { animation-delay: calc(19 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(21) { animation-delay: calc(20 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(22) { animation-delay: calc(21 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(23) { animation-delay: calc(22 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(24) { animation-delay: calc(23 * var(--dt-stagger-interval)); }
.dt-stagger-container > :nth-child(n+25) {
animation-delay: calc(24 * var(--dt-stagger-interval));
}
/* ============================================================================ /* ============================================================================
Transition Utilities Transition Utilities
============================================================================ */ ============================================================================ */

View File

@@ -19,6 +19,8 @@
[data-brand="dt"] .card { [data-brand="dt"] .card {
--dt-card-progress: 0; --dt-card-progress: 0;
--_card-pad: var(--space-6); --_card-pad: var(--space-6);
display: flex;
flex-direction: column;
position: relative; position: relative;
isolation: isolate; isolation: isolate;
background: var(--dt-card-color, var(--color-primary)); background: var(--dt-card-color, var(--color-primary));
@@ -44,17 +46,21 @@
z-index: -1; z-index: -1;
/* Inner clip-path: left edge at bevel-sm to create progress bar zone. /* Inner clip-path: left edge at bevel-sm to create progress bar zone.
The progress bar occupies x=0 to x=bevel-sm with its own borders. The progress bar occupies x=0 to x=bevel-sm with its own borders.
Inner surface starts at bevel-sm, so the left frame is wider than 3px. */ Inner surface starts at bevel-sm, so the left frame is wider than 3px.
Diagonal vertices use bevel-md + 3px (not bevel-md alone) so the
perpendicular border width on the 45-degree diagonal visually matches
the 3px straight edges despite sub-pixel antialiasing. */
clip-path: polygon(var(--bevel-sm) 3px, clip-path: polygon(var(--bevel-sm) 3px,
calc(100% - 3px) 3px, calc(100% - 3px) 3px,
calc(100% - 3px) calc(100% - var(--bevel-md)), calc(100% - 3px) calc(100% - var(--bevel-md) - 3px),
calc(100% - var(--bevel-md)) calc(100% - 3px), calc(100% - var(--bevel-md) - 3px) calc(100% - 3px),
var(--bevel-sm) calc(100% - 3px)); var(--bevel-sm) calc(100% - 3px));
} }
/* Card Progress Bar — structural fill area on the left edge /* Card Progress Bar — structural fill area on the left edge
Sits between the 3px frame borders, follows bottom-left bevel diagonal. Sits between the 3px frame borders, follows bottom-left bevel diagonal.
Gradient fills from bottom (accent) to top (surface) based on --dt-card-progress. Gradient fills top-to-bottom based on --dt-card-progress so the card
visually "opens" as the bar descends.
Filled portion: fully opaque accent color. Unfilled portion: semi-transparent surface. Filled portion: fully opaque accent color. Unfilled portion: semi-transparent surface.
At 0%: all semi-transparent surface. At 100%: all opaque accent. At 0%: all semi-transparent surface. At 100%: all opaque accent.
Override --dt-progress-empty-opacity to control unfilled portion transparency. */ Override --dt-progress-empty-opacity to control unfilled portion transparency. */
@@ -70,7 +76,7 @@
calc(var(--bevel-sm) - 3px) calc(100% - 6px), calc(var(--bevel-sm) - 3px) calc(100% - 6px),
3px calc(100% - var(--bevel-sm))); 3px calc(100% - var(--bevel-sm)));
background: linear-gradient(to top, background: linear-gradient(to bottom,
rgba(var(--dt-card-color-rgb, var(--color-primary-rgb)), 1) calc(var(--dt-card-progress, 0) * 1%), rgba(var(--dt-card-color-rgb, var(--color-primary-rgb)), 1) calc(var(--dt-card-progress, 0) * 1%),
rgba(var(--color-surface-rgb), rgba(var(--color-surface-rgb),
var(--dt-progress-empty-opacity, 0.6)) calc(var(--dt-card-progress, 0) * 1%)); var(--dt-progress-empty-opacity, 0.6)) calc(var(--dt-card-progress, 0) * 1%));
@@ -79,12 +85,32 @@
} }
/* Card children — ensure body content renders above the progress bar (::after z-index:1) */ /* Card children — ensure body content renders above the progress bar (::after z-index:1) */
[data-brand="dt"] .dt-bevel-card > *:not(.dt-card-badge), [data-brand="dt"] .dt-bevel-card > *:not(.dt-badge-bottom-right):not(.dt-card-badge),
[data-brand="dt"] .card > *:not(.dt-card-badge) { [data-brand="dt"] .card > *:not(.dt-badge-bottom-right):not(.dt-card-badge) {
position: relative; position: relative;
z-index: 2; z-index: 2;
} }
/* Card Grow — stretch to fill available space in flex/grid containers.
By default the body (content area) grows. Use .dt-card-grow-title to
grow the header instead. */
.dt-card-grow {
flex: 1 1 0%;
height: 100%;
}
.dt-card-grow > .card-title {
flex: 0 0 auto;
}
.dt-card-grow:not(.dt-card-grow-title) > :last-child {
flex: 1 1 auto;
}
.dt-card-grow.dt-card-grow-title > .card-title {
flex: 1 1 auto;
}
/* Card Header — storefront .card-header pattern /* Card Header — storefront .card-header pattern
Title starts at bevel-sm (past the progress bar zone) and extends to the right edge. Title starts at bevel-sm (past the progress bar zone) and extends to the right edge.
Below it, the ::before dark surface shows through for the body area. */ Below it, the ::before dark surface shows through for the body area. */
@@ -102,7 +128,6 @@
background: var(--dt-card-color, var(--color-primary)); background: var(--dt-card-color, var(--color-primary));
color: var(--color-bg); color: var(--color-bg);
font-weight: 900; font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
font-size: 1rem; font-size: 1rem;
position: relative; position: relative;
@@ -125,17 +150,16 @@
background: var(--dt-card-color, var(--color-primary)); background: var(--dt-card-color, var(--color-primary));
} }
/* Bottom-right bevel on card body content (img/video). Clip-path matches /* Bottom-right bevel on card body content. Clip-path matches
the card's inner surface bevel so the 3px structural border is visible the card's inner surface bevel so the 3px structural border is visible
at the diagonal corner. Badge is not clipped (it's a sibling, not img). */ at the diagonal corner. Badge overlays are excluded. */
[data-brand="dt"] .card-body-flush > img, [data-brand="dt"] .card-body-flush > *:not(.dt-badge-bottom-right):not(.dt-card-badge) {
[data-brand="dt"] .card-body-flush > video {
display: block; display: block;
clip-path: polygon( clip-path: polygon(
0% 0%, 0% 0%,
100% 0%, 100% 0%,
100% calc(100% - var(--bevel-md) + 3px), 100% calc(100% - var(--bevel-md)),
calc(100% - var(--bevel-md) + 3px) 100%, calc(100% - var(--bevel-md)) 100%,
0% 100%); 0% 100%);
} }
@@ -202,6 +226,9 @@
[data-brand="dt"] .badge { [data-brand="dt"] .badge {
--badge-border-color: var(--color-border); --badge-border-color: var(--color-border);
--badge-fill: var(--color-surface); --badge-fill: var(--color-surface);
/* inline-block so a wrapping badge stays one box — clip-path and the
::before fill fragment across line boxes on a plain inline span */
display: inline-block;
position: relative; position: relative;
isolation: isolate; isolation: isolate;
background: var(--badge-border-color); background: var(--badge-border-color);
@@ -272,6 +299,37 @@
color: var(--color-bg); color: var(--color-bg);
} }
/* Mode-variant badges — bridge .mode-* vars to badge color vars */
[data-brand="dt"] .badge.mode-normal {
--badge-border-color: var(--mode-normal);
--badge-fill: var(--mode-normal);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-emphasis {
--badge-border-color: var(--mode-emphasis);
--badge-fill: var(--mode-emphasis);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-warning {
--badge-border-color: var(--mode-warning);
--badge-fill: var(--mode-warning);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-success {
--badge-border-color: var(--mode-success);
--badge-fill: var(--mode-success);
color: var(--color-bg);
}
[data-brand="dt"] .badge.mode-other {
--badge-border-color: var(--mode-other);
--badge-fill: var(--mode-other);
color: var(--color-bg);
}
/* ============================================================================ /* ============================================================================
Opt-in Bevel Utility Classes Opt-in Bevel Utility Classes
Use these for custom elements that need bevel shapes Use these for custom elements that need bevel shapes
@@ -326,7 +384,9 @@
} }
.dt-bevel-media > img, .dt-bevel-media > img,
.dt-bevel-media > video { .dt-bevel-media > video,
.dt-bevel-media > model-viewer,
.dt-bevel-media > iframe {
display: block; display: block;
clip-path: polygon( clip-path: polygon(
calc(var(--bevel-md) + 1px) 3px, calc(var(--bevel-md) + 1px) 3px,
@@ -337,11 +397,20 @@
3px calc(var(--bevel-md) + 1px)); 3px calc(var(--bevel-md) + 1px));
} }
/* Video letterboxing — respect natural aspect ratio with black bars */
.dt-bevel-media > video {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
/* Media frame placeholder — shows when no image/video is present. /* Media frame placeholder — shows when no image/video is present.
Surface-colored background is already provided by ::before. Surface-colored background is already provided by ::before.
This adds centered "MISSING MEDIA" text via ::after. */ This adds centered "LOADING MEDIA" text via ::after. Mostly seen while
lazy media loads, so it must read as a loading state, not an error. */
.dt-bevel-media::after { .dt-bevel-media::after {
content: 'MISSING MEDIA'; content: 'LOADING MEDIA';
position: absolute; position: absolute;
inset: 0; inset: 0;
display: flex; display: flex;
@@ -352,7 +421,6 @@
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase;
pointer-events: none; pointer-events: none;
} }
@@ -360,7 +428,7 @@
Uses surface background and centered placeholder text. */ Uses surface background and centered placeholder text. */
.dt-badge-parent:empty::after, .dt-badge-parent:empty::after,
.card-body-flush:empty::after { .card-body-flush:empty::after {
content: 'MISSING MEDIA'; content: 'LOADING MEDIA';
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -369,7 +437,6 @@
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase;
background: var(--color-surface); background: var(--color-surface);
} }
@@ -450,14 +517,13 @@
/* ============================================================================ /* ============================================================================
Card Color Modes — per-instance color overrides Card Color Modes — per-instance color overrides
Sets --dt-card-color, --dt-card-color-selected, --dt-card-color-rgb, Sets --dt-card-color, --dt-card-color-selected, --dt-card-color-rgb,
--dt-glow-color, and --accent-mode for use by all card sub-components. and --accent-mode for use by all card sub-components.
Source: dt-shopify-storefront .mode-* classes Source: dt-shopify-storefront .mode-* classes
============================================================================ */ ============================================================================ */
[data-brand="dt"] .mode-normal { [data-brand="dt"] .mode-normal {
--dt-card-color: var(--mode-normal); --dt-card-color: var(--mode-normal);
--dt-card-color-rgb: var(--mode-normal-rgb); --dt-card-color-rgb: var(--mode-normal-rgb);
--dt-card-color-selected: var(--mode-normal-selected); --dt-card-color-selected: var(--mode-normal-selected);
--dt-glow-color: var(--mode-normal);
--accent-mode: var(--mode-emphasis); --accent-mode: var(--mode-emphasis);
} }
@@ -465,7 +531,6 @@
--dt-card-color: var(--mode-emphasis); --dt-card-color: var(--mode-emphasis);
--dt-card-color-rgb: var(--mode-emphasis-rgb); --dt-card-color-rgb: var(--mode-emphasis-rgb);
--dt-card-color-selected: var(--mode-emphasis-selected); --dt-card-color-selected: var(--mode-emphasis-selected);
--dt-glow-color: var(--mode-emphasis);
--accent-mode: var(--mode-normal); --accent-mode: var(--mode-normal);
} }
@@ -473,7 +538,6 @@
--dt-card-color: var(--mode-warning); --dt-card-color: var(--mode-warning);
--dt-card-color-rgb: var(--mode-warning-rgb); --dt-card-color-rgb: var(--mode-warning-rgb);
--dt-card-color-selected: var(--mode-warning-selected); --dt-card-color-selected: var(--mode-warning-selected);
--dt-glow-color: var(--mode-warning);
--accent-mode: var(--mode-emphasis); --accent-mode: var(--mode-emphasis);
} }
@@ -481,7 +545,6 @@
--dt-card-color: var(--mode-success); --dt-card-color: var(--mode-success);
--dt-card-color-rgb: var(--mode-success-rgb); --dt-card-color-rgb: var(--mode-success-rgb);
--dt-card-color-selected: var(--mode-success-selected); --dt-card-color-selected: var(--mode-success-selected);
--dt-glow-color: var(--mode-success);
--accent-mode: var(--mode-emphasis); --accent-mode: var(--mode-emphasis);
} }
@@ -489,7 +552,6 @@
--dt-card-color: var(--mode-other); --dt-card-color: var(--mode-other);
--dt-card-color-rgb: var(--mode-other-rgb); --dt-card-color-rgb: var(--mode-other-rgb);
--dt-card-color-selected: var(--mode-other-selected); --dt-card-color-selected: var(--mode-other-selected);
--dt-glow-color: var(--mode-other);
--accent-mode: var(--mode-normal); --accent-mode: var(--mode-normal);
} }
@@ -497,15 +559,17 @@
/* ============================================================================ /* ============================================================================
Card Badge — bottom-right product type chip (LAB, BUNDLE, etc.) Card Badge — bottom-right product type chip (LAB, BUNDLE, etc.)
Source: dt-shopify-storefront CyberProductCard LabChip / BundleChip Source: dt-shopify-storefront CyberProductCard LabChip / BundleChip
Plain flat rectangle. Inherits card color via --dt-card-color. Positioned at card level so the card's clip-path bevels the badge corner.
Consumer provides badge content/styling; card handles placement.
============================================================================ */ ============================================================================ */
/* Badge parent — provides position context for absolutely-positioned badges.
Use on the image container inside a card, or on any element that wraps content + badge. */ /* Legacy: standalone badge parent for non-card contexts */
.dt-badge-parent { .dt-badge-parent {
position: relative; position: relative;
} }
.dt-card-badge { /* Legacy: standalone badge positioning for non-card contexts */
.dt-badge-bottom-right {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
right: 8px; right: 8px;
@@ -513,19 +577,32 @@
padding: 2px calc(8px + 0.2rem) 2px 8px; padding: 2px calc(8px + 0.2rem) 2px 8px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
background-color: var(--dt-card-color, var(--color-primary)); background-color: var(--dt-card-color, var(--color-primary));
color: var(--color-bg); color: var(--color-bg);
} }
/* Inside card body-flush, offset badge position to compensate for the /* Card-level badge slot — positioned so the card's bevel clip-path
3px margin from the card edge. This makes the badge position relative clips the badge's bottom-right corner at 45 degrees.
to the card's bevel corner match the media frame's placement exactly Values tuned so the badge is mostly visible with just the corner clipped. */
(8px from card edge in both cases). */ .card > .dt-card-badge,
.card-body-flush > .dt-card-badge { .dt-bevel-card > .dt-card-badge {
bottom: 5px; position: absolute;
right: 5px; bottom: calc(var(--bevel-md) * 0.25);
right: calc(var(--bevel-md) * -0.15);
z-index: 4;
background: var(--dt-card-color, var(--color-primary));
color: var(--color-bg);
padding: 0.35em 2em 0.35em 1em;
font-size: 0.85rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
[data-brand="classic"] .card > .dt-card-badge,
[data-brand="classic"] .dt-bevel-card > .dt-card-badge {
border-radius: 0.375rem;
} }
/* ============================================================================ /* ============================================================================
@@ -547,7 +624,6 @@
font-weight: 600; font-weight: 600;
font-size: 0.875rem; font-size: 0.875rem;
letter-spacing: 0.05em; letter-spacing: 0.05em;
text-transform: uppercase;
cursor: pointer; cursor: pointer;
transition: background-color var(--transition-fast), transition: background-color var(--transition-fast),
color var(--transition-fast), color var(--transition-fast),

View File

@@ -2,55 +2,184 @@
/* Source: dt-shopify-storefront UseCaseLegend component */ /* Source: dt-shopify-storefront UseCaseLegend component */
/* ============================================================================ /* ============================================================================
Feature Legend — icon grid with rotated labels Feature Legend — interactive icon grid with rotated labels, hover details,
? toggle, bordered grid with dark cell backgrounds.
============================================================================ */ ============================================================================ */
.dt-feature-legend { .dt-feature-legend {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0;
} }
/* ---- Header bar ---- */
.dt-feature-legend-header { .dt-feature-legend-header {
padding: 8px 16px; display: flex;
flex-direction: row;
background: var(--dt-card-color, var(--color-primary)); background: var(--dt-card-color, var(--color-primary));
color: var(--color-bg); color: var(--color-bg);
transition: height 0.2s ease-out, opacity 0.2s ease-out;
}
.dt-feature-legend-header-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 8px 12px 6px;
}
.dt-feature-legend-title {
font-weight: 700; font-weight: 700;
font-size: 1.75rem;
line-height: 1.1;
color: var(--color-bg);
}
.dt-feature-legend-detail {
font-size: 0.875rem; font-size: 0.875rem;
text-transform: uppercase; margin-top: 2px;
letter-spacing: 0.05em; min-height: 1.25em;
}
.dt-feature-legend-detail .dt-feature-supported { color: var(--color-bg); }
.dt-feature-legend-detail .dt-feature-disabled { color: var(--color-bg); opacity: 0.7; }
.dt-feature-legend-detail .dt-feature-unsupported { color: #7f1d1d; }
.dt-feature-legend-hint {
opacity: 0.5;
}
/* ? toggle button on right edge */
.dt-feature-legend-toggle {
display: flex;
justify-content: center;
align-items: center;
min-width: 30px;
background: var(--dt-card-color, var(--color-primary));
color: var(--color-bg);
border: none;
font-weight: 700;
font-size: 1.25rem;
cursor: pointer;
transition: opacity 0.15s;
}
.dt-feature-legend-toggle:hover {
opacity: 0.8;
}
/* ---- Grid body ---- */
.dt-feature-legend-body {
display: flex;
flex-direction: row;
overflow: hidden;
} }
.dt-feature-legend-grid { .dt-feature-legend-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0; justify-content: space-around;
flex: 1;
border: 2px solid var(--dt-card-color, var(--color-primary));
padding-top: 8px;
padding-bottom: 8px;
overflow: visible;
transition: padding 0.3s ease-out;
} }
.dt-feature-legend-grid.with-labels {
padding-top: 160px;
padding-bottom: 8px;
}
.dt-feature-legend-grid.with-labels.with-labels-bottom {
padding-bottom: 160px;
}
/* ---- Grid item / cell ---- */
.dt-feature-legend-item { .dt-feature-legend-item {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 4px; justify-content: center;
padding: 8px 4px; padding: 8px 4px;
flex: 0 0 20%; /* 5 columns */ background: var(--color-bg);
cursor: pointer;
} }
/* ---- Icon ---- */
.dt-feature-legend-icon { .dt-feature-legend-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 42px; font-size: 42px;
line-height: 1; line-height: 1;
color: var(--dt-feature-color, var(--color-primary)); color: var(--dt-feature-color, var(--color-primary));
transition: opacity 0.15s;
} }
.dt-feature-legend-label { .dt-feature-legend-item:hover .dt-feature-legend-icon {
font-size: 0.75rem; animation: dt-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* ---- Rotated vertical label ---- */
.dt-feature-legend-rotated-label {
position: absolute;
left: 0;
right: 0;
display: flex;
justify-content: center;
padding: 8px 2px;
z-index: 1;
opacity: 0;
transition: opacity 0.3s ease-out 0.15s; /* delay so padding opens first */
pointer-events: none;
}
.dt-feature-legend-rotated-label.label-visible {
opacity: 1;
pointer-events: auto;
}
.dt-feature-legend-rotated-label.label-above {
bottom: 100%;
}
.dt-feature-legend-rotated-label.label-below {
top: 100%;
}
.dt-feature-legend-rotated-label span {
writing-mode: vertical-rl;
transform: rotate(180deg);
color: var(--dt-feature-color, var(--color-primary));
font-size: 1rem;
font-weight: 600; font-weight: 600;
color: var(--dt-feature-color, var(--color-text-primary)); white-space: nowrap;
text-align: center; padding: 4px 2px;
line-height: 1.2;
overflow-wrap: break-word;
} }
/* Feature state color classes */ /* ---- Feature state color classes ---- */
.dt-feature-supported { --dt-feature-color: var(--mode-normal, var(--color-primary)); } .dt-feature-supported { --dt-feature-color: var(--mode-normal, var(--color-primary)); }
.dt-feature-disabled { --dt-feature-color: var(--mode-emphasis, var(--color-secondary)); } .dt-feature-disabled { --dt-feature-color: var(--mode-emphasis, var(--color-secondary)); }
.dt-feature-unsupported { --dt-feature-color: var(--mode-warning, var(--color-error)); } .dt-feature-unsupported { --dt-feature-color: var(--mode-warning, var(--color-error)); }
/* ---- Variant header colors ---- */
.mode-normal .dt-feature-legend-header,
.mode-normal .dt-feature-legend-toggle { background: var(--mode-normal, var(--color-primary)); }
.mode-normal .dt-feature-legend-grid { border-color: var(--mode-normal, var(--color-primary)); }
.mode-emphasis .dt-feature-legend-header,
.mode-emphasis .dt-feature-legend-toggle { background: var(--mode-emphasis, var(--color-secondary)); }
.mode-emphasis .dt-feature-legend-grid { border-color: var(--mode-emphasis, var(--color-secondary)); }
.mode-warning .dt-feature-legend-header,
.mode-warning .dt-feature-legend-toggle { background: var(--mode-warning, var(--color-error)); }
.mode-warning .dt-feature-legend-grid { border-color: var(--mode-warning, var(--color-error)); }
.mode-success .dt-feature-legend-header,
.mode-success .dt-feature-legend-toggle { background: var(--mode-success, var(--color-accent)); }
.mode-success .dt-feature-legend-grid { border-color: var(--mode-success, var(--color-accent)); }
.mode-other .dt-feature-legend-header,
.mode-other .dt-feature-legend-toggle { background: var(--mode-other, var(--color-other)); }
.mode-other .dt-feature-legend-grid { border-color: var(--mode-other, var(--color-other)); }

View File

@@ -143,49 +143,38 @@
/* ============================================================================ /* ============================================================================
Radio Button — Hexagonal indicator Radio Button — Hexagonal indicator
Matches RN DTRadioGroup: outlined hexagon with inner filled dot on check. Matches RN DTRadioGroup: outlined hexagon with inner filled dot on check.
Outer hexagon is always stroked (accent border); inner dot appears on check. Uses a custom span (.dt-radio-hex) since ::before on <input> is unreliable.
============================================================================ */ ============================================================================ */
[data-brand="dt"] input[type="radio"] { [data-brand="dt"] .dt-radio-hex {
appearance: none; position: relative;
-webkit-appearance: none; display: inline-block;
display: grid;
place-content: center;
width: 22px; width: 22px;
height: 22px; height: 22px;
flex-shrink: 0; flex-shrink: 0;
margin: 0;
border: none;
background: var(--color-primary); background: var(--color-primary);
cursor: pointer;
clip-path: polygon( clip-path: polygon(
50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25% 50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%
); );
transition: background var(--transition-fast); transition: background var(--transition-fast);
} }
/* Inner surface — creates the outlined hexagon look (accent border + transparent center) */ /* Inner surface — outlined hexagon look (accent border + dark center) */
[data-brand="dt"] input[type="radio"]::before { [data-brand="dt"] .dt-radio-hex-inner {
content: '';
position: absolute; position: absolute;
inset: 2px; inset: 2px;
background: var(--color-bg); background: var(--color-bg);
clip-path: polygon( clip-path: polygon(
50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25% 50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%
); );
transition: background var(--transition-fast); transition: all var(--transition-fast);
} }
/* Checked: inner dot fills with accent color (smaller inset for the dot effect) */ /* Checked: inner dot fills with accent color (smaller inset) */
[data-brand="dt"] input[type="radio"]:checked::before { [data-brand="dt"] .dt-radio-hex.checked .dt-radio-hex-inner {
inset: 5px; inset: 5px;
background: var(--color-primary); background: var(--color-primary);
} }
[data-brand="dt"] input[type="radio"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Radio group option container */ /* Radio group option container */
.dt-radio-option { .dt-radio-option {
display: flex; display: flex;
@@ -262,7 +251,6 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
text-transform: uppercase;
font-weight: 600; font-weight: 600;
font-size: 0.875rem; font-size: 0.875rem;
letter-spacing: 0.05em; letter-spacing: 0.05em;
@@ -362,7 +350,6 @@
font-weight: 600; font-weight: 600;
font-size: 0.875rem; font-size: 0.875rem;
letter-spacing: 0.05em; letter-spacing: 0.05em;
text-transform: uppercase;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-fast);
} }
@@ -403,11 +390,6 @@
animation: dt-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: dt-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
/* Nested level indentation */
.dt-menu-item[style*="--dt-menu-level"] {
padding-left: calc(0.75rem + var(--dt-menu-level, 0) * 1rem);
}
/* Classic brand: rounded, thinner borders */ /* Classic brand: rounded, thinner borders */
[data-brand="classic"] .dt-menu-item { [data-brand="classic"] .dt-menu-item {
border-top-width: 2px; border-top-width: 2px;
@@ -436,7 +418,6 @@
border-top: 5px solid var(--color-primary); border-top: 5px solid var(--color-primary);
border-bottom: 1px solid rgba(var(--color-primary-rgb), 0.2); border-bottom: 1px solid rgba(var(--color-primary-rgb), 0.2);
font-weight: 600; font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
cursor: pointer; cursor: pointer;
} }

View File

@@ -1,81 +0,0 @@
/* dt-web-theme: Glow & Neon Effects */
/* Source: dt-shopify-storefront glow patterns */
/* */
/* Elements with clip-path use filter: drop-shadow() instead of box-shadow */
/* because clip-path clips box-shadow but filter follows the clipped shape. */
/* ============================================================================
Button Glows — filter: drop-shadow (buttons have clip-path)
============================================================================ */
[data-brand="dt"] .btn-primary {
box-shadow: none;
filter: drop-shadow(0 0 10px rgba(var(--color-primary-rgb), 0.3));
}
[data-brand="dt"] .btn-primary:hover {
box-shadow: none;
filter: drop-shadow(0 0 15px rgba(var(--color-accent-rgb), 0.4));
}
[data-brand="dt"] .btn-secondary:hover {
filter: drop-shadow(0 0 8px rgba(var(--color-primary-rgb), 0.3));
}
[data-brand="dt"] .btn-danger {
box-shadow: none;
filter: drop-shadow(0 0 10px rgba(var(--color-error-rgb), 0.3));
}
[data-brand="dt"] .btn-danger:hover {
box-shadow: none;
filter: drop-shadow(0 0 15px rgba(var(--color-error-rgb), 0.4));
}
/* ============================================================================
Link Glow
============================================================================ */
[data-brand="dt"] a:hover {
text-shadow: 0 0 8px var(--dt-glow-color, var(--color-primary));
}
/* ============================================================================
Terminal Inset Glow (no clip-path, box-shadow OK)
============================================================================ */
[data-brand="dt"] .terminal {
box-shadow: inset 0 0 10px rgba(var(--color-accent-rgb), 0.05),
0 0 10px rgba(var(--color-accent-rgb), 0.1);
}
/* ============================================================================
Card Hover Glow — filter: drop-shadow (cards have clip-path)
============================================================================ */
[data-brand="dt"] .card:hover,
[data-brand="dt"] .dt-bevel-card:hover {
filter: drop-shadow(0 0 8px rgba(var(--dt-card-color-rgb, var(--color-primary-rgb)), 0.3));
}
/* ============================================================================
Input Focus Glow (no clip-path on inputs, box-shadow OK)
============================================================================ */
[data-brand="dt"] .input:focus {
box-shadow: 0 4px 0 1px var(--color-primary);
}
/* ============================================================================
Generic Glow Utilities
============================================================================ */
.dt-glow {
filter: drop-shadow(0 0 10px rgba(var(--color-primary-rgb), 0.3));
}
.dt-glow-strong {
filter: drop-shadow(0 0 15px rgba(var(--color-primary-rgb), 0.5));
}
.dt-glow-inset {
box-shadow: inset 0 0 10px rgba(var(--color-primary-rgb), 0.1);
}
.dt-text-glow {
text-shadow: 0 0 8px currentColor;
}

View File

@@ -13,7 +13,6 @@
/* Component styles — DT visual identity */ /* Component styles — DT visual identity */
@import './components/bevels.css'; @import './components/bevels.css';
@import './components/glows.css';
@import './components/forms-dt.css'; @import './components/forms-dt.css';
@import './components/animations.css'; @import './components/animations.css';
@import './components/scrollbar.css'; @import './components/scrollbar.css';