@aceshooting/lyra-ui 21.0.0 → 21.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 (272) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +6 -3
  3. package/custom-elements.json +1 -1
  4. package/dist/ai/runtime.js +1 -1
  5. package/dist/ai/types.d.ts +13 -5
  6. package/dist/cli/lyra-rename-ledger.mjs +1 -0
  7. package/dist/cli/migrate-wa.mjs +33 -13
  8. package/dist/cli/migration-contract.json +1 -1
  9. package/dist/components/agent-tools/agent-trace/agent-trace.class.d.ts +5 -3
  10. package/dist/components/agent-tools/agent-trace/agent-trace.class.js +3 -2
  11. package/dist/components/agent-tools/tool-call-block/tool-call-block.class.d.ts +15 -3
  12. package/dist/components/agent-tools/tool-call-block/tool-call-block.class.js +3 -3
  13. package/dist/components/agent-tools/tool-call-chip/tool-call-chip.class.d.ts +24 -8
  14. package/dist/components/agent-tools/tool-call-chip/tool-call-chip.class.js +3 -3
  15. package/dist/components/agent-tools/tool-param-form/tool-param-form.class.js +4 -0
  16. package/dist/components/agent-tools/tool-result-dialog/tool-result-dialog.class.d.ts +9 -5
  17. package/dist/components/agent-tools/tool-result-dialog/tool-result-dialog.class.js +2 -2
  18. package/dist/components/agent-tools/tool-result-dialog/tool-result-dialog.styles.js +1 -1
  19. package/dist/components/agent-tools/tool-status.js +2 -2
  20. package/dist/components/agent-tools/tool-timeline/tool-timeline.class.d.ts +8 -0
  21. package/dist/components/agent-tools/tool-timeline/tool-timeline.class.js +3 -2
  22. package/dist/components/agent-tools/tool-timeline/tool-timeline.styles.js +1 -1
  23. package/dist/components/agent-tools/trace-tree/span.d.ts +2 -2
  24. package/dist/components/charts/chart/box-plot.class.d.ts +3 -1
  25. package/dist/components/charts/chart/box-plot.class.js +9 -9
  26. package/dist/components/charts/chart/chart-bidi.d.ts +109 -0
  27. package/dist/components/charts/chart/chart-bidi.js +1 -0
  28. package/dist/components/charts/chart/chart.class.d.ts +17 -2
  29. package/dist/components/charts/chart/chart.class.js +10 -8
  30. package/dist/components/charts/chart/histogram.class.js +1 -1
  31. package/dist/components/charts/chart/lite-chart.class.d.ts +24 -5
  32. package/dist/components/charts/chart/lite-chart.class.js +9 -9
  33. package/dist/components/charts/chart/lite-chart.styles.js +1 -1
  34. package/dist/components/conversation/code-block/shiki-dark-theme.d.ts +2 -2
  35. package/dist/components/conversation/code-block/shiki-dark-theme.js +1 -1
  36. package/dist/components/conversation/conversation-item/conversation-item.class.d.ts +18 -2
  37. package/dist/components/conversation/conversation-item/conversation-item.class.js +3 -3
  38. package/dist/components/conversation/conversation-item/conversation-item.styles.js +1 -1
  39. package/dist/components/conversation/markdown/markdown-base.class.js +1 -1
  40. package/dist/components/conversation/markdown/markdown-core.class.d.ts +3 -1
  41. package/dist/components/conversation/markdown/markdown.class.d.ts +3 -1
  42. package/dist/components/conversation/message-actions/message-actions.class.d.ts +7 -2
  43. package/dist/components/conversation/message-actions/message-actions.class.js +2 -2
  44. package/dist/components/conversation/message-parts/message-parts.class.d.ts +6 -2
  45. package/dist/components/conversation/message-parts/message-parts.class.js +5 -3
  46. package/dist/components/conversation/selection-toolbar/selection-toolbar.class.d.ts +3 -2
  47. package/dist/components/conversation/selection-toolbar/selection-toolbar.class.js +1 -1
  48. package/dist/components/conversation/streaming-text/streaming-text-base.class.d.ts +1 -5
  49. package/dist/components/conversation/streaming-text/streaming-text-base.class.js +2 -2
  50. package/dist/components/conversation/streaming-text/streaming-text-core.class.d.ts +7 -1
  51. package/dist/components/conversation/streaming-text/streaming-text-core.class.js +3 -4
  52. package/dist/components/conversation/streaming-text/streaming-text.class.d.ts +7 -1
  53. package/dist/components/conversation/streaming-text/streaming-text.class.js +3 -4
  54. package/dist/components/conversation/thread-list/thread-list.class.d.ts +13 -2
  55. package/dist/components/conversation/thread-list/thread-list.class.js +3 -3
  56. package/dist/components/data/data-grid/data-grid.class.js +1 -1
  57. package/dist/components/data/document-library/document-library.class.js +2 -0
  58. package/dist/components/data/flow-canvas/flow-canvas.class.js +1 -1
  59. package/dist/components/data/flow-minimap/flow-minimap.class.js +1 -1
  60. package/dist/components/data/flow-node/flow-node.class.d.ts +6 -2
  61. package/dist/components/data/flow-node/flow-node.class.js +6 -2
  62. package/dist/components/data/flow-node/flow-node.styles.js +1 -1
  63. package/dist/components/data/gauge/gauge.styles.js +1 -1
  64. package/dist/components/data/heatmap/heatmap.class.d.ts +3 -0
  65. package/dist/components/data/heatmap/heatmap.class.js +2 -2
  66. package/dist/components/data/heatmap/heatmap.styles.js +1 -1
  67. package/dist/components/data/sparkline/sparkline.class.d.ts +5 -3
  68. package/dist/components/data/stat/stat.class.d.ts +4 -3
  69. package/dist/components/data/table/table.class.d.ts +32 -5
  70. package/dist/components/data/table/table.class.js +3 -2
  71. package/dist/components/forms/button/button.class.d.ts +7 -1
  72. package/dist/components/forms/button/button.styles.js +1 -1
  73. package/dist/components/forms/date-picker/date-input.styles.js +1 -1
  74. package/dist/components/forms/input/input.styles.js +1 -1
  75. package/dist/components/forms/rubric-form/rubric-form.class.js +1 -0
  76. package/dist/components/layout/app-rail/app-rail.class.d.ts +50 -11
  77. package/dist/components/layout/app-rail/app-rail.class.js +2 -2
  78. package/dist/components/layout/command-palette/command-palette.class.js +1 -1
  79. package/dist/components/layout/menu/menu.class.d.ts +6 -5
  80. package/dist/components/layout/split-panel/split-panel.class.d.ts +2 -1
  81. package/dist/components/media/flag/language-map.d.ts +8 -0
  82. package/dist/components/media/flag/language-map.js +1 -1
  83. package/dist/components/media/map/map-loader.d.ts +7 -1
  84. package/dist/components/media/map/map.class.d.ts +111 -5
  85. package/dist/components/media/map/map.class.js +3 -3
  86. package/dist/components/media/media-card/media-card.class.d.ts +18 -5
  87. package/dist/components/media/media-card/media-card.class.js +1 -1
  88. package/dist/components/overlays/overlay/popover.class.d.ts +20 -0
  89. package/dist/components/overlays/overlay/popover.class.js +2 -2
  90. package/dist/components/retrieval/graph/graph.class.d.ts +3 -3
  91. package/dist/components/retrieval/graph/graph.class.js +1 -1
  92. package/dist/components/retrieval/knowledge-graph-explorer/knowledge-graph-explorer.class.d.ts +5 -3
  93. package/dist/components/retrieval/knowledge-graph-explorer/knowledge-graph-explorer.class.js +3 -2
  94. package/dist/components/retrieval/mind-map/mind-map.class.d.ts +3 -3
  95. package/dist/components/retrieval/mind-map/mind-map.class.js +1 -1
  96. package/dist/components/retrieval/retrieval-results/retrieval-results.class.d.ts +4 -1
  97. package/dist/components/retrieval/retrieval-results/retrieval-results.class.js +5 -1
  98. package/dist/components/retrieval/source-picker/source-picker.class.d.ts +4 -1
  99. package/dist/components/retrieval/source-picker/source-picker.class.js +5 -1
  100. package/dist/components/utility/icon/icon.class.d.ts +9 -3
  101. package/dist/components/utility/icon/icon.class.js +1 -1
  102. package/dist/components/utility/mutation-observer/mutation-observer.class.d.ts +14 -8
  103. package/dist/components/utility/mutation-observer/mutation-observer.class.js +1 -1
  104. package/dist/components/viewers/document-viewer/document-viewer.class.d.ts +6 -1
  105. package/dist/components/viewers/document-viewer/document-viewer.class.js +7 -1
  106. package/dist/components/viewers/document-viewer/registry.d.ts +12 -3
  107. package/dist/components/viewers/geojson-view/geojson-view.class.d.ts +13 -5
  108. package/dist/components/viewers/geojson-view/geojson-view.class.js +1 -1
  109. package/dist/components/viewers/geojson-view/geojson-viewer.class.d.ts +3 -2
  110. package/dist/custom-elements-jsx.d.ts +1 -1
  111. package/dist/events.d.ts +644 -1242
  112. package/dist/internal/anchored-overlay-runtime.d.ts +2 -2
  113. package/dist/internal/cell-range.d.ts +0 -6
  114. package/dist/internal/cell-range.js +1 -1
  115. package/dist/internal/css-token-length.d.ts +7 -0
  116. package/dist/internal/css-token-length.js +1 -0
  117. package/dist/internal/default-strings.generated.d.ts +1 -1
  118. package/dist/internal/default-strings.generated.js +1 -1
  119. package/dist/internal/deferred-focus-return.d.ts +7 -4
  120. package/dist/internal/deferred-focus-return.js +1 -1
  121. package/dist/internal/dev-mode-attribute-warning.d.ts +35 -0
  122. package/dist/internal/dev-mode-attribute-warning.js +1 -1
  123. package/dist/internal/localization-runtime.js +1 -1
  124. package/dist/internal/localization-types.d.ts +1 -1
  125. package/dist/internal/localization.js +2 -2
  126. package/dist/internal/package-metadata.d.ts +1 -1
  127. package/dist/internal/package-metadata.js +1 -1
  128. package/dist/internal/papaparse-loader.d.ts +1 -1
  129. package/dist/internal/papaparse-loader.js +1 -1
  130. package/dist/internal/positioner.d.ts +10 -0
  131. package/dist/internal/positioner.js +1 -1
  132. package/dist/internal/shared-unions.d.ts +2 -1
  133. package/dist/internal/slotted-overlay-controller.d.ts +20 -0
  134. package/dist/internal/slotted-overlay-controller.js +1 -0
  135. package/dist/internal/text-quote.d.ts +1 -1
  136. package/dist/internal/text-quote.js +1 -1
  137. package/dist/internal/theme-observation.d.ts +4 -0
  138. package/dist/internal/theme-observation.js +1 -0
  139. package/dist/internal/theme-watcher.js +1 -1
  140. package/dist/localization.d.ts +6 -1
  141. package/dist/localization.js +1 -1
  142. package/dist/lyra.d.ts +1 -1
  143. package/dist/lyra.js +1 -1
  144. package/dist/svelte.d.ts +1 -1
  145. package/dist/testing/lyra-tag-event-map.js +1 -1
  146. package/dist/translations/ar/agent-tools.js +1 -1
  147. package/dist/translations/cs/agent-tools.js +1 -1
  148. package/dist/translations/da/agent-tools.js +1 -1
  149. package/dist/translations/de/agent-tools.js +1 -1
  150. package/dist/translations/de-CH/agent-tools.js +1 -1
  151. package/dist/translations/es/agent-tools.js +1 -1
  152. package/dist/translations/fa/agent-tools.js +1 -1
  153. package/dist/translations/fi/agent-tools.js +1 -1
  154. package/dist/translations/fr/agent-tools.js +1 -1
  155. package/dist/translations/he/agent-tools.js +1 -1
  156. package/dist/translations/hi/agent-tools.js +1 -1
  157. package/dist/translations/hr/agent-tools.js +1 -1
  158. package/dist/translations/hu/agent-tools.js +1 -1
  159. package/dist/translations/id/agent-tools.js +1 -1
  160. package/dist/translations/it/agent-tools.js +1 -1
  161. package/dist/translations/ja/agent-tools.js +1 -1
  162. package/dist/translations/kk/agent-tools.js +1 -1
  163. package/dist/translations/ko/agent-tools.js +1 -1
  164. package/dist/translations/nb/agent-tools.js +1 -1
  165. package/dist/translations/nl/agent-tools.js +1 -1
  166. package/dist/translations/nn/agent-tools.js +1 -1
  167. package/dist/translations/pl/agent-tools.js +1 -1
  168. package/dist/translations/pt-BR/agent-tools.js +1 -1
  169. package/dist/translations/pt-PT/agent-tools.js +1 -1
  170. package/dist/translations/pt-PT/charts.js +1 -1
  171. package/dist/translations/pt-PT/conversation.js +1 -1
  172. package/dist/translations/pt-PT/data.js +1 -1
  173. package/dist/translations/pt-PT/forms.js +1 -1
  174. package/dist/translations/pt-PT/layout.js +1 -1
  175. package/dist/translations/pt-PT/media.js +1 -1
  176. package/dist/translations/pt-PT/retrieval.js +1 -1
  177. package/dist/translations/pt-PT/shared.js +1 -1
  178. package/dist/translations/pt-PT/utility.js +1 -1
  179. package/dist/translations/pt-PT/viewers.js +2 -2
  180. package/dist/translations/ro/agent-tools.js +1 -1
  181. package/dist/translations/ru/agent-tools.js +1 -1
  182. package/dist/translations/sl/agent-tools.js +1 -1
  183. package/dist/translations/sv/agent-tools.js +1 -1
  184. package/dist/translations/tr/agent-tools.js +1 -1
  185. package/dist/translations/uk/agent-tools.js +1 -1
  186. package/dist/translations/zh-CN/agent-tools.js +1 -1
  187. package/dist/translations/zh-TW/agent-tools.js +1 -1
  188. package/dist/utilities/index.d.ts +1 -1
  189. package/dist/utilities/index.js +1 -1
  190. package/dist/utilities/localization.d.ts +28 -10
  191. package/dist/vue.d.ts +1 -1
  192. package/llms/components/lr-accordion-item.md +2 -344
  193. package/llms/components/lr-accordion.md +2 -344
  194. package/llms/components/lr-app-rail-group.md +2 -721
  195. package/llms/components/lr-app-rail-item.md +2 -721
  196. package/llms/components/lr-app-rail.md +29 -4
  197. package/llms/components/lr-bar-chart.md +2 -87
  198. package/llms/components/lr-box-plot.md +1 -1
  199. package/llms/components/lr-breadcrumb-item.md +2 -67
  200. package/llms/components/lr-bubble-chart.md +2 -87
  201. package/llms/components/lr-button.md +7 -0
  202. package/llms/components/lr-chart.md +4 -5
  203. package/llms/components/lr-chip-group.md +2 -272
  204. package/llms/components/lr-conversation-item.md +20 -2
  205. package/llms/components/lr-date-input.md +2 -457
  206. package/llms/components/lr-date-picker.md +6 -2
  207. package/llms/components/lr-document-library.md +3 -3
  208. package/llms/components/lr-document-viewer.md +14 -5
  209. package/llms/components/lr-doughnut-chart.md +2 -87
  210. package/llms/components/lr-dropdown-item.md +2 -306
  211. package/llms/components/lr-dropdown.md +6 -0
  212. package/llms/components/lr-flag.md +9 -1
  213. package/llms/components/lr-flow-node.md +4 -1
  214. package/llms/components/lr-geojson-view.md +3 -90
  215. package/llms/components/lr-geojson-viewer.md +13 -5
  216. package/llms/components/lr-icon-button.md +4 -1
  217. package/llms/components/lr-icon.md +14 -6
  218. package/llms/components/lr-input.md +9 -2
  219. package/llms/components/lr-lite-chart.md +17 -9
  220. package/llms/components/lr-map.md +87 -2
  221. package/llms/components/lr-markdown-core.md +5 -4
  222. package/llms/components/lr-markdown.md +4 -2
  223. package/llms/components/lr-media-card.md +10 -5
  224. package/llms/components/lr-menu-item.md +2 -306
  225. package/llms/components/lr-menu.md +9 -6
  226. package/llms/components/lr-menubar-item.md +2 -125
  227. package/llms/components/lr-message-actions.md +4 -1
  228. package/llms/components/lr-message-parts.md +14 -5
  229. package/llms/components/lr-mutation-observer.md +12 -6
  230. package/llms/components/lr-navigation-menu-item.md +2 -237
  231. package/llms/components/lr-number-input.md +4 -0
  232. package/llms/components/lr-option.md +2 -712
  233. package/llms/components/lr-pie-chart.md +2 -87
  234. package/llms/components/lr-polar-area-chart.md +2 -87
  235. package/llms/components/lr-popover.md +10 -0
  236. package/llms/components/lr-prompt-input.md +5 -1
  237. package/llms/components/lr-radar-chart.md +2 -87
  238. package/llms/components/lr-reorder-item.md +2 -157
  239. package/llms/components/lr-result-field.md +2 -117
  240. package/llms/components/lr-retrieval-results.md +3 -1
  241. package/llms/components/lr-rubric-form.md +3 -2
  242. package/llms/components/lr-scatter-chart.md +2 -87
  243. package/llms/components/lr-selection-toolbar.md +4 -2
  244. package/llms/components/lr-source-card.md +2 -186
  245. package/llms/components/lr-source-picker.md +3 -1
  246. package/llms/components/lr-sparkline.md +9 -4
  247. package/llms/components/lr-split-panel.md +5 -3
  248. package/llms/components/lr-stat.md +4 -3
  249. package/llms/components/lr-streaming-text-core.md +7 -5
  250. package/llms/components/lr-streaming-text.md +9 -6
  251. package/llms/components/lr-swatch-picker.md +130 -0
  252. package/llms/components/lr-tag.md +2 -177
  253. package/llms/components/lr-thread-list.md +27 -4
  254. package/llms/components/lr-timeline-item.md +2 -120
  255. package/llms/components/lr-toast-item.md +2 -223
  256. package/llms/components/lr-tool-call-block.md +12 -3
  257. package/llms/components/lr-tool-call-chip.md +12 -6
  258. package/llms/components/lr-tool-param-form.md +3 -2
  259. package/llms/components/lr-tool-result-dialog.md +7 -3
  260. package/llms/components/lr-tool-result-view.md +4 -3
  261. package/llms/components/lr-tool-timeline.md +12 -5
  262. package/llms/components/lr-tree-item.md +2 -354
  263. package/llms/index.md +22 -4
  264. package/llms/migration.md +75 -1
  265. package/llms/peers.md +1 -1
  266. package/llms/shared.md +86 -39
  267. package/llms-full.txt +739 -163
  268. package/package.json +6 -6
  269. package/registrations.json +1 -1
  270. package/vscode-css-data.json +1 -1
  271. package/vscode-html-data.json +1 -1
  272. package/web-types.json +1 -1
