@vscode/webview-themes 0.0.0-stage → 0.0.2-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +303 -2
  3. package/ThirdPartyNotices.txt +28 -0
  4. package/catalog-sources.json +11 -0
  5. package/dist/extractor/cli.d.ts +2 -0
  6. package/dist/extractor/cli.js +49 -0
  7. package/dist/extractor/extract.d.ts +9 -0
  8. package/dist/extractor/extract.js +135 -0
  9. package/dist/extractor/probe.cjs +201 -0
  10. package/dist/extractor/probe.d.cts +1 -0
  11. package/dist/extractor/themeFiles.d.ts +3 -0
  12. package/dist/extractor/themeFiles.js +100 -0
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +2 -0
  15. package/dist/schema.d.ts +60 -0
  16. package/dist/schema.js +108 -0
  17. package/dist/theme.d.ts +10 -0
  18. package/dist/theme.js +96 -0
  19. package/package.json +55 -5
  20. package/themes/catalog.json +25749 -0
  21. package/themes/github-github-vscode-theme-dark-colorblind.css +999 -0
  22. package/themes/github-github-vscode-theme-dark-colorblind.json +942 -0
  23. package/themes/github-github-vscode-theme-dark-default.css +999 -0
  24. package/themes/github-github-vscode-theme-dark-default.json +942 -0
  25. package/themes/github-github-vscode-theme-dark-dimmed.css +999 -0
  26. package/themes/github-github-vscode-theme-dark-dimmed.json +942 -0
  27. package/themes/github-github-vscode-theme-dark-high-contrast.css +999 -0
  28. package/themes/github-github-vscode-theme-dark-high-contrast.json +896 -0
  29. package/themes/github-github-vscode-theme-dark.css +999 -0
  30. package/themes/github-github-vscode-theme-dark.json +944 -0
  31. package/themes/github-github-vscode-theme-light-colorblind.css +999 -0
  32. package/themes/github-github-vscode-theme-light-colorblind.json +942 -0
  33. package/themes/github-github-vscode-theme-light-default.css +999 -0
  34. package/themes/github-github-vscode-theme-light-default.json +942 -0
  35. package/themes/github-github-vscode-theme-light-high-contrast.css +999 -0
  36. package/themes/github-github-vscode-theme-light-high-contrast.json +909 -0
  37. package/themes/github-github-vscode-theme-light.css +999 -0
  38. package/themes/github-github-vscode-theme-light.json +944 -0
  39. package/themes/index.json +397 -0
  40. package/themes/vscode-theme-abyss-abyss-color-theme.css +999 -0
  41. package/themes/vscode-theme-abyss-abyss-color-theme.json +907 -0
  42. package/themes/vscode-theme-defaults-2026-dark.css +999 -0
  43. package/themes/vscode-theme-defaults-2026-dark.json +961 -0
  44. package/themes/vscode-theme-defaults-2026-light.css +999 -0
  45. package/themes/vscode-theme-defaults-2026-light.json +961 -0
  46. package/themes/vscode-theme-defaults-dark-modern.css +999 -0
  47. package/themes/vscode-theme-defaults-dark-modern.json +941 -0
  48. package/themes/vscode-theme-defaults-dark-plus.css +999 -0
  49. package/themes/vscode-theme-defaults-dark-plus.json +912 -0
  50. package/themes/vscode-theme-defaults-dark-vs.css +999 -0
  51. package/themes/vscode-theme-defaults-dark-vs.json +912 -0
  52. package/themes/vscode-theme-defaults-hc-black.css +999 -0
  53. package/themes/vscode-theme-defaults-hc-black.json +805 -0
  54. package/themes/vscode-theme-defaults-hc-light.css +999 -0
  55. package/themes/vscode-theme-defaults-hc-light.json +839 -0
  56. package/themes/vscode-theme-defaults-light-modern.css +999 -0
  57. package/themes/vscode-theme-defaults-light-modern.json +943 -0
  58. package/themes/vscode-theme-defaults-light-plus.css +999 -0
  59. package/themes/vscode-theme-defaults-light-plus.json +917 -0
  60. package/themes/vscode-theme-defaults-light-vs.css +999 -0
  61. package/themes/vscode-theme-defaults-light-vs.json +917 -0
  62. package/themes/vscode-theme-kimbie-dark-kimbie-dark-color-theme.css +999 -0
  63. package/themes/vscode-theme-kimbie-dark-kimbie-dark-color-theme.json +908 -0
  64. package/themes/vscode-theme-monokai-dimmed-dimmed-monokai-color-theme.css +999 -0
  65. package/themes/vscode-theme-monokai-dimmed-dimmed-monokai-color-theme.json +907 -0
  66. package/themes/vscode-theme-monokai-monokai-color-theme.css +999 -0
  67. package/themes/vscode-theme-monokai-monokai-color-theme.json +909 -0
  68. package/themes/vscode-theme-quietlight-quietlight-color-theme.css +999 -0
  69. package/themes/vscode-theme-quietlight-quietlight-color-theme.json +911 -0
  70. package/themes/vscode-theme-red-red-color-theme.css +999 -0
  71. package/themes/vscode-theme-red-red-color-theme.json +908 -0
  72. package/themes/vscode-theme-solarized-dark-solarized-dark-color-theme.css +999 -0
  73. package/themes/vscode-theme-solarized-dark-solarized-dark-color-theme.json +909 -0
  74. package/themes/vscode-theme-solarized-light-solarized-light-color-theme.css +999 -0
  75. package/themes/vscode-theme-solarized-light-solarized-light-color-theme.json +911 -0
  76. package/themes/vscode-theme-tomorrow-night-blue-tomorrow-night-blue-color-theme.css +999 -0
  77. package/themes/vscode-theme-tomorrow-night-blue-tomorrow-night-blue-color-theme.json +907 -0
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.run = run;
37
+ const vscode = __importStar(require("vscode"));
38
+ const node_crypto_1 = require("node:crypto");
39
+ const promises_1 = require("node:fs/promises");
40
+ const node_path_1 = require("node:path");
41
+ async function run() {
42
+ const { object, text, parseCatalog } = await import('../schema.js');
43
+ const request = object(JSON.parse(process.env.WEBVIEW_THEME_REQUEST ?? 'null'), 'extraction request');
44
+ if (!Array.isArray(request.themes) || !request.themes.every(id => typeof id === 'string')) {
45
+ throw new Error('Expected requested theme IDs');
46
+ }
47
+ if (!Array.isArray(request.extensions) || !request.extensions.every(id => typeof id === 'string')) {
48
+ throw new Error('Expected requested extension IDs');
49
+ }
50
+ const available = [];
51
+ const extensions = [];
52
+ for (const extension of vscode.extensions.all) {
53
+ const manifest = object(extension.packageJSON, 'extension manifest');
54
+ if (!manifest.contributes) {
55
+ continue;
56
+ }
57
+ const contributes = object(manifest.contributes, 'contributes');
58
+ if (!contributes.themes && !contributes.colors) {
59
+ continue;
60
+ }
61
+ extensions.push({ id: extension.id, version: text(manifest.version, `${extension.id}.version`) });
62
+ if (contributes.themes) {
63
+ if (!Array.isArray(contributes.themes)) {
64
+ throw new Error(`${extension.id}: expected themes array`);
65
+ }
66
+ for (const value of contributes.themes) {
67
+ const theme = object(value, 'theme contribution');
68
+ const kind = kindFromUiTheme(theme.uiTheme);
69
+ available.push({
70
+ id: text(theme.id ?? theme.label, 'theme ID'), kind,
71
+ extensionId: extension.id,
72
+ themePath: text(theme.path, 'theme path'),
73
+ });
74
+ }
75
+ }
76
+ }
77
+ available.sort((a, b) => compare(a.id, b.id));
78
+ extensions.sort((a, b) => compare(a.id, b.id));
79
+ for (const id of request.extensions) {
80
+ if (!extensions.some(extension => extension.id.toLowerCase() === id)) {
81
+ throw new Error(`VS Code did not load the requested theme/color extension ${id}`);
82
+ }
83
+ }
84
+ if (new Set(available.map(theme => theme.id)).size !== available.length) {
85
+ throw new Error('Multiple installed themes have the same settings ID');
86
+ }
87
+ const selected = request.themes.length ? request.themes.map(id => {
88
+ const theme = available.find(theme => theme.id === id);
89
+ if (!theme) {
90
+ throw new Error(`Unknown theme: ${id}. Available: ${available.map(theme => theme.id).join(', ')}`);
91
+ }
92
+ return theme;
93
+ }).sort((a, b) => compare(a.id, b.id)) : available;
94
+ const themes = [];
95
+ for (const theme of selected) {
96
+ console.log(`Extracting ${theme.id}`);
97
+ await vscode.workspace.getConfiguration('workbench').update('colorTheme', theme.id, vscode.ConfigurationTarget.Global);
98
+ themes.push({
99
+ ...await captureTheme(theme.id, theme.kind),
100
+ extensionId: theme.extensionId,
101
+ themePath: theme.themePath,
102
+ });
103
+ }
104
+ const product = object(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(vscode.env.appRoot, 'product.json'), 'utf8')), 'VS Code product');
105
+ const catalog = parseCatalog({
106
+ schemaVersion: 1,
107
+ source: {
108
+ vscodeVersion: vscode.version,
109
+ vscodeCommit: text(product.commit, 'VS Code build commit'),
110
+ platform: process.platform,
111
+ arch: process.arch,
112
+ extensions,
113
+ },
114
+ themes,
115
+ });
116
+ await (0, promises_1.writeFile)(text(request.output, 'output path'), `${JSON.stringify(catalog, null, 2)}\n`);
117
+ }
118
+ function kindFromUiTheme(value) {
119
+ switch (value) {
120
+ case 'vs': return 'vscode-light';
121
+ case 'vs-dark': return 'vscode-dark';
122
+ case 'hc-black': return 'vscode-high-contrast';
123
+ case 'hc-light': return 'vscode-high-contrast-light';
124
+ default: throw new Error(`Unknown contributed uiTheme: ${String(value)}`);
125
+ }
126
+ }
127
+ async function captureTheme(id, expectedKind) {
128
+ const { getThemeKind, object, parseTheme } = await import('../schema.js');
129
+ const panel = vscode.window.createWebviewPanel('webview-theme-probe', `Extracting ${id}`, vscode.ViewColumn.One, {
130
+ enableScripts: true,
131
+ localResourceRoots: [],
132
+ });
133
+ try {
134
+ return await new Promise((resolve, reject) => {
135
+ const timer = setTimeout(() => {
136
+ listener.dispose();
137
+ reject(new Error(`Timed out waiting for injected webview theme ${id}`));
138
+ }, 30_000);
139
+ const listener = panel.webview.onDidReceiveMessage((value) => {
140
+ try {
141
+ const message = object(value, 'webview response');
142
+ if (message.error) {
143
+ throw new Error(`Theme probe failed: ${String(message.error)}`);
144
+ }
145
+ const theme = parseTheme(message);
146
+ const kind = getThemeKind(theme);
147
+ if (theme.id !== id || kind !== expectedKind) {
148
+ throw new Error(`Requested ${id} (${expectedKind}), received ${theme.id} (${kind})`);
149
+ }
150
+ resolve({ id: theme.id, label: theme.label, kind, variables: theme.cssVariableValues });
151
+ }
152
+ catch (error) {
153
+ reject(error);
154
+ }
155
+ finally {
156
+ clearTimeout(timer);
157
+ listener.dispose();
158
+ }
159
+ });
160
+ const nonce = (0, node_crypto_1.randomBytes)(16).toString('hex');
161
+ panel.webview.html = `<!doctype html>
162
+ <html><head><meta charset="utf-8">
163
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'">
164
+ </head><body><script nonce="${nonce}">
165
+ const api = acquireVsCodeApi();
166
+ const expected = ${JSON.stringify(id).replace(/</g, '\\u003c')};
167
+ let done = false;
168
+ function capture() {
169
+ if (done || document.body.dataset.vscodeThemeId !== expected) { return; }
170
+ const style = document.documentElement.style;
171
+ const variables = {};
172
+ for (const key of Array.from(style).sort()) {
173
+ if (key.startsWith('--vscode-') || key === '--text-link-decoration') {
174
+ variables[key] = style.getPropertyValue(key);
175
+ }
176
+ }
177
+ if (!variables['--vscode-editor-background']) { return; }
178
+ done = true;
179
+ observer.disconnect();
180
+ api.postMessage({
181
+ id: document.body.dataset.vscodeThemeId,
182
+ label: document.body.dataset.vscodeThemeName,
183
+ kind: document.body.dataset.vscodeThemeKind,
184
+ variables
185
+ });
186
+ }
187
+ window.addEventListener('error', event => api.postMessage({ error: event.message }));
188
+ const observer = new MutationObserver(capture);
189
+ observer.observe(document.documentElement, { attributes: true });
190
+ observer.observe(document.body, { attributes: true });
191
+ capture();
192
+ </script></body></html>`;
193
+ });
194
+ }
195
+ finally {
196
+ panel.dispose();
197
+ }
198
+ }
199
+ function compare(a, b) {
200
+ return a < b ? -1 : a > b ? 1 : 0;
201
+ }
@@ -0,0 +1 @@
1
+ export declare function run(): Promise<void>;
@@ -0,0 +1,3 @@
1
+ import { type ThemeCatalog } from '../schema.js';
2
+ export declare function generateThemeFiles(input: ThemeCatalog): ReadonlyMap<string, string>;
3
+ export declare function writeThemeFiles(directory: string, catalog: ThemeCatalog): Promise<void>;
@@ -0,0 +1,100 @@
1
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { object, parseCatalog, parseTheme, text } from '../schema.js';
4
+ export function generateThemeFiles(input) {
5
+ const catalog = parseCatalog(input);
6
+ const knownVariables = [...new Set(catalog.themes.flatMap(theme => Object.keys(theme.variables)))].sort();
7
+ const files = new Map();
8
+ const entries = [];
9
+ for (const input of catalog.themes) {
10
+ const extensionId = text(input.extensionId, 'theme.extensionId');
11
+ const themePath = text(input.themePath, 'theme.themePath').replace(/\\/g, '/').replace(/^\.\//, '');
12
+ const asset = themePath.replace(/^themes\//, '').replace(/\.[^.\/]+$/, '');
13
+ const id = `${extensionId}-${asset}`.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
14
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) {
15
+ throw new Error(`Cannot create theme ID from ${extensionId}:${themePath}`);
16
+ }
17
+ if (files.has(`${id}.json`)) {
18
+ throw new Error(`Theme ID collision: ${id}. Extension IDs and asset paths must produce distinct slugs.`);
19
+ }
20
+ const parsed = parseTheme(input);
21
+ const theme = {
22
+ id,
23
+ label: parsed.label,
24
+ settingsId: input.id,
25
+ light: parsed.light,
26
+ highContrast: parsed.highContrast,
27
+ cssVariableValues: Object.fromEntries(Object.entries(parsed.cssVariableValues).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)),
28
+ };
29
+ const className = `vscode-theme-${id}`;
30
+ files.set(`${id}.json`, json(theme));
31
+ const declarations = knownVariables.map(name => {
32
+ const value = theme.cssVariableValues[name] ?? 'initial';
33
+ if (/[{};\r\n]|\/\*|\*\//.test(value)) {
34
+ throw new Error(`Cannot serialize CSS value for ${id}: ${name}`);
35
+ }
36
+ const property = name.replace(/[^a-zA-Z0-9_-]/gu, character => `\\${character.codePointAt(0).toString(16)} `);
37
+ return ` ${property}: ${value};`;
38
+ });
39
+ files.set(`${id}.css`, `/* Generated by @vscode/webview-themes. Do not edit. */\n.${className} {\n${declarations.join('\n')}\n}\n`);
40
+ entries.push({
41
+ id, label: theme.label, settingsId: input.id, extensionId, themePath,
42
+ light: theme.light, highContrast: theme.highContrast, className,
43
+ json: `./${id}.json`, css: `./${id}.css`,
44
+ });
45
+ }
46
+ entries.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
47
+ const index = { schemaVersion: 1, source: catalog.source, themes: entries };
48
+ files.set('catalog.json', json(catalog));
49
+ files.set('index.json', json(index));
50
+ return new Map([...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
51
+ }
52
+ export async function writeThemeFiles(directory, catalog) {
53
+ const files = generateThemeFiles(catalog);
54
+ await mkdir(directory, { recursive: true });
55
+ const existing = await readdir(directory);
56
+ const previousFiles = new Set();
57
+ if (existing.includes('index.json')) {
58
+ const index = object(JSON.parse(await readFile(join(directory, 'index.json'), 'utf8')), 'previous theme index');
59
+ if (index.schemaVersion !== 1 || !Array.isArray(index.themes)) {
60
+ throw new Error('Cannot update an unrecognized theme index');
61
+ }
62
+ for (const value of index.themes) {
63
+ const entry = object(value, 'previous theme');
64
+ for (const extension of ['json', 'css']) {
65
+ const path = text(entry[extension], `theme.${extension}`);
66
+ if (!/^\.\/[a-z0-9]+(?:-[a-z0-9]+)*\.(?:json|css)$/.test(path)) {
67
+ throw new Error(`Unsafe generated theme path: ${path}`);
68
+ }
69
+ previousFiles.add(path.slice(2));
70
+ }
71
+ }
72
+ }
73
+ for (const name of existing) {
74
+ if (files.has(name) && name !== 'index.json' && name !== 'catalog.json' && !previousFiles.has(name)) {
75
+ throw new Error(`Refusing to overwrite untracked theme file: ${name}`);
76
+ }
77
+ }
78
+ // Publish the index last so it never points at files that have not been written yet.
79
+ const ordered = [...files].filter(([name]) => name !== 'index.json');
80
+ ordered.push(['index.json', files.get('index.json')]);
81
+ for (const [name, content] of ordered) {
82
+ const destination = join(directory, name);
83
+ const staging = `${destination}.tmp-${process.pid}`;
84
+ try {
85
+ await writeFile(staging, content);
86
+ await rename(staging, destination);
87
+ }
88
+ finally {
89
+ await rm(staging, { force: true });
90
+ }
91
+ }
92
+ for (const name of previousFiles) {
93
+ if (!files.has(name)) {
94
+ await rm(join(directory, name), { force: true });
95
+ }
96
+ }
97
+ }
98
+ function json(value) {
99
+ return `${JSON.stringify(value, null, 2)}\n`;
100
+ }
@@ -0,0 +1,4 @@
1
+ export { applyTheme, applyThemeToDocument } from './theme.js';
2
+ export type { ThemeController } from './theme.js';
3
+ export { getTheme, getThemeKind, parseCatalog, parseTheme } from './schema.js';
4
+ export type { LegacyTheme, Theme, ThemeCatalog, ThemeIndex, ThemeIndexEntry, ThemeKind, ThemeSource } from './schema.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { applyTheme, applyThemeToDocument } from './theme.js';
2
+ export { getTheme, getThemeKind, parseCatalog, parseTheme } from './schema.js';
@@ -0,0 +1,60 @@
1
+ export interface Theme {
2
+ readonly id: string;
3
+ readonly label: string;
4
+ readonly settingsId?: string;
5
+ readonly light: boolean;
6
+ readonly highContrast: boolean;
7
+ readonly cssVariableValues: Readonly<Record<string, string>>;
8
+ /** @deprecated Compatibility accessor on parsed themes. Use light and highContrast. */
9
+ readonly kind?: ThemeKind;
10
+ /** @deprecated Compatibility accessor on parsed themes. Use cssVariableValues. */
11
+ readonly variables?: Readonly<Record<string, string>>;
12
+ }
13
+ export interface ThemeIndex {
14
+ readonly schemaVersion: 1;
15
+ readonly source: ThemeSource;
16
+ readonly themes: readonly ThemeIndexEntry[];
17
+ }
18
+ export interface ThemeIndexEntry extends Omit<Theme, 'cssVariableValues' | 'kind' | 'variables'> {
19
+ readonly settingsId: string;
20
+ readonly extensionId: string;
21
+ readonly themePath: string;
22
+ readonly className: string;
23
+ readonly json: string;
24
+ readonly css: string;
25
+ }
26
+ export interface ThemeSource {
27
+ readonly vscodeVersion: string;
28
+ readonly vscodeCommit: string;
29
+ readonly platform: string;
30
+ readonly arch: string;
31
+ readonly extensions: readonly {
32
+ readonly id: string;
33
+ readonly version: string;
34
+ }[];
35
+ }
36
+ /** @deprecated Use the lightweight ThemeIndex and individual Theme files. */
37
+ export interface ThemeCatalog {
38
+ readonly schemaVersion: 1;
39
+ readonly source: ThemeSource;
40
+ readonly themes: readonly LegacyTheme[];
41
+ }
42
+ /** @deprecated Input format supported for existing fixture integrations. */
43
+ export interface LegacyTheme {
44
+ readonly id: string;
45
+ readonly label: string;
46
+ readonly kind: ThemeKind;
47
+ readonly variables: Readonly<Record<string, string>>;
48
+ readonly extensionId?: string;
49
+ readonly themePath?: string;
50
+ }
51
+ export type ThemeKind = 'vscode-light' | 'vscode-dark' | 'vscode-high-contrast' | 'vscode-high-contrast-light';
52
+ export declare const themeKinds: readonly ThemeKind[];
53
+ /** @deprecated Load an individual theme JSON file and use parseTheme. */
54
+ export declare function getTheme(catalog: ThemeCatalog, id: string): Theme;
55
+ /** @deprecated Parses the compatibility aggregate catalog. */
56
+ export declare function parseCatalog(value: unknown): ThemeCatalog;
57
+ export declare function parseTheme(value: unknown): Theme;
58
+ export declare function getThemeKind(theme: Pick<Theme, 'light' | 'highContrast'>): ThemeKind;
59
+ export declare function object(value: unknown, name: string): Record<string, unknown>;
60
+ export declare function text(value: unknown, name: string): string;
package/dist/schema.js ADDED
@@ -0,0 +1,108 @@
1
+ export const themeKinds = [
2
+ 'vscode-light', 'vscode-dark', 'vscode-high-contrast', 'vscode-high-contrast-light',
3
+ ];
4
+ /** @deprecated Load an individual theme JSON file and use parseTheme. */
5
+ export function getTheme(catalog, id) {
6
+ const theme = catalog.themes.find(theme => theme.id === id);
7
+ if (!theme) {
8
+ throw new Error(`Unknown theme: ${id}`);
9
+ }
10
+ return parseTheme(theme);
11
+ }
12
+ /** @deprecated Parses the compatibility aggregate catalog. */
13
+ export function parseCatalog(value) {
14
+ const catalog = object(value, 'catalog');
15
+ if (catalog.schemaVersion !== 1) {
16
+ throw new Error('Unsupported theme catalog schemaVersion; expected 1');
17
+ }
18
+ const source = object(catalog.source, 'source');
19
+ if (!Array.isArray(source.extensions) || !Array.isArray(catalog.themes) || catalog.themes.length === 0) {
20
+ throw new Error('Expected source.extensions and a non-empty themes array');
21
+ }
22
+ const themes = catalog.themes.map(value => {
23
+ const input = object(value, 'theme');
24
+ const theme = parseTheme(input);
25
+ return {
26
+ id: theme.id,
27
+ label: theme.label,
28
+ kind: getThemeKind(theme),
29
+ variables: theme.cssVariableValues,
30
+ ...(input.extensionId === undefined ? {} : { extensionId: text(input.extensionId, 'extensionId') }),
31
+ ...(input.themePath === undefined ? {} : { themePath: text(input.themePath, 'themePath') }),
32
+ };
33
+ });
34
+ if (new Set(themes.map(theme => theme.id)).size !== themes.length) {
35
+ throw new Error('Duplicate theme IDs in catalog');
36
+ }
37
+ return {
38
+ schemaVersion: 1,
39
+ source: {
40
+ vscodeVersion: text(source.vscodeVersion, 'source.vscodeVersion'),
41
+ vscodeCommit: text(source.vscodeCommit, 'source.vscodeCommit'),
42
+ platform: text(source.platform, 'source.platform'),
43
+ arch: text(source.arch, 'source.arch'),
44
+ extensions: source.extensions.map(value => {
45
+ const extension = object(value, 'extension');
46
+ return { id: text(extension.id, 'extension.id'), version: text(extension.version, 'extension.version') };
47
+ }),
48
+ },
49
+ themes,
50
+ };
51
+ }
52
+ export function parseTheme(value) {
53
+ const theme = object(value, 'theme');
54
+ const legacy = !('cssVariableValues' in theme);
55
+ const kind = legacy ? themeKinds.find(kind => kind === theme.kind) : undefined;
56
+ if (legacy && !kind) {
57
+ throw new Error(`Unknown theme kind: ${String(theme.kind)}`);
58
+ }
59
+ const light = legacy ? kind === 'vscode-light' || kind === 'vscode-high-contrast-light' : boolean(theme.light, 'light');
60
+ const highContrast = legacy ? kind === 'vscode-high-contrast' || kind === 'vscode-high-contrast-light' : boolean(theme.highContrast, 'highContrast');
61
+ const entries = Object.entries(object(legacy ? theme.variables : theme.cssVariableValues, 'theme.cssVariableValues')).map(([key, value]) => {
62
+ if (!key.startsWith('--vscode-') && key !== '--text-link-decoration') {
63
+ throw new Error(`Unexpected theme variable: ${key}`);
64
+ }
65
+ return [key, text(value, key)];
66
+ });
67
+ const variables = Object.fromEntries(entries);
68
+ for (const required of ['--vscode-editor-background', '--vscode-editor-foreground', '--vscode-font-family']) {
69
+ if (!variables[required]) {
70
+ throw new Error(`Theme is missing ${required}`);
71
+ }
72
+ }
73
+ const result = {
74
+ id: text(theme.id, 'theme.id'),
75
+ label: text(theme.label, 'theme.label'),
76
+ ...(theme.settingsId === undefined ? {} : { settingsId: text(theme.settingsId, 'theme.settingsId') }),
77
+ light,
78
+ highContrast,
79
+ cssVariableValues: variables,
80
+ };
81
+ return Object.defineProperties(result, {
82
+ kind: { get: () => getThemeKind(result) },
83
+ variables: { get: () => result.cssVariableValues },
84
+ });
85
+ }
86
+ export function getThemeKind(theme) {
87
+ return theme.highContrast
88
+ ? theme.light ? 'vscode-high-contrast-light' : 'vscode-high-contrast'
89
+ : theme.light ? 'vscode-light' : 'vscode-dark';
90
+ }
91
+ function boolean(value, name) {
92
+ if (typeof value !== 'boolean') {
93
+ throw new Error(`Expected ${name} to be a boolean`);
94
+ }
95
+ return value;
96
+ }
97
+ export function object(value, name) {
98
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
99
+ throw new Error(`Expected ${name} to be an object`);
100
+ }
101
+ return value;
102
+ }
103
+ export function text(value, name) {
104
+ if (typeof value !== 'string' || value.length === 0) {
105
+ throw new Error(`Expected ${name} to be a non-empty string`);
106
+ }
107
+ return value;
108
+ }
@@ -0,0 +1,10 @@
1
+ import { type LegacyTheme, type Theme } from './schema.js';
2
+ export interface ThemeController {
3
+ readonly theme: Theme;
4
+ setTheme(theme: Theme | LegacyTheme): void;
5
+ dispose(): void;
6
+ }
7
+ /** Applies variables and VS Code theme metadata to a fixture subtree. */
8
+ export declare function applyTheme(element: HTMLElement, theme: Theme | LegacyTheme): ThemeController;
9
+ /** Use in an isolated iframe to also support selectors such as body.vscode-dark. */
10
+ export declare function applyThemeToDocument(document: Document, theme: Theme | LegacyTheme): ThemeController;
package/dist/theme.js ADDED
@@ -0,0 +1,96 @@
1
+ import { getThemeKind, parseTheme, themeKinds } from './schema.js';
2
+ /** Applies variables and VS Code theme metadata to a fixture subtree. */
3
+ export function applyTheme(element, theme) {
4
+ return createController(element, element, theme);
5
+ }
6
+ /** Use in an isolated iframe to also support selectors such as body.vscode-dark. */
7
+ export function applyThemeToDocument(document, theme) {
8
+ if (!document.body) {
9
+ throw new Error('Cannot apply a theme before the document body exists');
10
+ }
11
+ return createController(document.documentElement, document.body, theme);
12
+ }
13
+ const owners = new WeakSet();
14
+ const attributes = ['data-vscode-theme-id', 'data-vscode-theme-name', 'data-vscode-theme-kind'];
15
+ function createController(variablesTarget, metadataTarget, initial) {
16
+ if (owners.has(variablesTarget) || owners.has(metadataTarget)) {
17
+ throw new Error('This element already has a theme controller; use setTheme instead');
18
+ }
19
+ let theme = parseTheme(initial);
20
+ const savedVariables = new Map();
21
+ const savedAttributes = attributes.map(name => [name, metadataTarget.getAttribute(name)]);
22
+ const savedClasses = themeKinds.filter(kind => metadataTarget.classList.contains(kind));
23
+ let disposed = false;
24
+ const remember = (key) => {
25
+ if (!savedVariables.has(key)) {
26
+ savedVariables.set(key, {
27
+ value: variablesTarget.style.getPropertyValue(key),
28
+ priority: variablesTarget.style.getPropertyPriority(key),
29
+ });
30
+ }
31
+ };
32
+ // An absent token must not inherit another fixture's or the Explorer's theme.
33
+ const computedStyle = variablesTarget.ownerDocument.defaultView?.getComputedStyle(variablesTarget);
34
+ if (computedStyle) {
35
+ for (let i = 0; i < computedStyle.length; i++) {
36
+ const key = computedStyle.item(i);
37
+ if (key.startsWith('--vscode-') || key === '--text-link-decoration') {
38
+ remember(key);
39
+ }
40
+ }
41
+ }
42
+ const apply = () => {
43
+ Object.keys(theme.cssVariableValues).forEach(remember);
44
+ for (const key of savedVariables.keys()) {
45
+ variablesTarget.style.setProperty(key, theme.cssVariableValues[key] ?? 'initial');
46
+ }
47
+ const kind = getThemeKind(theme);
48
+ metadataTarget.classList.remove(...themeKinds);
49
+ metadataTarget.classList.add(kind);
50
+ if (theme.light && theme.highContrast) {
51
+ metadataTarget.classList.add('vscode-high-contrast');
52
+ }
53
+ metadataTarget.setAttribute('data-vscode-theme-id', theme.settingsId ?? theme.id);
54
+ metadataTarget.setAttribute('data-vscode-theme-name', theme.label);
55
+ metadataTarget.setAttribute('data-vscode-theme-kind', kind);
56
+ };
57
+ owners.add(variablesTarget);
58
+ owners.add(metadataTarget);
59
+ apply();
60
+ return {
61
+ get theme() { return theme; },
62
+ setTheme(value) {
63
+ if (disposed) {
64
+ throw new Error('Cannot update a disposed theme controller');
65
+ }
66
+ theme = parseTheme(value);
67
+ apply();
68
+ },
69
+ dispose() {
70
+ if (disposed) {
71
+ return;
72
+ }
73
+ disposed = true;
74
+ for (const [key, saved] of savedVariables) {
75
+ if (saved.value) {
76
+ variablesTarget.style.setProperty(key, saved.value, saved.priority);
77
+ }
78
+ else {
79
+ variablesTarget.style.removeProperty(key);
80
+ }
81
+ }
82
+ for (const [name, value] of savedAttributes) {
83
+ if (value === null) {
84
+ metadataTarget.removeAttribute(name);
85
+ }
86
+ else {
87
+ metadataTarget.setAttribute(name, value);
88
+ }
89
+ }
90
+ metadataTarget.classList.remove(...themeKinds);
91
+ metadataTarget.classList.add(...savedClasses);
92
+ owners.delete(variablesTarget);
93
+ owners.delete(metadataTarget);
94
+ },
95
+ };
96
+ }
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
- "name": "@vscode/webview-themes",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@vscode/webview-themes",
3
+ "version": "0.0.2-1",
4
+ "description": "Actual VS Code webview themes for component fixtures, with an automated extractor",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/microsoft/vscode-packages.git",
10
+ "directory": "vscode-webview-themes"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./catalog.json": "./themes/catalog.json",
18
+ "./index.json": "./themes/index.json",
19
+ "./themes/*.json": "./themes/*.json",
20
+ "./themes/*.css": "./themes/*.css"
21
+ },
22
+ "sideEffects": [
23
+ "**/*.css"
24
+ ],
25
+ "bin": {
26
+ "vscode-webview-themes": "./dist/extractor/cli.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "themes",
31
+ "catalog-sources.json",
32
+ "README.md",
33
+ "LICENSE",
34
+ "ThirdPartyNotices.txt"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json",
38
+ "test": "node --import tsx --test test/*.test.ts",
39
+ "test:integration": "node --import tsx --test test/integration/*.test.ts",
40
+ "update-catalog": "node scripts/update-catalog.mjs",
41
+ "extract": "node dist/extractor/cli.js"
42
+ },
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.15.30",
48
+ "@types/vscode": "^1.100.0",
49
+ "@vscode/test-electron": "2.5.2",
50
+ "fflate": "0.8.2",
51
+ "playwright": "1.61.1",
52
+ "tsx": "4.22.4",
53
+ "typescript": "^5.9.3"
54
+ },
55
+ "gitHead": "efe1bb2fbeea60dd3b22314db182c8f4fdc2ea92"
56
+ }