Skip to content

Compute modular-scale derived tokens in JS and improve ratio UI - #457

Merged
jackgranatowski merged 3 commits into
mainfrom
claude/configurator-sync-framework-plugins-56vzb3
Jun 30, 2026
Merged

Compute modular-scale derived tokens in JS and improve ratio UI#457
jackgranatowski merged 3 commits into
mainfrom
claude/configurator-sync-framework-plugins-56vzb3

Conversation

@jackgranatowski

Copy link
Copy Markdown
Contributor

Summary

This PR adds server-side computation of modular-scale derived tokens (text sizes, spacing, radius, borders, motion) in JavaScript, injecting them as unlayered CSS to override hardcoded framework defaults. It also refactors the modular-scale ratio UI to support independent mobile/desktop ratios with always-visible custom inputs alongside preset selectors.

Key Changes

Derived Token Computation (persistence.ts)

  • New computeDerivedOverrides() function pre-computes all derived tokens (e.g., --sf-text-s, --sf-space-l) from source tokens using the same fluidClamp() formula the CSS framework uses
  • Tokens are injected as unlayered :root CSS, which beats any @layer declaration, ensuring accurate live preview even when legacy WP settings emit hardcoded clamp values
  • Supports five token families: text (9 steps), display (3 steps), space (9 steps), radius (9 steps + special values), border-width (4 steps), and motion/duration (5 steps + animation delays)
  • Helper functions: getNum() (safe number parsing), fmt() (precision formatting), fluidClamp() (mirrors CSS formula)

Live Preview & Save Integration

  • injectLivePreview() now includes derived tokens alongside source tokens
  • wpSave() validates REST URL is same-origin before POST to prevent CSRF
  • PreviewPanel.svelte applies derived overrides when rendering preview iframes

Modular-Scale Ratio UI (ClaimField.svelte)

  • Removed single-preset model (activeRatioValue, onRatioPreset props)
  • New independent ratio model: mobile (ratioMin) and desktop (ratioMax) each have their own preset dropdown + always-visible custom number input
  • Presets are computed per-side (activeRatioMin, activeRatioMax derived values) so each breakpoint highlights independently
  • Users can pick a preset and fine-tune it per side without losing the preset context
  • Cleaner layout: two rows (mobile/desktop) with label, preset select, and custom input

Supporting Changes

  • SliderRow.svelte: Improved raw mode UX — shows raw input when editing even if not a CSS expression; clears override on blur if input is empty
  • ColorInput.svelte: Fixed autofocus using tick() to ensure DOM is ready before focus
  • TypographyPanel.svelte & SpacingPanel.svelte: Removed activeRatio derived value and onRatioPreset callback; updated ClampField prop usage
  • BordersPanel.svelte: Fixed parseFloat() fallback to use isNaN() check instead of falsy coercion
  • CommandPalette.svelte: Added tabindex="-1" to dialog for better accessibility
  • ShadowsPanel.svelte: Added missing aria-label to glow toggle button
  • App.svelte: Wrapped initial lastSavedOverrides in untrack() to prevent spurious change detection

Implementation Details

  • Unlayered CSS strategy: Derived tokens are injected at :root level without @layer, ensuring they override any framework defaults or legacy hardcoded values inside @layer slashed.overrides
  • Precision formatting: fmt() strips trailing zeros and handles edge cases (empty string, negative zero) to produce clean CSS values
  • Modular-scale math: fluidClamp() applies the scale factor to both min/max bounds before computing the clamp, matching the framework's CSS formula exactly
  • Ratio independence: Each breakpoint's active preset is computed separately, allowing asymmetric configurations (e.g., 1.25 on mobile, 1.333 on desktop)

https://claude.ai/code/session_0124dAf4BHiKD1uESapMZdnK

claude added 2 commits June 30, 2026 08:54
Syncs configurator UI fixes that were made in the SLASHED-Plugins repo
back to the framework source so they survive future sync-core.mjs runs.

Changes ported from plugin commits (c6b0c8a..HEAD in admin-app/src/):

- ClampField: redesign modular scale ratio to independent mobile/desktop
  selectors + always-visible custom inputs; remove shared activeRatioValue
  and onRatioPreset props
