@miragon/mcp-toolkit-ui 2.5.0 → 2.7.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 +8 -2
- 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.map +1 -1
- package/dist/app/builder/CanvasRow.js +12 -14
- 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 +6 -5
- package/dist/app/builder/CatalogueSheet.js.map +1 -1
- 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.js +3 -3
- package/dist/app/builder/LayoutSurface.js.map +1 -1
- package/dist/app/builder/PipelineStrip.d.ts +3 -1
- package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
- package/dist/app/builder/PipelineStrip.js +11 -6
- package/dist/app/builder/PipelineStrip.js.map +1 -1
- package/dist/app/builder/SaveDialog.d.ts.map +1 -1
- package/dist/app/builder/SaveDialog.js +2 -2
- package/dist/app/builder/SaveDialog.js.map +1 -1
- package/dist/app/builder/Toolbar.d.ts +3 -1
- package/dist/app/builder/Toolbar.d.ts.map +1 -1
- package/dist/app/builder/Toolbar.js +9 -3
- package/dist/app/builder/Toolbar.js.map +1 -1
- package/dist/app/builder/Workspace.js +2 -2
- package/dist/app/builder/Workspace.js.map +1 -1
- package/dist/app/builder/builder-model.d.ts +3 -2
- package/dist/app/builder/builder-model.d.ts.map +1 -1
- package/dist/app/builder/builder-model.js +5 -4
- 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/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 +1 -0
- package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
- package/dist/app/builder/use-builder-catalogue.js +6 -0
- package/dist/app/builder/use-builder-catalogue.js.map +1 -1
- 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 +1 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js.map +1 -1
- package/dist/app/layout-builder.d.ts +7 -2
- package/dist/app/layout-builder.d.ts.map +1 -1
- package/dist/app/layout-builder.js +17 -5
- package/dist/app/layout-builder.js.map +1 -1
- package/dist/app/mcp-app-view-chrome.d.ts +94 -20
- package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
- package/dist/app/mcp-app-view-chrome.js +109 -40
- package/dist/app/mcp-app-view-chrome.js.map +1 -1
- package/dist/app/mcp-app-view-states.d.ts +15 -0
- package/dist/app/mcp-app-view-states.d.ts.map +1 -0
- package/dist/app/mcp-app-view-states.js +34 -0
- package/dist/app/mcp-app-view-states.js.map +1 -0
- package/dist/app/mcp-app-view.d.ts +40 -5
- package/dist/app/mcp-app-view.d.ts.map +1 -1
- package/dist/app/mcp-app-view.js +174 -128
- package/dist/app/mcp-app-view.js.map +1 -1
- package/dist/app/mcp-toolkit-app.d.ts +2 -1
- package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
- package/dist/app/mcp-toolkit-app.js +25 -3
- package/dist/app/mcp-toolkit-app.js.map +1 -1
- package/dist/app/use-view-lifecycle.d.ts +38 -0
- package/dist/app/use-view-lifecycle.d.ts.map +1 -0
- package/dist/app/use-view-lifecycle.js +79 -0
- package/dist/app/use-view-lifecycle.js.map +1 -0
- package/dist/app/view-lifecycle.d.ts +109 -0
- package/dist/app/view-lifecycle.d.ts.map +1 -0
- package/dist/app/view-lifecycle.js +120 -0
- package/dist/app/view-lifecycle.js.map +1 -0
- package/dist/app/widget-props-sheet.d.ts +3 -9
- package/dist/app/widget-props-sheet.d.ts.map +1 -1
- package/dist/app/widget-props-sheet.js +6 -13
- 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.map +1 -1
- package/dist/components/DrillButton.js +2 -1
- 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 +17 -0
- package/dist/components/EmptyState.js.map +1 -0
- package/dist/components/ErrorState.d.ts +50 -0
- package/dist/components/ErrorState.d.ts.map +1 -0
- package/dist/components/ErrorState.js +60 -0
- package/dist/components/ErrorState.js.map +1 -0
- package/dist/components/FilterBar.d.ts +1 -1
- package/dist/components/FilterBar.d.ts.map +1 -1
- package/dist/components/FilterBar.js +7 -4
- 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 +10 -3
- package/dist/components/KpiGrid.d.ts.map +1 -1
- package/dist/components/KpiGrid.js +31 -9
- package/dist/components/KpiGrid.js.map +1 -1
- package/dist/components/ListFooter.d.ts +3 -2
- package/dist/components/ListFooter.d.ts.map +1 -1
- package/dist/components/ListFooter.js +14 -4
- package/dist/components/ListFooter.js.map +1 -1
- package/dist/components/Pills.d.ts +10 -6
- package/dist/components/Pills.d.ts.map +1 -1
- package/dist/components/Pills.js +12 -6
- package/dist/components/Pills.js.map +1 -1
- package/dist/components/Toast.d.ts +37 -0
- package/dist/components/Toast.d.ts.map +1 -0
- package/dist/components/Toast.js +91 -0
- package/dist/components/Toast.js.map +1 -0
- package/dist/components/WidgetHeader.js +1 -1
- package/dist/hooks/use-tool-result-recovery.d.ts +23 -1
- package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
- package/dist/hooks/use-tool-result-recovery.js +21 -7
- package/dist/hooks/use-tool-result-recovery.js.map +1 -1
- package/dist/hooks/use-view-data.d.ts +6 -0
- package/dist/hooks/use-view-data.d.ts.map +1 -1
- package/dist/hooks/use-view-data.js +6 -1
- package/dist/hooks/use-view-data.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -4
- package/dist/index.js.map +1 -1
- 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/toast-model.d.ts +46 -0
- package/dist/lib/toast-model.d.ts.map +1 -0
- package/dist/lib/toast-model.js +36 -0
- package/dist/lib/toast-model.js.map +1 -0
- package/dist/lib/tone-utils.d.ts +43 -9
- package/dist/lib/tone-utils.d.ts.map +1 -1
- package/dist/lib/tone-utils.js +36 -7
- package/dist/lib/tone-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 +223 -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 +151 -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 +150 -0
- package/dist/locale/messages-en.js.map +1 -0
- package/dist/locale/toolkit-messages.d.ts +39 -0
- package/dist/locale/toolkit-messages.d.ts.map +1 -0
- package/dist/locale/toolkit-messages.js +50 -0
- package/dist/locale/toolkit-messages.js.map +1 -0
- package/dist/primitives/alert.d.ts.map +1 -1
- package/dist/primitives/alert.js +4 -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 +14 -6
- package/dist/primitives/badge.js.map +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 +14 -7
- 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 +5 -2
- 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 +12 -6
- 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/render-cases.d.ts.map +1 -1
- package/dist/render-cases.js +20 -2
- package/dist/render-cases.js.map +1 -1
- 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 +196 -39
- package/dist/theme/create-theme.d.ts.map +1 -1
- package/dist/theme/create-theme.js +189 -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 +13 -8
- package/src/app/builder/BuilderBanners.tsx +26 -13
- package/src/app/builder/CanvasRow.tsx +22 -21
- package/src/app/builder/CatalogueSheet.tsx +16 -14
- package/src/app/builder/EmptyHint.tsx +16 -11
- package/src/app/builder/LayoutSurface.tsx +7 -7
- package/src/app/builder/PipelineStrip.tsx +26 -17
- package/src/app/builder/SaveDialog.tsx +11 -12
- package/src/app/builder/Toolbar.tsx +19 -7
- package/src/app/builder/Workspace.tsx +5 -3
- 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 +5 -4
- package/src/app/builder/labels.ts +15 -121
- package/src/app/builder/status-chip.tsx +8 -4
- package/src/app/builder/use-builder-catalogue.ts +6 -0
- package/src/app/builder/use-save-dashboard.ts +5 -0
- package/src/app/index.ts +8 -1
- package/src/app/layout-builder.tsx +23 -3
- package/src/app/mcp-app-view-chrome.test.tsx +347 -14
- package/src/app/mcp-app-view-chrome.tsx +278 -100
- package/src/app/mcp-app-view-states.tsx +100 -0
- package/src/app/mcp-app-view.test.tsx +258 -0
- package/src/app/mcp-app-view.tsx +292 -185
- package/src/app/mcp-toolkit-app.tsx +31 -7
- package/src/app/use-view-lifecycle.ts +116 -0
- package/src/app/view-lifecycle.test.ts +227 -0
- package/src/app/view-lifecycle.ts +174 -0
- package/src/app/widget-props-sheet.tsx +13 -26
- package/src/app/widget-renderer.render.test.tsx +131 -0
- package/src/app/widget-renderer.tsx +136 -38
- package/src/components/DrillButton.tsx +2 -1
- package/src/components/EmptyState.tsx +81 -0
- package/src/components/ErrorState.tsx +182 -0
- package/src/components/FilterBar.tsx +9 -6
- package/src/components/GroupCard.tsx +3 -2
- package/src/components/KpiGrid.tsx +75 -44
- package/src/components/ListFooter.tsx +17 -6
- package/src/components/Pills.tsx +20 -11
- package/src/components/Toast.tsx +188 -0
- package/src/components/WidgetHeader.tsx +1 -1
- package/src/components/feedback-states.test.tsx +225 -0
- package/src/components/tone-figures.test.tsx +54 -0
- package/src/fonts.css +19 -0
- package/src/globals.css +9 -165
- package/src/hooks/use-tool-result-recovery.test.ts +25 -1
- package/src/hooks/use-tool-result-recovery.ts +34 -6
- package/src/hooks/use-view-data.test.tsx +60 -0
- package/src/hooks/use-view-data.ts +12 -1
- package/src/i18n-coverage.test.tsx +323 -0
- package/src/index.ts +57 -5
- 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/toast-model.test.ts +68 -0
- package/src/lib/toast-model.ts +63 -0
- package/src/lib/tone-utils.test.ts +47 -14
- package/src/lib/tone-utils.ts +61 -13
- package/src/locale/index.ts +13 -1
- package/src/locale/locale-provider.test.tsx +87 -0
- package/src/locale/locale-provider.tsx +90 -3
- package/src/locale/message-types.ts +221 -0
- package/src/locale/messages-de.ts +159 -0
- package/src/locale/messages-en.ts +157 -0
- package/src/locale/toolkit-messages.test.ts +216 -0
- package/src/locale/toolkit-messages.ts +82 -0
- package/src/no-hardcoded-copy.test.ts +432 -0
- package/src/primitives/alert.tsx +4 -2
- package/src/primitives/badge.tsx +14 -7
- package/src/primitives/button.tsx +13 -8
- package/src/primitives/card.tsx +4 -4
- package/src/primitives/dialog.tsx +32 -7
- package/src/primitives/input.tsx +5 -3
- package/src/primitives/scroll-area.tsx +1 -1
- package/src/primitives/select.tsx +6 -3
- package/src/primitives/sheet.tsx +26 -6
- package/src/primitives/skeleton.tsx +1 -1
- package/src/primitives/switch.tsx +4 -2
- package/src/primitives/tabs.tsx +9 -6
- package/src/render-cases.tsx +45 -1
- package/src/render-scan.ts +75 -0
- package/src/theme/cd-tokens.generated.css +48 -0
- package/src/theme/cd-tokens.test.ts +107 -0
- package/src/theme/cd-tokens.ts +67 -0
- package/src/theme/create-theme.test.ts +243 -5
- package/src/theme/create-theme.ts +313 -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 +568 -0
- package/src/theme/theme-provider.test.tsx +82 -0
- package/src/theme/theme-provider.tsx +60 -24
- package/src/theme.css +314 -0
- package/ui-catalog.json +344 -17
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { isValidElement, type ReactElement } from "react"
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
3
|
+
import { describe, expect, it } from "vitest"
|
|
4
|
+
import type { WidgetProps } from "@miragon/mcp-toolkit-core"
|
|
5
|
+
import { EmptyState } from "./components/EmptyState.js"
|
|
6
|
+
import { ErrorState } from "./components/ErrorState.js"
|
|
7
|
+
import { ToastViewport } from "./components/Toast.js"
|
|
8
|
+
import { LocaleProvider } from "./locale/locale-provider.js"
|
|
9
|
+
import { TOOLKIT_MESSAGES, type ToolkitMessages } from "./locale/toolkit-messages.js"
|
|
10
|
+
import { createToast } from "./lib/toast-model.js"
|
|
11
|
+
import { RENDER_CASES } from "./render-cases.js"
|
|
12
|
+
import { userFacingTexts } from "./render-scan.js"
|
|
13
|
+
import { StepErrors, ViewToolbar, type ViewData } from "./app/mcp-app-view-chrome.js"
|
|
14
|
+
import { ViewPlaceholder } from "./app/mcp-app-view-states.js"
|
|
15
|
+
import { LayoutBuilder } from "./app/layout-builder.js"
|
|
16
|
+
import { BuilderBanners } from "./app/builder/BuilderBanners.js"
|
|
17
|
+
import { PipelineStrip } from "./app/builder/PipelineStrip.js"
|
|
18
|
+
import { StatusChip } from "./app/builder/status-chip.js"
|
|
19
|
+
import { Toolbar } from "./app/builder/Toolbar.js"
|
|
20
|
+
import { WidgetCrashFallback, WidgetRenderer } from "./app/widget-renderer.js"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render half of the i18n guard (the static half is `no-hardcoded-copy.test.ts`):
|
|
24
|
+
* render every component under the German locale with a MARKED catalog —
|
|
25
|
+
* each catalog string wrapped in ⟦…⟧ — and scan every text node and text
|
|
26
|
+
* attribute. Whatever is neither marked (it came from the catalog) nor a
|
|
27
|
+
* fixture value the case passed in (it came from props) is a hard-coded
|
|
28
|
+
* string that a German user would read in English.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const OPEN = "⟦"
|
|
32
|
+
const CLOSE = "⟧"
|
|
33
|
+
|
|
34
|
+
function marked(messages: ToolkitMessages): ToolkitMessages {
|
|
35
|
+
const mark = (value: unknown): unknown =>
|
|
36
|
+
typeof value === "function"
|
|
37
|
+
? (...args: unknown[]) => `${OPEN}${(value as (...a: unknown[]) => string)(...args)}${CLOSE}`
|
|
38
|
+
: `${OPEN}${String(value)}${CLOSE}`
|
|
39
|
+
return Object.fromEntries(
|
|
40
|
+
Object.entries(messages).map(([section, entries]) => [
|
|
41
|
+
section,
|
|
42
|
+
Object.fromEntries(
|
|
43
|
+
Object.entries(entries as Record<string, unknown>).map(([k, v]) => [k, mark(v)]),
|
|
44
|
+
),
|
|
45
|
+
]),
|
|
46
|
+
) as unknown as ToolkitMessages
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const PSEUDO = marked(TOOLKIT_MESSAGES.de)
|
|
50
|
+
|
|
51
|
+
/** Every string/number a case hands to the tree through props (its fixtures). */
|
|
52
|
+
function fixtureStrings(node: unknown, out = new Set<string>(), seen = new Set<unknown>()) {
|
|
53
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
54
|
+
const text = String(node).trim()
|
|
55
|
+
if (text.length >= 2) out.add(text)
|
|
56
|
+
return out
|
|
57
|
+
}
|
|
58
|
+
if (!node || typeof node !== "object" || seen.has(node)) return out
|
|
59
|
+
seen.add(node)
|
|
60
|
+
if (node instanceof Error) return fixtureStrings(node.message, out, seen)
|
|
61
|
+
if (isValidElement(node)) return fixtureStrings(node.props, out, seen)
|
|
62
|
+
for (const value of Array.isArray(node) ? node : Object.values(node)) {
|
|
63
|
+
fixtureStrings(value, out, seen)
|
|
64
|
+
}
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Text a German user would read that came neither from the catalog nor from props. */
|
|
69
|
+
function leaks(element: ReactElement): string[] {
|
|
70
|
+
const html = renderToStaticMarkup(
|
|
71
|
+
<LocaleProvider locale="de" toolkitMessages={PSEUDO}>
|
|
72
|
+
{element}
|
|
73
|
+
</LocaleProvider>,
|
|
74
|
+
)
|
|
75
|
+
const fixtures = [...fixtureStrings(element)].sort((a, b) => b.length - a.length)
|
|
76
|
+
const found: string[] = []
|
|
77
|
+
for (const { source, value } of userFacingTexts(html)) {
|
|
78
|
+
let rest = value
|
|
79
|
+
const innermost = /⟦[^⟦⟧]*⟧/g
|
|
80
|
+
while (innermost.test(rest)) rest = rest.replace(innermost, " ")
|
|
81
|
+
rest = rest.replace(/RC:[\w:-]+/g, " ")
|
|
82
|
+
for (const fixture of fixtures) rest = rest.split(fixture).join(" ")
|
|
83
|
+
if (/\p{L}/u.test(rest)) found.push(`${source}: "${value}" → "${rest.trim()}"`)
|
|
84
|
+
}
|
|
85
|
+
return found
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const noop = (): void => {}
|
|
89
|
+
const asyncNoop = (): Promise<void> => Promise.resolve()
|
|
90
|
+
|
|
91
|
+
const view: ViewData = {
|
|
92
|
+
title: "Prozessdefinitionen",
|
|
93
|
+
context: { keys: {}, stepIds: [], stepData: {}, errors: [] },
|
|
94
|
+
layout: { rows: [] },
|
|
95
|
+
_refreshParams: { layout: { rows: [] } },
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function Crash(): never {
|
|
99
|
+
throw new Error("kaputt")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function Fine({ keys }: WidgetProps) {
|
|
103
|
+
return <span>{typeof keys.label === "string" ? keys.label : ""}</span>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The toolkit chrome no catalogued render case reaches. */
|
|
107
|
+
const CHROME_CASES: { name: string; render: () => ReactElement }[] = [
|
|
108
|
+
{
|
|
109
|
+
name: "ViewPlaceholder waiting + slow hint",
|
|
110
|
+
render: () => (
|
|
111
|
+
<ViewPlaceholder
|
|
112
|
+
lifecycle={{ phase: "waiting", slow: true }}
|
|
113
|
+
labels={PSEUDO.view}
|
|
114
|
+
onRetry={noop}
|
|
115
|
+
/>
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "ViewPlaceholder error",
|
|
120
|
+
render: () => (
|
|
121
|
+
<ViewPlaceholder
|
|
122
|
+
lifecycle={{ phase: "error", reason: "[503] engine down", canRetry: true }}
|
|
123
|
+
labels={PSEUDO.view}
|
|
124
|
+
onRetry={noop}
|
|
125
|
+
/>
|
|
126
|
+
),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "ViewPlaceholder error without a retry",
|
|
130
|
+
render: () => (
|
|
131
|
+
<ViewPlaceholder
|
|
132
|
+
lifecycle={{ phase: "error", reason: undefined, canRetry: false }}
|
|
133
|
+
labels={PSEUDO.view}
|
|
134
|
+
onRetry={noop}
|
|
135
|
+
/>
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "StepErrors without a refresh",
|
|
140
|
+
render: () => (
|
|
141
|
+
<StepErrors
|
|
142
|
+
hidden={false}
|
|
143
|
+
errors={[{ stepId: "inc", reason: "[503] engine down" }]}
|
|
144
|
+
labels={PSEUDO.view}
|
|
145
|
+
/>
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "ViewPlaceholder cancelled",
|
|
150
|
+
render: () => (
|
|
151
|
+
<ViewPlaceholder lifecycle={{ phase: "cancelled" }} labels={PSEUDO.view} onRetry={noop} />
|
|
152
|
+
),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "ViewToolbar",
|
|
156
|
+
render: () => (
|
|
157
|
+
<ViewToolbar
|
|
158
|
+
hidden={false}
|
|
159
|
+
viewData={view}
|
|
160
|
+
builderAvailable
|
|
161
|
+
isFullscreen={false}
|
|
162
|
+
canFullscreen
|
|
163
|
+
isRefreshing={false}
|
|
164
|
+
labels={PSEUDO.view}
|
|
165
|
+
titleMode="show"
|
|
166
|
+
titleLevel={2}
|
|
167
|
+
onRefresh={asyncNoop}
|
|
168
|
+
onEnterBuildMode={noop}
|
|
169
|
+
onToggleFullscreen={asyncNoop}
|
|
170
|
+
/>
|
|
171
|
+
),
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "StepErrors",
|
|
175
|
+
render: () => (
|
|
176
|
+
<StepErrors
|
|
177
|
+
hidden={false}
|
|
178
|
+
errors={[{ stepId: "inc", reason: "[503] engine down" }]}
|
|
179
|
+
steps={[{ id: "inc", step: "camunda7:incidents" }]}
|
|
180
|
+
labels={PSEUDO.view}
|
|
181
|
+
onRetry={noop}
|
|
182
|
+
/>
|
|
183
|
+
),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "WidgetRenderer: unknown widget + crashing widget",
|
|
187
|
+
render: () => (
|
|
188
|
+
<WidgetRenderer
|
|
189
|
+
layout={{ rows: [{ row: [{ widget: "demo:gone" }, { widget: "demo:fine" }] }] }}
|
|
190
|
+
keys={{ label: "Rechnung 42" }}
|
|
191
|
+
errors={[]}
|
|
192
|
+
widgets={{ "demo:fine": Fine }}
|
|
193
|
+
/>
|
|
194
|
+
),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "WidgetCrashFallback",
|
|
198
|
+
render: () => (
|
|
199
|
+
<WidgetCrashFallback widgetId="demo:crash" error={new Error("kaputt")} onRetry={noop} />
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "ToastViewport",
|
|
204
|
+
render: () => (
|
|
205
|
+
<ToastViewport
|
|
206
|
+
toasts={[createToast(1, { tone: "success", title: "Job neu eingeplant." })]}
|
|
207
|
+
onDismiss={noop}
|
|
208
|
+
/>
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "ErrorState defaults",
|
|
213
|
+
render: () => <ErrorState title="Fehler" details="raw" onRetry={noop} />,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "EmptyState",
|
|
217
|
+
render: () => <EmptyState title="Leer" description="Nichts zu tun." />,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "LayoutBuilder (empty: data-sources hint)",
|
|
221
|
+
render: () => <LayoutBuilder widgets={{}} callTool={() => Promise.resolve({})} />,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "LayoutBuilder (tabs, cells, unknown widget)",
|
|
225
|
+
render: () => (
|
|
226
|
+
<LayoutBuilder
|
|
227
|
+
initialLayout={{
|
|
228
|
+
tabs: [
|
|
229
|
+
{ label: "Übersicht", rows: [{ row: [{ widget: "demo:gone" }] }, { row: [] }] },
|
|
230
|
+
{ label: "Details-Tab", rows: [] },
|
|
231
|
+
],
|
|
232
|
+
}}
|
|
233
|
+
widgets={{ "demo:fine": Crash }}
|
|
234
|
+
callTool={() => Promise.resolve({})}
|
|
235
|
+
/>
|
|
236
|
+
),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "Builder toolbar",
|
|
240
|
+
render: () => (
|
|
241
|
+
<Toolbar
|
|
242
|
+
L={PSEUDO.builder}
|
|
243
|
+
previewMode={false}
|
|
244
|
+
isBusy={false}
|
|
245
|
+
status="pending"
|
|
246
|
+
liveKeys={2}
|
|
247
|
+
reachableCount={5}
|
|
248
|
+
unreachableCount={1}
|
|
249
|
+
onOpenCatalogue={noop}
|
|
250
|
+
onOpenSave={noop}
|
|
251
|
+
onDone={noop}
|
|
252
|
+
hasAnyCell
|
|
253
|
+
/>
|
|
254
|
+
),
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "Builder data sources",
|
|
258
|
+
render: () => (
|
|
259
|
+
<PipelineStrip
|
|
260
|
+
L={PSEUDO.builder}
|
|
261
|
+
keyEntries={[
|
|
262
|
+
{ name: "demo:id", rawValue: "42" },
|
|
263
|
+
{ name: "demo:live", rawValue: "" },
|
|
264
|
+
]}
|
|
265
|
+
stepEntries={[{ id: "s1", step: "demo:load" }]}
|
|
266
|
+
keyCatalog={[]}
|
|
267
|
+
availableSteps={[
|
|
268
|
+
{ id: "demo:load", app: "demo", dataType: "demo:t", requires: [], produces: [] },
|
|
269
|
+
]}
|
|
270
|
+
stepsByApp={[]}
|
|
271
|
+
liveKeys={{ "demo:live": 7 }}
|
|
272
|
+
onAddKey={noop}
|
|
273
|
+
onUpdateKey={noop}
|
|
274
|
+
onRemoveKey={noop}
|
|
275
|
+
onAddStep={noop}
|
|
276
|
+
onUpdateStep={noop}
|
|
277
|
+
onRemoveStep={noop}
|
|
278
|
+
status="refreshing"
|
|
279
|
+
/>
|
|
280
|
+
),
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "Builder banners + status chips",
|
|
284
|
+
render: () => (
|
|
285
|
+
<div>
|
|
286
|
+
<BuilderBanners
|
|
287
|
+
L={PSEUDO.builder}
|
|
288
|
+
catalogueToolName="get-builder-catalogue"
|
|
289
|
+
refreshError="timeout"
|
|
290
|
+
validationIssues={["demo:id fehlt"]}
|
|
291
|
+
/>
|
|
292
|
+
<StatusChip status="idle" L={PSEUDO.builder} />
|
|
293
|
+
<StatusChip status="pending" L={PSEUDO.builder} />
|
|
294
|
+
<StatusChip status="refreshing" L={PSEUDO.builder} />
|
|
295
|
+
</div>
|
|
296
|
+
),
|
|
297
|
+
},
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
describe("no English leaks into a German view", () => {
|
|
301
|
+
it("the marked catalog marks every string (the scan can see catalog text)", () => {
|
|
302
|
+
expect(PSEUDO.view.loading).toBe(`⟦${TOOLKIT_MESSAGES.de.view.loading}⟧`)
|
|
303
|
+
expect(PSEUDO.builder.saved("x")).toBe(`⟦${TOOLKIT_MESSAGES.de.builder.saved("x")}⟧`)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
for (const renderCase of [...RENDER_CASES, ...CHROME_CASES]) {
|
|
307
|
+
it(`${renderCase.name} renders only catalog text and props under de`, () => {
|
|
308
|
+
expect(leaks(renderCase.render())).toEqual([])
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
it("catches a hard-coded string (gate self-test)", () => {
|
|
313
|
+
const Leaky = ({ name }: { name: string }) => (
|
|
314
|
+
<button type="button" aria-label="Close">
|
|
315
|
+
{name} Load more
|
|
316
|
+
</button>
|
|
317
|
+
)
|
|
318
|
+
expect(leaks(<Leaky name="Rechnung" />)).toEqual([
|
|
319
|
+
'text: "Rechnung Load more" → "Load more"',
|
|
320
|
+
'aria-label: "Close" → "Close"',
|
|
321
|
+
])
|
|
322
|
+
})
|
|
323
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -71,9 +71,13 @@ export type { UseToolQueryOptions } from "./hooks/use-tool-query.js"
|
|
|
71
71
|
export { useDebouncedValue } from "./hooks/use-debounced-value.js"
|
|
72
72
|
|
|
73
73
|
// Tone token system — the semantic status tones (ToneVariant + TONE_* maps)
|
|
74
|
-
// behind the tone-aware components below. Pure data + class strings.
|
|
75
|
-
|
|
74
|
+
// behind the tone-aware components below. Pure data + class strings. Three
|
|
75
|
+
// roles per tone: colour (TONE_DOT/TONE_BORDER/TONE_ICON), tint (TONE_SOFT,
|
|
76
|
+
// foreground text on it), text on a plain surface (TONE_TEXT, the -ink role).
|
|
77
|
+
export { TONE_SOFT, TONE_DOT, TONE_TEXT, TONE_BORDER, TONE_ICON } from "./lib/tone-utils.js"
|
|
76
78
|
export type { ToneVariant } from "./lib/tone-utils.js"
|
|
79
|
+
// The kit's opaque focus ring as class strings, for custom controls.
|
|
80
|
+
export { FOCUS_RING, FOCUS_RING_INSET } from "./lib/focus-ring.js"
|
|
77
81
|
|
|
78
82
|
// Composed components — focused widget building blocks assembled from the
|
|
79
83
|
// primitives. Pure (React + primitives + cn), so they live in the root barrel.
|
|
@@ -96,6 +100,14 @@ export type { DrillButtonProps } from "./components/DrillButton.js"
|
|
|
96
100
|
export { ListFooter } from "./components/ListFooter.js"
|
|
97
101
|
export type { ListFooterProps } from "./components/ListFooter.js"
|
|
98
102
|
|
|
103
|
+
// Feedback and states — one pattern each for toasts, empty and error states.
|
|
104
|
+
export { ToastProvider, useToast } from "./components/Toast.js"
|
|
105
|
+
export type { ToastApi, ToastOptions, ToastTone } from "./components/Toast.js"
|
|
106
|
+
export { EmptyState } from "./components/EmptyState.js"
|
|
107
|
+
export type { EmptyStateProps, StateHeadingLevel } from "./components/EmptyState.js"
|
|
108
|
+
export { ErrorState } from "./components/ErrorState.js"
|
|
109
|
+
export type { ErrorStateProps, ErrorStateTone } from "./components/ErrorState.js"
|
|
110
|
+
|
|
99
111
|
// Providers
|
|
100
112
|
export { AppQueryProvider, useCallTool, queryClient } from "./providers/query-provider.js"
|
|
101
113
|
|
|
@@ -103,7 +115,23 @@ export { AppQueryProvider, useCallTool, queryClient } from "./providers/query-pr
|
|
|
103
115
|
// `mcp-use/react`), so it belongs in this root barrel: a consumer can re-skin
|
|
104
116
|
// the toolkit's primitives from a single switch without pulling in the host
|
|
105
117
|
// runtime. See packages/ui/src/theme/ and docs/guides/white-labeling.md.
|
|
106
|
-
|
|
118
|
+
// The defaults are the Miragon theme (theme.css over the vendored --cd-*
|
|
119
|
+
// corporate-design tokens); a brand overrides the ROLE_TOKENS only.
|
|
120
|
+
export {
|
|
121
|
+
createTheme,
|
|
122
|
+
resolveThemeVars,
|
|
123
|
+
ROLE_TOKENS,
|
|
124
|
+
ThemeProvider,
|
|
125
|
+
useTheme,
|
|
126
|
+
usePortalContainer,
|
|
127
|
+
themePresets,
|
|
128
|
+
CD_TOKENS,
|
|
129
|
+
CD_TOKENS_VERSION,
|
|
130
|
+
fontFaces,
|
|
131
|
+
GEIST_FONT_FILES,
|
|
132
|
+
adoptHostFontVariables,
|
|
133
|
+
HOST_FONT_VARIABLES,
|
|
134
|
+
} from "./theme/index.js"
|
|
107
135
|
export type {
|
|
108
136
|
ThemeTokens,
|
|
109
137
|
ThemeDefinition,
|
|
@@ -113,6 +141,9 @@ export type {
|
|
|
113
141
|
ThemeContextValue,
|
|
114
142
|
ThemeProviderProps,
|
|
115
143
|
ThemePresetId,
|
|
144
|
+
CdTokenName,
|
|
145
|
+
FontFacesOptions,
|
|
146
|
+
InlineStyleTarget,
|
|
116
147
|
} from "./theme/index.js"
|
|
117
148
|
|
|
118
149
|
// i18n / localization. Browser-safe (React only, no `mcp-use/react`), so it
|
|
@@ -120,8 +151,29 @@ export type {
|
|
|
120
151
|
// <LocaleProvider locale=… translator=…> and reads the active locale with
|
|
121
152
|
// useLocale/useTranslate. Pairs with `createTranslator` from
|
|
122
153
|
// `@miragon/mcp-toolkit-core`. See packages/ui/src/locale/.
|
|
123
|
-
export {
|
|
124
|
-
|
|
154
|
+
export {
|
|
155
|
+
LocaleProvider,
|
|
156
|
+
useLocale,
|
|
157
|
+
useTranslate,
|
|
158
|
+
useToolkitMessages,
|
|
159
|
+
TOOLKIT_MESSAGES,
|
|
160
|
+
formatCount,
|
|
161
|
+
resolveToolkitMessages,
|
|
162
|
+
} from "./locale/index.js"
|
|
163
|
+
export type {
|
|
164
|
+
LocaleContextValue,
|
|
165
|
+
LocaleProviderProps,
|
|
166
|
+
BoundTranslate,
|
|
167
|
+
BuilderMessages,
|
|
168
|
+
CommonMessages,
|
|
169
|
+
ListFooterMessages,
|
|
170
|
+
PropsSheetMessages,
|
|
171
|
+
RendererMessages,
|
|
172
|
+
ToolkitLocale,
|
|
173
|
+
ToolkitMessages,
|
|
174
|
+
ToolkitMessagesOverride,
|
|
175
|
+
ViewMessages,
|
|
176
|
+
} from "./locale/index.js"
|
|
125
177
|
|
|
126
178
|
// NOTE: MCP App shell (McpAppView, WidgetRenderer) is exported from the
|
|
127
179
|
// `./app` subpath only. Keeping it out of the main barrel prevents consumers
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The toolkit's focus indicator as Tailwind classes: an opaque 2px ring in the
|
|
3
|
+
* `--focus` role (blau-link on light, blau-hell on dark — 3:1 or more against
|
|
4
|
+
* every surface, see `theme/theme-css.test.ts`), offset from the control so it
|
|
5
|
+
* reads on any fill. Every interactive kit control uses it; spread it into a
|
|
6
|
+
* custom control's `className` so keyboard focus looks the same everywhere.
|
|
7
|
+
* (`theme.css` also gives every element without its own ring an opaque
|
|
8
|
+
* `:focus-visible` outline in the same colour.)
|
|
9
|
+
*
|
|
10
|
+
* The ring is a box-shadow, which forced-colors mode (Windows High Contrast)
|
|
11
|
+
* drops. `outline-hidden` — never `outline-none` — hides the outline only
|
|
12
|
+
* outside that mode and keeps a transparent 2px outline in it, which the
|
|
13
|
+
* browser paints in the system colour, so focus stays visible there too.
|
|
14
|
+
*/
|
|
15
|
+
export const FOCUS_RING =
|
|
16
|
+
"outline-hidden focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
17
|
+
|
|
18
|
+
/** {@link FOCUS_RING} drawn inside the element — for cells and rows flush with a container edge. */
|
|
19
|
+
export const FOCUS_RING_INSET =
|
|
20
|
+
"outline-hidden focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-inset"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { DENSE_ICON_STROKE, ICON_STROKE } from "./icon-stroke.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ONE mechanism for the Lucide stroke (CI §11): `LucideProvider` with the two
|
|
6
|
+
* constants of `icon-stroke.ts` — 2.5 for the view (`McpAppView`), 2 in dense
|
|
7
|
+
* chrome bars. No CSS rule (`svg.lucide { stroke-width }`) in the theme, and no
|
|
8
|
+
* per-icon `strokeWidth` in kit code, so the two can never disagree.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
interface ImportMeta {
|
|
13
|
+
glob(
|
|
14
|
+
pattern: string | string[],
|
|
15
|
+
options: { query: "?raw"; import: "default"; eager: true },
|
|
16
|
+
): Record<string, string>
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const STYLESHEETS = import.meta.glob(["../*.css", "../theme/*.css"], {
|
|
21
|
+
query: "?raw",
|
|
22
|
+
import: "default",
|
|
23
|
+
eager: true,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const SOURCES = import.meta.glob(["../**/*.tsx", "!../**/*.test.tsx", "!../render-cases*.tsx"], {
|
|
27
|
+
query: "?raw",
|
|
28
|
+
import: "default",
|
|
29
|
+
eager: true,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("Lucide stroke: one mechanism", () => {
|
|
33
|
+
it("is 2.5 for the view and 2 in dense bars (CI §11)", () => {
|
|
34
|
+
expect(ICON_STROKE).toBe(2.5)
|
|
35
|
+
expect(DENSE_ICON_STROKE).toBe(2)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("is never set in CSS (theme.css, globals.css, fonts.css, the vendored tokens)", () => {
|
|
39
|
+
expect(Object.keys(STYLESHEETS)).toEqual(
|
|
40
|
+
expect.arrayContaining(["../theme.css", "../globals.css"]),
|
|
41
|
+
)
|
|
42
|
+
for (const [file, css] of Object.entries(STYLESHEETS)) {
|
|
43
|
+
expect(css, file).not.toMatch(/stroke-width/)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("is set only on a LucideProvider, from the icon-stroke constants", () => {
|
|
48
|
+
const uses: string[] = []
|
|
49
|
+
for (const [file, source] of Object.entries(SOURCES)) {
|
|
50
|
+
for (const m of source.matchAll(/<(\w+)[^<>]*\bstrokeWidth=\{?([^}\s>]+)\}?/g)) {
|
|
51
|
+
uses.push(`${file}: <${m[1]} strokeWidth=${m[2]}>`)
|
|
52
|
+
expect(m[1], file).toBe("LucideProvider")
|
|
53
|
+
expect(["ICON_STROKE", "DENSE_ICON_STROKE"], file).toContain(m[2])
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// The view and the three dense bars (view toolbar, builder toolbar, pipeline strip).
|
|
57
|
+
expect(uses.length).toBeGreaterThanOrEqual(4)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lucide stroke widths of the toolkit's chrome (CI §11, on the 24 px grid).
|
|
3
|
+
* Set once through `LucideProvider`: `McpAppView` applies {@link ICON_STROKE}
|
|
4
|
+
* to everything it renders (its toast stack included), dense bars narrow it
|
|
5
|
+
* to {@link DENSE_ICON_STROKE}. Icons of a consumer's own `lucide-react` follow
|
|
6
|
+
* the provider only when they resolve to the same module instance (dedupe it).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Default stroke of the toolkit's icons. */
|
|
10
|
+
export const ICON_STROKE = 2.5
|
|
11
|
+
|
|
12
|
+
/** Stroke in dense chrome bars (the view toolbar, the builder's bars). */
|
|
13
|
+
export const DENSE_ICON_STROKE = 2
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
createToast,
|
|
4
|
+
MAX_VISIBLE_TOASTS,
|
|
5
|
+
TOAST_DURATION_MS,
|
|
6
|
+
TOAST_ERROR_DURATION_MS,
|
|
7
|
+
toastDuration,
|
|
8
|
+
toastReducer,
|
|
9
|
+
type ToastItem,
|
|
10
|
+
} from "./toast-model.js"
|
|
11
|
+
|
|
12
|
+
const item = (id: number): ToastItem => createToast(id, { title: `t${id}` })
|
|
13
|
+
|
|
14
|
+
describe("toastDuration (CI §8: about 2.8 s, errors about 5 s)", () => {
|
|
15
|
+
it("defaults to 2.8 s and gives errors 5 s", () => {
|
|
16
|
+
expect(TOAST_DURATION_MS).toBe(2800)
|
|
17
|
+
expect(TOAST_ERROR_DURATION_MS).toBe(5000)
|
|
18
|
+
expect(toastDuration("success")).toBe(2800)
|
|
19
|
+
expect(toastDuration("info")).toBe(2800)
|
|
20
|
+
expect(toastDuration("warning")).toBe(2800)
|
|
21
|
+
expect(toastDuration("danger")).toBe(5000)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("honours an explicit positive duration and ignores an unusable one", () => {
|
|
25
|
+
expect(toastDuration("danger", 1200)).toBe(1200)
|
|
26
|
+
expect(toastDuration("info", 0)).toBe(2800)
|
|
27
|
+
expect(toastDuration("info", -5)).toBe(2800)
|
|
28
|
+
expect(toastDuration("danger", Number.NaN)).toBe(5000)
|
|
29
|
+
expect(toastDuration("danger", Number.POSITIVE_INFINITY)).toBe(5000)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe("createToast", () => {
|
|
34
|
+
it("defaults to the info tone and drops an empty description", () => {
|
|
35
|
+
expect(createToast(1, { title: "Saved" })).toEqual({
|
|
36
|
+
id: 1,
|
|
37
|
+
tone: "info",
|
|
38
|
+
title: "Saved",
|
|
39
|
+
duration: 2800,
|
|
40
|
+
})
|
|
41
|
+
expect(createToast(2, { title: "x", description: "" })).not.toHaveProperty("description")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("keeps tone, description and the tone's duration", () => {
|
|
45
|
+
expect(createToast(3, { tone: "danger", title: "Failed", description: "503" })).toEqual({
|
|
46
|
+
id: 3,
|
|
47
|
+
tone: "danger",
|
|
48
|
+
title: "Failed",
|
|
49
|
+
description: "503",
|
|
50
|
+
duration: 5000,
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe("toastReducer", () => {
|
|
56
|
+
it("appends and keeps at most the newest MAX_VISIBLE_TOASTS", () => {
|
|
57
|
+
expect(MAX_VISIBLE_TOASTS).toBe(3)
|
|
58
|
+
let state: ToastItem[] = []
|
|
59
|
+
for (let id = 1; id <= 4; id++) state = toastReducer(state, { type: "add", toast: item(id) })
|
|
60
|
+
expect(state.map((t) => t.id)).toEqual([2, 3, 4])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("dismisses by id and keeps the list identity when nothing matches", () => {
|
|
64
|
+
const state = [item(1), item(2)]
|
|
65
|
+
expect(toastReducer(state, { type: "dismiss", id: 1 }).map((t) => t.id)).toEqual([2])
|
|
66
|
+
expect(toastReducer(state, { type: "dismiss", id: 9 })).toBe(state)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure model behind `ToastProvider` / `useToast`: the toast list as a reducer
|
|
3
|
+
* plus the timing rules, so both are testable without a DOM (the house path,
|
|
4
|
+
* FITNESS.md) and the component stays a thin view over them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Status tone of a toast — the four status roles (`--success` … `--info`). */
|
|
8
|
+
export type ToastTone = "success" | "warning" | "danger" | "info"
|
|
9
|
+
|
|
10
|
+
/** What a caller passes to `useToast().show(...)`. */
|
|
11
|
+
export interface ToastOptions {
|
|
12
|
+
/** Status tone; drives the edge, the icon and the default duration. Default `"info"`. */
|
|
13
|
+
tone?: ToastTone
|
|
14
|
+
/** The message. Say what happened, in one short sentence ("Job retried."). */
|
|
15
|
+
title: string
|
|
16
|
+
/** Optional second line (what you can do next, or the affected object). */
|
|
17
|
+
description?: string
|
|
18
|
+
/** Auto-dismiss after this many ms. Defaults per {@link toastDuration}. */
|
|
19
|
+
duration?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A toast as it sits in the list. */
|
|
23
|
+
export interface ToastItem {
|
|
24
|
+
id: number
|
|
25
|
+
tone: ToastTone
|
|
26
|
+
title: string
|
|
27
|
+
description?: string
|
|
28
|
+
duration: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Default lifetime of a toast (CI §8: about 2.8 s). */
|
|
32
|
+
export const TOAST_DURATION_MS = 2800
|
|
33
|
+
/** Lifetime of an error toast (CI §8: about 5 s) — long enough to read the reason. */
|
|
34
|
+
export const TOAST_ERROR_DURATION_MS = 5000
|
|
35
|
+
/** At most this many toasts stack; the oldest leaves first. */
|
|
36
|
+
export const MAX_VISIBLE_TOASTS = 3
|
|
37
|
+
|
|
38
|
+
/** The lifetime of a toast: an explicit positive duration, else the tone's default. */
|
|
39
|
+
export function toastDuration(tone: ToastTone, explicit?: number): number {
|
|
40
|
+
if (explicit !== undefined && Number.isFinite(explicit) && explicit > 0) return explicit
|
|
41
|
+
return tone === "danger" ? TOAST_ERROR_DURATION_MS : TOAST_DURATION_MS
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Normalise caller options into a list item. */
|
|
45
|
+
export function createToast(id: number, options: ToastOptions): ToastItem {
|
|
46
|
+
const tone = options.tone ?? "info"
|
|
47
|
+
return {
|
|
48
|
+
id,
|
|
49
|
+
tone,
|
|
50
|
+
title: options.title,
|
|
51
|
+
...(options.description ? { description: options.description } : {}),
|
|
52
|
+
duration: toastDuration(tone, options.duration),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ToastAction = { type: "add"; toast: ToastItem } | { type: "dismiss"; id: number }
|
|
57
|
+
|
|
58
|
+
/** Add appends (dropping the oldest beyond {@link MAX_VISIBLE_TOASTS}); dismiss removes by id. */
|
|
59
|
+
export function toastReducer(state: readonly ToastItem[], action: ToastAction): ToastItem[] {
|
|
60
|
+
if (action.type === "add") return [...state, action.toast].slice(-MAX_VISIBLE_TOASTS)
|
|
61
|
+
const next = state.filter((t) => t.id !== action.id)
|
|
62
|
+
return next.length === state.length ? (state as ToastItem[]) : next
|
|
63
|
+
}
|