@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
@@ -7,7 +7,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { html, nothing } from 'lit';
8
8
  import { property, state, query } from 'lit/decorators.js';
9
9
  import { LyraElement } from '../../internal/lyra-element.js';
10
+ import { activateOverlay } from '../../internal/overlay-manager.js';
10
11
  import { defineElement } from '../../internal/prefix.js';
12
+ import { lockScroll } from '../../internal/scroll-lock.js';
11
13
  import { isRtl } from '../../internal/rtl.js';
12
14
  import { styles } from './split.styles.js';
13
15
  const KEYBOARD_STEP = 2;
@@ -32,16 +34,48 @@ const NO_MAX_PX = 1_000_000;
32
34
  * icon-only/collapsed UI of its own; slotted content is expected to adapt to
33
35
  * its own clamped width (e.g. via its own container query).
34
36
  *
37
+ * `collapseState` is a public accessor with force/auto semantics mirroring
38
+ * `<lyra-app-rail>`'s `mode`: it's normally derived automatically from the
39
+ * measured container width (via a `ResizeObserver` on `[part="base"]`)
40
+ * whenever it crosses `railBreakpoint`/`floatBreakpoint`, but assigning it a
41
+ * concrete `'wide'`/`'rail'`/`'floating'` value pins it there and stops that
42
+ * automatic tracking — useful for a consumer-driven toggle (e.g. a button
43
+ * that forces `'floating'` regardless of width). Assigning the write-only
44
+ * `'auto'` sentinel releases the pin and immediately re-derives the state
45
+ * from the current measured width, resuming automatic tracking. `'auto'` is
46
+ * never a value this getter returns.
47
+ *
48
+ * The `'floating'` state is a hidden-by-default drawer, gated by `open`
49
+ * (mirrors `<lyra-app-rail>`'s mobile overlay): while `collapseState` is
50
+ * `'floating'` and `open` is `false` (the default), the collapsing panel
51
+ * renders nothing — `hidden`, out of the accessibility tree, not just
52
+ * visually hidden — instead of the always-visible overlay card this state
53
+ * rendered before `open` existed. Setting `open = true` reveals it as a
54
+ * focus-trapped floating panel with a `[part="backdrop"]` scrim; Escape or a
55
+ * backdrop click set `open` back to `false`. `open` is preserved (not reset)
56
+ * while `collapseState` isn't `'floating'`, but no drawer chrome renders
57
+ * until it is again — except that leaving `'floating'` while `open` is
58
+ * `true` (a breakpoint crossing back to `'wide'`/`'rail'`, or a forced
59
+ * reassignment) also closes it, the same way `<lyra-app-rail>` closes its
60
+ * mobile overlay when leaving `'mobile'` while open.
61
+ *
35
62
  * @customElement lyra-split
36
63
  * @event lyra-resize - `detail: { sizes }`, fired on every drag step/release
37
64
  * and every keyboard step.
38
65
  * @event lyra-split-collapse-change - `detail: { state }` (`SplitCollapseChangeDetail`),
39
66
  * fired whenever the responsive `collapseState` actually transitions between
40
- * `'wide'`/`'rail'`/`'floating'`. Only relevant when `collapse` isn't `"none"`;
41
- * never fires otherwise.
67
+ * `'wide'`/`'rail'`/`'floating'` whether from a breakpoint crossing or an
68
+ * explicit `collapseState` assignment. Only relevant when `collapse` isn't
69
+ * `"none"`; never fires otherwise. Not fired for a redundant reassignment
70
+ * to the state already in effect.
71
+ * @event lyra-split-constraints-invalid - `detail: SplitConstraintIssueDetail`,
72
+ * fired once when the configured panel minimums/maximums cannot describe a
73
+ * layout that fits the track. The splitter rejects that infeasible set for
74
+ * interaction and falls back to a normalized percent minimum.
42
75
  * @slot - Panels to arrange side by side (or stacked, when `orientation="vertical"`); each direct child becomes one resizable panel.
43
76
  * @csspart base - The flex layout wrapper (`position: relative`, so the `'floating'` collapse state can anchor to it).
44
77
  * @csspart divider - Each divider between two panels; carries `aria-disabled="true"` and is drag/keyboard-inert while its adjacent panel is collapsed (`'rail'`/`'floating'`).
78
+ * @csspart backdrop - The `'floating'` drawer's scrim. Only rendered while `collapseState === 'floating'` and `open`.
45
79
  */
46
80
  export class LyraSplit extends LyraElement {
47
81
  constructor() {
@@ -73,10 +107,26 @@ export class LyraSplit extends LyraElement {
73
107
  /** Container width (px) below which the collapsing pane instead becomes an
74
108
  * absolutely-positioned overlay above the other pane (`'floating'` state). */
75
109
  this.floatBreakpoint = 400;
110
+ /** Whether the `'floating'` collapse state's drawer is shown. Only
111
+ * meaningful while `collapseState` is `'floating'` — the value is
112
+ * preserved (not reset) while another state is active, but no drawer
113
+ * chrome renders until `collapseState` is `'floating'` again. Defaults to
114
+ * `false`: the collapsing pane renders nothing while floating until a
115
+ * consumer opts in by setting this (or it's forced open programmatically)
116
+ * — see the class doc. */
117
+ this.open = false;
76
118
  this.panelCount = 0;
77
- /** The collapsing pane's current responsive state; always `'wide'` (a
78
- * no-op) while `collapse === 'none'`. */
79
- this.collapseState = 'wide';
119
+ this._collapseState = 'wide';
120
+ // Whether ResizeObserver-driven measurement is currently ignored because a
121
+ // consumer forced a specific collapseState -- see the accessor doc.
122
+ this._forced = false;
123
+ // Derived from collapseState === 'floating' && open -- tracked as its own
124
+ // field (rather than recomputed inline everywhere) so willUpdate can detect
125
+ // the specific false->true/true->false transition regardless of which of
126
+ // the two source properties changed (mirrors lyra-app-rail's
127
+ // `overlayActive`).
128
+ this.overlayActive = false;
129
+ this.justOpened = false;
80
130
  // Keyed by pointerId so an interrupted or concurrent (multi-touch) drag on
81
131
  // one divider never reads or clobbers another pointer's drag state.
82
132
  this.drags = new Map();
@@ -86,10 +136,14 @@ export class LyraSplit extends LyraElement {
86
136
  // lyra-split-authored styles cleared instead of carrying them around stale
87
137
  // forever.
88
138
  this.stylizedPanels = new Set();
139
+ this.constraintIssueKey = '';
89
140
  this.onSlotChange = () => {
90
141
  this.panelCount = this.children.length;
91
142
  this.ensureSizes();
92
143
  };
144
+ this.onBackdropClick = () => {
145
+ this.overlayHandle?.dismissBackdrop();
146
+ };
93
147
  this.onPointerDown = (e, index) => {
94
148
  // The divider adjacent to a currently rail/floating-collapsed pane isn't
95
149
  // draggable — mirrors the `[part="divider"][aria-disabled="true"]`
@@ -98,11 +152,12 @@ export class LyraSplit extends LyraElement {
98
152
  // wouldn't stop).
99
153
  if (this.isDividerDisabled(index))
100
154
  return;
101
- const base = this.renderRoot.querySelector('[part="base"]');
155
+ // The divider that dispatched this is itself a child of [part="base"],
156
+ // so baseEl is guaranteed to be already rendered here.
102
157
  this.drags.set(e.pointerId, {
103
158
  index,
104
159
  startPos: this.orientation === 'vertical' ? e.clientY : e.clientX,
105
- base,
160
+ base: this.baseEl,
106
161
  appliedDelta: 0,
107
162
  });
108
163
  e.target.setPointerCapture(e.pointerId);
@@ -194,6 +249,19 @@ export class LyraSplit extends LyraElement {
194
249
  };
195
250
  }
196
251
  static { this.styles = [LyraElement.styles, styles]; }
252
+ // `collapseState` needs a custom accessor (force/auto semantics -- see the
253
+ // class doc) rather than the usual @property()-generated one -- registered
254
+ // here, alongside the decorator-declared properties below, the same way
255
+ // lyra-app-rail's `mode` is. `reflect: true` still applies: LitElement's
256
+ // update loop reflects any `reflect`-flagged property to its attribute
257
+ // generically by reading `this[name]` after render, regardless of whether
258
+ // that property has a custom or auto-generated accessor. `attribute` is
259
+ // spelled out (unlike `mode`, a single word whose Lit-default lowercase
260
+ // form already reads fine) so the reflected attribute stays kebab-case,
261
+ // matching this file's other attribute-reflected properties.
262
+ static { this.properties = {
263
+ collapseState: { reflect: true, attribute: 'collapse-state', noAccessor: true },
264
+ }; }
197
265
  connectedCallback() {
198
266
  super.connectedCallback();
199
267
  this.panelCount = this.children.length;
@@ -206,6 +274,23 @@ export class LyraSplit extends LyraElement {
206
274
  // own ResizeObserver.
207
275
  if (this.collapse !== 'none')
208
276
  this.armCollapseObserver();
277
+ // A reconnect (e.g. a drag-and-drop reparent keeping this same element
278
+ // instance) fires disconnectedCallback then connectedCallback
279
+ // synchronously with no update in between, so willUpdate never reruns to
280
+ // notice the floating drawer is still active -- restore its shared
281
+ // registration and scroll lock. Mirrors lyra-app-rail's identical
282
+ // reconnect handling for its mobile overlay.
283
+ if (this.hasUpdated && this.overlayActive) {
284
+ if (this.overlayHandle?.isActive()) {
285
+ this.overlayHandle.resume();
286
+ }
287
+ else {
288
+ this.activateFloatingOverlay();
289
+ }
290
+ if (!this.releaseScrollLock)
291
+ this.releaseScrollLock = lockScroll(this.ownerDocument);
292
+ queueMicrotask(() => this.overlayHandle?.focusInitial());
293
+ }
209
294
  }
210
295
  disconnectedCallback() {
211
296
  super.disconnectedCallback();
@@ -216,11 +301,106 @@ export class LyraSplit extends LyraElement {
216
301
  window.removeEventListener('pointercancel', this.onPointerUp);
217
302
  window.removeEventListener('lostpointercapture', this.onPointerUp);
218
303
  this.collapseResizeObserver?.disconnect();
304
+ this.releaseScrollLock?.();
305
+ this.releaseScrollLock = undefined;
306
+ this.overlayHandle?.suspend();
219
307
  }
220
308
  firstUpdated() {
221
309
  if (this.collapse !== 'none')
222
310
  this.armCollapseObserver();
223
311
  }
312
+ /** Both branches here derive one reactive property from another with no
313
+ * DOM measurement involved, so they belong in `willUpdate()` rather than
314
+ * `updated()`: setting a reactive property from `updated()`/
315
+ * `firstUpdated()` schedules a *second* update on top of the one that
316
+ * just finished, which Lit's dev-mode console flags ("scheduled an
317
+ * update ... after an update completed"). Both of these were real and
318
+ * reproducible in normal usage, not just test artifacts --
319
+ * `ensureSizes()` on any direct `sizes` assignment whose length doesn't
320
+ * match `panelCount` (e.g. a consumer correcting a stale layout), and the
321
+ * `collapseState` reset on every `collapse` -> `'none'` transition. The
322
+ * `collapse !== 'none'` re-arm path in `syncCollapseObserver()` still
323
+ * needs a freshly measured container width and stays in `updated()` --
324
+ * see `armCollapseObserver()`'s own doc comment for why *that* one is the
325
+ * documented exception instead (mirrors virtual-list.ts's
326
+ * `attachContainerListeners()`). */
327
+ willUpdate(changed) {
328
+ if (changed.has('sizes') || this.sizes.length === 0)
329
+ this.ensureSizes();
330
+ if (changed.has('collapse') && this.collapse === 'none') {
331
+ this.resetCollapseState();
332
+ }
333
+ if (changed.has('open') || changed.has('collapseState')) {
334
+ const next = this._collapseState === 'floating' && this.open;
335
+ if (next !== this.overlayActive) {
336
+ this.overlayActive = next;
337
+ if (next) {
338
+ this.justOpened = true;
339
+ this.activateFloatingOverlay();
340
+ }
341
+ else {
342
+ this.deactivateFloatingOverlay();
343
+ }
344
+ }
345
+ }
346
+ }
347
+ /**
348
+ * The collapsing pane's current responsive state. Always one of the three
349
+ * real states — never `'auto'` — reflecting either the live measured
350
+ * width or, once forced, whatever was last assigned. See the class doc for
351
+ * the full force/auto contract (mirrors `<lyra-app-rail>`'s `mode`).
352
+ */
353
+ get collapseState() {
354
+ return this._collapseState;
355
+ }
356
+ set collapseState(next) {
357
+ if (next === 'auto') {
358
+ this._forced = false;
359
+ this.updateCollapseState(this.currentMeasuredWidth());
360
+ return;
361
+ }
362
+ this._forced = true;
363
+ this.applyCollapseStateChange(next);
364
+ }
365
+ /** The container width (px) `[part="base"]` is measured at right now --
366
+ * used to re-derive `collapseState` immediately when a forced value is
367
+ * released back to `'auto'`, outside of the `ResizeObserver`'s own
368
+ * (async, entry-driven) callback. Mirrors the synchronous initial read
369
+ * `armCollapseObserver()` already does when it (re-)arms. */
370
+ currentMeasuredWidth() {
371
+ return this.baseEl?.clientWidth ?? 0;
372
+ }
373
+ /** Resets `collapseState` back to its `'wide'` default and releases any
374
+ * force, bypassing the public accessor's pin/event-emit side effects --
375
+ * used only for the `collapse -> 'none'` housekeeping reset in
376
+ * `willUpdate()`, which is purely internal bookkeeping (the feature is
377
+ * off entirely), not a state transition a consumer forced or observed. */
378
+ resetCollapseState() {
379
+ this._forced = false;
380
+ if (this._collapseState === 'wide')
381
+ return;
382
+ const old = this._collapseState;
383
+ this._collapseState = 'wide';
384
+ this.requestUpdate('collapseState', old);
385
+ }
386
+ /** Applies an actual `collapseState` transition (no-op if `next` matches
387
+ * the current value): updates the field, triggers Lit's reflect/re-render
388
+ * via `requestUpdate`, and fires `lyra-split-collapse-change`. Shared by
389
+ * both the `ResizeObserver`-driven measurement path (`updateCollapseState`)
390
+ * and the accessor's forced-value assignment, so the two never duplicate
391
+ * this logic. Leaving `'floating'` while `open` is still `true` also
392
+ * closes the drawer -- mirrors `<lyra-app-rail>` closing its mobile
393
+ * overlay when `mode` leaves `'mobile'` while open. */
394
+ applyCollapseStateChange(next) {
395
+ if (next === this._collapseState)
396
+ return;
397
+ const old = this._collapseState;
398
+ this._collapseState = next;
399
+ this.requestUpdate('collapseState', old);
400
+ this.emit('lyra-split-collapse-change', { state: next });
401
+ if (next !== 'floating' && this.open)
402
+ this.open = false;
403
+ }
224
404
  get storageFullKey() {
225
405
  return this.storageKey ? `lyra-split:${this.storageKey}:${this.panelCount}` : undefined;
226
406
  }
@@ -298,10 +478,9 @@ export class LyraSplit extends LyraElement {
298
478
  * container resize between calls is always picked up — same live read
299
479
  * `onPointerMove` already does via `drag.base.clientWidth/clientHeight`. */
300
480
  getContainerSize() {
301
- const base = this.renderRoot.querySelector('[part="base"]');
302
- if (!base)
481
+ if (!this.baseEl)
303
482
  return 0;
304
- return this.orientation === 'vertical' ? base.clientHeight : base.clientWidth;
483
+ return this.orientation === 'vertical' ? this.baseEl.clientHeight : this.baseEl.clientWidth;
305
484
  }
306
485
  /** The physical panel index `collapse: 'start' | 'end'` resolves to, or
307
486
  * `-1` when collapse is off (`'none'`) or there are fewer than 2 panels
@@ -338,16 +517,19 @@ export class LyraSplit extends LyraElement {
338
517
  return index === adjacent;
339
518
  }
340
519
  /** Classifies a measured container width into the collapsing pane's
341
- * responsive state and, only on an actual transition, updates
342
- * `collapseState` and fires `lyra-split-collapse-change`. */
520
+ * responsive state and, only on an actual transition, applies it (via the
521
+ * same `applyCollapseStateChange()` the accessor's forced-value path
522
+ * uses). Gated behind `_forced` so a pinned `collapseState` is never
523
+ * silently overwritten by a subsequent resize — this is also what the
524
+ * accessor's `'auto'` release calls (with the current measured width) to
525
+ * re-derive the state without duplicating the width-vs-breakpoint logic. */
343
526
  updateCollapseState(width) {
344
527
  if (this.collapse === 'none')
345
528
  return;
346
- const next = width < this.floatBreakpoint ? 'floating' : width < this.railBreakpoint ? 'rail' : 'wide';
347
- if (next === this.collapseState)
529
+ if (this._forced)
348
530
  return;
349
- this.collapseState = next;
350
- this.emit('lyra-split-collapse-change', { state: next });
531
+ const next = width < this.floatBreakpoint ? 'floating' : width < this.railBreakpoint ? 'rail' : 'wide';
532
+ this.applyCollapseStateChange(next);
351
533
  }
352
534
  /** Creates (idempotently) and (re-)observes `[part="base"]` with the
353
535
  * collapse-state `ResizeObserver` — a no-op until `baseEl` exists (see
@@ -370,32 +552,90 @@ export class LyraSplit extends LyraElement {
370
552
  }
371
553
  }
372
554
  /** Reacts to a live `collapse` property change (as opposed to the
373
- * connect/first-render arming above): turns the observer on/off, and
374
- * resets `collapseState` back to its `'wide'` default when collapse is
375
- * switched off so no stale rail/floating styling survives it. */
555
+ * connect/first-render arming above): turns the observer on/off. The
556
+ * `collapseState` reset for the `'none'` case lives in `willUpdate()`
557
+ * instead (see its doc comment) so no stale rail/floating styling
558
+ * survives switching collapse off, without the extra render pass a
559
+ * property set here would cost. */
376
560
  syncCollapseObserver() {
377
561
  if (this.collapse === 'none') {
378
562
  this.collapseResizeObserver?.disconnect();
379
- this.collapseState = 'wide';
380
563
  return;
381
564
  }
382
565
  this.armCollapseObserver();
383
566
  }
384
- /** Resolves panel `index`'s effective percent min/max for this clamp pass:
385
- * a `panelConstraints` entry's px bound converted against the live
386
- * `containerSize`, or the component's plain percent `min` (no upper
387
- * bound) when the panel carries no constraint for that side. */
388
- percentBounds(index, containerSize) {
389
- const constraint = this.panelConstraints[index];
390
- let min = this.min;
391
- let max = Infinity;
392
- if (constraint && containerSize > 0) {
393
- if (constraint.minPx != null)
394
- min = (constraint.minPx / containerSize) * 100;
395
- if (constraint.maxPx != null)
396
- max = (constraint.maxPx / containerSize) * 100;
567
+ /** The safe fallback for a shared percent minimum. A value above the
568
+ * available share is still useful as a consumer intent, but it cannot be
569
+ * honored for every panel at once, so reduce it proportionally. */
570
+ normalizedDefaultMin() {
571
+ if (this.panelCount <= 0)
572
+ return 0;
573
+ const requested = Number.isFinite(this.min) ? Math.max(0, this.min) : 0;
574
+ // Leave one keyboard step of aggregate slack after rejecting an
575
+ // infeasible configuration, so an equal starting layout remains
576
+ // resizable instead of merely becoming a different frozen layout.
577
+ const available = Math.max(0, 100 - KEYBOARD_STEP);
578
+ return Math.min(requested, available / this.panelCount);
579
+ }
580
+ /** Resolves all panel bounds together so aggregate feasibility is checked
581
+ * before an individual divider is asked to clamp a pair. An invalid set is
582
+ * rejected as a whole: interaction uses the safe shared minimum instead of
583
+ * exposing a divider whose minimum is greater than its maximum. */
584
+ resolveConstraintBounds(containerSize) {
585
+ const fallbackMin = this.normalizedDefaultMin();
586
+ const fallback = Array.from({ length: this.panelCount }, () => ({ min: fallbackMin, max: Infinity }));
587
+ if (this.panelCount <= 0)
588
+ return { bounds: [], usePanelConstraints: true };
589
+ const bounds = [];
590
+ let minimumTotal = 0;
591
+ let maximumTotal = 0;
592
+ let hasUnboundedMaximum = false;
593
+ let issueReason;
594
+ for (let index = 0; index < this.panelCount; index++) {
595
+ const constraint = this.panelConstraints[index];
596
+ let min = Number.isFinite(this.min) ? Math.max(0, this.min) : 0;
597
+ let max = Infinity;
598
+ if (constraint) {
599
+ if (constraint.minPx != null && Number.isFinite(constraint.minPx) && constraint.minPx >= 0 && containerSize > 0) {
600
+ min = (constraint.minPx / containerSize) * 100;
601
+ }
602
+ if (constraint.maxPx != null && Number.isFinite(constraint.maxPx) && constraint.maxPx >= 0 && containerSize > 0) {
603
+ max = (constraint.maxPx / containerSize) * 100;
604
+ }
605
+ if ((constraint.minPx != null && (!Number.isFinite(constraint.minPx) || constraint.minPx < 0)) ||
606
+ (constraint.maxPx != null && (!Number.isFinite(constraint.maxPx) || constraint.maxPx < 0))) {
607
+ issueReason ??= 'minimum-exceeds-maximum';
608
+ }
609
+ }
610
+ if (min > max)
611
+ issueReason ??= 'minimum-exceeds-maximum';
612
+ bounds.push({ min, max });
613
+ minimumTotal += min;
614
+ if (Number.isFinite(max))
615
+ maximumTotal += max;
616
+ else
617
+ hasUnboundedMaximum = true;
397
618
  }
398
- return { min, max };
619
+ if (issueReason == null && minimumTotal > 100 + 1e-9)
620
+ issueReason = 'minimum-total';
621
+ if (issueReason == null && !hasUnboundedMaximum && maximumTotal < 100 - 1e-9)
622
+ issueReason = 'maximum-total';
623
+ if (issueReason == null)
624
+ return { bounds, usePanelConstraints: true };
625
+ return {
626
+ bounds: fallback,
627
+ usePanelConstraints: false,
628
+ issue: {
629
+ reason: issueReason,
630
+ panelCount: this.panelCount,
631
+ minimumTotal,
632
+ maximumTotal: hasUnboundedMaximum ? null : maximumTotal,
633
+ containerSize,
634
+ },
635
+ };
636
+ }
637
+ percentBounds(index, containerSize) {
638
+ return this.resolveConstraintBounds(containerSize).bounds[index] ?? { min: 0, max: Infinity };
399
639
  }
400
640
  clampPair(sizes, i, delta, containerSize = 0) {
401
641
  const next = [...sizes];
@@ -424,11 +664,43 @@ export class LyraSplit extends LyraElement {
424
664
  if (commit)
425
665
  this.persist();
426
666
  }
667
+ /** The collapsing pane's light-DOM element itself — the `'floating'`
668
+ * drawer's focus-trap/backdrop target. There's no separate shadow-DOM
669
+ * panel to trap focus within (unlike `<lyra-app-rail>`'s `[part="panel"]`):
670
+ * the slotted panel *is* the floating drawer, just repositioned via
671
+ * inline styles in `updated()`. */
672
+ get floatingPanelEl() {
673
+ const index = this.collapsingIndex;
674
+ return index === -1 ? null : (this.children[index] ?? null);
675
+ }
676
+ activateFloatingOverlay() {
677
+ this.releaseScrollLock ??= lockScroll(this.ownerDocument);
678
+ this.overlayHandle = activateOverlay({
679
+ host: this,
680
+ panel: () => this.floatingPanelEl,
681
+ onEscape: () => (this.open = false),
682
+ onBackdrop: () => (this.open = false),
683
+ });
684
+ }
685
+ deactivateFloatingOverlay() {
686
+ this.releaseScrollLock?.();
687
+ this.releaseScrollLock = undefined;
688
+ this.overlayHandle?.deactivate();
689
+ this.overlayHandle = undefined;
690
+ }
427
691
  updated(changed) {
428
- if (changed.has('sizes') || this.sizes.length === 0)
429
- this.ensureSizes();
430
692
  if (changed.has('collapse'))
431
693
  this.syncCollapseObserver();
694
+ const constraintResolution = this.resolveConstraintBounds(this.getContainerSize());
695
+ const issue = constraintResolution.issue;
696
+ const issueKey = issue
697
+ ? `${issue.reason}:${issue.panelCount}:${issue.minimumTotal}:${issue.maximumTotal ?? 'unbounded'}`
698
+ : '';
699
+ if (issueKey !== this.constraintIssueKey) {
700
+ this.constraintIssueKey = issueKey;
701
+ if (issue)
702
+ this.emit('lyra-split-constraints-invalid', issue);
703
+ }
432
704
  const panels = [...this.children];
433
705
  const current = new Set(panels);
434
706
  // A panel that dropped out of the slot (removed from the light DOM)
@@ -445,6 +717,7 @@ export class LyraSplit extends LyraElement {
445
717
  stale.style.removeProperty('inset-inline-start');
446
718
  stale.style.removeProperty('inset-inline-end');
447
719
  stale.style.removeProperty('inline-size');
720
+ stale.hidden = false;
448
721
  delete stale.dataset.collapseState;
449
722
  }
450
723
  }
@@ -456,7 +729,7 @@ export class LyraSplit extends LyraElement {
456
729
  const collapsingIndex = this.collapseActive ? this.collapsingIndex : -1;
457
730
  panels.forEach((panel, i) => {
458
731
  const percent = this.sizes[i] ?? 0;
459
- const constraint = this.panelConstraints[i];
732
+ const constraint = constraintResolution.usePanelConstraints ? this.panelConstraints[i] : undefined;
460
733
  // Collapse-only inline styles are always cleared first, then
461
734
  // re-applied below only for the panel(s) that need them this pass —
462
735
  // simpler than diffing which of the 5 properties changed.
@@ -465,17 +738,30 @@ export class LyraSplit extends LyraElement {
465
738
  panel.style.removeProperty('inset-inline-start');
466
739
  panel.style.removeProperty('inset-inline-end');
467
740
  panel.style.removeProperty('inline-size');
741
+ panel.hidden = false;
468
742
  if (collapsingIndex === i && this.collapseState === 'rail') {
469
743
  // Fixed rail width instead of the normal clamp()/percent flex-basis.
470
744
  panel.style.flex = `0 0 ${this.railWidth}`;
471
745
  }
746
+ else if (collapsingIndex === i && this.collapseState === 'floating' && !this.open) {
747
+ // Hidden-by-default drawer (see the class doc): rendering nothing
748
+ // (not just visually collapsed, so it's out of the accessibility
749
+ // tree too) until a consumer sets `open`. The pane still vacates the
750
+ // flex flow the same way the open/floating branch below does, via
751
+ // `hidden`'s UA `display: none` rather than `position: absolute`, so
752
+ // the other pane below still grows to fill the space regardless of
753
+ // which of the two branches applied.
754
+ panel.style.flex = 'none';
755
+ panel.hidden = true;
756
+ }
472
757
  else if (collapsingIndex === i && this.collapseState === 'floating') {
473
- // Lifted out of the flex flow entirely as an overlay card, anchored
474
- // to its own logical start/end edge, spanning the full cross-axis
475
- // extent — [part="base"] carries `position: relative` for this to
476
- // anchor against (see split.styles.ts). Sized at its own normal
477
- // percent width (i.e. what it would render at in the `'wide'`
478
- // state), so there's no visual size jump the moment it un-floats.
758
+ // `this.open` is true here. Lifted out of the flex flow entirely as
759
+ // an overlay card, anchored to its own logical start/end edge,
760
+ // spanning the full cross-axis extent — [part="base"] carries
761
+ // `position: relative` for this to anchor against (see
762
+ // split.styles.ts). Sized at its own normal percent width (i.e. what
763
+ // it would render at in the `'wide'` state), so there's no visual
764
+ // size jump the moment it un-floats.
479
765
  panel.style.flex = 'none';
480
766
  panel.style.position = 'absolute';
481
767
  panel.style.setProperty('inset-block', '0');
@@ -519,14 +805,28 @@ export class LyraSplit extends LyraElement {
519
805
  else {
520
806
  this.removeAttribute('data-collapse-state');
521
807
  }
808
+ // Runs after this render (not willUpdate) so the floating panel's
809
+ // repositioned geometry above has already landed before the focus call
810
+ // below can rely on it -- mirrors lyra-app-rail's/lyra-dialog's identical
811
+ // ordering rationale for their own overlay's initial focus.
812
+ if (this.justOpened) {
813
+ this.justOpened = false;
814
+ this.overlayHandle?.focusInitial();
815
+ }
522
816
  }
523
817
  render() {
524
818
  const dividers = [];
819
+ const bounds = this.resolveConstraintBounds(this.getContainerSize()).bounds;
525
820
  for (let i = 0; i < this.panelCount - 1; i++) {
526
- // The achievable max for this divider is bounded by its two adjacent
527
- // panels, not the whole track — pushing past it would starve a panel
528
- // further down the line even though this pair still has room.
529
- const valueMax = (this.sizes[i] ?? 0) + (this.sizes[i + 1] ?? 0) - this.min;
821
+ // The achievable range is bounded by both adjacent panels, not the
822
+ // whole track — pushing past it would starve the partner even though
823
+ // this pair still has room. Keep max >= min for imperfect persisted or
824
+ // consumer-supplied starting sizes.
825
+ const pairTotal = (this.sizes[i] ?? 0) + (this.sizes[i + 1] ?? 0);
826
+ const a = bounds[i] ?? { min: 0, max: Infinity };
827
+ const b = bounds[i + 1] ?? { min: 0, max: Infinity };
828
+ const valueMin = Math.max(a.min, pairTotal - b.max);
829
+ const valueMax = Math.max(valueMin, Math.min(a.max, pairTotal - b.min));
530
830
  const disabled = this.isDividerDisabled(i);
531
831
  dividers.push(html `<div
532
832
  part="divider"
@@ -534,7 +834,7 @@ export class LyraSplit extends LyraElement {
534
834
  aria-label="Resize divider between panel ${i + 1} and panel ${i + 2}"
535
835
  aria-orientation=${this.orientation === 'vertical' ? 'horizontal' : 'vertical'}
536
836
  aria-valuenow=${Math.round(this.sizes[i] ?? 0)}
537
- aria-valuemin=${this.min}
837
+ aria-valuemin=${Math.round(valueMin)}
538
838
  aria-valuemax=${Math.round(valueMax)}
539
839
  aria-disabled=${disabled ? 'true' : nothing}
540
840
  tabindex=${disabled ? '-1' : '0'}
@@ -543,7 +843,12 @@ export class LyraSplit extends LyraElement {
543
843
  @keydown=${(e) => this.onDividerKeyDown(e, i)}
544
844
  ></div>`);
545
845
  }
546
- return html `<div part="base"><slot @slotchange=${this.onSlotChange}></slot>${dividers}</div>`;
846
+ const showBackdrop = this.collapseState === 'floating' && this.open;
847
+ return html `<div part="base">
848
+ <slot @slotchange=${this.onSlotChange}></slot>
849
+ ${dividers}
850
+ ${showBackdrop ? html `<div part="backdrop" @click=${this.onBackdropClick}></div>` : nothing}
851
+ </div>`;
547
852
  }
548
853
  }
549
854
  __decorate([
@@ -574,11 +879,11 @@ __decorate([
574
879
  property({ type: Number, attribute: 'float-breakpoint' })
575
880
  ], LyraSplit.prototype, "floatBreakpoint", void 0);
576
881
  __decorate([
577
- state()
578
- ], LyraSplit.prototype, "panelCount", void 0);
882
+ property({ type: Boolean, reflect: true })
883
+ ], LyraSplit.prototype, "open", void 0);
579
884
  __decorate([
580
885
  state()
581
- ], LyraSplit.prototype, "collapseState", void 0);
886
+ ], LyraSplit.prototype, "panelCount", void 0);
582
887
  __decorate([
583
888
  query('[part="base"]')
584
889
  ], LyraSplit.prototype, "baseEl", void 0);
@@ -4,6 +4,10 @@ export const styles = css `
4
4
  display: block;
5
5
  inline-size: 100%;
6
6
  block-size: 100%;
7
+ /* Component-specific -- not a shared design token, so a consumer can
8
+ retheme it without a raw literal leaking into the public API (same
9
+ rationale as lyra-app-rail's --lyra-app-rail-overlay-color). */
10
+ --lyra-split-overlay-color: rgb(0 0 0 / 0.5);
7
11
  }
8
12
  [part='base'] {
9
13
  display: flex;
@@ -64,13 +68,26 @@ export const styles = css `
64
68
  /* The 'floating' collapse state's overlay "card" look -- geometry
65
69
  (position/inset-*/inline-size) is set inline by split.ts's updated(),
66
70
  matching how the ordinary flex/order styling is applied; only the
67
- visual/stacking treatment lives here. */
71
+ visual/stacking treatment lives here. z-index is above [part="backdrop"]
72
+ (below), so the drawer renders on top of its own scrim. */
68
73
  ::slotted([data-collapse-state='floating']) {
69
74
  z-index: 1;
70
75
  background: var(--lyra-color-surface);
71
76
  border-radius: var(--lyra-radius);
72
77
  box-shadow: var(--lyra-shadow);
73
78
  }
79
+ /* The 'floating' drawer's scrim -- only rendered while collapseState is
80
+ 'floating' and open (see split.ts's render()). Scoped to [part="base"]
81
+ (position: absolute against its position: relative ancestor) rather than
82
+ a viewport-fixed overlay like lyra-app-rail's mobile backdrop: the
83
+ floating panel itself is only ever positioned relative to this
84
+ component's own box, never the full page. */
85
+ [part='backdrop'] {
86
+ position: absolute;
87
+ inset: 0;
88
+ z-index: 0;
89
+ background: var(--lyra-split-overlay-color);
90
+ }
74
91
  /* Rail-clamped content can easily overflow the fixed rail-width — clip it
75
92
  rather than letting it blow out the layout; the panel's own content is
76
93
  expected to adapt to the narrower width itself (e.g. via a container