@elabs-ai/components-ui 4.1.0 → 4.2.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 (193) hide show
  1. package/dist/{chunk-JKPPTL63.js → chunk-FCH4ZN6G.js} +12 -3
  2. package/dist/chunk-FCH4ZN6G.js.map +1 -0
  3. package/dist/form.js +3 -3
  4. package/dist/form.js.map +1 -1
  5. package/dist/index.d.ts +126 -35
  6. package/dist/index.js +3346 -2079
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/cn.js +10 -1
  9. package/dist/lib/cn.js.map +1 -1
  10. package/package.json +3 -3
  11. package/src/__contract__/attribution-panel.contract.test.tsx +49 -0
  12. package/src/__contract__/badge.contract.test.tsx +49 -0
  13. package/src/__contract__/bounded-number.contract.test.tsx +49 -0
  14. package/src/__contract__/button.contract.test.tsx +49 -0
  15. package/src/__contract__/color-picker.contract.test.tsx +49 -0
  16. package/src/__contract__/combobox.contract.test.tsx +49 -0
  17. package/src/__contract__/command-trigger.contract.test.tsx +49 -0
  18. package/src/__contract__/copyable-value.contract.test.tsx +49 -0
  19. package/src/__contract__/date-picker.contract.test.tsx +49 -0
  20. package/src/__contract__/date-range-picker.contract.test.tsx +49 -0
  21. package/src/__contract__/empty-state.contract.test.tsx +49 -0
  22. package/src/__contract__/error-state.contract.test.tsx +49 -0
  23. package/src/__contract__/icon-button.contract.test.tsx +49 -0
  24. package/src/__contract__/input.contract.test.tsx +49 -0
  25. package/src/__contract__/key-value-editor.contract.test.tsx +49 -0
  26. package/src/__contract__/list-editor.contract.test.tsx +49 -0
  27. package/src/__contract__/loading-state.contract.test.tsx +49 -0
  28. package/src/__contract__/metric-card.contract.test.tsx +49 -0
  29. package/src/__contract__/model-picker.contract.test.tsx +49 -0
  30. package/src/__contract__/nav-notifications.contract.test.tsx +49 -0
  31. package/src/__contract__/number-input.contract.test.tsx +49 -0
  32. package/src/__contract__/rating.contract.test.tsx +49 -0
  33. package/src/__contract__/schema-form.contract.test.tsx +49 -0
  34. package/src/__contract__/section-header.contract.test.tsx +49 -0
  35. package/src/__contract__/segmented-field.contract.test.tsx +49 -0
  36. package/src/__contract__/slider-number.contract.test.tsx +49 -0
  37. package/src/__contract__/status-badge.contract.test.tsx +49 -0
  38. package/src/__contract__/tag-input.contract.test.tsx +49 -0
  39. package/src/__contract__/text.contract.test.tsx +49 -0
  40. package/src/__contract__/textarea.contract.test.tsx +49 -0
  41. package/src/__contract__/timeline.contract.test.tsx +49 -0
  42. package/src/__contract__/toggle.contract.test.tsx +49 -0
  43. package/src/__contract__/transfer.contract.test.tsx +49 -0
  44. package/src/__contract__/tree-select.contract.test.tsx +49 -0
  45. package/src/__contract__/tree.contract.test.tsx +49 -0
  46. package/src/__contract__/virtual-select.contract.test.tsx +49 -0
  47. package/src/__contract__/workspace-picker.contract.test.tsx +49 -0
  48. package/src/components/accordion/accordion.tsx +13 -4
  49. package/src/components/alert/alert.tsx +11 -3
  50. package/src/components/alert-dialog/alert-dialog.test.tsx +32 -0
  51. package/src/components/alert-dialog/alert-dialog.tsx +28 -6
  52. package/src/components/app-shell/app-shell.test.tsx +99 -0
  53. package/src/components/app-shell/app-shell.tsx +29 -10
  54. package/src/components/attribution-panel/attribution-panel.tsx +2 -0
  55. package/src/components/attribution-panel/attributions.generated.ts +2 -2
  56. package/src/components/avatar/avatar.tsx +4 -1
  57. package/src/components/badge/badge.test.tsx +8 -0
  58. package/src/components/badge/badge.tsx +14 -4
  59. package/src/components/bento-grid/bento-grid.test.tsx +23 -2
  60. package/src/components/bento-grid/bento-grid.tsx +10 -0
  61. package/src/components/bounded-number/bounded-number.test.tsx +1 -1
  62. package/src/components/bounded-number/bounded-number.tsx +78 -75
  63. package/src/components/breadcrumb/breadcrumb.tsx +3 -2
  64. package/src/components/button/button.tsx +18 -12
  65. package/src/components/button-group/button-group.tsx +1 -1
  66. package/src/components/calendar/calendar.tsx +3 -2
  67. package/src/components/card/card.test.tsx +24 -2
  68. package/src/components/card/card.tsx +47 -6
  69. package/src/components/carousel/carousel.test.tsx +169 -0
  70. package/src/components/carousel/carousel.tsx +93 -32
  71. package/src/components/change-review/change-review.tsx +38 -29
  72. package/src/components/checkbox/checkbox.tsx +1 -0
  73. package/src/components/collapsible-panel/use-collapsible-panel.ts +0 -3
  74. package/src/components/color-picker/color-picker.tsx +17 -11
  75. package/src/components/combobox/combobox.stories.tsx +10 -0
  76. package/src/components/combobox/combobox.test.tsx +54 -0
  77. package/src/components/combobox/combobox.tsx +25 -11
  78. package/src/components/command/command.test.tsx +15 -0
  79. package/src/components/command/command.tsx +8 -6
  80. package/src/components/context-menu/context-menu.tsx +16 -7
  81. package/src/components/context-rail/context-rail.tsx +4 -4
  82. package/src/components/copyable-value/copyable-value.stories.tsx +10 -0
  83. package/src/components/copyable-value/copyable-value.test.tsx +28 -1
  84. package/src/components/copyable-value/copyable-value.tsx +12 -1
  85. package/src/components/date-picker/date-picker.stories.tsx +9 -0
  86. package/src/components/date-picker/date-picker.test.tsx +52 -0
  87. package/src/components/date-picker/date-picker.tsx +13 -15
  88. package/src/components/date-range-picker/date-range-picker.stories.tsx +9 -0
  89. package/src/components/date-range-picker/date-range-picker.test.tsx +76 -1
  90. package/src/components/date-range-picker/date-range-picker.tsx +39 -28
  91. package/src/components/descriptions/descriptions.tsx +1 -1
  92. package/src/components/dialog/dialog.test.tsx +8 -3
  93. package/src/components/dialog/dialog.tsx +12 -4
  94. package/src/components/drawer/drawer.tsx +23 -6
  95. package/src/components/dropdown-menu/dropdown-menu.tsx +4 -4
  96. package/src/components/field/field.tsx +2 -0
  97. package/src/components/field-row/field-row.tsx +2 -0
  98. package/src/components/file-upload/file-upload.test.tsx +83 -10
  99. package/src/components/file-upload/file-upload.tsx +150 -28
  100. package/src/components/form/form.tsx +4 -2
  101. package/src/components/hover-card/hover-card.tsx +1 -0
  102. package/src/components/icon-button/icon-button.tsx +1 -1
  103. package/src/components/input/input.tsx +7 -3
  104. package/src/components/input-group/input-group.tsx +6 -6
  105. package/src/components/input-otp/input-otp.tsx +3 -1
  106. package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +2 -2
  107. package/src/components/label/label.tsx +1 -1
  108. package/src/components/list-editor/list-editor.test.tsx +24 -0
  109. package/src/components/list-editor/list-editor.tsx +126 -52
  110. package/src/components/locale-provider/locale-keys.test.ts +134 -0
  111. package/src/components/locale-provider/messages.ts +321 -0
  112. package/src/components/mention-input/mention-input.stories.tsx +3 -3
  113. package/src/components/mention-input/mention-input.test.tsx +51 -1
  114. package/src/components/mention-input/mention-input.tsx +58 -8
  115. package/src/components/menubar/menubar.tsx +16 -6
  116. package/src/components/nav-main/nav-main.tsx +7 -3
  117. package/src/components/nav-notifications/nav-notifications.test.tsx +11 -0
  118. package/src/components/nav-notifications/nav-notifications.tsx +11 -5
  119. package/src/components/nav-user/nav-user.tsx +5 -5
  120. package/src/components/navigation-menu/navigation-menu.tsx +16 -4
  121. package/src/components/number-input/number-input.stories.tsx +7 -3
  122. package/src/components/number-input/number-input.test.tsx +47 -0
  123. package/src/components/number-input/number-input.tsx +65 -24
  124. package/src/components/pagination/pagination.test.tsx +20 -1
  125. package/src/components/pagination/pagination.tsx +18 -14
  126. package/src/components/popover/popover.tsx +1 -0
  127. package/src/components/progress/progress.tsx +2 -0
  128. package/src/components/rating/rating.tsx +2 -0
  129. package/src/components/resizable/resizable.test.tsx +76 -0
  130. package/src/components/resizable/resizable.tsx +31 -5
  131. package/src/components/revision-timeline/revision-timeline.tsx +7 -2
  132. package/src/components/schema-form/schema-form-store.ts +506 -0
  133. package/src/components/schema-form/schema-form.test.tsx +122 -1
  134. package/src/components/schema-form/schema-form.tsx +163 -327
  135. package/src/components/section-header/section-header.tsx +2 -2
  136. package/src/components/select/select.tsx +7 -5
  137. package/src/components/separator/separator.tsx +1 -0
  138. package/src/components/sheet/sheet.test.tsx +39 -0
  139. package/src/components/sheet/sheet.tsx +40 -9
  140. package/src/components/side-dock/side-dock.test.tsx +26 -0
  141. package/src/components/side-dock/side-dock.tsx +15 -2
  142. package/src/components/sidebar/sidebar.test.tsx +35 -1
  143. package/src/components/sidebar/sidebar.tsx +20 -6
  144. package/src/components/skeleton/skeleton.tsx +1 -0
  145. package/src/components/slider-number/slider-number.stories.tsx +4 -1
  146. package/src/components/slider-number/slider-number.test.tsx +1 -1
  147. package/src/components/sonner/sonner.tsx +2 -0
  148. package/src/components/spinner/spinner.test.tsx +18 -0
  149. package/src/components/spinner/spinner.tsx +14 -4
  150. package/src/components/state-panel/state-panel.test.tsx +3 -3
  151. package/src/components/state-panel/state-panel.tsx +15 -8
  152. package/src/components/switch/switch.tsx +2 -0
  153. package/src/components/table/table.stories.tsx +3 -1
  154. package/src/components/table/table.test.tsx +9 -2
  155. package/src/components/table/table.tsx +18 -8
  156. package/src/components/tabs/index.ts +10 -1
  157. package/src/components/tabs/tabs.stories.tsx +48 -0
  158. package/src/components/tabs/tabs.test.tsx +67 -0
  159. package/src/components/tabs/tabs.tsx +175 -77
  160. package/src/components/tag-input/tag-input.tsx +4 -2
  161. package/src/components/team-switcher/team-switcher.tsx +3 -3
  162. package/src/components/textarea/textarea.tsx +1 -1
  163. package/src/components/theme-switcher/theme-switcher.stories.tsx +76 -0
  164. package/src/components/theme-switcher/theme-switcher.test.tsx +182 -0
  165. package/src/components/theme-switcher/theme-switcher.tsx +134 -10
  166. package/src/components/timeline/timeline.tsx +0 -3
  167. package/src/components/toggle/toggle.tsx +7 -2
  168. package/src/components/toggle-group/toggle-group.tsx +2 -0
  169. package/src/components/tooltip/tooltip.tsx +2 -1
  170. package/src/components/top-nav/top-nav.tsx +1 -1
  171. package/src/components/transfer/transfer.tsx +35 -30
  172. package/src/components/tree/tree.test.tsx +41 -0
  173. package/src/components/tree/tree.tsx +77 -22
  174. package/src/components/tree/use-tree-keyboard.ts +37 -14
  175. package/src/components/tree-select/tree-select.stories.tsx +9 -0
  176. package/src/components/tree-select/tree-select.test.tsx +5 -0
  177. package/src/components/tree-select/tree-select.tsx +11 -14
  178. package/src/components/typography/prose.test.tsx +3 -2
  179. package/src/components/typography/prose.tsx +3 -3
  180. package/src/components/virtual-select/virtual-select.stories.tsx +9 -0
  181. package/src/components/virtual-select/virtual-select.test.tsx +28 -2
  182. package/src/components/virtual-select/virtual-select.tsx +56 -18
  183. package/src/components/wizard/wizard.test.tsx +9 -0
  184. package/src/components/wizard/wizard.tsx +12 -4
  185. package/src/index.ts +5 -0
  186. package/src/lib/cn.ts +9 -0
  187. package/src/lib/csv.ts +48 -0
  188. package/src/lib/diff-rows.ts +2 -0
  189. package/src/lib/merge-refs.ts +2 -0
  190. package/src/lib/use-controllable-state.ts +47 -0
  191. package/src/lib/use-mobile.test.tsx +47 -0
  192. package/src/lib/use-mobile.ts +26 -10
  193. package/dist/chunk-JKPPTL63.js.map +0 -1