- SliderRow: fix raw input clearing while typing; keep raw mode visible
  while editing; reset on blur when empty instead of on every keystroke
- BordersPanel / SpacingPanel / TypographyPanel: drop activeRatioValue +
  onRatioPreset call-sites after ClampField API change; fix parseFloat
  returning 0 for falsy values (use explicit isNaN guard)
- persistence: fix derived/ov merge order in injectLivePreview so user
  overrides win; add same-origin guard to wpSave REST URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124dAf4BHiKD1uESapMZdnK
…rder/motion

Ports all configurator-side changes from SLASHED-Plugins PR #109 to the
framework so they survive future sync-core.mjs runs.

persistence.ts — full computeDerivedOverrides() engine (exported):
- Adds radius/border/motion derived-token computation alongside existing
  text/space logic (RADIUS_STEPS, BORDER_WIDTH_STEPS, DURATION_STEPS)
- injectLivePreview() now injects derived tokens as unlayered :root CSS
  so scale knobs are reflected immediately without waiting for a save

PreviewPanel.svelte — withDerivedOverrides() applied in all three
preview CSS injection paths (buildIframeHTML + both live-update calls)
so preview iframes reflect scale knobs immediately

App.svelte — untrack() wraps lastSavedOverrides initialiser to avoid
spurious dirty-state on mount

CommandPalette.svelte — tabindex="-1" on dialog root for keyboard focus

ColorInput.svelte — replace autofocus with tick()+bind:this $effect
to avoid SSR warnings and race conditions on re-mount

SliderRow.svelte — init rawDraft as '' instead of currentRaw to prevent
stale value flashing before the sync $effect runs

ShadowsPanel.svelte — aria-label on glow toggle button

Not ported (plugin-specific): AppOverlay.svelte changes, class-css-generator.php

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124dAf4BHiKD1uESapMZdnK
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@jackgranatowski, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 37a566de-15d9-49a1-b89a-cf6aa0acfb39

📥 Commits

Reviewing files that changed from the base of the PR and between 1e873ff and ad2e902.

📒 Files selected for processing (11)
  • configurator/src/App.svelte
  • configurator/src/components/CommandPalette.svelte
  • configurator/src/components/inputs/ClampField.svelte
  • configurator/src/components/inputs/ColorInput.svelte
  • configurator/src/components/inputs/SliderRow.svelte
  • configurator/src/components/panels/BordersPanel.svelte
  • configurator/src/components/panels/ShadowsPanel.svelte
  • configurator/src/components/panels/SpacingPanel.svelte
  • configurator/src/components/panels/TypographyPanel.svelte
  • configurator/src/components/shell/PreviewPanel.svelte
  • configurator/src/lib/persistence.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/configurator-sync-framework-plugins-56vzb3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Compute modular-scale derived tokens in JS and improve ratio UI

✨ Enhancement 🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Adds computeDerivedOverrides() in persistence.ts to pre-compute all modular-scale output
 tokens (text, space, radius, border-width, motion/duration) in JS using the same fluidClamp()
 formula as the CSS framework, injecting them as unlayered :root CSS to override legacy hardcoded
 values in @layer slashed.overrides.
• Refactors ClampField.svelte ratio UI from a single shared preset model to independent
 mobile/desktop rows, each with its own preset ` and always-visible custom `, removing the
 activeRatioValue/onRatioPreset props.
• Fixes several UX bugs: SliderRow no longer clears overrides while typing (defers reset to blur),
 ColorInput uses tick() for reliable autofocus, BordersPanel fixes parseFloat falsy-zero bug
 with isNaN, and App.svelte wraps initial state in untrack() to prevent spurious change
 detection.
• Adds CSRF protection to wpSave() by validating the REST URL is same-origin before POSTing.
• Minor accessibility improvements: tabindex="-1" on CommandPalette dialog, aria-label on
 ShadowsPanel glow toggle.
Diagram

