@aceshooting/lyra-ui 23.0.0 → 24.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 (280) hide show
  1. package/CHANGELOG.md +16 -39
  2. package/README.md +72 -35
  3. package/custom-elements.json +1 -1
  4. package/design-tokens.json +1 -1
  5. package/dist/cli/lyra-rename-ledger.mjs +1 -1
  6. package/dist/cli/migration-contract.json +1 -1
  7. package/dist/cli/migration-contract.mjs +1 -1
  8. package/dist/cli/migration-renames.mjs +2 -2
  9. package/dist/cli/migration-transforms.mjs +2 -2
  10. package/dist/components/agent-tools/approval-queue/approval-queue.class.d.ts +1 -4
  11. package/dist/components/agent-tools/approval-queue/approval-queue.class.js +1 -3
  12. package/dist/components/agent-tools/compare-panel/compare-panel.class.d.ts +1 -4
  13. package/dist/components/agent-tools/compare-panel/compare-panel.class.js +1 -1
  14. package/dist/components/agent-tools/confirm-bar/confirm-bar.class.d.ts +1 -7
  15. package/dist/components/agent-tools/confirm-bar/confirm-bar.class.js +2 -2
  16. package/dist/components/agent-tools/context-inspector/context-inspector.class.d.ts +1 -1
  17. package/dist/components/agent-tools/evaluation-run/evaluation-run.class.d.ts +2 -5
  18. package/dist/components/agent-tools/evaluation-run/evaluation-run.class.js +1 -3
  19. package/dist/components/agent-tools/terminal/terminal.class.d.ts +1 -4
  20. package/dist/components/agent-tools/terminal/terminal.class.js +2 -2
  21. package/dist/components/agent-tools/tool-approval-dialog/tool-approval-dialog.class.d.ts +1 -7
  22. package/dist/components/agent-tools/tool-approval-dialog/tool-approval-dialog.class.js +1 -1
  23. package/dist/components/agent-tools/tool-timeline/tool-timeline.class.d.ts +1 -4
  24. package/dist/components/agent-tools/tool-timeline/tool-timeline.class.js +1 -3
  25. package/dist/components/charts/chart/lite-chart-axis-layout.d.ts +13 -0
  26. package/dist/components/charts/chart/lite-chart-axis-layout.js +1 -0
  27. package/dist/components/charts/chart/lite-chart.class.d.ts +1 -11
  28. package/dist/components/charts/chart/lite-chart.class.js +4 -4
  29. package/dist/components/conversation/agent-workspace/agent-workspace.class.d.ts +13 -3
  30. package/dist/components/conversation/agent-workspace/agent-workspace.class.js +1 -2
  31. package/dist/components/conversation/chat-message/chat-message.class.d.ts +3 -9
  32. package/dist/components/conversation/chat-message/chat-message.class.js +1 -1
  33. package/dist/components/conversation/code-block/code-block-base.class.js +1 -1
  34. package/dist/components/conversation/code-block/code-block-core.class.d.ts +6 -8
  35. package/dist/components/conversation/code-block/code-block-shared.d.ts +1 -4
  36. package/dist/components/conversation/code-block/code-block.class.d.ts +4 -6
  37. package/dist/components/conversation/markdown/markdown-base.class.d.ts +1 -3
  38. package/dist/components/conversation/markdown/markdown-base.class.js +1 -1
  39. package/dist/components/conversation/markdown/markdown-core.class.d.ts +1 -4
  40. package/dist/components/conversation/markdown/markdown.class.d.ts +1 -4
  41. package/dist/components/conversation/message-actions/message-actions.class.d.ts +1 -4
  42. package/dist/components/conversation/message-actions/message-actions.class.js +1 -1
  43. package/dist/components/conversation/message-feedback/message-feedback.class.d.ts +1 -4
  44. package/dist/components/conversation/message-feedback/message-feedback.class.js +2 -2
  45. package/dist/components/conversation/message-parts/message-parts.class.d.ts +2 -5
  46. package/dist/components/conversation/message-parts/message-parts.class.js +3 -3
  47. package/dist/components/conversation/streaming-text/streaming-text-base.class.d.ts +1 -1
  48. package/dist/components/conversation/streaming-text/streaming-text-core.class.d.ts +2 -0
  49. package/dist/components/conversation/streaming-text/streaming-text-core.class.js +0 -1
  50. package/dist/components/conversation/streaming-text/streaming-text.class.d.ts +2 -0
  51. package/dist/components/conversation/streaming-text/streaming-text.class.js +0 -1
  52. package/dist/components/conversation/thread-list/thread-list.class.d.ts +6 -17
  53. package/dist/components/conversation/thread-list/thread-list.class.js +6 -6
  54. package/dist/components/data/context-meter/context-meter.class.d.ts +1 -6
  55. package/dist/components/data/context-meter/context-meter.class.js +1 -1
  56. package/dist/components/data/data-grid/data-grid.class.d.ts +0 -1
  57. package/dist/components/data/data-grid/data-grid.class.js +2 -2
  58. package/dist/components/data/document-library/document-library.class.d.ts +6 -13
  59. package/dist/components/data/document-library/document-library.class.js +1 -2
  60. package/dist/components/data/heatmap/heatmap-grid-navigation.d.ts +7 -0
  61. package/dist/components/data/heatmap/heatmap-grid-navigation.js +1 -0
  62. package/dist/components/data/heatmap/heatmap.class.d.ts +3 -23
  63. package/dist/components/data/heatmap/heatmap.class.js +2 -2
  64. package/dist/components/data/sequence-strip/sequence-strip.class.d.ts +3 -9
  65. package/dist/components/data/sequence-strip/sequence-strip.class.js +3 -3
  66. package/dist/components/data/table/table.class.d.ts +5 -14
  67. package/dist/components/data/table/table.class.js +6 -6
  68. package/dist/components/forms/button/button.class.d.ts +2 -2
  69. package/dist/components/forms/button/button.class.js +9 -9
  70. package/dist/components/forms/combobox/combobox.class.d.ts +1 -4
  71. package/dist/components/forms/combobox/combobox.class.js +2 -2
  72. package/dist/components/forms/input/time-input.class.d.ts +1 -1
  73. package/dist/components/forms/input/time-input.class.js +1 -1
  74. package/dist/components/forms/select/select.class.d.ts +1 -2
  75. package/dist/components/forms/swatch-picker/swatch-picker.class.d.ts +3 -1
  76. package/dist/components/forms/swatch-picker/swatch-picker.class.js +1 -1
  77. package/dist/components/layout/app-rail/app-rail-item.class.d.ts +3 -6
  78. package/dist/components/layout/app-rail/app-rail-item.class.js +1 -1
  79. package/dist/components/layout/app-rail/app-rail.class.d.ts +2 -4
  80. package/dist/components/layout/app-rail/app-rail.class.js +1 -1
  81. package/dist/components/layout/app-rail-group/app-rail-group.class.d.ts +2 -6
  82. package/dist/components/layout/app-rail-group/app-rail-group.class.js +1 -1
  83. package/dist/components/layout/carousel/carousel.class.d.ts +1 -4
  84. package/dist/components/layout/carousel/carousel.class.js +2 -2
  85. package/dist/components/layout/command-palette/command-palette.class.js +1 -1
  86. package/dist/components/layout/details/details.class.d.ts +2 -4
  87. package/dist/components/layout/details/details.class.js +1 -1
  88. package/dist/components/layout/dock-panel/dock-panel.class.d.ts +3 -7
  89. package/dist/components/layout/dock-panel/dock-panel.class.js +1 -1
  90. package/dist/components/layout/menu/menu.class.js +1 -1
  91. package/dist/components/layout/multi-split/multi-split.class.d.ts +2 -4
  92. package/dist/components/layout/multi-split/multi-split.class.js +1 -1
  93. package/dist/components/layout/navigation-menu-item/navigation-menu-item.class.d.ts +2 -4
  94. package/dist/components/layout/navigation-menu-item/navigation-menu-item.class.js +1 -1
  95. package/dist/components/layout/reorder-list/reorder-item.class.d.ts +1 -4
  96. package/dist/components/layout/reorder-list/reorder-item.class.js +2 -2
  97. package/dist/components/media/attachment-trigger/attachment-trigger.class.d.ts +5 -9
  98. package/dist/components/media/attachment-trigger/attachment-trigger.class.js +5 -5
  99. package/dist/components/media/av-player/av-player.class.js +3 -3
  100. package/dist/components/media/file-input/file-input.class.d.ts +3 -6
  101. package/dist/components/media/file-input/file-input.class.js +3 -3
  102. package/dist/components/media/lightbox/lightbox.class.d.ts +11 -20
  103. package/dist/components/media/lightbox/lightbox.class.js +1 -1
  104. package/dist/components/media/map/map-data-layers.d.ts +20 -0
  105. package/dist/components/media/map/map-data-layers.js +1 -0
  106. package/dist/components/media/map/map.class.js +3 -3
  107. package/dist/components/media/media-time.d.ts +2 -0
  108. package/dist/components/media/media-time.js +1 -0
  109. package/dist/components/media/video/video.class.js +5 -5
  110. package/dist/components/media/video-playlist/video-playlist.class.js +2 -2
  111. package/dist/components/overlays/callout/callout.class.d.ts +2 -5
  112. package/dist/components/overlays/callout/callout.class.js +2 -2
  113. package/dist/components/overlays/dialog/dialog.class.d.ts +4 -7
  114. package/dist/components/overlays/dialog/dialog.class.js +2 -2
  115. package/dist/components/overlays/overlay/dropdown.class.d.ts +5 -10
  116. package/dist/components/overlays/overlay/dropdown.class.js +2 -2
  117. package/dist/components/overlays/overlay/popover.class.d.ts +2 -3
  118. package/dist/components/overlays/popup/popup.class.js +1 -1
  119. package/dist/components/overlays/progress/progress-bar.class.d.ts +1 -4
  120. package/dist/components/overlays/progress/progress-bar.class.js +2 -2
  121. package/dist/components/overlays/progress/progress-ring.class.d.ts +1 -4
  122. package/dist/components/overlays/progress/progress-ring.class.js +2 -2
  123. package/dist/components/overlays/progress/progress-shared.d.ts +3 -3
  124. package/dist/components/overlays/progress/progress-shared.js +1 -1
  125. package/dist/components/overlays/toast/toast-item.class.js +1 -1
  126. package/dist/components/retrieval/graph/graph.class.d.ts +5 -10
  127. package/dist/components/retrieval/graph/graph.class.js +1 -1
  128. package/dist/components/retrieval/knowledge-base/knowledge-base.class.d.ts +7 -14
  129. package/dist/components/retrieval/knowledge-base/knowledge-base.class.js +2 -3
  130. package/dist/components/retrieval/knowledge-graph-explorer/knowledge-graph-explorer.class.d.ts +2 -8
  131. package/dist/components/retrieval/memory-panel/memory-panel.class.js +0 -4
  132. package/dist/components/retrieval/rag-answer/rag-answer.class.js +1 -1
  133. package/dist/components/utility/export-button/export-button.class.d.ts +6 -10
  134. package/dist/components/utility/export-button/export-button.class.js +1 -1
  135. package/dist/components/utility/mention-popover/mention-popover.class.js +1 -1
  136. package/dist/components/viewers/geojson-view/geojson-view.class.d.ts +1 -1
  137. package/dist/components/viewers/highlight-layer/highlight-layer-timing.js +1 -1
  138. package/dist/components/viewers/pdf-viewer/pdf-viewer.class.js +4 -4
  139. package/dist/events.d.ts +17 -78
  140. package/dist/framework-types.d.ts +1 -1
  141. package/dist/internal/catalog-picker.js +1 -1
  142. package/dist/internal/css-motion-time.d.ts +7 -0
  143. package/dist/internal/css-motion-time.js +1 -0
  144. package/dist/internal/data-state-renderer.d.ts +1 -3
  145. package/dist/internal/data-state-renderer.js +1 -1
  146. package/dist/internal/localization-runtime.d.ts +2 -0
  147. package/dist/internal/localization-runtime.js +1 -1
  148. package/dist/internal/lyra-element.js +2 -2
  149. package/dist/internal/package-metadata.d.ts +1 -1
  150. package/dist/internal/package-metadata.js +1 -1
  151. package/dist/looks/data.css +1 -1
  152. package/dist/looks/terminal.css +1 -1
  153. package/dist/lyra.d.ts +1 -592
  154. package/dist/lyra.js +1 -1
  155. package/dist/testing/event-factory.d.ts +2 -2
  156. package/dist/testing/interaction-drivers.d.ts +5 -3
  157. package/dist/testing/lyra-tag-event-map.js +1 -1
  158. package/dist/theme/looks/data.d.ts +1 -1
  159. package/dist/theme/looks/data.js +1 -1
  160. package/dist/theme/looks/terminal.d.ts +1 -1
  161. package/dist/theme/looks/terminal.js +1 -1
  162. package/dist/theme/theme.d.ts +2 -80
  163. package/dist/theme/theme.js +1 -1
  164. package/llms/components/lr-agent-workspace.md +24 -2
  165. package/llms/components/lr-alert.md +1 -1
  166. package/llms/components/lr-animation.md +1 -1
  167. package/llms/components/lr-app-rail.md +8 -10
  168. package/llms/components/lr-approval-queue.md +2 -4
  169. package/llms/components/lr-attachment-trigger.md +3 -4
  170. package/llms/components/lr-badge.md +2 -2
  171. package/llms/components/lr-button.md +45 -99
  172. package/llms/components/lr-callout.md +3 -4
  173. package/llms/components/lr-carousel.md +3 -5
  174. package/llms/components/lr-chat-message.md +1 -2
  175. package/llms/components/lr-claim-evidence.md +1 -1
  176. package/llms/components/lr-code-block-core.md +2 -2
  177. package/llms/components/lr-code-block.md +5 -6
  178. package/llms/components/lr-color-picker.md +1 -1
  179. package/llms/components/lr-combobox.md +5 -6
  180. package/llms/components/lr-compare-panel.md +2 -4
  181. package/llms/components/lr-confirm-bar.md +3 -7
  182. package/llms/components/lr-context-inspector.md +3 -2
  183. package/llms/components/lr-context-menu.md +2 -2
  184. package/llms/components/lr-context-meter.md +2 -4
  185. package/llms/components/lr-copy-button.md +1 -1
  186. package/llms/components/lr-data-grid.md +4 -7
  187. package/llms/components/lr-details.md +3 -3
  188. package/llms/components/lr-dialog.md +4 -6
  189. package/llms/components/lr-dock-panel.md +5 -6
  190. package/llms/components/lr-document-library.md +5 -7
  191. package/llms/components/lr-drawer.md +3 -3
  192. package/llms/components/lr-dropdown.md +4 -5
  193. package/llms/components/lr-eval-run.md +3 -5
  194. package/llms/components/lr-export-button.md +4 -7
  195. package/llms/components/lr-file-input.md +0 -5
  196. package/llms/components/lr-funnel.md +1 -1
  197. package/llms/components/lr-graph.md +3 -7
  198. package/llms/components/lr-icon.md +1 -1
  199. package/llms/components/lr-include.md +1 -1
  200. package/llms/components/lr-input.md +31 -79
  201. package/llms/components/lr-json-viewer.md +1 -1
  202. package/llms/components/lr-knowledge-base.md +4 -6
  203. package/llms/components/lr-knowledge-graph-explorer.md +2 -5
  204. package/llms/components/lr-known-date.md +1 -1
  205. package/llms/components/lr-lightbox.md +9 -7
  206. package/llms/components/lr-lite-chart.md +1 -5
  207. package/llms/components/lr-markdown-core.md +3 -4
  208. package/llms/components/lr-markdown.md +3 -5
  209. package/llms/components/lr-mcp-app.md +1 -1
  210. package/llms/components/lr-message-actions.md +2 -3
  211. package/llms/components/lr-message-feedback.md +1 -3
  212. package/llms/components/lr-message-parts.md +4 -5
  213. package/llms/components/lr-multi-split.md +8 -7
  214. package/llms/components/lr-native-time-input.md +1 -1
  215. package/llms/components/lr-navigation-menu.md +5 -5
  216. package/llms/components/lr-number-input.md +3 -1
  217. package/llms/components/lr-otp-input.md +2 -0
  218. package/llms/components/lr-page.md +1 -1
  219. package/llms/components/lr-pan-zoom.md +1 -1
  220. package/llms/components/lr-phone-input.md +3 -1
  221. package/llms/components/lr-popover.md +2 -2
  222. package/llms/components/lr-progress-bar.md +3 -5
  223. package/llms/components/lr-progress-ring.md +5 -7
  224. package/llms/components/lr-prompt-input.md +1 -1
  225. package/llms/components/lr-prompt-queue.md +1 -1
  226. package/llms/components/lr-prompt-studio.md +1 -1
  227. package/llms/components/lr-rag-eval-dashboard.md +1 -1
  228. package/llms/components/lr-rating.md +2 -2
  229. package/llms/components/lr-realtime-session.md +1 -1
  230. package/llms/components/lr-reorder-item.md +1 -1
  231. package/llms/components/lr-reorder-list.md +2 -3
  232. package/llms/components/lr-retrieval-compare.md +1 -1
  233. package/llms/components/lr-select.md +3 -1
  234. package/llms/components/lr-selection-toolbar.md +1 -1
  235. package/llms/components/lr-sequence-strip.md +3 -8
  236. package/llms/components/lr-span-waterfall.md +1 -1
  237. package/llms/components/lr-split-panel.md +1 -1
  238. package/llms/components/lr-streaming-text-core.md +2 -1
  239. package/llms/components/lr-streaming-text.md +3 -2
  240. package/llms/components/lr-subagent-panel.md +1 -1
  241. package/llms/components/lr-swatch-picker.md +7 -6
  242. package/llms/components/lr-switch.md +3 -1
  243. package/llms/components/lr-table.md +33 -85
  244. package/llms/components/lr-terminal.md +2 -5
  245. package/llms/components/lr-textarea.md +1 -1
  246. package/llms/components/lr-thread-list.md +8 -14
  247. package/llms/components/lr-time-input.md +1 -1
  248. package/llms/components/lr-toggle-group.md +2 -2
  249. package/llms/components/lr-toggle.md +1 -1
  250. package/llms/components/lr-tool-approval-dialog.md +4 -7
  251. package/llms/components/lr-tool-call-block.md +1 -1
  252. package/llms/components/lr-tool-timeline.md +2 -5
  253. package/llms/components/lr-video-playlist.md +1 -1
  254. package/llms/components/lr-video.md +1 -1
  255. package/llms/components/lr-widget-renderer.md +7 -6
  256. package/llms/components/lr-zoomable-frame.md +1 -1
  257. package/llms/index.md +3 -618
  258. package/llms/migration.md +614 -614
  259. package/llms/shared/events-and-types.md +32 -31
  260. package/llms/shared/frameworks-and-ssr.md +7 -3
  261. package/llms/shared/imports-and-registration.md +22 -22
  262. package/llms/shared/localization-and-rtl.md +2 -2
  263. package/llms/shared/native-styles-and-utilities.md +263 -0
  264. package/llms/shared/styles-and-tokens.md +85 -676
  265. package/llms/shared/testing-and-utilities.md +12 -11
  266. package/llms/shared/v23-to-v24-migration.md +167 -0
  267. package/llms/shared.md +149 -494
  268. package/llms/tokens.md +3 -3
  269. package/llms.txt +9 -4
  270. package/package.json +7 -311
  271. package/registrations.json +1 -1
  272. package/vscode-html-data.json +1 -1
  273. package/web-types.json +1 -1
  274. package/dist/ssr-loader.d.ts +0 -2
  275. package/dist/ssr-loader.js +0 -1
  276. package/dist/theme/presets/shadcn.d.ts +0 -22
  277. package/dist/theme/presets/shadcn.js +0 -1
  278. package/dist/theme/presets.d.ts +0 -49
  279. package/dist/theme/presets.js +0 -1
  280. package/dist/themes/shadcn.css +0 -1
