@filipgutica/ui 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Filip Gutica
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # @filipgutica/ui
2
+
3
+ A compact Vue 3 design system built on [Reka UI](https://reka-ui.com), with
4
+ Tailwind CSS 4 tokens and safe VS Code/Open VSX color-theme normalization.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ pnpm add @filipgutica/ui
10
+ ```
11
+
12
+ Import Tailwind and the shared theme files from your application stylesheet:
13
+
14
+ ```css
15
+ @import "tailwindcss";
16
+ @import "@filipgutica/ui/theme.css";
17
+ @import "@filipgutica/ui/components.css";
18
+ ```
19
+
20
+ The theme exposes Tailwind utilities such as `bg-ui-bg`, `bg-ui-surface`,
21
+ `text-ui-text`, `text-ui-muted`, `border-ui-border`, and `text-ui-error`.
22
+
23
+ ## Vue components
24
+
25
+ ```vue
26
+ <template>
27
+ <UiButton variant="secondary" size="compact">
28
+ Inspect evidence
29
+ </UiButton>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import { UiButton } from '@filipgutica/ui'
34
+ </script>
35
+ ```
36
+
37
+ ## VS Code themes
38
+
39
+ The browser-safe theme entry point parses VS Code JSON/JSONC and maps supported
40
+ workbench colors into the package's finite semantic token contract:
41
+
42
+ ```ts
43
+ import { applySemanticTheme, parseVsCodeTheme } from '@filipgutica/ui/theme'
44
+
45
+ const theme = parseVsCodeTheme({ source, fileName })
46
+ applySemanticTheme({ root: document.documentElement, theme })
47
+ ```
48
+
49
+ The Node-only Open VSX entry point searches and imports theme extensions without
50
+ executing extension code:
51
+
52
+ ```ts
53
+ import { createOpenVsxThemeService } from '@filipgutica/ui/open-vsx'
54
+
55
+ const themes = await createOpenVsxThemeService().search('Catppuccin')
56
+ ```
57
+
58
+ Open VSX packages are untrusted input. The importer restricts origins, validates
59
+ identifiers and archive paths, caps downloaded and expanded sizes, checks the
60
+ advertised SHA-256 checksum, and retains only normalized colors. Consumers that
61
+ redistribute theme assets remain responsible for their licenses.
62
+
63
+ ## Boundaries
64
+
65
+ - Open VSX themes control color and light/dark/high-contrast appearance only.
66
+ - Typography, spacing, radius, motion, and component structure remain owned by
67
+ this package so applications keep a consistent design language.
68
+ - `@filipgutica/ui/open-vsx` is Node-only. Do not import it into browser bundles.
69
+
70
+ ## Development
71
+
72
+ ```sh
73
+ pnpm install
74
+ pnpm verify
75
+ ```
@@ -0,0 +1,24 @@
1
+ export interface UiButtonProps {
2
+ disabled?: boolean;
3
+ size?: 'default' | 'compact';
4
+ type?: 'button' | 'submit' | 'reset';
5
+ variant?: 'primary' | 'secondary' | 'text';
6
+ }
7
+ declare var __VLS_8: {};
8
+ type __VLS_Slots = {} & {
9
+ default?: (props: typeof __VLS_8) => any;
10
+ };
11
+ declare const __VLS_base: import("vue").DefineComponent<UiButtonProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<UiButtonProps> & Readonly<{}>, {
12
+ disabled: boolean;
13
+ size: "default" | "compact";
14
+ type: "button" | "submit" | "reset";
15
+ variant: "primary" | "secondary" | "text";
16
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
18
+ declare const _default: typeof __VLS_export;
19
+ export default _default;
20
+ type __VLS_WithSlots<T, S> = T & {
21
+ new (): {
22
+ $slots: S;
23
+ };
24
+ };
@@ -0,0 +1,2 @@
1
+ export { default as UiButton } from './components/UiButton.vue';
2
+ export type { UiButtonProps } from './components/UiButton.vue';
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ import { createBlock as e, defineComponent as t, openBlock as n, renderSlot as r, unref as i, withCtx as a } from "vue";
2
+ import { Primitive as o } from "reka-ui";
3
+ //#endregion
4
+ //#region src/components/UiButton.vue
5
+ var s = /* @__PURE__ */ t({
6
+ __name: "UiButton",
7
+ props: {
8
+ disabled: {
9
+ type: Boolean,
10
+ default: !1
11
+ },
12
+ size: { default: "default" },
13
+ type: { default: "button" },
14
+ variant: { default: "primary" }
15
+ },
16
+ setup(t) {
17
+ return (s, c) => (n(), e(i(o), {
18
+ as: "button",
19
+ class: "fg-button",
20
+ "data-size": t.size,
21
+ "data-variant": t.variant,
22
+ disabled: t.disabled,
23
+ type: t.type
24
+ }, {
25
+ default: a(() => [r(s.$slots, "default")]),
26
+ _: 3
27
+ }, 8, [
28
+ "data-size",
29
+ "data-variant",
30
+ "disabled",
31
+ "type"
32
+ ]));
33
+ }
34
+ });
35
+ //#endregion
36
+ export { s as UiButton };
@@ -0,0 +1,11 @@
1
+ import type { OpenVsxThemeSearchResult, ThemeImportResult } from '../theme/index.js';
2
+ type PreferredAppearance = 'light' | 'dark';
3
+ interface OpenVsxThemeServiceDependencies {
4
+ fetchImpl?: typeof fetch;
5
+ }
6
+ export interface OpenVsxThemeService {
7
+ search: (query: string) => Promise<OpenVsxThemeSearchResult>;
8
+ importTheme: (extensionId: string, preferredAppearance: PreferredAppearance) => Promise<ThemeImportResult>;
9
+ }
10
+ export declare const createOpenVsxThemeService: ({ fetchImpl, }?: OpenVsxThemeServiceDependencies) => OpenVsxThemeService;
11
+ export {};
@@ -0,0 +1,342 @@
1
+ import { ThemeValidationError as e, parseVsCodeTheme as t } from "./theme.js";
2
+ import { parse as n } from "jsonc-parser";
3
+ import { createHash as r } from "node:crypto";
4
+ import i from "jszip";
5
+ //#region src/open-vsx/index.ts
6
+ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144, l = 262144, u = 1048576, d = 20971520, f = 5e3, p = 104857600, m = 200, h = 40, g = 8, _ = 15e3, v = class extends Error {
7
+ code;
8
+ constructor(e, t) {
9
+ super(t), this.name = "OpenVsxThemeError", this.code = e;
10
+ }
11
+ }, y = (e) => typeof e == "object" && !!e && !Array.isArray(e), b = (e, t) => {
12
+ let r = [], i = n(e, r, { allowTrailingComma: !0 });
13
+ if (r.length > 0 || !y(i)) throw new v("unsupported-extension", `${t} is not valid JSON.`);
14
+ return i;
15
+ }, x = (e) => {
16
+ if (typeof e != "string") return null;
17
+ try {
18
+ let t = new URL(e);
19
+ return t.origin.toLowerCase() === a ? t.toString() : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }, S = (e) => {
24
+ let t = /^([a-z0-9][a-z0-9_-]{0,99})\.([a-z0-9][a-z0-9_-]{0,99})$/i.exec(e);
25
+ if (!t?.[1] || !t[2]) throw new v("unsupported-extension", "The selected Open VSX extension identifier is invalid.");
26
+ return [t[1], t[2]];
27
+ }, C = (e) => {
28
+ let t = y(e.contributes) ? e.contributes : null;
29
+ return Array.isArray(t?.themes) ? t.themes.filter(y) : [];
30
+ }, w = async (e, t, n) => {
31
+ let r = Number(e.headers.get("content-length"));
32
+ if (Number.isFinite(r) && r > t) throw new v("file-too-large", n);
33
+ if (!e.body) {
34
+ let r = new Uint8Array(await e.arrayBuffer());
35
+ if (r.byteLength > t) throw new v("file-too-large", n);
36
+ return r;
37
+ }
38
+ let i = e.body.getReader(), a = [], o = 0;
39
+ try {
40
+ for (;;) {
41
+ let { done: e, value: r } = await i.read();
42
+ if (e) break;
43
+ if (o += r.byteLength, o > t) throw await i.cancel(), new v("file-too-large", n);
44
+ a.push(r);
45
+ }
46
+ } finally {
47
+ i.releaseLock();
48
+ }
49
+ let s = new Uint8Array(o), c = 0;
50
+ for (let e of a) s.set(e, c), c += e.byteLength;
51
+ return s;
52
+ }, T = (e) => async ({ url: t, limit: n, failureMessage: r, tooLargeMessage: i }) => {
53
+ let a = new AbortController(), o = setTimeout(() => a.abort(), _);
54
+ try {
55
+ let o = x(String(t));
56
+ if (!o) throw new v("open-vsx-unavailable", r);
57
+ let s = await e(o, {
58
+ redirect: "follow",
59
+ signal: a.signal
60
+ });
61
+ if (!x(s.url || o) || !s.ok) throw new v("open-vsx-unavailable", r);
62
+ return await w(s, n, i);
63
+ } catch (e) {
64
+ throw e instanceof v ? e : new v("open-vsx-unavailable", a.signal.aborted ? "Open VSX took too long to respond." : r);
65
+ } finally {
66
+ clearTimeout(o);
67
+ }
68
+ }, E = (e) => {
69
+ if (!y(e)) return null;
70
+ let t = typeof e.namespace == "string" ? e.namespace.trim() : "", n = typeof e.name == "string" ? e.name.trim() : "";
71
+ if (!t || !n) return null;
72
+ let r = typeof e.displayName == "string" && e.displayName.trim() ? e.displayName.trim() : n;
73
+ return {
74
+ id: `${t}.${n}`,
75
+ name: r.slice(0, 100),
76
+ publisher: t.slice(0, 100),
77
+ description: typeof e.description == "string" ? e.description.slice(0, 300) : "",
78
+ downloadCount: typeof e.downloadCount == "number" && Number.isFinite(e.downloadCount) ? Math.max(0, e.downloadCount) : 0
79
+ };
80
+ }, D = (e) => {
81
+ if (e.length > 1024 || e.includes("\0") || e.startsWith("/") || /^[a-z]:/i.test(e)) throw new v("unsupported-extension", "A theme path is not safe.");
82
+ }, O = ({ path: e, initialSegments: t, minimumDepth: n }) => {
83
+ D(e);
84
+ let r = [...t];
85
+ for (let t of e.replaceAll("\\", "/").split("/")) if (!(!t || t === ".")) {
86
+ if (t === "..") {
87
+ if (r.length <= n) throw new v("unsupported-extension", "A theme path escapes its package.");
88
+ r.pop();
89
+ } else r.push(t);
90
+ }
91
+ return r.join("/");
92
+ }, k = (e, t = "extension/") => {
93
+ let n = t.split("/").slice(0, -1);
94
+ return n[0] !== "extension" && n.unshift("extension"), O({
95
+ path: e,
96
+ initialSegments: n,
97
+ minimumDepth: 1
98
+ });
99
+ }, A = (e) => {
100
+ let t = O({
101
+ path: e,
102
+ initialSegments: [],
103
+ minimumDepth: 0
104
+ });
105
+ if (!t) throw new v("unsupported-extension", "The extension package has an invalid file name.");
106
+ return t;
107
+ }, j = (e) => {
108
+ let t = e.byteLength, n = Math.max(0, t - 65557), r = t - 22;
109
+ for (; r >= n && (e.getUint32(r, !0) !== 101010256 || r + 22 + e.getUint16(r + 20, !0) !== t);) --r;
110
+ if (r < n) throw new v("unsupported-extension", "The extension package is not a valid ZIP file.");
111
+ return r;
112
+ }, M = (e) => {
113
+ let t = j(e), n = e.getUint32(t + 12, !0), r = e.getUint32(t + 16, !0), i = r + n;
114
+ if (i !== t || i > e.byteLength) throw new v("unsupported-extension", "The extension package has an invalid ZIP directory.");
115
+ return {
116
+ start: r,
117
+ end: i
118
+ };
119
+ }, N = ({ bytes: e, view: t, offset: n, directoryEnd: r }) => {
120
+ if (n + 46 > r || t.getUint32(n, !0) !== 33639248) throw new v("unsupported-extension", "The extension package has an invalid ZIP directory.");
121
+ let i = t.getUint32(n + 20, !0), a = t.getUint32(n + 24, !0), o = t.getUint16(n + 28, !0), s = t.getUint16(n + 30, !0), c = t.getUint16(n + 32, !0), l = n + 46 + o + s + c;
122
+ if (l > r || i === 4294967295 || a === 4294967295 || a > 0 && (i === 0 || a / i > m)) throw new v("unsupported-extension", "The extension package has unsafe ZIP metadata.");
123
+ try {
124
+ return {
125
+ name: new TextDecoder("utf-8", { fatal: !0 }).decode(e.subarray(n + 46, n + 46 + o)),
126
+ uncompressed: a,
127
+ nextOffset: l
128
+ };
129
+ } catch {
130
+ throw new v("unsupported-extension", "The extension package has an invalid file name.");
131
+ }
132
+ }, P = (e) => {
133
+ let t = new DataView(e.buffer, e.byteOffset, e.byteLength), n = M(t), r = /* @__PURE__ */ new Map(), i = 0, a = 0, o = n.start;
134
+ for (; o < n.end;) {
135
+ if (a += 1, a > f) throw new v("unsupported-extension", "The extension package has an invalid ZIP directory.");
136
+ let s = N({
137
+ bytes: e,
138
+ view: t,
139
+ offset: o,
140
+ directoryEnd: n.end
141
+ });
142
+ if (i += s.uncompressed, i > p) throw new v("file-too-large", "The extension package expands beyond the safe limit.");
143
+ r.set(A(s.name), s.uncompressed), o = s.nextOffset;
144
+ }
145
+ return r;
146
+ }, F = async ({ zip: e, sizes: t, path: n, description: r }) => {
147
+ let i = t.get(n);
148
+ if (i === void 0) throw new v("unsupported-extension", `${r} is missing.`);
149
+ if (i > u) throw new v("file-too-large", `${r} is too large.`);
150
+ let a = e.file(n);
151
+ if (!a) throw new v("unsupported-extension", `${r} is missing.`);
152
+ return a.async("string");
153
+ }, I = async ({ zip: e, sizes: t, path: n, ancestors: r = /* @__PURE__ */ new Set() }) => {
154
+ if (r.size >= g || r.has(n)) throw new v("unsupported-extension", "Theme includes are cyclic or too deeply nested.");
155
+ let i = b(await F({
156
+ zip: e,
157
+ sizes: t,
158
+ path: n,
159
+ description: `Theme file ${n}`
160
+ }), `Theme file ${n}`);
161
+ if (typeof i.include != "string") return i;
162
+ let a = new Set(r);
163
+ a.add(n);
164
+ let o = await I({
165
+ zip: e,
166
+ sizes: t,
167
+ path: k(i.include, n),
168
+ ancestors: a
169
+ });
170
+ return {
171
+ ...o,
172
+ ...i,
173
+ colors: {
174
+ ...y(o.colors) ? o.colors : {},
175
+ ...y(i.colors) ? i.colors : {}
176
+ }
177
+ };
178
+ }, L = (e) => e === "vs" ? "light" : e === "vs-dark" ? "dark" : e === "hc-black" ? "hc" : e === "hc-light" ? "hcLight" : null, R = (e, t) => e.find((e) => t === "dark" ? e.appearance === "dark" || e.appearance === "high-contrast-dark" : e.appearance === "light" || e.appearance === "high-contrast-light") ?? e[0], z = async ({ request: e, extensionId: t }) => {
179
+ let [n, r] = S(t), i = await e({
180
+ url: `${a}/api/${encodeURIComponent(n)}/${encodeURIComponent(r)}`,
181
+ limit: c,
182
+ failureMessage: "Open VSX theme details are unavailable right now.",
183
+ tooLargeMessage: "Open VSX returned unexpectedly large theme details."
184
+ }), o = JSON.parse(new TextDecoder().decode(i));
185
+ if (!y(o) || !y(o.files)) throw new v("unsupported-extension", "Open VSX returned malformed theme details.");
186
+ let s = x(o.files.manifest), l = x(o.files.download), u = x(o.files.sha256), d = typeof o.version == "string" ? o.version : "";
187
+ if (!s || !l || !u || !d) throw new v("unsupported-extension", "Open VSX returned malformed theme details.");
188
+ return {
189
+ extensionName: r,
190
+ manifestUrl: s,
191
+ packageUrl: l,
192
+ checksumUrl: u,
193
+ version: d
194
+ };
195
+ }, B = async ({ request: e, manifestUrl: t }) => {
196
+ let n = await e({
197
+ url: t,
198
+ limit: l,
199
+ failureMessage: "That extension has no readable manifest.",
200
+ tooLargeMessage: "That extension manifest is too large."
201
+ }), r = C(b(new TextDecoder().decode(n), "The extension manifest"));
202
+ if (r.length === 0 || r.length > h) throw new v("unsupported-extension", "That extension does not contain a supported number of color themes.");
203
+ }, V = async ({ request: e, packageUrl: t, checksumUrl: n }) => {
204
+ let [i, a] = await Promise.all([e({
205
+ url: t,
206
+ limit: d,
207
+ failureMessage: "That Open VSX theme could not be downloaded.",
208
+ tooLargeMessage: "That theme extension is too large to import safely."
209
+ }), e({
210
+ url: n,
211
+ limit: 256,
212
+ failureMessage: "That Open VSX theme has no readable checksum.",
213
+ tooLargeMessage: "That Open VSX checksum response is invalid."
214
+ })]), o = new TextDecoder().decode(a).trim().split(/\s+/)[0], s = r("sha256").update(i).digest("hex");
215
+ if (!o || !/^[a-f\d]{64}$/i.test(o)) throw new v("integrity-failed", "That Open VSX theme has an invalid checksum.");
216
+ if (o.toLowerCase() !== s) throw new v("integrity-failed", "That Open VSX theme failed its integrity check.");
217
+ return i;
218
+ }, H = async (e) => {
219
+ let t = P(e);
220
+ try {
221
+ return {
222
+ zip: await i.loadAsync(e),
223
+ sizes: t
224
+ };
225
+ } catch {
226
+ throw new v("unsupported-extension", "That extension package could not be opened.");
227
+ }
228
+ }, U = async ({ zip: e, sizes: t, extensionId: n, version: r }) => {
229
+ let i = b(await F({
230
+ zip: e,
231
+ sizes: t,
232
+ path: "extension/package.json",
233
+ description: "The packaged extension manifest"
234
+ }), "The packaged extension manifest");
235
+ if ((typeof i.publisher == "string" && typeof i.name == "string" ? `${i.publisher}.${i.name}` : "").toLowerCase() !== n.toLowerCase() || i.version !== r) throw new v("integrity-failed", "The downloaded package does not match the selected Open VSX theme.");
236
+ return i;
237
+ }, W = async ({ zip: e, sizes: n, manifest: r, fallbackName: i }) => {
238
+ let a = C(r);
239
+ if (a.length === 0 || a.length > h) throw new v("unsupported-extension", "That extension has no compatible color themes.");
240
+ let o = [];
241
+ for (let r of a) {
242
+ if (typeof r.path != "string") throw new v("unsupported-extension", "A contributed theme has no valid path.");
243
+ let a = k(r.path), s = await I({
244
+ zip: e,
245
+ sizes: n,
246
+ path: a
247
+ }), c = L(r.uiTheme), l = typeof r.label == "string" && r.label.trim() ? r.label.trim() : i;
248
+ o.push(t({
249
+ source: JSON.stringify({
250
+ ...s,
251
+ name: l,
252
+ ...c ? { type: c } : {}
253
+ }),
254
+ fileName: a
255
+ }));
256
+ }
257
+ return o;
258
+ }, G = ({ fetchImpl: t = fetch } = {}) => {
259
+ let n = T(t);
260
+ return {
261
+ search: async (e) => {
262
+ let t = e.trim();
263
+ if (!t) return {
264
+ status: "success",
265
+ themes: []
266
+ };
267
+ if (t.length > 100) return {
268
+ status: "error",
269
+ message: "Search terms must be 100 characters or fewer."
270
+ };
271
+ try {
272
+ let e = new URL(o);
273
+ e.searchParams.set("query", t), e.searchParams.set("category", "Themes"), e.searchParams.set("sortBy", "downloadCount"), e.searchParams.set("sortOrder", "desc"), e.searchParams.set("size", "12");
274
+ let r = await n({
275
+ url: e,
276
+ limit: s,
277
+ failureMessage: "Open VSX search is unavailable right now.",
278
+ tooLargeMessage: "Open VSX returned an unexpectedly large search response."
279
+ }), i = JSON.parse(new TextDecoder().decode(r));
280
+ if (!y(i) || !Array.isArray(i.extensions)) throw Error("invalid response");
281
+ return {
282
+ status: "success",
283
+ themes: i.extensions.flatMap((e) => {
284
+ let t = E(e);
285
+ return t ? [t] : [];
286
+ }).slice(0, 12)
287
+ };
288
+ } catch {
289
+ return {
290
+ status: "error",
291
+ message: "Open VSX search is unavailable right now."
292
+ };
293
+ }
294
+ },
295
+ importTheme: async (t, r) => {
296
+ try {
297
+ let e = await z({
298
+ request: n,
299
+ extensionId: t
300
+ });
301
+ await B({
302
+ request: n,
303
+ manifestUrl: e.manifestUrl
304
+ });
305
+ let { zip: i, sizes: a } = await H(await V({
306
+ request: n,
307
+ ...e
308
+ }));
309
+ return {
310
+ status: "success",
311
+ theme: R(await W({
312
+ zip: i,
313
+ sizes: a,
314
+ manifest: await U({
315
+ zip: i,
316
+ sizes: a,
317
+ extensionId: t,
318
+ version: e.version
319
+ }),
320
+ fallbackName: e.extensionName
321
+ }), r)
322
+ };
323
+ } catch (t) {
324
+ return t instanceof e || t instanceof v ? {
325
+ status: "error",
326
+ error: {
327
+ code: t.code,
328
+ message: t.message
329
+ }
330
+ } : {
331
+ status: "error",
332
+ error: {
333
+ code: "open-vsx-unavailable",
334
+ message: "Could not import that Open VSX theme."
335
+ }
336
+ };
337
+ }
338
+ }
339
+ };
340
+ };
341
+ //#endregion
342
+ export { G as createOpenVsxThemeService };
@@ -0,0 +1,125 @@
1
+ export type ThemeAppearance = 'light' | 'dark' | 'high-contrast-light' | 'high-contrast-dark';
2
+ export interface SemanticThemeTokens {
3
+ pageBackground: string;
4
+ surface: string;
5
+ surfaceRaised: string;
6
+ surfaceMuted: string;
7
+ textPrimary: string;
8
+ textMuted: string;
9
+ border: string;
10
+ borderSubtle: string;
11
+ link: string;
12
+ accent: string;
13
+ focus: string;
14
+ buttonBackground: string;
15
+ buttonForeground: string;
16
+ buttonHoverBackground: string;
17
+ inputBackground: string;
18
+ inputForeground: string;
19
+ inputBorder: string;
20
+ inputPlaceholder: string;
21
+ rowHoverBackground: string;
22
+ rowSelectedBackground: string;
23
+ rowSelectedForeground: string;
24
+ codeBackground: string;
25
+ codeForeground: string;
26
+ error: string;
27
+ warning: string;
28
+ success: string;
29
+ info: string;
30
+ progress: string;
31
+ }
32
+ export interface NormalizedTheme {
33
+ name: string;
34
+ appearance: ThemeAppearance;
35
+ tokens: SemanticThemeTokens;
36
+ }
37
+ export type ThemeImportErrorCode = ThemeValidationErrorCode | 'file-too-large' | 'integrity-failed' | 'open-vsx-unavailable' | 'persistence-failed' | 'read-failed' | 'unsupported-extension' | 'unsupported-file-type';
38
+ export interface ThemeImportError {
39
+ code: ThemeImportErrorCode;
40
+ message: string;
41
+ }
42
+ export type ThemeImportResult = {
43
+ status: 'cancelled';
44
+ } | {
45
+ status: 'success';
46
+ theme: NormalizedTheme;
47
+ } | {
48
+ status: 'error';
49
+ error: ThemeImportError;
50
+ };
51
+ export type ThemeRemovalResult = {
52
+ status: 'success';
53
+ } | {
54
+ status: 'error';
55
+ error: ThemeImportError;
56
+ };
57
+ export interface OpenVsxThemeSummary {
58
+ id: string;
59
+ name: string;
60
+ publisher: string;
61
+ description: string;
62
+ downloadCount: number;
63
+ }
64
+ export type OpenVsxThemeSearchResult = {
65
+ status: 'success';
66
+ themes: OpenVsxThemeSummary[];
67
+ } | {
68
+ status: 'error';
69
+ message: string;
70
+ };
71
+ export type ThemeValidationErrorCode = 'invalid-color' | 'invalid-shape' | 'malformed-json' | 'missing-colors' | 'unsupported-type';
72
+ export declare class ThemeValidationError extends Error {
73
+ readonly code: ThemeValidationErrorCode;
74
+ constructor(code: ThemeValidationErrorCode, message: string);
75
+ }
76
+ declare const semanticTokenKeys: readonly ["pageBackground", "surface", "surfaceRaised", "surfaceMuted", "textPrimary", "textMuted", "border", "borderSubtle", "link", "accent", "focus", "buttonBackground", "buttonForeground", "buttonHoverBackground", "inputBackground", "inputForeground", "inputBorder", "inputPlaceholder", "rowHoverBackground", "rowSelectedBackground", "rowSelectedForeground", "codeBackground", "codeForeground", "error", "warning", "success", "info", "progress"];
77
+ export declare const parseVsCodeTheme: ({ source, fileName, }: {
78
+ source: string;
79
+ fileName: string;
80
+ }) => NormalizedTheme;
81
+ export declare const isNormalizedTheme: (value: unknown) => value is NormalizedTheme;
82
+ export declare const semanticCssVariables: {
83
+ readonly pageBackground: "--color-bg";
84
+ readonly surface: "--color-surface";
85
+ readonly surfaceRaised: "--color-surface-raised";
86
+ readonly surfaceMuted: "--color-surface-muted";
87
+ readonly textPrimary: "--color-text";
88
+ readonly textMuted: "--color-muted";
89
+ readonly border: "--color-border";
90
+ readonly borderSubtle: "--color-border-subtle";
91
+ readonly link: "--color-link";
92
+ readonly accent: "--color-accent";
93
+ readonly focus: "--color-focus";
94
+ readonly buttonBackground: "--color-button-bg";
95
+ readonly buttonForeground: "--color-button-text";
96
+ readonly buttonHoverBackground: "--color-button-hover";
97
+ readonly inputBackground: "--color-input-bg";
98
+ readonly inputForeground: "--color-input-text";
99
+ readonly inputBorder: "--color-input-border";
100
+ readonly inputPlaceholder: "--color-input-placeholder";
101
+ readonly rowHoverBackground: "--color-row-hover";
102
+ readonly rowSelectedBackground: "--color-row-selected";
103
+ readonly rowSelectedForeground: "--color-row-selected-text";
104
+ readonly codeBackground: "--color-code";
105
+ readonly codeForeground: "--color-code-text";
106
+ readonly error: "--color-error";
107
+ readonly warning: "--color-warning";
108
+ readonly success: "--color-success";
109
+ readonly info: "--color-info";
110
+ readonly progress: "--color-progress";
111
+ };
112
+ export interface SemanticThemeRoot {
113
+ classList: {
114
+ toggle: (name: string, force?: boolean) => unknown;
115
+ };
116
+ style: {
117
+ removeProperty: (name: string) => unknown;
118
+ setProperty: (name: string, value: string) => unknown;
119
+ };
120
+ }
121
+ export declare const applySemanticTheme: ({ root, theme, }: {
122
+ root: SemanticThemeRoot;
123
+ theme: NormalizedTheme | null;
124
+ }) => void;
125
+ export { semanticTokenKeys };
package/dist/theme.js ADDED
@@ -0,0 +1,375 @@
1
+ import { parse as e, printParseErrorCode as t } from "jsonc-parser";
2
+ //#region src/theme/index.ts
3
+ var n = class extends Error {
4
+ code;
5
+ constructor(e, t) {
6
+ super(t), this.name = "ThemeValidationError", this.code = e;
7
+ }
8
+ }, r = /* @__PURE__ */ "pageBackground.surface.surfaceRaised.surfaceMuted.textPrimary.textMuted.border.borderSubtle.link.accent.focus.buttonBackground.buttonForeground.buttonHoverBackground.inputBackground.inputForeground.inputBorder.inputPlaceholder.rowHoverBackground.rowSelectedBackground.rowSelectedForeground.codeBackground.codeForeground.error.warning.success.info.progress".split("."), i = /* @__PURE__ */ new Set(/* @__PURE__ */ "editor.background,editor.foreground,foreground,sideBar.background,panel.background,editorWidget.background,descriptionForeground,disabledForeground,panel.border,sideBar.border,contrastBorder,contrastActiveBorder,focusBorder,textLink.foreground,textLink.activeForeground,button.background,button.foreground,button.hoverBackground,input.background,input.foreground,input.border,input.placeholderForeground,list.hoverBackground,list.activeSelectionBackground,list.activeSelectionForeground,list.inactiveSelectionBackground,list.inactiveSelectionForeground,errorForeground,editorError.foreground,notificationsErrorIcon.foreground,notificationsWarningIcon.foreground,editorWarning.foreground,notificationsInfoIcon.foreground,editorInfo.foreground,testing.iconPassed,terminal.ansiGreen,progressBar.background".split(",")), a = {
9
+ pageBackground: "#EFF1F5",
10
+ surface: "#E6E9EF",
11
+ surfaceRaised: "#FFFFFF",
12
+ surfaceMuted: "#DCE0E8",
13
+ textPrimary: "#4C4F69",
14
+ textMuted: "#6C6F85",
15
+ border: "#ACB0BE",
16
+ borderSubtle: "#CCD0DA",
17
+ link: "#1E66F5",
18
+ accent: "#8839EF",
19
+ focus: "#1E66F5",
20
+ buttonBackground: "#1E66F5",
21
+ buttonForeground: "#FFFFFF",
22
+ buttonHoverBackground: "#1A5AD7",
23
+ inputBackground: "#FFFFFF",
24
+ inputForeground: "#4C4F69",
25
+ inputBorder: "#9CA0B0",
26
+ inputPlaceholder: "#7C7F93",
27
+ rowHoverBackground: "#DCE0E8",
28
+ rowSelectedBackground: "#D9E2FA",
29
+ rowSelectedForeground: "#4C4F69",
30
+ codeBackground: "#E6E9EF",
31
+ codeForeground: "#4C4F69",
32
+ error: "#D20F39",
33
+ warning: "#8A5600",
34
+ success: "#287D3C",
35
+ info: "#176B87",
36
+ progress: "#8839EF"
37
+ }, o = {
38
+ pageBackground: "#1E1E2E",
39
+ surface: "#181825",
40
+ surfaceRaised: "#242435",
41
+ surfaceMuted: "#313244",
42
+ textPrimary: "#CDD6F4",
43
+ textMuted: "#A6ADC8",
44
+ border: "#585B70",
45
+ borderSubtle: "#45475A",
46
+ link: "#89B4FA",
47
+ accent: "#CBA6F7",
48
+ focus: "#89B4FA",
49
+ buttonBackground: "#89B4FA",
50
+ buttonForeground: "#11111B",
51
+ buttonHoverBackground: "#B4BEFE",
52
+ inputBackground: "#11111B",
53
+ inputForeground: "#CDD6F4",
54
+ inputBorder: "#6C7086",
55
+ inputPlaceholder: "#9399B2",
56
+ rowHoverBackground: "#313244",
57
+ rowSelectedBackground: "#45475A",
58
+ rowSelectedForeground: "#CDD6F4",
59
+ codeBackground: "#11111B",
60
+ codeForeground: "#CDD6F4",
61
+ error: "#F38BA8",
62
+ warning: "#F9E2AF",
63
+ success: "#A6E3A1",
64
+ info: "#89DCEB",
65
+ progress: "#CBA6F7"
66
+ }, s = (e) => typeof e == "object" && !!e && !Array.isArray(e), c = (e) => typeof e == "string" && /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(e), l = (e) => typeof e == "string" && /^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(e), u = (e) => e.length === 4 || e.length === 5 ? `#${[...e.slice(1)].map((e) => e.repeat(2)).join("")}` : e, d = (e) => {
67
+ let t = e.length === 4 || e.length === 5 ? [...e.slice(1)].map((e) => e.repeat(2)).join("") : e.slice(1);
68
+ return [
69
+ Number.parseInt(t.slice(0, 2), 16),
70
+ Number.parseInt(t.slice(2, 4), 16),
71
+ Number.parseInt(t.slice(4, 6), 16),
72
+ t.length === 8 ? Number.parseInt(t.slice(6, 8), 16) / 255 : 1
73
+ ];
74
+ }, f = (e, t, n) => {
75
+ let [r, i, a] = d(n), o = (e) => {
76
+ let [t, n, o, s] = d(e);
77
+ return [
78
+ t * s + r * (1 - s),
79
+ n * s + i * (1 - s),
80
+ o * s + a * (1 - s)
81
+ ];
82
+ }, s = ([e, t, n]) => {
83
+ let r = [
84
+ e,
85
+ t,
86
+ n
87
+ ].map((e) => {
88
+ let t = e / 255;
89
+ return t <= .04045 ? t / 12.92 : ((t + .055) / 1.055) ** 2.4;
90
+ });
91
+ return .2126 * r[0] + .7152 * r[1] + .0722 * r[2];
92
+ }, c = s(o(e)), l = s(o(t));
93
+ return (Math.max(c, l) + .05) / (Math.min(c, l) + .05);
94
+ }, p = ({ background: e, preferred: t, underlay: n }) => f(t, e, n) >= 4.5 ? t : [
95
+ t,
96
+ "#000000",
97
+ "#FFFFFF"
98
+ ].reduce((t, r) => f(r, e, n) > f(t, e, n) ? r : t), m = ({ color: e, against: t, fallback: n }) => f(e, t, t) >= 3 ? e : f(n, t, t) >= 3 ? n : p({
99
+ background: t,
100
+ preferred: n,
101
+ underlay: t
102
+ }), h = ({ color: e, background: t, underlay: n, fallback: r }) => f(e, t, n) >= 4.5 ? e : p({
103
+ background: t,
104
+ preferred: r,
105
+ underlay: n
106
+ }), g = ({ color: e, backgrounds: t, underlay: n, fallbacks: r }) => {
107
+ let i = [e, ...r];
108
+ return i.find((e) => t.every((t) => f(e, t, n) >= 4.5)) || i.reduce((e, r) => Math.min(...t.map((e) => f(r, e, n))) > Math.min(...t.map((t) => f(e, t, n))) ? r : e);
109
+ }, _ = (e, t) => d(e)[3] === 1 ? e : t, v = (e) => {
110
+ let [t, n, r] = d(e);
111
+ return (.2126 * t + .7152 * n + .0722 * r) / 255 < .5 ? "dark" : "light";
112
+ }, y = (e, t) => {
113
+ if (e === void 0) return v(t);
114
+ if (e === "light") return "light";
115
+ if (e === "dark") return "dark";
116
+ if (e === "hc") return "high-contrast-dark";
117
+ if (e === "hcLight") return "high-contrast-light";
118
+ throw new n("unsupported-type", "The theme type must be \"light\", \"dark\", \"hc\", or \"hcLight\".");
119
+ }, b = (e, t, n) => {
120
+ for (let n of t) {
121
+ let t = e[n];
122
+ if (c(t)) return u(t);
123
+ }
124
+ return n;
125
+ }, x = (e, t) => {
126
+ let n = e.slice(0, t).split("\n");
127
+ return {
128
+ line: n.length,
129
+ column: (n.at(-1)?.length ?? 0) + 1
130
+ };
131
+ }, S = (e) => ((e.split(/[\\/]/).at(-1) ?? "Imported theme").replace(/\.jsonc?$/i, "") || "Imported theme").slice(0, 80), C = ({ colors: e, appearance: t, background: n, foreground: r }) => {
132
+ let i = t === "dark" || t === "high-contrast-dark" ? o : a, s = b(e, t.startsWith("high-contrast") ? [
133
+ "contrastBorder",
134
+ "contrastActiveBorder",
135
+ "panel.border",
136
+ "sideBar.border",
137
+ "focusBorder"
138
+ ] : [
139
+ "panel.border",
140
+ "sideBar.border",
141
+ "contrastBorder",
142
+ "input.border",
143
+ "focusBorder"
144
+ ], i.border), c = t.startsWith("high-contrast") ? m({
145
+ color: s,
146
+ against: n,
147
+ fallback: i.border
148
+ }) : s, l = b(e, [
149
+ "textLink.foreground",
150
+ "focusBorder",
151
+ "progressBar.background",
152
+ "button.background"
153
+ ], i.link), u = b(e, [
154
+ "focusBorder",
155
+ "progressBar.background",
156
+ "textLink.foreground",
157
+ "button.background"
158
+ ], l), d = p({
159
+ background: n,
160
+ preferred: r,
161
+ underlay: n
162
+ }), _ = b(e, [
163
+ "sideBar.background",
164
+ "panel.background",
165
+ "editor.background"
166
+ ], i.surface), v = f(d, _, n) >= 4.5 ? _ : n, y = b(e, [
167
+ "panel.background",
168
+ "sideBar.background",
169
+ "editorWidget.background",
170
+ "editor.background"
171
+ ], v), x = f(d, y, n) >= 4.5 ? y : v, S = g({
172
+ color: l,
173
+ backgrounds: [n, v],
174
+ underlay: n,
175
+ fallbacks: [i.link, d]
176
+ }), C = m({
177
+ color: u,
178
+ against: v,
179
+ fallback: i.accent
180
+ }), w = g({
181
+ color: b(e, [
182
+ "descriptionForeground",
183
+ "disabledForeground",
184
+ "foreground",
185
+ "editor.foreground"
186
+ ], i.textMuted),
187
+ backgrounds: [n, v],
188
+ underlay: n,
189
+ fallbacks: [i.textMuted, d]
190
+ }), T = b(e, [
191
+ "editorWidget.background",
192
+ "list.hoverBackground",
193
+ "sideBar.background",
194
+ "panel.background"
195
+ ], i.surfaceMuted), E = f(d, T, v) >= 4.5 && f(w, T, v) >= 4.5 ? T : v, D = b(e, ["button.background"], i.buttonBackground), O = p({
196
+ background: D,
197
+ preferred: b(e, ["button.foreground"], i.buttonForeground),
198
+ underlay: v
199
+ }), k = b(e, ["button.hoverBackground", "button.background"], i.buttonHoverBackground), A = f(O, k, v) >= 4.5 ? k : D, j = b(e, [
200
+ "focusBorder",
201
+ "contrastActiveBorder",
202
+ "textLink.activeForeground"
203
+ ], i.focus), M = b(e, [
204
+ "panel.border",
205
+ "sideBar.border",
206
+ "contrastBorder"
207
+ ], c), N = t.startsWith("high-contrast") ? m({
208
+ color: M,
209
+ against: v,
210
+ fallback: c
211
+ }) : M, P = b(e, ["input.background"], v), F = b(e, t.startsWith("high-contrast") ? [
212
+ "contrastBorder",
213
+ "input.border",
214
+ "focusBorder"
215
+ ] : [
216
+ "input.border",
217
+ "contrastBorder",
218
+ "focusBorder"
219
+ ], c), I = (t, r) => h({
220
+ color: b(e, t, r),
221
+ background: v,
222
+ underlay: n,
223
+ fallback: r
224
+ }), L = b(e, ["list.hoverBackground"], i.rowHoverBackground), R = f(d, L, v) >= 4.5 ? L : v;
225
+ return {
226
+ pageBackground: n,
227
+ surface: v,
228
+ surfaceRaised: x,
229
+ surfaceMuted: E,
230
+ textPrimary: d,
231
+ textMuted: w,
232
+ border: c,
233
+ borderSubtle: N,
234
+ link: S,
235
+ accent: C,
236
+ focus: m({
237
+ color: j,
238
+ against: n,
239
+ fallback: i.focus
240
+ }),
241
+ buttonBackground: D,
242
+ buttonForeground: O,
243
+ buttonHoverBackground: A,
244
+ inputBackground: P,
245
+ inputForeground: p({
246
+ background: P,
247
+ preferred: b(e, [
248
+ "input.foreground",
249
+ "editor.foreground",
250
+ "foreground"
251
+ ], d),
252
+ underlay: v
253
+ }),
254
+ inputBorder: t.startsWith("high-contrast") ? m({
255
+ color: F,
256
+ against: P,
257
+ fallback: c
258
+ }) : F,
259
+ inputPlaceholder: h({
260
+ color: b(e, ["input.placeholderForeground", "descriptionForeground"], w),
261
+ background: P,
262
+ underlay: v,
263
+ fallback: i.textMuted
264
+ }),
265
+ rowHoverBackground: R,
266
+ rowSelectedBackground: b(e, ["list.activeSelectionBackground", "list.inactiveSelectionBackground"], i.rowSelectedBackground),
267
+ rowSelectedForeground: p({
268
+ background: b(e, ["list.activeSelectionBackground", "list.inactiveSelectionBackground"], i.rowSelectedBackground),
269
+ preferred: b(e, ["list.activeSelectionForeground", "list.inactiveSelectionForeground"], i.rowSelectedForeground),
270
+ underlay: v
271
+ }),
272
+ codeBackground: b(e, ["editor.background"], i.codeBackground),
273
+ codeForeground: p({
274
+ background: b(e, ["editor.background"], i.codeBackground),
275
+ preferred: b(e, ["editor.foreground", "foreground"], i.codeForeground),
276
+ underlay: n
277
+ }),
278
+ error: I([
279
+ "errorForeground",
280
+ "notificationsErrorIcon.foreground",
281
+ "editorError.foreground"
282
+ ], i.error),
283
+ warning: I(["notificationsWarningIcon.foreground", "editorWarning.foreground"], i.warning),
284
+ success: I(["testing.iconPassed", "terminal.ansiGreen"], i.success),
285
+ info: I([
286
+ "notificationsInfoIcon.foreground",
287
+ "editorInfo.foreground",
288
+ "textLink.foreground"
289
+ ], i.info),
290
+ progress: m({
291
+ color: b(e, [
292
+ "progressBar.background",
293
+ "focusBorder",
294
+ "textLink.foreground"
295
+ ], C),
296
+ against: E,
297
+ fallback: i.progress
298
+ })
299
+ };
300
+ }, w = ({ source: r, fileName: l }) => {
301
+ let u = [], d = e(r, u, { allowTrailingComma: !0 }), f = u[0];
302
+ if (f) {
303
+ let { line: e, column: i } = x(r, f.offset);
304
+ throw new n("malformed-json", `The theme could not be parsed (${t(f.error)} at line ${e}, column ${i}).`);
305
+ }
306
+ if (!s(d)) throw new n("invalid-shape", "The theme file must contain a JSON object.");
307
+ if (d.name !== void 0 && typeof d.name != "string") throw new n("invalid-shape", "The theme name must be a string when provided.");
308
+ if (!s(d.colors)) throw new n("invalid-shape", "The theme must contain a \"colors\" object.");
309
+ let p = d.colors;
310
+ for (let e of i) {
311
+ let t = p[e];
312
+ if (t !== void 0 && !c(t)) throw new n("invalid-color", `The color for "${e}" must be a 3-, 4-, 6-, or 8-digit hexadecimal value.`);
313
+ }
314
+ let m = b(p, [
315
+ "editor.background",
316
+ "sideBar.background",
317
+ "panel.background"
318
+ ], ""), h = b(p, ["editor.foreground", "foreground"], "");
319
+ if (!m || !h) throw new n("missing-colors", "The theme must define a supported background color and foreground color.");
320
+ let g = y(d.type, m), v = _(m, (g === "dark" || g === "high-contrast-dark" ? o : a).pageBackground);
321
+ return {
322
+ name: d.name?.trim().replace(/\s+/g, " ").slice(0, 80) || S(l),
323
+ appearance: g,
324
+ tokens: C({
325
+ colors: p,
326
+ appearance: g,
327
+ background: v,
328
+ foreground: h
329
+ })
330
+ };
331
+ }, T = (e) => {
332
+ if (!s(e) || typeof e.name != "string" || e.name.trim().length === 0 || e.name.length > 80) return !1;
333
+ let t = e.appearance;
334
+ if (t !== "light" && t !== "dark" && t !== "high-contrast-light" && t !== "high-contrast-dark") return !1;
335
+ let n = e.tokens;
336
+ return !s(n) || Object.keys(n).length !== r.length ? !1 : r.every((e) => l(n[e]));
337
+ }, E = {
338
+ pageBackground: "--color-bg",
339
+ surface: "--color-surface",
340
+ surfaceRaised: "--color-surface-raised",
341
+ surfaceMuted: "--color-surface-muted",
342
+ textPrimary: "--color-text",
343
+ textMuted: "--color-muted",
344
+ border: "--color-border",
345
+ borderSubtle: "--color-border-subtle",
346
+ link: "--color-link",
347
+ accent: "--color-accent",
348
+ focus: "--color-focus",
349
+ buttonBackground: "--color-button-bg",
350
+ buttonForeground: "--color-button-text",
351
+ buttonHoverBackground: "--color-button-hover",
352
+ inputBackground: "--color-input-bg",
353
+ inputForeground: "--color-input-text",
354
+ inputBorder: "--color-input-border",
355
+ inputPlaceholder: "--color-input-placeholder",
356
+ rowHoverBackground: "--color-row-hover",
357
+ rowSelectedBackground: "--color-row-selected",
358
+ rowSelectedForeground: "--color-row-selected-text",
359
+ codeBackground: "--color-code",
360
+ codeForeground: "--color-code-text",
361
+ error: "--color-error",
362
+ warning: "--color-warning",
363
+ success: "--color-success",
364
+ info: "--color-info",
365
+ progress: "--color-progress"
366
+ }, D = ({ root: e, theme: t }) => {
367
+ for (let n of r) {
368
+ let r = E[n];
369
+ t ? e.style.setProperty(r, t.tokens[n]) : e.style.removeProperty(r);
370
+ }
371
+ let n = t?.appearance;
372
+ e.classList.toggle("dark", n === "dark" || n === "high-contrast-dark"), e.classList.toggle("high-contrast", n === "high-contrast-light" || n === "high-contrast-dark");
373
+ };
374
+ //#endregion
375
+ export { n as ThemeValidationError, D as applySemanticTheme, T as isNormalizedTheme, w as parseVsCodeTheme, E as semanticCssVariables, r as semanticTokenKeys };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@filipgutica/ui",
3
+ "version": "0.1.0",
4
+ "description": "A compact Vue design system with Tailwind CSS and VS Code theme support",
5
+ "type": "module",
6
+ "sideEffects": [
7
+ "**/*.css"
8
+ ],
9
+ "files": [
10
+ "dist",
11
+ "src/theme.css",
12
+ "src/components.css",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./theme": {
22
+ "types": "./dist/theme/index.d.ts",
23
+ "import": "./dist/theme.js"
24
+ },
25
+ "./open-vsx": {
26
+ "types": "./dist/open-vsx/index.d.ts",
27
+ "import": "./dist/open-vsx.js"
28
+ },
29
+ "./theme.css": "./src/theme.css",
30
+ "./components.css": "./src/components.css"
31
+ },
32
+ "scripts": {
33
+ "build": "vite build && vue-tsc -p tsconfig.build.json --emitDeclarationOnly",
34
+ "prepublishOnly": "pnpm verify",
35
+ "test": "vitest run",
36
+ "typecheck": "vue-tsc -p tsconfig.json --noEmit",
37
+ "verify": "pnpm typecheck && pnpm test && pnpm build && pnpm pack --dry-run"
38
+ },
39
+ "engines": {
40
+ "node": ">=22.12"
41
+ },
42
+ "packageManager": "pnpm@11.6.0",
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/filipgutica/ui.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/filipgutica/ui/issues"
53
+ },
54
+ "homepage": "https://github.com/filipgutica/ui#readme",
55
+ "dependencies": {
56
+ "jsonc-parser": "^3.3.1",
57
+ "jszip": "^3.10.1",
58
+ "reka-ui": "^2.10.4"
59
+ },
60
+ "peerDependencies": {
61
+ "vue": ">=3.4.0"
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^26.0.0",
65
+ "@vitejs/plugin-vue": "^6.0.0",
66
+ "@vue/test-utils": "^2.4.6",
67
+ "happy-dom": "^20.12.2",
68
+ "tailwindcss": "^4.3.3",
69
+ "typescript": "^5.9.3",
70
+ "vite": "^8.2.2",
71
+ "vitest": "^4.1.11",
72
+ "vue": "^3.5.42",
73
+ "vue-tsc": "^3.3.11"
74
+ }
75
+ }
@@ -0,0 +1,59 @@
1
+ @layer components {
2
+ .fg-button {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ gap: 0.5rem;
7
+ border: 1px solid transparent;
8
+ border-radius: var(--radius-control);
9
+ padding: 0.65rem 0.9rem;
10
+ color: var(--color-button-text);
11
+ background: var(--color-button-bg);
12
+ font: inherit;
13
+ font-weight: 650;
14
+ cursor: pointer;
15
+ }
16
+
17
+ .fg-button:hover:not(:disabled) {
18
+ background: var(--color-button-hover);
19
+ }
20
+
21
+ .fg-button:focus-visible {
22
+ outline: 3px solid var(--color-focus);
23
+ outline-offset: 2px;
24
+ }
25
+
26
+ .fg-button:disabled {
27
+ cursor: wait;
28
+ opacity: 0.6;
29
+ }
30
+
31
+ .fg-button[data-size="compact"] {
32
+ padding: 0.45rem 0.65rem;
33
+ font-size: 0.78rem;
34
+ }
35
+
36
+ .fg-button[data-variant="secondary"] {
37
+ border-color: var(--color-border);
38
+ color: var(--color-text);
39
+ background: var(--color-surface);
40
+ }
41
+
42
+ .fg-button[data-variant="text"] {
43
+ padding-inline: 0;
44
+ color: var(--color-link);
45
+ background: transparent;
46
+ text-decoration: underline;
47
+ text-underline-offset: 0.2em;
48
+ }
49
+
50
+ .fg-button[data-variant="text"]:hover:not(:disabled) {
51
+ color: var(--color-link);
52
+ background: transparent;
53
+ }
54
+
55
+ :root.high-contrast .fg-button {
56
+ border-width: 2px;
57
+ border-color: var(--color-border);
58
+ }
59
+ }
package/src/theme.css ADDED
@@ -0,0 +1,96 @@
1
+ @custom-variant dark (&:where(.dark, .dark *));
2
+
3
+ :root {
4
+ color-scheme: light;
5
+ --color-bg: #eff1f5;
6
+ --color-surface: #e6e9ef;
7
+ --color-surface-raised: #fff;
8
+ --color-surface-muted: #dce0e8;
9
+ --color-text: #4c4f69;
10
+ --color-muted: #6c6f85;
11
+ --color-link: #1e66f5;
12
+ --color-accent: #8839ef;
13
+ --color-focus: #1e66f5;
14
+ --color-border: #acb0be;
15
+ --color-border-subtle: #ccd0da;
16
+ --color-button-bg: #1e66f5;
17
+ --color-button-text: #fff;
18
+ --color-button-hover: #1a5ad7;
19
+ --color-input-bg: #fff;
20
+ --color-input-text: #4c4f69;
21
+ --color-input-border: #9ca0b0;
22
+ --color-input-placeholder: #7c7f93;
23
+ --color-row-hover: #dce0e8;
24
+ --color-row-selected: #d9e2fa;
25
+ --color-row-selected-text: #4c4f69;
26
+ --color-code: #e6e9ef;
27
+ --color-code-text: #4c4f69;
28
+ --color-error: #d20f39;
29
+ --color-warning: #8a5600;
30
+ --color-success: #287d3c;
31
+ --color-info: #176b87;
32
+ --color-progress: #8839ef;
33
+ --color-shadow: rgb(0 0 0 / 8%);
34
+ --color-overlay: rgb(0 0 0 / 42%);
35
+ --color-error-surface: color-mix(in srgb, var(--color-error) 12%, var(--color-surface));
36
+ --color-warning-surface: color-mix(in srgb, var(--color-warning) 13%, var(--color-surface));
37
+ --color-success-surface: color-mix(in srgb, var(--color-success) 12%, var(--color-surface));
38
+ --color-info-surface: color-mix(in srgb, var(--color-info) 12%, var(--color-surface));
39
+ --radius-control: 0.55rem;
40
+ --radius-panel: 0.9rem;
41
+ --duration-fast: 180ms;
42
+ }
43
+
44
+ :root.dark {
45
+ color-scheme: dark;
46
+ --color-bg: #1e1e2e;
47
+ --color-surface: #181825;
48
+ --color-surface-raised: #242435;
49
+ --color-surface-muted: #313244;
50
+ --color-text: #cdd6f4;
51
+ --color-muted: #a6adc8;
52
+ --color-link: #89b4fa;
53
+ --color-accent: #cba6f7;
54
+ --color-focus: #89b4fa;
55
+ --color-border: #585b70;
56
+ --color-border-subtle: #45475a;
57
+ --color-button-bg: #89b4fa;
58
+ --color-button-text: #11111b;
59
+ --color-button-hover: #b4befe;
60
+ --color-input-bg: #11111b;
61
+ --color-input-text: #cdd6f4;
62
+ --color-input-border: #6c7086;
63
+ --color-input-placeholder: #9399b2;
64
+ --color-row-hover: #313244;
65
+ --color-row-selected: #45475a;
66
+ --color-row-selected-text: #cdd6f4;
67
+ --color-code: #11111b;
68
+ --color-code-text: #cdd6f4;
69
+ --color-error: #f38ba8;
70
+ --color-warning: #f9e2af;
71
+ --color-success: #a6e3a1;
72
+ --color-info: #89dceb;
73
+ --color-progress: #cba6f7;
74
+ --color-shadow: rgb(0 0 0 / 28%);
75
+ --color-overlay: rgb(0 0 0 / 62%);
76
+ }
77
+
78
+ @theme inline {
79
+ --color-ui-bg: var(--color-bg);
80
+ --color-ui-surface: var(--color-surface);
81
+ --color-ui-surface-raised: var(--color-surface-raised);
82
+ --color-ui-surface-muted: var(--color-surface-muted);
83
+ --color-ui-text: var(--color-text);
84
+ --color-ui-muted: var(--color-muted);
85
+ --color-ui-link: var(--color-link);
86
+ --color-ui-accent: var(--color-accent);
87
+ --color-ui-focus: var(--color-focus);
88
+ --color-ui-border: var(--color-border);
89
+ --color-ui-border-subtle: var(--color-border-subtle);
90
+ --color-ui-error: var(--color-error);
91
+ --color-ui-warning: var(--color-warning);
92
+ --color-ui-success: var(--color-success);
93
+ --color-ui-info: var(--color-info);
94
+ --radius-ui-control: var(--radius-control);
95
+ --radius-ui-panel: var(--radius-panel);
96
+ }