graph TD
    A["App.svelte"] --> B["PreviewPanel.svelte"]
    A --> C["TypographyPanel.svelte"]
    A --> D["SpacingPanel.svelte"]
    A --> E["BordersPanel.svelte"]
    C --> F["ClampField.svelte"]
    D --> F
    F --> G["SliderRow.svelte"]
    B --> H["persistence.ts\ncomputeDerivedOverrides"]
    A --> H
    H --> I[("CSS :root\nunlayered")]
    B --> I

    subgraph Legend
      direction LR
      _comp["Component"] ~~~ _lib(["Library/Util"]) ~~~ _out[("CSS Output")]
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Higher-priority @layer injection
  • ➕ Keeps all CSS inside the layer model, which is more predictable for cascade debugging
  • ➖ Requires knowing and replicating the full @layer order at runtime
  • ➖ Fragile if the framework adds new layers in the future
2. CSS custom property fallback chaining
  • ➕ No JS math required; purely declarative
  • ➖ Cannot override already-resolved hardcoded clamp() values emitted by legacy WP settings
  • ➖ Would require changes to the framework CSS itself

Recommendation: The unlayered CSS injection approach is the right call for overriding @layer declarations without modifying the framework CSS. The main alternative worth considering is a CSS @layer with higher priority (e.g., declaring a new layer after slashed.overrides), but that would require knowing the full layer order at runtime. The current approach is simpler and more robust.

Files changed (11) +243 / -67

Enhancement (5) +217 / -39
persistence.tsAdd computeDerivedOverrides() engine and same-origin CSRF guard for wpSave +157/-2

Add computeDerivedOverrides() engine and same-origin CSRF guard for wpSave

• Introduces 'computeDerivedOverrides()' which pre-computes all modular-scale output tokens (9 text steps, 3 display steps, 9 space steps, 9 radius steps, 4 border-width steps, 5 duration steps + delays) using 'fluidClamp()' mirroring the CSS framework formula. Derived tokens are merged into the injected ':root' CSS as unlayered declarations so they beat any '@layer' values. 'wpSave()' now validates the REST URL is same-origin before fetching to prevent CSRF.

configurator/src/lib/persistence.ts

ClampField.svelteRefactor modular-scale ratio UI to independent mobile/desktop rows +49/-34

Refactor modular-scale ratio UI to independent mobile/desktop rows

• Removes the shared 'activeRatioValue' and 'onRatioPreset' props. Replaces the grid of preset buttons with two rows (mobile/desktop), each containing a '<select>' for presets and an always-visible '<input type=number>' for fine-tuning. Active preset is now computed independently per breakpoint via 'activeRatioMin'/'activeRatioMax' derived values.

configurator/src/components/inputs/ClampField.svelte

PreviewPanel.svelteApply derived token overrides when rendering preview iframes +9/-3

Apply derived token overrides when rendering preview iframes

• Adds 'withDerivedOverrides()' helper that merges computed derived tokens (user overrides win) before encoding CSS for preview iframes. Applied in all three preview rendering paths: initial build, live update, and split-view.

configurator/src/components/shell/PreviewPanel.svelte

CommandPalette.svelteAdd tabindex=-1 to dialog for accessibility +1/-0

Add tabindex=-1 to dialog for accessibility

• Adds 'tabindex="-1"' to the dialog element so it can receive programmatic focus, improving keyboard navigation and screen reader compatibility.

configurator/src/components/CommandPalette.svelte

ShadowsPanel.svelteAdd aria-label to glow toggle button +1/-0

Add aria-label to glow toggle button

• Adds a dynamic 'aria-label' ("Enable shadow glow" / "Disable shadow glow") to the icon-only glow toggle button for screen reader accessibility.

configurator/src/components/panels/ShadowsPanel.svelte

Bug fix (4) +26 / -16
SliderRow.svelteFix raw input: keep visible while editing, defer reset to blur +11/-11

Fix raw input: keep visible while editing, defer reset to blur

• Adds 'isEditing' to the 'showRaw' derived condition so the raw input stays visible while the user is focused on it. Moves the empty-value 'onReset()' call from 'oninput' to 'onblur' so typing is never interrupted by a reset.

configurator/src/components/inputs/SliderRow.svelte

ColorInput.svelteFix autofocus using tick() to ensure DOM is ready +9/-1

Fix autofocus using tick() to ensure DOM is ready

• Replaces the 'autofocus' attribute with a '$effect' that calls 'tick().then(() => editInput?.focus())', ensuring the input element is mounted before focus is attempted.

