@filipgutica/ui 0.2.1 → 0.3.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/README.md CHANGED
@@ -79,6 +79,28 @@ redistribute theme assets remain responsible for their licenses.
79
79
  this package so applications keep a consistent design language.
80
80
  - `@filipgutica/ui/open-vsx` is Node-only. Do not import it into browser bundles.
81
81
 
82
+ ## Docs and sandbox
83
+
84
+ Start the component docs and sandbox:
85
+
86
+ ```sh
87
+ pnpm docs:dev
88
+ ```
89
+
90
+ Build the provider-neutral static assets:
91
+
92
+ ```sh
93
+ pnpm docs:build
94
+ ```
95
+
96
+ The docs use hash routes, so the static assets do not require rewrite rules.
97
+ The local Vite server also provides the same-origin `/api/open-vsx` endpoint.
98
+ The theme picker uses this endpoint to search and import Open VSX themes.
99
+
100
+ A production host must provide the same API endpoint for live Open VSX imports.
101
+ Netlify and Amplify can use a serverless adapter. GitHub Pages needs an external
102
+ API or a catalog of normalized themes built in advance.
103
+
82
104
  ## Development
83
105
 
84
106
  ```sh
@@ -90,7 +112,10 @@ Commits follow the Conventional Commits format. Releases run from `main`:
90
112
 
91
113
  - `fix:` publishes a patch release
92
114
  - `feat:` publishes a minor release
93
- - a `BREAKING CHANGE:` footer publishes a major release
115
+ - `BREAKING CHANGE:` or `[BREAKING CHANGE]` anywhere in a commit body publishes
116
+ a major release
117
+ - the same markers anywhere in an associated merged PR body publish a major
118
+ release
94
119
 
95
120
  The `release.yml` workflow publishes through npm trusted publishing with GitHub
96
121
  OIDC. Configure the npm package's trusted publisher once with user
package/dist/open-vsx.js CHANGED
@@ -3,36 +3,36 @@ import { parse as n } from "jsonc-parser";
3
3
  import { createHash as r } from "node:crypto";
4
4
  import i from "jszip";
5
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 {
6
+ var a = "https://open-vsx.org", o = "https://openvsx.eclipsecontent.org", s = `${a}/api/-/search`, c = 524288, l = 262144, u = 262144, d = 1048576, f = 20971520, p = 5e3, m = 104857600, h = 200, g = 40, _ = 8, v = 15e3, y = class extends Error {
7
7
  code;
8
8
  constructor(e, t) {
9
9
  super(t), this.name = "OpenVsxThemeError", this.code = e;
10
10
  }
11
- }, y = (e) => typeof e == "object" && !!e && !Array.isArray(e), b = (e, t) => {
11
+ }, b = (e) => typeof e == "object" && !!e && !Array.isArray(e), x = (e, t) => {
12
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.`);
13
+ if (r.length > 0 || !b(i)) throw new y("unsupported-extension", `${t} is not valid JSON.`);
14
14
  return i;
