setOpen(!open)} aria-expanded={open}>
-
+
{tokenValueLabel(effective, colorScheme) || "theme default"}
@@ -263,58 +294,72 @@ function ColorControl({ token, value, ctx, resolved, onChange }: {
{open && (
- {!COMMON_COLORS_KEYS.includes(token.key as (typeof COMMON_COLORS_KEYS)[number]) && (
+
+ setPickerTab("common")}>
+ Common
+
+ setPickerTab("custom")}>
+ Pick color
+
+
+ {pickerTab === "common" ? (
<>
-
Common — brings its own light & dark
-
- {COMMON_COLORS_KEYS.map((key) => (
-
onChange(`tokens.${key}`)}>
-
-
- ))}
+ {!COMMON_COLORS_KEYS.includes(token.key as (typeof COMMON_COLORS_KEYS)[number]) && (
+
+
+ {COMMON_COLORS_KEYS.map((key) => (
+ onChange(`tokens.${key}`)}>
+
+
+ ))}
+
+
+ )}
+
+
+ {base.map(([k, v]) => (
+ pick(`colors.${k}`)}>
+
+
+ ))}
+
+
+ {(["palette", "primary"] as const).map((name) => ramp(name).length > 0 && (
+
+
+ {ramp(name).map(([step, v]) => (
+ pick(`colors.${name}.${step}`)}>
+
+
+ ))}
+
+
+ ))}
+ >
+ ) : (
+ <>
+
Applies to {colorScheme} mode only
+
+ pick(e.target.value)} />
+ e.target.value && pick(e.target.value)} />
>
)}
-
Base
-
- {base.map(([k, v]) => (
- pick(`colors.${k}`)}>
-
-
- ))}
-
- {(["primary", "palette"] as const).map((name) => ramp(name).length > 0 && (
-
-
{name === "primary" ? "Primary" : "Palette"}
-
- {ramp(name).map(([step, v]) => (
- pick(`colors.${name}.${step}`)}>
-
-
- ))}
-
-
- ))}
-
Custom — {colorScheme} only
-
- pick(e.target.value)} />
- e.target.value && pick(e.target.value)} />
-
)}
@@ -406,16 +451,21 @@ export function DensitySizeControl({ token, ctx, value, resolved, effectiveRef,
// `controlBorder`, not `border`, on anything meant to read as an outline: the
// panel is `surfaceRaised`, and dark `border` *is* `surfaceRaised` (theme.ts).
-const rowWrap: React.CSSProperties = { marginBottom: ui.space(3) };
-const rowHead: React.CSSProperties = {
- display: "flex", alignItems: "center", justifyContent: "space-between",
- gap: ui.space(2), marginBottom: ui.space(1),
+/** Theme Builder's two columns: label + ⓘ left, a fixed control column right. */
+const rowWrap: React.CSSProperties = {
+ display: "grid", gridTemplateColumns: "minmax(0, 1fr) 176px", alignItems: "center",
+ gap: ui.space(2), marginBottom: ui.space(2),
};
-const rowLabel: React.CSSProperties = { fontSize: 13, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" };
+const rowLabelCell: React.CSSProperties = {
+ display: "inline-flex", alignItems: "center", gap: ui.space(1), minWidth: 0,
+};
+/** The popovers' positioning context — each control column anchors its own. */
+const rowControl: React.CSSProperties = { position: "relative", minWidth: 0 };
+const anchor: React.CSSProperties = { position: "relative" };
+const rowLabel: React.CSSProperties = { ...ui.type.base, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" };
const ellipsis: React.CSSProperties = { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" };
-const hint: React.CSSProperties = { fontSize: 12, color: ui.color.textMuted, marginTop: ui.space(1) };
const control: React.CSSProperties = {
- flex: 1, minWidth: 0, width: "100%", fontFamily: ui.font.ui, fontSize: 13,
+ flex: 1, minWidth: 0, width: "100%", fontFamily: ui.font.ui, ...ui.type.base,
padding: `${ui.space(1)} ${ui.space(2)}`,
border: `1px solid ${ui.color.controlBorder}`, borderRadius: ui.radius.sm,
color: ui.color.text, background: ui.color.surface,
@@ -428,18 +478,36 @@ const resetBtn: React.CSSProperties = {
border: "none", background: "none", color: ui.color.accentText, fontSize: 12,
cursor: "pointer", padding: `0 ${ui.space(1)}`, flex: "0 0 auto",
};
+/** Floating, Theme Builder-style: anchored under the trigger, right-aligned so
+ * it can be wider than the control column without leaving the 400px drawer.
+ * `surfaceRaised` + the popover shadow — the recipe every shell popover uses. */
const popover: React.CSSProperties = {
- marginTop: ui.space(2), padding: ui.space(2),
+ position: "absolute", top: "calc(100% + 4px)", right: 0, zIndex: 30,
+ width: 288, maxWidth: "80vw", padding: ui.space(2),
border: `1px solid ${ui.color.controlBorder}`, borderRadius: ui.radius.md,
- background: ui.color.surfaceMuted,
+ background: ui.color.surfaceRaised, boxShadow: ui.shadow.popover,
+};
+/** Theme Builder's segmented controller: a muted track, the selected segment a
+ * raised pill — not the accent-filled buttons this used to draw. */
+const segmented: React.CSSProperties = {
+ display: "flex", gap: ui.space(1), padding: ui.space(1),
+ background: ui.color.surfaceMuted, borderRadius: ui.radius.md,
+ marginBottom: ui.space(2),
};
-const segmented: React.CSSProperties = { display: "flex", gap: ui.space(1), marginBottom: ui.space(2) };
const segment = (on: boolean): React.CSSProperties => ({
- flex: 1, fontSize: 12, padding: `${ui.space(1)} 0`, cursor: "pointer", borderRadius: ui.radius.sm,
- border: `1px solid ${on ? ui.color.accent : ui.color.controlBorder}`,
- background: on ? ui.color.accent : ui.color.surface,
- color: on ? ui.color.accentContrast : ui.color.text,
+ flex: 1, ...ui.type.base, padding: `${ui.space(1)} 0`, cursor: "pointer", borderRadius: ui.radius.sm,
+ textTransform: "capitalize", fontFamily: ui.font.ui,
+ border: `1px solid ${on ? ui.color.controlBorder : "transparent"}`,
+ background: on ? ui.color.surfaceRaised : "transparent",
+ color: on ? ui.color.text : ui.color.textMuted,
+ fontWeight: on ? 600 : 400,
});
+/** The collapsible group header inside the colour dropdown. */
+const groupToggle: React.CSSProperties = {
+ display: "inline-flex", alignItems: "center", gap: ui.space(1),
+ margin: `${ui.space(1)} 0`, padding: 0, border: "none", background: "none",
+ fontFamily: ui.font.ui, ...ui.type.base, color: ui.color.text, cursor: "pointer",
+};
const scrollList: React.CSSProperties = { maxHeight: 190, overflowY: "auto" };
/** The resolved value trailing a scale entry. `textMuted`, not the old
* `opacity: 0.6` — it has to stay legible on the hover fill. */
@@ -460,14 +528,12 @@ const swatch: React.CSSProperties = {
width: 16, height: 16, borderRadius: ui.radius.sm, flex: "0 0 16px",
border: `1px solid ${ui.color.controlBorder}`, display: "inline-block",
};
+/** The dropdown's pick targets — Theme Builder draws these at ~28px, and a 16px
+ * square is a hard thing to hit with a mouse. */
+const swatchLg: React.CSSProperties = { ...swatch, width: 24, height: 24, flex: "0 0 24px" };
/** Same split as `listItem` — `.hot-swatch-btn` carries the border colour. */
const swatchBtn: React.CSSProperties = {
padding: 2, borderRadius: ui.radius.sm, cursor: "pointer", lineHeight: 0,
borderWidth: 2, borderStyle: "solid", background: "none",
};
-const swatchInput: React.CSSProperties = {
- width: 30, flex: "0 0 30px", padding: 0, height: 26,
- border: `1px solid ${ui.color.controlBorder}`, borderRadius: ui.radius.sm,
- background: ui.color.surface, cursor: "pointer",
-};
const unitTag: React.CSSProperties = { fontSize: 12, color: ui.color.textMuted };
diff --git a/runner/e2e/panels.spec.ts b/runner/e2e/panels.spec.ts
index ac7dddb2f..4e706563d 100644
--- a/runner/e2e/panels.spec.ts
+++ b/runner/e2e/panels.spec.ts
@@ -69,23 +69,31 @@ test.describe("drawer chrome", () => {
expect(style?.width).toBe(400);
});
+ // The full-height drawer (docs-assistant geometry) covers the top bar, so with a
+ // panel open the other trigger is under it and a pointer click is intercepted —
+ // by design. The drawer traps nothing, so the triggers stay keyboard-reachable,
+ // and keyboard activation is the path these two tests now take. The mutual
+ // exclusion they prove is unchanged: App.tsx closes one panel as it opens the other.
+
test("only one drawer is open at a time", async ({ page }) => {
await openPlayground(page, "light");
await page.getByRole("button", { name: "Ask AI", exact: true }).click();
await expect(page.locator(CHAT)).toBeVisible();
- await page.getByRole("button", { name: "Style", exact: true }).click();
+ await page.getByRole("button", { name: "Style", exact: true }).focus();
+ await page.keyboard.press("Enter");
await expect(page.locator(STYLE)).toBeVisible();
await expect(page.locator(CHAT)).toHaveCount(0);
});
- test("swapping drawers leaves focus on the trigger that was clicked", async ({ page }) => {
+ test("swapping drawers leaves focus on the trigger that was activated", async ({ page }) => {
await openPlayground(page, "light");
const askAi = page.getByRole("button", { name: "Ask AI", exact: true });
const style = page.getByRole("button", { name: "Style", exact: true });
await askAi.click();
- await style.click();
+ await style.focus();
+ await page.keyboard.press("Enter");
await expect(page.locator(STYLE)).toBeVisible();
// The closing drawer used to focus *its* trigger on unmount — which happens
@@ -196,7 +204,11 @@ test.describe("dark mode legibility", () => {
await page.getByRole("button", { name: "Ask AI", exact: true }).click();
const textarea = await paint(page, `${CHAT} textarea`);
expect(textarea.background).not.toBe("rgb(255, 255, 255)");
- expect(textarea.background).toBe("rgb(7, 6, 4)");
+ // Transparent by design since the docs-assistant restyle (`.da-input`): the
+ // composer is flush with the panel, so what must not be white is the fill
+ // actually behind it — the drawer's own `surfaceRaised`.
+ expect(textarea.background).toBe("rgba(0, 0, 0, 0)");
+ expect(textarea.parentBackground).toBe("rgb(34, 34, 34)");
});
test("control outlines are visible against the surface they sit on", async ({ page }) => {
@@ -281,9 +293,10 @@ test.describe("chat transcript", () => {
const chipLocator = page.locator(`${CHAT} code`, { hasText: "src/index.tsx" });
const chip = await paint(page, chipLocator);
expect(chip.borderColor).toBe("rgb(53, 53, 53)");
- // And the edit box around it — the nearest ancestor that draws a border —
- // is the other half of the same hairline: same token, same failure mode.
- const editBox = await paint(page, chipLocator.locator('xpath=ancestor::div[contains(@style, "border")][1]'));
+ // And the edit box around it — the card the chip sits in — is the other half
+ // of the same hairline: same token, same failure mode. By class since the
+ // panel moved off inline styles (panels.css).
+ const editBox = await paint(page, chipLocator.locator('xpath=ancestor::div[contains(@class, "hot-chat-edit-box")][1]'));
expect(editBox.borderColor).toBe("rgb(53, 53, 53)");
expect(editBox.borderColor).not.toBe(editBox.parentBackground);
diff --git a/runner/e2e/style-panel.spec.ts b/runner/e2e/style-panel.spec.ts
index 32f716814..9045704a0 100644
--- a/runner/e2e/style-panel.spec.ts
+++ b/runner/e2e/style-panel.spec.ts
@@ -42,13 +42,12 @@ function state(page: Page) {
return page.evaluate(() => JSON.parse(localStorage.getItem("hot-runner-theme") ?? "null"));
}
-/** A TokenControl row, addressed by the one stable handle it has: the label
- * span's `title`, which carries the token key (labels repeat — six rows are
- * called "Background Color"). */
+/** A TokenControl row, addressed by `data-token` — the documented test contract
+ * on the row (labels repeat — six rows are called "Background Color"). This
+ * used to walk the row's internal structure (`> div > span[title]`), which the
+ * Theme Builder relayout broke; the attribute is the hook that can't. */
function tokenRow(page: Page, key: string) {
- return page.locator(`${STYLE} div`).filter({
- has: page.locator(`> div > span[title="${key}"]`),
- }).last();
+ return page.locator(`${STYLE} [data-token="${key}"]`);
}
/** The module row landing in the file tree is the apply signal — the panel
diff --git a/runner/packages/editor-shell/src/AccountMenu.tsx b/runner/packages/editor-shell/src/AccountMenu.tsx
index edd794b0f..99efcb97c 100644
--- a/runner/packages/editor-shell/src/AccountMenu.tsx
+++ b/runner/packages/editor-shell/src/AccountMenu.tsx
@@ -224,7 +224,7 @@ const menuRow = (disabled?: boolean): CSSProperties => ({
color: disabled ? theme.color.textMuted : theme.color.text,
opacity: disabled ? 0.5 : 1,
fontFamily: theme.font.ui,
- fontSize: 13,
+ ...theme.type.base,
textAlign: "left",
whiteSpace: "nowrap",
cursor: disabled ? "default" : "pointer",
diff --git a/runner/packages/editor-shell/src/BoxInfo.tsx b/runner/packages/editor-shell/src/BoxInfo.tsx
index d6e7052b1..94b156b2e 100644
--- a/runner/packages/editor-shell/src/BoxInfo.tsx
+++ b/runner/packages/editor-shell/src/BoxInfo.tsx
@@ -127,8 +127,9 @@ const section: CSSProperties = {
const body: CSSProperties = {
display: "flex",
flexDirection: "column",
- // 10px is the design's own step here and lands off the 4px scale on purpose.
- gap: 10,
+ // The frame draws 10 here (48:6755); snapped to the 8px rhythm — the project
+ // spaces on the 4px grid and 10 was one of only two values off it.
+ gap: theme.space(2),
padding: `${theme.space(2)} ${theme.space(3)} ${theme.space(3)}`,
background: theme.color.surface,
};
@@ -146,21 +147,19 @@ const titleText: CSSProperties = {
const descriptionText = (expanded: boolean): CSSProperties => ({
margin: 0,
fontFamily: theme.font.ui,
- fontSize: 10,
- lineHeight: "20px",
+ ...theme.type.base,
color: theme.color.textMuted,
...(expanded ? {} : { maxHeight: 100, overflow: "hidden" }),
});
const moreButton: CSSProperties = {
- margin: `2px 0 0`,
+ margin: `${theme.space(1)} 0 0`,
padding: 0,
border: "none",
background: "none",
color: theme.color.accentText,
fontFamily: theme.font.ui,
- fontSize: 10,
- lineHeight: "16px",
+ ...theme.type.row,
cursor: "pointer",
};
@@ -188,7 +187,6 @@ const badgeLabel: CSSProperties = {
const createdText: CSSProperties = {
margin: 0,
fontFamily: theme.font.ui,
- fontSize: 10,
- lineHeight: "20px",
+ ...theme.type.base,
color: theme.color.textMuted,
};
diff --git a/runner/packages/editor-shell/src/CodeEditor.tsx b/runner/packages/editor-shell/src/CodeEditor.tsx
index e5bb01513..51e519381 100644
--- a/runner/packages/editor-shell/src/CodeEditor.tsx
+++ b/runner/packages/editor-shell/src/CodeEditor.tsx
@@ -1,11 +1,12 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
-import CodeMirror, { type BasicSetupOptions, type EditorView, type ViewUpdate } from "@uiw/react-codemirror";
+import CodeMirror, { EditorView, Prec, type BasicSetupOptions, type ViewUpdate } from "@uiw/react-codemirror";
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
import { javascript } from "@codemirror/lang-javascript";
import { html } from "@codemirror/lang-html";
import { css } from "@codemirror/lang-css";
import { json } from "@codemirror/lang-json";
import { vue } from "@codemirror/lang-vue";
+import { theme } from "./theme.js";
import { useTheme } from "./useTheme.js";
function languageFor(path: string) {
@@ -41,10 +42,88 @@ function languageFor(path: string) {
const BASIC_SETUP = {
lineNumbers: true,
highlightActiveLine: true,
- foldGutter: true,
+ // The designed gutter (48:6738) is line numbers alone — the fold column made it a
+ // two-column strip the design does not have. Folding itself survives: the default
+ // keymap's fold bindings don't ride on the gutter.
+ foldGutter: false,
autocompletion: true,
} as const satisfies BasicSetupOptions;
+/** The editor pane as the design draws it (Figma 48:6719 light / 31:6597 dark):
+ * Fira Code 12/20, a 16px inset all around, and a chromeless gutter — no fill, no
+ * divider, numbers right-aligned at 40% of the ink colour. Split from `GUTTER_INK`
+ * below because everything here is mode-invariant or var()-resolved. `Prec.high`
+ * is not decoration: the `theme` prop outranks plain `extensions` entries, so
+ * without it githubDark's #0d1117 background wins over `&`'s (measured) — the
+ * design's dark editor is `editorBg` (#19191c). */
+const CHROME = {
+ "&": { fontSize: "12px", backgroundColor: "var(--hot-color-editor-bg)" },
+ ".cm-scroller": { fontFamily: theme.font.mono, lineHeight: "20px" },
+ ".cm-content": { padding: "16px 0" },
+ ".cm-line": { padding: "0 16px" },
+ ".cm-gutters": { backgroundColor: "transparent", border: "none", paddingLeft: "16px" },
+ // Default is "0 3px 0 5px", which would break the designed 16px number→code gap.
+ ".cm-lineNumbers .cm-gutterElement": { padding: "0" },
+ ".cm-activeLineGutter": { backgroundColor: "transparent" },
+
+ // The Mod-F search panel (@codemirror/search's stock DOM), which otherwise
+ // ships CodeMirror's UA-grey look: gradient buttons, unstyled fields, a bare ×.
+ // Restated in the shell's own idiom — `surfaceRaised` band over a hairline,
+ // controls as `surface` + `controlBorder` + 4px radius at 12/20, the same
+ // recipe as `.hot-btn-ghost` and the panel selects. All var()-resolved, so one
+ // block serves both modes. Flex + gap replaces the markup's whitespace-node
+ // spacing, which is what put every control on its own arbitrary offset.
+ ".cm-panels": { backgroundColor: "var(--hot-color-surface-raised)", color: "var(--hot-color-text)" },
+ ".cm-panels-bottom": { borderTop: "1px solid var(--hot-color-control-border)" },
+ ".cm-panel.cm-search": {
+ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "8px",
+ padding: "8px 16px", paddingRight: "40px",
+ fontFamily: "var(--hot-font-ui)", fontSize: "12px", lineHeight: "20px",
+ },
+ // The markup separates its find and replace rows with a lone
; as a flex
+ // item that renders as nothing and the two rows shuffle together at narrow
+ // pane widths. A 100% basis turns it back into a row break.
+ ".cm-panel.cm-search br": { flexBasis: "100%", height: "0" },
+ ".cm-panel.cm-search .cm-textfield": {
+ font: "inherit", flex: "1 1 128px", width: "auto", margin: "0", padding: "4px 8px",
+ background: "var(--hot-color-surface)", color: "var(--hot-color-text)",
+ border: "1px solid var(--hot-color-control-border)", borderRadius: "4px",
+ },
+ ".cm-panel.cm-search .cm-textfield:focus": {
+ outline: "none", borderColor: "var(--hot-color-accent)",
+ },
+ ".cm-panel.cm-search .cm-button": {
+ font: "inherit", margin: "0", padding: "4px 8px", cursor: "pointer",
+ // The stock look is a grey gradient; `none` alone leaves it showing.
+ backgroundImage: "none",
+ background: "var(--hot-color-surface)", color: "var(--hot-color-text)",
+ border: "1px solid var(--hot-color-control-border)", borderRadius: "4px",
+ textTransform: "capitalize",
+ },
+ ".cm-panel.cm-search .cm-button:active": { background: "var(--hot-color-surface-muted)" },
+ ".cm-panel.cm-search label": {
+ display: "inline-flex", alignItems: "center", gap: "4px",
+ fontSize: "12px", textTransform: "capitalize", cursor: "pointer",
+ },
+ ".cm-panel.cm-search input[type=checkbox]": {
+ margin: "0", accentColor: "var(--hot-color-accent)",
+ },
+ ".cm-panel.cm-search button[name=close]": {
+ position: "absolute", top: "8px", right: "8px",
+ width: "24px", height: "24px", padding: "0",
+ display: "inline-flex", alignItems: "center", justifyContent: "center",
+ background: "transparent", border: "none", borderRadius: "4px",
+ color: "var(--hot-color-text-muted)", fontSize: "16px", cursor: "pointer",
+ },
+ ".cm-panel.cm-search button[name=close]:hover": {
+ background: "var(--hot-color-hover)", color: "var(--hot-color-text)",
+ },
+} as const;
+/** 40% of black/white per mode — the one part of the chrome no shell token or
+ * var() covers, hence two theme instances instead of one. */
+const chromeLight = Prec.high(EditorView.theme({ ...CHROME, ".cm-lineNumbers": { color: "rgba(0, 0, 0, 0.4)" } }));
+const chromeDark = Prec.high(EditorView.theme({ ...CHROME, ".cm-lineNumbers": { color: "rgba(255, 255, 255, 0.4)" } }));
+
/** Caret position, 1-based, as the status bar shows it. */
export interface CursorPosition {
line: number;
@@ -78,8 +157,11 @@ export function CodeEditor({
onCursorChange,
onCreateEditor,
}: CodeEditorProps) {
- const extensions = useMemo(() => languageFor(path), [path]);
const { mode } = useTheme();
+ const extensions = useMemo(
+ () => [...languageFor(path), mode === "dark" ? chromeDark : chromeLight],
+ [path, mode],
+ );
// Both handlers are insulated behind a ref, because `useCodeMirror` reconfigures the
// whole extension set from an effect that lists `onChange` and `onUpdate` among its
@@ -121,7 +203,7 @@ export function CodeEditor({
(null);
+ // Slide-in from the right, as the docs assistant enters (`.da-panel`'s 0.2s
+ // transform). The panel mounts offscreen and moves to 0 one frame later — a
+ // transition needs a painted "from" state, and setting the resting transform
+ // in the same render as the mount would skip the animation entirely. Close is
+ // an unmount in App.tsx, so there is no slide-out; matching the docs' exit
+ // would mean keeping closed drawers mounted, which is a bigger contract change
+ // than an entrance animation justifies.
+ const [entered, setEntered] = useState(false);
+ useEffect(() => {
+ const raf = requestAnimationFrame(() => setEntered(true));
+ return () => cancelAnimationFrame(raf);
+ }, []);
+
/**
* Who had focus when this drawer opened — captured **during the first render**,
* not in a mount effect, and restored only if focus is still ours to give back.
@@ -105,13 +127,21 @@ export function Drawer({
}, [onClose]);
return (
-
-
- {title}
+
+
+
+ {icon}
+ {title}
+
@@ -122,85 +152,44 @@ export function Drawer({
{subheader}
{/* The only scrolling region: title row, subheader and footer stay put. */}
- {children}
+ {children}
- {footer !== undefined && }
+ {footer !== undefined && }
);
}
-// `zIndex` 900 — under `Dialog`'s 1000. A dialog opened from inside a drawer
-// (Share, from the preview bar) has to paint above it.
+// A real stylesheet, not inline objects (the app-wide move off inline styles):
+// registered once at module scope through `installCss`, colours as `var(--hot-…)`
+// references so the mode flip keeps working. Only per-instance values stay inline
+// — `width` (a prop) and the chat panel's `footerStyle` override.
//
-// It starts *below* the 72px top bar rather than at `top: 0`, which is what both
-// panels used to do. Covering the bar covered their own triggers: with a drawer
-// open, the other panel's button — and the open one's own toggle — sat under the
-// panel, and Playwright reported the `` intercepting the click. Mutually
-// exclusive panels you cannot switch between with the mouse are only mutually
-// exclusive on paper (Escape and ✕ were the only ways out). Height follows, or the
-// panel would overhang the viewport by the height of the bar.
-const panel: CSSProperties = {
- position: "fixed",
- top: s.topBar.height,
- right: 0,
- height: `calc(100% - ${s.topBar.height}px)`,
- maxWidth: "95vw",
- zIndex: 900,
- display: "flex",
- flexDirection: "column",
- // `controlBorder`, not `border`: in dark, `border` *is* `surfaceRaised`, so the
- // drawer's own edge against the workspace would vanish (theme.ts, ADR-0028).
- borderLeft: `1px solid ${theme.color.controlBorder}`,
- background: theme.color.surfaceRaised,
- boxShadow: theme.shadow.panel,
- color: theme.color.text,
- fontFamily: theme.font.ui,
-};
-
-const titleRow: CSSProperties = {
- display: "flex",
- alignItems: "center",
- justifyContent: "space-between",
- gap: theme.space(3),
- flex: "0 0 auto",
- padding: `${theme.space(3)} ${theme.space(4)}`,
- // A divider between two same-tone regions, so `border` is right here — the rule
- // reads in both modes and `controlBorder` would make it shout (ADR-0028).
- borderBottom: `1px solid ${theme.color.border}`,
-};
-
-const heading: CSSProperties = {
- margin: 0,
- fontSize: 18,
- fontWeight: 600,
- color: theme.color.text,
-};
-
-// No inline `background`: it would outrank `.hot-icon-btn:hover` (ADR-0026).
-const closeButton: CSSProperties = {
- display: "inline-flex",
- alignItems: "center",
- justifyContent: "center",
- width: 28,
- height: 28,
- flex: "0 0 auto",
- marginRight: -4,
- padding: 0,
- border: "none",
- borderRadius: theme.radius.md,
- color: theme.color.textMuted,
- cursor: "pointer",
-};
-
-const body: CSSProperties = {
- flex: 1,
- minHeight: 0,
- overflowY: "auto",
-};
-
-const foot: CSSProperties = {
- flex: "0 0 auto",
- padding: `${theme.space(3)} ${theme.space(4)}`,
- borderTop: `1px solid ${theme.color.border}`,
- background: theme.color.surfaceMuted,
-};
+// The choices the rules encode:
+// * `z-index` 900 — under `Dialog`'s 1000. A dialog opened from inside a drawer
+// (Share, from the preview bar) has to paint above it.
+// * Full viewport height, over the 72px top bar — the docs assistant's geometry
+// (`.da-panel[data-open]`), adopted deliberately after shipping the
+// below-the-bar variant first. The cost is known and accepted: with a drawer
+// open both panel triggers sit under it, so switching panels by pointer means
+// closing first — the same trade the docs panel makes with its own header. The
+// triggers stay keyboard-reachable; the drawer is non-modal and traps nothing.
+// * Entrance is the docs assistant's 0.2s slide, transform-only so nothing
+// inside reflows; `data-entered` flips one frame after mount so the
+// transition has a painted "from" state.
+// * `controlBorder` on the panel edge and both rules, not `border`: in dark,
+// `border` *is* `surfaceRaised`, so all three hairlines would vanish
+// (ADR-0028). In light the two tokens share a value.
+// * The title is `type.base` at 600 with the icon inline (`.da-title`) — the
+// design's scale tops out at 12px (48:6560), so no third size for a heading.
+// * `.hot-drawer-close` sets no background: it would outrank
+// `.hot-icon-btn:hover` (ADR-0026).
+const DRAWER_CSS = `
+.hot-drawer{position:fixed;top:0;right:0;height:100%;max-width:95vw;z-index:900;display:flex;flex-direction:column;border-left:1px solid ${theme.color.controlBorder};background:${theme.color.surfaceRaised};box-shadow:${theme.shadow.panel};color:${theme.color.text};font-family:${theme.font.ui};transition:transform 0.2s ease;transform:translateX(100%)}
+.hot-drawer[data-entered]{transform:translateX(0)}
+.hot-drawer-header{display:flex;align-items:center;justify-content:space-between;gap:${theme.space(3)};flex:0 0 auto;padding:${theme.space(3)} ${theme.space(6)};border-bottom:1px solid ${theme.color.controlBorder}}
+.hot-drawer-title{display:flex;align-items:center;gap:${theme.space(2)};margin:0;font-size:${theme.type.base.fontSize}px;line-height:${theme.type.base.lineHeight};font-weight:600;color:${theme.color.text}}
+.hot-drawer-close{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;flex:0 0 auto;margin-right:-4px;padding:0;border:none;border-radius:${theme.radius.md};color:${theme.color.textMuted};cursor:pointer}
+.hot-drawer-body{flex:1;min-height:0;overflow-y:auto}
+.hot-drawer-foot{flex:0 0 auto;padding:${theme.space(3)} ${theme.space(6)};border-top:1px solid ${theme.color.controlBorder};background:${theme.color.surfaceMuted}}
+`;
+installCss("hot-drawer-css", DRAWER_CSS);
diff --git a/runner/packages/editor-shell/src/EditorTabs.tsx b/runner/packages/editor-shell/src/EditorTabs.tsx
index 431a90283..d277386d7 100644
--- a/runner/packages/editor-shell/src/EditorTabs.tsx
+++ b/runner/packages/editor-shell/src/EditorTabs.tsx
@@ -234,7 +234,11 @@ const tab = (isFirst: boolean): CSSProperties => ({
display: "flex",
alignItems: "center",
gap: 4,
- padding: 6,
+ // 4, not the frame's 6 (48:6724): the strip is a fixed 36px and the file icon a
+ // fixed 24px, so 6 was the only value that filled it exactly — but the project
+ // spaces on a 4px grid, and with `alignItems: center` the icon centres itself in
+ // the 28px content box just the same.
+ padding: theme.space(1),
boxSizing: "border-box",
// Without this, tabs compress to fit instead of overflowing and the strip never
// scrolls.
diff --git a/runner/packages/editor-shell/src/FileTree.tsx b/runner/packages/editor-shell/src/FileTree.tsx
index 89b6d9322..c6c3768c2 100644
--- a/runner/packages/editor-shell/src/FileTree.tsx
+++ b/runner/packages/editor-shell/src/FileTree.tsx
@@ -692,8 +692,7 @@ const slot: CSSProperties = {
const label = (strong: boolean): CSSProperties => ({
fontFamily: theme.font.ui,
- fontSize: 10,
- lineHeight: "16px",
+ ...theme.type.row,
color: strong ? theme.color.text : theme.color.textMuted,
overflow: "hidden",
textOverflow: "ellipsis",
@@ -718,14 +717,13 @@ const dropHint: CSSProperties = {
right: theme.space(3),
bottom: 4,
margin: 0,
- padding: "4px 6px",
+ padding: `${theme.space(1)} ${theme.space(2)}`,
borderRadius: theme.radius.sm,
border: `1px solid ${theme.color.accentBorder}`,
background: theme.color.surfaceRaised,
color: theme.color.accentText,
fontFamily: theme.font.ui,
- fontSize: 10,
- lineHeight: "16px",
+ ...theme.type.row,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
@@ -736,15 +734,13 @@ const dropHint: CSSProperties = {
const noteText: CSSProperties = {
margin: `2px ${theme.space(3)}`,
fontFamily: theme.font.ui,
- fontSize: 10,
- lineHeight: "14px",
+ ...theme.type.row,
color: theme.color.textMuted,
};
const confirmBody: CSSProperties = {
margin: 0,
- fontSize: 13,
- lineHeight: 1.5,
+ ...theme.type.base,
color: theme.color.textMuted,
};
@@ -765,7 +761,7 @@ const ghostButton: CSSProperties = {
background: "transparent",
color: theme.color.text,
fontFamily: theme.font.ui,
- fontSize: 13,
+ ...theme.type.base,
cursor: "pointer",
};
@@ -783,7 +779,7 @@ const editInput: CSSProperties = {
boxSizing: "border-box",
fontFamily: theme.font.mono,
fontSize: 12,
- padding: "4px 6px",
+ padding: `${theme.space(1)} ${theme.space(2)}`,
border: `1px solid ${theme.color.accent}`,
borderRadius: theme.radius.sm,
// Raised, not `surface`: this floats over the recessed sidebar, which is
diff --git a/runner/packages/editor-shell/src/PreviewBar.tsx b/runner/packages/editor-shell/src/PreviewBar.tsx
index 0befdf8c6..f354c0e73 100644
--- a/runner/packages/editor-shell/src/PreviewBar.tsx
+++ b/runner/packages/editor-shell/src/PreviewBar.tsx
@@ -283,17 +283,22 @@ function Notice({ text }: { text: string }) {
);
}
+/** Square and the full 36px of the bar — a flush field in the bar's own band,
+ * like the chat composer, not a floating pill. The hairline is an inset
+ * box-shadow rather than a border so the field's box stays exactly the bar's
+ * height with no double edge against the bar's own rules. */
const customVersion: React.CSSProperties = {
width: 181,
- height: 26,
+ height: "100%",
flex: "0 0 auto",
- margin: `0 ${theme.space(2)}`,
+ margin: 0,
padding: `0 ${theme.space(2)}`,
- borderRadius: theme.radius.md,
- border: `1px solid ${theme.color.border}`,
+ borderRadius: 0,
+ border: "none",
+ boxShadow: `inset 0 0 0 1px ${theme.color.controlBorder}`,
background: theme.color.surface,
color: theme.color.text,
fontFamily: theme.font.mono,
- fontSize: 12,
+ ...theme.type.base,
boxSizing: "border-box",
};
diff --git a/runner/packages/editor-shell/src/PreviewPane.tsx b/runner/packages/editor-shell/src/PreviewPane.tsx
index dea4b8a94..4b44fa013 100644
--- a/runner/packages/editor-shell/src/PreviewPane.tsx
+++ b/runner/packages/editor-shell/src/PreviewPane.tsx
@@ -208,7 +208,7 @@ const overlay: CSSProperties = {
color: theme.color.text,
};
-const headline: CSSProperties = { margin: 0, fontSize: 13 };
+const headline: CSSProperties = { margin: 0, ...theme.type.base };
const syncPill: CSSProperties = {
position: "absolute",
@@ -218,7 +218,7 @@ const syncPill: CSSProperties = {
display: "flex",
alignItems: "center",
gap: theme.space(2),
- padding: "4px 10px",
+ padding: `${theme.space(1)} ${theme.space(2)}`,
borderRadius: theme.radius.md,
background: theme.color.accent,
color: theme.color.accentContrast,
@@ -258,7 +258,7 @@ const detailsButton: CSSProperties = {
display: "inline-flex",
alignItems: "center",
gap: 4,
- padding: `2px ${theme.space(2)}`,
+ padding: `${theme.space(1)} ${theme.space(2)}`,
border: "none",
borderRadius: theme.radius.sm,
background: "transparent",
@@ -300,7 +300,7 @@ const errorCard: CSSProperties = {
const errorTitle: CSSProperties = {
margin: 0,
- fontSize: 13,
+ ...theme.type.base,
fontWeight: 600,
color: theme.color.danger,
};
@@ -319,7 +319,7 @@ const retryButton: CSSProperties = {
background: theme.color.accent,
color: theme.color.accentContrast,
fontFamily: theme.font.ui,
- fontSize: 13,
+ ...theme.type.base,
fontWeight: 600,
cursor: "pointer",
};
diff --git a/runner/packages/editor-shell/src/SideNav.tsx b/runner/packages/editor-shell/src/SideNav.tsx
index abda4084e..b52daa068 100644
--- a/runner/packages/editor-shell/src/SideNav.tsx
+++ b/runner/packages/editor-shell/src/SideNav.tsx
@@ -101,7 +101,7 @@ export const SIDE_NAV_WIDTH = 320;
const sideNav: CSSProperties = {
display: "flex",
flexDirection: "column",
- gap: 2,
+ gap: theme.space(1),
padding: theme.space(2),
borderRight: `1px solid ${theme.color.border}`,
background: theme.color.surfaceSunken,
@@ -118,7 +118,7 @@ const navRow: CSSProperties = {
borderRadius: theme.radius.md,
color: theme.color.text,
fontFamily: theme.font.ui,
- fontSize: 13,
+ ...theme.type.base,
textAlign: "left",
textDecoration: "none",
cursor: "pointer",
@@ -130,7 +130,7 @@ const subNav: CSSProperties = {
gap: 1,
// Aligned under the parent row's label, not its icon, so the indent reads as
// "inside Guide" rather than as a second column.
- margin: `2px 0 ${theme.space(1)} ${theme.space(6)}`,
+ margin: `${theme.space(1)} 0 ${theme.space(1)} ${theme.space(6)}`,
paddingLeft: theme.space(3),
borderLeft: `1px solid ${theme.color.border}`,
};
@@ -146,7 +146,7 @@ const subRow = (active: boolean): CSSProperties => ({
// exists at all.
color: active ? theme.color.accentText : theme.color.textMuted,
fontFamily: theme.font.ui,
- fontSize: 12.5,
+ ...theme.type.base,
textDecoration: "none",
// No `background` when inactive: `.hot-menu-row`'s hover lives in the app's
// stylesheet and an inline value would outrank it (ADR-0026).
diff --git a/runner/packages/editor-shell/src/TopBar.tsx b/runner/packages/editor-shell/src/TopBar.tsx
index a8e194135..161771ae3 100644
--- a/runner/packages/editor-shell/src/TopBar.tsx
+++ b/runner/packages/editor-shell/src/TopBar.tsx
@@ -125,8 +125,6 @@ export function TopBar({
)}
-
-
{/* Download is gated on having files, not on auth. The design's top-right is
"Download (authed) / Sign in (anon)" — `72:15697` draws `Sign in` alone —
but Download has always worked for anonymous visitors, so ADR-0023 rule 1
@@ -154,6 +152,11 @@ export function TopBar({
)}
+ {/* After Download, before the avatar: display preference, not a workspace
+ action, so it sits at the passive end of the bar rather than between
+ Fork/Save and Download. */}
+
+
{/* Sign in used to sit here, beside Download (`72:15697`). It moved to the
preview status bar in DEV-2505: signing in is `@handsontable.com`-only
and internal, while most visitors to this page are external (a client on
@@ -195,7 +198,7 @@ const actionButton: React.CSSProperties = {
background: "transparent",
color: theme.color.text,
fontFamily: theme.font.ui,
- fontSize: 13,
+ ...theme.type.base,
fontWeight: 600,
cursor: "pointer",
whiteSpace: "nowrap",
diff --git a/runner/packages/editor-shell/src/icons/ui.tsx b/runner/packages/editor-shell/src/icons/ui.tsx
index 148d41080..02ce8688d 100644
--- a/runner/packages/editor-shell/src/icons/ui.tsx
+++ b/runner/packages/editor-shell/src/icons/ui.tsx
@@ -57,6 +57,7 @@ import {
IconBook as TablerBook,
IconBrandGithub as TablerBrandGithub,
IconBrandReactNative as TablerBrandReactNative,
+ IconArrowUp as TablerArrowUp,
IconChartBar as TablerChartBar,
IconChevronDown as TablerChevronDown,
IconChevronLeft as TablerChevronLeft,
@@ -68,6 +69,7 @@ import {
IconDownload as TablerDownload,
IconExternalLink as TablerExternalLink,
IconFolderPlus as TablerFolderPlus,
+ IconInfoCircle as TablerInfoCircle,
IconLayoutSidebarLeftCollapse as TablerLayoutSidebarLeftCollapse,
IconLayoutSidebarLeftExpand as TablerLayoutSidebarLeftExpand,
IconListDetails as TablerListDetails,
@@ -108,6 +110,9 @@ function ui(displayName: string, Base: TablerIcon) {
export const IconBook = ui("IconBook", TablerBook);
export const IconBrandGithub = ui("IconBrandGithub", TablerBrandGithub);
export const IconBrandReactNative = ui("IconBrandReactNative", TablerBrandReactNative);
+// The chat composer's send glyph (DEV-2047 restyle) — the docs assistant sends
+// with ↑, and the two assistants should read as one product.
+export const IconArrowUp = ui("IconArrowUp", TablerArrowUp);
export const IconChartBar = ui("IconChartBar", TablerChartBar);
export const IconChevronDown = ui("IconChevronDown", TablerChevronDown);
export const IconChevronLeft = ui("IconChevronLeft", TablerChevronLeft);
@@ -119,6 +124,9 @@ export const IconDotsVertical = ui("IconDotsVertical", TablerDotsVertical);
export const IconDownload = ui("IconDownload", TablerDownload);
export const IconExternalLink = ui("IconExternalLink", TablerExternalLink);
export const IconFolderPlus = ui("IconFolderPlus", TablerFolderPlus);
+// The Style panel's per-token info mark (Theme Builder's row treatment): the
+// token description moved off the row into a tooltip behind this.
+export const IconInfoCircle = ui("IconInfoCircle", TablerInfoCircle);
export const IconLayoutSidebarLeftCollapse = ui(
"IconLayoutSidebarLeftCollapse",
TablerLayoutSidebarLeftCollapse,
diff --git a/runner/packages/editor-shell/src/styles.ts b/runner/packages/editor-shell/src/styles.ts
index b9aa1de7d..a7707a690 100644
--- a/runner/packages/editor-shell/src/styles.ts
+++ b/runner/packages/editor-shell/src/styles.ts
@@ -104,14 +104,14 @@ export const s = {
background: t.color.surface,
color: t.color.text,
fontFamily: t.font.ui,
- fontSize: 13,
+ ...t.type.base,
}),
/** The pill's text. Every call site drew its own identical copy of this — the
* editor's, My Demos', and now Settings' — so it lives here once. */
pillLabel: {
fontFamily: t.font.ui,
- fontSize: 13,
+ ...t.type.base,
fontWeight: 500,
color: t.color.text,
} satisfies CSSProperties,
@@ -137,7 +137,7 @@ export const s = {
borderBottom: `1px solid ${t.color.border}`,
background: t.color.surface,
fontFamily: t.font.ui,
- fontSize: 13,
+ ...t.type.base,
color: t.color.text,
} satisfies CSSProperties,
@@ -173,7 +173,7 @@ export const s = {
background: t.color.surfaceSunken,
boxShadow: `inset 0 -1px 0 ${t.color.border}`,
fontFamily: t.font.ui,
- fontSize: 13,
+ ...t.type.base,
color: t.color.text,
} satisfies CSSProperties,
@@ -190,7 +190,7 @@ export const s = {
background: "transparent",
color: t.color.textMuted,
fontFamily: t.font.ui,
- fontSize: 13,
+ ...t.type.base,
textAlign: "left",
cursor: "pointer",
} satisfies CSSProperties,
@@ -206,7 +206,7 @@ export const s = {
background: "transparent",
color: t.color.text,
fontFamily: t.font.ui,
- fontSize: 13,
+ ...t.type.base,
cursor: "pointer",
whiteSpace: "nowrap",
} satisfies CSSProperties,
@@ -235,9 +235,9 @@ export const s = {
background: active ? t.color.accentSoft : "transparent",
color: active ? t.color.text : t.color.textMuted,
fontFamily: t.font.ui,
- fontSize: 13,
+ ...t.type.base,
fontWeight: active ? 600 : 400,
- padding: `6px ${t.space(2)}`,
+ padding: `${t.space(1)} ${t.space(2)}`,
cursor: "pointer",
whiteSpace: "nowrap",
}),
@@ -372,7 +372,7 @@ export const s = {
padding: t.space(4),
textAlign: "center",
fontFamily: t.font.ui,
- fontSize: 13,
+ ...t.type.base,
color: t.color.textMuted,
} satisfies CSSProperties,
@@ -424,8 +424,8 @@ export const s = {
select: {
fontFamily: t.font.mono,
- fontSize: 13,
- padding: `5px 8px`,
+ ...t.type.base,
+ padding: `${t.space(1)} ${t.space(2)}`,
borderRadius: t.radius.md,
border: `1px solid ${t.color.border}`,
background: t.color.surface,
diff --git a/runner/packages/editor-shell/src/theme.ts b/runner/packages/editor-shell/src/theme.ts
index 4fe06f517..5d91c26c6 100644
--- a/runner/packages/editor-shell/src/theme.ts
+++ b/runner/packages/editor-shell/src/theme.ts
@@ -68,7 +68,10 @@ const LIGHT_COLORS = {
controlBorder: "#e7e7e9", // horizon/palette/100
text: "#262624", // horizon/palette/700
textMuted: "#727272",
- hover: "rgba(120, 130, 150, 0.16)", // neutral row/button rollover
+ // Neutral row/button rollover. 0.10, down from 0.16: on white surfaces the
+ // old value read as a selection rather than a hover — most visibly on the
+ // Style panel's rows and its tab strip.
+ hover: "rgba(120, 130, 150, 0.1)",
// Panes that wrap third-party surfaces. `editorBg` sits behind CodeMirror,
// `previewBg` behind the demo iframe — both must track the shell, because
@@ -165,7 +168,24 @@ export const theme = {
shadow: refs("shadow", LIGHT_SHADOWS),
font: {
ui: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`,
- mono: `"JetBrains Mono", "SFMono-Regular", ui-monospace, Menlo, Consolas, monospace`,
+ mono: `"Fira Code", "JetBrains Mono", "SFMono-Regular", ui-monospace, Menlo, Consolas, monospace`,
+ },
+ /** The design's whole type scale, read off frame `48:6560` — two sizes, four
+ * roles. Every text in the frame is one of these; nothing is 13px, and
+ * hierarchy is carried by colour and tracking, not extra sizes. Spread one of
+ * these into a style object rather than writing `fontSize` by hand — the 13px
+ * the shell shipped with was exactly the kind of drift this exists to stop.
+ * - `base` 12/20: primary UI text — pills, tabs, buttons, body copy (48:6583)
+ * - `small` 10/20: secondary lines — descriptions, status bar (48:6758, 48:6742)
+ * - `row` 12/16: dense tree rows — files, dependency URLs. The frame draws
+ * these at 10 (72:16918); raised to 12 by decision — 10px file names were
+ * too small to read — with the 16px line kept so the 24px rows stay 24.
+ * - `label` 10/20 +0.8px: uppercase section headers (48:6752) */
+ type: {
+ base: { fontSize: 12, lineHeight: "20px" },
+ small: { fontSize: 10, lineHeight: "20px" },
+ row: { fontSize: 12, lineHeight: "16px" },
+ label: { fontSize: 10, lineHeight: "20px", letterSpacing: "0.8px" },
},
radius: { sm: "4px", md: "8px", lg: "12px" },
space: (n: number) => `${n * 4}px`,
@@ -206,20 +226,33 @@ const KEYFRAMES = `@keyframes hot-spin{to{transform:rotate(360deg)}}`;
export const THEME_CSS = [
block(":root", "light", LIGHT_COLORS, LIGHT_SHADOWS),
block(`:root[${THEME_ATTR}="dark"]`, "dark", DARK_COLORS, DARK_SHADOWS),
+ // The font stacks as variables, for the same reason the colours are: component
+ // stylesheets (the app's `panels.css`, the shell's `installCss` strings) have no
+ // way to reach the TS constants, and a stack pasted into CSS drifts.
+ `:root{--hot-font-ui:${theme.font.ui};--hot-font-mono:${theme.font.mono}}`,
KEYFRAMES,
].join("\n");
const STYLE_ID = "hot-theme-vars";
-/** Inject `THEME_CSS` once. Called at module scope below, so the variables exist
- * before React's first paint regardless of which module pulled `theme.ts` in. */
-export function installThemeCss(): void {
+/** Inject a stylesheet once, keyed by id. The shell can't import `.css` files —
+ * it ships as source and typechecks standalone — so components that need real
+ * selectors (pseudo-elements, hover, attribute states) register a CSS string at
+ * module scope with this instead. Values in those strings must be `var(--hot-…)`
+ * references, never literals: the mode flip works by swapping variables. */
+export function installCss(id: string, css: string): void {
if (typeof document === "undefined") return;
- if (document.getElementById(STYLE_ID)) return;
+ if (document.getElementById(id)) return;
const el = document.createElement("style");
- el.id = STYLE_ID;
- el.textContent = THEME_CSS;
+ el.id = id;
+ el.textContent = css;
document.head.appendChild(el);
}
+/** Inject `THEME_CSS` once. Called at module scope below, so the variables exist
+ * before React's first paint regardless of which module pulled `theme.ts` in. */
+export function installThemeCss(): void {
+ installCss(STYLE_ID, THEME_CSS);
+}
+
installThemeCss();
diff --git a/runner/pnpm-lock.yaml b/runner/pnpm-lock.yaml
index 5eb8b0012..d262c94e4 100644
--- a/runner/pnpm-lock.yaml
+++ b/runner/pnpm-lock.yaml
@@ -26,6 +26,9 @@ importers:
apps/authoring:
dependencies:
+ '@fontsource/fira-code':
+ specifier: ^5.3.0
+ version: 5.3.0
'@handsontable/demo-editor-shell':
specifier: workspace:*
version: link:../../packages/editor-shell
@@ -825,6 +828,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@fontsource/fira-code@5.3.0':
+ resolution: {integrity: sha512-EJL968RJRkakubAj/coU8pSUaeTE5UNoRjtzAr6kGiSZ3jWuN8/AKWHwym/PFUaQL1q7IL/H+EXs4358YhrTBQ==}
+
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
@@ -2427,6 +2433,8 @@ snapshots:
'@esbuild/win32-x64@0.28.1':
optional: true
+ '@fontsource/fira-code@5.3.0': {}
+
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.34.5':