From 2d15e64889ca8f1ed91b5e5f42f5d09084510a6f Mon Sep 17 00:00:00 2001 From: julioest Date: Tue, 20 Oct 2026 12:56:46 -0400 Subject: [PATCH 1/4] Move the gateway UI to the style guide palette The Gateway config UI and the shared-ui sheets now take every value from the Gateway UI style guide instead of Cursor's dark theme, and no stylesheet references a --cursor- token. Each hue carries one meaning: orange for the primary action and the selection, green for success, blue for links, amber for warnings, and red for destructive actions. The nav keeps its current layout under the new names. - tokens.css defines the palette directly: a chrome surface and three panel steps, three line weights, three text tiers, and each color family with a lighter text tint. The Cursor primitives, aliases, syntax colors, and unused spacing steps are gone. - The type scale starts from a 14.5px root, radii run 8, 9, 10, 12, and 14px, and controls are 24, 28, and 32px tall. - Buttons are 28px: the primary is an orange wash, secondary and outline share one raised neutral style, and danger is a red tint. A disabled primary or danger button drops its family color, since half opacity alone still reads as ready to press. - Fields are bordered on the page surface and light their border on focus. Cards, tables, banners, and model rows sit on the raised panel with a line border. - The About page's license link stays link blue. --- .../ui/src/components/tab-bar.test.mjs | 8 +- .../ui/src/pages/models-page.test.mjs | 10 +- .../ui/src/pages/settings-page.test.mjs | 25 +- .../gateway/config-ui/ui/src/styles/base.css | 15 +- .../config-ui/ui/src/styles/controls.css | 57 +-- .../config-ui/ui/src/styles/layout.css | 429 ++++++++++-------- .../config-ui/ui/src/styles/tokens.test.mjs | 150 +++--- crates/shared-ui/controls.css | 149 +++--- crates/shared-ui/dropdown.css | 27 +- crates/shared-ui/modal.css | 24 +- crates/shared-ui/package.json | 2 +- crates/shared-ui/shimmer.css | 12 +- crates/shared-ui/status-bar.css | 2 +- crates/shared-ui/toast.css | 8 +- crates/shared-ui/tokens.css | 338 ++++---------- 15 files changed, 580 insertions(+), 676 deletions(-) diff --git a/crates/gateway/config-ui/ui/src/components/tab-bar.test.mjs b/crates/gateway/config-ui/ui/src/components/tab-bar.test.mjs index 9fcac64db..8a089fa98 100644 --- a/crates/gateway/config-ui/ui/src/components/tab-bar.test.mjs +++ b/crates/gateway/config-ui/ui/src/components/tab-bar.test.mjs @@ -94,14 +94,14 @@ test("the nav column is sticky and clamp-wide, 48px from the content, and icon-o assert.equal(cell.get("border-radius"), "var(--radius)", "a 6px radius"); assert.equal(cell.get("font-size"), "var(--font-size-sm)", "12px text"); assert.equal(cell.get("line-height"), "var(--line-height-sm)", "16px lines"); - assert.equal(cell.get("color"), "var(--cursor-text-secondary)"); + assert.equal(cell.get("color"), "var(--dim)"); const hover = await bundledDeclarations(".tab:hover"); const selected = await bundledDeclarations(".tab[aria-current=page]"); - assert.equal(hover.get("background"), "var(--cursor-bg-quaternary)", "6% hover fill"); - assert.equal(selected.get("background"), "var(--cursor-bg-quaternary)", "6% selected fill"); + assert.equal(hover.get("background"), "var(--panel-2)", "the hover raises the cell"); + assert.equal(selected.get("background"), "var(--panel-2)", "the selected cell too"); const divider = await bundledDeclarations(".tab-divider"); - assert.equal(squash(divider.get("border-top")), "1pxsolidvar(--cursor-stroke-tertiary)"); + assert.equal(squash(divider.get("border-top")), "1pxsolidvar(--line-soft)"); const narrow = await bundledDeclarations(".tab-nav", { media: NARROW }); assert.equal(narrow.get("width"), "40px", "a 40px column"); diff --git a/crates/gateway/config-ui/ui/src/pages/models-page.test.mjs b/crates/gateway/config-ui/ui/src/pages/models-page.test.mjs index 044791674..8abd3b4cc 100644 --- a/crates/gateway/config-ui/ui/src/pages/models-page.test.mjs +++ b/crates/gateway/config-ui/ui/src/pages/models-page.test.mjs @@ -362,16 +362,18 @@ test("a non-numeric entry in a model's numeric field raises an alert under the i assert.equal(fixed.querySelector("input").value, "512", "the edit is kept"); }); -test("model rows sit 12px apart with no hover fill, and the search box takes Cursor's padding", async () => { +test("model rows sit 12px apart, mark the selection with an accent rule, and the search box is a pill", async () => { const list = await bundledDeclarations(".model-list"); assert.equal(list.get("display"), "flex"); assert.equal(list.get("flex-direction"), "column"); assert.equal(list.get("gap"), "var(--space-3)", "a 12px gap"); const hover = await bundledDeclarations(".model-row:hover"); - assert.equal(hover.get("background"), undefined, "no hover fill"); + assert.equal(hover.get("background"), "var(--panel-2)", "a hover raises the row"); + const selected = await bundledDeclarations(".model-row[aria-current=true]"); + assert.equal(selected.get("border-inline-start-color"), "var(--accent)"); const search = await bundledDeclarations(".models-toolbar .input"); - assert.equal(squash(search.get("padding")), "5px12px", "padding 5px 12px"); - assert.equal(search.get("border-radius"), "var(--radius-sm)", "a 4px radius"); + assert.equal(squash(search.get("padding")), "5px14px", "padding 5px 14px"); + assert.equal(search.get("border-radius"), "var(--radius-full)", "a pill"); const spin = await bundledDeclarations(".models-refresh.is-loading svg"); assert.match(spin.get("animation") ?? "", /spin/, "the icon rotates while loading"); }); diff --git a/crates/gateway/config-ui/ui/src/pages/settings-page.test.mjs b/crates/gateway/config-ui/ui/src/pages/settings-page.test.mjs index 524468ccc..7727a0460 100644 --- a/crates/gateway/config-ui/ui/src/pages/settings-page.test.mjs +++ b/crates/gateway/config-ui/ui/src/pages/settings-page.test.mjs @@ -183,6 +183,8 @@ test("the About panel renders the medallion, version, and license link", async ( const license = root.querySelector(".about-license"); assert.equal(license.href, "https://www.boost.org/LICENSE_1_0.txt"); assert.equal(license.rel, "noopener"); + const licenseRule = await bundledDeclarations(".about-license"); + assert.equal(licenseRule.get("color"), "var(--blue-text)", "the license link reads as a link, not an action"); }); test("a settings section puts its title above the card and lays each row as label, control, help", async () => { @@ -210,18 +212,19 @@ test("a settings section puts its title above the card and lays each row as labe assert.deepEqual(order, ["label", "control", "help"], "the grid reads label, control, then help"); }); -test("the card is a 4% tinted 12px block with no border or padding, and its rows are grid cells", async () => { +test("the card is a raised 12px block with a line border and no padding, and its rows are grid cells", async () => { const card = await bundledDeclarations(".settings-card"); assert.equal(card.get("border"), "0", "no border on the wrapper"); assert.equal(card.get("padding"), "0", "no padding on the wrapper"); assert.equal(card.get("background"), "none"); const heading = await bundledDeclarations(".settings-card .section-heading"); - assert.equal(heading.get("font-size"), "var(--font-size-sm)", "a 12px title"); - assert.equal(heading.get("color"), "var(--cursor-text-secondary)"); + assert.equal(heading.get("font-size"), "var(--font-size-lg)", "a 15px title"); + assert.equal(heading.get("color"), "var(--text)"); const body = await bundledDeclarations(".settings-card > .section-body"); - assert.equal(body.get("background"), "var(--cursor-bg-quinary)", "a 4% tint"); + assert.equal(body.get("background"), "var(--panel-2)", "the raised surface"); + assert.equal(squash(body.get("border")), "1pxsolidvar(--line)", "a line border"); assert.equal(body.get("border-radius"), "var(--radius-xl)", "a 12px radius"); assert.equal(body.get("padding"), "0"); @@ -230,11 +233,11 @@ test("the card is a 4% tinted 12px block with no border or padding, and its rows const control = await bundledDeclarations(".field-row>:not(.field-head):not(.field-help)"); assert.equal(control.get("grid-column"), "2", "the control sits on the right"); const label = await bundledDeclarations(".field-head label"); - assert.equal(label.get("font-size"), "var(--font-size-base)", "a 13px label"); + assert.equal(label.get("font-size"), "var(--font-size-base)", "a 13.5px label"); assert.equal(label.get("font-weight"), "400"); const help = await bundledDeclarations(".field-help"); - assert.equal(help.get("font-size"), "var(--font-size-base)", "13px help text"); - assert.equal(help.get("color"), "var(--cursor-text-secondary)"); + assert.equal(help.get("font-size"), "var(--font-size-base)", "13.5px help text"); + assert.equal(help.get("color"), "var(--dim)"); }); test("banners are 12px cards, the pending one in the accent tint, and callouts carry a 4px left bar", async () => { @@ -242,15 +245,15 @@ test("banners are 12px cards, the pending one in the accent tint, and callouts c assert.equal(banner.get("border-radius"), "var(--radius-xl)"); assert.equal(squash(banner.get("padding")), "12px14px"); const pending = await bundledDeclarations(".banner-pending"); - assert.equal(pending.get("background"), "var(--cursor-accent-subtle)", "accent at 8%"); + assert.equal(pending.get("background"), "var(--accent-subtle)", "accent at 8%"); assert.equal( squash(pending.get("border-color")), - "color-mix(insrgb,var(--cursor-accent)20%,transparent)", + "color-mix(insrgb,var(--accent)20%,transparent)", "accent at 20%", ); for (const [selector, color] of [ - [".banner-restart", "var(--warning)"], - [".banner-warning", "var(--warning)"], + [".banner-restart", "var(--warn)"], + [".banner-warning", "var(--warn)"], [".banner-danger", "var(--danger)"], ]) { const callout = await bundledDeclarations(selector); diff --git a/crates/gateway/config-ui/ui/src/styles/base.css b/crates/gateway/config-ui/ui/src/styles/base.css index 2f4ea94c7..ccdf78a86 100644 --- a/crates/gateway/config-ui/ui/src/styles/base.css +++ b/crates/gateway/config-ui/ui/src/styles/base.css @@ -1,6 +1,6 @@ /* Foundation stylesheet: cascade layer order, reset, typography, and focus defaults. The design tokens come from shared-ui/tokens.css (the - Cursor Dark sheet both Runvil UIs share), imported by main.ts + Gateway UI style guide's palette), imported by main.ts ahead of this file so the layer order below is declared before any layered component rules. Later selectors stay low-specificity because precedence is settled by the layer order declared here. */ @@ -52,20 +52,21 @@ html { height: 100%; + font-size: var(--font-size-root); } body { min-height: 100vh; min-height: 100svh; - background: var(--bg-primary); - color: var(--text-primary); + background: var(--panel); + color: var(--text); font-family: var(--font); font-size: var(--font-size-base); } h1 { - font-size: 1.25rem; - font-weight: 600; + font-size: var(--font-size-title); + font-weight: var(--font-weight-title); } code, @@ -76,11 +77,11 @@ } a { - color: var(--cursor-text-link); + color: var(--blue-text); } :focus-visible { - outline: 2px solid var(--cursor-accent); + outline: 2px solid var(--accent); outline-offset: 2px; } diff --git a/crates/gateway/config-ui/ui/src/styles/controls.css b/crates/gateway/config-ui/ui/src/styles/controls.css index 3c91e63d3..516c3e647 100644 --- a/crates/gateway/config-ui/ui/src/styles/controls.css +++ b/crates/gateway/config-ui/ui/src/styles/controls.css @@ -29,7 +29,7 @@ row, so no hover wash and no pointer. */ .menu-group-label { padding: var(--space-2) var(--space-2) var(--space-1); - color: var(--text-secondary); + color: var(--dim); font-size: var(--font-size-xs); font-weight: 600; letter-spacing: 0.04em; @@ -62,8 +62,8 @@ border-radius: 9999px; background: linear-gradient( to right, - var(--text-secondary) calc(var(--slider-progress) * 100%), - var(--cursor-bg-tertiary) calc(var(--slider-progress) * 100%) + var(--dim) calc(var(--slider-progress) * 100%), + var(--panel-3) calc(var(--slider-progress) * 100%) ); } .slider::-webkit-slider-thumb { @@ -73,82 +73,85 @@ margin-top: -5px; border: 0; border-radius: 50%; - background: var(--text-secondary); + background: var(--dim); transition: box-shadow 140ms ease-out; } .slider:hover::-webkit-slider-thumb, .slider:active::-webkit-slider-thumb { - box-shadow: 0 0 0 8px var(--cursor-bg-secondary); + box-shadow: 0 0 0 8px var(--line); } .slider::-moz-range-track { height: 4px; border-radius: 9999px; - background: var(--cursor-bg-tertiary); + background: var(--panel-3); } .slider::-moz-range-progress { height: 4px; border-radius: 9999px; - background: var(--text-secondary); + background: var(--dim); } .slider::-moz-range-thumb { width: 14px; height: 14px; border: 0; border-radius: 50%; - background: var(--text-secondary); + background: var(--dim); transition: box-shadow 140ms ease-out; } .slider:hover::-moz-range-thumb, .slider:active::-moz-range-thumb { - box-shadow: 0 0 0 8px var(--cursor-bg-secondary); + box-shadow: 0 0 0 8px var(--line); } .slider:disabled { opacity: 0.5; cursor: not-allowed; } - /* Quant-style pill: h-6 rounded-full px-2.5, ~11.5px text. The - accent variant reads the Cursor accent on its 8% tint. */ + /* Tag pill: 24px tall and fully rounded, 11px text, a line border on + the raised surface. A status reads as a tinted pill: the accent + variant carries the accent on its 8% fill under a 35% border. */ .pill { display: inline-flex; align-items: center; gap: 0.25rem; - height: 1.5rem; + height: var(--height-xs); padding-inline: 0.625rem; - border-radius: 9999px; - background: var(--cursor-bg-quaternary); - color: var(--text-primary); - font-size: 0.71875rem; + border: 1px solid var(--line); + border-radius: var(--radius-full); + background: var(--panel-2); + color: var(--dim); + font-size: var(--font-size-xs); font-weight: 500; white-space: nowrap; } .pill-accent { - background: var(--cursor-accent-subtle); - color: var(--cursor-accent); + border-color: color-mix(in srgb, var(--accent) var(--tint-line), transparent); + background: color-mix(in srgb, var(--accent) var(--tint-fill), transparent); + color: var(--accent); } - /* Chip/tag input: a wrapping soft field holding removable chips and - a bare inline text entry. */ + /* Chip/tag input: a wrapping bordered field, like .input, holding + removable chips and a bare inline text entry. */ .chip-input { display: flex; flex-wrap: wrap; align-items: center; gap: 0.375rem; min-height: 2.25rem; - padding: 0.375rem 0.875rem; - border: 1px solid transparent; - border-radius: 1.125rem; - background: var(--cursor-bg-quaternary); + padding: 0.375rem var(--space-3); + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--panel); } .chip-input:focus-within { - border-color: var(--cursor-stroke-primary); + border-color: var(--line-focus); } .chip-input > input { flex: 1; min-width: 6ch; border: 0; background: transparent; - color: var(--text-primary); + color: var(--text); } /* The container's focus-within stroke is the visible focus indicator for the inner entry; a second ring inside the field @@ -178,7 +181,7 @@ inset: -4px; } .chip-remove:hover { - background: var(--cursor-bg-secondary); + background: var(--line); } @media (prefers-reduced-motion: reduce) { diff --git a/crates/gateway/config-ui/ui/src/styles/layout.css b/crates/gateway/config-ui/ui/src/styles/layout.css index 973e69f72..61829a7a3 100644 --- a/crates/gateway/config-ui/ui/src/styles/layout.css +++ b/crates/gateway/config-ui/ui/src/styles/layout.css @@ -16,8 +16,8 @@ height: 2.25rem; padding-inline: 0.875rem; border-radius: 9999px; - background: var(--cursor-accent); - color: var(--cursor-text-invert); + background: var(--accent); + color: var(--on-accent); font-weight: 500; text-decoration: none; transform: translateY(-200%); @@ -35,8 +35,8 @@ gap: var(--space-2); min-height: var(--space-10); padding-inline: var(--space-3); - background: var(--bg-secondary); - border-bottom: 1px solid var(--border); + background: var(--chrome); + border-bottom: 1px solid var(--line); } /* The desk: the nav column and the content column, 48px apart. */ @@ -52,8 +52,8 @@ padding-block-end: var(--space-4); } - /* Settings nav [Cursor]: a sticky column of cells, 12px text and 12px - icons, a 6% fill for the hovered and the selected cell, and a hairline + /* Settings nav: a sticky column of cells, 12px text and 12px icons, the + raised surface for the hovered and the selected cell, and a soft line between groups. A narrow window keeps only the icons. */ .tab-nav { position: sticky; @@ -70,7 +70,7 @@ gap: var(--space-1-5); padding: 4px 6px; border-radius: var(--radius); - color: var(--cursor-text-secondary); + color: var(--dim); font-size: var(--font-size-sm); font-weight: 400; line-height: var(--line-height-sm); @@ -81,15 +81,15 @@ flex: none; } .tab:hover { - background: var(--cursor-bg-quaternary); + background: var(--panel-2); } .tab[aria-current="page"] { - background: var(--cursor-bg-quaternary); - color: var(--cursor-text-primary); + background: var(--panel-2); + color: var(--text); } .tab-divider { margin-block: var(--space-1); - border-top: 1px solid var(--cursor-stroke-tertiary); + border-top: 1px solid var(--line-soft); } @media (max-width: 709.98px) { .tab-nav { @@ -142,12 +142,12 @@ .split-list { flex: 0 1 clamp(460px, 32%, 620px); max-width: 44%; - border-right: 1px solid var(--border); + border-right: 1px solid var(--line); } } /* Persistent banners (pending changes, restart required): 12px cards with - a 12px 14px padding. The pending-changes banner is the accent at 8% with + a 12px 14px padding. The pending-changes banner is the accent tint with an accent border at 20%; the restart, warning, and danger callouts carry a 4px left bar. The explicit display beats the UA [hidden] rule, so the hidden state needs its own counterpart. */ @@ -156,10 +156,10 @@ align-items: center; gap: var(--space-3); padding: 12px 14px; - border: 1px solid var(--cursor-stroke-secondary); + border: 1px solid var(--line); border-radius: var(--radius-xl); - background: var(--cursor-bg-quaternary); - color: var(--cursor-text-primary); + background: var(--panel-2); + color: var(--text); } .banner[hidden] { display: none; @@ -169,8 +169,8 @@ margin-block-end: var(--space-3); } .banner-pending { - border-color: color-mix(in srgb, var(--cursor-accent) 20%, transparent); - background: var(--cursor-accent-subtle); + border-color: color-mix(in srgb, var(--accent) 20%, transparent); + background: var(--accent-subtle); } .banner-restart, .banner-warning, @@ -178,7 +178,7 @@ border-left-width: 4px; } .banner-restart { - border-left-color: var(--warning); + border-left-color: var(--warn); } .banner-danger { border-left-color: var(--danger); @@ -204,10 +204,10 @@ width: 320px; max-width: calc(100vw - 2rem); padding: var(--space-3) var(--space-4) 0; - border: 1px solid var(--cursor-stroke-secondary); + border: 1px solid var(--line); border-radius: var(--radius-xl); - background: var(--cursor-editor); - color: var(--cursor-text-primary); + background: var(--panel); + color: var(--text); animation: modal-form-open var(--duration-slower) var(--ease-out-cubic) both; } .modal > h2 { @@ -248,24 +248,24 @@ flex-direction: column; gap: 0.625rem; padding: var(--space-4); - border: 0; + border: 1px solid var(--line); border-radius: var(--radius-xl); - background: var(--cursor-bg-quinary); + background: var(--panel-2); } .metric-label { - color: var(--text-secondary); - font-size: 0.6875rem; + color: var(--faint); + font-size: var(--font-size-xs); font-weight: 600; - letter-spacing: 0.08em; + letter-spacing: 0.05em; text-transform: uppercase; } .metric-value { - font-family: var(--font-mono); - font-size: 0.875rem; + font-size: 1.25rem; + font-weight: 600; font-variant-numeric: tabular-nums; } .progress__fill.is-warning { - background: var(--warning); + background: var(--warn); } .progress__fill.is-danger { background: var(--danger); @@ -291,9 +291,9 @@ gap: 1rem; width: min(20rem, 100%); padding: 2rem; - border: 1px solid var(--border); - border-radius: 0.75rem; - background: var(--bg-secondary); + border: 1px solid var(--line); + border-radius: var(--radius-xl); + background: var(--panel-2); text-align: center; } .key-card form { @@ -309,7 +309,7 @@ input it describes. */ .field-error { margin: 0; - color: var(--cursor-danger); + color: var(--danger); font-size: var(--font-size-sm); line-height: var(--line-height-sm); } @@ -326,7 +326,7 @@ } .tab-version { font-size: 0.75rem; - color: var(--text-secondary); + color: var(--dim); } /* Connection dot: gray until the first API outcome arrives. The @@ -335,10 +335,10 @@ width: 0.5rem; height: 0.5rem; border-radius: 50%; - background: var(--text-secondary); + background: var(--dim); } .status-dot.is-ok { - background: var(--success); + background: var(--good); } .status-dot.is-bad { background: var(--danger); @@ -358,10 +358,10 @@ display: inline-flex; width: 1rem; margin-inline-end: 0.375rem; - color: var(--cursor-accent); + color: var(--accent); } .menu-item.is-pending { - background: var(--cursor-accent-subtle); + background: var(--accent-subtle); } /* Apply overlay activity: the one row holds the spinner and the @@ -379,14 +379,14 @@ display: flex; align-items: center; gap: 0.5rem; - color: var(--text-secondary); + color: var(--dim); } .stage.is-active, .stage.is-done { - color: var(--text-primary); + color: var(--text); } .stage.is-failed { - color: var(--danger-text); + color: var(--danger); } .stage-icon { display: inline-flex; @@ -398,8 +398,8 @@ .spinner { width: 14px; height: 14px; - border: 2px solid var(--cursor-bg-secondary); - border-top-color: var(--cursor-accent); + border: 2px solid var(--line); + border-top-color: var(--accent); border-radius: 50%; animation: spin 800ms linear infinite; } @@ -412,7 +412,7 @@ /* Stub view empty state. */ .page-empty { margin-block-start: 0.5rem; - color: var(--text-secondary); + color: var(--dim); } } @@ -430,8 +430,9 @@ flex: 1; min-width: 10rem; height: auto; - padding: 5px 12px; - border-radius: var(--radius-sm); + padding: 5px 14px; + border-radius: var(--radius-full); + background: var(--panel-2); } .models-refresh { display: inline-flex; @@ -443,30 +444,37 @@ border: 0; border-radius: var(--radius); background: transparent; - color: var(--cursor-text-secondary); + color: var(--dim); cursor: pointer; } .models-refresh:hover { - background: var(--cursor-bg-tertiary); - color: var(--cursor-text-primary); + background: var(--panel-3); + color: var(--text); } .models-refresh.is-loading svg { animation: spin 800ms linear infinite; } .filter-chips { - display: flex; - gap: 0.25rem; + display: inline-flex; + flex-wrap: wrap; + gap: var(--space-1); + padding: 3px; + border: 1px solid var(--line-soft); + border-radius: var(--radius-full); + background: var(--panel); } .filter-chip { - border: 1px solid transparent; + border: 0; + background: none; + color: var(--dim); cursor: pointer; } .filter-chip:hover { - background: var(--cursor-bg-tertiary); + color: var(--text); } .filter-chip[aria-pressed="true"] { - background: var(--cursor-accent-subtle); - border-color: var(--cursor-accent); + background: color-mix(in srgb, var(--accent) 14%, transparent); + color: var(--accent); } .filter-chip:disabled { opacity: 0.6; @@ -487,7 +495,7 @@ .model-list-empty { margin: 0; padding: var(--space-5) var(--space-3); - color: var(--cursor-text-tertiary); + color: var(--faint); text-align: center; } .model-row { @@ -495,12 +503,17 @@ align-items: center; gap: 0.5rem; padding: 0.625rem 0.75rem; - border-radius: var(--radius); - color: var(--text-primary); + border-inline-start: 2px solid transparent; + border-radius: var(--radius-sm); + color: var(--text); text-decoration: none; } .model-row[aria-current="true"] { - background: var(--cursor-accent-subtle); + border-inline-start-color: var(--accent); + background: var(--panel-2); + } + .model-row:hover { + background: var(--panel-2); } .model-name { font-family: var(--font-mono); @@ -510,7 +523,7 @@ .model-row .source-icon { display: inline-flex; margin-inline-start: auto; - color: var(--text-secondary); + color: var(--dim); } /* Loading skeletons: pulsing surface rectangles standing in for @@ -519,7 +532,7 @@ height: 2.5rem; margin-block-end: 0.5rem; border-radius: var(--radius); - background: var(--bg-tertiary); + background: var(--panel-3); animation: skeleton-pulse 1.2s ease-in-out infinite alternate; } @keyframes skeleton-pulse { @@ -536,14 +549,14 @@ .orphan-section { margin-block-start: 1rem; padding-block-start: 0.75rem; - border-block-start: 1px solid var(--border); + border-block-start: 1px solid var(--line); } .orphan-heading { margin: 0 0 0.5rem; - color: var(--text-secondary); - font-size: 0.6875rem; + color: var(--faint); + font-size: var(--font-size-xs); font-weight: 600; - letter-spacing: 0.08em; + letter-spacing: 0.05em; text-transform: uppercase; } .orphan-row { @@ -554,7 +567,7 @@ } .orphan-size { margin-inline-start: auto; - color: var(--text-secondary); + color: var(--dim); font-size: 0.75rem; font-variant-numeric: tabular-nums; } @@ -587,7 +600,7 @@ flex-direction: column; gap: 0.5rem; padding-block-end: 1rem; - border-block-end: 1px solid var(--border); + border-block-end: 1px solid var(--line); } /* The editable title: an input drawn as the heading until hover or focus reveals the field surface. */ @@ -597,14 +610,14 @@ border: 1px solid transparent; border-radius: var(--radius); background: transparent; - color: var(--text-primary); + color: var(--text); font-family: var(--font-mono); - font-size: 1.375rem; - font-weight: 600; + font-size: var(--font-size-title); + font-weight: var(--font-weight-title); } .detail-title:hover, .detail-title:focus-visible { - background: var(--cursor-bg-quaternary); + background: var(--panel-2); } .detail-meta { display: flex; @@ -612,7 +625,7 @@ gap: 0.5rem; } .detail-status { - color: var(--text-secondary); + color: var(--dim); font-size: 0.8125rem; } .detail-source { @@ -620,7 +633,7 @@ align-items: center; gap: 0.5rem; margin: 0; - color: var(--text-secondary); + color: var(--dim); } .detail-actions { display: flex; @@ -635,18 +648,18 @@ .file-cache-path { flex: 1; min-width: 12rem; - color: var(--text-secondary); + color: var(--dim); } .file-status[data-status="downloaded"] { - color: var(--success-text); + color: var(--good-text); } .file-status[data-status="missing"] { - color: var(--warning); + color: var(--warn); } .detail-section { padding-block: 0.75rem; - border-block-end: 1px solid var(--border); + border-block-end: 1px solid var(--line); } .section-heading { margin: 0; @@ -658,7 +671,7 @@ padding: 0.375rem 0; border: 0; background: none; - color: var(--text-primary); + color: var(--text); font-size: 0.9375rem; font-weight: 600; text-align: start; @@ -668,7 +681,7 @@ content: ""; margin-inline-end: 0.5rem; border-block: 4px solid transparent; - border-inline-start: 6px solid var(--text-secondary); + border-inline-start: 6px solid var(--dim); transition: transform 140ms ease-out; } .section-toggle[aria-expanded="true"]::before { @@ -685,7 +698,7 @@ margin-block: 0.25rem; } - /* One registry field, a Cursor Settings row: the label and its help text + /* One registry field, a settings row: the label and its help text stack on the left, the control sits on the right, and an alert for a bad entry runs under both. The row's children arrive in the order head, control, help, so the grid places them by role rather than by order. */ @@ -696,7 +709,7 @@ row-gap: var(--space-1); align-items: center; padding: var(--space-3); - border-block-end: 1px solid var(--cursor-stroke-quaternary); + border-block-end: 1px solid var(--line-soft); } .field-row:last-child { border-block-end: 0; @@ -738,7 +751,7 @@ } .field-help { margin: 0; - color: var(--cursor-text-secondary); + color: var(--dim); font-size: var(--font-size-base); } @@ -755,31 +768,39 @@ } .cloud-table { width: 100%; - border-collapse: collapse; + border: 1px solid var(--line); + border-collapse: separate; + border-spacing: 0; + border-radius: var(--radius-xl); + background: var(--panel-2); + overflow: hidden; font-size: 0.8125rem; } .cloud-table th, .cloud-table td { padding: 0.5rem 0.625rem; - border-block-end: 1px solid var(--border); + border-block-end: 1px solid var(--line); text-align: start; } .cloud-table thead th { - color: var(--text-secondary); - font-size: 0.6875rem; + color: var(--faint); + font-size: var(--font-size-xs); + font-weight: 600; + letter-spacing: 0.05em; text-transform: uppercase; + border-block-end-color: var(--line); } .cloud-name-id { display: block; - color: var(--text-secondary); + color: var(--dim); font-size: 0.75rem; } .cloud-variant-row .cloud-name-primary { - color: var(--text-secondary); + color: var(--dim); } .cloud-no-add { display: block; - color: var(--text-secondary); + color: var(--dim); font-size: 0.75rem; } .cloud-add__body { @@ -795,7 +816,7 @@ .cloud-add__label { display: grid; gap: var(--space-1); - color: var(--cursor-text-secondary); + color: var(--dim); font-size: var(--font-size-sm); } .cloud-add__label .input { @@ -820,7 +841,7 @@ } .chat-template-custom label, .chat-template-resolution dt { - color: var(--text-secondary); + color: var(--dim); font-size: 0.75rem; } .chat-template-resolution { @@ -835,7 +856,7 @@ } .chat-template-resolution dd { margin: 0; - color: var(--text-primary); + color: var(--text); font-size: 0.75rem; overflow-wrap: anywhere; } @@ -844,7 +865,7 @@ width: 0.375rem; height: 0.375rem; border-radius: 50%; - background: var(--cursor-accent); + background: var(--accent); } .field-reset { display: inline-flex; @@ -856,12 +877,12 @@ border: 0; border-radius: 50%; background: transparent; - color: var(--text-secondary); + color: var(--dim); cursor: pointer; } .field-reset:hover { - background: var(--cursor-bg-tertiary); - color: var(--text-primary); + background: var(--panel-3); + color: var(--text); } .slider-row { @@ -879,7 +900,7 @@ font-variant-numeric: tabular-nums; } .readout-suffix { - color: var(--text-secondary); + color: var(--dim); font-size: 0.75rem; font-variant-numeric: tabular-nums; white-space: nowrap; @@ -902,22 +923,25 @@ width: 100%; padding: 0.625rem 0.75rem; border: 0; - border-radius: var(--radius); + border-inline-start: 2px solid transparent; + border-radius: var(--radius-sm); background: none; - color: var(--text-primary); + color: var(--text); text-align: start; cursor: pointer; } .result-row:hover { - background: var(--bg-tertiary); + background: var(--panel-2); } .result-row[aria-pressed="true"] { - background: var(--cursor-accent-subtle); + border-inline-start-color: var(--accent); + background: var(--panel-2); } .result-avatar { flex-shrink: 0; - border-radius: 9px; - background: var(--bg-tertiary); + border: 1px solid var(--line-soft); + border-radius: var(--radius-lg); + background: var(--logo-tile); } .result-main { display: flex; @@ -931,7 +955,7 @@ align-items: center; gap: 0.625rem; margin: 0; - color: var(--text-secondary); + color: var(--dim); font-size: 0.6875rem; font-variant-numeric: tabular-nums; } @@ -946,8 +970,8 @@ .hub-detail-title { margin: 0; font-family: var(--font-mono); - font-size: 1.375rem; - font-weight: 600; + font-size: var(--font-size-title); + font-weight: var(--font-weight-title); overflow-wrap: anywhere; } .hub-publisher { @@ -955,11 +979,11 @@ align-items: center; gap: 0.375rem; margin: 0; - color: var(--text-secondary); + color: var(--dim); } .verified-badge { display: inline-flex; - color: var(--cursor-accent); + color: var(--accent); } .pill-row { display: flex; @@ -971,24 +995,30 @@ /* Quant picker table [Unsloth] quant selector + [LM Studio] fit. */ .quant-table { width: 100%; - border-collapse: collapse; + border: 1px solid var(--line); + border-collapse: separate; + border-spacing: 0; + border-radius: var(--radius-xl); + background: var(--panel-2); + overflow: hidden; font-size: 0.8125rem; } .quant-table th, .quant-table td { padding: 0.5rem 0.625rem; - border-block-end: 1px solid var(--border); + border-block-end: 1px solid var(--line); text-align: start; } .quant-table thead th { - color: var(--text-secondary); - font-size: 0.6875rem; + color: var(--faint); + font-size: var(--font-size-xs); font-weight: 600; - letter-spacing: 0.08em; + letter-spacing: 0.05em; text-transform: uppercase; + border-block-end-color: var(--line); } .quant-table .is-recommended { - background: var(--cursor-accent-subtle); + background: var(--accent-subtle); } .quant-name { display: flex; @@ -1010,33 +1040,42 @@ /* Fit badges: lightened text companions keep AA on the surfaces. */ .fit-badge { + border: 1px solid var(--line); + background: none; + color: var(--dim); white-space: nowrap; } .fit-badge[data-fit="gpu"] { - color: var(--success-text); + border-color: color-mix(in srgb, var(--good) var(--tint-line), transparent); + background: color-mix(in srgb, var(--good) var(--tint-fill), transparent); + color: var(--good-text); } .fit-badge[data-fit="partial"] { - color: var(--warning); + border-color: color-mix(in srgb, var(--warn) var(--tint-line), transparent); + background: color-mix(in srgb, var(--warn) var(--tint-fill), transparent); + color: var(--warn); } .fit-badge[data-fit="cpu"] { - color: var(--text-secondary); + color: var(--dim); } .fit-badge[data-fit="none"] { - color: var(--danger-text); + border-color: color-mix(in srgb, var(--danger) var(--tint-line), transparent); + background: color-mix(in srgb, var(--danger) var(--tint-fill), transparent); + color: var(--danger); } /* Rendered README below the detail card. */ .readme { margin-block-start: 1rem; padding-block-start: 1rem; - border-block-start: 1px solid var(--border); + border-block-start: 1px solid var(--line); } .readme-heading { margin: 0 0 0.5rem; - color: var(--text-secondary); - font-size: 0.6875rem; + color: var(--faint); + font-size: var(--font-size-xs); font-weight: 600; - letter-spacing: 0.08em; + letter-spacing: 0.05em; text-transform: uppercase; } .markdown { @@ -1054,9 +1093,10 @@ font-size: 1.125rem; } .markdown pre { - padding: 0.75rem; - border-radius: var(--radius); - background: var(--bg-tertiary); + padding: 0.75rem 0.875rem; + border: 1px solid var(--line); + border-radius: var(--radius-lg); + background: var(--panel-2); overflow-x: auto; } .markdown code { @@ -1067,12 +1107,12 @@ } .markdown :is(th, td) { padding: 0.375rem 0.625rem; - border: 1px solid var(--border); + border: 1px solid var(--line); } .markdown blockquote { padding-inline-start: 0.75rem; - border-inline-start: 3px solid var(--border); - color: var(--text-secondary); + border-inline-start: 3px solid var(--line); + color: var(--dim); } } @@ -1085,7 +1125,7 @@ flex-direction: column; } .banner-warning { - border-left-color: var(--warning); + border-left-color: var(--warn); } /* Nav column [Unsloth] settings category list: stacked pill links. */ @@ -1119,21 +1159,21 @@ } .settings-nav-link { display: block; - padding: 4px 6px; - border-radius: var(--radius); - color: var(--cursor-text-secondary); - font-size: var(--font-size-sm); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-sm); + color: var(--dim); + font-size: var(--font-size-base); font-weight: 400; line-height: var(--line-height-sm); text-decoration: none; } .settings-nav-link:hover { - background: var(--cursor-bg-quaternary); - color: var(--cursor-text-primary); + background: var(--panel-2); + color: var(--text); } .settings-nav-link[aria-current="true"] { - background: var(--cursor-bg-quaternary); - color: var(--cursor-text-primary); + background: var(--accent-subtle); + color: var(--accent); } .settings-panel { flex: 1; @@ -1143,9 +1183,9 @@ gap: 1rem; } - /* A settings section: its title (12px, secondary) sits above a 4% tinted - 12px card that has no border or padding of its own - its rows pad - themselves. The wrapper stays bare. */ + /* A settings section: its 15px title sits above a raised 12px card with + a line border and no padding of its own - its rows pad themselves. + The wrapper stays bare. */ .settings-card { padding: 0; border: 0; @@ -1155,17 +1195,18 @@ display: flex; align-items: center; gap: var(--space-2); - margin-block-end: var(--space-1-5); - color: var(--cursor-text-secondary); - font-size: var(--font-size-sm); - font-weight: 400; + margin-block-end: var(--space-2); + color: var(--text); + font-size: var(--font-size-lg); + font-weight: 600; line-height: var(--line-height-sm); } .settings-card > .section-body, .settings-card > .env-body { padding: 0; + border: 1px solid var(--line); border-radius: var(--radius-xl); - background: var(--cursor-bg-quinary); + background: var(--panel-2); overflow: hidden; } .settings-card > .env-body { @@ -1178,7 +1219,7 @@ /* System live extras. */ .metric-sub { margin: 0; - color: var(--text-secondary); + color: var(--dim); font-size: 0.75rem; } .metric-label svg { @@ -1192,16 +1233,16 @@ inset-block: -0.1875rem; inset-inline-start: calc(var(--progress) * 100%); width: 1px; - background: var(--border); + background: var(--line); } .vendor-chip { font-weight: 600; } .gpu-devices { padding: var(--space-4); - border: 0; + border: 1px solid var(--line); border-radius: var(--radius-xl); - background: var(--cursor-bg-quinary); + background: var(--panel-2); } .gpu-device-row { display: flex; @@ -1221,7 +1262,7 @@ gap: 0.5rem; } .secret-mask { - color: var(--text-secondary); + color: var(--dim); letter-spacing: 0.2em; } .secret-field .input { @@ -1238,7 +1279,7 @@ font-weight: 600; } .about-license { - color: var(--cursor-accent); + color: var(--blue-text); } } @@ -1274,17 +1315,19 @@ min-width: 0; padding: 0.5rem 0.75rem; border: 0; - border-radius: var(--radius); + border-inline-start: 2px solid transparent; + border-radius: var(--radius-sm); background: none; - color: var(--text-primary); + color: var(--text); text-align: start; cursor: pointer; } .profile-select:hover { - background: var(--bg-tertiary); + background: var(--panel-2); } .profile-select[aria-pressed="true"] { - background: var(--cursor-accent-subtle); + border-inline-start-color: var(--accent); + background: var(--panel-2); } /* The "No profile" row is not a checklist: a static label with its own Set Active instead of a pressable row that opens an editor. */ @@ -1294,7 +1337,7 @@ gap: 0.5rem; min-width: 0; padding: 0.5rem 0.75rem; - color: var(--text-secondary); + color: var(--dim); } .profile-none .set-active-none { margin-inline-start: auto; @@ -1305,12 +1348,13 @@ align-items: center; gap: 0.375rem; margin: 0; - color: var(--text-secondary); + color: var(--dim); font-size: 0.8125rem; } .pill-stale { - background: color-mix(in srgb, var(--warning) 20%, transparent); - color: var(--warning); + border-color: color-mix(in srgb, var(--warn) var(--tint-line), transparent); + background: color-mix(in srgb, var(--warn) var(--tint-fill), transparent); + color: var(--warn); } .profile-name { font-family: var(--font-mono); @@ -1321,10 +1365,10 @@ display: flex; flex-direction: column; gap: 0.75rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--bg-secondary); + padding: var(--space-4); + border: 1px solid var(--line); + border-radius: var(--radius-xl); + background: var(--panel-2); } .profile-summary-title { display: flex; @@ -1336,21 +1380,21 @@ } .profiles-heading { margin: 0; - color: var(--text-secondary); - font-size: 0.6875rem; + color: var(--faint); + font-size: var(--font-size-xs); font-weight: 600; - letter-spacing: 0.08em; + letter-spacing: 0.05em; text-transform: uppercase; } .start-from { margin: 0 0 0.75rem; padding: 0.625rem 0.75rem; - border: 1px solid var(--border); + border: 1px solid var(--line); border-radius: var(--radius); } .start-from legend { padding-inline: 0.25rem; - color: var(--text-secondary); + color: var(--dim); font-size: 0.75rem; } .radio-row { @@ -1379,7 +1423,7 @@ gap: var(--space-3); } .env-body .section-heading { - color: var(--cursor-text-secondary); + color: var(--dim); font-size: var(--font-size-sm); font-weight: 400; line-height: var(--line-height-sm); @@ -1395,7 +1439,7 @@ align-items: center; gap: var(--space-2); padding-block: var(--space-2); - border-block-end: 1px solid var(--cursor-stroke-quaternary); + border-block-end: 1px solid var(--line-soft); } .env-key { min-width: 12rem; @@ -1407,7 +1451,7 @@ font-family: var(--font); } .secret-state { - color: var(--cursor-text-tertiary); + color: var(--faint); font-size: var(--font-size-sm); } .secret-state[hidden] { @@ -1427,7 +1471,7 @@ } .env-used-by { flex-basis: 100%; - color: var(--text-secondary); + color: var(--dim); font-size: 0.75rem; } .env-add-row { @@ -1448,7 +1492,7 @@ } .hf-status { font-size: 0.8125rem; - color: var(--text-secondary); + color: var(--dim); } .review-modal { @@ -1463,13 +1507,13 @@ } .diff-table :is(th, td) { padding: 0.375rem 0.5rem; - border-block-end: 1px solid var(--border); + border-block-end: 1px solid var(--line); text-align: start; vertical-align: top; overflow-wrap: anywhere; } .diff-table thead th { - color: var(--text-secondary); + color: var(--dim); font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.08em; @@ -1480,10 +1524,10 @@ font-weight: 400; } .diff-running { - color: var(--text-secondary); + color: var(--dim); } .diff-pending { - color: var(--cursor-accent); + color: var(--accent); } .profile-editor-header, @@ -1515,7 +1559,7 @@ margin-block-end: 0.5rem; } .shuttle-count { - color: var(--text-secondary); + color: var(--dim); font-size: 0.75rem; } .shuttle-search { @@ -1527,7 +1571,7 @@ max-height: 22rem; margin: 0; padding: 0.25rem; - border: 1px solid var(--border-strong); + border: 1px solid var(--line); border-radius: var(--radius); list-style: none; overflow-y: auto; @@ -1545,10 +1589,10 @@ cursor: pointer; } .shuttle-option:hover { - background: var(--bg-tertiary); + background: var(--panel-3); } .shuttle-option[aria-selected="true"] { - background: var(--cursor-accent-subtle); + background: var(--accent-subtle); } .profile-kind { margin-inline-start: auto; @@ -1569,9 +1613,9 @@ } .vram-summary { padding: 0.75rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--cursor-bg-quinary); + border: 1px solid var(--line); + border-radius: var(--radius-xl); + background: var(--panel-2); } .vram-total, .vram-budget, @@ -1590,15 +1634,15 @@ border: 0; border-radius: 50%; background: transparent; - color: var(--text-secondary); + color: var(--dim); cursor: help; } .vram-unknown, .vram-budget[data-state="warning"] { - color: var(--warning); + color: var(--warn); } .vram-budget[data-state="over"] { - color: var(--danger-text); + color: var(--danger); } .discover-types { flex-basis: 100%; @@ -1624,6 +1668,9 @@ .has-status-bar .desk { padding-block-end: calc(var(--status-bar-height) + 1rem); } + .has-status-bar .tab-nav { + padding-block-end: calc(var(--status-bar-height) + var(--space-3)); + } /* Endpoint LEDs in the desk's indicators group: green ready, amber provisioning, gray unconfigured. The fills match the connection @@ -1642,13 +1689,13 @@ width: 0.5rem; height: 0.5rem; border-radius: 50%; - background: var(--text-secondary); + background: var(--dim); } .status-led[data-state="ready"] .status-led-dot { - background: var(--success); + background: var(--good); } .status-led[data-state="provisioning"] .status-led-dot { - background: var(--warning); + background: var(--warn); } .status-bar-summary { font-variant-numeric: tabular-nums; diff --git a/crates/gateway/config-ui/ui/src/styles/tokens.test.mjs b/crates/gateway/config-ui/ui/src/styles/tokens.test.mjs index f700108cb..7ee65903f 100644 --- a/crates/gateway/config-ui/ui/src/styles/tokens.test.mjs +++ b/crates/gateway/config-ui/ui/src/styles/tokens.test.mjs @@ -1,5 +1,5 @@ // Pins the CSS wiring end to end: the esbuild bundle must emit -// dist/app.css containing the shared Cursor Dark design tokens +// dist/app.css containing the style guide's design tokens // (shared-ui/tokens.css) and the cascade layer order, and // dist/index.html must link that stylesheet - a dropped import in // main.ts or a dropped would ship an unstyled UI without any @@ -18,13 +18,8 @@ const distDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", ". test("the bundled stylesheet defines the design tokens and layer order", async () => { const css = await readFile(path.join(distDir, "app.css"), "utf8"); // \s* tolerates minified output; /i tolerates hex case changes. - assert.match(css, /--cursor-base:\s*#F0F0F0/i, "the Cursor Dark primitive is defined"); - assert.match(css, /--accent:\s*#e4b570/i, "the Runvil gold accent token is defined"); - assert.match( - css, - /--bg-primary:\s*var\(--cursor-editor\)/, - "the page background aliases the Cursor editor surface", - ); + assert.match(css, /--panel:\s*#131520/i, "the page surface is defined"); + assert.match(css, /--accent:\s*#ff8a1e/i, "the orange accent token is defined"); assert.match( css, /@layer reset,\s*base,\s*components,\s*utilities/, @@ -32,8 +27,8 @@ test("the bundled stylesheet defines the design tokens and layer order", async ( ); assert.match( css, - /textarea\.input\s*\{[^}]*height:\s*auto[^}]*border-radius:\s*0?\.75rem/, - "multiline inputs keep the rounded rectangle radius and natural height", + /textarea\.input\s*\{[^}]*height:\s*auto[^}]*border-radius:\s*var\(--radius\)/, + "multiline inputs keep the field radius and natural height", ); assert.match( css, @@ -51,79 +46,85 @@ test("the desk page links the bundled stylesheet", async () => { ); }); -// The gateway sheet carries the same IDE values as @workshop/look, because -// the two UIs are copies of one Cursor look. Hex literals compare without -// case; a value written as a formula compares as written. -const IDE_TOKENS = { - "--cursor-red": "#E34671", - "--cursor-blue": "#81A1C1", - "--cursor-cyan": "#88C0D0", - "--cursor-accent": "#81A1C1", - "--cursor-sidebar": "#141414", - "--cursor-shadow-primary": "#00000066", - "--cursor-bg-active": "#F0F0F01E", - "--cursor-bg-focused": "#F0F0F01E", - "--cursor-text-link": "#81A1C1", - "--cursor-text-invert": "#181818", - "--radius-xs": "2px", +// The palette is the Gateway UI style guide's, value for value. Hex +// literals compare without case. +const STYLE_GUIDE_TOKENS = { + "--chrome": "#0f1118", + "--panel": "#131520", + "--panel-2": "#171a26", + "--panel-3": "#1c2030", + "--line": "#262b3a", + "--line-soft": "#20242f", + "--line-focus": "#33405e", + "--text": "#eceef4", + "--dim": "#9aa2b4", + "--faint": "#697082", + "--accent": "#ff8a1e", + "--accent-dim": "#8a4f16", + "--good": "#4bbf73", + "--good-text": "#5fd08a", + "--blue": "#4d8dfe", + "--blue-text": "#6ea6ff", + "--warn": "#e8b06a", + "--danger": "#e06a6a", + "--logo-tile": "#0c0e14", + "--font-size-root": "14.5px", + "--radius": "9px", + "--radius-xl": "12px", }; -test("the bundled stylesheet carries the IDE token values", async () => { +test("the bundled stylesheet carries the style guide's palette", async () => { const css = await readFile(path.join(distDir, "app.css"), "utf8"); - for (const [name, value] of Object.entries(IDE_TOKENS)) { + for (const [name, value] of Object.entries(STYLE_GUIDE_TOKENS)) { assert.match(css, new RegExp(`${name}:\\s*${value}\\s*;`, "i"), `${name} is ${value}`); } // The minifier writes 300ms as .3s, so the duration is pinned by value. assert.match(css, /--duration-slower:\s*(300ms|\.3s)\s*;/, "the slow transition is 300ms"); - assert.match( - css, - /--cursor-shadow-secondary:\s*color-mix\(in srgb,\s*var\(--cursor-shadow-primary\) 60%,\s*transparent\)/, - "the secondary shadow ink is 60% of the widget shadow", - ); - assert.match( - css, - /--cursor-shadow-tertiary:\s*color-mix\(in srgb,\s*var\(--cursor-shadow-primary\) 30%,\s*transparent\)/, - "the tertiary shadow ink is 30% of the widget shadow", - ); - assert.match( - css, - /--cursor-accent-hover:\s*color-mix\(in srgb,\s*white 10%,\s*var\(--cursor-accent\)\)/, - "the accent hover mixes 10% white into the accent", - ); - assert.match( - css, - /--code-font:\s*Consolas, Menlo, Monaco, "Droid Sans Mono", "Courier New", monospace/, - "the code font is Cursor's stack", - ); + assert.doesNotMatch(css, /--cursor-/, "no Cursor Dark token survives"); }); -test("the tertiary and quaternary text names are the Cursor tiers, not swapped literals", async () => { - const css = await readFile(path.join(distDir, "app.css"), "utf8"); - assert.match(css, /--text-tertiary:\s*var\(--cursor-text-tertiary\)\s*;/); - assert.match(css, /--text-quaternary:\s*var\(--cursor-text-quaternary\)\s*;/); - assert.match(css, /--cursor-text-tertiary:\s*color-mix\(in srgb,\s*var\(--cursor-base\) 60%/); - assert.match(css, /--cursor-text-quaternary:\s*color-mix\(in srgb,\s*var\(--cursor-base\) 36%/); +test("the root size is the style guide's 14.5px", async () => { + const html = await bundledDeclarations("html"); + assert.equal(html.get("font-size"), "var(--font-size-root)"); }); -test("buttons take Cursor's 24px control values and the accent primary", async () => { +test("the primary button is the orange wash, the rest neutral raised, danger a red tint", async () => { const button = await bundledDeclarations(".button"); - assert.equal(button.get("height"), "var(--height-sm)", "24px tall"); - assert.equal(button.get("padding-inline"), "var(--space-2)", "8px padding"); - assert.equal(button.get("border-radius"), "var(--radius)", "a 6px radius"); - assert.equal(button.get("font-size"), "var(--font-size-base)", "13px"); - assert.equal(button.get("font-weight"), "400"); + assert.equal(button.get("height"), "var(--height-sm)", "28px tall"); + assert.equal(button.get("padding-inline"), "var(--space-3)", "12px padding"); + assert.equal(button.get("border-radius"), "var(--radius)", "a 9px radius"); + assert.equal(button.get("color"), "var(--dim)"); const small = await bundledDeclarations(".button-sm"); - assert.equal(small.get("height"), "var(--height-xs)", "20px tall"); - assert.equal(small.get("border-radius"), "var(--radius-sm)", "a 4px radius"); + assert.equal(small.get("height"), "var(--height-xs)", "24px tall"); + assert.equal(small.get("border-radius"), "var(--radius-sm)", "an 8px radius"); const primary = await bundledDeclarations(".button-primary"); - assert.equal(primary.get("background"), "var(--cursor-accent)", "the accent replaces the gold"); - assert.equal(primary.get("color"), "var(--cursor-text-invert)"); - const hover = await bundledDeclarations(".button-primary:hover"); - assert.equal(hover.get("background"), "var(--cursor-accent-hover)"); - const secondary = await bundledDeclarations(".button-secondary"); - const secondaryHover = await bundledDeclarations(".button-secondary:hover"); - assert.equal(secondary.get("background"), "var(--cursor-bg-tertiary)", "an 8% fill"); - assert.equal(secondaryHover.get("background"), "var(--cursor-bg-secondary)", "a 14% hover"); + assert.equal(squash(primary.get("border-color")), "var(--accent-dim)"); + assert.match(squash(primary.get("background")), /^linear-gradient\(180deg,color-mix\(insrgb,var\(--accent\)18%,transparent\)/); + assert.equal(primary.get("color"), "var(--accent)"); + const neutral = await bundledDeclarations(".button-outline"); + assert.equal(neutral.get("border-color"), "var(--line)"); + assert.equal(neutral.get("background"), "var(--panel-2)"); + const neutralHover = await bundledDeclarations(".button-outline:hover"); + assert.equal(neutralHover.get("background"), "var(--panel-3)", "a hover steps the surface up"); + const danger = await bundledDeclarations(".button-danger"); + assert.equal(danger.get("color"), "var(--danger)"); + const disabled = await bundledDeclarations(".button:disabled"); + assert.match(disabled.get("opacity"), /^0?\.5$/, "the minifier may drop the leading zero"); + for (const selector of [".button-primary:disabled", ".button-danger:disabled"]) { + const disabledAction = await bundledDeclarations(selector); + assert.equal(disabledAction.get("border-color"), "var(--line)", `${selector} loses its family border`); + assert.equal(disabledAction.get("background"), "var(--panel-2)", `${selector} loses its wash`); + assert.equal(disabledAction.get("color"), "var(--faint)", `${selector} loses its text color`); + } +}); + +test("fields are bordered and light their border on focus", async () => { + const input = await bundledDeclarations(".input"); + assert.equal(squash(input.get("border")), "1pxsolidvar(--line)"); + assert.equal(input.get("border-radius"), "var(--radius)"); + assert.equal(input.get("background"), "var(--panel)"); + const focus = await bundledDeclarations(".input:focus"); + assert.equal(focus.get("border-color"), "var(--line-focus)"); }); test("the switch is the single shared 32x20 track with a 16px thumb that moves 12px", async () => { @@ -131,10 +132,10 @@ test("the switch is the single shared 32x20 track with a 16px thumb that moves 1 const track = await bundledDeclarations(".switch"); assert.equal(track.get("width"), "32px"); assert.equal(track.get("height"), "20px"); - assert.equal(track.get("background"), "var(--cursor-bg-secondary)", "14% white when off"); + assert.equal(track.get("background"), "var(--line)", "the line color when off"); assert.equal(squash(track.get("transition")), "background-colorvar(--duration-slow)ease"); const on = await bundledDeclarations(".switch[aria-checked=true]"); - assert.equal(on.get("background"), "var(--cursor-green)"); + assert.equal(on.get("background"), "var(--good)"); const thumb = await bundledDeclarations(".switch::after"); assert.equal(thumb.get("width"), "16px"); assert.equal(thumb.get("height"), "16px"); @@ -149,15 +150,16 @@ test("modals take the form modal values", async () => { const dialog = await bundledDeclarations(".modal-dialog"); assert.equal(dialog.get("width"), "320px", "a 320px card"); assert.equal(dialog.get("border-radius"), "var(--radius-xl)", "a 12px radius"); - assert.equal(squash(dialog.get("border")), "1pxsolidvar(--cursor-stroke-secondary)", "a 12% stroke"); + assert.equal(squash(dialog.get("border")), "1pxsolidvar(--line)", "a line border"); + assert.equal(dialog.get("background"), "var(--panel-2)", "the raised surface"); assert.equal(dialog.get("box-shadow"), "none"); assert.match(dialog.get("animation") ?? "", /var\(--duration-slower\)/, "opens in 300ms"); const title = await bundledDeclarations(".modal-dialog > h2"); assert.equal(squash(title.get("padding")), "var(--space-3)var(--space-4)", "header 12px 16px"); - assert.equal(title.get("font-size"), "var(--font-size-lg)", "a 14px title"); + assert.equal(title.get("font-size"), "var(--font-size-lg)", "a 15px title"); const actions = await bundledDeclarations(".modal-actions"); assert.equal(actions.get("padding"), "10px", "footer padding 10px"); - assert.equal(squash(actions.get("border-top")), "1pxsolidvar(--cursor-stroke-tertiary)", "an 8% top border"); + assert.equal(squash(actions.get("border-top")), "1pxsolidvar(--line-soft)", "a soft top divider"); }); test("both modal families drop the scale-in under reduced motion", async () => { diff --git a/crates/shared-ui/controls.css b/crates/shared-ui/controls.css index 450f0aa24..f6385dac3 100644 --- a/crates/shared-ui/controls.css +++ b/crates/shared-ui/controls.css @@ -1,31 +1,31 @@ -/* Button, input, select, and switch base classes shared by both UIs, on - the Cursor Dark tokens from tokens.css. The rules live in the components - cascade layer: a consuming UI that declares its own layer order (the - gateway's base.css declares `reset, base, components, utilities`) - keeps its overrides winning on specificity, and a consuming UI with - no layers lets its unlayered rules override these. Interactive classes - inherit the consuming UI's focus convention - no focus frames are - added here. +/* Button, input, select, and switch base classes, on the tokens from + tokens.css. The rules live in the components cascade layer: a + consuming UI that declares its own layer order (the gateway's base.css + declares `reset, base, components, utilities`) keeps its overrides + winning on specificity, and a consuming UI with no layers lets its + unlayered rules override these. Interactive classes inherit the + consuming UI's focus convention - no focus frames are added here. - The buttons and the switch carry Cursor's control values, the same ones - @workshop/look uses; the input and select keep the gateway's own soft - surface. */ + The values are the Gateway UI style guide's: one orange primary action + per view, neutral raised buttons for the rest, a red tint for + destructive actions, and bordered fields that light their border on + focus. */ @layer components { - /* Buttons: 24px tall, 8px padding, 6px radius, 13px weight 400. -sm is - 20px tall with a 4px radius; -xs is the same size, kept for the - gateway's compact rows. A variant class picks the fill. */ + /* Buttons: 28px tall, 12px padding, 9px radius. -sm and -xs are 24px + tall with an 8px radius; -xs also drops to 12px text. A variant + class picks the fill; a bare .button is a borderless text button. */ .button { display: inline-flex; align-items: center; justify-content: center; - gap: var(--space-1); + gap: var(--space-1-5); height: var(--height-sm); - padding-inline: var(--space-2); + padding-inline: var(--space-3); border: 1px solid transparent; border-radius: var(--radius); background: transparent; - color: var(--cursor-text-primary); + color: var(--dim); font-size: var(--font-size-base); font-weight: 400; white-space: nowrap; @@ -33,97 +33,118 @@ transition: background-color var(--duration-normal) ease-out, color var(--duration-normal) ease-out, border-color var(--duration-normal) ease-out; } + .button:hover { + color: var(--text); + } .button:disabled { - border-color: transparent; - background: var(--cursor-control-disabled-bg); - color: var(--cursor-control-disabled-text); + opacity: 0.5; pointer-events: none; } .button-sm, .button-xs { height: var(--height-xs); + padding-inline: var(--space-2); border-radius: var(--radius-sm); } .button-xs { font-size: var(--font-size-sm); } - /* Dark label on Cursor's accent; hover mixes 10% white into it. */ + /* The primary action: an orange wash over the dim accent border, with + accent text. */ .button-primary { - background: var(--cursor-accent); - color: var(--cursor-text-invert); + border-color: var(--accent-dim); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--accent) 18%, transparent), + color-mix(in srgb, var(--accent) 6%, transparent) + ); + color: var(--accent); + font-weight: 600; } .button-primary:hover { - background: var(--cursor-accent-hover); + border-color: var(--accent); + color: var(--accent-hover); } - .button-secondary { - background: var(--cursor-bg-tertiary); + /* Neutral raised buttons: a hover steps the surface up one level and + lifts the text. */ + .button-secondary, + .button-outline { + border-color: var(--line); + background: var(--panel-2); } - .button-secondary:hover { - background: var(--cursor-bg-secondary); + .button-secondary:hover, + .button-outline:hover { + background: var(--panel-3); } .button-ghost { background: transparent; } .button-ghost:hover { - background: var(--cursor-bg-tertiary); - } - .button-outline { - border-color: var(--cursor-stroke-secondary); - background: transparent; - } - .button-outline:hover { - background: var(--cursor-bg-quaternary); + background: var(--panel-2); } .button-danger { - border-color: var(--danger); - background: color-mix(in srgb, var(--danger) 10%, transparent); - color: var(--danger-text); + border-color: color-mix(in srgb, var(--danger) 30%, transparent); + background: color-mix(in srgb, var(--danger) 7%, transparent); + color: var(--danger); } .button-danger:hover { - background: color-mix(in srgb, var(--danger) 15%, transparent); + background: color-mix(in srgb, var(--danger) 14%, transparent); + color: var(--danger); + } + /* A disabled action drops its family color: at half opacity alone, an + orange or red wash still reads as ready to press. */ + .button-primary:disabled, + .button-danger:disabled { + border-color: var(--line); + background: var(--panel-2); + color: var(--faint); } - /* Text input: rounded-full on the Cursor input surface. */ + /* Text input: a bordered field on the page surface; focus lights the + border. */ .input { height: 2.25rem; - padding-inline: 0.875rem; - border: 1px solid transparent; - border-radius: 9999px; - background: var(--cursor-bg-input-surface); - color: var(--text-primary); - font-size: 0.8125rem; + padding-inline: var(--space-3); + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--panel); + color: var(--text); + font-size: var(--font-size-base); } .input::placeholder { - color: var(--input-placeholder); + color: var(--faint); } input::-ms-reveal { display: none; } - .input:hover { - background: var(--cursor-bg-tertiary); + .input:focus { + border-color: var(--line-focus); + } + .input:disabled { + opacity: 0.45; } .input[aria-invalid="true"] { - border-color: var(--cursor-danger); + border-color: color-mix(in srgb, var(--danger) 60%, transparent); } textarea.input { height: auto; padding-block: 0.625rem; - border-radius: 0.75rem; + border-radius: var(--radius); } - /* Select trigger: same pill-and-soft-surface family as .input. */ + /* Select trigger: the same bordered field as .input. */ .select { display: inline-flex; align-items: center; justify-content: space-between; gap: 0.5rem; height: 2.25rem; - padding-inline: 0.875rem; - border: 1px solid transparent; - border-radius: 9999px; - background: var(--cursor-bg-input-surface); - color: var(--text-primary); - font-size: 0.8125rem; + padding-inline: var(--space-3); + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--panel); + color: var(--text); + font-size: var(--font-size-base); cursor: pointer; } .select::after { @@ -135,7 +156,7 @@ transform: translateY(-0.125rem) rotate(45deg); } .select:hover { - background: var(--cursor-bg-tertiary); + background: var(--panel-2); } .select:disabled { opacity: 0.5; @@ -145,8 +166,8 @@ height: 2rem; } - /* Switch, on a