Repository navigation
feat(platform): add the Paper design system and isolated workbench - #9667
emir-karabeg wants to merge 5 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
[Low impact] The changes stay within a new, standalone Platform workbench and repository tooling. The PR appears safe to merge, with two non-blocking workbench interaction fixes. FindingsSummaryAdds an isolated Platform workbench with shared controls, Paper recipes, theme foundations, fixture data, and design checks.
|
| <input | ||
| aria-label={searchLabel} | ||
| placeholder={searchLabel} | ||
| value={query} | ||
| data-type='T060' | ||
| onChange={(event) => setQuery(event.target.value)} |
There was a problem hiding this comment.
Pressing Enter in the SearchableSelect search box submits the surrounding dialog form. In the workbench’s “Default role” step, this calls advance() without choosing the matching role. The previous role stays selected. Keep Enter inside the picker while it is open rather than letting it submit the form.
| .viewport { | ||
| max-block-size: var(--spacing-154); | ||
| min-block-size: 0; | ||
| overflow: hidden; |
There was a problem hiding this comment.
Catalog rows cannot be reached
This wrapper has only max-block-size, so the nested ScrollArea cannot resolve its percentage height against a definite parent height. It grows with its content, and the wrapper hides everything beyond the limit. For example, C1082 contains the twenty-row C1080 list, but its later rows cannot be scrolled into view. Give the wrapper a definite block-size, as the workbench chat list already does.
There was a problem hiding this comment.
15 issues found across 91 files
Confidence score: 3/5
scripts/sync-platform-recipes.tsemits numeric positions as unitless CSS lengths, which browsers discard. Add units to numeric values for dimensional properties.specimen.module.cssgives the viewport only a maximum height, so the nested ScrollArea may never overflow or scroll. Give it a definite height.searchable-select.tsxdrops the typed query when the user chooses create, so the consumer cannot create the requested item. Pass the current query toonCreate.workspace-fields.tsxdoes not retain or submit typed addresses, so they can be lost. Add a callback or form value for the entered addresses.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/sim/platform/design-system/components/range-picker.tsx">
<violation number="1" location="apps/sim/platform/design-system/components/range-picker.tsx:33">
P3: A disabled picker is visually indistinguishable from an enabled one and still shows a pointer cursor, because `.trigger` has no `:disabled` rule. Add a `.trigger:disabled` rule in `range-picker.module.css` with `cursor: default` and `opacity: var(--opacity-disabled)`, matching the other disabled controls.</violation>
<violation number="2" location="apps/sim/platform/design-system/components/range-picker.tsx:64">
P3: Re-selecting the current option still calls `onValueChange`, so consumers get a change event with no change. Guard the state update and callback with `option !== value`, and still close the menu.</violation>
</file>
<file name="scripts/sync-platform-foundations.ts">
<violation number="1" location="scripts/sync-platform-foundations.ts:36">
P2: Any fourth argument other than `--check` silently runs the write path. Reject unknown modes so a mistyped `--check` cannot regenerate the files and report success.</violation>
<violation number="2" location="scripts/sync-platform-foundations.ts:52">
P2: The `>= 4` cutoff keeps 1–3px spacing and radius tokens in px, so they will not scale with the root font size while the rest of the scale does. The header only justifies keeping strokes in px. Scope the exemption to stroke types, or convert every scalable px value.</violation>
<violation number="3" location="scripts/sync-platform-foundations.ts:65">
P3: `tokens.css` is written before the type recipes are validated. If a type style is missing, the run throws after updating only `tokens.css`, leaving the two generated files out of sync. Build all outputs in memory, then write them together.</violation>
</file>
<file name="scripts/sync-platform-recipes.ts">
<violation number="1" location="scripts/sync-platform-recipes.ts:212">
P2: Numeric style literals are emitted as unitless CSS lengths, so browsers discard positions such as the generated `left: 16` and `top: 6`. Normalize numeric values with units for dimensional properties while preserving unitless properties and zero.</violation>
</file>
<file name="scripts/platform-variant-usage.ts">
<violation number="1" location="scripts/platform-variant-usage.ts:86">
P2: A local `const` holding JSX is walked only once, under the first prop signature that reaches it. The `seen.has(decl)` guard in the identifier branch blocks re-walks with different props, so variants passed through that local can be missed. Key the guard by the props signature as well as the declaration, or walk the initializer on every reference.</violation>
</file>
<file name="apps/sim/platform/preview/specimen.module.css">
<violation number="1" location="apps/sim/platform/preview/specimen.module.css:24">
P2: `.viewport` caps the height with `max-block-size`, which does not give the nested ScrollArea a definite height. Its `max-block-size: 100%` resolves to none, so the scroll region never overflows and the `overflow: hidden` here clips the remaining content with no way to reach it. Use `block-size` so the child percentage resolves against a definite height.</violation>
</file>
<file name="apps/sim/platform/design-system/styles/control-roles.css">
<violation number="1" location="apps/sim/platform/design-system/styles/control-roles.css:8">
P3: In light theme, `--control-danger-hover` and `--control-danger-pressed` both resolve to the same `--color-light-danger` as the resting `--control-danger`, so destructive buttons show no hover or pressed feedback. Map these to a distinct light-theme red shade, as the dark block already does for hover and pressed. Primary pressed in light has the same problem and should get a distinct value too.</violation>
</file>
<file name="apps/sim/platform/design-system/components/search-field.tsx">
<violation number="1" location="apps/sim/platform/design-system/components/search-field.tsx:34">
P2: The shared `event.shiftKey` guard also blocks `/`, even though `event.key === '/'` already pins the character. On layouts that type `/` with Shift (for example German or Spanish), the shortcut never fires. Limit the Shift exclusion to the `⌘K` branch.</violation>
</file>
<file name="apps/sim/platform/design-system/components/fields.tsx">
<violation number="1" location="apps/sim/platform/design-system/components/fields.tsx:71">
P2: Optional fields lose the "(optional)" marker for assistive tech. The `aria-label={label}` set by `Input`, `TextArea` and `Select` overrides the `<label>` text, so the marker is dropped from the control's name. Pass the optional-aware label to the control in `TextField`, `TextAreaField` and `SelectField`.</violation>
</file>
<file name="apps/sim/platform/preview/workbench.tsx">
<violation number="1" location="apps/sim/platform/preview/workbench.tsx:32">
P3: The theme and fixture-state links drop `destination`, so toggling theme or state resets a selected workspace section to Dashboard. `href()` carries `recipe` explicitly but not `destination`, while the specimen's tab links keep it. Carry `destination` through the same helper.</violation>
</file>
<file name="apps/sim/platform/design-system/components/searchable-select.tsx">
<violation number="1" location="apps/sim/platform/design-system/components/searchable-select.tsx:90">
P2: The create action cannot tell the consumer what to create. `onCreate` takes no arguments and the typed search text is never passed, so a creatable select loses the user's input. Pass the current query to `onCreate` and type it as `(value: string) => void`.</violation>
</file>
<file name="apps/sim/platform/design-system/components/controls.tsx">
<violation number="1" location="apps/sim/platform/design-system/components/controls.tsx:182">
P2: `AvatarGroup` already renders the first two people, so this overflow popover repeats them instead of listing only hidden members; pass `people.slice(visibleCount)` to `PeoplePopover`.</violation>
</file>
<file name="apps/sim/platform/design-system/components/workspace-fields.tsx">
<violation number="1" location="apps/sim/platform/design-system/components/workspace-fields.tsx:90">
P2: Typing an address never updates `recipients` or reaches the caller, and this input has no `name` for form submission. Add an add/change callback or expose a form value so entered addresses are retained.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Turn on auto-fix | Re-trigger cubic
| const scalable = new Set(['fontSize', 'lineHeight', 'container', 'spacing', 'breakpoint', 'radius']) | ||
| function tokenValue(token: Token) { | ||
| const size = /^(\d+(?:\.\d+)?)px$/.exec(token.value) | ||
| return size && scalable.has(token.type) && Number(size[1]) >= 4 |
There was a problem hiding this comment.
P2: The >= 4 cutoff keeps 1–3px spacing and radius tokens in px, so they will not scale with the root font size while the rest of the scale does. The header only justifies keeping strokes in px. Scope the exemption to stroke types, or convert every scalable px value.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/sync-platform-foundations.ts, line 52:
<comment>The `>= 4` cutoff keeps 1–3px spacing and radius tokens in px, so they will not scale with the root font size while the rest of the scale does. The header only justifies keeping strokes in px. Scope the exemption to stroke types, or convert every scalable px value.</comment>
<file context>
@@ -0,0 +1,101 @@
+const scalable = new Set(['fontSize', 'lineHeight', 'container', 'spacing', 'breakpoint', 'radius'])
+function tokenValue(token: Token) {
+ const size = /^(\d+(?:\.\d+)?)px$/.exec(token.value)
+ return size && scalable.has(token.type) && Number(size[1]) >= 4
+ ? `${Number(size[1]) / 16}rem`
+ : token.value
</file context>
| const { typography }: { typography: TypeMaster[] } = JSON.parse(readFileSync(registryFile, 'utf8')) | ||
| const styles: Record<string, TypeStyle> = JSON.parse(readFileSync(stylesFile, 'utf8')) | ||
| const directory = path.resolve(import.meta.dirname, '../apps/sim/platform/design-system/styles') | ||
| const compare = mode === '--check' |
There was a problem hiding this comment.
P2: Any fourth argument other than --check silently runs the write path. Reject unknown modes so a mistyped --check cannot regenerate the files and report success.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/sync-platform-foundations.ts, line 36:
<comment>Any fourth argument other than `--check` silently runs the write path. Reject unknown modes so a mistyped `--check` cannot regenerate the files and report success.</comment>
<file context>
@@ -0,0 +1,101 @@
+const { typography }: { typography: TypeMaster[] } = JSON.parse(readFileSync(registryFile, 'utf8'))
+const styles: Record<string, TypeStyle> = JSON.parse(readFileSync(stylesFile, 'utf8'))
+const directory = path.resolve(import.meta.dirname, '../apps/sim/platform/design-system/styles')
+const compare = mode === '--check'
+function emit(filename: string, css: string) {
+ const file = path.join(directory, filename)
</file context>
| if (!ts.isPropertyAssignment(prop)) continue | ||
| const value = prop.initializer | ||
| if (ts.isStringLiteral(value) || ts.isNumericLiteral(value)) | ||
| style[prop.name.getText(file)] = value.text |
There was a problem hiding this comment.
P2: Numeric style literals are emitted as unitless CSS lengths, so browsers discard positions such as the generated left: 16 and top: 6. Normalize numeric values with units for dimensional properties while preserving unitless properties and zero.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/sync-platform-recipes.ts, line 212:
<comment>Numeric style literals are emitted as unitless CSS lengths, so browsers discard positions such as the generated `left: 16` and `top: 6`. Normalize numeric values with units for dimensional properties while preserving unitless properties and zero.</comment>
<file context>
@@ -0,0 +1,630 @@
+ if (!ts.isPropertyAssignment(prop)) continue
+ const value = prop.initializer
+ if (ts.isStringLiteral(value) || ts.isNumericLiteral(value))
+ style[prop.name.getText(file)] = value.text
+ }
+ }
</file context>
| } | ||
| } | ||
| function visit(node: ts.Node) { | ||
| if (ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node)) return |
There was a problem hiding this comment.
P2: A local const holding JSX is walked only once, under the first prop signature that reaches it. The seen.has(decl) guard in the identifier branch blocks re-walks with different props, so variants passed through that local can be missed. Key the guard by the props signature as well as the declaration, or walk the initializer on every reference.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/platform-variant-usage.ts, line 86:
<comment>A local `const` holding JSX is walked only once, under the first prop signature that reaches it. The `seen.has(decl)` guard in the identifier branch blocks re-walks with different props, so variants passed through that local can be missed. Key the guard by the props signature as well as the declaration, or walk the initializer on every reference.</comment>
<file context>
@@ -0,0 +1,187 @@
+ }
+ }
+ function visit(node: ts.Node) {
+ if (ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node)) return
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
+ const decl = declaration(node.tagName)
</file context>
| value={option} | ||
| selected={option === value} | ||
| onSelect={() => { | ||
| setValue(option) |
There was a problem hiding this comment.
P3: Re-selecting the current option still calls onValueChange, so consumers get a change event with no change. Guard the state update and callback with option !== value, and still close the menu.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/platform/design-system/components/range-picker.tsx, line 64:
<comment>Re-selecting the current option still calls `onValueChange`, so consumers get a change event with no change. Guard the state update and callback with `option !== value`, and still close the menu.</comment>
<file context>
@@ -0,0 +1,75 @@
+ value={option}
+ selected={option === value}
+ onSelect={() => {
+ setValue(option)
+ setOpen(false)
+ onValueChange?.(option)
</file context>
| type='button' | ||
| className={styles.trigger} | ||
| aria-label={value ? `${label}: ${value}` : label} | ||
| disabled={!options.length} |
There was a problem hiding this comment.
P3: A disabled picker is visually indistinguishable from an enabled one and still shows a pointer cursor, because .trigger has no :disabled rule. Add a .trigger:disabled rule in range-picker.module.css with cursor: default and opacity: var(--opacity-disabled), matching the other disabled controls.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/platform/design-system/components/range-picker.tsx, line 33:
<comment>A disabled picker is visually indistinguishable from an enabled one and still shows a pointer cursor, because `.trigger` has no `:disabled` rule. Add a `.trigger:disabled` rule in `range-picker.module.css` with `cursor: default` and `opacity: var(--opacity-disabled)`, matching the other disabled controls.</comment>
<file context>
@@ -0,0 +1,75 @@
+ type='button'
+ className={styles.trigger}
+ aria-label={value ? `${label}: ${value}` : label}
+ disabled={!options.length}
+ data-size={size}
+ data-type='T060'
</file context>
| `/* Shared foundation. Reference px become rem at a 16px root; strokes retain their measured width. */\n[data-platform-ui] {\n${ordered.map((t) => ` ${t.name}: ${tokenValue(t)};`).join('\n')}\n}` | ||
| ) | ||
|
|
||
| const recipes = new Map<string, string[]>() |
There was a problem hiding this comment.
P3: tokens.css is written before the type recipes are validated. If a type style is missing, the run throws after updating only tokens.css, leaving the two generated files out of sync. Build all outputs in memory, then write them together.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/sync-platform-foundations.ts, line 65:
<comment>`tokens.css` is written before the type recipes are validated. If a type style is missing, the run throws after updating only `tokens.css`, leaving the two generated files out of sync. Build all outputs in memory, then write them together.</comment>
<file context>
@@ -0,0 +1,101 @@
+ `/* Shared foundation. Reference px become rem at a 16px root; strokes retain their measured width. */\n[data-platform-ui] {\n${ordered.map((t) => ` ${t.name}: ${tokenValue(t)};`).join('\n')}\n}`
+)
+
+const recipes = new Map<string, string[]>()
+for (const { key, base } of typography) {
+ const type = styles[base]
</file context>
| --control-primary-pressed: var(--color-light-text-strong); | ||
| --control-danger: var(--color-light-danger); | ||
| --control-danger-soft: var(--color-red-soft); | ||
| --control-danger-hover: var(--color-light-danger); |
There was a problem hiding this comment.
P3: In light theme, --control-danger-hover and --control-danger-pressed both resolve to the same --color-light-danger as the resting --control-danger, so destructive buttons show no hover or pressed feedback. Map these to a distinct light-theme red shade, as the dark block already does for hover and pressed. Primary pressed in light has the same problem and should get a distinct value too.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/platform/design-system/styles/control-roles.css, line 8:
<comment>In light theme, `--control-danger-hover` and `--control-danger-pressed` both resolve to the same `--color-light-danger` as the resting `--control-danger`, so destructive buttons show no hover or pressed feedback. Map these to a distinct light-theme red shade, as the dark block already does for hover and pressed. Primary pressed in light has the same problem and should get a distinct value too.</comment>
<file context>
@@ -0,0 +1,45 @@
+ --control-primary-pressed: var(--color-light-text-strong);
+ --control-danger: var(--color-light-danger);
+ --control-danger-soft: var(--color-red-soft);
+ --control-danger-hover: var(--color-light-danger);
+ --control-danger-pressed: var(--color-light-danger);
+ --control-hover-border: var(--color-ink-20);
</file context>
| const workspace = organization?.workspaces[0] | ||
| const href = (nextTheme: string, nextState: string) => { | ||
| const params = new URLSearchParams({ theme: nextTheme, state: nextState }) | ||
| if (recipe) params.set('recipe', recipe) |
There was a problem hiding this comment.
P3: The theme and fixture-state links drop destination, so toggling theme or state resets a selected workspace section to Dashboard. href() carries recipe explicitly but not destination, while the specimen's tab links keep it. Carry destination through the same helper.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/platform/preview/workbench.tsx, line 32:
<comment>The theme and fixture-state links drop `destination`, so toggling theme or state resets a selected workspace section to Dashboard. `href()` carries `recipe` explicitly but not `destination`, while the specimen's tab links keep it. Carry `destination` through the same helper.</comment>
<file context>
@@ -0,0 +1,221 @@
+ const workspace = organization?.workspaces[0]
+ const href = (nextTheme: string, nextState: string) => {
+ const params = new URLSearchParams({ theme: nextTheme, state: nextState })
+ if (recipe) params.set('recipe', recipe)
+ return `/?${params}`
+ }
</file context>
Adds the new Platform design system and a standalone Next.js workbench under
apps/sim/platform. The library covers 608 Paper masters with evidence of a Platform use. It pairs native interactive controls with typed structural recipes and centralizes typography, color, materials, icons and responsive sizing.The workbench supports light and dark themes, empty organizations, populated workspaces and long content. Repeated rows, attachment batches and plots take typed records. Chart marks, labels, totals and tooltips follow the same data even when its length differs from the Paper specimen. Sample data loads only with
SIM_PLATFORM_FIXTURES=1. RunSIM_PLATFORM_FIXTURES=1 bun run --cwd apps/sim dev:platformand open port 3100.The implementation skill and scoped agent instructions define how to promote a feature component, retire a variant and verify a change against Paper. CI checks ownership, styling boundaries, token references and supported variants, including unused variants behind unreachable branches and inherited style props. Browser tests exercise keyboard interaction, selection, nested popovers and dialogs, fixture isolation, variable-length collections and charts, scrolling, type scaling and responsive layouts.
Validation includes the production build, Playwright, repository lint, TypeScript, the audit suite and the full repository test suite. The 26-case production suite also requests all 608 catalog selections through the workbench. Dashboard table coverage includes number formatting, zero versus missing values and six-, four- and three-column layouts.
The main-view and modal value pickers now share the same Paper popup surface and item recipe. Unstyled Base UI primitives own selection, filtering, keyboard navigation, hover submenus and collision-aware placement. Focus preserves the resting paint across Platform controls. The design audit enforces the rule and browser tests exercise the actual inputs, cascade and dialog layers.