react-semaphor 0.1.326 → 0.1.328
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/analytics-protocol/index.cjs +1 -1
- package/dist/analytics-protocol/index.js +1 -1
- package/dist/brand-studio/index.cjs +5 -5
- package/dist/brand-studio/index.js +23 -24
- package/dist/chunks/_commonjsHelpers-BVfed4GL.js +28 -0
- package/dist/chunks/_commonjsHelpers-DwTZ_eVU.js +1 -0
- package/dist/chunks/{braces-DVaMJoCd.js → braces-CG8viaD2.js} +1 -1
- package/dist/chunks/{braces-dHeaioTd.js → braces-DSaa_4Oc.js} +1 -1
- package/dist/chunks/{calendar-preferences-dialog-mqU_uOmY.js → calendar-preferences-dialog-DD_qAthL.js} +1 -1
- package/dist/chunks/{calendar-preferences-dialog--2jD-eKu.js → calendar-preferences-dialog-JRmNJptJ.js} +3 -3
- package/dist/chunks/{chevrons-up-down-DNQE-uEp.js → chevrons-up-down-CBa0uh0X.js} +1 -1
- package/dist/chunks/{chevrons-up-down-CLbzBHoI.js → chevrons-up-down-ChDNqVP7.js} +1 -1
- package/dist/chunks/dashboard-briefing-launcher-BKdJFwH5.js +106 -0
- package/dist/chunks/{dashboard-briefing-launcher-DsjXUPFq.js → dashboard-briefing-launcher-BfyNkd8i.js} +1106 -1108
- package/dist/chunks/{dashboard-controls-DgNkM1yT.js → dashboard-controls-0pZDLvFL.js} +218 -218
- package/dist/chunks/{dashboard-controls-XCa9PZuU.js → dashboard-controls-C0xm1QMR.js} +10 -10
- package/dist/chunks/dashboard-json-BW5OVZ6m.js +1 -0
- package/dist/chunks/{dashboard-json-DkA6Bf0Y.js → dashboard-json-DsruhRPD.js} +14 -13
- package/dist/chunks/date-formatter-B4EBSe9C.js +1 -0
- package/dist/chunks/date-formatter-CzcPZx39.js +416 -0
- package/dist/chunks/edit-dashboard-visual-DQyJ7SSv.js +178 -0
- package/dist/chunks/{edit-dashboard-visual-Q5hW5MXk.js → edit-dashboard-visual-g5SZZahJ.js} +8242 -7784
- package/dist/chunks/index-1JWDPCun.js +1935 -0
- package/dist/chunks/index-BdjXTQt4.js +1444 -0
- package/dist/chunks/{index-DwfDwtv2.js → index-BmKr-K7J.js} +117670 -113627
- package/dist/chunks/index-C1l78BIx.js +3247 -0
- package/dist/chunks/lib-Ce3zosXY.js +38 -0
- package/dist/chunks/lib-DFvr9fM4.js +5500 -0
- package/dist/chunks/{palette-DtmNws9a.js → palette-D0YmAqBS.js} +1 -1
- package/dist/chunks/{palette-DpcvHAiY.js → palette-DQgq3edH.js} +1 -1
- package/dist/chunks/{resource-management-panel-CGwWKZOP.js → resource-management-panel-CIfBh46E.js} +1 -1
- package/dist/chunks/{resource-management-panel-D2oAQkKi.js → resource-management-panel-Cj19pw9M.js} +145 -145
- package/dist/chunks/switch-BvTzw2AW.js +173 -0
- package/dist/chunks/{switch-j3AThLFk.js → switch-De31SR3q.js} +2752 -2717
- package/dist/chunks/typescript-Cmizj1hi.js +446 -0
- package/dist/chunks/typescript-H1EwZsOb.js +159820 -0
- package/dist/chunks/use-create-flow-overlay-state-BTQiKRD1.js +26 -0
- package/dist/chunks/{use-create-flow-overlay-state-CP6GDQAX.js → use-create-flow-overlay-state-IcHP0l39.js} +150 -151
- package/dist/chunks/{use-visual-utils-CSXU-96k.js → use-visual-utils-DQ5zGYD2.js} +13 -13
- package/dist/chunks/{use-visual-utils-NpOW5uZ1.js → use-visual-utils-XF-AqV8o.js} +1 -1
- package/dist/chunks/{validators-DvTxl9i0.js → validators-DDAweCzB.js} +67 -48
- package/dist/chunks/validators-odlRJblR.js +2 -0
- package/dist/dashboard/index.cjs +1 -1
- package/dist/dashboard/index.js +1 -1
- package/dist/dashboard-authoring/index.cjs +3 -3
- package/dist/dashboard-authoring/index.js +907 -527
- package/dist/data-app-builder/index.cjs +1 -0
- package/dist/data-app-builder/index.js +4 -0
- package/dist/data-app-builder-browser-runtime/index.cjs +1 -0
- package/dist/data-app-builder-browser-runtime/index.js +10 -0
- package/dist/data-app-sdk/index.cjs +1 -1
- package/dist/data-app-sdk/index.js +208 -196
- package/dist/format-utils/index.cjs +4 -4
- package/dist/format-utils/index.js +22 -21
- package/dist/index.cjs +1 -1
- package/dist/index.js +14 -14
- package/dist/style.css +1 -1
- package/dist/surfboard/index.cjs +1 -1
- package/dist/surfboard/index.js +2 -2
- package/dist/types/analytics-protocol.d.ts +1 -0
- package/dist/types/dashboard-authoring.d.ts +79 -2
- package/dist/types/dashboard.d.ts +17 -2
- package/dist/types/data-app-builder-browser-runtime.d.ts +185 -0
- package/dist/types/data-app-builder.d.ts +630 -0
- package/dist/types/data-app-sdk.d.ts +15 -1
- package/dist/types/format-utils.d.ts +25 -0
- package/dist/types/main.d.ts +67 -10
- package/dist/types/shared.d.ts +17 -2
- package/dist/types/surfboard.d.ts +17 -2
- package/dist/types/types.d.ts +32 -3
- package/package.json +11 -1
- package/dist/chunks/dashboard-briefing-launcher-DguNfFr8.js +0 -106
- package/dist/chunks/dashboard-json-DiBd65Xf.js +0 -1
- package/dist/chunks/date-formatter-D9Bvw5Qk.js +0 -1
- package/dist/chunks/date-formatter-DyIOb6uC.js +0 -333
- package/dist/chunks/edit-dashboard-visual-BC9eOc4U.js +0 -178
- package/dist/chunks/index-B1-46PoR.js +0 -1303
- package/dist/chunks/save-BY7WF62U.js +0 -21
- package/dist/chunks/save-Cv_XRb94.js +0 -6
- package/dist/chunks/switch-5aQ_jcLH.js +0 -168
- package/dist/chunks/use-create-flow-overlay-state-BQPXP0AM.js +0 -26
- package/dist/chunks/validators-DJUMR5An.js +0 -2
|
@@ -0,0 +1,1935 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const O="semaphor-data-app-browser-sandbox-files:v1",x="browser://semaphor-data-app",R="src/main.tsx",u=[{path:"src/App.tsx",contents:`import { DataApp } from "./data-app"
|
|
2
|
+
|
|
3
|
+
export default function App() {
|
|
4
|
+
return <DataApp />
|
|
5
|
+
}
|
|
6
|
+
`},{path:"src/main.tsx",contents:`import { StrictMode } from "react"
|
|
7
|
+
import { createRoot } from "react-dom/client"
|
|
8
|
+
import App from "./App"
|
|
9
|
+
import "./index.css"
|
|
10
|
+
|
|
11
|
+
createRoot(document.getElementById("root")!).render(
|
|
12
|
+
<StrictMode>
|
|
13
|
+
<App />
|
|
14
|
+
</StrictMode>,
|
|
15
|
+
)
|
|
16
|
+
`},{path:"src/index.css",contents:`@import "tailwindcss";
|
|
17
|
+
|
|
18
|
+
@theme {
|
|
19
|
+
--color-background: #ffffff;
|
|
20
|
+
--color-foreground: #09090b;
|
|
21
|
+
--color-card: #ffffff;
|
|
22
|
+
--color-card-foreground: #09090b;
|
|
23
|
+
--color-muted: #f4f4f5;
|
|
24
|
+
--color-muted-foreground: #71717a;
|
|
25
|
+
--color-border: #e4e4e7;
|
|
26
|
+
--color-input: #e4e4e7;
|
|
27
|
+
--color-ring: #18181b;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
body {
|
|
31
|
+
margin: 0;
|
|
32
|
+
min-width: 320px;
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
background: #f6f7f8;
|
|
35
|
+
}
|
|
36
|
+
`},{path:"src/data-app/index.tsx",contents:`import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
|
|
37
|
+
import { Activity, BarChart3, RefreshCw, Users } from "lucide-react"
|
|
38
|
+
import {
|
|
39
|
+
useSemaphorInput,
|
|
40
|
+
useSemaphorInputOptions,
|
|
41
|
+
useSemaphorMetric,
|
|
42
|
+
useSemaphorRecords,
|
|
43
|
+
} from "react-semaphor/data-app-sdk"
|
|
44
|
+
|
|
45
|
+
import { Badge } from "@/components/ui/badge"
|
|
46
|
+
import { Button } from "@/components/ui/button"
|
|
47
|
+
import {
|
|
48
|
+
Card,
|
|
49
|
+
CardContent,
|
|
50
|
+
CardDescription,
|
|
51
|
+
CardHeader,
|
|
52
|
+
CardTitle,
|
|
53
|
+
} from "@/components/ui/card"
|
|
54
|
+
import {
|
|
55
|
+
ChartContainer,
|
|
56
|
+
ChartTooltip,
|
|
57
|
+
ChartTooltipContent,
|
|
58
|
+
type ChartConfig,
|
|
59
|
+
} from "@/components/ui/chart"
|
|
60
|
+
import {
|
|
61
|
+
Select,
|
|
62
|
+
SelectContent,
|
|
63
|
+
SelectItem,
|
|
64
|
+
SelectTrigger,
|
|
65
|
+
SelectValue,
|
|
66
|
+
} from "@/components/ui/select"
|
|
67
|
+
import {
|
|
68
|
+
Table,
|
|
69
|
+
TableBody,
|
|
70
|
+
TableCell,
|
|
71
|
+
TableHead,
|
|
72
|
+
TableHeader,
|
|
73
|
+
TableRow,
|
|
74
|
+
} from "@/components/ui/table"
|
|
75
|
+
|
|
76
|
+
export function DataApp() {
|
|
77
|
+
const segmentOptions = useSemaphorInputOptions({
|
|
78
|
+
id: "segment-options",
|
|
79
|
+
dataset: "customers",
|
|
80
|
+
field: "customer_segment",
|
|
81
|
+
})
|
|
82
|
+
const regionOptions = useSemaphorInputOptions({
|
|
83
|
+
id: "region-options",
|
|
84
|
+
dataset: "customers",
|
|
85
|
+
field: "region",
|
|
86
|
+
})
|
|
87
|
+
const segment = useSemaphorInput({
|
|
88
|
+
id: "segment",
|
|
89
|
+
kind: "filter",
|
|
90
|
+
field: "customer_segment",
|
|
91
|
+
value: "",
|
|
92
|
+
options: [{ value: "", label: "All segments" }, ...segmentOptions.options],
|
|
93
|
+
})
|
|
94
|
+
const region = useSemaphorInput({
|
|
95
|
+
id: "region",
|
|
96
|
+
kind: "filter",
|
|
97
|
+
field: "region",
|
|
98
|
+
value: "",
|
|
99
|
+
options: [{ value: "", label: "All regions" }, ...regionOptions.options],
|
|
100
|
+
})
|
|
101
|
+
const revenue = useSemaphorMetric({
|
|
102
|
+
id: "revenue",
|
|
103
|
+
dataset: "orders",
|
|
104
|
+
metric: "revenue",
|
|
105
|
+
comparison: "previous_period",
|
|
106
|
+
inputs: [segment, region],
|
|
107
|
+
})
|
|
108
|
+
const arr = useSemaphorMetric({
|
|
109
|
+
id: "arr",
|
|
110
|
+
dataset: "customers",
|
|
111
|
+
metric: "arr",
|
|
112
|
+
inputs: [segment, region],
|
|
113
|
+
})
|
|
114
|
+
const tickets = useSemaphorMetric({
|
|
115
|
+
id: "tickets",
|
|
116
|
+
dataset: "customers",
|
|
117
|
+
metric: "open_tickets",
|
|
118
|
+
inputs: [segment, region],
|
|
119
|
+
})
|
|
120
|
+
const customerHealth = useSemaphorRecords({
|
|
121
|
+
id: "customer-health",
|
|
122
|
+
dataset: "customers",
|
|
123
|
+
dimensions: ["customer_name", "customer_segment", "region"],
|
|
124
|
+
measures: ["health_score", "open_tickets", "arr"],
|
|
125
|
+
inputs: [segment, region],
|
|
126
|
+
limit: 8,
|
|
127
|
+
})
|
|
128
|
+
const chartData = customerHealth.records.map((row) => ({
|
|
129
|
+
customer: String(row.customer_name).split(" ")[0],
|
|
130
|
+
arr: Number(row.arr || 0),
|
|
131
|
+
}))
|
|
132
|
+
const chartConfig = {
|
|
133
|
+
arr: {
|
|
134
|
+
label: "ARR",
|
|
135
|
+
color: "#2563eb",
|
|
136
|
+
},
|
|
137
|
+
} satisfies ChartConfig
|
|
138
|
+
|
|
139
|
+
const resetFilters = () => {
|
|
140
|
+
segment.setValue("")
|
|
141
|
+
region.setValue("")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<main className="min-h-screen bg-zinc-50 p-4 text-zinc-950 sm:p-6">
|
|
146
|
+
<section className="mx-auto flex max-w-6xl flex-col gap-4">
|
|
147
|
+
<div className="flex flex-col gap-3 rounded-lg border border-zinc-200 bg-white p-4 shadow-sm lg:flex-row lg:items-center lg:justify-between">
|
|
148
|
+
<div className="min-w-0">
|
|
149
|
+
<Badge variant="secondary" className="mb-2 gap-1.5">
|
|
150
|
+
<BarChart3 className="size-3.5" />
|
|
151
|
+
Approved Browser Sandbox template
|
|
152
|
+
</Badge>
|
|
153
|
+
<h1 className="text-2xl font-semibold tracking-tight">Customer Revenue Console</h1>
|
|
154
|
+
<p className="mt-1 text-sm text-zinc-500">
|
|
155
|
+
Semaphor metrics, filters, shadcn source components, Tailwind, and Recharts in one browser-native starter.
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="grid gap-2 sm:grid-cols-[1fr_1fr_auto] lg:w-[520px]">
|
|
159
|
+
<label className="text-xs font-medium text-zinc-600">
|
|
160
|
+
Segment
|
|
161
|
+
<Select value={String(segment.value ?? "")} onValueChange={segment.setValue}>
|
|
162
|
+
<SelectTrigger className="mt-1 w-full">
|
|
163
|
+
<SelectValue placeholder="All segments" />
|
|
164
|
+
</SelectTrigger>
|
|
165
|
+
<SelectContent>
|
|
166
|
+
{segment.options.map((option) => (
|
|
167
|
+
<SelectItem key={option.value} value={String(option.value)}>
|
|
168
|
+
{option.label}
|
|
169
|
+
</SelectItem>
|
|
170
|
+
))}
|
|
171
|
+
</SelectContent>
|
|
172
|
+
</Select>
|
|
173
|
+
</label>
|
|
174
|
+
<label className="text-xs font-medium text-zinc-600">
|
|
175
|
+
Region
|
|
176
|
+
<Select value={String(region.value ?? "")} onValueChange={region.setValue}>
|
|
177
|
+
<SelectTrigger className="mt-1 w-full">
|
|
178
|
+
<SelectValue placeholder="All regions" />
|
|
179
|
+
</SelectTrigger>
|
|
180
|
+
<SelectContent>
|
|
181
|
+
{region.options.map((option) => (
|
|
182
|
+
<SelectItem key={option.value} value={String(option.value)}>
|
|
183
|
+
{option.label}
|
|
184
|
+
</SelectItem>
|
|
185
|
+
))}
|
|
186
|
+
</SelectContent>
|
|
187
|
+
</Select>
|
|
188
|
+
</label>
|
|
189
|
+
<Button type="button" variant="outline" className="self-end" onClick={resetFilters}>
|
|
190
|
+
<RefreshCw className="size-4" />
|
|
191
|
+
Reset
|
|
192
|
+
</Button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div className="grid gap-3 md:grid-cols-3">
|
|
197
|
+
<Card>
|
|
198
|
+
<CardHeader className="flex flex-row items-center justify-between gap-2">
|
|
199
|
+
<div>
|
|
200
|
+
<CardDescription>Total revenue</CardDescription>
|
|
201
|
+
<CardTitle className="mt-1 text-2xl text-zinc-950">
|
|
202
|
+
{revenue.value?.toLocaleString() ?? "0"}
|
|
203
|
+
</CardTitle>
|
|
204
|
+
</div>
|
|
205
|
+
<Activity className="size-4 text-zinc-400" />
|
|
206
|
+
</CardHeader>
|
|
207
|
+
</Card>
|
|
208
|
+
<Card>
|
|
209
|
+
<CardHeader className="flex flex-row items-center justify-between gap-2">
|
|
210
|
+
<div>
|
|
211
|
+
<CardDescription>Customer ARR</CardDescription>
|
|
212
|
+
<CardTitle className="mt-1 text-2xl text-zinc-950">
|
|
213
|
+
{arr.value?.toLocaleString() ?? "0"}
|
|
214
|
+
</CardTitle>
|
|
215
|
+
</div>
|
|
216
|
+
<Users className="size-4 text-zinc-400" />
|
|
217
|
+
</CardHeader>
|
|
218
|
+
</Card>
|
|
219
|
+
<Card>
|
|
220
|
+
<CardHeader className="flex flex-row items-center justify-between gap-2">
|
|
221
|
+
<div>
|
|
222
|
+
<CardDescription>Open tickets</CardDescription>
|
|
223
|
+
<CardTitle className="mt-1 text-2xl text-zinc-950">
|
|
224
|
+
{tickets.value?.toLocaleString() ?? "0"}
|
|
225
|
+
</CardTitle>
|
|
226
|
+
</div>
|
|
227
|
+
<Badge variant={tickets.value > 30 ? "destructive" : "secondary"}>
|
|
228
|
+
{tickets.value > 30 ? "Watch" : "Stable"}
|
|
229
|
+
</Badge>
|
|
230
|
+
</CardHeader>
|
|
231
|
+
</Card>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_420px]">
|
|
235
|
+
<Card>
|
|
236
|
+
<CardHeader>
|
|
237
|
+
<CardTitle>ARR by account</CardTitle>
|
|
238
|
+
<CardDescription>Real Recharts rendered through the shadcn chart wrapper.</CardDescription>
|
|
239
|
+
</CardHeader>
|
|
240
|
+
<CardContent>
|
|
241
|
+
<ChartContainer config={chartConfig} className="h-[300px] w-full">
|
|
242
|
+
<BarChart data={chartData} margin={{ left: 0, right: 12 }}>
|
|
243
|
+
<CartesianGrid vertical={false} />
|
|
244
|
+
<XAxis dataKey="customer" tickLine={false} axisLine={false} tickMargin={8} />
|
|
245
|
+
<YAxis tickLine={false} axisLine={false} tickMargin={8} width={52} />
|
|
246
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
247
|
+
<Bar dataKey="arr" fill="var(--color-arr)" radius={[4, 4, 0, 0]} />
|
|
248
|
+
</BarChart>
|
|
249
|
+
</ChartContainer>
|
|
250
|
+
</CardContent>
|
|
251
|
+
</Card>
|
|
252
|
+
|
|
253
|
+
<Card>
|
|
254
|
+
<CardHeader>
|
|
255
|
+
<CardTitle>Template coverage</CardTitle>
|
|
256
|
+
<CardDescription>Bundled source files the model can edit in-browser.</CardDescription>
|
|
257
|
+
</CardHeader>
|
|
258
|
+
<CardContent className="flex flex-col gap-2 text-sm">
|
|
259
|
+
{["shadcn card/table/button/badge/chart", "Tailwind v4 compiler", "Recharts SVG charts", "Semaphor data hooks"].map((item) => (
|
|
260
|
+
<div key={item} className="flex items-center justify-between gap-3 rounded-md border border-zinc-200 px-3 py-2">
|
|
261
|
+
<span className="text-zinc-700">{item}</span>
|
|
262
|
+
<Badge variant="outline">Ready</Badge>
|
|
263
|
+
</div>
|
|
264
|
+
))}
|
|
265
|
+
</CardContent>
|
|
266
|
+
</Card>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<Card>
|
|
270
|
+
<CardHeader>
|
|
271
|
+
<CardTitle>Customer health watchlist</CardTitle>
|
|
272
|
+
<CardDescription>Filtered fixture rows shaped through the Semaphor data app SDK.</CardDescription>
|
|
273
|
+
</CardHeader>
|
|
274
|
+
<CardContent>
|
|
275
|
+
<div className="overflow-x-auto rounded-lg border border-zinc-200">
|
|
276
|
+
<Table>
|
|
277
|
+
<TableHeader>
|
|
278
|
+
<TableRow>
|
|
279
|
+
<TableHead>Customer</TableHead>
|
|
280
|
+
<TableHead>Segment</TableHead>
|
|
281
|
+
<TableHead>Region</TableHead>
|
|
282
|
+
<TableHead className="text-right">Health</TableHead>
|
|
283
|
+
<TableHead className="text-right">Tickets</TableHead>
|
|
284
|
+
<TableHead className="text-right">ARR</TableHead>
|
|
285
|
+
</TableRow>
|
|
286
|
+
</TableHeader>
|
|
287
|
+
<TableBody>
|
|
288
|
+
{customerHealth.records.map((row) => (
|
|
289
|
+
<TableRow key={row.customer_name}>
|
|
290
|
+
<TableCell className="font-medium text-zinc-900">{row.customer_name}</TableCell>
|
|
291
|
+
<TableCell>{row.customer_segment}</TableCell>
|
|
292
|
+
<TableCell>{row.region}</TableCell>
|
|
293
|
+
<TableCell className="text-right">{row.health_score}</TableCell>
|
|
294
|
+
<TableCell className="text-right">{row.open_tickets}</TableCell>
|
|
295
|
+
<TableCell className="text-right">
|
|
296
|
+
{Number(row.arr || 0).toLocaleString()}
|
|
297
|
+
</TableCell>
|
|
298
|
+
</TableRow>
|
|
299
|
+
))}
|
|
300
|
+
</TableBody>
|
|
301
|
+
</Table>
|
|
302
|
+
</div>
|
|
303
|
+
</CardContent>
|
|
304
|
+
</Card>
|
|
305
|
+
</section>
|
|
306
|
+
</main>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
`},{path:"src/lib/utils.ts",contents:`import { clsx, type ClassValue } from "clsx"
|
|
310
|
+
import { twMerge } from "tailwind-merge"
|
|
311
|
+
|
|
312
|
+
export function cn(...inputs: ClassValue[]) {
|
|
313
|
+
return twMerge(clsx(inputs))
|
|
314
|
+
}
|
|
315
|
+
`},{path:"src/components/ui/badge.tsx",contents:`import type { HTMLAttributes } from "react"
|
|
316
|
+
|
|
317
|
+
import { cn } from "@/lib/utils"
|
|
318
|
+
|
|
319
|
+
type BadgeVariant = "default" | "secondary" | "outline" | "destructive"
|
|
320
|
+
|
|
321
|
+
const variantClasses: Record<BadgeVariant, string> = {
|
|
322
|
+
default: "border-transparent bg-zinc-900 text-white",
|
|
323
|
+
secondary: "border-transparent bg-zinc-100 text-zinc-900",
|
|
324
|
+
outline: "border-zinc-200 bg-white text-zinc-700",
|
|
325
|
+
destructive: "border-transparent bg-red-100 text-red-700",
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function Badge({
|
|
329
|
+
className,
|
|
330
|
+
variant = "default",
|
|
331
|
+
...props
|
|
332
|
+
}: HTMLAttributes<HTMLSpanElement> & { variant?: BadgeVariant }) {
|
|
333
|
+
return (
|
|
334
|
+
<span
|
|
335
|
+
{...props}
|
|
336
|
+
className={cn(
|
|
337
|
+
"inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium",
|
|
338
|
+
variantClasses[variant],
|
|
339
|
+
className,
|
|
340
|
+
)}
|
|
341
|
+
/>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
`},{path:"src/components/ui/button.tsx",contents:`import type { ButtonHTMLAttributes } from "react"
|
|
345
|
+
|
|
346
|
+
import { cn } from "@/lib/utils"
|
|
347
|
+
|
|
348
|
+
type ButtonVariant = "default" | "outline" | "ghost"
|
|
349
|
+
type ButtonSize = "default" | "sm"
|
|
350
|
+
|
|
351
|
+
const variantClasses: Record<ButtonVariant, string> = {
|
|
352
|
+
default: "bg-zinc-900 text-white hover:bg-zinc-700",
|
|
353
|
+
outline: "border border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-50",
|
|
354
|
+
ghost: "text-zinc-700 hover:bg-zinc-100",
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const sizeClasses: Record<ButtonSize, string> = {
|
|
358
|
+
default: "h-9 px-3",
|
|
359
|
+
sm: "h-8 px-2.5 text-xs",
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function Button({
|
|
363
|
+
className,
|
|
364
|
+
variant = "default",
|
|
365
|
+
size = "default",
|
|
366
|
+
...props
|
|
367
|
+
}: ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
368
|
+
variant?: ButtonVariant
|
|
369
|
+
size?: ButtonSize
|
|
370
|
+
}) {
|
|
371
|
+
return (
|
|
372
|
+
<button
|
|
373
|
+
{...props}
|
|
374
|
+
className={cn(
|
|
375
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium outline-none transition-colors disabled:pointer-events-none disabled:opacity-50",
|
|
376
|
+
variantClasses[variant],
|
|
377
|
+
sizeClasses[size],
|
|
378
|
+
className,
|
|
379
|
+
)}
|
|
380
|
+
/>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
`},{path:"src/components/ui/card.tsx",contents:`import type { HTMLAttributes } from "react"
|
|
384
|
+
|
|
385
|
+
import { cn } from "@/lib/utils"
|
|
386
|
+
|
|
387
|
+
export function Card(props: HTMLAttributes<HTMLDivElement>) {
|
|
388
|
+
return <div {...props} className={cn("rounded-lg border border-zinc-200 bg-white shadow-sm", props.className)} />
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function CardHeader(props: HTMLAttributes<HTMLDivElement>) {
|
|
392
|
+
return <div {...props} className={cn("p-4 pb-2", props.className)} />
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function CardTitle(props: HTMLAttributes<HTMLHeadingElement>) {
|
|
396
|
+
return <h2 {...props} className={cn("text-sm font-medium text-zinc-600", props.className)} />
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function CardDescription(props: HTMLAttributes<HTMLParagraphElement>) {
|
|
400
|
+
return <p {...props} className={cn("text-xs text-zinc-500", props.className)} />
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function CardContent(props: HTMLAttributes<HTMLDivElement>) {
|
|
404
|
+
return <div {...props} className={cn("p-4 pt-2", props.className)} />
|
|
405
|
+
}
|
|
406
|
+
`},{path:"src/components/ui/select.tsx",contents:`import * as React from "react"
|
|
407
|
+
|
|
408
|
+
import { cn } from "@/lib/utils"
|
|
409
|
+
|
|
410
|
+
type SelectContextValue = {
|
|
411
|
+
value?: string
|
|
412
|
+
selectedLabel?: React.ReactNode
|
|
413
|
+
open: boolean
|
|
414
|
+
setOpen: (open: boolean) => void
|
|
415
|
+
setValue: (value: string, label: React.ReactNode) => void
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const SelectContext = React.createContext<SelectContextValue | null>(null)
|
|
419
|
+
|
|
420
|
+
function useSelectContext(component: string) {
|
|
421
|
+
const context = React.useContext(SelectContext)
|
|
422
|
+
if (!context) {
|
|
423
|
+
throw new Error(component + " must be used within Select")
|
|
424
|
+
}
|
|
425
|
+
return context
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function Select({
|
|
429
|
+
value,
|
|
430
|
+
defaultValue,
|
|
431
|
+
onValueChange,
|
|
432
|
+
children,
|
|
433
|
+
}: {
|
|
434
|
+
value?: string
|
|
435
|
+
defaultValue?: string
|
|
436
|
+
onValueChange?: (value: string) => void
|
|
437
|
+
children: React.ReactNode
|
|
438
|
+
}) {
|
|
439
|
+
const isControlled = Object.prototype.hasOwnProperty.call(
|
|
440
|
+
arguments[0] || {},
|
|
441
|
+
"value",
|
|
442
|
+
)
|
|
443
|
+
const [localValue, setLocalValue] = React.useState(defaultValue)
|
|
444
|
+
const [selectedLabel, setSelectedLabel] = React.useState<React.ReactNode>()
|
|
445
|
+
const [open, setOpen] = React.useState(false)
|
|
446
|
+
const currentValue = isControlled ? value : localValue
|
|
447
|
+
|
|
448
|
+
const setNextValue = React.useCallback(
|
|
449
|
+
(nextValue: string, label: React.ReactNode) => {
|
|
450
|
+
if (!isControlled) setLocalValue(nextValue)
|
|
451
|
+
setSelectedLabel(label)
|
|
452
|
+
onValueChange?.(nextValue)
|
|
453
|
+
setOpen(false)
|
|
454
|
+
},
|
|
455
|
+
[isControlled, onValueChange],
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<SelectContext.Provider
|
|
460
|
+
value={{
|
|
461
|
+
value: currentValue,
|
|
462
|
+
selectedLabel,
|
|
463
|
+
open,
|
|
464
|
+
setOpen,
|
|
465
|
+
setValue: setNextValue,
|
|
466
|
+
}}
|
|
467
|
+
>
|
|
468
|
+
<div className="relative inline-block min-w-0">{children}</div>
|
|
469
|
+
</SelectContext.Provider>
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function SelectTrigger({
|
|
474
|
+
className,
|
|
475
|
+
children,
|
|
476
|
+
...props
|
|
477
|
+
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
478
|
+
const context = useSelectContext("SelectTrigger")
|
|
479
|
+
|
|
480
|
+
return (
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
data-slot="select-trigger"
|
|
484
|
+
aria-expanded={context.open}
|
|
485
|
+
onClick={() => context.setOpen(!context.open)}
|
|
486
|
+
className={cn(
|
|
487
|
+
"flex h-9 min-w-32 items-center justify-between gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 shadow-sm outline-none transition-colors hover:bg-zinc-50 focus:border-zinc-400 disabled:pointer-events-none disabled:opacity-50",
|
|
488
|
+
className,
|
|
489
|
+
)}
|
|
490
|
+
{...props}
|
|
491
|
+
>
|
|
492
|
+
<span className="min-w-0 flex-1 truncate text-left">{children}</span>
|
|
493
|
+
<span className="text-xs text-zinc-400">▾</span>
|
|
494
|
+
</button>
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function SelectValue({ placeholder }: { placeholder?: string }) {
|
|
499
|
+
const context = useSelectContext("SelectValue")
|
|
500
|
+
return (
|
|
501
|
+
<span
|
|
502
|
+
data-slot="select-value"
|
|
503
|
+
className={cn(!context.selectedLabel && "text-zinc-400")}
|
|
504
|
+
>
|
|
505
|
+
{context.selectedLabel || context.value || placeholder || "Select"}
|
|
506
|
+
</span>
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function SelectContent({
|
|
511
|
+
className,
|
|
512
|
+
children,
|
|
513
|
+
}: {
|
|
514
|
+
className?: string
|
|
515
|
+
children: React.ReactNode
|
|
516
|
+
}) {
|
|
517
|
+
const context = useSelectContext("SelectContent")
|
|
518
|
+
if (!context.open) return null
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<div
|
|
522
|
+
data-slot="select-content"
|
|
523
|
+
className={cn(
|
|
524
|
+
"absolute left-0 top-[calc(100%+4px)] z-50 max-h-64 min-w-full overflow-auto rounded-md border border-zinc-200 bg-white p-1 text-zinc-900 shadow-lg",
|
|
525
|
+
className,
|
|
526
|
+
)}
|
|
527
|
+
>
|
|
528
|
+
{children}
|
|
529
|
+
</div>
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function SelectItem({
|
|
534
|
+
value,
|
|
535
|
+
className,
|
|
536
|
+
children,
|
|
537
|
+
}: {
|
|
538
|
+
value: string
|
|
539
|
+
className?: string
|
|
540
|
+
children: React.ReactNode
|
|
541
|
+
}) {
|
|
542
|
+
const context = useSelectContext("SelectItem")
|
|
543
|
+
const selected = context.value === value
|
|
544
|
+
|
|
545
|
+
return (
|
|
546
|
+
<button
|
|
547
|
+
type="button"
|
|
548
|
+
data-slot="select-item"
|
|
549
|
+
data-state={selected ? "checked" : "unchecked"}
|
|
550
|
+
onClick={() => context.setValue(value, children)}
|
|
551
|
+
className={cn(
|
|
552
|
+
"flex w-full items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-none hover:bg-zinc-100 focus:bg-zinc-100",
|
|
553
|
+
selected && "bg-zinc-100 font-medium",
|
|
554
|
+
className,
|
|
555
|
+
)}
|
|
556
|
+
>
|
|
557
|
+
<span className="min-w-0 truncate">{children}</span>
|
|
558
|
+
{selected ? <span className="text-xs text-zinc-500">✓</span> : null}
|
|
559
|
+
</button>
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function SelectGroup({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
564
|
+
return <div data-slot="select-group" className={cn("p-1", className)} {...props} />
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function SelectLabel({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
568
|
+
return <div data-slot="select-label" className={cn("px-2 py-1.5 text-xs font-medium text-zinc-500", className)} {...props} />
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function SelectSeparator({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
572
|
+
return <div data-slot="select-separator" className={cn("-mx-1 my-1 h-px bg-zinc-200", className)} {...props} />
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function SelectScrollUpButton() {
|
|
576
|
+
return null
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function SelectScrollDownButton() {
|
|
580
|
+
return null
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export {
|
|
584
|
+
Select,
|
|
585
|
+
SelectContent,
|
|
586
|
+
SelectGroup,
|
|
587
|
+
SelectItem,
|
|
588
|
+
SelectLabel,
|
|
589
|
+
SelectScrollDownButton,
|
|
590
|
+
SelectScrollUpButton,
|
|
591
|
+
SelectSeparator,
|
|
592
|
+
SelectTrigger,
|
|
593
|
+
SelectValue,
|
|
594
|
+
}
|
|
595
|
+
`},{path:"src/components/ui/progress.tsx",contents:`import type { HTMLAttributes } from "react"
|
|
596
|
+
|
|
597
|
+
import { cn } from "@/lib/utils"
|
|
598
|
+
|
|
599
|
+
function clampProgress(value: number | null | undefined) {
|
|
600
|
+
if (typeof value !== "number" || Number.isNaN(value)) return 0
|
|
601
|
+
return Math.max(0, Math.min(100, value))
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function Progress({
|
|
605
|
+
className,
|
|
606
|
+
value,
|
|
607
|
+
...props
|
|
608
|
+
}: HTMLAttributes<HTMLDivElement> & { value?: number | null }) {
|
|
609
|
+
const safeValue = clampProgress(value)
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
<div
|
|
613
|
+
data-slot="progress"
|
|
614
|
+
role="progressbar"
|
|
615
|
+
aria-valuemin={0}
|
|
616
|
+
aria-valuemax={100}
|
|
617
|
+
aria-valuenow={safeValue}
|
|
618
|
+
className={cn(
|
|
619
|
+
"relative h-2 w-full overflow-hidden rounded-full bg-zinc-100",
|
|
620
|
+
className,
|
|
621
|
+
)}
|
|
622
|
+
{...props}
|
|
623
|
+
>
|
|
624
|
+
<div
|
|
625
|
+
data-slot="progress-indicator"
|
|
626
|
+
className="h-full w-full flex-1 rounded-full bg-zinc-900 transition-transform"
|
|
627
|
+
style={{ transform: "translateX(-" + (100 - safeValue) + "%)" }}
|
|
628
|
+
/>
|
|
629
|
+
</div>
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export { Progress }
|
|
634
|
+
`},{path:"src/components/ui/calendar.tsx",contents:`import * as React from "react"
|
|
635
|
+
|
|
636
|
+
import { cn } from "@/lib/utils"
|
|
637
|
+
|
|
638
|
+
type CalendarMode = "single" | "range"
|
|
639
|
+
type DateRange = { from?: Date; to?: Date }
|
|
640
|
+
|
|
641
|
+
function startOfMonth(date: Date) {
|
|
642
|
+
return new Date(date.getFullYear(), date.getMonth(), 1)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function addMonths(date: Date, count: number) {
|
|
646
|
+
return new Date(date.getFullYear(), date.getMonth() + count, 1)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function isSameDay(a?: Date, b?: Date) {
|
|
650
|
+
return Boolean(
|
|
651
|
+
a &&
|
|
652
|
+
b &&
|
|
653
|
+
a.getFullYear() === b.getFullYear() &&
|
|
654
|
+
a.getMonth() === b.getMonth() &&
|
|
655
|
+
a.getDate() === b.getDate(),
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function isBetween(date: Date, from?: Date, to?: Date) {
|
|
660
|
+
if (!from || !to) return false
|
|
661
|
+
const value = date.getTime()
|
|
662
|
+
return value >= from.getTime() && value <= to.getTime()
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function monthDays(month: Date) {
|
|
666
|
+
const first = startOfMonth(month)
|
|
667
|
+
const start = new Date(first)
|
|
668
|
+
start.setDate(first.getDate() - first.getDay())
|
|
669
|
+
|
|
670
|
+
return Array.from({ length: 42 }, (_, index) => {
|
|
671
|
+
const date = new Date(start)
|
|
672
|
+
date.setDate(start.getDate() + index)
|
|
673
|
+
return date
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function formatDate(value?: Date) {
|
|
678
|
+
return value
|
|
679
|
+
? value.toLocaleDateString(undefined, {
|
|
680
|
+
month: "short",
|
|
681
|
+
day: "numeric",
|
|
682
|
+
year: "numeric",
|
|
683
|
+
})
|
|
684
|
+
: ""
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function Calendar({
|
|
688
|
+
className,
|
|
689
|
+
mode = "single",
|
|
690
|
+
selected,
|
|
691
|
+
defaultMonth,
|
|
692
|
+
month,
|
|
693
|
+
onMonthChange,
|
|
694
|
+
onSelect,
|
|
695
|
+
}: {
|
|
696
|
+
className?: string
|
|
697
|
+
mode?: CalendarMode
|
|
698
|
+
selected?: Date | DateRange
|
|
699
|
+
defaultMonth?: Date
|
|
700
|
+
month?: Date
|
|
701
|
+
onMonthChange?: (month: Date) => void
|
|
702
|
+
onSelect?: (value: Date | DateRange | undefined) => void
|
|
703
|
+
}) {
|
|
704
|
+
const [localMonth, setLocalMonth] = React.useState(
|
|
705
|
+
startOfMonth(month || defaultMonth || new Date()),
|
|
706
|
+
)
|
|
707
|
+
const currentMonth = startOfMonth(month || localMonth)
|
|
708
|
+
const range = mode === "range" && selected && !(selected instanceof Date)
|
|
709
|
+
? selected
|
|
710
|
+
: undefined
|
|
711
|
+
const selectedDate = selected instanceof Date ? selected : undefined
|
|
712
|
+
|
|
713
|
+
const setMonth = React.useCallback(
|
|
714
|
+
(nextMonth: Date) => {
|
|
715
|
+
const normalized = startOfMonth(nextMonth)
|
|
716
|
+
if (!month) setLocalMonth(normalized)
|
|
717
|
+
onMonthChange?.(normalized)
|
|
718
|
+
},
|
|
719
|
+
[month, onMonthChange],
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
const selectDate = React.useCallback(
|
|
723
|
+
(date: Date) => {
|
|
724
|
+
if (mode === "range") {
|
|
725
|
+
const currentRange =
|
|
726
|
+
selected && !(selected instanceof Date) ? selected : undefined
|
|
727
|
+
if (!currentRange?.from || currentRange.to) {
|
|
728
|
+
onSelect?.({ from: date, to: undefined })
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
if (date.getTime() < currentRange.from.getTime()) {
|
|
732
|
+
onSelect?.({ from: date, to: currentRange.from })
|
|
733
|
+
return
|
|
734
|
+
}
|
|
735
|
+
onSelect?.({ from: currentRange.from, to: date })
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
onSelect?.(date)
|
|
740
|
+
},
|
|
741
|
+
[mode, onSelect, selected],
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
return (
|
|
745
|
+
<div
|
|
746
|
+
data-slot="calendar"
|
|
747
|
+
className={cn(
|
|
748
|
+
"w-fit rounded-md border border-zinc-200 bg-white p-3 text-zinc-900 shadow-sm",
|
|
749
|
+
className,
|
|
750
|
+
)}
|
|
751
|
+
>
|
|
752
|
+
<div className="mb-3 flex items-center justify-between gap-3">
|
|
753
|
+
<button
|
|
754
|
+
type="button"
|
|
755
|
+
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900"
|
|
756
|
+
onClick={() => setMonth(addMonths(currentMonth, -1))}
|
|
757
|
+
aria-label="Previous month"
|
|
758
|
+
>
|
|
759
|
+
{"<"}
|
|
760
|
+
</button>
|
|
761
|
+
<div className="min-w-32 text-center text-sm font-medium">
|
|
762
|
+
{currentMonth.toLocaleDateString(undefined, {
|
|
763
|
+
month: "long",
|
|
764
|
+
year: "numeric",
|
|
765
|
+
})}
|
|
766
|
+
</div>
|
|
767
|
+
<button
|
|
768
|
+
type="button"
|
|
769
|
+
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900"
|
|
770
|
+
onClick={() => setMonth(addMonths(currentMonth, 1))}
|
|
771
|
+
aria-label="Next month"
|
|
772
|
+
>
|
|
773
|
+
{">"}
|
|
774
|
+
</button>
|
|
775
|
+
</div>
|
|
776
|
+
|
|
777
|
+
<div className="grid grid-cols-7 gap-1 text-center text-[11px] font-medium uppercase tracking-[0.08em] text-zinc-500">
|
|
778
|
+
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((day) => (
|
|
779
|
+
<div key={day} className="h-6 leading-6">
|
|
780
|
+
{day}
|
|
781
|
+
</div>
|
|
782
|
+
))}
|
|
783
|
+
</div>
|
|
784
|
+
<div className="mt-1 grid grid-cols-7 gap-1">
|
|
785
|
+
{monthDays(currentMonth).map((date) => {
|
|
786
|
+
const outside = date.getMonth() !== currentMonth.getMonth()
|
|
787
|
+
const selectedSingle = isSameDay(date, selectedDate)
|
|
788
|
+
const selectedRangeStart = isSameDay(date, range?.from)
|
|
789
|
+
const selectedRangeEnd = isSameDay(date, range?.to)
|
|
790
|
+
const selectedRangeMiddle = isBetween(date, range?.from, range?.to)
|
|
791
|
+
const selectedState =
|
|
792
|
+
selectedSingle || selectedRangeStart || selectedRangeEnd
|
|
793
|
+
const inRange = selectedRangeMiddle && !selectedState
|
|
794
|
+
|
|
795
|
+
return (
|
|
796
|
+
<button
|
|
797
|
+
key={date.toISOString()}
|
|
798
|
+
type="button"
|
|
799
|
+
data-slot="calendar-day"
|
|
800
|
+
data-selected={selectedState || undefined}
|
|
801
|
+
data-range-middle={inRange || undefined}
|
|
802
|
+
onClick={() => selectDate(date)}
|
|
803
|
+
className={cn(
|
|
804
|
+
"h-8 w-8 rounded-md text-center text-sm tabular-nums outline-none transition-colors hover:bg-zinc-100 focus:bg-zinc-100",
|
|
805
|
+
outside && "text-zinc-300",
|
|
806
|
+
inRange && "bg-zinc-100 text-zinc-900",
|
|
807
|
+
selectedState && "bg-zinc-900 text-white hover:bg-zinc-800 focus:bg-zinc-800",
|
|
808
|
+
)}
|
|
809
|
+
>
|
|
810
|
+
{date.getDate()}
|
|
811
|
+
</button>
|
|
812
|
+
)
|
|
813
|
+
})}
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function CalendarDayButton(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
820
|
+
return <button type="button" {...props} />
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function DatePicker({
|
|
824
|
+
value,
|
|
825
|
+
onValueChange,
|
|
826
|
+
placeholder = "Pick a date",
|
|
827
|
+
className,
|
|
828
|
+
}: {
|
|
829
|
+
value?: Date
|
|
830
|
+
onValueChange?: (value: Date | undefined) => void
|
|
831
|
+
placeholder?: string
|
|
832
|
+
className?: string
|
|
833
|
+
}) {
|
|
834
|
+
const [open, setOpen] = React.useState(false)
|
|
835
|
+
|
|
836
|
+
return (
|
|
837
|
+
<div className={cn("relative inline-block", className)}>
|
|
838
|
+
<button
|
|
839
|
+
type="button"
|
|
840
|
+
className="inline-flex h-9 min-w-40 items-center justify-between gap-2 rounded-md border border-zinc-200 bg-white px-3 text-sm text-zinc-900 shadow-sm hover:bg-zinc-50"
|
|
841
|
+
onClick={() => setOpen((next) => !next)}
|
|
842
|
+
>
|
|
843
|
+
<span className={cn(!value && "text-zinc-400")}>
|
|
844
|
+
{value ? formatDate(value) : placeholder}
|
|
845
|
+
</span>
|
|
846
|
+
<span className="text-xs text-zinc-400">▾</span>
|
|
847
|
+
</button>
|
|
848
|
+
{open ? (
|
|
849
|
+
<div className="absolute left-0 top-[calc(100%+4px)] z-50">
|
|
850
|
+
<Calendar
|
|
851
|
+
selected={value}
|
|
852
|
+
defaultMonth={value}
|
|
853
|
+
onSelect={(nextValue) => {
|
|
854
|
+
onValueChange?.(nextValue instanceof Date ? nextValue : undefined)
|
|
855
|
+
setOpen(false)
|
|
856
|
+
}}
|
|
857
|
+
/>
|
|
858
|
+
</div>
|
|
859
|
+
) : null}
|
|
860
|
+
</div>
|
|
861
|
+
)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export { Calendar, CalendarDayButton, DatePicker }
|
|
865
|
+
`},{path:"src/components/ui/chart.tsx",contents:`"use client"
|
|
866
|
+
|
|
867
|
+
import * as React from "react"
|
|
868
|
+
import * as RechartsPrimitive from "recharts"
|
|
869
|
+
|
|
870
|
+
import { cn } from "@/lib/utils"
|
|
871
|
+
|
|
872
|
+
export type ChartConfig = {
|
|
873
|
+
[key: string]: {
|
|
874
|
+
label?: React.ReactNode
|
|
875
|
+
color?: string
|
|
876
|
+
theme?: {
|
|
877
|
+
light?: string
|
|
878
|
+
dark?: string
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
type ChartContextProps = {
|
|
884
|
+
config: ChartConfig
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
|
888
|
+
|
|
889
|
+
function useChart() {
|
|
890
|
+
const context = React.useContext(ChartContext)
|
|
891
|
+
if (!context) {
|
|
892
|
+
throw new Error("useChart must be used within a ChartContainer")
|
|
893
|
+
}
|
|
894
|
+
return context
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export function ChartContainer({
|
|
898
|
+
id,
|
|
899
|
+
className,
|
|
900
|
+
children,
|
|
901
|
+
config,
|
|
902
|
+
...props
|
|
903
|
+
}: React.ComponentProps<"div"> & {
|
|
904
|
+
config: ChartConfig
|
|
905
|
+
children: React.ReactNode
|
|
906
|
+
}) {
|
|
907
|
+
const uniqueId = React.useId().replace(/:/g, "")
|
|
908
|
+
const chartId = "chart-" + (id || uniqueId)
|
|
909
|
+
|
|
910
|
+
return (
|
|
911
|
+
<ChartContext.Provider value={{ config }}>
|
|
912
|
+
<div
|
|
913
|
+
data-chart={chartId}
|
|
914
|
+
className={cn(
|
|
915
|
+
"flex aspect-video justify-center text-xs text-muted-foreground [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-none",
|
|
916
|
+
className,
|
|
917
|
+
)}
|
|
918
|
+
{...props}
|
|
919
|
+
>
|
|
920
|
+
<ChartStyle id={chartId} config={config} />
|
|
921
|
+
<RechartsPrimitive.ResponsiveContainer>
|
|
922
|
+
{children}
|
|
923
|
+
</RechartsPrimitive.ResponsiveContainer>
|
|
924
|
+
</div>
|
|
925
|
+
</ChartContext.Provider>
|
|
926
|
+
)
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function ChartStyle({ id, config }: { id: string; config: ChartConfig }) {
|
|
930
|
+
const variables = Object.entries(config)
|
|
931
|
+
.map(([key, item]) => {
|
|
932
|
+
const color = item.theme?.light || item.color
|
|
933
|
+
return color ? " --color-" + key + ": " + color + ";" : null
|
|
934
|
+
})
|
|
935
|
+
.filter(Boolean)
|
|
936
|
+
|
|
937
|
+
if (!variables.length) return null
|
|
938
|
+
|
|
939
|
+
return (
|
|
940
|
+
<style
|
|
941
|
+
dangerouslySetInnerHTML={{
|
|
942
|
+
__html: "[data-chart=\\"" + id + "\\"] {\\n" + variables.join("\\n") + "\\n}",
|
|
943
|
+
}}
|
|
944
|
+
/>
|
|
945
|
+
)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
export const ChartTooltip = RechartsPrimitive.Tooltip
|
|
949
|
+
export const ChartLegend = RechartsPrimitive.Legend
|
|
950
|
+
|
|
951
|
+
export function ChartTooltipContent({
|
|
952
|
+
active,
|
|
953
|
+
payload,
|
|
954
|
+
label,
|
|
955
|
+
className,
|
|
956
|
+
hideLabel = false,
|
|
957
|
+
hideIndicator = false,
|
|
958
|
+
}: {
|
|
959
|
+
active?: boolean
|
|
960
|
+
payload?: Array<{
|
|
961
|
+
color?: string
|
|
962
|
+
dataKey?: string | number
|
|
963
|
+
name?: string | number
|
|
964
|
+
value?: React.ReactNode
|
|
965
|
+
}>
|
|
966
|
+
label?: React.ReactNode
|
|
967
|
+
className?: string
|
|
968
|
+
hideLabel?: boolean
|
|
969
|
+
hideIndicator?: boolean
|
|
970
|
+
}) {
|
|
971
|
+
const { config } = useChart()
|
|
972
|
+
|
|
973
|
+
if (!active || !payload?.length) return null
|
|
974
|
+
|
|
975
|
+
return (
|
|
976
|
+
<div className={cn("grid min-w-[8rem] gap-1.5 rounded-lg border border-border bg-background px-2.5 py-1.5 text-xs shadow-xl", className)}>
|
|
977
|
+
{!hideLabel && label ? <div className="font-medium text-foreground">{label}</div> : null}
|
|
978
|
+
<div className="grid gap-1.5">
|
|
979
|
+
{payload.map((item) => {
|
|
980
|
+
const key = String(item.dataKey || item.name || "")
|
|
981
|
+
const chartItem = config[key]
|
|
982
|
+
const color = item.color || chartItem?.color || "var(--color-" + key + ")"
|
|
983
|
+
|
|
984
|
+
return (
|
|
985
|
+
<div key={key} className="flex min-w-0 items-center gap-2">
|
|
986
|
+
{!hideIndicator ? (
|
|
987
|
+
<span
|
|
988
|
+
className="h-2.5 w-2.5 shrink-0 rounded-[2px]"
|
|
989
|
+
style={{ backgroundColor: color }}
|
|
990
|
+
/>
|
|
991
|
+
) : null}
|
|
992
|
+
<span className="min-w-0 flex-1 truncate text-muted-foreground">
|
|
993
|
+
{chartItem?.label || item.name || key}
|
|
994
|
+
</span>
|
|
995
|
+
<span className="font-mono font-medium tabular-nums text-foreground">
|
|
996
|
+
{typeof item.value === "number" ? item.value.toLocaleString() : item.value}
|
|
997
|
+
</span>
|
|
998
|
+
</div>
|
|
999
|
+
)
|
|
1000
|
+
})}
|
|
1001
|
+
</div>
|
|
1002
|
+
</div>
|
|
1003
|
+
)
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
export function ChartLegendContent({
|
|
1007
|
+
payload,
|
|
1008
|
+
className,
|
|
1009
|
+
}: {
|
|
1010
|
+
payload?: Array<{ value?: string | number; color?: string }>
|
|
1011
|
+
className?: string
|
|
1012
|
+
}) {
|
|
1013
|
+
const { config } = useChart()
|
|
1014
|
+
|
|
1015
|
+
if (!payload?.length) return null
|
|
1016
|
+
|
|
1017
|
+
return (
|
|
1018
|
+
<div className={cn("flex flex-wrap items-center justify-center gap-4 text-xs", className)}>
|
|
1019
|
+
{payload.map((item) => {
|
|
1020
|
+
const key = String(item.value || "")
|
|
1021
|
+
const chartItem = config[key]
|
|
1022
|
+
return (
|
|
1023
|
+
<div key={key} className="flex items-center gap-1.5">
|
|
1024
|
+
<span
|
|
1025
|
+
className="h-2.5 w-2.5 rounded-[2px]"
|
|
1026
|
+
style={{ backgroundColor: item.color || chartItem?.color || "var(--color-" + key + ")" }}
|
|
1027
|
+
/>
|
|
1028
|
+
<span className="text-muted-foreground">{chartItem?.label || key}</span>
|
|
1029
|
+
</div>
|
|
1030
|
+
)
|
|
1031
|
+
})}
|
|
1032
|
+
</div>
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
`},{path:"src/components/ui/table.tsx",contents:`import type { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from "react"
|
|
1036
|
+
|
|
1037
|
+
import { cn } from "@/lib/utils"
|
|
1038
|
+
|
|
1039
|
+
export function Table(props: HTMLAttributes<HTMLTableElement>) {
|
|
1040
|
+
return <table {...props} className={cn("w-full caption-bottom text-sm", props.className)} />
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export function TableHeader(props: HTMLAttributes<HTMLTableSectionElement>) {
|
|
1044
|
+
return <thead {...props} className={cn("bg-zinc-50 text-xs uppercase text-zinc-500", props.className)} />
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export function TableBody(props: HTMLAttributes<HTMLTableSectionElement>) {
|
|
1048
|
+
return <tbody {...props} className={cn("divide-y divide-zinc-200 bg-white", props.className)} />
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export function TableRow(props: HTMLAttributes<HTMLTableRowElement>) {
|
|
1052
|
+
return <tr {...props} className={cn("border-b border-zinc-200 last:border-0", props.className)} />
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export function TableHead(props: ThHTMLAttributes<HTMLTableCellElement>) {
|
|
1056
|
+
return <th {...props} className={cn("h-10 whitespace-nowrap px-4 text-left align-middle font-medium", props.className)} />
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
export function TableCell(props: TdHTMLAttributes<HTMLTableCellElement>) {
|
|
1060
|
+
return <td {...props} className={cn("whitespace-nowrap px-4 py-3 align-middle text-zinc-600", props.className)} />
|
|
1061
|
+
}
|
|
1062
|
+
`}],C=[{id:"semaphor-analytics",name:"Semaphor Analytics Starter",description:"Approved client-side React starter with Semaphor data hooks, shadcn source components, Tailwind v4, and Recharts.",files:u}],M=C[0].id;function p(e){return e.map(t=>({path:m(t.path),contents:t.contents})).sort((t,r)=>t.path.localeCompare(r.path))}function W(e=M){return C.find(t=>t.id===e)||C[0]}const Z={id:"semaphor-browser-runtime-v1",name:"Semaphor Browser Runtime",execution:"browser-sandbox",moduleSystem:"virtual-tsx-commonjs",cssPipeline:{mode:"tailwind-v4-or-fallback",status:"The runtime tries Tailwind v4 candidate compilation and reports when it falls back to bundled utility CSS. Unsupported package CSS requires Local Bridge."},packages:[{name:"react",source:"host",status:"supported"},{name:"react-dom/client",source:"host",status:"supported"},{name:"react-semaphor/data-app-sdk",source:"runtime",status:"supported"},{name:"@semaphor/data-app-sdk",source:"runtime",status:"supported"},{name:"lucide-react",source:"shim",status:"partial"},{name:"recharts",source:"host",status:"partial"},{name:"clsx",source:"runtime",status:"supported"},{name:"tailwind-merge",source:"runtime",status:"supported"},{name:"class-variance-authority",source:"shim",status:"partial"},{name:"tailwindcss",source:"runtime",status:"partial"}],components:[{name:"badge",importPath:"@/components/ui/badge",sourceFile:"src/components/ui/badge.tsx",implementation:"source",dependencies:[]},{name:"button",importPath:"@/components/ui/button",sourceFile:"src/components/ui/button.tsx",implementation:"source",dependencies:[]},{name:"card",importPath:"@/components/ui/card",sourceFile:"src/components/ui/card.tsx",implementation:"source",dependencies:[]},{name:"chart",importPath:"@/components/ui/chart",sourceFile:"src/components/ui/chart.tsx",implementation:"source",dependencies:["recharts"]},{name:"select",importPath:"@/components/ui/select",sourceFile:"src/components/ui/select.tsx",implementation:"source",dependencies:[]},{name:"progress",importPath:"@/components/ui/progress",sourceFile:"src/components/ui/progress.tsx",implementation:"source",dependencies:[]},{name:"calendar",importPath:"@/components/ui/calendar",sourceFile:"src/components/ui/calendar.tsx",implementation:"source",dependencies:[]},{name:"table",importPath:"@/components/ui/table",sourceFile:"src/components/ui/table.tsx",implementation:"source",dependencies:[]}],limits:{writeScope:["src/**"],editableGlobs:["src/**"],unsupported:["package edits","arbitrary npm installs","server or Node APIs","Vite plugin changes","environment files"]}},y={projectRoot:x,framework:"vite-react",packageManager:"browser-sandbox",runtimeManifest:Z,packageJson:{dependencies:{"@semaphor/data-app-sdk":"bundled","class-variance-authority":"bundled",clsx:"bundled","lucide-react":"bundled",react:"bundled","react-dom":"bundled","react-semaphor":"bundled",recharts:"bundled","tailwind-merge":"bundled",tailwindcss:"bundled"},devDependencies:{"@vitejs/plugin-react":"browser-sandbox",typescript:"browser-sandbox",vite:"browser-sandbox"},scripts:{dev:"browser-sandbox dev",build:"browser-sandbox build",typecheck:"browser-sandbox validate"}},componentsJson:{aliases:{ui:"@/components/ui"}},tsconfig:{baseUrl:".",paths:{"@/*":["src/*"]},jsx:"react-jsx",strict:!0,noUnusedLocals:!0,noUnusedParameters:!0},files:{source:u.map(e=>e.path),root:["package.json","vite.config.ts","tsconfig.json"],editable:u.map(e=>e.path),editableGlobs:["src/**"],approvalRequiredGlobs:["package.json","package-lock.json",".env*","*.config.*","vite.config.*","tsconfig*.json"],styleEntries:["src/index.css"],localModules:u.map(e=>e.path).filter(e=>/\.(ts|tsx|js|jsx)$/.test(e))},validation:{typecheck:"browser sandbox static validation",build:"browser sandbox static export"},capabilities:{version:1,source:"browser-sandbox-template",uiComponents:[{name:"badge",importPath:"@/components/ui/badge",exports:["Badge"],example:{imports:'import { Badge } from "@/components/ui/badge"',usage:'<Badge variant="secondary">Ready</Badge>'}},{name:"button",importPath:"@/components/ui/button",exports:["Button"],example:{imports:'import { Button } from "@/components/ui/button"',usage:'<Button variant="outline">Reset</Button>'}},{name:"card",importPath:"@/components/ui/card",exports:["Card","CardContent","CardDescription","CardHeader","CardTitle"],example:{imports:'import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"',usage:"<Card><CardHeader><CardTitle>Title</CardTitle><CardDescription>Context</CardDescription></CardHeader><CardContent>Content</CardContent></Card>"}},{name:"chart",importPath:"@/components/ui/chart",exports:["ChartConfig","ChartContainer","ChartLegend","ChartLegendContent","ChartTooltip","ChartTooltipContent"],example:{imports:'import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart"',usage:'<ChartContainer config={chartConfig} className="h-[280px] w-full"><BarChart data={data}><ChartTooltip content={<ChartTooltipContent />} /></BarChart></ChartContainer>'}},{name:"select",importPath:"@/components/ui/select",exports:["Select","SelectContent","SelectGroup","SelectItem","SelectLabel","SelectSeparator","SelectTrigger","SelectValue"],example:{imports:'import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"',usage:'<Select value={value} onValueChange={setValue}><SelectTrigger className="w-48"><SelectValue placeholder="Segment" /></SelectTrigger><SelectContent><SelectItem value="enterprise">Enterprise</SelectItem></SelectContent></Select>'}},{name:"progress",importPath:"@/components/ui/progress",exports:["Progress"],example:{imports:'import { Progress } from "@/components/ui/progress"',usage:'<Progress value={72} className="h-2" />'}},{name:"calendar",importPath:"@/components/ui/calendar",exports:["Calendar","CalendarDayButton","DatePicker"],example:{imports:'import { DatePicker } from "@/components/ui/calendar"',usage:'<DatePicker value={date} onValueChange={setDate} placeholder="Order date" />'}},{name:"table",importPath:"@/components/ui/table",exports:["Table","TableBody","TableCell","TableHead","TableHeader","TableRow"],example:{imports:'import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"',usage:"<Table><TableHeader><TableRow><TableHead>Name</TableHead></TableRow></TableHeader><TableBody><TableRow><TableCell>Northstar</TableCell></TableRow></TableBody></Table>"}}],dataApis:[{package:"react-semaphor/data-app-sdk",source:"bundled",exports:["SemaphorDataAppProvider","useSemaphorInput","useSemaphorInputOptions","useSemaphorMetric","useSemaphorRecords"],datasets:[{name:"orders",fields:["category","customer_segment","gross_margin","order_date","orders","region","revenue","satisfaction_score"]},{name:"customers",fields:["arr","customer_name","customer_segment","health_score","last_active_days","open_tickets","region"]}],examples:["const segmentInput = useSemaphorInput({ id: 'segment', kind: 'filter', field: 'customer_segment' })","const revenue = useSemaphorMetric({ id: 'revenue', dataset: 'orders', metric: 'revenue', inputs: [segmentInput] })","const rows = useSemaphorRecords({ id: 'trend', dataset: 'orders', dimensions: ['order_date'], measures: ['revenue'], inputs: [segmentInput] })"],constraints:["Semaphor input kind is only 'filter' or 'control'.","Pass input objects into metrics/records via inputs: [input].","useSemaphorMetric config uses metric, not measures.","useSemaphorRecords returns { records }, and records are plain objects."]}]}};function U(){return typeof window<"u"}function m(e){return e.replace(/\\/g,"/").replace(/\/+/g,"/").replace(/^\.\//,"")}function Q(e){const t=new Map(p(e).map(r=>[m(r.path),r]));for(const r of u){const a=m(r.path);t.has(a)||t.set(a,{path:a,contents:r.contents})}return Array.from(t.values()).sort((r,a)=>r.path.localeCompare(a.path))}function f(){if(!U())return p(u);try{const e=window.localStorage.getItem(O);if(!e)return p(u);const t=JSON.parse(e);if(!Array.isArray(t)||t.length===0)return p(u);const r=Q(t);return r.length!==t.length&&D(r),r}catch{return p(u)}}function D(e){U()&&window.localStorage.setItem(O,JSON.stringify(e))}function q(e=f()){return new Map(e.map(t=>[m(t.path),t.contents]))}function ee(e){const t=e.map(r=>m(r.path)).sort();return Object.assign(Object.assign({},y),{files:Object.assign(Object.assign({},y.files),{source:t,editable:t.filter(r=>/^src\/.+\.(ts|tsx|js|jsx|css|json)$/.test(r)),styleEntries:t.filter(r=>/(^|\/)(index|app|globals)\.css$/i.test(r)),localModules:t.filter(r=>/\.(ts|tsx|js|jsx)$/.test(r))})})}function te(){return C.map(e=>({id:e.id,name:e.name,description:e.description,fileCount:e.files.length}))}function re(e=M){D(p(W(e).files))}function ae(){return ee(f())}function ne(e){const t=q();return e.map(r=>({path:r,contents:t.get(m(r))||""}))}function oe(){return f().map(e=>({path:m(e.path),size:e.contents.length})).sort((e,t)=>e.path.localeCompare(t.path))}function se(e=M){const t=W(e);return{provider:"browser-sandbox-template",summary:`Seeded the ${t.name} template.`,changes:[{kind:"edit",label:`Replaced the virtual browser workspace with ${t.files.length} approved template files`}],files:p(t.files),replaceExistingFiles:!0,generationMeta:{model:"approved-template",strategy:t.id,durationMs:0,outputChars:t.files.reduce((r,a)=>r+a.contents.length,0)}}}function ie({files:e,sourcePath:t}){return{provider:"browser-sandbox-local-import",summary:`Imported approved browser files from ${t}.`,changes:[{kind:"edit",label:`Replaced the virtual browser workspace with ${e.length} local template files`}],files:p(e),replaceExistingFiles:!0,generationMeta:{model:"local-template-import",strategy:"dev-local-import",durationMs:0,outputChars:e.reduce((r,a)=>r+a.contents.length,0)}}}function v({command:e,ok:t,stderr:r="",stdout:a="",startedAt:n}){return{command:e,durationMs:Date.now()-n,exitCode:t?0:1,ok:t,stdout:a,stderr:r}}function le(e){const t=new Set([".ts",".tsx",".js",".jsx",".css",".json"]),r=[];for(const a of e){const n=m(a.path),o=n.match(/\.[^.]+$/),i=(o==null?void 0:o[0])||"";if(!a.path||n.startsWith("../")||n.startsWith("/")){r.push(`${a.path}: generated file path must be a normalized relative project path.`);continue}if(!n.startsWith("src/")){r.push(`${a.path}: browser sandbox revisions may only write files under src/**.`);continue}t.has(i)||r.push(`${a.path}: generated source files must use one of ${[...t].join(", ")}.`)}return{ok:r.length===0,diagnostics:r}}function ce(e){const t=new Set,r=[/import\s+(?:type\s+)?(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/g,/export\s+(?:type\s+)?[^'"]+\s+from\s+['"]([^'"]+)['"]/g,/import\(\s*['"]([^'"]+)['"]\s*\)/g];for(const a of r){let n;for(;n=a.exec(e);)t.add(n[1])}return[...t]}function de(e){if(e.startsWith("@")){const[t,r]=e.split("/");return r?`${t}/${r}`:e}return e.split("/")[0]}function me(e,t,r){const a=[],n=o=>{a.push(o);for(const i of[".tsx",".ts",".jsx",".js",".css",".json"])a.push(`${o}${i}`);for(const i of["index.tsx","index.ts","index.jsx","index.js"])a.push(`${o}/${i}`)};if(t.startsWith(".")){const o=m(e).split("/");o.pop();const i=[...o,...t.split("/")],l=[];for(const s of i)if(!(!s||s===".")){if(s===".."){l.pop();continue}l.push(s)}n(l.join("/"))}return t.startsWith("@/")&&n(`src/${t.slice(2)}`),a.some(o=>r.has(m(o)))}function ue(e,t=f()){var r,a;const n=Object.assign(Object.assign({},(r=y.packageJson)===null||r===void 0?void 0:r.dependencies),(a=y.packageJson)===null||a===void 0?void 0:a.devDependencies),o=new Set([...t.map(s=>m(s.path)),...e.map(s=>m(s.path))]),i=new Set(["child_process","crypto","fs","http","https","node:child_process","node:crypto","node:fs","node:http","node:https","node:path","path"]),l=[];for(const s of e)for(const d of ce(s.contents)){const b=de(d);if(i.has(d)||i.has(b)){l.push(`${s.path}: blocked server/runtime import "${d}". Browser sandbox apps must run in the browser.`);continue}if(d.startsWith(".")||d.startsWith("@/")){me(s.path,d,o)||l.push(`${s.path}: local import "${d}" does not resolve from the browser sandbox files.`);continue}n[b]||l.push(`${s.path}: package import "${d}" is not available in Browser Sandbox. Switch to Local Bridge for arbitrary npm packages.`)}return{ok:l.length===0,diagnostics:l}}function pe(e){const t=Date.now(),r=le(e.files),a=e.replaceExistingFiles?[]:f();if(!r.ok){const s=v({command:"browser sandbox write policy validation",ok:!1,stderr:r.diagnostics.join(`
|
|
1063
|
+
`),startedAt:t});return{ok:!1,projectRoot:x,provider:e.provider||"browser-sandbox",validation:"write-policy-validation",summary:e.summary,changes:e.changes,generationMeta:e.generationMeta,files:e.files.map(d=>({path:d.path,size:d.contents.length})),changedFiles:[],writePolicyValidation:r,attempts:[{label:e.attemptLabel||"initial",ok:!1,command:s}],error:"Generated files failed Browser Sandbox write policy validation."}}const n=ue(e.files,a);if(!n.ok){const s=v({command:"browser sandbox import validation",ok:!1,stderr:n.diagnostics.join(`
|
|
1064
|
+
`),startedAt:t});return{ok:!1,projectRoot:x,provider:e.provider||"browser-sandbox",validation:"import-validation",summary:e.summary,changes:e.changes,generationMeta:e.generationMeta,files:e.files.map(d=>({path:d.path,size:d.contents.length})),changedFiles:[],importValidation:n,attempts:[{label:e.attemptLabel||"initial",ok:!1,command:s}],error:"Generated files failed Browser Sandbox import validation."}}const o=e.replaceExistingFiles?new Map:q(),i=[];for(const s of e.files){const d=m(s.path);o.set(d,s.contents),i.push(d)}D([...o.entries()].map(([s,d])=>({path:s,contents:d})).sort((s,d)=>s.path.localeCompare(d.path)));const l=v({command:"browser sandbox static validation",ok:!0,stdout:"Write policy and import validation passed. Browser preview refresh is handled by app-builder.",startedAt:t});return{ok:!0,projectRoot:x,provider:e.provider||"browser-sandbox",validation:"typecheck",summary:e.summary,changes:e.changes,generationMeta:e.generationMeta,files:e.files.map(s=>({path:s.path,size:s.contents.length})),changedFiles:i,importValidation:n,attempts:[{label:e.attemptLabel||"initial",ok:!0,command:l}],command:l}}function fe(){const e={path:"src/data-app/index.tsx",contents:`import { useEffect, useRef, useState } from "react"
|
|
1065
|
+
import { Activity, AlertTriangle, BarChart3, Users } from "lucide-react"
|
|
1066
|
+
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
|
|
1067
|
+
import {
|
|
1068
|
+
useSemaphorInput,
|
|
1069
|
+
useSemaphorInputOptions,
|
|
1070
|
+
useSemaphorMetric,
|
|
1071
|
+
useSemaphorRecords,
|
|
1072
|
+
} from "react-semaphor/data-app-sdk"
|
|
1073
|
+
|
|
1074
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
1075
|
+
import {
|
|
1076
|
+
ChartContainer,
|
|
1077
|
+
ChartTooltip,
|
|
1078
|
+
ChartTooltipContent,
|
|
1079
|
+
type ChartConfig,
|
|
1080
|
+
} from "@/components/ui/chart"
|
|
1081
|
+
import {
|
|
1082
|
+
Select,
|
|
1083
|
+
SelectContent,
|
|
1084
|
+
SelectItem,
|
|
1085
|
+
SelectTrigger,
|
|
1086
|
+
SelectValue,
|
|
1087
|
+
} from "@/components/ui/select"
|
|
1088
|
+
|
|
1089
|
+
export function DataApp() {
|
|
1090
|
+
const statusRef = useRef<HTMLSpanElement | null>(null)
|
|
1091
|
+
const [runtimeStatus, setRuntimeStatus] = useState("mounting")
|
|
1092
|
+
|
|
1093
|
+
useEffect(() => {
|
|
1094
|
+
statusRef.current?.setAttribute("data-ready", "true")
|
|
1095
|
+
setRuntimeStatus("interactive")
|
|
1096
|
+
}, [])
|
|
1097
|
+
|
|
1098
|
+
const segmentOptions = useSemaphorInputOptions({
|
|
1099
|
+
id: "segment-options",
|
|
1100
|
+
dataset: "customers",
|
|
1101
|
+
field: "customer_segment",
|
|
1102
|
+
})
|
|
1103
|
+
const regionOptions = useSemaphorInputOptions({
|
|
1104
|
+
id: "region-options",
|
|
1105
|
+
dataset: "customers",
|
|
1106
|
+
field: "region",
|
|
1107
|
+
})
|
|
1108
|
+
const segment = useSemaphorInput({
|
|
1109
|
+
id: "segment",
|
|
1110
|
+
kind: "filter",
|
|
1111
|
+
field: "customer_segment",
|
|
1112
|
+
value: "",
|
|
1113
|
+
options: [{ value: "", label: "All segments" }, ...segmentOptions.options],
|
|
1114
|
+
})
|
|
1115
|
+
const region = useSemaphorInput({
|
|
1116
|
+
id: "region",
|
|
1117
|
+
kind: "filter",
|
|
1118
|
+
field: "region",
|
|
1119
|
+
value: "",
|
|
1120
|
+
options: [{ value: "", label: "All regions" }, ...regionOptions.options],
|
|
1121
|
+
})
|
|
1122
|
+
const revenue = useSemaphorMetric({
|
|
1123
|
+
id: "revenue",
|
|
1124
|
+
dataset: "orders",
|
|
1125
|
+
metric: "revenue",
|
|
1126
|
+
comparison: "previous_period",
|
|
1127
|
+
inputs: [segment, region],
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
const orders = useSemaphorMetric({
|
|
1131
|
+
id: "orders",
|
|
1132
|
+
dataset: "orders",
|
|
1133
|
+
metric: "orders",
|
|
1134
|
+
comparison: "previous_period",
|
|
1135
|
+
inputs: [segment, region],
|
|
1136
|
+
})
|
|
1137
|
+
const arr = useSemaphorMetric({
|
|
1138
|
+
id: "arr",
|
|
1139
|
+
dataset: "customers",
|
|
1140
|
+
metric: "arr",
|
|
1141
|
+
inputs: [segment, region],
|
|
1142
|
+
})
|
|
1143
|
+
const tickets = useSemaphorMetric({
|
|
1144
|
+
id: "tickets",
|
|
1145
|
+
dataset: "customers",
|
|
1146
|
+
metric: "open_tickets",
|
|
1147
|
+
inputs: [segment, region],
|
|
1148
|
+
})
|
|
1149
|
+
const customerHealth = useSemaphorRecords({
|
|
1150
|
+
id: "customer-health",
|
|
1151
|
+
dataset: "customers",
|
|
1152
|
+
dimensions: ["customer_name", "customer_segment", "region"],
|
|
1153
|
+
measures: ["health_score", "open_tickets", "arr"],
|
|
1154
|
+
inputs: [segment, region],
|
|
1155
|
+
limit: 6,
|
|
1156
|
+
})
|
|
1157
|
+
const chartData = customerHealth.records.map((row) => ({
|
|
1158
|
+
customer: String(row.customer_name).split(" ")[0],
|
|
1159
|
+
arr: Number(row.arr || 0),
|
|
1160
|
+
open_tickets: Number(row.open_tickets || 0),
|
|
1161
|
+
}))
|
|
1162
|
+
const chartConfig = {
|
|
1163
|
+
arr: {
|
|
1164
|
+
label: "ARR",
|
|
1165
|
+
color: "#2563eb",
|
|
1166
|
+
},
|
|
1167
|
+
open_tickets: {
|
|
1168
|
+
label: "Open tickets",
|
|
1169
|
+
color: "#f97316",
|
|
1170
|
+
},
|
|
1171
|
+
} satisfies ChartConfig
|
|
1172
|
+
|
|
1173
|
+
return (
|
|
1174
|
+
<main className="min-h-screen bg-zinc-50 p-4 text-zinc-950 sm:p-6">
|
|
1175
|
+
<section className="mx-auto max-w-6xl space-y-4">
|
|
1176
|
+
<div className="flex flex-col gap-3 rounded-xl border border-zinc-200 bg-white p-4 shadow-sm lg:flex-row lg:items-center lg:justify-between">
|
|
1177
|
+
<div className="min-w-0">
|
|
1178
|
+
<div className="mb-2 inline-flex items-center gap-2 rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700">
|
|
1179
|
+
<BarChart3 className="size-4" />
|
|
1180
|
+
<span ref={statusRef}>Real React runtime: {runtimeStatus}</span>
|
|
1181
|
+
</div>
|
|
1182
|
+
<h1 className="text-2xl font-semibold tracking-tight">Revenue Command Center</h1>
|
|
1183
|
+
<p className="mt-1 text-sm text-zinc-500">
|
|
1184
|
+
Responsive filters, KPI cards, and customer watchlist render in-browser.
|
|
1185
|
+
</p>
|
|
1186
|
+
</div>
|
|
1187
|
+
<div className="grid gap-2 sm:grid-cols-2 lg:w-[360px]">
|
|
1188
|
+
<label className="text-xs font-medium text-zinc-600">
|
|
1189
|
+
Segment
|
|
1190
|
+
<Select value={String(segment.value ?? "")} onValueChange={segment.setValue}>
|
|
1191
|
+
<SelectTrigger className="mt-1 w-full">
|
|
1192
|
+
<SelectValue placeholder="All segments" />
|
|
1193
|
+
</SelectTrigger>
|
|
1194
|
+
<SelectContent>
|
|
1195
|
+
{segment.options.map((option) => (
|
|
1196
|
+
<SelectItem key={option.value} value={String(option.value)}>
|
|
1197
|
+
{option.label}
|
|
1198
|
+
</SelectItem>
|
|
1199
|
+
))}
|
|
1200
|
+
</SelectContent>
|
|
1201
|
+
</Select>
|
|
1202
|
+
</label>
|
|
1203
|
+
<label className="text-xs font-medium text-zinc-600">
|
|
1204
|
+
Region
|
|
1205
|
+
<Select value={String(region.value ?? "")} onValueChange={region.setValue}>
|
|
1206
|
+
<SelectTrigger className="mt-1 w-full">
|
|
1207
|
+
<SelectValue placeholder="All regions" />
|
|
1208
|
+
</SelectTrigger>
|
|
1209
|
+
<SelectContent>
|
|
1210
|
+
{region.options.map((option) => (
|
|
1211
|
+
<SelectItem key={option.value} value={String(option.value)}>
|
|
1212
|
+
{option.label}
|
|
1213
|
+
</SelectItem>
|
|
1214
|
+
))}
|
|
1215
|
+
</SelectContent>
|
|
1216
|
+
</Select>
|
|
1217
|
+
</label>
|
|
1218
|
+
</div>
|
|
1219
|
+
</div>
|
|
1220
|
+
|
|
1221
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
1222
|
+
<Card>
|
|
1223
|
+
<CardHeader className="flex flex-row items-center justify-between gap-2">
|
|
1224
|
+
<CardTitle>Revenue</CardTitle>
|
|
1225
|
+
<Activity className="size-4 text-zinc-400" />
|
|
1226
|
+
</CardHeader>
|
|
1227
|
+
<CardContent>
|
|
1228
|
+
<div className="text-3xl font-semibold">
|
|
1229
|
+
{revenue.value?.toLocaleString() ?? "0"}
|
|
1230
|
+
</div>
|
|
1231
|
+
<p className="mt-1 text-xs text-zinc-500">Current order fixture total</p>
|
|
1232
|
+
</CardContent>
|
|
1233
|
+
</Card>
|
|
1234
|
+
<Card>
|
|
1235
|
+
<CardHeader className="flex flex-row items-center justify-between gap-2">
|
|
1236
|
+
<CardTitle>Orders</CardTitle>
|
|
1237
|
+
<BarChart3 className="size-4 text-zinc-400" />
|
|
1238
|
+
</CardHeader>
|
|
1239
|
+
<CardContent>
|
|
1240
|
+
<div className="text-3xl font-semibold">
|
|
1241
|
+
{orders.value?.toLocaleString() ?? "0"}
|
|
1242
|
+
</div>
|
|
1243
|
+
<p className="mt-1 text-xs text-zinc-500">Filtered order volume</p>
|
|
1244
|
+
</CardContent>
|
|
1245
|
+
</Card>
|
|
1246
|
+
<Card>
|
|
1247
|
+
<CardHeader className="flex flex-row items-center justify-between gap-2">
|
|
1248
|
+
<CardTitle>Customer ARR</CardTitle>
|
|
1249
|
+
<Users className="size-4 text-zinc-400" />
|
|
1250
|
+
</CardHeader>
|
|
1251
|
+
<CardContent>
|
|
1252
|
+
<div className="text-3xl font-semibold">
|
|
1253
|
+
{arr.value?.toLocaleString() ?? "0"}
|
|
1254
|
+
</div>
|
|
1255
|
+
<p className="mt-1 text-xs text-zinc-500">Responds to segment and region</p>
|
|
1256
|
+
</CardContent>
|
|
1257
|
+
</Card>
|
|
1258
|
+
<Card>
|
|
1259
|
+
<CardHeader className="flex flex-row items-center justify-between gap-2">
|
|
1260
|
+
<CardTitle>Open Tickets</CardTitle>
|
|
1261
|
+
<AlertTriangle className="size-4 text-zinc-400" />
|
|
1262
|
+
</CardHeader>
|
|
1263
|
+
<CardContent>
|
|
1264
|
+
<div className="text-3xl font-semibold">
|
|
1265
|
+
{tickets.value?.toLocaleString() ?? "0"}
|
|
1266
|
+
</div>
|
|
1267
|
+
<p className="mt-1 text-xs text-zinc-500">Support pressure signal</p>
|
|
1268
|
+
</CardContent>
|
|
1269
|
+
</Card>
|
|
1270
|
+
</div>
|
|
1271
|
+
|
|
1272
|
+
<Card>
|
|
1273
|
+
<CardHeader>
|
|
1274
|
+
<CardTitle>Customer ARR by account</CardTitle>
|
|
1275
|
+
</CardHeader>
|
|
1276
|
+
<CardContent>
|
|
1277
|
+
<ChartContainer config={chartConfig} className="h-[280px] w-full">
|
|
1278
|
+
<BarChart data={chartData} margin={{ left: 0, right: 12 }}>
|
|
1279
|
+
<CartesianGrid vertical={false} />
|
|
1280
|
+
<XAxis
|
|
1281
|
+
dataKey="customer"
|
|
1282
|
+
tickLine={false}
|
|
1283
|
+
axisLine={false}
|
|
1284
|
+
tickMargin={8}
|
|
1285
|
+
/>
|
|
1286
|
+
<YAxis
|
|
1287
|
+
tickLine={false}
|
|
1288
|
+
axisLine={false}
|
|
1289
|
+
tickMargin={8}
|
|
1290
|
+
width={52}
|
|
1291
|
+
/>
|
|
1292
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
1293
|
+
<Bar dataKey="arr" fill="var(--color-arr)" radius={[4, 4, 0, 0]} />
|
|
1294
|
+
</BarChart>
|
|
1295
|
+
</ChartContainer>
|
|
1296
|
+
</CardContent>
|
|
1297
|
+
</Card>
|
|
1298
|
+
|
|
1299
|
+
<Card>
|
|
1300
|
+
<CardHeader>
|
|
1301
|
+
<CardTitle>Customer health watchlist</CardTitle>
|
|
1302
|
+
</CardHeader>
|
|
1303
|
+
<CardContent>
|
|
1304
|
+
<div className="overflow-x-auto rounded-lg border border-zinc-200">
|
|
1305
|
+
<table className="min-w-full text-left text-sm">
|
|
1306
|
+
<thead className="bg-zinc-50 text-xs uppercase text-zinc-500">
|
|
1307
|
+
<tr>
|
|
1308
|
+
<th className="px-4 py-3 font-medium">Customer</th>
|
|
1309
|
+
<th className="px-4 py-3 font-medium">Segment</th>
|
|
1310
|
+
<th className="px-4 py-3 font-medium">Region</th>
|
|
1311
|
+
<th className="px-4 py-3 text-right font-medium">Health</th>
|
|
1312
|
+
<th className="px-4 py-3 text-right font-medium">Tickets</th>
|
|
1313
|
+
<th className="px-4 py-3 text-right font-medium">ARR</th>
|
|
1314
|
+
</tr>
|
|
1315
|
+
</thead>
|
|
1316
|
+
<tbody className="divide-y divide-zinc-200 bg-white">
|
|
1317
|
+
{customerHealth.records.map((row) => (
|
|
1318
|
+
<tr key={row.customer_name}>
|
|
1319
|
+
<td className="whitespace-nowrap px-4 py-3 font-medium text-zinc-900">
|
|
1320
|
+
{row.customer_name}
|
|
1321
|
+
</td>
|
|
1322
|
+
<td className="whitespace-nowrap px-4 py-3 text-zinc-600">
|
|
1323
|
+
{row.customer_segment}
|
|
1324
|
+
</td>
|
|
1325
|
+
<td className="whitespace-nowrap px-4 py-3 text-zinc-600">{row.region}</td>
|
|
1326
|
+
<td className="whitespace-nowrap px-4 py-3 text-right font-medium">
|
|
1327
|
+
{row.health_score}
|
|
1328
|
+
</td>
|
|
1329
|
+
<td className="whitespace-nowrap px-4 py-3 text-right">
|
|
1330
|
+
{row.open_tickets}
|
|
1331
|
+
</td>
|
|
1332
|
+
<td className="whitespace-nowrap px-4 py-3 text-right">
|
|
1333
|
+
{Number(row.arr || 0).toLocaleString()}
|
|
1334
|
+
</td>
|
|
1335
|
+
</tr>
|
|
1336
|
+
))}
|
|
1337
|
+
</tbody>
|
|
1338
|
+
</table>
|
|
1339
|
+
</div>
|
|
1340
|
+
</CardContent>
|
|
1341
|
+
</Card>
|
|
1342
|
+
</section>
|
|
1343
|
+
</main>
|
|
1344
|
+
)
|
|
1345
|
+
}
|
|
1346
|
+
`};return{provider:"browser-sandbox",summary:"Applied a deterministic Browser Sandbox smoke revision.",changes:[{kind:"edit",label:"Replaced the virtual browser workspace with a known-good smoke app"}],files:u.map(t=>t.path===e.path?e:t),replaceExistingFiles:!0,generationMeta:{model:"deterministic-smoke",strategy:"browser-sandbox-smoke",durationMs:0,outputChars:0}}}function ge(){const e=Date.now(),t=v({command:"browser sandbox build",ok:!1,stderr:"Static export is not implemented yet. Next step: compile the virtual filesystem with the Browser Sandbox compiler worker, then upload dist/** to S3.",startedAt:e});return{ok:!1,projectRoot:x,provider:"browser-sandbox",validation:"build",changedFiles:[],attempts:[{label:"publish",ok:!1,command:t}],command:t,error:t.stderr}}async function he(){const e=f(),t=[];try{const r=await Promise.resolve().then(()=>require("./typescript-Cmizj1hi.js")).then(i=>i.typescript),a={},n=e.filter(i=>/\.(ts|tsx|js|jsx)$/.test(i.path));for(const i of n){const l=m(i.path),s=r.transpileModule(i.contents,{compilerOptions:{esModuleInterop:!0,jsx:r.JsxEmit.ReactJSX,module:r.ModuleKind.CommonJS,target:r.ScriptTarget.ES2020},fileName:l,reportDiagnostics:!0});for(const d of s.diagnostics||[])t.push(be(r,d));a[l]=s.outputText}if(a[R]||t.push(`${R}: browser sandbox entry file is missing.`),t.length>0)return{html:E(t),diagnostics:t};const o=await xe(e);return{html:je({css:o.css,diagnostics:t,entryPath:R,moduleSources:a}),diagnostics:t,warnings:o.warnings,cssMode:o.mode}}catch(r){const a=r instanceof Error?r.message:String(r);return{html:E([`Browser Sandbox compiler failed before rendering: ${a}`]),diagnostics:[a]}}}function be(e,t){const r=t.file&&typeof t.start=="number"?(()=>{const n=t.file.getLineAndCharacterOfPosition(t.start);return`${t.file.fileName}:${n.line+1}:${n.character+1}`})():"browser-sandbox",a=e.flattenDiagnosticMessageText(t.messageText,`
|
|
1347
|
+
`);return`${r}: ${a}`}async function xe(e){const t=e.filter(r=>r.path.endsWith(".css")).map(r=>r.contents).join(`
|
|
1348
|
+
`);try{const{compile:r}=await Promise.resolve().then(()=>require("./lib-Ce3zosXY.js")),a=we(t),n=await r(a,{from:void 0,loadStylesheet:ve});return{css:`${z}
|
|
1349
|
+
${n.build([...K(e)])}`,mode:"tailwind",warnings:[]}}catch(r){const a=r instanceof Error?r.message:String(r),n=t.replace(/@import\s+["']tailwindcss["'];?/g,"");return{css:[z,`/* Browser Sandbox Tailwind compiler failed; using fallback utility CSS. ${a} */`,Re(e),n].join(`
|
|
1350
|
+
`),mode:"fallback",warnings:[`Tailwind compiler was unavailable, so Browser Sandbox used fallback utility CSS. ${a}`]}}}function we(e){return/@import\s+["']tailwindcss(?:\/[^"']*)?["'];?/.test(e)||/@tailwind\s+/.test(e)?e:`@import "tailwindcss";
|
|
1351
|
+
${e}`}const j=new Map;async function ve(e,t){const r=e.replace(/^\.\//,"tailwindcss/"),a=r==="tailwindcss"?"/browser-sandbox/tailwind/index.css":r==="tailwindcss/theme"?"/browser-sandbox/tailwind/theme.css":r==="tailwindcss/preflight"?"/browser-sandbox/tailwind/preflight.css":r==="tailwindcss/utilities"?"/browser-sandbox/tailwind/utilities.css":null;if(!a)throw new Error(`Unsupported Tailwind stylesheet import: ${e}`);let n=j.get(a);if(!n){const o=await fetch(a);if(!o.ok)throw new Error(`Unable to load Tailwind stylesheet ${a}: ${o.status}`);n=await o.text(),j.set(a,n)}return{base:t||new URL("/browser-sandbox/tailwind/",window.location.origin).href,content:n,path:a}}const Ce={sm:"640px",md:"768px",lg:"1024px",xl:"1280px","2xl":"1536px"},ye={hover:":hover",focus:":focus",active:":active",disabled:":disabled"},G={0:"0",px:"1px","0.5":"0.125rem",1:"0.25rem","1.5":"0.375rem",2:"0.5rem","2.5":"0.625rem",3:"0.75rem","3.5":"0.875rem",4:"1rem",5:"1.25rem",6:"1.5rem",7:"1.75rem",8:"2rem",9:"2.25rem",10:"2.5rem",11:"2.75rem",12:"3rem",14:"3.5rem",16:"4rem",20:"5rem",24:"6rem",28:"7rem",32:"8rem",36:"9rem",40:"10rem",44:"11rem",48:"12rem",56:"14rem",64:"16rem",72:"18rem",80:"20rem",96:"24rem"},Se={"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%"},A={none:"none",xs:"20rem",sm:"24rem",md:"28rem",lg:"32rem",xl:"36rem","2xl":"42rem","3xl":"48rem","4xl":"56rem","5xl":"64rem","6xl":"72rem","7xl":"80rem",full:"100%",screen:"100vw"},L={xs:"font-size: 0.75rem; line-height: 1rem;",sm:"font-size: 0.875rem; line-height: 1.25rem;",base:"font-size: 1rem; line-height: 1.5rem;",lg:"font-size: 1.125rem; line-height: 1.75rem;",xl:"font-size: 1.25rem; line-height: 1.75rem;","2xl":"font-size: 1.5rem; line-height: 2rem;","3xl":"font-size: 1.875rem; line-height: 2.25rem;","4xl":"font-size: 2.25rem; line-height: 2.5rem;","5xl":"font-size: 3rem; line-height: 1;","6xl":"font-size: 3.75rem; line-height: 1;"},Ne={none:"0",sm:"0.125rem",DEFAULT:"0.25rem",md:"0.375rem",lg:"0.5rem",xl:"0.75rem","2xl":"1rem","3xl":"1.5rem",full:"9999px"},ke={sm:"0 1px 2px rgba(24, 24, 27, 0.06)",DEFAULT:"0 1px 3px rgba(24, 24, 27, 0.1), 0 1px 2px rgba(24, 24, 27, 0.06)",md:"0 4px 6px -1px rgba(24, 24, 27, 0.1), 0 2px 4px -2px rgba(24, 24, 27, 0.1)",lg:"0 10px 15px -3px rgba(24, 24, 27, 0.1), 0 4px 6px -4px rgba(24, 24, 27, 0.1)",none:"none"},Te={slate:{50:"#f8fafc",100:"#f1f5f9",200:"#e2e8f0",300:"#cbd5e1",400:"#94a3b8",500:"#64748b",600:"#475569",700:"#334155",800:"#1e293b",900:"#0f172a",950:"#020617"},zinc:{50:"#fafafa",100:"#f4f4f5",200:"#e4e4e7",300:"#d4d4d8",400:"#a1a1aa",500:"#71717a",600:"#52525b",700:"#3f3f46",800:"#27272a",900:"#18181b",950:"#09090b"},neutral:{50:"#fafafa",100:"#f5f5f5",200:"#e5e5e5",300:"#d4d4d4",400:"#a3a3a3",500:"#737373",600:"#525252",700:"#404040",800:"#262626",900:"#171717",950:"#0a0a0a"},red:{50:"#fef2f2",100:"#fee2e2",200:"#fecaca",500:"#ef4444",600:"#dc2626",700:"#b91c1c",900:"#7f1d1d"},amber:{50:"#fffbeb",100:"#fef3c7",200:"#fde68a",500:"#f59e0b",600:"#d97706",700:"#b45309",900:"#78350f"},yellow:{50:"#fefce8",100:"#fef9c3",200:"#fef08a",500:"#eab308",600:"#ca8a04",700:"#a16207",900:"#713f12"},green:{50:"#f0fdf4",100:"#dcfce7",200:"#bbf7d0",500:"#22c55e",600:"#16a34a",700:"#15803d",900:"#14532d"},emerald:{50:"#ecfdf5",100:"#d1fae5",200:"#a7f3d0",500:"#10b981",600:"#059669",700:"#047857",900:"#064e3b"},blue:{50:"#eff6ff",100:"#dbeafe",200:"#bfdbfe",500:"#3b82f6",600:"#2563eb",700:"#1d4ed8",900:"#1e3a8a"},indigo:{50:"#eef2ff",100:"#e0e7ff",200:"#c7d2fe",500:"#6366f1",600:"#4f46e5",700:"#4338ca",900:"#312e81"},purple:{50:"#faf5ff",100:"#f3e8ff",200:"#e9d5ff",500:"#a855f7",600:"#9333ea",700:"#7e22ce",900:"#581c87"},rose:{50:"#fff1f2",100:"#ffe4e6",200:"#fecdd3",500:"#f43f5e",600:"#e11d48",700:"#be123c",900:"#881337"}},V={background:"#ffffff",foreground:"#09090b",card:"#ffffff","card-foreground":"#09090b",muted:"#f4f4f5","muted-foreground":"#71717a",primary:"#18181b","primary-foreground":"#ffffff",secondary:"#f4f4f5","secondary-foreground":"#18181b",accent:"#f4f4f5","accent-foreground":"#18181b",destructive:"#dc2626","destructive-foreground":"#ffffff",border:"#e4e4e7",input:"#e4e4e7",ring:"#18181b",white:"#ffffff",black:"#000000",transparent:"transparent"},J={block:"display: block;","inline-block":"display: inline-block;",inline:"display: inline;",flex:"display: flex;","inline-flex":"display: inline-flex;",grid:"display: grid;",hidden:"display: none;",table:"display: table;","table-row":"display: table-row;","table-cell":"display: table-cell;",relative:"position: relative;",absolute:"position: absolute;",fixed:"position: fixed;",sticky:"position: sticky;","flex-row":"flex-direction: row;","flex-col":"flex-direction: column;","flex-wrap":"flex-wrap: wrap;","flex-nowrap":"flex-wrap: nowrap;","flex-1":"flex: 1 1 0%;","flex-auto":"flex: 1 1 auto;","flex-none":"flex: none;",grow:"flex-grow: 1;","grow-0":"flex-grow: 0;",shrink:"flex-shrink: 1;","shrink-0":"flex-shrink: 0;","items-start":"align-items: flex-start;","items-center":"align-items: center;","items-end":"align-items: flex-end;","items-stretch":"align-items: stretch;","justify-start":"justify-content: flex-start;","justify-center":"justify-content: center;","justify-end":"justify-content: flex-end;","justify-between":"justify-content: space-between;","justify-around":"justify-content: space-around;","content-start":"align-content: flex-start;","content-center":"align-content: center;","self-start":"align-self: flex-start;","self-center":"align-self: center;","self-stretch":"align-self: stretch;","overflow-hidden":"overflow: hidden;","overflow-auto":"overflow: auto;","overflow-x-auto":"overflow-x: auto;","overflow-y-auto":"overflow-y: auto;","overflow-x-hidden":"overflow-x: hidden;","overflow-y-hidden":"overflow-y: hidden;",truncate:"overflow: hidden; text-overflow: ellipsis; white-space: nowrap;","whitespace-nowrap":"white-space: nowrap;","whitespace-normal":"white-space: normal;","text-left":"text-align: left;","text-center":"text-align: center;","text-right":"text-align: right;","align-middle":"vertical-align: middle;","font-normal":"font-weight: 400;","font-medium":"font-weight: 500;","font-semibold":"font-weight: 600;","font-bold":"font-weight: 700;",uppercase:"text-transform: uppercase;",lowercase:"text-transform: lowercase;",capitalize:"text-transform: capitalize;","tracking-tight":"letter-spacing: 0;","tracking-normal":"letter-spacing: 0;","tracking-wide":"letter-spacing: 0.025em;","tracking-wider":"letter-spacing: 0.05em;","leading-none":"line-height: 1;","leading-tight":"line-height: 1.25;","leading-snug":"line-height: 1.375;","leading-normal":"line-height: 1.5;","leading-relaxed":"line-height: 1.625;","min-h-screen":"min-height: 100vh;","h-screen":"height: 100vh;","w-screen":"width: 100vw;","w-full":"width: 100%;","h-full":"height: 100%;","min-w-0":"min-width: 0;","min-h-0":"min-height: 0;","min-w-full":"min-width: 100%;","border-collapse":"border-collapse: collapse;","border-separate":"border-collapse: separate;","object-cover":"object-fit: cover;","object-contain":"object-fit: contain;","aspect-square":"aspect-ratio: 1 / 1;","aspect-video":"aspect-ratio: 16 / 9;","cursor-pointer":"cursor: pointer;","cursor-default":"cursor: default;","select-none":"user-select: none;","pointer-events-none":"pointer-events: none;"};function Re(e){const t=new Set;for(const r of K(e)){const a=ze(r);a&&t.add(a)}return[...t].join(`
|
|
1352
|
+
`)}function K(e){const t=new Set,r=a=>{a.split(/\s+/).map(n=>n.trim().replace(/[;,]+$/g,"")).filter(Boolean).filter(n=>!n.includes("\\")&&!n.includes('"')).filter(_e).forEach(n=>t.add(n))};for(const a of e){if(!/\.(ts|tsx|js|jsx)$/.test(a.path))continue;const n=/\bclassName\s*=\s*(["'`])([^"'`]+)\1/g;let o;for(;o=n.exec(a.contents);)r(o[2]);const i=/(["'`])([^"'`]*(?:-|:|\[)[^"'`]*)\1/g;let l;for(;l=i.exec(a.contents);)r(l[2])}return t}function _e(e){if(!e||e.includes("${")||e.startsWith("@"))return!1;const t=X(e).base;return t?e.includes("[")||e.includes(":")||t in J||/^(accent|basis|bg|border|caret|col-span|content|decoration|delay|divide-y|divide-x|divide|duration|ease|fill|font|from|gap|gap-x|gap-y|grid-cols|grid-rows|h|inset|items|justify|leading|m|mb|ml|mr|mt|mx|my|max-h|max-w|min-h|min-w|object|opacity|order|outline|overflow|overscroll|p|pb|pl|pr|pt|px|py|ring|rotate|rounded|row-span|scale|scroll|self|shadow|shrink|size|skew|space-y|space-x|stroke|text|to|top|right|bottom|left|tracking|transition|translate|via|w|whitespace|z)-/.test(t)||/^(tabular-nums|sr-only|not-sr-only|truncate|container|grow|isolate|invisible|visible)$/.test(t)||/^-?(mt|mr|mb|ml|mx|my|m|top|right|bottom|left)-/.test(t):!1}function X(e){const t=e.split(":");return{variants:t.slice(0,-1),base:t[t.length-1]}}function ze(e){const{variants:t,base:r}=X(e),a=Me(r);if(!a)return null;const n=t.map(l=>ye[l]).filter(Boolean).join(""),o=`.${Pe(e)}${n}`;let i=a.includes("&")?a.replace(/&/g,o):`${o} { ${a} }`;for(const l of t.slice().reverse()){const s=Ce[l];s&&(i=`@media (min-width: ${s}) { ${i} }`)}return i}function Me(e){const t=J[e];if(t)return t;const r=e.startsWith("-"),a=r?e.slice(1):e,n=a.match(/^(p|px|py|pt|pr|pb|pl|m|mx|my|mt|mr|mb|ml|gap|gap-x|gap-y|space-y|space-x)-(.+)$/);if(n){const c=_(n[2]);return c?De(n[1],r?`-${c}`:c):null}const o=a.match(/^(size|w|h|min-w|max-w|min-h|max-h)-(.+)$/);if(o){const c=$e(o[2],o[1]);if(!c)return null;const P={size:"size",w:"width",h:"height","min-w":"min-width","max-w":"max-width","min-h":"min-height","max-h":"max-height"}[o[1]];return P==="size"?`width: ${c}; height: ${c};`:`${P}: ${c};`}const i=a.match(/^grid-cols-(.+)$/);if(i){const c=I(i[1]);return c?`grid-template-columns: ${c};`:null}const l=a.match(/^grid-rows-(.+)$/);if(l){const c=I(l[1]);return c?`grid-template-rows: ${c};`:null}const s=a.match(/^col-span-(\d+)$/);if(s)return`grid-column: span ${s[1]} / span ${s[1]};`;const d=a.match(/^row-span-(\d+)$/);if(d)return`grid-row: span ${d[1]} / span ${d[1]};`;const b=Be(a);if(b)return b;const S=a.match(/^rounded(?:-(.+))?$/);if(S){const c=Ne[S[1]||"DEFAULT"]||h(S[1]||"");return c?`border-radius: ${c};`:null}const B=a.match(/^shadow(?:-(.+))?$/);if(B){const c=ke[B[1]||"DEFAULT"];return c?`box-shadow: ${c};`:null}const H=He(a);if(H)return H;const N=a.match(/^text-(.+)$/);if(N&&L[N[1]])return L[N[1]];const k=a.match(/^leading-(.+)$/);if(k){const c=G[k[1]]||h(k[1]);return c?`line-height: ${c};`:null}const $=a.match(/^opacity-(\d+)$/);if($)return`opacity: ${Number($[1])/100};`;const w=a.match(/^z-(.+)$/);if(w)return`z-index: ${w[1]==="auto"?"auto":h(w[1])||w[1]};`;const T=a.match(/^(inset|top|right|bottom|left)-(.+)$/);if(T){const c=_(T[2]);return c?`${T[1]}: ${r?`-${c}`:c};`:null}return null}function De(e,t){switch(e){case"p":return`padding: ${t};`;case"px":return`padding-left: ${t}; padding-right: ${t};`;case"py":return`padding-top: ${t}; padding-bottom: ${t};`;case"pt":return`padding-top: ${t};`;case"pr":return`padding-right: ${t};`;case"pb":return`padding-bottom: ${t};`;case"pl":return`padding-left: ${t};`;case"m":return`margin: ${t};`;case"mx":return`margin-left: ${t}; margin-right: ${t};`;case"my":return`margin-top: ${t}; margin-bottom: ${t};`;case"mt":return`margin-top: ${t};`;case"mr":return`margin-right: ${t};`;case"mb":return`margin-bottom: ${t};`;case"ml":return`margin-left: ${t};`;case"gap":return`gap: ${t};`;case"gap-x":return`column-gap: ${t};`;case"gap-y":return`row-gap: ${t};`;case"space-y":return`& > * + * { margin-top: ${t}; }`;case"space-x":return`& > * + * { margin-left: ${t}; }`;default:return""}}function Be(e){if(e==="border")return"border-width: 1px; border-style: solid; border-color: #e4e4e7;";if(e==="border-0")return"border-width: 0;";if(e==="border-t")return"border-top-width: 1px; border-top-style: solid; border-top-color: #e4e4e7;";if(e==="border-r")return"border-right-width: 1px; border-right-style: solid; border-right-color: #e4e4e7;";if(e==="border-b")return"border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: #e4e4e7;";if(e==="border-l")return"border-left-width: 1px; border-left-style: solid; border-left-color: #e4e4e7;";const t=e.match(/^border-(\d+)$/);if(t)return`border-width: ${t[1]}px; border-style: solid;`;if(e==="divide-y")return"& > * + * { border-top-width: 1px; border-top-style: solid; border-color: #e4e4e7; }";if(e==="divide-x")return"& > * + * { border-left-width: 1px; border-left-style: solid; border-color: #e4e4e7; }";const r=g(e,"divide-");if(r)return`& > * + * { border-color: ${r}; }`;const a=g(e,"border-");return a?`border-color: ${a};`:null}function He(e){const t=g(e,"bg-");if(t)return`background-color: ${t};`;const r=g(e,"text-");if(r)return`color: ${r};`;const a=g(e,"fill-");if(a)return`fill: ${a};`;const n=g(e,"stroke-");return n?`stroke: ${n};`:null}function g(e,t){var r;if(!e.startsWith(t))return null;const a=e.slice(t.length),n=h(a);if(n)return n;if(V[a])return V[a];const o=a.split("-"),i=o.pop(),l=o.join("-");return!i||!l?null:((r=Te[l])===null||r===void 0?void 0:r[i])||null}function $e(e,t){return t==="max-w"&&A[e]?A[e]:e==="auto"?"auto":e==="full"?"100%":e==="screen"?t.includes("h")?"100vh":"100vw":e==="min"?"min-content":e==="max"?"max-content":e==="fit"?"fit-content":_(e)}function _(e){return G[e]||Se[e]||h(e)}function I(e){const t=h(e);if(t)return t;const r=Number(e);return Number.isInteger(r)&&r>0?`repeat(${r}, minmax(0, 1fr))`:e==="none"?"none":null}function h(e){if(!(e!=null&&e.startsWith("["))||!e.endsWith("]"))return null;const t=e.slice(1,-1).replace(/_/g," ");return/[;{}<>]/.test(t)?null:t}function Pe(e){return e.replace(/([^a-zA-Z0-9_-])/g,"\\$1")}const z=`
|
|
1353
|
+
* { box-sizing: border-box; }
|
|
1354
|
+
html, body, #root { margin: 0; min-width: 320px; min-height: 100%; }
|
|
1355
|
+
body { font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f6f7f8; color: #18181b; }
|
|
1356
|
+
button, input, select, textarea { font: inherit; }
|
|
1357
|
+
button, select { cursor: pointer; }
|
|
1358
|
+
table { border-collapse: collapse; width: 100%; }
|
|
1359
|
+
th, td { text-align: inherit; vertical-align: middle; }
|
|
1360
|
+
svg { display: block; flex-shrink: 0; }
|
|
1361
|
+
.min-h-screen { min-height: 100vh; }
|
|
1362
|
+
.bg-zinc-50 { background: #fafafa; }
|
|
1363
|
+
.bg-white { background: #ffffff; }
|
|
1364
|
+
.bg-zinc-900 { background: #18181b; }
|
|
1365
|
+
.bg-blue-50 { background: #eff6ff; }
|
|
1366
|
+
.bg-emerald-50 { background: #ecfdf5; }
|
|
1367
|
+
.bg-red-50 { background: #fef2f2; }
|
|
1368
|
+
.text-zinc-950 { color: #09090b; }
|
|
1369
|
+
.text-zinc-900 { color: #18181b; }
|
|
1370
|
+
.text-zinc-800 { color: #27272a; }
|
|
1371
|
+
.text-zinc-700 { color: #3f3f46; }
|
|
1372
|
+
.text-zinc-600 { color: #52525b; }
|
|
1373
|
+
.text-zinc-500 { color: #71717a; }
|
|
1374
|
+
.text-white { color: #ffffff; }
|
|
1375
|
+
.text-blue-700 { color: #1d4ed8; }
|
|
1376
|
+
.text-emerald-700 { color: #047857; }
|
|
1377
|
+
.text-red-700 { color: #b91c1c; }
|
|
1378
|
+
.mx-auto { margin-left: auto; margin-right: auto; }
|
|
1379
|
+
.mt-1 { margin-top: 0.25rem; }
|
|
1380
|
+
.mt-2 { margin-top: 0.5rem; }
|
|
1381
|
+
.mt-3 { margin-top: 0.75rem; }
|
|
1382
|
+
.mt-4 { margin-top: 1rem; }
|
|
1383
|
+
.mb-2 { margin-bottom: 0.5rem; }
|
|
1384
|
+
.mb-3 { margin-bottom: 0.75rem; }
|
|
1385
|
+
.mb-4 { margin-bottom: 1rem; }
|
|
1386
|
+
.ml-auto { margin-left: auto; }
|
|
1387
|
+
.flex { display: flex; }
|
|
1388
|
+
.inline-flex { display: inline-flex; }
|
|
1389
|
+
.grid { display: grid; }
|
|
1390
|
+
.hidden { display: none; }
|
|
1391
|
+
.block { display: block; }
|
|
1392
|
+
.items-center { align-items: center; }
|
|
1393
|
+
.items-start { align-items: flex-start; }
|
|
1394
|
+
.justify-between { justify-content: space-between; }
|
|
1395
|
+
.justify-center { justify-content: center; }
|
|
1396
|
+
.gap-1 { gap: 0.25rem; }
|
|
1397
|
+
.gap-2 { gap: 0.5rem; }
|
|
1398
|
+
.gap-3 { gap: 0.75rem; }
|
|
1399
|
+
.gap-4 { gap: 1rem; }
|
|
1400
|
+
.space-y-1 > * + * { margin-top: 0.25rem; }
|
|
1401
|
+
.space-y-2 > * + * { margin-top: 0.5rem; }
|
|
1402
|
+
.space-y-3 > * + * { margin-top: 0.75rem; }
|
|
1403
|
+
.max-w-3xl { max-width: 48rem; }
|
|
1404
|
+
.max-w-4xl { max-width: 56rem; }
|
|
1405
|
+
.max-w-5xl { max-width: 64rem; }
|
|
1406
|
+
.w-full { width: 100%; }
|
|
1407
|
+
.h-full { height: 100%; }
|
|
1408
|
+
.size-4 { width: 1rem; height: 1rem; }
|
|
1409
|
+
.size-5 { width: 1.25rem; height: 1.25rem; }
|
|
1410
|
+
.p-2 { padding: 0.5rem; }
|
|
1411
|
+
.p-3 { padding: 0.75rem; }
|
|
1412
|
+
.p-4 { padding: 1rem; }
|
|
1413
|
+
.p-6 { padding: 1.5rem; }
|
|
1414
|
+
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
|
|
1415
|
+
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
|
1416
|
+
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
|
1417
|
+
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
|
|
1418
|
+
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
|
1419
|
+
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
|
1420
|
+
.pt-2 { padding-top: 0.5rem; }
|
|
1421
|
+
.pb-2 { padding-bottom: 0.5rem; }
|
|
1422
|
+
.rounded { border-radius: 0.25rem; }
|
|
1423
|
+
.rounded-md { border-radius: 0.375rem; }
|
|
1424
|
+
.rounded-lg { border-radius: 0.5rem; }
|
|
1425
|
+
.border { border-width: 1px; border-style: solid; border-color: #e4e4e7; }
|
|
1426
|
+
.border-zinc-100 { border-color: #f4f4f5; }
|
|
1427
|
+
.border-zinc-200 { border-color: #e4e4e7; }
|
|
1428
|
+
.border-blue-100 { border-color: #dbeafe; }
|
|
1429
|
+
.shadow-sm { box-shadow: 0 1px 2px rgba(24, 24, 27, 0.06); }
|
|
1430
|
+
.text-xs { font-size: 0.75rem; line-height: 1rem; }
|
|
1431
|
+
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
|
1432
|
+
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
|
1433
|
+
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
|
|
1434
|
+
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
|
1435
|
+
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
|
|
1436
|
+
.font-medium { font-weight: 500; }
|
|
1437
|
+
.font-semibold { font-weight: 600; }
|
|
1438
|
+
.font-bold { font-weight: 700; }
|
|
1439
|
+
.uppercase { text-transform: uppercase; }
|
|
1440
|
+
.tracking-tight { letter-spacing: 0; }
|
|
1441
|
+
.leading-5 { line-height: 1.25rem; }
|
|
1442
|
+
.leading-6 { line-height: 1.5rem; }
|
|
1443
|
+
.overflow-hidden { overflow: hidden; }
|
|
1444
|
+
.overflow-auto { overflow: auto; }
|
|
1445
|
+
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1446
|
+
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1447
|
+
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
1448
|
+
@media (min-width: 640px) {
|
|
1449
|
+
.sm\\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1450
|
+
.sm\\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
1451
|
+
}
|
|
1452
|
+
`;function je({css:e,diagnostics:t,entryPath:r,moduleSources:a}){return`<!doctype html>
|
|
1453
|
+
<html>
|
|
1454
|
+
<head>
|
|
1455
|
+
<meta charset="utf-8" />
|
|
1456
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1457
|
+
<style>${Y(e)}</style>
|
|
1458
|
+
</head>
|
|
1459
|
+
<body>
|
|
1460
|
+
<div id="root"></div>
|
|
1461
|
+
<script>
|
|
1462
|
+
window.__SEMAPHOR_SANDBOX__ = {
|
|
1463
|
+
entryPath: ${JSON.stringify(r)},
|
|
1464
|
+
moduleSources: ${F(a)},
|
|
1465
|
+
diagnostics: ${F(t)}
|
|
1466
|
+
};
|
|
1467
|
+
<\/script>
|
|
1468
|
+
<script>${Le()}<\/script>
|
|
1469
|
+
</body>
|
|
1470
|
+
</html>`}function E(e){return`<!doctype html>
|
|
1471
|
+
<html>
|
|
1472
|
+
<head>
|
|
1473
|
+
<meta charset="utf-8" />
|
|
1474
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1475
|
+
<style>${Y(z)}</style>
|
|
1476
|
+
</head>
|
|
1477
|
+
<body>
|
|
1478
|
+
<main class="min-h-screen bg-zinc-50 p-6 text-zinc-950">
|
|
1479
|
+
<section class="mx-auto max-w-4xl rounded-lg border border-zinc-200 bg-white p-4 shadow-sm">
|
|
1480
|
+
<h1 class="text-lg font-semibold">Browser Sandbox compile failed</h1>
|
|
1481
|
+
<pre class="mt-3 overflow-auto rounded-md bg-zinc-900 p-3 text-xs text-white">${Ae(e.join(`
|
|
1482
|
+
`))}</pre>
|
|
1483
|
+
</section>
|
|
1484
|
+
</main>
|
|
1485
|
+
</body>
|
|
1486
|
+
</html>`}function F(e){return JSON.stringify(e).replace(/</g,"\\u003c")}function Y(e){return e.replace(/<\/style/gi,"<\\/style")}function Ae(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function Le(){return`
|
|
1487
|
+
(() => {
|
|
1488
|
+
const sandbox = window.__SEMAPHOR_SANDBOX__;
|
|
1489
|
+
const moduleSources = sandbox.moduleSources || {};
|
|
1490
|
+
const moduleCache = {};
|
|
1491
|
+
const datasets = createDatasets();
|
|
1492
|
+
const vendor = window.parent?.__SEMAPHOR_BROWSER_SANDBOX_VENDOR__;
|
|
1493
|
+
if (!vendor?.React || !vendor?.ReactDOMClient) {
|
|
1494
|
+
throw new Error("Browser Sandbox vendor runtime is not available from the app-builder host.");
|
|
1495
|
+
}
|
|
1496
|
+
const React = vendor.React;
|
|
1497
|
+
const ReactDOMClient = vendor.ReactDOMClient;
|
|
1498
|
+
const recharts = vendor.Recharts || createRechartsFallback(React);
|
|
1499
|
+
const jsxRuntime = {
|
|
1500
|
+
Fragment: React.Fragment,
|
|
1501
|
+
jsx(type, props, key) {
|
|
1502
|
+
return React.createElement(type, key === undefined ? props : { ...(props || {}), key });
|
|
1503
|
+
},
|
|
1504
|
+
jsxs(type, props, key) {
|
|
1505
|
+
return React.createElement(type, key === undefined ? props : { ...(props || {}), key });
|
|
1506
|
+
},
|
|
1507
|
+
};
|
|
1508
|
+
const reactPackage = { ...React, default: React, __esModule: true };
|
|
1509
|
+
const reactDomClientPackage = { ...ReactDOMClient, default: ReactDOMClient, __esModule: true };
|
|
1510
|
+
|
|
1511
|
+
function createRechartsFallback(React) {
|
|
1512
|
+
function ChartShell({ children, className = "", ...props }) {
|
|
1513
|
+
return React.createElement(
|
|
1514
|
+
"div",
|
|
1515
|
+
{
|
|
1516
|
+
...props,
|
|
1517
|
+
className: ["flex min-h-[180px] items-center justify-center rounded-md border border-dashed border-zinc-200 bg-zinc-50 text-xs text-zinc-500", className]
|
|
1518
|
+
.filter(Boolean)
|
|
1519
|
+
.join(" "),
|
|
1520
|
+
},
|
|
1521
|
+
React.createElement("div", { className: "space-y-2 text-center" }, [
|
|
1522
|
+
React.createElement("div", { key: "label", className: "font-medium text-zinc-700" }, "Chart preview"),
|
|
1523
|
+
React.createElement("div", { key: "meta" }, "Recharts is not bundled in this host; generated chart code is still import-valid."),
|
|
1524
|
+
React.createElement("div", { key: "children", className: "hidden" }, children),
|
|
1525
|
+
]),
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function NullPart({ children }) {
|
|
1530
|
+
return React.createElement(React.Fragment, null, children || null);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
return {
|
|
1534
|
+
Area: NullPart,
|
|
1535
|
+
AreaChart: ChartShell,
|
|
1536
|
+
Bar: NullPart,
|
|
1537
|
+
BarChart: ChartShell,
|
|
1538
|
+
CartesianGrid: NullPart,
|
|
1539
|
+
Cell: NullPart,
|
|
1540
|
+
Legend: NullPart,
|
|
1541
|
+
Line: NullPart,
|
|
1542
|
+
LineChart: ChartShell,
|
|
1543
|
+
Pie: NullPart,
|
|
1544
|
+
PieChart: ChartShell,
|
|
1545
|
+
ResponsiveContainer: ({ children, className = "", ...props }) =>
|
|
1546
|
+
React.createElement("div", { ...props, className: ["h-full w-full", className].filter(Boolean).join(" ") }, children),
|
|
1547
|
+
Tooltip: NullPart,
|
|
1548
|
+
XAxis: NullPart,
|
|
1549
|
+
YAxis: NullPart,
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function createIcon(name) {
|
|
1554
|
+
return function Icon(props = {}) {
|
|
1555
|
+
return React.createElement(
|
|
1556
|
+
"span",
|
|
1557
|
+
{
|
|
1558
|
+
...props,
|
|
1559
|
+
className: ["inline-flex size-5 items-center justify-center", props.className]
|
|
1560
|
+
.filter(Boolean)
|
|
1561
|
+
.join(" "),
|
|
1562
|
+
title: props.title || name,
|
|
1563
|
+
},
|
|
1564
|
+
"◇",
|
|
1565
|
+
);
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const lucideReact = new Proxy({}, { get: (_target, key) => createIcon(String(key)) });
|
|
1570
|
+
|
|
1571
|
+
function useSemaphorRuntime() {
|
|
1572
|
+
return { token: "browser-sandbox", apiBaseUrl: "/api" };
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function SemaphorDataAppProvider({ children }) {
|
|
1576
|
+
return children;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
function useSemaphorInput(config) {
|
|
1580
|
+
const [value, setValue] = React.useState(config.value ?? config.defaultValue);
|
|
1581
|
+
const options = (config.options || []).map((option) =>
|
|
1582
|
+
typeof option === "object" ? option : { value: option, label: String(option) },
|
|
1583
|
+
);
|
|
1584
|
+
const input = {
|
|
1585
|
+
id: config.id,
|
|
1586
|
+
kind: config.kind,
|
|
1587
|
+
value,
|
|
1588
|
+
setValue(nextValue) {
|
|
1589
|
+
config.onValueChange?.(nextValue);
|
|
1590
|
+
setValue(nextValue);
|
|
1591
|
+
},
|
|
1592
|
+
options,
|
|
1593
|
+
isActive: hasInputValue(value),
|
|
1594
|
+
};
|
|
1595
|
+
if (config.kind === "filter") {
|
|
1596
|
+
input.filter = {
|
|
1597
|
+
field: config.field,
|
|
1598
|
+
operator: config.operator || "=",
|
|
1599
|
+
value,
|
|
1600
|
+
};
|
|
1601
|
+
} else {
|
|
1602
|
+
input.control = {
|
|
1603
|
+
role: config.role,
|
|
1604
|
+
value,
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
return input;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function useSemaphorInputOptions(config) {
|
|
1611
|
+
const rows = readDataset(config.dataset);
|
|
1612
|
+
const values = [...new Set(rows.map((row) => row[config.field]).filter((value) => typeof value === "string" || typeof value === "number"))];
|
|
1613
|
+
return {
|
|
1614
|
+
id: config.id,
|
|
1615
|
+
options: values.slice(0, config.limit || 50).map((value) => ({ value, label: String(value) })),
|
|
1616
|
+
isLoading: false,
|
|
1617
|
+
error: null,
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function useSemaphorMetric(config) {
|
|
1622
|
+
useSemaphorRuntime();
|
|
1623
|
+
const rows = applyInputs(readDataset(config.dataset), config.inputs);
|
|
1624
|
+
const value = sumField(rows, config.metric);
|
|
1625
|
+
const comparisonValue =
|
|
1626
|
+
config.comparison === "target"
|
|
1627
|
+
? config.targetValue ?? null
|
|
1628
|
+
: config.comparison && config.comparison !== "none"
|
|
1629
|
+
? Math.round(value * 0.91)
|
|
1630
|
+
: null;
|
|
1631
|
+
const delta = comparisonValue === null ? null : value - comparisonValue;
|
|
1632
|
+
const deltaPercent = comparisonValue && comparisonValue !== 0 ? delta / comparisonValue : null;
|
|
1633
|
+
return {
|
|
1634
|
+
id: config.id,
|
|
1635
|
+
value,
|
|
1636
|
+
comparisonValue,
|
|
1637
|
+
delta,
|
|
1638
|
+
deltaPercent,
|
|
1639
|
+
trendline: buildTrendline(rows, config.dateField, config.metric),
|
|
1640
|
+
isLoading: false,
|
|
1641
|
+
error: null,
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function useSemaphorRecords(config) {
|
|
1646
|
+
useSemaphorRuntime();
|
|
1647
|
+
const rows = applyInputs(readDataset(config.dataset), config.inputs);
|
|
1648
|
+
const records = shapeRecords({
|
|
1649
|
+
rows,
|
|
1650
|
+
measures: config.measures || [],
|
|
1651
|
+
dimensions: config.dimensions || [],
|
|
1652
|
+
inputs: config.inputs || [],
|
|
1653
|
+
limit: config.limit,
|
|
1654
|
+
});
|
|
1655
|
+
return {
|
|
1656
|
+
id: config.id,
|
|
1657
|
+
records,
|
|
1658
|
+
isLoading: false,
|
|
1659
|
+
error: null,
|
|
1660
|
+
metadata: {
|
|
1661
|
+
dataset: config.dataset,
|
|
1662
|
+
rowCount: records.length,
|
|
1663
|
+
source: "fixture",
|
|
1664
|
+
},
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const semaphorSdk = {
|
|
1669
|
+
SemaphorDataAppProvider,
|
|
1670
|
+
useSemaphorRuntime,
|
|
1671
|
+
useSemaphorInput,
|
|
1672
|
+
useSemaphorInputOptions,
|
|
1673
|
+
useSemaphorMetric,
|
|
1674
|
+
useSemaphorRecords,
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
function cx(...values) {
|
|
1678
|
+
const classes = [];
|
|
1679
|
+
const visit = (value) => {
|
|
1680
|
+
if (!value) return;
|
|
1681
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
1682
|
+
classes.push(String(value));
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (Array.isArray(value)) {
|
|
1686
|
+
value.forEach(visit);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
if (typeof value === "object") {
|
|
1690
|
+
Object.entries(value).forEach(([key, enabled]) => {
|
|
1691
|
+
if (enabled) classes.push(key);
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
values.forEach(visit);
|
|
1696
|
+
return classes.join(" ");
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const packages = {
|
|
1700
|
+
react: reactPackage,
|
|
1701
|
+
"react/jsx-runtime": jsxRuntime,
|
|
1702
|
+
"react-dom/client": reactDomClientPackage,
|
|
1703
|
+
"lucide-react": lucideReact,
|
|
1704
|
+
recharts,
|
|
1705
|
+
"@semaphor/data-app-sdk": semaphorSdk,
|
|
1706
|
+
"react-semaphor/data-app-sdk": semaphorSdk,
|
|
1707
|
+
clsx: { clsx: cx, default: cx },
|
|
1708
|
+
"tailwind-merge": { twMerge: cx },
|
|
1709
|
+
"class-variance-authority": { cva: () => () => "" },
|
|
1710
|
+
};
|
|
1711
|
+
|
|
1712
|
+
function executeModule(moduleId) {
|
|
1713
|
+
if (moduleCache[moduleId]) return moduleCache[moduleId].exports;
|
|
1714
|
+
const code = moduleSources[moduleId];
|
|
1715
|
+
if (!code) throw new Error("Missing sandbox module: " + moduleId);
|
|
1716
|
+
const module = { exports: {} };
|
|
1717
|
+
moduleCache[moduleId] = module;
|
|
1718
|
+
const localRequire = (specifier) => requireFrom(moduleId, specifier);
|
|
1719
|
+
new Function("require", "exports", "module", code)(
|
|
1720
|
+
localRequire,
|
|
1721
|
+
module.exports,
|
|
1722
|
+
module,
|
|
1723
|
+
);
|
|
1724
|
+
return module.exports;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function requireFrom(importerPath, specifier) {
|
|
1728
|
+
if (specifier.endsWith(".css")) return {};
|
|
1729
|
+
if (packages[specifier]) return packages[specifier];
|
|
1730
|
+
const resolved = resolveSpecifier(importerPath, specifier);
|
|
1731
|
+
return executeModule(resolved);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function resolveSpecifier(importerPath, specifier) {
|
|
1735
|
+
const candidates = [];
|
|
1736
|
+
const addCandidates = (basePath) => {
|
|
1737
|
+
candidates.push(basePath);
|
|
1738
|
+
[".tsx", ".ts", ".jsx", ".js"].forEach((extension) => candidates.push(basePath + extension));
|
|
1739
|
+
["index.tsx", "index.ts", "index.jsx", "index.js"].forEach((indexFile) => candidates.push(basePath + "/" + indexFile));
|
|
1740
|
+
};
|
|
1741
|
+
if (specifier.startsWith("@/")) {
|
|
1742
|
+
addCandidates("src/" + specifier.slice(2));
|
|
1743
|
+
} else if (specifier.startsWith(".")) {
|
|
1744
|
+
const importerParts = importerPath.split("/");
|
|
1745
|
+
importerParts.pop();
|
|
1746
|
+
const parts = [...importerParts, ...specifier.split("/")];
|
|
1747
|
+
const resolvedParts = [];
|
|
1748
|
+
for (const part of parts) {
|
|
1749
|
+
if (!part || part === ".") continue;
|
|
1750
|
+
if (part === "..") resolvedParts.pop();
|
|
1751
|
+
else resolvedParts.push(part);
|
|
1752
|
+
}
|
|
1753
|
+
addCandidates(resolvedParts.join("/"));
|
|
1754
|
+
} else {
|
|
1755
|
+
throw new Error("Unsupported package in Browser Sandbox runtime: " + specifier);
|
|
1756
|
+
}
|
|
1757
|
+
const found = candidates.find((candidate) => moduleSources[candidate]);
|
|
1758
|
+
if (!found) throw new Error("Cannot resolve " + specifier + " from " + importerPath);
|
|
1759
|
+
return found;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function readDataset(dataset) {
|
|
1763
|
+
return datasets[dataset] || datasets.orders;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function hasInputValue(value) {
|
|
1767
|
+
if (value === undefined || value === null) return false;
|
|
1768
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
1769
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
1770
|
+
return true;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function applyInputs(rows, inputs) {
|
|
1774
|
+
return rows.filter((row) =>
|
|
1775
|
+
(inputs || []).every((input) => {
|
|
1776
|
+
if (input.kind !== "filter" || !input.filter || !hasInputValue(input.value)) return true;
|
|
1777
|
+
const candidate = row[input.filter.field];
|
|
1778
|
+
const value = input.value;
|
|
1779
|
+
switch (input.filter.operator) {
|
|
1780
|
+
case "in":
|
|
1781
|
+
return Array.isArray(value) ? value.includes(candidate) : candidate === value;
|
|
1782
|
+
case "not in":
|
|
1783
|
+
return Array.isArray(value) ? !value.includes(candidate) : candidate !== value;
|
|
1784
|
+
case "!=":
|
|
1785
|
+
return candidate !== value;
|
|
1786
|
+
case ">":
|
|
1787
|
+
return Number(candidate) > Number(value);
|
|
1788
|
+
case ">=":
|
|
1789
|
+
return Number(candidate) >= Number(value);
|
|
1790
|
+
case "<":
|
|
1791
|
+
return Number(candidate) < Number(value);
|
|
1792
|
+
case "<=":
|
|
1793
|
+
return Number(candidate) <= Number(value);
|
|
1794
|
+
case "between":
|
|
1795
|
+
return Array.isArray(value) && value.length >= 2
|
|
1796
|
+
? String(candidate) >= String(value[0]) && String(candidate) <= String(value[1])
|
|
1797
|
+
: true;
|
|
1798
|
+
case "=":
|
|
1799
|
+
default:
|
|
1800
|
+
return candidate === value;
|
|
1801
|
+
}
|
|
1802
|
+
}),
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function sumField(rows, field) {
|
|
1807
|
+
return rows.reduce((sum, row) => sum + Number(row[field] || 0), 0);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function buildTrendline(rows, dateField, metric) {
|
|
1811
|
+
if (!dateField) return [];
|
|
1812
|
+
return groupRows(rows, [dateField], [metric], []).map((row) => ({
|
|
1813
|
+
date: String(row[dateField] || ""),
|
|
1814
|
+
value: Number(row[metric] || 0),
|
|
1815
|
+
}));
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function shapeRecords(params) {
|
|
1819
|
+
const grain = params.inputs.find((input) => input.kind === "control" && input.control?.role === "grain")?.value;
|
|
1820
|
+
const dimensions = params.dimensions.map((dimension) =>
|
|
1821
|
+
grain && /date/i.test(dimension) ? dimension + ":" + grain : dimension,
|
|
1822
|
+
);
|
|
1823
|
+
const records =
|
|
1824
|
+
params.measures.length > 0 && dimensions.length > 0
|
|
1825
|
+
? groupRows(params.rows, dimensions, params.measures, params.inputs)
|
|
1826
|
+
: params.rows;
|
|
1827
|
+
return records.slice(0, params.limit || 500);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function groupRows(rows, dimensions, measures, inputs) {
|
|
1831
|
+
const groups = new Map();
|
|
1832
|
+
for (const row of rows) {
|
|
1833
|
+
const dimensionValues = dimensions.map((dimension) => readDimensionValue(row, dimension, inputs));
|
|
1834
|
+
const key = JSON.stringify(dimensionValues);
|
|
1835
|
+
const existing =
|
|
1836
|
+
groups.get(key) ||
|
|
1837
|
+
Object.fromEntries(dimensions.map((dimension, index) => [cleanDimensionName(dimension), dimensionValues[index]]));
|
|
1838
|
+
for (const measure of measures) {
|
|
1839
|
+
existing[measure] = Number(existing[measure] || 0) + Number(row[measure] || 0);
|
|
1840
|
+
}
|
|
1841
|
+
groups.set(key, existing);
|
|
1842
|
+
}
|
|
1843
|
+
return [...groups.values()];
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function readDimensionValue(row, dimension) {
|
|
1847
|
+
const [field, grain] = dimension.split(":");
|
|
1848
|
+
const value = row[field];
|
|
1849
|
+
if (!grain || typeof value !== "string") return value;
|
|
1850
|
+
if (grain === "month") return value.slice(0, 7);
|
|
1851
|
+
if (grain === "year") return value.slice(0, 4);
|
|
1852
|
+
return value;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function cleanDimensionName(dimension) {
|
|
1856
|
+
return dimension.split(":")[0];
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function createDatasets() {
|
|
1860
|
+
const orders = [
|
|
1861
|
+
{ order_id: "ord_1001", order_date: "2026-01-08", customer_segment: "Technology", region: "West", category: "Software", revenue: 128400, gross_margin: 74200, orders: 164, satisfaction_score: 91 },
|
|
1862
|
+
{ order_id: "ord_1002", order_date: "2026-01-19", customer_segment: "Healthcare", region: "Northeast", category: "Services", revenue: 84600, gross_margin: 41800, orders: 93, satisfaction_score: 87 },
|
|
1863
|
+
{ order_id: "ord_1003", order_date: "2026-02-04", customer_segment: "Technology", region: "West", category: "Hardware", revenue: 151200, gross_margin: 81600, orders: 188, satisfaction_score: 93 },
|
|
1864
|
+
{ order_id: "ord_1004", order_date: "2026-02-21", customer_segment: "Retail", region: "South", category: "Software", revenue: 97300, gross_margin: 52200, orders: 121, satisfaction_score: 82 },
|
|
1865
|
+
{ order_id: "ord_1005", order_date: "2026-03-03", customer_segment: "Technology", region: "Central", category: "Services", revenue: 173900, gross_margin: 93100, orders: 206, satisfaction_score: 94 },
|
|
1866
|
+
{ order_id: "ord_1006", order_date: "2026-03-14", customer_segment: "Healthcare", region: "West", category: "Software", revenue: 112700, gross_margin: 65800, orders: 138, satisfaction_score: 89 },
|
|
1867
|
+
{ order_id: "ord_1007", order_date: "2026-04-02", customer_segment: "Retail", region: "Northeast", category: "Hardware", revenue: 134500, gross_margin: 62300, orders: 149, satisfaction_score: 84 },
|
|
1868
|
+
{ order_id: "ord_1008", order_date: "2026-04-20", customer_segment: "Technology", region: "South", category: "Software", revenue: 196800, gross_margin: 111900, orders: 232, satisfaction_score: 95 },
|
|
1869
|
+
{ order_id: "ord_1009", order_date: "2026-05-07", customer_segment: "Healthcare", region: "Central", category: "Services", revenue: 126300, gross_margin: 69100, orders: 157, satisfaction_score: 90 },
|
|
1870
|
+
{ order_id: "ord_1010", order_date: "2026-05-24", customer_segment: "Technology", region: "West", category: "Software", revenue: 218600, gross_margin: 128300, orders: 251, satisfaction_score: 96 },
|
|
1871
|
+
];
|
|
1872
|
+
const customers = [
|
|
1873
|
+
{ customer_id: "cus_001", customer_name: "Northstar Systems", customer_segment: "Technology", region: "West", health_score: 94, arr: 420000, open_tickets: 2, last_active_days: 1 },
|
|
1874
|
+
{ customer_id: "cus_002", customer_name: "Meridian Health", customer_segment: "Healthcare", region: "Northeast", health_score: 86, arr: 280000, open_tickets: 4, last_active_days: 3 },
|
|
1875
|
+
{ customer_id: "cus_003", customer_name: "Atlas Retail Group", customer_segment: "Retail", region: "South", health_score: 72, arr: 190000, open_tickets: 9, last_active_days: 8 },
|
|
1876
|
+
{ customer_id: "cus_004", customer_name: "Helio Cloud", customer_segment: "Technology", region: "Central", health_score: 88, arr: 335000, open_tickets: 3, last_active_days: 2 },
|
|
1877
|
+
{ customer_id: "cus_005", customer_name: "Cedar Care", customer_segment: "Healthcare", region: "West", health_score: 81, arr: 245000, open_tickets: 5, last_active_days: 4 },
|
|
1878
|
+
{ customer_id: "cus_006", customer_name: "Urban Outfit Co", customer_segment: "Retail", region: "Northeast", health_score: 67, arr: 160000, open_tickets: 12, last_active_days: 11 },
|
|
1879
|
+
];
|
|
1880
|
+
return { orders, customers };
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function showRuntimeError(error) {
|
|
1884
|
+
const root = document.getElementById("root");
|
|
1885
|
+
root.innerHTML = "";
|
|
1886
|
+
const main = document.createElement("main");
|
|
1887
|
+
main.className = "min-h-screen bg-zinc-50 p-6 text-zinc-950";
|
|
1888
|
+
main.innerHTML = '<section class="mx-auto max-w-4xl rounded-lg border border-zinc-200 bg-white p-4 shadow-sm"><h1 class="text-lg font-semibold">Browser Sandbox runtime failed</h1><pre class="mt-3 overflow-auto rounded-md bg-zinc-900 p-3 text-xs text-white"></pre></section>';
|
|
1889
|
+
main.querySelector("pre").textContent = error?.stack || error?.message || String(error);
|
|
1890
|
+
root.appendChild(main);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
try {
|
|
1894
|
+
executeModule(sandbox.entryPath);
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
showRuntimeError(error);
|
|
1897
|
+
}
|
|
1898
|
+
})();
|
|
1899
|
+
`}function Ve(){const e=f(),t=e.find(a=>a.path==="src/data-app/index.tsx"),r=((t==null?void 0:t.contents)||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");return`<!doctype html>
|
|
1900
|
+
<html>
|
|
1901
|
+
<head>
|
|
1902
|
+
<meta charset="utf-8" />
|
|
1903
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1904
|
+
<style>
|
|
1905
|
+
body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f6f7f8; color: #18181b; }
|
|
1906
|
+
main { min-height: 100vh; padding: 24px; box-sizing: border-box; }
|
|
1907
|
+
.shell { max-width: 980px; margin: 0 auto; }
|
|
1908
|
+
.badge { display: inline-flex; align-items: center; border: 1px solid #bfdbfe; background: #eff6ff; color: #1d4ed8; border-radius: 6px; padding: 4px 8px; font-size: 12px; font-weight: 600; }
|
|
1909
|
+
.panel { margin-top: 16px; border: 1px solid #e4e4e7; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 2px rgba(24, 24, 27, 0.04); }
|
|
1910
|
+
.panel header { padding: 14px 16px; border-bottom: 1px solid #e4e4e7; }
|
|
1911
|
+
.panel h1 { margin: 0; font-size: 16px; }
|
|
1912
|
+
.panel p { margin: 6px 0 0; color: #71717a; font-size: 13px; line-height: 1.5; }
|
|
1913
|
+
pre { margin: 0; max-height: 520px; overflow: auto; padding: 16px; background: #09090b; color: #e4e4e7; font-size: 12px; line-height: 1.6; }
|
|
1914
|
+
.files { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1915
|
+
.file { border-radius: 5px; background: #f4f4f5; color: #52525b; padding: 3px 7px; font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
1916
|
+
</style>
|
|
1917
|
+
</head>
|
|
1918
|
+
<body>
|
|
1919
|
+
<main>
|
|
1920
|
+
<div class="shell">
|
|
1921
|
+
<span class="badge">Browser Sandbox alpha</span>
|
|
1922
|
+
<section class="panel">
|
|
1923
|
+
<header>
|
|
1924
|
+
<h1>Virtual app workspace</h1>
|
|
1925
|
+
<p>The direct browser runtime is storing generated files in localStorage and validating write/import policy. The compiler worker and hot React preview are the next layer behind this mode.</p>
|
|
1926
|
+
<div class="files">
|
|
1927
|
+
${e.map(a=>`<span class="file">${a.path}</span>`).join("")}
|
|
1928
|
+
</div>
|
|
1929
|
+
</header>
|
|
1930
|
+
<pre>${r}</pre>
|
|
1931
|
+
</section>
|
|
1932
|
+
</div>
|
|
1933
|
+
</main>
|
|
1934
|
+
</body>
|
|
1935
|
+
</html>`}exports.applyBrowserSandboxRevision=pe;exports.browserSandboxPreviewHtml=Ve;exports.compileBrowserSandboxPreviewHtml=he;exports.createBrowserSandboxImportedTemplateRevision=ie;exports.createBrowserSandboxSmokeRevision=fe;exports.createBrowserSandboxTemplateRevision=se;exports.listBrowserSandboxFiles=oe;exports.listBrowserSandboxTemplates=te;exports.readBrowserSandboxFiles=ne;exports.readBrowserSandboxWorkspaceContext=ae;exports.resetBrowserSandbox=re;exports.validateBrowserSandboxPublish=ge;
|