@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
@@ -9,74 +9,74 @@ import { property, state } from 'lit/decorators.js';
9
9
  import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { defineElement } from '../../internal/prefix.js';
11
11
  import { lockScroll } from '../../internal/scroll-lock.js';
12
+ import { activateOverlay } from '../../internal/overlay-manager.js';
12
13
  import { nextId, srOnly } from '../../internal/a11y.js';
14
+ import { closeIcon } from '../../internal/icons.js';
13
15
  import { styles } from './dialog.styles.js';
14
- const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
15
16
  const HEADING_SELECTOR = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
16
- // Shadow-piercing so a slotted custom element's real focusable target (e.g.
17
- // an <input> inside its own shadow root) is found even though the host tag
18
- // itself doesn't match FOCUSABLE_SELECTOR. Deliberately duplicated from
19
- // lyra-widget's identical helper rather than imported/shared -- widget's
20
- // fullscreen mode is a separate feature and this component must not depend
21
- // on it (see the module doc in dialog.ts's authoring notes).
22
- function collectFocusable(el) {
23
- const result = [];
24
- if (el.matches(FOCUSABLE_SELECTOR)) {
25
- result.push(el);
26
- }
27
- if (el instanceof HTMLSlotElement) {
28
- for (const assigned of el.assignedElements({ flatten: true })) {
29
- result.push(...collectFocusable(assigned));
30
- }
31
- return result;
32
- }
33
- const container = el.shadowRoot ?? el;
34
- for (const child of Array.from(container.children)) {
35
- result.push(...collectFocusable(child));
36
- }
37
- return result;
38
- }
39
- /** Whether `el` is actually laid out/paintable -- `checkVisibility()` (falling
40
- * back to `getClientRects().length` where unsupported) correctly follows
41
- * flattened-tree/slot assignment, unlike `offsetParent`. Mirrors
42
- * lyra-widget's identical helper. */
43
- function isRendered(el) {
44
- return el.checkVisibility ? el.checkVisibility() : el.getClientRects().length > 0;
45
- }
46
17
  /**
47
18
  * `<lyra-dialog>` — a general-purpose modal/overlay. `role="dialog"`,
48
19
  * focus-trapped while open, dismissible via Escape or a backdrop click, and
49
- * scroll-locks the document for as long as it's open. Chrome stays minimal
50
- * no built-in title bar or close button; a consumer supplies a heading and
51
- * any close affordance itself via the default/`footer` slots.
20
+ * scroll-locks the document for as long as it's open. Chrome stays minimal by
21
+ * default — no built-in title bar or close button; a consumer supplies a
22
+ * heading and any close affordance itself via the default/`footer` slots.
23
+ * `heading`/`closable` are an opt-in convenience for the common case where
24
+ * hand-building that chrome isn't worth it (see below).
52
25
  *
53
- * Accessible name: if a heading element (`h1`–`h6` or `[role="heading"]`) is
54
- * a *direct child* (not inside `slot="footer"`), its text content becomes
55
- * `aria-label` on the panel. Otherwise, when `label` is set, an invisible
56
- * (`.sr-only`, exposed as the `label` part) element carrying that text is
57
- * rendered inside the panel and `aria-labelledby` points at it instead.
58
- * Either way `label` itself never renders visible chrome — a slotted heading
59
- * is what a sighted user sees; `::part(label)` can be restyled to make the
60
- * sr-only text visible too, if a consumer wants that instead of slotting a
61
- * heading.
26
+ * Accessible name / visible header, in priority order:
27
+ * 1. If a heading element (`h1`–`h6` or `[role="heading"]`) is a *direct
28
+ * child* (not inside `slot="footer"`), its text content becomes
29
+ * `aria-label` on the panel unchanged, and takes priority over `heading`
30
+ * below so an existing consumer that already slots its own heading keeps
31
+ * rendering it exactly as before.
32
+ * 2. Otherwise, when `heading` is set, a visible header row (`part="header"`)
33
+ * renders containing that text (`part="heading"`), which becomes the
34
+ * `aria-labelledby` target.
35
+ * 3. Otherwise, when `label` is set, an invisible (`.sr-only`, exposed as the
36
+ * `label` part) element carrying that text is rendered inside the panel
37
+ * and `aria-labelledby` points at it instead.
38
+ * Only one of cases 2/3 ever renders at a time, so exactly one element ever
39
+ * claims `aria-labelledby`. `label` itself never renders visible chrome on
40
+ * its own — `::part(label)` can be restyled to make the sr-only text visible,
41
+ * or `heading` can be set instead, if a consumer wants visible chrome without
42
+ * slotting a real heading element.
62
43
  *
63
- * The heading case deliberately uses `aria-label` (a copied string) rather
64
- * than `aria-labelledby` pointing at the heading's `id`: the heading is
44
+ * The slotted-heading case deliberately uses `aria-label` (a copied string)
45
+ * rather than `aria-labelledby` pointing at the heading's `id`: the heading is
65
46
  * *light-DOM* content while `[part="panel"]` lives in this element's
66
47
  * *shadow* tree, and an ID-reference attribute can't resolve across that
67
48
  * boundary (verified against axe's `aria-dialog-name` rule) — unlike the
68
- * `label`-prop case above, where the sr-only element is rendered inside the
69
- * same shadow root it labels, so `aria-labelledby` there is safe.
49
+ * `heading`/`label`-prop cases above, where the target element is rendered
50
+ * inside the same shadow root it labels, so `aria-labelledby` there is safe.
51
+ *
52
+ * `closable` renders a close (X) button in the header row (creating one, with
53
+ * no heading text, if `heading` is unset) that closes the dialog via the same
54
+ * `close()` path as Escape/backdrop-dismiss, with reason `'close-button'`.
55
+ *
56
+ * Stacking: opening one `<lyra-dialog>` while another is already open (e.g. a
57
+ * `confirm()` launched from within an already-open dialog) is supported --
58
+ * Escape and the Tab focus trap only ever act on the topmost open dialog, so
59
+ * dialogs beneath it stay open and untouched until the one on top closes.
70
60
  *
71
61
  * @customElement lyra-dialog
72
62
  * @slot - The dialog body.
73
63
  * @slot footer - Action buttons, rendered in a bottom row.
74
64
  * @event lyra-dialog-close - `detail: DialogCloseReason`. Fired whenever the
75
- * dialog is dismissed via Escape, a backdrop click, or a `close()` call.
65
+ * dialog is dismissed via Escape, a backdrop click, the built-in close
66
+ * button (`closable`), a `close()` call, or (with reason `'unmount'`)
67
+ * removal from the DOM by anything else while still open.
76
68
  * @csspart backdrop - The full-viewport scrim behind the panel.
77
- * @csspart panel - The dialog panel itself (`role="dialog"` while open).
69
+ * @csspart panel - The dialog panel itself (`role="dialog"` while open). Its
70
+ * max-inline-size is controlled by the `--lyra-dialog-max-width` custom
71
+ * property (default `32rem`).
72
+ * @csspart header - The header row, rendered when `heading` is set (and no
73
+ * heading is slotted) and/or `closable` is `true`.
74
+ * @csspart heading - The visible `heading`-text element inside `header`,
75
+ * rendered only when `heading` is set and no heading is slotted.
76
+ * @csspart close-button - The built-in close button, rendered inside `header`
77
+ * only when `closable` is `true`.
78
78
  * @csspart label - The invisible `label`-text element used for
79
- * `aria-labelledby` when no heading is slotted.
79
+ * `aria-labelledby` when no heading is slotted and `heading` is unset.
80
80
  * @csspart body - The wrapper around the default slot.
81
81
  * @csspart footer - The wrapper around the `footer` slot.
82
82
  */
