@miragon/mcp-toolkit-ui 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/app/adapt-data-widget.d.ts +33 -0
- package/dist/app/adapt-data-widget.d.ts.map +1 -0
- package/dist/app/adapt-data-widget.js +35 -0
- package/dist/app/adapt-data-widget.js.map +1 -0
- package/dist/app/builder/CatalogueSheet.d.ts +13 -0
- package/dist/app/builder/CatalogueSheet.d.ts.map +1 -0
- package/dist/app/builder/CatalogueSheet.js +33 -0
- package/dist/app/builder/CatalogueSheet.js.map +1 -0
- package/dist/app/builder/PipelineStrip.d.ts +22 -0
- package/dist/app/builder/PipelineStrip.d.ts.map +1 -0
- package/dist/app/builder/PipelineStrip.js +31 -0
- package/dist/app/builder/PipelineStrip.js.map +1 -0
- package/dist/app/builder/PreviewPane.d.ts +13 -0
- package/dist/app/builder/PreviewPane.d.ts.map +1 -0
- package/dist/app/builder/PreviewPane.js +21 -0
- package/dist/app/builder/PreviewPane.js.map +1 -0
- package/dist/app/builder/SaveDialog.d.ts +14 -0
- package/dist/app/builder/SaveDialog.d.ts.map +1 -0
- package/dist/app/builder/SaveDialog.js +14 -0
- package/dist/app/builder/SaveDialog.js.map +1 -0
- package/dist/app/builder/Toolbar.d.ts +17 -0
- package/dist/app/builder/Toolbar.d.ts.map +1 -0
- package/dist/app/builder/Toolbar.js +15 -0
- package/dist/app/builder/Toolbar.js.map +1 -0
- package/dist/app/builder/Workspace.d.ts +28 -0
- package/dist/app/builder/Workspace.d.ts.map +1 -0
- package/dist/app/builder/Workspace.js +117 -0
- package/dist/app/builder/Workspace.js.map +1 -0
- package/dist/app/builder/builder-model.d.ts +86 -0
- package/dist/app/builder/builder-model.d.ts.map +1 -0
- package/dist/app/builder/builder-model.js +174 -0
- package/dist/app/builder/builder-model.js.map +1 -0
- package/dist/app/builder/builder-reducer.d.ts +71 -0
- package/dist/app/builder/builder-reducer.d.ts.map +1 -0
- package/dist/app/builder/builder-reducer.js +201 -0
- package/dist/app/builder/builder-reducer.js.map +1 -0
- package/dist/app/builder/labels.d.ts +78 -0
- package/dist/app/builder/labels.d.ts.map +1 -0
- package/dist/app/builder/labels.js +71 -0
- package/dist/app/builder/labels.js.map +1 -0
- package/dist/app/builder/status-chip.d.ts +10 -0
- package/dist/app/builder/status-chip.d.ts.map +1 -0
- package/dist/app/builder/status-chip.js +16 -0
- package/dist/app/builder/status-chip.js.map +1 -0
- package/dist/app/host-bridge.d.ts +211 -0
- package/dist/app/host-bridge.d.ts.map +1 -0
- package/dist/app/host-bridge.js +247 -0
- package/dist/app/host-bridge.js.map +1 -0
- package/dist/app/index.d.ts +16 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +18 -0
- package/dist/app/index.js.map +1 -0
- package/dist/app/layout-builder.d.ts +48 -0
- package/dist/app/layout-builder.d.ts.map +1 -0
- package/dist/app/layout-builder.js +418 -0
- package/dist/app/layout-builder.js.map +1 -0
- package/dist/app/mcp-app-view.d.ts +43 -0
- package/dist/app/mcp-app-view.d.ts.map +1 -0
- package/dist/app/mcp-app-view.js +169 -0
- package/dist/app/mcp-app-view.js.map +1 -0
- package/dist/app/mcp-toolkit-app.d.ts +18 -0
- package/dist/app/mcp-toolkit-app.d.ts.map +1 -0
- package/dist/app/mcp-toolkit-app.js +22 -0
- package/dist/app/mcp-toolkit-app.js.map +1 -0
- package/dist/app/use-host-actions.d.ts +57 -0
- package/dist/app/use-host-actions.d.ts.map +1 -0
- package/dist/app/use-host-actions.js +42 -0
- package/dist/app/use-host-actions.js.map +1 -0
- package/dist/app/widget-fixture.d.ts +180 -0
- package/dist/app/widget-fixture.d.ts.map +1 -0
- package/dist/app/widget-fixture.js +273 -0
- package/dist/app/widget-fixture.js.map +1 -0
- package/dist/app/widget-props-sheet.d.ts +20 -0
- package/dist/app/widget-props-sheet.d.ts.map +1 -0
- package/dist/app/widget-props-sheet.js +87 -0
- package/dist/app/widget-props-sheet.js.map +1 -0
- package/dist/app/widget-renderer.d.ts +87 -0
- package/dist/app/widget-renderer.d.ts.map +1 -0
- package/dist/app/widget-renderer.js +105 -0
- package/dist/app/widget-renderer.js.map +1 -0
- package/dist/components/DrillButton.d.ts +22 -0
- package/dist/components/DrillButton.d.ts.map +1 -0
- package/dist/components/DrillButton.js +13 -0
- package/dist/components/DrillButton.js.map +1 -0
- package/dist/components/FilterBar.d.ts +28 -0
- package/dist/components/FilterBar.d.ts.map +1 -0
- package/dist/components/FilterBar.js +12 -0
- package/dist/components/FilterBar.js.map +1 -0
- package/dist/components/GridLayout.d.ts +26 -0
- package/dist/components/GridLayout.d.ts.map +1 -0
- package/dist/components/GridLayout.js +31 -0
- package/dist/components/GridLayout.js.map +1 -0
- package/dist/components/GroupCard.d.ts +21 -0
- package/dist/components/GroupCard.d.ts.map +1 -0
- package/dist/components/GroupCard.js +24 -0
- package/dist/components/GroupCard.js.map +1 -0
- package/dist/components/KpiGrid.d.ts +43 -0
- package/dist/components/KpiGrid.d.ts.map +1 -0
- package/dist/components/KpiGrid.js +35 -0
- package/dist/components/KpiGrid.js.map +1 -0
- package/dist/components/ListFooter.d.ts +22 -0
- package/dist/components/ListFooter.d.ts.map +1 -0
- package/dist/components/ListFooter.js +13 -0
- package/dist/components/ListFooter.js.map +1 -0
- package/dist/components/Pills.d.ts +25 -0
- package/dist/components/Pills.d.ts.map +1 -0
- package/dist/components/Pills.js +17 -0
- package/dist/components/Pills.js.map +1 -0
- package/dist/components/SectionHeading.d.ts +16 -0
- package/dist/components/SectionHeading.d.ts.map +1 -0
- package/dist/components/SectionHeading.js +10 -0
- package/dist/components/SectionHeading.js.map +1 -0
- package/dist/components/WidgetHeader.d.ts +21 -0
- package/dist/components/WidgetHeader.d.ts.map +1 -0
- package/dist/components/WidgetHeader.js +11 -0
- package/dist/components/WidgetHeader.js.map +1 -0
- package/dist/hooks/index.d.ts +11 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-debounced-value.d.ts +7 -0
- package/dist/hooks/use-debounced-value.d.ts.map +1 -0
- package/dist/hooks/use-debounced-value.js +15 -0
- package/dist/hooks/use-debounced-value.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +6 -0
- package/dist/hooks/use-mobile.d.ts.map +1 -0
- package/dist/hooks/use-mobile.js +19 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/hooks/use-tool-query.d.ts +21 -0
- package/dist/hooks/use-tool-query.d.ts.map +1 -0
- package/dist/hooks/use-tool-query.js +47 -0
- package/dist/hooks/use-tool-query.js.map +1 -0
- package/dist/hooks/use-tool-result-recovery.d.ts +54 -0
- package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -0
- package/dist/hooks/use-tool-result-recovery.js +93 -0
- package/dist/hooks/use-tool-result-recovery.js.map +1 -0
- package/dist/hooks/use-view-data.d.ts +19 -0
- package/dist/hooks/use-view-data.d.ts.map +1 -0
- package/dist/hooks/use-view-data.js +18 -0
- package/dist/hooks/use-view-data.js.map +1 -0
- package/dist/hooks/use-view-tool-query.d.ts +18 -0
- package/dist/hooks/use-view-tool-query.d.ts.map +1 -0
- package/dist/hooks/use-view-tool-query.js +29 -0
- package/dist/hooks/use-view-tool-query.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/parse-tool-result.d.ts +39 -0
- package/dist/lib/parse-tool-result.d.ts.map +1 -0
- package/dist/lib/parse-tool-result.js +66 -0
- package/dist/lib/parse-tool-result.js.map +1 -0
- package/dist/lib/tone-utils.d.ts +18 -0
- package/dist/lib/tone-utils.d.ts.map +1 -0
- package/dist/lib/tone-utils.js +25 -0
- package/dist/lib/tone-utils.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/locale/index.d.ts +3 -0
- package/dist/locale/index.d.ts.map +1 -0
- package/dist/locale/index.js +7 -0
- package/dist/locale/index.js.map +1 -0
- package/dist/locale/locale-provider.d.ts +50 -0
- package/dist/locale/locale-provider.d.ts.map +1 -0
- package/dist/locale/locale-provider.js +47 -0
- package/dist/locale/locale-provider.js.map +1 -0
- package/dist/primitives/alert.d.ts +10 -0
- package/dist/primitives/alert.d.ts.map +1 -0
- package/dist/primitives/alert.js +25 -0
- package/dist/primitives/alert.js.map +1 -0
- package/dist/primitives/badge.d.ts +8 -0
- package/dist/primitives/badge.d.ts.map +1 -0
- package/dist/primitives/badge.js +34 -0
- package/dist/primitives/badge.js.map +1 -0
- package/dist/primitives/button.d.ts +11 -0
- package/dist/primitives/button.d.ts.map +1 -0
- package/dist/primitives/button.js +36 -0
- package/dist/primitives/button.js.map +1 -0
- package/dist/primitives/card.d.ts +12 -0
- package/dist/primitives/card.d.ts.map +1 -0
- package/dist/primitives/card.js +25 -0
- package/dist/primitives/card.js.map +1 -0
- package/dist/primitives/dialog.d.ts +18 -0
- package/dist/primitives/dialog.d.ts.map +1 -0
- package/dist/primitives/dialog.js +38 -0
- package/dist/primitives/dialog.js.map +1 -0
- package/dist/primitives/input.d.ts +4 -0
- package/dist/primitives/input.d.ts.map +1 -0
- package/dist/primitives/input.js +7 -0
- package/dist/primitives/input.js.map +1 -0
- package/dist/primitives/scroll-area.d.ts +6 -0
- package/dist/primitives/scroll-area.d.ts.map +1 -0
- package/dist/primitives/scroll-area.js +11 -0
- package/dist/primitives/scroll-area.js.map +1 -0
- package/dist/primitives/select.d.ts +16 -0
- package/dist/primitives/select.d.ts.map +1 -0
- package/dist/primitives/select.js +38 -0
- package/dist/primitives/select.js.map +1 -0
- package/dist/primitives/separator.d.ts +4 -0
- package/dist/primitives/separator.d.ts.map +1 -0
- package/dist/primitives/separator.js +9 -0
- package/dist/primitives/separator.js.map +1 -0
- package/dist/primitives/sheet.d.ts +15 -0
- package/dist/primitives/sheet.d.ts.map +1 -0
- package/dist/primitives/sheet.js +41 -0
- package/dist/primitives/sheet.js.map +1 -0
- package/dist/primitives/skeleton.d.ts +3 -0
- package/dist/primitives/skeleton.d.ts.map +1 -0
- package/dist/primitives/skeleton.js +7 -0
- package/dist/primitives/skeleton.js.map +1 -0
- package/dist/primitives/switch.d.ts +5 -0
- package/dist/primitives/switch.d.ts.map +1 -0
- package/dist/primitives/switch.js +8 -0
- package/dist/primitives/switch.js.map +1 -0
- package/dist/primitives/table.d.ts +11 -0
- package/dist/primitives/table.d.ts.map +1 -0
- package/dist/primitives/table.js +28 -0
- package/dist/primitives/table.js.map +1 -0
- package/dist/primitives/tabs.d.ts +12 -0
- package/dist/primitives/tabs.d.ts.map +1 -0
- package/dist/primitives/tabs.js +29 -0
- package/dist/primitives/tabs.js.map +1 -0
- package/dist/providers/query-provider.d.ts +11 -0
- package/dist/providers/query-provider.d.ts.map +1 -0
- package/dist/providers/query-provider.js +20 -0
- package/dist/providers/query-provider.js.map +1 -0
- package/dist/theme/create-theme.d.ts +130 -0
- package/dist/theme/create-theme.d.ts.map +1 -0
- package/dist/theme/create-theme.js +65 -0
- package/dist/theme/create-theme.js.map +1 -0
- package/dist/theme/index.d.ts +7 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +7 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/presets.d.ts +25 -0
- package/dist/theme/presets.d.ts.map +1 -0
- package/dist/theme/presets.js +68 -0
- package/dist/theme/presets.js.map +1 -0
- package/dist/theme/theme-provider.d.ts +69 -0
- package/dist/theme/theme-provider.d.ts.map +1 -0
- package/dist/theme/theme-provider.js +79 -0
- package/dist/theme/theme-provider.js.map +1 -0
- package/package.json +71 -0
- package/src/app/adapt-data-widget.tsx +51 -0
- package/src/app/builder/CatalogueSheet.tsx +230 -0
- package/src/app/builder/PipelineStrip.tsx +260 -0
- package/src/app/builder/PreviewPane.tsx +72 -0
- package/src/app/builder/SaveDialog.tsx +95 -0
- package/src/app/builder/Toolbar.tsx +95 -0
- package/src/app/builder/Workspace.tsx +483 -0
- package/src/app/builder/builder-model.test.ts +351 -0
- package/src/app/builder/builder-model.ts +213 -0
- package/src/app/builder/builder-reducer.test.ts +441 -0
- package/src/app/builder/builder-reducer.ts +289 -0
- package/src/app/builder/labels.ts +139 -0
- package/src/app/builder/status-chip.tsx +37 -0
- package/src/app/host-bridge.test.ts +206 -0
- package/src/app/host-bridge.tsx +371 -0
- package/src/app/index.ts +51 -0
- package/src/app/layout-builder.tsx +872 -0
- package/src/app/mcp-app-view.tsx +367 -0
- package/src/app/mcp-toolkit-app.tsx +25 -0
- package/src/app/use-host-actions.test.ts +16 -0
- package/src/app/use-host-actions.tsx +88 -0
- package/src/app/widget-fixture.test.ts +108 -0
- package/src/app/widget-fixture.tsx +441 -0
- package/src/app/widget-props-sheet.tsx +285 -0
- package/src/app/widget-renderer.test.ts +42 -0
- package/src/app/widget-renderer.tsx +200 -0
- package/src/components/DrillButton.tsx +37 -0
- package/src/components/FilterBar.tsx +68 -0
- package/src/components/GridLayout.tsx +49 -0
- package/src/components/GroupCard.tsx +52 -0
- package/src/components/KpiGrid.tsx +134 -0
- package/src/components/ListFooter.tsx +48 -0
- package/src/components/Pills.tsx +45 -0
- package/src/components/SectionHeading.tsx +26 -0
- package/src/components/WidgetHeader.tsx +51 -0
- package/src/globals.css +168 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/use-debounced-value.ts +15 -0
- package/src/hooks/use-mobile.ts +24 -0
- package/src/hooks/use-tool-query.ts +63 -0
- package/src/hooks/use-tool-result-recovery.test.ts +65 -0
- package/src/hooks/use-tool-result-recovery.ts +144 -0
- package/src/hooks/use-view-data.ts +31 -0
- package/src/hooks/use-view-tool-query.ts +38 -0
- package/src/index.ts +134 -0
- package/src/lib/parse-tool-result.test.ts +139 -0
- package/src/lib/parse-tool-result.ts +89 -0
- package/src/lib/tone-utils.test.ts +35 -0
- package/src/lib/tone-utils.ts +38 -0
- package/src/lib/utils.ts +6 -0
- package/src/locale/index.ts +7 -0
- package/src/locale/locale-provider.tsx +81 -0
- package/src/primitives/alert.tsx +60 -0
- package/src/primitives/badge.tsx +49 -0
- package/src/primitives/button.tsx +62 -0
- package/src/primitives/card.tsx +92 -0
- package/src/primitives/dialog.tsx +144 -0
- package/src/primitives/input.tsx +21 -0
- package/src/primitives/scroll-area.tsx +54 -0
- package/src/primitives/select.tsx +173 -0
- package/src/primitives/separator.tsx +21 -0
- package/src/primitives/sheet.tsx +134 -0
- package/src/primitives/skeleton.tsx +13 -0
- package/src/primitives/switch.tsx +24 -0
- package/src/primitives/table.tsx +87 -0
- package/src/primitives/tabs.tsx +79 -0
- package/src/providers/query-provider.tsx +38 -0
- package/src/theme/create-theme.test.ts +108 -0
- package/src/theme/create-theme.ts +158 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/presets.ts +82 -0
- package/src/theme/theme-provider.tsx +152 -0
- package/src/ui-catalog.test.ts +112 -0
- package/ui-catalog.json +1280 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react"
|
|
2
|
+
import { Button } from "../primitives/button.js"
|
|
3
|
+
import { Input } from "../primitives/input.js"
|
|
4
|
+
import {
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
} from "../primitives/select.js"
|
|
11
|
+
import {
|
|
12
|
+
Sheet,
|
|
13
|
+
SheetContent,
|
|
14
|
+
SheetDescription,
|
|
15
|
+
SheetHeader,
|
|
16
|
+
SheetTitle,
|
|
17
|
+
} from "../primitives/sheet.js"
|
|
18
|
+
import { Switch } from "../primitives/switch.js"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tiny JSON-Schema-driven editor for a layout cell's `props`. The
|
|
22
|
+
* LayoutBuilder opens this sheet when the user clicks "Configure" on a
|
|
23
|
+
* cell whose widget advertises a `propsSchema` in the framework manifest.
|
|
24
|
+
*
|
|
25
|
+
* Scope (v1):
|
|
26
|
+
* - Object-root schemas only.
|
|
27
|
+
* - Top-level properties: `string`, `number`, `integer`, `boolean`.
|
|
28
|
+
* - String enums render as a Select.
|
|
29
|
+
* - Nested objects/arrays/$ref/anyOf are not supported — the field
|
|
30
|
+
* shows an "edit JSON manually" hint so authors know to fall back.
|
|
31
|
+
*
|
|
32
|
+
* The sheet writes a clean `Record<string, unknown>` (empty values
|
|
33
|
+
* stripped) and reports `undefined` when the user clears every field, so
|
|
34
|
+
* the cell's `props` field stays absent rather than `{}` — matches the
|
|
35
|
+
* "no props" wire format used elsewhere.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
interface JsonSchemaProperty {
|
|
39
|
+
type?: string
|
|
40
|
+
description?: string
|
|
41
|
+
enum?: unknown[]
|
|
42
|
+
default?: unknown
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface JsonSchemaObject {
|
|
46
|
+
type?: string
|
|
47
|
+
properties?: Record<string, JsonSchemaProperty>
|
|
48
|
+
required?: string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WidgetPropsSheetLabels {
|
|
52
|
+
title?: string
|
|
53
|
+
description?: string
|
|
54
|
+
apply?: string
|
|
55
|
+
cancel?: string
|
|
56
|
+
reset?: string
|
|
57
|
+
noProps?: string
|
|
58
|
+
unsetOption?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const DEFAULT_LABELS: Required<WidgetPropsSheetLabels> = {
|
|
62
|
+
title: "Configure widget",
|
|
63
|
+
description: "Set per-instance props for this widget.",
|
|
64
|
+
apply: "Apply",
|
|
65
|
+
cancel: "Cancel",
|
|
66
|
+
reset: "Reset",
|
|
67
|
+
noProps: "This widget takes no per-instance props.",
|
|
68
|
+
unsetOption: "(unset)",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const UNSET_SENTINEL = "__unset__"
|
|
72
|
+
|
|
73
|
+
export interface WidgetPropsSheetProps {
|
|
74
|
+
open: boolean
|
|
75
|
+
onOpenChange: (open: boolean) => void
|
|
76
|
+
widgetId: string
|
|
77
|
+
schema: Record<string, unknown> | undefined
|
|
78
|
+
value: Record<string, unknown> | undefined
|
|
79
|
+
onApply: (next: Record<string, unknown> | undefined) => void
|
|
80
|
+
labels?: WidgetPropsSheetLabels
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function WidgetPropsSheet({
|
|
84
|
+
open,
|
|
85
|
+
onOpenChange,
|
|
86
|
+
widgetId,
|
|
87
|
+
schema,
|
|
88
|
+
value,
|
|
89
|
+
onApply,
|
|
90
|
+
labels,
|
|
91
|
+
}: WidgetPropsSheetProps) {
|
|
92
|
+
const L = { ...DEFAULT_LABELS, ...labels }
|
|
93
|
+
const obj = (schema ?? {}) as JsonSchemaObject
|
|
94
|
+
const [draft, setDraft] = useState<Record<string, unknown>>(value ?? {})
|
|
95
|
+
|
|
96
|
+
// Reset the form to the saved value on the open→true transition so a
|
|
97
|
+
// user who closes the sheet without applying gets their unsaved edits
|
|
98
|
+
// discarded next time. Render-time state update with a "prev" tracker
|
|
99
|
+
// is the React-recommended alternative to a setState-in-effect.
|
|
100
|
+
const [prevOpen, setPrevOpen] = useState(open)
|
|
101
|
+
if (open !== prevOpen) {
|
|
102
|
+
setPrevOpen(open)
|
|
103
|
+
if (open) setDraft(value ?? {})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const properties = obj.properties ?? {}
|
|
107
|
+
const propEntries = Object.entries(properties)
|
|
108
|
+
|
|
109
|
+
const handleApply = () => {
|
|
110
|
+
const cleaned: Record<string, unknown> = {}
|
|
111
|
+
for (const [k, v] of Object.entries(draft)) {
|
|
112
|
+
if (v === "" || v === undefined || v === null) continue
|
|
113
|
+
cleaned[k] = v
|
|
114
|
+
}
|
|
115
|
+
onApply(Object.keys(cleaned).length > 0 ? cleaned : undefined)
|
|
116
|
+
onOpenChange(false)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
121
|
+
<SheetContent className="w-[400px] sm:max-w-md">
|
|
122
|
+
<SheetHeader>
|
|
123
|
+
<SheetTitle>{L.title}</SheetTitle>
|
|
124
|
+
<SheetDescription>
|
|
125
|
+
<span className="bg-muted rounded px-1 font-mono text-xs">{widgetId}</span> —{" "}
|
|
126
|
+
{L.description}
|
|
127
|
+
</SheetDescription>
|
|
128
|
+
</SheetHeader>
|
|
129
|
+
<div className="flex flex-col gap-4 px-4 py-4">
|
|
130
|
+
{propEntries.length === 0 ? (
|
|
131
|
+
<p className="text-muted-foreground text-sm">{L.noProps}</p>
|
|
132
|
+
) : (
|
|
133
|
+
propEntries.map(([key, prop]) => (
|
|
134
|
+
<PropertyField
|
|
135
|
+
key={key}
|
|
136
|
+
name={key}
|
|
137
|
+
prop={prop}
|
|
138
|
+
value={draft[key]}
|
|
139
|
+
unsetLabel={L.unsetOption}
|
|
140
|
+
onChange={(v) =>
|
|
141
|
+
setDraft((prev) => {
|
|
142
|
+
if (v === undefined) {
|
|
143
|
+
const rest: Record<string, unknown> = {}
|
|
144
|
+
for (const [k, val] of Object.entries(prev)) {
|
|
145
|
+
if (k !== key) rest[k] = val
|
|
146
|
+
}
|
|
147
|
+
return rest
|
|
148
|
+
}
|
|
149
|
+
return { ...prev, [key]: v }
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
153
|
+
))
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
<div className="flex items-center justify-end gap-2 border-t px-4 py-3">
|
|
157
|
+
<Button variant="ghost" onClick={() => setDraft({})}>
|
|
158
|
+
{L.reset}
|
|
159
|
+
</Button>
|
|
160
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
161
|
+
{L.cancel}
|
|
162
|
+
</Button>
|
|
163
|
+
<Button onClick={handleApply}>{L.apply}</Button>
|
|
164
|
+
</div>
|
|
165
|
+
</SheetContent>
|
|
166
|
+
</Sheet>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function PropertyField({
|
|
171
|
+
name,
|
|
172
|
+
prop,
|
|
173
|
+
value,
|
|
174
|
+
unsetLabel,
|
|
175
|
+
onChange,
|
|
176
|
+
}: {
|
|
177
|
+
name: string
|
|
178
|
+
prop: JsonSchemaProperty
|
|
179
|
+
value: unknown
|
|
180
|
+
unsetLabel: string
|
|
181
|
+
onChange: (v: unknown) => void
|
|
182
|
+
}) {
|
|
183
|
+
const id = `prop-${name}`
|
|
184
|
+
|
|
185
|
+
if (prop.type === "string" && Array.isArray(prop.enum)) {
|
|
186
|
+
const options = prop.enum.filter((v): v is string => typeof v === "string")
|
|
187
|
+
const current = typeof value === "string" && value !== "" ? value : UNSET_SENTINEL
|
|
188
|
+
return (
|
|
189
|
+
<FieldShell id={id} name={name} description={prop.description}>
|
|
190
|
+
<Select
|
|
191
|
+
value={current}
|
|
192
|
+
onValueChange={(v) => onChange(v === UNSET_SENTINEL ? undefined : v)}
|
|
193
|
+
>
|
|
194
|
+
<SelectTrigger id={id}>
|
|
195
|
+
<SelectValue placeholder={unsetLabel} />
|
|
196
|
+
</SelectTrigger>
|
|
197
|
+
<SelectContent>
|
|
198
|
+
<SelectItem value={UNSET_SENTINEL}>{unsetLabel}</SelectItem>
|
|
199
|
+
{options.map((opt) => (
|
|
200
|
+
<SelectItem key={opt} value={opt}>
|
|
201
|
+
{opt}
|
|
202
|
+
</SelectItem>
|
|
203
|
+
))}
|
|
204
|
+
</SelectContent>
|
|
205
|
+
</Select>
|
|
206
|
+
</FieldShell>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (prop.type === "string") {
|
|
211
|
+
const current = typeof value === "string" ? value : ""
|
|
212
|
+
return (
|
|
213
|
+
<FieldShell id={id} name={name} description={prop.description}>
|
|
214
|
+
<Input
|
|
215
|
+
id={id}
|
|
216
|
+
value={current}
|
|
217
|
+
onChange={(e) => onChange(e.target.value === "" ? undefined : e.target.value)}
|
|
218
|
+
/>
|
|
219
|
+
</FieldShell>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (prop.type === "number" || prop.type === "integer") {
|
|
224
|
+
const current = typeof value === "number" ? String(value) : ""
|
|
225
|
+
return (
|
|
226
|
+
<FieldShell id={id} name={name} description={prop.description}>
|
|
227
|
+
<Input
|
|
228
|
+
id={id}
|
|
229
|
+
type="number"
|
|
230
|
+
value={current}
|
|
231
|
+
onChange={(e) => {
|
|
232
|
+
const raw = e.target.value
|
|
233
|
+
if (raw === "") return onChange(undefined)
|
|
234
|
+
const parsed =
|
|
235
|
+
prop.type === "integer" ? Number.parseInt(raw, 10) : Number.parseFloat(raw)
|
|
236
|
+
if (Number.isFinite(parsed)) onChange(parsed)
|
|
237
|
+
}}
|
|
238
|
+
/>
|
|
239
|
+
</FieldShell>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (prop.type === "boolean") {
|
|
244
|
+
const current = value === true
|
|
245
|
+
return (
|
|
246
|
+
<FieldShell id={id} name={name} description={prop.description}>
|
|
247
|
+
<Switch
|
|
248
|
+
id={id}
|
|
249
|
+
checked={current}
|
|
250
|
+
onCheckedChange={(checked) => onChange(checked || undefined)}
|
|
251
|
+
/>
|
|
252
|
+
</FieldShell>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<FieldShell id={id} name={name} description={prop.description}>
|
|
258
|
+
<p className="text-muted-foreground text-xs italic">
|
|
259
|
+
Unsupported type "{prop.type ?? "(none)"}" — edit raw JSON to set this field.
|
|
260
|
+
</p>
|
|
261
|
+
</FieldShell>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function FieldShell({
|
|
266
|
+
id,
|
|
267
|
+
name,
|
|
268
|
+
description,
|
|
269
|
+
children,
|
|
270
|
+
}: {
|
|
271
|
+
id: string
|
|
272
|
+
name: string
|
|
273
|
+
description?: string
|
|
274
|
+
children: ReactNode
|
|
275
|
+
}) {
|
|
276
|
+
return (
|
|
277
|
+
<div className="flex flex-col gap-1.5">
|
|
278
|
+
<label htmlFor={id} className="text-sm font-medium">
|
|
279
|
+
{name}
|
|
280
|
+
</label>
|
|
281
|
+
{children}
|
|
282
|
+
{description ? <p className="text-muted-foreground text-xs">{description}</p> : null}
|
|
283
|
+
</div>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import type { PipelineContext } from "@miragon/mcp-toolkit-core"
|
|
3
|
+
import { contextHasNoData } from "./widget-renderer.js"
|
|
4
|
+
|
|
5
|
+
const ctx = (over: Partial<PipelineContext> = {}): PipelineContext => ({
|
|
6
|
+
steps: {},
|
|
7
|
+
keys: {},
|
|
8
|
+
errors: [],
|
|
9
|
+
...over,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe("contextHasNoData", () => {
|
|
13
|
+
it("is true for an empty context (no steps, no keys)", () => {
|
|
14
|
+
expect(contextHasNoData(ctx())).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("is true even when there are errors but no data", () => {
|
|
18
|
+
expect(contextHasNoData(ctx({ errors: [{ stepId: "x", reason: "boom" }] }))).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("is false when keys are present", () => {
|
|
22
|
+
expect(contextHasNoData(ctx({ keys: { "demo:id": "INV-1" } }))).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("is false when step data is present", () => {
|
|
26
|
+
expect(
|
|
27
|
+
contextHasNoData(
|
|
28
|
+
ctx({
|
|
29
|
+
steps: {
|
|
30
|
+
result: {
|
|
31
|
+
data: { total: 1 },
|
|
32
|
+
keys: {},
|
|
33
|
+
_app: "demo",
|
|
34
|
+
_step: "result",
|
|
35
|
+
_dataType: "demo:data",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Component, type ComponentType, type ReactNode, type ErrorInfo } from "react"
|
|
2
|
+
import type { LayoutConfig, PipelineContext, RowDef, WidgetProps } from "@miragon/mcp-toolkit-core"
|
|
3
|
+
import { normalizeLayout } from "@miragon/mcp-toolkit-core"
|
|
4
|
+
import { GridLayout, GridItem } from "../components/GridLayout.js"
|
|
5
|
+
import { Card, CardContent } from "../primitives/card.js"
|
|
6
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../primitives/tabs.js"
|
|
7
|
+
|
|
8
|
+
export type WidgetComponent = ComponentType<WidgetProps>
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Catches render-time crashes from a single widget so one broken widget
|
|
12
|
+
* can't take down the entire iframe view (or the builder canvas/preview).
|
|
13
|
+
*
|
|
14
|
+
* Exported so the visual builder's canvas + preview render sites reuse the
|
|
15
|
+
* same isolation the rendered `WidgetRenderer` already had — every place a
|
|
16
|
+
* widget component is rendered goes through {@link RenderedWidget}, which
|
|
17
|
+
* wraps the component in this boundary.
|
|
18
|
+
*/
|
|
19
|
+
export class WidgetErrorBoundary extends Component<
|
|
20
|
+
{ widgetId: string; children: ReactNode },
|
|
21
|
+
{ error: Error | null }
|
|
22
|
+
> {
|
|
23
|
+
override state: { error: Error | null } = { error: null }
|
|
24
|
+
static getDerivedStateFromError(error: Error) {
|
|
25
|
+
return { error }
|
|
26
|
+
}
|
|
27
|
+
override componentDidCatch(error: Error, info: ErrorInfo) {
|
|
28
|
+
console.error(`Widget ${this.props.widgetId} crashed:`, error, info)
|
|
29
|
+
}
|
|
30
|
+
override render() {
|
|
31
|
+
if (this.state.error) {
|
|
32
|
+
return (
|
|
33
|
+
<Card>
|
|
34
|
+
<CardContent className="text-muted-foreground py-3 text-sm">
|
|
35
|
+
Widget "{this.props.widgetId}" failed to render.
|
|
36
|
+
</CardContent>
|
|
37
|
+
</Card>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
return this.props.children
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* True when the pipeline context carries no widget data at all — no step
|
|
46
|
+
* results and no keys. A widget rendered against an empty context typically
|
|
47
|
+
* paints an empty shell (Finding [4]); callers use this to swap in a
|
|
48
|
+
* "data appears after refresh" hint instead.
|
|
49
|
+
*
|
|
50
|
+
* Intentionally coarse (per-view, not per-widget): the toolkit can't know
|
|
51
|
+
* which slice each widget reads without running its adapter, so it only
|
|
52
|
+
* degrades when there is provably nothing for any widget to show.
|
|
53
|
+
*/
|
|
54
|
+
export function contextHasNoData(context: PipelineContext): boolean {
|
|
55
|
+
return Object.keys(context.steps).length === 0 && Object.keys(context.keys).length === 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The single render site for a layout cell's widget, shared across the
|
|
60
|
+
* rendered view (`RowsRenderer`), the builder canvas, and the builder
|
|
61
|
+
* preview so all three isolate crashes identically:
|
|
62
|
+
* - missing component → "not bundled" fallback,
|
|
63
|
+
* - render crash → caught by {@link WidgetErrorBoundary} (Finding [1]),
|
|
64
|
+
* - (builder only, opt-in) empty pipeline context → a dezent
|
|
65
|
+
* "no data yet" hint (Finding [4]).
|
|
66
|
+
*
|
|
67
|
+
* The empty-state hint is gated behind `showEmptyHint` and defaults off so
|
|
68
|
+
* the shipped rendered view is unchanged: self-fetching control widgets
|
|
69
|
+
* there render correctly against an empty key-only context. The builder's
|
|
70
|
+
* canvas/preview opt in because their live context is empty until the first
|
|
71
|
+
* catalogue refresh resolves, and an empty widget shell reads as broken.
|
|
72
|
+
*
|
|
73
|
+
* `notBundledClassName` lets each surface match its own chrome (the builder
|
|
74
|
+
* uses denser padding/text than the rendered view).
|
|
75
|
+
*/
|
|
76
|
+
export function RenderedWidget({
|
|
77
|
+
widget: Widget,
|
|
78
|
+
widgetId,
|
|
79
|
+
props,
|
|
80
|
+
cellProps,
|
|
81
|
+
showEmptyHint = false,
|
|
82
|
+
notBundledClassName = "text-muted-foreground rounded border border-dashed p-4 text-center text-xs",
|
|
83
|
+
}: {
|
|
84
|
+
widget: WidgetComponent | undefined
|
|
85
|
+
widgetId: string
|
|
86
|
+
props: WidgetProps
|
|
87
|
+
cellProps?: Record<string, unknown>
|
|
88
|
+
showEmptyHint?: boolean
|
|
89
|
+
notBundledClassName?: string
|
|
90
|
+
}) {
|
|
91
|
+
if (!Widget) {
|
|
92
|
+
return <div className={notBundledClassName}>Widget "{widgetId}" not bundled.</div>
|
|
93
|
+
}
|
|
94
|
+
if (showEmptyHint && contextHasNoData(props.context)) {
|
|
95
|
+
return (
|
|
96
|
+
<div className={notBundledClassName}>
|
|
97
|
+
No data for this widget yet — it appears after a refresh.
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
return (
|
|
102
|
+
<WidgetErrorBoundary widgetId={widgetId}>
|
|
103
|
+
<Widget {...props} widgetProps={cellProps} />
|
|
104
|
+
</WidgetErrorBoundary>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface WidgetRendererProps {
|
|
109
|
+
layout: LayoutConfig
|
|
110
|
+
keys: Record<string, unknown>
|
|
111
|
+
stepData?: Record<
|
|
112
|
+
string,
|
|
113
|
+
{ data: unknown; keys: Record<string, unknown>; _app: string; _dataType: string }
|
|
114
|
+
>
|
|
115
|
+
errors: { stepId: string; reason: string }[]
|
|
116
|
+
/**
|
|
117
|
+
* Map of widget ID to React component. Consumers provide this so that the
|
|
118
|
+
* renderer can dispatch layout entries like `{widget: "camunda7:incident-panel"}`
|
|
119
|
+
* to the concrete component. The map is usually aggregated from each module's
|
|
120
|
+
* widget exports at the consumer's `main.tsx` entry point.
|
|
121
|
+
*/
|
|
122
|
+
widgets: Record<string, WidgetComponent>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function RowsRenderer({
|
|
126
|
+
rows,
|
|
127
|
+
props,
|
|
128
|
+
widgets,
|
|
129
|
+
}: {
|
|
130
|
+
rows: RowDef[]
|
|
131
|
+
props: WidgetProps
|
|
132
|
+
widgets: Record<string, WidgetComponent>
|
|
133
|
+
}) {
|
|
134
|
+
return (
|
|
135
|
+
<div className="flex flex-col gap-4">
|
|
136
|
+
{rows.map((row, rowIdx) => (
|
|
137
|
+
<GridLayout key={rowIdx}>
|
|
138
|
+
{row.row.map((cell, cellIdx) => {
|
|
139
|
+
const WidgetComponent = widgets[cell.widget]
|
|
140
|
+
// Keep the rendered view's original behaviour of skipping cells
|
|
141
|
+
// whose component isn't bundled (no placeholder in the shipped
|
|
142
|
+
// view), while still routing the resolvable ones through the
|
|
143
|
+
// shared `RenderedWidget` for error-boundary + empty-state.
|
|
144
|
+
if (!WidgetComponent) return null
|
|
145
|
+
// Use a composite key so the same widget can appear in multiple
|
|
146
|
+
// cells of one row (e.g. side-by-side scoped dashboards) without
|
|
147
|
+
// colliding on React's reconciliation key.
|
|
148
|
+
return (
|
|
149
|
+
<GridItem key={`${cell.widget}-${cellIdx}`} span={cell.span ?? 12}>
|
|
150
|
+
<RenderedWidget
|
|
151
|
+
widget={WidgetComponent}
|
|
152
|
+
widgetId={cell.widget}
|
|
153
|
+
props={props}
|
|
154
|
+
cellProps={cell.props}
|
|
155
|
+
/>
|
|
156
|
+
</GridItem>
|
|
157
|
+
)
|
|
158
|
+
})}
|
|
159
|
+
</GridLayout>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function WidgetRenderer({ layout, keys, stepData, errors, widgets }: WidgetRendererProps) {
|
|
166
|
+
const steps: Record<string, PipelineContext["steps"][string]> = {}
|
|
167
|
+
if (stepData) {
|
|
168
|
+
for (const [id, result] of Object.entries(stepData)) {
|
|
169
|
+
steps[id] = { ...result, _step: id }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const pipelineContext: PipelineContext = {
|
|
173
|
+
steps,
|
|
174
|
+
keys,
|
|
175
|
+
errors,
|
|
176
|
+
}
|
|
177
|
+
const widgetProps: WidgetProps = { keys, context: pipelineContext }
|
|
178
|
+
const normalized = normalizeLayout(layout)
|
|
179
|
+
|
|
180
|
+
if ("tabs" in normalized) {
|
|
181
|
+
return (
|
|
182
|
+
<Tabs defaultValue={normalized.tabs[0]?.label}>
|
|
183
|
+
<TabsList>
|
|
184
|
+
{normalized.tabs.map((tab) => (
|
|
185
|
+
<TabsTrigger key={tab.label} value={tab.label}>
|
|
186
|
+
{tab.label}
|
|
187
|
+
</TabsTrigger>
|
|
188
|
+
))}
|
|
189
|
+
</TabsList>
|
|
190
|
+
{normalized.tabs.map((tab) => (
|
|
191
|
+
<TabsContent key={tab.label} value={tab.label}>
|
|
192
|
+
<RowsRenderer rows={tab.rows} props={widgetProps} widgets={widgets} />
|
|
193
|
+
</TabsContent>
|
|
194
|
+
))}
|
|
195
|
+
</Tabs>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return <RowsRenderer rows={normalized.rows} props={widgetProps} widgets={widgets} />
|
|
200
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
export interface DrillButtonProps {
|
|
4
|
+
/** Button label. */
|
|
5
|
+
children: ReactNode
|
|
6
|
+
/** Drill/navigation handler — wire to in-widget navigation (e.g. `showWidget`). */
|
|
7
|
+
onDrill: () => void
|
|
8
|
+
/** Optional leading glyph (e.g. a domain icon). The trailing → is always rendered. */
|
|
9
|
+
icon?: ReactNode
|
|
10
|
+
/** `sm` for table rows, `md` for surface headers. Default `"sm"`. */
|
|
11
|
+
size?: "sm" | "md"
|
|
12
|
+
/** Accessible label when the visible text is terse. */
|
|
13
|
+
ariaLabel?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The single, deterministic in-widget navigation control: a neutral outline
|
|
18
|
+
* button with a trailing → that drills into a detail/sub-view. Use it for
|
|
19
|
+
* every in-app drill (row "Open", header nav) so they read identically and stay
|
|
20
|
+
* visually quieter than primary/agentic actions. Decoupled from any host — pass
|
|
21
|
+
* an `onDrill` callback (e.g. wrapping `useHostActions().showWidget`).
|
|
22
|
+
*/
|
|
23
|
+
export function DrillButton({ children, onDrill, icon, size = "sm", ariaLabel }: DrillButtonProps) {
|
|
24
|
+
const pad = size === "md" ? "px-3 py-1.5 text-sm" : "px-2.5 py-1 text-xs"
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={onDrill}
|
|
29
|
+
aria-label={ariaLabel}
|
|
30
|
+
className={`border-border text-foreground hover:bg-muted focus-visible:ring-ring inline-flex items-center gap-1 rounded-md border font-medium transition-colors outline-none focus-visible:ring-2 ${pad}`}
|
|
31
|
+
>
|
|
32
|
+
{icon && <span aria-hidden="true">{icon}</span>}
|
|
33
|
+
{children}
|
|
34
|
+
<span aria-hidden="true">→</span>
|
|
35
|
+
</button>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Input } from "../primitives/input.js"
|
|
2
|
+
|
|
3
|
+
export interface FilterChip {
|
|
4
|
+
/** Stable id passed back to `onChipToggle`. */
|
|
5
|
+
id: string
|
|
6
|
+
/** Visible chip label. */
|
|
7
|
+
label: string
|
|
8
|
+
/** Optional count rendered after the label. */
|
|
9
|
+
count?: number | string
|
|
10
|
+
/** Whether the chip is currently active. */
|
|
11
|
+
active?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FilterBarProps {
|
|
15
|
+
/** Current search text (controlled). */
|
|
16
|
+
search: string
|
|
17
|
+
/** Placeholder for the search input. Default `"Filter…"`. */
|
|
18
|
+
searchPlaceholder?: string
|
|
19
|
+
/** Called with the new search text on every keystroke. */
|
|
20
|
+
onSearchChange: (next: string) => void
|
|
21
|
+
/** Filter chips rendered to the right of the search input. */
|
|
22
|
+
chips: FilterChip[]
|
|
23
|
+
/** Called with a chip id when it is toggled. */
|
|
24
|
+
onChipToggle: (id: string) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Search input + toggleable chip row for filtering a list/table of widget
|
|
29
|
+
* items. Pair with `useDebouncedValue` to avoid re-querying on every keystroke.
|
|
30
|
+
*/
|
|
31
|
+
export function FilterBar({
|
|
32
|
+
search,
|
|
33
|
+
searchPlaceholder = "Filter…",
|
|
34
|
+
onSearchChange,
|
|
35
|
+
chips,
|
|
36
|
+
onChipToggle,
|
|
37
|
+
}: FilterBarProps) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
40
|
+
<Input
|
|
41
|
+
type="search"
|
|
42
|
+
value={search}
|
|
43
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
44
|
+
placeholder={searchPlaceholder}
|
|
45
|
+
className="h-9 min-w-[220px] flex-1 text-sm"
|
|
46
|
+
/>
|
|
47
|
+
<div className="flex flex-wrap gap-1.5">
|
|
48
|
+
{chips.map((chip) => (
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
key={chip.id}
|
|
52
|
+
onClick={() => onChipToggle(chip.id)}
|
|
53
|
+
className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors ${
|
|
54
|
+
chip.active
|
|
55
|
+
? "bg-accent border-border text-accent-foreground font-semibold"
|
|
56
|
+
: "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
57
|
+
}`}
|
|
58
|
+
>
|
|
59
|
+
{chip.label}
|
|
60
|
+
{chip.count !== undefined && chip.count !== "" && (
|
|
61
|
+
<span className="tabular-nums opacity-60">{chip.count}</span>
|
|
62
|
+
)}
|
|
63
|
+
</button>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../lib/utils.js"
|
|
3
|
+
|
|
4
|
+
export interface GridLayoutProps {
|
|
5
|
+
/** `GridItem` children to lay out across the 12 columns. */
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
/** Extra classes merged onto the grid container (e.g. to change the gap). */
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 12-column responsive grid container. Wrap `GridItem`s in it to compose
|
|
13
|
+
* dashboard rows; this is the same grid `WidgetRenderer` uses internally.
|
|
14
|
+
*/
|
|
15
|
+
export function GridLayout({ children, className }: GridLayoutProps) {
|
|
16
|
+
return <div className={cn("grid grid-cols-12 gap-4", className)}>{children}</div>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GridItemProps {
|
|
20
|
+
/** Column span from 1–12. Defaults to a full-width `12`. */
|
|
21
|
+
span?: number
|
|
22
|
+
/** Cell content. */
|
|
23
|
+
children: React.ReactNode
|
|
24
|
+
/** Extra classes merged onto the cell. */
|
|
25
|
+
className?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const spanClasses: Record<number, string> = {
|
|
29
|
+
1: "col-span-1",
|
|
30
|
+
2: "col-span-2",
|
|
31
|
+
3: "col-span-3",
|
|
32
|
+
4: "col-span-4",
|
|
33
|
+
5: "col-span-5",
|
|
34
|
+
6: "col-span-6",
|
|
35
|
+
7: "col-span-7",
|
|
36
|
+
8: "col-span-8",
|
|
37
|
+
9: "col-span-9",
|
|
38
|
+
10: "col-span-10",
|
|
39
|
+
11: "col-span-11",
|
|
40
|
+
12: "col-span-12",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A cell inside a `GridLayout`, spanning `span` of the 12 columns. Out-of-range
|
|
45
|
+
* spans fall back to full width.
|
|
46
|
+
*/
|
|
47
|
+
export function GridItem({ span = 12, children, className }: GridItemProps) {
|
|
48
|
+
return <div className={cn(spanClasses[span] ?? "col-span-12", className)}>{children}</div>
|
|
49
|
+
}
|