@miragon/mcp-toolkit-ui 2.6.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 +7 -1
- package/dist/app/builder/BuilderBanners.d.ts +7 -2
- package/dist/app/builder/BuilderBanners.d.ts.map +1 -1
- package/dist/app/builder/BuilderBanners.js +8 -3
- package/dist/app/builder/BuilderBanners.js.map +1 -1
- package/dist/app/builder/CanvasRow.d.ts.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 +9 -4
- 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,432 @@
|
|
|
1
|
+
import ts from "typescript"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { TEXT_ATTRIBUTES } from "./render-scan.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Static half of the i18n guard (the render half is `i18n-coverage.test.tsx`):
|
|
7
|
+
* no toolkit component may carry user-facing copy in its source. Every word a
|
|
8
|
+
* user reads or hears comes from the locale catalog (`locale/messages-*.ts`)
|
|
9
|
+
* or from the consumer's props. This catches what an SSR render cannot reach —
|
|
10
|
+
* portal content (dialogs, sheets, select menus) and branches the fixtures do
|
|
11
|
+
* not hit.
|
|
12
|
+
*
|
|
13
|
+
* Flagged in every `.tsx` under src (tests and the render-case fixtures
|
|
14
|
+
* excepted):
|
|
15
|
+
* - JSX text containing a letter;
|
|
16
|
+
* - a string literal with a letter rendered as a child (`{"…"}`, `{x ? "a" : "b"}`,
|
|
17
|
+
* `{x && "a"}`, template literals);
|
|
18
|
+
* - such a literal in a text attribute of a DOM element (aria-label, title,
|
|
19
|
+
* placeholder, alt, …), or in any prop of a component except the
|
|
20
|
+
* non-text props below (variants, sizes, roles, ids, …).
|
|
21
|
+
*
|
|
22
|
+
* And in every `.ts`/`.tsx` source, the feedback copy rule: the sentences of
|
|
23
|
+
* a feedback surface carry catalog text only, never a literal and never a raw
|
|
24
|
+
* error. Feedback surfaces are the `title`/`description` of a
|
|
25
|
+
* `<x>.show({ … })` toast call and of `ErrorState`/`EmptyState`, and the
|
|
26
|
+
* content of an inline alert (any element with `role="alert"` or
|
|
27
|
+
* `role="status"`, and the `Alert` primitive). A raw error is `e.message`,
|
|
28
|
+
* `String(e)`, an `errorText(e)` helper, or a value named as one once it sits
|
|
29
|
+
* in state or props (`error`, `err`, `reason`, `saveError`, `callErrorReason`
|
|
30
|
+
* …). The raw reason belongs in the console or under an ErrorState's
|
|
31
|
+
* "Details", not in a sentence the user reads.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
declare global {
|
|
35
|
+
interface ImportMeta {
|
|
36
|
+
glob(
|
|
37
|
+
pattern: string | string[],
|
|
38
|
+
options: { query: "?raw"; import: "default"; eager: true },
|
|
39
|
+
): Record<string, string>
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SOURCES = import.meta.glob(["./**/*.tsx", "!./**/*.test.tsx", "!./render-cases*.tsx"], {
|
|
44
|
+
query: "?raw",
|
|
45
|
+
import: "default",
|
|
46
|
+
eager: true,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const TS_SOURCES = import.meta.glob(["./**/*.ts", "!./**/*.test.ts", "!./**/*.d.ts"], {
|
|
50
|
+
query: "?raw",
|
|
51
|
+
import: "default",
|
|
52
|
+
eager: true,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/** Component props that never carry user-facing copy. */
|
|
56
|
+
const NON_TEXT_PROPS = new Set([
|
|
57
|
+
"className",
|
|
58
|
+
"variant",
|
|
59
|
+
"size",
|
|
60
|
+
"type",
|
|
61
|
+
"side",
|
|
62
|
+
"role",
|
|
63
|
+
"tone",
|
|
64
|
+
"iconTone",
|
|
65
|
+
"titleAs",
|
|
66
|
+
"titleMode",
|
|
67
|
+
"orientation",
|
|
68
|
+
"align",
|
|
69
|
+
"value",
|
|
70
|
+
"defaultValue",
|
|
71
|
+
"id",
|
|
72
|
+
"name",
|
|
73
|
+
"htmlFor",
|
|
74
|
+
"href",
|
|
75
|
+
"target",
|
|
76
|
+
"rel",
|
|
77
|
+
"mode",
|
|
78
|
+
"asChild",
|
|
79
|
+
"dir",
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
const HAS_LETTER = /\p{L}/u
|
|
83
|
+
const TEXT_ATTRS = new Set<string>(TEXT_ATTRIBUTES)
|
|
84
|
+
|
|
85
|
+
/** String literals in an expression that would end up rendered as text. */
|
|
86
|
+
function renderedLiterals(node: ts.Expression): ts.Node[] {
|
|
87
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return [node]
|
|
88
|
+
if (ts.isTemplateExpression(node)) return [node.head, ...node.templateSpans.map((s) => s.literal)]
|
|
89
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node)) {
|
|
90
|
+
return renderedLiterals(node.expression)
|
|
91
|
+
}
|
|
92
|
+
if (ts.isConditionalExpression(node)) {
|
|
93
|
+
return [...renderedLiterals(node.whenTrue), ...renderedLiterals(node.whenFalse)]
|
|
94
|
+
}
|
|
95
|
+
if (ts.isBinaryExpression(node)) {
|
|
96
|
+
const op = node.operatorToken.kind
|
|
97
|
+
if (op === ts.SyntaxKind.AmpersandAmpersandToken) return renderedLiterals(node.right)
|
|
98
|
+
if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) {
|
|
99
|
+
return [...renderedLiterals(node.left), ...renderedLiterals(node.right)]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function literalText(node: ts.Node): string {
|
|
106
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text
|
|
107
|
+
if (ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
|
|
108
|
+
return node.text
|
|
109
|
+
}
|
|
110
|
+
return ""
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function tagName(attr: ts.JsxAttribute): string {
|
|
114
|
+
const opening = attr.parent.parent
|
|
115
|
+
return opening.tagName.getText()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether an attribute is rendered text: the text attributes on any element;
|
|
120
|
+
* on a component additionally every prop that is not a known non-text prop.
|
|
121
|
+
*/
|
|
122
|
+
function isTextAttribute(attr: ts.JsxAttribute): boolean {
|
|
123
|
+
const name = attr.name.getText()
|
|
124
|
+
if (TEXT_ATTRS.has(name)) return true
|
|
125
|
+
if (/^[a-z]/.test(tagName(attr))) return false
|
|
126
|
+
if (name.endsWith("ClassName")) return false
|
|
127
|
+
return !NON_TEXT_PROPS.has(name) && !name.startsWith("data-") && !name.startsWith("aria-")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hardcodedCopy(file: string, source: string): string[] {
|
|
131
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
132
|
+
const found: string[] = []
|
|
133
|
+
const report = (node: ts.Node, text: string) => {
|
|
134
|
+
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf))
|
|
135
|
+
found.push(`${file}:${line + 1}: "${text.trim().replace(/\s+/g, " ")}"`)
|
|
136
|
+
}
|
|
137
|
+
const visit = (node: ts.Node) => {
|
|
138
|
+
if (ts.isJsxText(node) && HAS_LETTER.test(node.text)) report(node, node.text)
|
|
139
|
+
if (ts.isJsxExpression(node) && node.expression) {
|
|
140
|
+
const parent = node.parent
|
|
141
|
+
const rendered =
|
|
142
|
+
ts.isJsxElement(parent) ||
|
|
143
|
+
ts.isJsxFragment(parent) ||
|
|
144
|
+
(ts.isJsxAttribute(parent) && isTextAttribute(parent))
|
|
145
|
+
if (rendered) {
|
|
146
|
+
for (const lit of renderedLiterals(node.expression)) {
|
|
147
|
+
const text = literalText(lit)
|
|
148
|
+
if (HAS_LETTER.test(text)) report(lit, text)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (
|
|
153
|
+
ts.isJsxAttribute(node) &&
|
|
154
|
+
node.initializer &&
|
|
155
|
+
ts.isStringLiteral(node.initializer) &&
|
|
156
|
+
HAS_LETTER.test(node.initializer.text) &&
|
|
157
|
+
isTextAttribute(node)
|
|
158
|
+
) {
|
|
159
|
+
report(node.initializer, node.initializer.text)
|
|
160
|
+
}
|
|
161
|
+
ts.forEachChild(node, visit)
|
|
162
|
+
}
|
|
163
|
+
visit(sf)
|
|
164
|
+
return found
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Calls that turn a caught error into text. */
|
|
168
|
+
const RAW_ERROR_CALLS = new Set(["errorText", "errorMessage", "String"])
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Names a caught error (or its text) takes once it is held in state or passed
|
|
172
|
+
* as a prop: `e`, `err`, `error`, `reason`, `cause`, or a name ending in
|
|
173
|
+
* `Error`/`Reason` (`saveError`, `callErrorReason`). Flags (`isError`,
|
|
174
|
+
* `hasError`, `showError`) are not errors.
|
|
175
|
+
*/
|
|
176
|
+
const RAW_ERROR_NAME = /^(?:e|err|error|reason|cause)$|^(?!(?:is|has|show)[A-Z]).*(?:Error|Reason)$/
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Whether an expression reads a raw error: `.message`/`.stack`, a raw-error
|
|
180
|
+
* call, or a reference to a value named as an error. Declarations, property
|
|
181
|
+
* names, types and the attributes of nested JSX are not reads; the text
|
|
182
|
+
* children of nested JSX are.
|
|
183
|
+
*/
|
|
184
|
+
function readsRawError(node: ts.Node): boolean {
|
|
185
|
+
if (ts.isTypeNode(node) || ts.isJsxAttributes(node) || ts.isJsxSelfClosingElement(node)) {
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
189
|
+
if (node.name.text === "message" || node.name.text === "stack") return true
|
|
190
|
+
return readsRawError(node.expression)
|
|
191
|
+
}
|
|
192
|
+
if (
|
|
193
|
+
ts.isCallExpression(node) &&
|
|
194
|
+
ts.isIdentifier(node.expression) &&
|
|
195
|
+
RAW_ERROR_CALLS.has(node.expression.text)
|
|
196
|
+
) {
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
if (ts.isIdentifier(node)) return RAW_ERROR_NAME.test(node.text)
|
|
200
|
+
if (ts.isJsxElement(node) || ts.isJsxFragment(node)) return node.children.some(readsRawError)
|
|
201
|
+
if (ts.isParameter(node) || ts.isBindingElement(node) || ts.isVariableDeclaration(node)) {
|
|
202
|
+
return node.initializer ? readsRawError(node.initializer) : false
|
|
203
|
+
}
|
|
204
|
+
if (ts.isPropertyAssignment(node)) return readsRawError(node.initializer)
|
|
205
|
+
return ts.forEachChild(node, readsRawError) ?? false
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** `title`/`description` of `<x>.show({ … })` calls that are literals or raw errors. */
|
|
209
|
+
function toastCopyLeaks(file: string, source: string): string[] {
|
|
210
|
+
const kind = file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
|
211
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, kind)
|
|
212
|
+
const found: string[] = []
|
|
213
|
+
const visit = (node: ts.Node) => {
|
|
214
|
+
const options = ts.isCallExpression(node) ? node.arguments[0] : undefined
|
|
215
|
+
if (
|
|
216
|
+
ts.isCallExpression(node) &&
|
|
217
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
218
|
+
node.expression.name.text === "show" &&
|
|
219
|
+
options &&
|
|
220
|
+
ts.isObjectLiteralExpression(options)
|
|
221
|
+
) {
|
|
222
|
+
for (const prop of options.properties) {
|
|
223
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
224
|
+
const key = prop.name.getText(sf)
|
|
225
|
+
if (key !== "title" && key !== "description") continue
|
|
226
|
+
const literal = renderedLiterals(prop.initializer).some((lit) =>
|
|
227
|
+
HAS_LETTER.test(literalText(lit)),
|
|
228
|
+
)
|
|
229
|
+
if (literal || readsRawError(prop.initializer)) {
|
|
230
|
+
const { line } = sf.getLineAndCharacterOfPosition(prop.getStart(sf))
|
|
231
|
+
found.push(`${file}:${line + 1}: ${key}: ${prop.initializer.getText(sf)}`)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
ts.forEachChild(node, visit)
|
|
236
|
+
}
|
|
237
|
+
visit(sf)
|
|
238
|
+
return found
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Feedback components whose `title`/`description` are sentences the user reads. */
|
|
242
|
+
const FEEDBACK_COMPONENTS = new Set(["ErrorState", "EmptyState"])
|
|
243
|
+
const FEEDBACK_PROPS = new Set(["title", "description"])
|
|
244
|
+
/** Components that are an inline alert by themselves (their role is built in). */
|
|
245
|
+
const ALERT_COMPONENTS = new Set(["Alert"])
|
|
246
|
+
|
|
247
|
+
function jsxTag(node: ts.JsxElement | ts.JsxSelfClosingElement): string {
|
|
248
|
+
return (ts.isJsxElement(node) ? node.openingElement : node).tagName.getText()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Whether a JSX element is an inline alert: `role="alert"|"status"` or the `Alert` primitive. */
|
|
252
|
+
function isInlineAlert(node: ts.JsxElement): boolean {
|
|
253
|
+
if (ALERT_COMPONENTS.has(jsxTag(node))) return true
|
|
254
|
+
return node.openingElement.attributes.properties.some(
|
|
255
|
+
(attr) =>
|
|
256
|
+
ts.isJsxAttribute(attr) &&
|
|
257
|
+
attr.name.getText() === "role" &&
|
|
258
|
+
attr.initializer !== undefined &&
|
|
259
|
+
ts.isStringLiteral(attr.initializer) &&
|
|
260
|
+
(attr.initializer.text === "alert" || attr.initializer.text === "status"),
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Raw errors in the JSX feedback surfaces: the `title`/`description` of an
|
|
266
|
+
* ErrorState or EmptyState, and the content of an inline alert. (Literals in
|
|
267
|
+
* them are the hard-coded-copy gate's job.)
|
|
268
|
+
*/
|
|
269
|
+
function feedbackCopyLeaks(file: string, source: string): string[] {
|
|
270
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
271
|
+
const found: string[] = []
|
|
272
|
+
const report = (node: ts.Node, where: string) => {
|
|
273
|
+
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf))
|
|
274
|
+
found.push(`${file}:${line + 1}: ${where}: ${node.getText(sf).replace(/\s+/g, " ")}`)
|
|
275
|
+
}
|
|
276
|
+
const visit = (node: ts.Node) => {
|
|
277
|
+
if (
|
|
278
|
+
ts.isJsxAttribute(node) &&
|
|
279
|
+
node.initializer &&
|
|
280
|
+
FEEDBACK_PROPS.has(node.name.getText()) &&
|
|
281
|
+
FEEDBACK_COMPONENTS.has(tagName(node)) &&
|
|
282
|
+
readsRawError(node.initializer)
|
|
283
|
+
) {
|
|
284
|
+
report(node.initializer, `${tagName(node)} ${node.name.getText()}`)
|
|
285
|
+
}
|
|
286
|
+
if (ts.isJsxElement(node) && isInlineAlert(node)) {
|
|
287
|
+
for (const child of node.children) {
|
|
288
|
+
if (readsRawError(child)) report(child, `${jsxTag(node)} alert content`)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
ts.forEachChild(node, visit)
|
|
292
|
+
}
|
|
293
|
+
visit(sf)
|
|
294
|
+
return found
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
describe("feedback copy comes from the catalog, never from a raw error", () => {
|
|
298
|
+
it("scans the feedback surfaces", () => {
|
|
299
|
+
const tsx = Object.entries(SOURCES)
|
|
300
|
+
expect(tsx.filter(([, src]) => /<(ErrorState|EmptyState)\b/.test(src)).length).toBeGreaterThan(
|
|
301
|
+
3,
|
|
302
|
+
)
|
|
303
|
+
expect(tsx.filter(([, src]) => /role="(alert|status)"/.test(src)).length).toBeGreaterThan(2)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it("finds no raw error in an ErrorState/EmptyState title or description, or in an inline alert", () => {
|
|
307
|
+
const offenders = Object.entries(SOURCES).flatMap(([file, source]) =>
|
|
308
|
+
feedbackCopyLeaks(file, source),
|
|
309
|
+
)
|
|
310
|
+
expect(
|
|
311
|
+
offenders,
|
|
312
|
+
"Say what happened and what to do in catalog words (locale/messages-*.ts); pass the raw reason as an ErrorState's `details` (or log it with console.warn).",
|
|
313
|
+
).toEqual([])
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it("catches the shapes it promises to catch (gate self-test)", () => {
|
|
317
|
+
const src = `
|
|
318
|
+
export const A = ({ L, e }: P) => <ErrorState title={L.x} description={e.message} />
|
|
319
|
+
export const B = ({ error }: P) => <ErrorState title={error} details={error} />
|
|
320
|
+
export const C = ({ L, err }: P) => <EmptyState title={L.empty} description={String(err)} />
|
|
321
|
+
export const D = ({ L, error }: P) => (
|
|
322
|
+
<div role="alert">
|
|
323
|
+
<span>{L.saveError}:</span> {error}
|
|
324
|
+
</div>
|
|
325
|
+
)
|
|
326
|
+
export const E = ({ L, name, refreshError }: P) => (
|
|
327
|
+
<div role="status">
|
|
328
|
+
<p>{L.catalogueError}</p>
|
|
329
|
+
<p><span>{name}</span>: {refreshError}</p>
|
|
330
|
+
</div>
|
|
331
|
+
)
|
|
332
|
+
export const F = ({ reason }: P) => <Alert><AlertDescription>{reason}</AlertDescription></Alert>
|
|
333
|
+
export const G = ({ L, error, isError }: P) => (
|
|
334
|
+
<>
|
|
335
|
+
<ErrorState title={L.saveError} description={L.saveErrorHint} details={error} />
|
|
336
|
+
{isError && <p role="alert">{L.failed}</p>}
|
|
337
|
+
<p role="status">{L.cancelled}</p>
|
|
338
|
+
<div role="alert">{items.map(({ id, reason }) => <li key={id}>{id}</li>)}</div>
|
|
339
|
+
<ErrorState title={human?.title ?? L.errorTitle} description={(x as ViewError).hint} />
|
|
340
|
+
</>
|
|
341
|
+
)
|
|
342
|
+
`
|
|
343
|
+
expect(
|
|
344
|
+
feedbackCopyLeaks("self.tsx", src).map((s) => s.replace(/^self\.tsx:\d+: /, "")),
|
|
345
|
+
).toEqual([
|
|
346
|
+
"ErrorState description: {e.message}",
|
|
347
|
+
"ErrorState title: {error}",
|
|
348
|
+
"EmptyState description: {String(err)}",
|
|
349
|
+
"div alert content: {error}",
|
|
350
|
+
"div alert content: <p><span>{name}</span>: {refreshError}</p>",
|
|
351
|
+
"Alert alert content: <AlertDescription>{reason}</AlertDescription>",
|
|
352
|
+
])
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
describe("toast copy comes from the catalog, never from a raw error", () => {
|
|
357
|
+
it("scans the toast call sites", () => {
|
|
358
|
+
const all = { ...SOURCES, ...TS_SOURCES }
|
|
359
|
+
expect(Object.keys(all)).toContain("./app/mcp-app-view.tsx")
|
|
360
|
+
expect(Object.values(all).filter((src) => /\.show\(\{/.test(src)).length).toBeGreaterThan(1)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it("finds no literal and no raw error in a toast's title or description", () => {
|
|
364
|
+
const offenders = Object.entries({ ...SOURCES, ...TS_SOURCES }).flatMap(([file, source]) =>
|
|
365
|
+
toastCopyLeaks(file, source),
|
|
366
|
+
)
|
|
367
|
+
expect(
|
|
368
|
+
offenders,
|
|
369
|
+
"Say what happened and what to do in catalog words (locale/messages-*.ts); log the raw reason (console.warn) or show it under an ErrorState's Details.",
|
|
370
|
+
).toEqual([])
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it("catches the shapes it promises to catch (gate self-test)", () => {
|
|
374
|
+
const src = `
|
|
375
|
+
toast.show({ tone: "danger", title: "Refresh failed", description: errorText(e) })
|
|
376
|
+
toast.show({ title: L.refreshFailed, description: e.message })
|
|
377
|
+
toast.show({ title: L.x, description: String(err) })
|
|
378
|
+
toast.show({ title: L.refreshFailed, description: refreshError })
|
|
379
|
+
toast.show({ tone: "success", title: L.saved(name) })
|
|
380
|
+
toast.show({ title: L.refreshFailed, description: L.refreshFailedHint })
|
|
381
|
+
toast.show({ title: L.failed, description: isError ? L.a : L.b })
|
|
382
|
+
other.show(options)
|
|
383
|
+
`
|
|
384
|
+
expect(toastCopyLeaks("self.ts", src).map((s) => s.replace(/^self\.ts:\d+: /, ""))).toEqual([
|
|
385
|
+
'title: "Refresh failed"',
|
|
386
|
+
"description: errorText(e)",
|
|
387
|
+
"description: e.message",
|
|
388
|
+
"description: String(err)",
|
|
389
|
+
"description: refreshError",
|
|
390
|
+
])
|
|
391
|
+
})
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
describe("no hard-coded copy in toolkit components", () => {
|
|
395
|
+
it("scans the component sources", () => {
|
|
396
|
+
// A broken glob would make the gate vacuous.
|
|
397
|
+
expect(Object.keys(SOURCES).length).toBeGreaterThan(40)
|
|
398
|
+
expect(Object.keys(SOURCES)).toContain("./app/mcp-app-view-chrome.tsx")
|
|
399
|
+
expect(Object.keys(SOURCES)).toContain("./primitives/dialog.tsx")
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it("finds every user-facing word in the catalog or in props, never in the source", () => {
|
|
403
|
+
const offenders = Object.entries(SOURCES).flatMap(([file, source]) =>
|
|
404
|
+
hardcodedCopy(file, source),
|
|
405
|
+
)
|
|
406
|
+
expect(
|
|
407
|
+
offenders,
|
|
408
|
+
"Move these strings into the toolkit catalog (locale/message-types.ts + messages-en.ts + messages-de.ts) and read them via useToolkitMessages() or the component's labels.",
|
|
409
|
+
).toEqual([])
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it("catches the shapes it promises to catch (gate self-test)", () => {
|
|
413
|
+
const src = `
|
|
414
|
+
export const A = () => <p>Loading…</p>
|
|
415
|
+
export const B = ({ x }: { x: boolean }) => <span>{x ? "Load more" : "…"}</span>
|
|
416
|
+
export const C = () => <button aria-label="Close" title={"Shut"} className="text-sm" />
|
|
417
|
+
export const D = () => <ErrorState title="Oops" variant="inline" data-slot="x" />
|
|
418
|
+
export const E = ({ n }: { n: number }) => <i>{\`\${n} rows\`}</i>
|
|
419
|
+
export const F = ({ ok }: { ok: boolean }) => <b>{ok && "Done"}{ok === Boolean("x") && 1}</b>
|
|
420
|
+
export const G = ({ L }: { L: { a: string } }) => <p title={L.a}>{L.a} · {"→"}</p>
|
|
421
|
+
`
|
|
422
|
+
expect(hardcodedCopy("self.tsx", src).map((s) => s.replace(/^self\.tsx:\d+: /, ""))).toEqual([
|
|
423
|
+
'"Loading…"',
|
|
424
|
+
'"Load more"',
|
|
425
|
+
'"Close"',
|
|
426
|
+
'"Shut"',
|
|
427
|
+
'"Oops"',
|
|
428
|
+
'"rows"',
|
|
429
|
+
'"Done"',
|
|
430
|
+
])
|
|
431
|
+
})
|
|
432
|
+
})
|
package/src/primitives/alert.tsx
CHANGED
|
@@ -4,13 +4,15 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
4
4
|
import { cn } from "../lib/utils.js"
|
|
5
5
|
|
|
6
6
|
const alertVariants = cva(
|
|
7
|
-
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-
|
|
7
|
+
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-md border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
11
|
default: "bg-card text-card-foreground",
|
|
12
|
+
// Tone model: tint, a border and icon in the danger colour; title and
|
|
13
|
+
// description in the foreground (CI §3.3: text on a tint is black).
|
|
12
14
|
destructive:
|
|
13
|
-
"bg-
|
|
15
|
+
"border-danger bg-danger-soft text-foreground *:data-[slot=alert-title]:text-foreground *:data-[slot=alert-description]:text-foreground [&>svg]:text-danger",
|
|
14
16
|
},
|
|
15
17
|
},
|
|
16
18
|
defaultVariants: {
|
package/src/primitives/badge.tsx
CHANGED
|
@@ -2,20 +2,27 @@ import { mergeProps } from "@base-ui/react/merge-props"
|
|
|
2
2
|
import { useRender } from "@base-ui/react/use-render"
|
|
3
3
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
4
|
|
|
5
|
+
import { FOCUS_RING } from "../lib/focus-ring.js"
|
|
6
|
+
import { TONE_BORDER, TONE_SOFT } from "../lib/tone-utils.js"
|
|
5
7
|
import { cn } from "../lib/utils.js"
|
|
6
8
|
|
|
9
|
+
// Badges and chips take radius sm (CI §6). The status variants follow the tone
|
|
10
|
+
// model: tint + foreground text, rim in the tone (TONE_SOFT + TONE_BORDER;
|
|
11
|
+
// CI §3.3 "-soft: Text darauf --cd-schwarz"). The ink is never text on a tint.
|
|
7
12
|
const badgeVariants = cva(
|
|
8
|
-
|
|
13
|
+
`group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-sm border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all ${FOCUS_RING} has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-danger [&>svg]:pointer-events-none [&>svg]:size-3!`,
|
|
9
14
|
{
|
|
10
15
|
variants: {
|
|
11
16
|
variant: {
|
|
12
|
-
default: "bg-primary text-primary-foreground [a]:hover:bg-primary
|
|
17
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary-hover",
|
|
13
18
|
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
14
|
-
destructive:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
destructive: `${TONE_SOFT.danger} ${TONE_BORDER.danger}`,
|
|
20
|
+
success: `${TONE_SOFT.success} ${TONE_BORDER.success}`,
|
|
21
|
+
warning: `${TONE_SOFT.warning} ${TONE_BORDER.warning}`,
|
|
22
|
+
info: `${TONE_SOFT.info} ${TONE_BORDER.info}`,
|
|
23
|
+
outline: "border-border text-foreground [a]:hover:bg-muted",
|
|
24
|
+
ghost: "hover:bg-muted",
|
|
25
|
+
link: "text-link underline-offset-4 hover:underline",
|
|
19
26
|
},
|
|
20
27
|
},
|
|
21
28
|
defaultVariants: {
|
|
@@ -2,29 +2,34 @@ import * as React from "react"
|
|
|
2
2
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
3
|
import { Slot } from "radix-ui"
|
|
4
4
|
|
|
5
|
+
import { FOCUS_RING } from "../lib/focus-ring.js"
|
|
5
6
|
import { cn } from "../lib/utils.js"
|
|
6
7
|
|
|
8
|
+
// Miragon CI §8: radius md, weight 600; primary = blue fill with white text and
|
|
9
|
+
// shadow-1, hover deeper blue + shadow-2 + a 1px lift; secondary/outline =
|
|
10
|
+
// surface with a line border, hover blue border and text on the accent tint.
|
|
7
11
|
const buttonVariants = cva(
|
|
8
|
-
|
|
12
|
+
`inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-semibold whitespace-nowrap transition-[color,background-color,border-color,box-shadow,translate] disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-danger ${FOCUS_RING} [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
|
|
9
13
|
{
|
|
10
14
|
variants: {
|
|
11
15
|
variant: {
|
|
12
|
-
default:
|
|
16
|
+
default:
|
|
17
|
+
"bg-primary text-primary-foreground shadow-1 hover:bg-primary-hover hover:shadow-2 hover:-translate-y-px active:translate-y-0",
|
|
13
18
|
destructive:
|
|
14
|
-
"bg-destructive text-
|
|
19
|
+
"bg-destructive text-destructive-foreground shadow-1 hover:bg-destructive/90 hover:shadow-2",
|
|
15
20
|
outline:
|
|
16
|
-
"border
|
|
21
|
+
"border border-border bg-background text-foreground hover:border-focus hover:bg-accent hover:text-accent-foreground",
|
|
17
22
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
18
|
-
ghost: "hover:bg-accent hover:text-accent-foreground
|
|
19
|
-
link: "text-
|
|
23
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
24
|
+
link: "text-link underline-offset-4 hover:underline",
|
|
20
25
|
},
|
|
21
26
|
size: {
|
|
22
27
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
23
|
-
xs: "h-6 gap-1 rounded-
|
|
28
|
+
xs: "h-6 gap-1 rounded-sm px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
24
29
|
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
|
25
30
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
26
31
|
icon: "size-9",
|
|
27
|
-
"icon-xs": "size-6 rounded-
|
|
32
|
+
"icon-xs": "size-6 rounded-sm [&_svg:not([class*='size-'])]:size-3",
|
|
28
33
|
"icon-sm": "size-8",
|
|
29
34
|
"icon-lg": "size-10",
|
|
30
35
|
},
|
package/src/primitives/card.tsx
CHANGED
|
@@ -12,7 +12,7 @@ function Card({
|
|
|
12
12
|
data-slot="card"
|
|
13
13
|
data-size={size}
|
|
14
14
|
className={cn(
|
|
15
|
-
"group/card bg-card text-card-foreground ring-
|
|
15
|
+
"group/card bg-card text-card-foreground ring-border flex flex-col gap-4 overflow-hidden rounded-md py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-md *:[img:last-child]:rounded-b-md",
|
|
16
16
|
className,
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
|
@@ -25,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
25
25
|
<div
|
|
26
26
|
data-slot="card-header"
|
|
27
27
|
className={cn(
|
|
28
|
-
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-
|
|
28
|
+
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-md px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
|
29
29
|
className,
|
|
30
30
|
)}
|
|
31
31
|
{...props}
|
|
@@ -38,7 +38,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
38
38
|
<div
|
|
39
39
|
data-slot="card-title"
|
|
40
40
|
className={cn(
|
|
41
|
-
"font-heading text-base leading-snug font-
|
|
41
|
+
"font-heading text-base leading-snug font-semibold group-data-[size=sm]/card:text-sm",
|
|
42
42
|
className,
|
|
43
43
|
)}
|
|
44
44
|
{...props}
|
|
@@ -81,7 +81,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
81
81
|
<div
|
|
82
82
|
data-slot="card-footer"
|
|
83
83
|
className={cn(
|
|
84
|
-
"bg-muted/50 flex items-center rounded-b-
|
|
84
|
+
"bg-muted/50 flex items-center rounded-b-md border-t p-4 group-data-[size=sm]/card:p-3",
|
|
85
85
|
className,
|
|
86
86
|
)}
|
|
87
87
|
{...props}
|