@@ -87,8 +87,14 @@ export class LyraDialog extends LyraElement {
87
87
  this.open = false;
88
88
  /** Accessible name used when no heading is slotted — see the class doc for the full fallback order. */
89
89
  this.label = '';
90
+ /** Renders a built-in close (X) button in the header row (creating one,
91
+ * with no heading text, if `heading` is unset), wired to the same
92
+ * `close()` path Escape/backdrop-dismiss already use, with reason
93
+ * `'close-button'`. */
94
+ this.closable = false;
90
95
  this.hasFooterSlot = false;
91
96
  this.srLabelId = nextId('dialog-label');
97
+ this.headingId = nextId('dialog-heading');
92
98
  this.onDefaultSlotChange = () => {
93
99
  this.detectHeading();
94
100
  };
@@ -96,34 +102,10 @@ export class LyraDialog extends LyraElement {
96
102
  this.hasFooterSlot = e.target.assignedElements({ flatten: true }).length > 0;
97
103
  };
98
104
  this.onBackdropClick = () => {
99
- this.close('backdrop');
105
+ this.overlay?.dismissBackdrop();
100
106
  };
101
- this.onDocKeyDown = (e) => {
102
- if (!this.open)
103
- return;
104
- if (e.key === 'Escape') {
105
- e.preventDefault();
106
- this.close('escape');
107
- return;
108
- }
109
- if (e.key !== 'Tab')
110
- return;
111
- const focusable = this.getFocusableElements();
112
- if (focusable.length === 0) {
113
- e.preventDefault();
114
- return;
115
- }
116
- const first = focusable[0];
117
- const last = focusable[focusable.length - 1];
118
- const active = this.getActiveElement();
119
- if (e.shiftKey && active === first) {
120
- e.preventDefault();
121
- last.focus();
122
- }
123
- else if (!e.shiftKey && active === last) {
124
- e.preventDefault();
125
- first.focus();
126
- }
107
+ this.onCloseButtonClick = () => {
108
+ this.close('close-button');
127
109
  };
128
110
  }
129
111
  static { this.styles = [LyraElement.styles, srOnly, styles]; }
@@ -134,35 +116,18 @@ export class LyraDialog extends LyraElement {
134
116
  }
135
117
  if (changed.has('open')) {
136
118
  if (this.open) {
137
- // Captured here (before render) rather than from a click event on
138
- // some specific internal control -- unlike lyra-widget's fullscreen
139
- // toggle, a dialog's trigger typically lives *outside* the
140
- // component entirely, so "whatever had focus right before open"
141
- // is the only generally correct definition of "the trigger".
142
- const active = this.getActiveElement();
143
- this.lastTrigger = active instanceof HTMLElement ? active : undefined;
144
- this.releaseScrollLock = lockScroll();
145
- document.addEventListener('keydown', this.onDocKeyDown);
119
+ this.activateOverlay();
146
120
  }
147
121
  else {
148
- this.releaseScrollLock?.();
149
- this.releaseScrollLock = undefined;
150
- document.removeEventListener('keydown', this.onDocKeyDown);
122
+ this.deactivateOverlay();
151
123
  }
152
124
  }
153
125
  }
