Skip to content

Commit 1a8a486

Browse files
authored
fix(ui): close package-wide gaps from the component library PRs (#1047)
Closes #1046 and the overlays gaps deferred from #1039; list/selection tokens stay deferred to the Tree suite (#1037). Also adopts VS Code 1.131 Modern UI tokens and motion, gated behind prefers-reduced-motion and a data-ui-style="stable" fallback. - Add secondary Button, LoadingState panel, and menu items (checkbox, radio, label, keybinding) via a shared menuPart factory - Add formatKeybinding for native-style keybinding hints - Fix button focus-ring precedence, toggled IconButton hover state, and stray default tooltips - Make TooltipProvider a shared, public-export delay provider - Preserve activity cues under reduced motion; fix theme re-render bug - Add 1.131 tokens with older-host fallbacks; resync Storybook themes - Move Storybook devDependencies into the pnpm catalog
1 parent 15d4c8f commit 1a8a486

44 files changed

Lines changed: 1015 additions & 379 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.storybook/global.css

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ html body {
1919
overflow: auto;
2020
}
2121

22-
/* Shrink the root from a full-width block to its content so Pixel's
23-
autofit crop stays tight. */
22+
/* Shrink-wrap the content so Pixel's autofit crop stays tight. */
2423
#storybook-root {
2524
width: fit-content;
2625
}