configurator/src/components/inputs/ColorInput.svelte

BordersPanel.svelteFix parseFloat falsy-zero bug with explicit isNaN guard +4/-2

Fix parseFloat falsy-zero bug with explicit isNaN guard

• Replaces 'parseFloat(raw) || r.default' with an explicit 'isNaN' check in both 'getRadiusVal' and 'getComponentVal', preventing valid '0' values from being incorrectly replaced by the default.

configurator/src/components/panels/BordersPanel.svelte

App.svelteWrap initial lastSavedOverrides in untrack() to prevent spurious change detection +2/-2

Wrap initial lastSavedOverrides in untrack() to prevent spurious change detection

• Wraps the '{ ...overrides }' spread in 'untrack()' when initializing 'lastSavedOverrides' state, preventing Svelte from tracking the reactive 'overrides' object during initialization and triggering false pending-change signals.

configurator/src/App.svelte

Refactor (2) +0 / -12
SpacingPanel.svelteRemove shared activeRatio derived value and onRatioPreset callback +0/-6

Remove shared activeRatio derived value and onRatioPreset callback

• Drops the 'activeRatio' derived value and the 'onRatioPreset' bulk-change callback from the ClampField usage, aligning with the new independent per-breakpoint ratio API.

configurator/src/components/panels/SpacingPanel.svelte

TypographyPanel.svelteRemove shared activeRatio derived value and onRatioPreset callback +0/-6

Remove shared activeRatio derived value and onRatioPreset callback

• Drops the 'activeRatio' derived value and the 'onRatioPreset' bulk-change callback from the ClampField usage, aligning with the new independent per-breakpoint ratio API.

configurator/src/components/panels/TypographyPanel.svelte

@qodo-code-review

qodo-code-review Bot commented Jun 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 6 rules

Grey Divider


Action required

1. Reduced-motion overridden ✓ Resolved 🐞 Bug ≡ Correctness
Description
computeDerivedOverrides() emits fixed --sf-duration-* tokens whenever --sf-motion-scale is
present, and these are injected as unlayered :root overrides after the framework CSS. This
overrides the framework’s @media (prefers-reduced-motion: reduce) duration clamps, causing
reduced-motion users to still see full-duration motion in preview/embedded rendering.
Code

configurator/src/lib/persistence.ts[R151-160]

