Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ pnpm-lock.yaml
/storybook-static/

# Golden snapshots are written verbatim; prettier would drift them out of sync.
**/__golden__/
**/__golden__/

# Generated VS Code theme dumps are written verbatim, never formatted.
.storybook/themes/generated/
25 changes: 25 additions & 0 deletions .storybook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Storybook

Stories render inside an environment that mirrors a real VS Code webview.

## Themes

`themes/generated/themes.json` holds the exact `--vscode-*` variables a real
VS Code instance injects per built-in theme, and `default-styles.css` is the
stylesheet VS Code injects into every webview. `preview.ts` applies both, plus
the `data-vscode-theme-kind` body attribute.

To refresh the snapshots after a VS Code release, bump `VSCODE_VERSION` in
`themes/sync.mjs` and run:

```sh
xvfb-run -a pnpm sync:vscode-themes
```

The script launches the pinned VS Code via `@vscode/test-electron`, cycles
the four built-in themes, and dumps what each webview receives.

## Fonts

VS Code uses the host OS font, so the dumped `--vscode-font-family` resolves
per machine; rendering inside actual VS Code is always exact.
108 changes: 13 additions & 95 deletions .storybook/global.css
Original file line number Diff line number Diff line change
@@ -1,108 +1,26 @@
html {
/* Storybook stand-ins for what the VS Code host provides outside the
webview document: the sidebar surface color and viewport sizing.
Typography and text color come from themes/generated/default-styles.css. */

html,
#storybook-preview-wrapper {
background: var(--vscode-sideBar-background);
}

body {
margin: 0;
/* Real webviews pad the body 0 20px; stories frame themselves. */
padding: 0;
overflow: hidden;
}

#storybook-preview-wrapper {
background-color: var(--vscode-sideBar-background);
/* Outranks the body { overflow: hidden } that tasks' app CSS applies
globally; stories must stay scrollable. */
html body {
overflow: auto;
}

#root {
overscroll-behavior-x: none;
background-color: var(--vscode-sideBar-background);
color: var(--vscode-editor-foreground);
font-family: var(--vscode-font-family);
font-weight: var(--vscode-font-weight);
font-size: var(--vscode-font-size);
margin: 0;
max-width: 100%;
/* arbitrary size choice for the rough VSCode sidebar size */
/* arbitrary size choice for the rough VSCode sidebar size; stories
needing a wider canvas override it via parameters.rootWidth */
width: 300px;
}

img,
video {
max-width: 100%;
max-height: 100%;
}

a,
a code {
color: var(--vscode-textLink-foreground);
}

p > a {
text-decoration: var(--text-link-decoration);
}

a:hover {
color: var(--vscode-textLink-activeForeground);
}

a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}

code {
font-family: var(--monaco-monospace-font);
color: var(--vscode-textPreformat-foreground);
background-color: var(--vscode-textPreformat-background);
padding: 1px 3px;
border-radius: 4px;
}

pre code {
padding: 0;
}

blockquote {
background: var(--vscode-textBlockQuote-background);
border-color: var(--vscode-textBlockQuote-border);
}

kbd {
background-color: var(--vscode-keybindingLabel-background);
color: var(--vscode-keybindingLabel-foreground);
border-style: solid;
border-width: 1px;
border-radius: 3px;
border-color: var(--vscode-keybindingLabel-border);
border-bottom-color: var(--vscode-keybindingLabel-bottomBorder);
box-shadow: inset 0 -1px 0 var(--vscode-widget-shadow);
vertical-align: middle;
padding: 1px 3px;
}

::-webkit-scrollbar {
width: 10px;
height: 10px;
}

::-webkit-scrollbar-corner {
background-color: var(--vscode-editor-background);
}

::-webkit-scrollbar-thumb {
background-color: var(--vscode-scrollbarSlider-background);
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--vscode-scrollbarSlider-hoverBackground);
}
::-webkit-scrollbar-thumb:active {
background-color: var(--vscode-scrollbarSlider-activeBackground);
}
::highlight(find-highlight) {
background-color: var(--vscode-editor-findMatchHighlightBackground);
}
::highlight(current-find-highlight) {
background-color: var(--vscode-editor-findMatchBackground);
}
4 changes: 4 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import babel from "@rolldown/plugin-babel";
import { reactCompilerPreset } from "@vitejs/plugin-react";
import { mergeConfig } from "vite";

import type { StorybookConfig } from "@storybook/react-vite";
Expand All @@ -12,6 +14,8 @@ const config: StorybookConfig = {
viteFinal(baseConfig) {
return mergeConfig(baseConfig, {
assetsInclude: ["**/*.ttf", "**/*.woff", "**/*.woff2"],
// Compile with the React Compiler, matching production webview builds
plugins: [babel({ presets: [reactCompilerPreset()] })],
});
},
};
Expand Down
4 changes: 0 additions & 4 deletions .storybook/playground-themes.d.ts

This file was deleted.

87 changes: 38 additions & 49 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
/// <reference types="vite/client" />

import codiconCssUrl from "@vscode/codicons/dist/codicon.css?url";
import { theme as darkTheme } from "@vscode-elements/webview-playground/dist/themes/dark-v2.js";
import { theme as hcDarkTheme } from "@vscode-elements/webview-playground/dist/themes/hc-dark.js";
import { theme as hcLightTheme } from "@vscode-elements/webview-playground/dist/themes/hc-light.js";
import { theme as lightTheme } from "@vscode-elements/webview-playground/dist/themes/light-v2.js";
import { createElement, useEffect } from "react";
import { createElement } from "react";

import "./global.css";
import "./themes/generated/default-styles.css";
import themeDumps from "./themes/generated/themes.json";

