Skip to content

feat: batch 10 — shadow safety + coverage GAPs + kpi-tile variant UX#21

Merged
simonsangla merged 1 commit intomainfrom
feat/batch10-followup
Apr 16, 2026
Merged

feat: batch 10 — shadow safety + coverage GAPs + kpi-tile variant UX#21
simonsangla merged 1 commit intomainfrom
feat/batch10-followup

Conversation

@simonsangla
Copy link
Copy Markdown
Owner

Summary

Closes the inspector findings from Batch 9 (PR #20): 1 P1 (CSS injection via unguarded shadow strings), 2 GAPs (export emission tests + integration tests), and 1 P2 (kpi-tile metric variant lacked user-facing affordance). 6 tasks T-130..T-135 in 3 tiers, 13/13 batch-10 criteria covered.

Tasks

  • T-130cssShadow validator now rejects ; } { /* */ newlines < >. Multi-layer rgba/hsla still works (parens + commas allowed). Cavekit-schema R8 revised to mandate this contract.
  • T-131tests/export/newTokens.test.ts asserts every export format emits the 5 new color slots + 4 shadows + 5 radii with the right shape (kebab-case CSS, $-prefixed SCSS, theme.extend.boxShadow/borderRadius for Tailwind, type:'boxShadow'/'dimension' for Style Dictionary).
  • T-132tests/persistence/integration.test.tsx covers toJSON round-trip preserving shadows + radii, every preset preserved through save/load, themeToStyleVars superset of preview-consumed vars, and the inheritance chain end-to-end.
  • T-133 — same file: 8 malicious shadow payloads (semicolon escape, brace open/close, comment open/close, newline, angle brackets) all rejected as corrupt by loadTheme. Benign multi-layer rgba shadow still loads.
  • T-134 — kpi-tile gets an in-card Tile/Metric pill toggle. Cavekit-widgets R4 revised to specify: only kpi-tile exposes per-widget config UI, variant choice is transient per-session (not persisted, not in any export), toggle uses aria-pressed. Event propagation stopped so toggle clicks don't flip the parent switch.
  • T-135 — 5 new behavior tests covering toggle rendering, default state, click-flips-aria-pressed, switch-isolation, and single-widget-only scope.

Cavekit revisions

  • cavekit-schema.md R8 — explicit character blocklist for shadow values
  • cavekit-widgets.md R4 — kpi-tile metric variant scope clarified

Validation

  • lint: 0 errors
  • typecheck: pass
  • test: 261/261 (220 prior + 41 new)
  • build: pass (vite ~107ms; 18.09kB CSS / 293kB JS)

Browser verification

  • Tile/Metric pill toggle visible below the kpi-tile card
  • Default state: Tile pressed (filled with theme primary), Metric outlined
  • Click Metric → toggle flips, kpi-tile preview switches to top-hairline serif "1,284" / "ACTIVE USERS" rendering
  • Switch role="switch" on the kpi-tile card itself NOT toggled by variant clicks (event isolation working)
  • All themes recolor every preview (including new Tile/Metric pills) via inherited CSS vars

Test plan

  • npm run lint — clean
  • npm run typecheck — clean
  • npm run test — 261/261
  • npm run build — succeeds
  • Browser: Tile/Metric toggle visible, click flips variant, switch isolated
  • Browser: malicious imported theme rejected (verified via test, not browser)

🤖 Generated with Claude Code

Closes the inspector findings from Batch 9:
- 1 P1 (CSS injection via unguarded shadow strings)
- GAP #1 (T-129 integration tests)
- GAP #2 (export emission tests for new tokens)
- 1 P2 (kpi-tile metric variant lacks user-facing affordance)

T-130 — cssShadow validator tightened. Rejects `;` `}` `{` `/*` `*/`
newlines `<` `>`. These are the characters sufficient to break out of a
`:root { --shadow-x: VALUE; }` declaration in CSS exports or
`$shadow-x: VALUE;` in SCSS. Multi-layer rgba/hsla still works because
parens and commas are NOT in the blocklist. Cavekit-schema R8 revised to
mandate this contract explicitly.

T-131 — tests/export/newTokens.test.ts asserts every export format
(toCSSVars/Variant, toSCSSVars/Variant, toTailwindConfig/Variant,
toStyleDictionary/Variant, toJSON/Variant, toTSObject/Variant) emits the
5 new color slots, 4 shadow slots, and 5 radius slots with the right
shape (kebab-case CSS, $-prefixed SCSS, theme.extend.boxShadow + .borderRadius
for Tailwind, type:'boxShadow'/'dimension' for Style Dictionary).

T-132 — tests/persistence/integration.test.tsx covers:
- save → load → toJSON round-trip preserves shadows + radii (custom values
  + every preset)
- themeToStyleVars output is a complete superset of every CSS var that
  WidgetPreview reads
- A wrapping div with the preset's CSS vars actually carries those values
  to a child WidgetPreview (proves the inheritance chain end-to-end)

T-133 — same file, 8 malicious shadow payloads (semicolon escape, brace
open/close, comment open/close, newline, angle brackets) all rejected as
corrupt by loadTheme. Benign multi-layer rgba shadow still loads as ok.

T-134 — kpi-tile gets an in-card Tile/Metric pill toggle. Cavekit-widgets
R4 revised to specify: (a) only kpi-tile exposes per-widget config UI,
(b) variant choice is transient per-session (not persisted, not in any
export), (c) toggle uses aria-pressed for both states. Implementation
keeps the variant in WidgetSelector local state and stops event
propagation so toggle clicks don't flip the parent switch.

T-135 — 5 new behavior tests on WidgetSelector covering toggle
rendering, default state, click-flips-aria-pressed, switch-isolation,
and single-widget-only scope (exactly one variant-toggle row in the
entire selector — no other widget gains config UI).

Validation: lint 0 errors, typecheck pass, test 261/261 (220 prior + 41
new), build pass (vite ~107ms).

Cavekit revisions:
- cavekit-schema R8: explicit character blocklist for shadow values
- cavekit-widgets R4: kpi-tile metric variant scope clarified

Build site: context/plans/build-site-batch10.md (6 tasks, 3 tiers,
13/13 criteria covered).
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
theme-forge Ready Ready Preview, Comment Apr 16, 2026 4:48pm

@simonsangla simonsangla merged commit 12fb203 into main Apr 16, 2026
3 checks passed
@simonsangla simonsangla deleted the feat/batch10-followup branch April 16, 2026 16:49
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements Batch 10 changes, focusing on security hardening for shadow tokens and enhancing the kpi-tile widget. Key updates include a character blocklist in the cssShadow validator to prevent CSS injection, comprehensive export emission tests for new tokens, and a transient variant toggle for the kpi-tile preview. Feedback indicates that the use of e.stopPropagation() in the WidgetSelector component is redundant because the variant buttons are siblings to the main toggle rather than nested within it.

Comment on lines +111 to +115
onClick={(e) => {
// Stop propagation so this doesn't toggle the parent switch
e.stopPropagation()
setKpiVariant(v)
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of e.stopPropagation() here is redundant and potentially misleading. Since the variant buttons are siblings of the main cardBtn (the widget selection toggle) and not nested within it, clicking a variant button will not trigger the cardBtn's onClick handler. The DOM structure already provides the event isolation mentioned in the implementation notes.

Suggested change
onClick={(e) => {
// Stop propagation so this doesn't toggle the parent switch
e.stopPropagation()
setKpiVariant(v)
}}
onClick={() => {
setKpiVariant(v)
}}

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.

1 participant