@miragon/mcp-toolkit-ui 2.6.0 → 2.8.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.
- package/README.md +14 -5
- package/dist/app/adapt-data-widget.d.ts +1 -22
- package/dist/app/adapt-data-widget.d.ts.map +1 -1
- package/dist/app/adapt-data-widget.js +1 -35
- package/dist/app/adapt-data-widget.js.map +1 -1
- package/dist/app/builder/BuilderBanners.d.ts +7 -2
- package/dist/app/builder/BuilderBanners.d.ts.map +1 -1
- package/dist/app/builder/BuilderBanners.js +8 -3
- package/dist/app/builder/BuilderBanners.js.map +1 -1
- package/dist/app/builder/CanvasRow.d.ts +4 -2
- package/dist/app/builder/CanvasRow.d.ts.map +1 -1
- package/dist/app/builder/CanvasRow.js +29 -17
- package/dist/app/builder/CanvasRow.js.map +1 -1
- package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
- package/dist/app/builder/CatalogueSheet.js +14 -6
- package/dist/app/builder/CatalogueSheet.js.map +1 -1
- package/dist/app/builder/CellPropsSheet.d.ts +7 -3
- package/dist/app/builder/CellPropsSheet.d.ts.map +1 -1
- package/dist/app/builder/CellPropsSheet.js +10 -3
- package/dist/app/builder/CellPropsSheet.js.map +1 -1
- package/dist/app/builder/DiscardDialog.d.ts +18 -0
- package/dist/app/builder/DiscardDialog.d.ts.map +1 -0
- package/dist/app/builder/DiscardDialog.js +18 -0
- package/dist/app/builder/DiscardDialog.js.map +1 -0
- package/dist/app/builder/EmptyHint.d.ts +4 -0
- package/dist/app/builder/EmptyHint.d.ts.map +1 -1
- package/dist/app/builder/EmptyHint.js +6 -1
- package/dist/app/builder/EmptyHint.js.map +1 -1
- package/dist/app/builder/LayoutSurface.d.ts +33 -9
- package/dist/app/builder/LayoutSurface.d.ts.map +1 -1
- package/dist/app/builder/LayoutSurface.js +74 -23
- package/dist/app/builder/LayoutSurface.js.map +1 -1
- package/dist/app/builder/PipelineStrip.d.ts +5 -2
- package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
- package/dist/app/builder/PipelineStrip.js +33 -15
- package/dist/app/builder/PipelineStrip.js.map +1 -1
- package/dist/app/builder/SaveDialog.d.ts +9 -1
- package/dist/app/builder/SaveDialog.d.ts.map +1 -1
- package/dist/app/builder/SaveDialog.js +9 -3
- package/dist/app/builder/SaveDialog.js.map +1 -1
- package/dist/app/builder/Toolbar.d.ts +14 -4
- package/dist/app/builder/Toolbar.d.ts.map +1 -1
- package/dist/app/builder/Toolbar.js +24 -7
- package/dist/app/builder/Toolbar.js.map +1 -1
- package/dist/app/builder/Workspace.d.ts +23 -3
- package/dist/app/builder/Workspace.d.ts.map +1 -1
- package/dist/app/builder/Workspace.js +58 -8
- package/dist/app/builder/Workspace.js.map +1 -1
- package/dist/app/builder/builder-model.d.ts +146 -3
- package/dist/app/builder/builder-model.d.ts.map +1 -1
- package/dist/app/builder/builder-model.js +196 -5
- package/dist/app/builder/builder-model.js.map +1 -1
- package/dist/app/builder/labels.d.ts +13 -62
- package/dist/app/builder/labels.d.ts.map +1 -1
- package/dist/app/builder/labels.js +9 -58
- package/dist/app/builder/labels.js.map +1 -1
- package/dist/app/builder/reveal.d.ts +10 -0
- package/dist/app/builder/reveal.d.ts.map +1 -0
- package/dist/app/builder/reveal.js +10 -0
- package/dist/app/builder/reveal.js.map +1 -0
- package/dist/app/builder/status-chip.d.ts.map +1 -1
- package/dist/app/builder/status-chip.js +3 -2
- package/dist/app/builder/status-chip.js.map +1 -1
- package/dist/app/builder/use-builder-catalogue.d.ts +5 -1
- package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
- package/dist/app/builder/use-builder-catalogue.js +50 -17
- package/dist/app/builder/use-builder-catalogue.js.map +1 -1
- package/dist/app/builder/use-builder-session.d.ts +30 -0
- package/dist/app/builder/use-builder-session.d.ts.map +1 -0
- package/dist/app/builder/use-builder-session.js +58 -0
- package/dist/app/builder/use-builder-session.js.map +1 -0
- package/dist/app/builder/use-display-resolver.d.ts +17 -0
- package/dist/app/builder/use-display-resolver.d.ts.map +1 -0
- package/dist/app/builder/use-display-resolver.js +16 -0
- package/dist/app/builder/use-display-resolver.js.map +1 -0
- package/dist/app/builder/use-save-dashboard.d.ts +3 -1
- package/dist/app/builder/use-save-dashboard.d.ts.map +1 -1
- package/dist/app/builder/use-save-dashboard.js +3 -1
- package/dist/app/builder/use-save-dashboard.js.map +1 -1
- package/dist/app/index.d.ts +8 -3
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +8 -2
- package/dist/app/index.js.map +1 -1
- package/dist/app/layout-builder.d.ts +18 -25
- package/dist/app/layout-builder.d.ts.map +1 -1
- package/dist/app/layout-builder.js +84 -78
- package/dist/app/layout-builder.js.map +1 -1
- package/dist/app/mcp-app-view-chrome.d.ts +103 -20
- package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
- package/dist/app/mcp-app-view-chrome.js +125 -40
- package/dist/app/mcp-app-view-chrome.js.map +1 -1
- package/dist/app/mcp-app-view-states.d.ts +23 -0
- package/dist/app/mcp-app-view-states.d.ts.map +1 -0
- package/dist/app/mcp-app-view-states.js +55 -0
- package/dist/app/mcp-app-view-states.js.map +1 -0
- package/dist/app/mcp-app-view.d.ts +72 -5
- package/dist/app/mcp-app-view.d.ts.map +1 -1
- package/dist/app/mcp-app-view.js +241 -141
- package/dist/app/mcp-app-view.js.map +1 -1
- package/dist/app/mcp-toolkit-app.d.ts +8 -2
- package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
- package/dist/app/mcp-toolkit-app.js +33 -4
- package/dist/app/mcp-toolkit-app.js.map +1 -1
- package/dist/app/mcp-use-host-bridge.d.ts +15 -6
- package/dist/app/mcp-use-host-bridge.d.ts.map +1 -1
- package/dist/app/mcp-use-host-bridge.js +51 -22
- package/dist/app/mcp-use-host-bridge.js.map +1 -1
- package/dist/app/model-context.d.ts +71 -0
- package/dist/app/model-context.d.ts.map +1 -0
- package/dist/app/model-context.js +101 -0
- package/dist/app/model-context.js.map +1 -0
- package/dist/app/use-build-mode-focus.d.ts +33 -0
- package/dist/app/use-build-mode-focus.d.ts.map +1 -0
- package/dist/app/use-build-mode-focus.js +59 -0
- package/dist/app/use-build-mode-focus.js.map +1 -0
- package/dist/app/use-view-actions.d.ts +31 -0
- package/dist/app/use-view-actions.d.ts.map +1 -0
- package/dist/app/use-view-actions.js +81 -0
- package/dist/app/use-view-actions.js.map +1 -0
- package/dist/app/use-view-lifecycle.d.ts +42 -0
- package/dist/app/use-view-lifecycle.d.ts.map +1 -0
- package/dist/app/use-view-lifecycle.js +81 -0
- package/dist/app/use-view-lifecycle.js.map +1 -0
- package/dist/app/view-lifecycle.d.ts +185 -0
- package/dist/app/view-lifecycle.d.ts.map +1 -0
- package/dist/app/view-lifecycle.js +207 -0
- package/dist/app/view-lifecycle.js.map +1 -0
- package/dist/app/widget-fixture.d.ts +10 -3
- package/dist/app/widget-fixture.d.ts.map +1 -1
- package/dist/app/widget-fixture.js +21 -8
- package/dist/app/widget-fixture.js.map +1 -1
- package/dist/app/widget-props-sheet.d.ts +6 -10
- package/dist/app/widget-props-sheet.d.ts.map +1 -1
- package/dist/app/widget-props-sheet.js +25 -20
- package/dist/app/widget-props-sheet.js.map +1 -1
- package/dist/app/widget-renderer.d.ts +41 -1
- package/dist/app/widget-renderer.d.ts.map +1 -1
- package/dist/app/widget-renderer.js +82 -31
- package/dist/app/widget-renderer.js.map +1 -1
- package/dist/components/DrillButton.d.ts +9 -6
- package/dist/components/DrillButton.d.ts.map +1 -1
- package/dist/components/DrillButton.js +9 -6
- package/dist/components/DrillButton.js.map +1 -1
- package/dist/components/EmptyState.d.ts +33 -0
- package/dist/components/EmptyState.d.ts.map +1 -0
- package/dist/components/EmptyState.js +19 -0
- package/dist/components/EmptyState.js.map +1 -0
- package/dist/components/ErrorState.d.ts +53 -0
- package/dist/components/ErrorState.d.ts.map +1 -0
- package/dist/components/ErrorState.js +77 -0
- package/dist/components/ErrorState.js.map +1 -0
- package/dist/components/FilterBar.d.ts +16 -5
- package/dist/components/FilterBar.d.ts.map +1 -1
- package/dist/components/FilterBar.js +13 -5
- package/dist/components/FilterBar.js.map +1 -1
- package/dist/components/GroupCard.d.ts.map +1 -1
- package/dist/components/GroupCard.js +2 -1
- package/dist/components/GroupCard.js.map +1 -1
- package/dist/components/KpiGrid.d.ts +70 -18
- package/dist/components/KpiGrid.d.ts.map +1 -1
- package/dist/components/KpiGrid.js +112 -23
- package/dist/components/KpiGrid.js.map +1 -1
- package/dist/components/ListFooter.d.ts +32 -4
- package/dist/components/ListFooter.d.ts.map +1 -1
- package/dist/components/ListFooter.js +35 -6
- package/dist/components/ListFooter.js.map +1 -1
- package/dist/components/PagedListFooter.d.ts +57 -0
- package/dist/components/PagedListFooter.d.ts.map +1 -0
- package/dist/components/PagedListFooter.js +27 -0
- package/dist/components/PagedListFooter.js.map +1 -0
- package/dist/components/Pills.d.ts +36 -12
- package/dist/components/Pills.d.ts.map +1 -1
- package/dist/components/Pills.js +29 -8
- package/dist/components/Pills.js.map +1 -1
- package/dist/components/SectionHeading.d.ts +3 -1
- package/dist/components/SectionHeading.d.ts.map +1 -1
- package/dist/components/SectionHeading.js +6 -2
- package/dist/components/SectionHeading.js.map +1 -1
- package/dist/components/Toast.d.ts +43 -0
- package/dist/components/Toast.d.ts.map +1 -0
- package/dist/components/Toast.js +106 -0
- package/dist/components/Toast.js.map +1 -0
- package/dist/components/WidgetHeader.d.ts +34 -10
- package/dist/components/WidgetHeader.d.ts.map +1 -1
- package/dist/components/WidgetHeader.js +11 -6
- package/dist/components/WidgetHeader.js.map +1 -1
- package/dist/hooks/index.d.ts +4 -4
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/tool-result-recovery-model.d.ts +155 -0
- package/dist/hooks/tool-result-recovery-model.d.ts.map +1 -0
- package/dist/hooks/tool-result-recovery-model.js +273 -0
- package/dist/hooks/tool-result-recovery-model.js.map +1 -0
- package/dist/hooks/use-tool-query.d.ts +56 -14
- package/dist/hooks/use-tool-query.d.ts.map +1 -1
- package/dist/hooks/use-tool-query.js +102 -25
- package/dist/hooks/use-tool-query.js.map +1 -1
- package/dist/hooks/use-tool-result-recovery.d.ts +61 -44
- package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
- package/dist/hooks/use-tool-result-recovery.js +62 -110
- package/dist/hooks/use-tool-result-recovery.js.map +1 -1
- package/dist/hooks/use-view-data.d.ts +55 -7
- package/dist/hooks/use-view-data.d.ts.map +1 -1
- package/dist/hooks/use-view-data.js +81 -11
- package/dist/hooks/use-view-data.js.map +1 -1
- package/dist/hooks/use-view-tool-query.d.ts +12 -7
- package/dist/hooks/use-view-tool-query.d.ts.map +1 -1
- package/dist/hooks/use-view-tool-query.js +15 -14
- package/dist/hooks/use-view-tool-query.js.map +1 -1
- package/dist/index.d.ts +27 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -7
- package/dist/index.js.map +1 -1
- package/dist/lib/focus-return.d.ts +61 -0
- package/dist/lib/focus-return.d.ts.map +1 -0
- package/dist/lib/focus-return.js +46 -0
- package/dist/lib/focus-return.js.map +1 -0
- package/dist/lib/focus-ring.d.ts +18 -0
- package/dist/lib/focus-ring.d.ts.map +1 -0
- package/dist/lib/focus-ring.js +18 -0
- package/dist/lib/focus-ring.js.map +1 -0
- package/dist/lib/icon-stroke.d.ts +12 -0
- package/dist/lib/icon-stroke.d.ts.map +1 -0
- package/dist/lib/icon-stroke.js +12 -0
- package/dist/lib/icon-stroke.js.map +1 -0
- package/dist/lib/parse-tool-result.d.ts +23 -1
- package/dist/lib/parse-tool-result.d.ts.map +1 -1
- package/dist/lib/parse-tool-result.js +38 -2
- package/dist/lib/parse-tool-result.js.map +1 -1
- package/dist/lib/toast-model.d.ts +86 -0
- package/dist/lib/toast-model.d.ts.map +1 -0
- package/dist/lib/toast-model.js +102 -0
- package/dist/lib/toast-model.js.map +1 -0
- package/dist/lib/tone-utils.d.ts +95 -9
- package/dist/lib/tone-utils.d.ts.map +1 -1
- package/dist/lib/tone-utils.js +100 -9
- package/dist/lib/tone-utils.js.map +1 -1
- package/dist/lib/tool-query.d.ts +107 -0
- package/dist/lib/tool-query.d.ts.map +1 -0
- package/dist/lib/tool-query.js +182 -0
- package/dist/lib/tool-query.js.map +1 -0
- package/dist/lib/tool-result-observer.d.ts +108 -0
- package/dist/lib/tool-result-observer.d.ts.map +1 -0
- package/dist/lib/tool-result-observer.js +158 -0
- package/dist/lib/tool-result-observer.js.map +1 -0
- package/dist/lib/typography.d.ts +37 -0
- package/dist/lib/typography.d.ts.map +1 -0
- package/dist/lib/typography.js +41 -0
- package/dist/lib/typography.js.map +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +11 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/locale/index.d.ts +3 -1
- package/dist/locale/index.d.ts.map +1 -1
- package/dist/locale/index.js +2 -1
- package/dist/locale/index.js.map +1 -1
- package/dist/locale/locale-provider.d.ts +42 -1
- package/dist/locale/locale-provider.d.ts.map +1 -1
- package/dist/locale/locale-provider.js +44 -3
- package/dist/locale/locale-provider.js.map +1 -1
- package/dist/locale/message-types.d.ts +336 -0
- package/dist/locale/message-types.d.ts.map +1 -0
- package/dist/locale/message-types.js +7 -0
- package/dist/locale/message-types.js.map +1 -0
- package/dist/locale/messages-de.d.ts +9 -0
- package/dist/locale/messages-de.d.ts.map +1 -0
- package/dist/locale/messages-de.js +185 -0
- package/dist/locale/messages-de.js.map +1 -0
- package/dist/locale/messages-en.d.ts +8 -0
- package/dist/locale/messages-en.d.ts.map +1 -0
- package/dist/locale/messages-en.js +184 -0
- package/dist/locale/messages-en.js.map +1 -0
- package/dist/locale/toolkit-messages.d.ts +57 -0
- package/dist/locale/toolkit-messages.d.ts.map +1 -0
- package/dist/locale/toolkit-messages.js +103 -0
- package/dist/locale/toolkit-messages.js.map +1 -0
- package/dist/primitives/alert.d.ts +1 -1
- package/dist/primitives/alert.d.ts.map +1 -1
- package/dist/primitives/alert.js +6 -2
- package/dist/primitives/alert.js.map +1 -1
- package/dist/primitives/badge.d.ts +1 -1
- package/dist/primitives/badge.d.ts.map +1 -1
- package/dist/primitives/badge.js +19 -6
- package/dist/primitives/badge.js.map +1 -1
- package/dist/primitives/button.d.ts +1 -1
- package/dist/primitives/button.d.ts.map +1 -1
- package/dist/primitives/button.js +12 -8
- package/dist/primitives/button.js.map +1 -1
- package/dist/primitives/card.js +4 -4
- package/dist/primitives/card.js.map +1 -1
- package/dist/primitives/dialog.d.ts +7 -3
- package/dist/primitives/dialog.d.ts.map +1 -1
- package/dist/primitives/dialog.js +22 -8
- package/dist/primitives/dialog.js.map +1 -1
- package/dist/primitives/input.d.ts.map +1 -1
- package/dist/primitives/input.js +4 -1
- package/dist/primitives/input.js.map +1 -1
- package/dist/primitives/scroll-area.js +1 -1
- package/dist/primitives/scroll-area.js.map +1 -1
- package/dist/primitives/select.d.ts.map +1 -1
- package/dist/primitives/select.js +6 -3
- package/dist/primitives/select.js.map +1 -1
- package/dist/primitives/sheet.d.ts +3 -1
- package/dist/primitives/sheet.d.ts.map +1 -1
- package/dist/primitives/sheet.js +17 -7
- package/dist/primitives/sheet.js.map +1 -1
- package/dist/primitives/skeleton.js +1 -1
- package/dist/primitives/skeleton.js.map +1 -1
- package/dist/primitives/switch.d.ts.map +1 -1
- package/dist/primitives/switch.js +2 -1
- package/dist/primitives/switch.js.map +1 -1
- package/dist/primitives/tabs.d.ts.map +1 -1
- package/dist/primitives/tabs.js +5 -3
- package/dist/primitives/tabs.js.map +1 -1
- package/dist/providers/query-provider.d.ts +32 -1
- package/dist/providers/query-provider.d.ts.map +1 -1
- package/dist/providers/query-provider.js +53 -4
- package/dist/providers/query-provider.js.map +1 -1
- package/dist/render-cases-primitives.js +1 -1
- package/dist/render-cases-primitives.js.map +1 -1
- package/dist/render-cases.d.ts.map +1 -1
- package/dist/render-cases.js +48 -6
- package/dist/render-cases.js.map +1 -1
- package/dist/render-effects.d.ts +42 -0
- package/dist/render-effects.d.ts.map +1 -0
- package/dist/render-effects.js +81 -0
- package/dist/render-effects.js.map +1 -0
- package/dist/render-scan.d.ts +25 -0
- package/dist/render-scan.d.ts.map +1 -0
- package/dist/render-scan.js +66 -0
- package/dist/render-scan.js.map +1 -0
- package/dist/theme/cd-tokens.d.ts +66 -0
- package/dist/theme/cd-tokens.d.ts.map +1 -0
- package/dist/theme/cd-tokens.js +64 -0
- package/dist/theme/cd-tokens.js.map +1 -0
- package/dist/theme/create-theme.d.ts +214 -39
- package/dist/theme/create-theme.d.ts.map +1 -1
- package/dist/theme/create-theme.js +204 -13
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/theme/fonts.d.ts +38 -0
- package/dist/theme/fonts.d.ts.map +1 -0
- package/dist/theme/fonts.js +56 -0
- package/dist/theme/fonts.js.map +1 -0
- package/dist/theme/host-fonts.d.ts +39 -0
- package/dist/theme/host-fonts.d.ts.map +1 -0
- package/dist/theme/host-fonts.js +44 -0
- package/dist/theme/host-fonts.js.map +1 -0
- package/dist/theme/index.d.ts +8 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +5 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/portal-container.d.ts +16 -0
- package/dist/theme/portal-container.d.ts.map +1 -0
- package/dist/theme/portal-container.js +19 -0
- package/dist/theme/portal-container.js.map +1 -0
- package/dist/theme/presets.d.ts +24 -9
- package/dist/theme/presets.d.ts.map +1 -1
- package/dist/theme/presets.js +44 -11
- package/dist/theme/presets.js.map +1 -1
- package/dist/theme/theme-provider.d.ts +27 -9
- package/dist/theme/theme-provider.d.ts.map +1 -1
- package/dist/theme/theme-provider.js +42 -20
- package/dist/theme/theme-provider.js.map +1 -1
- package/package.json +11 -5
- package/src/app/adapt-data-widget.tsx +2 -42
- package/src/app/builder/BuilderBanners.tsx +26 -13
- package/src/app/builder/CanvasRow.tsx +79 -32
- package/src/app/builder/CatalogueSheet.tsx +58 -35
- package/src/app/builder/CellPropsSheet.tsx +17 -2
- package/src/app/builder/DiscardDialog.tsx +59 -0
- package/src/app/builder/EmptyHint.tsx +16 -11
- package/src/app/builder/LayoutSurface.tsx +207 -67
- package/src/app/builder/PipelineStrip.tsx +265 -156
- package/src/app/builder/SaveDialog.tsx +25 -14
- package/src/app/builder/Toolbar.tsx +154 -57
- package/src/app/builder/Workspace.tsx +226 -56
- package/src/app/builder/builder-feedback.test.tsx +75 -0
- package/src/app/builder/builder-model.test.ts +5 -0
- package/src/app/builder/builder-model.ts +346 -6
- package/src/app/builder/builder-model.ux.test.ts +456 -0
- package/src/app/builder/builder-ux.test.tsx +844 -0
- package/src/app/builder/labels.ts +15 -121
- package/src/app/builder/reveal.ts +10 -0
- package/src/app/builder/status-chip.tsx +8 -4
- package/src/app/builder/use-builder-catalogue.ts +62 -19
- package/src/app/builder/use-builder-session.ts +68 -0
- package/src/app/builder/use-display-resolver.ts +24 -0
- package/src/app/builder/use-save-dashboard.ts +5 -0
- package/src/app/index.ts +20 -3
- package/src/app/layout-builder.tsx +193 -186
- package/src/app/mcp-app-view-chrome.test.tsx +487 -14
- package/src/app/mcp-app-view-chrome.tsx +328 -100
- package/src/app/mcp-app-view-states.tsx +140 -0
- package/src/app/mcp-app-view.effects.test.tsx +292 -0
- package/src/app/mcp-app-view.test.tsx +478 -0
- package/src/app/mcp-app-view.tsx +447 -190
- package/src/app/mcp-toolkit-app.test.tsx +40 -0
- package/src/app/mcp-toolkit-app.tsx +39 -8
- package/src/app/mcp-use-host-bridge.test.tsx +180 -0
- package/src/app/mcp-use-host-bridge.tsx +67 -24
- package/src/app/model-context.test.tsx +116 -0
- package/src/app/model-context.tsx +125 -0
- package/src/app/use-build-mode-focus.ts +62 -0
- package/src/app/use-view-actions.ts +89 -0
- package/src/app/use-view-lifecycle.ts +122 -0
- package/src/app/view-lifecycle.test.ts +443 -0
- package/src/app/view-lifecycle.ts +334 -0
- package/src/app/widget-fixture.effects.test.tsx +51 -0
- package/src/app/widget-fixture.render.test.tsx +81 -0
- package/src/app/widget-fixture.tsx +34 -11
- package/src/app/widget-props-sheet.tsx +67 -39
- package/src/app/widget-renderer.render.test.tsx +131 -0
- package/src/app/widget-renderer.tsx +136 -38
- package/src/components/DrillButton.tsx +24 -10
- package/src/components/EmptyState.tsx +83 -0
- package/src/components/ErrorState.tsx +204 -0
- package/src/components/FilterBar.test.tsx +82 -0
- package/src/components/FilterBar.tsx +56 -27
- package/src/components/GroupCard.tsx +3 -2
- package/src/components/KpiGrid.test.tsx +279 -0
- package/src/components/KpiGrid.tsx +329 -91
- package/src/components/ListFooter.test.tsx +116 -0
- package/src/components/ListFooter.tsx +110 -12
- package/src/components/PagedListFooter.test.tsx +140 -0
- package/src/components/PagedListFooter.tsx +107 -0
- package/src/components/Pills.test.tsx +77 -0
- package/src/components/Pills.tsx +78 -16
- package/src/components/SectionHeading.tsx +9 -3
- package/src/components/Toast.tsx +206 -0
- package/src/components/WidgetHeader.test.tsx +78 -0
- package/src/components/WidgetHeader.tsx +63 -16
- package/src/components/feedback-states.test.tsx +314 -0
- package/src/components/tone-figures.test.tsx +58 -0
- package/src/fonts.css +19 -0
- package/src/globals.css +9 -165
- package/src/hooks/index.ts +6 -3
- package/src/hooks/tool-result-recovery-model.test.ts +744 -0
- package/src/hooks/tool-result-recovery-model.ts +418 -0
- package/src/hooks/use-tool-query.effects.test.tsx +205 -0
- package/src/hooks/use-tool-query.ts +156 -33
- package/src/hooks/use-tool-result-recovery.effects.test.tsx +141 -0
- package/src/hooks/use-tool-result-recovery.test.ts +28 -5
- package/src/hooks/use-tool-result-recovery.ts +101 -131
- package/src/hooks/use-view-data.test.tsx +169 -0
- package/src/hooks/use-view-data.ts +131 -10
- package/src/hooks/use-view-tool-query.ts +25 -19
- package/src/i18n-coverage.test.tsx +425 -0
- package/src/index.ts +102 -13
- package/src/lib/focus-return.test.ts +288 -0
- package/src/lib/focus-return.ts +96 -0
- package/src/lib/focus-ring.ts +20 -0
- package/src/lib/icon-stroke.test.ts +59 -0
- package/src/lib/icon-stroke.ts +13 -0
- package/src/lib/parse-tool-result.test.ts +46 -1
- package/src/lib/parse-tool-result.ts +40 -2
- package/src/lib/toast-model.test.ts +233 -0
- package/src/lib/toast-model.ts +165 -0
- package/src/lib/tone-utils.test.ts +125 -16
- package/src/lib/tone-utils.ts +144 -15
- package/src/lib/tool-query.test.ts +569 -0
- package/src/lib/tool-query.ts +253 -0
- package/src/lib/tool-result-observer.test.ts +296 -0
- package/src/lib/tool-result-observer.ts +211 -0
- package/src/lib/typography.test.ts +45 -0
- package/src/lib/typography.ts +47 -0
- package/src/lib/utils.ts +12 -1
- package/src/locale/index.ts +14 -1
- package/src/locale/locale-provider.test.tsx +87 -0
- package/src/locale/locale-provider.tsx +90 -3
- package/src/locale/message-types.ts +337 -0
- package/src/locale/messages-de.ts +198 -0
- package/src/locale/messages-en.ts +196 -0
- package/src/locale/toolkit-messages.test.ts +349 -0
- package/src/locale/toolkit-messages.ts +143 -0
- package/src/no-hardcoded-copy.test.ts +432 -0
- package/src/primitives/alert.tsx +7 -2
- package/src/primitives/badge.tsx +19 -7
- package/src/primitives/button.tsx +13 -8
- package/src/primitives/card.tsx +4 -4
- package/src/primitives/dialog.tsx +42 -8
- package/src/primitives/input.tsx +5 -3
- package/src/primitives/scroll-area.tsx +1 -1
- package/src/primitives/select.tsx +7 -4
- package/src/primitives/sheet.tsx +33 -6
- package/src/primitives/skeleton.tsx +1 -1
- package/src/primitives/switch.tsx +4 -2
- package/src/primitives/tabs.tsx +9 -6
- package/src/providers/query-provider.test.tsx +154 -0
- package/src/providers/query-provider.tsx +66 -5
- package/src/render-cases-primitives.tsx +1 -1
- package/src/render-cases.tsx +82 -6
- package/src/render-effects.tsx +96 -0
- package/src/render-scan.ts +75 -0
- package/src/theme/cd-tokens.generated.css +48 -0
- package/src/theme/cd-tokens.test.ts +109 -0
- package/src/theme/cd-tokens.ts +67 -0
- package/src/theme/create-theme.test.ts +253 -5
- package/src/theme/create-theme.ts +341 -41
- package/src/theme/fonts.test.ts +85 -0
- package/src/theme/fonts.ts +72 -0
- package/src/theme/host-fonts.test.ts +135 -0
- package/src/theme/host-fonts.ts +52 -0
- package/src/theme/index.ts +15 -1
- package/src/theme/portal-container.ts +20 -0
- package/src/theme/presets.ts +44 -11
- package/src/theme/theme-css.test.ts +619 -0
- package/src/theme/theme-provider.test.tsx +82 -0
- package/src/theme/theme-provider.tsx +60 -24
- package/src/theme.css +344 -0
- package/ui-catalog.json +783 -49
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { ToolkitMessages } from "./message-types.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* English catalog — the fallback for every locale the toolkit does not ship.
|
|
5
|
+
* Sentence case, active voice, one "…" character for ellipses, no dashes as
|
|
6
|
+
* connectors; guarded by `toolkit-messages.test.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export const en: ToolkitMessages = {
|
|
9
|
+
common: {
|
|
10
|
+
close: "Close",
|
|
11
|
+
retry: "Try again",
|
|
12
|
+
details: "Details",
|
|
13
|
+
dismiss: "Close notification",
|
|
14
|
+
notifications: "Notifications",
|
|
15
|
+
loadFailed: "This content couldn't be loaded",
|
|
16
|
+
loadFailedHint: "Try again. If it fails again, ask in the chat.",
|
|
17
|
+
},
|
|
18
|
+
view: {
|
|
19
|
+
loading: "Loading view…",
|
|
20
|
+
cancelled: "Cancelled. Ask in the chat to open this view again.",
|
|
21
|
+
refresh: "Refresh",
|
|
22
|
+
refreshing: "Refreshing…",
|
|
23
|
+
enterFullscreen: "Fullscreen",
|
|
24
|
+
exitFullscreen: "Exit fullscreen",
|
|
25
|
+
build: "Edit view",
|
|
26
|
+
slowHint: "This is taking longer than usual. If nothing happens, ask in the chat again.",
|
|
27
|
+
errorTitle: "This view couldn't be loaded",
|
|
28
|
+
errorHint: "Try again. If it fails again, ask in the chat.",
|
|
29
|
+
errorHintNoRetry: "Ask in the chat to open this view again.",
|
|
30
|
+
retry: "Try again",
|
|
31
|
+
blockedTitle: "The chat didn't pass the result to this view",
|
|
32
|
+
blockedHint: "Load the view yourself, or ask in the chat for the current state.",
|
|
33
|
+
blockedHintNoRetry:
|
|
34
|
+
"The action already ran, so the view doesn't start it again. Ask in the chat for the current state.",
|
|
35
|
+
blockedRetry: "Load view",
|
|
36
|
+
errorDetails: "Details",
|
|
37
|
+
stepErrorsTitle: "Some data couldn't be loaded",
|
|
38
|
+
stepErrorsHint: "Try again to load the missing data.",
|
|
39
|
+
refreshFailed: "Refresh didn't work.",
|
|
40
|
+
refreshFailedHint: "You're seeing the last loaded data. Try again in a moment.",
|
|
41
|
+
fullscreenFailed: "Couldn't switch the display mode.",
|
|
42
|
+
fullscreenFailedHint: "The view stays as it is. Try again in a moment.",
|
|
43
|
+
dataAsOf: (time) => `Reloaded at ${time}`,
|
|
44
|
+
},
|
|
45
|
+
listFooter: {
|
|
46
|
+
showing: ({ shown, total, noun }) => `Showing ${shown} of ${total} ${noun}`.trimEnd(),
|
|
47
|
+
defaultNoun: "rows",
|
|
48
|
+
loadMore: "Load more",
|
|
49
|
+
loadingMore: "Loading…",
|
|
50
|
+
updating: "Updating…",
|
|
51
|
+
noResults: "No entries",
|
|
52
|
+
refreshFailed: "Couldn't update the list",
|
|
53
|
+
refreshFailedHint: "You're seeing the previous result. Try again in a moment.",
|
|
54
|
+
loadMoreFailed: "Couldn't load more entries",
|
|
55
|
+
loadMoreFailedHint: "The entries above are current. Try again in a moment.",
|
|
56
|
+
},
|
|
57
|
+
filterBar: { searchPlaceholder: "Filter…", searchLabel: "Filter", chipsLabel: "Filters" },
|
|
58
|
+
pills: { live: "Live" },
|
|
59
|
+
renderer: {
|
|
60
|
+
widgetUnavailable: "This element isn't available in this version.",
|
|
61
|
+
widgetCrashed: "This element couldn't be displayed.",
|
|
62
|
+
noDataYet: "No data yet. It appears after the next refresh.",
|
|
63
|
+
},
|
|
64
|
+
builder: {
|
|
65
|
+
title: "Edit view",
|
|
66
|
+
builderBadge: "Editing",
|
|
67
|
+
previewBadge: "Preview",
|
|
68
|
+
catalogue: "Catalogue",
|
|
69
|
+
save: "Save as dashboard",
|
|
70
|
+
done: "Done",
|
|
71
|
+
apply: "Apply",
|
|
72
|
+
applyHint: "Shows your changes in this view and saves nothing.",
|
|
73
|
+
discard: "Discard",
|
|
74
|
+
discardHint: "Leaves the editor. The view stays as it was.",
|
|
75
|
+
discardHintStandalone: "Resets your changes.",
|
|
76
|
+
saveHint: "Saves the layout as a dashboard you can open again later.",
|
|
77
|
+
unsavedChanges: "Unsaved changes",
|
|
78
|
+
discardDialogTitle: "Discard your changes?",
|
|
79
|
+
discardDialogDescription:
|
|
80
|
+
"You lose your changes to this view. The view stays as it was before you edited it.",
|
|
81
|
+
discardDialogDescriptionSaved:
|
|
82
|
+
"The view goes back to how it was before you edited it. Your saved dashboard keeps your changes.",
|
|
83
|
+
keepEditing: "Keep editing",
|
|
84
|
+
discardConfirm: "Discard changes",
|
|
85
|
+
advanced: "Advanced",
|
|
86
|
+
advancedHint: "Shows the data sources, the catalogue and the technical IDs.",
|
|
87
|
+
renameTabNamed: (label) => `Rename tab “${label}”`,
|
|
88
|
+
removeTabNamed: (label) => `Remove tab “${label}”`,
|
|
89
|
+
rowTarget: "New elements go here",
|
|
90
|
+
stepProvides: "provides",
|
|
91
|
+
keyValueFor: (key) => (key ? `Value of ${key}` : "Value"),
|
|
92
|
+
viewLayoutTab: "Layout",
|
|
93
|
+
viewPipelineTab: "Data sources",
|
|
94
|
+
viewPreviewTab: "Preview",
|
|
95
|
+
saveDialogTitle: "Save as dashboard",
|
|
96
|
+
saveDialogDescription:
|
|
97
|
+
"Saves the layout and its data sources as a dashboard you can open again later. To change this view, use Apply.",
|
|
98
|
+
saveDialogDescriptionStandalone:
|
|
99
|
+
"Saves the layout and its data sources as a dashboard you can open again later.",
|
|
100
|
+
saveNameLabel: "Name",
|
|
101
|
+
saveDescriptionLabel: "Description",
|
|
102
|
+
saveConfirm: "Save dashboard",
|
|
103
|
+
cancel: "Cancel",
|
|
104
|
+
pipelineHeader: "Data sources",
|
|
105
|
+
keysHeader: "Keys",
|
|
106
|
+
stepsHeader: "Steps",
|
|
107
|
+
keyName: "Key",
|
|
108
|
+
keyValue: 'Value, "text", true, {…}',
|
|
109
|
+
stepContextId: "Step ID",
|
|
110
|
+
stepPickerLabel: "Pick a step",
|
|
111
|
+
addKey: "Add key",
|
|
112
|
+
addStep: "Add step",
|
|
113
|
+
emptyKeys: "No keys yet.",
|
|
114
|
+
emptySteps: "No steps yet.",
|
|
115
|
+
statusUpToDate: "Up to date",
|
|
116
|
+
statusPending: "Updating soon…",
|
|
117
|
+
statusRefreshing: "Updating…",
|
|
118
|
+
paletteHeader: "Elements",
|
|
119
|
+
paletteLoading: "Loading elements…",
|
|
120
|
+
paletteEmpty: "No elements available yet. Elements appear as soon as their data is there.",
|
|
121
|
+
canvasEmpty: "Drag an element here or click one in the list.",
|
|
122
|
+
canvasDropHint: "Drop to add",
|
|
123
|
+
addRow: "Add row",
|
|
124
|
+
addTab: "Add tab",
|
|
125
|
+
removeRow: "Remove row",
|
|
126
|
+
renameTab: "Rename tab",
|
|
127
|
+
removeTab: "Remove tab",
|
|
128
|
+
tabsLabel: "Tabs",
|
|
129
|
+
catalogueDescription: "Every key and element the server knows, available or not.",
|
|
130
|
+
catalogueKeys: "Keys",
|
|
131
|
+
catalogueSteps: "Steps",
|
|
132
|
+
catalogueUnreachable: "Unavailable elements",
|
|
133
|
+
catalogueUnreachableEmpty: "Every element is available.",
|
|
134
|
+
catalogueProducedBy: "produced by",
|
|
135
|
+
catalogueConsumedBy: "used by",
|
|
136
|
+
catalogueNeedsKeys: "needs",
|
|
137
|
+
cataloguePickKey: "Add as key",
|
|
138
|
+
catalogueAddProducingStep: "Add producing step",
|
|
139
|
+
catalogueLive: "live",
|
|
140
|
+
catalogueMissing: "missing",
|
|
141
|
+
catalogueError: "Couldn't refresh the catalogue",
|
|
142
|
+
catalogueErrorHint:
|
|
143
|
+
"The elements and data sources shown may be out of date. Try again. If it fails again, ask in the chat.",
|
|
144
|
+
validationWarning: "This view won't render as configured",
|
|
145
|
+
saveError: "The dashboard couldn't be saved",
|
|
146
|
+
saveErrorHint:
|
|
147
|
+
"Your changes are still here. Try saving again in a moment. If it fails again, ask in the chat.",
|
|
148
|
+
renameDuplicate: "A tab with that name already exists.",
|
|
149
|
+
statsLive: "live",
|
|
150
|
+
statsReachable: "available",
|
|
151
|
+
statsUnreachable: "unavailable",
|
|
152
|
+
emptyHintTitle: "No elements available yet.",
|
|
153
|
+
emptyHintBody:
|
|
154
|
+
"Add a key or a step under data sources. Elements appear as soon as their data is there.",
|
|
155
|
+
openDataSources: "Open data sources",
|
|
156
|
+
saveNamePlaceholder: "Name of the dashboard",
|
|
157
|
+
saveDescriptionPlaceholder: "Optional",
|
|
158
|
+
saved: (name) => `Dashboard “${name}” saved.`,
|
|
159
|
+
renameHint: "Rename a tab with F2, a double-click or its pencil button.",
|
|
160
|
+
renameTitle: "F2 or double-click to rename",
|
|
161
|
+
autoApplies: "Changes show in the preview right away",
|
|
162
|
+
noMoreKeys: "No more keys available.",
|
|
163
|
+
parsedType: "Detected type",
|
|
164
|
+
valueType: (code) =>
|
|
165
|
+
({ str: "text", num: "number", bool: "yes/no", arr: "list", obj: "object", null: "empty" })[
|
|
166
|
+
code
|
|
167
|
+
] ?? code,
|
|
168
|
+
removeKey: "Remove key",
|
|
169
|
+
removeStep: "Remove step",
|
|
170
|
+
noInputs: "no inputs",
|
|
171
|
+
noOutputs: "no outputs",
|
|
172
|
+
rowLabel: (index) => `Row ${index}`,
|
|
173
|
+
elementCount: (count) => (count === 1 ? "1 element" : `${count} elements`),
|
|
174
|
+
moveRowUp: "Move row up",
|
|
175
|
+
moveRowDown: "Move row down",
|
|
176
|
+
columnSpan: "Column span",
|
|
177
|
+
configureElement: "Configure element",
|
|
178
|
+
configureElementSet: (count) => `Configure element (${count} set)`,
|
|
179
|
+
removeElement: "Remove element",
|
|
180
|
+
resizeElement: "Resize element",
|
|
181
|
+
noKeysRegistered: "No keys registered.",
|
|
182
|
+
noStepsRegistered: "No steps registered.",
|
|
183
|
+
catalogueStepPrefix: "step",
|
|
184
|
+
catalogueElementPrefix: "element",
|
|
185
|
+
},
|
|
186
|
+
propsSheet: {
|
|
187
|
+
title: "Configure element",
|
|
188
|
+
description: "Settings for this element.",
|
|
189
|
+
apply: "Apply",
|
|
190
|
+
cancel: "Cancel",
|
|
191
|
+
reset: "Reset",
|
|
192
|
+
noProps: "This element has no settings.",
|
|
193
|
+
unsetOption: "Not set",
|
|
194
|
+
unsupportedType: (type) => `This field type (${type}) can't be edited here.`,
|
|
195
|
+
},
|
|
196
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
formatClockTime,
|
|
4
|
+
formatCount,
|
|
5
|
+
resolveFormatLocale,
|
|
6
|
+
resolveToolkitLocale,
|
|
7
|
+
resolveToolkitMessages,
|
|
8
|
+
TOOLKIT_LOCALES,
|
|
9
|
+
TOOLKIT_MESSAGES,
|
|
10
|
+
type ToolkitMessages,
|
|
11
|
+
} from "./toolkit-messages.js"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The catalog guard: every toolkit string follows the house copy rules in
|
|
15
|
+
* BOTH locales (brand-tone: no dash as a connector, one "…" character, no
|
|
16
|
+
* framework jargon; German with du, AI not KI, "CIB seven"; English in
|
|
17
|
+
* sentence case), and the German catalog is a real translation, not a copy.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Sample arguments for the function-valued messages. */
|
|
21
|
+
const SAMPLES: Record<string, unknown[]> = {
|
|
22
|
+
showing: [{ shown: "8", total: "42", noun: "tasks" }],
|
|
23
|
+
saved: ["weekly ops"],
|
|
24
|
+
rowLabel: [3],
|
|
25
|
+
elementCount: [2],
|
|
26
|
+
configureElementSet: [2],
|
|
27
|
+
unsupportedType: ["array"],
|
|
28
|
+
valueType: ["num"],
|
|
29
|
+
renameTabNamed: ["Übersicht"],
|
|
30
|
+
removeTabNamed: ["Übersicht"],
|
|
31
|
+
keyValueFor: ["demo:id"],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Every message of a catalog as `[path, rendered text]`. */
|
|
35
|
+
function flatten(messages: ToolkitMessages): [string, string][] {
|
|
36
|
+
const out: [string, string][] = []
|
|
37
|
+
for (const [section, entries] of Object.entries(messages)) {
|
|
38
|
+
for (const [key, value] of Object.entries(entries as Record<string, unknown>)) {
|
|
39
|
+
const text =
|
|
40
|
+
typeof value === "function"
|
|
41
|
+
? (value as (...args: unknown[]) => string)(...(SAMPLES[key] ?? []))
|
|
42
|
+
: value
|
|
43
|
+
out.push([`${section}.${key}`, String(text)])
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const shape = (messages: ToolkitMessages) =>
|
|
50
|
+
Object.fromEntries(
|
|
51
|
+
Object.entries(messages).map(([section, entries]) => [
|
|
52
|
+
section,
|
|
53
|
+
Object.fromEntries(
|
|
54
|
+
Object.entries(entries as Record<string, unknown>).map(([k, v]) => [k, typeof v]),
|
|
55
|
+
),
|
|
56
|
+
]),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
describe("toolkit catalogs", () => {
|
|
60
|
+
it("ship en and de with the identical section/key shape and value kinds", () => {
|
|
61
|
+
expect(TOOLKIT_LOCALES).toEqual(["en", "de"])
|
|
62
|
+
expect(shape(TOOLKIT_MESSAGES.de)).toEqual(shape(TOOLKIT_MESSAGES.en))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
for (const locale of TOOLKIT_LOCALES) {
|
|
66
|
+
describe(`${locale} copy rules`, () => {
|
|
67
|
+
const entries = flatten(TOOLKIT_MESSAGES[locale])
|
|
68
|
+
|
|
69
|
+
it("has no empty or padded message", () => {
|
|
70
|
+
for (const [path, text] of entries) {
|
|
71
|
+
expect(text.trim().length, path).toBeGreaterThan(0)
|
|
72
|
+
expect(text, path).toBe(text.trim())
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("uses no dash as a connector (A2) and the ellipsis character, never three dots", () => {
|
|
77
|
+
for (const [path, text] of entries) {
|
|
78
|
+
expect(text, path).not.toMatch(/—| – | - /)
|
|
79
|
+
expect(text, path).not.toContain("...")
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it("speaks no framework jargon to users (no 'pipeline')", () => {
|
|
84
|
+
for (const [path, text] of entries) expect(text, path).not.toMatch(/pipeline/i)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("writes the brand spellings (AI, CIB seven)", () => {
|
|
88
|
+
for (const [path, text] of entries) {
|
|
89
|
+
expect(text, path).not.toMatch(/\bKI\b/)
|
|
90
|
+
expect(text, path).not.toMatch(/CIB Seven|CIB SEVEN/)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
it("de addresses the reader with du — never Sie/Ihnen/Ihr or an impersonal 'man'", () => {
|
|
97
|
+
for (const [path, text] of flatten(TOOLKIT_MESSAGES.de)) {
|
|
98
|
+
expect(text, path).not.toMatch(/\b(Sie|Ihnen|Ihr|Ihre|Ihren|Ihrem|Ihrer)\b/)
|
|
99
|
+
expect(text, path).not.toMatch(/\bman\b/)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("en is in sentence case — no two capitalised words in a row within a sentence", () => {
|
|
104
|
+
const capitalised = (word: string) => /^[A-Z][a-z]/.test(word)
|
|
105
|
+
for (const [path, text] of flatten(TOOLKIT_MESSAGES.en)) {
|
|
106
|
+
const words = text.split(/\s+/)
|
|
107
|
+
for (let i = 1; i < words.length; i++) {
|
|
108
|
+
const prev = words[i - 1] ?? ""
|
|
109
|
+
const word = words[i] ?? ""
|
|
110
|
+
const titleCasePair = capitalised(prev) && !/[.!?:]$/.test(prev) && capitalised(word)
|
|
111
|
+
expect(titleCasePair, `${path}: "${prev} ${word}"`).toBe(false)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("de is a translation: only loanwords may equal the English text", () => {
|
|
117
|
+
const en = new Map(flatten(TOOLKIT_MESSAGES.en))
|
|
118
|
+
const identical = flatten(TOOLKIT_MESSAGES.de)
|
|
119
|
+
.filter(([path, text]) => en.get(path) === text)
|
|
120
|
+
.map(([, text]) => text)
|
|
121
|
+
expect([...new Set(identical)].sort()).toEqual([
|
|
122
|
+
"Details",
|
|
123
|
+
"Layout",
|
|
124
|
+
"Live",
|
|
125
|
+
"Name",
|
|
126
|
+
"Optional",
|
|
127
|
+
"Tabs",
|
|
128
|
+
])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("renders counts and names into the parametrised messages", () => {
|
|
132
|
+
const { en, de } = TOOLKIT_MESSAGES
|
|
133
|
+
expect(en.listFooter.showing({ shown: "8", total: "42", noun: "tasks" })).toBe(
|
|
134
|
+
"Showing 8 of 42 tasks",
|
|
135
|
+
)
|
|
136
|
+
expect(de.listFooter.showing({ shown: "8", total: "42", noun: "Aufgaben" })).toBe(
|
|
137
|
+
"Zeigt 8 von 42 Aufgaben",
|
|
138
|
+
)
|
|
139
|
+
// An empty noun leaves no trailing blank.
|
|
140
|
+
expect(en.listFooter.showing({ shown: "1", total: "2", noun: "" })).toBe("Showing 1 of 2")
|
|
141
|
+
expect(de.listFooter.showing({ shown: "1", total: "2", noun: "" })).toBe("Zeigt 1 von 2")
|
|
142
|
+
expect(en.builder.saved("Ops")).toBe("Dashboard “Ops” saved.")
|
|
143
|
+
expect(de.builder.saved("Ops")).toBe("Dashboard „Ops“ gespeichert.")
|
|
144
|
+
expect(en.builder.elementCount(1)).toBe("1 element")
|
|
145
|
+
expect(en.builder.elementCount(3)).toBe("3 elements")
|
|
146
|
+
expect(de.builder.elementCount(1)).toBe("1 Element")
|
|
147
|
+
expect(de.builder.elementCount(0)).toBe("0 Elemente")
|
|
148
|
+
expect(en.builder.rowLabel(2)).toBe("Row 2")
|
|
149
|
+
expect(de.builder.rowLabel(2)).toBe("Zeile 2")
|
|
150
|
+
expect(en.builder.configureElementSet(2)).toBe("Configure element (2 set)")
|
|
151
|
+
expect(de.builder.configureElementSet(2)).toBe("Element konfigurieren (2 gesetzt)")
|
|
152
|
+
expect(en.builder.valueType("bool")).toBe("yes/no")
|
|
153
|
+
expect(de.builder.valueType("bool")).toBe("Ja/Nein")
|
|
154
|
+
expect(de.builder.valueType("arr")).toBe("Liste")
|
|
155
|
+
expect(en.builder.valueType("future")).toBe("future")
|
|
156
|
+
expect(de.builder.valueType("future")).toBe("future")
|
|
157
|
+
expect(en.propsSheet.unsupportedType("array")).toBe(
|
|
158
|
+
"This field type (array) can't be edited here.",
|
|
159
|
+
)
|
|
160
|
+
expect(de.propsSheet.unsupportedType("array")).toBe(
|
|
161
|
+
"Diesen Feldtyp (array) kannst du hier nicht bearbeiten.",
|
|
162
|
+
)
|
|
163
|
+
expect(en.builder.renameTabNamed("Ops")).toBe("Rename tab “Ops”")
|
|
164
|
+
expect(de.builder.renameTabNamed("Ops")).toBe("Tab „Ops“ umbenennen")
|
|
165
|
+
expect(en.builder.removeTabNamed("Ops")).toBe("Remove tab “Ops”")
|
|
166
|
+
expect(de.builder.removeTabNamed("Ops")).toBe("Tab „Ops“ entfernen")
|
|
167
|
+
expect(en.builder.keyValueFor("demo:id")).toBe("Value of demo:id")
|
|
168
|
+
expect(de.builder.keyValueFor("demo:id")).toBe("Wert von demo:id")
|
|
169
|
+
// A key row without a name yet still has an accessible name.
|
|
170
|
+
expect(en.builder.keyValueFor("")).toBe("Value")
|
|
171
|
+
expect(de.builder.keyValueFor("")).toBe("Wert")
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("builder: names Apply, Discard and Save apart in user words (U18)", () => {
|
|
175
|
+
const { en, de } = TOOLKIT_MESSAGES
|
|
176
|
+
expect([en.builder.apply, en.builder.discard, en.builder.save]).toEqual([
|
|
177
|
+
"Apply",
|
|
178
|
+
"Discard",
|
|
179
|
+
"Save as dashboard",
|
|
180
|
+
])
|
|
181
|
+
expect([de.builder.apply, de.builder.discard, de.builder.save]).toEqual([
|
|
182
|
+
"Übernehmen",
|
|
183
|
+
"Verwerfen",
|
|
184
|
+
"Als Dashboard speichern",
|
|
185
|
+
])
|
|
186
|
+
expect(de.builder.advanced).toBe("Erweitert")
|
|
187
|
+
// The refresh state no longer reuses the Apply verb.
|
|
188
|
+
for (const m of [en.builder, de.builder]) {
|
|
189
|
+
expect(m.statusPending).not.toMatch(/apply|übernomm/i)
|
|
190
|
+
expect(m.statusRefreshing).not.toMatch(/apply|übernomm/i)
|
|
191
|
+
}
|
|
192
|
+
// Rename is announced with its keyboard path, not double-click alone.
|
|
193
|
+
expect(en.builder.renameHint).toContain("F2")
|
|
194
|
+
expect(de.builder.renameHint).toContain("F2")
|
|
195
|
+
expect(en.builder.renameTitle).toContain("F2")
|
|
196
|
+
expect(de.builder.renameTitle).toContain("F2")
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe("resolveToolkitLocale", () => {
|
|
201
|
+
it("maps a BCP 47 tag onto its shipped primary language", () => {
|
|
202
|
+
expect(resolveToolkitLocale("de")).toBe("de")
|
|
203
|
+
expect(resolveToolkitLocale("de-DE")).toBe("de")
|
|
204
|
+
expect(resolveToolkitLocale("de_AT")).toBe("de")
|
|
205
|
+
expect(resolveToolkitLocale(" DE-ch ")).toBe("de")
|
|
206
|
+
expect(resolveToolkitLocale("en-US")).toBe("en")
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("falls back to English for any other or missing locale", () => {
|
|
210
|
+
expect(resolveToolkitLocale("fr-FR")).toBe("en")
|
|
211
|
+
expect(resolveToolkitLocale("")).toBe("en")
|
|
212
|
+
expect(resolveToolkitLocale(undefined)).toBe("en")
|
|
213
|
+
expect(resolveToolkitLocale("deu")).toBe("en")
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe("resolveToolkitMessages", () => {
|
|
218
|
+
it("returns the shipped catalog itself without an override (stable identity)", () => {
|
|
219
|
+
expect(resolveToolkitMessages("de-DE")).toBe(TOOLKIT_MESSAGES.de)
|
|
220
|
+
expect(resolveToolkitMessages("es")).toBe(TOOLKIT_MESSAGES.en)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it("merges an override per section and keeps every other string", () => {
|
|
224
|
+
const merged = resolveToolkitMessages("de", {
|
|
225
|
+
listFooter: { loadMore: "Weitere laden" },
|
|
226
|
+
view: { refresh: "Neu laden" },
|
|
227
|
+
})
|
|
228
|
+
expect(merged.listFooter.loadMore).toBe("Weitere laden")
|
|
229
|
+
expect(merged.listFooter.loadingMore).toBe(TOOLKIT_MESSAGES.de.listFooter.loadingMore)
|
|
230
|
+
expect(merged.view.refresh).toBe("Neu laden")
|
|
231
|
+
expect(merged.view.retry).toBe(TOOLKIT_MESSAGES.de.view.retry)
|
|
232
|
+
for (const section of [
|
|
233
|
+
"common",
|
|
234
|
+
"filterBar",
|
|
235
|
+
"pills",
|
|
236
|
+
"renderer",
|
|
237
|
+
"builder",
|
|
238
|
+
"propsSheet",
|
|
239
|
+
] as const) {
|
|
240
|
+
expect(merged[section]).toEqual(TOOLKIT_MESSAGES.de[section])
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
describe("resolveToolkitMessages: the deprecated builder `done`", () => {
|
|
246
|
+
it("labels Apply with an override of `done`", () => {
|
|
247
|
+
const merged = resolveToolkitMessages("de", { builder: { done: "Los" } })
|
|
248
|
+
expect(merged.builder.apply).toBe("Los")
|
|
249
|
+
expect(merged.builder.done).toBe("Los")
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it("keeps an `apply` override over `done`", () => {
|
|
253
|
+
const merged = resolveToolkitMessages("en", { builder: { done: "Finish", apply: "Show" } })
|
|
254
|
+
expect(merged.builder.apply).toBe("Show")
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it("leaves Apply alone without a `done` override", () => {
|
|
258
|
+
expect(resolveToolkitMessages("en", { builder: { save: "Keep" } }).builder.apply).toBe(
|
|
259
|
+
TOOLKIT_MESSAGES.en.builder.apply,
|
|
260
|
+
)
|
|
261
|
+
expect(resolveToolkitMessages("de", { builder: { apply: "Zeigen" } }).builder.apply).toBe(
|
|
262
|
+
"Zeigen",
|
|
263
|
+
)
|
|
264
|
+
expect(resolveToolkitMessages("de", { view: { refresh: "Neu" } }).builder).toEqual(
|
|
265
|
+
TOOLKIT_MESSAGES.de.builder,
|
|
266
|
+
)
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe("formatCount", () => {
|
|
271
|
+
it("groups digits per locale", () => {
|
|
272
|
+
expect(formatCount(1234567, "en")).toBe("1,234,567")
|
|
273
|
+
expect(formatCount(1234567, "de-DE")).toBe("1.234.567")
|
|
274
|
+
expect(formatCount(7, "de")).toBe("7")
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it("falls back to English for an invalid locale tag instead of throwing", () => {
|
|
278
|
+
expect(formatCount(1234, "not a locale!")).toBe("1,234")
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
describe("formatClockTime (the view's data-as-of stamp)", () => {
|
|
283
|
+
const at = Date.UTC(2026, 9, 11, 14, 32)
|
|
284
|
+
|
|
285
|
+
it("formats hours and minutes per locale in the host's time zone", () => {
|
|
286
|
+
expect(formatClockTime(at, "de-DE", "UTC")).toBe("14:32")
|
|
287
|
+
expect(formatClockTime(at, "de-DE", "Europe/Berlin")).toBe("16:32")
|
|
288
|
+
expect(formatClockTime(at, "en-GB", "UTC")).toBe("14:32")
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it("never pads the hour of a 12-hour clock: 2:32 PM, not 02:32 PM", () => {
|
|
292
|
+
expect(formatClockTime(at, "en-US", "UTC")).toBe("2:32 PM")
|
|
293
|
+
expect(formatClockTime(Date.UTC(2026, 9, 11, 9, 5), "en-US", "UTC")).toBe("9:05 AM")
|
|
294
|
+
// Minutes keep two digits.
|
|
295
|
+
expect(formatClockTime(Date.UTC(2026, 9, 11, 14, 5), "de-DE", "UTC")).toBe("14:05")
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it("a zone the runtime rejects drops the zone, never the locale", () => {
|
|
299
|
+
// Windows ids reach a view from some hosts; German stays a 24-hour clock.
|
|
300
|
+
expect(formatClockTime(at, "de", "W. Europe Standard Time")).toMatch(/^\d{2}:\d{2}$/)
|
|
301
|
+
expect(formatClockTime(at, "de-DE", "Not/AZone")).toMatch(/^\d{2}:\d{2}$/)
|
|
302
|
+
expect(formatClockTime(at, "en-US", "Not/AZone")).toMatch(/^[1-9]\d?:\d\d [AP]M$/)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it("an invalid tag falls back to English, in the host's zone when it is valid", () => {
|
|
306
|
+
expect(() => formatClockTime(at, "not a locale!", "UTC")).not.toThrow()
|
|
307
|
+
expect(formatClockTime(at, "not a locale!", "UTC")).toBe("2:32 PM")
|
|
308
|
+
expect(formatClockTime(at, "not a locale!", "Not/AZone")).toMatch(/^[1-9]\d?:\d\d [AP]M$/)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it("the catalogs stamp it in words that carry its meaning", () => {
|
|
312
|
+
expect(TOOLKIT_MESSAGES.en.view.dataAsOf("2:32 PM")).toBe("Reloaded at 2:32 PM")
|
|
313
|
+
expect(TOOLKIT_MESSAGES.de.view.dataAsOf("14:32")).toBe("Neu geladen um 14:32 Uhr")
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
describe("resolveFormatLocale (a time in the language of its sentence)", () => {
|
|
318
|
+
it("formats in the language the words come from, never the host's other language", () => {
|
|
319
|
+
// A German profile on an en-US host: German words, a German clock.
|
|
320
|
+
expect(resolveFormatLocale("de", "en-US")).toBe("de")
|
|
321
|
+
expect(resolveFormatLocale("en", "de-DE")).toBe("en")
|
|
322
|
+
expect(
|
|
323
|
+
formatClockTime(Date.UTC(2026, 9, 11, 14, 32), resolveFormatLocale("de", "en-US"), "UTC"),
|
|
324
|
+
).toBe("14:32")
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it("keeps the regional tag of the same language: en-GB keeps its 24-hour clock", () => {
|
|
328
|
+
expect(resolveFormatLocale("en", "en-GB")).toBe("en-GB")
|
|
329
|
+
expect(resolveFormatLocale("en-GB", "en-GB")).toBe("en-GB")
|
|
330
|
+
expect(resolveFormatLocale("de", "DE_AT")).toBe("DE-AT")
|
|
331
|
+
expect(resolveFormatLocale(" de ", "de-CH ")).toBe("de-CH")
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it("prefers the text locale's own region over the host's", () => {
|
|
335
|
+
expect(resolveFormatLocale("en-GB", "en-US")).toBe("en-GB")
|
|
336
|
+
expect(resolveFormatLocale("de-AT", "de-DE")).toBe("de-AT")
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it("maps a language without a catalog onto the catalog's, like the words", () => {
|
|
340
|
+
// French words fall back to English, so does their clock.
|
|
341
|
+
expect(resolveFormatLocale("fr-FR", "fr-FR")).toBe("en")
|
|
342
|
+
expect(resolveFormatLocale(undefined, "de-DE")).toBe("en")
|
|
343
|
+
expect(resolveFormatLocale(undefined, "en-AU")).toBe("en-AU")
|
|
344
|
+
expect(resolveFormatLocale("de")).toBe("de")
|
|
345
|
+
// A bare tag or a lookalike prefix is no region of the language.
|
|
346
|
+
expect(resolveFormatLocale("en", "en")).toBe("en")
|
|
347
|
+
expect(resolveFormatLocale("de", "dex-DE")).toBe("de")
|
|
348
|
+
})
|
|
349
|
+
})
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The toolkit's own UI strings — every caption, label, placeholder and
|
|
3
|
+
* accessible name a toolkit component renders — as one typed catalog per
|
|
4
|
+
* locale. Components read the active locale's catalog through
|
|
5
|
+
* `useToolkitMessages()` (the nearest `LocaleProvider`), so a consumer only
|
|
6
|
+
* passes a locale; nothing in the toolkit hard-codes English any more.
|
|
7
|
+
*
|
|
8
|
+
* Pure data + functions (no React, no `mcp-use/react`): browser-safe and
|
|
9
|
+
* shared by the root barrel and the `./app` shell alike.
|
|
10
|
+
*/
|
|
11
|
+
import type { BuilderMessages, ToolkitMessages } from "./message-types.js"
|
|
12
|
+
import { de } from "./messages-de.js"
|
|
13
|
+
import { en } from "./messages-en.js"
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
BuilderMessages,
|
|
17
|
+
CommonMessages,
|
|
18
|
+
FilterBarMessages,
|
|
19
|
+
ListFooterMessages,
|
|
20
|
+
PropsSheetMessages,
|
|
21
|
+
RendererMessages,
|
|
22
|
+
ToolkitMessages,
|
|
23
|
+
ViewMessages,
|
|
24
|
+
} from "./message-types.js"
|
|
25
|
+
|
|
26
|
+
/** A partial override of a {@link ToolkitMessages} catalog, per section. */
|
|
27
|
+
export type ToolkitMessagesOverride = {
|
|
28
|
+
[K in keyof ToolkitMessages]?: Partial<ToolkitMessages[K]>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The locales the toolkit ships a catalog for. English is the fallback. */
|
|
32
|
+
export const TOOLKIT_LOCALES = ["en", "de"] as const
|
|
33
|
+
export type ToolkitLocale = (typeof TOOLKIT_LOCALES)[number]
|
|
34
|
+
|
|
35
|
+
/** The shipped catalogs, keyed by {@link ToolkitLocale}. */
|
|
36
|
+
export const TOOLKIT_MESSAGES: Readonly<Record<ToolkitLocale, ToolkitMessages>> = { en, de }
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Map any BCP 47 tag (`"de-DE"`, `"de_AT"`, `"EN"`) onto a shipped catalog:
|
|
40
|
+
* its primary language subtag when the toolkit ships it, English otherwise.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveToolkitLocale(locale: string | undefined): ToolkitLocale {
|
|
43
|
+
const primary = (locale ?? "").trim().toLowerCase().split(/[-_]/)[0]
|
|
44
|
+
return (TOOLKIT_LOCALES as readonly string[]).includes(primary ?? "")
|
|
45
|
+
? (primary as ToolkitLocale)
|
|
46
|
+
: "en"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The catalog for `locale`, with `override` merged over it section by
|
|
51
|
+
* section. Without an override the shipped catalog object itself is returned
|
|
52
|
+
* (referentially stable — safe as a memo dependency).
|
|
53
|
+
*/
|
|
54
|
+
export function resolveToolkitMessages(
|
|
55
|
+
locale: string | undefined,
|
|
56
|
+
override?: ToolkitMessagesOverride,
|
|
57
|
+
): ToolkitMessages {
|
|
58
|
+
const base = TOOLKIT_MESSAGES[resolveToolkitLocale(locale)]
|
|
59
|
+
if (!override) return base
|
|
60
|
+
return {
|
|
61
|
+
common: { ...base.common, ...override.common },
|
|
62
|
+
view: { ...base.view, ...override.view },
|
|
63
|
+
listFooter: { ...base.listFooter, ...override.listFooter },
|
|
64
|
+
filterBar: { ...base.filterBar, ...override.filterBar },
|
|
65
|
+
pills: { ...base.pills, ...override.pills },
|
|
66
|
+
renderer: { ...base.renderer, ...override.renderer },
|
|
67
|
+
builder: mergeBuilder(base.builder, override.builder),
|
|
68
|
+
propsSheet: { ...base.propsSheet, ...override.propsSheet },
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The builder section with its override. An override of the deprecated
|
|
74
|
+
* `done` still labels the commit button, Apply, unless it sets `apply` too.
|
|
75
|
+
*/
|
|
76
|
+
function mergeBuilder(
|
|
77
|
+
base: BuilderMessages,
|
|
78
|
+
override: Partial<BuilderMessages> | undefined,
|
|
79
|
+
): BuilderMessages {
|
|
80
|
+
const merged = { ...base, ...override }
|
|
81
|
+
if (override?.done !== undefined && override.apply === undefined) merged.apply = override.done
|
|
82
|
+
return merged
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format a count for display in `locale` (grouping separators per locale:
|
|
87
|
+
* `1,234` / `1.234`). An invalid tag falls back to English instead of
|
|
88
|
+
* throwing inside a render.
|
|
89
|
+
*/
|
|
90
|
+
export function formatCount(value: number, locale: string | undefined): string {
|
|
91
|
+
try {
|
|
92
|
+
return new Intl.NumberFormat(locale).format(value)
|
|
93
|
+
} catch {
|
|
94
|
+
return new Intl.NumberFormat("en").format(value)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The tag a toolkit sentence formats its numbers and times in: the language
|
|
100
|
+
* its words come from (`textLocale`, mapped onto a shipped catalog the way
|
|
101
|
+
* {@link resolveToolkitMessages} maps it), with the regional tag of that
|
|
102
|
+
* language from `textLocale` or else `hostLocale`, so `en-GB` keeps its
|
|
103
|
+
* 24-hour clock. A tag of another language never mixes in: German words
|
|
104
|
+
* never carry an English clock ("Neu geladen um 4:32 PM Uhr").
|
|
105
|
+
*/
|
|
106
|
+
export function resolveFormatLocale(textLocale: string | undefined, hostLocale?: string): string {
|
|
107
|
+
const language = resolveToolkitLocale(textLocale)
|
|
108
|
+
const regionOfLanguage = (tag: string | undefined): string | undefined => {
|
|
109
|
+
const normal = tag?.trim().replace(/_/g, "-")
|
|
110
|
+
if (!normal?.includes("-")) return undefined
|
|
111
|
+
return normal.toLowerCase().split("-")[0] === language ? normal : undefined
|
|
112
|
+
}
|
|
113
|
+
return regionOfLanguage(textLocale) ?? regionOfLanguage(hostLocale) ?? language
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format the clock time (hours and minutes) of `epochMs` in `locale`, in the
|
|
118
|
+
* host's `timeZone` when it reports one: `14:32` / `2:32 PM`. The hour is
|
|
119
|
+
* `numeric`, so each locale pads it its own way (a 12-hour clock never shows
|
|
120
|
+
* `02:32 PM`). A zone the runtime rejects (a Windows id, say) drops the zone,
|
|
121
|
+
* never the locale; an invalid tag falls back to English. It never throws
|
|
122
|
+
* inside a render.
|
|
123
|
+
*/
|
|
124
|
+
export function formatClockTime(
|
|
125
|
+
epochMs: number,
|
|
126
|
+
locale: string | undefined,
|
|
127
|
+
timeZone?: string,
|
|
128
|
+
): string {
|
|
129
|
+
const clock: Intl.DateTimeFormatOptions = { hour: "numeric", minute: "2-digit" }
|
|
130
|
+
const attempts: readonly (readonly [string | undefined, string | undefined])[] = [
|
|
131
|
+
[locale, timeZone],
|
|
132
|
+
[locale, undefined],
|
|
133
|
+
["en", timeZone],
|
|
134
|
+
]
|
|
135
|
+
for (const [tag, zone] of attempts) {
|
|
136
|
+
try {
|
|
137
|
+
return new Intl.DateTimeFormat(tag, { ...clock, timeZone: zone }).format(epochMs)
|
|
138
|
+
} catch {
|
|
139
|
+
// The next attempt drops what the runtime rejected.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return new Intl.DateTimeFormat("en", clock).format(epochMs)
|
|
143
|
+
}
|