Skip to content

feat(platform): add the Paper design system and isolated workbench - #9667

Open
emir-karabeg wants to merge 5 commits into
stagingfrom
improvement/platform
Open

emir-karabeg wants to merge 5 commits into
stagingfrom
improvement/platform

Conversation

@emir-karabeg

@emir-karabeg emir-karabeg commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

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. Run SIM_PLATFORM_FIXTURES=1 bun run --cwd apps/sim dev:platform and 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.

@emir-karabeg
emir-karabeg requested a review from a team as a code owner November 4, 2026 09:47
@vercel

vercel Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
docs Ignored Ignored Preview Nov 4, 2026 7:43pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[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.

Findings

  1. P2 Role search skips a step ▶
  2. P2 Catalog rows cannot be reached ▶

Summary

Adds an isolated Platform workbench with shared controls, Paper recipes, theme foundations, fixture data, and design checks.

  • Platform controls and recipes share Paper-based styles and cataloged designs.
  • The Platform workbench previews Paper designs in themes and data states.
  • Design checks and sync tools keep Platform recipes tied to their source rules.
  • Platform guidance keeps the new app separate from the existing Sim UI.
  • The Platform app has its own run, build, and test entry points.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Page["Workbench page"] --> Gate{"SIM_PLATFORM_FIXTURES = 1?"}
  Gate -->|Yes| Fixtures["Load fixture scenario"]
  Gate -->|No| Empty["Return no preview data"]
  Fixtures --> Workbench["Workbench"]
  Empty --> Workbench
  Workbench --> Controls["Native controls"]
  Workbench --> Catalog["Paper recipe catalog"]
Loading

Reviews (1) · Last reviewed commit: "feat(platform): add the Paper design sys..." · Reviewed by Greptile

Comment on lines +62 to +67
<input
aria-label={searchLabel}
placeholder={searchLabel}
value={query}
data-type='T060'
onChange={(event) => setQuery(event.target.value)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Role search skips a step

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.

Comment on lines +23 to +26
.viewport {
max-block-size: var(--spacing-154);
min-block-size: 0;
overflow: hidden;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

15 issues found across 91 files

Confidence score: 3/5

  • scripts/sync-platform-recipes.ts emits numeric positions as unitless CSS lengths, which browsers discard. Add units to numeric values for dimensional properties.
  • specimen.module.css gives the viewport only a maximum height, so the nested ScrollArea may never overflow or scroll. Give it a definite height.
  • searchable-select.tsx drops the typed query when the user chooses create, so the consumer cannot create the requested item. Pass the current query to onCreate.
  • workspace-fields.tsx does 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

@cubic-dev-ai cubic-dev-ai Bot Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Fix with cubic

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'

@cubic-dev-ai cubic-dev-ai Bot Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Fix with cubic

if (!ts.isPropertyAssignment(prop)) continue
const value = prop.initializer
if (ts.isStringLiteral(value) || ts.isNumericLiteral(value))
style[prop.name.getText(file)] = value.text

@cubic-dev-ai cubic-dev-ai Bot Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Fix with cubic

}
}
function visit(node: ts.Node) {
if (ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node)) return

@cubic-dev-ai cubic-dev-ai Bot Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Fix with cubic

value={option}
selected={option === value}
onSelect={() => {
setValue(option)

@cubic-dev-ai cubic-dev-ai Bot Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Fix with cubic

type='button'
className={styles.trigger}
aria-label={value ? `${label}: ${value}` : label}
disabled={!options.length}

@cubic-dev-ai cubic-dev-ai Bot Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Fix with cubic

`/* 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[]>()

@cubic-dev-ai cubic-dev-ai Bot Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Fix with cubic

--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);

@cubic-dev-ai cubic-dev-ai Bot Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Fix with cubic

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)

@cubic-dev-ai cubic-dev-ai Bot Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Fix with cubic

This branch was previously deployed

1 inactive (outdated) deployment
Preview — 94354c26 Deployed Nov 4, 2026 by vercel[bot]
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