15
- }, x = (e) => {
15
+ }, S = (e, { allowResourceOrigin: t = !1 } = {}) => {
16
16
  if (typeof e != "string") return null;
17
17
  try {
18
- let t = new URL(e);
19
- return t.origin.toLowerCase() === a ? t.toString() : null;
18
+ let n = new URL(e), r = n.origin.toLowerCase();
19
+ return r === a || t && r === o ? n.toString() : null;
20
20
  } catch {
21
21
  return null;
22
22
  }
23
- }, S = (e) => {
23
+ }, C = (e) => {
24
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.");
25
+ if (!t?.[1] || !t[2]) throw new y("unsupported-extension", "The selected Open VSX extension identifier is invalid.");
26
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) => {
27
+ }, w = (e) => {
28
+ let t = b(e.contributes) ? e.contributes : null;
29
+ return Array.isArray(t?.themes) ? t.themes.filter(b) : [];
30
+ }, T = async (e, t, n) => {
31
31
  let r = Number(e.headers.get("content-length"));
32
- if (Number.isFinite(r) && r > t) throw new v("file-too-large", n);
32
+ if (Number.isFinite(r) && r > t) throw new y("file-too-large", n);
33
33
  if (!e.body) {
34
34
  let r = new Uint8Array(await e.arrayBuffer());
35
- if (r.byteLength > t) throw new v("file-too-large", n);
35
+ if (r.byteLength > t) throw new y("file-too-large", n);
36
36
  return r;
37
37
  }
38
38
  let i = e.body.getReader(), a = [], o = 0;
@@ -40,7 +40,7 @@ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144,
40
40
  for (;;) {
41
41
  let { done: e, value: r } = await i.read();
42
42
  if (e) break;
43
- if (o += r.byteLength, o > t) throw await i.cancel(), new v("file-too-large", n);
43
+ if (o += r.byteLength, o > t) throw await i.cancel(), new y("file-too-large", n);
44
44
  a.push(r);
45
45
  }
46
46
  } finally {
@@ -49,24 +49,24 @@ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144,
49
49
  let s = new Uint8Array(o), c = 0;
50
50
  for (let e of a) s.set(e, c), c += e.byteLength;
51
51
  return s;
52
- }, T = (e) => async ({ url: t, limit: n, failureMessage: r, tooLargeMessage: i }) => {
53
- let a = new AbortController(), o = setTimeout(() => a.abort(), _);
52
+ }, E = (e) => async ({ url: t, limit: n, failureMessage: r, tooLargeMessage: i, allowResourceOrigin: a = !1 }) => {
53
+ let o = new AbortController(), s = setTimeout(() => o.abort(), v);
54
54
  try {
55
- let o = x(String(t));
56
- if (!o) throw new v("open-vsx-unavailable", r);
57
- let s = await e(o, {
55
+ let s = S(String(t), { allowResourceOrigin: a });
56
+ if (!s) throw new y("open-vsx-unavailable", r);
57
+ let c = await e(s, {
58
58
  redirect: "follow",
59
- signal: a.signal
59
+ signal: o.signal
60
60
  });
61
- if (!x(s.url || o) || !s.ok) throw new v("open-vsx-unavailable", r);
62
- return await w(s, n, i);
61
+ if (!S(c.url || s, { allowResourceOrigin: a }) || !c.ok) throw new y("open-vsx-unavailable", r);
62
+ return await T(c, n, i);
63
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);
64
+ throw e instanceof y ? e : new y("open-vsx-unavailable", o.signal.aborted ? "Open VSX took too long to respond." : r);
65
65
  } finally {
66
- clearTimeout(o);
66
+ clearTimeout(s);
67
67
  }
68
- }, E = (e) => {
69
- if (!y(e)) return null;
68
+ }, D = (e) => {
69
+ if (!b(e)) return null;
70
70
  let t = typeof e.namespace == "string" ? e.namespace.trim() : "", n = typeof e.name == "string" ? e.name.trim() : "";
71
71
  if (!t || !n) return null;
72
72
  let r = typeof e.displayName == "string" && e.displayName.trim() ? e.displayName.trim() : n;
@@ -77,49 +77,49 @@ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144,
77
77
  description: typeof e.description == "string" ? e.description.slice(0, 300) : "",
78
78
  downloadCount: typeof e.downloadCount == "number" && Number.isFinite(e.downloadCount) ? Math.max(0, e.downloadCount) : 0
79
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);
80
+ }, O = (e) => {
81
+ if (e.length > 1024 || e.includes("\0") || e.startsWith("/") || /^[a-z]:/i.test(e)) throw new y("unsupported-extension", "A theme path is not safe.");
82
+ }, k = ({ path: e, initialSegments: t, minimumDepth: n }) => {
83
+ O(e);
84
84
  let r = [...t];
85
85
  for (let t of e.replaceAll("\\", "/").split("/")) if (!(!t || t === ".")) {
86
86
  if (t === "..") {
87
- if (r.length <= n) throw new v("unsupported-extension", "A theme path escapes its package.");
87
+ if (r.length <= n) throw new y("unsupported-extension", "A theme path escapes its package.");
88
88
  r.pop();
89
89
  } else r.push(t);
90
90
  }
91
91
  return r.join("/");
92
- }, k = (e, t = "extension/") => {
92
+ }, A = (e, t = "extension/") => {
93
93
  let n = t.split("/").slice(0, -1);
94
- return n[0] !== "extension" && n.unshift("extension"), O({
94
+ return n[0] !== "extension" && n.unshift("extension"), k({
95
95
  path: e,
96
96
  initialSegments: n,
97
97
  minimumDepth: 1
98
98
  });
99
- }, A = (e) => {
100
- let t = O({
99
+ }, j = (e) => {
100
+ let t = k({
101
101
  path: e,
102
102
  initialSegments: [],
103
103
  minimumDepth: 0
104
104
  });
105
- if (!t) throw new v("unsupported-extension", "The extension package has an invalid file name.");
105
+ if (!t) throw new y("unsupported-extension", "The extension package has an invalid file name.");
106
106
  return t;
107
- }, j = (e) => {
107
+ }, M = (e) => {
108
108
  let t = e.byteLength, n = Math.max(0, t - 65557), r = t - 22;
109
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.");
110
+ if (r < n) throw new y("unsupported-extension", "The extension package is not a valid ZIP file.");
111
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.");
112
+ }, N = (e) => {
113
+ let t = M(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 y("unsupported-extension", "The extension package has an invalid ZIP directory.");
115
115
  return {
116
116
  start: r,
117
117
  end: i
118
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.");
119
+ }, P = ({ bytes: e, view: t, offset: n, directoryEnd: r }) => {
120
+ if (n + 46 > r || t.getUint32(n, !0) !== 33639248) throw new y("unsupported-extension", "The extension package has an invalid ZIP directory.");
121
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.");
122
+ if (l > r || i === 4294967295 || a === 4294967295 || a > 0 && (i === 0 || a / i > h)) throw new y("unsupported-extension", "The extension package has unsafe ZIP metadata.");
123
123
  try {
124
124
  return {
125
125
  name: new TextDecoder("utf-8", { fatal: !0 }).decode(e.subarray(n + 46, n + 46 + o)),
@@ -127,32 +127,32 @@ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144,
127
127
  nextOffset: l
128
128
  };
129
129
  } catch {
130
- throw new v("unsupported-extension", "The extension package has an invalid file name.");
130
+ throw new y("unsupported-extension", "The extension package has an invalid file name.");
131
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;
132
+ }, F = (e) => {
133
+ let t = new DataView(e.buffer, e.byteOffset, e.byteLength), n = N(t), r = /* @__PURE__ */ new Map(), i = 0, a = 0, o = n.start;
134
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({
135
+ if (a += 1, a > p) throw new y("unsupported-extension", "The extension package has an invalid ZIP directory.");
136
+ let s = P({
137
137
  bytes: e,
138
138
  view: t,
139
139
  offset: o,
140
140
  directoryEnd: n.end
141
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;
142
+ if (i += s.uncompressed, i > m) throw new y("file-too-large", "The extension package expands beyond the safe limit.");
143
+ r.set(j(s.name), s.uncompressed), o = s.nextOffset;
144
144
  }
145
145
  return r;
146
- }, F = async ({ zip: e, sizes: t, path: n, description: r }) => {
146
+ }, I = async ({ zip: e, sizes: t, path: n, description: r }) => {
147
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.`);
148
+ if (i === void 0) throw new y("unsupported-extension", `${r} is missing.`);
149
+ if (i > d) throw new y("file-too-large", `${r} is too large.`);
150
150
  let a = e.file(n);
151
- if (!a) throw new v("unsupported-extension", `${r} is missing.`);
151
+ if (!a) throw new y("unsupported-extension", `${r} is missing.`);
152
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({
153
+ }, L = async ({ zip: e, sizes: t, path: n, ancestors: r = /* @__PURE__ */ new Set() }) => {
154
+ if (r.size >= _ || r.has(n)) throw new y("unsupported-extension", "Theme includes are cyclic or too deeply nested.");
155
+ let i = x(await I({
156
156
  zip: e,
157
157
  sizes: t,
158
158
  path: n,
@@ -161,90 +161,93 @@ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144,
161
161
  if (typeof i.include != "string") return i;
162
162
  let a = new Set(r);
163
163
  a.add(n);
164
- let o = await I({
164
+ let o = await L({
165
165
  zip: e,
166
166
  sizes: t,
167
- path: k(i.include, n),
167
+ path: A(i.include, n),
168
168
  ancestors: a
169
169
  });
170
170
  return {
171
171
  ...o,
172
172
  ...i,
173
173
  colors: {
174
- ...y(o.colors) ? o.colors : {},
175
- ...y(i.colors) ? i.colors : {}
174
+ ...b(o.colors) ? o.colors : {},
175
+ ...b(i.colors) ? i.colors : {}
176
176
  }
177
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({
178
+ }, R = (e) => e === "vs" ? "light" : e === "vs-dark" ? "dark" : e === "hc-black" ? "hc" : e === "hc-light" ? "hcLight" : null, z = (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], B = async ({ request: e, extensionId: t }) => {
179
+ let [n, r] = C(t), i = await e({
180
180
  url: `${a}/api/${encodeURIComponent(n)}/${encodeURIComponent(r)}`,
181
- limit: c,
181
+ limit: l,
182
182
  failureMessage: "Open VSX theme details are unavailable right now.",
183
183
  tooLargeMessage: "Open VSX returned unexpectedly large theme details."
184
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.");
185
+ if (!b(o) || !b(o.files)) throw new y("unsupported-extension", "Open VSX returned malformed theme details.");
186
+ let s = { allowResourceOrigin: !0 }, c = S(o.files.manifest, s), u = S(o.files.download, s), d = S(o.files.sha256, s), f = typeof o.version == "string" ? o.version : "";
187
+ if (!c || !u || !d || !f) throw new y("unsupported-extension", "Open VSX returned malformed theme details.");
188
188
  return {
189
189
  extensionName: r,
190
- manifestUrl: s,
191
- packageUrl: l,
192
- checksumUrl: u,
193
- version: d
190
+ manifestUrl: c,
191
+ packageUrl: u,
192
+ checksumUrl: d,
193
+ version: f
194
194
  };
195
- }, B = async ({ request: e, manifestUrl: t }) => {
195
+ }, V = async ({ request: e, manifestUrl: t }) => {
196
196
  let n = await e({
197
197
  url: t,
198
- limit: l,
198
+ limit: u,
199
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 }) => {
200
+ tooLargeMessage: "That extension manifest is too large.",
201
+ allowResourceOrigin: !0
202
+ }), r = w(x(new TextDecoder().decode(n), "The extension manifest"));
203
+ if (r.length === 0 || r.length > g) throw new y("unsupported-extension", "That extension does not contain a supported number of color themes.");
204
+ }, H = async ({ request: e, packageUrl: t, checksumUrl: n }) => {
204
205
  let [i, a] = await Promise.all([e({
205
206
  url: t,
206
- limit: d,
207
+ limit: f,
207
208
  failureMessage: "That Open VSX theme could not be downloaded.",
208
- tooLargeMessage: "That theme extension is too large to import safely."
209
+ tooLargeMessage: "That theme extension is too large to import safely.",
210
+ allowResourceOrigin: !0
209
211
  }), e({
210
212
  url: n,
211
213
  limit: 256,
212
214
  failureMessage: "That Open VSX theme has no readable checksum.",
213
- tooLargeMessage: "That Open VSX checksum response is invalid."
215
+ tooLargeMessage: "That Open VSX checksum response is invalid.",
216
+ allowResourceOrigin: !0
214
217
  })]), 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.");
218
+ if (!o || !/^[a-f\d]{64}$/i.test(o)) throw new y("integrity-failed", "That Open VSX theme has an invalid checksum.");
219
+ if (o.toLowerCase() !== s) throw new y("integrity-failed", "That Open VSX theme failed its integrity check.");
217
220
  return i;
218
- }, H = async (e) => {
219
- let t = P(e);
221
+ }, U = async (e) => {
222
+ let t = F(e);
220
223
  try {
221
224
  return {
222
225
  zip: await i.loadAsync(e),
223
226
  sizes: t
224
227
  };
225
228
  } catch {
226
- throw new v("unsupported-extension", "That extension package could not be opened.");
229
+ throw new y("unsupported-extension", "That extension package could not be opened.");
227
230
  }
228
- }, U = async ({ zip: e, sizes: t, extensionId: n, version: r }) => {
229
- let i = b(await F({
231
+ }, W = async ({ zip: e, sizes: t, extensionId: n, version: r }) => {
232
+ let i = x(await I({
230
233
  zip: e,
231
234
  sizes: t,
232
235
  path: "extension/package.json",
233
236
  description: "The packaged extension manifest"
234
237
  }), "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.");
238
+ if ((typeof i.publisher == "string" && typeof i.name == "string" ? `${i.publisher}.${i.name}` : "").toLowerCase() !== n.toLowerCase() || i.version !== r) throw new y("integrity-failed", "The downloaded package does not match the selected Open VSX theme.");
236
239
  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
+ }, G = async ({ zip: e, sizes: n, manifest: r, fallbackName: i }) => {
241
+ let a = w(r);
242
+ if (a.length === 0 || a.length > g) throw new y("unsupported-extension", "That extension has no compatible color themes.");
240
243
  let o = [];
241
244
  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({
245
+ if (typeof r.path != "string") throw new y("unsupported-extension", "A contributed theme has no valid path.");
246
+ let a = A(r.path), s = await L({
244
247
  zip: e,
245
248
  sizes: n,
246
249
  path: a
247
- }), c = L(r.uiTheme), l = typeof r.label == "string" && r.label.trim() ? r.label.trim() : i;
250
+ }), c = R(r.uiTheme), l = typeof r.label == "string" && r.label.trim() ? r.label.trim() : i;
248
251
  o.push(t({
249
252
  source: JSON.stringify({
250
253
  ...s,
@@ -255,8 +258,8 @@ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144,
255
258
  }));
256
259
  }
257
260
  return o;
258
- }, G = ({ fetchImpl: t = fetch } = {}) => {
259
- let n = T(t);
261
+ }, K = ({ fetchImpl: t = fetch } = {}) => {
262
+ let n = E(t);
260
263
  return {
261
264
  search: async (e) => {
262
265
  let t = e.trim();
@@ -269,21 +272,35 @@ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144,
269
272
  message: "Search terms must be 100 characters or fewer."
270
273
  };
271
274
  try {
272
- let e = new URL(o);
275
+ let e = new URL(s);
273
276
  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
277
  let r = await n({
275
278
  url: e,
276
- limit: s,
279
+ limit: c,
277
280
  failureMessage: "Open VSX search is unavailable right now.",
278
281
  tooLargeMessage: "Open VSX returned an unexpectedly large search response."
279
282
  }), i = JSON.parse(new TextDecoder().decode(r));
280
- if (!y(i) || !Array.isArray(i.extensions)) throw Error("invalid response");
283
+ if (!b(i) || !Array.isArray(i.extensions)) throw Error("invalid response");
284
+ let a = i.extensions.flatMap((e) => {
285
+ let t = D(e);
286
+ return t ? [t] : [];
287
+ }).slice(0, 12);
281
288
  return {
282
289
  status: "success",
283
- themes: i.extensions.flatMap((e) => {
284
- let t = E(e);
285
- return t ? [t] : [];
286
- }).slice(0, 12)
290
+ themes: (await Promise.all(a.map(async (e) => {
291
+ try {
292
+ let t = await B({
293
+ request: n,
294
+ extensionId: e.id
295
+ });
296
+ return await V({
297
+ request: n,
298
+ manifestUrl: t.manifestUrl
299
+ }), e;
300
+ } catch {
301
+ return null;
302
+ }
303
+ }))).filter((e) => e !== null)
287
304
  };
288
305
  } catch {
289
306
  return {
@@ -294,24 +311,24 @@ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144,
294
311
  },
295
312
  importTheme: async (t, r) => {
296
313
  try {
297
- let e = await z({
314
+ let e = await B({
298
315
  request: n,
299
316
  extensionId: t
300
317
  });
301
- await B({
318
+ await V({
302
319
  request: n,
303
320
  manifestUrl: e.manifestUrl
304
321
  });
305
- let { zip: i, sizes: a } = await H(await V({
322
+ let { zip: i, sizes: a } = await U(await H({
306
323
  request: n,
307
324
  ...e
308
325
  }));
309
326
  return {
310
327
  status: "success",
311
- theme: R(await W({
328
+ theme: z(await G({
312
329
  zip: i,
313
330
  sizes: a,
314
- manifest: await U({
331
+ manifest: await W({
315
332
  zip: i,
316
333
  sizes: a,
317
334
  extensionId: t,
@@ -321,7 +338,7 @@ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144,
321
338
  }), r)
322
339
  };
323
340
  } catch (t) {
324
- return t instanceof e || t instanceof v ? {
341
+ return t instanceof e || t instanceof y ? {
325
342
  status: "error",
326
343
  error: {
327
344
  code: t.code,
@@ -339,4 +356,4 @@ var a = "https://open-vsx.org", o = `${a}/api/-/search`, s = 524288, c = 262144,
339
356
  };
340
357
  };
341
358
  //#endregion
342
- export { G as createOpenVsxThemeService };
359
+ export { K as createOpenVsxThemeService };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filipgutica/ui",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "A compact Vue design system with Tailwind CSS and VS Code theme support",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -31,12 +31,17 @@
31
31
  },
32
32
  "scripts": {
33
33
  "build": "vite build && vue-tsc -p tsconfig.build.json --emitDeclarationOnly",
34
+ "docs:build": "vite build --config docs/vite.config.ts",
35
+ "docs:dev": "vite --config docs/vite.config.ts",
36
+ "docs:preview": "vite preview --config docs/vite.config.ts",
37
+ "docs:smoke": "node scripts/docs-smoke.mjs",
38
+ "docs:typecheck": "vue-tsc -p docs/tsconfig.json --noEmit",
34
39
  "prepublishOnly": "pnpm verify",
35
40
  "release": "semantic-release",
36
41
  "release:dry-run": "semantic-release --dry-run --no-ci",
37
42
  "test": "vitest run",
38
43
  "typecheck": "vue-tsc -p tsconfig.json --noEmit",
39
- "verify": "pnpm typecheck && pnpm test && pnpm build && pnpm pack --dry-run && publint"
44
+ "verify": "pnpm typecheck && pnpm docs:typecheck && pnpm test && pnpm build && pnpm docs:build && pnpm pack --dry-run && publint"
40
45
  },
41
46
  "engines": {
42
47
  "node": ">=22.12"
@@ -67,6 +72,7 @@
67
72
  "@commitlint/cli": "^21.2.2",
68
73
  "@commitlint/config-conventional": "^21.2.2",
69
74
  "@semantic-release/commit-analyzer": "13.0.1",
75
+ "@tailwindcss/vite": "^4.3.3",
70
76
  "@types/node": "^26.0.0",
71
77
  "@vitejs/plugin-vue": "^6.0.0",
72
78
  "@vue/test-utils": "^2.4.6",
@@ -3,10 +3,16 @@
3
3
  .fg-input,
4
4
  .fg-select,
5
5
  .fg-dialog__close {
6
- min-height: 2.5rem;
6
+ min-height: 2.75rem;
7
7
  border: 1px solid transparent;
8
8
  border-radius: var(--radius-control);
9
9
  font: inherit;
10
+ transition:
11
+ background-color var(--duration-fast) ease,
12
+ border-color var(--duration-fast) ease,
13
+ color var(--duration-fast) ease,
14
+ box-shadow var(--duration-fast) ease,
15
+ transform var(--duration-fast) ease;
10
16
  }
11
17
 
12
18
  .fg-button {
@@ -14,10 +20,12 @@
14
20
  align-items: center;
15
21
  justify-content: center;
16
22
  gap: 0.5rem;
17
- padding: 0.55rem 0.85rem;
23
+ padding: 0.65rem 1rem;
18
24
  color: var(--color-button-text);
19
25
  background: var(--color-button-bg);
20
- font-weight: 680;
26
+ box-shadow: var(--shadow-control);
27
+ font-weight: 700;
28
+ letter-spacing: -0.01em;
21
29
  line-height: 1.25;
22
30
  cursor: pointer;
23
31
  }
@@ -26,6 +34,11 @@
26
34
  background: var(--color-button-hover);
27
35
  }
28
36
 
37
+ .fg-button:active:not(:disabled) {
38
+ transform: translateY(1px);
39
+ box-shadow: none;
40
+ }
41
+
29
42
  .fg-button:disabled,
30
43
  .fg-input:disabled,
31
44
  .fg-select:disabled,
@@ -38,9 +51,20 @@
38
51
  cursor: wait;
39
52
  }
40
53
 
54
+ .fg-button[aria-busy="true"]::before {
55
+ width: 0.85rem;
56
+ height: 0.85rem;
57
+ flex: none;
58
+ border: 2px solid currentColor;
59
+ border-right-color: transparent;
60
+ border-radius: 50%;
61
+ content: "";
62
+ animation: fg-button-spin 700ms linear infinite;
63
+ }
64
+
41
65
  .fg-button[data-size="compact"] {
42
- min-height: 2rem;
43
- padding: 0.35rem 0.6rem;
66
+ min-height: 2.25rem;
67
+ padding: 0.4rem 0.7rem;
44
68
  font-size: 0.8125rem;
45
69
  }
46
70
 
@@ -59,11 +83,20 @@
59
83
  .fg-button[data-variant="ghost"] {
60
84
  color: var(--color-muted);
61
85
  background: transparent;
86
+ box-shadow: none;
62
87
  }
63
88
 
64
89
  .fg-button[data-variant="danger"] {
65
90
  color: var(--color-error);
66
91
  background: var(--color-error-surface);
92
+ box-shadow: none;
93
+ }
94
+
95
+ .fg-button[data-variant="danger"]:hover:not(:disabled) {
96
+ border-color: var(--color-error);
97
+ color: var(--color-error);
98
+ background: var(--color-error-surface);
99
+ box-shadow: inset 0 0 0 1px var(--color-error);
67
100
  }
68
101
 
69
102
  .fg-button[data-variant="text"] {
@@ -73,6 +106,7 @@
73
106
  background: transparent;
74
107
  text-decoration: underline;
75
108
  text-underline-offset: 0.2em;
109
+ box-shadow: none;
76
110
  }
77
111
 
78
112
  .fg-button[data-variant="text"]:hover:not(:disabled) {
@@ -87,6 +121,7 @@
87
121
  border-color: var(--color-input-border);
88
122
  color: var(--color-input-text);
89
123
  background: var(--color-input-bg);
124
+ box-shadow: var(--shadow-control);
90
125
  line-height: 1.35;
91
126
  }
92
127
 
@@ -107,9 +142,9 @@
107
142
  }
108
143
 
109
144
  .fg-field__label {
110
- color: var(--color-muted);
145
+ color: var(--color-text);
111
146
  font-size: 0.8125rem;
112
- font-weight: 680;
147
+ font-weight: 700;
113
148
  }
114
149
 
115
150
  .fg-field__description,
@@ -132,14 +167,15 @@
132
167
  .fg-checkbox__control {
133
168
  display: inline-grid;
134
169
  flex: none;
135
- width: 1.15rem;
136
- height: 1.15rem;
170
+ width: 1.25rem;
171
+ height: 1.25rem;
137
172
  margin-top: 0.1rem;
138
173
  place-items: center;
139
174
  border: 1px solid var(--color-input-border);
140
175
  border-radius: 0.3rem;
141
176
  color: var(--color-button-text);
142
177
  background: var(--color-input-bg);
178
+ box-shadow: var(--shadow-control);
143
179
  }
144
180
 
145
181
  .fg-checkbox__control[data-state="checked"] {
@@ -180,14 +216,15 @@
180
216
  gap: 0.75rem;
181
217
  padding: 0.85rem 1rem;
182
218
  border: 1px solid var(--color-border-subtle);
219
+ border-left: 3px solid var(--color-border);
183
220
  border-radius: var(--radius-control);
184
221
  color: var(--color-text);
185
222
  background: var(--color-surface);
186
223
  }
187
224
 
188
- .fg-alert[data-tone="info"] { background: var(--color-info-surface); }
189
- .fg-alert[data-tone="success"] { background: var(--color-success-surface); }
190
- .fg-alert[data-tone="warning"] { background: var(--color-warning-surface); }
225
+ .fg-alert[data-tone="info"] { border-left-color: var(--color-info); background: var(--color-info-surface); }
226
+ .fg-alert[data-tone="success"] { border-left-color: var(--color-success); background: var(--color-success-surface); }
227
+ .fg-alert[data-tone="warning"] { border-left-color: var(--color-warning); background: var(--color-warning-surface); }
191
228
  .fg-alert[data-tone="danger"],
192
229
  .fg-alert[data-tone="error"] {
193
230
  border-color: var(--color-error);
@@ -199,6 +236,7 @@
199
236
  border: 1px solid var(--color-border-subtle);
200
237
  border-radius: var(--radius-panel);
201
238
  background: var(--color-surface-raised);
239
+ box-shadow: 0 1px 2px var(--color-shadow);
202
240
  }
203
241
 
204
242
  .fg-surface[data-padding="compact"] { padding: 0.85rem; }
@@ -225,6 +263,7 @@
225
263
  inset: 0;
226
264
  z-index: 50;
227
265
  background: var(--color-overlay);
266
+ backdrop-filter: blur(2px);
228
267
  }
229
268
 
230
269
  .fg-dialog__content {
@@ -259,7 +298,8 @@
259
298
  .fg-dialog__title {
260
299
  margin: 0;
261
300
  font-size: 1.1rem;
262
- font-weight: 750;
301
+ font-weight: 760;
302
+ letter-spacing: -0.02em;
263
303
  }
264
304
 
265
305
  .fg-dialog__description {
@@ -277,7 +317,7 @@
277
317
  .fg-dialog__close {
278
318
  display: inline-grid;
279
319
  flex: none;
280
- width: 2.5rem;
320
+ width: 2.75rem;
281
321
  padding: 0;
282
322
  place-items: center;
283
323
  color: var(--color-muted);
@@ -288,6 +328,10 @@
288
328
 
289
329
  .fg-dialog__close:hover { color: var(--color-text); background: var(--color-row-hover); }
290
330
 
331
+ @keyframes fg-button-spin {
332
+ to { transform: rotate(360deg); }
333
+ }
334
+
291
335
  .fg-button:focus-visible,
292
336
  .fg-input:focus-visible,
293
337
  .fg-select:focus-visible,
@@ -322,6 +366,12 @@
322
366
  }
323
367
 
324
368
  @media (prefers-reduced-motion: reduce) {
369
+ .fg-button,
370
+ .fg-input,
371
+ .fg-select,
372
+ .fg-dialog__close,
325
373
  .fg-progress__indicator { transition: none; }
374
+
375
+ .fg-button[aria-busy="true"]::before { animation-duration: 1.4s; }
326
376
  }
327
377
  }
package/src/theme.css CHANGED
@@ -2,78 +2,79 @@
2
2
 
3
3
  :root {
4
4
  color-scheme: light;
5
- --color-bg: #eff1f5;
6
- --color-surface: #e6e9ef;
5
+ --color-bg: #ffffff;
6
+ --color-surface: #f7f8fa;
7
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;
8
+ --color-surface-muted: #eef1f5;
9
+ --color-text: #101828;
10
+ --color-muted: #5d6979;
11
+ --color-link: #155eef;
12
+ --color-accent: #4d7c0f;
13
+ --color-focus: #155eef;
14
+ --color-border: #667085;
15
+ --color-border-subtle: #d8dee8;
16
+ --color-button-bg: #155eef;
17
17
  --color-button-text: #fff;
18
- --color-button-hover: #1a5ad7;
18
+ --color-button-hover: #004eeb;
19
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%);
20
+ --color-input-text: #101828;
21
+ --color-input-border: #667085;
22
+ --color-input-placeholder: #667085;
23
+ --color-row-hover: #f2f4f7;
24
+ --color-row-selected: #eaf1ff;
25
+ --color-row-selected-text: #143d8f;
26
+ --color-code: #0c1426;
27
+ --color-code-text: #edf2ff;
28
+ --color-error: #c01048;
29
+ --color-warning: #854a0e;
30
+ --color-success: #067647;
31
+ --color-info: #175cd3;
32
+ --color-progress: #155eef;
33
+ --color-shadow: rgb(16 24 40 / 10%);
34
34
  --color-overlay: rgb(0 0 0 / 42%);
35
35
  --color-error-surface: color-mix(in srgb, var(--color-error) 12%, var(--color-surface));
36
36
  --color-warning-surface: color-mix(in srgb, var(--color-warning) 13%, var(--color-surface));
37
37
  --color-success-surface: color-mix(in srgb, var(--color-success) 12%, var(--color-surface));
38
38
  --color-info-surface: color-mix(in srgb, var(--color-info) 12%, var(--color-surface));
39
- --radius-control: 0.55rem;
40
- --radius-panel: 0.75rem;
39
+ --radius-control: 0.375rem;
40
+ --radius-panel: 0.5rem;
41
41
  --radius-pill: 999px;
42
- --duration-fast: 180ms;
43
- --shadow-dialog: 0 20px 64px rgb(0 0 0 / 24%);
42
+ --duration-fast: 140ms;
43
+ --shadow-control: 0 1px 2px rgb(16 24 40 / 8%);
44
+ --shadow-dialog: 0 24px 80px rgb(5 12 24 / 28%);
44
45
  }
45
46
 
46
47
  :root.dark {
47
48
  color-scheme: dark;
48
- --color-bg: #1e1e2e;
49
- --color-surface: #181825;
50
- --color-surface-raised: #242435;
51
- --color-surface-muted: #313244;
52
- --color-text: #cdd6f4;
53
- --color-muted: #a6adc8;
54
- --color-link: #89b4fa;
55
- --color-accent: #cba6f7;
56
- --color-focus: #89b4fa;
57
- --color-border: #585b70;
58
- --color-border-subtle: #45475a;
59
- --color-button-bg: #89b4fa;
60
- --color-button-text: #11111b;
61
- --color-button-hover: #b4befe;
62
- --color-input-bg: #11111b;
63
- --color-input-text: #cdd6f4;
64
- --color-input-border: #6c7086;
65
- --color-input-placeholder: #9399b2;
66
- --color-row-hover: #313244;
67
- --color-row-selected: #45475a;
68
- --color-row-selected-text: #cdd6f4;
69
- --color-code: #11111b;
70
- --color-code-text: #cdd6f4;
71
- --color-error: #f38ba8;
72
- --color-warning: #f9e2af;
73
- --color-success: #a6e3a1;
74
- --color-info: #89dceb;
75
- --color-progress: #cba6f7;
76
- --color-shadow: rgb(0 0 0 / 28%);
49
+ --color-bg: #0b1020;
50
+ --color-surface: #111827;
51
+ --color-surface-raised: #151d2f;
52
+ --color-surface-muted: #202a3d;
53
+ --color-text: #f4f7ff;
54
+ --color-muted: #a8b3c7;
55
+ --color-link: #8fb4ff;
56
+ --color-accent: #b7ef53;
57
+ --color-focus: #8fb4ff;
58
+ --color-border: #647189;
59
+ --color-border-subtle: #2d384d;
60
+ --color-button-bg: #6e9cff;
61
+ --color-button-text: #071126;
62
+ --color-button-hover: #8fb4ff;
63
+ --color-input-bg: #0d1526;
64
+ --color-input-text: #f4f7ff;
65
+ --color-input-border: #647189;
66
+ --color-input-placeholder: #8995aa;
67
+ --color-row-hover: #1b2538;
68
+ --color-row-selected: #1b315b;
69
+ --color-row-selected-text: #dbe7ff;
70
+ --color-code: #070d19;
71
+ --color-code-text: #edf2ff;
72
+ --color-error: #ff8aad;
73
+ --color-warning: #f8c477;
74
+ --color-success: #75e0a7;
75
+ --color-info: #84adff;
76
+ --color-progress: #8fb4ff;
77
+ --color-shadow: rgb(0 0 0 / 32%);
77
78
  --color-overlay: rgb(0 0 0 / 62%);
78
79
  }
79
80