@glasshome/widget-sdk 1.18.1 → 1.20.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.
|
@@ -26,6 +26,8 @@ export interface GestureHandlers {
|
|
|
26
26
|
dispose: () => void;
|
|
27
27
|
/** Where a hold is filling from, relative to the element; `fired` once it opened. */
|
|
28
28
|
hold?: () => HoldFlood | null;
|
|
29
|
+
/** True while a hold does something. */
|
|
30
|
+
holds?: () => boolean;
|
|
29
31
|
/** Keyboard door: Enter or Space taps, the context-menu key or Shift+F10 holds. */
|
|
30
32
|
onKeyDown?: (e: KeyboardEvent) => void;
|
|
31
33
|
}
|
|
@@ -50,6 +50,10 @@ export interface ReactiveWidgetContext {
|
|
|
50
50
|
developer?: () => boolean;
|
|
51
51
|
/** The tile's box on the page, so a held tile's sheet opens beside it. Absent: it opens centred. */
|
|
52
52
|
anchor?: () => HTMLElement | undefined;
|
|
53
|
+
/** Host hook: whether holding this widget opens anything. Absent on hosts that predate this field. */
|
|
54
|
+
onHoldable?: (holds: boolean) => void;
|
|
55
|
+
/** Host hook: a hold on this widget fired. Absent on hosts that predate this field. */
|
|
56
|
+
onHeld?: () => void;
|
|
53
57
|
}
|
|
54
58
|
export declare const WidgetCtx: import("solid-js").Context<ReactiveWidgetContext | undefined>;
|
|
55
59
|
export declare function useWidgetContext(): ReactiveWidgetContext;
|
package/dist/index.js
CHANGED
|
@@ -331,20 +331,20 @@ function xn(e, t) {
|
|
|
331
331
|
let t = i();
|
|
332
332
|
if (t.tap || t.hold || t.slide) {
|
|
333
333
|
if (a.isDown = !0, a.isTouch = e.pointerType === "touch", a.element = e.currentTarget, p(a.element), a.startX = e.clientX, a.startY = e.clientY, a.startTime = Date.now(), a.hasMoved = !1, a.sliding = !1, t.hold) {
|
|
334
|
-
let
|
|
334
|
+
let r = t.hold.delay ?? 550, i = a.element.getBoundingClientRect(), o = e.clientX - i.left, c = e.clientY - i.top, l = Math.hypot(Math.max(o, i.width - o), Math.max(c, i.height - c));
|
|
335
335
|
a.graceTimer = setTimeout(() => {
|
|
336
336
|
a.graceTimer = null, s({
|
|
337
|
-
x:
|
|
338
|
-
y:
|
|
339
|
-
r:
|
|
337
|
+
x: o,
|
|
338
|
+
y: c,
|
|
339
|
+
r: l,
|
|
340
340
|
fired: !1
|
|
341
341
|
});
|
|
342
|
-
}, Math.min(150,
|
|
342
|
+
}, Math.min(150, r)), a.holdTimer = setTimeout(() => {
|
|
343
343
|
a.holdTimer = null, a.isDown && !a.hasMoved && (s((e) => e && {
|
|
344
344
|
...e,
|
|
345
345
|
fired: !0
|
|
346
|
-
}), bn.bump(), a.element && h(a.element), t.hold?.action(), a.isDown = !1);
|
|
347
|
-
},
|
|
346
|
+
}), bn.bump(), a.element && h(a.element), t.hold?.action(), n?.onHeld?.(), a.isDown = !1);
|
|
347
|
+
}, r);
|
|
348
348
|
}
|
|
349
349
|
if (!a.isTouch && t.slide) try {
|
|
350
350
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
@@ -388,7 +388,7 @@ function xn(e, t) {
|
|
|
388
388
|
onKeyDown: (e) => {
|
|
389
389
|
if (e.target !== e.currentTarget) return;
|
|
390
390
|
let t = i();
|
|
391
|
-
(e.key === "Enter" || e.key === " ") && t.tap ? (e.preventDefault(), t.tap()) : (e.key === "ContextMenu" || e.shiftKey && e.key === "F10") && t.hold && (e.preventDefault(), t.hold.action());
|
|
391
|
+
(e.key === "Enter" || e.key === " ") && t.tap ? (e.preventDefault(), t.tap()) : (e.key === "ContextMenu" || e.shiftKey && e.key === "F10") && t.hold && (e.preventDefault(), t.hold.action(), n?.onHeld?.());
|
|
392
392
|
},
|
|
393
393
|
onPointerDown: v,
|
|
394
394
|
onPointerMove: y,
|
|
@@ -410,6 +410,7 @@ function xn(e, t) {
|
|
|
410
410
|
return e.tap || e.hold ? "manipulation" : "auto";
|
|
411
411
|
},
|
|
412
412
|
hold: o,
|
|
413
|
+
holds: () => !!i().hold,
|
|
413
414
|
dispose: () => {
|
|
414
415
|
m(), u &&= (u.disconnect(), null), l = null, c = null;
|
|
415
416
|
}
|
|
@@ -1120,7 +1121,7 @@ function Vr(t) {
|
|
|
1120
1121
|
let i = rn(n), a = xn(() => ({ hold: i?.hasSheet() ? { action: () => (q.get(n ?? {}) ?? q.get(d))?.() } : void 0 }));
|
|
1121
1122
|
k(a.dispose);
|
|
1122
1123
|
let o = () => t.emptyState && n ? r : t.gestures ?? (i?.hasSheet() ? a : void 0);
|
|
1123
|
-
E(() => i?.setTone(t.color ?? (t.tone ? `var(--tone-${t.tone})` : void 0)));
|
|
1124
|
+
n && !(tn in n) && (E(() => n?.onHoldable?.(!t.emptyState && !!o()?.holds?.())), k(() => n?.onHoldable?.(!1))), E(() => i?.setTone(t.color ?? (t.tone ? `var(--tone-${t.tone})` : void 0)));
|
|
1124
1125
|
let [s, c] = O(), [l, u] = O({
|
|
1125
1126
|
width: 0,
|
|
1126
1127
|
height: 0
|
|
@@ -1286,35 +1287,35 @@ Z.Content = fr, Z.Icon = kn, Z.Title = _r, Z.Status = mr, Z.Value = Nr, Z.Slider
|
|
|
1286
1287
|
//#region src/framework/panel/panel.tsx
|
|
1287
1288
|
var Ur = /*#__PURE__*/ B("<section class=glasshome-panel-section><h3 class=glasshome-panel-label>"), Wr = /*#__PURE__*/ B("<div class=glasshome-panel-rows>"), Gr = /*#__PURE__*/ B("<span class=\"glasshome-widget-slider-fill glasshome-panel-row-fill\">"), Kr = /*#__PURE__*/ B("<span class=\"glasshome-widget-icon glass glasshome-panel-row-icon\">"), qr = /*#__PURE__*/ B("<span class=glasshome-panel-row-state>"), Jr = /*#__PURE__*/ B("<span class=glasshome-panel-row-text><span class=glasshome-panel-row-name>"), Yr = /*#__PURE__*/ B("<span class=glasshome-panel-row-trailing>"), Xr = /*#__PURE__*/ B("<div class=glasshome-panel-row-wrap>"), Zr = /*#__PURE__*/ B("<div>"), Qr = /*#__PURE__*/ B("<div class=\"glasshome-panel-facts glass\">"), $r = /*#__PURE__*/ B("<div class=glasshome-panel-fact><span class=glasshome-panel-row-text><span class=glasshome-panel-fact-label></span><span class=glasshome-panel-fact-value>"), ei = 450, ti = 8, ni = (e, t, n) => Math.min(n, Math.max(t, e)), ri = "button, a, input, select, textarea, [role=\"slider\"], [role=\"radio\"], [role=\"switch\"]";
|
|
1288
1289
|
function ii(e) {
|
|
1289
|
-
let t, n = !1,
|
|
1290
|
+
let t = A(W), n, r = !1, i = !1, a, o = 0, s = (t, n) => {
|
|
1290
1291
|
let r = e.slide?.();
|
|
1291
1292
|
if (!r) return 0;
|
|
1292
1293
|
let i = t.getBoundingClientRect(), a = e.axis === "x" ? (n.clientX - i.left) / i.width : 1 - (n.clientY - i.top) / i.height, o = r.min ?? 0, s = r.max ?? 100;
|
|
1293
1294
|
return Math.round(ni(o + a * (s - o), o, s));
|
|
1294
|
-
},
|
|
1295
|
-
clearTimeout(
|
|
1295
|
+
}, c = () => {
|
|
1296
|
+
clearTimeout(a), n = void 0;
|
|
1296
1297
|
};
|
|
1297
|
-
return k(() => clearTimeout(
|
|
1298
|
-
onPointerDown: (
|
|
1299
|
-
if (
|
|
1300
|
-
let
|
|
1301
|
-
|
|
1302
|
-
x:
|
|
1303
|
-
y:
|
|
1304
|
-
id:
|
|
1305
|
-
},
|
|
1306
|
-
|
|
1298
|
+
return k(() => clearTimeout(a)), {
|
|
1299
|
+
onPointerDown: (o) => {
|
|
1300
|
+
if (o.button !== 0 || n) return;
|
|
1301
|
+
let s = o.target instanceof Element ? o.target.closest(ri) : null;
|
|
1302
|
+
s && s !== o.currentTarget || (n = {
|
|
1303
|
+
x: o.clientX,
|
|
1304
|
+
y: o.clientY,
|
|
1305
|
+
id: o.pointerId
|
|
1306
|
+
}, o.currentTarget.setPointerCapture?.(o.pointerId), r = !1, i = !1, e.onHold && (a = setTimeout(() => {
|
|
1307
|
+
i = !0, e.onHold?.(), t?.onHeld?.();
|
|
1307
1308
|
}, ei)));
|
|
1308
1309
|
},
|
|
1309
|
-
onPointerMove: (
|
|
1310
|
-
if (!
|
|
1311
|
-
let c =
|
|
1312
|
-
!
|
|
1310
|
+
onPointerMove: (t) => {
|
|
1311
|
+
if (!n || t.pointerId !== n.id || i) return;
|
|
1312
|
+
let c = t.currentTarget, l = e.axis === "x" ? Math.abs(t.clientX - n.x) : Math.abs(t.clientY - n.y);
|
|
1313
|
+
!r && e.slide?.() && l > ti && (r = !0, clearTimeout(a)), r ? (o = s(c, t), e.slide?.()?.onChange(o)) : Math.hypot(t.clientX - n.x, t.clientY - n.y) > ti && clearTimeout(a);
|
|
1313
1314
|
},
|
|
1314
|
-
onPointerUp: (
|
|
1315
|
-
|
|
1315
|
+
onPointerUp: (t) => {
|
|
1316
|
+
n && t.pointerId === n.id && (r ? e.slide?.()?.onCommit?.(o) : i || e.onTap?.(), c());
|
|
1316
1317
|
},
|
|
1317
|
-
onPointerCancel:
|
|
1318
|
+
onPointerCancel: c
|
|
1318
1319
|
};
|
|
1319
1320
|
}
|
|
1320
1321
|
function ai(e) {
|
|
@@ -2778,7 +2779,7 @@ function Qa() {
|
|
|
2778
2779
|
//#region src/version.ts
|
|
2779
2780
|
var $a = {
|
|
2780
2781
|
name: "@glasshome/widget-sdk",
|
|
2781
|
-
version: "1.
|
|
2782
|
+
version: "1.20.0",
|
|
2782
2783
|
description: "SDK for building GlassHome dashboard widgets with SolidJS",
|
|
2783
2784
|
keywords: [
|
|
2784
2785
|
"dashboard",
|
|
@@ -2801,7 +2802,8 @@ var $a = {
|
|
|
2801
2802
|
files: [
|
|
2802
2803
|
"dist",
|
|
2803
2804
|
"preview",
|
|
2804
|
-
"tailwind-sources.css"
|
|
2805
|
+
"tailwind-sources.css",
|
|
2806
|
+
"guide"
|
|
2805
2807
|
],
|
|
2806
2808
|
type: "module",
|
|
2807
2809
|
sideEffects: !1,
|
package/guide/widgets.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# GlassHome widgets
|
|
2
|
+
|
|
3
|
+
Matches the installed `@glasshome/widget-sdk`. Wins over memory and older docs.
|
|
4
|
+
|
|
5
|
+
## Loop
|
|
6
|
+
|
|
7
|
+
1. `bun widget add --name <name>`: `src/<name>/` with a working sensor tile (`index.tsx`, `manifest.json`). Start from it.
|
|
8
|
+
2. Write it. `examples` in `defineWidget` (label, size, config) are what preview renders. Demo entity ids: `node_modules/@glasshome/sync-layer/dist/demo/ids.d.ts`; a wrong id renders the empty state. Missing a kind of entity: use a stand-in and config to show each state.
|
|
9
|
+
3. `bun widget build`: typecheck, bundle, validate. Fix every error before previewing.
|
|
10
|
+
4. `bun widget preview <name> --sizes grid`: every example at common tile sizes, light and dark, into `preview/sweep/` with a contact sheet each. Open the images and look. First run: `bun add -d playwright && bunx playwright install chromium`. A crash fails the render with its error.
|
|
11
|
+
- Narrow: `--sizes 150x156,340x242`, `--theme dark`, `--example 0`.
|
|
12
|
+
- Change state: `--config '<json>'`, `--service '<domain.service>|<entity_id>|<json>'`, `--at 2026-06-15T21:00:00`, `--click '<selector>'`.
|
|
13
|
+
- Inspect: `--eval '<expr>'` prints per render (`root` = shadow root).
|
|
14
|
+
5. Repeat 3-4 until 1×1, 2×2 and a big tile read right in both themes.
|
|
15
|
+
6. Live: `bun widget connect <dashboard-url>`.
|
|
16
|
+
7. Ship: `bun widget publish --name <name> --bump patch|minor|major`. Version lives in `manifest.json`.
|
|
17
|
+
|
|
18
|
+
## Imports
|
|
19
|
+
|
|
20
|
+
- Everything from `@glasshome/widget-sdk`, UI primitives included.
|
|
21
|
+
- Never `@glasshome/ui` (unchecked version, dead tile on mismatch) or `@glasshome/sync-layer` (second store). Keep `@glasshome/ui` installed; the build reads its CSS.
|
|
22
|
+
- Home Assistant only via SDK hooks (`useEntity`, `useEntities`, `useService`, `useToggle`, …). Declare every read/controlled domain in manifest capabilities.
|
|
23
|
+
- SDK too old for something in the docs: `bun widget upgrade`.
|
|
24
|
+
- Edit the inline `manifest` in `defineWidget`; build copies it into `manifest.json` (name, icon, sizes, capabilities, examples, schema). `version` and `sdkVersion` live only in `manifest.json`.
|
|
25
|
+
- Config: `defineConfig` + `field.*`. `field.entity(domain)` stores a one-item array: read `config.entityIds[0]`.
|
|
26
|
+
|
|
27
|
+
## Tile
|
|
28
|
+
|
|
29
|
+
Parts inside `Widget.Content`; they lay out and resize themselves. Never measure to lay out.
|
|
30
|
+
|
|
31
|
+
| Need | Part |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Colour | `tone` (`accent info success warning danger neutral`) or `color` on `Widget`; `accent` on `Widget.Content` for state colour |
|
|
34
|
+
| Icon, name, eyebrow, chips | `Widget.Head` (`active`, `count`, `aside`) |
|
|
35
|
+
| Big value, unit, art | `Widget.Hero` |
|
|
36
|
+
| Control row | `Widget.Controls` with `Widget.Stepper`, `Widget.Choice`, `Widget.Action` |
|
|
37
|
+
| Stateless action | `Widget.Action`; `useConfirm()` + `<Widget confirmed>` when the whole tile runs it |
|
|
38
|
+
| Small fact | `Widget.Chip` |
|
|
39
|
+
| Photo behind tile | `Widget.Backdrop` |
|
|
40
|
+
| Own full-bleed layer | `Widget.Layer` |
|
|
41
|
+
| Faint corner icon | `Widget.Glyph` |
|
|
42
|
+
| Controllable continuous value | `Widget.SliderFill` |
|
|
43
|
+
|
|
44
|
+
## Sizes
|
|
45
|
+
|
|
46
|
+
1×1 to 8×8. Under 130px tall or 140px wide the tile is compact: `eyebrow` and `sub` hide, so carry state in `tone` and the icon. Tokens, never px: `--widget-text-value`, `--widget-text-name`, `--widget-text-meta`, `--widget-text-sub`, `--widget-text-caption`, `--widget-control-h`, `--widget-grid-pad`, `--widget-grid-gap`, `--widget-icon-box`, `--widget-icon-glyph`. Layout by container query (`@[150px]:…`, `@container widget (…)`). What renders by `useWidgetDimensions()`, inside `<Widget>` only.
|
|
47
|
+
|
|
48
|
+
## Sheet
|
|
49
|
+
|
|
50
|
+
Render `<WidgetDialog>` next to `<Widget>`, spreading the dialog parts (`ResponsiveDialog*`, `Button`, `SchemaForm`, `Tabs*`, see the scaffold) and `useWidgetDialog().dialogProps`. Hold opens the sheet on its own once `sheet` is passed; no gesture wiring. Only what the tile lacks (group members, colours, modes, forecast). Pass `sheet` only when there is something: `{...(extras() ? { sheet: () => <MySheet /> } : {})}`. Build from `PanelSection`, `PanelRows`, `PanelRow`, `PanelEntityRow`, `PanelFacts`, `ToggleGroup`, `SwatchPicker`, `TemperatureBar`, `ColorDisc`. Never repeat the tile's value, name or art. Widget CSS does not reach it.
|
|
51
|
+
|
|
52
|
+
## Primitives
|
|
53
|
+
|
|
54
|
+
`Badge`, `Button`, `ButtonGroup`, `CountPill`, `Icon`, `Input`, `Label`, `Progress`, `SchemaForm`, `SectionIcon`, `SectionTitle`, `Select`, `Slider`, `Switch`, `Tabs`, `Toggle`, `ToggleGroup`, `Carousel`, `ColorWheel`, `ColorSlider`, `ResponsiveDialog` + parts. `Icon`, never `iconify-icon`. Never restyle with own classes.
|
|
55
|
+
|
|
56
|
+
## Rules
|
|
57
|
+
|
|
58
|
+
- No hand-rolling what the SDK has: sliders, steppers, dialogs, sheets, hold gestures, loading/error states.
|
|
59
|
+
- Theme vars only (`--foreground`, `--muted-foreground`, `--card`, `--primary`, `--border`, `--success`, `--warning`, `--destructive`, `--radius`). Never declare them on `:host`; no own colour/radius tokens; no `--color-*`.
|
|
60
|
+
- Own CSS = layout and bespoke art. No custom panel/row/chip/button chrome.
|
|
61
|
+
- Tailwind classes literal in source, never concatenated. `dark:` works; `isDark()` for the boolean.
|
|
62
|
+
- No dot status lamps. No thin vertical bars (read as a text caret). No all-caps. Separator ` · `.
|
|
63
|
+
- One icon per item, not per line. Icons, fills and pictures before text.
|
|
64
|
+
- Lead with the answer (value + unit, or a verdict); one small line explains it. Never show a value twice. States per device class (Open/Closed, Detected/Clear).
|
|
65
|
+
- Continuous values keep the full-tile slider; buttons beside it. A stepper never stands alone.
|
|
66
|
+
- Dialog content scrolls in `ResponsiveDialogBody`; no own `overflow`/`max-height` in a dialog.
|
|
67
|
+
- Groups: one tap brings all members to one state; locks never unlock in one tap; doors, gates, garage doors never join bulk actions.
|
|
68
|
+
- Readings update text in place; never rebuild DOM. `Index` for lists of readings. Over 20 s of changing demo data a widget adds and removes no DOM nodes.
|
|
69
|
+
- Everything sits on an edge; text left, values/actions right. One accent per view; defaults quiet. No card in a card.
|
|
70
|
+
- Motion: colours morph, never snap; time transitions with `--duration-state` / `--ease-morph` (zeroed under reduced motion). Decorative loops scale by `var(--motion-ambient, 0)`. Gate on `useReducedMotion()`; pause offscreen with `useIntersectionPause()`.
|
|
71
|
+
|
|
72
|
+
## More
|
|
73
|
+
|
|
74
|
+
Docs as markdown: https://glasshome.app/llms.txt, `https://glasshome.app/md/widgets/<page>` (`widget-sheets`, `widget-styling`, `widget-sdk`, `widget-api-reference`, `widget-capabilities`, `widget-previews`). Official widgets as worked examples: https://github.com/glasshome/widgets (`src/button`, `src/sensor`); where they differ from this guide, the guide wins.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/widget-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "SDK for building GlassHome dashboard widgets with SolidJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dashboard",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
27
|
"preview",
|
|
28
|
-
"tailwind-sources.css"
|
|
28
|
+
"tailwind-sources.css",
|
|
29
|
+
"guide"
|
|
29
30
|
],
|
|
30
31
|
"type": "module",
|
|
31
32
|
"sideEffects": false,
|