@glasshome/widget-sdk 1.9.0 → 1.9.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.
@@ -33,6 +33,22 @@ export declare function isWidgetExternal(id: string): boolean;
33
33
  * BUILD_CACHE_DIR (the theme `@import`) are the SDK's business, not the widget's.
34
34
  */
35
35
  export declare function isDirectUiImportSource(id: string, code: string): boolean;
36
+ /**
37
+ * Warns (never fails) when widget source imports @glasshome/ui directly.
38
+ * Warn-only counterpart to syncLayerImportGuard: direct imports keep working
39
+ * until 2.0, but they bypass the sdkVersion gate, so ui can drift against
40
+ * published widgets with nothing checking compatibility. Detection scans
41
+ * module source in `transform` because externalized specifiers never reach
42
+ * `resolveId` (rollup consults `external` for the bare id first).
43
+ *
44
+ * `warned` carries the files already reported across every build in one run, so
45
+ * a module shared by N widgets is named once and not once per bundle it lands in.
46
+ */
47
+ export declare function createUiImportTracker(warned?: Set<string>): {
48
+ scan(id: string, code: string): void;
49
+ /** Offending files this run has not reported yet; marks them reported. */
50
+ take(): string[];
51
+ };
36
52
  interface DiscoveredWidget {
37
53
  name: string;
38
54
  entry: string;
@@ -146,31 +146,45 @@ function R(t, n) {
146
146
  let i = e.find((e) => e.id === "direct-ui-import");
147
147
  return i?.sourcePattern ? new RegExp(i.sourcePattern).test(n) : !1;
148
148
  }
149
- function z() {
150
- let n = /* @__PURE__ */ new Set();
149
+ function z(e = /* @__PURE__ */ new Set()) {
150
+ let t = /* @__PURE__ */ new Set();
151
+ return {
152
+ scan(e, n) {
153
+ R(e, n) && t.add(N(_(process.cwd(), e.split("?")[0] ?? e)));
154
+ },
155
+ take() {
156
+ let n = [...t].filter((t) => !e.has(t));
157
+ for (let t of n) e.add(t);
158
+ return n;
159
+ }
160
+ };
161
+ }
162
+ function B(n) {
163
+ let r = z(n);
151
164
  return {
152
165
  name: "glasshome-widget:ui-import-guard",
153
166
  apply: "build",
154
167
  enforce: "pre",
155
168
  transform(e, t) {
156
- return R(t, e) && n.add(N(_(process.cwd(), t.split("?")[0] ?? t))), null;
169
+ return r.scan(t, e), null;
157
170
  },
158
171
  buildEnd() {
159
- if (n.size === 0) return;
160
- let r = e.find((e) => e.id === "direct-ui-import"), i = r ? t(r) : "[@glasshome/widget-sdk] Direct @glasshome/ui imports are deprecated; import the same export from \"@glasshome/widget-sdk\".", a = [...n].map((e) => ` ${e}`).join("\n");
161
- console.warn(`${i}\n Direct @glasshome/ui import(s) in:\n${a}`);
172
+ let n = r.take();
173
+ if (n.length === 0) return;
174
+ let i = e.find((e) => e.id === "direct-ui-import"), a = i ? t(i) : "[@glasshome/widget-sdk] Direct @glasshome/ui imports are deprecated; import the same export from \"@glasshome/widget-sdk\".", o = n.map((e) => ` ${e}`).join("\n");
175
+ console.warn(`${a}\n Direct @glasshome/ui import(s) in:\n${o}`);
162
176
  }
163
177
  };
164
178
  }
165
- var B = "node_modules/.cache/glasshome-widgets";
166
- function V(e) {
179
+ var V = "node_modules/.cache/glasshome-widgets";
180
+ function H(e) {
167
181
  try {
168
182
  return m(g(process.cwd(), "package.json")).resolve(e);
169
183
  } catch {
170
184
  return null;
171
185
  }
172
186
  }
173
- var H = [
187
+ var U = [
174
188
  "--radius",
175
189
  "--background",
176
190
  "--foreground",
@@ -180,21 +194,21 @@ var H = [
180
194
  "--muted",
181
195
  "--accent"
182
196
  ];
183
- function U(e, t) {
197
+ function W(e, t) {
184
198
  let n = g(e, `${t}.css`);
185
199
  if (!s(n)) return;
186
200
  let r = l(n, "utf-8");
187
201
  for (let e of r.matchAll(/:host[^{]*\{([^}]*)\}/g)) {
188
- let n = e[1] ?? "", r = H.find((e) => n.includes(`${e}:`));
202
+ let n = e[1] ?? "", r = U.find((e) => n.includes(`${e}:`));
189
203
  if (r) throw Error(`[glasshome-widget] "${t}.css" defines theme variable "${r}" on :host. Theme variables must inherit from the document so widgets follow the household theme — never re-scope them into the widget's shadow root.`);
190
204
  }
191
205
  }
192
- function W(e, t, n) {
193
- let r = v(e, B);
206
+ function G(e, t, n) {
207
+ let r = v(e, V);
194
208
  c(r, { recursive: !0 });
195
- let i = V("@glasshome/ui/styles");
209
+ let i = H("@glasshome/ui/styles");
196
210
  if (!i) throw Error("[widget-sdk] @glasshome/ui is required to build widgets (it provides the theme every GlassHome widget ships with). Add it to your widget's dependencies.");
197
- let a = v(h(i), "../.."), o = V("@glasshome/widget-sdk/tailwind-sources"), l = o ? h(o) : null, u = [
211
+ let a = v(h(i), "../.."), o = H("@glasshome/widget-sdk/tailwind-sources"), l = o ? h(o) : null, u = [
198
212
  g(a, "src"),
199
213
  g(a, "dist"),
200
214
  ...l ? [g(l, "src"), g(l, "dist")] : [],
@@ -218,12 +232,12 @@ function W(e, t, n) {
218
232
  ""
219
233
  ].join("\n")), f;
220
234
  }
221
- function G() {
235
+ function K() {
222
236
  let e = x();
223
237
  for (let t of e) t.apply ??= "build";
224
238
  return e;
225
239
  }
226
- function K(e) {
240
+ function q(e) {
227
241
  let t = [];
228
242
  if (!s(e)) return t;
229
243
  for (let n of u(e)) {
@@ -237,7 +251,7 @@ function K(e) {
237
251
  }
238
252
  return t;
239
253
  }
240
- function q(e, t) {
254
+ function J(e, t) {
241
255
  let r = [];
242
256
  if (s(e)) for (let a of u(e)) {
243
257
  if (!f(g(e, a)).isDirectory()) continue;
@@ -260,8 +274,8 @@ function q(e, t) {
260
274
  };
261
275
  s(t) || c(t, { recursive: !0 }), p(g(t, "registry.json"), JSON.stringify(a, null, 2)), console.log(`[registry] Generated registry.json with ${r.length} widget(s)`);
262
276
  }
263
- async function J(e) {
264
- let { build: t } = await import("vite"), n = process.cwd(), r = v(n, e?.srcDir ?? "src"), i = v(n, e?.outDir ?? "dist"), a = K(r);
277
+ async function Y(e) {
278
+ let { build: t } = await import("vite"), n = process.cwd(), r = v(n, e?.srcDir ?? "src"), i = v(n, e?.outDir ?? "dist"), a = q(r);
265
279
  if (a.length === 0) {
266
280
  console.warn("[glasshome-widgets] No widgets found in", r);
267
281
  return;
@@ -271,19 +285,20 @@ async function J(e) {
271
285
  a = a.filter((e) => t.has(e.name));
272
286
  }
273
287
  e?.only || s(i) && d(i, { recursive: !0 }), s(i) || c(i, { recursive: !0 });
288
+ let o = /* @__PURE__ */ new Set();
274
289
  for (let r of a) await t({
275
290
  configFile: !1,
276
291
  root: n,
277
292
  plugins: [
278
- ...G(),
293
+ ...K(),
279
294
  ...e?.plugins ?? [],
280
295
  I(),
281
- z()
296
+ B(o)
282
297
  ],
283
298
  ...e?.viteConfig,
284
299
  build: {
285
300
  lib: {
286
- entry: W(n, r.name, r.entry),
301
+ entry: G(n, r.name, r.entry),
287
302
  formats: ["es"],
288
303
  fileName: r.name,
289
304
  cssFileName: r.name
@@ -295,15 +310,15 @@ async function J(e) {
295
310
  ...e?.viteConfig?.build
296
311
  },
297
312
  logLevel: "warn"
298
- }), U(i, r.name), await k({
313
+ }), W(i, r.name), await k({
299
314
  outFile: g(i, `${r.name}.js`),
300
315
  hashFile: g(h(r.entry), ".schema-hash"),
301
316
  widgetName: r.name,
302
317
  manifestPath: g(h(r.entry), "manifest.json")
303
318
  });
304
- q(r, i);
319
+ J(r, i);
305
320
  }
306
- function Y(e) {
321
+ function X(e) {
307
322
  let t = e?.entry ?? "src/index.tsx", n = {
308
323
  name: "glasshome-widget:build",
309
324
  apply: "build",
@@ -311,7 +326,7 @@ function Y(e) {
311
326
  let e = process.cwd();
312
327
  return { build: {
313
328
  lib: {
314
- entry: W(e, "index", v(e, t)),
329
+ entry: G(e, "index", v(e, t)),
315
330
  formats: ["es"],
316
331
  fileName: "index",
317
332
  cssFileName: "index"
@@ -357,7 +372,7 @@ function Y(e) {
357
372
  name: "glasshome-widget:schema",
358
373
  apply: "build",
359
374
  async closeBundle() {
360
- U(v(process.cwd(), "dist"), "index"), await k({
375
+ W(v(process.cwd(), "dist"), "index"), await k({
361
376
  outFile: v(process.cwd(), "dist", "index.js"),
362
377
  hashFile: v(process.cwd(), ".schema-hash"),
363
378
  widgetName: "index",
@@ -366,15 +381,15 @@ function Y(e) {
366
381
  }
367
382
  };
368
383
  return [
369
- ...G(),
384
+ ...K(),
370
385
  n,
371
386
  I(),
372
- z(),
387
+ B(),
373
388
  i,
374
389
  r
375
390
  ];
376
391
  }
377
- function X(e) {
392
+ function Z(e) {
378
393
  let t = e?.srcDir ?? "src", n = e?.outDir ?? "dist", r = [];
379
394
  return [{
380
395
  name: "glasshome-widgets:build",
@@ -393,7 +408,7 @@ function X(e) {
393
408
  if (e === "\0glasshome-noop") return "export {}";
394
409
  },
395
410
  async closeBundle() {
396
- await J({
411
+ await Y({
397
412
  srcDir: t,
398
413
  outDir: n,
399
414
  plugins: r
@@ -402,4 +417,4 @@ function X(e) {
402
417
  }];
403
418
  }
404
419
  //#endregion
405
- export { C as MAX_EXAMPLES, J as buildWidgets, K as discoverWidgets, q as generateRegistry, Y as glasshomeWidget, X as glasshomeWidgets, R as isDirectUiImportSource, F as isWidgetExternal, k as runSchemaGuard };
420
+ export { C as MAX_EXAMPLES, Y as buildWidgets, z as createUiImportTracker, q as discoverWidgets, J as generateRegistry, X as glasshomeWidget, Z as glasshomeWidgets, R as isDirectUiImportSource, F as isWidgetExternal, k as runSchemaGuard };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/widget-sdk",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "SDK for building GlassHome dashboard widgets with SolidJS",
5
5
  "keywords": [
6
6
  "glasshome",
@@ -51,13 +51,13 @@
51
51
  "tailwind-sources.css"
52
52
  ],
53
53
  "scripts": {
54
- "build": "vite build && tsc",
54
+ "build": "vite build && tsc && bun scripts/check-host-singleton.ts",
55
55
  "dev": "vite build --watch",
56
56
  "test": "bun test",
57
57
  "typecheck:test": "tsc --noEmit -p tsconfig.test.json"
58
58
  },
59
59
  "peerDependencies": {
60
- "@glasshome/sync-layer": "0.5.0",
60
+ "@glasshome/sync-layer": "0.6.0",
61
61
  "@glasshome/ui": "1.3.0",
62
62
  "solid-js": "^1.9.11"
63
63
  },
@@ -1,33 +0,0 @@
1
- import { createContext as e, useContext as t } from "solid-js";
2
- //#region src/framework/hooks/use-widget-context.ts
3
- var n = e();
4
- function r() {
5
- let e = t(n);
6
- if (!e) throw Error("useWidgetContext must be used within a Widget component");
7
- return e;
8
- }
9
- //#endregion
10
- //#region src/framework/theming/tokens.css?raw
11
- var i = "/* Authoritative SDK CSS contract. Injected once on first Widget mount\n * via framework/theming/tokens.ts.\n *\n * All widget-internal visual scale is driven from CSS custom properties +\n * container queries on the `.glasshome-widget` shell (`container-type: size`).\n * No JS classifier, no size tier enum.\n */\n\n:root {\n --tone-success: oklch(0.70 0.18 145);\n --tone-warning: oklch(0.80 0.17 75);\n --tone-danger: oklch(0.64 0.22 27);\n --tone-info: oklch(0.66 0.20 245);\n --tone-neutral: oklch(0.65 0.02 250);\n --tone-accent: oklch(0.74 0.18 60);\n\n --widget-grad-strength: 1;\n --widget-glow-default: 0.4;\n --widget-border-highlight: oklch(0.98 0 0 / 0.4);\n}\n\n.dark {\n --tone-success: oklch(0.74 0.18 145);\n --tone-warning: oklch(0.82 0.17 75);\n --tone-danger: oklch(0.68 0.22 27);\n --tone-info: oklch(0.70 0.20 245);\n --tone-neutral: oklch(0.69 0.02 250);\n --tone-accent: oklch(0.78 0.18 60);\n\n --widget-grad-strength: 1.1;\n --widget-glow-default: 0.5;\n --widget-border-highlight: oklch(1 0 0 / 0.08);\n}\n\n@property --widget-color {\n syntax: \"<color>\";\n inherits: true;\n initial-value: oklch(0.65 0.02 250);\n}\n\n/* === Widget shell ===\n * Fluid scale tokens — tied to the shell's own container box (cqb=height, cqi=width).\n * Icon/pad/gap track HEIGHT (cqb) not the shortest side: two widgets of equal\n * height match regardless of width (a 2×2 and a 4×2 get the same icon). cqmin\n * starved portrait tiles by pinning the icon to the narrow width. Tune\n * multipliers / clamps to adjust density. */\n.glasshome-widget {\n --widget-color: var(--tone-neutral);\n --widget-glow-strength: var(--widget-glow-default);\n\n --widget-icon-box: clamp(44px, 25cqb, 58px);\n --widget-icon-glyph: clamp(24px, 14cqb, 33px);\n --widget-pad: clamp(8px, 3.5cqb, 22px);\n --widget-gap: clamp(6px, 2cqb, 14px);\n --widget-title-size: clamp(12px, 2cqi, 18px);\n --widget-subtitle: clamp(12px, 2.5cqi, 20px);\n --widget-status-size: clamp(14px, 5cqi, 36px);\n --widget-value-size: clamp(18px, 7cqi, 48px);\n --widget-badge-size: clamp(11px, 1.6cqi, 14px);\n}\n\n/* Shell material. Two channels, both host/variant-driven:\n * - `--widget-backdrop`: the live backdrop filter (the shared glass recipe is\n * blur + saturate, matching ui's CARD_BLUR). Default none — a variant opts\n * in (classic-glass), and a performant-blur host disables it via override\n * sheet when it paints a precomputed frost instead.\n * - `--glass-frost`: one full background layer (image + position / size +\n * repeat) composited at the BOTTOM, under the accent gradient — the same\n * frost slot ui's `.glass` formula ends its background list with. Unset →\n * none. A performant-blur host sets it per shell with the viewport-aligned\n * slice of its pre-blurred bitmap. */\n.glasshome-widget {\n background:\n var(--widget-gradient,\n linear-gradient(135deg,\n color-mix(in oklch, var(--widget-color) calc(22% * var(--widget-grad-strength)), transparent),\n color-mix(in oklch, var(--widget-color-to, var(--widget-color)) calc(11% * var(--widget-grad-strength)), transparent)\n )\n ),\n var(--glass-frost, none);\n backdrop-filter: var(--widget-backdrop, none);\n -webkit-backdrop-filter: var(--widget-backdrop, none);\n box-shadow: inset 0 1px 0 var(--widget-border-highlight);\n}\n\n/* === Slot: content layout === */\n/* Default: column. Flip to row only on wide-and-short widgets (matches the\n * old contentLayout heuristic: aspect > 1 AND height < 150px). */\n.glasshome-widget-content {\n position: relative;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n width: 100%;\n height: 100%;\n padding: var(--widget-pad);\n gap: var(--widget-gap);\n overflow: hidden;\n}\n@container widget (min-aspect-ratio: 1) and (max-height: 149px) {\n .glasshome-widget-content {\n flex-direction: row;\n align-items: center;\n justify-content: flex-start;\n }\n}\n\n/* === Compact 1×1 ===\n * Small in BOTH axes → stack the tile: icon on top, label + status centered\n * under it. Icon stays moderate (not a hero) so the text keeps room. Wide\n * tiles (2×1) fail the width test and keep the row layout above. Placed after\n * the row-flip so it wins. */\n@container widget (max-width: 189px) and (max-height: 189px) {\n .glasshome-widget-content {\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n gap: clamp(4px, 2.5cqmin, 8px);\n }\n .glasshome-widget-content > * { align-items: center; text-align: center; }\n .glasshome-widget-icon {\n --widget-icon-box: clamp(36px, 30cqmin, 56px);\n --widget-icon-glyph: clamp(20px, 17cqmin, 32px);\n }\n .glasshome-widget-title { font-size: clamp(11px, 6cqmin, 14px); max-width: 100%; }\n .glasshome-widget-status,\n .glasshome-widget-value { font-size: clamp(13px, 7cqmin, 17px); }\n .glasshome-widget-content [class*=\"ml-auto\"] { margin-left: 0 !important; }\n}\n.glasshome-widget-content [class*=\"ml-auto\"] {\n position: relative;\n z-index: 30;\n}\n\n/* === Slot: icon === */\n.glasshome-widget-icon {\n width: var(--widget-icon-box);\n height: var(--widget-icon-box);\n border-radius: var(--radius-sm, 12px);\n background: var(--widget-icon-color, var(--widget-color));\n box-shadow: 0 0 calc(var(--widget-glow-strength) * 30px)\n color-mix(in oklch, var(--widget-icon-color, var(--widget-color)) 50%, transparent);\n color: oklch(from var(--widget-icon-color, var(--widget-color)) calc(0.52 + sign(0.5 - l) * 0.43) calc(c * 0.2) h);\n}\n.glasshome-widget-icon-glyph {\n font-size: var(--widget-icon-glyph);\n line-height: 1;\n}\n.glasshome-widget-icon-stack {\n position: absolute;\n width: 102%;\n height: 102%;\n border-radius: var(--radius-sm, 12px);\n}\n.glasshome-widget-icon-stack-2 { bottom: -14%; right: -10%; opacity: 0.45; z-index: -2; }\n.glasshome-widget-icon-stack-3 { bottom: -28%; right: -20%; opacity: 0.20; z-index: -3; }\n\n/* === Slot: text === */\n.glasshome-widget-title {\n font-size: var(--widget-title-size);\n line-height: 1.2;\n color: color-mix(in oklch, var(--color-foreground, currentColor) 60%, transparent);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.glasshome-widget-subtitle {\n font-size: var(--widget-subtitle);\n color: color-mix(in oklch, var(--color-foreground, currentColor) 60%, transparent);\n}\n.glasshome-widget-status {\n font-size: var(--widget-status-size);\n font-weight: 700;\n color: var(--color-foreground, currentColor);\n line-height: 1.1;\n}\n.glasshome-widget-value {\n font-size: var(--widget-value-size);\n font-weight: 700;\n color: var(--color-foreground, currentColor);\n line-height: 1.05;\n}\n.glasshome-widget-badge {\n font-size: var(--widget-badge-size);\n font-weight: 500;\n padding: 0.125rem 0.5rem;\n border-radius: 9999px;\n background: color-mix(in oklch, var(--color-foreground, currentColor) 20%, transparent);\n color: var(--color-foreground, currentColor);\n}\n\n.glasshome-widget-loading {\n background: color-mix(in oklch, var(--widget-color) 20%, transparent);\n}\n\n/* === Slider fill ===\n * Value flows in via --widget-fill-value (0-100). Vertical orientation auto-\n * detected via container query. */\n.glasshome-widget-slider-fill {\n --widget-fill-value: 0;\n background: color-mix(in oklch, var(--widget-icon-color, var(--widget-color)) 30%, transparent);\n box-shadow: 0 0 calc(var(--widget-glow-strength) * 40px)\n color-mix(in oklch, var(--widget-icon-color, var(--widget-color)) 40%, transparent);\n clip-path: inset(0 calc((100 - var(--widget-fill-value)) * 1%) 0 0);\n}\n@container widget (max-aspect-ratio: 1) {\n .glasshome-widget-slider-fill {\n clip-path: inset(calc((100 - var(--widget-fill-value)) * 1%) 0 0 0);\n }\n}\n", a = !1, o, s = /* @__PURE__ */ new WeakSet();
12
- function c() {
13
- return o || (o = new CSSStyleSheet(), o.replaceSync(i.replace(/:root\b/g, ":host").replace(/^\.dark\b/gm, ":host(.dark)"))), o;
14
- }
15
- function l(e) {
16
- if (typeof ShadowRoot < "u" && e instanceof ShadowRoot) {
17
- if (s.has(e)) return;
18
- let t = c();
19
- e.adoptedStyleSheets.includes(t) || (e.adoptedStyleSheets = [...e.adoptedStyleSheets, t]), s.add(e);
20
- return;
21
- }
22
- if (a) return;
23
- let t = e ?? (typeof document > "u" ? void 0 : document);
24
- if (!t) return;
25
- if (t.head.querySelector?.("style[data-glasshome-tokens]")) {
26
- a = !0;
27
- return;
28
- }
29
- let n = t.createElement("style");
30
- n.setAttribute("data-glasshome-tokens", ""), n.textContent = i, t.head.appendChild(n), a = !0;
31
- }
32
- //#endregion
33
- export { n, r, l as t };