import type { Preview } from "@storybook/react-vite";
import type { WebviewApi } from "vscode-webview";
Expand Down Expand Up @@ -44,25 +42,33 @@ if (
document.head.appendChild(link);
}

// VS Code injects --vscode-font-family at runtime, but the upstream
// vscode-elements theme data omits it. Set a static default so
// Storybook (and Pixel) renders with a predictable sans-serif
// stack instead of falling back to the browser default (Times).
const VSCODE_FONT_FAMILY =
'"Segoe WPC", "Segoe UI", system-ui, "Ubuntu", "Droid Sans", sans-serif';
/**
* Applies a captured VS Code theme dump (`pnpm sync:vscode-themes`) as one
* `:root` stylesheet and mirrors VS Code's body attribute for theme-aware
* hooks. Synchronous and idempotent, so stories render fully themed.
*/
let appliedTheme: string | undefined;

function applyTheme(requested: string): void {
const slug = (
requested in themeDumps.themes ? requested : "dark"
) as keyof typeof themeDumps.themes;
if (appliedTheme === slug) {
return;
}
appliedTheme = slug;

const THEMES: Record<
string,
{ variables: Array<[string, string]>; kind: string }
> = {
light: { variables: lightTheme, kind: "vscode-light" },
dark: { variables: darkTheme, kind: "vscode-dark" },
"high-contrast": { variables: hcDarkTheme, kind: "vscode-high-contrast" },
"high-contrast-light": {
variables: hcLightTheme,
kind: "vscode-high-contrast-light",
},
};
let style = document.getElementById("vscode-theme-variables");
if (!style) {
style = document.createElement("style");
style.id = "vscode-theme-variables";
document.head.appendChild(style);
}
style.textContent = `:root {${themeDumps.themes[slug]
.map(([property, value]) => `${property}: ${value};`)
.join("")}}`;
document.body.setAttribute("data-vscode-theme-kind", `vscode-${slug}`);
}

const preview: Preview = {
parameters: {
Expand Down Expand Up @@ -95,32 +101,15 @@ const preview: Preview = {
},
decorators: [
(Story, context) => {
const { variables, kind } =
THEMES[context.globals.theme as string] ?? THEMES.dark;

useEffect(() => {
const root = document.documentElement.style;
root.setProperty("--vscode-font-family", VSCODE_FONT_FAMILY);

// Mirror VS Code's body attribute so theme-aware hooks work in Storybook.
document.body.setAttribute("data-vscode-theme-kind", kind);

// Apply CSS custom properties to the document root
variables.forEach(([property, value]) => {
root.setProperty(property, value);
});

// Cleanup function to remove properties when unmounting
return () => {
variables.forEach(([property]) => {
root.removeProperty(property);
});
root.removeProperty("--vscode-font-family");
document.body.removeAttribute("data-vscode-theme-kind");
};
}, [variables, kind]);

return createElement("div", { id: "root" }, createElement(Story));
applyTheme(context.globals.theme as string);
return createElement(
"div",
{
id: "root",
style: { width: context.parameters.rootWidth as string },
},
createElement(Story),
);
},
],
};
Expand Down
97 changes: 97 additions & 0 deletions .storybook/themes/generated/default-styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* Generated by pnpm sync:vscode-themes from VS Code 1.128.0. Do not edit by hand. */
@layer vscode-default {
html {
scrollbar-color: var(--vscode-scrollbarSlider-background) var(--vscode-editor-background);
}

body {
overscroll-behavior-x: none;
background-color: transparent;
color: var(--vscode-editor-foreground);
font-family: var(--vscode-font-family);
font-weight: var(--vscode-font-weight);
font-size: var(--vscode-font-size);
margin: 0;
padding: 0 20px;
}

img, video {
max-width: 100%;
max-height: 100%;
}

a, a code {
color: var(--vscode-textLink-foreground);
}

p > a {
text-decoration: var(--text-link-decoration);
}

a:hover {
color: var(--vscode-textLink-activeForeground);
}

a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}

code {
font-family: var(--monaco-monospace-font);
color: var(--vscode-textPreformat-foreground);
background-color: var(--vscode-textPreformat-background);
padding: 1px 3px;
border-radius: 4px;
}

pre code {
padding: 0;
}

blockquote {
background: var(--vscode-textBlockQuote-background);
border-color: var(--vscode-textBlockQuote-border);
}

kbd {
background-color: var(--vscode-keybindingLabel-background);
color: var(--vscode-keybindingLabel-foreground);
border-style: solid;
border-width: 1px;
border-radius: 3px;
border-color: var(--vscode-keybindingLabel-border);
border-bottom-color: var(--vscode-keybindingLabel-bottomBorder);
box-shadow: inset 0 -1px 0 var(--vscode-widget-shadow);
vertical-align: middle;
padding: 1px 3px;
}

::-webkit-scrollbar {
width: 10px;
height: 10px;
}

::-webkit-scrollbar-corner {
background-color: var(--vscode-editor-background);
}

::-webkit-scrollbar-thumb {
background-color: var(--vscode-scrollbarSlider-background);
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--vscode-scrollbarSlider-hoverBackground);
}
::-webkit-scrollbar-thumb:active {
background-color: var(--vscode-scrollbarSlider-activeBackground);
}
::highlight(find-highlight) {
background-color: var(--vscode-editor-findMatchHighlightBackground);
}
::highlight(current-find-highlight) {
background-color: var(--vscode-editor-findMatchBackground);
}
}
1 change: 1 addition & 0 deletions .storybook/themes/generated/themes.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions .storybook/themes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "theme-sync-fixture",
"description": "Fixture extension loaded by sync.mjs (pnpm sync:vscode-themes)",
"publisher": "coder",
"version": "0.0.1",
"private": true,
"type": "commonjs",
"engines": {
"vscode": "^1.105.0"
}
}
Loading