glimpse-sdk 0.7.0 → 0.8.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
@@ -15,19 +15,19 @@ npm install glimpse-sdk
15
15
  Create and live-run a TypeScript applet project with the Glimpse tooling:
16
16
 
17
17
  ```sh
18
- glimpse-applet new counter --lang typescript
18
+ glimpse-shell applets new counter --lang typescript
19
19
  cd counter
20
- glimpse-applet dev
20
+ glimpse-shell applets dev
21
21
  ```
22
22
 
23
- Read `docs/custom-applets/tooling.md` for project layout, `applet.toml`, dev applets, linking, and diagnostics.
23
+ Read `docs/custom-applets/tooling.md` for project layout, `applet.toml`, dev applets, local linking, distribution, and diagnostics.
24
24
 
25
25
  ## Goals
26
26
 
27
27
  - typed protocol models
28
28
  - typed widget builders
29
29
  - async runtime
30
- - explicit typed handler registration
30
+ - widget-local callbacks, with explicit typed handler registration available when needed
31
31
  - separate `status(state)` and `popover(state)` methods; state mutation via `await this.setState(...)`
32
32
 
33
33
  ## Example
@@ -35,12 +35,11 @@ Read `docs/custom-applets/tooling.md` for project layout, `applet.toml`, dev app
35
35
  ```ts
36
36
  import {
37
37
  Applet,
38
- Box,
39
- Button,
38
+ Column,
40
39
  Hero,
41
- Icon,
42
- StatusItem,
43
40
  Label,
41
+ StatusItem,
42
+ Tile,
44
43
  type TreeNode,
45
44
  } from "glimpse-sdk";
46
45
 
@@ -50,42 +49,43 @@ interface DeployState {
50
49
  }
51
50
 
52
51
  class DeployApplet extends Applet<DeployState> {
53
- protected initialState(): DeployState {
54
- return { version: "2026.04.07", status: "Ready" };
55
- }
56
-
57
52
  constructor() {
58
53
  super();
59
- this.onClick("deploy_now", async () => {
60
- await this.setState({ status: "Deploying" });
61
- });
54
+ }
55
+
56
+ protected initialState(): DeployState {
57
+ return { version: "2026.04.07", status: "Ready" };
62
58
  }
63
59
 
64
60
  protected async status(state: DeployState): Promise<StatusItem[]> {
65
61
  return [
66
62
  new StatusItem({
67
63
  id: "deploy",
68
- icon: Icon.name("software-update-available-symbolic"),
64
+ icon: "software-update-available-symbolic",
69
65
  label: state.status,
70
66
  }),
71
67
  ];
72
68
  }
73
69
 
74
70
  protected async popover(state: DeployState): Promise<TreeNode | null> {
75
- return Box.vertical([
76
- new Hero({
77
- icon: Icon.name("software-update-available-symbolic"),
78
- title: "Deploy",
79
- subtitle: state.version,
80
- }),
81
- new Label("Version"),
82
- new Button({
83
- id: "deploy_now",
84
- label: "Deploy now",
85
- icon: "media-playback-start-symbolic",
86
- variant: "primary",
87
- }),
88
- ]);
71
+ return new Column({
72
+ children: [
73
+ new Hero({
74
+ icon: "software-update-available-symbolic",
75
+ title: "Deploy",
76
+ subtitle: state.version,
77
+ }),
78
+ new Label("Version"),
79
+ new Tile({
80
+ id: "deploy_now",
81
+ primary: "Deploy now",
82
+ left_icon: "media-playback-start-symbolic",
83
+ on_click: async () => {
84
+ await this.setState({ status: "Deploying" });
85
+ },
86
+ }),
87
+ ],
88
+ });
89
89
  }
90
90
  }
91
91
 
@@ -94,7 +94,9 @@ await new DeployApplet().run();
94
94
 
95
95
  ## Handler Registration
96
96
 
97
- Use explicit registration helpers instead of decorators:
97
+ Prefer widget-local callbacks for controls rendered in `popover(state)`.
98
+ Explicit registration helpers are still available when the handler should live
99
+ outside the widget tree:
98
100
 
99
101
  - `this.onClick(id, handler)`
100
102
  - `this.onScroll(id, handler)`
@@ -4,12 +4,20 @@ export declare class StatusItem {
4
4
  icon?: string;
5
5
  label?: string;
6
6
  tooltip?: string;
7
+ cssClasses?: string[];
7
8
  };
8
9
  constructor(options?: {
9
10
  id?: string;
10
11
  icon?: string;
11
12
  label?: string;
12
13
  tooltip?: string;
14
+ cssClasses?: string[];
13
15
  });
16
+ /**
17
+ * Return a copy with `cssClass` appended to `cssClasses`. Immutable-update
18
+ * form that mirrors the Rust SDK's `css_class()` builder so SDK-hopping
19
+ * users see equivalent ergonomics across languages.
20
+ */
21
+ withCssClass(cssClass: string): StatusItem;
14
22
  toProtocol(): Record<string, unknown>;
15
23
  }
@@ -3,6 +3,17 @@ export class StatusItem {
3
3
  constructor(options = {}) {
4
4
  this.options = options;
5
5
  }
6
+ /**
7
+ * Return a copy with `cssClass` appended to `cssClasses`. Immutable-update
8
+ * form that mirrors the Rust SDK's `css_class()` builder so SDK-hopping
9
+ * users see equivalent ergonomics across languages.
10
+ */
11
+ withCssClass(cssClass) {
12
+ return new StatusItem({
13
+ ...this.options,
14
+ cssClasses: [...(this.options.cssClasses ?? []), cssClass],
15
+ });
16
+ }
6
17
  toProtocol() {
7
18
  const payload = {};
8
19
  if (this.options.id !== undefined) {
@@ -17,6 +28,9 @@ export class StatusItem {
17
28
  if (this.options.tooltip !== undefined) {
18
29
  payload.tooltip = this.options.tooltip;
19
30
  }
31
+ if (this.options.cssClasses && this.options.cssClasses.length > 0) {
32
+ payload.css_classes = [...this.options.cssClasses];
33
+ }
20
34
  return payload;
21
35
  }
22
36
  }
@@ -1,6 +1,8 @@
1
1
  export type InlineHandler = (event: unknown) => void | Promise<void>;
2
2
  export declare class InlineHandlerRegistry {
3
3
  readonly handlers: Map<string, InlineHandler>;
4
+ generatedId(event: "click" | "toggle" | "change", path: number[]): string;
5
+ targetId(event: "click" | "toggle" | "change", id: string | undefined, path: number[]): string;
4
6
  add(event: "click" | "toggle" | "change", id: string, handler: InlineHandler): void;
5
7
  }
6
8
  export type BadgeKind = "default" | "success" | "warning" | "error" | "accent";
@@ -195,7 +197,6 @@ export declare class Tile extends WidgetBase {
195
197
  left_icon?: string;
196
198
  left?: TreeNode;
197
199
  right?: TreeNode;
198
- activatable?: boolean;
199
200
  on_click?: InlineHandler;
200
201
  };
201
202
  readonly type: string;
@@ -206,7 +207,6 @@ export declare class Tile extends WidgetBase {
206
207
  left_icon?: string;
207
208
  left?: TreeNode;
208
209
  right?: TreeNode;
209
- activatable?: boolean;
210
210
  on_click?: InlineHandler;
211
211
  });
212
212
  data(): Record<string, unknown>;
@@ -1,5 +1,12 @@
1
1
  export class InlineHandlerRegistry {
2
2
  handlers = new Map();
3
+ generatedId(event, path) {
4
+ const suffix = path.length === 0 ? "root" : path.join(".");
5
+ return `__glimpse:${event}:${suffix}`;
6
+ }
7
+ targetId(event, id, path) {
8
+ return id ?? this.generatedId(event, path);
9
+ }
3
10
  add(event, id, handler) {
4
11
  this.handlers.set(`${event}:${id}`, handler);
5
12
  }
@@ -73,8 +80,10 @@ export class Hero extends WidgetBase {
73
80
  return out;
74
81
  }
75
82
  bindHandlers(registry, path) {
76
- if (this.options.id && this.options.on_toggle)
83
+ if (this.options.on_toggle) {
84
+ this.options.id = registry.targetId("toggle", this.options.id, path);
77
85
  registry.add("toggle", this.options.id, this.options.on_toggle);
86
+ }
78
87
  this.options.trailing?.bindHandlers(registry, [...path, 0]);
79
88
  }
80
89
  }
@@ -250,7 +259,7 @@ export class Tile extends WidgetBase {
250
259
  this.options = options;
251
260
  }
252
261
  data() {
253
- const out = { ...this.commonData(), primary: this.options.primary, activatable: this.options.activatable ?? false };
262
+ const out = { ...this.commonData(), primary: this.options.primary };
254
263
  for (const key of ["id", "secondary", "left_icon"])
255
264
  if (this.options[key] !== undefined)
256
265
  out[key] = this.options[key];
@@ -263,8 +272,10 @@ export class Tile extends WidgetBase {
263
272
  return out;
264
273
  }
265
274
  bindHandlers(registry, path) {
266
- if (this.options.id && this.options.on_click)
275
+ if (this.options.on_click) {
276
+ this.options.id = registry.targetId("click", this.options.id, path);
267
277
  registry.add("click", this.options.id, this.options.on_click);
278
+ }
268
279
  this.options.left?.bindHandlers(registry, [...path, 0]);
269
280
  this.options.right?.bindHandlers(registry, [...path, 1]);
270
281
  }
@@ -286,8 +297,10 @@ export class SegmentedTile extends Tile {
286
297
  }
287
298
  bindHandlers(registry, path) {
288
299
  super.bindHandlers(registry, path);
289
- if (this.options.id && this.options.on_toggle)
300
+ if (this.options.on_toggle) {
301
+ this.options.id = registry.targetId("toggle", this.options.id, path);
290
302
  registry.add("toggle", this.options.id, this.options.on_toggle);
303
+ }
291
304
  this.options.child?.bindHandlers(registry, [...path, 2]);
292
305
  }
293
306
  }
@@ -335,8 +348,10 @@ export class ExpanderTile extends WidgetBase {
335
348
  return out;
336
349
  }
337
350
  bindHandlers(registry, path) {
338
- if (this.options.id && this.options.on_toggle)
351
+ if (this.options.on_toggle) {
352
+ this.options.id = registry.targetId("toggle", this.options.id, path);
339
353
  registry.add("toggle", this.options.id, this.options.on_toggle);
354
+ }
340
355
  this.options.left?.bindHandlers(registry, [...path, 0]);
341
356
  this.options.child?.bindHandlers(registry, [...path, 1]);
342
357
  }
@@ -382,8 +397,10 @@ export class ChoiceTile extends WidgetBase {
382
397
  return out;
383
398
  }
384
399
  bindHandlers(registry, path) {
385
- if (this.options.id && this.options.on_click)
400
+ if (this.options.on_click) {
401
+ this.options.id = registry.targetId("click", this.options.id, path);
386
402
  registry.add("click", this.options.id, this.options.on_click);
403
+ }
387
404
  this.options.left?.bindHandlers(registry, [...path, 0]);
388
405
  }
389
406
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glimpse-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Typed async framework for building Glimpse exec applets",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
package/src/protocol.ts CHANGED
@@ -5,9 +5,22 @@ export class StatusItem {
5
5
  icon?: string;
6
6
  label?: string;
7
7
  tooltip?: string;
8
+ cssClasses?: string[];
8
9
  } = {},
9
10
  ) {}
10
11
 
12
+ /**
13
+ * Return a copy with `cssClass` appended to `cssClasses`. Immutable-update
14
+ * form that mirrors the Rust SDK's `css_class()` builder so SDK-hopping
15
+ * users see equivalent ergonomics across languages.
16
+ */
17
+ withCssClass(cssClass: string): StatusItem {
18
+ return new StatusItem({
19
+ ...this.options,
20
+ cssClasses: [...(this.options.cssClasses ?? []), cssClass],
21
+ });
22
+ }
23
+
11
24
  toProtocol(): Record<string, unknown> {
12
25
  const payload: Record<string, unknown> = {};
13
26
  if (this.options.id !== undefined) {
@@ -22,6 +35,9 @@ export class StatusItem {
22
35
  if (this.options.tooltip !== undefined) {
23
36
  payload.tooltip = this.options.tooltip;
24
37
  }
38
+ if (this.options.cssClasses && this.options.cssClasses.length > 0) {
39
+ payload.css_classes = [...this.options.cssClasses];
40
+ }
25
41
  return payload;
26
42
  }
27
43
  }
package/src/widgets.ts CHANGED
@@ -2,6 +2,15 @@ export type InlineHandler = (event: unknown) => void | Promise<void>;
2
2
  export class InlineHandlerRegistry {
3
3
  readonly handlers = new Map<string, InlineHandler>();
4
4
 
5
+ generatedId(event: "click" | "toggle" | "change", path: number[]): string {
6
+ const suffix = path.length === 0 ? "root" : path.join(".");
7
+ return `__glimpse:${event}:${suffix}`;
8
+ }
9
+
10
+ targetId(event: "click" | "toggle" | "change", id: string | undefined, path: number[]): string {
11
+ return id ?? this.generatedId(event, path);
12
+ }
13
+
5
14
  add(event: "click" | "toggle" | "change", id: string, handler: InlineHandler): void {
6
15
  this.handlers.set(`${event}:${id}`, handler);
7
16
  }
@@ -82,7 +91,10 @@ export class Hero extends WidgetBase {
82
91
  return out;
83
92
  }
84
93
  bindHandlers(registry: InlineHandlerRegistry, path: number[]): void {
85
- if (this.options.id && this.options.on_toggle) registry.add("toggle", this.options.id, this.options.on_toggle);
94
+ if (this.options.on_toggle) {
95
+ this.options.id = registry.targetId("toggle", this.options.id, path);
96
+ registry.add("toggle", this.options.id, this.options.on_toggle);
97
+ }
86
98
  this.options.trailing?.bindHandlers(registry, [...path, 0]);
87
99
  }
88
100
  }
@@ -212,9 +224,9 @@ export class PopoverShell extends ChildrenWidget {
212
224
 
213
225
  export class Tile extends WidgetBase {
214
226
  readonly type: string = "tile";
215
- constructor(protected readonly options: CommonOptions & { primary: string; id?: string; secondary?: string; left_icon?: string; left?: TreeNode; right?: TreeNode; activatable?: boolean; on_click?: InlineHandler }) { super(options); }
227
+ constructor(protected readonly options: CommonOptions & { primary: string; id?: string; secondary?: string; left_icon?: string; left?: TreeNode; right?: TreeNode; on_click?: InlineHandler }) { super(options); }
216
228
  data(): Record<string, unknown> {
217
- const out: Record<string, unknown> = { ...this.commonData(), primary: this.options.primary, activatable: this.options.activatable ?? false };
229
+ const out: Record<string, unknown> = { ...this.commonData(), primary: this.options.primary };
218
230
  for (const key of ["id", "secondary", "left_icon"] as const) if (this.options[key] !== undefined) out[key] = this.options[key];
219
231
  const left = child(this.options.left);
220
232
  const right = child(this.options.right);
@@ -223,7 +235,10 @@ export class Tile extends WidgetBase {
223
235
  return out;
224
236
  }
225
237
  bindHandlers(registry: InlineHandlerRegistry, path: number[]): void {
226
- if (this.options.id && this.options.on_click) registry.add("click", this.options.id, this.options.on_click);
238
+ if (this.options.on_click) {
239
+ this.options.id = registry.targetId("click", this.options.id, path);
240
+ registry.add("click", this.options.id, this.options.on_click);
241
+ }
227
242
  this.options.left?.bindHandlers(registry, [...path, 0]);
228
243
  this.options.right?.bindHandlers(registry, [...path, 1]);
229
244
  }
@@ -241,7 +256,10 @@ export class SegmentedTile extends Tile {
241
256
  }
242
257
  bindHandlers(registry: InlineHandlerRegistry, path: number[]): void {
243
258
  super.bindHandlers(registry, path);
244
- if (this.options.id && this.options.on_toggle) registry.add("toggle", this.options.id, this.options.on_toggle);
259
+ if (this.options.on_toggle) {
260
+ this.options.id = registry.targetId("toggle", this.options.id, path);
261
+ registry.add("toggle", this.options.id, this.options.on_toggle);
262
+ }
245
263
  this.options.child?.bindHandlers(registry, [...path, 2]);
246
264
  }
247
265
  }
@@ -275,7 +293,10 @@ export class ExpanderTile extends WidgetBase {
275
293
  return out;
276
294
  }
277
295
  bindHandlers(registry: InlineHandlerRegistry, path: number[]): void {
278
- if (this.options.id && this.options.on_toggle) registry.add("toggle", this.options.id, this.options.on_toggle);
296
+ if (this.options.on_toggle) {
297
+ this.options.id = registry.targetId("toggle", this.options.id, path);
298
+ registry.add("toggle", this.options.id, this.options.on_toggle);
299
+ }
279
300
  this.options.left?.bindHandlers(registry, [...path, 0]);
280
301
  this.options.child?.bindHandlers(registry, [...path, 1]);
281
302
  }
@@ -308,7 +329,10 @@ export class ChoiceTile extends WidgetBase {
308
329
  return out;
309
330
  }
310
331
  bindHandlers(registry: InlineHandlerRegistry, path: number[]): void {
311
- if (this.options.id && this.options.on_click) registry.add("click", this.options.id, this.options.on_click);
332
+ if (this.options.on_click) {
333
+ this.options.id = registry.targetId("click", this.options.id, path);
334
+ registry.add("click", this.options.id, this.options.on_click);
335
+ }
312
336
  this.options.left?.bindHandlers(registry, [...path, 0]);
313
337
  }
314
338
  }