@miragon/mcp-toolkit-ui 2.7.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 +7 -4
- 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.js +1 -1
- 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 +21 -7
- 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 +11 -4
- 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/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 +2 -1
- package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
- package/dist/app/builder/PipelineStrip.js +26 -13
- 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 +8 -2
- package/dist/app/builder/SaveDialog.js.map +1 -1
- package/dist/app/builder/Toolbar.d.ts +12 -4
- package/dist/app/builder/Toolbar.d.ts.map +1 -1
- package/dist/app/builder/Toolbar.js +18 -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 +143 -1
- package/dist/app/builder/builder-model.d.ts.map +1 -1
- package/dist/app/builder/builder-model.js +191 -1
- package/dist/app/builder/builder-model.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/use-builder-catalogue.d.ts +4 -1
- package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
- package/dist/app/builder/use-builder-catalogue.js +44 -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/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 +13 -25
- package/dist/app/layout-builder.d.ts.map +1 -1
- package/dist/app/layout-builder.js +75 -81
- package/dist/app/layout-builder.js.map +1 -1
- package/dist/app/mcp-app-view-chrome.d.ts +12 -3
- package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
- package/dist/app/mcp-app-view-chrome.js +21 -5
- package/dist/app/mcp-app-view-chrome.js.map +1 -1
- package/dist/app/mcp-app-view-states.d.ts +11 -3
- package/dist/app/mcp-app-view-states.d.ts.map +1 -1
- package/dist/app/mcp-app-view-states.js +29 -8
- package/dist/app/mcp-app-view-states.js.map +1 -1
- package/dist/app/mcp-app-view.d.ts +33 -1
- package/dist/app/mcp-app-view.d.ts.map +1 -1
- package/dist/app/mcp-app-view.js +181 -127
- package/dist/app/mcp-app-view.js.map +1 -1
- package/dist/app/mcp-toolkit-app.d.ts +6 -1
- package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
- package/dist/app/mcp-toolkit-app.js +8 -1
- 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 +4 -0
- package/dist/app/use-view-lifecycle.d.ts.map +1 -1
- package/dist/app/use-view-lifecycle.js +2 -0
- package/dist/app/use-view-lifecycle.js.map +1 -1
- package/dist/app/view-lifecycle.d.ts +80 -4
- package/dist/app/view-lifecycle.d.ts.map +1 -1
- package/dist/app/view-lifecycle.js +90 -3
- package/dist/app/view-lifecycle.js.map +1 -1
- 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 +3 -1
- package/dist/app/widget-props-sheet.d.ts.map +1 -1
- package/dist/app/widget-props-sheet.js +22 -10
- package/dist/app/widget-props-sheet.js.map +1 -1
- package/dist/app/widget-renderer.js +1 -1
- 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 +8 -6
- package/dist/components/DrillButton.js.map +1 -1
- package/dist/components/EmptyState.d.ts.map +1 -1
- package/dist/components/EmptyState.js +4 -2
- package/dist/components/EmptyState.js.map +1 -1
- package/dist/components/ErrorState.d.ts +5 -2
- package/dist/components/ErrorState.d.ts.map +1 -1
- package/dist/components/ErrorState.js +25 -8
- package/dist/components/ErrorState.js.map +1 -1
- package/dist/components/FilterBar.d.ts +15 -4
- package/dist/components/FilterBar.d.ts.map +1 -1
- package/dist/components/FilterBar.js +11 -6
- package/dist/components/FilterBar.js.map +1 -1
- package/dist/components/KpiGrid.d.ts +66 -21
- package/dist/components/KpiGrid.d.ts.map +1 -1
- package/dist/components/KpiGrid.js +101 -34
- package/dist/components/KpiGrid.js.map +1 -1
- package/dist/components/ListFooter.d.ts +31 -4
- package/dist/components/ListFooter.d.ts.map +1 -1
- package/dist/components/ListFooter.js +30 -11
- 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 +27 -7
- package/dist/components/Pills.d.ts.map +1 -1
- package/dist/components/Pills.js +21 -6
- 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 +9 -3
- package/dist/components/Toast.d.ts.map +1 -1
- package/dist/components/Toast.js +31 -16
- package/dist/components/Toast.js.map +1 -1
- 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 +47 -52
- package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
- package/dist/hooks/use-tool-result-recovery.js +58 -120
- package/dist/hooks/use-tool-result-recovery.js.map +1 -1
- package/dist/hooks/use-view-data.d.ts +52 -10
- package/dist/hooks/use-view-data.d.ts.map +1 -1
- package/dist/hooks/use-view-data.js +81 -16
- 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 +17 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -6
- 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/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 +41 -1
- package/dist/lib/toast-model.d.ts.map +1 -1
- package/dist/lib/toast-model.js +67 -1
- package/dist/lib/toast-model.js.map +1 -1
- package/dist/lib/tone-utils.d.ts +58 -6
- package/dist/lib/tone-utils.d.ts.map +1 -1
- package/dist/lib/tone-utils.js +66 -4
- 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 +1 -1
- package/dist/locale/index.d.ts.map +1 -1
- package/dist/locale/message-types.d.ts +117 -4
- package/dist/locale/message-types.d.ts.map +1 -1
- package/dist/locale/messages-de.d.ts.map +1 -1
- package/dist/locale/messages-de.js +44 -10
- package/dist/locale/messages-de.js.map +1 -1
- package/dist/locale/messages-en.d.ts.map +1 -1
- package/dist/locale/messages-en.js +44 -10
- package/dist/locale/messages-en.js.map +1 -1
- package/dist/locale/toolkit-messages.d.ts +19 -1
- package/dist/locale/toolkit-messages.d.ts.map +1 -1
- package/dist/locale/toolkit-messages.js +54 -1
- package/dist/locale/toolkit-messages.js.map +1 -1
- package/dist/primitives/alert.d.ts +1 -1
- package/dist/primitives/alert.d.ts.map +1 -1
- package/dist/primitives/alert.js +2 -0
- 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 +9 -4
- package/dist/primitives/badge.js.map +1 -1
- package/dist/primitives/button.d.ts +1 -1
- package/dist/primitives/button.js +1 -1
- package/dist/primitives/button.js.map +1 -1
- package/dist/primitives/dialog.d.ts +1 -1
- package/dist/primitives/dialog.d.ts.map +1 -1
- package/dist/primitives/dialog.js +10 -3
- package/dist/primitives/dialog.js.map +1 -1
- package/dist/primitives/select.js +1 -1
- package/dist/primitives/select.js.map +1 -1
- package/dist/primitives/sheet.d.ts +1 -1
- package/dist/primitives/sheet.d.ts.map +1 -1
- package/dist/primitives/sheet.js +6 -2
- package/dist/primitives/sheet.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 +30 -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/theme/create-theme.d.ts +19 -1
- package/dist/theme/create-theme.d.ts.map +1 -1
- package/dist/theme/create-theme.js +15 -0
- package/dist/theme/create-theme.js.map +1 -1
- package/package.json +5 -4
- package/src/app/adapt-data-widget.tsx +2 -42
- package/src/app/builder/BuilderBanners.tsx +1 -1
- package/src/app/builder/CanvasRow.tsx +63 -17
- package/src/app/builder/CatalogueSheet.tsx +50 -29
- package/src/app/builder/CellPropsSheet.tsx +17 -2
- package/src/app/builder/DiscardDialog.tsx +59 -0
- package/src/app/builder/LayoutSurface.tsx +206 -66
- package/src/app/builder/PipelineStrip.tsx +253 -153
- package/src/app/builder/SaveDialog.tsx +14 -2
- package/src/app/builder/Toolbar.tsx +144 -59
- package/src/app/builder/Workspace.tsx +226 -58
- package/src/app/builder/builder-model.ts +341 -2
- 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/reveal.ts +10 -0
- package/src/app/builder/use-builder-catalogue.ts +56 -19
- package/src/app/builder/use-builder-session.ts +68 -0
- package/src/app/builder/use-display-resolver.ts +24 -0
- package/src/app/index.ts +12 -2
- package/src/app/layout-builder.tsx +178 -191
- package/src/app/mcp-app-view-chrome.test.tsx +141 -1
- package/src/app/mcp-app-view-chrome.tsx +56 -6
- package/src/app/mcp-app-view-states.tsx +51 -11
- package/src/app/mcp-app-view.effects.test.tsx +292 -0
- package/src/app/mcp-app-view.test.tsx +222 -2
- package/src/app/mcp-app-view.tsx +267 -117
- package/src/app/mcp-toolkit-app.test.tsx +40 -0
- package/src/app/mcp-toolkit-app.tsx +8 -1
- 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 +6 -0
- package/src/app/view-lifecycle.test.ts +216 -0
- package/src/app/view-lifecycle.ts +165 -5
- 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 +57 -16
- package/src/app/widget-renderer.tsx +1 -1
- package/src/components/DrillButton.tsx +23 -10
- package/src/components/EmptyState.tsx +6 -4
- package/src/components/ErrorState.tsx +31 -9
- package/src/components/FilterBar.test.tsx +82 -0
- package/src/components/FilterBar.tsx +52 -26
- package/src/components/KpiGrid.test.tsx +279 -0
- package/src/components/KpiGrid.tsx +297 -90
- package/src/components/ListFooter.test.tsx +116 -0
- package/src/components/ListFooter.tsx +102 -15
- 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 +63 -10
- package/src/components/SectionHeading.tsx +9 -3
- package/src/components/Toast.tsx +37 -19
- package/src/components/WidgetHeader.test.tsx +78 -0
- package/src/components/WidgetHeader.tsx +63 -16
- package/src/components/feedback-states.test.tsx +98 -9
- package/src/components/tone-figures.test.tsx +8 -4
- 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 +3 -4
- package/src/hooks/use-tool-result-recovery.ts +84 -142
- package/src/hooks/use-view-data.test.tsx +118 -9
- package/src/hooks/use-view-data.ts +128 -18
- package/src/hooks/use-view-tool-query.ts +25 -19
- package/src/i18n-coverage.test.tsx +104 -2
- package/src/index.ts +48 -11
- package/src/lib/focus-return.test.ts +288 -0
- package/src/lib/focus-return.ts +96 -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 +166 -1
- package/src/lib/toast-model.ts +104 -2
- package/src/lib/tone-utils.test.ts +86 -10
- package/src/lib/tone-utils.ts +89 -8
- 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 +1 -0
- package/src/locale/message-types.ts +118 -2
- package/src/locale/messages-de.ts +49 -10
- package/src/locale/messages-en.ts +49 -10
- package/src/locale/toolkit-messages.test.ts +133 -0
- package/src/locale/toolkit-messages.ts +63 -2
- package/src/primitives/alert.tsx +3 -0
- package/src/primitives/badge.tsx +9 -4
- package/src/primitives/button.tsx +1 -1
- package/src/primitives/dialog.tsx +10 -1
- package/src/primitives/select.tsx +1 -1
- package/src/primitives/sheet.tsx +7 -0
- 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 +38 -6
- package/src/render-effects.tsx +96 -0
- package/src/theme/cd-tokens.test.ts +3 -1
- package/src/theme/create-theme.test.ts +10 -0
- package/src/theme/create-theme.ts +28 -0
- package/src/theme/theme-css.test.ts +51 -0
- package/src/theme.css +31 -1
- package/ui-catalog.json +448 -41
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { FOCUS_RING } from "../lib/focus-ring.js"
|
|
2
|
+
import { cn } from "../lib/utils.js"
|
|
2
3
|
import { useToolkitLocale, useToolkitMessages } from "../locale/locale-provider.js"
|
|
4
|
+
import type { ListFooterMessages } from "../locale/message-types.js"
|
|
3
5
|
import { formatCount } from "../locale/toolkit-messages.js"
|
|
4
6
|
|
|
5
7
|
export interface ListFooterProps {
|
|
@@ -15,14 +17,40 @@ export interface ListFooterProps {
|
|
|
15
17
|
onLoadMore: () => void
|
|
16
18
|
/** Plural noun for the count, e.g. `"instances"`. Default: the locale's word for rows. */
|
|
17
19
|
noun?: string
|
|
20
|
+
/**
|
|
21
|
+
* A new first page is in flight while these rows stay on screen (a search,
|
|
22
|
+
* a filter, a refetch): the status line adds a visible "Updating…".
|
|
23
|
+
*/
|
|
24
|
+
refreshing?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* The rows on screen are the PREVIOUS result (a changed search or filter is
|
|
27
|
+
* in flight or failed): the count and "Load more" hide, so the old count
|
|
28
|
+
* never reads as the new search's answer.
|
|
29
|
+
*/
|
|
30
|
+
stale?: boolean
|
|
31
|
+
/** Override strings of the active catalog's `listFooter` section. */
|
|
32
|
+
labels?: Partial<ListFooterMessages>
|
|
33
|
+
/**
|
|
34
|
+
* Make the count line a polite live region (`role="status"`). Default
|
|
35
|
+
* `true`. Set `false` only where the caller already announces the count in
|
|
36
|
+
* a live region of its own, so a screen reader hears it once.
|
|
37
|
+
*/
|
|
38
|
+
announce?: boolean
|
|
39
|
+
className?: string
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
/**
|
|
21
43
|
* Footer for a paginated list: an honest "Showing X of Y" total plus an
|
|
22
44
|
* explicit "Load more" control. Deliberately not infinite scroll — the user
|
|
23
|
-
* gets the total, a reachable footer, and control over fetching.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
45
|
+
* gets the total, a reachable footer, and control over fetching. The count
|
|
46
|
+
* is a polite live region (`role="status"`), so a keyboard or screen-reader
|
|
47
|
+
* user hears the new total after "Load more" or a filter change; while a
|
|
48
|
+
* first page is in flight it also says "Updating…". With nothing to count and
|
|
49
|
+
* nothing in flight it shows nothing, but the live region stays mounted
|
|
50
|
+
* (visually hidden) and says "No entries": a search that finds nothing is
|
|
51
|
+
* announced too, and the region is never re-created with its news already in
|
|
52
|
+
* it (a screen reader stays silent then). Text and number format follow the
|
|
53
|
+
* active locale (`LocaleProvider`, else the host's inside `McpAppView`).
|
|
26
54
|
*/
|
|
27
55
|
export function ListFooter({
|
|
28
56
|
shown,
|
|
@@ -31,20 +59,40 @@ export function ListFooter({
|
|
|
31
59
|
loadingMore = false,
|
|
32
60
|
onLoadMore,
|
|
33
61
|
noun,
|
|
62
|
+
refreshing = false,
|
|
63
|
+
stale = false,
|
|
64
|
+
labels,
|
|
65
|
+
announce = true,
|
|
66
|
+
className,
|
|
34
67
|
}: ListFooterProps) {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
68
|
+
const listFooter = { ...useToolkitMessages().listFooter, ...labels }
|
|
69
|
+
const counted = total > 0 && !stale
|
|
70
|
+
const visible = counted || refreshing
|
|
71
|
+
// Without a live region of its own there is nothing to keep mounted.
|
|
72
|
+
if (!visible && !announce) return null
|
|
38
73
|
return (
|
|
39
|
-
<div
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
<div
|
|
75
|
+
data-slot="list-footer"
|
|
76
|
+
// The same element tree in both states, so React keeps the live region.
|
|
77
|
+
className={visible ? cn(FOOTER_CLASS, className) : "sr-only"}
|
|
78
|
+
>
|
|
79
|
+
<p
|
|
80
|
+
role={announce ? "status" : undefined}
|
|
81
|
+
data-slot="list-footer-status"
|
|
82
|
+
className="flex flex-wrap items-center gap-x-2"
|
|
83
|
+
>
|
|
84
|
+
<StatusLine
|
|
85
|
+
messages={listFooter}
|
|
86
|
+
shown={shown}
|
|
87
|
+
total={total}
|
|
88
|
+
noun={noun}
|
|
89
|
+
counted={counted}
|
|
90
|
+
refreshing={refreshing}
|
|
91
|
+
// Zero rows, not a stale previous result (its failure the caller reports).
|
|
92
|
+
empty={!visible && !stale}
|
|
93
|
+
/>
|
|
94
|
+
</p>
|
|
95
|
+
{counted && hasMore && (
|
|
48
96
|
<button
|
|
49
97
|
type="button"
|
|
50
98
|
onClick={onLoadMore}
|
|
@@ -57,3 +105,42 @@ export function ListFooter({
|
|
|
57
105
|
</div>
|
|
58
106
|
)
|
|
59
107
|
}
|
|
108
|
+
|
|
109
|
+
const FOOTER_CLASS =
|
|
110
|
+
"text-small text-muted-foreground flex items-center justify-between gap-3 px-1 py-2"
|
|
111
|
+
|
|
112
|
+
/** What the live region says: the count, "Updating…", or "No entries" at zero rows. */
|
|
113
|
+
function StatusLine({
|
|
114
|
+
messages,
|
|
115
|
+
shown,
|
|
116
|
+
total,
|
|
117
|
+
noun,
|
|
118
|
+
counted,
|
|
119
|
+
refreshing,
|
|
120
|
+
empty,
|
|
121
|
+
}: {
|
|
122
|
+
messages: ListFooterMessages
|
|
123
|
+
shown: number
|
|
124
|
+
total: number
|
|
125
|
+
noun: string | undefined
|
|
126
|
+
counted: boolean
|
|
127
|
+
refreshing: boolean
|
|
128
|
+
empty: boolean
|
|
129
|
+
}) {
|
|
130
|
+
const locale = useToolkitLocale()
|
|
131
|
+
return (
|
|
132
|
+
<>
|
|
133
|
+
{empty && <span>{messages.noResults}</span>}
|
|
134
|
+
{counted && (
|
|
135
|
+
<span>
|
|
136
|
+
{messages.showing({
|
|
137
|
+
shown: formatCount(shown, locale),
|
|
138
|
+
total: formatCount(total, locale),
|
|
139
|
+
noun: noun ?? messages.defaultNoun,
|
|
140
|
+
})}
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
{refreshing && <span data-slot="list-footer-updating">{messages.updating}</span>}
|
|
144
|
+
</>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { LocaleProvider } from "../locale/locale-provider.js"
|
|
4
|
+
import { TOOLKIT_MESSAGES } from "../locale/toolkit-messages.js"
|
|
5
|
+
import { attribute, openingTags, userFacingTexts } from "../render-scan.js"
|
|
6
|
+
import { PagedListFooter, type PagedListState } from "./PagedListFooter.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The paged-list tail on its SSR markup (the house render path): a page-0
|
|
10
|
+
* failure over the previous rows and a load-more failure stay two separate
|
|
11
|
+
* alerts with their own retries; the count is a live region that says
|
|
12
|
+
* "Updating…" while page 0 is in flight and hides while the rows are stale.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const noop = (): void => {}
|
|
16
|
+
const loadMore = (): void => {}
|
|
17
|
+
const retry = (): void => {}
|
|
18
|
+
|
|
19
|
+
/** A settled page 0: two of three rows on screen, nothing in flight. */
|
|
20
|
+
function paged(overrides: Partial<PagedListState> = {}): PagedListState {
|
|
21
|
+
return {
|
|
22
|
+
items: ["a", "b"],
|
|
23
|
+
firstPage: {},
|
|
24
|
+
total: 3,
|
|
25
|
+
hasMore: true,
|
|
26
|
+
loadMore,
|
|
27
|
+
retry,
|
|
28
|
+
...overrides,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function render(state: PagedListState, locale = "en") {
|
|
33
|
+
return renderToStaticMarkup(
|
|
34
|
+
<LocaleProvider locale={locale}>
|
|
35
|
+
<PagedListFooter paged={state} noun="items" />
|
|
36
|
+
</LocaleProvider>,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const alerts = (html: string) =>
|
|
41
|
+
openingTags(html, "div").filter((t) => attribute(t, "role") === "alert")
|
|
42
|
+
const statusOf = (html: string) =>
|
|
43
|
+
/<p role="status"[^>]*>([\s\S]*?)<\/p>/
|
|
44
|
+
.exec(html)?.[1]
|
|
45
|
+
?.replace(/<[^>]*>/g, " ")
|
|
46
|
+
.replace(/\s+/g, " ")
|
|
47
|
+
.trim()
|
|
48
|
+
|
|
49
|
+
describe("PagedListFooter — settled and in-flight states", () => {
|
|
50
|
+
it("a settled page: the count in the live region, Load more, no alert", () => {
|
|
51
|
+
const html = render(paged())
|
|
52
|
+
expect(statusOf(html)).toBe("Showing 2 of 3 items")
|
|
53
|
+
expect(alerts(html)).toHaveLength(0)
|
|
54
|
+
expect(html).toContain("Load more</button>")
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("a search that finds nothing: the live region stays and says so, nothing visible", () => {
|
|
58
|
+
const html = render(paged({ items: [], total: 0, hasMore: false }))
|
|
59
|
+
expect(statusOf(html)).toBe("No entries")
|
|
60
|
+
expect(html.match(/role="status"/g)).toHaveLength(1)
|
|
61
|
+
const footer = openingTags(html, "div").find((t) => /data-slot="list-footer"/.test(t)) ?? ""
|
|
62
|
+
expect(attribute(footer, "class")).toBe("sr-only")
|
|
63
|
+
expect(statusOf(render(paged({ items: [], total: 0, hasMore: false }), "de"))).toBe(
|
|
64
|
+
TOOLKIT_MESSAGES.de.listFooter.noResults,
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("a new search in flight: a visible Updating…, no count beside the previous rows", () => {
|
|
69
|
+
const html = render(paged({ stale: true, refreshing: true }))
|
|
70
|
+
expect(statusOf(html)).toBe("Updating…")
|
|
71
|
+
expect(html).not.toMatch(/Showing/)
|
|
72
|
+
expect(html).not.toContain("Load more")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("a same-filter refetch: Updating… and the count of THIS filter stay", () => {
|
|
76
|
+
expect(statusOf(render(paged({ refreshing: true })))).toBe("Showing 2 of 3 items Updating…")
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe("PagedListFooter — two failures, two retries", () => {
|
|
81
|
+
const failed = new Error("[503] engine down")
|
|
82
|
+
|
|
83
|
+
it("a failed search over the previous rows: a warning alert whose retry re-runs page 0", () => {
|
|
84
|
+
const html = render(paged({ stale: true, error: failed }))
|
|
85
|
+
const [alert] = alerts(html)
|
|
86
|
+
expect(attribute(alert ?? "", "data-tone")).toBe("warning")
|
|
87
|
+
const texts = userFacingTexts(html).map((t) => t.value)
|
|
88
|
+
expect(texts).toContain(TOOLKIT_MESSAGES.en.listFooter.refreshFailed)
|
|
89
|
+
expect(texts).toContain(TOOLKIT_MESSAGES.en.listFooter.refreshFailedHint)
|
|
90
|
+
// The raw reason sits under Details, never in the sentence.
|
|
91
|
+
expect(html).toMatch(/<summary[^>]*>Details<\/summary><pre[^>]*>\[503\] engine down<\/pre>/)
|
|
92
|
+
expect(html).toContain("Try again</button>")
|
|
93
|
+
expect(html).not.toMatch(/Showing/)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("leaves a page-0 failure WITHOUT rows on screen to the caller's loading guard", () => {
|
|
97
|
+
expect(alerts(render(paged({ firstPage: null, error: failed })))).toHaveLength(0)
|
|
98
|
+
expect(alerts(render(paged({ firstPage: undefined, error: failed })))).toHaveLength(0)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("a failed load-more: a danger alert next to a still-current count", () => {
|
|
102
|
+
const html = render(paged({ loadMoreError: failed }))
|
|
103
|
+
const [alert] = alerts(html)
|
|
104
|
+
expect(attribute(alert ?? "", "data-tone")).toBe("danger")
|
|
105
|
+
expect(userFacingTexts(html).map((t) => t.value)).toContain(
|
|
106
|
+
TOOLKIT_MESSAGES.en.listFooter.loadMoreFailed,
|
|
107
|
+
)
|
|
108
|
+
expect(statusOf(html)).toBe("Showing 2 of 3 items")
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it("keeps both failures apart, each with its own retry, in German", () => {
|
|
112
|
+
const html = render(paged({ stale: true, error: failed, loadMoreError: failed }), "de")
|
|
113
|
+
expect(alerts(html).map((a) => attribute(a, "data-tone"))).toEqual(["warning", "danger"])
|
|
114
|
+
const texts = userFacingTexts(html).map((t) => t.value)
|
|
115
|
+
expect(texts).toContain("Die Liste konnte nicht aktualisiert werden")
|
|
116
|
+
expect(texts).toContain("Weitere Einträge konnten nicht geladen werden")
|
|
117
|
+
expect(openingTags(html, "button")).toHaveLength(2)
|
|
118
|
+
expect(html.match(/Erneut versuchen<\/button>/g)).toHaveLength(2)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("offers no retry it cannot run, and takes the caller's labels", () => {
|
|
122
|
+
// A paged state without a page-0 retry callback.
|
|
123
|
+
const noRetryState = paged({ error: failed })
|
|
124
|
+
delete noRetryState.retry
|
|
125
|
+
const withoutRetry = render(noRetryState)
|
|
126
|
+
expect(alerts(withoutRetry)).toHaveLength(1)
|
|
127
|
+
// Only Load more is a button: the alert offers no retry without paged.retry.
|
|
128
|
+
expect(openingTags(withoutRetry, "button")).toHaveLength(1)
|
|
129
|
+
expect(withoutRetry).not.toContain("Try again</button>")
|
|
130
|
+
const custom = renderToStaticMarkup(
|
|
131
|
+
<PagedListFooter
|
|
132
|
+
paged={paged({ loadMoreError: failed, loadMore: noop })}
|
|
133
|
+
labels={{ loadMoreFailed: "More tasks didn't load" }}
|
|
134
|
+
retryLabel="Load again"
|
|
135
|
+
/>,
|
|
136
|
+
)
|
|
137
|
+
expect(userFacingTexts(custom).map((t) => t.value)).toContain("More tasks didn't load")
|
|
138
|
+
expect(custom).toContain("Load again</button>")
|
|
139
|
+
})
|
|
140
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useToolkitMessages } from "../locale/locale-provider.js"
|
|
2
|
+
import type { ListFooterMessages } from "../locale/message-types.js"
|
|
3
|
+
import { ErrorState } from "./ErrorState.js"
|
|
4
|
+
import { ListFooter } from "./ListFooter.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What {@link PagedListFooter} reads from a paged list — structural, so a
|
|
8
|
+
* paged-query hook's result (`items`, `total`, `loadMore`, …) passes as is.
|
|
9
|
+
*/
|
|
10
|
+
export interface PagedListState {
|
|
11
|
+
/** The rows on screen (all loaded pages); its length is the "shown" count. */
|
|
12
|
+
items: readonly unknown[]
|
|
13
|
+
/** The server's total for the rows on screen. */
|
|
14
|
+
total: number
|
|
15
|
+
/** Whether another page exists. */
|
|
16
|
+
hasMore: boolean
|
|
17
|
+
/** Fetch the next page (also the retry of a failed load-more). */
|
|
18
|
+
loadMore: () => void
|
|
19
|
+
/** A next page is in flight. */
|
|
20
|
+
loadingMore?: boolean
|
|
21
|
+
/** A first page is in flight while rows are on screen (a search, a filter, a refetch). */
|
|
22
|
+
refreshing?: boolean
|
|
23
|
+
/** The rows on screen are the previous result: a changed search or filter is in flight or failed. */
|
|
24
|
+
stale?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* The first page that is on screen, if any. A first-page {@link error} only
|
|
27
|
+
* belongs in the footer while one is shown; without rows on screen it is
|
|
28
|
+
* the load error the caller's loading guard renders.
|
|
29
|
+
*/
|
|
30
|
+
firstPage?: unknown
|
|
31
|
+
/** The first-page failure (see {@link firstPage}). */
|
|
32
|
+
error?: Error | null
|
|
33
|
+
/** Re-run the first page — the retry of {@link error}. */
|
|
34
|
+
retry?: () => void
|
|
35
|
+
/** The last load-more failure; {@link loadMore} retries it, the rows above stay current. */
|
|
36
|
+
loadMoreError?: Error | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PagedListFooterProps {
|
|
40
|
+
/** The paged list's state. */
|
|
41
|
+
paged: PagedListState
|
|
42
|
+
/** Plural noun for "Showing X of Y {noun}". Default: the locale's word for rows. */
|
|
43
|
+
noun?: string
|
|
44
|
+
/** Override strings of the active catalog's `listFooter` section. */
|
|
45
|
+
labels?: Partial<ListFooterMessages>
|
|
46
|
+
/** Retry button label of both error lines. Default: the locale's "Try again". */
|
|
47
|
+
retryLabel?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The raw reason of a failure, for the "Details" disclosure (never the sentence). */
|
|
51
|
+
function rawReason(error: Error): string {
|
|
52
|
+
return error.message || error.name
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The tail of a paged list: its failures and the honest {@link ListFooter}.
|
|
57
|
+
* Two failures stay apart because their retries differ — a failed first page
|
|
58
|
+
* over the rows still on screen ("You're seeing the previous result", its
|
|
59
|
+
* retry re-runs page 0) and a failed load-more ("The entries above are
|
|
60
|
+
* current", its retry fetches the next offset). Each is an inline
|
|
61
|
+
* `ErrorState` (role="alert") with the raw reason under "Details". The
|
|
62
|
+
* footer's count line is a polite live region that says "Updating…" while a
|
|
63
|
+
* first page is in flight, hides the count while the rows are stale, and
|
|
64
|
+
* stays mounted at zero rows ("No entries", visually hidden), so a search
|
|
65
|
+
* that finds nothing is announced.
|
|
66
|
+
*/
|
|
67
|
+
export function PagedListFooter({ paged, noun, labels, retryLabel }: PagedListFooterProps) {
|
|
68
|
+
const catalog = useToolkitMessages().listFooter
|
|
69
|
+
const L = { ...catalog, ...labels }
|
|
70
|
+
const refreshError = paged.firstPage != null ? (paged.error ?? null) : null
|
|
71
|
+
return (
|
|
72
|
+
<div data-slot="paged-list-footer" className="flex flex-col gap-2">
|
|
73
|
+
{refreshError && (
|
|
74
|
+
<ErrorState
|
|
75
|
+
variant="inline"
|
|
76
|
+
tone="warning"
|
|
77
|
+
title={L.refreshFailed}
|
|
78
|
+
description={L.refreshFailedHint}
|
|
79
|
+
details={rawReason(refreshError)}
|
|
80
|
+
onRetry={paged.retry}
|
|
81
|
+
retryLabel={retryLabel}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
{paged.loadMoreError && (
|
|
85
|
+
<ErrorState
|
|
86
|
+
variant="inline"
|
|
87
|
+
title={L.loadMoreFailed}
|
|
88
|
+
description={L.loadMoreFailedHint}
|
|
89
|
+
details={rawReason(paged.loadMoreError)}
|
|
90
|
+
onRetry={paged.loadMore}
|
|
91
|
+
retryLabel={retryLabel}
|
|
92
|
+
/>
|
|
93
|
+
)}
|
|
94
|
+
<ListFooter
|
|
95
|
+
shown={paged.items.length}
|
|
96
|
+
total={paged.total}
|
|
97
|
+
hasMore={paged.hasMore}
|
|
98
|
+
loadingMore={paged.loadingMore}
|
|
99
|
+
onLoadMore={paged.loadMore}
|
|
100
|
+
noun={noun}
|
|
101
|
+
refreshing={paged.refreshing}
|
|
102
|
+
stale={paged.stale}
|
|
103
|
+
labels={labels}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { attribute, openingTags, userFacingTexts } from "../render-scan.js"
|
|
4
|
+
import { TONE_BORDER, TONE_DOT, TONE_SOFT, TONE_VARIANTS } from "../lib/tone-utils.js"
|
|
5
|
+
import { CountPill, LivePill, StatusBadge } from "./Pills.js"
|
|
6
|
+
|
|
7
|
+
const classOf = (tag: string | undefined) => attribute(tag ?? "", "class") ?? ""
|
|
8
|
+
|
|
9
|
+
describe("StatusBadge: the state in words, marked in its tone", () => {
|
|
10
|
+
it.each(TONE_VARIANTS)("%s: tint, edge and a decorative dot next to foreground text", (tone) => {
|
|
11
|
+
const html = renderToStaticMarkup(<StatusBadge tone={tone}>Suspended</StatusBadge>)
|
|
12
|
+
const [badge, dot] = openingTags(html, "span")
|
|
13
|
+
expect(attribute(badge ?? "", "data-tone")).toBe(tone)
|
|
14
|
+
expect(classOf(badge)).toContain(TONE_SOFT[tone])
|
|
15
|
+
expect(classOf(badge)).toContain(TONE_BORDER[tone])
|
|
16
|
+
expect(classOf(badge)).toContain("text-small")
|
|
17
|
+
expect(attribute(dot ?? "", "aria-hidden")).toBe("true")
|
|
18
|
+
expect(classOf(dot)).toContain(TONE_DOT[tone])
|
|
19
|
+
// The words carry the state; no tone colour reaches them.
|
|
20
|
+
expect(html).not.toMatch(/\btext-(?:success|warning|danger|info)(?:-ink)?\b/)
|
|
21
|
+
expect(userFacingTexts(html).map((t) => t.value)).toEqual(["Suspended"])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("maps a retired tone name onto the vocabulary (critical → danger)", () => {
|
|
25
|
+
const html = renderToStaticMarkup(<StatusBadge tone="critical">Open</StatusBadge>)
|
|
26
|
+
expect(attribute(openingTags(html, "span")[0] ?? "", "data-tone")).toBe("danger")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("requires its text: colour is never the only signal", () => {
|
|
30
|
+
// @ts-expect-error — children are mandatory
|
|
31
|
+
const bare = <StatusBadge tone="danger" />
|
|
32
|
+
// @ts-expect-error — null is no text either
|
|
33
|
+
const nothing = <StatusBadge tone="danger">{null}</StatusBadge>
|
|
34
|
+
expect(bare.props).not.toHaveProperty("children")
|
|
35
|
+
expect(nothing.props).toHaveProperty("children", null)
|
|
36
|
+
const mixed = renderToStaticMarkup(
|
|
37
|
+
<StatusBadge tone="info">
|
|
38
|
+
{3} open {"incidents"}
|
|
39
|
+
</StatusBadge>,
|
|
40
|
+
)
|
|
41
|
+
expect(mixed.replace(/<!-- -->/g, "")).toContain("3 open incidents")
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe("LivePill and CountPill", () => {
|
|
46
|
+
it("LivePill pulses only without reduced motion and defaults to the locale's Live", () => {
|
|
47
|
+
const html = renderToStaticMarkup(<LivePill />)
|
|
48
|
+
const dot = openingTags(html, "span")[1] ?? ""
|
|
49
|
+
expect(classOf(dot)).toContain("motion-safe:animate-pulse")
|
|
50
|
+
expect(classOf(dot).split(/\s+/)).not.toContain("animate-pulse")
|
|
51
|
+
expect(html).toContain(">Live</span>")
|
|
52
|
+
expect(classOf(openingTags(html, "span")[0])).toContain(TONE_SOFT.info)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("CountPill keeps a neutral count in the foreground on the muted tint", () => {
|
|
56
|
+
const html = renderToStaticMarkup(<CountPill>7</CountPill>)
|
|
57
|
+
expect(classOf(openingTags(html, "span")[0])).toContain("bg-muted text-foreground")
|
|
58
|
+
expect(html).not.toContain("text-muted-foreground")
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("both take a className and resolve tone aliases", () => {
|
|
62
|
+
const live = renderToStaticMarkup(
|
|
63
|
+
<LivePill tone="ok" className="ml-2">
|
|
64
|
+
On
|
|
65
|
+
</LivePill>,
|
|
66
|
+
)
|
|
67
|
+
expect(classOf(openingTags(live, "span")[0])).toContain(TONE_SOFT.success)
|
|
68
|
+
expect(classOf(openingTags(live, "span")[0])).toContain("ml-2")
|
|
69
|
+
const count = renderToStaticMarkup(
|
|
70
|
+
<CountPill tone="error" className="ml-auto">
|
|
71
|
+
3
|
|
72
|
+
</CountPill>,
|
|
73
|
+
)
|
|
74
|
+
expect(classOf(openingTags(count, "span")[0])).toContain(TONE_BORDER.danger)
|
|
75
|
+
expect(classOf(openingTags(count, "span")[0])).toContain("ml-auto")
|
|
76
|
+
})
|
|
77
|
+
})
|
package/src/components/Pills.tsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import type { ReactNode } from "react"
|
|
1
|
+
import type { ReactElement, ReactNode } from "react"
|
|
2
2
|
import { useToolkitMessages } from "../locale/locale-provider.js"
|
|
3
|
-
import { TONE_BORDER, TONE_DOT, TONE_SOFT, type
|
|
3
|
+
import { resolveTone, TONE_BORDER, TONE_DOT, TONE_SOFT, type ToneName } from "../lib/tone-utils.js"
|
|
4
|
+
import { cn } from "../lib/utils.js"
|
|
4
5
|
|
|
5
6
|
export interface LivePillProps {
|
|
6
7
|
/** Status tone driving the dot, tint and border. Default `"info"`. */
|
|
7
|
-
tone?:
|
|
8
|
+
tone?: ToneName
|
|
8
9
|
/** Pill label. Defaults to the locale's "Live". */
|
|
9
10
|
children?: ReactNode
|
|
11
|
+
className?: string
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -15,38 +17,89 @@ export interface LivePillProps {
|
|
|
15
17
|
* text in the foreground), so it inherits the active theme. The pulse stops
|
|
16
18
|
* under `prefers-reduced-motion`.
|
|
17
19
|
*/
|
|
18
|
-
export function LivePill({ tone = "info", children }: LivePillProps) {
|
|
20
|
+
export function LivePill({ tone = "info", children, className }: LivePillProps) {
|
|
19
21
|
const messages = useToolkitMessages()
|
|
22
|
+
const t = resolveTone(tone, "info")
|
|
20
23
|
return (
|
|
21
24
|
<span
|
|
22
|
-
className={
|
|
25
|
+
className={cn(
|
|
26
|
+
"text-small inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 font-semibold",
|
|
27
|
+
TONE_SOFT[t],
|
|
28
|
+
TONE_BORDER[t],
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
23
31
|
>
|
|
24
32
|
<span
|
|
25
33
|
aria-hidden="true"
|
|
26
|
-
className={
|
|
34
|
+
className={cn("size-1.5 rounded-full motion-safe:animate-pulse", TONE_DOT[t])}
|
|
27
35
|
/>
|
|
28
36
|
{children ?? messages.pills.live}
|
|
29
37
|
</span>
|
|
30
38
|
)
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
/** The words a {@link StatusBadge} must carry: text, a number or an element — never nothing. */
|
|
42
|
+
export type StatusBadgeText = string | number | ReactElement | (string | number | ReactElement)[]
|
|
43
|
+
|
|
44
|
+
export interface StatusBadgeProps {
|
|
45
|
+
/** The state, in words ("Suspended", "3 open incidents"). Required: colour is never the only signal. */
|
|
46
|
+
children: StatusBadgeText
|
|
47
|
+
/** The state's tone: tint, edge and dot. */
|
|
48
|
+
tone: ToneName
|
|
49
|
+
className?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A state in words, marked in its tone: tint, edge and a static dot next to
|
|
54
|
+
* foreground text (CI §3.3: the colour marks the state, the words say it).
|
|
55
|
+
* For a detail-page header (`WidgetHeader`'s `badge`) or a table cell. The
|
|
56
|
+
* text is required — a badge without words would make colour the only
|
|
57
|
+
* signal. The pulsing dot is {@link LivePill}'s live-data semantic.
|
|
58
|
+
*/
|
|
59
|
+
export function StatusBadge({ children, tone, className }: StatusBadgeProps) {
|
|
60
|
+
const t = resolveTone(tone)
|
|
61
|
+
return (
|
|
62
|
+
<span
|
|
63
|
+
data-slot="status-badge"
|
|
64
|
+
data-tone={t}
|
|
65
|
+
className={cn(
|
|
66
|
+
"text-small inline-flex w-fit items-center gap-1.5 rounded-sm border px-2.5 py-0.5 font-semibold",
|
|
67
|
+
TONE_SOFT[t],
|
|
68
|
+
TONE_BORDER[t],
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
<span aria-hidden="true" className={cn("size-1.5 shrink-0 rounded-full", TONE_DOT[t])} />
|
|
73
|
+
{children}
|
|
74
|
+
</span>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
33
78
|
export interface CountPillProps {
|
|
34
79
|
/** Status tone driving the tint and border. Default `"neutral"`. */
|
|
35
|
-
tone?:
|
|
80
|
+
tone?: ToneName
|
|
36
81
|
/** The count/value to display (tabular numbers). */
|
|
37
82
|
children: ReactNode
|
|
83
|
+
className?: string
|
|
38
84
|
}
|
|
39
85
|
|
|
40
86
|
/**
|
|
41
87
|
* Compact count badge — tabular numbers on a tinted background with a border
|
|
42
88
|
* in the tone. Use as the right-aligned indicator on a list/group row (e.g. a
|
|
43
89
|
* per-group item count). The number stays in the foreground in every tone
|
|
44
|
-
* (no coloured digits): the tint and the border carry the tone.
|
|
90
|
+
* (no coloured digits): the tint and the border carry the tone. A count
|
|
91
|
+
* without a state stays `neutral`.
|
|
45
92
|
*/
|
|
46
|
-
export function CountPill({ tone = "neutral", children }: CountPillProps) {
|
|
93
|
+
export function CountPill({ tone = "neutral", children, className }: CountPillProps) {
|
|
94
|
+
const t = resolveTone(tone)
|
|
47
95
|
return (
|
|
48
96
|
<span
|
|
49
|
-
className={
|
|
97
|
+
className={cn(
|
|
98
|
+
"text-dense inline-flex min-w-10 items-center justify-center rounded-sm border px-2.5 py-0.5 font-semibold tabular-nums",
|
|
99
|
+
TONE_SOFT[t],
|
|
100
|
+
TONE_BORDER[t],
|
|
101
|
+
className,
|
|
102
|
+
)}
|
|
50
103
|
>
|
|
51
104
|
{children}
|
|
52
105
|
</span>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react"
|
|
2
|
+
import { MICRO_LABEL } from "../lib/typography.js"
|
|
3
|
+
import { cn } from "../lib/utils.js"
|
|
2
4
|
|
|
3
5
|
export interface SectionHeadingProps {
|
|
4
6
|
/** The section title. */
|
|
@@ -12,14 +14,18 @@ export interface SectionHeadingProps {
|
|
|
12
14
|
/**
|
|
13
15
|
* Section title row — a small semibold heading with an optional muted hint or
|
|
14
16
|
* control on the right. Use to label a block inside a widget body (e.g. a list
|
|
15
|
-
* or sub-table) without the visual weight of a full card header.
|
|
17
|
+
* or sub-table) without the visual weight of a full card header. The title is
|
|
18
|
+
* a caption ({@link MICRO_LABEL}: 13 px semibold, normal case); a section that
|
|
19
|
+
* needs a real heading takes `text-section` (21 px) instead.
|
|
16
20
|
*/
|
|
17
21
|
export function SectionHeading({ title, hint, trailing }: SectionHeadingProps) {
|
|
18
22
|
return (
|
|
19
|
-
<div
|
|
23
|
+
<div
|
|
24
|
+
className={cn("text-muted-foreground mb-3 flex items-center justify-between", MICRO_LABEL)}
|
|
25
|
+
>
|
|
20
26
|
<span>{title}</span>
|
|
21
27
|
{(hint || trailing) && (
|
|
22
|
-
<span className="text-muted-foreground text-
|
|
28
|
+
<span className="text-muted-foreground text-small font-medium">{trailing ?? hint}</span>
|
|
23
29
|
)}
|
|
24
30
|
</div>
|
|
25
31
|
)
|