@jxsuite/ui 0.5.1 → 0.7.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.
@@ -45,9 +45,11 @@ export interface ToastState {
45
45
  timeout: number;
46
46
  /** `info`, `positive`, `negative` or `warning`. */
47
47
  variant: string;
48
- [key: string]: unknown;
49
48
  }
50
49
 
50
+ /** A `jx-toast` element, with the `open` accessor its document installs. */
51
+ type ToastElement = HTMLElement & { open?: boolean };
52
+
51
53
  /** One toast's clock: what is left of it, when this run started, and what is holding it. */
52
54
  interface Clock {
53
55
  timer: ReturnType<typeof setTimeout> | null;
@@ -60,25 +62,25 @@ interface Clock {
60
62
  }
61
63
 
62
64
  /** Which hold an event asks for. Three channels, because they overlap and must not cancel. */
63
- const HOLD_REASON: Readonly<Record<string, string>> = {
64
- focusin: "focus",
65
- focusout: "focus",
66
- [TOAST_PAUSE]: "stack",
67
- [TOAST_RESUME]: "stack",
68
- pointerenter: "pointer",
69
- pointerleave: "pointer",
70
- pointerout: "pointer",
71
- pointerover: "pointer",
72
- };
65
+ const HOLD_REASON: ReadonlyMap<string, "focus" | "pointer" | "stack"> = new Map([
66
+ ["focusin", "focus"],
67
+ ["focusout", "focus"],
68
+ [TOAST_PAUSE, "stack"],
69
+ [TOAST_RESUME, "stack"],
70
+ ["pointerenter", "pointer"],
71
+ ["pointerleave", "pointer"],
72
+ ["pointerout", "pointer"],
73
+ ["pointerover", "pointer"],
74
+ ]);
73
75
 
74
76
  /** Each scope's clock. */
75
- const clocks = new WeakMap<object, Clock>();
77
+ const clocks = new WeakMap<ToastState, Clock>();
76
78
 
77
79
  /** The element each scope belongs to, learned at mount. */
78
- const hosts = new WeakMap<object, HTMLElement>();
80
+ const hosts = new WeakMap<ToastState, HTMLElement>();
79
81
 
80
82
  /** This scope's clock, minted on first sight. */
81
- function clockOf(state: object): Clock {
83
+ function clockOf(state: ToastState): Clock {
82
84
  const found = clocks.get(state);
83
85
  if (found) {
84
86
  return found;
@@ -106,7 +108,7 @@ function ownerOf(event: Event): HTMLElement | null {
106
108
  * @returns True when the destination is still inside the element the listener is bound to.
107
109
  */
108
110
  export function staysInside(event: Event): boolean {
109
- const to = (event as { relatedTarget?: unknown }).relatedTarget;
111
+ const to = "relatedTarget" in event ? event.relatedTarget : null;
110
112
  const from = event.currentTarget;
111
113
  return to instanceof Node && from instanceof Node && from.contains(to);
112
114
  }
@@ -147,12 +149,11 @@ function schedule(state: ToastState): void {
147
149
  * @param host The `jx-toast` element.
148
150
  * @param reason Why it closed.
149
151
  */
150
- export function closeToast(host: HTMLElement, reason: ToastCloseReason): void {
151
- const el = host as HTMLElement & { open?: boolean };
152
- if (el.open !== true) {
152
+ export function closeToast(host: ToastElement, reason: ToastCloseReason): void {
153
+ if (host.open !== true) {
153
154
  return;
154
155
  }
155
- el.open = false;
156
+ host.open = false;
156
157
  host.dispatchEvent(new CustomEvent("close", { bubbles: true, detail: { reason } }));
157
158
  }
158
159
 
@@ -207,7 +208,7 @@ export function onToastUnmount(state: ToastState): void {
207
208
  */
208
209
  export function onToastHold(state: ToastState, event: Event): void {
209
210
  const clock = clockOf(state);
210
- const reason = HOLD_REASON[event.type] ?? event.type;
211
+ const reason = HOLD_REASON.get(event.type) ?? event.type;
211
212
  if (clock.timer !== null) {
212
213
  clock.remaining = Math.max(1, clock.remaining - (Date.now() - clock.startedAt));
213
214
  }
@@ -225,7 +226,7 @@ export function onToastRelease(state: ToastState, event: Event): void {
225
226
  if (staysInside(event)) {
226
227
  return;
227
228
  }
228
- clockOf(state).holds.delete(HOLD_REASON[event.type] ?? event.type);
229
+ clockOf(state).holds.delete(HOLD_REASON.get(event.type) ?? event.type);
229
230
  schedule(state);
230
231
  }
231
232
 
@@ -63,7 +63,6 @@ const TEXT_TYPES = new Set(["", "text", "search", "url", "tel", "password", "ema
63
63
  export interface ToolbarState {
64
64
  orientation?: string;
65
65
  label?: string;
66
- [key: string]: unknown;
67
66
  }
68
67
 
69
68
  /** A toolbar item: a control, or the kit element that wraps one. */
@@ -98,7 +97,7 @@ function itemFor(bar: HTMLElement, node: HTMLElement): ItemElement {
98
97
  let item: ItemElement = node;
99
98
  for (let el = node.parentElement; el && el !== bar; el = el.parentElement) {
100
99
  if (el.localName.includes("-")) {
101
- item = el as ItemElement;
100
+ item = el;
102
101
  }
103
102
  }
104
103
  return item;
@@ -249,7 +248,7 @@ function atEdge(node: HTMLInputElement | HTMLTextAreaElement, forward: boolean):
249
248
  // A live selection is a gesture in progress; the arrow that collapses it is the control's.
250
249
  return false;
251
250
  }
252
- return forward ? start >= String(node.value ?? "").length : start <= 0;
251
+ return forward ? start >= node.value.length : start <= 0;
253
252
  }
254
253
 
255
254
  /** Whether `node` is a control with a text caret the arrow keys move. */
@@ -18,18 +18,18 @@
18
18
  * authors to prefer, and on Firefox and Safari it is the shape that shows nothing at all without
19
19
  * this module.
20
20
  *
21
- * "AN OLDER ENGINE" INCLUDES THE DESKTOP APP. Studio ships inside Electrobun's CEF, and the pinned
22
- * release is Chromium 147 (`vendor/electrobun/package/src/shared/cef-version.ts`) — five releases
23
- * short of the attribute. So until that bump reaches 152 the ONLY end-user path to Studio runs the
24
- * fallback for every hinted button in its chrome, several hundred of them, and this module is not a
25
- * corner for other people's browsers but the behaviour a user sees. Three consequences are designed
26
- * for rather than tolerated: focus opens the tip only when it is KEYBOARD focus, the way the
27
- * platform's interest-on-focus does, so a click does not pin a tip open; Escape is ONE listener per
28
- * root shared by every bound tip, not one per tip on the document, so a keystroke in Studio walks
29
- * one handler rather than one per button; and a tip that is MOVED — a keyed `$map` reordering its
30
- * rows, a button re-parented into a toolbar — keeps its binding, because the runtime fires
31
- * `disconnectedCallback` for a move and never re-runs `onMount`, so an unmount that disposed at
32
- * once would strip a reordered row of its tooltip for good.
21
+ * "AN OLDER ENGINE" INCLUDES THE DESKTOP APP. Studio ships inside Electron, whose bundled Chromium
22
+ * trails the stable channel and can sit releases short of the attribute. Until Electron's Chromium
23
+ * reaches it the ONLY end-user path to Studio runs the fallback for every hinted button in its
24
+ * chrome, several hundred of them, and this module is not a corner for other people's browsers but
25
+ * the behaviour a user sees. Three consequences are designed for rather than tolerated: focus opens
26
+ * the tip only when it is KEYBOARD focus, the way the platform's interest-on-focus does, so a click
27
+ * does not pin a tip open; Escape is ONE listener per root shared by every bound tip, not one per
28
+ * tip on the document, so a keystroke in Studio walks one handler rather than one per button; and a
29
+ * tip that is MOVED — a keyed `$map` reordering its rows, a button re-parented into a toolbar —
30
+ * keeps its binding, because the runtime fires `disconnectedCallback` for a move and never re-runs
31
+ * `onMount`, so an unmount that disposed at once would strip a reordered row of its tooltip for
32
+ * good.
33
33
  *
34
34
  * IT ADDS LISTENERS TO AN ELEMENT THE KIT DOES NOT OWN, and that is said out loud rather than left
35
35
  * to be discovered in review. specs/ui.md §2 principle 5 forbids WRITING ATTRIBUTES or styles on
@@ -51,8 +51,9 @@
51
51
  * @docs extending/ui-kit
52
52
  */
53
53
 
54
+ import { isFunction } from "@jxsuite/schema/guards";
55
+
54
56
  import { close, openAt, supportsAnchorPositioning } from "./popover.ts";
55
- import type { PopoverElement } from "./popover.ts";
56
57
 
57
58
  /** The reactive scope a `jx-tooltip` document hands its handlers. */
58
59
  export interface TooltipState {
@@ -66,9 +67,26 @@ export interface TooltipState {
66
67
  placement?: string;
67
68
  /** Whether the platform is placing the tip against the control it was shown from. */
68
69
  anchored?: boolean;
69
- [key: string]: unknown;
70
+ /**
71
+ * The id of the control the tip is wired to, from the reflected `for` attribute; null when a host
72
+ * clears it.
73
+ */
74
+ for?: string | null;
75
+ }
76
+
77
+ /** The reactive scope a hinted kit control hands {@link mintHintId}. */
78
+ export interface HintState {
79
+ /** The control's own id stem, minted at mount. */
80
+ uid?: string;
70
81
  }
71
82
 
83
+ /**
84
+ * The root a tip's id lookups run against. An element's root node is the Document, the
85
+ * DocumentFragment or ShadowRoot holding it, or — while it is detached — its topmost ancestor
86
+ * element. Every one of them can be queried; only the first two can look an id up directly.
87
+ */
88
+ type LookupRoot = ParentNode & { getElementById?: (elementId: string) => HTMLElement | null };
89
+
72
90
  /**
73
91
  * How long a tip stays up after the pointer leaves the control, in milliseconds.
74
92
  *
@@ -113,7 +131,7 @@ function watchEscape(root: Node, dismiss: () => void): () => void {
113
131
  if (!entry) {
114
132
  const set = new Set<() => void>();
115
133
  const listener = (event: Event): void => {
116
- if ((event as KeyboardEvent).key !== "Escape") {
134
+ if (!(event instanceof KeyboardEvent) || event.key !== "Escape") {
117
135
  return;
118
136
  }
119
137
  for (const fn of set) {
@@ -197,7 +215,7 @@ const watchers = new WeakMap<object, () => void>();
197
215
  */
198
216
  export function supportsInterestInvokers(): boolean {
199
217
  return (
200
- typeof HTMLButtonElement === "function" && "interestForElement" in HTMLButtonElement.prototype
218
+ isFunction(globalThis.HTMLButtonElement) && "interestForElement" in HTMLButtonElement.prototype
201
219
  );
202
220
  }
203
221
 
@@ -216,12 +234,12 @@ function platformOwns(tip: HTMLElement, trigger: Element): boolean {
216
234
 
217
235
  /** Show the tip from its control, so the platform knows what the hint belongs to. */
218
236
  function showTip(tip: HTMLElement, source: Element | null): void {
219
- openAt(tip as PopoverElement, source);
237
+ openAt(tip, source);
220
238
  }
221
239
 
222
240
  /** Hide the tip. `open` and `data-open` follow from the platform's own toggle. */
223
241
  function hideTip(tip: HTMLElement): void {
224
- close(tip as PopoverElement);
242
+ close(tip);
225
243
  }
226
244
 
227
245
  /**
@@ -291,7 +309,7 @@ export function bindTooltip(tip: HTMLElement, trigger: Element): () => void {
291
309
  };
292
310
  const showLater = (): void => {
293
311
  cancel();
294
- const delay = Number((tip as { delay?: unknown }).delay) || 0;
312
+ const delay = "delay" in tip ? Number(tip.delay) || 0 : 0;
295
313
  showTimer = setTimeout(show, delay);
296
314
  };
297
315
  const hideLater = (): void => {
@@ -389,8 +407,10 @@ export function bindTooltip(tip: HTMLElement, trigger: Element): () => void {
389
407
  * miss.
390
408
  */
391
409
  function triggerOf(state: TooltipState, tip: HTMLElement): HTMLElement | null {
392
- const root = tip.getRootNode() as Document | ShadowRoot;
393
- const named = typeof state["for"] === "string" ? state["for"] : "";
410
+ /* SAFETY: `tip` is an Element, and the root node of an element is never a Text, Comment or Attr
411
+ node: it is a Document, a DocumentFragment or an Element, each of them a ParentNode. */
412
+ const root = tip.getRootNode() as LookupRoot;
413
+ const named = state.for ?? "";
394
414
  const byFor = named === "" ? null : (root.getElementById?.(named) ?? null);
395
415
  if (byFor) {
396
416
  return byFor;
@@ -414,7 +434,9 @@ function anchorOf(state: TooltipState, tip: HTMLElement): HTMLElement | null {
414
434
  if (wired) {
415
435
  return wired;
416
436
  }
417
- const root = tip.getRootNode() as Document | ShadowRoot;
437
+ /* SAFETY: `tip` is an Element, and the root node of an element is never a Text, Comment or Attr
438
+ node: it is a Document, a DocumentFragment or an Element, each of them a ParentNode. */
439
+ const root = tip.getRootNode() as LookupRoot;
418
440
  const own = tip.id;
419
441
  if (!/^[\w-]+$/.test(own)) {
420
442
  return null;
@@ -464,7 +486,7 @@ export function placeTooltip(state: TooltipState, tip: HTMLElement): void {
464
486
  state.flipped = true;
465
487
  }
466
488
  };
467
- if (typeof requestAnimationFrame === "function") {
489
+ if (isFunction(globalThis.requestAnimationFrame)) {
468
490
  requestAnimationFrame(measure);
469
491
  } else {
470
492
  measure();
@@ -516,7 +538,7 @@ export function measureAnchoredFlip(state: TooltipState, tip: HTMLElement): () =
516
538
  }
517
539
  state.flipped = own.bottom <= box.top;
518
540
  };
519
- if (!anchor || typeof requestAnimationFrame !== "function") {
541
+ if (!anchor || !isFunction(globalThis.requestAnimationFrame)) {
520
542
  /* No control to be on a side of, or no frames to coalesce to: one synchronous reading, which
521
543
  is nothing at all in the first case, and no watch. */
522
544
  measure();
@@ -539,7 +561,7 @@ export function measureAnchoredFlip(state: TooltipState, tip: HTMLElement): () =
539
561
  listener before it scrolls. Removal keys on `capture` alone, which is why it is spelled twice. */
540
562
  window.addEventListener("scroll", schedule, { capture: true, passive: true });
541
563
  window.addEventListener("resize", schedule);
542
- const observer = typeof ResizeObserver === "function" ? new ResizeObserver(schedule) : null;
564
+ const observer = isFunction(globalThis.ResizeObserver) ? new ResizeObserver(schedule) : null;
543
565
  if (observer) {
544
566
  observer.observe(tip);
545
567
  observer.observe(anchor);
@@ -576,8 +598,8 @@ export function onTooltipBeforeToggle(state: TooltipState, event: Event): void {
576
598
  if (!(tip instanceof HTMLElement)) {
577
599
  return;
578
600
  }
579
- const opening = (event as { newState?: string }).newState === "open";
580
- const { source } = event as Event & { source?: unknown };
601
+ const opening = "newState" in event && event.newState === "open";
602
+ const source = "source" in event ? event.source : null;
581
603
  const from = source instanceof Element ? source : anchorOf(state, tip);
582
604
  state.anchored = opening && from !== null;
583
605
  if (!opening) {
@@ -602,7 +624,7 @@ export function onTooltipToggle(state: TooltipState, event: Event): void {
602
624
  if (!(tip instanceof HTMLElement)) {
603
625
  return;
604
626
  }
605
- const opening = (event as { newState?: string }).newState === "open";
627
+ const opening = "newState" in event && event.newState === "open";
606
628
  state.open = opening;
607
629
  if (!opening) {
608
630
  // A closed tip has no side, and the watch that was following it has nothing left to follow.
@@ -713,7 +735,7 @@ let mintedHints = 0;
713
735
  * @param state The control's reactive scope, whose `uid` this writes.
714
736
  * @param host The `jx-action-button`, `jx-button` or `jx-switch` element.
715
737
  */
716
- export function mintHintId(state: Record<string, unknown>, host: HTMLElement): void {
738
+ export function mintHintId(state: HintState, host: HTMLElement): void {
717
739
  mintedHints += 1;
718
- state["uid"] = `${host.localName}-${mintedHints}`;
740
+ state.uid = `${host.localName}-${mintedHints}`;
719
741
  }
@@ -50,6 +50,8 @@
50
50
  * @docs extending/ui-kit
51
51
  */
52
52
 
53
+ import { isFunction } from "@jxsuite/schema/guards";
54
+
53
55
  /** The tag of the tree. */
54
56
  const TREE = "jx-tree";
55
57
  /** The tag of one row. */
@@ -100,7 +102,6 @@ export interface TreeState {
100
102
  anchor?: string;
101
103
  padtop?: number;
102
104
  padbottom?: number;
103
- [key: string]: unknown;
104
105
  }
105
106
 
106
107
  /** A row element with the property accessors its own document installs. */
@@ -153,9 +154,7 @@ const handoffs = new WeakMap<HTMLElement, Handoff>();
153
154
  /** The tree an event's `currentTarget` is. */
154
155
  function hostOf(event: Event): TreeElement | null {
155
156
  const target = event.currentTarget;
156
- return target instanceof HTMLElement && target.localName === TREE
157
- ? (target as TreeElement)
158
- : null;
157
+ return target instanceof HTMLElement && target.localName === TREE ? target : null;
159
158
  }
160
159
 
161
160
  /** Whether `tree` is the nearest tree around `node` — a key inside a nested tree is its own. */
@@ -209,7 +208,7 @@ function isDisabled(item: ItemElement): boolean {
209
208
  */
210
209
  function labelOf(item: ItemElement): string {
211
210
  const named = propOf(item, "label", "label");
212
- return named === "" ? (item.textContent ?? "") : named;
211
+ return named === "" ? item.textContent : named;
213
212
  }
214
213
 
215
214
  /**
@@ -255,12 +254,12 @@ export function itemsOf(
255
254
  *
256
255
  * @param tree The `jx-tree` element.
257
256
  */
258
- export function syncTree(tree: HTMLElement): void {
257
+ export function syncTree(tree: TreeElement): void {
259
258
  const items = itemsOf(tree, { includeDisabled: true });
260
259
  if (items.length === 0) {
261
260
  return;
262
261
  }
263
- const current = String((tree as TreeElement).current ?? "");
262
+ const current = String(tree.current ?? "");
264
263
  const enabled = items.filter((item) => !isDisabled(item));
265
264
  const caret = enabled.find((item) => valueOf(item) === current) ?? enabled[0] ?? null;
266
265
  for (const item of items) {
@@ -425,8 +424,19 @@ function activeIndex(items: ItemElement[], target: EventTarget | null): number {
425
424
  );
426
425
  }
427
426
 
427
+ /**
428
+ * What a tree event carries: a row's value for `change` and `activate`, else the event's own detail
429
+ * record.
430
+ */
431
+ type TreeEventDetail =
432
+ | string
433
+ | TreeExpandDetail
434
+ | TreeMoveDetail
435
+ | TreeSelectDetail
436
+ | TreeTypeaheadDetail;
437
+
428
438
  /** Say a public thing, from the tree, so `e.target` is the element a host wrote. */
429
- function announce(host: HTMLElement, name: string, detail: unknown): void {
439
+ function announce(host: HTMLElement, name: string, detail: TreeEventDetail): void {
430
440
  host.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }));
431
441
  }
432
442
 
@@ -441,7 +451,7 @@ function moveTo(host: HTMLElement, scope: TreeState, item: ItemElement): void {
441
451
  scope.current = valueOf(item);
442
452
  syncTree(host);
443
453
  item.focus();
444
- if (typeof item.scrollIntoView === "function") {
454
+ if (isFunction(item.scrollIntoView)) {
445
455
  item.scrollIntoView({ block: "nearest" });
446
456
  }
447
457
  announce(host, "change", valueOf(item));
package/src/color.ts CHANGED
@@ -25,6 +25,9 @@
25
25
  * @docs extending/ui-kit
26
26
  */
27
27
 
28
+ import { isString } from "@jxsuite/schema/guards";
29
+ import type { JsonValue } from "@jxsuite/schema/types";
30
+
28
31
  /** An sRGB colour. `r`, `g` and `b` are 0-255 and need not be integers; `a` is 0-1. */
29
32
  export interface Rgb {
30
33
  r: number;
@@ -65,6 +68,18 @@ export function clamp(value: number, min: number, max: number): number {
65
68
  return value < min ? min : value > max ? max : value;
66
69
  }
67
70
 
71
+ /**
72
+ * Which way each arrow key moves a colour element's value: Up and Right increase, Down and Left
73
+ * decrease. `jx-color-area` measures its vertical axis upward from the bottom edge, so ArrowUp
74
+ * raising brightness is the same rule as ArrowUp raising a slider's value.
75
+ */
76
+ export const ARROW_SIGN: ReadonlyMap<string, -1 | 1> = new Map([
77
+ ["ArrowDown", -1],
78
+ ["ArrowLeft", -1],
79
+ ["ArrowRight", 1],
80
+ ["ArrowUp", 1],
81
+ ]);
82
+
68
83
  /**
69
84
  * `value` snapped to the grid `step` describes, measured from `min` and held inside `[min, max]`.
70
85
  *
@@ -427,7 +442,7 @@ export function preferredInk(rgb: Rgb): "black" | "white" {
427
442
  * @param color The colour, as it was written.
428
443
  * @returns `"black"`, `"white"`, or `""`.
429
444
  */
430
- export function inkOn(color: unknown): string {
431
- const parsed = typeof color === "string" ? parseColor(color) : null;
445
+ export function inkOn(color: JsonValue | undefined): string {
446
+ const parsed = isString(color) ? parseColor(color) : null;
432
447
  return parsed ? preferredInk({ ...parsed, a: 1 }) : "";
433
448
  }
package/src/documents.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * bytes the shell runs.
5
5
  */
6
6
  import { overlayScopeFor } from "@jxsuite/schema/overlays";
7
- import type { JxDocument, JxElement } from "@jxsuite/schema/types";
7
+ import type { JsonPrimitive, JxDocument } from "@jxsuite/schema/types";
8
8
  import jxAccordion from "../components/jx-accordion.json";
9
9
  import jxAccordionItem from "../components/jx-accordion-item.json";
10
10
  import jxActionButton from "../components/jx-action-button.json";
@@ -44,48 +44,74 @@ import jxTreeItem from "../components/jx-tree-item.json";
44
44
  import jxToastHost from "../components/jx-toast-host.json";
45
45
  import jxToolbar from "../components/jx-toolbar.json";
46
46
 
47
+ /**
48
+ * A value as TypeScript types a JSON module import. Under the root config's
49
+ * `exactOptionalPropertyTypes` that is always a `JsonValue`, but `packages/desktop` compiles this
50
+ * file without the flag, and there an array of differently shaped objects is normalised by giving
51
+ * each object the others' keys as `?: undefined`, which `JsonValue` refuses.
52
+ */
53
+ type ImportedJson =
54
+ | JsonPrimitive
55
+ | undefined
56
+ | readonly ImportedJson[]
57
+ | { readonly [key: string]: ImportedJson };
58
+
59
+ /**
60
+ * A bundled component file as the document the runtime defines it from.
61
+ *
62
+ * @param {Readonly<Record<string, ImportedJson>>} json - One of the `components/*.json` imports
63
+ * above
64
+ * @returns {JxDocument} The same object, typed as the document it is
65
+ */
66
+ function kitDocument(json: Readonly<Record<string, ImportedJson>>): JxDocument {
67
+ /* SAFETY: every argument is a `components/*.json` file of this package, and `conformance.test.ts`
68
+ validates each entry of `documents` against the Jx document schema (ui.md §3.2), so the JSON
69
+ is a JxDocument; TypeScript only sees the literal shape the JSON import infers. */
70
+ return json as JxDocument;
71
+ }
72
+
47
73
  /** Tag name → document, in registration order (a document's `$elements` come before it). */
48
- export const documents: Readonly<Record<string, JxDocument>> = {
49
- "jx-icon": jxIcon as unknown as JxDocument,
50
- "jx-spinner": jxSpinner as unknown as JxDocument,
74
+ export const documents = {
75
+ "jx-icon": kitDocument(jxIcon),
76
+ "jx-spinner": kitDocument(jxSpinner),
51
77
  // Before the buttons: each renders a `jx-tooltip` for its `hint`.
52
- "jx-tooltip": jxTooltip as unknown as JxDocument,
53
- "jx-button": jxButton as unknown as JxDocument,
54
- "jx-action-button": jxActionButton as unknown as JxDocument,
55
- "jx-menu-item": jxMenuItem as unknown as JxDocument,
56
- "jx-menu": jxMenu as unknown as JxDocument,
57
- "jx-popover": jxPopover as unknown as JxDocument,
58
- "jx-textfield": jxTextfield as unknown as JxDocument,
59
- "jx-checkbox": jxCheckbox as unknown as JxDocument,
60
- "jx-switch": jxSwitch as unknown as JxDocument,
61
- "jx-number-field": jxNumberField as unknown as JxDocument,
62
- "jx-select": jxSelect as unknown as JxDocument,
63
- "jx-option": jxOption as unknown as JxDocument,
64
- "jx-listbox": jxListbox as unknown as JxDocument,
65
- "jx-combobox": jxCombobox as unknown as JxDocument,
66
- "jx-action-group": jxActionGroup as unknown as JxDocument,
67
- "jx-toolbar": jxToolbar as unknown as JxDocument,
68
- "jx-tab": jxTab as unknown as JxDocument,
69
- "jx-tab-panel": jxTabPanel as unknown as JxDocument,
70
- "jx-tabs": jxTabs as unknown as JxDocument,
71
- "jx-tree-item": jxTreeItem as unknown as JxDocument,
72
- "jx-tree": jxTree as unknown as JxDocument,
73
- "jx-accordion-item": jxAccordionItem as unknown as JxDocument,
74
- "jx-accordion": jxAccordion as unknown as JxDocument,
75
- "jx-divider": jxDivider as unknown as JxDocument,
76
- "jx-split": jxSplit as unknown as JxDocument,
77
- "jx-kbd": jxKbd as unknown as JxDocument,
78
- "jx-dot": jxDot as unknown as JxDocument,
79
- "jx-field": jxField as unknown as JxDocument,
80
- "jx-dialog": jxDialog as unknown as JxDocument,
81
- "jx-toast": jxToast as unknown as JxDocument,
82
- "jx-toast-host": jxToastHost as unknown as JxDocument,
83
- "jx-color-area": jxColorArea as unknown as JxDocument,
84
- "jx-color-slider": jxColorSlider as unknown as JxDocument,
85
- "jx-swatch": jxSwatch as unknown as JxDocument,
86
- "jx-swatch-group": jxSwatchGroup as unknown as JxDocument,
87
- "jx-color-field": jxColorField as unknown as JxDocument,
88
- };
78
+ "jx-tooltip": kitDocument(jxTooltip),
79
+ "jx-button": kitDocument(jxButton),
80
+ "jx-action-button": kitDocument(jxActionButton),
81
+ "jx-menu-item": kitDocument(jxMenuItem),
82
+ "jx-menu": kitDocument(jxMenu),
83
+ "jx-popover": kitDocument(jxPopover),
84
+ "jx-textfield": kitDocument(jxTextfield),
85
+ "jx-checkbox": kitDocument(jxCheckbox),
86
+ "jx-switch": kitDocument(jxSwitch),
87
+ "jx-number-field": kitDocument(jxNumberField),
88
+ "jx-select": kitDocument(jxSelect),
89
+ "jx-option": kitDocument(jxOption),
90
+ "jx-listbox": kitDocument(jxListbox),
91
+ "jx-combobox": kitDocument(jxCombobox),
92
+ "jx-action-group": kitDocument(jxActionGroup),
93
+ "jx-toolbar": kitDocument(jxToolbar),
94
+ "jx-tab": kitDocument(jxTab),
95
+ "jx-tab-panel": kitDocument(jxTabPanel),
96
+ "jx-tabs": kitDocument(jxTabs),
97
+ "jx-tree-item": kitDocument(jxTreeItem),
98
+ "jx-tree": kitDocument(jxTree),
99
+ "jx-accordion-item": kitDocument(jxAccordionItem),
100
+ "jx-accordion": kitDocument(jxAccordion),
101
+ "jx-divider": kitDocument(jxDivider),
102
+ "jx-split": kitDocument(jxSplit),
103
+ "jx-kbd": kitDocument(jxKbd),
104
+ "jx-dot": kitDocument(jxDot),
105
+ "jx-field": kitDocument(jxField),
106
+ "jx-dialog": kitDocument(jxDialog),
107
+ "jx-toast": kitDocument(jxToast),
108
+ "jx-toast-host": kitDocument(jxToastHost),
109
+ "jx-color-area": kitDocument(jxColorArea),
110
+ "jx-color-slider": kitDocument(jxColorSlider),
111
+ "jx-swatch": kitDocument(jxSwatch),
112
+ "jx-swatch-group": kitDocument(jxSwatchGroup),
113
+ "jx-color-field": kitDocument(jxColorField),
114
+ } as const satisfies Readonly<Record<string, JxDocument>>;
89
115
 
90
116
  /** Every tag the kit defines. */
91
117
  export const KIT_TAGS: readonly string[] = Object.keys(documents);
@@ -104,7 +130,7 @@ export const KIT_TAGS: readonly string[] = Object.keys(documents);
104
130
  * asks the same question of a project's `*.class.json` definitions, and two derivations of "what
105
131
  * makes a tag a popover" is two chances to disagree about one project.
106
132
  */
107
- const KIT_SCOPE = overlayScopeFor(Object.values(documents) as JxElement[]);
133
+ const KIT_SCOPE = overlayScopeFor(Object.values(documents));
108
134
 
109
135
  /** Tags whose definition declares `popover`. */
110
136
  export const POPOVER_TAGS: ReadonlySet<string> = KIT_SCOPE.popoverTags ?? new Set();
package/src/icons.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  *
6
6
  * @docs extending/ui-kit
7
7
  */
8
+ import { isString } from "@jxsuite/schema/guards";
9
+ import type { JsonValue } from "@jxsuite/schema/types";
8
10
  import manifest from "../icons/manifest.json";
9
11
  import { ICON_VIEW_BOX, ICON_WEIGHTS } from "./icons-build.ts";
10
12
  import type { IconManifest, IconWeight } from "./icons-build.ts";
@@ -12,7 +14,8 @@ import type { IconManifest, IconWeight } from "./icons-build.ts";
12
14
  export { ICON_VIEW_BOX, ICON_WEIGHTS } from "./icons-build.ts";
13
15
  export type { IconWeight } from "./icons-build.ts";
14
16
 
15
- const { icons } = manifest as IconManifest;
17
+ const iconManifest: IconManifest = manifest;
18
+ const { icons } = iconManifest;
16
19
 
17
20
  /** Every icon name the kit ships, sorted. */
18
21
  export const ICON_NAMES: readonly string[] = Object.keys(icons);
@@ -33,10 +36,11 @@ const warned = new Set<string>();
33
36
  * every listed icon carries; a name it lacks warns once and draws nothing, so a typo shows as a gap
34
37
  * rather than a crash or a wrong glyph.
35
38
  *
36
- * Called from the `jx-icon` document with the element's own `name` and `weight` state.
39
+ * Called from the `jx-icon` document with the element's own `name` and `weight` state. Both are
40
+ * declared strings, but a property write can store any value, so neither is trusted to be one.
37
41
  */
38
- export function iconPath(name: unknown, weight: unknown): string {
39
- const key = typeof name === "string" ? name : "";
42
+ export function iconPath(name: JsonValue | undefined, weight: JsonValue | undefined): string {
43
+ const key = isString(name) ? name : "";
40
44
  const entry = icons[key];
41
45
  if (!entry) {
42
46
  if (key && !warned.has(key)) {
@@ -45,11 +49,9 @@ export function iconPath(name: unknown, weight: unknown): string {
45
49
  }
46
50
  return "";
47
51
  }
48
- const w = (ICON_WEIGHTS as readonly string[]).includes(String(weight))
49
- ? (weight as IconWeight)
50
- : "regular";
52
+ const w = ICON_WEIGHTS.find((candidate) => candidate === String(weight)) ?? "regular";
51
53
  return entry[w] ?? entry.regular ?? "";
52
54
  }
53
55
 
54
56
  /** The viewBox `jx-icon` draws on; the manifest's, so a rebuilt manifest cannot drift from it. */
55
- export const iconViewBox: string = (manifest as IconManifest).viewBox || ICON_VIEW_BOX;
57
+ export const iconViewBox: string = iconManifest.viewBox || ICON_VIEW_BOX;
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * @docs extending/ui-kit
11
11
  */
12
12
  import { defineElement, preloadDocument, preloadModule } from "@jxsuite/runtime";
13
+ import type { JxScope } from "@jxsuite/runtime/types";
13
14
  import * as color from "./color.ts";
14
15
  import * as colorArea from "./behaviors/color-area.ts";
15
16
  import * as colorField from "./behaviors/color-field.ts";
@@ -50,8 +51,12 @@ export {
50
51
  /** The base every kit document resolves its references against. */
51
52
  export const KIT_BASE = "jx-ui:/components/";
52
53
 
53
- /** Behaviour modules, under the `$src` specifiers the documents use. */
54
- export const KIT_MODULES: Readonly<Record<string, Record<string, unknown>>> = {
54
+ /**
55
+ * Behaviour modules, under the `$src` specifiers the documents use. The runtime reads each
56
+ * namespace's exports into scope, so every entry must be a `JxScope`: export name → value, narrowed
57
+ * where the runtime uses it.
58
+ */
59
+ export const KIT_MODULES = {
55
60
  "jx-ui:/behaviors/color-area.ts": colorArea,
56
61
  "jx-ui:/behaviors/color-field.ts": colorField,
57
62
  "jx-ui:/behaviors/color-slider.ts": colorSlider,
@@ -74,7 +79,7 @@ export const KIT_MODULES: Readonly<Record<string, Record<string, unknown>>> = {
74
79
  "jx-ui:/behaviors/toast-host.ts": toastHost,
75
80
  "jx-ui:/color.ts": color,
76
81
  "jx-ui:/icons.ts": icons,
77
- };
82
+ } as const satisfies Readonly<Record<string, JxScope>>;
78
83
 
79
84
  export interface RegisterUiOptions {
80
85
  /** Adopt the theme into the document as well. Default is true. */