@miragon/mcp-toolkit-ui 2.7.0 → 2.8.1
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 +14 -6
- package/dist/components/DrillButton.d.ts.map +1 -1
- package/dist/components/DrillButton.js +14 -7
- 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.test.tsx +48 -0
- package/src/components/DrillButton.tsx +26 -11
- 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
|
@@ -0,0 +1,82 @@
|
|
|
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 } from "../render-scan.js"
|
|
6
|
+
import { FOCUS_RING } from "../lib/focus-ring.js"
|
|
7
|
+
import { TONE_BORDER, TONE_SOFT } from "../lib/tone-utils.js"
|
|
8
|
+
import { FilterBar } from "./FilterBar.js"
|
|
9
|
+
|
|
10
|
+
const noop = (): void => {}
|
|
11
|
+
const classOf = (tag: string | undefined) => attribute(tag ?? "", "class") ?? ""
|
|
12
|
+
|
|
13
|
+
function render(props: Partial<Parameters<typeof FilterBar>[0]> = {}, locale = "en") {
|
|
14
|
+
return renderToStaticMarkup(
|
|
15
|
+
<LocaleProvider locale={locale}>
|
|
16
|
+
<FilterBar
|
|
17
|
+
search=""
|
|
18
|
+
onSearchChange={noop}
|
|
19
|
+
chips={[
|
|
20
|
+
{ id: "open", label: "Open", count: 3, active: true },
|
|
21
|
+
{ id: "done", label: "Done" },
|
|
22
|
+
]}
|
|
23
|
+
onChipToggle={noop}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
</LocaleProvider>,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("FilterBar: a labelled search and toggle chips", () => {
|
|
31
|
+
it("labels the search input in the locale (a placeholder is no label)", () => {
|
|
32
|
+
const [input] = openingTags(render(), "input")
|
|
33
|
+
expect(attribute(input ?? "", "type")).toBe("search")
|
|
34
|
+
expect(attribute(input ?? "", "aria-label")).toBe(TOOLKIT_MESSAGES.en.filterBar.searchLabel)
|
|
35
|
+
expect(attribute(input ?? "", "placeholder")).toBe("Filter…")
|
|
36
|
+
const [german] = openingTags(render({}, "de-DE"), "input")
|
|
37
|
+
expect(attribute(german ?? "", "aria-label")).toBe("Filtern")
|
|
38
|
+
expect(attribute(german ?? "", "placeholder")).toBe("Filtern…")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("takes the caller's search label and placeholder", () => {
|
|
42
|
+
const [input] = openingTags(
|
|
43
|
+
render({ searchAriaLabel: "Filter tasks", searchPlaceholder: "Filter tasks…" }),
|
|
44
|
+
"input",
|
|
45
|
+
)
|
|
46
|
+
expect(attribute(input ?? "", "aria-label")).toBe("Filter tasks")
|
|
47
|
+
expect(attribute(input ?? "", "placeholder")).toBe("Filter tasks…")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("groups the chips under a name and marks each one's state with aria-pressed", () => {
|
|
51
|
+
const html = render()
|
|
52
|
+
const group = openingTags(html, "div").find((t) => attribute(t, "role") === "group")
|
|
53
|
+
expect(attribute(group ?? "", "aria-label")).toBe("Filters")
|
|
54
|
+
expect(
|
|
55
|
+
attribute(openingTags(render({ chipsAriaLabel: "Priority" }), "div")[1] ?? "", "aria-label"),
|
|
56
|
+
).toBe("Priority")
|
|
57
|
+
const [open, done] = openingTags(html, "button")
|
|
58
|
+
expect(attribute(open ?? "", "aria-pressed")).toBe("true")
|
|
59
|
+
expect(attribute(done ?? "", "aria-pressed")).toBe("false")
|
|
60
|
+
for (const chip of [open, done]) {
|
|
61
|
+
expect(attribute(chip ?? "", "type")).toBe("button")
|
|
62
|
+
expect(classOf(chip)).toContain(FOCUS_RING)
|
|
63
|
+
expect(classOf(chip)).toContain("text-small")
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("shows the active chip as a selection: info tint and edge around foreground text", () => {
|
|
68
|
+
const [open, done] = openingTags(render(), "button")
|
|
69
|
+
expect(classOf(open)).toContain(TONE_SOFT.info)
|
|
70
|
+
expect(classOf(open)).toContain(TONE_BORDER.info)
|
|
71
|
+
expect(classOf(open)).not.toMatch(/text-info|text-accent-foreground/)
|
|
72
|
+
expect(classOf(done)).toContain("text-muted-foreground")
|
|
73
|
+
expect(classOf(done)).not.toContain("bg-info-soft")
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("renders no empty group without chips and merges a className", () => {
|
|
77
|
+
const html = render({ chips: [], className: "mb-4" })
|
|
78
|
+
expect(html).not.toContain('role="group"')
|
|
79
|
+
expect(classOf(openingTags(html, "div")[0])).toContain("mb-4")
|
|
80
|
+
expect(html).not.toMatch(/text-xs|text-\[\d+px\]/)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { FOCUS_RING } from "../lib/focus-ring.js"
|
|
2
|
+
import { TONE_BORDER, TONE_SOFT } from "../lib/tone-utils.js"
|
|
3
|
+
import { cn } from "../lib/utils.js"
|
|
2
4
|
import { useToolkitMessages } from "../locale/locale-provider.js"
|
|
3
5
|
import { Input } from "../primitives/input.js"
|
|
4
6
|
|
|
5
7
|
export interface FilterChip {
|
|
6
8
|
/** Stable id passed back to `onChipToggle`. */
|
|
7
9
|
id: string
|
|
8
|
-
/** Visible chip label. */
|
|
10
|
+
/** Visible chip label (its accessible name, with the count). */
|
|
9
11
|
label: string
|
|
10
12
|
/** Optional count rendered after the label. */
|
|
11
13
|
count?: number | string
|
|
12
|
-
/** Whether the chip is currently active. */
|
|
14
|
+
/** Whether the chip is currently active (`aria-pressed`). */
|
|
13
15
|
active?: boolean
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -18,54 +20,78 @@ export interface FilterBarProps {
|
|
|
18
20
|
search: string
|
|
19
21
|
/** Placeholder for the search input. Default: the locale's "Filter…". */
|
|
20
22
|
searchPlaceholder?: string
|
|
23
|
+
/**
|
|
24
|
+
* Accessible name of the search input. Default: the locale's "Filter" — a
|
|
25
|
+
* placeholder is no label (it disappears while you type).
|
|
26
|
+
*/
|
|
27
|
+
searchAriaLabel?: string
|
|
21
28
|
/** Called with the new search text on every keystroke. */
|
|
22
29
|
onSearchChange: (next: string) => void
|
|
23
30
|
/** Filter chips rendered to the right of the search input. */
|
|
24
31
|
chips: FilterChip[]
|
|
25
32
|
/** Called with a chip id when it is toggled. */
|
|
26
33
|
onChipToggle: (id: string) => void
|
|
34
|
+
/** Names the chip row (`role="group"`). Default: the locale's "Filters". */
|
|
35
|
+
chipsAriaLabel?: string
|
|
36
|
+
className?: string
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
/**
|
|
30
40
|
* Search input + toggleable chip row for filtering a list/table of widget
|
|
31
|
-
* items.
|
|
41
|
+
* items. The input is labelled; every chip is a toggle button whose state is
|
|
42
|
+
* `aria-pressed`. An active chip is a selection in the CI pattern: the info
|
|
43
|
+
* tint and edge around foreground text. Pair with `useDebouncedValue` to
|
|
44
|
+
* avoid re-querying on every keystroke.
|
|
32
45
|
*/
|
|
33
46
|
export function FilterBar({
|
|
34
47
|
search,
|
|
35
48
|
searchPlaceholder,
|
|
49
|
+
searchAriaLabel,
|
|
36
50
|
onSearchChange,
|
|
37
51
|
chips,
|
|
38
52
|
onChipToggle,
|
|
53
|
+
chipsAriaLabel,
|
|
54
|
+
className,
|
|
39
55
|
}: FilterBarProps) {
|
|
40
|
-
const
|
|
56
|
+
const { filterBar } = useToolkitMessages()
|
|
41
57
|
return (
|
|
42
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
58
|
+
<div data-slot="filter-bar" className={cn("flex flex-wrap items-center gap-2", className)}>
|
|
43
59
|
<Input
|
|
44
60
|
type="search"
|
|
45
61
|
value={search}
|
|
46
62
|
onChange={(e) => onSearchChange(e.target.value)}
|
|
47
|
-
placeholder={searchPlaceholder ??
|
|
48
|
-
|
|
63
|
+
placeholder={searchPlaceholder ?? filterBar.searchPlaceholder}
|
|
64
|
+
aria-label={searchAriaLabel ?? filterBar.searchLabel}
|
|
65
|
+
className="text-dense md:text-dense h-9 min-w-[220px] flex-1"
|
|
49
66
|
/>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
{chips.length > 0 && (
|
|
68
|
+
<div
|
|
69
|
+
role="group"
|
|
70
|
+
aria-label={chipsAriaLabel ?? filterBar.chipsLabel}
|
|
71
|
+
className="flex flex-wrap gap-1.5"
|
|
72
|
+
>
|
|
73
|
+
{chips.map((chip) => (
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
key={chip.id}
|
|
77
|
+
onClick={() => onChipToggle(chip.id)}
|
|
78
|
+
aria-pressed={Boolean(chip.active)}
|
|
79
|
+
className={cn(
|
|
80
|
+
"text-small inline-flex items-center gap-1.5 rounded-sm border px-2.5 py-1 font-medium transition-colors",
|
|
81
|
+
FOCUS_RING,
|
|
82
|
+
chip.active
|
|
83
|
+
? cn(TONE_SOFT.info, TONE_BORDER.info, "font-semibold")
|
|
84
|
+
: "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{chip.label}
|
|
88
|
+
{chip.count !== undefined && chip.count !== "" && (
|
|
89
|
+
<span className="font-normal tabular-nums">{chip.count}</span>
|
|
90
|
+
)}
|
|
91
|
+
</button>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
69
95
|
</div>
|
|
70
96
|
)
|
|
71
97
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
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_ICON } from "../lib/tone-utils.js"
|
|
5
|
+
import { KpiGrid, KpiGridSkeleton, kpiColumns } from "./KpiGrid.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The lifted (widget-shell) KpiGrid on its SSR markup: responsive columns,
|
|
9
|
+
* neutral figures with the state as a dot or a Lucide icon, and clickable
|
|
10
|
+
* cells whose accessible name is what you SEE (WCAG 2.5.3) while the action
|
|
11
|
+
* goes into the description.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const noop = (): void => {}
|
|
15
|
+
const classOf = (tag: string | undefined) => attribute(tag ?? "", "class") ?? ""
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The strip's clipping frame, its grid and its cells (the grid's direct
|
|
19
|
+
* children: plain boxes, or buttons when clickable), read off the markup.
|
|
20
|
+
*/
|
|
21
|
+
function strip(html: string) {
|
|
22
|
+
const start = html.indexOf('data-slot="kpi-strip"')
|
|
23
|
+
const tags = html.slice(start).match(/<(?:div|button)(?=[\s>])[^>]*>/g) ?? []
|
|
24
|
+
const [grid, ...rest] = tags
|
|
25
|
+
const frame = openingTags(html, "div").find((t) => /data-slot="kpi-strip"/.test(t))
|
|
26
|
+
return { frame, grid, cells: rest.filter((t) => /\bborder-r border-b\b/.test(t)) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const kpis = (count: number) =>
|
|
30
|
+
Array.from({ length: count }, (_, idx) => ({ label: `KPI ${idx + 1}`, value: idx }))
|
|
31
|
+
|
|
32
|
+
describe("kpiColumns: responsive columns, clamped to 1–6", () => {
|
|
33
|
+
it("falls back to 2 columns in narrow frames from 4 cells on", () => {
|
|
34
|
+
expect(kpiColumns(1)).toBe("grid-cols-1")
|
|
35
|
+
expect(kpiColumns(2)).toBe("grid-cols-2")
|
|
36
|
+
expect(kpiColumns(3)).toBe("grid-cols-3")
|
|
37
|
+
expect(kpiColumns(4)).toBe("grid-cols-2 sm:grid-cols-4")
|
|
38
|
+
expect(kpiColumns(5)).toBe("grid-cols-2 sm:grid-cols-5")
|
|
39
|
+
expect(kpiColumns(6)).toBe("grid-cols-2 sm:grid-cols-3 lg:grid-cols-6")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("clamps out-of-range counts instead of dropping the grid", () => {
|
|
43
|
+
expect(kpiColumns(0)).toBe("grid-cols-1")
|
|
44
|
+
expect(kpiColumns(-3)).toBe("grid-cols-1")
|
|
45
|
+
expect(kpiColumns(Number.NaN)).toBe("grid-cols-1")
|
|
46
|
+
expect(kpiColumns(9)).toBe(kpiColumns(6))
|
|
47
|
+
expect(kpiColumns(4.7)).toBe(kpiColumns(4))
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe("KpiGrid strip: neutral figures, the state beside them", () => {
|
|
52
|
+
const html = renderToStaticMarkup(
|
|
53
|
+
<KpiGrid
|
|
54
|
+
cells={[
|
|
55
|
+
{ label: "Open incidents", value: 18, tone: "danger" },
|
|
56
|
+
{ label: "Legacy", value: 2, tone: "critical" },
|
|
57
|
+
{ label: "Running", value: 40, tone: "neutral" },
|
|
58
|
+
{ label: "Plain", value: 7, fraction: "/9" },
|
|
59
|
+
]}
|
|
60
|
+
/>,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
it("draws the tone as a decorative dot, the alias 'critical' as danger", () => {
|
|
64
|
+
const dots = openingTags(html, "span").filter((t) => /data-tone=/.test(t))
|
|
65
|
+
expect(dots.map((t) => attribute(t, "data-tone"))).toEqual(["danger", "danger"])
|
|
66
|
+
for (const dot of dots) {
|
|
67
|
+
expect(attribute(dot, "aria-hidden")).toBe("true")
|
|
68
|
+
expect(classOf(dot)).toContain("bg-danger")
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("keeps every figure in the foreground at the page-title size", () => {
|
|
73
|
+
expect(html).toMatch(/class="text-title text-foreground[^"]*tabular-nums">18</)
|
|
74
|
+
expect(html).not.toMatch(/text-(?:danger|warning|success|info)(?:-ink)?\b/)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("flows 4 cells responsively with wrap-safe dividers", () => {
|
|
78
|
+
const { frame, grid, cells } = strip(html)
|
|
79
|
+
expect(classOf(grid)).toContain("grid-cols-2 sm:grid-cols-4")
|
|
80
|
+
expect(classOf(frame)).toContain("border-y")
|
|
81
|
+
expect(cells).toHaveLength(4)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("renders no text below the 13 px floor", () => {
|
|
85
|
+
expect(html).not.toMatch(/text-xs|text-\[\d+px\]|uppercase/)
|
|
86
|
+
expect(html).toContain("text-small")
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe("KpiGrid strip: an incomplete last row keeps the card colour", () => {
|
|
91
|
+
// 5 cells in 2 columns (a narrow inline frame) and 7 in 6 (lg) leave empty
|
|
92
|
+
// tracks. Whatever shows through them is the frame's background, so the
|
|
93
|
+
// dividers belong on the cells and the frame is the card.
|
|
94
|
+
it.each([5, 7])("%i cells: dividers on every cell, nothing paints in the divider colour", (n) => {
|
|
95
|
+
const html = renderToStaticMarkup(<KpiGrid cells={kpis(n)} />)
|
|
96
|
+
const { frame, grid, cells } = strip(html)
|
|
97
|
+
expect(classOf(grid)).toContain(kpiColumns(n))
|
|
98
|
+
expect(classOf(frame)).toContain("bg-card")
|
|
99
|
+
// The grid reaches 1 px past the clipping frame: the outer rules fall away.
|
|
100
|
+
expect(classOf(frame)).toContain("overflow-hidden")
|
|
101
|
+
expect(classOf(grid)).toMatch(/(^| )-mr-px -mb-px grid( |$)/)
|
|
102
|
+
expect(cells).toHaveLength(n)
|
|
103
|
+
for (const cell of cells) {
|
|
104
|
+
expect(classOf(cell)).toContain("border-border")
|
|
105
|
+
expect(classOf(cell)).not.toMatch(/(^| )bg-/)
|
|
106
|
+
}
|
|
107
|
+
expect(html).not.toMatch(/\bbg-border\b|\bgap-px\b/)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("clickable cells carry the same dividers under their hover surface", () => {
|
|
111
|
+
const html = renderToStaticMarkup(
|
|
112
|
+
<KpiGrid cells={kpis(5).map((cell) => ({ ...cell, onClick: noop }))} />,
|
|
113
|
+
)
|
|
114
|
+
const { cells } = strip(html)
|
|
115
|
+
expect(cells).toHaveLength(5)
|
|
116
|
+
for (const cell of cells) expect(classOf(cell)).toContain("hover:bg-muted")
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe("KpiGrid clickable cells (WCAG 2.5.3, label in name)", () => {
|
|
121
|
+
const html = renderToStaticMarkup(
|
|
122
|
+
<KpiGrid
|
|
123
|
+
cells={[
|
|
124
|
+
{
|
|
125
|
+
label: "Open incidents",
|
|
126
|
+
value: 18,
|
|
127
|
+
tone: "danger",
|
|
128
|
+
onClick: noop,
|
|
129
|
+
actionHint: "Opens the incidents dashboard",
|
|
130
|
+
},
|
|
131
|
+
{ label: "Running", value: 40, onClick: noop },
|
|
132
|
+
]}
|
|
133
|
+
/>,
|
|
134
|
+
)
|
|
135
|
+
const [first, second] = openingTags(html, "button")
|
|
136
|
+
|
|
137
|
+
it("names a clickable cell by its visible label and value, never an aria-label", () => {
|
|
138
|
+
expect(attribute(first ?? "", "aria-label")).toBeUndefined()
|
|
139
|
+
expect(attribute(second ?? "", "aria-label")).toBeUndefined()
|
|
140
|
+
const button = html.slice(html.indexOf("<button"), html.indexOf("</button>"))
|
|
141
|
+
const visible = userFacingTexts(button.replace(/<span id="[^"]*" hidden="">[^<]*<\/span>/, ""))
|
|
142
|
+
expect(visible.map((t) => t.value)).toEqual(["Open incidents", "18"])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("describes the action through aria-describedby on a hidden hint", () => {
|
|
146
|
+
const describedBy = attribute(first ?? "", "aria-describedby")
|
|
147
|
+
expect(describedBy).toBeTruthy()
|
|
148
|
+
const hint = openingTags(html, "span").find((t) => attribute(t, "id") === describedBy)
|
|
149
|
+
expect(hint).toBeDefined()
|
|
150
|
+
expect(attribute(hint ?? "", "hidden")).toBe("")
|
|
151
|
+
expect(html).toContain(`id="${describedBy}" hidden="">Opens the incidents dashboard</span>`)
|
|
152
|
+
expect(attribute(second ?? "", "aria-describedby")).toBeUndefined()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("is a native button with the kit's inset focus ring and a Lucide chevron, no glyph", () => {
|
|
156
|
+
expect(attribute(first ?? "", "type")).toBe("button")
|
|
157
|
+
expect(classOf(first)).toContain("focus-visible:ring-inset")
|
|
158
|
+
expect(classOf(first)).toContain("outline-hidden")
|
|
159
|
+
expect(html).toMatch(/<svg[^>]*class="lucide lucide-chevron-right[^"]*"/)
|
|
160
|
+
for (const svg of openingTags(html, "svg")) expect(attribute(svg, "aria-hidden")).toBe("true")
|
|
161
|
+
expect(html).not.toContain("›")
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it("still applies a legacy ariaLabel (deprecated) for existing callers", () => {
|
|
165
|
+
const legacy = renderToStaticMarkup(
|
|
166
|
+
<KpiGrid cells={[{ label: "Total", value: 19, onClick: noop, ariaLabel: "Show all" }]} />,
|
|
167
|
+
)
|
|
168
|
+
expect(attribute(openingTags(legacy, "button")[0] ?? "", "aria-label")).toBe("Show all")
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe("KpiGrid trend line: neutral words, the state as an icon or a dot", () => {
|
|
173
|
+
const html = renderToStaticMarkup(
|
|
174
|
+
<KpiGrid
|
|
175
|
+
cells={[
|
|
176
|
+
{ label: "Up", value: 1, trend: "+12%", trendDirection: "up" },
|
|
177
|
+
{ label: "Down", value: 2, trend: "-4%", trendDirection: "down" },
|
|
178
|
+
{ label: "Override", value: 3, trend: "-3", trendDirection: "down", trendTone: "neutral" },
|
|
179
|
+
{ label: "Dot", value: 4, trend: "+2", trendTone: "warning" },
|
|
180
|
+
{ label: "Flat", value: 5, trend: "±0", trendDirection: "flat" },
|
|
181
|
+
{ label: "Plain", value: 6, trend: "same" },
|
|
182
|
+
]}
|
|
183
|
+
/>,
|
|
184
|
+
)
|
|
185
|
+
const trends = html.split('data-slot="kpi-trend"').slice(1)
|
|
186
|
+
|
|
187
|
+
it("draws a Lucide direction icon in the direction's tone (up = worse)", () => {
|
|
188
|
+
expect(trends[0]).toMatch(/lucide-trending-up/)
|
|
189
|
+
expect(trends[0]).toContain(TONE_ICON.danger)
|
|
190
|
+
expect(trends[1]).toMatch(/lucide-trending-down/)
|
|
191
|
+
expect(trends[1]).toContain(TONE_ICON.success)
|
|
192
|
+
expect(trends[4]).toMatch(/lucide-minus/)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it("lets trendTone override the direction's tone, and draws a dot without a direction", () => {
|
|
196
|
+
expect(trends[2]).toMatch(/lucide-trending-down/)
|
|
197
|
+
expect(trends[2]).toContain(TONE_ICON.neutral)
|
|
198
|
+
expect(trends[3]).not.toContain("<svg")
|
|
199
|
+
expect(trends[3]).toMatch(/data-tone="warning"[^>]*class="[^"]*bg-warning/)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it("keeps the trend words in the foreground next to a state, quiet without one", () => {
|
|
203
|
+
expect(trends[0]).toMatch(/<span class="text-foreground">\+12%<\/span>/)
|
|
204
|
+
expect(trends[3]).toMatch(/<span class="text-foreground">\+2<\/span>/)
|
|
205
|
+
expect(trends[2]).toMatch(/<span class="text-muted-foreground">-3<\/span>/)
|
|
206
|
+
expect(trends[5]).toMatch(/<span class="text-muted-foreground">same<\/span>/)
|
|
207
|
+
expect(trends[5]).not.toMatch(/<svg|data-tone/)
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe("KpiGrid soft variant, group and header", () => {
|
|
212
|
+
it("soft: each cell a card in its tone's tint and edge around foreground text", () => {
|
|
213
|
+
const html = renderToStaticMarkup(
|
|
214
|
+
<KpiGrid
|
|
215
|
+
variant="soft"
|
|
216
|
+
cells={[
|
|
217
|
+
{ label: "Failed", value: 4, tone: "danger", onClick: noop, actionHint: "Open" },
|
|
218
|
+
{ label: "Total", value: 12 },
|
|
219
|
+
]}
|
|
220
|
+
/>,
|
|
221
|
+
)
|
|
222
|
+
const failed = openingTags(html, "button")[0] ?? ""
|
|
223
|
+
expect(classOf(failed)).toMatch(/bg-danger-soft/)
|
|
224
|
+
expect(classOf(failed)).toMatch(/border-danger/)
|
|
225
|
+
expect(classOf(failed)).toMatch(/text-foreground/)
|
|
226
|
+
expect(classOf(failed)).toContain("focus-visible:ring-offset-2")
|
|
227
|
+
const total = openingTags(html, "div").find((t) => /bg-muted/.test(t) && /rounded-lg/.test(t))
|
|
228
|
+
expect(classOf(total)).toContain("border-border")
|
|
229
|
+
expect(html).not.toMatch(/border-y/)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it("names the group when asked and renders the header label in normal case at 13 px", () => {
|
|
233
|
+
const html = renderToStaticMarkup(
|
|
234
|
+
<KpiGrid
|
|
235
|
+
boxed
|
|
236
|
+
ariaLabel="Health"
|
|
237
|
+
header={{ label: "Status", badge: "live" }}
|
|
238
|
+
cells={[{ label: "Up", value: 1 }]}
|
|
239
|
+
/>,
|
|
240
|
+
)
|
|
241
|
+
const root = openingTags(html, "div")[0] ?? ""
|
|
242
|
+
expect(attribute(root, "role")).toBe("group")
|
|
243
|
+
expect(attribute(root, "aria-label")).toBe("Health")
|
|
244
|
+
expect(classOf(root)).toContain("rounded-lg border")
|
|
245
|
+
expect(html).toMatch(/text-small font-semibold[^"]*"><span>Status<\/span>/)
|
|
246
|
+
expect(html).toContain(">live</span>")
|
|
247
|
+
const unnamed = renderToStaticMarkup(<KpiGrid cells={[{ label: "Up", value: 1 }]} />)
|
|
248
|
+
expect(attribute(openingTags(unnamed, "div")[0] ?? "", "role")).toBeUndefined()
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe("KpiGridSkeleton mirrors the grid's geometry", () => {
|
|
253
|
+
it("strip: busy, one placeholder cell per KPI, the same responsive columns", () => {
|
|
254
|
+
const html = renderToStaticMarkup(<KpiGridSkeleton cells={4} />)
|
|
255
|
+
expect(attribute(openingTags(html, "div")[0] ?? "", "aria-busy")).toBe("true")
|
|
256
|
+
expect(strip(html).cells).toHaveLength(4)
|
|
257
|
+
expect(html).toContain("grid-cols-2 sm:grid-cols-4")
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it("strip: an incomplete last row while loading has the loaded grid's frame, grid and cells", () => {
|
|
261
|
+
const loading = strip(renderToStaticMarkup(<KpiGridSkeleton cells={5} />))
|
|
262
|
+
const loaded = strip(renderToStaticMarkup(<KpiGrid cells={kpis(5)} />))
|
|
263
|
+
expect(classOf(loading.frame)).toBe(classOf(loaded.frame))
|
|
264
|
+
expect(classOf(loading.grid)).toBe(classOf(loaded.grid))
|
|
265
|
+
expect(loading.cells.map(classOf)).toEqual(loaded.cells.map(classOf))
|
|
266
|
+
// Only the placeholder bars take the border colour, never the surface behind a track.
|
|
267
|
+
for (const tag of [loading.frame, loading.grid, ...loading.cells]) {
|
|
268
|
+
expect(classOf(tag)).not.toMatch(/\bbg-border\b/)
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it("soft and boxed variants keep the loaded card shapes", () => {
|
|
273
|
+
const soft = renderToStaticMarkup(<KpiGridSkeleton cells={3} variant="soft" />)
|
|
274
|
+
expect(openingTags(soft, "div").filter((t) => /rounded-lg border p-4/.test(t))).toHaveLength(3)
|
|
275
|
+
const boxed = renderToStaticMarkup(<KpiGridSkeleton cells={2} boxed />)
|
|
276
|
+
expect(classOf(openingTags(boxed, "div")[0])).toContain("rounded-lg border")
|
|
277
|
+
expect(renderToStaticMarkup(<KpiGridSkeleton cells={-1} />)).not.toContain("px-5 py-4")
|
|
278
|
+
})
|
|
279
|
+
})
|