+  if (hasMotion) {
+    const scale = getNum(ov, '--sf-motion-scale', 1);
+    for (const [name, base] of DURATION_STEPS) {
+      derived[`--sf-duration-${name}`] = `${fmt(base * scale)}ms`;
+    }
+    derived['--sf-duration-none']             = '0ms';
+    derived['--sf-theme-transition-duration'] = `${fmt(300 * scale)}ms`;
+    for (let i = 1; i <= 5; i += 1) {
+      derived[`--sf-animation-delay-${i}`] = `${fmt(75 * i * scale)}ms`;
+    }
Relevance

⭐⭐⭐ High

They’ve accepted reduced-motion correctness fixes; motion preview specifically updated to honor
reduced-motion behavior.

PR-#371
PR-#336

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The framework sets all --sf-duration-* tokens to ~0 under prefers-reduced-motion: reduce, but
the PR computes and injects unlayered --sf-duration-* values whenever --sf-motion-scale is
present; because preview overrides are injected after the framework CSS, these computed values win
in the cascade and defeat reduced-motion behavior.

configurator/src/lib/persistence.ts[151-161]
configurator/src/lib/persistence.ts[208-221]
configurator/src/components/shell/PreviewPanel.svelte[489-496]
core/accessibility.css[35-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new JS-derived motion tokens (`--sf-duration-*`, `--sf-animation-delay-*`, etc.) are injected as unlayered `:root` CSS and can override the framework’s `@media (prefers-reduced-motion: reduce)` overrides, so reduced-motion preferences are ignored in the configurator preview.

### Issue Context
- The framework explicitly overrides `--sf-duration-*` under `@media (prefers-reduced-motion: reduce)`.
- The PR injects derived overrides *after* the framework CSS in preview iframes and in the parent live preview style element.

### Fix Focus Areas
- Ensure derived **motion** tokens do not apply when `prefers-reduced-motion: reduce` is active.
 - Option A: omit motion-derived tokens when `matchMedia('(prefers-reduced-motion: reduce)')` matches.
 - Option B: emit motion-derived tokens inside `@media (prefers-reduced-motion: no-preference)` (requires not using `fa()` for those tokens, or extending it to support wrapping).

- configurator/src/lib/persistence.ts[151-161]
- configurator/src/lib/persistence.ts[208-241]
- configurator/src/components/shell/PreviewPanel.svelte[470-559]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Radius relationship drift ✓ Resolved 🐞 Bug ≡ Correctness
Description
computeDerivedOverrides() hardcodes --sf-radius-pill and --sf-radius-outer based on
--sf-radius-scale, which breaks the framework’s intended relationships (pill aliases full, and
outer depends on --sf-radius-m). This can produce inconsistent results when users override
--sf-radius-full or --sf-radius-m (both are configurable in the Borders panel) while also
setting --sf-radius-scale.
Code

configurator/src/lib/persistence.ts[R133-142]

+  if (hasRadius) {
+    const scale = getNum(ov, '--sf-radius-scale', 1);
+    for (const [name, base] of RADIUS_STEPS) {
+      derived[`--sf-radius-${name}`] = `${fmt(base * scale)}px`;
+    }
+    derived['--sf-radius-none']  = '0';
+    derived['--sf-radius-full']  = '9999px';
+    derived['--sf-radius-pill']  = '9999px';
+    derived['--sf-radius-outer'] = `calc(${fmt(8 * scale)}px + var(--sf-component-pad))`;
+  }
Relevance

⭐⭐ Medium

Indirect evidence they enforce token contract; no prior precedent on radius alias relationships in
JS-derived overrides.

PR-#48
PR-#439

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The framework defines --sf-radius-pill as var(--sf-radius-full) and --sf-radius-outer as
calc(var(--sf-radius-m) + var(--sf-component-pad)), but the PR replaces these with fixed values
based on --sf-radius-scale. Since the UI can set --sf-radius-scale (presets) and also override
--sf-radius-m (fine-tune sliders), the derived pill/outer values can diverge from the
framework’s intended token graph.

configurator/src/lib/persistence.ts[133-142]
core/tokens.css[1211-1240]
configurator/src/components/panels/BordersPanel.svelte[20-33]
configurator/src/lib/stylePresets.ts[3-9]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`computeDerivedOverrides()` currently computes `--sf-radius-pill` and `--sf-radius-outer` as fixed expressions derived from `--sf-radius-scale`. In the framework, `pill` is defined as an alias of `full`, and `outer` is defined in terms of `--sf-radius-m`, so these should remain relationship-based to stay consistent when users override `--sf-radius-full` or `--sf-radius-m`.

### Issue Context
- Users can set `--sf-radius-scale` via presets.
- Users can separately override radius output tokens like `--sf-radius-m` via sliders/raw mode.

### Fix Focus Areas
- Update derived radius specials to mirror core semantics:
 - `--sf-radius-pill: var(--sf-radius-full)`
 - `--sf-radius-outer: calc(var(--sf-radius-m) + var(--sf-component-pad))`
 - Keep `--sf-radius-full: 9999px` and `--sf-radius-none: 0` as constants.

- configurator/src/lib/persistence.ts[133-142]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread configurator/src/lib/persistence.ts Outdated
- computeDerivedOverrides now accepts { reduceMotion } option; when true,
  all --sf-duration-* tokens are skipped so unlayered :root CSS does not
  override the framework's @media (prefers-reduced-motion: reduce) rules
  that live inside @layer slashed.overrides
- injectLivePreview and PreviewPanel.withDerivedOverrides both read
  window.matchMedia("(prefers-reduced-motion: reduce)").matches and pass it
- --sf-radius-pill is now var(--sf-radius-full) instead of a hardcoded 9999px
  value, so a fine-tune override on --sf-radius-full propagates correctly
- --sf-radius-outer is now calc(var(--sf-radius-m) + var(--sf-component-pad))
  instead of a scale-baked pixel value, mirroring the framework token graph

Co-Authored-By: Claude <noreply@anthropic.com>
@jackgranatowski
jackgranatowski merged commit 2b6b287 into main Jun 30, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants