@aceshooting/lyra-ui 2.0.0 → 2.1.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.
Files changed (224) hide show
  1. package/README.md +87 -14
  2. package/custom-elements.json +11555 -5539
  3. package/dist/components/app-rail/app-rail-item.d.ts +30 -0
  4. package/dist/components/app-rail/app-rail-item.js +68 -0
  5. package/dist/components/app-rail/app-rail-item.styles.d.ts +1 -0
  6. package/dist/components/app-rail/app-rail-item.styles.js +65 -0
  7. package/dist/components/app-rail/app-rail.d.ts +21 -24
  8. package/dist/components/app-rail/app-rail.js +68 -132
  9. package/dist/components/app-rail/app-rail.styles.js +5 -2
  10. package/dist/components/attachment-chip/attachment-chip.d.ts +38 -11
  11. package/dist/components/attachment-chip/attachment-chip.js +73 -29
  12. package/dist/components/attachment-trigger/attachment-trigger.d.ts +13 -0
  13. package/dist/components/attachment-trigger/attachment-trigger.js +13 -1
  14. package/dist/components/attachment-trigger/attachment-trigger.styles.js +28 -6
  15. package/dist/components/chart/bar-chart.js +2 -18
  16. package/dist/components/chart/box-plot.d.ts +25 -1
  17. package/dist/components/chart/box-plot.js +136 -6
  18. package/dist/components/chart/box-plot.styles.js +13 -0
  19. package/dist/components/chart/bubble-chart.js +2 -18
  20. package/dist/components/chart/chart.d.ts +54 -10
  21. package/dist/components/chart/chart.js +182 -39
  22. package/dist/components/chart/doughnut-chart.js +2 -18
  23. package/dist/components/chart/histogram-bin.d.ts +5 -2
  24. package/dist/components/chart/histogram-bin.js +12 -4
  25. package/dist/components/chart/histogram.js +8 -16
  26. package/dist/components/chart/line-chart.js +2 -18
  27. package/dist/components/chart/lite-chart.d.ts +80 -3
  28. package/dist/components/chart/lite-chart.js +250 -53
  29. package/dist/components/chart/pie-chart.js +2 -18
  30. package/dist/components/chart/polar-area-chart.js +2 -18
  31. package/dist/components/chart/radar-chart.js +2 -18
  32. package/dist/components/chart/scatter-chart.js +2 -18
  33. package/dist/components/chat-composer/chat-composer.d.ts +42 -1
  34. package/dist/components/chat-composer/chat-composer.js +84 -3
  35. package/dist/components/chat-composer/chat-composer.styles.js +11 -1
  36. package/dist/components/chat-message/chat-message.d.ts +32 -4
  37. package/dist/components/chat-message/chat-message.js +57 -17
  38. package/dist/components/checkbox/checkbox.d.ts +66 -8
  39. package/dist/components/checkbox/checkbox.js +173 -50
  40. package/dist/components/checkbox/checkbox.styles.js +9 -2
  41. package/dist/components/chip/chip-group.js +8 -2
  42. package/dist/components/chip/chip.js +9 -2
  43. package/dist/components/citation-badge/citation-badge.js +21 -3
  44. package/dist/components/code-block/code-block.d.ts +38 -0
  45. package/dist/components/code-block/code-block.js +72 -7
  46. package/dist/components/code-block/code-loader.d.ts +44 -1
  47. package/dist/components/code-block/code-loader.js +63 -0
  48. package/dist/components/combobox/combobox.d.ts +46 -5
  49. package/dist/components/combobox/combobox.js +175 -35
  50. package/dist/components/combobox/combobox.styles.js +3 -3
  51. package/dist/components/context-meter/context-meter.js +17 -8
  52. package/dist/components/context-meter/context-meter.styles.js +0 -6
  53. package/dist/components/conversation-item/conversation-item.d.ts +13 -16
  54. package/dist/components/conversation-item/conversation-item.js +29 -20
  55. package/dist/components/date-picker/calendar-core.d.ts +24 -0
  56. package/dist/components/date-picker/calendar-core.js +51 -4
  57. package/dist/components/date-picker/date-input.d.ts +77 -10
  58. package/dist/components/date-picker/date-input.js +278 -73
  59. package/dist/components/date-picker/date-picker.d.ts +28 -4
  60. package/dist/components/date-picker/date-picker.js +95 -37
  61. package/dist/components/dialog/confirm.js +3 -6
  62. package/dist/components/dialog/dialog.d.ts +75 -31
  63. package/dist/components/dialog/dialog.js +147 -131
  64. package/dist/components/dialog/dialog.styles.js +46 -2
  65. package/dist/components/dock-panel/dock-panel.d.ts +8 -0
  66. package/dist/components/dock-panel/dock-panel.js +15 -1
  67. package/dist/components/dock-panel/dock-panel.styles.js +15 -1
  68. package/dist/components/document-preview/document-preview.d.ts +15 -7
  69. package/dist/components/document-preview/document-preview.js +29 -13
  70. package/dist/components/empty/empty.d.ts +1 -2
  71. package/dist/components/empty/empty.js +18 -12
  72. package/dist/components/empty/empty.styles.js +0 -3
  73. package/dist/components/export-button/export-button.d.ts +12 -0
  74. package/dist/components/export-button/export-button.js +33 -5
  75. package/dist/components/export-button/export-button.styles.js +5 -1
  76. package/dist/components/file-input/accept.js +2 -0
  77. package/dist/components/file-input/file-input.d.ts +10 -1
  78. package/dist/components/file-input/file-input.js +36 -5
  79. package/dist/components/flag/flag.d.ts +1 -1
  80. package/dist/components/flag/flag.js +15 -1
  81. package/dist/components/flag/flag.styles.js +1 -1
  82. package/dist/components/flag/language-map.d.ts +4 -1
  83. package/dist/components/flag/language-map.js +6 -3
  84. package/dist/components/gauge/gauge.d.ts +10 -1
  85. package/dist/components/gauge/gauge.js +28 -9
  86. package/dist/components/generation-status/generation-status.d.ts +13 -5
  87. package/dist/components/generation-status/generation-status.js +59 -5
  88. package/dist/components/graph/graph.d.ts +37 -3
  89. package/dist/components/graph/graph.js +132 -20
  90. package/dist/components/heatmap/calendar-grid.d.ts +15 -6
  91. package/dist/components/heatmap/calendar-grid.js +17 -6
  92. package/dist/components/heatmap/heatmap-scale.d.ts +5 -1
  93. package/dist/components/heatmap/heatmap-scale.js +8 -2
  94. package/dist/components/heatmap/heatmap.d.ts +166 -25
  95. package/dist/components/heatmap/heatmap.js +372 -89
  96. package/dist/components/json-viewer/json-viewer.d.ts +22 -5
  97. package/dist/components/json-viewer/json-viewer.js +115 -37
  98. package/dist/components/json-viewer/json-viewer.styles.js +2 -1
  99. package/dist/components/kbd/kbd.js +24 -7
  100. package/dist/components/live-region/live-region.d.ts +10 -1
  101. package/dist/components/live-region/live-region.js +42 -6
  102. package/dist/components/map/map.d.ts +24 -1
  103. package/dist/components/map/map.js +89 -24
  104. package/dist/components/markdown/markdown-loader.d.ts +24 -0
  105. package/dist/components/markdown/markdown-loader.js +35 -1
  106. package/dist/components/markdown/markdown.d.ts +46 -8
  107. package/dist/components/markdown/markdown.js +136 -31
  108. package/dist/components/markdown/markdown.styles.js +7 -1
  109. package/dist/components/media-card/media-card.d.ts +25 -17
  110. package/dist/components/media-card/media-card.js +43 -65
  111. package/dist/components/mention-popover/mention-popover.d.ts +4 -1
  112. package/dist/components/mention-popover/mention-popover.js +23 -5
  113. package/dist/components/menu/menu-item.d.ts +32 -3
  114. package/dist/components/menu/menu-item.js +94 -7
  115. package/dist/components/menu/menu-item.styles.js +8 -0
  116. package/dist/components/menu/menu.d.ts +25 -1
  117. package/dist/components/menu/menu.js +99 -3
  118. package/dist/components/menu/menu.styles.js +3 -2
  119. package/dist/components/model-select/model-select.d.ts +48 -3
  120. package/dist/components/model-select/model-select.js +120 -22
  121. package/dist/components/model-select/model-select.styles.js +17 -3
  122. package/dist/components/model-settings-panel/model-settings-panel.d.ts +25 -1
  123. package/dist/components/model-settings-panel/model-settings-panel.js +65 -1
  124. package/dist/components/playback/playback.d.ts +6 -2
  125. package/dist/components/playback/playback.js +72 -29
  126. package/dist/components/playback/playback.styles.js +6 -1
  127. package/dist/components/responsive-panel/responsive-panel.d.ts +29 -16
  128. package/dist/components/responsive-panel/responsive-panel.js +130 -132
  129. package/dist/components/responsive-panel/responsive-panel.styles.js +1 -1
  130. package/dist/components/result-card/result-card.d.ts +8 -2
  131. package/dist/components/result-card/result-card.js +9 -3
  132. package/dist/components/result-card/result-card.styles.js +0 -1
  133. package/dist/components/select/select.d.ts +59 -20
  134. package/dist/components/select/select.js +125 -43
  135. package/dist/components/skeleton/skeleton.d.ts +3 -0
  136. package/dist/components/skeleton/skeleton.js +7 -1
  137. package/dist/components/skeleton/skeleton.styles.js +5 -4
  138. package/dist/components/slider/slider.d.ts +57 -5
  139. package/dist/components/slider/slider.js +195 -80
  140. package/dist/components/source-card/source-card.d.ts +17 -5
  141. package/dist/components/source-card/source-card.js +55 -8
  142. package/dist/components/source-card/source-card.styles.js +1 -1
  143. package/dist/components/source-list/source-list.js +9 -1
  144. package/dist/components/sparkline/sparkline.js +34 -10
  145. package/dist/components/split/split.d.ts +141 -14
  146. package/dist/components/split/split.js +357 -52
  147. package/dist/components/split/split.styles.js +18 -1
  148. package/dist/components/stat/stat.d.ts +10 -4
  149. package/dist/components/stat/stat.js +46 -11
  150. package/dist/components/stream-status/stream-status.d.ts +27 -11
  151. package/dist/components/stream-status/stream-status.js +95 -13
  152. package/dist/components/streaming-text/streaming-text.d.ts +9 -3
  153. package/dist/components/streaming-text/streaming-text.js +25 -10
  154. package/dist/components/streaming-text/streaming-text.styles.js +3 -2
  155. package/dist/components/switch/switch.d.ts +52 -7
  156. package/dist/components/switch/switch.js +111 -40
  157. package/dist/components/switch/switch.styles.js +1 -1
  158. package/dist/components/table/table.d.ts +18 -3
  159. package/dist/components/table/table.js +88 -20
  160. package/dist/components/tabs/tabs.d.ts +23 -0
  161. package/dist/components/tabs/tabs.js +76 -12
  162. package/dist/components/tabs/tabs.styles.js +11 -0
  163. package/dist/components/thinking-panel/thinking-panel.d.ts +3 -1
  164. package/dist/components/thinking-panel/thinking-panel.js +34 -9
  165. package/dist/components/thinking-panel/thinking-panel.styles.js +6 -0
  166. package/dist/components/time-range/time-range.d.ts +59 -2
  167. package/dist/components/time-range/time-range.js +163 -67
  168. package/dist/components/toast/toast-item.d.ts +12 -3
  169. package/dist/components/toast/toast-item.js +90 -53
  170. package/dist/components/toast/toast-item.styles.js +59 -28
  171. package/dist/components/toast/toast.js +17 -5
  172. package/dist/components/toast/toaster.js +10 -1
  173. package/dist/components/tool-approval-dialog/tool-approval-dialog.d.ts +11 -10
  174. package/dist/components/tool-approval-dialog/tool-approval-dialog.js +50 -124
  175. package/dist/components/tool-approval-dialog/tool-approval-dialog.styles.js +1 -1
  176. package/dist/components/tool-call-chip/tool-call-chip.d.ts +18 -2
  177. package/dist/components/tool-call-chip/tool-call-chip.js +86 -25
  178. package/dist/components/tool-param-form/tool-param-form.d.ts +89 -28
  179. package/dist/components/tool-param-form/tool-param-form.js +350 -106
  180. package/dist/components/tool-result-dialog/tool-result-dialog.d.ts +27 -7
  181. package/dist/components/tool-result-dialog/tool-result-dialog.js +80 -115
  182. package/dist/components/tool-result-dialog/tool-result-dialog.styles.js +1 -1
  183. package/dist/components/tool-result-view/registry.d.ts +0 -8
  184. package/dist/components/tool-result-view/registry.js +7 -2
  185. package/dist/components/tool-result-view/tool-result-view.d.ts +6 -3
  186. package/dist/components/tool-result-view/tool-result-view.js +18 -4
  187. package/dist/components/tool-select-dialog/tool-select-dialog.d.ts +11 -11
  188. package/dist/components/tool-select-dialog/tool-select-dialog.js +44 -133
  189. package/dist/components/tool-select-dialog/tool-select-dialog.styles.js +13 -4
  190. package/dist/components/tree/tree-node.d.ts +4 -1
  191. package/dist/components/tree/tree-node.js +4 -1
  192. package/dist/components/tree/tree.d.ts +17 -8
  193. package/dist/components/tree/tree.js +21 -20
  194. package/dist/components/typing-indicator/typing-indicator.d.ts +12 -2
  195. package/dist/components/typing-indicator/typing-indicator.js +16 -4
  196. package/dist/components/typing-indicator/typing-indicator.styles.js +2 -2
  197. package/dist/components/virtual-list/virtual-list.d.ts +56 -19
  198. package/dist/components/virtual-list/virtual-list.js +184 -38
  199. package/dist/components/virtual-list/virtual-list.styles.js +12 -0
  200. package/dist/components/widget/widget.d.ts +5 -4
  201. package/dist/components/widget/widget.js +54 -120
  202. package/dist/components/widget/widget.styles.js +8 -2
  203. package/dist/components/word-cloud/word-cloud-layout.d.ts +7 -0
  204. package/dist/components/word-cloud/word-cloud-layout.js +20 -6
  205. package/dist/components/word-cloud/word-cloud.d.ts +22 -2
  206. package/dist/components/word-cloud/word-cloud.js +45 -6
  207. package/dist/components/word-cloud/word-cloud.styles.js +13 -1
  208. package/dist/internal/anchored-validity.d.ts +1 -0
  209. package/dist/internal/anchored-validity.js +86 -0
  210. package/dist/internal/form-associated.d.ts +7 -0
  211. package/dist/internal/form-associated.js +43 -4
  212. package/dist/internal/numbers.d.ts +12 -0
  213. package/dist/internal/numbers.js +26 -0
  214. package/dist/internal/overlay-manager.d.ts +51 -0
  215. package/dist/internal/overlay-manager.js +523 -0
  216. package/dist/internal/positioner.js +27 -3
  217. package/dist/internal/safe-url.d.ts +12 -0
  218. package/dist/internal/safe-url.js +41 -0
  219. package/dist/internal/tokens.styles.js +17 -11
  220. package/dist/lyra.d.ts +5 -2
  221. package/dist/lyra.js +3 -1
  222. package/llms-full.txt +970 -426
  223. package/llms.txt +5 -4
  224. package/package.json +19 -2
@@ -1,4 +1,4 @@
1
- import { type TemplateResult } from 'lit';
1
+ import { type TemplateResult, type PropertyValues } from 'lit';
2
2
  import { LyraElement } from '../../internal/lyra-element.js';
3
3
  /**
4
4
  * `<lyra-json-viewer>` — a collapsible, copyable tree view for an arbitrary
@@ -17,10 +17,10 @@ import { LyraElement } from '../../internal/lyra-element.js';
17
17
  * @csspart toolbar - The wrapper around the top-level copy button (only rendered when `copyable`).
18
18
  * @csspart tree - The wrapper around the rendered node tree.
19
19
  * @csspart key - An object property key or array index label.
20
- * @csspart value - A primitive value's text -- carries `data-type` (`string`/`number`/`boolean`/`null`/`undefined`) for per-type coloring, and `data-match` while it matches `search`.
20
+ * @csspart value - A primitive value's text -- carries `data-type` (`string`/`number`/`boolean`/`null`/`undefined`, or `circular` for a self-reference marker in place of a re-visited container's subtree) for per-type coloring, and `data-match` while it matches `search`.
21
21
  * @csspart bracket - A `{`, `}`, `[`, or `]` delimiter.
22
22
  * @csspart toggle - A container node's expand/collapse button (hidden, but present for row alignment, on leaf/empty nodes).
23
- * @csspart copy-button - A copy-to-clipboard button -- the top-level one (in `toolbar`) or a per-node one (only rendered when `copyable`).
23
+ * @csspart copy-button - A copy-to-clipboard button -- the top-level one (in `toolbar`, labelled "Copy JSON to clipboard") or a per-node one (only rendered when `copyable`; labelled with its own key/type, e.g. "Copy age", so assistive tech can tell rows apart).
24
24
  */