package/llms/shared.md CHANGED
@@ -59,8 +59,8 @@ way, and moving to the replacement removes the warning. A test suite that fails
59
59
  while deliberately exercising a deprecated form can pre-seed Lit's development-mode
60
60
  `globalThis.litIssuedWarnings` set, before the element renders, with the key
61
61
  `lyra-deprecated:<owner>:<kind>:<name>` — `<kind>` and `<name>` as in the deprecation record.
62
- `<owner>` is the element name or package-relative module specifier; for example,
63
- `lyra-deprecated:./theme.js:function:setLyraTheme`.
62
+ `<owner>` is the element name or package-relative module specifier. Theme migration guidance is in
63
+ [styles and tokens](styles-and-tokens.md).
64
64
 
65
65
  ### Release history and upgrade notes
66
66
 
@@ -81,6 +81,13 @@ The major-version landmarks after 9.0.0 are:
81
81
  that themselves overrode the hook are affected. Collection snapshot enforcement moved to a
82
82
  decorator-agnostic accessor seam instead.
83
83
 
84
+ - **24.0.0:** removed eligible v22 compatibility routes, including package-root component
85
+ constructors, duplicate nested registration entries, the combined `ssr-loader.js` entry, and the
86
+ retired theme/preset facade. Use stable tag-shaped registration entries, a component's
87
+ registration-free `.class.js` subpath, the split SSR/hydration entries, and the current style API.
88
+ The [v23-to-v24 migration guide](v23-to-v24-migration.md) gives the removed routes, ordered
89
+ consumer cleanup and retained-profile instructions.
90
+
84
91
  ### The support window
85
92
 
86
93
  Compatibility promises are bounded by a published support window, not by "evergreen browsers":
@@ -105,9 +112,9 @@ import "@aceshooting/lyra-ui/components/lr-combobox.js"; // registers <lr-combob
105
112
  import "@aceshooting/lyra-ui/components/lr-table.js"; // registers <lr-table>
106
113
  ```
107
114
 
108
- The older family-shaped registration paths remain supported for compatibility. Class-only
109
- `.class.js` entries still use their owning family path because they intentionally expose source
110
- organization and do not register a tag.
115
+ Family barrel entries remain supported and register their family. Class-only `.class.js` entries
116
+ use their owning family/component path because they intentionally expose source organization and
117
+ do not register a tag.
111
118
 
112
119
  New application code can import any component through its stable tag-shaped path:
113
120
 
@@ -118,8 +125,7 @@ import "@aceshooting/lyra-ui/components/lr-tool-call-block.js";
118
125
  ```
119
126
 
