@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
|
@@ -1,29 +1,40 @@
|
|
|
1
1
|
import type { CSSProperties } from "react"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* pair, the surface/accent colours, the border/ring, and the shape/typography
|
|
8
|
-
* knobs — expressed in plain, design-system terms.
|
|
4
|
+
* The toolkit's theming surface in plain, design-system terms: one optional
|
|
5
|
+
* field per ROLE variable defined in `packages/ui/src/theme.css` (the role-token
|
|
6
|
+
* contract, see {@link ROLE_TOKENS}), plus the legacy shadcn knobs it always had.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `font-family` lists.
|
|
8
|
+
* The defaults are the Miragon theme — every role points at a vendored
|
|
9
|
+
* `--cd-*` corporate-design token, in light and in dark. A brand overrides
|
|
10
|
+
* only the roles it cares about and inherits the rest. Colour values are any
|
|
11
|
+
* CSS colour string (`#rrggbb`, `oklch(...)`, `color-mix(...)`), lengths any
|
|
12
|
+
* CSS length, fonts `font-family` lists, durations CSS times.
|
|
15
13
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* Roles a component must never take from a host's palette — primary, link,
|
|
15
|
+
* focus, the status tones and the input border — are brand decisions; a host
|
|
16
|
+
* maps only the neutral roles (background, card, foreground, border, …).
|
|
17
|
+
*
|
|
18
|
+
* Unlisted variables (`--chart-*`, `--sidebar-*`, `--popover-foreground`, …)
|
|
19
|
+
* stay themeable in CSS. See {@link TOKEN_TO_CSS_VAR} for the exact mapping.
|
|
20
20
|
*/
|
|
21
21
|
export interface ThemeTokens {
|
|
22
|
-
/** Brand colour. Maps to `--primary`. */
|
|
22
|
+
/** Brand colour: primary button fill, selection. Maps to `--primary`. */
|
|
23
23
|
primary?: string
|
|
24
24
|
/** Text/icon colour on a `primary` surface. Maps to `--primary-foreground`. */
|
|
25
25
|
primaryForeground?: string
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Primary button fill on hover. Maps to `--primary-hover`. A theme that sets
|
|
28
|
+
* `primary` but never `primaryHover` gets `primary` at 90 % (the pre-role
|
|
29
|
+
* hover) from {@link resolveThemeVars}.
|
|
30
|
+
*/
|
|
31
|
+
primaryHover?: string
|
|
32
|
+
/**
|
|
33
|
+
* Link and interactive text. Maps to `--link`. A theme that sets `primary`
|
|
34
|
+
* but never `link` gets links in `primary` (the pre-role link colour).
|
|
35
|
+
*/
|
|
36
|
+
link?: string
|
|
37
|
+
/** Hover/selection highlight surface. Maps to `--accent`. */
|
|
27
38
|
accent?: string
|
|
28
39
|
/** Text colour on an `accent` surface. Maps to `--accent-foreground`. */
|
|
29
40
|
accentForeground?: string
|
|
@@ -35,14 +46,92 @@ export interface ThemeTokens {
|
|
|
35
46
|
card?: string
|
|
36
47
|
/** Text colour on a `card` surface. Maps to `--card-foreground`. */
|
|
37
48
|
cardForeground?: string
|
|
38
|
-
/**
|
|
49
|
+
/** Menu, select and dialog surface. Maps to `--popover`. */
|
|
50
|
+
popover?: string
|
|
51
|
+
/** Quiet surface (table heads, skeletons, tab rails). Maps to `--muted`. */
|
|
52
|
+
muted?: string
|
|
53
|
+
/** Secondary text — must keep 4.5:1 on `muted`. Maps to `--muted-foreground`. */
|
|
54
|
+
mutedForeground?: string
|
|
55
|
+
/** Secondary button / badge surface. Maps to `--secondary`. */
|
|
56
|
+
secondary?: string
|
|
57
|
+
/** Decorative dividers and outlines (no contrast claim). Maps to `--border`. */
|
|
39
58
|
border?: string
|
|
40
|
-
/**
|
|
59
|
+
/** Input border — must keep 3:1 on its surface. Maps to `--input`. */
|
|
60
|
+
input?: string
|
|
61
|
+
/** Legacy focus-ring colour (shadcn). Maps to `--ring`. */
|
|
41
62
|
ring?: string
|
|
42
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Focus ring and focused-input border, opaque, 3:1. Maps to `--focus`. A
|
|
65
|
+
* theme that sets `ring` but never `focus` gets its focus ring in `ring`.
|
|
66
|
+
*/
|
|
67
|
+
focus?: string
|
|
68
|
+
/** Destructive action fill. Maps to `--destructive`. */
|
|
69
|
+
destructive?: string
|
|
70
|
+
/**
|
|
71
|
+
* Text/icon colour on a `destructive` fill (white by default; keep 4.5:1).
|
|
72
|
+
* Maps to `--destructive-foreground`.
|
|
73
|
+
*/
|
|
74
|
+
destructiveForeground?: string
|
|
75
|
+
/** Success colour — fill, dot, border, icon (never text on light). Maps to `--success`. */
|
|
76
|
+
success?: string
|
|
77
|
+
/** Success tint surface. Maps to `--success-soft`. */
|
|
78
|
+
successSoft?: string
|
|
79
|
+
/** Text colour of the success tone. Maps to `--success-ink`. */
|
|
80
|
+
successInk?: string
|
|
81
|
+
/** Warning colour — fill, dot, border, icon (never text on light). Maps to `--warning`. */
|
|
82
|
+
warning?: string
|
|
83
|
+
/** Warning tint surface. Maps to `--warning-soft`. */
|
|
84
|
+
warningSoft?: string
|
|
85
|
+
/** Text colour of the warning tone. Maps to `--warning-ink`. */
|
|
86
|
+
warningInk?: string
|
|
87
|
+
/** Danger colour — fill, dot, border, icon. Maps to `--danger`. */
|
|
88
|
+
danger?: string
|
|
89
|
+
/** Danger tint surface. Maps to `--danger-soft`. */
|
|
90
|
+
dangerSoft?: string
|
|
91
|
+
/** Text colour of the danger tone. Maps to `--danger-ink`. */
|
|
92
|
+
dangerInk?: string
|
|
93
|
+
/** Info colour — fill, dot, border, icon. Maps to `--info`. */
|
|
94
|
+
info?: string
|
|
95
|
+
/** Info tint surface. Maps to `--info-soft`. */
|
|
96
|
+
infoSoft?: string
|
|
97
|
+
/** Text colour of the info tone. Maps to `--info-ink`. */
|
|
98
|
+
infoInk?: string
|
|
99
|
+
/**
|
|
100
|
+
* Base corner radius (the legacy shadcn knob). Drives `rounded-xl` and up;
|
|
101
|
+
* applied through a theme ({@link ThemeProvider}, {@link resolveThemeVars}
|
|
102
|
+
* or `toStyle()`) it also derives `radiusSm`/`radiusMd`/`radiusLg` when those
|
|
103
|
+
* are not set. In plain CSS `--radius` no longer reshapes `rounded-sm/md/lg`:
|
|
104
|
+
* set `--radius-sm`/`--radius-md`/`--radius-lg` there. Maps to `--radius`.
|
|
105
|
+
*/
|
|
43
106
|
radius?: string
|
|
107
|
+
/** Badges and chips. Maps to `--radius-sm` (`rounded-sm`). */
|
|
108
|
+
radiusSm?: string
|
|
109
|
+
/** The standard: buttons, inputs, cards, dialogs. Maps to `--radius-md` (`rounded-md`). */
|
|
110
|
+
radiusMd?: string
|
|
111
|
+
/** Large panels and tiles. Maps to `--radius-lg` (`rounded-lg`). */
|
|
112
|
+
radiusLg?: string
|
|
113
|
+
/** Pills and avatars. Maps to `--radius-pill` (`rounded-pill`). */
|
|
114
|
+
radiusPill?: string
|
|
115
|
+
/** Subtle elevation. Maps to `--shadow-1` (`shadow-1`). */
|
|
116
|
+
shadow1?: string
|
|
117
|
+
/** Cards, hovered primary buttons. Maps to `--shadow-2` (`shadow-2`). */
|
|
118
|
+
shadow2?: string
|
|
119
|
+
/** Popovers and dialogs. Maps to `--shadow-3` (`shadow-3`). */
|
|
120
|
+
shadow3?: string
|
|
121
|
+
/** Dialog/sheet backdrop. Maps to `--scrim` (`bg-scrim`). */
|
|
122
|
+
scrim?: string
|
|
123
|
+
/** Hover-speed transitions. Maps to `--motion-fast` (the default duration). */
|
|
124
|
+
motionFast?: string
|
|
125
|
+
/** Standard transitions. Maps to `--motion-base`. */
|
|
126
|
+
motionBase?: string
|
|
127
|
+
/** Large enter/leave transitions. Maps to `--motion-slow`. */
|
|
128
|
+
motionSlow?: string
|
|
129
|
+
/** Easing curve. Maps to `--ease` (the default timing function). */
|
|
130
|
+
ease?: string
|
|
44
131
|
/** Body font-family list. Maps to `--font-sans`. */
|
|
45
132
|
fontSans?: string
|
|
133
|
+
/** Code, ids and stack traces. Maps to `--font-mono`. */
|
|
134
|
+
fontMono?: string
|
|
46
135
|
/** Heading font-family list. Maps to `--font-heading`. */
|
|
47
136
|
fontHeading?: string
|
|
48
137
|
}
|
|
@@ -51,27 +140,147 @@ export interface ThemeTokens {
|
|
|
51
140
|
type CssVarName = `--${string}`
|
|
52
141
|
|
|
53
142
|
/**
|
|
54
|
-
* The single source of truth mapping each
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
143
|
+
* The single source of truth mapping each {@link ThemeTokens} field to its real
|
|
144
|
+
* CSS variable name in `theme.css`. `tokensToVars` walks this map, so adding a
|
|
145
|
+
* field here (and to {@link ThemeTokens}) is all that is needed to extend the
|
|
146
|
+
* public theming surface.
|
|
58
147
|
*/
|
|
59
148
|
export const TOKEN_TO_CSS_VAR = {
|
|
60
149
|
primary: "--primary",
|
|
61
150
|
primaryForeground: "--primary-foreground",
|
|
151
|
+
primaryHover: "--primary-hover",
|
|
152
|
+
link: "--link",
|
|
62
153
|
accent: "--accent",
|
|
63
154
|
accentForeground: "--accent-foreground",
|
|
64
155
|
background: "--background",
|
|
65
156
|
foreground: "--foreground",
|
|
66
157
|
card: "--card",
|
|
67
158
|
cardForeground: "--card-foreground",
|
|
159
|
+
popover: "--popover",
|
|
160
|
+
muted: "--muted",
|
|
161
|
+
mutedForeground: "--muted-foreground",
|
|
162
|
+
secondary: "--secondary",
|
|
68
163
|
border: "--border",
|
|
164
|
+
input: "--input",
|
|
69
165
|
ring: "--ring",
|
|
166
|
+
focus: "--focus",
|
|
167
|
+
destructive: "--destructive",
|
|
168
|
+
destructiveForeground: "--destructive-foreground",
|
|
169
|
+
success: "--success",
|
|
170
|
+
successSoft: "--success-soft",
|
|
171
|
+
successInk: "--success-ink",
|
|
172
|
+
warning: "--warning",
|
|
173
|
+
warningSoft: "--warning-soft",
|
|
174
|
+
warningInk: "--warning-ink",
|
|
175
|
+
danger: "--danger",
|
|
176
|
+
dangerSoft: "--danger-soft",
|
|
177
|
+
dangerInk: "--danger-ink",
|
|
178
|
+
info: "--info",
|
|
179
|
+
infoSoft: "--info-soft",
|
|
180
|
+
infoInk: "--info-ink",
|
|
70
181
|
radius: "--radius",
|
|
182
|
+
radiusSm: "--radius-sm",
|
|
183
|
+
radiusMd: "--radius-md",
|
|
184
|
+
radiusLg: "--radius-lg",
|
|
185
|
+
radiusPill: "--radius-pill",
|
|
186
|
+
shadow1: "--shadow-1",
|
|
187
|
+
shadow2: "--shadow-2",
|
|
188
|
+
shadow3: "--shadow-3",
|
|
189
|
+
scrim: "--scrim",
|
|
190
|
+
motionFast: "--motion-fast",
|
|
191
|
+
motionBase: "--motion-base",
|
|
192
|
+
motionSlow: "--motion-slow",
|
|
193
|
+
ease: "--ease",
|
|
71
194
|
fontSans: "--font-sans",
|
|
195
|
+
fontMono: "--font-mono",
|
|
72
196
|
fontHeading: "--font-heading",
|
|
73
197
|
} as const satisfies Record<keyof ThemeTokens, CssVarName>
|
|
74
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The role-token contract: every variable the toolkit defines for components
|
|
201
|
+
* to use (`theme.css`), in light and — for colours — dark. Components in this
|
|
202
|
+
* package, and in apps built on it, style through these names only, never
|
|
203
|
+
* through raw colours or Tailwind palette classes.
|
|
204
|
+
*/
|
|
205
|
+
export const ROLE_TOKENS = [
|
|
206
|
+
"--background",
|
|
207
|
+
"--card",
|
|
208
|
+
"--popover",
|
|
209
|
+
"--muted",
|
|
210
|
+
"--secondary",
|
|
211
|
+
"--foreground",
|
|
212
|
+
"--muted-foreground",
|
|
213
|
+
"--border",
|
|
214
|
+
"--input",
|
|
215
|
+
"--primary",
|
|
216
|
+
"--primary-foreground",
|
|
217
|
+
"--primary-hover",
|
|
218
|
+
"--link",
|
|
219
|
+
"--ring",
|
|
220
|
+
"--focus",
|
|
221
|
+
"--success",
|
|
222
|
+
"--warning",
|
|
223
|
+
"--danger",
|
|
224
|
+
"--info",
|
|
225
|
+
"--success-soft",
|
|
226
|
+
"--warning-soft",
|
|
227
|
+
"--danger-soft",
|
|
228
|
+
"--info-soft",
|
|
229
|
+
"--success-ink",
|
|
230
|
+
"--warning-ink",
|
|
231
|
+
"--danger-ink",
|
|
232
|
+
"--info-ink",
|
|
233
|
+
"--destructive",
|
|
234
|
+
"--radius-sm",
|
|
235
|
+
"--radius-md",
|
|
236
|
+
"--radius-lg",
|
|
237
|
+
"--radius-pill",
|
|
238
|
+
"--shadow-1",
|
|
239
|
+
"--shadow-2",
|
|
240
|
+
"--shadow-3",
|
|
241
|
+
"--scrim",
|
|
242
|
+
"--motion-fast",
|
|
243
|
+
"--motion-base",
|
|
244
|
+
"--motion-slow",
|
|
245
|
+
"--ease",
|
|
246
|
+
"--font-sans",
|
|
247
|
+
"--font-mono",
|
|
248
|
+
] as const satisfies readonly CssVarName[]
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Variables whose toolkit value is the same in light and dark (theme.css
|
|
252
|
+
* declares them once, or identically in both modes). A theme's LIGHT value for
|
|
253
|
+
* one of these also applies in dark mode — a brand's button colour, radius or
|
|
254
|
+
* font does not change with the mode. Every other variable (surfaces, text,
|
|
255
|
+
* links, focus, status tones) takes its dark value from the theme's `dark`
|
|
256
|
+
* tokens or, failing that, from the toolkit's dark defaults — never from the
|
|
257
|
+
* light value, which would put light surfaces into a dark scope.
|
|
258
|
+
* Pinned against theme.css by `theme-css.test.ts`.
|
|
259
|
+
*/
|
|
260
|
+
export const MODE_INVARIANT_VARS: ReadonlySet<CssVarName> = new Set<CssVarName>([
|
|
261
|
+
"--primary",
|
|
262
|
+
"--primary-foreground",
|
|
263
|
+
"--primary-hover",
|
|
264
|
+
"--destructive",
|
|
265
|
+
"--destructive-foreground",
|
|
266
|
+
"--input",
|
|
267
|
+
"--radius",
|
|
268
|
+
"--radius-sm",
|
|
269
|
+
"--radius-md",
|
|
270
|
+
"--radius-lg",
|
|
271
|
+
"--radius-pill",
|
|
272
|
+
"--shadow-1",
|
|
273
|
+
"--shadow-2",
|
|
274
|
+
"--shadow-3",
|
|
275
|
+
"--motion-fast",
|
|
276
|
+
"--motion-base",
|
|
277
|
+
"--motion-slow",
|
|
278
|
+
"--ease",
|
|
279
|
+
"--font-sans",
|
|
280
|
+
"--font-mono",
|
|
281
|
+
"--font-heading",
|
|
282
|
+
])
|
|
283
|
+
|
|
75
284
|
/** A serializable map of CSS custom properties (`--primary` → value). */
|
|
76
285
|
export type ThemeVars = Record<CssVarName, string>
|
|
77
286
|
|
|
@@ -85,14 +294,18 @@ export type ThemeVars = Record<CssVarName, string>
|
|
|
85
294
|
* logged, or persisted and rehydrated.
|
|
86
295
|
*/
|
|
87
296
|
export interface ThemeDefinition {
|
|
88
|
-
/** CSS variables applied at the theme scope (overrides `:root`). */
|
|
297
|
+
/** CSS variables applied at the theme scope in light mode (overrides `:root`). */
|
|
89
298
|
vars: ThemeVars
|
|
90
|
-
/** Dark-mode CSS variables, applied
|
|
299
|
+
/** Dark-mode CSS variables, applied at the theme scope in dark mode. */
|
|
91
300
|
darkVars?: ThemeVars
|
|
92
301
|
/**
|
|
93
|
-
* The light
|
|
94
|
-
* valid `CSSProperties` keys)
|
|
95
|
-
*
|
|
302
|
+
* The theme's light-mode variables as a React `style` object (CSS custom
|
|
303
|
+
* properties are valid `CSSProperties` keys): exactly what
|
|
304
|
+
* {@link ThemeProvider} applies in light mode, i.e.
|
|
305
|
+
* {@link resolveThemeVars}`(theme, "light")` — {@link vars} plus the values
|
|
306
|
+
* derived from the legacy knobs (sm/md/lg from `radius`, …). Handy for
|
|
307
|
+
* inlining onto a wrapper element without the provider; for dark mode use
|
|
308
|
+
* {@link resolveThemeVars}`(theme, "dark")`.
|
|
96
309
|
*/
|
|
97
310
|
toStyle(): CSSProperties
|
|
98
311
|
}
|
|
@@ -119,29 +332,30 @@ export function tokensToVars(tokens: ThemeTokens): ThemeVars {
|
|
|
119
332
|
/** Options for {@link createTheme}. */
|
|
120
333
|
export interface CreateThemeOptions {
|
|
121
334
|
/**
|
|
122
|
-
* Dark-mode token overrides.
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
335
|
+
* Dark-mode token overrides. In dark mode a theme scope gets the toolkit's
|
|
336
|
+
* dark defaults, the theme's mode-invariant light tokens (brand button
|
|
337
|
+
* colour, radii, fonts — see {@link MODE_INVARIANT_VARS}) and these. Provide
|
|
338
|
+
* what differs in dark — typically `background`, `card`, `link`.
|
|
126
339
|
*/
|
|
127
340
|
dark?: ThemeTokens
|
|
128
341
|
}
|
|
129
342
|
|
|
130
343
|
/**
|
|
131
|
-
* Build a serializable {@link ThemeDefinition} from a
|
|
344
|
+
* Build a serializable {@link ThemeDefinition} from a set of brand
|
|
132
345
|
* {@link ThemeTokens}. This is the one switch a white-label client flips: pass
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* `
|
|
346
|
+
* the brand's roles (and, optionally, dark overrides), and every toolkit
|
|
347
|
+
* primitive that reads the role tokens — `Card`, `Button`, `Badge`, `bg-card`,
|
|
348
|
+
* `text-link`, `rounded-md` — re-skins to match. Without a theme the toolkit
|
|
349
|
+
* renders in the Miragon theme.
|
|
136
350
|
*
|
|
137
351
|
* Pure and side-effect-free: it only maps fields to CSS-variable names. Apply
|
|
138
352
|
* the result with {@link ThemeProvider} (recommended) or by spreading
|
|
139
|
-
* `theme
|
|
353
|
+
* {@link resolveThemeVars}`(theme, mode)` onto a wrapper element.
|
|
140
354
|
*
|
|
141
355
|
* @example
|
|
142
356
|
* const brand = createTheme(
|
|
143
|
-
* { primary: "
|
|
144
|
-
* { dark: {
|
|
357
|
+
* { primary: "#0f766e", link: "#0f766e", focus: "#0f766e", radiusMd: "6px" },
|
|
358
|
+
* { dark: { link: "#5eead4", focus: "#5eead4" } },
|
|
145
359
|
* )
|
|
146
360
|
* // <ThemeProvider theme={brand}>…</ThemeProvider>
|
|
147
361
|
*/
|
|
@@ -150,9 +364,67 @@ export function createTheme(tokens: ThemeTokens, opts?: CreateThemeOptions): The
|
|
|
150
364
|
const darkTokens = opts?.dark
|
|
151
365
|
const darkVars = darkTokens ? tokensToVars(darkTokens) : undefined
|
|
152
366
|
|
|
153
|
-
|
|
367
|
+
const theme: ThemeDefinition = {
|
|
154
368
|
vars,
|
|
155
369
|
...(darkVars && Object.keys(darkVars).length > 0 ? { darkVars } : {}),
|
|
156
|
-
toStyle: () =>
|
|
370
|
+
toStyle: () => resolveThemeVars(theme, "light"),
|
|
371
|
+
}
|
|
372
|
+
return theme
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Whether the theme sets `name` itself, in light or in dark. */
|
|
376
|
+
function declares(theme: ThemeDefinition, name: CssVarName): boolean {
|
|
377
|
+
return name in theme.vars || (theme.darkVars !== undefined && name in theme.darkVars)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Fill the roles a theme written against the pre-role API leaves unset, from
|
|
382
|
+
* the knobs it does set, so it keeps ONE brand instead of half Miragon:
|
|
383
|
+
*
|
|
384
|
+
* - the base `--radius` reshapes the sm/md/lg steps (CI ratio 6 : 12 : 16);
|
|
385
|
+
* - `--primary` gives `--primary-hover` (90 %, the old `bg-primary/90`) and
|
|
386
|
+
* `--link` (the old link button was `text-primary`);
|
|
387
|
+
* - `--ring` gives `--focus` (the old focus ring).
|
|
388
|
+
*
|
|
389
|
+
* A role the theme sets itself — in either mode — is never derived, and an
|
|
390
|
+
* explicit value always wins.
|
|
391
|
+
*/
|
|
392
|
+
function withDerivedVars(vars: ThemeVars, theme: ThemeDefinition): ThemeVars {
|
|
393
|
+
const derived: ThemeVars = {}
|
|
394
|
+
const radius = vars["--radius"]
|
|
395
|
+
if (radius) {
|
|
396
|
+
derived["--radius-sm"] = `calc(${radius} * 0.5)`
|
|
397
|
+
derived["--radius-md"] = radius
|
|
398
|
+
derived["--radius-lg"] = `calc(${radius} * 4 / 3)`
|
|
399
|
+
}
|
|
400
|
+
const primary = vars["--primary"]
|
|
401
|
+
if (primary && !declares(theme, "--primary-hover")) {
|
|
402
|
+
derived["--primary-hover"] = `color-mix(in srgb, ${primary} 90%, transparent)`
|
|
403
|
+
}
|
|
404
|
+
if (primary && !declares(theme, "--link")) derived["--link"] = primary
|
|
405
|
+
const ring = vars["--ring"]
|
|
406
|
+
if (ring && !declares(theme, "--focus")) derived["--focus"] = ring
|
|
407
|
+
return { ...derived, ...vars }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The CSS variables a theme applies at its scope in `mode`. Light mode: the
|
|
412
|
+
* theme's `vars`. Dark mode: its mode-invariant light vars plus its `darkVars`
|
|
413
|
+
* — never the other light vars, which would beat the toolkit's dark defaults
|
|
414
|
+
* (mcp-toolkit#178 N35). Both modes add what the legacy knobs derive (a base
|
|
415
|
+
* `--radius` → sm/md/lg; `--primary` → hover and link; `--ring` → focus)
|
|
416
|
+
* where the theme does not set those roles itself.
|
|
417
|
+
* `null`/`undefined` yields `{}` (the toolkit defaults apply).
|
|
418
|
+
*/
|
|
419
|
+
export function resolveThemeVars(
|
|
420
|
+
theme: ThemeDefinition | null | undefined,
|
|
421
|
+
mode: "light" | "dark",
|
|
422
|
+
): ThemeVars {
|
|
423
|
+
if (!theme) return {}
|
|
424
|
+
if (mode === "light") return withDerivedVars(theme.vars, theme)
|
|
425
|
+
const carried: ThemeVars = {}
|
|
426
|
+
for (const [name, value] of Object.entries(theme.vars) as [CssVarName, string][]) {
|
|
427
|
+
if (MODE_INVARIANT_VARS.has(name)) carried[name] = value
|
|
157
428
|
}
|
|
429
|
+
return withDerivedVars({ ...carried, ...theme.darkVars }, theme)
|
|
158
430
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { describe, expect, it } from "vitest"
|
|
4
|
+
import { GEIST_FONT_FILES, fontFaces } from "./fonts.js"
|
|
5
|
+
|
|
6
|
+
describe("fontFaces (opt-in Geist by URL)", () => {
|
|
7
|
+
// Built inside each test: a describe-scope value is computed once at
|
|
8
|
+
// collection, outside any mutant Stryker activates.
|
|
9
|
+
const css = () => fontFaces({ baseUrl: "https://mcp.example.com/fonts" })
|
|
10
|
+
|
|
11
|
+
it("declares Geist and Geist Mono — the families the font tokens name first", () => {
|
|
12
|
+
expect(css()).toContain('font-family: "Geist";')
|
|
13
|
+
expect(css()).toContain('font-family: "Geist Mono";')
|
|
14
|
+
expect(css().match(/@font-face \{/g)).toHaveLength(2)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("points at the latin variable woff2 files under the base URL", () => {
|
|
18
|
+
expect(css()).toContain(
|
|
19
|
+
'src: url("https://mcp.example.com/fonts/geist-latin-wght-normal.woff2") format("woff2");',
|
|
20
|
+
)
|
|
21
|
+
expect(css()).toContain(
|
|
22
|
+
'src: url("https://mcp.example.com/fonts/geist-mono-latin-wght-normal.woff2") format("woff2");',
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("covers the whole weight axis, swaps by default and limits itself to latin", () => {
|
|
27
|
+
expect(css().match(/font-weight: 100 900;/g)).toHaveLength(2)
|
|
28
|
+
expect(css().match(/font-display: swap;/g)).toHaveLength(2)
|
|
29
|
+
expect(css().match(/font-style: normal;/g)).toHaveLength(2)
|
|
30
|
+
expect(css().match(/unicode-range: U\+0000-00FF,/g)).toHaveLength(2)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("renders two complete @font-face rules, one declaration per line", () => {
|
|
34
|
+
const range =
|
|
35
|
+
"U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD"
|
|
36
|
+
const rule = (family: string, file: string) =>
|
|
37
|
+
[
|
|
38
|
+
"@font-face {",
|
|
39
|
+
` font-family: "${family}";`,
|
|
40
|
+
" font-style: normal;",
|
|
41
|
+
" font-display: swap;",
|
|
42
|
+
" font-weight: 100 900;",
|
|
43
|
+
` src: url("/f/${file}") format("woff2");`,
|
|
44
|
+
` unicode-range: ${range};`,
|
|
45
|
+
"}",
|
|
46
|
+
].join("\n")
|
|
47
|
+
expect(fontFaces({ baseUrl: "/f" })).toBe(
|
|
48
|
+
`${rule("Geist", "geist-latin-wght-normal.woff2")}\n${rule("Geist Mono", "geist-mono-latin-wght-normal.woff2")}`,
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("strips every leading slash of a file name but keeps its inner path", () => {
|
|
53
|
+
const out = fontFaces({ baseUrl: "/f", files: { sans: "//a.woff2", mono: "sub/b.woff2" } })
|
|
54
|
+
expect(out).toContain('url("/f/a.woff2")')
|
|
55
|
+
expect(out).toContain('url("/f/sub/b.woff2")')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("joins base URL and file name with exactly one slash", () => {
|
|
59
|
+
const out = fontFaces({ baseUrl: "/fonts///", files: { sans: "/a.woff2", mono: "b.woff2" } })
|
|
60
|
+
expect(out).toContain('url("/fonts/a.woff2")')
|
|
61
|
+
expect(out).toContain('url("/fonts/b.woff2")')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("honours a custom font-display strategy", () => {
|
|
65
|
+
expect(fontFaces({ baseUrl: "/f", display: "optional" })).toContain("font-display: optional;")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('refuses URLs that could break out of url("…")', () => {
|
|
69
|
+
for (const baseUrl of ['/f")}body{', "/f'x", "/f x", "/f(", "/f\\", "/f<x>"]) {
|
|
70
|
+
expect(() => fontFaces({ baseUrl }), baseUrl).toThrow(/refusing a font URL/)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("names the files @fontsource-variable ships, so a server can copy them as-is", () => {
|
|
75
|
+
for (const [pkg, file] of [
|
|
76
|
+
["@fontsource-variable/geist", GEIST_FONT_FILES.sans],
|
|
77
|
+
["@fontsource-variable/geist-mono", GEIST_FONT_FILES.mono],
|
|
78
|
+
] as const) {
|
|
79
|
+
const shipped = readFileSync(
|
|
80
|
+
new URL(`../../node_modules/${pkg}/files/${file}`, import.meta.url),
|
|
81
|
+
)
|
|
82
|
+
expect(shipped.byteLength, `${pkg}/files/${file}`).toBeGreaterThan(1000)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in Geist delivery by URL. The toolkit's font tokens name "Geist" and
|
|
3
|
+
* "Geist Mono" first but load no font: an app either imports
|
|
4
|
+
* `@miragon/mcp-toolkit-ui/fonts.css` (bundled by the build) or serves the
|
|
5
|
+
* woff2 files itself and declares them with {@link fontFaces}. The second path
|
|
6
|
+
* keeps MCP App views small — the files are fetched once and cached instead of
|
|
7
|
+
* inlined as base64 into every view; allow their origin in the view's CSP
|
|
8
|
+
* `resourceDomains`. Where the fonts never load, the host's font renders (if
|
|
9
|
+
* the host sends one, see `adoptHostFontVariables`), else the system stack.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The latin variable woff2 files, as `@fontsource-variable/geist(-mono)` ship them. */
|
|
13
|
+
export const GEIST_FONT_FILES = {
|
|
14
|
+
sans: "geist-latin-wght-normal.woff2",
|
|
15
|
+
mono: "geist-mono-latin-wght-normal.woff2",
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
/** The latin subset both files cover (identical to fontsource's `latin` range). */
|
|
19
|
+
const LATIN_UNICODE_RANGE =
|
|
20
|
+
"U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD"
|
|
21
|
+
|
|
22
|
+
/** Options for {@link fontFaces}. */
|
|
23
|
+
export interface FontFacesOptions {
|
|
24
|
+
/** Where the woff2 files are served from, e.g. `https://mcp.example.com/fonts`. */
|
|
25
|
+
baseUrl: string
|
|
26
|
+
/** File names under `baseUrl`. Defaults to {@link GEIST_FONT_FILES}. */
|
|
27
|
+
files?: { sans?: string; mono?: string }
|
|
28
|
+
/** `font-display` strategy. Default `"swap"`: text renders at once in the fallback. */
|
|
29
|
+
display?: "auto" | "block" | "swap" | "fallback" | "optional"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Characters that could break out of the CSS `url("…")` the helper writes. */
|
|
33
|
+
const UNSAFE_URL_CHARS = /["'\\()\s<>]/
|
|
34
|
+
|
|
35
|
+
function fontUrl(baseUrl: string, file: string): string {
|
|
36
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/${file.replace(/^\/+/, "")}`
|
|
37
|
+
if (UNSAFE_URL_CHARS.test(url)) {
|
|
38
|
+
throw new Error(`fontFaces: refusing a font URL with quote, bracket or whitespace: ${url}`)
|
|
39
|
+
}
|
|
40
|
+
return url
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function face(family: string, url: string, display: string): string {
|
|
44
|
+
return [
|
|
45
|
+
"@font-face {",
|
|
46
|
+
` font-family: "${family}";`,
|
|
47
|
+
" font-style: normal;",
|
|
48
|
+
` font-display: ${display};`,
|
|
49
|
+
" font-weight: 100 900;",
|
|
50
|
+
` src: url("${url}") format("woff2");`,
|
|
51
|
+
` unicode-range: ${LATIN_UNICODE_RANGE};`,
|
|
52
|
+
"}",
|
|
53
|
+
].join("\n")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The `@font-face` rules for Geist and Geist Mono (latin, variable weight
|
|
58
|
+
* 100–900) served from `baseUrl`. Pure — inject the string where your app
|
|
59
|
+
* builds its document, e.g. `<style>{fontFaces({ baseUrl })}</style>`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* fontFaces({ baseUrl: "https://mcp.example.com/fonts" })
|
|
63
|
+
* // @font-face { font-family: "Geist"; … src: url("https://mcp.example.com/fonts/geist-latin-wght-normal.woff2") … }
|
|
64
|
+
*/
|
|
65
|
+
export function fontFaces({ baseUrl, files, display = "swap" }: FontFacesOptions): string {
|
|
66
|
+
const sans = files?.sans ?? GEIST_FONT_FILES.sans
|
|
67
|
+
const mono = files?.mono ?? GEIST_FONT_FILES.mono
|
|
68
|
+
return [
|
|
69
|
+
face("Geist", fontUrl(baseUrl, sans), display),
|
|
70
|
+
face("Geist Mono", fontUrl(baseUrl, mono), display),
|
|
71
|
+
].join("\n")
|
|
72
|
+
}
|