.storybook/preview.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ if (
4444

4545
/**
4646
* Applies a captured VS Code theme dump (`pnpm sync:vscode-themes`) as one
47-
* `:root` stylesheet and mirrors VS Code's body attribute for theme-aware
48-
* hooks. Synchronous and idempotent, so stories render fully themed.
47+
* `:root` stylesheet and mirrors VS Code's body theme attributes for
48+
* theme-aware hooks. Synchronous and idempotent, so stories render fully
49+
* themed.
4950
*/
5051
let appliedTheme: string | undefined;
5152

@@ -68,13 +69,14 @@ function applyTheme(requested: string): void {
6869
.map(([property, value]) => `${property}: ${value};`)
6970
.join("")}}`;
7071
document.body.setAttribute("data-vscode-theme-kind", `vscode-${slug}`);
72+
document.body.setAttribute("data-vscode-theme-id", slug);
7173
}
7274

7375
/* Pixel's autofit crop follows in-flow layout, but portalled overlays
7476
(menus, tooltips) are out of flow and would be cropped away. Grow the
75-
story root to cover any element portalled to body. Relies on the
76-
padded (top-left anchored) layout: growth only extends right and
77-
down, so already-positioned overlays never move. */
77+
story root to cover them. Relies on the padded (top-left anchored)
78+
layout: growth only extends right and down, so already-positioned
79+
overlays never move. */
7880
function fitRootToPortals(): void {
7981
const root = document.getElementById("root");
8082
if (!root) {
@@ -83,19 +85,10 @@ function fitRootToPortals(): void {
8385
const origin = root.getBoundingClientRect();
8486
let right = 0;
8587
let bottom = 0;
86-
for (const el of document.body.children) {
87-
// Skip Storybook chrome (root, loaders, error display, a11y helpers)
88-
if (
89-
!(el instanceof HTMLElement) ||
90-
el.id.startsWith("storybook-") ||
91-
el.classList.contains("sb-wrapper")
92-
) {
93-
continue;
94-
}
95-
const rect = el.getBoundingClientRect();
96-
if (rect.width === 0 || rect.height === 0) {
97-
continue;
98-
}
88+
for (const overlay of document.querySelectorAll(
89+
"[data-radix-popper-content-wrapper]",
90+
)) {
91+
const rect = overlay.getBoundingClientRect();
9992
right = Math.max(right, rect.right - origin.left);
10093
bottom = Math.max(bottom, rect.bottom - origin.top);
10194
}
@@ -142,9 +135,27 @@ const preview: Preview = {
142135
dynamicTitle: true,
143136
},
144137
},
138+
uiStyle: {
139+
description: "packages/ui styling baseline",
140+
defaultValue: "modern",
141+
toolbar: {
142+
title: "UI style",
143+
icon: "beaker",
144+
items: [
145+
{ value: "modern", title: "Modern UI" },
146+
{ value: "stable", title: "Stable parity" },
147+
],
148+
dynamicTitle: true,
149+
},
150+
},
145151
},
146152
decorators: [
147153
(Story, context) => {
154+
if (context.globals.uiStyle === "stable") {
155+
document.documentElement.setAttribute("data-ui-style", "stable");
156+
} else {
157+
document.documentElement.removeAttribute("data-ui-style");
158+
}
148159
applyTheme(context.globals.theme as string);
149160
return createElement(
150161
"div",

.storybook/themes/generated/default-styles.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Generated by pnpm sync:vscode-themes from VS Code 1.128.0. Do not edit by hand. */
1+
/* Generated by pnpm sync:vscode-themes from VS Code 1.131.0. Do not edit by hand. */
22
@layer vscode-default {
33
html {
44
scrollbar-color: var(--vscode-scrollbarSlider-background) var(--vscode-editor-background);

.storybook/themes/generated/themes.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

.storybook/themes/sync.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const HERE = dirname(fileURLToPath(import.meta.url));
1919
const OUTPUT_DIR = join(HERE, "generated");
2020

2121
/** Pinned so dumps are reproducible; bump to resync against a newer VS Code. */
22-
const VSCODE_VERSION = "1.128.0";
22+
const VSCODE_VERSION = "1.131.0";
2323

2424
async function main() {
2525
// Isolated profile: theme switching must not touch the shared

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@
843843
"prettier": "^3.9.6",
844844
"react": "catalog:",
845845
"react-dom": "catalog:",
846-
"storybook": "^10.5.6",
846+
"storybook": "catalog:",
847847
"typescript": "catalog:",
848848
"typescript-eslint": "^8.66.0",
849849
"utf-8-validate": "^6.0.6",

packages/ui/README.md

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ import "@repo/ui/codicon.css";
1919

2020
`tokens.css` is the only layer that references VS Code's injected
2121
`--vscode-*` variables. Components reference `--ui-*` tokens only.
22+
`--ui-background` is the sidebar surface, the common webview host; a webview
23+
hosted in an editor tab or bottom panel uses `--ui-panel-background` instead,
24+
since VS Code gives webviews no host signal to resolve it automatically.
25+
26+
The radius and spacing tokens mirror VS Code's scale (`baseSizes.ts`), but
27+
only the rungs components actually use are declared; add one when a component
28+
needs it. Native menu, button, and hover paddings are hardcoded rather than
29+
scale-derived, so parity-pinned values stay literals.
2230

2331
Component CSS is inherit-first: typography and text color come from the
2432
webview (`font: inherit`), and controls center content with a fixed height
@@ -30,58 +38,48 @@ Every component forwards `className` and `style` to its root element, and
3038
default rules use single-class specificity, so a consumer class imported
3139
after the library overrides any default (width, height, spacing).
3240

41+
Where VS Code's stable rendering and its Modern UI preview
42+
(`workbench.experimental.modernUI`) diverge, components follow Modern UI,
43+
and new components should too. Webviews get no signal for the setting, so
44+
the default cannot follow the host. Until the design settles,
45+
`data-ui-style="stable"` on the document root restores the stable-parity
46+
menu motion; Storybook's "UI style" toolbar switch toggles it live.
47+
3348
## Overlays
3449

3550
`Tooltip`, `ContextMenu`, and `DropdownMenu` wrap the Radix primitives,
3651
styled to match the native VS Code menu and hover widgets. Menus expose
3752
Radix's compound parts as flat named exports (`DropdownMenuTrigger`,
38-
`DropdownMenuItem`, …); `Tooltip` is a single component taking a `content`
39-
prop, with a 500ms show delay matching VS Code's `workbench.hover.delay`
40-
default.
53+
`DropdownMenuItem`, `DropdownMenuCheckboxItem`, …): checkbox and radio
54+
items show a check in the icon gutter, `*Label` renders a group heading, and
55+
`*Keybinding` renders a shortcut hint. Pass `keys` the same `key`/`mac`/
56+
`win`/`linux` fields as a keybindings contribution to get the current OS's
57+
binding in its native label style (`⇧⌘R` on macOS, `Ctrl+Shift+R`
58+
elsewhere); `formatKeybinding` does the same for other surfaces, such as
59+
tooltips.
60+
61+
`Tooltip` is a single component taking a `content` prop, and requires a
62+
`TooltipProvider` ancestor. Mount one provider per app so that a pointer
63+
moving between nearby triggers skips the show delay, like native hovers.
64+
The delay defaults to 500ms, matching VS Code's `workbench.hover.delay`,
65+
and tooltips stop growing at half the window height.
4166

4267
Overlay content is portalled to `body`, inherits webview typography from
4368
there, and shares the `.ui-overlay` base for stacking, border, shadow,
44-
and scrolling. Menus fade in like native menus, gated on `data-state` so
45-
an interrupted entry animation cannot delay unmounting. High contrast,
46-
`forced-colors`, and `prefers-reduced-motion` are handled.
69+
and scrolling. Menus default to the Modern UI motion: they scale and fade in
70+
from the trigger corner and fade out on close, with Radix holding unmount
71+
until the exit animation ends. High contrast, `forced-colors`, and
72+
`prefers-reduced-motion` are handled.
4773

4874
## Known gaps
4975

50-
Deliberate deferrals, fine to fix later.
51-
52-
Overlays:
53-
54-
- Menus only support plain action items; Radix's checkbox/radio items,
55-
group labels, and keybinding hints have no styled wrappers yet.
56-
- Moving the pointer from one tooltip trigger straight to another replays
57-
the full 500ms delay, where native shows the next hover instantly. The
58-
fix is one shared `TooltipProvider` per app instead of one per
59-
`Tooltip`.
6076
- Overlay shadows are darker than native in dark themes: menus in VS Code
6177
use `shadow-lg`, which webviews cannot read, so the closest available
6278
`widget.shadow` stands in.
63-
- A very tall tooltip fills most of the viewport before it scrolls, where
64-
native hovers stop at half the window height.
65-
66-
Package-wide:
67-
68-
- There is no `Button`; the VS Code button style exists only inside the
69-
state panels, and secondary-button colors have no `--ui-*` tokens.
70-
- Only the Empty and Error panels ship; a Loading panel would need the
71-
shared panel skeleton, which stays internal.
72-
- The token layer maps what shipped components need: there are no
73-
list/selection-row, spacing, typography, or z-index tokens, and the
74-
`--ui-radius-*` tokens are only adopted by the overlays, with older
75-
controls hardcoding their radii.
76-
- `--ui-background` assumes a sidebar webview; a webview hosted in an
77-
editor tab or panel renders on the sidebar color.
78-
- `useVscodeTheme` reports the theme kind only; switching between two
79-
themes of the same kind does not notify subscribers.
80-
- Under `prefers-reduced-motion` the indeterminate `ProgressBar` renders
81-
as a full bar and the `Spinner` as a static ring, with no other
82-
activity cue.
83-
- Story helpers compile against root-hoisted Storybook packages; a
84-
standalone split needs its own Storybook devDependencies.
79+
- Keybinding hints show the contributed defaults the consumer passes, not
80+
user remaps: VS Code exposes no API for extensions to resolve a command's
81+
effective keybinding.
82+
- List/selection-row tokens are deferred to the Tree suite (#1037).
8583

8684
## Codicons
8785

packages/ui/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"imports": {
2020
"#cx": "./src/cx.ts",
2121
"#codicons": "./src/codicons.ts",
22-
"#storybook": "./src/storybook.tsx"
22+
"#storybook": "./src/storybook.ts"
2323
},
2424
"scripts": {
2525
"typecheck": "tsc --noEmit"
@@ -35,10 +35,12 @@
3535
"react-dom": "catalog:"
3636
},
3737
"devDependencies": {
38+
"@storybook/react-vite": "catalog:",
3839
"@types/react": "catalog:",
3940
"@vscode-elements/react-elements": "catalog:",
4041
"react": "catalog:",
4142
"react-dom": "catalog:",
43+
"storybook": "catalog:",
4244
"typescript": "catalog:"
4345
}
4446
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* monaco-text-button, flattened to a flex row: its 4px padding, 16px
2+
leading, and 1px border make the 26px height. */
3+
.ui-button {
4+
height: 26px;
5+
padding: 0 8px;
6+
color: var(--ui-button-foreground);
7+
background: var(--ui-button-background);
8+
border: 1px solid var(--ui-button-border);
9+
border-radius: var(--ui-radius-small);
10+
cursor: pointer;
11+
}
12+
13+
.ui-button:hover:not(:disabled) {
14+
background: var(--ui-button-hover-background);
15+
}
16+
17+
.ui-button:focus {
18+
outline: 1px solid var(--ui-focus-border);
19+
outline-offset: 2px;
20+
}
21+
22+
.ui-button--secondary {
23+
color: var(--ui-button-secondary-foreground);
24+
background: var(--ui-button-secondary-background);
25+
border-color: var(--ui-button-secondary-border);
26+
}
27+
28+
.ui-button--secondary:hover:not(:disabled) {
29+
background: var(--ui-button-secondary-hover-background);
30+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { PIXEL_ALL_THEMES } from "#storybook";
2+
3+
import { Button } from "./Button";
4+
5+
import type { Meta, StoryObj } from "@storybook/react-vite";
6+
7+
const ButtonStates = (): React.JSX.Element => (
8+
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
9+
<Button>Start workspace</Button>
10+
<Button variant="secondary">Open logs</Button>
11+
<Button disabled>Rebuild</Button>
12+
</div>
13+
);
14+
15+
const meta: Meta<typeof ButtonStates> = {
16+
title: "UI/Button",
17+
component: ButtonStates,
18+
parameters: { pixel: PIXEL_ALL_THEMES },
19+
};
20+
export default meta;
21+
type Story = StoryObj<typeof ButtonStates>;
22+
23+
export const States: Story = {};

0 commit comments

Comments
 (0)