120
127
  `llms/index.md` lists every tag and its owning implementation module. The stable tag-shaped alias
121
- and the family-shaped compatibility path both resolve through the package's `./components/*`
122
- export.
128
+ and the supported family barrel each resolve through the package's component exports.
123
129
 
124
130
  The package root is a pure, side-effect-free export surface. Import a component entry to register
125
131
  only that tag, or use the explicit `all.js` entry when whole-library registration is needed:
@@ -128,17 +134,11 @@ only that tag, or use the explicit `all.js` entry when whole-library registratio
128
134
  import "@aceshooting/lyra-ui/all.js"; // explicitly registers the root-included tags
129
135
  ```
130
136
 
131
- Import component classes from their family-shaped `.class.js` subpaths. Deprecated root
132
- component-class exports refer to the component constructor bindings themselves, including a
133
- constructor re-exported from a module that also exports helpers or types. That deprecation does not
134
- extend to sibling helpers, constants, or types; those remain supported unless they have their own
135
- deprecation record. Use the owning component's granular `.class.js` subpath for class imports.
136
- Family-shaped registration imports are also deprecated. Both deprecated root component-class exports
137
- and family-shaped registration imports keep working throughout the compatibility window, with
138
- removal no earlier than 24.0.0. Stable tag-shaped registration imports and class-only subpaths are
139
- the canonical routes. The package root's helpers and types remain available. `all.js` is
140
- side-effectful and registers the root-included inventory; prefer tag-shaped imports in application
141
- code for smaller bundles.
137
+ The package root does not re-export component constructors. Import a class from its owning
138
+ component's `.class.js` subpath; import the stable tag-shaped entry to register it. The root remains
139
+ a side-effect-free home for shared utilities and types. Family registration barrels and `all.js`
140
+ remain side-effectful convenience entries; prefer tag-shaped imports for smaller application
141
+ bundles.
142
142
 
143
143
  The entry points, then:
144
144
 
@@ -177,7 +177,7 @@ The entry points, then:
177
177
  LyraAnchor,
178
178
  LyraHighlight,
179
179
  AnchorTargetCapabilities,
180
- } from "@aceshooting/lyra-ui/components/viewers/document-viewer/document-viewer.js";
180
+ } from "@aceshooting/lyra-ui/components/lr-document-viewer.js";
181
181
  ```
182
182
  Prefer the granular entry for component-local imports; use the root export when an application
183
183
  deliberately shares the contracts across several viewer integrations.
@@ -185,13 +185,13 @@ The entry points, then:
185
185
  a pre-resolved `src`) additionally needs
186
186
  `import '@aceshooting/lyra-ui/components/media/flag/flag-peer.js';` once.
187
187
  - **Other subpaths.** `@aceshooting/lyra-ui/theme.css` (ready-made light/dark theme),
