Skip to content

feat: Token lock#5678

Merged
kof merged 25 commits into
webstudio-is:mainfrom
shreyanshkotak:lock-tokens
Apr 18, 2026
Merged

feat: Token lock#5678
kof merged 25 commits into
webstudio-is:mainfrom
shreyanshkotak:lock-tokens

Conversation

@shreyanshkotak
Copy link
Copy Markdown
Contributor

@shreyanshkotak shreyanshkotak commented Apr 6, 2026

Description

This feature is related to #5103. This adds lock/unlock support for token style sources and prevents editing locked tokens from the style panel, including keyboard-focus paths, while keeping token management actions available.

Lock option in token:
image

Lock icon for token with properties viewable but not editable:
image

Unlock option for locked token:
image

Steps for reproduction

  1. Select an element that uses a token in the Style Sources control.
  2. Open the token menu and click Lock token.
  3. Try editing styles in the style panel with mouse and keyboard.
  4. Expect the locked token to stay unchanged: edit controls are unavailable/inert, focus does not enter locked sections, and token actions like rename, duplicate, detach, delete, and unlock still work.

Code Review

  • hi @kof, I need you to do
    • conceptual review (architecture, feature-correctness)
    • detailed review (read every line)
    • test it on preview

Before requesting a review

  • made a self-review
  • added inline comments where things may be not obvious (the "why", not "what")

Before merging

  • tested locally and on preview environment (preview dev login: 0000)
  • updated test cases document
  • added tests
  • if any new env variables are added, added them to .env file

Add a persisted locked flag for token style sources and expose lock/unlock in the style source menu.

When a token is locked, prevent style edits across the style panel. Keep token management actions like rename, duplicate, detach, and delete available.

Show a lock icon on locked tokens.
Comment thread apps/builder/app/builder/shared/collapsible-section.tsx Outdated
Comment thread apps/builder/app/shared/style-source-utils.ts
Comment thread packages/design-system/src/utilities.ts Outdated
Comment thread apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx Outdated
Copy link
Copy Markdown
Member

@kof kof left a comment

Choose a reason for hiding this comment

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

making progress, almost there

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 7, 2026

🚀 Deployed!

📍 Preview: https://pr-5678.development.webstudio.is

Note: Adding new commits will remove the safe-to-deploy label and require re-approval.

@kof kof temporarily deployed to development April 7, 2026 12:04 — with GitHub Actions Inactive
@kof kof temporarily deployed to development April 7, 2026 12:04 — with GitHub Actions Inactive
remove the computed nanostore and instead derive lock state locally
using isStyleSourceLocked() utility with $selectedStyleSource where
needed
@shreyanshkotak
Copy link
Copy Markdown
Contributor Author

@kof refactored components to used a disabled property when the token is locked

@github-actions
Copy link
Copy Markdown

🚀 Deployed!

📍 Preview: https://pr-5678.development.webstudio.is

Note: Adding new commits will remove the safe-to-deploy label and require re-approval.

Comment thread packages/design-system/src/components/color-picker.tsx Outdated
@kof
Copy link
Copy Markdown
Member

kof commented Apr 17, 2026

image

I think it can be just "Lock"

@kof
Copy link
Copy Markdown
Member

kof commented Apr 17, 2026

image

When token is locked but has no styles, that circle icon is disproportionally bigger than the lock, one needs to change, not sure which

@kof
Copy link
Copy Markdown
Member

kof commented Apr 17, 2026

Seems like the lock icon comes from somewheree else, its not the lucid one

@shreyanshkotak
Copy link
Copy Markdown
Contributor Author

Seems like the lock icon comes from somewheree else, its not the lucid one

the icon was pulled from the webstudio figma library. can replace it with lucid one

When token is locked but has no styles, that circle icon is disproportionally bigger than the lock, one needs to change, not sure which

updating the icon should make the proportions more reasonable

@kof
Copy link
Copy Markdown
Member

kof commented Apr 17, 2026

We have a new one,

@kof
Copy link
Copy Markdown
Member

