diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index f6803f8b..ace7b52b 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -27,6 +27,21 @@ jobs:
with:
languages: ${{ matrix.language }}
queries: security-extended
+ # Compiled/minified bundles committed for distribution, not hand-written
+ # source. Scanning them is both unhelpful (GitHub can't even render a
+ # code preview for minified JS) and unstable: their line numbers shift
+ # on every rebuild, so CodeQL's alert fingerprinting can't match an
+ # alert here across commits — every push reports the same handful of
+ # flagged patterns as "new", even after the underlying source (already
+ # scanned separately, since these directories are 100% build output) is
+ # fixed. The real source lives in admin-app/src/ and
+ # integrations/bricks/editor-app/src/, both still fully scanned.
+ # `paths-ignore` is not a direct action input on this codeql-action
+ # version — it must go inside the inline `config` YAML.
+ config: |
+ paths-ignore:
+ - SLASHED-for-WP/assets/admin-app/**
+ - SLASHED-for-WP/integrations/bricks/assets/editor-app/**
- uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index fdf83ca9..39048a1d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -68,9 +68,15 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin "$DEFAULT_BRANCH"
- # Preserve the files updated by update-framework before we switch
- # branches -- git checkout resets tracked files to the branch state.
- mkdir -p /tmp/fw-dist /tmp/fw-data /tmp/fw-bricks-data
+ # Preserve the files updated by update-framework and build:admin-app
+ # before we switch branches -- git checkout resets tracked files to
+ # the branch state. This must include everything npm run sync (run
+ # as build:admin-app's prebuild hook) vendors from the configurator
+ # -- admin-app/src/, its manifest, and framework-css/ -- otherwise
+ # the configurator changes synced for this release are built into
+ # the zip but never committed back to the default branch.
+ mkdir -p /tmp/fw-dist /tmp/fw-data /tmp/fw-bricks-data \
+ /tmp/fw-admin-app-src /tmp/fw-admin-app-framework-css /tmp/fw-assets-admin-app
cp -a SLASHED-for-WP/dist/. /tmp/fw-dist/
cp -a SLASHED-for-WP/data/. /tmp/fw-data/
cp -a SLASHED-for-WP/integrations/bricks/data/. /tmp/fw-bricks-data/
@@ -80,6 +86,10 @@ jobs:
cp scripts/registry-sources.js /tmp/fw-registry-sources.js
cp package.json /tmp/fw-package.json
cp package-lock.json /tmp/fw-package-lock.json
+ cp -a SLASHED-for-WP/admin-app/src/. /tmp/fw-admin-app-src/
+ cp SLASHED-for-WP/admin-app/.vendored-manifest.json /tmp/fw-admin-app-manifest.json
+ cp -a SLASHED-for-WP/admin-app/framework-css/. /tmp/fw-admin-app-framework-css/
+ cp -a SLASHED-for-WP/assets/admin-app/. /tmp/fw-assets-admin-app/
git checkout "$DEFAULT_BRANCH"
@@ -93,6 +103,14 @@ jobs:
cp /tmp/fw-registry-sources.js scripts/registry-sources.js
cp /tmp/fw-package.json package.json
cp /tmp/fw-package-lock.json package-lock.json
+ rm -rf SLASHED-for-WP/admin-app/src
+ mkdir -p SLASHED-for-WP/admin-app/src
+ cp -a /tmp/fw-admin-app-src/. SLASHED-for-WP/admin-app/src/
+ cp /tmp/fw-admin-app-manifest.json SLASHED-for-WP/admin-app/.vendored-manifest.json
+ rm -rf SLASHED-for-WP/admin-app/framework-css SLASHED-for-WP/assets/admin-app
+ mkdir -p SLASHED-for-WP/admin-app/framework-css SLASHED-for-WP/assets/admin-app
+ cp -a /tmp/fw-admin-app-framework-css/. SLASHED-for-WP/admin-app/framework-css/
+ cp -a /tmp/fw-assets-admin-app/. SLASHED-for-WP/assets/admin-app/
# Re-run changelog-release on the default branch so it picks up the
# committed tag (the release workflow checks out the tag ref above).
@@ -107,7 +125,11 @@ jobs:
SLASHED-for-WP/integrations/gutenberg/slashed-gutenberg.php \
scripts/registry-sources.js \
package.json \
- package-lock.json
+ package-lock.json \
+ SLASHED-for-WP/admin-app/src/ \
+ SLASHED-for-WP/admin-app/.vendored-manifest.json \
+ SLASHED-for-WP/admin-app/framework-css/ \
+ SLASHED-for-WP/assets/admin-app/
git diff --staged --quiet && echo "nothing to commit" && exit 0
git commit -m "chore: sync framework + update docs for $TAG"
git push origin "$DEFAULT_BRANCH"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 287233ce..78ba9351 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,12 @@ framework version is tracked separately via the `SLASHED_*_CSS_REF` constants.
## [Unreleased]
+### Fixed
+
+- Sync vendored configurator (`admin-app/src/`) with framework PRs #460/#461 — saved themes, motion panel overhaul, base color palette and live semantic preview — which had silently failed to land.
+- `release.yml` now commits the configurator sync (`admin-app/src/`, `.vendored-manifest.json`, `framework-css/`, `assets/admin-app/`) back to the default branch instead of discarding it after each release build.
+- Remove `.syncignore` entries for files no longer divergent from upstream `SLASHED#443`.
+
## [0.4.16] - 2026-06-30
## [0.4.16] - 2026-06-30
## [0.4.16] - 2026-06-30
diff --git a/SLASHED-for-WP/admin-app/.syncignore b/SLASHED-for-WP/admin-app/.syncignore
index 9649f5d1..3fef6930 100644
--- a/SLASHED-for-WP/admin-app/.syncignore
+++ b/SLASHED-for-WP/admin-app/.syncignore
@@ -3,25 +3,11 @@
# Plugin-specific build wiring lives OUTSIDE src/ (vite.config.js, package.json,
# svelte.config.js, tsconfig.json) and is never touched by the sync.
-# The following files are part of codeslash-dev/slashed#443 (the companion
-# upstream PR that adds @framework-css aliases + the WP persistence seam).
-# Those changes live on the feature branch and have not yet merged to main.
-#
-# Modified files: the CI prebuild sync fetches from main and would overwrite
-# these with pre-alias / pre-persistence versions, breaking the build and runtime.
-# New files: the sync now wipes src/ before fetching, so any file that doesn't
-# exist on main would be deleted unless listed here.
-# Remove all five lines once slashed#443 merges to main.
-src/main.ts
-src/App.svelte
-src/components/shell/PreviewPanel.svelte
-src/vite-env.d.ts
-src/lib/persistence.ts
+# codeslash-dev/slashed#443 (the companion upstream PR that added @framework-css
+# aliases + the WP persistence seam) merged to main on 2026-06-28. main.ts,
+# App.svelte, PreviewPanel.svelte, vite-env.d.ts, lib/persistence.ts, and
+# StudioHeader.svelte are no longer plugin-divergent — they sync normally now.
# Plugin-specific: frontend overlay (not in upstream configurator).
src/plugin-main.ts
src/AppOverlay.svelte
-
-# StudioHeader has WP-specific Save button additions (hasPendingChanges/saveState/onSave)
-# that live on the feature branch in upstream; keep until the companion PR merges to main.
-src/components/shell/StudioHeader.svelte
diff --git a/SLASHED-for-WP/admin-app/.vendored-manifest.json b/SLASHED-for-WP/admin-app/.vendored-manifest.json
index d60ac41b..7bc294c7 100644
--- a/SLASHED-for-WP/admin-app/.vendored-manifest.json
+++ b/SLASHED-for-WP/admin-app/.vendored-manifest.json
@@ -3,14 +3,18 @@
"description": "Files vendored from the SLASHED framework configurator. DO NOT edit directly — make changes in codeslash-dev/SLASHED → configurator/src/ and re-run npm run sync.",
"frameworkRepo": "https://github.com/codeslash-dev/SLASHED",
"configuratorSrc": "configurator/src/",
- "source": "local:/home/user/SLASHED/configurator/src",
- "syncedAt": "2026-06-30T09:21:41.107Z"
+ "source": "local",
+ "syncedAt": "2026-06-30T21:51:15.209Z"
},
"vendoredFiles": [
{
"file": "src/app.css",
"source": "local:configurator/src/app.css"
},
+ {
+ "file": "src/App.svelte",
+ "source": "local:configurator/src/App.svelte"
+ },
{
"file": "src/components/CommandPalette.svelte",
"source": "local:configurator/src/components/CommandPalette.svelte"
@@ -43,10 +47,6 @@
"file": "src/components/inputs/SliderRow.svelte",
"source": "local:configurator/src/components/inputs/SliderRow.svelte"
},
- {
- "file": "src/components/inputs/StylePresetCards.svelte",
- "source": "local:configurator/src/components/inputs/StylePresetCards.svelte"
- },
{
"file": "src/components/inputs/TokenRow.svelte",
"source": "local:configurator/src/components/inputs/TokenRow.svelte"
@@ -119,6 +119,10 @@
"file": "src/components/panels/WcagPanel.svelte",
"source": "local:configurator/src/components/panels/WcagPanel.svelte"
},
+ {
+ "file": "src/components/shell/PreviewPanel.svelte",
+ "source": "local:configurator/src/components/shell/PreviewPanel.svelte"
+ },
{
"file": "src/components/shell/SidebarNav.svelte",
"source": "local:configurator/src/components/shell/SidebarNav.svelte"
@@ -127,6 +131,10 @@
"file": "src/components/shell/StatusBar.svelte",
"source": "local:configurator/src/components/shell/StatusBar.svelte"
},
+ {
+ "file": "src/components/shell/StudioHeader.svelte",
+ "source": "local:configurator/src/components/shell/StudioHeader.svelte"
+ },
{
"file": "src/data/api-index.generated.json",
"source": "local:configurator/src/data/api-index.generated.json"
@@ -167,6 +175,10 @@
"file": "src/lib/lumlockerPreview.svelte.ts",
"source": "local:configurator/src/lib/lumlockerPreview.svelte.ts"
},
+ {
+ "file": "src/lib/persistence.ts",
+ "source": "local:configurator/src/lib/persistence.ts"
+ },
{
"file": "src/lib/powerKnobs.ts",
"source": "local:configurator/src/lib/powerKnobs.ts"
@@ -176,16 +188,20 @@
"source": "local:configurator/src/lib/previewResolver.svelte.ts"
},
{
- "file": "src/lib/stylePresets.ts",
- "source": "local:configurator/src/lib/stylePresets.ts"
+ "file": "src/lib/savedThemes.ts",
+ "source": "local:configurator/src/lib/savedThemes.ts"
},
{
- "file": "src/lib/themes.ts",
- "source": "local:configurator/src/lib/themes.ts"
+ "file": "src/main.ts",
+ "source": "local:configurator/src/main.ts"
},
{
"file": "src/types.ts",
"source": "local:configurator/src/types.ts"
+ },
+ {
+ "file": "src/vite-env.d.ts",
+ "source": "local:configurator/src/vite-env.d.ts"
}
]
}
diff --git a/SLASHED-for-WP/admin-app/framework-css/badges/slashed.full.css b/SLASHED-for-WP/admin-app/framework-css/badges/slashed.full.css
index 166168d1..b8ad4e7e 100644
--- a/SLASHED-for-WP/admin-app/framework-css/badges/slashed.full.css
+++ b/SLASHED-for-WP/admin-app/framework-css/badges/slashed.full.css
@@ -1,4 +1,4 @@
-/* SLASHED v0.6.25 — slashed.full.css */
+/* SLASHED v0.6.29 — slashed.full.css */
/* ─── core/layers.css ─── */
/* SLASHED — core/layers.css
@@ -859,56 +859,43 @@
/* ----------------------------------------------------------
- base — V-shaped ramp (inverted tint direction)
+ base — fixed-lightness ramp (mode-independent, monotonic)
────────────────────────────────────────────────────────
- Unlike every other family, base is a near-white (light mode)
- or near-black (dark mode) surface token, not a mid-lightness
- brand colour. There is nowhere "lighter" to go from near-white,
- so the tint direction is inverted: steps 50-400 mix --sf-color-text
- INTO base (progressively darker in light mode, progressively
- lighter in dark mode). Steps 600-950 mix base INTO text in both
- modes (the same direction as other families' shades).
-
- Approximate OKLCH L values — default palette, light mode:
- base-50 ≈ 0.946 barely off-surface (dividers, zebra rows)
- base-100 ≈ 0.931 subtle surface variant
- base-200 ≈ 0.900 light gray (well / inset areas)
- base-300 ≈ 0.840 medium-light gray
- base-400 ≈ 0.748 medium gray (secondary UI elements)
- base-500 ≈ 0.960 base itself (source token)
- base-600 ≈ 0.850 medium-light (toward text, from 500)
- base-700 ≈ 0.762
- base-800 ≈ 0.630
- base-900 ≈ 0.337
- base-950 ≈ 0.170 near-black
-
- Dark mode reverses the visual: base-50 becomes near-white-ish,
- base-950 becomes near-black-ish, base-500 is the dark surface.
-
- SEMANTIC ALIAS MAPPING (consistent position in ramp, not absolute L):
- superlight = base-50 (closest to surface — barely distinguishable)
- xlight = base-200 (light gray)
- lighter = base-400 (medium gray — "lighter contrast" toward text)
- darker = base-600 (one step past base toward text from shade side)
- xdark = base-800
- superdark = base-950 (near ink color)
-
- When you need surface-level differentiation, prefer
- --sf-color-inset / --sf-color-raised / --sf-color-bg over the
- numbered scale. For overlays, prefer the alpha variants (-a10…-a80)
- which are mode-safe by transparency and compose cleanly on any bg.
+ base is a surface-elevation family, not a mid-lightness brand
+ colour. The ramp uses oklch() relative syntax with fixed L steps
+ spanning 0.97 → 0.06, preserving the base source's hue and chroma.
+ 50 is always near-white; 950 is always near-black regardless of
+ light/dark mode or the source colour's position.
+
+ OKLCH L values (fixed, mode-independent):
+ base-50 = 0.97 near-white
+ base-100 = 0.94 very light gray
+ base-200 = 0.88 light gray
+ base-300 = 0.78 medium-light gray
+ base-400 = 0.65 medium gray
+ base-500 = 0.50 mid gray
+ base-600 = 0.37 medium-dark gray
+ base-700 = 0.27 dark gray
+ base-800 = 0.17 very dark gray
+ base-900 = 0.11 near-black
+ base-950 = 0.06 near-black
+
+ For mode-adaptive surface differentiation use the surface tokens
+ (--sf-color-inset / --sf-color-raised / --sf-color-bg) which derive
+ from --sf-color-base via relative oklch and adapt to light/dark mode.
+ For overlays use alpha variants (-a10…-a80).
---------------------------------------------------------- */
- --sf-color-base-50: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-50), var(--sf-color-base));
- --sf-color-base-100: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-100), var(--sf-color-base));
- --sf-color-base-200: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-200), var(--sf-color-base));
- --sf-color-base-300: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-300), var(--sf-color-base));
- --sf-color-base-400: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-400), var(--sf-color-base));
- --sf-color-base-500: var(--sf-color-base);
- --sf-color-base-600: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-600), var(--sf-color-text));
- --sf-color-base-700: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-700), var(--sf-color-text));
- --sf-color-base-800: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-800), var(--sf-color-text));
- --sf-color-base-900: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-900), var(--sf-color-text));
- --sf-color-base-950: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-950), var(--sf-color-text));
+ --sf-color-base-50: oklch(from var(--sf-color-base) 0.97 c h);
+ --sf-color-base-100: oklch(from var(--sf-color-base) 0.94 c h);
+ --sf-color-base-200: oklch(from var(--sf-color-base) 0.88 c h);
+ --sf-color-base-300: oklch(from var(--sf-color-base) 0.78 c h);
+ --sf-color-base-400: oklch(from var(--sf-color-base) 0.65 c h);
+ --sf-color-base-500: oklch(from var(--sf-color-base) 0.50 c h);
+ --sf-color-base-600: oklch(from var(--sf-color-base) 0.37 c h);
+ --sf-color-base-700: oklch(from var(--sf-color-base) 0.27 c h);
+ --sf-color-base-800: oklch(from var(--sf-color-base) 0.17 c h);
+ --sf-color-base-900: oklch(from var(--sf-color-base) 0.11 c h);
+ --sf-color-base-950: oklch(from var(--sf-color-base) 0.06 c h);
--sf-color-base-a5: oklch(from var(--sf-color-base) l c h / 0.05);
--sf-color-base-a10: oklch(from var(--sf-color-base) l c h / 0.10);
@@ -916,14 +903,13 @@
--sf-color-base-a50: oklch(from var(--sf-color-base) l c h / 0.50);
--sf-color-base-a80: oklch(from var(--sf-color-base) l c h / 0.80);
- --sf-color-base-superlight: var(--sf-color-base-50);
- --sf-color-base-xlight: var(--sf-color-base-200);
- --sf-color-base-lighter: var(--sf-color-base-400);
- --sf-color-base-darker: var(--sf-color-base-600);
- --sf-color-base-xdark: var(--sf-color-base-800);
- --sf-color-base-superdark: var(--sf-color-base-950);
- --sf-color-base--hover: var(--sf-color-base-darker);
- --sf-color-base--active: var(--sf-color-base-xdark);
+ /* No relative aliases (lighter/darker/xlight/…) on base: it is an
+ absolute scale, so a step's name would lie depending on where the
+ source sits (e.g. "lighter" would be darker than a near-white base
+ in light mode). For relative, mode-adaptive surface elevation use
+ --sf-color-bg / --sf-color-inset / --sf-color-raised instead. */
+ --sf-color-base--hover: oklch(from var(--sf-color-base) clamp(0.01, calc(l - 0.04 + var(--sf-is-dark) * 0.08), 0.99) c h);
+ --sf-color-base--active: oklch(from var(--sf-color-base) clamp(0.01, calc(l - 0.08 + var(--sf-is-dark) * 0.16), 0.99) c h);
--sf-color-base-subtle: var(--sf-color-base-a10);
--sf-color-base-muted: var(--sf-color-base-a30);
--sf-color-base-ghost: var(--sf-color-base-a5);
diff --git a/SLASHED-for-WP/admin-app/framework-css/core/tokens.css b/SLASHED-for-WP/admin-app/framework-css/core/tokens.css
index 3c87633f..4bc09994 100644
--- a/SLASHED-for-WP/admin-app/framework-css/core/tokens.css
+++ b/SLASHED-for-WP/admin-app/framework-css/core/tokens.css
@@ -834,56 +834,43 @@
/* ----------------------------------------------------------
- base — V-shaped ramp (inverted tint direction)
+ base — fixed-lightness ramp (mode-independent, monotonic)
────────────────────────────────────────────────────────
- Unlike every other family, base is a near-white (light mode)
- or near-black (dark mode) surface token, not a mid-lightness
- brand colour. There is nowhere "lighter" to go from near-white,
- so the tint direction is inverted: steps 50-400 mix --sf-color-text
- INTO base (progressively darker in light mode, progressively
- lighter in dark mode). Steps 600-950 mix base INTO text in both
- modes (the same direction as other families' shades).
-
- Approximate OKLCH L values — default palette, light mode:
- base-50 ≈ 0.946 barely off-surface (dividers, zebra rows)
- base-100 ≈ 0.931 subtle surface variant
- base-200 ≈ 0.900 light gray (well / inset areas)
- base-300 ≈ 0.840 medium-light gray
- base-400 ≈ 0.748 medium gray (secondary UI elements)
- base-500 ≈ 0.960 base itself (source token)
- base-600 ≈ 0.850 medium-light (toward text, from 500)
- base-700 ≈ 0.762
- base-800 ≈ 0.630
- base-900 ≈ 0.337
- base-950 ≈ 0.170 near-black
-
- Dark mode reverses the visual: base-50 becomes near-white-ish,
- base-950 becomes near-black-ish, base-500 is the dark surface.
-
- SEMANTIC ALIAS MAPPING (consistent position in ramp, not absolute L):
- superlight = base-50 (closest to surface — barely distinguishable)
- xlight = base-200 (light gray)
- lighter = base-400 (medium gray — "lighter contrast" toward text)
- darker = base-600 (one step past base toward text from shade side)
- xdark = base-800
- superdark = base-950 (near ink color)
-
- When you need surface-level differentiation, prefer
- --sf-color-inset / --sf-color-raised / --sf-color-bg over the
- numbered scale. For overlays, prefer the alpha variants (-a10…-a80)
- which are mode-safe by transparency and compose cleanly on any bg.
+ base is a surface-elevation family, not a mid-lightness brand
+ colour. The ramp uses oklch() relative syntax with fixed L steps
+ spanning 0.97 → 0.06, preserving the base source's hue and chroma.
+ 50 is always near-white; 950 is always near-black regardless of
+ light/dark mode or the source colour's position.
+
+ OKLCH L values (fixed, mode-independent):
+ base-50 = 0.97 near-white
+ base-100 = 0.94 very light gray
+ base-200 = 0.88 light gray
+ base-300 = 0.78 medium-light gray
+ base-400 = 0.65 medium gray
+ base-500 = 0.50 mid gray
+ base-600 = 0.37 medium-dark gray
+ base-700 = 0.27 dark gray
+ base-800 = 0.17 very dark gray
+ base-900 = 0.11 near-black
+ base-950 = 0.06 near-black
+
+ For mode-adaptive surface differentiation use the surface tokens
+ (--sf-color-inset / --sf-color-raised / --sf-color-bg) which derive
+ from --sf-color-base via relative oklch and adapt to light/dark mode.
+ For overlays use alpha variants (-a10…-a80).
---------------------------------------------------------- */
- --sf-color-base-50: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-50), var(--sf-color-base));
- --sf-color-base-100: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-100), var(--sf-color-base));
- --sf-color-base-200: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-200), var(--sf-color-base));
- --sf-color-base-300: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-300), var(--sf-color-base));
- --sf-color-base-400: color-mix(in oklab, var(--sf-color-text) var(--sf-palette-mix-400), var(--sf-color-base));
- --sf-color-base-500: var(--sf-color-base);
- --sf-color-base-600: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-600), var(--sf-color-text));
- --sf-color-base-700: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-700), var(--sf-color-text));
- --sf-color-base-800: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-800), var(--sf-color-text));
- --sf-color-base-900: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-900), var(--sf-color-text));
- --sf-color-base-950: color-mix(in oklab, var(--sf-color-base) var(--sf-palette-mix-950), var(--sf-color-text));
+ --sf-color-base-50: oklch(from var(--sf-color-base) 0.97 c h);
+ --sf-color-base-100: oklch(from var(--sf-color-base) 0.94 c h);
+ --sf-color-base-200: oklch(from var(--sf-color-base) 0.88 c h);
+ --sf-color-base-300: oklch(from var(--sf-color-base) 0.78 c h);
+ --sf-color-base-400: oklch(from var(--sf-color-base) 0.65 c h);
+ --sf-color-base-500: oklch(from var(--sf-color-base) 0.50 c h);
+ --sf-color-base-600: oklch(from var(--sf-color-base) 0.37 c h);
+ --sf-color-base-700: oklch(from var(--sf-color-base) 0.27 c h);
+ --sf-color-base-800: oklch(from var(--sf-color-base) 0.17 c h);
+ --sf-color-base-900: oklch(from var(--sf-color-base) 0.11 c h);
+ --sf-color-base-950: oklch(from var(--sf-color-base) 0.06 c h);
--sf-color-base-a5: oklch(from var(--sf-color-base) l c h / 0.05);
--sf-color-base-a10: oklch(from var(--sf-color-base) l c h / 0.10);
@@ -891,14 +878,13 @@
--sf-color-base-a50: oklch(from var(--sf-color-base) l c h / 0.50);
--sf-color-base-a80: oklch(from var(--sf-color-base) l c h / 0.80);
- --sf-color-base-superlight: var(--sf-color-base-50);
- --sf-color-base-xlight: var(--sf-color-base-200);
- --sf-color-base-lighter: var(--sf-color-base-400);
- --sf-color-base-darker: var(--sf-color-base-600);
- --sf-color-base-xdark: var(--sf-color-base-800);
- --sf-color-base-superdark: var(--sf-color-base-950);
- --sf-color-base--hover: var(--sf-color-base-darker);
- --sf-color-base--active: var(--sf-color-base-xdark);
+ /* No relative aliases (lighter/darker/xlight/…) on base: it is an
+ absolute scale, so a step's name would lie depending on where the
+ source sits (e.g. "lighter" would be darker than a near-white base
+ in light mode). For relative, mode-adaptive surface elevation use
+ --sf-color-bg / --sf-color-inset / --sf-color-raised instead. */
+ --sf-color-base--hover: oklch(from var(--sf-color-base) clamp(0.01, calc(l - 0.04 + var(--sf-is-dark) * 0.08), 0.99) c h);
+ --sf-color-base--active: oklch(from var(--sf-color-base) clamp(0.01, calc(l - 0.08 + var(--sf-is-dark) * 0.16), 0.99) c h);
--sf-color-base-subtle: var(--sf-color-base-a10);
--sf-color-base-muted: var(--sf-color-base-a30);
--sf-color-base-ghost: var(--sf-color-base-a5);
diff --git a/SLASHED-for-WP/admin-app/src/App.svelte b/SLASHED-for-WP/admin-app/src/App.svelte
index 9fed0039..e855442f 100644
--- a/SLASHED-for-WP/admin-app/src/App.svelte
+++ b/SLASHED-for-WP/admin-app/src/App.svelte
@@ -1,6 +1,7 @@
-
-
-
{label}
-
- {#each presets as p (p.id)}
- onApply(p.patch)}
- class={`px-3 py-1.5 rounded-lg text-[11px] font-bold border transition-all cursor-pointer ${
- current === p.id
- ? "bg-indigo-600/20 border-indigo-500/50 text-indigo-300"
- : "bg-white/4 border-white/8 text-slate-400 hover:text-slate-200 hover:bg-white/8"
- }`}
- >
- {p.label}
-
- {/each}
-
-
diff --git a/SLASHED-for-WP/admin-app/src/components/inputs/TokenRow.svelte b/SLASHED-for-WP/admin-app/src/components/inputs/TokenRow.svelte
index fb90767d..9807e872 100644
--- a/SLASHED-for-WP/admin-app/src/components/inputs/TokenRow.svelte
+++ b/SLASHED-for-WP/admin-app/src/components/inputs/TokenRow.svelte
@@ -39,7 +39,7 @@
{#if type === "color"}
{:else}
diff --git a/SLASHED-for-WP/admin-app/src/components/panels/BordersPanel.svelte b/SLASHED-for-WP/admin-app/src/components/panels/BordersPanel.svelte
index 292efcb9..67be49da 100644
--- a/SLASHED-for-WP/admin-app/src/components/panels/BordersPanel.svelte
+++ b/SLASHED-for-WP/admin-app/src/components/panels/BordersPanel.svelte
@@ -1,16 +1,13 @@
@@ -34,7 +76,7 @@
{#if overridesCount > 0}
You have {overridesCount} active override{overridesCount !== 1 ? "s" : ""} .
{:else}
- Start by picking a theme preset below, or dive into any domain.
+ Pick any domain below to get started.
{/if}
{#if overridesCount > 0}
@@ -47,57 +89,99 @@
{/if}
-
+
-
Theme presets
-
- {#each THEME_PRESETS.slice(0, 6) as theme (theme.id)}
+
Token domains
+
+ {#each TOKEN_DOMAINS as d (d.id)}
+ {@const count = domainCount(d.id)}
+ {@const DomainIcon = d.icon}
onApplyTheme(theme)}
- class="flex items-start gap-2 p-3 rounded-xl bg-white/4 border border-white/8 hover:bg-white/8 hover:border-white/14 transition-all cursor-pointer text-left group"
+ onclick={() => onSelect(d.id)}
+ class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/6 transition-colors cursor-pointer group text-left"
>
- {theme.icon}
-
-
{theme.name}
-
{theme.blurb}
+
+
+
+ {#if count > 0}
+
{count}
+ {/if}
+
→
{/each}
- onSelect("themes")}
- class="mt-2 w-full text-[10px] text-slate-500 hover:text-indigo-400 transition-colors cursor-pointer text-center py-1"
- >
- View all themes →
-
-
+
-
Token domains
+
Tools
- {#each DOMAIN_CARDS as d (d.id)}
- {@const domainOverrides = Object.keys(overrides).filter((k) => k.includes(d.id.slice(0, 5))).length}
- {@const DomainIcon = d.icon}
+ {#each TOOLS as t (t.id)}
+ {@const ToolIcon = t.icon}
onSelect(d.id)}
+ onclick={() => onSelect(t.id)}
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/6 transition-colors cursor-pointer group text-left"
>
-
+
-
{d.label}
-
{d.desc}
+
{t.label}
+
{t.desc}
- {#if domainOverrides > 0}
-
- {domainOverrides}
-
- {/if}
→
{/each}
+
+
+
+
+
Saved themes
+
onSelect("themes")}
+ class="text-[10px] text-slate-500 hover:text-indigo-400 transition-colors cursor-pointer"
+ >Manage →
+
+
+
+ { if (e.key === "Enter") handleQuickSave(); }}
+ class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded-lg px-2 py-1.5 text-[11px] text-slate-200 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
+ />
+
+
+
+ {#if savedThemes.length === 0}
+
No saved themes yet. Configure tokens, then save.
+ {:else}
+
+ {#each savedThemes.slice(0, 6) as theme (theme.id)}
+
onApplyTheme(theme.overrides)}
+ class="flex items-start gap-2 p-3 rounded-xl bg-white/4 border border-white/8 hover:bg-white/8 hover:border-white/14 transition-all cursor-pointer text-left group"
+ >
+ {theme.icon}
+
+
{theme.name}
+
{theme.blurb}
+
+
+ {/each}
+
+ {/if}
+
diff --git a/SLASHED-for-WP/admin-app/src/components/panels/MacrosPanel.svelte b/SLASHED-for-WP/admin-app/src/components/panels/MacrosPanel.svelte
index c656af51..62203918 100644
--- a/SLASHED-for-WP/admin-app/src/components/panels/MacrosPanel.svelte
+++ b/SLASHED-for-WP/admin-app/src/components/panels/MacrosPanel.svelte
@@ -35,13 +35,13 @@
// Prose spacing knobs. Defaults reference space tokens — the numbers below are
// the approximate resolved rem values, used only as the slider's idle position.
const PROSE_SPACE = [
- { label: "Heading gap", token: "--sf-prose-heading-gap", def: 0.75, max: 3 },
- { label: "List gap", token: "--sf-prose-list-gap", def: 0.5, max: 2 },
- { label: "Block margin", token: "--sf-prose-block-margin", def: 1, max: 4 },
- { label: "Media margin", token: "--sf-prose-media-margin", def: 1, max: 4 },
- { label: "Figure margin", token: "--sf-prose-figure-margin", def: 1.5, max: 4 },
- { label: "Blockquote padding",token: "--sf-prose-blockquote-padding",def: 1, max: 3 },
- { label: "HR margin", token: "--sf-prose-hr-margin", def: 1.5, max: 4 },
+ { label: "Heading gap", token: "--sf-prose-heading-gap", def: 0.75, max: 3, raw: "var(--sf-space-s)" },
+ { label: "List gap", token: "--sf-prose-list-gap", def: 0.5, max: 2, raw: "var(--sf-space-xs)" },
+ { label: "Block margin", token: "--sf-prose-block-margin", def: 1, max: 4, raw: "var(--sf-space-m)" },
+ { label: "Media margin", token: "--sf-prose-media-margin", def: 1, max: 4, raw: "var(--sf-space-m)" },
+ { label: "Figure margin", token: "--sf-prose-figure-margin", def: 1.5, max: 4, raw: "var(--sf-space-l)" },
+ { label: "Blockquote padding",token: "--sf-prose-blockquote-padding",def: 1, max: 3, raw: "var(--sf-space-m)" },
+ { label: "HR margin", token: "--sf-prose-hr-margin", def: 1.5, max: 4, raw: "var(--sf-space-l)" },
];
let showFlow = $state(false);
@@ -94,6 +94,9 @@
overridden={"--sf-flow-space" in overrides}
onChange={(v) => onSet("--sf-flow-space", `${v}rem`)}
onReset={() => onReset("--sf-flow-space")}
+ rawDefault="var(--sf-content-gap)"
+ currentRaw={overrides["--sf-flow-space"]}
+ onRawSet={(v) => onSet("--sf-flow-space", v)}
/>
{#each [0, 1, 2] as i (i)}
@@ -191,6 +194,9 @@
overridden={"--sf-scroll-shadow-size" in overrides}
onChange={(v) => onSet("--sf-scroll-shadow-size", `${v}rem`)}
onReset={() => onReset("--sf-scroll-shadow-size")}
+ rawDefault="2rem"
+ currentRaw={overrides["--sf-scroll-shadow-size"]}
+ onRawSet={(v) => onSet("--sf-scroll-shadow-size", v)}
/>
onSet(t.token, `${v}rem`)}
onReset={() => onReset(t.token)}
+ rawDefault={t.raw}
+ currentRaw={overrides[t.token]}
+ onRawSet={(v) => onSet(t.token, v)}
/>
{/each}
onSet("--sf-prose-media-radius", `${v}px`)}
onReset={() => onReset("--sf-prose-media-radius")}
+ rawDefault="var(--sf-radius-m)"
+ currentRaw={overrides["--sf-prose-media-radius"]}
+ onRawSet={(v) => onSet("--sf-prose-media-radius", v)}
/>
Marker color
diff --git a/SLASHED-for-WP/admin-app/src/components/panels/MotionPanel.svelte b/SLASHED-for-WP/admin-app/src/components/panels/MotionPanel.svelte
index ccdffd5a..1157fea3 100644
--- a/SLASHED-for-WP/admin-app/src/components/panels/MotionPanel.svelte
+++ b/SLASHED-for-WP/admin-app/src/components/panels/MotionPanel.svelte
@@ -19,54 +19,59 @@
];
const EASINGS = [
- { label: "Linear", token: "--sf-ease-linear", value: "linear", preview: "M0,40 L40,0" },
- { label: "Ease out", token: "--sf-ease-out", value: "cubic-bezier(0.25, 0, 0.15, 1)", preview: "M0,40 C8,40 28,0 40,0" },
- { label: "Ease in", token: "--sf-ease-in", value: "cubic-bezier(0.5, 0, 0.75, 0.25)", preview: "M0,40 C12,40 32,32 40,0" },
- { label: "Ease in-out",token: "--sf-ease-in-out", value: "cubic-bezier(0.4, 0, 0.2, 1)", preview: "M0,40 C8,40 32,0 40,0" },
- { label: "Spring", token: "--sf-ease-spring", value: "linear(0, 0.5, 1.1, 0.95, 1.02, 1)", preview: "M0,40 C10,20 20,-4 28,-2 S38,2 40,0" },
+ { label: "Linear", token: "--sf-ease-linear", value: "linear", preview: "M0,40 L40,0" },
+ { label: "Ease out", token: "--sf-ease-out", value: "cubic-bezier(0.25, 0, 0.15, 1)", preview: "M0,40 C8,40 28,0 40,0" },
+ { label: "Ease in", token: "--sf-ease-in", value: "cubic-bezier(0.5, 0, 0.75, 0.25)", preview: "M0,40 C12,40 32,32 40,0" },
+ { label: "Ease in-out",token: "--sf-ease-in-out", value: "cubic-bezier(0.4, 0, 0.2, 1)", preview: "M0,40 C8,40 32,0 40,0" },
+ { label: "Spring", token: "--sf-ease-spring", value: "linear(0, 0.5, 1.1, 0.95, 1.02, 1)", preview: "M0,40 C10,20 20,-4 28,-2 S38,2 40,0" },
{ label: "Bounce", token: "--sf-ease-bounce", value: "linear(0, 0.35 18%, 1 32%, 0.86 42%, 1.02 56%, 0.98 72%, 1)", preview: "M0,40 L12,16 L20,0 L26,6 L32,-1 L36,1 L40,0" },
- { label: "Elastic", token: "--sf-ease-elastic", value: "linear(0, 0.3, 1.2, 0.9, 1.05, 1)", preview: "M0,40 C8,40 16,-10 24,-2 S36,2 40,0" },
- { label: "Overshoot", token: "--sf-ease-overshoot", value: "cubic-bezier(0.34, 1.56, 0.64, 1)", preview: "M0,40 C8,40 20,-10 40,0" },
+ { label: "Elastic", token: "--sf-ease-elastic", value: "linear(0, 0.3, 1.2, 0.9, 1.05, 1)", preview: "M0,40 C8,40 16,-10 24,-2 S36,2 40,0" },
+ { label: "Overshoot", token: "--sf-ease-overshoot", value: "linear(0, 0.6 30%, 1.08 55%, 0.98 75%, 1)", preview: "M0,40 C8,40 20,-10 40,0" },
];
- // animation-delay-N base is 75ms * N; stagger base is 75ms
const STAGGER_TOKENS = ["--sf-animation-delay-1","--sf-animation-delay-2","--sf-animation-delay-3","--sf-animation-delay-4","--sf-animation-delay-5"];
const knobs = KNOBS_BY_DOMAIN["motion"] ?? [];
- let scale = $derived(parseFloat(overrides["--sf-motion-scale"] ?? "1"));
+ let scale = $derived((() => { const v = parseFloat(overrides["--sf-motion-scale"] ?? "1"); return isFinite(v) ? v : 1; })());
let motionDisabled = $derived(overrides["--sf-motion-scale"] === "0");
- let themeTransition = $derived(parseFloat(overrides["--sf-theme-transition-duration"]?.replace("ms","") ?? String(300 * scale)));
- let staggerBase = $derived(() => {
+ let themeTransition = $derived((() => { const v = parseFloat(overrides["--sf-theme-transition-duration"]?.replace("ms","") ?? String(300 * scale)); return isFinite(v) ? v : Math.round(300 * scale); })());
+ let staggerBase = $derived.by(() => {
const raw = overrides[STAGGER_TOKENS[0]];
if (raw) return parseFloat(raw.replace("ms",""));
return 75 * scale;
});
- // Animation demo state
+ // Animation demo — user picks which easing + duration to feel.
let animating = $state(false);
let animOffsetX = $state(0);
+ let demoEase = $state("--sf-ease-out");
+ let demoDuration = $state("--sf-duration-normal");
- let showGlobalScale = $state(false);
- let showThemeTransition = $state(false);
- let showDurationOverrides = $state(false);
- let showStaggerBase = $state(false);
- let showEasingCurves = $state(false);
- let showAnimationDemo = $state(false);
- let showDurationPreview = $state(false);
- let showScrollTimeline = $state(false);
+ let showDurations = $state(false);
+ let showEasing = $state(false);
+ let showStagger = $state(false);
+ let showAdvanced = $state(false);
function getDuration(token: string, base: number): number {
const raw = overrides[token];
- if (raw) return parseFloat(raw);
+ if (raw) {
+ const parsed = parseFloat(raw);
+ if (isFinite(parsed)) return parsed;
+ }
return Math.round(base * scale);
}
+ function easeValue(token: string): string {
+ return overrides[token] ?? EASINGS.find((e) => e.token === token)?.value ?? "ease";
+ }
+
function playDemo() {
if (animating) return;
+ const base = DURATIONS.find((d) => d.token === demoDuration)?.base ?? 250;
+ const dur = getDuration(demoDuration, base);
animating = true;
animOffsetX = 180;
- const dur = getDuration("--sf-duration-normal", 250);
setTimeout(() => {
animOffsetX = 0;
setTimeout(() => { animating = false; }, dur);
@@ -75,9 +80,7 @@
function setStaggerBase(baseMs: number) {
const patch: Record
= {};
- for (let i = 1; i <= 5; i++) {
- patch[`--sf-animation-delay-${i}`] = `${Math.round(baseMs * i)}ms`;
- }
+ for (let i = 1; i <= 5; i++) patch[`--sf-animation-delay-${i}`] = `${Math.round(baseMs * i)}ms`;
onBulkChange(patch);
}
@@ -114,71 +117,66 @@
{#if motionDisabled}
-
Motion is disabled — duration sliders have no effect while scale is 0.
+
Motion is disabled — duration & easing have no effect while scale is 0.
{/if}
-
-
-
{ showGlobalScale = !showGlobalScale; }}
- aria-expanded={showGlobalScale}
- class="w-full flex items-center justify-between cursor-pointer"
- >
- Global scale
- {showGlobalScale ? "▲" : "▼"}
-
- {#if showGlobalScale}
-
- {#each knobs as k (k.name)}
-
val === null ? onReset(name) : onSet(name, val)}
- />
- {/each}
-
- {/if}
-
-
-
-
-
+
- { showThemeTransition = !showThemeTransition; }}
- aria-expanded={showThemeTransition}
- class="w-full flex items-center justify-between cursor-pointer"
- >
- Theme transition
- {showThemeTransition ? "▲" : "▼"}
-
- {#if showThemeTransition}
- onSet("--sf-theme-transition-duration", `${v}ms`)}
- onReset={() => onReset("--sf-theme-transition-duration")}
- />
- {/if}
+ Live preview
+
+
+
d.token === demoDuration)?.base ?? 250)}ms ${easeValue(demoEase)}`}
+ >
+
+
+ { demoEase = (e.target as HTMLSelectElement).value; }}
+ class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded-lg px-2 py-1.5 text-[10px] text-slate-200 focus:outline-none focus:border-indigo-500 cursor-pointer"
+ >
+ {#each EASINGS as e (e.token)}
+ {e.label}
+ {/each}
+
+ { demoDuration = (e.target as HTMLSelectElement).value; }}
+ class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded-lg px-2 py-1.5 text-[10px] text-slate-200 focus:outline-none focus:border-indigo-500 cursor-pointer"
+ >
+ {#each DURATIONS as d (d.token)}
+ {d.label}
+ {/each}
+
+ {animating ? "…" : "▶ Play"}
+
+
-
-
+
+
{ showDurationOverrides = !showDurationOverrides; }}
- aria-expanded={showDurationOverrides}
+ onclick={() => { showDurations = !showDurations; }}
+ aria-expanded={showDurations}
class="w-full flex items-center justify-between cursor-pointer"
>
- Duration overrides
- {showDurationOverrides ? "▲" : "▼"}
+ Durations
+ {showDurations ? "▲" : "▼"}
- {#if showDurationOverrides}
+ {#if showDurations}
- Set absolute ms values to override the fluid scale calculation.
+ Named duration tokens. Drag to set an absolute ms value, overriding the global scale.
{#if motionDisabled} (No effect while motion is disabled.) {/if}
{#each DURATIONS as d (d.token)}
@@ -195,22 +193,94 @@
onReset={() => onReset(d.token)}
/>
{/each}
+
+
+ {#each DURATIONS as d (d.label)}
+ {@const actual = getDuration(d.token, d.base)}
+
+
{d.label}
+
+
{actual}ms
+
+ {/each}
+
+ {/if}
+
+
+
+
+
+
+ { showEasing = !showEasing; }}
+ aria-expanded={showEasing}
+ class="w-full flex items-center justify-between cursor-pointer"
+ >
+ Easing curves
+ {showEasing ? "▲" : "▼"}
+
+ {#if showEasing}
+
+ Edit the named easing tokens. Accepts any CSS timing function —
+ cubic-bezier(…) or
+ linear(…) . Use “Live preview” above to feel a curve.
+
+
+ {#each EASINGS as e (e.token)}
+ {@const isOverridden = e.token in overrides}
+
+
+
+
+
+
+
+
{e.label}
+
+ { demoEase = e.token; playDemo(); }}
+ class="text-[8px] text-slate-500 hover:text-indigo-400 cursor-pointer"
+ >preview
+ {#if isOverridden}
+ onReset(e.token)} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer">reset
+ {/if}
+
+
+
{
+ const v = (ev.target as HTMLInputElement).value.trim();
+ v ? onSet(e.token, v) : onReset(e.token);
+ }}
+ class="w-full bg-white/5 border border-white/10 rounded px-1.5 py-1 text-[9px] font-mono text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
+ />
+
+
+
+ {/each}
+
{/if}
-
+
{ showStaggerBase = !showStaggerBase; }}
- aria-expanded={showStaggerBase}
+ onclick={() => { showStagger = !showStagger; }}
+ aria-expanded={showStagger}
class="w-full flex items-center justify-between cursor-pointer"
>
- Stagger base
- {showStaggerBase ? "▲" : "▼"}
+ Stagger
+ {showStagger ? "▲" : "▼"}
- {#if showStaggerBase}
+ {#if showStagger}
One slider sets all five stagger delays (delay-1 through delay-5 are multiples of this base).
@@ -222,17 +292,16 @@
{/if}
t in overrides)}
onChange={(v) => setStaggerBase(v)}
onReset={resetStagger}
/>
-
{#each [1,2,3,4,5] as n (n)}
- {@const delayMs = Math.round(staggerBase() * n)}
+ {@const delayMs = Math.round(staggerBase * n)}
–{n}
@@ -247,146 +316,65 @@
-
-
+
+
{ showEasingCurves = !showEasingCurves; }}
- aria-expanded={showEasingCurves}
- class="w-full flex items-center justify-between cursor-pointer"
+ onclick={() => { showAdvanced = !showAdvanced; }}
+ aria-expanded={showAdvanced}
+ class="w-full flex items-center justify-between text-slate-600 hover:text-slate-400 transition-colors cursor-pointer"
>
- Easing curves
- {showEasingCurves ? "▲" : "▼"}
+ Advanced
+ {showAdvanced ? "▲" : "▼"}
- {#if showEasingCurves}
-
- Customize the named easing tokens used throughout the design system.
-
-
- {#each EASINGS as e (e.token)}
- {@const isOverridden = e.token in overrides}
-
-
- {e.label}
- {#if isOverridden}
- onReset(e.token)}
- class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer"
- >reset
- {/if}
-
-
-
-
-
{e.token.replace("--sf-ease-", "")}
-
+ {#if showAdvanced}
+
+
+ {#each knobs as k (k.name)}
+
val === null ? onReset(name) : onSet(name, val)}
+ />
{/each}
- {/if}
-
-
-
-
-
-
- { showAnimationDemo = !showAnimationDemo; }}
- aria-expanded={showAnimationDemo}
- class="w-full flex items-center justify-between cursor-pointer"
- >
- Animation demo
- {showAnimationDemo ? "▲" : "▼"}
-
- {#if showAnimationDemo}
-
-
-
- {animating ? "Animating…" : "▶ Play"}
-
-
- {/if}
-
-
-
-
-
- { showScrollTimeline = !showScrollTimeline; }}
- aria-expanded={showScrollTimeline}
- class="w-full flex items-center justify-between cursor-pointer"
- >
- Scroll timeline range
- {showScrollTimeline ? "▲" : "▼"}
-
- {#if showScrollTimeline}
-
- Default animation-range for scroll-driven animations using the .sf-scroll-timeline utility.
-
- {#each [
- { label: "Range start", token: "--sf-scroll-timeline-range-start", placeholder: "entry 0%" },
- { label: "Range end", token: "--sf-scroll-timeline-range-end", placeholder: "cover 30%" },
- ] as r (r.token)}
-
-
{r.label}
-
{
- const v = (e.target as HTMLInputElement).value.trim();
- v ? onSet(r.token, v) : onReset(r.token);
- }}
- class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded px-1.5 py-1 text-[9px] font-mono text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
- />
- {#if r.token in overrides}
-
onReset(r.token)} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer shrink-0">reset
- {/if}
-
- {/each}
- {/if}
-
-
-
+
+
onSet("--sf-theme-transition-duration", `${v}ms`)}
+ onReset={() => onReset("--sf-theme-transition-duration")}
+ />
-
-
-
{ showDurationPreview = !showDurationPreview; }}
- aria-expanded={showDurationPreview}
- class="w-full flex items-center justify-between cursor-pointer mb-2"
- >
- Duration preview
- {showDurationPreview ? "▲" : "▼"}
-
- {#if showDurationPreview}
-
- {#each DURATIONS as d (d.label)}
- {@const actual = getDuration(d.token, d.base)}
-
-
{d.label}
-
-
{actual}ms
+
+
+
+ Default animation-range for scroll-driven animations using the
+ .sf-scroll-timeline utility.
+
+ {#each [
+ { label: "Range start", token: "--sf-scroll-timeline-range-start", placeholder: "entry 0%" },
+ { label: "Range end", token: "--sf-scroll-timeline-range-end", placeholder: "cover 30%" },
+ ] as r (r.token)}
+
+
{r.label}
+
{
+ const v = (e.target as HTMLInputElement).value.trim();
+ v ? onSet(r.token, v) : onReset(r.token);
+ }}
+ class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded px-1.5 py-1 text-[9px] font-mono text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
+ />
+ {#if r.token in overrides}
+
onReset(r.token)} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer shrink-0">reset
+ {/if}
{/each}
{/if}
-
+
diff --git a/SLASHED-for-WP/admin-app/src/components/panels/ShadowsPanel.svelte b/SLASHED-for-WP/admin-app/src/components/panels/ShadowsPanel.svelte
index 0f3da6ad..145d62bf 100644
--- a/SLASHED-for-WP/admin-app/src/components/panels/ShadowsPanel.svelte
+++ b/SLASHED-for-WP/admin-app/src/components/panels/ShadowsPanel.svelte
@@ -1,75 +1,54 @@
-
-
+
+
+ {#each SHADOW_STEPS as step, i (step)}
+
+ {/each}
+
-
-
- { showShadowAppearance = !showShadowAppearance; }}
- aria-expanded={showShadowAppearance}
- class="w-full flex items-center justify-between cursor-pointer"
- >
- Shadow appearance
- {showShadowAppearance ? "▲" : "▼"}
-
- {#if showShadowAppearance}
-
- {#each knobs as k (k.name)}
-
val === null ? onReset(name) : onSet(name, val)}
- />
- {/each}
-
-
- onSet("--sf-shadow-lightness", String(v))}
- onReset={() => onReset("--sf-shadow-lightness")}
- />
- {/if}
-
+
+
onSet("--sf-shadow-lightness", String(v))}
+ onReset={() => onReset("--sf-shadow-lightness")}
+ />
@@ -148,33 +127,42 @@
Which brand color radiates the glow. Defaults to primary.
{/if}
+
+
+
+ {#if glowDisabled}
+
Glow disabled.
+ {/if}
{/if}
-
-
+
+
{ showElevationPreview = !showElevationPreview; }}
- aria-expanded={showElevationPreview}
- class="w-full flex items-center justify-between cursor-pointer mb-2"
+ onclick={() => { showAdvanced = !showAdvanced; }}
+ aria-expanded={showAdvanced}
+ class="w-full flex items-center justify-between text-slate-600 hover:text-slate-400 transition-colors cursor-pointer"
>
- Elevation preview
- {showElevationPreview ? "▲" : "▼"}
+ Advanced
+ {showAdvanced ? "▲" : "▼"}
- {#if showElevationPreview}
-
- {#each SHADOW_STEPS as step, i (step)}
-
+ {#if showAdvanced}
+
+ {#each knobs as k (k.name)}
+
val === null ? onReset(name) : onSet(name, val)}
+ />
{/each}
{/if}
-
+
diff --git a/SLASHED-for-WP/admin-app/src/components/panels/SpacingPanel.svelte b/SLASHED-for-WP/admin-app/src/components/panels/SpacingPanel.svelte
index 8a2945f1..1f988581 100644
--- a/SLASHED-for-WP/admin-app/src/components/panels/SpacingPanel.svelte
+++ b/SLASHED-for-WP/admin-app/src/components/panels/SpacingPanel.svelte
@@ -5,12 +5,11 @@
import SliderRow from '../inputs/SliderRow.svelte';
import ClampField from '../inputs/ClampField.svelte';
- let { overrides, onSet, onReset, onBulkChange }: {
+ let { overrides, onSet, onReset }: {
tokens: SlashedToken[];
overrides: Record
;
onSet: (name: string, value: string) => void;
onReset: (name: string) => void;
- onBulkChange: (patch: Record) => void;
} = $props();
const RATIO_PRESETS = [
@@ -21,12 +20,6 @@
{ label: "Golden — 1.618", value: 1.618 },
];
- const DENSITY_PRESETS = [
- { label: "Compact", desc: "Tight UI, data-heavy", patch: { "--sf-space-scale": "0.75", "--sf-section-scale": "0.7", "--sf-space-base-min": "0.75", "--sf-space-base-max": "1.25" } },
- { label: "Comfortable", desc: "Balanced — default", patch: { "--sf-space-scale": null as null, "--sf-section-scale": null as null, "--sf-space-base-min": null as null, "--sf-space-base-max": null as null } },
- { label: "Spacious", desc: "Generous, editorial", patch: { "--sf-space-scale": "1.3", "--sf-section-scale": "1.4", "--sf-space-base-min": "1.25", "--sf-space-base-max": "2.75" } },
- ];
-
const SPACE_STEPS = ["2xs", "xs", "s", "m", "l", "xl", "2xl", "3xl", "4xl"];
const knobs = KNOBS_BY_DOMAIN["spacing"] ?? [];
@@ -48,20 +41,35 @@
let vwMin = $derived(num("--sf-fluid-min-vw", 22.5));
let vwMax = $derived(num("--sf-fluid-max-vw", 90));
- let activeDensity = $derived(DENSITY_PRESETS.find((d) =>
- Object.entries(d.patch).every(([k, v]) =>
- v === null ? !(k in overrides) : overrides[k] === v
- )
- ));
-
let showLayoutGap = $state(false);
- let showDensityPresets = $state(false);
let showModularScale = $state(false);
- let showSpacePreview = $state(false);
+ let showAdvanced = $state(false);
+
+
+ Space scale preview
+
+ {#each SPACE_STEPS as step, i (step)}
+ {@const midBase = (baseMin + baseMax) / 2}
+ {@const ratio = (ratioMin + ratioMax) / 2}
+ {@const offset = i - 3}
+ {@const rawRem = offset >= 0 ? midBase * Math.pow(ratio, offset) : midBase / Math.pow(ratio, -offset)}
+ {@const scaled = rawRem * spaceScale}
+ {@const barWidth = Math.min(scaled * 28, 240)}
+
+
{step}
+
+
{scaled.toFixed(2)}rem
+
+ {/each}
+
+
+
+
+
-
-
- { showDensityPresets = !showDensityPresets; }}
- aria-expanded={showDensityPresets}
- class="w-full flex items-center justify-between cursor-pointer"
- >
- Density presets
- {showDensityPresets ? "▲" : "▼"}
-
- {#if showDensityPresets}
-
- {#each DENSITY_PRESETS as d (d.label)}
- onBulkChange(d.patch as Record)}
- class={`flex flex-col items-center gap-1 p-3 rounded-xl border transition-all cursor-pointer ${
- activeDensity?.label === d.label
- ? "bg-indigo-500/15 border-indigo-500/40 text-indigo-200"
- : "border-white/8 text-slate-400 hover:bg-white/5 hover:text-slate-200"
- }`}
- >
- {d.label}
- {d.desc}
-
- {/each}
-
- {/if}
-
-
-
-
onSet("--sf-space-ratio-min", String(v))}
onRatioMaxChange={(v) => onSet("--sf-space-ratio-max", String(v))}
/>
-
-
- {#each knobs as k (k.name)}
-
val === null ? onReset(name) : onSet(name, val)}
- />
- {/each}
-
{/if}
-
-
+
+
{ showSpacePreview = !showSpacePreview; }}
- aria-expanded={showSpacePreview}
- class="w-full flex items-center justify-between cursor-pointer mb-2"
+ onclick={() => { showAdvanced = !showAdvanced; }}
+ aria-expanded={showAdvanced}
+ class="w-full flex items-center justify-between text-slate-600 hover:text-slate-400 transition-colors cursor-pointer"
>
- Space scale preview
- {showSpacePreview ? "▲" : "▼"}
+ Advanced
+ {showAdvanced ? "▲" : "▼"}
- {#if showSpacePreview}
-
- {#each SPACE_STEPS as step, i (step)}
- {@const midBase = (baseMin + baseMax) / 2}
- {@const ratio = (ratioMin + ratioMax) / 2}
- {@const offset = i - 4}
- {@const rawRem = offset >= 0 ? midBase * Math.pow(ratio, offset) : midBase / Math.pow(ratio, -offset)}
- {@const scaled = rawRem * spaceScale}
- {@const barWidth = Math.min(scaled * 28, 240)}
-
-
{step}
-
-
{scaled.toFixed(2)}rem
-
+ {#if showAdvanced}
+
+ {#each knobs as k (k.name)}
+
val === null ? onReset(name) : onSet(name, val)}
+ />
{/each}
{/if}
diff --git a/SLASHED-for-WP/admin-app/src/components/panels/ThemesPanel.svelte b/SLASHED-for-WP/admin-app/src/components/panels/ThemesPanel.svelte
index ae9c4548..f972e078 100644
--- a/SLASHED-for-WP/admin-app/src/components/panels/ThemesPanel.svelte
+++ b/SLASHED-for-WP/admin-app/src/components/panels/ThemesPanel.svelte
@@ -1,71 +1,139 @@
-
Theme presets
+
Themes
- Apply a curated set of overrides with one click. Existing overrides outside the theme's scope are preserved.
+ Save your current configuration as a named theme, then re-apply it any time.
+ Themes are stored in this browser.
-
- {#each THEME_PRESETS as theme (theme.id)}
- {@const tokenCount = Object.keys(theme.overrides).length}
-
-
-
{theme.icon}
-
-
{theme.name}
-
{theme.blurb}
-
- {#if theme.tags}
- {#each theme.tags as tag (tag)}
- {tag}
- {/each}
- {/if}
- {#if tokenCount > 0}
- {tokenCount} overrides
- {/if}
-
-
-
onApplyTheme(theme)}
- class={`px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all cursor-pointer shrink-0 ${
- theme.id === "default"
- ? "border border-rose-500/30 text-rose-400 hover:bg-rose-500/10"
- : "bg-indigo-600/20 border border-indigo-500/30 text-indigo-300 hover:bg-indigo-600/30"
- }`}
- >
- {theme.id === "default" ? "Reset all" : "Apply"}
-
-
+
+
+
Save current
+
+ { if (e.key === "Enter") handleSave(); }}
+ class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded-lg px-2 py-1.5 text-[11px] text-slate-200 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
+ />
+
+
+ Save
+
+
+
+ {#if overridesCount === 0}
+ No active overrides yet — adjust some tokens first.
+ {:else}
+ Captures all {overridesCount} active override{overridesCount === 1 ? "" : "s"}.
+ {/if}
+
+
- {#if tokenCount > 0}
-
- {#each Object.entries(theme.overrides).slice(0, 5) as [k, v] (k)}
-
- {#if k.includes("color") || k.includes("source")}
-
+
+
+ {#if themes.length === 0}
+
+
No saved themes yet.
+
Configure your tokens, then save them above.
+
+ {:else}
+ {#each themes as theme (theme.id)}
+ {@const tokenCount = Object.keys(theme.overrides).length}
+
+
+
{theme.icon}
+
+
+ {theme.name}
+ {#if activeId === theme.id}
+
{/if}
- {k.replace("--sf-", "")}
- {/each}
- {#if tokenCount > 5}
-
+{tokenCount - 5} more
- {/if}
+
{tokenCount} override{tokenCount === 1 ? "" : "s"}
+
+
+ onApplyTheme(theme.overrides)}
+ class="px-3 py-1.5 rounded-lg text-[11px] font-bold bg-indigo-600/20 border border-indigo-500/30 text-indigo-300 hover:bg-indigo-600/30 transition-all cursor-pointer"
+ >Apply
+ handleDelete(theme.id)}
+ class="p-1.5 rounded-lg border border-white/8 text-slate-500 hover:text-rose-400 hover:border-rose-500/30 transition-all cursor-pointer opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 focus:opacity-100"
+ >
+
- {/if}
-
- {/each}
+
+ {#if tokenCount > 0}
+
+ {#each Object.entries(theme.overrides).slice(0, 5) as [k, v] (k)}
+
+ {#if k.includes("color") || k.includes("source")}
+
+ {/if}
+
{k.replace("--sf-", "")}
+
+ {/each}
+ {#if tokenCount > 5}
+
+{tokenCount - 5} more
+ {/if}
+
+ {/if}
+
+ {/each}
+ {/if}
+
+
+ {#if overridesCount > 0}
+
Reset all overrides
+ {/if}
diff --git a/SLASHED-for-WP/admin-app/src/components/panels/TypographyPanel.svelte b/SLASHED-for-WP/admin-app/src/components/panels/TypographyPanel.svelte
index 8d1fe2da..81870107 100644
--- a/SLASHED-for-WP/admin-app/src/components/panels/TypographyPanel.svelte
+++ b/SLASHED-for-WP/admin-app/src/components/panels/TypographyPanel.svelte
@@ -66,7 +66,7 @@
{ label: "normal", value: "normal" },
];
- const WEIGHT_OPTIONS = ["300","400","500","600","700","800"];
+ const WEIGHT_OPTIONS = ["100","200","300","400","500","600","700","800","900"];
const WEIGHT_DEFAULTS: Record
= {
"--sf-font-weight-body": "400",
"--sf-font-weight-heading": "600",
@@ -166,6 +166,7 @@
let showBodyText = $state(false);
let showDisplayType = $state(false);
let showModularScale = $state(false);
+ let showScaleAdvanced = $state(false);
let showScalePreview = $state(false);
let showAdvancedType = $state(false);
let showLineLengths = $state(false);
@@ -195,7 +196,7 @@
const link = document.createElement("link");
link.id = id;
link.rel = "stylesheet";
- link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontName)}:wght@300;400;500;600;700&display=swap`;
+ link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontName)}:wght@100;200;300;400;500;600;700;800;900&display=swap`;
document.head.appendChild(link);
}
}
@@ -759,17 +760,6 @@
onRatioMaxChange={(v) => onSet("--sf-text-ratio-max", String(v))}
/>
-
-
- {#each knobs as k (k.name)}
-
val === null ? onReset(name) : onSet(name, val)}
- />
- {/each}
-
-
Line height scale
@@ -818,6 +808,29 @@
{/each}
+
+
+
+
{ showScaleAdvanced = !showScaleAdvanced; }}
+ aria-expanded={showScaleAdvanced}
+ class="w-full flex items-center justify-between text-slate-600 hover:text-slate-400 transition-colors cursor-pointer"
+ >
+ Advanced
+ {showScaleAdvanced ? "▲" : "▼"}
+
+ {#if showScaleAdvanced}
+
+ {#each knobs as k (k.name)}
+
val === null ? onReset(name) : onSet(name, val)}
+ />
+ {/each}
+
+ {/if}
+
{/if}
diff --git a/SLASHED-for-WP/admin-app/src/components/shell/PreviewPanel.svelte b/SLASHED-for-WP/admin-app/src/components/shell/PreviewPanel.svelte
index cba7c2f0..378c0bb1 100644
--- a/SLASHED-for-WP/admin-app/src/components/shell/PreviewPanel.svelte
+++ b/SLASHED-for-WP/admin-app/src/components/shell/PreviewPanel.svelte
@@ -22,16 +22,14 @@
onTemplateChange: (t: PreviewTemplate) => void;
} = $props();
-
function withDerivedOverrides(ov: Record
): Record {
- const derived = computeDerivedOverrides(ov);
+ const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
+ const derived = computeDerivedOverrides(ov, { reduceMotion });
return Object.keys(derived).length > 0 ? { ...derived, ...ov } : ov;
}
const TEMPLATES: { id: PreviewTemplate; label: string }[] = [
{ id: "marketing", label: "Marketing" },
- { id: "docs", label: "Docs" },
- { id: "dashboard", label: "Dashboard" },
{ id: "components", label: "Components" },
{ id: "stylescape", label: "Stylescape" },
];
@@ -83,378 +81,336 @@
}
}
+ // Preview "skin": presentational classes for the demo templates. SLASHED is
+ // BEM-first and ships no utility classes, so the demos lean on real framework
+ // classes (.pv-btn, .pv-card, .sf-container, …) plus this thin skin for the
+ // bits the framework has no class for (token swatch grids, the type-scale
+ // ramp, demo chrome). EVERY value here references a live --sf-* token, so the
+ // configurator panels drive these classes exactly like the framework's own —
+ // and there is not a single inline style left in the template bodies.
+ const RAMP_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
+ const RADII = ["xs", "s", "m", "l", "xl", "2xl", "full"];
+ const SHADOWS = ["xs", "s", "m", "l", "xl", "2xl"];
+ const SPACES = ["3xs", "2xs", "xs", "s", "m", "l", "xl", "2xl", "3xl", "4xl"];
+ const TYPE_STEPS = ["display-l", "display-m", "display-s", "2xl", "xl", "l", "m", "s", "xs"];
+
+ function previewSkinCSS(): string {
+ const ramp = (cls: string, token: string, fallback?: string) =>
+ RAMP_STEPS.map(
+ (s) =>
+ `.pv-sw--${cls}-${s}{background:var(--sf-color-${token}-${s}${
+ fallback ? `,var(--sf-color-${fallback}-${s})` : ""
+ });}`
+ ).join("");
+ return `
+ /* Text roles (core ships no utility classes — these mirror semantic tokens) */
+ .pv-eyebrow{font-size:var(--sf-text-xs);font-weight:var(--sf-font-weight-heading);text-transform:uppercase;letter-spacing:0.08em;color:var(--sf-color-text--muted);}
+ .pv-lead{font-size:var(--sf-text-l);color:var(--sf-color-text--secondary);}
+ .pv-muted{color:var(--sf-color-text--muted);}
+ .pv-secondary{color:var(--sf-color-text--secondary);}
+ .pv-accent{color:var(--sf-color-primary-600);}
+ .pv-on-primary{color:var(--sf-color-text--on-primary);}
+ .pv-center-text{text-align:center;}
+ .pv-emoji{font-size:2rem;line-height:1;}
+ .pv-brand{font-weight:700;font-size:var(--sf-text-l);color:var(--sf-color-primary-600);}
+ .pv-measure{max-inline-size:34rem;}
+ /* BEM components — .sf-btn / .sf-card / .sf-tag / .sf-field are staged off in
+ core until v0.8, so the preview ships its own token-driven equivalents.
+ These mirror the framework's reserved definitions one-to-one. */
+ .pv-btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--sf-space-2xs);padding-block:var(--sf-space-xs);padding-inline:var(--sf-space-m);min-block-size:var(--sf-touch-target);font-family:inherit;font-size:var(--sf-text-m);font-weight:var(--sf-font-weight-interactive);line-height:var(--sf-leading-tight);white-space:nowrap;text-decoration:none;border:var(--sf-border-width-1) solid var(--sf-color-action);border-radius:var(--sf-radius-m);cursor:pointer;background:var(--sf-color-action);color:var(--sf-color-text--on-action);transition:var(--sf-transition-form-field);}
+ .pv-btn--primary{background:var(--sf-color-primary);color:var(--sf-color-text--on-primary);border-color:var(--sf-color-primary);}
+ .pv-btn--secondary{background:transparent;color:var(--sf-color-action);border-color:var(--sf-color-action);}
+ .pv-btn--ghost{background:transparent;color:var(--sf-color-action);border-color:transparent;}
+ .pv-btn--neutral{background:var(--sf-color-neutral);color:var(--sf-color-text--on-neutral);border-color:var(--sf-color-neutral);}
+ .pv-btn--danger{background:var(--sf-color-danger);color:var(--sf-color-text--on-danger);border-color:var(--sf-color-danger);}
+ .pv-btn--block{inline-size:100%;}
+ .pv-btn:disabled{opacity:var(--sf-opacity-disabled);cursor:not-allowed;}
+ /* No 'display' here: .pv-card is unlayered and would beat .sf-stack's
+ layered flex, breaking 'pv-card sf-stack' combos. Default block is fine. */
+ .pv-card{padding:var(--sf-space-l);background:var(--sf-color-surface);border:var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border);border-radius:var(--sf-radius-l);box-shadow:var(--sf-shadow-s);}
+ .pv-tag{display:inline-flex;align-items:center;gap:var(--sf-space-2xs);padding-block:var(--sf-space-3xs,0.125rem);padding-inline:var(--sf-space-xs);font-size:var(--sf-text-xs);line-height:var(--sf-leading-tight);white-space:nowrap;background:var(--sf-color-inset);border:var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border);border-radius:var(--sf-radius-s);color:var(--sf-color-text);}
+ .pv-tag--primary{color:var(--sf-color-action);border-color:var(--sf-color-action);background:color-mix(in oklab,var(--sf-color-action) 8%,var(--sf-color-surface));}
+ .pv-tag--info{color:var(--sf-color-info);border-color:var(--sf-color-info);background:color-mix(in oklab,var(--sf-color-info) 8%,var(--sf-color-surface));}
+ .pv-field{display:grid;gap:var(--sf-space-2xs);align-content:start;}
+ .pv-field__label{font-size:var(--sf-text-s);font-weight:var(--sf-font-weight-interactive);color:var(--sf-color-text);}
+ /* Demo chrome */
+ .pv-header{padding-block:var(--sf-space-s);border-block-end:var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border);background:var(--sf-color-surface);}
+ .pv-panel{background:var(--sf-color-surface);border-inline-end:var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border);}
+ .pv-cta{background:var(--sf-gradient-primary);border-radius:var(--sf-radius-xl);}
+ .pv-code-block{font-family:var(--sf-font-mono);font-size:var(--sf-text-s);background:var(--sf-color-inset);border:var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border);border-radius:var(--sf-radius-m);padding:var(--sf-space-m) var(--sf-space-l);color:var(--sf-color-text);}
+ /* Sidebar nav */
+ .pv-nav{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:2px;}
+ .pv-nav__item{padding:var(--sf-space-2xs) var(--sf-space-xs);border-radius:var(--sf-radius-s);font-size:var(--sf-text-s);color:var(--sf-color-text--secondary);cursor:pointer;}
+ .pv-nav__item.is-active{background:var(--sf-color-primary-100);color:var(--sf-color-primary-700);font-weight:600;}
+ /* Stat blocks */
+ .pv-stat{font-size:var(--sf-text-2xl);font-weight:800;line-height:var(--sf-leading-tight);color:var(--sf-color-heading);}
+ .pv-stat-label{font-size:var(--sf-text-xs);font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--sf-color-text--muted);}
+ .pv-delta{font-size:var(--sf-text-xs);}
+ .pv-delta--up{color:var(--sf-color-success);}
+ .pv-delta--down{color:var(--sf-color-danger);}
+ .pv-delta--flat{color:var(--sf-color-warning);}
+ /* Type-scale ramp — one class per scale step, all token-driven */
+ ${TYPE_STEPS.map((t) => `.pv-type--${t}{font-size:var(--sf-text-${t});line-height:var(--sf-leading-tight);}`).join("")}
+ /* Colour ramps */
+ .pv-ramp{display:flex;gap:2px;block-size:36px;border-radius:var(--sf-radius-m);overflow:hidden;}
+ .pv-ramp>*{flex:1;}
+ ${ramp("primary", "primary")}
+ ${ramp("base", "base")}
+ ${ramp("action", "action", "primary")}
+ .pv-chip{display:block;inline-size:28px;block-size:28px;border-radius:var(--sf-radius-s);}
+ .pv-chip--success{background:var(--sf-color-success);}
+ .pv-chip--warning{background:var(--sf-color-warning);}
+ .pv-chip--danger{background:var(--sf-color-danger);}
+ .pv-chip--info{background:var(--sf-color-info);}
+ /* Gradients */
+ .pv-grad{block-size:44px;border-radius:var(--sf-radius-m);border:var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border);}
+ .pv-grad--primary{background:var(--sf-gradient-primary);}
+ .pv-grad--brand{background:var(--sf-gradient-brand);}
+ .pv-grad--tertiary{background:var(--sf-gradient-tertiary);}
+ .pv-grad--surface{background:var(--sf-gradient-surface);}
+ /* Radius + shadow demo boxes */
+ .pv-demo-box{inline-size:52px;block-size:52px;background:var(--sf-color-primary-100);border:2px solid var(--sf-color-primary-300);}
+ ${RADII.map((r) => `.pv-radius--${r}{border-radius:var(--sf-radius-${r});}`).join("")}
+ .pv-shadow-box{inline-size:52px;block-size:52px;background:var(--sf-color-surface);border-radius:var(--sf-radius-m);}
+ ${SHADOWS.map((s) => `.pv-shadow--${s}{box-shadow:var(--sf-shadow-${s});}`).join("")}
+ /* Spacing scale bars */
+ .pv-space-bar{background:var(--sf-color-primary-400);border-radius:2px;min-inline-size:3px;min-block-size:3px;}
+ ${SPACES.map((s) => {
+ // --sf-space-3xs is not a defined core token (core starts at 2xs); give it
+ // the same fallback the framework uses elsewhere so the bar still renders.
+ const size = s === "3xs" ? "var(--sf-space-3xs,0.125rem)" : `var(--sf-space-${s})`;
+ return `.pv-space--${s}{inline-size:${size};block-size:${size};}`;
+ }).join("")}
+ .pv-swatch-label{font-size:var(--sf-text-2xs);font-family:var(--sf-font-mono);color:var(--sf-color-text--muted);}
+ /* Stylescape demo layout */
+ .pv-heading{font-family:var(--sf-font-heading);color:var(--sf-color-heading);}
+ .pv-strong{font-weight:var(--sf-font-weight-heading);}
+ .pv-ramp-row{display:grid;grid-template-columns:4rem 1fr;gap:var(--sf-space-s);align-items:center;}
+ .pv-spacing-track{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--sf-space-xs);}
+ /* Tinted card variants — unlayered, so they reliably override .pv-card's bg */
+ .pv-card--primary{background:var(--sf-color-primary);color:var(--sf-color-text--on-primary);}
+ .pv-card--soft{background:var(--sf-color-primary-100);color:var(--sf-color-primary-700);}`;
+ }
+
const MARKETING_BODY = `
-