@@ -9,725 +9,6 @@
9
9
  - **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
10
10
  - **Deprecations** none
11
11
  - **Optional peers** none
12
- - **Themeable via** 8 parts, 8 custom properties — see this component's own `@csspart`/`@cssprop` list below
13
- - **Documented with** `lr-app-rail`, `lr-app-rail-item` (same section below)
12
+ - **Themeable via** 8 parts, 8 custom properties — see `lr-app-rail.md`
13
+ - **Documented with** `lr-app-rail`: see [lr-app-rail.md](./lr-app-rail.md).
14
14
  - **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`
15
-
16
- ---
17
-
18
- ## `lr-app-rail`
19
-
20
- A responsive navigation rail, and the library's application sidebar, that adapts across three presentations as the _viewport_ narrows (not
21
- this element's own inline size): `'full'` (nav items show icon + label, inline), `'icon-only'` (a
22
- narrower inline rail, icons only), and `'mobile'` (hidden behind a toggle button; opening it shows a
23
- focus-trapped floating overlay over the page). First-party invention (no `wa-*`/`sl-*` counterpart).
24
- Breakpoints are viewport-width `matchMedia()` queries against `icon-only-breakpoint`/
25
- `mobile-breakpoint`, not a `ResizeObserver` on this element — presentation tracks the actual device/
26
- window width the way a native OS shell's navigation does, not however much horizontal space a
27
- particular layout happens to give it. `[part="base"]` (the inline `'full'`/`'icon-only'`
28
- presentation) and `[part="panel"]` (the mobile overlay) are the _same_ element promoted in place
29
- across modes (mirrors `<lr-widget>`'s fullscreen mode) — never both at once, and slotted nav
30
- content is never duplicated.
31
-
32
- In mobile mode the closed `[part="panel"]` has no shadow and parks one pixel beyond the inline-start
33
- edge, including at fractional widths. The open-only `--lr-app-rail-panel-shadow` defaults to
34
- `var(--lr-shadow-l)`. Elevation snaps with the open state while the panel slides. Only real open/close
35
- changes animate: runtime direction changes and entering mobile mode leave a closed panel parked
36
- without a sweep. Closed content remains inert. Keep the rail outside ancestors establishing a fixed
37
- containing block (`transform`, `filter`, `will-change: transform`, or layout/paint containment),
38
- because geometric parking otherwise follows that ancestor's edge instead of the viewport.
39
-
40
- Opting in to `resizable` adds a continuously draggable width for the `'full'` state: a
41
- `[part="resizer"]` handle (pointer-drag and Left/Right-arrow keyboard stepping, RTL-aware) clamped to
42
- `[minRailWidthPx, maxRailWidthPx]`. Set `storageKey` (attribute `storage-key`) to persist the fields
43
- selected by `persist` to `localStorage` under `lr-app-rail:${storageKey}` and restore them on the
44
- next mount (mirrors `lr-multi-split`'s `storage-key`; effective `mode` is breakpoint-derived and never
45
- persisted). The backward-compatible allowlist is `open width`; use
46
- `persist="width preferred-mode"` for durable layout preference without restoring the transient
47
- mobile overlay. Without a `storageKey` there is no persistence — listen for `lr-rail-resize` and
48
- persist its committed `widthPx` yourself. Listen for the preceding cancelable
49
- `lr-rail-resize-request` event when a host needs to veto a proposed width.
50
- `preferredMode` separately lets a host manually prefer `'full'`/`'icon-only'` for the non-mobile
51
- breakpoint axis (e.g. a user's own collapse toggle) while `mobile-breakpoint` continues to be tracked
52
- automatically regardless — it's only consulted while `forceMode` is `'auto'` or unset; an explicit
53
- `forceMode` value takes full priority. A `preferredMode` restored from `localStorage` on mount is
54
- observable the same way a live change is: it fires `lr-mode-change` too (see **Events** below),
55
- letting a consumer that syncs app chrome to the rail's mode pick up the restored value on load.
56
-
57
- **Properties:**
58
-
59
- - `frame?: LyraFrame` — optional `'card'` floating frame or `'plain'` edgeless inline surface.
60
- Unset retains the flush rail. Unknown values clear the attribute. In mobile mode framing is inert.
61
- - `triggerCollapses: boolean = false` (attribute `trigger-collapses`) — extend the `trigger`/`for`
62
- association to full/icon-only modes, managing `aria-expanded`, `aria-controls` and
63
- `aria-keyshortcuts`. Wire the trigger's click to `toggle()`. Independent of `collapsible`, which
64
- alone decides whether the built-in collapse control renders.
65
- - `hotkey: string = ''` — optional chord such as `mod+b`, `ctrl+b`, `meta+b`, or `alt+b`.
66
- Requires a non-Shift modifier. Removing the attribute restores the empty default. See the shortcut
67
- behavior below.
68
-
69
-
70
- - `mode: LyraAppRailMode` (custom accessor, reflected, read-only as of 9.0.0) — always resolves to
71
- one of the three real modes (`'full'|'icon-only'|'mobile'`), never `'auto'`; assigning it now
72
- throws (`el.mode = 'icon-only'` -> TypeError).
73
- - `forceMode?: 'full' | 'icon-only' | 'auto'` (attribute `force-mode`, reflected) — replaces `mode`'s
74
- former write side as of 9.0.0. Assigning `'full'`/`'icon-only'` pins that mode and stops the
75
- element responding to breakpoint changes; assigning `'auto'` (or leaving it unset) releases the
76
- pin and resumes automatic viewport tracking. `'mobile'` can never be force-pinned here — the
77
- mobile breakpoint is always tracked automatically regardless, mirroring `preferredMode`'s scope
78
- below; widen `mobile-breakpoint` for a guaranteed-mobile state instead. Settable via the
79
- `force-mode` attribute too (`force-mode="icon-only"`, `force-mode="auto"`).
80
- - `iconOnlyBreakpoint: string = '960px'` (attribute `icon-only-breakpoint`) — any valid CSS length,
81
- used directly in a `(max-width: ...)` media query; below it the rail switches from `'full'` to
82
- `'icon-only'`.
83
- - `mobileBreakpoint: string = '600px'` (attribute `mobile-breakpoint`) — same mechanism; below it the
84
- rail switches from `'icon-only'` to `'mobile'`. Should be smaller than `iconOnlyBreakpoint` to
85
- produce all three states as the viewport narrows.
86
- - `open: boolean = false` (reflected) — whether the mobile floating overlay is shown. Only meaningful
87
- while `mode` is `'mobile'`; leaving mobile mode closes it so a later mobile transition cannot
88
- restore a stale modal. Set this directly, or use the built-in toggle button — there is no separate
89
- `show()`/`hide()` pair.
90
- - `label?: string` — optional accessible name for the rail's navigation landmark and mobile dialog.
91
- Every nonempty supplied string is honored literally; only absence/empty uses the localized
92
- navigation fallback. A host-level `aria-label` attribute (including an explicit empty value)
93
- takes precedence.
94
- - `preferredMode?: 'full' | 'icon-only' | null` (attribute `preferred-mode`) — manually prefers
95
- `'full'` or `'icon-only'` for the non-mobile breakpoint axis, while `mobile-breakpoint` continues to
96
- be tracked automatically regardless — e.g. a user's manual collapse toggle that should still yield
97
- to a genuinely too-narrow-for-any-inline-rail viewport. Only consulted while `forceMode` is
98
- `'auto'` or unset (see above); an explicit `forceMode` value takes full priority.
99
- Unset (the default, `null`) reproduces the original breakpoint-only behavior exactly.
100
- - `collapsible: boolean = false` (reflected) — opts in a desktop collapse control rendered inside
101
- `[part="header"]`. It flips the rail between its `'full'` and `'icon-only'` presentations by
102
- writing `preferredMode`, so the `mobile-breakpoint` keeps being tracked automatically and a
103
- genuinely too-narrow viewport still wins. The control is not rendered at all while `mode` is
104
- `'mobile'`, and `[part="header"]`'s layout is unchanged when this is unset. The collapse survives
105
- a reload only when `storage-key` is set AND `persist` includes `preferred-mode` — the default
106
- `persist` is `open width`, which does not. Pair them: `persist="width preferred-mode"`. Either
107
- route announces itself through the existing `lr-mode-change` event; there is no new event.
108
- - `hideToggle: boolean = false` (reflected, attribute `hide-toggle`) — suppresses the built-in mobile
109
- `[part='toggle']` hamburger/OPEN button, for a consumer that already owns an external mobile-menu
110
- trigger wired to this rail's own `open` property (pair it with `trigger`/`for` below so focus
111
- still returns to that external trigger on close). `false` (the default) reproduces the exact
112
- existing output. This does not remove the button once the overlay is open: at that point it has
113
- been reparented inside the trapped `[part="panel"]` (see the CSS parts entry below) as the
114
- panel's only in-panel dismiss control, and hiding it there too would leave the open panel with no
115
- in-panel way to close it at all besides Escape/backdrop.
116
- - `trigger: HTMLElement | null = null` (attribute: false) — direct reference to an external element
117
- that opens this rail's mobile overlay (e.g. an application-chrome hamburger button used together
118
- with `hideToggle`). When set (or resolved through `for`), closing the overlay by any path —
119
- Escape, backdrop click, a nav-item click, or the built-in toggle itself — returns focus to it, the
120
- same guarantee the built-in toggle's own click already gets. The return is attempted as the
121
- overlay closes and, if that attempt could not land (a host commonly keeps its own menu button
122
- `visibility: hidden` while the drawer is open and re-shows it from its own render in response to
123
- `lr-toggle`), again once the close's update has completed and one animation frame has passed, so
124
- the host's re-render has applied. That second pass only acts while focus is still inside the rail
125
- or has fallen to `<body>` — focus moved elsewhere in the meantime is never taken back — and
126
- focuses the first candidate that can actually hold focus: the trigger, then the element that held
127
- focus when the overlay opened, then the built-in `[part='toggle']` (unavailable under
128
- `hideToggle`), and finally the rail host; when even the host cannot take focus (hidden, or inert —
129
- under an inert ancestor or behind a stacked modal), focus is left where it is. The host
130
- temporarily receives `tabindex="-1"` only when it has no authored tabindex; the temporary
131
- attribute is removed once focus moves off the host or the rail disconnects, while a blur caused
132
- only by the window or tab losing system focus (the host stays the focused element) leaves it in
133
- place, so focus is still on the host when the window regains focus. Needed because a consumer's
134
- own JS-driven `open = true` never focuses anything, and even a real click does not reliably focus
135
- its target in every browser. Resolved when the overlay opens and again when it closes, so
136
- reassigning it (or `for`) while the overlay is open changes where focus returns; when the
137
- association no longer resolves at close, the target resolved at open still applies. Read alongside
138
- `for`; this direct reference wins when both resolve to different elements. Unset (the default,
139
- `null`), no external element is a return candidate: a close still returns focus to the built-in
140
- toggle when its own click opened the overlay, otherwise to the element that held focus at open,
141
- and the deferred pass still runs over its remaining candidates (that element, the built-in toggle,
142
- then the rail host) — only the trigger candidate depends on the association. The resolved trigger
143
- also receives `aria-expanded` (rendered in both states) and `aria-controls` pointing at the rail's
144
- own panel, so an external control announces the overlay's state across the shadow boundary.
145
- Because a light-DOM element cannot hold a raw reference into another element's shadow tree,
146
- engines resolve `aria-controls` to the `<lr-app-rail>` host itself; either resolution is correct.
147
- The association applies while `mode` is `'mobile'`, or in full/icon-only when `trigger-collapses`
148
- is set; otherwise it is released. Disconnect also releases it, and reassigning `trigger` moves the
149
- state to the new element.
150
- - `for: string = ''` — id of an external element that opens this rail's mobile overlay, the
151
- label/`htmlFor`-style alternative to assigning `trigger` directly (mirrors `<lr-page-rail>`'s
152
- `for`). Resolved against this element's own root (shadow root or document) when the overlay opens
153
- and again when it closes. Ignored once `trigger` is itself set.
154
- - `resizable: boolean = false` (reflected) — opts a continuously draggable width in for the `'full'`
155
- state, exposing a `[part='resizer']` handle clamped to `[minRailWidthPx, maxRailWidthPx]`. `false`
156
- (the default) renders no resizer and leaves the fixed-width `--lr-app-rail-width` CSS token
157
- exactly as before this property existed.
158
- - `railWidthPx?: number` (attribute `rail-width-px`) — the rail's current width in px while
159
- `resizable`; settable/gettable directly. Unset defers to `--lr-app-rail-width`'s own resolved
160
- width.
161
- - `storageKey?: string` (attribute `storage-key`) — when set, persists the fields selected by
162
- `persist` to `localStorage` under `lr-app-rail:${storageKey}` and restores them on the next
163
- mount. Each field is restored only when the consumer has not assigned it on that same mount: an
164
- `open`/`rail-width-px`/`preferred-mode` attribute, or a `.open=${false}`-style binding, wins over
165
- stored state, and a restored `open` fires no `lr-toggle`. Effective `mode` is breakpoint-derived
166
- and never persisted. Unset means no persistence.
167
- - `persist: string = 'open width'` — whitespace-separated field allowlist used with `storageKey`.
168
- Valid `LyraAppRailPersistField` tokens are `open`, `width` (`railWidthPx`), and `preferred-mode`
169
- (`preferredMode`). The default preserves the existing open+width behavior. Use
170
- `persist="width preferred-mode"` when overlay-open state is controlled or should stay
171
- session-only.
172
- - `minRailWidthPx: number = 190` (attribute `min-rail-width-px`) — minimum `railWidthPx` a
173
- drag/keyboard resize can reach.
174
- - `maxRailWidthPx: number = 440` (attribute `max-rail-width-px`) — maximum `railWidthPx` a
175
- drag/keyboard resize can reach.
176
- - `dragging: boolean = false` (reflected, read-only as of 9.0.0) — `true` for the duration of an
177
- active pointer-driven resize drag (not a keyboard step); reflected so a consumer (or this
178
- component's own styles) can suppress `[part='base']`'s `transition: inline-size` during the drag,
179
- which otherwise visibly "chases" the pointer instead of tracking it 1:1. This component always
180
- owned every drag transition itself; assigning it now throws (`el.dragging = true` -> TypeError).
181
-
182
- Also settable as a plain `aria-label` attribute (not a reactive property): overrides the computed
183
- `label`/localized-default accessible name on both the navigation landmark and the mobile dialog
184
- role, matching `<lr-date-input>`'s `accessibleLabel`.
185
-
186
- **Methods:** `toggle(): void` opens/closes the mobile overlay through cancelable `lr-toggle`, or
187
- flips the full/icon-only preference. It is a no-op while disconnected. While pinned, it records
188
- only the preference; releasing `forceMode` applies it.
189
-
190
- `toggleCollapse(): void` performs the same `'full'`/`'icon-only'` flip `collapsible`'s
191
- built-in control does, for a consumer rendering its own control (app chrome, a command palette, a
192
- keyboard shortcut). A no-op while `mode` is `'mobile'`. While `forceMode` pins the mode the
193
- preference alternates relative to `preferredMode ?? mode` on every call and takes effect once the pin is released.
194
-
195
- **Events:** `lr-mode-change` (`detail: LyraAppRailModeChangeDetail` = `{ mode: LyraAppRailMode }`; the
196
- effective mode changed, whether from a breakpoint crossing, a `forceMode` assignment, or a
197
- persisted `preferred-mode` restored on mount (`storage-key` + `persist="preferred-mode"`) — the
198
- restored-on-mount case fires once, from the first `updated()` after that mount's render and
199
- attribute reflection have both landed, rather than synchronously during the mount itself; it is
200
- not fired for a redundant reassignment to the mode already in effect, nor when no preferred mode
201
- was persisted), `lr-toggle`
202
- (`detail: LyraAppRailToggleDetail` = `{ open: boolean }`; the mobile overlay is opening or closing — via
203
- the built-in toggle button, Escape, a backdrop click, a nav-item click while open, or a
204
- breakpoint/forced mode change leaving `'mobile'` while open — not fired when a consumer sets `open`
205
- directly. Cancelable for every trigger except the forced mode-change close, which always applies —
206
- vetoing that one would leave `open` stuck `true` in a mode where it's meaningless; call
207
- `preventDefault()` to keep the overlay as it is for the other triggers),
208
- `lr-rail-resize-request` (`detail: LyraAppRailResizeDetail` = `{ widthPx: number }`; a cancelable
209
- proposed width from drag or keyboard stepping, emitted before the component assigns
210
- `railWidthPx` — call `preventDefault()` to keep the current width. A synchronous request listener
211
- that disables resizing, leaves full mode, or disconnects the rail also cancels the proposal,
212
- preserving any width the listener assigned and publishing no accepted resize. Assigning only
213
- `railWidthPx` does not veto the proposal. It is not fired when a consumer sets `railWidthPx` directly), and `lr-rail-resize` (`detail: LyraAppRailResizeDetail` =
214
- `{ widthPx: number }`; non-cancelable committed width, emitted immediately for a genuine keyboard
215
- step and once at pointerup for a genuine drag. Clamped/no-op steps, canceled/lost gestures, and
216
- consumer property writes emit no committed event).
217
-
218
- **Slots:** default (nav items — generic slotted content, e.g. `<a>`/`<button>` elements the consumer
219
- builds with its own icon+label structure; clicking anywhere in this slot closes the mobile overlay if
220
- open), `header` (logo/brand content, shown above the nav items in every mode), `footer` (a trailing
221
- user/settings trigger, shown below the nav items).
222
-
223
- **CSS parts:** `base`, `header`, `nav`, `footer`, `toggle` (hidden via CSS outside `'mobile'` mode, or
224
- -- while it is not also serving as the panel's only in-panel dismiss control -- via `hideToggle`;
225
- reparented to be the first child of `[part="panel"]` for exactly as long as the mobile overlay is
226
- open, so the shared focus trap, scoped to the panel alone, can reach it and Tab cycles through it
227
- like `<lr-dialog>`'s in-panel close button, then moved back to its resting position, a sibling
228
- immediately ahead of `[part="panel"]`, once closed; the same button element is reused throughout,
229
- never destroyed/recreated, so a reference captured before opening remains valid after closing, and
230
- it renders as its own reserved row ahead of the `header` slot rather than an absolute overlay on
231
- top of it, so a wide/slotted header is never obscured), `backdrop`, `panel` (`base`/`panel` are mutually exclusive on the same
232
- underlying element — see above), `resizer` (the `resizable` opt-in's drag handle, only rendered while
233
- `resizable` and `mode` is `'full'`; its hit target is `--lr-icon-button-size`-wide), `resizer-track`
234
- (the slim 3px visible drag line centered inside that hit target, tinted `--lr-color-brand` on hover),
235
- `collapse-toggle` (the opt-in desktop collapse control, rendered inside `[part="header"]` only while
236
- `collapsible` is set and `mode` is not `'mobile'`; it renders `aria-expanded` in both states, points
237
- `aria-controls` at `[part="nav"]` — the item list whose presentation actually changes, never the
238
- containing `[part="base"]`/`[part="panel"]` — and takes a localized name from the
239
- `appRailCollapse`/`appRailExpand` keys) and `collapse-icon` (the chevron wrapper, mirrored by its own
240
- `transform` under RTL). Collapsing to `'icon-only'` keeps each item's own accessible name and
241
- clips its `label`/`meta` visually, while hiding nested disclosure controls and child lists — so `aria-expanded` reports which of the two
242
- presentations is on screen, for magnifier and braille users, rather than announcing hidden content.
243
-
244
- **Themeable custom properties:**
245
- `--lr-app-rail-frame-gap` (default `var(--lr-space-s)`), `--lr-app-rail-frame-radius` (default
246
- `var(--lr-radius)`), and `--lr-app-rail-frame-shadow` (default `var(--lr-shadow-s)`) customize a
247
- card frame. The card host uses `display: flow-root` to contain its margins; a consumer overriding
248
- host `display` owns that formatting context. Plain frames default to a transparent background,
249
- while an explicit `--lr-app-rail-background` still wins. Forced colors restores the plain frame's
250
- boundary. `--lr-app-rail-panel-shadow` (default `var(--lr-shadow-l)`) affects only the open mobile
251
- panel; no value can paint elevation while closed. Prefer that token over an unqualified
252
- `::part(panel)` shadow rule, which would also paint while closed; state-scope a part override with
253
- `lr-app-rail[mode="mobile"][open]::part(panel)`.
254
-
255
- Other hooks: `--lr-app-rail-width` (default `15rem` — the inline rail width in
256
- `'full'` mode), `--lr-app-rail-icon-width` (default `4rem` — the inline rail width in `'icon-only'`
257
- mode), `--lr-app-rail-mobile-width` (default `18rem`, capped at `85vw` — the mobile overlay panel
258
- width), `--lr-app-rail-overlay-color` (default `var(--lr-color-overlay)` — the mobile backdrop scrim
259
- color; component-specific since no shared token exists), `--lr-app-rail-panel-inset-block-start`
260
- (default `0`, applied to both `[part="panel"]` and `[part="backdrop"]` — raise it to leave room for
261
- a fixed app bar/status area above the drawer instead of the panel/scrim starting flush with the
262
- viewport top), `--lr-app-rail-panel-radius` (default `0` — uniform corner radius of `[part="panel"]`;
263
- pairs naturally with a nonzero `--lr-app-rail-panel-inset-block-start`, which exposes the panel's top
264
- corners). Four direction-aware per-corner tokens each default to `--lr-app-rail-panel-radius`, so
265
- setting only the uniform token still rounds all four corners exactly as before:
266
- `--lr-app-rail-panel-radius-start-start` and `--lr-app-rail-panel-radius-end-start` (logical
267
- `border-start-start-radius`/`border-end-start-radius` — the two corners at the panel's own flush
268
- inline-start edge, since the drawer always sits flush against `inset-inline-start: 0`) and
269
- `--lr-app-rail-panel-radius-start-end`/`--lr-app-rail-panel-radius-end-end` (logical
270
- `border-start-end-radius`/`border-end-end-radius` — the two corners away from that flush edge, the
271
- pair a flush-against-one-edge drawer typically rounds). All four are logical, so which physical
272
- corner each one paints swaps under `dir="rtl"` with no second consumer rule.
273
- `--lr-app-rail-panel-overflow-block` (default `auto`) and
274
- `--lr-app-rail-panel-overflow-inline` (default `clip`) — `[part="panel"]`'s logical overflow axes;
275
- either non-`visible` value clips a `position: fixed` popup opened by a slotted/nav-item control
276
- (e.g. a slotted `<lr-select>`/`<lr-menu>`) whenever its rendered box extends past the panel,
277
- regardless of that popup's own containing block. Per the CSS overflow spec, a lone `visible` axis
278
- paired with a non-`visible` other axis computes as `auto` instead (still clipping) — set **both**
279
- tokens to `visible` together to actually stop the clipping, accepting that wide header/footer
280
- content can then scroll/bleed both ways. `--lr-app-rail-background` (default
281
- `var(--lr-color-surface)` — `[part="base"]`'s background, the docked non-overlay presentation) and
282
- `--lr-app-rail-panel-background` (default `var(--lr-color-surface-overlay)` — `[part="panel"]`'s
283
- background, the mobile overlay presentation; kept separate from `--lr-app-rail-background`/
284
- `--lr-app-rail-overlay-color` since the panel is deliberately themed as a modal surface, not the
285
- docked rail chrome). `--lr-app-rail-header-padding` and `--lr-app-rail-footer-padding` (both default
286
- `var(--lr-space-m)`) retune `[part="header"]`/`[part="footer"]`'s padding independently.
287
- `--lr-app-rail-header-min-block-size` (default `auto`, the property's own initial value, so unset
288
- reproduces today's exact height) reserves a minimum height for `[part="header"]`, for content that
289
- mounts or resizes asynchronously. `--lr-app-rail-nav-padding` and `--lr-app-rail-nav-gap` (default
290
- `var(--lr-space-s)`/`var(--lr-space-xs)`, the values this rule hard-coded before either token
291
- existed) retune `[part="nav"]`'s own padding and inter-item gap — the rail's vertical rhythm,
292
- previously reachable only through `::part(nav)`. Plus shared
293
- tokens (`--lr-color-border`, `--lr-color-border-subtle`,
294
- `--lr-color-surface`, `--lr-color-text`, `--lr-color-brand`, `--lr-color-brand-quiet`,
295
- `--lr-space-*`, `--lr-radius`, `--lr-shadow-l` (the mobile overlay panel's default elevation, while
296
- open only), `--lr-shadow-s` (the `frame="card"` default elevation), `--lr-icon-button-size`,
297
- `--lr-focus-ring-*`, `--lr-transition-base`). The rail's inline-end edge and its header/footer rules
298
- use the decorative `--lr-color-border-subtle`, except while the resizer renders (`resizable` in
299
- `'full'` mode): its track is transparent at rest, so the edge is then the separator's only visible
300
- mark and stays on `--lr-color-border`. `resizable`'s width is driven entirely by
301
- `railWidthPx`'s inline `inline-size` style rather than a new custom property.
302
- The mobile toggle's hover/pressed background and foreground are independently inheritable through
303
- `--lr-app-rail-toggle-hover-bg`, `--lr-app-rail-toggle-hover-color`,
304
- `--lr-app-rail-toggle-active-bg`, and `--lr-app-rail-toggle-active-color`. `[part="collapse-toggle"]`
305
- has the matching set: `--lr-app-rail-collapse-toggle-hover-bg` (default
306
- `var(--lr-color-brand-quiet)`), `--lr-app-rail-collapse-toggle-hover-color` (default
307
- `var(--lr-color-brand)`), `--lr-app-rail-collapse-toggle-active-bg` (no default) and
308
- `--lr-app-rail-collapse-toggle-active-color` (default `var(--lr-color-brand)`). The resizer track uses
309
- `--lr-app-rail-resizer-hover-bg` and `--lr-app-rail-resizer-active-bg`. Each hook is an inline
310
- fallback at its exact state rule and preserves the previous brand or active-mix value when unset.
311
-
312
- **Optional peer deps:** none.
313
-
314
- ```html
315
- <lr-app-rail
316
- label="Main navigation"
317
- icon-only-breakpoint="960px"
318
- mobile-breakpoint="600px"
319
- resizable
320
- >
321
- <span slot="header"><img src="/logo.svg" alt="Acme" /></span>
322
- <a href="/inbox" aria-label="Inbox"
323
- ><svg aria-hidden="true">...</svg><span>Inbox</span></a
324
- >
325
- <a href="/settings" aria-label="Settings"
326
- ><svg aria-hidden="true">...</svg><span>Settings</span></a
327
- >
328
- <span slot="footer"><button>Profile</button></span>
329
- </lr-app-rail>
330
- <script type="module">
331
- const rail = document.querySelector("lr-app-rail");
332
- rail.addEventListener("lr-rail-resize-request", (e) => {
333
- if (e.detail.widthPx > 360) e.preventDefault();
334
- });
335
- rail.addEventListener("lr-rail-resize", (e) =>
336
- localStorage.setItem("railWidthPx", String(e.detail.widthPx))
337
- );
338
- </script>
339
- ```
340
-
341
- ```ts
342
- rail.forceMode = "icon-only"; // force a presentation regardless of viewport width
343
- rail.forceMode = "auto"; // release the force, resume live breakpoint tracking
344
- ```
345
-
346
- The package root also exports a pure `computeAppRailMode(iconOnlyMatches: boolean, mobileMatches:
347
- boolean, preferredMode?: 'full' | 'icon-only' | null): LyraAppRailMode` resolver (plus the
348
- `LyraAppRailMode`/`LyraAppRailModeInput`/`LyraAppRailPreferredMode`/`LyraAppRailPersistField`/
349
- `LyraAppRailModeChangeDetail`/`LyraAppRailToggleDetail`/`LyraAppRailResizeDetail` types) — the same
350
- logic the element's internal `matchMedia` listeners call, exposed standalone so a
351
- consumer can compute or unit-test the same resolution without a real browser window. `mobileMatches`
352
- wins over everything else when true (the viewport is narrower than both breakpoints at once);
353
- otherwise `preferredMode` (when set) wins over `iconOnlyMatches`.
354
-
355
- The mobile state keeps its own panel template rather than nesting `<lr-dialog>`, while its modal
356
- behavior participates in the shared overlay stack. It is a plain `<div>` with an explicit
357
- `role="navigation"` (swapping to `role="dialog"` while the overlay is open) rather than a literal
358
- `<nav>` tag, since a
359
- `<nav>`'s implicit role can't be overridden to `role="dialog"` without an `aria-allowed-role`
360
- violation. In `'icon-only'` mode, slotted nav items lose their visible text label — give each one a
361
- real accessible name (`aria-label`, visually hidden text, or `title`) regardless, since this
362
- component only lays out whatever is slotted and can't inspect or fix up a consumer's own markup.
363
-
364
- **Known gotchas:**
365
-
366
- - `mode` is the readonly `LyraAppRailMode` result. `forceMode` accepts the non-mobile
367
- `LyraAppRailPreferredMode` values plus the `'auto'` release sentinel, so whether the rail is
368
- pinned or tracking the viewport remains directly observable.
369
- - reassigning `icon-only-breakpoint`/`mobile-breakpoint` after first render tears down and rebuilds
370
- the `matchMedia` listeners, but does not itself clear `forceMode` — if a consumer set
371
- `forceMode = 'icon-only'`, changing the breakpoints won't resume auto-tracking until
372
- `forceMode = 'auto'` is set explicitly.
373
- - leaving `'mobile'` mode while `open` (via a breakpoint crossing or a `forceMode` reassignment)
374
- auto-closes the overlay through the same path as the toggle button, so `lr-toggle` still fires
375
- and the scroll lock/focus trap still release normally — a consumer listening only for explicit
376
- toggle-button clicks would miss this closure.
377
- - the mobile panel is also given `inert` whenever `mode === 'mobile'` and `open` is `false` — it's
378
- removed from the accessibility tree and tab order via `inert` at the same time it's hidden visually
379
- via `transform: translateX(calc(-100% - var(--lr-size-1px)))`, both applied simultaneously rather than one implying the other.
380
- - the offscreen slide direction for the mobile panel is flipped for RTL via a `:dir(rtl)` CSS
381
- selector (`translateX(calc(100% + var(--lr-size-1px)))`), not through the shared `internal/rtl.ts` JS helper used for pointer/
382
- keyboard math elsewhere in this library — a physical `transform` isn't expressible with logical
383
- properties, so this one case is handled purely in CSS.
384
- - a reconnect that preserves the same element instance (e.g. a drag-and-drop reparent) resumes its
385
- shared overlay registration and re-acquires the scroll lock if the overlay was still active
386
- across the move — `disconnectedCallback`/`connectedCallback` fire back-to-back with no update in
387
- between, so `willUpdate()` alone wouldn't otherwise notice.
388
- - `resizable`'s drag handle is pointer-only (`pointerdown`/`pointermove`/`pointerup`/
389
- `pointercancel`/`lostpointercapture`) plus discrete Left/Right-arrow keyboard stepping (8px per
390
- press, RTL-aware) — there's no dedicated touch gesture beyond what Pointer Events already unify.
391
- - reassigning `railWidthPx` while `resizable` is unset has no visible effect on the rendered width —
392
- the fixed-width `--lr-app-rail-width` token still governs `'full'`-mode width until `resizable`
393
- is also set.
394
- - reassigning `icon-only-breakpoint`/`mobile-breakpoint`/`preferredMode` does not itself un-force a
395
- previously-forced `mode` — same caveat as above, `preferredMode` is only consulted while `mode`
396
- isn't force-pinned.
397
- - the built-in toggle physically moves in the DOM: it is a sibling immediately ahead of
398
- `[part="base"]`/`[part="panel"]` while closed (or outside mobile mode), and the first child of
399
- `[part="panel"]` while the mobile overlay is open. This is a real reparent via `insertBefore`
400
- (not a template-conditional recreate), so an event listener or a captured element reference
401
- stays valid across the transition — a MutationObserver watching a specific fixed container would
402
- still need to account for the move.
403
- - opening the mobile overlay moves initial focus to the first focusable nav item (or the panel
404
- itself when nothing in the slotted content is focusable), never to the toggle — even now that
405
- the toggle lives inside the panel as its structurally-first child. Tab still reaches it as part
406
- of the trap's normal cycle.
407
-
408
- ### Building an app sidebar
409
-
410
- The rail's shortcut uses the same grammar and per-window owner registry as `lr-command-palette`.
411
- The last-connected eligible owner acts once. Rails ignore repeat/composition/key-less events,
412
- `defaultPrevented`, text-entry and contenteditable targets, invisible/inert rails, and pinned modes.
413
- A pinned rail still accepts explicit `toggle()` calls to record a preference. Chords match the
414
- printed key; ASCII letter/digit chords fall back to `event.code` only when the event key is
415
- non-ASCII, preserving Dvorak/AZERTY printed-letter behavior. Mod resolves to Meta on macOS and
416
- Control elsewhere. Built-in controls and applicable external triggers expose `aria-keyshortcuts`.
417
- `lr-button` and `lr-icon-button` forward this and `aria-expanded` to their focused control; other
418
- custom triggers must implement forwarding themselves. Cross-shadow element-reference
419
- `aria-controls` forwarding by those custom triggers is not claimed.
420
-
421
- A hidden rail is not a desktop-offcanvas mode: `toggle()` never clears `hidden`, hotkeys ignore it,
422
- and a `trigger-collapses` trigger reports the last inline presentation. Collapse normally preserves
423
- focus. A focused resizer returns focus to the rail, and a hidden nested control returns it to the
424
- visible parent item.
425
-
426
- **Standalone recipe: viewport offcanvas, one trigger.**
427
-
428
- ```html
429
- <div class="shell">
430
- <lr-app-rail id="sidebar" label="Workspace" frame="card" hide-toggle trigger-collapses
431
- for="sidebar-trigger" hotkey="mod+b" storage-key="app" persist="preferred-mode">
432
- <lr-app-rail-group heading="Platform">
433
- <lr-app-rail-item href="/projects" current tooltip><span slot="icon">▦</span>Projects</lr-app-rail-item>
434
- </lr-app-rail-group>
435
- <lr-divider></lr-divider>
436
- </lr-app-rail>
437
- <main><button id="sidebar-trigger" type="button">Toggle sidebar</button></main>
438
- </div>
439
- ```
440
-
441
- ```css
442
- .shell { display: flex; block-size: 100dvh; }
443
- .shell main { flex: 1; min-inline-size: 0; }
444
- lr-app-rail lr-divider { align-self: stretch; }
445
- ```
446
-
447
- ```js
448
- const rail = document.getElementById('sidebar');
449
- document.getElementById('sidebar-trigger').addEventListener('click', () => rail.toggle());
450
- ```
451
-
452
- Import the rail, group, item and divider from their granular `components/lr-*.js` entries. The
453
- rail owns mobile offcanvas at its viewport breakpoint (600px default). For an inset composition,
454
- use `frame="plain"` and style the sibling main region as a card with surface, radius, border and
455
- shadow tokens. A raised sidebar tint is opt-in with
456
- `--lr-app-rail-background: var(--lr-color-surface-raised)`.
457
-
458
- **Page recipe: allocation offcanvas, two view-scoped controls.**
459
-
460
- ```html
461
- <lr-page id="workspace">
462
- <lr-app-rail slot="navigation" id="workspace-nav" label="Workspace" frame="plain"
463
- mobile-breakpoint="0px" icon-only-breakpoint="0px" collapsible hotkey="mod+b"
464
- storage-key="app" persist="preferred-mode">
465
- <lr-app-rail-item href="/projects"><span slot="icon">▦</span>Projects</lr-app-rail-item>
466
- </lr-app-rail>
467
- <p>Main content</p>
468
- </lr-page>
469
- ```
470
-
471
- ```css
472
- lr-page[view='mobile'] lr-app-rail::part(collapse-toggle) { display: none; }
473
- lr-app-rail lr-divider { align-self: stretch; }
474
- ```
475
-
476
- ```js
477
- const page = document.getElementById('workspace');
478
- const nav = document.getElementById('workspace-nav');
479
- const syncRail = () => { nav.forceMode = page.view === 'mobile' ? 'full' : 'auto'; };
480
- const observer = new MutationObserver(syncRail);
481
- observer.observe(page, { attributes: true, attributeFilter: ['view'] });
482
- syncRail();
483
- // Call observer.disconnect() when disposing this application composition.
484
- ```
485
-
486
- `lr-page` owns the sole drawer at narrow allocations and its built-in or slotted
487
- `navigation-toggle` owns mobile trigger ARIA. The rail's built-in collapse control owns desktop
488
- collapse. An external desktop control may use `trigger-collapses` if hidden in mobile view.
489
- The pin shows the full rail inside the drawer and preserves its desktop preference. The shortcut
490
- acts only on desktop; it cannot open the page drawer. Give the rail a distinct `label`, since the
491
- page and rail navigation landmarks nest. This recipe uses the page's default start-side placement.
492
-
493
- ### `lr-app-rail-item`
494
-
495
- An explicit navigation item for `<lr-app-rail>`. It renders an accessible link when `href` is
496
- set and enabled, otherwise a button; the rail can add its `icon-only` presentation state without
497
- removing the label from the accessibility tree.
498
-
499
- In icon-only presentation the nested disclosure and children list are hidden while `expanded`
500
- is preserved. Returning full restores the list. If a disclosure or descendant had focus, it moves
501
- to the outermost visible parent item's base control. The parent link remains operable. The `end`
502
- slot stays visible; reserve an icon-only rail width of at least twice the nav padding plus the icon
503
- square, item gap and end content. A 1.5rem badge requires 5.5rem with default tokens. Keep end actions
504
- out of default-width compact rails, or use a rail that never collapses.
505
-
506
- **Properties:**
507
-
508
- - `href: string = ''` — optional destination. Without it, the item renders as a button.
509
- - `target: string = ''` — optional link target.
510
- - `disabled: boolean = false` (reflected) — prevents activation while retaining the item in the rail.
511
- - `current: boolean = false` (reflected) — marks this as the destination for the current page/view;
512
- reflects `aria-current="page"` on `[part='base']` and drives the current visual treatment. The rail
513
- has no built-in routing, so the consumer sets this per item (e.g. by comparing `href` against the
514
- current location). `active`, a deprecated alias in both property and attribute form, was removed
515
- in 16.0.0 (available since 11.2.0; eligible for removal from 13.0.0) — use `current`.
516
- - `tooltip: boolean = false` (reflected) — opt-in hover or keyboard-focus flyout (the focused control matches `:focus-visible` and no pointer press preceded it)
517
- (`[part='tooltip']`) showing
518
- this item's label text while the rail's `icon-only` mode (set externally by the parent
519
- `<lr-app-rail>` as the viewport narrows) hides it from view. No effect outside icon-only mode,
520
- since the label is already visible there. `false` (the default) reproduces the exact existing
521
- output.
522
- - `expanded: boolean = false` (reflected) — whether this item's own `children` are shown. `false`
523
- reproduces exactly what an item without this property rendered before this feature existed.
524
- Driven through the same request/commit pair as `<lr-app-rail-group>`'s `open`, see Events below.
525
-
526
- A host `aria-label` is copied to the rendered native link or button by attribute presence,
527
- including an explicitly empty value; without it, the default slot supplies the native name. The
528
- same precedence supplies the tooltip text when that opt-in flyout is visible, and the disclosure's
529
- interpolated `{label}` (see Events below).
530
-
531
- **Events:** `lr-toggle-request` — cancelable, emitted before `expanded` changes from the built-in
532
- disclosure (`detail: { open }` — the field is named `open`, matching `<lr-app-rail-group>`'s
533
- identical event name and detail shape exactly). Call `preventDefault()` to keep the current state,
534
- or assign `expanded` from the listener to resolve it yourself; a write during the dispatch
535
- suppresses the default commit even when it assigns the value the property already held. Not
536
- emitted for a direct `expanded` write. `lr-toggle` — non-cancelable, emitted after `expanded` is
537
- written, never for a vetoed or listener-resolved request (`detail: { open }`).
538
-
539
- **Methods:** `click(): void` activates the internal native link or button; it is a no-op while
540
- `disabled`.
541
-
542
- If an `href`/`disabled` update replaces a focused link or button, focus follows an available native
543
- replacement. When that replacement is disabled or inert, focus returns to the available element
544
- that led into the item, or to the stable owning rail surface when no return target exists. A newer
545
- external focus move is always preserved, and this repair dispatches no activation event.
546
-
547
- **Slots:** default (the visible label), `icon` (the leading decorative icon, always hidden from
548
- assistive technology and inert across its flattened subtree; the default slot or host `aria-label`
549
- names the native control, which remains the sole action).
550
-
551
- - `meta` slot — secondary trailing text (an unread count, a keyboard shortcut). Rendered as a
552
- SIBLING of the item's own link/button, so its text is not part of the item's accessible name and a
553
- pointer landing on it does not activate the item. Visually clipped in `icon-only` mode exactly as
554
- the label is, staying available to assistive technology.
555
- - `end` slot — trailing controls or adornments (an overflow-menu trigger, a status badge). Also a
556
- sibling of the link/button — the shape `<lr-details>` uses for `header-actions` — so a slotted
557
- control keeps its own click, keyboard activation and focus order instead of being swallowed.
558
- Unlike `meta` it stays visible in `icon-only` mode, where it shares the narrow rail's width with
559
- the icon.
560
- - `children` slot — nested `<lr-app-rail-item>`s disclosed beneath this item (the
561
- treeitem-with-link pattern: the row itself navigates, a separate disclosure expands its own
562
- child rows). Slotting anything into it grows a built-in `[part="toggle"]` disclosure button as a
563
- SIBLING of the item's own link/button, never nested inside it, so the link keeps navigating on
564
- its own and the disclosure keeps toggling on its own — clicking one never triggers the other.
565
- Leaving `children` empty renders neither the disclosure nor `[part="children"]` at all: an item
566
- authored without any `children` content renders byte-identically to one authored before this
567
- slot existed. The disclosure carries `aria-expanded` (both states) and `aria-controls` pointing
568
- at `[part="children"]`'s id, and a localized accessible name interpolating this item's own label
569
- (`Expand {label}`/`Collapse {label}` in the default locale — no literal fallback, so a
570
- `registerLyraLocale()` translation or a `.strings` override always reaches it). `<lr-app-rail-group>`
571
- cannot express this pattern: its collapsible heading *is* the toggle, so a navigable link cannot
572
- live inside it without nesting an interactive element inside a button.
573
-
574
- `icon-only` forwards from this item onto every `<lr-app-rail-item>` it directly owns through
575
- `children` — including ones appended later — exactly how `<lr-app-rail-group>` forwards onto the
576
- items and nested groups it owns. The disclosure is a fixed icon-button-sized square in full
577
- presentation, sharing the link/button state tokens. Icon-only hides it and the nested list while
578
- preserving expansion and recovering focus to the visible parent item. There is no ancestor-current treatment — `<lr-app-rail-group>` has
579
- no equivalent concept for a group containing the current item, so none is invented here either; a
580
- current descendant stays perceivable only through its own `current` property.
581
-
582
- Both wrappers (`[part="meta"]`, `[part="end"]`) are hidden while empty, so an item using neither
583
- renders exactly as before. Note that while the mobile overlay is open, a click anywhere in the
584
- rail's default slot closes it — including a click on an `end` control; that is the rail's documented
585
- nav-slot behaviour, not new to these slots.
586
-
587
- **CSS parts:** `base`, `icon`, `label`, `current-indicator` (a decorative inline indicator rendered
588
- only while the item is `current`/`aria-current="page"`, mirroring `<lr-conversation-item>`'s
589
- shipped `active-indicator` part — suppressed by default while `icon-only`, see the current-ring
590
- tokens below), `tooltip` (the hover or keyboard-focus flyout (the focused control matches `:focus-visible` and no pointer press preceded it), only rendered while
591
- `tooltip` is set, the item is `icon-only`, and it is hovered or keyboard-focused), `meta` (the wrapper around the `meta` slot,
592
- hidden while empty), `end` (the wrapper around the `end` slot, hidden while empty), `toggle` (the
593
- `children` disclosure, rendered only while something is slotted into `children`; a sibling of
594
- `base`, never nested inside it), `toggle-icon` (the wrapper around the disclosure chevron,
595
- direction-aware through this wrapper's own `transform` — mirrors `<lr-app-rail-group>`'s own
596
- `[part="toggle-icon"]`) and `children` (the wrapper around the `children` slot, rendered only
597
- alongside `toggle`; hidden — but present, so `aria-controls` keeps resolving — while `expanded` is
598
- `false`).
599
-
600
- **Themeable custom properties:** `--lr-app-rail-item-current-bg` (default
601
- `var(--lr-color-brand-quiet)`), `--lr-app-rail-item-current-color` (default
602
- `var(--lr-color-brand)`), and `--lr-app-rail-item-current-font-weight` (default
603
- `var(--lr-font-weight-semibold)`) — background, text/icon color, and font weight of the `current`
604
- (`aria-current="page"`) item. All three are scoped to `[aria-current='page']` only and declared as
605
- inline `var()` fallbacks at the point of use, never on `:host`, so any can be set on the item itself
606
- _or on any ancestor_ — including on `<lr-app-rail>` or a wrapper above it, to retheme every item's
607
- current state at once. `::part(base)[aria-current='page']` is invalid CSS (Shadow Parts forbids an
608
- attribute selector after `::part()`), so before these hooks the only lever was overriding the
609
- library-wide `--lr-color-brand-quiet`/`--lr-color-brand`/`--lr-font-weight-semibold` tokens, which
610
- repainted every other element reading them. Unset, each falls back to the token its rule used
611
- before. `--lr-app-rail-item-current-font-weight` mirrors `<lr-stepper>`'s
612
- `--lr-stepper-current-font-weight` and `<lr-segmented>`'s `--lr-segmented-selected-font-weight`.
613
- `--lr-app-rail-item-current-indicator-color` (default `var(--lr-color-brand)`),
614
- `--lr-app-rail-item-current-indicator-width` (default `var(--lr-size-2px)`), and
615
- `--lr-app-rail-item-current-indicator-inset-inline` (default `0 auto`; set `auto 0` to place the
616
- indicator at the inline-end edge instead) theme `[part="current-indicator"]`.
617
- `--lr-app-rail-item-current-indicator-display` (no default; unset resolves to `none` while
618
- `icon-only`) restores the indicator bar in icon-only presentation — a full-height edge bar reads
619
- as a rendering glitch on the square icon-only tile, so it is suppressed there by default; full
620
- presentation is unaffected either way, since its own `[part="current-indicator"]` rule declares no
621
- `display` at all. `--lr-app-rail-item-current-ring` (no default; unset resolves to `none` in full
622
- presentation and an inset ring in icon-only presentation) sets `box-shadow` on `[part="base"]`
623
- while current: unset, icon-only gets an inset ring automatically — the non-color-only signal
624
- (WCAG 1.4.1) that replaces the bar suppressed there, since full presentation already conveys
625
- current state through the indicator bar and `--lr-app-rail-item-current-font-weight`. Setting this
626
- token explicitly applies the same value in both presentations.
627
- Ordinary interaction states are independently inheritable through
628
- `--lr-app-rail-item-hover-bg`, `--lr-app-rail-item-hover-color`,
629
- `--lr-app-rail-item-active-bg`, and `--lr-app-rail-item-active-color`, again retaining the former
630
- brand/active-mix values as fallbacks.
631
- `--lr-app-rail-item-min-block-size` (default `var(--lr-icon-button-size)`, floor-clamped to that
632
- same token regardless of the override so the row's own hit target can never shrink below the WCAG
633
- 2.5.8 minimum), `--lr-app-rail-item-padding` (default `var(--lr-space-s)`),
634
- `--lr-app-rail-item-gap` (default `var(--lr-space-s)`, the gap between `[part="icon"]` and
635
- `[part="label"]`, and now also between the item's control and the `meta`/`end` adornments),
636
- `--lr-app-rail-item-meta-color` (default `var(--lr-color-text-quiet)`),
637
- `--lr-app-rail-item-meta-font-size` (default `var(--lr-font-size-sm)`),
638
- `--lr-app-rail-item-icon-size` (default `var(--lr-icon-button-size)`, not
639
- floor-clamped since the icon is decorative, not itself a pointer target), and
640
- `--lr-app-rail-item-font-size` (default `inherit`, set after the `font` shorthand so only the size
641
- is retuned while family/weight/line-height stay inherited) retune the row's geometry.
642
- While `icon-only`, `[part="base"]` resolves to a square hit target matching the icon-button
643
- footprint used elsewhere in this library (`aspect-ratio: 1` against its already floor-clamped
644
- block size) instead of stretching across the rail's icon column. `--lr-app-rail-item-icon-only-size`
645
- (no default) sizes that square directly — both `inline-size` and `block-size`, and the row's own
646
- `min-block-size` floor — independent of `--lr-app-rail-item-min-block-size`, so a taller expanded
647
- row and an icon-only square pinned to `--lr-icon-button-size` can coexist. Unset, the square is
648
- still derived via `aspect-ratio: 1` against the row's block size exactly as before.
649
-
650
- **`--lr-positioning-strategy`** (16.0.0) — the icon-only flyout tooltip reads this same cascading
651
- `absolute`/`fixed` override documented on `<lr-popover>` when it is (re)positioned, falling back to
652
- its own `fixed` default when nothing is set. There is no per-instance `positioning-strategy`
653
- property on `<lr-app-rail-item>`; set the custom property on `:root`, a theme, or one clipping
654
- ancestor to change every unset rail item's flyout beneath it.
655
-
656
- `--lr-app-rail-item-indent` (default `var(--lr-space-l)`) sets `[part="children"]`'s
657
- `padding-inline-start`. Applied once per nesting level — a doubly-nested `children` list compounds
658
- two insets automatically, since each level's own `[part="children"]` applies the token again.
659
- Logical, so it mirrors under `dir="rtl"` with no separate rule.
660
-
661
- **Optional peer deps:** none.
662
-
663
- ---
664
-
665
- ### `lr-app-rail-group`
666
-
667
- Titles, and optionally collapses, a section of `<lr-app-rail-item>`s. Grouping is by composition —
668
- the group holds whatever it is given; there is no items array and no renderer callback, so it can
669
- never disagree with what is rendered inside it.
670
-
671
- ```html
672
- <script type="module">
673
- import '@aceshooting/lyra-ui/components/layout/app-rail-group/app-rail-group.js';
674
- </script>
675
-
676
- <lr-app-rail>
677
- <lr-app-rail-group heading="Workspaces" collapsible>
678
- <button slot="header-actions" aria-label="Add workspace">+</button>
679
- <lr-app-rail-item href="/atlas" current>Atlas</lr-app-rail-item>
680
- <lr-app-rail-item href="/beacon">Beacon</lr-app-rail-item>
681
- </lr-app-rail-group>
682
- </lr-app-rail>
683
- ```
684
-
685
- **Properties:**
686
-
687
- - `heading: string = ''` — the section title. The `heading` slot replaces it when populated.
688
- - `headingLevel: number = 3` (attribute `heading-level`) — the `aria-level` the heading landmark
689
- reports. Clamped to 1-6 and rounded; a non-finite value falls back to `3`. Settable because a rail
690
- sits at a different depth in every page that embeds it.
691
- - `collapsible: boolean = false` (reflected) — opts in the built-in collapse control. The heading's
692
- own text becomes the button carrying `aria-expanded` and `aria-controls`, which is the accordion
693
- pattern; an unnamed group falls back to a localized `Collapse`/`Expand` name.
694
- - `open: boolean = true` (reflected) — whether the content is shown. Carries a true-default
695
- converter, so `open="false"` parses from markup (a plain presence-based boolean cannot). `open`
696
- governs visibility whether or not `collapsible` is set, so a consumer can drive collapse entirely
697
- from its own chrome.
698
-
699
- **Events:** `lr-toggle-request` — cancelable, emitted before `open` changes from the built-in
700
- control (`detail: { open }`). Call `preventDefault()` to keep the current state, or assign `open`
701
- from the listener to resolve it yourself; a write during the dispatch suppresses the default commit
702
- even when it assigns the value the property already held. Not emitted for a direct `open` write.
703
- `lr-toggle` — non-cancelable, emitted after `open` is written, never for a vetoed or
704
- listener-resolved request (`detail: { open }`).
705
-
706
- **Slots:** default — the group's items, and any nested `<lr-app-rail-group>`s; `heading` — rich
707
- heading content; `header-actions` — controls beside the heading, rendered as a sibling of the
708
- collapse control so activating one never toggles the group.
709
-
710
- **CSS parts:** `base`, `header`, `heading`, `heading-text`, `toggle`, `toggle-icon`,
711
- `header-actions`, `content`.
712
-
713
- **Themeable custom properties:** `--lr-app-rail-group-gap` (default `var(--lr-space-xs)`),
714
- `--lr-app-rail-group-padding-block` (default `var(--lr-space-xs)`),
715
- `--lr-app-rail-group-heading-color` (default `var(--lr-color-text-quiet)`),
716
- `--lr-app-rail-group-heading-font-size` (default `var(--lr-font-size-sm)`),
717
- `--lr-app-rail-group-hover-bg` (default `var(--lr-color-brand-quiet)`),
718
- `--lr-app-rail-group-hover-color` (default `var(--lr-color-brand)`),
719
- `--lr-app-rail-group-active-bg` (no default), `--lr-app-rail-group-active-color` (default
720
- `var(--lr-color-brand)`).
721
-
722
- The owning rail marks a slotted group `icon-only` exactly as it marks a slotted item, and the group
723
- forwards that to the items and nested groups it *directly* owns — including ones appended later —
724
- so grouping survives the rail's icon-only presentation. A nested group re-forwards in turn, so
725
- exactly one element ever writes `icon-only` onto any given node and a nested group clips its own
726
- heading too. In that mode the heading text is clipped out of layout — whether or not the group is
727
- collapsible — but stays in the accessibility tree. While `collapsible` too, `[part="toggle"]`
728
- resolves to a square hit target matching the icon-button footprint used elsewhere in this library,
729
- instead of stretching across the header row (mirroring `<lr-app-rail-item>`'s own `[part="base"]`).
730
-
731
- **Optional peer deps:** none.
732
-
733
- ---