188
- `@aceshooting/lyra-ui/themes/shadcn.css` (opt-in shadcn/ui look, imported after `theme.css` — see
189
- [The shadcn look](#the-shadcn-look--themesshadcncss)),
188
+ `@aceshooting/lyra-ui/looks/shadcn.css` (opt-in shadcn/ui look, imported after `theme.css` — see
189
+ [The shadcn look](#the-shadcn-look--looksshadcncss)),
190
190
  `@aceshooting/lyra-ui/tokens-root.css` (opt-in: the curated resolved `--lr-*` tokens at `:root`,
191
191
  so your own components can read them),
192
192
  `@aceshooting/lyra-ui/native.css` (opt-in native-element styles inside `.lr-native`),
193
193
  `@aceshooting/lyra-ui/utilities.css` (opt-in light-DOM layout/text/typography utilities),
194
- `@aceshooting/lyra-ui/theme.js` (the zero-dependency mode/accent runtime),
194
+ `@aceshooting/lyra-ui/theme.js` (the zero-dependency style runtime),
195
195
  `@aceshooting/lyra-ui/localization.js` (side-effect-free locale runtime),
196
196
  `@aceshooting/lyra-ui/autoloader.js` (side-effect-free on-demand tag loading),
197
197
  `@aceshooting/lyra-ui/autoloader-cdn.js` (browser-guarded auto-start side effect),
@@ -376,7 +376,8 @@ Lyra-specific events are `lr-`-prefixed `CustomEvent`s (`lr-change`, `lr-input`,
376
376
  dispatched through `LyraElement`'s `protected emit<T>(name, detail?, options?)`: **bubbling,
377
377
  composed, and non-cancelable by default**, with the payload on `event.detail`. A component that
378
378
  offers a genuine veto point opts into `{ cancelable: true }` and checks `defaultPrevented` before
379
- acting (as `lr-export` does) — that is called out per component. Native wrappers may additionally
379
+ acting (as `lr-export-request` on `<lr-export-button>` does) — that is called out per component.
380
+ Native wrappers may additionally
380
381
  relay unprefixed `Event`, `InputEvent`, or `FocusEvent` instances; each component section documents
381
382
  the exact native names, constructors, bubbling, and cancelability it supports.
382
383
 
@@ -393,6 +394,10 @@ Every one of those names is also typed — per component through its own event m
393
394
  through `@aceshooting/lyra-ui/events` for listeners on an ancestor, `document` or `window`. See
394
395
  "TypeScript" below.
395
396
 
397
+ For event-detail compatibility fields removed in the v23-to-v24 upgrade,
398
+ use step 7 of the [migration checklist](v23-to-v24-migration.md#migrate-a-project-in-reviewable-steps).
399
+ Those nested detail fields are separate from deprecated event names and are not automatic renames.
400
+
396
401
  ## TypeScript
397
402
 
398
403
  - **Per-component event maps.** Every component with events exports a `Lyra<Name>EventMap` type, and
@@ -401,7 +406,7 @@ through `@aceshooting/lyra-ui/events` for listeners on an ancestor, `document` o
401
406
  ```ts
402
407
  import { LyraTable } from "@aceshooting/lyra-ui/components/data/table/table.class.js";
403
408
  const table = document.querySelector("lr-table") as LyraTable;
404
- table.addEventListener("lr-sort", (event) => event.detail.key); // typed
409
+ table.addEventListener("lr-sort", (event) => event.detail.sortKey); // typed
405
410
  ```
406
411
  - **`HTMLElementTagNameMap`** is augmented in the `.class.d.ts` files. `document.querySelector('lr-table')`
407
412
  is only typed as `LyraTable` when that class module is in the type graph — importing just the
@@ -440,13 +445,13 @@ through `@aceshooting/lyra-ui/events` for listeners on an ancestor, `document` o
440
445
  `LyraGlobalEventMap` is exported as well, for writing your own typed helper over it.
441
446
 
442
447
  - **The surface is per-event type aliases, not runtime event classes.** `LyraSortEvent` and its
443
- generated siblings are `type` aliases over the owning component's own map entry
444
- (`LyraTableEventMap['lr-sort']`) — there is nothing to `new`, and `instanceof LyraSortEvent` is
445
- not a thing. The module compiles to `export {};`: shipping runtime event subclasses to type a
446
- listener would cost every consumer runtime bytes for a compile-time concern, so it deliberately
447
- costs zero.
448
+ generated siblings are `type` aliases over component event-map entries. `LyraSortEvent` unions
449
+ the `lr-sort` entries for document-library, eval-dataset and table. These aliases have no runtime
450
+ constructor and cannot be used with `new` or `instanceof`. The module compiles to `export {};`:
451
+ shipping runtime event subclasses to type a listener would cost every consumer runtime bytes for
452
+ a compile-time concern, so it deliberately costs zero.
448
453
  - **A shared event name narrows to the union of its dispatchers.** One name can come from several
449
- components with different details — `lr-select` from five, `lr-selection-change` from five — so
454
+ components with different details, such as `lr-select` and `lr-selection-change`, so
450
455
  its global entry is the _union_ of their entries, and `event.detail` there exposes only what all
451
456
  of them share. Index the owning component's own map when you need one component's exact detail:
452
457
 
@@ -1130,8 +1135,8 @@ page you are on — that is what keeps two overlapping surfaces reading in the c
1130
1135
  - **`lr-base`** — contains the explicitly scoped native-element rules only when the optional
1131
1136
  `native.css` asset is imported.
1132
1137
  - **`lr-theme`** — where every `--lr-theme-*` token `theme.css` ships is declared.
1133
- - **`lr-theme-preset`** — an optional look preset layered over those tokens, such as
1134
- [`themes/shadcn.css`](#the-shadcn-look--themesshadcncss). Empty unless one is imported.
1138
+ - **`lr-theme-preset`** — the compatibility layer name that holds optional look stylesheets, such as
1139
+ [`looks/shadcn.css`](#the-shadcn-look--looksshadcncss). Empty unless one is imported.
1135
1140
  - **`lr-utilities`** — contains exact `lr-*` classes only when the optional `utilities.css` asset is
1136
1141
  imported.
1137
1142
  - **`lr-overrides`** — named so an application can opt its own rules into a defined position
@@ -1186,111 +1191,39 @@ first statement the browser sees is appended at the end, so an older four-name s
1186
1191
  `lr-theme-preset`) would rank an imported look preset above `lr-overrides` and your own layer. Every Lyra stylesheet that declares an order repeats the same five names for exactly this
1187
1192
  reason.
1188
1193
 
1189
- ### The shadcn look — `themes/shadcn.css`
1194
+ <a id="the-shadcn-look--themesshadcncss"></a>
1190
1195
 
1191
- One more import restyles every component after [shadcn/ui](https://ui.shadcn.com)'s
1192
- "new-york" style on its Neutral base colour:
1196
+ ### The shadcn look — looks/shadcn.css
1193
1197
 
1194
- ```ts
1198
+ Import the base resolver and the optional look stylesheet, then select the look on the scope that should receive it:
1199
+
1200
+ ~~~ts
1195
1201
  import "@aceshooting/lyra-ui/theme.css";
1196
- import "@aceshooting/lyra-ui/themes/shadcn.css";
1197
- ```
1202
+ import "@aceshooting/lyra-ui/looks/shadcn.css";
1203
+ import { setLyraStyle } from "@aceshooting/lyra-ui/theme.js";
1198
1204
 
1199
- The preset sets only `--lr-theme-*` inputs, so no component API changes. Every rule sits in the
1200
- `lr-theme-preset` layer, and the file repeats `theme.css`'s layer statement, so:
1201
-
1202
- - **load order does not matter** — the preset beats `theme.css` whichever is emitted first;
1203
- - **any unlayered rule of yours beats both**, exactly as it beats `theme.css` alone;
1204
- - **`setLyraTheme({ accent })` still wins** — the runtime writes inline style on `<html>`, which beats
1205
- every layer.
1206
-
1207
- **Modes.** The light block applies to `:root, .lr-light, [data-lr-theme='light'], .light` and the
1208
- dark block to `.lr-dark, [data-lr-theme='dark'], .dark` (declared after light, same specificity). An
1209
- application that toggles `.dark` on `<html>` the shadcn way gets Lyra's dark mode with no extra
1210
- wiring, and a nested `.dark` (or `.light`) region is themed on its own, native `color-scheme`
1211
- included. Only the preset knows `.dark`/`.light`: `theme.css` never reads those class names, since an
1212
- application may already use them for something else. That is why each preset block also repeats
1213
- `theme.css`'s own value, for that mode, of every input the preset does not restyle — the success,
1214
- warning and remaining danger slots, the scrims, and the chart, graph and terminal ramps — so a bare
1215
- `.dark` switches those too instead of leaving light status tints and light chart colours on a dark
1216
- page.
1217
-
1218
- | Input | Light | Dark | Notes |
1219
- | --- | --- | --- | --- |
1220
- | `color-surface-default` / `-raised` / `-overlay` | `#ffffff` / `#fafafa` / `#ffffff` | `#0a0a0a` / `#171717` / `#171717` | page, card/sidebar, popover |
1221
- | `color-text-normal` / `-quiet` | `#0a0a0a` / `#737373` | `#fafafa` / `#a1a1a1` | |
1222
- | `color-surface-border`, `color-border-strong` | `#919191` | `#646464` | control boundaries, 3:1 on page and raised surface |
1223
- | `color-surface-border-subtle` | `#e5e5e5` | `rgb(255 255 255 / 0.1)` | decorative edges only |
1224
- | `color-brand-*`, `color-neutral-*` | quiet `#f5f5f5`, normal `#e5e5e5`, loud `#171717` | quiet `#262626`, normal `#404040`, loud `#e5e5e5` | monochrome: primary and secondary are one family |
1225
- | `color-danger-fill-loud` / `-on-loud` | `#d6000a` / `#ffffff` | `#ff6467` / `#0a0a0a` | other danger, success and warning slots keep Lyra's values |
1226
- | `color-focus`, `focus-ring-width`, `focus-ring-offset` | `#8b8b8b`, `3px`, `0px` | `#787878`, `3px`, `0px` | |
1227
- | `color-mix-partner` | `#737373` | `#737373` | hover/press mix toward a mid grey |
1228
- | `border-radius-m` / `-xs` | `0.5rem` / `0.25rem` | same | |
1229
- | `font-family-body` / `-mono` | `'Geist', 'Inter', ui-sans-serif, system-ui, sans-serif` / `'Geist Mono', ui-monospace, …` | same | fonts are named, not shipped |
1230
- | `font-size-m` | `0.875rem` | same | shadcn UI text is `text-sm` |
1231
- | `form-control-height-s` / `-m` / `-l`, `icon-button-size` | `2rem` / `2.25rem` / `2.5rem`, `2.25rem` | same | the 44px coarse-pointer floor still applies |
1232
- | `shadow-xs` … `shadow-xl` | Tailwind geometry, alpha 0.05 / 0.1 | same geometry, alphas ×3 | `--lr-theme-shadow-color` still tints them |
1233
-
1234
- Every name in the first column is `--lr-theme-` plus the cell. The default `<lr-button>`
1235
- (`variant="neutral" appearance="accent"`) is shadcn's primary; `appearance="filled"` is secondary,
1236
- `"outlined"` outline, `"plain"` ghost, `variant="danger"` destructive, `appearance="link"` link.
1237
-
1238
- **Deliberate deviations from shadcn**, each keeping a contrast guarantee shadcn does not make — the
1239
- repository's contrast gate re-measures every value in both modes:
1240
-
1241
- - **Control borders** use the control grey above, not shadcn's `#e5e5e5` hairline (1.26:1 on white).
1242
- The hairline survives as `--lr-color-border-subtle`, for decoration only.
1243
- - **Focus** is an opaque grey, 3.41:1 / 4.48:1 against the page. shadcn's half-transparent ring
1244
- measures about 1.5:1 / 1.9:1.
1245
- - **Danger.** Components set text in the loud danger colour — error messages on the page, the body of
1246
- a danger callout on the quiet danger tint. shadcn's `#e7000b` measures 4.10:1 on that tint, so the
1247
- preset uses `#d6000a`, the same red a notch darker. Dark danger is a light red under near-black
1248
- text, not white on a dark red, for the same reason.
1249
- - **Charts** keep Lyra's validated series ramp: two of shadcn's chart colours fall below 3:1 on
1250
- white, and the ramp is also checked for colour-blind separation.
1251
- - **Hover and press** mix toward a mid grey. The default mixes toward the text colour, and shadcn's
1252
- primary _is_ the text colour, so the default button would show no hover or press state at all.
1253
- - **Code-fence syntax colouring** (`lr-code-block` / `lr-markdown`'s keyword/string/comment tokens)
1254
- is the one surface the preset does not repaint: it stays GitHub's light/dark Shiki theme pair in
1255
- both modes, with no property yet to substitute a different pair. Only which of the two fixed
1256
- themes paints (light or dark) tracks the preset, through the same light/dark switch every other
1257
- surface here reads. Unlike charts, above, this one carries no `--lr-theme-*` input at all — a
1258
- deliberate scope line, not an oversight.
1259
-
1260
- **Accents.** `lr-button`'s default renders the `neutral` role, and a bare accent only re-derives
1261
- `brand`, so the preset aliases neutral's loud slots to brand's in both modes
1262
- (`--lr-theme-color-neutral-fill-loud: var(--lr-theme-color-brand-fill-loud)`, and likewise
1263
- `on-loud` and `border-loud`). With no accent that is the brand literal, so nothing changes; with
1264
- `setLyraTheme({ accent: GEMSTONES.emerald.fill })` the default button, checked checkboxes and
1265
- radios, and the focus ring follow the accent, while neutral's quiet and normal tiers — shadcn's
1266
- secondary and muted — stay grey. **Switch modes through `setLyraTheme({ mode })`** (or the
1267
- no-flash bootstrap) in an application that uses accents: the runtime derives the accent ramp for the
1268
- mode it resolved and writes `data-lr-theme`, which the preset honours. Toggling `.dark` alone would
1269
- leave a light-mode accent ramp on a dark page. Tailwind users can point `dark:` at
1270
- `[data-lr-theme=dark]` to share that one switch.
1271
-
1272
- **Overriding the preset.** An unlayered `:root { --lr-theme-… }` wins, as always. A mode-specific
1273
- override must name the mode selectors, because the preset re-declares every input it sets on each of
1274
- them — so a nested `.dark` region re-applies the preset's dark value over one you set on `:root`:
1205
+ setLyraStyle({ look: "shadcn" });
1206
+ ~~~
1275
1207
 
1276
- ```css
1277
- :root, .lr-light, [data-lr-theme="light"], .light { --lr-theme-color-brand-fill-loud: #4f46e5; }
1278
- .lr-dark, [data-lr-theme="dark"], .dark { --lr-theme-color-brand-fill-loud: #818cf8; }
1279
- ```
1208
+ The stylesheet declares values only under data-lr-look="shadcn". It composes with the independent mode, surface, density, and accent axes. For a local region, set data-lr-look="shadcn" on that region or use applyLyraStyleScope(). A nested data-lr-mode="dark" still changes mode without changing the look.
1280
1209
 
1281
- Two document-scope conveniences re-derive only on Lyra's own mode selectors
1282
- (`.lr-light`/`.lr-dark`/`data-lr-theme`), not on `.dark`/`.light`: the resolved layer published by
1283
- [`tokens-root.css`](#reading-the-resolved-tokens-from-your-own-components--tokens-rootcss) and the
1284
- `--lr-focus-ring` composite `theme.css` declares for your own `outline: var(--lr-focus-ring)` rules.
1285
- A nested region whose _own_ light-DOM elements read either should carry `data-lr-theme="dark"`
1286
- alongside `.dark`. Components are unaffected; they re-derive both on their own `:host`.
1210
+ The look sets --lr-theme-* inputs, so no component API changes. Its rules use Lyra's named cascade layers: unlayered application rules still win, and runtime accent ramps still take precedence over look values for the roles they paint. Import order between theme.css and looks/shadcn.css does not change the layer order.
1287
1211
 
1288
- **Component hooks are not part of the preset** — it sets theme inputs only. For shadcn's raised
1289
- sidebar tone, opt `lr-app-rail` in with one line:
1212
+ The shadcn look uses Neutral for primary and secondary controls. Its default button appearance is primary; filled is secondary, outlined is outline, plain is ghost, and danger remains destructive. The measured palette keeps control borders at least 3:1, uses an opaque focus color, keeps chart series contrast-qualified, and mixes hover and press toward mid grey so the monochrome primary still has a visible interaction state. Code-fence syntax colors continue to use the built-in light/dark Shiki themes.
1290
1213
 
1291
- ```css
1292
- lr-app-rail { --lr-app-rail-bg: var(--lr-color-surface-raised); }
1293
- ```
1214
+ Accents are independent from the look. A named accent such as emerald recolors the default button, checked controls, and focus ring while secondary and muted neutral tiers stay grey. Select mode with setLyraStyle({ mode: "system" }) or an explicit light/dark choice so the runtime resolves the accent ramp and updates data-lr-theme. Toggling an application class alone does not update an accent ramp managed by the runtime.
1215
+
1216
+ Override the look through an unlayered application rule or the lr-overrides layer:
1217
+
1218
+ ~~~css
1219
+ @layer lr-overrides {
1220
+ [data-lr-look="shadcn"] {
1221
+ --lr-theme-color-brand-fill-loud: #4f46e5;
1222
+ }
1223
+ }
1224
+ ~~~
1225
+
1226
+ The former global themes/shadcn.css facade and its data-lr-theme-preset behavior have been removed. Use looks/shadcn.css and the data-lr-look axis for new styling.
1294
1227
 
1295
1228
  ### Where an override actually reaches
1296
1229
 
@@ -1472,7 +1405,7 @@ dark palette. Two things switch that fallback off:
1472
1405
 
1473
1406
  Note the asymmetry: the _light_ pin is read on the component itself (`:host([data-lr-theme='light'])`),
1474
1407
  while a _dark_ ancestor is followed through `:host-context()`. Putting `data-lr-theme="light"` on
1475
- `<html>` — what `theme.js`'s `setLyraTheme({ mode: 'light' })` does — pins the page through
1408
+ `<html>` — what `setLyraStyle({ mode: 'light' })` does — pins the page through
1476
1409
  `theme.css`'s real `--lr-theme-*` values, which inherit into every shadow root. Without `theme.css`
1477
1410
  there are no such values to inherit, so put the attribute on the components you actually need
1478
1411
  pinned.
@@ -1480,340 +1413,59 @@ pinned.
1480
1413
  The token layer also sets `:host([hidden]) { display: none !important; }` and an inherited
1481
1414
  `box-sizing: border-box` reset.
1482
1415
 
1483
- ### Theme mode/accent/surface runtime (`@aceshooting/lyra-ui/theme.js`)
1416
+ ### Style runtime — @aceshooting/lyra-ui/theme.js
1484
1417
 
1485
- Flipping the mode class/attribute above is something every app ends up hand-rolling — persist a
1486
- choice, apply it on load, avoid the flash of wrong theme before the app boots. `theme.js` is that
1487
- runtime, published as its own subpath: **zero dependencies, no Lit, no component imports, and no
1488
- side effects on import**, so an app can persist and apply a theme without pulling the component
1489
- graph into its first-paint bundle.
1418
+ The standalone style runtime has no Lit or component dependency and does not touch the document or storage when imported. It stores the selected style under localStorage['lyra-theme'], applies it to the document root, and announces changes through lr-style-change.
1490
1419
 
1491
- ```ts
1492
- import { setLyraTheme, getLyraTheme } from "@aceshooting/lyra-ui/theme.js";
1493
-
1494
- setLyraTheme({ mode: "dark" }); // unspecified fields keep their current value
1495
- setLyraTheme({ accent: "#7c3aed" }); // mode stays 'dark'; brand-only shorthand
1496
- getLyraTheme(); // → { mode: 'dark', accent: '#7c3aed', surface: null }
1497
- setLyraTheme({ accent: { danger: "#dc2626", success: "#16a34a" } }); // per-role, brand untouched
1498
- setLyraTheme({ accent: { brand: { light: "#2563eb", dark: "#f59e0b" } } }); // per-mode base color
1499
- setLyraTheme({ surface: "#0b0f1a" }); // mixes every ramp against this instead of the mode default
1500
- setLyraTheme({ mode: "auto" }); // follows the OS, including later changes
1501
- setLyraTheme({
1502
- tokens: {
1503
- "--lr-theme-border-radius-m": "0.5rem", // both modes
1504
- "--lr-theme-color-surface-default": { light: "#ffffff", dark: "#0a0a0a" }, // per mode
1505
- },
1506
- }); // replaces any previous token map wholesale
1507
- setLyraTheme({ tokens: null }); // back to whatever the stylesheets say
1508
- setLyraTheme({ mode: "unset", accent: null, surface: null, tokens: null }); // removes Lyra's overrides
1509
- ```
1420
+ ~~~ts
1421
+ import {
1422
+ defineLyraLook,
1423
+ getLyraStyle,
1424
+ resetLyraStyle,
1425
+ setLyraStyle,
1426
+ } from "@aceshooting/lyra-ui/theme.js";
1510
1427
 
1511
- - **`setLyraTheme({ mode?, accent?, surface?, tokens? })`** persists to `localStorage['lyra-theme']`,
1512
- applies to `document.documentElement`, and dispatches `lr-theme-change` on `window` with
1513
- `detail: { mode, accent, surface, tokens? }`. Fields you omit keep their current value; pass `null` to
1514
- clear a field. It **never throws** — when `localStorage` is unavailable (private browsing,
1515
- quota, a sandboxed iframe) it degrades to apply-without-persist, and the "fields you omit keep
1516
- their current value" rule still holds across calls in that state: the merge falls back to the
1517
- last theme applied in this session rather than to the default.
1518
- - **`getLyraTheme()`** returns `{ mode, accent, surface }`, defaulting to
1519
- `{ mode: 'auto', accent: null, surface: null }` when nothing is stored or the stored value is
1520
- malformed. Storage is re-read on every call — no in-memory cache — so a value written by another
1521
- tab or a previous session is picked up cold. Where storage is unreadable or unwritable it
1522
- reports the theme last applied, so the return value always describes what the document is
1523
- actually showing and a toggle UI bound to it stays in sync.
1524
- - **`mode`** is `'light' | 'dark' | 'auto' | 'unset'`. `'light'`/`'dark'` set **both
1525
- `data-lr-theme`** (the
1526
- attribute `theme.css` actually keys its palette blocks on) **and `data-theme`** (the generic
1527
- attribute canvas-rendered components watch, so `lr-chart`/`lr-heatmap`/`lr-qr-code` repaint on
1528
- the switch rather than keeping stale colors — see `llms/components/lr-chart.md`). `'auto'`
1529
- resolves `prefers-color-scheme` immediately and keeps following changes. `'unset'` removes both
1530
- attributes; use it when the application owns mode selection through another cascade.
1531
- - **`accent`** is either an absolute CSS color — shorthand for `{ brand: <that color> }`, and the
1532
- only shape prior to 16.0.0 — or a per-role record
1533
- `{ brand?, success?, warning?, danger?, neutral? }`. Only the roles you supply are (re)derived;
1534
- an omitted role keeps whatever the static palette (`llms/tokens.md`) already provides. For each
1535
- supplied role, Lyra derives the complete quiet/normal/loud fill, border, and paired on-color
1536
- ramp as inline `--lr-theme-color-<role>-*` inputs; `brand` additionally keeps `--lr-theme-accent`
1537
- (a compatibility value holding the raw brand color, resolved for the active mode when the role
1538
- is per-mode) and `--lr-theme-color-focus`, which no other role drives. Each paired foreground is
1539
- selected for at least 4.5:1 contrast against its fill; normal/loud borders and the brand focus
1540
- color are adjusted to at least 3:1 against the resolved surface (see `surface` below). Malformed
1541
- values, CSS-wide keywords, `currentColor`, system colors, relative-color syntax, and unresolved
1542
- `var()` expressions fail closed to `null` — at the whole `accent` field for a bare-string call,
1543
- or at just that one role (or role/mode branch) for a per-role record, so one bad role does not
1544
- take the others down with it. Pass `accent: null` to restore the palette supplied by
1545
- `theme.css` entirely.
1546
- - **Per-mode accent.** Each role's value can itself be a bare color/`null` (applied to both
1547
- resolved modes, as above) or a `{ light?, dark? }` map deriving that role's ramp from a
1548
- genuinely *different base color* per resolved mode — not merely a different tint weight of the
1549
- same hue — for example `{ brand: { light: "#2563eb", dark: "#f59e0b" } }`. An omitted branch
1550
- (or a role/branch that fails validation) keeps that mode's inherited/palette default; the branch
1551
- actually painted follows the *resolved* mode, so it updates automatically when `mode: 'auto'`
1552
- follows a `prefers-color-scheme` change. The stored/returned/event-detail shape always mirrors
1553
- what you supplied (bare color or `{ light, dark }`), never collapsed to a single resolved color.
1554
- - **`surface`** is an absolute CSS color used as every role's ramp mix base, instead of the
1555
- shipped light/dark defaults (`#1a1a1a` dark / `#ffffff` light). It follows the same absolute
1556
- CSS color and fail-closed-to-`null` rules as `accent`; an alpha channel is composited against
1557
- the mode's own default surface before use. `null` (the default) keeps those shipped defaults.
1558
- Supplying `surface` changes the quiet/normal/loud mix ratios and every border/focus contrast
1559
- check for **every** currently-supplied role at once — it is one mix base per apply, not
1560
- per-role.
1561
-
1562
- These are `--lr-theme-*` inputs, so they reach every nested shadow root — see "Where an override
1563
- actually reaches" above for why setting a `--lr-*` token instead would not.
1564
- - **`tokens`** is a map of `--lr-theme-*` inputs written inline on `<html>`, persisted and restored
1565
- before first paint like the other fields. Each value is a CSS string for both modes or a
1566
- `{ light?, dark? }` pair; a `null` or omitted branch leaves that mode to the stylesheets. A map
1567
- always **replaces** the previous one wholesale — compose with object spread
1568
- (`{ ...LYRA_SHADCN_THEME_PRESET.theme.tokens, "--lr-theme-border-radius-m": "0" }`) — and
1569
- `tokens: null`, `{}` or a map with no valid entry removes it; omitting `tokens` keeps the current
1570
- map. Per-mode branches apply only while Lyra resolves a mode, so `mode: "unset"` writes the bare
1571
- values only (use the `themes/*.css` stylesheet instead when an application-owned `.dark` class
1572
- decides the mode). Inline `--lr-theme-*` values your application sets itself on `<html>` and that
1573
- are not in a map are never touched; a name that *is* in the map is owned by the runtime while the
1574
- map is applied, and an earlier inline value for it is not restored on removal.
1575
-
1576
- **Grammar and limits.** Invalid entries are dropped one by one (`setLyraTheme()` never throws;
1577
- `defineLyraThemePreset()` throws `TypeError` for the same cases). A name must match
1578
- `--lr-theme-[a-z0-9]+(-[a-z0-9]+)*`, be at most 80 characters, and not be `--lr-theme-accent`
1579
- (owned by `accent`). A value is trimmed and must be 1–256 characters; it may not contain a
1580
- control character or line break, a backslash, a backtick, any of
1581
- `; { } ! < > @ [ ] $ ^ | ~ = ? & :`, U+2028/U+2029, or a
1582
- comment delimiter; it may not be a CSS-wide keyword (`inherit`, `initial`, `unset`, `revert`,
1583
- `revert-layer`); every function must be one of `rgb rgba hsl hsla hwb lab lch oklab oklch color
1584
- color-mix light-dark calc min max clamp var cubic-bezier steps linear` (so `url()`, `image-set()`,
1585
- `attr()`, `env()` and unknown functions are rejected); and parentheses and quotes must balance. A
1586
- map holds at most 512 entries (more normalizes to no map at all). The check is DOM-free, so
1587
- `defineLyraThemePreset()`, the runtime, the bootstrap and the preset generator apply it
1588
- identically.
1589
-
1590
- **Contrast floor.** With a resolved mode, colour families the static contrast gate checks are
1591
- measured before they are written; a failing value is replaced by the nearest passing mix toward
1592
- black or white, serialized as `rgb(r g b)`, and a passing value is written verbatim. The snapshot
1593
- and the stored record always keep the value you supplied. The references are the explicit
1594
- `surface`, else the map's `--lr-theme-color-surface-default`, else the mode default (**S**:
1595
- `#ffffff` / `#1a1a1a`); the map's `color-surface-raised` else `#f6f8fa` / `#22272e` (**R**); the
1596
- painted `color-text-normal` else `#1a1a1a` / `#f2f2f2` (**T**); and the map's
1597
- `color-overlay-strong` else black at 0.92 / 0.95 over S (**O**). Names below omit `--lr-theme-`:
1598
-
1599
- | Tokens | Against | Floor |
1600
- | --- | --- | --- |
1601
- | `color-text-normal`, `color-text-quiet` | S and R | 4.5 |
1602
- | `color-<role>-on-<tier>`, when its `color-<role>-fill-<tier>` is in the map (a missing on-* is synthesized) | its fill | 4.5 |
1603
- | `color-on-strong-overlay` (synthesized when only `color-overlay-strong` is in the map) | O | 4.5 |
1604
- | `color-<role>-border-normal`/`-loud`, `color-surface-border`, `color-border-strong`, `color-focus` | S | 3 |
1605
- | `color-chart-<n>` | S | 3 |
1606
- | `terminal-color-<name>` | R | 4.5 |
1607
- | `terminal-bg-<name>` | T | 4.5 |
1608
-
1609
- **Unchecked, written verbatim:** `*-border-quiet` and `color-surface-border-subtle` (decorative by
1610
- contract); `color-<role>-fill-*` (measured through their on-* partner); an on-* whose fill-* is not
1611
- in the same map; `color-surface-default`, `-raised` and `-overlay` (references, not foregrounds —
1612
- text on `color-surface-overlay` is not measured); `color-overlay`, `color-no-data` and
1613
- `color-mix-partner`; chart-series colour-vision separation (the static gate only); every
1614
- non-colour token (fonts, radii, sizes, shadows, durations — including
1615
- `--lr-theme-icon-button-size`, whose target-size floor is yours to keep); and any value or
1616
- reference that does not resolve to a colour — `var()`, `light-dark()`, relative colours,
1617
- `currentColor`, system colours, and **every** value when a canvas is unavailable (no 2D context,
1618
- or pixel reads blocked). An unresolved row is written verbatim and synthesizes nothing; the accent
1619
- still fails closed as it always has. The runtime never reads computed styles, so a reference an
1620
- application stylesheet changes without the map carrying it is not seen.
1621
-
1622
- **Precedence.** accent ramp (for the roles it paints) > runtime tokens (inline on `<html>`) >
1623
- your unlayered CSS > `lr-theme-preset` (`themes/shadcn.css`) > `lr-theme` (`theme.css`) >
1624
- component fallbacks. Runtime tokens therefore beat an unlayered `:root { --lr-theme-*: … }`
1625
- override, like the accent does; put your override in the map instead. An element that re-declares
1626
- a `--lr-theme-*` input itself — a `.lr-dark`/`data-lr-theme` island, a `data-lr-theme`-pinned
1627
- component, app-scoped CSS — keeps its own value inside that subtree (the accent ramp has the same
1628
- limitation).
1629
-
1630
- **Snapshots.** `getLyraTheme()`, the stored record and the `lr-theme-change` /
1631
- `lr-theme-preset-change` details carry `tokens` **only while a map is applied** — the key is
1632
- absent, not `null`, otherwise, on every path including unwritable storage and the automatic mode
1633
- flip. The map is normalized and deep-frozen: trimmed strings, and every per-mode entry carries
1634
- both `light` and `dark` (`null` for a missing branch).
1635
- - **Unbalanced colours fail closed.** An `accent` (any role or branch) or `surface` with an
1636
- unclosed parenthesis or quote, such as `rgb(0 0 0`, is rejected to `null` like every other
1637
- malformed value. The browser's own parser closes such a construct implicitly, but written raw
1638
- into an inline `style` it swallows the declarations after it when that attribute is re-parsed.
1639
-
1640
- **Theme presets.** `@aceshooting/lyra-ui/theme/presets.js` exports
1641
- `LYRA_THEME_PRESETS`, `defineLyraThemePreset()` and `applyLyraThemePreset()`. Built-in keys are
1642
- `system`, `light`, `dark`, `unset`, `emerald`, `ruby`, `amethyst`, and `sapphire`; the gemstone
1643
- presets use system-following mode plus the named brand accent. Application presets use a stable
1644
- lowercase kebab-case `id` and a `theme: { mode?, accent?, surface? }` record — the same shapes
1645
- `setLyraTheme()` accepts, including a per-role `accent` record. `defineLyraThemePreset()`
1646
- validates the id and field shapes, freezes both records, and leaves CSS color-syntax validation to
1647
- the production runtime when the preset is applied:
1428
+ setLyraStyle({ mode: "dark" }); // unspecified axes keep their current value
1429
+ setLyraStyle({ accent: "sapphire" }); // named palette accent
1430
+ setLyraStyle({ accent: "#7c3aed" }); // absolute CSS color
1431
+ setLyraStyle({ accentBackground: "#0b0f1a" }); // reference surface for accent ramps
1432
+ setLyraStyle({ mode: "system" }); // follows prefers-color-scheme
1433
+ setLyraStyle({ look: "shadcn", surface: "glass", density: "compact" });
1434
+ const style = getLyraStyle();
1435
+ resetLyraStyle(["look"]); // reset only the look axis
1436
+ setLyraStyle({ mode: "unset", accent: null, accentBackground: null, overrides: null });
1437
+ ~~~
1648
1438
 
1649
- ```ts
1650
- import {
1651
- applyLyraThemePreset,
1652
- defineLyraThemePreset,
1653
- } from "@aceshooting/lyra-ui/theme/presets.js";
1654
-
1655
- applyLyraThemePreset("sapphire");
1656
- applyLyraThemePreset(
1657
- defineLyraThemePreset({
1658
- id: "application-ocean",
1659
- theme: {
1660
- mode: "dark",
1661
- accent: { brand: "#22d3ee", danger: "#dc2626" },
1662
- surface: "#0b0f1a",
1663
- },
1664
- })
1665
- );
1666
- ```
1439
+ setLyraStyle() accepts independent choices for look, surface, density, mode, accent, accentBackground, and overrides. Omitted axes keep their current selection; null resets an axis. The returned LyraStyle snapshot includes the selected values, a resolved mode when the choice is system, and lookForm to distinguish a stylesheet look from a runtime look.
1667
1440
 
1668
- Applying a preset uses the production runtime. When every explicitly requested field survives its
1669
- runtime validation, Lyra reflects the id to `data-lr-theme-preset` and emits
1670
- `lr-theme-preset-change` on `window` with `{ id, theme }`, where `theme` is the complete applied
1671
- snapshot. If runtime validation changes a field (for example, an invalid accent fails closed to
1672
- `null`), only the ordinary `lr-theme-change` event is emitted and no preset marker is written,
1673
- because the resulting state is not exactly that named preset. A direct `setLyraTheme()` call also
1674
- removes the preset marker.
1675
-
1676
- **Token presets: a look as its own axis.** A preset's `theme` may carry `tokens` (see `tokens`
1677
- above). `defineLyraThemePreset()` validates the whole map against the same grammar the runtime
1678
- applies and throws `TypeError` on any violation, so a map it accepts is never changed at run time and
1679
- the preset still claims its marker — even where a canvas is unavailable. A look is independent of
1680
- mode, accent and surface: a preset that omits those fields leaves them as they are, and the built-in
1681
- presets never mention `tokens`, so `applyLyraThemePreset("sapphire")` or `"dark"` keeps the current
1682
- look. Either order reaches the same state; only the marker (the last applied id) differs. The
1683
- shadcn look ships as a generated runtime preset, built from `themes/shadcn.css` so the two can never
1684
- drift:
1441
+ mode is light, dark, system, or unset. Explicit modes set both data-lr-theme and data-theme; system follows the operating system and updates both attributes when it changes; unset removes Lyra's mode attributes. The lr-style-change event carries { style, changed }, where changed lists the affected fields. Applications can listen to this one event for document style changes.
1685
1442
 
1686
- ```ts
1687
- import { applyLyraThemePreset, defineLyraThemePreset } from "@aceshooting/lyra-ui/theme/presets.js";
1688
- import { LYRA_SHADCN_THEME_PRESET } from "@aceshooting/lyra-ui/theme/presets/shadcn.js";
1689
-
1690
- applyLyraThemePreset(LYRA_SHADCN_THEME_PRESET); // look on; mode, accent and surface kept
1691
- applyLyraThemePreset("sapphire"); // an accent on top of the look
1692
-
1693
- // The exact stylesheet rendering (no accent ramp, default surface), keeping a marker:
1694
- applyLyraThemePreset(
1695
- defineLyraThemePreset({
1696
- id: "shadcn-exact",
1697
- theme: { ...LYRA_SHADCN_THEME_PRESET.theme, accent: null, surface: null },
1698
- })
1699
- );
1700
- ```
1443
+ accent accepts one of Lyra's named palettes or an absolute CSS color. A custom color is shorthand for a brand color; an object can set colors for brand, success, warning, danger, and neutral independently. Each role can use a { light, dark } pair. Lyra derives quiet, normal, and loud fill, border, and on-color ramps. Foregrounds meet 4.5:1 contrast against their fills; normal and loud borders and the brand focus color meet 3:1 against the selected reference surface. Invalid colors fail closed.
1701
1444
 
1702
- Choose the carrier by need. The runtime preset persists, is restored by the no-flash bootstrap
1703
- before first paint, needs no stylesheet swap, and is written inline, so it beats unlayered `:root`
1704
- overrides. The `themes/shadcn.css` stylesheet is the one to use for light/dark **islands**, for an
1705
- application-owned `.dark`/`.light` class, or for a page with no theme runtime. The runtime preset
1706
- module is a separate subpath, so an application that only uses `presets.js` pays nothing for it.
1707
-
1708
- **No-flash bootstrap.** `lyraThemeBootstrap` is a self-contained IIFE **string** (not a function),
1709
- meant to be inlined into a `<script>` in `<head>` **before any stylesheet**, so the persisted theme
1710
- is on the root element before first paint. It reads `localStorage['lyra-theme']`.
1711
- `createLyraThemeBootstrap({ storageKey })` returns the same kind of string for an application-owned
1712
- key, so an existing persistence layer can reuse the pre-paint half independently of
1713
- `setLyraTheme()`/`getLyraTheme()`. Calling the factory with no options returns the same string as
1714
- `lyraThemeBootstrap`. The result is a string precisely so this can happen in an unbundled
1715
- `<script>` context without shipping or parsing the module. Custom keys are escaped against HTML
1716
- script termination and JavaScript line separators. Under a Content Security Policy, give the
1717
- inline script the nonce or hash required by the application:
1445
+ accentBackground is an absolute CSS color used as the accent ramps' reference surface. surface is a separate visual treatment: solid or glass. density selects compact, comfortable, or touch. These axes remain independent, so changing the look does not reset mode, accent, surface, or density.
1718
1446
 
1719
- ```html
1447
+ A LyraLook is a named token definition. Define an application look with defineLyraLook({ id, tokens }), then pass it as the look choice. A look supplies --lr-theme-* inputs; overrides applies an independent map of those inputs to the document root. Both accept CSS strings or per-mode { light, dark } values. Overrides replace the previous map wholesale; compose them with object spread. Values are validated, trimmed, and deep-frozen. Invalid entries are ignored, and maps are limited to 512 entries.
1448
+
1449
+ The token grammar rejects CSS-wide keywords, control characters, unsafe punctuation, unbalanced functions or quotes, and functions outside the documented color and layout set. Color families checked by the static contrast gate are corrected to meet their contrast floor before painting. The snapshot and stored record retain the normalized requested values. Inline style ownership records only properties Lyra wrote and restores an earlier author value when a choice is cleared.
1450
+
1451
+ **Saved styles and the no-flash bootstrap.** getLyraStyle() re-reads storage on each call, so a value written by another tab is visible. If storage is unavailable, the runtime reports its last applied style and continues applying changes without persistence. lyraThemeBootstrap is a self-contained IIFE string for a script in head before stylesheets; it restores the saved style before first paint. createLyraThemeBootstrap({ storageKey }) produces the same string for an application-owned key. Both runtime and bootstrap safely read v1 records using mode: "auto", surface, and tokens, translating them to the current style axes. Keep this reader when migrating old saved preferences; new writes use the current style record.
1452
+
1453
+ The bootstrap string can be inlined with the application's CSP nonce or hash:
1454
+
1455
+ ~~~html
1720
1456
  <head>
1721
1457
  <script>
1722
1458
  /* server-inlines lyraThemeBootstrap here */
1723
1459
  </script>
1724
1460
  <link rel="stylesheet" href="/theme.css" />
1725
1461
  </head>
1726
- ```
1727
-
1728
- Both variants read a stored `{ mode, accent, surface }` record, resolve `auto`, and derive the same
1729
- per-role ramp(s) from the same math — the bootstrap re-implements it inline (self-contained, so it
1730
- can run before any module loads) rather than importing the runtime, but the two are tested to never
1731
- drift. A missing or malformed record receives the runtime's
1732
- `{ mode: 'auto', accent: null, surface: null }` default; blocked `localStorage` leaves the document
1733
- untouched rather than throwing before your app loads.
1734
-
1735
- The stored record also carries `tokens` while a map is applied, and the bootstrap restores it
1736
- before first paint: the same grammar, the same per-mode branch for the resolved mode, and the same
1737
- contrast floor as the runtime. It records the names it wrote on `<html>`, so the runtime's first
1738
- apply removes or rewrites exactly those. Token values are read from storage at run time and never
1739
- serialized into the script string. **The bootstrap's bytes change with library releases** — this
1740
- one included — so a deployment that pins a CSP hash for the inline script (or for
1741
- `theme-bootstrap.js`) must regenerate that hash when upgrading, or the browser blocks the bootstrap
1742
- and the first-paint flash returns.
1743
-
1744
- **External-file delivery for a strict CSP.** `@aceshooting/lyra-ui/theme-bootstrap.js` is a
1745
- third, non-module way to ship the same bootstrap: a static script asset published alongside the
1746
- package, containing exactly `lyraThemeBootstrap`'s bytes (both are produced from the same build
1747
- step, so they can never drift apart). Reference it with a plain `<script src>` in `<head>`,
1748
- still before any stylesheet:
1462
+ ~~~
1749
1463
 
1750
- ```html
1751
- <head>
1752
- <script src="/vendor/theme-bootstrap.js"></script>
1753
- <link rel="stylesheet" href="/theme.css" />
1754
- </head>
1755
- ```
1464
+ For a strict CSP that disallows inline scripts, the package also publishes theme-bootstrap.js, a classic script asset with identical bytes. Load it synchronously before stylesheets. Its optional data-lr-theme-storage-key and data-lr-theme-attributes attributes select an application-owned storage key or mode-attribute list; values are validated and fall back to defaults when invalid.
1756
1465
 
1757
- This exists for a Content-Security-Policy that forbids `unsafe-inline` and cannot mint a
1758
- per-response nonce — a static HTML entry, for example — where the documented inline-script
1759
- nonce/hash guidance above does not apply. Serving it same-origin (copy it into your build output,
1760
- or configure your bundler/static host to do so) needs no hash at all; hashing it for an even
1761
- stricter policy uses the same CSP `script-src` hash mechanism browsers already apply to any
1762
- external script resource.
1466
+ **Migrating the retired theme facade.** New code uses setLyraStyle() and getLyraStyle(). Map old auto mode to system, an old surface reference color to accentBackground, and an old token map to overrides; review custom CSS colors that share a gemstone name before choosing a named accent. Replace preset definitions with a LyraLook plus explicit style choices. Replace selectors for data-lr-theme-preset with the actual axis they need, such as data-lr-look="shadcn". Import looks/shadcn.css in place of the removed fixed themes/shadcn.css facade and select the look with setLyraStyle({ look: "shadcn" }) or a scoped data-lr-look attribute. Listen for lr-style-change and read event.detail.style and event.detail.changed; the old theme and preset events are no longer emitted. For the complete project-by-project sequence, see [Upgrading from v23 to v24](v23-to-v24-migration.md).
1763
1467
 
1764
- **Configuring the static asset from its own `<script>` tag.** `theme-bootstrap.js` must be loaded
1765
- as a plain classic script — never `type="module"` and never `async` — because it reads its own
1766
- configuration synchronously through `document.currentScript` while it runs, and that property is
1767
- `null` for both of those loading modes (as well as for anything scheduled after the script has
1768
- already finished executing). Two optional attributes on that same `<script>` tag override the
1769
- defaults without regenerating the file:
1770
-
1771
- ```html
1772
- <head>
1773
- <script
1774
- src="/vendor/theme-bootstrap.js"
1775
- data-lr-theme-storage-key="my-app-theme"
1776
- data-lr-theme-attributes="data-lr-theme data-theme"
1777
- ></script>
1778
- <link rel="stylesheet" href="/theme.css" />
1779
- </head>
1780
- ```
1781
-
1782
- - `data-lr-theme-storage-key` — the `localStorage` key to read, in place of the default
1783
- `'lyra-theme'`. Equivalent to `createLyraThemeBootstrap({ storageKey })`'s argument, but
1784
- resolved by the static file itself at parse time rather than baked in ahead of time. This is
1785
- what lets an application with its own pre-existing storage key use the static asset instead of
1786
- inlining a per-app copy.
1787
- - `data-lr-theme-attributes` — a space-separated list of attribute names to set on
1788
- `<html>` in place of the default `data-lr-theme data-theme` pair, replacing that list entirely
1789
- rather than adding to it.
1790
-
1791
- Both attributes are optional and independently validated; an absent, empty, oversized, or
1792
- malformed value falls back to the built-in default rather than throwing, so a `<script>` tag with
1793
- neither attribute — every existing deployment — behaves exactly as before. `data-lr-theme-storage-key`
1794
- must be a non-empty string of at most 200 characters (its content is otherwise unrestricted — it is
1795
- only ever used as an opaque `localStorage` key, never written to the DOM). `data-lr-theme-attributes`
1796
- must parse to one to eight tokens, each unique and each matching `data-[a-z0-9]+(-[a-z0-9]+)*` —
1797
- which rejects an event-handler name (`onload`), a native attribute (`style`, `class`, `id`), any
1798
- token containing whitespace, a quote, `=`, or a control character, an empty list, and a duplicated
1799
- token — because these attribute names reach `setAttribute()`/`removeAttribute()` on the document
1800
- root. A `document.currentScript` of `null` (module/async misuse, or a script tag re-read after it
1801
- finished running) is treated the same as no configuration at all.
1802
-
1803
- An application-owned key from `createLyraThemeBootstrap({ storageKey })` can still be inlined as
1804
- documented above; the static file's own script-tag attributes are the alternative for a strict-CSP
1805
- deployment that cannot inline that call.
1806
-
1807
- **Migrating from 15.x.** `accent` used to be exactly an absolute CSS color or `null`; that shape
1808
- still works unchanged (`setLyraTheme({ accent: '#7c3aed' })` keeps deriving only the brand ramp).
1809
- What changed is `LyraTheme` gaining a `surface` field alongside it — a strict superset for every
1810
- caller that only ever read/wrote `mode`/`accent`, since `getLyraTheme()` now also returns
1811
- `surface: null` by default. Only code that structurally compares the whole returned record (for
1812
- example `assert.deepEqual(getLyraTheme(), { mode, accent })`) needs the extra field added.
1813
-
1814
- **`tokens` is optional and absent unless used.** `LyraTheme` also gains an optional `tokens` field;
1815
- `getLyraTheme()`, the stored record and both event details omit the key entirely until a token map
1816
- is applied, so `assert.deepEqual(getLyraTheme(), { mode, accent, surface })` keeps passing.
1468
+ Historical v1 stored records remain readable by the runtime and bootstrap. The removed JavaScript functions, preset subpaths, preset event, root marker, and fixed stylesheet are not available in the current API.
1817
1469
 
1818
1470
  ### Invalidating canvas theme values
1819
1471
 
@@ -2084,7 +1736,7 @@ itself — gap and flow margins would add up (set `--lr-prose-flow-space: 0` if
2084
1736
  | --------------------- | ------------------------------------------------------------- | --------------------------------------- |
2085
1737
  | Heading sizes | 2 / 1.75 / 1.25 / 1.125rem | 2.25 / 1.875 / 1.5 / 1.25rem |
2086
1738
  | h1 weight | 700 | 800 |
2087
- | Tracking | `normal` unless the hook or theme input sets it; `themes/shadcn.css` sets -0.025em | tight |
1739
+ | Tracking | `normal` unless the hook or theme input sets it; `looks/shadcn.css` sets -0.025em | tight |
2088
1740
  | Flow spacing | 1rem (`--lr-prose-flow-space`) | 1.5rem |
2089
1741
  | Space before headings | 2 × flow = 2rem for every level | 2.5rem before h2, 2rem before h3 |
2090
1742
  | Paragraph leading | 1.6 | 1.75rem |
@@ -2154,7 +1806,7 @@ and opacity tokens first. These additional hooks customize only the light-DOM bu
2154
1806
  | `--lr-prose-quote-padding` | `--lr-space-l`; logical quote inset in `lr-prose` and `lr-typography` |
2155
1807
  | `--lr-prose-quote-border-width` | `--lr-border-width-thick` in `lr-prose`, `--lr-border-width-medium` in `lr-typography`; logical quote edge |
2156
1808
  | `--lr-heading-letter-spacing` | `--lr-theme-heading-letter-spacing`, then `normal`; heading tracking for `lr-heading-1`–`4` and `lr-typography` headings |
2157
- | `--lr-theme-heading-letter-spacing` | unset; theme input for heading tracking (no resolved `--lr-*` token); `themes/shadcn.css` sets it |
1809
+ | `--lr-theme-heading-letter-spacing` | unset; theme input for heading tracking (no resolved `--lr-*` token); `looks/shadcn.css` sets it |
2158
1810
  | `--lr-visually-hidden-size` | `--lr-size-1px` (1px); retained hidden box size |
2159
1811
  | `--lr-native-link-decoration-width` | `--lr-border-width-thin`; resting underline thickness |
2160
1812
  | `--lr-native-link-underline-offset` | `--lr-space-2xs`; underline offset |
@@ -2234,8 +1886,8 @@ The side-effect-free `@aceshooting/lyra-ui/localization.js` entry exports
2234
1886
  `LyraPluralMessage`, `LyraPluralCategory`, `LyraLocaleBridgeOptions` and `LyraLocaleBridgeCleanup`.
2235
1887
  The package root continues to re-export the runtime for compatibility and remains registration-free;
2236
1888
  use the dedicated entry when the application only needs locale setup and the narrower import graph.
2237
- The former `@aceshooting/lyra-ui/utilities/localization.js` entry point was removed in 23.0.0.
2238
- Change that import specifier to `@aceshooting/lyra-ui/localization.js`; the helper contracts are unchanged.
1889
+ The former utility-scoped entry point was removed in 23.0.0. Change that import specifier to
1890
+ `@aceshooting/lyra-ui/localization.js`; the helper contracts are unchanged.
2239
1891
  **`LYRA_DEFAULT_STRINGS` is the authoritative key list** (matching the `LyraMessageKey` union) —
2240
1892
  read it to find the key to override rather than guessing one. Date, number, byte, relative-time and
2241
1893
  calendar output goes through `Intl`.
@@ -2673,12 +2325,16 @@ and adapters emit empty JavaScript and do not register elements.
2673
2325
 
2674
2326
  ## SSR and declarative shadow DOM
2675
2327
 
2328
+ When replacing the removed combined `ssr-loader.js` route, split browser hydration from component
2329
+ registration and server rendering as shown below. The [v23-to-v24 migration guide](v23-to-v24-migration.md)
2330
+ has the ordered upgrade steps and environment-specific import mapping.
2331
+
2676
2332
  Root, `all.js`, and granular component imports are server-safe under the supported Node 22+ runtime.
2677
2333
  `@aceshooting/lyra-ui/ssr/all.js` is the **server-only** convenience entry: unlike the browser
2678
2334
  `all.js`, it registers the complete inventory including the optional-peer families — defining those
2679
2335
  tags never imports their peers (each component loads its own lazily, client-side), and the
2680
2336
  browser-bundle argument for excluding them does not apply to a server render. Use the public
2681
- `@aceshooting/lyra-ui/ssr-loader.js` entry for the tested Lit SSR contract. Its exported
2337
+ `@aceshooting/lyra-ui/ssr.js` entry for the tested Lit SSR contract. Its exported
2682
2338
  `LYRA_SSR_SUPPORT_MATRIX`, `LYRA_SSR_RENDER_AND_HYDRATE_TAGS`, and
2683
2339
  `LYRA_SSR_CLIENT_RENDER_TAGS` classify every inventory tag exactly once:
2684
2340
 
@@ -2691,7 +2347,7 @@ browser-bundle argument for excluding them does not apply to a server render. Us
2691
2347
  Server setup (the fallback must precede Lit's renderer):
2692
2348
 
2693
2349
  ```ts
2694
- import { lyraSsrElementRenderers } from "@aceshooting/lyra-ui/ssr-loader.js";
2350
+ import { lyraSsrElementRenderers } from "@aceshooting/lyra-ui/ssr.js";
2695
2351
  import { render, LitElementRenderer } from "@lit-labs/ssr";
2696
2352
  import { html } from "lit";
2697
2353
 
@@ -2700,7 +2356,7 @@ const result = render(html`<lr-page><main>Dashboard</main></lr-page>`, {
2700
2356
  });
2701
2357
  ```
2702
2358
 
2703
- In the browser, import `@aceshooting/lyra-ui/ssr-loader.js` before any other module that can import
2359
+ In the browser, import `@aceshooting/lyra-ui/hydration.js` before any other module that can import
2704
2360
  Lit. This installs `@lit-labs/ssr-client/lit-element-hydrate-support.js` before component
2705
2361
  registration. `getLyraSsrMode(tagName)` reads one tag's tier, and
2706
2362
  `diagnoseLyraHydration(document)` inspects current Lyra hosts in the supplied tree and every
@@ -2764,8 +2420,8 @@ real component — instead of hand-rolling a `CustomEvent` and guessing its shap
2764
2420
  ```ts
2765
2421
  import { createLyraEvent } from '@aceshooting/lyra-ui/testing';
2766
2422
 
2767
- const event = createLyraEvent('lr-confirm-bar', 'lr-approve', { args: null, waitUntil: () => {} });
2768
- event.cancelable; // true — lr-confirm-bar's own lr-approve call site is cancelable
2423
+ const event = createLyraEvent('lr-confirm-bar', 'lr-approve-request', { args: null, waitUntil: () => {} });
2424
+ event.cancelable; // true — lr-confirm-bar's own lr-approve-request call site is cancelable
2769
2425
  target.dispatchEvent(event);
2770
2426
  ```
2771
2427
 
@@ -2811,11 +2467,14 @@ await activateStep(stepper, 'review'); // clicks the [part="step"] button for th
2811
2467
  same `[part="option"]` row plus `data-value` delegated-click pattern for its own listbox/popup.
2812
2468
  `submitConfirmDecision()`
2813
2469
  accepts either `<lr-confirm-bar>` or `<lr-tool-approval-dialog>`, which render the identical
2814
- `[part="approve-button"]`/`[part="deny-button"]` pair for the same `lr-approve`/`lr-deny`
2815
- contract.
2470
+ `[part="approve-button"]`/`[part="deny-button"]` pair and emit cancelable
2471
+ `lr-approve-request`/`lr-deny-request` events. The driver awaits the component's render;
2472
+ if a listener calls `preventDefault()` on either component, or `waitUntil()` on the confirm bar,
2473
+ the action may still be pending when the driver returns.
2816
2474
 
2817
2475
  Each driver is `async` and awaits `updateComplete` before returning, so assertions written
2818
- immediately after it see fully-settled DOM/state. Each one also throws a plain `Error` — never a
2476
+ immediately after it see the render reached by that interaction. Host-driven async decisions
2477
+ still need their own completion check. Each driver also throws a plain `Error` — never a
2819
2478
  silent no-op — when the requested interaction cannot actually happen: the target is disabled, the
2820
2479
  option owner/stepper is read-only or has no matching row/step currently rendered, or the confirm
2821
2480
  bar is already decided. That is the exact failure mode a hand-rolled event misses (for example
@@ -2987,10 +2646,8 @@ they carry no `tag`: `components/media/flag/flag-peer.js`,
2987
2646
  stand in for. They are a separate array rather than tag-less rows mixed into `entries`, so a reader
2988
2647
  that keys `entries` by `tag` keeps working; `integrations`, `distModule` and `localeKeys` are all
2989
2648
  additive, which is why the schema version is unchanged. `registrationModule` is the `src/` path used
2990
- internally; `distModule` is that same module's own published deep specifier (e.g.
2991
- `./components/data/table/table.js`) so a caller holding either a per-tag alias or a deep import can
2992
- resolve the other without reading `src/` or walking `dist/` — for an integration bridge,
2993
- `distModule` equals `entry`, since there is no separate alias. `registers` is every `lr-*` tag
2649
+ internally; `distModule` is a retained published specifier and equals the stable `entry` for v24
2650
+ per-tag registrations and integration bridges. `registers` is every `lr-*` tag
2994
2651
  importing `entry` defines, direct or transitive, derived from the same transitive-import analysis
2995
2652
  `scripts/check-component-dependencies.mjs` already performs against the real registration graph
2996
2653
  (not a second hand-maintained list). `localeKeys` is every `LyraMessageKey` the registered tags can
@@ -3033,8 +2690,8 @@ same way `all.js` registers its root-included tags (the package root itself regi
3033
2690
  and want a single import; reach for the granular
3034
2691
  `@aceshooting/lyra-ui/components/<tag>.js` path when you do not, because a barrel cannot be
3035
2692
  tree-shaken down to the two elements you actually render. The tag-shaped path remains stable if a
3036
- component moves between Lyra's internal family folders; existing family-shaped registration paths
3037
- remain supported.
2693
+ component moves between Lyra's internal family folders. Duplicate nested registration paths were
2694
+ removed in v24; family barrels remain supported.
3038
2695
 
3039
2696
  ```js
3040
2697
  import "@aceshooting/lyra-ui/components/forms"; // every form control
@@ -5037,34 +4694,30 @@ These named interfaces and helper signatures are available to typed integrations
5037
4694
  copy, so a glyph rendered anywhere else on the page can match it exactly by consuming the same
5038
4695
  export and attribute.
5039
4696
 
5040
- - **`theme-presets-contracts`** — Shared utility contracts.
5041
- `applyLyraThemePreset(/* public names: presetOrName */): unknown`
5042
- `defineLyraThemePreset(/* public names: preset */): unknown`
5043
- `LyraThemePresetChangeDetail {
5044
- id: unknown;
5045
- theme: unknown;
5046
- }`
5047
- `LyraThemePreset {
5048
- id: unknown;
5049
- theme: unknown;
5050
- }`
5051
-
5052
4697
  - **`theme-theme-contracts`** — Shared utility contracts.
5053
- `createLyraThemeBootstrap(/* public names: options */): unknown`
5054
- `getLyraTheme(): unknown`
4698
+ `createLyraThemeBootstrap(options?: LyraThemeBootstrapOptions): string`
5055
4699
  `LyraThemeBootstrapOptions {
5056
- storageKey: unknown;
5057
- }`
5058
- `LyraTheme {
5059
- mode: unknown;
5060
- accent: unknown;
5061
- surface: unknown;
5062
- tokens: unknown;
4700
+ storageKey?: string;
5063
4701
  }`
5064
- `setLyraTheme(/* public names: theme */): unknown`
4702
+ `LyraThemeSemanticRole = 'brand' | 'success' | 'warning' | 'danger' | 'neutral'`
4703
+ `LyraThemeAccentValue = string | null | { light?: string | null; dark?: string | null }`
4704
+ `LyraThemeAccent = string | null | Partial<Record<LyraThemeSemanticRole, LyraThemeAccentValue>>`
4705
+ ``LyraThemeTokenName = `--lr-theme-${string}```
4706
+ `LyraThemeTokenValue = string | { light?: string | null; dark?: string | null }`
4707
+ `LyraThemeTokens = Readonly<Record<LyraThemeTokenName, LyraThemeTokenValue>>`
4708
+ `LyraStyleAxis = 'look' | 'surface' | 'density' | 'mode' | 'accent'`
4709
+ `LyraStyleField = LyraStyleAxis | 'accentBackground' | 'overrides'`
4710
+ `LyraLookId = 'lyra' | 'shadcn' | 'material' | (string & {})`
4711
+ `LyraSurface = 'solid' | 'glass'`
4712
+ `LyraDensity = 'compact' | 'comfortable' | 'touch'`
4713
+ `LyraMode = 'light' | 'dark' | 'system' | 'unset'`
4714
+ `LyraAccentName = 'emerald' | 'peridot' | 'topaz' | 'ruby' | 'tourmaline' | 'amethyst' | 'aquamarine' | 'sapphire' | 'hematite'`
4715
+ `LyraAccent = LyraAccentName | LyraThemeAccent`
4716
+ `LyraAccentBackground = string | { light?: string | null; dark?: string | null } | null`
5065
4717
  `applyLyraStyleScope(element: Element, choices: LyraStyleScopeChoices | null): void`
5066
4718
  `defineLyraLook<const Look extends LyraLook>(look: Look): Readonly<Look>`
5067
4719
  `getLyraStyle(): Readonly<LyraStyle>`
4720
+ `lyraThemeBootstrap: string`
5068
4721
  `LyraLook {
5069
4722
  readonly id: LyraLookId;
5070
4723
  readonly tokens: LyraThemeTokens;
@@ -5868,19 +5521,21 @@ unchanged. Step numbers and interactive context-meter legend rows use matching s
5868
5521
  pairs; control-border colors no longer act as their text backgrounds. Material uses 8% hover and
5869
5522
  12% press state layers with its `currentColor` mix partner, keeping filled-action labels at 4.5:1.
5870
5523
 
5871
- Runtime custom accents, including `setLyraTheme({ accent })` and legacy gemstone presets, now
5524
+ Runtime custom accents, including `setLyraStyle({ accent })` and legacy gemstone presets, now
5872
5525
  floor the loud fill to 4.5:1 against its quiet fill and the built-in reference surfaces because the
5873
5526
  same token paints accent text. Its on-loud foreground follows the corrected fill. For example,
5874
5527
  light shadcn emerald changes from `#34d399` with black on-loud text to `#1a6a4d` with white text
5875
5528
  (6.537:1). Checked controls, brand text and streaming accents follow that loud token; secondary
5876
5529
  neutral controls stay unchanged. The requested and persisted accent color is retained.
5877
5530
 
5878
- The compatibility `setLyraTheme()` API shares the same stored selection. Its `surface` remains an
5879
- accent reference color (the new API calls that `accentBackground`), and its `auto` mode corresponds
5880
- to `system`. A stored legacy CSS color such as `aquamarine` stays that color; it is not silently
5881
- converted into the gemstone of the same name. `parseLyraStyleRecord()` and `lyraStyleAttributes()`
5882
- provide DOM-free server-rendering helpers. Render attributes through the framework's normal HTML
5883
- escaping; the returned object is not an HTML string.
5531
+ The runtime and no-flash bootstrap still read v1 records written by the retired theme API. On
5532
+ read, auto becomes system and the old accent reference surface becomes accentBackground. A saved
5533
+ `tokens` map remains runtime-look tokens (`lookForm: 'runtime'`), separate from public `overrides`;
5534
+ without a valid saved look id, the runtime look is named `custom`. A CSS color such as aquamarine
5535
+ remains a CSS color and is not converted into the named gemstone. New writes use the current style
5536
+ record while retaining those runtime-look tokens. parseLyraStyleRecord() and
5537
+ lyraStyleAttributes() provide DOM-free server-rendering helpers; render the returned attributes
5538
+ through the framework's normal HTML escaping.
5884
5539
 
5885
5540
  Glass is a treatment for eligible navigation and floating chrome. It does not make content panels,
5886
5541
  forms, charts or dialogs translucent. Unsupported blur, reduced transparency, increased contrast and