154
- // Runs after render (not willUpdate) so [part="panel"] has already landed
155
- // in the DOM before the fallback .focus() call below can rely on it --
156
- // mirrors lyra-widget's identical ordering rationale for its fullscreen mode.
126
+ // Runs after render so the manager can resolve the panel and its composed
127
+ // focus targets, including controls projected through either slot.
157
128
  updated(changed) {
158
129
  if (changed.has('open') && this.open) {
159
- const first = this.getFocusableElements()[0];
160
- if (first) {
161
- first.focus();
162
- }
163
- else {
164
- this.shadowRoot?.querySelector('[part="panel"]')?.focus();
165
- }
130
+ this.overlay?.focusInitial();
166
131
  }
167
132
  }
168
133
  connectedCallback() {
@@ -171,16 +136,40 @@ export class LyraDialog extends LyraElement {
171
136
  // instance) fires disconnectedCallback then connectedCallback
172
137
  // synchronously with no update in between, so willUpdate never reruns to
173
138
  // notice `open` is still true -- restore the scroll lock/trap it dropped.
174
- if (this.hasUpdated && this.open && !this.releaseScrollLock) {
175
- this.releaseScrollLock = lockScroll();
176
- document.addEventListener('keydown', this.onDocKeyDown);
139
+ if (this.hasUpdated && this.open) {
140
+ if (this.overlay?.isActive()) {
141
+ this.overlay.resume();
142
+ this.releaseScrollLock ??= lockScroll(this.ownerDocument);
143
+ }
144
+ else {
145
+ this.activateOverlay();
146
+ }
147
+ queueMicrotask(() => this.overlay?.focusInitial());
177
148
  }
178
149
  }
179
150
  disconnectedCallback() {
180
151
  super.disconnectedCallback();
181
152
  this.releaseScrollLock?.();
182
153
  this.releaseScrollLock = undefined;
183
- document.removeEventListener('keydown', this.onDocKeyDown);
154
+ this.overlay?.suspend();
155
+ if (this.open) {
156
+ // A reparent (drag-and-drop moving this same element instance to a new
157
+ // parent) fires disconnectedCallback immediately followed by a
158
+ // synchronous connectedCallback, with no turn of the event loop in
159
+ // between -- deferring this check a microtask lets that case
160
+ // short-circuit here once isConnected is true again, so only a genuine
161
+ // removal (the element still disconnected once microtasks flush) ever
162
+ // reaches the assignment below. Without this, removing an open dialog
163
+ // any way other than its own close() (a consumer's own DOM cleanup, a
164
+ // parent re-render that drops it, etc.) never fires
165
+ // `lyra-dialog-close`, so e.g. confirm()'s returned promise hangs forever.
166
+ queueMicrotask(() => {
167
+ if (!this.isConnected && this.open) {
168
+ this.open = false;
169
+ this.emit('lyra-dialog-close', 'unmount');
170
+ }
171
+ });
172
+ }
184
173
  }
185
174
  // Only direct children are scanned -- a heading nested several layers deep
186
175
  // (or inside a slotted custom element's own shadow root) is left to the
@@ -197,43 +186,43 @@ export class LyraDialog extends LyraElement {
197
186
  /**
198
187
  * Close the dialog and return focus to whatever had it before the dialog
199
188
  * opened. `reason` is forwarded as the `lyra-dialog-close` detail --
200
- * built-in triggers pass `'escape'`/`'backdrop'`; a consumer's own close
201
- * affordance (e.g. a footer Cancel button) should call this directly with
202
- * its own reason string, so every dismissal path funnels through the same
203
- * event instead of the consumer having to also toggle `open` itself.
189
+ * built-in triggers pass `'escape'`/`'backdrop'`/`'close-button'`; a
190
+ * consumer's own close affordance (e.g. a footer Cancel button) should
191
+ * call this directly with its own reason string, so every dismissal path
192
+ * funnels through the same event instead of the consumer having to also
193
+ * toggle `open` itself.
204
194
  */
205
195
  close(reason = 'api') {
206
196
  if (!this.open)
207
197
  return;
208
198
  this.open = false;
209
199
  this.emit('lyra-dialog-close', reason);
210
- this.lastTrigger?.focus();
211
200
  }
212
- // Bounds Tab/Shift+Tab to the panel while open. Order follows the default
213
- // (body) slot, then the footer slot -- the same order the flattened tree
214
- // already tabs through.
215
- getFocusableElements() {
216
- const root = this.shadowRoot;
217
- if (!root)
218
- return [];
219
- const fromSlot = (selector) => {
220
- const slot = root.querySelector(selector);
221
- return slot ? slot.assignedElements({ flatten: true }).flatMap(collectFocusable) : [];
222
- };
223
- return [...fromSlot('slot:not([name])'), ...fromSlot('slot[name="footer"]')].filter(isRendered);
201
+ activateOverlay() {
202
+ if (this.overlay?.isActive())
203
+ return;
204
+ this.releaseScrollLock ??= lockScroll(this.ownerDocument);
205
+ this.overlay = activateOverlay({
206
+ host: this,
207
+ panel: () => this.shadowRoot?.querySelector('[part="panel"]') ?? null,
208
+ onEscape: () => this.close('escape'),
209
+ onBackdrop: () => this.close('backdrop'),
210
+ });
224
211
  }
225
- getActiveElement() {
226
- let active = document.activeElement;
227
- while (active) {
228
- const inner = active.shadowRoot?.activeElement ?? null;
229
- if (!inner)
230
- break;
231
- active = inner;
232
- }
233
- return active;
212
+ deactivateOverlay() {
213
+ this.releaseScrollLock?.();
214
+ this.releaseScrollLock = undefined;
215
+ this.overlay?.deactivate();
216
+ this.overlay = undefined;
234
217
  }
235
218
  render() {
236
- const useSrLabel = !this.headingText && this.label.length > 0;
219
+ // Priority order (see class doc): a slotted heading always wins; only
220
+ // when there isn't one does `heading` get a turn, then `label`'s
221
+ // sr-only fallback -- never more than one of the two below claims
222
+ // aria-labelledby for the same panel.
223
+ const useHeadingProp = !this.headingText && !!this.heading;
224
+ const useSrLabel = !this.headingText && !useHeadingProp && this.label.length > 0;
225
+ const showHeader = useHeadingProp || this.closable;
237
226
  return html `
238
227
  <div part="backdrop" @click=${this.onBackdropClick}></div>
239
228
  <div
@@ -241,12 +230,33 @@ export class LyraDialog extends LyraElement {
241
230
  role=${this.open ? 'dialog' : nothing}
242
231
  aria-modal=${this.open ? 'true' : nothing}
243
232
  aria-label=${this.headingText ?? nothing}
244
- aria-labelledby=${useSrLabel ? this.srLabelId : nothing}
233
+ aria-labelledby=${useHeadingProp ? this.headingId : useSrLabel ? this.srLabelId : nothing}
245
234
  tabindex="-1"
246
235
  >
247
236
  ${useSrLabel
248
237
  ? html `<span id=${this.srLabelId} part="label" class="sr-only">${this.label}</span>`
249
238
  : nothing}
239
+ ${showHeader
240
+ ? html `
241
+ <div part="header">
242
+ ${useHeadingProp
243
+ ? html `<span id=${this.headingId} part="heading">${this.heading}</span>`
244
+ : nothing}
245
+ ${this.closable
246
+ ? html `
247
+ <button
248
+ part="close-button"
249
+ type="button"
250
+ aria-label="Close"
251
+ @click=${this.onCloseButtonClick}
252
+ >
253
+ ${closeIcon()}
254
+ </button>
255
+ `
256
+ : nothing}
257
+ </div>
258
+ `
259
+ : nothing}
250
260
  <div part="body">
251
261
  <slot @slotchange=${this.onDefaultSlotChange}></slot>
252
262
  </div>
@@ -263,6 +273,12 @@ __decorate([
263
273
  __decorate([
264
274
  property()
265
275
  ], LyraDialog.prototype, "label", void 0);
276
+ __decorate([
277
+ property()
278
+ ], LyraDialog.prototype, "heading", void 0);
279
+ __decorate([
280
+ property({ type: Boolean, attribute: 'closable' })
281
+ ], LyraDialog.prototype, "closable", void 0);
266
282
  __decorate([
267
283
  state()
268
284
  ], LyraDialog.prototype, "hasFooterSlot", void 0);
@@ -9,7 +9,7 @@ export const styles = css `
9
9
  display: none;
10
10
  position: fixed;
11
11
  inset: 0;
12
- z-index: 1000;
12
+ z-index: var(--lyra-overlay-stack-index, 1000);
13
13
  align-items: center;
14
14
  justify-content: center;
15
15
  padding: var(--lyra-space-l);
@@ -26,7 +26,10 @@ export const styles = css `
26
26
  position: relative;
27
27
  display: flex;
28
28
  flex-direction: column;
29
- max-inline-size: min(32rem, 100%);
29
+ /* --lyra-dialog-max-width lets a consumer widen/narrow the panel per
30
+ instance (e.g. inline on the host) without overriding the whole rule --
31
+ same convention as lyra-media-card's --lyra-media-card-max-height. */
32
+ max-inline-size: min(var(--lyra-dialog-max-width, 32rem), 100%);
30
33
  max-block-size: 100%;
31
34
  background: var(--lyra-color-surface);
32
35
  border: 1px solid var(--lyra-color-border);
@@ -34,6 +37,47 @@ export const styles = css `
34
37
  box-shadow: var(--lyra-shadow);
35
38
  overflow: auto;
36
39
  }
40
+ [part='header'] {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: var(--lyra-space-s);
44
+ padding: var(--lyra-space-m) var(--lyra-space-l);
45
+ border-block-end: 1px solid var(--lyra-color-border);
46
+ }
47
+ [part='heading'] {
48
+ flex: 1 1 auto;
49
+ min-inline-size: 0;
50
+ margin: 0;
51
+ font-weight: 600;
52
+ overflow: hidden;
53
+ text-overflow: ellipsis;
54
+ white-space: nowrap;
55
+ }
56
+ [part='close-button'] {
57
+ flex: 0 0 auto;
58
+ margin-inline-start: auto;
59
+ display: inline-flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ min-inline-size: var(--lyra-icon-button-size);
63
+ min-block-size: var(--lyra-icon-button-size);
64
+ border: none;
65
+ background: transparent;
66
+ color: var(--lyra-color-text-quiet);
67
+ border-radius: var(--lyra-radius);
68
+ cursor: pointer;
69
+ }
70
+ [part='close-button']:hover {
71
+ background: var(--lyra-color-brand-quiet);
72
+ color: var(--lyra-color-brand);
73
+ }
74
+ [part='close-button']:focus-visible {
75
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
76
+ outline-offset: var(--lyra-focus-ring-offset);
77
+ }
78
+ [part='close-button'] svg {
79
+ display: block;
80
+ }
37
81
  [part='body'] {
38
82
  padding: var(--lyra-space-l);
39
83
  overflow: auto;
@@ -87,8 +87,16 @@ export declare class LyraDockPanel extends LyraElement {
87
87
  resizable: boolean;
88
88
  private drag;
89
89
  private readonly contentId;
90
+ private containerResizeObserver?;
90
91
  connectedCallback(): void;
91
92
  disconnectedCallback(): void;
93
+ /** Creates (idempotently) and (re-)observes the containing element with a
94
+ * `ResizeObserver` that just requests a re-render on every callback --
95
+ * `containerPx()`/`currentSizePx()` already read the live DOM on every
96
+ * render, so there's no separate cached value to update here, only the
97
+ * render that was otherwise never being triggered. A no-op when not yet
98
+ * connected (no `parentElement`). */
99
+ private armContainerResizeObserver;
92
100
  protected willUpdate(changed: PropertyValues): void;
93
101
  /** `'inline'` for the `start`/`end` edges (resizing changes `inline-size`), `'block'` for
94
102
  * `top`/`bottom` (resizing changes `block-size`). */
@@ -177,6 +177,7 @@ export class LyraDockPanel extends LyraElement {
177
177
  connectedCallback() {
178
178
  super.connectedCallback();
179
179
  this.applyHostSize();
180
+ this.armContainerResizeObserver();
180
181
  }
181
182
  disconnectedCallback() {
182
183
  super.disconnectedCallback();
@@ -185,6 +186,19 @@ export class LyraDockPanel extends LyraElement {
185
186
  window.removeEventListener('pointerup', this.onPointerUp);
186
187
  window.removeEventListener('pointercancel', this.onPointerUp);
187
188
  window.removeEventListener('lostpointercapture', this.onPointerUp);
189
+ this.containerResizeObserver?.disconnect();
190
+ }
191
+ /** Creates (idempotently) and (re-)observes the containing element with a
192
+ * `ResizeObserver` that just requests a re-render on every callback --
193
+ * `containerPx()`/`currentSizePx()` already read the live DOM on every
194
+ * render, so there's no separate cached value to update here, only the
195
+ * render that was otherwise never being triggered. A no-op when not yet
196
+ * connected (no `parentElement`). */
197
+ armContainerResizeObserver() {
198
+ if (!this.parentElement)
199
+ return;
200
+ this.containerResizeObserver ??= new ResizeObserver(() => this.requestUpdate());
201
+ this.containerResizeObserver.observe(this.parentElement);
188
202
  }
189
203
  // Applied in willUpdate (before render), not updated (after render): the
190
204
  // handle's aria-valuenow is computed during render from the host's own
@@ -275,7 +289,7 @@ export class LyraDockPanel extends LyraElement {
275
289
  if (this.edge === 'bottom')
276
290
  return this.collapsed ? -90 : 90;
277
291
  const pinnedPhysicalEnd = this.edge === 'end' ? !isRtl(this) : isRtl(this);
278
- return this.collapsed === pinnedPhysicalEnd ? 0 : 180;
292
+ return this.collapsed !== pinnedPhysicalEnd ? 0 : 180;
279
293
  }
280
294
  handleTemplate() {
281
295
  if (!this.resizable || this.collapsed)
@@ -19,11 +19,25 @@ export const styles = css `
19
19
  :host([edge='start']),
20
20
  :host([edge='end']) {
21
21
  block-size: 100%;
22
- min-inline-size: var(--lyra-dock-panel-collapsed-size);
23
22
  }
24
23
  :host([edge='top']),
25
24
  :host([edge='bottom']) {
26
25
  inline-size: 100%;
26
+ }
27
+ /* The collapsed-rail floor only applies once actually collapsed -- scoped
28
+ here rather than to the bare [edge] selectors above, so it can never
29
+ override a smaller explicit min-size (resolved in JS by
30
+ resolveBoundsPx()) while expanded. An unconditional floor here would
31
+ silently win over a min-size below the rail token's width, since a CSS
32
+ min-inline-size/min-block-size always wins over an inline size style
33
+ regardless of what value applySize() computed and announced via
34
+ aria-valuenow. */
35
+ :host([edge='start'][collapsed]),
36
+ :host([edge='end'][collapsed]) {
37
+ min-inline-size: var(--lyra-dock-panel-collapsed-size);
38
+ }
39
+ :host([edge='top'][collapsed]),
40
+ :host([edge='bottom'][collapsed]) {
27
41
  min-block-size: var(--lyra-dock-panel-collapsed-size);
28
42
  }
29
43
 
@@ -40,6 +40,13 @@ export type DocumentPreviewStatus = 'idle' | 'converting' | 'ready' | 'error';
40
40
  * that a *rendering* failure and a host's own state machine are different
41
41
  * concerns.
42
42
  *
43
+ * Every `src` is validated for the DOM/API sink that consumes it. Text
44
+ * fetches and image sources allow relative URLs plus `http:`, `https:`,
45
+ * `blob:`, and `data:`. Download links deliberately exclude `data:` because
46
+ * following a `data:text/html` URL can create an active document. Unsafe or
47
+ * malformed URLs never reach `fetch()`, an image `src`, or an anchor `href`;
48
+ * they render a non-interactive fallback/error instead.
49
+ *
43
50
  * Accessibility: the `"converting"` state (no numeric `progress`) is a
44
51
  * `role="status"` region wrapping a visually-hidden "Converting document…"
45
52
  * string. This is a *plain* static region, not routed through
@@ -61,10 +68,10 @@ export type DocumentPreviewStatus = 'idle' | 'converting' | 'ready' | 'error';
61
68
  * component doesn't natively support (i.e. whenever format dispatch would
62
69
  * otherwise fall through to "generic"). Ignored while `mime-type` resolves
63
70
  * to `text`/`image` dispatch, or while `status` is `"converting"`/`"error"`.
64
- * @event lyra-download - `detail: { src, filename }` — fired when the
65
- * generic-download fallback's link is activated. The browser download
66
- * itself needs no JS (a plain `<a download>` handles it); this is purely
67
- * for a host that wants to observe/log the download.
71
+ * @event lyra-download - `detail: { src, filename }` — fired when the safe
72
+ * generic-download fallback link is activated. The browser download itself
73
+ * needs no JS (a plain `<a download>` handles it); this is purely for a host
74
+ * that wants to observe/log the download.
68
75
  * @event lyra-render-error - `detail: { error }` — fired when this
69
76
  * component's own `text/*`/`application/json` `fetch(src)` fails. Distinct
70
77
  * from `status="error"`, which is entirely host-driven (see the class
@@ -75,13 +82,14 @@ export type DocumentPreviewStatus = 'idle' | 'converting' | 'ready' | 'error';
75
82
  * @csspart body - The wrapper around whichever content is currently showing (text/image preview, the generic fallback, the spinner, or the error message).
76
83
  * @csspart spinner - The converting/loading indicator — indeterminate (`role="status"`) or, once numeric progress is known, a determinate `role="progressbar"`. Used both for `status="converting"` and for this component's own in-flight text fetch.
77
84
  * @csspart error - The error message region (`role="alert"`) — used both for `status="error"` and for a failed text fetch.
78
- * @csspart download-link - The `<a download>` affordance in the generic fallback. Only rendered when `src` is set.
85
+ * @csspart download-link - The `<a download>` affordance in the generic fallback. Only rendered when `src` is set and safe for link navigation.
79
86
  */
80
87
  export declare class LyraDocumentPreview extends LyraElement {
81
88
  static styles: import("lit").CSSResultGroup[];
82
89
  /** URL to fetch (for `text`/`application/json`) or display (`image`, or as
83
- * the generic fallback's download `href`). Optional gracefully absent
84
- * while, e.g., a conversion is still in progress. */
90
+ * the generic fallback's download `href`). The value is validated against
91
+ * a sink-specific scheme allowlist before use. Optional — gracefully
92
+ * absent while, e.g., a conversion is still in progress. */
85
93
  src: string;
86
94
  /** Drives format dispatch — see the class doc. */
87
95
  mimeType: string;