25
25
  export declare class LyraJsonViewer extends LyraElement {
26
26
  static styles: import("lit").CSSResultGroup[];
@@ -34,16 +34,33 @@ export declare class LyraJsonViewer extends LyraElement {
34
34
  copyable: boolean;
35
35
  /** Case-insensitive substring match against keys/values; matches are highlighted and their ancestors auto-expanded. */
36
36
  search: string;
37
- /** Per-path (`JSON.stringify(path)`) explicit expand/collapse, overriding the `collapsedDepth`/search defaults once a node's toggle has been used. */
37
+ /**
38
+ * Per-path (`JSON.stringify(path)`) explicit expand/collapse, overriding
39
+ * the `collapsedDepth`/search defaults once a node's toggle has been used.
40
+ * Pruned in `willUpdate()` (to the paths still reachable in the tree)
41
+ * whenever `data` changes, so a long-lived instance bound to reshaping
42
+ * data -- this component's own stated streaming use case -- doesn't
43
+ * accumulate one entry per path ever toggled for the life of the instance.
44
+ */
38
45
  private expandedOverrides;
46
+ /** Memoized result of the last `computeSearch()` walk -- see `willUpdate()`. */
47
+ private searchState;
39
48
  private isExpanded;
40
49
  private toggleNode;
41
50
  private writeClipboard;
42
51
  private copy;
43
- /** Builds the key/value-match sets and the ancestor-paths-of-a-match set that `search` drives. */
52
+ /**
53
+ * Builds the key/value-match sets, the ancestor-paths-of-a-match set that
54
+ * `search` drives, and the full set of path keys reachable in the tree
55
+ * (`paths`, used to prune `expandedOverrides` -- see `willUpdate()`).
56
+ * Guards against a self-referencing `data` the same way `renderNode()`
57
+ * does: a container value already on the current recursion path is
58
+ * treated as a leaf instead of being walked again.
59
+ */
44
60
  private computeSearch;
45
61
  private renderCopyButton;
46
62
  private renderNode;
63
+ protected willUpdate(changed: PropertyValues): void;
47
64
  render(): TemplateResult;
48
65
  }
49
66
  declare global {
@@ -15,6 +15,7 @@ const EMPTY_SEARCH = {
15
15
  keyMatches: new Set(),
16
16
  valueMatches: new Set(),
17
17
  forceExpand: new Set(),
18
+ paths: new Set(),
18
19
  };
19
20
  function isPlainContainer(value) {
20
21
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -46,7 +47,12 @@ function valueType(value) {
46
47
  function formatPrimitive(value, type) {
47
48
  switch (type) {
48
49
  case 'string':
49
- return JSON.stringify(value);
50
+ // `type` here also covers valueType()'s function/symbol/bigint
51
+ // fallback (see the comment there) -- JSON.stringify() throws a
52
+ // TypeError for a BigInt, so only an actual string gets the
53
+ // quoted/escaped treatment; everything else falls back to a plain
54
+ // String() coercion, which renders "sensibly" without throwing.
55
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
50
56
  case 'null':
51
57
  return 'null';
52
58
  case 'undefined':
@@ -77,10 +83,10 @@ function previewText(value, type, count) {
77
83
  * @csspart toolbar - The wrapper around the top-level copy button (only rendered when `copyable`).
78
84
  * @csspart tree - The wrapper around the rendered node tree.
79
85
  * @csspart key - An object property key or array index label.
80
- * @csspart value - A primitive value's text -- carries `data-type` (`string`/`number`/`boolean`/`null`/`undefined`) for per-type coloring, and `data-match` while it matches `search`.
86
+ * @csspart value - A primitive value's text -- carries `data-type` (`string`/`number`/`boolean`/`null`/`undefined`, or `circular` for a self-reference marker in place of a re-visited container's subtree) for per-type coloring, and `data-match` while it matches `search`.
81
87
  * @csspart bracket - A `{`, `}`, `[`, or `]` delimiter.
82
88
  * @csspart toggle - A container node's expand/collapse button (hidden, but present for row alignment, on leaf/empty nodes).
83
- * @csspart copy-button - A copy-to-clipboard button -- the top-level one (in `toolbar`) or a per-node one (only rendered when `copyable`).
89
+ * @csspart copy-button - A copy-to-clipboard button -- the top-level one (in `toolbar`, labelled "Copy JSON to clipboard") or a per-node one (only rendered when `copyable`; labelled with its own key/type, e.g. "Copy age", so assistive tech can tell rows apart).
84
90
  */
85
91
  export class LyraJsonViewer extends LyraElement {
86
92
  constructor() {
@@ -91,8 +97,17 @@ export class LyraJsonViewer extends LyraElement {
91
97
  this.copyable = false;
92
98
  /** Case-insensitive substring match against keys/values; matches are highlighted and their ancestors auto-expanded. */
93
99
  this.search = '';
94
- /** Per-path (`JSON.stringify(path)`) explicit expand/collapse, overriding the `collapsedDepth`/search defaults once a node's toggle has been used. */
100
+ /**
101
+ * Per-path (`JSON.stringify(path)`) explicit expand/collapse, overriding
102
+ * the `collapsedDepth`/search defaults once a node's toggle has been used.
103
+ * Pruned in `willUpdate()` (to the paths still reachable in the tree)
104
+ * whenever `data` changes, so a long-lived instance bound to reshaping
105
+ * data -- this component's own stated streaming use case -- doesn't
106
+ * accumulate one entry per path ever toggled for the life of the instance.
107
+ */
95
108
  this.expandedOverrides = new Map();
109
+ /** Memoized result of the last `computeSearch()` walk -- see `willUpdate()`. */
110
+ this.searchState = EMPTY_SEARCH;
96
111
  }
97
112
  static { this.styles = [LyraElement.styles, styles]; }
98
113
  isExpanded(pathKey, depth, forceExpand) {
@@ -123,50 +138,70 @@ export class LyraJsonViewer extends LyraElement {
123
138
  }
124
139
  }
125
140
  copy(value) {
126
- const text = value === undefined ? 'undefined' : JSON.stringify(value, null, 2);
141
+ const text = value === undefined
142
+ ? 'undefined'
143
+ : // A BigInt anywhere inside `value` (not just at the root) throws a
144
+ // TypeError from the default JSON.stringify() serializer the same
145
+ // way formatPrimitive()'s unguarded call used to -- the replacer
146
+ // downgrades it to its decimal string form instead.
147
+ JSON.stringify(value, (_key, v) => (typeof v === 'bigint' ? v.toString() : v), 2);
127
148
  this.writeClipboard(text);
128
149
  this.emit('lyra-copy', { text });
129
150
  }
130
- /** Builds the key/value-match sets and the ancestor-paths-of-a-match set that `search` drives. */
151
+ /**
152
+ * Builds the key/value-match sets, the ancestor-paths-of-a-match set that
153
+ * `search` drives, and the full set of path keys reachable in the tree
154
+ * (`paths`, used to prune `expandedOverrides` -- see `willUpdate()`).
155
+ * Guards against a self-referencing `data` the same way `renderNode()`
156
+ * does: a container value already on the current recursion path is
157
+ * treated as a leaf instead of being walked again.
158
+ */
131
159
  computeSearch() {
132
160
  const query = this.search.trim().toLowerCase();
133
- if (!query)
134
- return EMPTY_SEARCH;
135
161
  const keyMatches = new Set();
136
162
  const valueMatches = new Set();
137
163
  const forceExpand = new Set();
164
+ const paths = new Set();
165
+ const ancestors = new WeakSet();
138
166
  const markAncestors = (path) => {
139
167
  for (let i = path.length - 1; i >= 0; i--)
140
168
  forceExpand.add(JSON.stringify(path.slice(0, i)));
141
169
  };
142
170
  const walk = (value, path, keyLabel) => {
143
171
  const pathKey = JSON.stringify(path);
144
- let hit = false;
145
- if (keyLabel !== undefined && keyLabel.toLowerCase().includes(query)) {
146
- keyMatches.add(pathKey);
147
- hit = true;
148
- }
172
+ paths.add(pathKey);
149
173
  const type = valueType(value);
150
- if (type !== 'object' && type !== 'array' && formatPrimitive(value, type).toLowerCase().includes(query)) {
151
- valueMatches.add(pathKey);
152
- hit = true;
174
+ if (query) {
175
+ let hit = false;
176
+ if (keyLabel !== undefined && keyLabel.toLowerCase().includes(query)) {
177
+ keyMatches.add(pathKey);
178
+ hit = true;
179
+ }
180
+ if (type !== 'object' && type !== 'array' && formatPrimitive(value, type).toLowerCase().includes(query)) {
181
+ valueMatches.add(pathKey);
182
+ hit = true;
183
+ }
184
+ if (hit)
185
+ markAncestors(path);
186
+ }
187
+ if ((type === 'object' || type === 'array') && !ancestors.has(value)) {
188
+ ancestors.add(value);
189
+ for (const [k, v] of entriesOf(value))
190
+ walk(v, [...path, k], String(k));
191
+ ancestors.delete(value);
153
192
  }
154
- if (hit)
155
- markAncestors(path);
156
- for (const [k, v] of entriesOf(value))
157
- walk(v, [...path, k], String(k));
158
193
  };
159
194
  walk(this.data, []);
160
- return { keyMatches, valueMatches, forceExpand };
195
+ return { keyMatches, valueMatches, forceExpand, paths };
161
196
  }
162
- renderCopyButton(value) {
197
+ renderCopyButton(value, label) {
163
198
  if (!this.copyable)
164
199
  return nothing;
165
200
  return html `
166
201
  <button
167
202
  part="copy-button"
168
203
  type="button"
169
- aria-label="Copy value"
204
+ aria-label="Copy ${label ?? 'value'}"
170
205
  @click=${(e) => {
171
206
  e.stopPropagation();
172
207
  this.copy(value);
@@ -176,10 +211,15 @@ export class LyraJsonViewer extends LyraElement {
176
211
  </button>
177
212
  `;
178
213
  }
179
- renderNode(value, path, keyLabel, depth, search) {
214
+ renderNode(value, path, keyLabel, depth, search, ancestors) {
180
215
  const pathKey = JSON.stringify(path);
181
216
  const type = valueType(value);
182
- const isContainer = type === 'object' || type === 'array';
217
+ // A container value already on the current recursion path (i.e. `data`
218
+ // self-references, directly or through a longer cycle) gets rendered as
219
+ // a leaf marker instead of recursing again -- recursing would blow the
220
+ // stack instead of degrading gracefully.
221
+ const isCircular = (type === 'object' || type === 'array') && ancestors.has(value);
222
+ const isContainer = (type === 'object' || type === 'array') && !isCircular;
183
223
  const entries = isContainer ? entriesOf(value) : [];
184
224
  const hasEntries = entries.length > 0;
185
225
  const expanded = hasEntries && this.isExpanded(pathKey, depth, search.forceExpand);
@@ -187,6 +227,17 @@ export class LyraJsonViewer extends LyraElement {
187
227
  const toggleLabel = keyLabel ?? (type === 'array' ? 'array' : type === 'object' ? 'object' : 'value');
188
228
  const openBracket = type === 'array' ? '[' : '{';
189
229
  const closeBracket = type === 'array' ? ']' : '}';
230
+ // Computed eagerly -- in this same synchronous call, rather than left for
231
+ // lit-html's `repeat` directive to invoke lazily during commit -- so the
232
+ // `ancestors.add()`/`.delete()` pair below brackets exactly the values on
233
+ // the real recursive descent through *this* subtree, regardless of
234
+ // whenever lit-html itself gets around to resolving the directive.
235
+ let childRows = [];
236
+ if (isContainer && expanded) {
237
+ ancestors.add(value);
238
+ childRows = entries.map(([k, v]) => this.renderNode(v, [...path, k], String(k), depth + 1, search, ancestors));
239
+ ancestors.delete(value);
240
+ }
190
241
  const headRow = html `
191
242
  <div class="row" style=${indentStyle}>
192
243
  <button
@@ -205,18 +256,24 @@ export class LyraJsonViewer extends LyraElement {
205
256
  ? html `<span part="key" ?data-match=${search.keyMatches.has(pathKey)}>${keyLabel}</span
206
257
  ><span class="colon">:</span>`
207
258
  : nothing}
208
- ${isContainer
259
+ ${isCircular
209
260
  ? html `
210
261
  <span part="bracket">${openBracket}</span>
211
- ${hasEntries && !expanded
212
- ? html `<span class="preview">${previewText(value, type, entries.length)}</span>`
213
- : nothing}
214
- ${!expanded ? html `<span part="bracket">${closeBracket}</span>` : nothing}
262
+ <span part="value" data-type="circular">Circular reference</span>
263
+ <span part="bracket">${closeBracket}</span>
215
264
  `
216
- : html `<span part="value" data-type=${type} ?data-match=${search.valueMatches.has(pathKey)}
217
- >${formatPrimitive(value, type)}</span
218
- >`}
219
- ${this.renderCopyButton(value)}
265
+ : isContainer
266
+ ? html `
267
+ <span part="bracket">${openBracket}</span>
268
+ ${hasEntries && !expanded
269
+ ? html `<span class="preview">${previewText(value, type, entries.length)}</span>`
270
+ : nothing}
271
+ ${!expanded ? html `<span part="bracket">${closeBracket}</span>` : nothing}
272
+ `
273
+ : html `<span part="value" data-type=${type} ?data-match=${search.valueMatches.has(pathKey)}
274
+ >${formatPrimitive(value, type)}</span
275
+ >`}
276
+ ${this.renderCopyButton(value, toggleLabel)}
220
277
  </div>
221
278
  `;
222
279
  // Always returned from this one call site, with a single conditional
@@ -231,7 +288,7 @@ export class LyraJsonViewer extends LyraElement {
231
288
  ${isContainer && expanded
232
289
  ? html `
233
290
  <div class="children">
234
- ${repeat(entries, ([k]) => JSON.stringify([...path, k]), ([k, v]) => this.renderNode(v, [...path, k], String(k), depth + 1, search))}
291
+ ${repeat(entries, ([k]) => JSON.stringify([...path, k]), (_entry, i) => childRows[i])}
235
292
  </div>
236
293
  <div class="row" style=${indentStyle}>
237
294
  <span class="toggle-space" aria-hidden="true"></span>
@@ -241,8 +298,29 @@ export class LyraJsonViewer extends LyraElement {
241
298
  : nothing}
242
299
  `;
243
300
  }
301
+ willUpdate(changed) {
302
+ if (!this.hasUpdated || changed.has('data') || changed.has('search')) {
303
+ const next = this.computeSearch();
304
+ if (!this.hasUpdated || changed.has('data')) {
305
+ // A path with no entry in `next.paths` no longer exists anywhere in
306
+ // the new tree, so its override has nothing left to apply to --
307
+ // dropping it here (rather than never, as before) is what keeps a
308
+ // long-lived instance bound to reshaping data from accumulating one
309
+ // Map entry per distinct path ever toggled over its whole lifetime.
310
+ let pruned = null;
311
+ for (const key of this.expandedOverrides.keys()) {
312
+ if (!next.paths.has(key)) {
313
+ pruned ??= new Map(this.expandedOverrides);
314
+ pruned.delete(key);
315
+ }
316
+ }
317
+ if (pruned)
318
+ this.expandedOverrides = pruned;
319
+ }
320
+ this.searchState = next;
321
+ }
322
+ }
244
323
  render() {
245
- const search = this.computeSearch();
246
324
  return html `
247
325
  <div part="base" style=${this.maxHeight ? `--lyra-json-viewer-max-height:${this.maxHeight}` : nothing}>
248
326
  ${this.copyable
@@ -257,7 +335,7 @@ export class LyraJsonViewer extends LyraElement {
257
335
  </button>
258
336
  </div>`
259
337
  : nothing}
260
- <div part="tree">${this.renderNode(this.data, [], undefined, 0, search)}</div>
338
+ <div part="tree">${this.renderNode(this.data, [], undefined, 0, this.searchState, new WeakSet())}</div>
261
339
  </div>
262
340
  `;
263
341
  }
@@ -132,7 +132,8 @@ export const styles = css `
132
132
  color: var(--lyra-color-warning);
133
133
  }
134
134
  [part='value'][data-type='null'],
135
- [part='value'][data-type='undefined'] {
135
+ [part='value'][data-type='undefined'],
136
+ [part='value'][data-type='circular'] {
136
137
  color: var(--lyra-color-text-quiet);
137
138
  font-style: italic;
138
139
  }
@@ -111,6 +111,17 @@ function detectIsMac() {
111
111
  // little cost, however small) from re-detecting it on every <lyra-kbd>
112
112
  // instance or every re-render.
113
113
  const IS_MAC = detectIsMac();
114
+ // The one "is there real content" predicate shared by both the initial
115
+ // synchronous seed (willUpdate, reading light-DOM childNodes) and the
116
+ // runtime slotchange handler (reading assignedNodes) — using two different
117
+ // predicates for the same question let them disagree: a text-only check
118
+ // would seed correctly for whitespace-only text but (wrongly) treat a
119
+ // content-less icon element as empty, and a node-count-only check would
120
+ // treat *any* assigned node — including a whitespace-only text node — as
121
+ // real content. Counting every element node as real content (regardless of
122
+ // its own text) while requiring non-whitespace text from text nodes gets
123
+ // both cases right in one place.
124
+ const hasRealContent = (nodes) => Array.from(nodes).some((n) => n.nodeType === Node.ELEMENT_NODE || (n.textContent ?? '').trim().length > 0);
114
125
  /**
115
126
  * `<lyra-kbd>` — a small chip representing a keyboard shortcut, rendering
116
127
  * the platform-appropriate glyph for cross-platform modifier keys (⌘ on
@@ -162,13 +173,13 @@ export class LyraKbd extends LyraElement {
162
173
  // keys rendering for one frame before the first slotchange event.
163
174
  this.hasCustomContent = false;
164
175
  this.onSlotChange = (e) => {
165
- this.hasCustomContent = e.target.assignedNodes({ flatten: true }).length > 0;
176
+ this.hasCustomContent = hasRealContent(e.target.assignedNodes({ flatten: true }));
166
177
  };
167
178
  }
168
179
  static { this.styles = [LyraElement.styles, styles]; }
169
180
  willUpdate() {
170
181
  if (!this.hasUpdated) {
171
- this.hasCustomContent = Array.from(this.childNodes).some((n) => (n.textContent ?? '').trim().length > 0);
182
+ this.hasCustomContent = hasRealContent(this.childNodes);
172
183
  }
173
184
  }
174
185
  render() {
@@ -185,15 +196,21 @@ export class LyraKbd extends LyraElement {
185
196
  // lyra-context-meter's/lyra-chart's canvas usage of the same pattern):
186
197
  // the individual glyphs and "+" separators aren't real words, so
187
198
  // exposing them as separate accessible-tree text would read worse than
188
- // the single spelled-out aria-label below. An empty `keys` renders
189
- // nothing visible, so it's marked aria-hidden instead of exposed as a
190
- // nameless image.
199
+ // the single spelled-out aria-label below. An empty `keys` (and no
200
+ // explicit override) renders nothing visible, so it's marked
201
+ // aria-hidden instead of exposed as a nameless image.
202
+ //
203
+ // role/aria-hidden are both derived from this same `ariaLabel` value
204
+ // (rather than each independently re-deriving "is there a label" from
205
+ // tokens.length/explicitLabel) so role="img" can never be absent while
206
+ // aria-label is present — that combination is an aria-prohibited-attr
207
+ // violation (a nameless-but-labeled, role-less element).
191
208
  const ariaLabel = explicitLabel || tokens.map((t) => t.word).join('+');
192
209
  return html `
193
210
  <span
194
211
  part="base"
195
- role=${tokens.length > 0 ? 'img' : nothing}
196
- aria-hidden=${tokens.length === 0 && !explicitLabel ? 'true' : nothing}
212
+ role=${ariaLabel ? 'img' : nothing}
213
+ aria-hidden=${ariaLabel ? nothing : 'true'}
197
214
  aria-label=${ariaLabel || nothing}
198
215
  >
199
216
  ${tokens.map((t, i) => html `
@@ -50,7 +50,14 @@ export type LiveRegionMode = 'polite' | 'assertive';
50
50
  export declare class LyraLiveRegion extends LyraElement {
51
51
  static styles: import("lit").CSSResultGroup[];
52
52
  /** `polite` (role="status") waits for the user to be idle; `assertive`
53
- * (role="alert") interrupts. Mirrors native `aria-live` semantics. */
53
+ * (role="alert") interrupts. Mirrors native `aria-live` semantics.
54
+ *
55
+ * `write()` also applies `role`/`aria-live` imperatively at announce time
56
+ * (not only through this property's template binding) so a caller that
57
+ * sets `mode` and immediately force-announces in the same synchronous
58
+ * turn -- e.g. a stream-status transition -- gets the new urgency and the
59
+ * new text landing together, rather than the text beating Lit's
60
+ * re-render to the DOM. */
54
61
  mode: LiveRegionMode;
55
62
  /** Throttle window in ms — see `Announcer` in `internal/announcer.ts`. */
56
63
  throttleMs: number;
@@ -58,6 +65,7 @@ export declare class LyraLiveRegion extends LyraElement {
58
65
  private lastWritten;
59
66
  private regionEl?;
60
67
  private reannounceHandle?;
68
+ private reannounceView?;
61
69
  private pendingWrite?;
62
70
  constructor();
63
71
  protected updated(changed: PropertyValues): void;
@@ -72,6 +80,7 @@ export declare class LyraLiveRegion extends LyraElement {
72
80
  */
73
81
  announce(text: string, options?: AnnounceOptions): void;
74
82
  private write;
83
+ private cancelReannounce;
75
84
  render(): TemplateResult;
76
85
  }
77
86
  declare global {
@@ -61,7 +61,14 @@ export class LyraLiveRegion extends LyraElement {
61
61
  constructor() {
62
62
  super();
63
63
  /** `polite` (role="status") waits for the user to be idle; `assertive`
64
- * (role="alert") interrupts. Mirrors native `aria-live` semantics. */
64
+ * (role="alert") interrupts. Mirrors native `aria-live` semantics.
65
+ *
66
+ * `write()` also applies `role`/`aria-live` imperatively at announce time
67
+ * (not only through this property's template binding) so a caller that
68
+ * sets `mode` and immediately force-announces in the same synchronous
69
+ * turn -- e.g. a stream-status transition -- gets the new urgency and the
70
+ * new text landing together, rather than the text beating Lit's
71
+ * re-render to the DOM. */
65
72
  this.mode = 'polite';
66
73
  /** Throttle window in ms — see `Announcer` in `internal/announcer.ts`. */
67
74
  this.throttleMs = 500;
@@ -94,10 +101,8 @@ export class LyraLiveRegion extends LyraElement {
94
101
  disconnectedCallback() {
95
102
  super.disconnectedCallback();
96
103
  this.announcer.cancel();
97
- if (this.reannounceHandle !== undefined) {
98
- cancelAnimationFrame(this.reannounceHandle);
99
- this.reannounceHandle = undefined;
100
- }
104
+ this.pendingWrite = undefined;
105
+ this.cancelReannounce();
101
106
  }
102
107
  /**
103
108
  * Queue `text` for announcement. Calls arriving within `throttle-ms` of
@@ -115,6 +120,13 @@ export class LyraLiveRegion extends LyraElement {
115
120
  this.pendingWrite = text;
116
121
  return;
117
122
  }
123
+ // A same-text reannounce frame still in flight is now stale the moment
124
+ // any new write() call comes in -- if it fired later it would clobber
125
+ // whatever text this call is about to land with the older, already-
126
+ // superseded string. Cancel it up front so at most one reannounce is
127
+ // ever pending and it can never overwrite a newer announcement.
128
+ this.cancelReannounce();
129
+ const assertive = this.mode === 'assertive';
118
130
  if (text === this.lastWritten) {
119
131
  // Screen readers announce a live region on text-content *change* --
120
132
  // re-writing the identical string is otherwise a silent no-op to
@@ -122,17 +134,41 @@ export class LyraLiveRegion extends LyraElement {
122
134
  // (rather than in the same task) gives the DOM an actual empty ->
123
135
  // populated transition to observe instead of a same-tick clear+set
124
136
  // that can coalesce into nothing ever appearing to change.
137
+ //
138
+ // role/aria-live are set imperatively here (not left to Lit's
139
+ // template binding) so a caller that sets `mode` and immediately
140
+ // force-announces -- e.g. stream-status's announceTransition() --
141
+ // gets the new urgency in the same synchronous operation as the text,
142
+ // rather than racing Lit's async re-render.
143
+ region.setAttribute('role', assertive ? 'alert' : 'status');
144
+ region.setAttribute('aria-live', assertive ? 'assertive' : 'polite');
125
145
  region.textContent = '';
126
- this.reannounceHandle = requestAnimationFrame(() => {
146
+ const view = region.ownerDocument.defaultView;
147
+ if (!view) {
148
+ region.textContent = text;
149
+ return;
150
+ }
151
+ this.reannounceView = view;
152
+ this.reannounceHandle = view.requestAnimationFrame(() => {
127
153
  this.reannounceHandle = undefined;
154
+ this.reannounceView = undefined;
128
155
  region.textContent = text;
129
156
  });
130
157
  }
131
158
  else {
159
+ region.setAttribute('role', assertive ? 'alert' : 'status');
160
+ region.setAttribute('aria-live', assertive ? 'assertive' : 'polite');
132
161
  region.textContent = text;
133
162
  }
134
163
  this.lastWritten = text;
135
164
  }
165
+ cancelReannounce() {
166
+ if (this.reannounceHandle === undefined)
167
+ return;
168
+ this.reannounceView?.cancelAnimationFrame(this.reannounceHandle);
169
+ this.reannounceHandle = undefined;
170
+ this.reannounceView = undefined;
171
+ }
136
172
  render() {
137
173
  const assertive = this.mode === 'assertive';
138
174
  return html `<div
@@ -41,10 +41,22 @@ export interface MapMarker {
41
41
  * choropleth GeoJSON layer, plus a raw `map` escape hatch. Requires the
42
42
  * optional peer dep `maplibre-gl` (consumers also import its CSS once).
43
43
  *
44
+ * The underlying `maplibregl.Map` — and the WebGL context it opens — isn't
45
+ * constructed until this element is first visible in the viewport (tracked
46
+ * via `IntersectionObserver`), even once the `maplibre-gl` peer dependency
47
+ * has finished loading. Browsers hard-cap concurrent WebGL contexts per
48
+ * page, so a grid/dashboard of many `<lyra-map>` instances only constructs
49
+ * the ones actually on-screen instead of racing to exhaust that budget the
50
+ * instant each one mounts. `map` stays `undefined` (and `lyra-map-load`
51
+ * doesn't fire) until construction actually happens.
52
+ *
44
53
  * @customElement lyra-map
45
54
  * @event lyra-map-load - Fired once the underlying maplibregl.Map loads.
46
55
  * @event lyra-map-click - `detail: { lngLat, feature? }`.
47
- * @csspart base, container, legend, legend-swatch
56
+ * @csspart base - The map wrapper.
57
+ * @csspart container - The maplibre container.
58
+ * @csspart legend - The map legend.
59
+ * @csspart legend-swatch - A legend color swatch.
48
60
  *
49
61
  * ⚠️ The default `mapStyle` (when unset) uses OpenStreetMap's demo tile
50
62
  * server, which is not suitable for production traffic — see the
@@ -61,6 +73,8 @@ export declare class LyraMap extends LyraElement {
61
73
  markers: MapMarker[];
62
74
  /** True until the lazy-loaded `maplibre-gl` peer dependency has settled (success or failure). */
63
75
  private loading;
76
+ private visible;
77
+ private intersectionObserver?;
64
78
  private containerEl?;
65
79
  private _map?;
66
80
  private _styleLoaded;
@@ -74,6 +88,15 @@ export declare class LyraMap extends LyraElement {
74
88
  get map(): import('maplibre-gl').Map | undefined;
75
89
  connectedCallback(): void;
76
90
  disconnectedCallback(): void;
91
+ /**
92
+ * Constructs the underlying `maplibregl.Map` — called once both the
93
+ * lazy-loaded `maplibre-gl` module has resolved and this element has been
94
+ * observed intersecting the viewport, whichever settles last. Idempotent
95
+ * (a no-op once `_map` already exists), so it's safe to call from both the
96
+ * `connectedCallback()` load path and the visibility path in `updated()`
97
+ * below without risking a double construction.
98
+ */
99
+ private tryConstructMap;
77
100
  protected updated(changed: PropertyValues): void;
78
101
  private applyChoropleth;
79
102
  /** Removes whatever choropleth layer/source is currently applied, if any. */