kof commented Apr 17, 2026

the icon

<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.66667 7.33334V4.66668C4.66667 3.78262 5.01786 2.93478 5.64298 2.30965C6.2681 1.68453 7.11594 1.33334 8 1.33334C8.88406 1.33334 9.7319 1.68453 10.357 2.30965C10.9821 2.93478 11.3333 3.78262 11.3333 4.66668V7.33334M3.33333 7.33334H12.6667C13.403 7.33334 14 7.9303 14 8.66668V13.3333C14 14.0697 13.403 14.6667 12.6667 14.6667H3.33333C2.59695 14.6667 2 14.0697 2 13.3333V8.66668C2 7.9303 2.59695 7.33334 3.33333 7.33334Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

@kof
Copy link
Copy Markdown
Member

kof commented Apr 17, 2026

Scrub on numeric css value input fields is still working, allowing to change things

@shreyanshkotak
Copy link
Copy Markdown
Contributor Author

Scrub on numeric css value input fields is still working, allowing to change things

the value resets if the user tries to change it this way, but I agree this should be similar to spacing section where scrub should be disabled as well. missed that one while testing

@kof
Copy link
Copy Markdown
Member

kof commented Apr 17, 2026

Here is the review from claude, I haven't really looked into it:

Ran terminal command:  git diff main...HEAD -- apps/builder/app/builder/features/style-panel/sections/space/layout.tsx 2>&1 | head -60

Searched for text `disabled` (`**/apps/builder/app/builder/features/style-panel/sections/space/layout.tsx`), 8 results

Let me check a few more things, particularly around the `workspace-router.server.ts` change flagged in the checkout output: 

Ran terminal command:  git diff main...HEAD -- apps/builder/app/services/workspace-router.server.ts 2>&1

Ran terminal command:  git diff main -- apps/builder/app/services/workspace-router.server.ts 2>&1 | head -10

Good - the workspace-router change was just a local git working-tree artifact from the PR checkout, not a code change.

Let me do one final check — look at the `ShadowContent` ToggleGroup which has `disabled` and `aria-disabled` that might conflict: 

Searched for text `disabled` (`**/apps/builder/app/builder/features/style-panel/shared/shadow-content.tsx`), 16 results