@@ -32,6 +32,7 @@ export const DEFAULT_MESSAGES: Record<string, MessageValue> = {
32
32
  // ── Generic (shared across packages — reuse these before minting a new key) ──
33
33
  close: "Close",
34
34
  copy: "Copy",
35
+ clear: "Clear",
35
36
  previous: "Previous",
36
37
  next: "Next",
37
38
  previousSlide: "Previous slide",
@@ -139,6 +140,8 @@ export const DEFAULT_MESSAGES: Record<string, MessageValue> = {
139
140
  "ui.schemaForm.testAction.success": "Connected",
140
141
  "ui.schemaForm.testAction.failure": "Test failed",
141
142
  // Pagination's ellipsis (sr-only — the visible glyph is decorative).
143
+ "ui.breadcrumb.label": "breadcrumb",
144
+ "ui.pagination.label": "pagination",
142
145
  "ui.pagination.morePages": "More pages",
143
146
  // Pagination's prev/next links — the ACCESSIBLE NAME (`aria-label`), distinct
144
147
  // from the visible `previous`/`next` text below it: an `aria-label` overrides
@@ -161,6 +164,11 @@ export const DEFAULT_MESSAGES: Record<string, MessageValue> = {
161
164
  "ui.tree.failedToLoad": "Failed to load",
162
165
  // ThemeSwitcher's "follow the OS" option, in both dropdown and toggle modes.
163
166
  "ui.themeSwitcher.system": "System",
167
+ // Group labels + scheme names in the family layout (ADR 0036).
168
+ "ui.themeSwitcher.theme": "Theme",
169
+ "ui.themeSwitcher.mode": "Mode",
170
+ "ui.themeSwitcher.light": "Light",
171
+ "ui.themeSwitcher.dark": "Dark",
164
172
  "ui.navNotifications.label": "Notifications",
165
173
  "ui.teamSwitcher.label": "Teams",
166
174
  "ui.teamSwitcher.addTeam": "Add team",
@@ -173,11 +181,99 @@ export const DEFAULT_MESSAGES: Record<string, MessageValue> = {
173
181
  // `viewer.content` vs this), so a translator can phrase either
174
182
  // independently and neither package depends on a string owned by another.
175
183
  "ui.table.scrollRegion": "Table contents, scrollable",
184
+ // NumberInput's step buttons (#4 i18n sweep).
185
+ "ui.numberInput.decrease": "Decrease",
186
+ "ui.numberInput.increase": "Increase",
187
+ // Combobox/VirtualSelect/TreeSelect each keep their own placeholder pair
188
+ // (English text coincides, but a translator may phrase a search box
189
+ // differently per surface — same rationale as `ui.table.scrollRegion`).
190
+ "ui.combobox.placeholder": "Select…",
191
+ "ui.combobox.searchPlaceholder": "Search…",
192
+ "ui.virtualSelect.placeholder": "Select…",
193
+ "ui.virtualSelect.searchPlaceholder": "Search…",
194
+ "ui.treeSelect.placeholder": "Select…",
195
+ "ui.treeSelect.moreSelected": "and {count} more selected",
196
+ "ui.fileUpload.selectedFiles": "Selected files",
197
+ "ui.fileUpload.dragDropHere": "Drag & drop files here",
198
+ "ui.fileUpload.browseFiles": "Browse files",
199
+ // Sidebar's mobile Sheet description (sr-only) and the collapse/expand
200
+ // trigger's shared name (SidebarTrigger's sr-only text + SidebarRail's
201
+ // aria-label/title).
202
+ "ui.sidebar.mobileDescription": "Displays the mobile sidebar.",
203
+ "ui.sidebar.toggle": "Toggle Sidebar",
204
+ "ui.revisionTimeline.label": "Revisions",
205
+ "ui.tree.retry": "Retry",
206
+ "ui.tree.retryLoadingChildren": "Retry loading children",
207
+ // Spinner's bare default label — kept WITHOUT an ellipsis (unlike the
208
+ // generic `loading` key) to stay byte-identical to its pre-i18n default.
209
+ "ui.spinner.label": "Loading",
210
+ "ui.statePanel.errorTitle": "Something went wrong",
211
+ "ui.statePanel.errorDescription": "An unexpected error occurred. Please try again.",
212
+ "ui.statePanel.errorEyebrow": "Error",
213
+ "ui.carousel.label": "Carousel",
214
+ // Transfer (#4 i18n sweep). `panelFallback`/`itemsFallback` back the two
215
+ // spots where the panel title isn't a plain string (a ReactNode) and the
216
+ // sr-only text falls back to a generic word instead.
217
+ "ui.transfer.selectAllIn": "Select all in {title}",
218
+ "ui.transfer.panelFallback": "panel",
219
+ "ui.transfer.searchIn": "Search {title}",
220
+ "ui.transfer.itemsFallback": "items",
221
+ "ui.transfer.searchPlaceholder": "Search…",
222
+ "ui.transfer.empty": "No items",
223
+ "ui.transfer.defaultSourceTitle": "Source",
224
+ "ui.transfer.defaultTargetTitle": "Target",
225
+ "ui.transfer.movedToTarget": {
226
+ one: "{count} item moved to the target list.",
227
+ other: "{count} items moved to the target list.",
228
+ },
229
+ "ui.transfer.movedToSource": {
230
+ one: "{count} item moved to the source list.",
231
+ other: "{count} items moved to the source list.",
232
+ },
233
+ "ui.transfer.moveControls": "Move controls",
234
+ "ui.transfer.moveSelectedRight": "Move selected right",
235
+ "ui.transfer.moveAllRight": "Move all right",
236
+ "ui.transfer.moveSelectedLeft": "Move selected left",
237
+ "ui.transfer.moveAllLeft": "Move all left",
238
+ // ChangeReview (#4 i18n sweep).
239
+ "ui.changeReview.heading": "Review changes",
240
+ "ui.changeReview.approvedCount": "{approved} of {total} approved",
241
+ "ui.changeReview.rejectAll": "Reject all",
242
+ "ui.changeReview.rejectAllLabel": "Reject all changes",
243
+ "ui.changeReview.approveAll": "Approve all",
244
+ "ui.changeReview.approveAllLabel": "Approve all changes",
245
+ "ui.changeReview.provenanceLabel": "Change provenance",
246
+ "ui.changeReview.emptyTitle": "No changes to review",
247
+ "ui.changeReview.emptyDescription":
248
+ "When an agent proposes edits, they'll appear here for your approval.",
249
+ "ui.changeReview.statusAdded": "added",
250
+ "ui.changeReview.statusRemoved": "removed",
251
+ "ui.changeReview.statusModified": "modified",
252
+ "ui.changeReview.phaseBefore": "Before",
253
+ "ui.changeReview.phaseAfter": "After",
254
+ "ui.changeReview.checkPassed": "Passed",
255
+ "ui.changeReview.checkFailed": "Failed",
256
+ "ui.changeReview.showDetail": "Show detail",
257
+ "ui.changeReview.hideDetail": "Hide detail",
258
+ "ui.changeReview.rejectHunk": "Reject hunk: {title}",
259
+ "ui.changeReview.approveHunk": "Approve hunk: {title}",
260
+ "ui.changeReview.changeType": "Change type: {status}",
261
+ "ui.changeReview.beforePrefix": "Before: ",
262
+ "ui.changeReview.afterPrefix": "After: ",
263
+ "ui.changeReview.approvedBadge": "Approved",
264
+ "ui.navNotifications.trigger": "Open notifications",
265
+ "ui.colorPicker.pickColor": "Pick color",
266
+ "ui.colorPicker.none": "None",
267
+ "ui.colorPicker.swatches": "Color swatches",
268
+ "ui.colorPicker.customHexLabel": "Custom hex",
269
+ "ui.colorPicker.notThemeAware": "(not theme-aware)",
270
+ "ui.colorPicker.customHex": "Custom hex color",
176
271
 
177
272
  // ── @elabs-ai/components-data ─────────────────────────────────────────────────────────────
178
273
  // The scroll region's accessible name is rendered ONLY when the table actually
179
274
  // overflows its container, so it is never announced for a table that fits.
180
275
  "data.table.scrollRegion": "Table contents, scrollable",
276
+ "data.table.loading": "Loading table data…",
181
277
  // Fallback name for a row's activation control when the row's first cell holds
182
278
  // no primitive value to name it after (see `rowActionLabel`).
183
279
  "data.table.rowAction": "Activate row",
@@ -232,6 +328,10 @@ export const DEFAULT_MESSAGES: Record<string, MessageValue> = {
232
328
  "data.table.reorderRoleDescription": "sortable",
233
329
  "data.facetFilter.clearFilters": "Clear filters",
234
330
  "data.columnPicker.toggleColumns": "Toggle columns",
331
+ "data.columnPicker.label": "Columns",
332
+ "data.searchInput.label": "Search",
333
+ "data.searchInput.placeholder": "Search…",
334
+ "data.searchInput.clear": "Clear search",
235
335
 
236
336
  // ── @elabs-ai/components-charts ───────────────────────────────────────────────────────────
237
337
  // Shared caption for any bare chart surface's layout-shaped skeleton
@@ -254,6 +354,43 @@ export const DEFAULT_MESSAGES: Record<string, MessageValue> = {
254
354
  // drift apart in translation.
255
355
  "charts.chartFrame.exportSvg": "Export as SVG",
256
356
  "charts.chartFrame.exportPng": "Export as PNG",
357
+ "charts.chartFrame.flipToTable": "Flip to table view",
358
+ "charts.chartFrame.showChart": "Show chart",
359
+ "charts.chartFrame.showAsTable": "Show as table",
360
+ "charts.chartFrame.downloadCsv": "Download CSV",
361
+ "charts.chartFrame.expandChart": "Expand chart",
362
+ "charts.chartFrame.expand": "Expand",
363
+ "charts.chartFrame.summary": "Summary",
364
+ "charts.chartFrame.noDataToSummarize": "No data to summarize.",
365
+ "charts.chartFrame.defaultTitle": "Chart",
366
+ "charts.chartFrame.summaryDetailLabel": "Chart summary",
367
+ "charts.legend.label": "Chart legend",
368
+ "charts.gantt.timeline": "Timeline",
369
+ "charts.gantt.dragToResizeColumn": "Drag to resize column",
370
+ "charts.gantt.viewControls": "Gantt view controls",
371
+ "charts.gantt.viewMode": "View mode",
372
+ "charts.gantt.viewModeLabel": "View mode:",
373
+ "charts.gantt.taskList": "Task list",
374
+ "charts.gantt.taskListLabel": "Tasks",
375
+ "charts.gantt.noTasksToDisplay": "No tasks to display",
376
+ "charts.gantt.unitDay": "Day",
377
+ "charts.gantt.unitWeek": "Week",
378
+ "charts.gantt.unitMonth": "Month",
379
+ "charts.gantt.unitQuarter": "Quarter",
380
+ "charts.gantt.unitHour": "Hour",
381
+ "charts.gantt.unitMinute": "Minute",
382
+ "charts.gantt.unitSecond": "Second",
383
+ "charts.gantt.unitMillisecond": "Millisecond",
384
+
385
+ // ── @elabs-ai/components-maps ─────────────────────────────────────────────────────────────
386
+ "maps.popup.close": "Close popup",
387
+ "maps.canvas.unavailableTitle": "Map unavailable",
388
+ "maps.canvas.unavailableDescription": "This browser can’t render WebGL maps.",
389
+
390
+ // ── @elabs-ai/components-flow ─────────────────────────────────────────────────────────────
391
+ "flow.inspectorPanel.close": "Close inspector",
392
+ "flow.inspectorPanel.title": "Inspector",
393
+ "flow.inspectorPanel.emptyMessage": "Select a node to see its details.",
257
394
 
258
395
  // ── @elabs-ai/components-ai ───────────────────────────────────────────────────────────────
259
396
  // Namespaced `ai.<area>.<key>` so package microcopy can't collide with the
@@ -271,6 +408,7 @@ export const DEFAULT_MESSAGES: Record<string, MessageValue> = {
271
408
  "ai.promptInput.stop": "Stop",
272
409
  "ai.agent.instructions": "Instructions",
273
410
  "ai.agent.tools": "Tools",
411
+ "ai.agent.outputSchema": "Output Schema",
274
412
  "ai.codeBlock.generating": "Generating…",
275
413
  "ai.composer.placeholder": "Ask me anything…",
276
414
  "ai.tokenUsage.usage": "Model context usage",
@@ -284,6 +422,7 @@ export const DEFAULT_MESSAGES: Record<string, MessageValue> = {
284
422
  // The mobile Sheet's sr-only title (#18) — distinct from `contextPanel.toggle`,
285
423
  // which labels the button that opens/closes it.
286
424
  "ai.contextPanel.title": "Context panel",
425
+ "ai.contextPanel.description": "Displays the chat context panel.",
287
426
  "ai.environmentVariables.toggleVisibility": "Toggle value visibility",
288
427
  "ai.gallery.label": "Image gallery",
289
428
  "ai.gallery.expandImage": "Expand image",
@@ -293,8 +432,11 @@ export const DEFAULT_MESSAGES: Record<string, MessageValue> = {
293
432
  // Shared by Tool's technical view and SchemaDisplay's request panel.
294
433
  "ai.schemaDisplay.parameters": "Parameters",
295
434
  "ai.schemaDisplay.response": "Response",
435
+ "ai.schemaDisplay.requestBody": "Request Body",
296
436
  // ReasoningTrigger's default not-yet-timed message (before `duration` is known).
297
437
  "ai.reasoning.thoughtDefault": "Thought for a few seconds",
438
+ "ai.reasoning.thinking": "Thinking…",
439
+ "ai.reasoning.thoughtForDuration": "Thought for {duration} seconds",
298
440
  "ai.stackTrace.empty": "No stack frames",
299
441
  "ai.webPreview.noConsoleOutput": "No console output",
300
442
  // PlanTrigger's icon-only collapse/expand control (sr-only).
@@ -439,6 +581,185 @@ export const DEFAULT_MESSAGES: Record<string, MessageValue> = {
439
581
  "ai.streamdown.copyLink": "Copy link",
440
582
  "ai.streamdown.copied": "Copied",
441
583
  "ai.streamdown.openLink": "Open link",
584
+ // Tool (#4 i18n sweep).
585
+ "ai.tool.showTechnicalDetails": "Show technical details",
586
+ "ai.tool.error": "Error",
587
+ "ai.tool.result": "Result",
588
+ // AssetPreview.
589
+ "ai.assetPreview.rowCount": { one: "{count} row", other: "{count} rows" },
590
+ "ai.assetPreview.noPreview": "No preview available…",
591
+ "ai.assetPreview.preview": "Preview",
592
+ "ai.assetPreview.raw": "Raw",
593
+ // WebPreview's sandboxed iframe accessible name.
594
+ "ai.webPreview.previewTitle": "Preview",
595
+ // FileTree's expand/collapse chevron control.
596
+ "ai.fileTree.expandFolder": "Expand {name}",
597
+ "ai.fileTree.collapseFolder": "Collapse {name}",
598
+ "ai.fileTree.noAssetsProduced": "No assets produced yet.",
599
+ "ai.conversation.download": "Download conversation",
600
+ // OpenInChat. `provider` is a brand name (ChatGPT, Claude, …) — kept
601
+ // untranslated data, interpolated into the translated "Open in …" phrase.
602
+ "ai.openInChat.trigger": "Open in chat",
603
+ "ai.openInChat.openInProvider": "Open in {provider}",
604
+ // TestResults.
605
+ "ai.testResults.passedCount": { one: "{count} passed", other: "{count} passed" },
606
+ "ai.testResults.failedCount": { one: "{count} failed", other: "{count} failed" },
607
+ "ai.testResults.skippedCount": { one: "{count} skipped", other: "{count} skipped" },
608
+ "ai.testResults.testsPassed": "{passed}/{total} tests passed",
609
+
610
+ // ── @elabs-ai/components-editor ───────────────────────────────────────────
611
+ // DecisionCard (ai-objects). Status vocabulary + the card's own accessible name
612
+ // and the "Alternatives considered" section (shared by its heading and its list's
613
+ // aria-label).
614
+ "editor.decisionCard.statusAccepted": "Accepted",
615
+ "editor.decisionCard.statusRejected": "Rejected",
616
+ "editor.decisionCard.statusProposed": "Proposed",
617
+ "editor.decisionCard.statusSuperseded": "Superseded",
618
+ "editor.decisionCard.label": "Decision: {label}",
619
+ "editor.decisionCard.alternativesConsidered": "Alternatives considered",
620
+ // KnowledgeCard (ai-objects).
621
+ "editor.knowledgeCard.label": "Knowledge fact",
622
+ "editor.knowledgeCard.heading": "Knowledge",
623
+ "editor.knowledgeCard.sources": "Sources",
624
+ "editor.knowledgeCard.source": "Source: {name}",
625
+ "editor.knowledgeCard.sourceUnresolved": "Source (unresolved): {name}",
626
+ // CalcBlock. `equals` is the sr-only prefix read before a computed value —
627
+ // intentionally ends in a space so it reads as a prefix (mirrors `ai.diffView.addedLine`).
628
+ "editor.calcBlock.error": "Error: {message}",
629
+ "editor.calcBlock.equals": "equals ",
630
+ "editor.calcBlock.total": "Total",
631
+ "editor.calcBlock.emptyBlock": "Empty calc block.",
632
+ // CopyButton. The "Copy" state reuses the shared generic `copy` key.
633
+ "editor.copyButton.copied": "Copied",
634
+ // EditorToolbar.
635
+ "editor.editorToolbar.language": "Language",
636
+ // Citations (markdown-academic).
637
+ "editor.citations.citationLabel": "Citation: {name}",
638
+ "editor.citations.unresolvedCitation": "Unresolved citation",
639
+ "editor.citations.unresolvedKey": "Unresolved: @{key}",
640
+ "editor.citations.references": "References",
641
+ // Math (markdown-academic).
642
+ "editor.math.renderError": "Could not render math",
643
+ "editor.math.renderErrorLabel": "Math (could not render): {tex}",
644
+ // CompletionMenu (markdown-editor).
645
+ "editor.completions.suggestions": "Suggestions",
646
+ "editor.completions.noSuggestions": "No suggestions",
647
+ // SlashMenu (markdown-editor).
648
+ "editor.slashMenu.insertBlock": "Insert block",
649
+ "editor.slashMenu.noMatchingBlocks": "No matching blocks",
650
+ // TableControlsView (markdown-editor/table-view — the WYSIWYG GFM table toolbar).
651
+ "editor.tableView.tableControls": "Table controls",
652
+ "editor.tableView.row": "Row",
653
+ "editor.tableView.col": "Col",
654
+ "editor.tableView.addRowAbove": "Add row above",
655
+ "editor.tableView.addRowBelow": "Add row below",
656
+ "editor.tableView.deleteRow": "Delete row",
657
+ "editor.tableView.addColumnLeft": "Add column left",
658
+ "editor.tableView.addColumnRight": "Add column right",
659
+ "editor.tableView.deleteColumn": "Delete column",
660
+ // Directive node-views (markdown-editor/directive-views — Milkdown WYSIWYG).
661
+ "editor.directiveViews.cardTitle": "Card title",
662
+ "editor.directiveViews.calloutTitle": "Callout title",
663
+ "editor.directiveViews.unknownBlock": "Unknown block: {name}",
664
+ "editor.directiveViews.unknownInlineBlock": "Unknown inline block:",
665
+ "editor.directiveViews.metricLabel": "Metric label",
666
+ "editor.directiveViews.metricLabelPlaceholder": "Label",
667
+ "editor.directiveViews.metricValue": "Metric value",
668
+ "editor.directiveViews.metricValuePlaceholder": "0",
669
+ "editor.directiveViews.editIteration": "Edit iteration…",
670
+ "editor.directiveViews.changeLayout": "Change layout",
671
+ "editor.directiveViews.transpose": "Transpose",
672
+ "editor.directiveViews.convertToStatic": "Convert to static",
673
+ "editor.directiveViews.needsEmbeddedValues": "— needs embedded values",
674
+ "editor.directiveViews.pivot": "Pivot",
675
+ "editor.directiveViews.iterate": "Iterate",
676
+ "editor.directiveViews.perItem": "· per {as}",
677
+ "editor.directiveViews.templateSuffix": "— template",
678
+ "editor.directiveViews.iterationActions": "Iteration actions",
679
+ "editor.directiveViews.iterationActionsTitle": "Iteration actions…",
680
+ // IterationBuilderDialog (markdown-iteration) — the guided iterate/pivot authoring modal.
681
+ "editor.iterationBuilder.pivotNoun": "pivot",
682
+ "editor.iterationBuilder.iterationNoun": "iteration",
683
+ "editor.iterationBuilder.editTitle": "Edit {noun}",
684
+ "editor.iterationBuilder.insertTitle": "Insert {noun}",
685
+ "editor.iterationBuilder.pivotDescription":
686
+ "Pick the row and column values, then write the per-cell template. The matrix below fills in live.",
687
+ "editor.iterationBuilder.iterationDescription":
688
+ "Add the list values, then write the per-row template. The result below fills in live.",
689
+ "editor.iterationBuilder.bindName": "Bind name",
690
+ "editor.iterationBuilder.bindNamePlaceholder": "item",
691
+ "editor.iterationBuilder.bindNameHintPrefix": "Use ",
692
+ "editor.iterationBuilder.bindNameHintSuffix": " in the template.",
693
+ "editor.iterationBuilder.rowValues": "Row values",
694
+ "editor.iterationBuilder.values": "Values",
695
+ "editor.iterationBuilder.valuePlaceholder": "Type a value, press Enter…",
696
+ "editor.iterationBuilder.columnValues": "Column values",
697
+ "editor.iterationBuilder.layout": "Layout",
698
+ "editor.iterationBuilder.perCellTemplate": "Per-cell template",
699
+ "editor.iterationBuilder.perRowTemplate": "Per-row template",
700
+ "editor.iterationBuilder.livePreview": "Live preview",
701
+ "editor.iterationBuilder.cancel": "Cancel",
702
+ "editor.iterationBuilder.save": "Save",
703
+ "editor.iterationBuilder.insert": "Insert",
704
+ // IterationTemplateDialog (markdown-iteration) — the lighter template-only modal.
705
+ "editor.templateDialog.editPivotTitle": "Edit pivot template",
706
+ "editor.templateDialog.editIterationTitle": "Edit iteration template",
707
+ "editor.templateDialog.descriptionPrefix": "The per-{unit} template. Use ",
708
+ "editor.templateDialog.descriptionMiddle": " placeholders (e.g. ",
709
+ "editor.templateDialog.descriptionSuffix":
710
+ ") — each is filled per {unit} when the block renders.",
711
+ "editor.templateDialog.editorLabel": "Iteration template editor",
712
+ "editor.templateDialog.cancel": "Cancel",
713
+ "editor.templateDialog.saveTemplate": "Save template",
714
+ // DocumentOutline (markdown-outline).
715
+ "editor.documentOutline.label": "Document outline",
716
+ "editor.documentOutline.empty": "No headings yet.",
717
+ // MarkdownToolbar (the source-pane formatting bar). Directive snippet labels
718
+ // are the Insert menu's fallback item text (`DIRECTIVE_SNIPPETS`); the snippet
719
+ // MARKDOWN they insert is example document content, not UI chrome, and stays
720
+ // English (see the `i18n-exempt` comments at each snippet).
721
+ "editor.markdownToolbar.label": "Markdown formatting",
722
+ "editor.markdownToolbar.bold": "Bold",
723
+ "editor.markdownToolbar.italic": "Italic",
724
+ "editor.markdownToolbar.inlineCode": "Inline code",
725
+ "editor.markdownToolbar.link": "Link",
726
+ "editor.markdownToolbar.headingLevel": "Heading level",
727
+ "editor.markdownToolbar.heading": "Heading",
728
+ "editor.markdownToolbar.headingLevelItem": "Heading {level}",
729
+ "editor.markdownToolbar.quote": "Quote",
730
+ "editor.markdownToolbar.bulletList": "Bullet list",
731
+ "editor.markdownToolbar.numberedList": "Numbered list",
732
+ "editor.markdownToolbar.divider": "Divider",
733
+ "editor.markdownToolbar.insertBlock": "Insert block",
734
+ "editor.markdownToolbar.insert": "Insert",
735
+ "editor.markdownToolbar.insertBrandBlock": "Insert brand block",
736
+ "editor.markdownToolbar.directiveCard": "Card",
737
+ "editor.markdownToolbar.directiveCallout": "Callout",
738
+ "editor.markdownToolbar.directiveMetric": "Metric",
739
+ "editor.markdownToolbar.directiveTimeline": "Timeline",
740
+ // MarkdownWorkspace's focus-writing toggle.
741
+ "editor.markdownWorkspace.focusWriting": "Focus writing",
742
+ "editor.markdownWorkspace.focus": "Focus",
743
+ "editor.markdownWorkspace.focusWritingHint": "Typewriter scrolling · inactive paragraphs dim",
744
+ // MermaidDiagram (the inline renderer).
745
+ "editor.mermaidDiagram.label": "Diagram",
746
+ "editor.mermaidDiagram.expand": "Expand diagram",
747
+ "editor.mermaidDiagram.downloadSvg": "Download diagram as SVG",
748
+ "editor.mermaidDiagram.copySource": "Copy diagram source",
749
+ "editor.mermaidDiagram.renderFailed": "Diagram failed to render",
750
+ "editor.mermaidDiagram.rendering": "Rendering diagram…",
751
+ // MermaidViewer (the expanded zoom/pan/search surface). `nodeCountHint` mirrors
752
+ // the ORIGINAL (pre-i18n) copy exactly, including its always-plural "nodes" —
753
+ // it is a static hint ("N nodes · type to filter"), not a real plural form, so
754
+ // it stays a plain interpolated string rather than a `PluralMessage`.
755
+ "editor.mermaidViewer.findPlaceholder": "Find in diagram…",
756
+ "editor.mermaidViewer.findLabel": "Find in diagram",
757
+ "editor.mermaidViewer.matchCount": { one: "{count} node", other: "{count} nodes" },
758
+ "editor.mermaidViewer.nodeCountHint": "{count} nodes · type to filter",
759
+ "editor.mermaidViewer.zoomOut": "Zoom out",
760
+ "editor.mermaidViewer.zoomIn": "Zoom in",
761
+ "editor.mermaidViewer.resetZoom": "Reset zoom to 100%",
762
+ "editor.mermaidViewer.fitDiagram": "Fit diagram",
442
763
 
443
764
  // ── @elabs-ai/components-viewer (ADR 0024) ─────────────────────────
444
765
  // FileViewer chrome. Every control here is icon-only, so these ARE the
@@ -77,7 +77,7 @@ function Field({
77
77
  }) {
78
78
  const id = useId();
79
79
  return (
80
- <div className="w-[28rem] space-y-2">
80
+ <div className="w-full max-w-[28rem] space-y-2">
81
81
  <Label htmlFor={id}>{label}</Label>
82
82
  <MentionInput options={options} {...props}>
83
83
  <MentionInputTextarea id={id} placeholder="Type @ to mention someone…" />
@@ -193,7 +193,7 @@ export const SerializedOutput: Story = {
193
193
  const payload = useMemo(() => serializeMentions(value), [value]);
194
194
 
195
195
  return (
196
- <div className="w-[28rem] space-y-4">
196
+ <div className="w-full max-w-[28rem] space-y-4">
197
197
  <div className="space-y-2">
198
198
  <Label htmlFor={id}>Comment</Label>
199
199
  <MentionInput options={ROSTER} value={value} onValueChange={setValue}>
@@ -235,7 +235,7 @@ export const AsyncRoster: Story = {
235
235
  const [options, setOptions] = useState<MentionOption[]>([]);
236
236
 
237
237
  return (
238
- <div className="w-[28rem] space-y-2">
238
+ <div className="w-full max-w-[28rem] space-y-2">
239
239
  <Label htmlFor={id}>Assign reviewers</Label>
240
240
  <MentionInput
241
241
  options={options}
@@ -1,6 +1,6 @@
1
1
  import { createRef, useState } from "react";
2
2
  import { describe, expect, it, vi } from "vitest";
3
- import { render, screen } from "@testing-library/react";
3
+ import { act, render, screen } from "@testing-library/react";
4
4
  import userEvent from "@testing-library/user-event";
5
5
  import {
6
6
  MentionInput,
@@ -615,3 +615,53 @@ describe("onQueryChange", () => {
615
615
  expect(onQueryChange).toHaveBeenLastCalledWith(null);
616
616
  });
617
617
  });
618
+
619
+ // ---------------------------------------------------------------------------
620
+ // Mirror re-measure — scoped MutationObserver (perf review §3.3)
621
+ // ---------------------------------------------------------------------------
622
+
623
+ describe("mirror re-measure observer scope", () => {
624
+ /**
625
+ * Regression lock for the fix: the observer that used to watch `<html>`
626
+ * AND `<body>` for `class`/`style`/`data-theme`/`data-density`/
627
+ * `data-decoration` now watches only the field's closest `[data-theme]`
628
+ * ancestor (`<html>` here, since jsdom's default document carries none),
629
+ * filtered to `data-theme`/`data-density`/`data-decoration` — so a
630
+ * `style` mutation on `<body>` (what Radix's scroll-lock does on every
631
+ * `Sheet`/`Dialog`/`Popover` open, anywhere in the app) no longer causes a
632
+ * re-measure, while a real theme/density/decoration attribute flip still
633
+ * does.
634
+ */
635
+ it("ignores an unrelated body style mutation but reacts to a theme attribute flip", async () => {
636
+ render(<Harness />);
637
+ // Let the mount-time layout effects (which call getComputedStyle
638
+ // themselves) settle before counting.
639
+ await Promise.resolve();
640
+ const spy = vi.spyOn(window, "getComputedStyle");
641
+ spy.mockClear();
642
+
643
+ // Simulate Radix's scroll-lock: an inline style write on <body>, nothing
644
+ // to do with this field's own theme/density/decoration.
645
+ document.body.style.paddingRight = "17px";
646
+ await Promise.resolve();
647
+ await Promise.resolve();
648
+ expect(spy).not.toHaveBeenCalled();
649
+ document.body.style.removeProperty("padding-right");
650
+
651
+ // A real theme attribute flip on the scope the observer actually
652
+ // watches (<html>, the field's closest `[data-theme]` ancestor — there
653
+ // is none here, so it falls back to the document root) DOES trigger a
654
+ // re-measure.
655
+ try {
656
+ await act(async () => {
657
+ document.documentElement.setAttribute("data-theme", "dark");
658
+ await vi.waitFor(() => {
659
+ expect(spy).toHaveBeenCalled();
660
+ });
661
+ });
662
+ } finally {
663
+ document.documentElement.removeAttribute("data-theme");
664
+ spy.mockRestore();
665
+ }
666
+ });
667
+ });
@@ -154,6 +154,45 @@ const MIRRORED_PROPERTIES = [
154
154
  "wordBreak",
155
155
  ] as const satisfies readonly (keyof CSSStyleDeclaration & string)[];
156
156
 
157
+ /**
158
+ * Extra keys `readMirrorStyle` writes onto the mirror style object BESIDES
159
+ * `MIRRORED_PROPERTIES` (some of which — `paddingRight`, `whiteSpace` — it
160
+ * overrides rather than adds). Used by `stylesEqual` below so the "did
161
+ * anything actually change" check compares every key the object can carry,
162
+ * without re-deriving that list from an object literal on every call.
163
+ */
164
+ const EXTRA_MIRROR_KEYS = [
165
+ "position",
166
+ "left",
167
+ "top",
168
+ "width",
169
+ "height",
170
+ "borderStyle",
171
+ "borderColor",
172
+ "overflow",
173
+ "margin",
174
+ ] as const;
175
+
176
+ /**
177
+ * Field-by-field comparison of two mirror style snapshots (perf review §3.3:
178
+ * "two JSON.stringify per callback"). Both objects always carry the exact
179
+ * same key set (`MIRRORED_PROPERTIES` + `EXTRA_MIRROR_KEYS`), so a plain
180
+ * per-key `!==` walk is both cheaper and exact — `JSON.stringify` also
181
+ * silently drops `undefined`-valued keys and is sensitive to key order in a
182
+ * way this object's construction never guarantees.
183
+ */
184
+ function stylesEqual(a: CSSProperties, b: CSSProperties): boolean {
185
+ const record = a as Record<string, unknown>;
186
+ const other = b as Record<string, unknown>;
187
+ for (const key of MIRRORED_PROPERTIES) {
188
+ if (record[key] !== other[key]) return false;
189
+ }
190
+ for (const key of EXTRA_MIRROR_KEYS) {
191
+ if (record[key] !== other[key]) return false;
192
+ }
193
+ return true;
194
+ }
195
+
157
196
  function readMirrorStyle(textarea: HTMLTextAreaElement): CSSProperties {
158
197
  const computed = getComputedStyle(textarea);
159
198
  const style: Record<string, string | number> = {};
@@ -540,9 +579,7 @@ export const MentionInput = forwardRef<HTMLDivElement, MentionInputProps>(functi
540
579
  const element = textareaRef.current;
541
580
  if (!element) return;
542
581
  const next = readMirrorStyle(element);
543
- setMirrorStyle((previous) =>
544
- previous && JSON.stringify(previous) === JSON.stringify(next) ? previous : next,
545
- );
582
+ setMirrorStyle((previous) => (previous && stylesEqual(previous, next) ? previous : next));
546
583
  }, []);
547
584
 
548
585
  useLayoutEffect(() => {
@@ -571,14 +608,27 @@ export const MentionInput = forwardRef<HTMLDivElement, MentionInputProps>(functi
571
608
 
572
609
  // Theme / density / decoration switches re-resolve the INHERITED font and
573
610
  // type scale, and a stale snapshot misaligns every chip on the surface.
574
- const rootObserver = new MutationObserver(remeasureMirror);
575
- for (const node of [document.documentElement, document.body]) {
576
- rootObserver.observe(node, {
611
+ //
612
+ // Scoped to the field's OWN nearest `[data-theme]` ancestor (falling
613
+ // back to `<html>` when the field isn't inside a scoped region), never
614
+ // the whole document — and filtered to only the three attributes a
615
+ // theme/density/decoration switch actually writes, never generic
616
+ // `class`/`style` on `<html>`/`<body>`. The broader, document-wide
617
+ // observer this replaced fired on every Radix `Sheet`/`Dialog`/`Popover`
618
+ // scroll-lock ANYWHERE in the app (it toggles inline `style` —
619
+ // `overflow`, `padding-right` — on `<body>` on every open/close), which
620
+ // has nothing to do with this field's own metrics (perf review §3.3).
621
+ const themeScope =
622
+ element.closest("[data-theme]") ??
623
+ (typeof document !== "undefined" ? document.documentElement : null);
624
+ if (themeScope) {
625
+ const themeObserver = new MutationObserver(remeasureMirror);
626
+ themeObserver.observe(themeScope, {
577
627
  attributes: true,
578
- attributeFilter: ["data-theme", "data-density", "data-decoration", "class", "style"],
628
+ attributeFilter: ["data-theme", "data-density", "data-decoration"],
579
629
  });
630
+ teardown.push(() => themeObserver.disconnect());
580
631
  }
581
- teardown.push(() => rootObserver.disconnect());
582
632
  }
583
633
 
584
634
  // A web font landing after first paint changes every advance width. This