@miragon/mcp-toolkit-ui 1.1.0 → 2.0.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/dist/app/builder/BuilderBanners.d.ts +19 -0
- package/dist/app/builder/BuilderBanners.d.ts.map +1 -0
- package/dist/app/builder/BuilderBanners.js +22 -0
- package/dist/app/builder/BuilderBanners.js.map +1 -0
- package/dist/app/builder/CanvasRow.d.ts +26 -0
- package/dist/app/builder/CanvasRow.d.ts.map +1 -0
- package/dist/app/builder/CanvasRow.js +98 -0
- package/dist/app/builder/CanvasRow.js.map +1 -0
- package/dist/app/builder/CellPropsSheet.d.ts +19 -0
- package/dist/app/builder/CellPropsSheet.d.ts.map +1 -0
- package/dist/app/builder/CellPropsSheet.js +28 -0
- package/dist/app/builder/CellPropsSheet.js.map +1 -0
- package/dist/app/builder/EmptyHint.d.ts +6 -0
- package/dist/app/builder/EmptyHint.d.ts.map +1 -0
- package/dist/app/builder/EmptyHint.js +10 -0
- package/dist/app/builder/EmptyHint.js.map +1 -0
- package/dist/app/builder/LayoutSurface.d.ts +27 -0
- package/dist/app/builder/LayoutSurface.d.ts.map +1 -0
- package/dist/app/builder/LayoutSurface.js +45 -0
- package/dist/app/builder/LayoutSurface.js.map +1 -0
- package/dist/app/builder/Workspace.d.ts +9 -3
- package/dist/app/builder/Workspace.d.ts.map +1 -1
- package/dist/app/builder/Workspace.js +3 -97
- package/dist/app/builder/Workspace.js.map +1 -1
- package/dist/app/builder/builder-reducer.d.ts.map +1 -1
- package/dist/app/builder/builder-reducer.js +176 -164
- package/dist/app/builder/builder-reducer.js.map +1 -1
- package/dist/app/builder/use-builder-catalogue.d.ts +33 -0
- package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -0
- package/dist/app/builder/use-builder-catalogue.js +155 -0
- package/dist/app/builder/use-builder-catalogue.js.map +1 -0
- package/dist/app/builder/use-key-step-entries.d.ts +22 -0
- package/dist/app/builder/use-key-step-entries.d.ts.map +1 -0
- package/dist/app/builder/use-key-step-entries.js +46 -0
- package/dist/app/builder/use-key-step-entries.js.map +1 -0
- package/dist/app/builder/use-save-dashboard.d.ts +33 -0
- package/dist/app/builder/use-save-dashboard.d.ts.map +1 -0
- package/dist/app/builder/use-save-dashboard.js +78 -0
- package/dist/app/builder/use-save-dashboard.js.map +1 -0
- package/dist/app/layout-builder.d.ts.map +1 -1
- package/dist/app/layout-builder.js +46 -277
- package/dist/app/layout-builder.js.map +1 -1
- package/dist/app/mcp-app-view-chrome.d.ts +77 -0
- package/dist/app/mcp-app-view-chrome.d.ts.map +1 -0
- package/dist/app/mcp-app-view-chrome.js +53 -0
- package/dist/app/mcp-app-view-chrome.js.map +1 -0
- package/dist/app/mcp-app-view.d.ts +3 -9
- package/dist/app/mcp-app-view.d.ts.map +1 -1
- package/dist/app/mcp-app-view.js +4 -31
- package/dist/app/mcp-app-view.js.map +1 -1
- package/dist/render-cases-primitives.d.ts +17 -0
- package/dist/render-cases-primitives.d.ts.map +1 -0
- package/dist/render-cases-primitives.js +88 -0
- package/dist/render-cases-primitives.js.map +1 -0
- package/dist/render-cases.d.ts +4 -0
- package/dist/render-cases.d.ts.map +1 -0
- package/dist/render-cases.js +140 -0
- package/dist/render-cases.js.map +1 -0
- package/package.json +10 -5
- package/src/app/builder/BuilderBanners.tsx +61 -0
- package/src/app/builder/CanvasRow.tsx +316 -0
- package/src/app/builder/CellPropsSheet.tsx +47 -0
- package/src/app/builder/EmptyHint.tsx +29 -0
- package/src/app/builder/LayoutSurface.tsx +140 -0
- package/src/app/builder/Workspace.tsx +33 -342
- package/src/app/builder/builder-reducer.ts +204 -170
- package/src/app/builder/use-builder-catalogue.ts +187 -0
- package/src/app/builder/use-key-step-entries.ts +60 -0
- package/src/app/builder/use-save-dashboard.ts +97 -0
- package/src/app/layout-builder.tsx +92 -497
- package/src/app/mcp-app-view-chrome.tsx +204 -0
- package/src/app/mcp-app-view.tsx +32 -140
- package/src/lib/parse-tool-result.property.test.ts +90 -0
- package/src/render-cases-primitives.tsx +255 -0
- package/src/render-cases.tsx +239 -0
- package/src/render-smoke.test.tsx +100 -0
- package/src/ui-catalog.test.ts +52 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { ReactElement } from "react"
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
AlertDescription,
|
|
5
|
+
AlertTitle,
|
|
6
|
+
Badge,
|
|
7
|
+
Button,
|
|
8
|
+
Card,
|
|
9
|
+
CardAction,
|
|
10
|
+
CardContent,
|
|
11
|
+
CardDescription,
|
|
12
|
+
CardFooter,
|
|
13
|
+
CardHeader,
|
|
14
|
+
CardTitle,
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogClose,
|
|
17
|
+
DialogContent,
|
|
18
|
+
DialogDescription,
|
|
19
|
+
DialogFooter,
|
|
20
|
+
DialogHeader,
|
|
21
|
+
DialogTitle,
|
|
22
|
+
DialogTrigger,
|
|
23
|
+
Input,
|
|
24
|
+
ScrollArea,
|
|
25
|
+
ScrollBar,
|
|
26
|
+
Select,
|
|
27
|
+
SelectContent,
|
|
28
|
+
SelectGroup,
|
|
29
|
+
SelectItem,
|
|
30
|
+
SelectLabel,
|
|
31
|
+
SelectSeparator,
|
|
32
|
+
SelectTrigger,
|
|
33
|
+
SelectValue,
|
|
34
|
+
Separator,
|
|
35
|
+
Sheet,
|
|
36
|
+
SheetClose,
|
|
37
|
+
SheetContent,
|
|
38
|
+
SheetDescription,
|
|
39
|
+
SheetFooter,
|
|
40
|
+
SheetHeader,
|
|
41
|
+
SheetTitle,
|
|
42
|
+
SheetTrigger,
|
|
43
|
+
Skeleton,
|
|
44
|
+
Switch,
|
|
45
|
+
Table,
|
|
46
|
+
TableBody,
|
|
47
|
+
TableCaption,
|
|
48
|
+
TableCell,
|
|
49
|
+
TableFooter,
|
|
50
|
+
TableHead,
|
|
51
|
+
TableHeader,
|
|
52
|
+
TableRow,
|
|
53
|
+
Tabs,
|
|
54
|
+
TabsContent,
|
|
55
|
+
TabsList,
|
|
56
|
+
TabsTrigger,
|
|
57
|
+
} from "./index.js"
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One SSR render case (see `render-cases.tsx`, which aggregates this module's
|
|
61
|
+
* primitive cases with the composed/host ones — split only to respect the
|
|
62
|
+
* 400-effective-line budget; the exported surface is `RENDER_CASES` there).
|
|
63
|
+
*/
|
|
64
|
+
export interface RenderCase {
|
|
65
|
+
/** Catalog entry name (`ui-catalog.json` → `components[].name`) this case covers. */
|
|
66
|
+
name: string
|
|
67
|
+
/** Distinctive text expected in the SSR markup, when the component can carry one. */
|
|
68
|
+
marker?: string
|
|
69
|
+
/** Renders the component with minimal realistic props. */
|
|
70
|
+
render: () => ReactElement
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const noop = (): void => {}
|
|
74
|
+
|
|
75
|
+
/** SSR render cases for the catalogued shadcn primitives (kind "primitive"). */
|
|
76
|
+
export const PRIMITIVE_RENDER_CASES: RenderCase[] = [
|
|
77
|
+
{
|
|
78
|
+
name: "Card",
|
|
79
|
+
marker: "RC:Card",
|
|
80
|
+
render: () => (
|
|
81
|
+
<Card size="sm">
|
|
82
|
+
<CardHeader>
|
|
83
|
+
<CardTitle>RC:Card</CardTitle>
|
|
84
|
+
<CardDescription>Order summary</CardDescription>
|
|
85
|
+
<CardAction>
|
|
86
|
+
<Badge variant="outline">new</Badge>
|
|
87
|
+
</CardAction>
|
|
88
|
+
</CardHeader>
|
|
89
|
+
<CardContent>Three orders this week.</CardContent>
|
|
90
|
+
<CardFooter>Updated just now</CardFooter>
|
|
91
|
+
</Card>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "Badge",
|
|
96
|
+
marker: "RC:Badge",
|
|
97
|
+
render: () => <Badge variant="secondary">RC:Badge</Badge>,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "Button",
|
|
101
|
+
marker: "RC:Button",
|
|
102
|
+
render: () => (
|
|
103
|
+
<Button variant="outline" size="sm" onClick={noop}>
|
|
104
|
+
RC:Button
|
|
105
|
+
</Button>
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
// The marker lands in the placeholder/value attributes of the markup.
|
|
110
|
+
name: "Input",
|
|
111
|
+
marker: "RC:Input",
|
|
112
|
+
render: () => <Input type="text" placeholder="RC:Input" defaultValue="42" onChange={noop} />,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
// SSR limitation: SelectContent renders through a Radix portal targeting
|
|
116
|
+
// document.body, which does not exist server-side — the portal yields
|
|
117
|
+
// nothing. The trigger + placeholder are the SSR surface; item/label
|
|
118
|
+
// rendering needs a browser host.
|
|
119
|
+
name: "Select",
|
|
120
|
+
marker: "RC:Select",
|
|
121
|
+
render: () => (
|
|
122
|
+
<Select onValueChange={noop}>
|
|
123
|
+
<SelectTrigger>
|
|
124
|
+
<SelectValue placeholder="RC:Select" />
|
|
125
|
+
</SelectTrigger>
|
|
126
|
+
<SelectContent>
|
|
127
|
+
<SelectGroup>
|
|
128
|
+
<SelectLabel>Period</SelectLabel>
|
|
129
|
+
<SelectItem value="7d">Last 7 days</SelectItem>
|
|
130
|
+
<SelectSeparator />
|
|
131
|
+
<SelectItem value="30d">Last 30 days</SelectItem>
|
|
132
|
+
</SelectGroup>
|
|
133
|
+
</SelectContent>
|
|
134
|
+
</Select>
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
// The marker lands in the aria-label attribute of the markup.
|
|
139
|
+
name: "Switch",
|
|
140
|
+
marker: "RC:Switch",
|
|
141
|
+
render: () => <Switch defaultChecked aria-label="RC:Switch" onCheckedChange={noop} />,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "Table",
|
|
145
|
+
marker: "RC:Table",
|
|
146
|
+
render: () => (
|
|
147
|
+
<Table>
|
|
148
|
+
<TableCaption>RC:Table</TableCaption>
|
|
149
|
+
<TableHeader>
|
|
150
|
+
<TableRow>
|
|
151
|
+
<TableHead>Order</TableHead>
|
|
152
|
+
<TableHead>Status</TableHead>
|
|
153
|
+
</TableRow>
|
|
154
|
+
</TableHeader>
|
|
155
|
+
<TableBody>
|
|
156
|
+
<TableRow>
|
|
157
|
+
<TableCell>o-1001</TableCell>
|
|
158
|
+
<TableCell>shipped</TableCell>
|
|
159
|
+
</TableRow>
|
|
160
|
+
</TableBody>
|
|
161
|
+
<TableFooter>
|
|
162
|
+
<TableRow>
|
|
163
|
+
<TableCell>1 order</TableCell>
|
|
164
|
+
<TableCell />
|
|
165
|
+
</TableRow>
|
|
166
|
+
</TableFooter>
|
|
167
|
+
</Table>
|
|
168
|
+
),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "Tabs",
|
|
172
|
+
marker: "RC:Tabs",
|
|
173
|
+
render: () => (
|
|
174
|
+
<Tabs defaultValue="overview" onValueChange={noop}>
|
|
175
|
+
<TabsList>
|
|
176
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
177
|
+
<TabsTrigger value="detail">Detail</TabsTrigger>
|
|
178
|
+
</TabsList>
|
|
179
|
+
<TabsContent value="overview">RC:Tabs</TabsContent>
|
|
180
|
+
<TabsContent value="detail">Per-order detail</TabsContent>
|
|
181
|
+
</Tabs>
|
|
182
|
+
),
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
// SSR limitation: DialogContent renders through a Radix portal targeting
|
|
186
|
+
// document.body, so the modal body cannot SSR even with `open` — the
|
|
187
|
+
// trigger is the SSR surface. Portal content needs a browser host.
|
|
188
|
+
name: "Dialog",
|
|
189
|
+
marker: "RC:Dialog",
|
|
190
|
+
render: () => (
|
|
191
|
+
<Dialog open onOpenChange={noop}>
|
|
192
|
+
<DialogTrigger>RC:Dialog</DialogTrigger>
|
|
193
|
+
<DialogContent>
|
|
194
|
+
<DialogHeader>
|
|
195
|
+
<DialogTitle>Archive order?</DialogTitle>
|
|
196
|
+
<DialogDescription>The order can be restored later.</DialogDescription>
|
|
197
|
+
</DialogHeader>
|
|
198
|
+
<DialogFooter>
|
|
199
|
+
<DialogClose>Cancel</DialogClose>
|
|
200
|
+
</DialogFooter>
|
|
201
|
+
</DialogContent>
|
|
202
|
+
</Dialog>
|
|
203
|
+
),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
// SSR limitation: same portal constraint as Dialog — SheetContent targets
|
|
207
|
+
// document.body; the trigger is the SSR surface.
|
|
208
|
+
name: "Sheet",
|
|
209
|
+
marker: "RC:Sheet",
|
|
210
|
+
render: () => (
|
|
211
|
+
<Sheet open onOpenChange={noop}>
|
|
212
|
+
<SheetTrigger>RC:Sheet</SheetTrigger>
|
|
213
|
+
<SheetContent side="right">
|
|
214
|
+
<SheetHeader>
|
|
215
|
+
<SheetTitle>Order o-1001</SheetTitle>
|
|
216
|
+
<SheetDescription>Edit without leaving the widget.</SheetDescription>
|
|
217
|
+
</SheetHeader>
|
|
218
|
+
<SheetFooter>
|
|
219
|
+
<SheetClose>Close</SheetClose>
|
|
220
|
+
</SheetFooter>
|
|
221
|
+
</SheetContent>
|
|
222
|
+
</Sheet>
|
|
223
|
+
),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "Alert",
|
|
227
|
+
marker: "RC:Alert",
|
|
228
|
+
render: () => (
|
|
229
|
+
<Alert variant="destructive">
|
|
230
|
+
<AlertTitle>RC:Alert</AlertTitle>
|
|
231
|
+
<AlertDescription>The engine rejected the job.</AlertDescription>
|
|
232
|
+
</Alert>
|
|
233
|
+
),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "ScrollArea",
|
|
237
|
+
marker: "RC:ScrollArea",
|
|
238
|
+
render: () => (
|
|
239
|
+
<ScrollArea className="h-24">
|
|
240
|
+
<div>RC:ScrollArea</div>
|
|
241
|
+
<ScrollBar orientation="vertical" />
|
|
242
|
+
</ScrollArea>
|
|
243
|
+
),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
// Pure placeholder box — no text content to mark.
|
|
247
|
+
name: "Skeleton",
|
|
248
|
+
render: () => <Skeleton className="h-6 w-32" />,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
// Decorative divider — no text content to mark.
|
|
252
|
+
name: "Separator",
|
|
253
|
+
render: () => <Separator orientation="horizontal" />,
|
|
254
|
+
},
|
|
255
|
+
]
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { ReactElement } from "react"
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
CountPill,
|
|
5
|
+
DrillButton,
|
|
6
|
+
FilterBar,
|
|
7
|
+
GridItem,
|
|
8
|
+
GridLayout,
|
|
9
|
+
GroupCard,
|
|
10
|
+
KpiGrid,
|
|
11
|
+
ListFooter,
|
|
12
|
+
LivePill,
|
|
13
|
+
LocaleProvider,
|
|
14
|
+
SectionHeading,
|
|
15
|
+
ThemeProvider,
|
|
16
|
+
themePresets,
|
|
17
|
+
useLocale,
|
|
18
|
+
WidgetHeader,
|
|
19
|
+
} from "./index.js"
|
|
20
|
+
import { createStandaloneHostBridge, HostBridgeProvider, useHostBridge } from "./app/host-bridge.js"
|
|
21
|
+
import { HostModelContext } from "./app/adapt-data-widget.js"
|
|
22
|
+
import { WidgetFixtureHost, type FixtureWidget } from "./app/widget-fixture.js"
|
|
23
|
+
import { PRIMITIVE_RENDER_CASES, type RenderCase } from "./render-cases-primitives.js"
|
|
24
|
+
|
|
25
|
+
export type { RenderCase } from "./render-cases-primitives.js"
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SSR render cases for every catalogued component/composite (FITNESS.md phase
|
|
29
|
+
* 5c). DATA module, not a test: `render-smoke.test.tsx` renders each case via
|
|
30
|
+
* `react-dom/server`'s `renderToStaticMarkup` (jsdom is deliberately renounced
|
|
31
|
+
* — SSR is the house render path) and enforces completeness against
|
|
32
|
+
* `ui-catalog.json` ∪ `ratchets/render-allowlist.json` (shrink-only).
|
|
33
|
+
*
|
|
34
|
+
* Each case mounts the component with minimal realistic props and, where the
|
|
35
|
+
* markup can carry one, a distinctive `marker` string the test asserts on.
|
|
36
|
+
* The primitive cases live in `render-cases-primitives.tsx` (file-length
|
|
37
|
+
* budget); this module aggregates them with the composed/host cases.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const noop = (): void => {}
|
|
41
|
+
|
|
42
|
+
/** Static bridge for the host-bridge cases — theme "dark" is the probed marker. */
|
|
43
|
+
const probeBridge = createStandaloneHostBridge({
|
|
44
|
+
callTool: () => Promise.resolve({}),
|
|
45
|
+
theme: "dark",
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
/** Proves HostBridgeProvider actually provides: reads the bridge from context. */
|
|
49
|
+
function BridgeThemeProbe(): ReactElement {
|
|
50
|
+
const bridge = useHostBridge()
|
|
51
|
+
return <span>RC:HostBridgeProvider {bridge.theme ?? "unset"}</span>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Proves LocaleProvider actually provides: reads the locale from context. */
|
|
55
|
+
function LocaleProbe(): ReactElement {
|
|
56
|
+
const locale = useLocale()
|
|
57
|
+
return <span>RC:LocaleProvider {locale}</span>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Minimal raw `({ data })` widget for the WidgetFixtureHost case. */
|
|
61
|
+
function FixtureProbeWidget({ data }: { data?: { label?: string } | null }): ReactElement {
|
|
62
|
+
return <div>RC:WidgetFixtureHost {data?.label ?? "no fixture"}</div>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const RENDER_CASES: RenderCase[] = [
|
|
66
|
+
...PRIMITIVE_RENDER_CASES,
|
|
67
|
+
|
|
68
|
+
// ── Composed components ──
|
|
69
|
+
{
|
|
70
|
+
name: "GridLayout",
|
|
71
|
+
marker: "RC:GridLayout",
|
|
72
|
+
render: () => (
|
|
73
|
+
<GridLayout>
|
|
74
|
+
<GridItem span={8}>
|
|
75
|
+
<div>RC:GridLayout</div>
|
|
76
|
+
</GridItem>
|
|
77
|
+
<GridItem span={4}>
|
|
78
|
+
<div>side tile</div>
|
|
79
|
+
</GridItem>
|
|
80
|
+
</GridLayout>
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "GridItem",
|
|
85
|
+
marker: "RC:GridItem",
|
|
86
|
+
render: () => (
|
|
87
|
+
<GridLayout>
|
|
88
|
+
<GridItem span={6}>
|
|
89
|
+
<div>RC:GridItem</div>
|
|
90
|
+
</GridItem>
|
|
91
|
+
</GridLayout>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "KpiGrid",
|
|
96
|
+
marker: "RC:KpiGrid",
|
|
97
|
+
render: () => (
|
|
98
|
+
<KpiGrid
|
|
99
|
+
boxed
|
|
100
|
+
header={{ label: "RC:KpiGrid", badge: "live" }}
|
|
101
|
+
cells={[
|
|
102
|
+
{
|
|
103
|
+
label: "Open",
|
|
104
|
+
value: 7,
|
|
105
|
+
tone: "danger",
|
|
106
|
+
trend: "+2 vs yesterday",
|
|
107
|
+
trendDirection: "up",
|
|
108
|
+
},
|
|
109
|
+
{ label: "Done", value: 12, fraction: "/19", tone: "success" },
|
|
110
|
+
{ label: "Total", value: 19, onClick: noop, ariaLabel: "Show all" },
|
|
111
|
+
]}
|
|
112
|
+
/>
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "LivePill",
|
|
117
|
+
marker: "RC:LivePill",
|
|
118
|
+
render: () => <LivePill tone="success">RC:LivePill</LivePill>,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "CountPill",
|
|
122
|
+
marker: "RC:CountPill",
|
|
123
|
+
render: () => <CountPill tone="warning">RC:CountPill</CountPill>,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "SectionHeading",
|
|
127
|
+
marker: "RC:SectionHeading",
|
|
128
|
+
render: () => <SectionHeading title="RC:SectionHeading" hint="3 shown" />,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "WidgetHeader",
|
|
132
|
+
marker: "RC:WidgetHeader",
|
|
133
|
+
render: () => (
|
|
134
|
+
<WidgetHeader
|
|
135
|
+
icon="✓"
|
|
136
|
+
iconTone="info"
|
|
137
|
+
title="RC:WidgetHeader"
|
|
138
|
+
sub={<LivePill>Live</LivePill>}
|
|
139
|
+
actions={
|
|
140
|
+
<Button size="sm" variant="ghost" onClick={noop}>
|
|
141
|
+
Refresh
|
|
142
|
+
</Button>
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "GroupCard",
|
|
149
|
+
marker: "RC:GroupCard",
|
|
150
|
+
render: () => (
|
|
151
|
+
<GroupCard expanded onToggle={noop} summary={<div>RC:GroupCard</div>}>
|
|
152
|
+
<div>Expanded body row</div>
|
|
153
|
+
</GroupCard>
|
|
154
|
+
),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
// The search marker lands in the input's value attribute.
|
|
158
|
+
name: "FilterBar",
|
|
159
|
+
marker: "RC:FilterBar",
|
|
160
|
+
render: () => (
|
|
161
|
+
<FilterBar
|
|
162
|
+
search="RC:FilterBar"
|
|
163
|
+
searchPlaceholder="Filter tasks…"
|
|
164
|
+
onSearchChange={noop}
|
|
165
|
+
chips={[
|
|
166
|
+
{ id: "high", label: "high", count: 2, active: true },
|
|
167
|
+
{ id: "low", label: "low" },
|
|
168
|
+
]}
|
|
169
|
+
onChipToggle={noop}
|
|
170
|
+
/>
|
|
171
|
+
),
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "DrillButton",
|
|
175
|
+
marker: "RC:DrillButton",
|
|
176
|
+
render: () => (
|
|
177
|
+
<DrillButton onDrill={noop} icon="↗" size="sm" ariaLabel="Open order o-1001">
|
|
178
|
+
RC:DrillButton
|
|
179
|
+
</DrillButton>
|
|
180
|
+
),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "ListFooter",
|
|
184
|
+
marker: "of 42 tasks",
|
|
185
|
+
render: () => <ListFooter shown={8} total={42} hasMore onLoadMore={noop} noun="tasks" />,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// ── Host seam / providers ──
|
|
189
|
+
{
|
|
190
|
+
name: "HostBridgeProvider",
|
|
191
|
+
marker: "RC:HostBridgeProvider dark",
|
|
192
|
+
render: () => (
|
|
193
|
+
<HostBridgeProvider bridge={probeBridge}>
|
|
194
|
+
<BridgeThemeProbe />
|
|
195
|
+
</HostBridgeProvider>
|
|
196
|
+
),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
// Outside an mcp-use view HostModelContext reports through the bridge in
|
|
200
|
+
// an effect (a no-op in SSR) and must still render its children.
|
|
201
|
+
name: "HostModelContext",
|
|
202
|
+
marker: "RC:HostModelContext",
|
|
203
|
+
render: () => (
|
|
204
|
+
<HostBridgeProvider bridge={probeBridge}>
|
|
205
|
+
<HostModelContext content="Viewing 3 open incidents">
|
|
206
|
+
<div>RC:HostModelContext</div>
|
|
207
|
+
</HostModelContext>
|
|
208
|
+
</HostBridgeProvider>
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "ThemeProvider",
|
|
213
|
+
marker: "RC:ThemeProvider",
|
|
214
|
+
render: () => (
|
|
215
|
+
<ThemeProvider theme={themePresets.miragon} mode="dark">
|
|
216
|
+
<div>RC:ThemeProvider</div>
|
|
217
|
+
</ThemeProvider>
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "LocaleProvider",
|
|
222
|
+
marker: "RC:LocaleProvider de",
|
|
223
|
+
render: () => (
|
|
224
|
+
<LocaleProvider locale="de">
|
|
225
|
+
<LocaleProbe />
|
|
226
|
+
</LocaleProvider>
|
|
227
|
+
),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "WidgetFixtureHost",
|
|
231
|
+
marker: "RC:WidgetFixtureHost fixture-data",
|
|
232
|
+
render: () => (
|
|
233
|
+
<WidgetFixtureHost
|
|
234
|
+
widget={FixtureProbeWidget as FixtureWidget}
|
|
235
|
+
data={{ label: "fixture-data" }}
|
|
236
|
+
/>
|
|
237
|
+
),
|
|
238
|
+
},
|
|
239
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import catalog from "../ui-catalog.json" with { type: "json" }
|
|
4
|
+
import allowlist from "../../../ratchets/render-allowlist.json" with { type: "json" }
|
|
5
|
+
import { RENDER_CASES } from "./render-cases.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render-coverage ratchet (FITNESS.md phase 5c). jsdom is deliberately
|
|
9
|
+
* renounced in this repo — SSR via `react-dom/server`'s `renderToStaticMarkup`
|
|
10
|
+
* is the house render path, and mutation testing does NOT measure the JSX
|
|
11
|
+
* render surface, so this suite is the compensating control:
|
|
12
|
+
*
|
|
13
|
+
* 1. every case in `render-cases.tsx` must SSR without throwing, produce
|
|
14
|
+
* non-empty markup, and contain its marker where one is set;
|
|
15
|
+
* 2. every component-like `ui-catalog.json` entry must have a render case OR
|
|
16
|
+
* a justified entry in `ratchets/render-allowlist.json` (shrink-only).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface CatalogEntry {
|
|
20
|
+
name: string
|
|
21
|
+
kind: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const catalogEntries = (catalog as unknown as { components: CatalogEntry[] }).components
|
|
25
|
+
|
|
26
|
+
/** Kinds that render as React elements and therefore need an SSR case. */
|
|
27
|
+
const COMPONENT_KINDS = new Set(["primitive", "component"])
|
|
28
|
+
/** Kinds that are not renderable on their own (values, hooks, types). */
|
|
29
|
+
const NON_RENDER_KINDS = new Set(["hook", "function", "object", "type"])
|
|
30
|
+
|
|
31
|
+
const componentEntryNames = new Set(
|
|
32
|
+
catalogEntries.filter((entry) => COMPONENT_KINDS.has(entry.kind)).map((entry) => entry.name),
|
|
33
|
+
)
|
|
34
|
+
const caseNames = new Set(RENDER_CASES.map((renderCase) => renderCase.name))
|
|
35
|
+
const allowlistedNames = new Set(allowlist.components.map((entry) => entry.name))
|
|
36
|
+
|
|
37
|
+
const GATE_HINT =
|
|
38
|
+
"add a RENDER_CASES entry in packages/ui/src/render-cases.tsx (SSR, no jsdom) " +
|
|
39
|
+
"or justify it in ratchets/render-allowlist.json (shrink-only)"
|
|
40
|
+
|
|
41
|
+
describe("RENDER_CASES (SSR render suite)", () => {
|
|
42
|
+
it("has one case per name — duplicates would mask a missing component", () => {
|
|
43
|
+
expect(caseNames.size).toBe(RENDER_CASES.length)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
for (const renderCase of RENDER_CASES) {
|
|
47
|
+
it(`${renderCase.name} SSR-renders to non-empty markup`, () => {
|
|
48
|
+
const html = renderToStaticMarkup(renderCase.render())
|
|
49
|
+
expect(html.length, `${renderCase.name} rendered empty markup`).toBeGreaterThan(0)
|
|
50
|
+
if (renderCase.marker) {
|
|
51
|
+
expect(html, `${renderCase.name} markup lost its marker`).toContain(renderCase.marker)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe("render coverage (catalog completeness gate)", () => {
|
|
58
|
+
it("classifies every catalog kind — a new kind must be triaged here, not skipped silently", () => {
|
|
59
|
+
for (const entry of catalogEntries) {
|
|
60
|
+
expect(
|
|
61
|
+
COMPONENT_KINDS.has(entry.kind) || NON_RENDER_KINDS.has(entry.kind),
|
|
62
|
+
`Unknown catalog kind "${entry.kind}" on "${entry.name}". Add it to COMPONENT_KINDS ` +
|
|
63
|
+
`(then ${GATE_HINT}) or to NON_RENDER_KINDS in render-smoke.test.tsx.`,
|
|
64
|
+
).toBe(true)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("covers every component-like catalog entry (RENDER_CASES ∪ render-allowlist)", () => {
|
|
69
|
+
const missing = [...componentEntryNames].filter(
|
|
70
|
+
(name) => !caseNames.has(name) && !allowlistedNames.has(name),
|
|
71
|
+
)
|
|
72
|
+
expect(
|
|
73
|
+
missing,
|
|
74
|
+
`Catalogued component(s) [${missing.join(", ")}] have no SSR render case — ${GATE_HINT}.`,
|
|
75
|
+
).toEqual([])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("every RENDER_CASES entry names a component-like catalog entry (no drift)", () => {
|
|
79
|
+
const unknown = RENDER_CASES.map((c) => c.name).filter((name) => !componentEntryNames.has(name))
|
|
80
|
+
expect(
|
|
81
|
+
unknown,
|
|
82
|
+
`RENDER_CASES entr${unknown.length === 1 ? "y" : "ies"} [${unknown.join(", ")}] match(es) no ` +
|
|
83
|
+
"component-like ui-catalog.json entry — rename the case to the catalog name or catalogue the component.",
|
|
84
|
+
).toEqual([])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("allowlist hygiene: reasons present, entries real, none double-listed", () => {
|
|
88
|
+
for (const entry of allowlist.components) {
|
|
89
|
+
expect(entry.reason, `${entry.name} needs a non-empty reason`).toBeTruthy()
|
|
90
|
+
expect(
|
|
91
|
+
componentEntryNames.has(entry.name),
|
|
92
|
+
`${entry.name} is allowlisted but is no component-like ui-catalog.json entry — remove the row (shrink-only)`,
|
|
93
|
+
).toBe(true)
|
|
94
|
+
expect(
|
|
95
|
+
caseNames.has(entry.name),
|
|
96
|
+
`${entry.name} is BOTH rendered in RENDER_CASES and allowlisted — remove the stale allowlist row`,
|
|
97
|
+
).toBe(false)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
package/src/ui-catalog.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import catalog from "../ui-catalog.json" with { type: "json" }
|
|
3
|
+
import allowlist from "../ui-catalog.allowlist.json" with { type: "json" }
|
|
3
4
|
import * as rootBarrel from "./index.js"
|
|
4
5
|
import * as appBarrel from "./app/index.js"
|
|
5
6
|
import * as hooksBarrel from "./hooks/index.js"
|
|
@@ -109,4 +110,55 @@ describe("ui-catalog.json", () => {
|
|
|
109
110
|
expect(Array.isArray(entry.props), `${entry.name} props`).toBe(true)
|
|
110
111
|
}
|
|
111
112
|
})
|
|
113
|
+
|
|
114
|
+
// ── Reverse direction (FITNESS.md, phase 5a) ──
|
|
115
|
+
// The catalog ships in the tarball and is LLM-consumer surface: a NEW
|
|
116
|
+
// export that reaches a barrel without a catalog entry (or a justified
|
|
117
|
+
// allowlist row) is invisible to every prompting agent — and previously
|
|
118
|
+
// fell through this guard, which only checked catalog → barrel.
|
|
119
|
+
|
|
120
|
+
/** Every value export a catalog entry or its parts account for. */
|
|
121
|
+
function cataloguedExports(): Set<string> {
|
|
122
|
+
const names = new Set<string>()
|
|
123
|
+
for (const entry of catalogData.components) {
|
|
124
|
+
names.add(entry.export)
|
|
125
|
+
for (const part of entry.parts ?? []) names.add(part)
|
|
126
|
+
}
|
|
127
|
+
return names
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Exported for the difference logic to stay testable (see fitness gates). */
|
|
131
|
+
function uncataloguedExports(barrel: Record<string, unknown>): string[] {
|
|
132
|
+
const catalogued = cataloguedExports()
|
|
133
|
+
const allowed = new Set(allowlist.allow.map((a) => a.export))
|
|
134
|
+
return Object.keys(barrel)
|
|
135
|
+
.filter((name) => !catalogued.has(name) && !allowed.has(name))
|
|
136
|
+
.sort()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
it("no uncatalogued export escapes the barrels (catalog ∪ allowlist)", () => {
|
|
140
|
+
for (const [importPath, barrel] of Object.entries(BARRELS)) {
|
|
141
|
+
const escaped = uncataloguedExports(barrel)
|
|
142
|
+
expect(
|
|
143
|
+
escaped,
|
|
144
|
+
`New export(s) [${escaped.join(", ")}] from ${importPath} are neither in ui-catalog.json nor in ui-catalog.allowlist.json. Catalogue them (name/export/importPath/kind/group/props/whenToUse — the catalog ships to LLM consumers) or add a justified allowlist row. The allowlist is shrink-only.`,
|
|
145
|
+
).toEqual([])
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("allowlist hygiene: entries carry reasons and still exist in a barrel", () => {
|
|
150
|
+
const allExports = new Set(Object.values(BARRELS).flatMap((b) => Object.keys(b)))
|
|
151
|
+
const catalogued = cataloguedExports()
|
|
152
|
+
for (const row of allowlist.allow) {
|
|
153
|
+
expect(row.reason, `${row.export} needs a reason`).toBeTruthy()
|
|
154
|
+
expect(
|
|
155
|
+
allExports.has(row.export),
|
|
156
|
+
`${row.export} is allowlisted but no barrel exports it — remove the row (shrink-only)`,
|
|
157
|
+
).toBe(true)
|
|
158
|
+
expect(
|
|
159
|
+
catalogued.has(row.export),
|
|
160
|
+
`${row.export} is BOTH catalogued and allowlisted — remove the stale allowlist row`,
|
|
161
|
+
).toBe(false)
|
|
162
|
+
}
|
|
163
|
+
})
|
|
112
164
|
})
|