Read [](file:///workspaces/webstudio/apps/builder/app/builder/features/style-panel/shared/shadow-content.tsx#220-220), lines 220 to 350

Ran terminal command:  git diff main...HEAD -- apps/builder/app/builder/features/style-panel/style-source/style-source-input.stories.tsx 2>&1 | head -60

Searched for text `locked` (`**/apps/builder/app/builder/features/style-panel/style-source/style-source-input.stories.tsx`), 7 results

I now have a comprehensive picture. Here's my code review:

---

## PR #5678 Code Review — `feat: Token lock`

**Status: Almost ready, a few gaps remain.**

### What's Working Well

The PR has solid defense-in-depth:

1. **Data layer** — `locked: z.boolean().optional()` in SDK schema, `isStyleSourceLocked` / `toggleStyleSourceLockMutable` utilities, `setStyleSourceLocked` action all look correct.
2. **Central guard** — `publishUpdates` in use-style-data.ts bails early if the selected style source is locked. This is the safety net even if a UI guard is missed.
3. **ReadonlyContext** — `ReadonlyProvider` wraps the entire `StylePanel`, deriving value from `isStyleSourceLocked($selectedStyleSource)`. Clean and React-idiomatic.
4. **Menu** — Lock/Unlock item correctly toggles; state selectors + "Add more" are hidden when `!canEditStyles`. Duplicate creates an unlocked copy. ✓
5. **Controls coverage** — All standard controls (`SelectControl`, `ToggleGroupControl`, `ColorControl`, `TextControl`, `FontFamilyControl`, `FontWeightControl`, `MenuControl`, `ToggleControl`, `PositionControl`, `ImageControl`) call `useReadonly()` and pass `disabled`. ✓
6. **Sections** — Transitions, Transforms, Backgrounds, Borders, Flex/Grid child, Layout, Space, Inset, Filters, CssEditor, RepeatedStyle all properly disable their interactive parts.
7. **Testing** — `use-style-data.test.ts`, `style-source-utils.test.tsx`, `style-source-menu.test.ts`, `style-sources.test.ts` all added. All 2553 tests pass.

---

### Issues Found

#### 1. shadow-content.tsx — CssValueInput still uses `aria-disabled` not `disabled` (actionable gap)

The individual shadow dimension inputs (X, Y, Blur, Spread offset, and Color) still pass `aria-disabled={disabledControls}` instead of `disabled={disabledControls}`:

```tsx
// Lines 236, 260, 284, 309, 341 — all say aria-disabled, not disabled:
<CssValueInputContainer
  property="outline-offset"
  aria-disabled={disabledControls}  // ← still old pattern
  ...

aria-disabled on a text input only sets the ARIA attribute — it does not prevent user interaction. So when a token is locked, users can still type in shadow X/Y/Blur/Spread inputs (changes just silently don't save due to the central guard). The ToggleGroup on line 382 was correctly upgraded to disabled={disabledControls}, but the CssValueInputContainer instances weren't.

This needs disabled={disabledControls} (not aria-disabled) on lines 236, 260, 284, 309, and the ColorPickerControl on line 341.

2. shadow-content.tsx — Redundant aria-disabled alongside disabled on ToggleGroup

Line 382–383 sets both disabled={disabledControls} and aria-disabled={disabledControls}. The aria-disabled is redundant when disabled is already set.

3. InsetControl / SpaceLayout — container-level pointerEvents: none vs per-control disabled

inset-control.tsx uses:

...(readonly ? { opacity: 0.5, pointerEvents: "none" } : undefined)

space/layout.tsx similarly uses aria-disabled + tabIndex. This is a pragmatic choice for visual grid controls that don't have native form elements, but kof previously flagged this approach. It's likely acceptable since there's no single disabled prop to set on a CSS grid layout widget — just worth confirming with kof.

4. Minor: No Storybook story for locked token visual state

style-source-input.stories.tsx has locked: false on all items. Adding one story with locked: true would make the lock icon and constraints visually verifiable during review.


Mental Model Summary

The PR's architecture is:

  1. StyleSourceToken.locked (SDK schema) → persisted to project data
  2. isStyleSourceLocked($selectedStyleSource) (builder state) → ReadonlyProvider value
  3. Every interactive control reads useReadonly()disabled prop
  4. publishUpdates also checks isStyleSourceLocked → silent no-op even if a control slips through
  5. Menu hides state selectors + "Add more" for locked tokens; Lock/Unlock menu item toggles locked field

The critical fix needed before merge is item #1 — the shadow-content.tsx CssValueInputContainer instances using aria-disabled instead of disabled.

@shreyanshkotak
Copy link
Copy Markdown
Contributor Author

@kof with the latest commits, the following updates should be in place

  • updated lock icon
  • updated "Lock" and "Unlock" for the token actions menu
  • scrub action in numeric input and position properties should be disabled when the token is locked
  • fixed the one pending instance of aria-disabled in favor of disabled
  • removed redundant logic from color-picker

@github-actions
Copy link
Copy Markdown

🚀 Deployed!

📍 Preview: https://pr-5678.development.webstudio.is

Note: Adding new commits will remove the safe-to-deploy label and require re-approval.

@kof
Copy link
Copy Markdown
Member

kof commented Apr 18, 2026

I think this is ready!

@kof
Copy link
Copy Markdown
Member

kof commented Apr 18, 2026

Another thing I was just wondering if it will be practical to have a single place to find/lock all tokens in some way. Usually you would want to lock the design system for example, but doing it one by one is a lot of work. If I could lock all tokens on that page this would be potentially solving it. Since people usually have design systems on a separate page. Separate PR though.

@kof
Copy link
Copy Markdown
Member

kof commented Apr 18, 2026

Merged! Great work!

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