@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
|
@@ -1,133 +1,371 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import { useId, type ReactNode } from "react"
|
|
2
|
+
import { ChevronRight, Minus, TrendingDown, TrendingUp, type LucideIcon } from "lucide-react"
|
|
3
|
+
import { FOCUS_RING_INSET, FOCUS_RING } from "../lib/focus-ring.js"
|
|
4
|
+
import {
|
|
5
|
+
resolveTone,
|
|
6
|
+
TONE_BORDER,
|
|
7
|
+
TONE_DOT,
|
|
8
|
+
TONE_ICON,
|
|
9
|
+
TONE_TINT,
|
|
10
|
+
type ToneName,
|
|
11
|
+
type ToneVariant,
|
|
12
|
+
} from "../lib/tone-utils.js"
|
|
13
|
+
import { MICRO_LABEL } from "../lib/typography.js"
|
|
14
|
+
import { cn } from "../lib/utils.js"
|
|
15
|
+
import { Skeleton } from "../primitives/skeleton.js"
|
|
3
16
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
type TrendDirection = "up" | "down" | "flat"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Direction → tone when no `trendTone` is set (incident-biased: up reads as
|
|
21
|
+
* worse). The tone shows ONLY as the direction icon's colour or a dot; the
|
|
22
|
+
* trend words stay neutral (no coloured digits, CI §3.3).
|
|
23
|
+
*/
|
|
24
|
+
const TREND_TONE: Record<TrendDirection, ToneVariant> = {
|
|
25
|
+
up: "danger",
|
|
26
|
+
down: "success",
|
|
27
|
+
flat: "neutral",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TREND_ICON: Record<TrendDirection, LucideIcon> = {
|
|
31
|
+
up: TrendingUp,
|
|
32
|
+
down: TrendingDown,
|
|
33
|
+
flat: Minus,
|
|
9
34
|
}
|
|
10
35
|
|
|
11
36
|
export interface KpiCell {
|
|
12
|
-
/** Caption shown above the value. */
|
|
37
|
+
/** Caption shown above the value. Part of the cell's accessible name. */
|
|
13
38
|
label: ReactNode
|
|
14
39
|
/** The headline figure. Pre-format numbers for locale/currency. */
|
|
15
40
|
value: ReactNode
|
|
16
41
|
/** Small fraction shown next to the value, e.g. ` /14`. */
|
|
17
42
|
fraction?: ReactNode
|
|
18
|
-
/** Optional trend line under the value (e.g. `"+12% vs last week"`). */
|
|
43
|
+
/** Optional trend line under the value (e.g. `"+12% vs last week"`); its words stay neutral. */
|
|
19
44
|
trend?: ReactNode
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Draws a Lucide direction icon (TrendingUp / TrendingDown / Minus) before
|
|
47
|
+
* the trend line, in the trend's tone: `up` reads as bad (danger), `down`
|
|
48
|
+
* as good (success) unless {@link KpiCell.trendTone} says otherwise.
|
|
49
|
+
*/
|
|
50
|
+
trendDirection?: TrendDirection
|
|
51
|
+
/**
|
|
52
|
+
* Explicit tone of the trend — overrides the direction's default. Shown as
|
|
53
|
+
* the direction icon's colour, or as a dot when there is no direction.
|
|
54
|
+
*/
|
|
55
|
+
trendTone?: ToneName
|
|
56
|
+
/**
|
|
57
|
+
* The state the figure stands for (e.g. `"danger"` for a breached metric),
|
|
58
|
+
* shown as a dot next to the label (strip) or as the card's tint and edge
|
|
59
|
+
* (`variant="soft"`). The figure itself stays in the foreground colour.
|
|
60
|
+
*/
|
|
61
|
+
tone?: ToneName
|
|
24
62
|
/** When set, the cell renders as a button — turns a metric into a nav entry point. */
|
|
25
63
|
onClick?: () => void
|
|
26
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* What activating the cell does ("Opens the incidents dashboard"). Read as
|
|
66
|
+
* the button's DESCRIPTION (`aria-describedby`), so its accessible name
|
|
67
|
+
* stays the visible label and value (WCAG 2.5.3, label in name).
|
|
68
|
+
*/
|
|
69
|
+
actionHint?: string
|
|
70
|
+
/**
|
|
71
|
+
* @deprecated Replaces the accessible name with text a voice-control user
|
|
72
|
+
* cannot see (WCAG 2.5.3). Say what the click does in
|
|
73
|
+
* {@link KpiCell.actionHint}; the visible label and value name the cell.
|
|
74
|
+
*/
|
|
27
75
|
ariaLabel?: string
|
|
28
76
|
}
|
|
29
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Columns per cell count. 4+ cells fall back to 2 columns in narrow frames
|
|
80
|
+
* (inline chat views are often under 500 px, where five cells side by side
|
|
81
|
+
* leave ~40 px each); 6 steps through 3.
|
|
82
|
+
*/
|
|
30
83
|
const COL_CLASS: Record<number, string> = {
|
|
31
84
|
1: "grid-cols-1",
|
|
32
85
|
2: "grid-cols-2",
|
|
33
86
|
3: "grid-cols-3",
|
|
34
|
-
4: "grid-cols-4",
|
|
35
|
-
5: "grid-cols-5",
|
|
36
|
-
6: "grid-cols-6",
|
|
87
|
+
4: "grid-cols-2 sm:grid-cols-4",
|
|
88
|
+
5: "grid-cols-2 sm:grid-cols-5",
|
|
89
|
+
6: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-6",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The responsive column classes for `count` cells (clamped to 1–6). */
|
|
93
|
+
export function kpiColumns(count: number): string {
|
|
94
|
+
const cols = Math.min(Math.max(Math.trunc(count) || 1, 1), 6)
|
|
95
|
+
return COL_CLASS[cols] ?? "grid-cols-1"
|
|
37
96
|
}
|
|
38
97
|
|
|
39
98
|
export interface KpiGridHeader {
|
|
40
|
-
/** Group label
|
|
99
|
+
/** Group label of the strip header (13 px semibold, normal case). */
|
|
41
100
|
label: ReactNode
|
|
42
101
|
/** Optional muted badge to the right of the label. */
|
|
43
102
|
badge?: ReactNode
|
|
44
103
|
}
|
|
45
104
|
|
|
46
105
|
export interface KpiGridProps {
|
|
47
|
-
/** The metric cells, flowed as columns (1–6
|
|
106
|
+
/** The metric cells, flowed as columns (1–6; more wrap onto further rows). */
|
|
48
107
|
cells: KpiCell[]
|
|
49
108
|
/** Wrap the strip in a bordered card with an optional group `header`. Default `false`. */
|
|
50
109
|
boxed?: boolean
|
|
51
|
-
/** Group header
|
|
110
|
+
/** Group header above the strip (strip variant), usually with `boxed`. */
|
|
52
111
|
header?: KpiGridHeader
|
|
112
|
+
/**
|
|
113
|
+
* `"strip"` (default): one bordered strip, the tone as a dot next to the
|
|
114
|
+
* label. `"soft"`: one card per cell in its tone's tint and edge (`boxed`
|
|
115
|
+
* and `header` don't apply).
|
|
116
|
+
*/
|
|
117
|
+
variant?: "strip" | "soft"
|
|
118
|
+
/** Names the group of metrics (`role="group"`); omit when a heading right above names it. */
|
|
119
|
+
ariaLabel?: string
|
|
120
|
+
className?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A small status dot; decorative — the label next to it carries the meaning. */
|
|
124
|
+
function ToneDot({ tone, className }: { tone: ToneVariant; className: string }) {
|
|
125
|
+
return (
|
|
126
|
+
<span
|
|
127
|
+
aria-hidden="true"
|
|
128
|
+
data-tone={tone}
|
|
129
|
+
className={cn("shrink-0 rounded-full", className, TONE_DOT[tone])}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
53
132
|
}
|
|
54
133
|
|
|
55
134
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* `boxed` + `header` for a labelled metric block; unboxed (default) renders the
|
|
60
|
-
* lighter `border-y` strip.
|
|
135
|
+
* The trend line: neutral words (the foreground next to a state, quiet
|
|
136
|
+
* without one) and the state beside them as a direction icon or a dot in the
|
|
137
|
+
* tone — never as coloured digits.
|
|
61
138
|
*/
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
139
|
+
function TrendLine({ cell }: { cell: KpiCell }) {
|
|
140
|
+
const direction = cell.trendDirection
|
|
141
|
+
const tone =
|
|
142
|
+
cell.trendTone !== undefined
|
|
143
|
+
? resolveTone(cell.trendTone)
|
|
144
|
+
: direction
|
|
145
|
+
? TREND_TONE[direction]
|
|
146
|
+
: "neutral"
|
|
147
|
+
const marked = tone !== "neutral"
|
|
148
|
+
const Icon = direction ? TREND_ICON[direction] : undefined
|
|
67
149
|
return (
|
|
68
|
-
<div className=
|
|
69
|
-
{
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
150
|
+
<div data-slot="kpi-trend" className="text-small mt-1.5 flex items-center gap-1.5">
|
|
151
|
+
{Icon ? (
|
|
152
|
+
<Icon aria-hidden="true" className={cn("size-4 shrink-0", TONE_ICON[tone])} />
|
|
153
|
+
) : (
|
|
154
|
+
marked && <ToneDot tone={tone} className="size-1.5" />
|
|
155
|
+
)}
|
|
156
|
+
<span className={marked ? "text-foreground" : "text-muted-foreground"}>{cell.trend}</span>
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Label, figure, fraction and trend — shared by the strip and soft variants. */
|
|
162
|
+
function KpiCellBody({ cell, variant }: { cell: KpiCell; variant: "strip" | "soft" }) {
|
|
163
|
+
const tone = resolveTone(cell.tone)
|
|
164
|
+
return (
|
|
165
|
+
<>
|
|
166
|
+
<div
|
|
167
|
+
className={cn(
|
|
168
|
+
"text-small flex items-center justify-between gap-2 font-medium",
|
|
169
|
+
variant === "strip" ? "text-muted-foreground" : "text-foreground",
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
<span className="inline-flex min-w-0 items-center gap-1.5">
|
|
173
|
+
{variant === "strip" && tone !== "neutral" && <ToneDot tone={tone} className="size-2" />}
|
|
174
|
+
<span className="min-w-0 break-words">{cell.label}</span>
|
|
175
|
+
</span>
|
|
176
|
+
{cell.onClick && <ChevronRight aria-hidden="true" className="size-4 shrink-0" />}
|
|
177
|
+
</div>
|
|
178
|
+
<div className="text-title text-foreground mt-1.5 leading-none tracking-tight break-words tabular-nums">
|
|
179
|
+
{cell.value}
|
|
180
|
+
{cell.fraction && (
|
|
181
|
+
<span className="text-dense text-muted-foreground ml-0.5 font-normal">
|
|
182
|
+
{cell.fraction}
|
|
183
|
+
</span>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
{cell.trend && <TrendLine cell={cell} />}
|
|
187
|
+
</>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** One cell: a button when clickable (named by its visible text), else a plain box. */
|
|
192
|
+
function KpiCellView({
|
|
193
|
+
cell,
|
|
194
|
+
variant,
|
|
195
|
+
className,
|
|
196
|
+
}: {
|
|
197
|
+
cell: KpiCell
|
|
198
|
+
variant: "strip" | "soft"
|
|
199
|
+
className: string
|
|
200
|
+
}) {
|
|
201
|
+
const hintId = useId()
|
|
202
|
+
const body = <KpiCellBody cell={cell} variant={variant} />
|
|
203
|
+
const tone = cell.tone === undefined ? undefined : resolveTone(cell.tone)
|
|
204
|
+
if (!cell.onClick) {
|
|
205
|
+
return (
|
|
206
|
+
<div data-tone={tone} className={className}>
|
|
207
|
+
{body}
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
return (
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onClick={cell.onClick}
|
|
215
|
+
aria-label={cell.ariaLabel}
|
|
216
|
+
aria-describedby={cell.actionHint ? hintId : undefined}
|
|
217
|
+
data-tone={tone}
|
|
218
|
+
className={cn(
|
|
219
|
+
"cursor-pointer text-left transition-colors",
|
|
220
|
+
variant === "strip" ? cn("hover:bg-muted", FOCUS_RING_INSET) : FOCUS_RING,
|
|
221
|
+
className,
|
|
222
|
+
)}
|
|
223
|
+
>
|
|
224
|
+
{body}
|
|
225
|
+
{/* Referenced by aria-describedby; `hidden` keeps it out of the name and the page. */}
|
|
226
|
+
{cell.actionHint && (
|
|
227
|
+
<span id={hintId} hidden>
|
|
228
|
+
{cell.actionHint}
|
|
229
|
+
</span>
|
|
230
|
+
)}
|
|
231
|
+
</button>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A soft card: the tone's tint and edge around neutral text. */
|
|
236
|
+
function softCardClass(tone: ToneName | undefined): string {
|
|
237
|
+
const t = resolveTone(tone)
|
|
238
|
+
return cn("text-foreground rounded-lg border p-4", TONE_TINT[t], TONE_BORDER[t])
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The strip's dividers. Each cell draws its own right and bottom rule, and the
|
|
243
|
+
* grid reaches 1 px past its clipping frame, so the rules on the outer edges
|
|
244
|
+
* fall away at any column count. An empty track in a short last row (5 cells
|
|
245
|
+
* in 2 columns, 7 in 6) keeps the card colour: a `gap-px` grid on a
|
|
246
|
+
* `bg-border` container would paint it as a solid block of the divider colour.
|
|
247
|
+
*/
|
|
248
|
+
const STRIP_FRAME = "bg-card overflow-hidden"
|
|
249
|
+
const STRIP_GRID = "-mr-px -mb-px grid"
|
|
250
|
+
const STRIP_CELL = "border-border min-w-0 border-r border-b px-5 py-4"
|
|
251
|
+
|
|
252
|
+
function KpiStripHeader({ header }: { header: KpiGridHeader }) {
|
|
253
|
+
return (
|
|
254
|
+
<div
|
|
255
|
+
className={cn(
|
|
256
|
+
"border-border bg-muted text-muted-foreground flex items-center gap-2 border-b px-4 py-2",
|
|
257
|
+
MICRO_LABEL,
|
|
78
258
|
)}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
259
|
+
>
|
|
260
|
+
<span>{header.label}</span>
|
|
261
|
+
{header.badge && (
|
|
262
|
+
<span className="border-border bg-card text-muted-foreground text-small rounded-sm border px-1.5 font-medium">
|
|
263
|
+
{header.badge}
|
|
264
|
+
</span>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* A row of headline metrics — the canonical way to show KPIs in a widget
|
|
272
|
+
* (replaces the single-metric KPICard). Cells flow as columns (1–6), falling
|
|
273
|
+
* back to 2 columns in narrow frames. A cell's `tone` shows as a dot next to
|
|
274
|
+
* its label (or as a soft card's tint and edge); figures stay in the
|
|
275
|
+
* foreground colour. A clickable cell is a button whose accessible name is
|
|
276
|
+
* its visible label and value; `actionHint` describes what the click does.
|
|
277
|
+
*/
|
|
278
|
+
export function KpiGrid({
|
|
279
|
+
cells,
|
|
280
|
+
boxed = false,
|
|
281
|
+
header,
|
|
282
|
+
variant = "strip",
|
|
283
|
+
ariaLabel,
|
|
284
|
+
className,
|
|
285
|
+
}: KpiGridProps) {
|
|
286
|
+
const colClass = kpiColumns(cells.length)
|
|
287
|
+
const group = ariaLabel ? { role: "group" as const, "aria-label": ariaLabel } : {}
|
|
288
|
+
if (variant === "soft") {
|
|
289
|
+
return (
|
|
290
|
+
<div {...group} data-slot="kpi-grid" className={cn("grid gap-4", colClass, className)}>
|
|
291
|
+
{cells.map((cell, idx) => (
|
|
292
|
+
<KpiCellView
|
|
293
|
+
key={idx}
|
|
294
|
+
cell={cell}
|
|
295
|
+
variant="soft"
|
|
296
|
+
className={cn("min-w-0", softCardClass(cell.tone))}
|
|
297
|
+
/>
|
|
298
|
+
))}
|
|
299
|
+
</div>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
return (
|
|
303
|
+
<div
|
|
304
|
+
{...group}
|
|
305
|
+
data-slot="kpi-grid"
|
|
306
|
+
className={cn(boxed && "border-border overflow-hidden rounded-lg border", className)}
|
|
307
|
+
>
|
|
308
|
+
{header && <KpiStripHeader header={header} />}
|
|
309
|
+
<div data-slot="kpi-strip" className={cn(STRIP_FRAME, !boxed && "border-border border-y")}>
|
|
310
|
+
<div className={cn(STRIP_GRID, colClass)}>
|
|
311
|
+
{cells.map((cell, idx) => (
|
|
312
|
+
<KpiCellView key={idx} cell={cell} variant="strip" className={STRIP_CELL} />
|
|
313
|
+
))}
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface KpiGridSkeletonProps {
|
|
321
|
+
/** How many cells the loaded grid will have. */
|
|
322
|
+
cells: number
|
|
323
|
+
/** The loaded grid's variant. Default `"strip"`. */
|
|
324
|
+
variant?: "strip" | "soft"
|
|
325
|
+
/** The loaded grid's `boxed`. Default `false`. */
|
|
326
|
+
boxed?: boolean
|
|
327
|
+
className?: string
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Loading placeholder with {@link KpiGrid}'s geometry (same wrapper, grid and
|
|
332
|
+
* cell padding in both variants), so the layout doesn't jump when the data
|
|
333
|
+
* arrives. `aria-busy`; pair it with a status line if the wait is long.
|
|
334
|
+
*/
|
|
335
|
+
export function KpiGridSkeleton({
|
|
336
|
+
cells,
|
|
337
|
+
variant = "strip",
|
|
338
|
+
boxed = false,
|
|
339
|
+
className,
|
|
340
|
+
}: KpiGridSkeletonProps) {
|
|
341
|
+
const colClass = kpiColumns(cells)
|
|
342
|
+
const count = Math.max(Math.trunc(cells) || 0, 0)
|
|
343
|
+
if (variant === "soft") {
|
|
344
|
+
return (
|
|
345
|
+
<div aria-busy="true" className={cn("grid gap-4", colClass, className)}>
|
|
346
|
+
{Array.from({ length: count }, (_, idx) => (
|
|
347
|
+
<div key={idx} className="bg-muted border-border rounded-lg border p-4">
|
|
348
|
+
<Skeleton className="h-4 w-20" />
|
|
349
|
+
<Skeleton className="mt-2 h-7 w-14" />
|
|
350
|
+
</div>
|
|
351
|
+
))}
|
|
352
|
+
</div>
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
return (
|
|
356
|
+
<div
|
|
357
|
+
aria-busy="true"
|
|
358
|
+
className={cn(boxed && "border-border overflow-hidden rounded-lg border", className)}
|
|
359
|
+
>
|
|
360
|
+
<div data-slot="kpi-strip" className={cn(STRIP_FRAME, !boxed && "border-border border-y")}>
|
|
361
|
+
<div className={cn(STRIP_GRID, colClass)}>
|
|
362
|
+
{Array.from({ length: count }, (_, idx) => (
|
|
363
|
+
<div key={idx} className={STRIP_CELL}>
|
|
364
|
+
<Skeleton className="h-4 w-16" />
|
|
365
|
+
<Skeleton className="mt-2.5 h-7 w-14" />
|
|
128
366
|
</div>
|
|
129
|
-
)
|
|
130
|
-
|
|
367
|
+
))}
|
|
368
|
+
</div>
|
|
131
369
|
</div>
|
|
132
370
|
</div>
|
|
133
371
|
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { LocaleProvider } from "../locale/locale-provider.js"
|
|
4
|
+
import { attribute, openingTags } from "../render-scan.js"
|
|
5
|
+
import { FOCUS_RING } from "../lib/focus-ring.js"
|
|
6
|
+
import { ListFooter, type ListFooterProps } from "./ListFooter.js"
|
|
7
|
+
|
|
8
|
+
const noop = (): void => {}
|
|
9
|
+
|
|
10
|
+
function render(props: Partial<ListFooterProps> = {}, locale = "en") {
|
|
11
|
+
return renderToStaticMarkup(
|
|
12
|
+
<LocaleProvider locale={locale}>
|
|
13
|
+
<ListFooter shown={8} total={42} hasMore onLoadMore={noop} noun="tasks" {...props} />
|
|
14
|
+
</LocaleProvider>,
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The text of the live region (the one `role="status"` element). */
|
|
19
|
+
function statusText(html: string): string | undefined {
|
|
20
|
+
const match = /<p role="status"[^>]*>([\s\S]*?)<\/p>/.exec(html)
|
|
21
|
+
return match?.[1]
|
|
22
|
+
?.replace(/<[^>]*>/g, " ")
|
|
23
|
+
.replace(/\s+/g, " ")
|
|
24
|
+
.trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The class of the footer's root (`sr-only` while nothing is visible). */
|
|
28
|
+
function footerClass(html: string): string | undefined {
|
|
29
|
+
return attribute(openingTags(html, "div")[0] ?? "", "class")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("ListFooter: an honest count in a polite live region", () => {
|
|
33
|
+
it("puts the count in ONE status region and Load more outside it", () => {
|
|
34
|
+
const html = render()
|
|
35
|
+
expect(html.match(/role="status"/g)).toHaveLength(1)
|
|
36
|
+
expect(statusText(html)).toBe("Showing 8 of 42 tasks")
|
|
37
|
+
const [button] = openingTags(html, "button")
|
|
38
|
+
expect(attribute(button ?? "", "type")).toBe("button")
|
|
39
|
+
expect(attribute(button ?? "", "class")).toContain(FOCUS_RING)
|
|
40
|
+
expect(html.indexOf("<button")).toBeGreaterThan(html.indexOf("</p>"))
|
|
41
|
+
expect(html).toContain("text-small")
|
|
42
|
+
expect(html).not.toContain("text-xs")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("says Updating… next to the count while a same-filter refetch runs", () => {
|
|
46
|
+
expect(statusText(render({ refreshing: true }))).toBe("Showing 8 of 42 tasks Updating…")
|
|
47
|
+
expect(statusText(render({ refreshing: true }, "de"))).toBe(
|
|
48
|
+
"Zeigt 8 von 42 tasks Wird aktualisiert…",
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("hides the count and Load more while the rows are the previous result", () => {
|
|
53
|
+
const inFlight = render({ stale: true, refreshing: true })
|
|
54
|
+
expect(statusText(inFlight)).toBe("Updating…")
|
|
55
|
+
expect(inFlight).not.toContain("<button")
|
|
56
|
+
// A failed search: the caller's alert reports it, the region stays, empty.
|
|
57
|
+
const failed = render({ stale: true })
|
|
58
|
+
expect(footerClass(failed)).toBe("sr-only")
|
|
59
|
+
expect(statusText(failed)).toBe("")
|
|
60
|
+
expect(failed).not.toContain("<button")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("shows nothing at zero rows, but keeps the live region and says so to screen readers", () => {
|
|
64
|
+
const empty = render({ total: 0, hasMore: false })
|
|
65
|
+
expect(footerClass(empty)).toBe("sr-only")
|
|
66
|
+
expect(statusText(empty)).toBe("No entries")
|
|
67
|
+
expect(statusText(render({ total: 0, hasMore: false }, "de"))).toBe("Keine Einträge")
|
|
68
|
+
expect(empty).not.toContain("<button")
|
|
69
|
+
expect(statusText(render({ total: 0, refreshing: true }))).toBe("Updating…")
|
|
70
|
+
expect(footerClass(render({ total: 0, refreshing: true }))).toContain("flex")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("keeps the SAME live region from a count to zero rows, so a search that finds nothing is announced", () => {
|
|
74
|
+
// React keeps an element whose type and position stay put; a region that
|
|
75
|
+
// unmounts and comes back with its news already in it is not announced.
|
|
76
|
+
const path = (html: string) =>
|
|
77
|
+
[...html.matchAll(/<(div|p)\b[^>]*?(data-slot="[^"]*")/g)].map((m) => `${m[1]}[${m[2]}]`)
|
|
78
|
+
const states = [
|
|
79
|
+
render(),
|
|
80
|
+
render({ refreshing: true }),
|
|
81
|
+
render({ total: 0, hasMore: false }),
|
|
82
|
+
render({ stale: true }),
|
|
83
|
+
]
|
|
84
|
+
for (const html of states) {
|
|
85
|
+
expect(path(html)).toEqual([
|
|
86
|
+
'div[data-slot="list-footer"]',
|
|
87
|
+
'p[data-slot="list-footer-status"]',
|
|
88
|
+
])
|
|
89
|
+
expect(html.match(/role="status"/g)).toHaveLength(1)
|
|
90
|
+
}
|
|
91
|
+
expect(states.map(statusText)).toEqual([
|
|
92
|
+
"Showing 8 of 42 tasks",
|
|
93
|
+
"Showing 8 of 42 tasks Updating…",
|
|
94
|
+
"No entries",
|
|
95
|
+
"",
|
|
96
|
+
])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("leaves the announcing to a caller that owns a live region (announce={false})", () => {
|
|
100
|
+
const html = render({ announce: false, refreshing: true })
|
|
101
|
+
expect(html).not.toContain('role="status"')
|
|
102
|
+
expect(html).toMatch(/<p data-slot="list-footer-status"[^>]*>.*Showing 8 of 42 tasks/)
|
|
103
|
+
expect(html).toContain("Updating…")
|
|
104
|
+
// Nothing visible and no region to keep: nothing at all.
|
|
105
|
+
expect(render({ announce: false, total: 0 })).toBe("")
|
|
106
|
+
expect(render({ announce: false, stale: true })).toBe("")
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("disables Load more while a page is in flight, and takes label overrides", () => {
|
|
110
|
+
const html = render({ loadingMore: true, labels: { loadingMore: "Fetching…" } })
|
|
111
|
+
const [button] = openingTags(html, "button")
|
|
112
|
+
expect(attribute(button ?? "", "disabled")).toBe("")
|
|
113
|
+
expect(html).toContain("Fetching…</button>")
|
|
114
|
+
expect(render({ hasMore: false })).not.toContain("<button")
|
|
115
|
+
})
|
|
116
|
+
})
|