@arbidocs/blocks 0.3.110
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/index.cjs +3736 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1213 -0
- package/dist/index.d.ts +1213 -0
- package/dist/index.js +3634 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
- package/src/components/grid.css +365 -0
- package/src/studio/studio.css +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3634 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
import { createContext, useState, useEffect, useCallback, useMemo, useContext, useRef } from 'react';
|
|
4
|
+
import { MessageSquare, Cpu, Code, Cloud, Layers, Target, Award, MapPin, Phone, Mail, Lock, Globe, Heart, Star, Rocket, Shield, Zap, Sparkles, Gavel, Scale, Bell, Activity, BarChart3, DollarSign, TrendingUp, Info, AlertCircle, CheckCircle, Clock, Calendar, Briefcase, Users, Folder, FileText, RotateCcw, Palette, Search, ArrowUpRight, ArrowDownRight, ImageIcon, ArrowRight, Quote, ChevronDown, XCircle, AlertTriangle, CheckCircle2, ArrowLeft, Loader2, ChevronUp, BrainCircuit, Bot, LayoutGrid, FileJson, Check, Save, Download } from 'lucide-react';
|
|
5
|
+
import { Input, Card, CardHeader, CardTitle, CardContent, useArbi, ArbiProvider, useAiTask, useWorkspaceDocs as useWorkspaceDocs$1, useSemanticSearch as useSemanticSearch$1, cn, Table, TableHeader, TableRow, TableHead, TableBody, TableCell, Button, Avatar, AvatarImage, AvatarFallback, AiMarkdown, Badge, Switch, useConfigs, useAgents, Checkbox, Tabs, TabsList, TabsTrigger, TabsContent, Separator, ArbiWebSocketProvider } from '@arbidocs/react';
|
|
6
|
+
export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, redlineStats, textToArtifact, toRedlineMarkdown } from '@arbidocs/react';
|
|
7
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
8
|
+
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { AgGridReact } from 'ag-grid-react';
|
|
10
|
+
import { themeQuartz, AllCommunityModule } from 'ag-grid-community';
|
|
11
|
+
import { Link, useParams } from 'react-router-dom';
|
|
12
|
+
import { Puck, Render } from '@measured/puck';
|
|
13
|
+
|
|
14
|
+
// src/prompts.ts
|
|
15
|
+
function matterToContext(m) {
|
|
16
|
+
return {
|
|
17
|
+
title: m.title,
|
|
18
|
+
client: m.clientName,
|
|
19
|
+
court: m.court ?? void 0,
|
|
20
|
+
claimNumber: m.claimNumber ?? void 0,
|
|
21
|
+
practice: m.practice,
|
|
22
|
+
summary: m.summary
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function createPromptLibrary(config) {
|
|
26
|
+
const {
|
|
27
|
+
firmName,
|
|
28
|
+
jurisdiction = "English",
|
|
29
|
+
language = "British English",
|
|
30
|
+
reviewerTitle = "qualified solicitor",
|
|
31
|
+
complianceOfficer = "MLRO"
|
|
32
|
+
} = config;
|
|
33
|
+
const HOUSE_RULES = `You are ${firmName}'s litigation AI, working for a regulated ${jurisdiction} law firm.
|
|
34
|
+
Ground every statement in the supplied documents and cite the source document (and page/section where possible) for each material point.
|
|
35
|
+
If the documents do not support a point, say so plainly rather than inventing it. Write in concise ${language} for a ${reviewerTitle}. This is a draft for a lawyer to review \u2014 never present it as final legal advice.`;
|
|
36
|
+
const withContext = (m, body) => {
|
|
37
|
+
if (!m) return `${HOUSE_RULES}
|
|
38
|
+
|
|
39
|
+
${body}`;
|
|
40
|
+
const facts = [
|
|
41
|
+
`Matter: ${m.title}`,
|
|
42
|
+
m.client && `Client: ${m.client}`,
|
|
43
|
+
m.court && `Forum: ${m.court}`,
|
|
44
|
+
m.claimNumber && `Case reference: ${m.claimNumber}`,
|
|
45
|
+
m.practice && `Practice area: ${m.practice}`
|
|
46
|
+
].filter(Boolean).join("\n");
|
|
47
|
+
return `${HOUSE_RULES}
|
|
48
|
+
|
|
49
|
+
Context for this matter:
|
|
50
|
+
${facts}
|
|
51
|
+
|
|
52
|
+
${body}`;
|
|
53
|
+
};
|
|
54
|
+
const prompts = {
|
|
55
|
+
/** Executive brief of the matter, from the live bundle. */
|
|
56
|
+
brief: (m) => withContext(
|
|
57
|
+
m,
|
|
58
|
+
`Produce a crisp executive brief of this matter for a partner picking it up cold. Cover, with citations: (1) what the dispute is about, (2) the parties and their roles, (3) the key issues in play, (4) the current procedural posture, and (5) the two or three things that most need attention next. Keep it under 300 words.`
|
|
59
|
+
),
|
|
60
|
+
/** Sourced chronology of events. */
|
|
61
|
+
chronology: (m) => withContext(
|
|
62
|
+
m,
|
|
63
|
+
`Build a chronology of every material dated event across the documents. Output a table with columns: Date | Event | Source (document + page). Order strictly by date. Include correspondence, transactions, procedural steps and factual events. Flag any date that is ambiguous or disputed in the documents.`
|
|
64
|
+
),
|
|
65
|
+
/** Disclosure / relevance + privilege review. */
|
|
66
|
+
disclosure: (m) => withContext(
|
|
67
|
+
m,
|
|
68
|
+
`Act as a disclosure reviewer. For each document, assess: (a) relevance to the issues in dispute (High / Medium / Low, with a one-line reason), and (b) whether it may attract legal professional privilege or contain sensitive/personal data that needs redaction \u2014 with your reasoning. Present as a table: Document | Relevance | Privilege/PII risk | Reason. Be conservative: when unsure about privilege, flag it for a lawyer.`
|
|
69
|
+
),
|
|
70
|
+
/** Merits / risk assessment — the real "AI outlook". */
|
|
71
|
+
risk: (m) => withContext(
|
|
72
|
+
m,
|
|
73
|
+
`Give a balanced merits assessment grounded in the documents. Cover: the strongest points for our client, the strongest points against, key evidential gaps, and the main quantum drivers. End with a single calibrated headline: an overall assessment of the client's position (Strong / Favourable / Balanced / Difficult) with a one-sentence justification. Cite sources throughout. Be candid about weaknesses \u2014 a partner needs the downside, not comfort.`
|
|
74
|
+
),
|
|
75
|
+
/** Extract deadlines / key dates with rule references. */
|
|
76
|
+
deadlines: (m) => withContext(
|
|
77
|
+
m,
|
|
78
|
+
`Extract every deadline, key date and time-limited step referenced in the documents. Output a table: Due date | Event | Rule/basis (e.g. CPR reference, contractual clause, court order) | Source document. Include implied deadlines (e.g. a defence due 28 days after service) and note the trigger. Flag anything overdue or imminent.`
|
|
79
|
+
),
|
|
80
|
+
/** Draft a document (letter / pleading / note). */
|
|
81
|
+
draft: (kind, m) => withContext(
|
|
82
|
+
m,
|
|
83
|
+
`Draft a first-pass ${kind} for this matter, grounded in the documents. Use the correct ${jurisdiction} litigation format and tone. Where a fact relies on a document, cite it inline in [square brackets] so the solicitor can verify. Leave clearly-marked [PLACEHOLDERS] where information is missing rather than guessing. Add a short "Points to verify before sending" list at the end.`
|
|
84
|
+
),
|
|
85
|
+
/** Free-form question about the matter. */
|
|
86
|
+
ask: (question, m) => withContext(m, `Question: ${question}`),
|
|
87
|
+
/**
|
|
88
|
+
* Draft a clean document artifact (output IS the document — no preamble).
|
|
89
|
+
*
|
|
90
|
+
* Pure single-pass generation from the matter context: it deliberately does
|
|
91
|
+
* NOT tell the agent to look up or cite the document bundle. Asking a drafting
|
|
92
|
+
* agent to "cite the source behind each fact" sends it into a retrieval loop
|
|
93
|
+
* that can spin for minutes before emitting a token; a lawyer wants a fast
|
|
94
|
+
* first draft to refine, so we draft from the known matter facts and mark
|
|
95
|
+
* genuinely-missing specifics as [PLACEHOLDER]. (Grounded, cited analysis is
|
|
96
|
+
* what {@link brief}/{@link risk} are for.)
|
|
97
|
+
*/
|
|
98
|
+
draftArtifact: (kind, instructions, m) => withContext(
|
|
99
|
+
m,
|
|
100
|
+
`Draft a ${kind} for this matter now, in a single pass, using only the matter context above \u2014 do not look anything up.${instructions ? ` Specific instructions: ${instructions}.` : ""}
|
|
101
|
+
|
|
102
|
+
Keep it tight \u2014 about 180\u2013240 words, the essential body only. Where a specific detail isn't given in the context, insert a clearly-marked [PLACEHOLDER] and keep drafting \u2014 never stop to search for it.
|
|
103
|
+
|
|
104
|
+
Output ONLY the finished document in clean Markdown \u2014 a short title, then the body in correct ${jurisdiction} litigation format. No commentary before or after the document.`
|
|
105
|
+
),
|
|
106
|
+
/**
|
|
107
|
+
* Revise a supplied passage. Returns ONLY the full revised text (no commentary),
|
|
108
|
+
* so the app can diff it against the original into a redline.
|
|
109
|
+
*/
|
|
110
|
+
revise: (original, instruction, m) => withContext(
|
|
111
|
+
m,
|
|
112
|
+
`Revise the following passage per this instruction: "${instruction}". Improve it for an ${jurisdiction} disputes matter \u2014 tighten drafting, close gaps, and align with the documents where relevant.
|
|
113
|
+
|
|
114
|
+
Return ONLY the complete revised passage as plain text. No preamble, no explanation, no markdown fences \u2014 just the revised words so they can be compared against the original.
|
|
115
|
+
|
|
116
|
+
PASSAGE:
|
|
117
|
+
"""${original}"""`
|
|
118
|
+
),
|
|
119
|
+
/** Morning digest across the matter. */
|
|
120
|
+
digest: (m) => withContext(
|
|
121
|
+
m,
|
|
122
|
+
`Write a short "start of day" brief for the fee-earner on this matter (5-6 bullet points). Surface: what changed or is newly relevant in the documents, the most pressing deadline or risk, and one concrete recommended next action. Be specific and cite sources. Keep it scannable.`
|
|
123
|
+
),
|
|
124
|
+
/** UTBMS-coded billing narrative from time context. */
|
|
125
|
+
billing: (activity, m) => withContext(
|
|
126
|
+
m,
|
|
127
|
+
`Draft a compliant, client-ready time-entry narrative for the following work: "${activity}". Write it in the third person, be specific about what was done and why it advanced the matter, avoid block-billing language, and suggest the most appropriate UTBMS task and activity codes. Keep it to 1-2 sentences.`
|
|
128
|
+
),
|
|
129
|
+
/** Closing / lessons-learned memo. */
|
|
130
|
+
closing: (m) => withContext(
|
|
131
|
+
m,
|
|
132
|
+
`Draft a matter closing memo grounded in the documents. Cover: the outcome, a concise narrative of how the matter resolved, key lessons for similar future matters, and any residual risks or follow-up obligations (limitation, undertakings, retention). Cite sources.`
|
|
133
|
+
),
|
|
134
|
+
/** Thought-leadership / insight article from the matter's themes. */
|
|
135
|
+
insight: (m) => withContext(
|
|
136
|
+
m,
|
|
137
|
+
`Using the legal and commercial themes in these documents, draft an 800-word thought-leadership insight article suitable for the firm's website. Anonymise all party-specific and confidential details \u2014 write about the legal principles and practical lessons, not this specific client. Give it a compelling headline and a two-sentence standfirst. This is marketing content; keep it authoritative but accessible.`
|
|
138
|
+
),
|
|
139
|
+
/** Auto-tagging / issue classification of documents. */
|
|
140
|
+
autoTag: (m) => withContext(
|
|
141
|
+
m,
|
|
142
|
+
`Propose a concise set of issue tags and document-type labels for this bundle so it can be organised. For each document, suggest 2-4 short tags (e.g. issue, document type, key party). Output as: Document | Suggested tags. Keep tags reusable and consistent across the set.`
|
|
143
|
+
),
|
|
144
|
+
/** Verify uploaded onboarding documents against a KYC/CDD checklist. */
|
|
145
|
+
kyc: (clientName, m) => withContext(
|
|
146
|
+
m,
|
|
147
|
+
`Act as ${firmName}'s client onboarding / KYC-CDD analyst for a new client, "${clientName}". Review the documents in this workspace against the firm's onboarding checklist and report each item as \u2705 satisfied, \u26A0\uFE0F partial, or \u274C missing, with a one-line reason citing the source.
|
|
148
|
+
|
|
149
|
+
Checklist:
|
|
150
|
+
1. Proof of identity (individual) or certificate of incorporation (entity)
|
|
151
|
+
2. Proof of registered/residential address
|
|
152
|
+
3. Beneficial ownership / corporate authority to instruct
|
|
153
|
+
4. Source of funds / source of wealth indication
|
|
154
|
+
5. Sanctions / PEP screening flags apparent from the papers
|
|
155
|
+
|
|
156
|
+
Output a Markdown table: Item | Status | Notes (source). End with an overall onboarding recommendation (Proceed / Proceed with conditions / Do not proceed) and any information still required. This is a compliance aid; the ${complianceOfficer} signs off.`
|
|
157
|
+
),
|
|
158
|
+
// ── Pitch / intake stage (may run before a workspace has full documents) ──
|
|
159
|
+
/** Triage a prospective enquiry. */
|
|
160
|
+
triage: (enquiry, m) => withContext(
|
|
161
|
+
m,
|
|
162
|
+
`A prospective client has made this enquiry:
|
|
163
|
+
|
|
164
|
+
"""${enquiry}"""
|
|
165
|
+
|
|
166
|
+
Triage it for the firm. Provide: (1) the likely practice area and a one-line matter description, (2) an initial risk/complexity rating with reasons, (3) the key questions we must ask before taking it on, and (4) any obvious limitation or urgency concerns. Be practical.`
|
|
167
|
+
),
|
|
168
|
+
/** Conflict-of-interest style check against the workspace. */
|
|
169
|
+
conflict: (parties, m) => withContext(
|
|
170
|
+
m,
|
|
171
|
+
`Run a conflict-awareness check. The prospective matter involves these parties/entities:
|
|
172
|
+
|
|
173
|
+
"""${parties}"""
|
|
174
|
+
|
|
175
|
+
Search the documents for any mention of these names or closely related entities that could indicate a conflict of interest or prior involvement. Report each potential match with the source and a plain-English explanation of why it might matter. If nothing is found, say so clearly. This is a screening aid, not a substitute for a formal conflict search.`
|
|
176
|
+
),
|
|
177
|
+
/** Capability statement / pitch letter. */
|
|
178
|
+
pitch: (opportunity, m) => withContext(
|
|
179
|
+
m,
|
|
180
|
+
`Draft a persuasive one-page capability statement to pitch for this opportunity:
|
|
181
|
+
|
|
182
|
+
"""${opportunity}"""
|
|
183
|
+
|
|
184
|
+
Lead with why the firm is the right choice, reference relevant experience at a thematic level (no confidential client details), set out the proposed team and approach, and close with a clear next step. Confident but not boastful.`
|
|
185
|
+
)
|
|
186
|
+
};
|
|
187
|
+
const WORKFLOW_ACTIONS = [
|
|
188
|
+
{ id: "brief", title: "Matter brief", prompt: prompts.brief },
|
|
189
|
+
{ id: "chronology", title: "Build a chronology", prompt: prompts.chronology },
|
|
190
|
+
{ id: "disclosure", title: "Review for disclosure", prompt: prompts.disclosure },
|
|
191
|
+
{ id: "risk", title: "Merits & risk", prompt: prompts.risk }
|
|
192
|
+
];
|
|
193
|
+
return { prompts, WORKFLOW_ACTIONS };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/theme/tokens.ts
|
|
197
|
+
var THEME_STYLE_ID = "arbi-theme-vars";
|
|
198
|
+
var TRIPLET_RE = /^-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?%\s+-?\d+(?:\.\d+)?%$/;
|
|
199
|
+
function parseRgb(color) {
|
|
200
|
+
if (color.startsWith("#")) {
|
|
201
|
+
let hex = color.slice(1);
|
|
202
|
+
if (hex.length === 3) {
|
|
203
|
+
hex = hex.split("").map((ch) => ch + ch).join("");
|
|
204
|
+
}
|
|
205
|
+
if (hex.length === 6 && /^[0-9a-fA-F]{6}$/.test(hex)) {
|
|
206
|
+
return {
|
|
207
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
208
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
209
|
+
b: parseInt(hex.slice(4, 6), 16)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const m = color.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
|
|
215
|
+
if (m) return { r: Number(m[1]), g: Number(m[2]), b: Number(m[3]) };
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function rgbToHslTriplet(r, g, b) {
|
|
219
|
+
const rn = r / 255;
|
|
220
|
+
const gn = g / 255;
|
|
221
|
+
const bn = b / 255;
|
|
222
|
+
const max = Math.max(rn, gn, bn);
|
|
223
|
+
const min = Math.min(rn, gn, bn);
|
|
224
|
+
const l = (max + min) / 2;
|
|
225
|
+
const d = max - min;
|
|
226
|
+
let h = 0;
|
|
227
|
+
let s = 0;
|
|
228
|
+
if (d !== 0) {
|
|
229
|
+
s = d / (1 - Math.abs(2 * l - 1));
|
|
230
|
+
switch (max) {
|
|
231
|
+
case rn:
|
|
232
|
+
h = 60 * ((gn - bn) / d % 6);
|
|
233
|
+
break;
|
|
234
|
+
case gn:
|
|
235
|
+
h = 60 * ((bn - rn) / d + 2);
|
|
236
|
+
break;
|
|
237
|
+
default:
|
|
238
|
+
h = 60 * ((rn - gn) / d + 4);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (h < 0) h += 360;
|
|
243
|
+
return `${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
244
|
+
}
|
|
245
|
+
function toHslTriplet(color) {
|
|
246
|
+
const c = color.trim();
|
|
247
|
+
if (TRIPLET_RE.test(c)) return c;
|
|
248
|
+
const rgb = parseRgb(c);
|
|
249
|
+
if (!rgb) return c;
|
|
250
|
+
return rgbToHslTriplet(rgb.r, rgb.g, rgb.b);
|
|
251
|
+
}
|
|
252
|
+
function renderTripletBlock(colors) {
|
|
253
|
+
return Object.entries(colors).map(([key, value]) => ` --${key}: ${toHslTriplet(value)};`).join("\n");
|
|
254
|
+
}
|
|
255
|
+
function emitCssVars(theme, dark) {
|
|
256
|
+
if (typeof document === "undefined") return;
|
|
257
|
+
const meta = [
|
|
258
|
+
` --font-display: ${theme.fonts.display};`,
|
|
259
|
+
` --font-sans: ${theme.fonts.sans};`,
|
|
260
|
+
` --radius: ${theme.radius}rem;`
|
|
261
|
+
].join("\n");
|
|
262
|
+
let css = `:root {
|
|
263
|
+
${renderTripletBlock(theme.colors)}
|
|
264
|
+
${meta}
|
|
265
|
+
}`;
|
|
266
|
+
if (dark) {
|
|
267
|
+
css += `
|
|
268
|
+
:root.dark {
|
|
269
|
+
${renderTripletBlock(dark)}
|
|
270
|
+
}`;
|
|
271
|
+
}
|
|
272
|
+
let style = document.getElementById(THEME_STYLE_ID);
|
|
273
|
+
if (!style) {
|
|
274
|
+
style = document.createElement("style");
|
|
275
|
+
style.id = THEME_STYLE_ID;
|
|
276
|
+
document.head.appendChild(style);
|
|
277
|
+
}
|
|
278
|
+
style.textContent = css;
|
|
279
|
+
}
|
|
280
|
+
function removeCssVars() {
|
|
281
|
+
if (typeof document === "undefined") return;
|
|
282
|
+
document.getElementById(THEME_STYLE_ID)?.remove();
|
|
283
|
+
}
|
|
284
|
+
function normalizeOptions(optionsOrRoot) {
|
|
285
|
+
if (typeof HTMLElement !== "undefined" && optionsOrRoot instanceof HTMLElement) {
|
|
286
|
+
return { root: optionsOrRoot };
|
|
287
|
+
}
|
|
288
|
+
return optionsOrRoot;
|
|
289
|
+
}
|
|
290
|
+
function applyThemeVars(theme, optionsOrRoot = {}) {
|
|
291
|
+
const { mode = "flat", dark, root = document.documentElement } = normalizeOptions(optionsOrRoot);
|
|
292
|
+
if (mode === "cssVars") {
|
|
293
|
+
emitCssVars(theme, dark);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
for (const [key, value] of Object.entries(theme.colors)) {
|
|
297
|
+
root.style.setProperty(`--color-${key}`, value);
|
|
298
|
+
}
|
|
299
|
+
root.style.setProperty("--font-display", theme.fonts.display);
|
|
300
|
+
root.style.setProperty("--font-sans", theme.fonts.sans);
|
|
301
|
+
root.style.setProperty("--radius", `${theme.radius}rem`);
|
|
302
|
+
}
|
|
303
|
+
function clearThemeVars(theme, optionsOrRoot = {}) {
|
|
304
|
+
const { mode = "flat", root = document.documentElement } = normalizeOptions(optionsOrRoot);
|
|
305
|
+
if (mode === "cssVars") {
|
|
306
|
+
removeCssVars();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
for (const key of Object.keys(theme.colors)) {
|
|
310
|
+
root.style.removeProperty(`--color-${key}`);
|
|
311
|
+
}
|
|
312
|
+
root.style.removeProperty("--font-display");
|
|
313
|
+
root.style.removeProperty("--font-sans");
|
|
314
|
+
root.style.removeProperty("--radius");
|
|
315
|
+
}
|
|
316
|
+
function createThemeStore(config) {
|
|
317
|
+
const { defaultTheme, presets, storageKey } = config;
|
|
318
|
+
const useStore = create()(
|
|
319
|
+
persist(
|
|
320
|
+
(set, get) => ({
|
|
321
|
+
...defaultTheme,
|
|
322
|
+
activePresetId: presets[0]?.id ?? null,
|
|
323
|
+
setColor: (key, value) => set((s) => ({ colors: { ...s.colors, [key]: value }, activePresetId: null })),
|
|
324
|
+
setFont: (which, value) => set((s) => ({ fonts: { ...s.fonts, [which]: value }, activePresetId: null })),
|
|
325
|
+
setRadius: (radius) => set({ radius, activePresetId: null }),
|
|
326
|
+
applyPreset: (presetId) => {
|
|
327
|
+
const preset = presets.find((p) => p.id === presetId);
|
|
328
|
+
if (!preset) return;
|
|
329
|
+
set({
|
|
330
|
+
colors: { ...preset.tokens.colors },
|
|
331
|
+
fonts: { ...preset.tokens.fonts },
|
|
332
|
+
radius: preset.tokens.radius,
|
|
333
|
+
activePresetId: presetId
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
reset: () => set({
|
|
337
|
+
colors: { ...defaultTheme.colors },
|
|
338
|
+
fonts: { ...defaultTheme.fonts },
|
|
339
|
+
radius: defaultTheme.radius,
|
|
340
|
+
activePresetId: presets[0]?.id ?? null
|
|
341
|
+
}),
|
|
342
|
+
snapshot: () => {
|
|
343
|
+
const { colors, fonts, radius } = get();
|
|
344
|
+
return { colors, fonts, radius };
|
|
345
|
+
}
|
|
346
|
+
}),
|
|
347
|
+
{ name: storageKey }
|
|
348
|
+
)
|
|
349
|
+
);
|
|
350
|
+
return { ...config, useStore };
|
|
351
|
+
}
|
|
352
|
+
function applyStoredTheme(bundle) {
|
|
353
|
+
const { colors, fonts, radius } = bundle.useStore.getState();
|
|
354
|
+
applyThemeVars({ colors, fonts, radius }, { mode: bundle.mode, dark: bundle.darkColors });
|
|
355
|
+
}
|
|
356
|
+
function ThemeEditor({
|
|
357
|
+
bundle,
|
|
358
|
+
variant = "panel",
|
|
359
|
+
preview,
|
|
360
|
+
onSave,
|
|
361
|
+
testIdPrefix = "theme"
|
|
362
|
+
}) {
|
|
363
|
+
const { useStore, tokens, presets, groupLabels, mode, darkColors } = bundle;
|
|
364
|
+
const colors = useStore((s) => s.colors);
|
|
365
|
+
const fonts = useStore((s) => s.fonts);
|
|
366
|
+
const radius = useStore((s) => s.radius);
|
|
367
|
+
const activePresetId = useStore((s) => s.activePresetId);
|
|
368
|
+
const setColor = useStore((s) => s.setColor);
|
|
369
|
+
const setFont = useStore((s) => s.setFont);
|
|
370
|
+
const setRadius = useStore((s) => s.setRadius);
|
|
371
|
+
const applyPreset = useStore((s) => s.applyPreset);
|
|
372
|
+
const reset = useStore((s) => s.reset);
|
|
373
|
+
const [saving, setSaving] = useState(false);
|
|
374
|
+
const [savedVia, setSavedVia] = useState(null);
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
applyThemeVars({ colors, fonts, radius }, { mode, dark: darkColors });
|
|
377
|
+
}, [colors, fonts, radius, mode, darkColors]);
|
|
378
|
+
const tid2 = (suffix) => `${testIdPrefix}-${suffix}`;
|
|
379
|
+
const groups = [...new Set(tokens.map((t) => t.group))];
|
|
380
|
+
const save = async () => {
|
|
381
|
+
if (!onSave) return;
|
|
382
|
+
setSaving(true);
|
|
383
|
+
const via = await onSave();
|
|
384
|
+
setSavedVia(typeof via === "string" ? via : "saved");
|
|
385
|
+
setSaving(false);
|
|
386
|
+
};
|
|
387
|
+
const Presets = /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
388
|
+
presets.map((p) => /* @__PURE__ */ jsx(
|
|
389
|
+
"button",
|
|
390
|
+
{
|
|
391
|
+
type: "button",
|
|
392
|
+
onClick: () => applyPreset(p.id),
|
|
393
|
+
className: `rounded-full border px-3 py-1 text-xs font-medium transition-colors ${activePresetId === p.id ? "border-primary bg-primary text-primary-foreground" : "border-border text-foreground hover:border-primary"}`,
|
|
394
|
+
"data-testid": tid2(`preset-${p.id}`),
|
|
395
|
+
children: p.label
|
|
396
|
+
},
|
|
397
|
+
p.id
|
|
398
|
+
)),
|
|
399
|
+
/* @__PURE__ */ jsxs(
|
|
400
|
+
"button",
|
|
401
|
+
{
|
|
402
|
+
type: "button",
|
|
403
|
+
onClick: reset,
|
|
404
|
+
className: "ml-auto inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground",
|
|
405
|
+
"data-testid": tid2("reset"),
|
|
406
|
+
children: [
|
|
407
|
+
/* @__PURE__ */ jsx(RotateCcw, { className: "size-3" }),
|
|
408
|
+
" Reset"
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
] });
|
|
413
|
+
const colorRow = (k, label) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
414
|
+
/* @__PURE__ */ jsx(
|
|
415
|
+
"input",
|
|
416
|
+
{
|
|
417
|
+
type: "color",
|
|
418
|
+
value: colors[k] ?? "#000000",
|
|
419
|
+
onChange: (e) => setColor(k, e.target.value),
|
|
420
|
+
className: "size-9 shrink-0 cursor-pointer rounded-md border border-border bg-transparent",
|
|
421
|
+
"aria-label": label,
|
|
422
|
+
"data-testid": tid2(`color-${k}`)
|
|
423
|
+
}
|
|
424
|
+
),
|
|
425
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
426
|
+
/* @__PURE__ */ jsx("span", { className: "block text-xs text-muted-foreground", children: label }),
|
|
427
|
+
/* @__PURE__ */ jsx(
|
|
428
|
+
Input,
|
|
429
|
+
{
|
|
430
|
+
value: colors[k] ?? "",
|
|
431
|
+
onChange: (e) => setColor(k, e.target.value),
|
|
432
|
+
className: "bg-card placeholder:text-muted-foreground/70 focus-visible:border-ring/60 focus-visible:ring-2 focus-visible:ring-ring/50 h-7 font-mono text-xs",
|
|
433
|
+
"data-testid": tid2(`hex-${k}`)
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
] })
|
|
437
|
+
] }, k);
|
|
438
|
+
const Typography = /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
439
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
440
|
+
/* @__PURE__ */ jsx("span", { className: "block text-xs text-muted-foreground", children: "Display font stack" }),
|
|
441
|
+
/* @__PURE__ */ jsx(
|
|
442
|
+
Input,
|
|
443
|
+
{
|
|
444
|
+
value: fonts.display,
|
|
445
|
+
onChange: (e) => setFont("display", e.target.value),
|
|
446
|
+
className: "bg-card placeholder:text-muted-foreground/70 focus-visible:border-ring/60 focus-visible:ring-2 focus-visible:ring-ring/50 mt-1 font-mono text-xs",
|
|
447
|
+
"data-testid": tid2("font-display")
|
|
448
|
+
}
|
|
449
|
+
)
|
|
450
|
+
] }),
|
|
451
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
452
|
+
/* @__PURE__ */ jsx("span", { className: "block text-xs text-muted-foreground", children: "Body font stack" }),
|
|
453
|
+
/* @__PURE__ */ jsx(
|
|
454
|
+
Input,
|
|
455
|
+
{
|
|
456
|
+
value: fonts.sans,
|
|
457
|
+
onChange: (e) => setFont("sans", e.target.value),
|
|
458
|
+
className: "bg-card placeholder:text-muted-foreground/70 focus-visible:border-ring/60 focus-visible:ring-2 focus-visible:ring-ring/50 mt-1 font-mono text-xs",
|
|
459
|
+
"data-testid": tid2("font-sans")
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
] }),
|
|
463
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
464
|
+
/* @__PURE__ */ jsxs("span", { className: "block text-xs text-muted-foreground", children: [
|
|
465
|
+
"Base radius \u2014 ",
|
|
466
|
+
radius,
|
|
467
|
+
"rem"
|
|
468
|
+
] }),
|
|
469
|
+
/* @__PURE__ */ jsx(
|
|
470
|
+
"input",
|
|
471
|
+
{
|
|
472
|
+
type: "range",
|
|
473
|
+
min: 0,
|
|
474
|
+
max: 1.5,
|
|
475
|
+
step: 0.125,
|
|
476
|
+
value: radius,
|
|
477
|
+
onChange: (e) => setRadius(Number(e.target.value)),
|
|
478
|
+
className: "mt-2 w-full accent-[color:var(--color-primary)]",
|
|
479
|
+
"data-testid": tid2("radius")
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
] })
|
|
483
|
+
] });
|
|
484
|
+
const Preview = /* @__PURE__ */ jsxs(Card, { variant: "elevated", className: "rounded-xl", "data-testid": tid2("preview"), children: [
|
|
485
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Live preview" }) }),
|
|
486
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "space-y-3", children: [
|
|
487
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-[var(--radius)] bg-primary p-4 text-primary-foreground", children: [
|
|
488
|
+
/* @__PURE__ */ jsx("p", { className: "font-display text-lg font-semibold", children: preview?.title ?? "Preview" }),
|
|
489
|
+
preview?.subtitle && /* @__PURE__ */ jsx("p", { className: "text-xs opacity-80", children: preview.subtitle })
|
|
490
|
+
] }),
|
|
491
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-6 gap-1.5", children: tokens.map((t) => /* @__PURE__ */ jsx(
|
|
492
|
+
"div",
|
|
493
|
+
{
|
|
494
|
+
title: t.label,
|
|
495
|
+
className: "aspect-square rounded-md border border-border",
|
|
496
|
+
style: { background: `var(--color-${t.key})` }
|
|
497
|
+
},
|
|
498
|
+
t.key
|
|
499
|
+
)) }),
|
|
500
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-[var(--radius)] border border-border bg-[color:var(--color-ai-accent-soft,var(--color-accent))] p-3", children: [
|
|
501
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-[color:var(--color-ai-accent,var(--color-primary))]", children: "AI action surface" }),
|
|
502
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Themed by the AI-kit accent tokens." })
|
|
503
|
+
] })
|
|
504
|
+
] })
|
|
505
|
+
] });
|
|
506
|
+
const SaveBar = onSave && /* @__PURE__ */ jsxs("div", { className: "mt-6 flex items-center gap-3", children: [
|
|
507
|
+
/* @__PURE__ */ jsxs(
|
|
508
|
+
"button",
|
|
509
|
+
{
|
|
510
|
+
type: "button",
|
|
511
|
+
onClick: () => void save(),
|
|
512
|
+
disabled: saving,
|
|
513
|
+
className: "inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-60",
|
|
514
|
+
"data-testid": tid2("save"),
|
|
515
|
+
children: [
|
|
516
|
+
savedVia ? /* @__PURE__ */ jsx(Check, { className: "size-4" }) : /* @__PURE__ */ jsx(Save, { className: "size-4" }),
|
|
517
|
+
saving ? "Saving\u2026" : savedVia ? "Saved" : "Save theme"
|
|
518
|
+
]
|
|
519
|
+
}
|
|
520
|
+
),
|
|
521
|
+
savedVia && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", "data-testid": tid2("via"), children: [
|
|
522
|
+
"stored via ",
|
|
523
|
+
savedVia
|
|
524
|
+
] })
|
|
525
|
+
] });
|
|
526
|
+
if (variant === "drawer") {
|
|
527
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col overflow-y-auto bg-card p-5", "data-testid": testIdPrefix, children: [
|
|
528
|
+
/* @__PURE__ */ jsx("div", { className: "mb-4 flex items-center justify-between", children: /* @__PURE__ */ jsx("h2", { className: "font-display text-lg font-semibold text-foreground", children: "Theme" }) }),
|
|
529
|
+
Presets,
|
|
530
|
+
/* @__PURE__ */ jsx("p", { className: "mt-5 mb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground", children: "Colours" }),
|
|
531
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-3", children: tokens.map((t) => colorRow(t.key, t.label)) }),
|
|
532
|
+
/* @__PURE__ */ jsx("p", { className: "mt-6 mb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground", children: "Typography & shape" }),
|
|
533
|
+
Typography,
|
|
534
|
+
SaveBar
|
|
535
|
+
] });
|
|
536
|
+
}
|
|
537
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid gap-6 lg:grid-cols-[1fr_320px]", "data-testid": testIdPrefix, children: [
|
|
538
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
539
|
+
/* @__PURE__ */ jsxs(Card, { variant: "elevated", className: "rounded-xl", children: [
|
|
540
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [
|
|
541
|
+
/* @__PURE__ */ jsx(Palette, { className: "size-4 text-[color:var(--color-primary)]" }),
|
|
542
|
+
" Presets"
|
|
543
|
+
] }) }),
|
|
544
|
+
/* @__PURE__ */ jsx(CardContent, { children: Presets })
|
|
545
|
+
] }),
|
|
546
|
+
groups.map((group) => /* @__PURE__ */ jsxs(
|
|
547
|
+
Card,
|
|
548
|
+
{
|
|
549
|
+
variant: "elevated",
|
|
550
|
+
className: "rounded-xl",
|
|
551
|
+
"data-testid": tid2(`group-${group}`),
|
|
552
|
+
children: [
|
|
553
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: groupLabels[group] ?? group }) }),
|
|
554
|
+
/* @__PURE__ */ jsx(CardContent, { className: "grid gap-3 sm:grid-cols-2", children: tokens.filter((t) => t.group === group).map((t) => colorRow(t.key, t.label)) })
|
|
555
|
+
]
|
|
556
|
+
},
|
|
557
|
+
group
|
|
558
|
+
)),
|
|
559
|
+
/* @__PURE__ */ jsxs(Card, { variant: "elevated", className: "rounded-xl", children: [
|
|
560
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Typography & shape" }) }),
|
|
561
|
+
/* @__PURE__ */ jsx(CardContent, { children: Typography })
|
|
562
|
+
] }),
|
|
563
|
+
SaveBar
|
|
564
|
+
] }),
|
|
565
|
+
/* @__PURE__ */ jsx("div", { className: "lg:sticky lg:top-4 lg:self-start", children: Preview })
|
|
566
|
+
] });
|
|
567
|
+
}
|
|
568
|
+
var ConnectionContext = createContext(null);
|
|
569
|
+
function ConnectionProvider({
|
|
570
|
+
config,
|
|
571
|
+
children
|
|
572
|
+
}) {
|
|
573
|
+
const arbi = useArbi();
|
|
574
|
+
const isLiveConfigured = config.apiUrl.length > 0;
|
|
575
|
+
const demoWorkspaceId = config.demoWorkspaceId ?? "demo-firm";
|
|
576
|
+
const [status, setStatus] = useState("disconnected");
|
|
577
|
+
const [user, setUser] = useState(null);
|
|
578
|
+
const [workspaceId, setWorkspaceId] = useState(null);
|
|
579
|
+
const [mode, setMode] = useState("demo");
|
|
580
|
+
const [error, setError] = useState(null);
|
|
581
|
+
const enterDemo = useCallback(() => {
|
|
582
|
+
setMode("demo");
|
|
583
|
+
setStatus("demo");
|
|
584
|
+
setError(null);
|
|
585
|
+
setUser({ name: config.demoUser.name, email: config.demoUser.email });
|
|
586
|
+
setWorkspaceId(demoWorkspaceId);
|
|
587
|
+
}, [config.demoUser.name, config.demoUser.email, demoWorkspaceId]);
|
|
588
|
+
const login = useCallback(
|
|
589
|
+
async (email, password) => {
|
|
590
|
+
setError(null);
|
|
591
|
+
if (!isLiveConfigured) {
|
|
592
|
+
enterDemo();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
setStatus("connecting");
|
|
596
|
+
setMode("live");
|
|
597
|
+
try {
|
|
598
|
+
await arbi.login(email, password);
|
|
599
|
+
const workspaces = await arbi.workspaces.list();
|
|
600
|
+
if (workspaces?.length) {
|
|
601
|
+
const preferred = config.workspaceId;
|
|
602
|
+
const target = (preferred ? workspaces.find((w) => w.external_id === preferred) : void 0) ?? workspaces[0];
|
|
603
|
+
await arbi.selectWorkspace(target.external_id);
|
|
604
|
+
setWorkspaceId(target.external_id);
|
|
605
|
+
}
|
|
606
|
+
setUser({ name: email.split("@")[0], email });
|
|
607
|
+
setStatus("authenticated");
|
|
608
|
+
} catch (e) {
|
|
609
|
+
setError(e instanceof Error ? e.message : "Sign-in failed. Check your credentials.");
|
|
610
|
+
setStatus("error");
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
[arbi, enterDemo, isLiveConfigured, config.workspaceId]
|
|
614
|
+
);
|
|
615
|
+
const logout = useCallback(() => {
|
|
616
|
+
if (mode === "live") {
|
|
617
|
+
void arbi.logout?.();
|
|
618
|
+
}
|
|
619
|
+
setUser(null);
|
|
620
|
+
setWorkspaceId(null);
|
|
621
|
+
setStatus("disconnected");
|
|
622
|
+
setError(null);
|
|
623
|
+
}, [arbi, mode]);
|
|
624
|
+
const setWorkspace = useCallback((id) => setWorkspaceId(id), []);
|
|
625
|
+
const value = useMemo(
|
|
626
|
+
() => ({
|
|
627
|
+
mode,
|
|
628
|
+
status,
|
|
629
|
+
user,
|
|
630
|
+
workspaceId,
|
|
631
|
+
apiConfigured: isLiveConfigured,
|
|
632
|
+
apiUrl: config.apiUrl,
|
|
633
|
+
error,
|
|
634
|
+
assistantConfig: config.assistantConfig,
|
|
635
|
+
fallbackWorkspaceId: config.workspaceId,
|
|
636
|
+
login,
|
|
637
|
+
enterDemo,
|
|
638
|
+
logout,
|
|
639
|
+
setWorkspace
|
|
640
|
+
}),
|
|
641
|
+
[
|
|
642
|
+
mode,
|
|
643
|
+
status,
|
|
644
|
+
user,
|
|
645
|
+
workspaceId,
|
|
646
|
+
isLiveConfigured,
|
|
647
|
+
config.apiUrl,
|
|
648
|
+
config.assistantConfig,
|
|
649
|
+
config.workspaceId,
|
|
650
|
+
error,
|
|
651
|
+
login,
|
|
652
|
+
enterDemo,
|
|
653
|
+
logout,
|
|
654
|
+
setWorkspace
|
|
655
|
+
]
|
|
656
|
+
);
|
|
657
|
+
return /* @__PURE__ */ jsx(ConnectionContext.Provider, { value, children });
|
|
658
|
+
}
|
|
659
|
+
function useConnection() {
|
|
660
|
+
const ctx = useContext(ConnectionContext);
|
|
661
|
+
if (!ctx) throw new Error("useConnection must be used within ConnectionProvider");
|
|
662
|
+
return ctx;
|
|
663
|
+
}
|
|
664
|
+
var isAuthenticated = (s) => s === "demo" || s === "authenticated";
|
|
665
|
+
function ConnectionBoundWebSocket({ children }) {
|
|
666
|
+
const { status } = useConnection();
|
|
667
|
+
return /* @__PURE__ */ jsx(ArbiWebSocketProvider, { enabled: status === "authenticated", children });
|
|
668
|
+
}
|
|
669
|
+
function LegalArbiProvider({
|
|
670
|
+
config,
|
|
671
|
+
children
|
|
672
|
+
}) {
|
|
673
|
+
const [queryClient] = useState(
|
|
674
|
+
() => new QueryClient({
|
|
675
|
+
defaultOptions: { queries: { staleTime: 3e4, retry: 1, refetchOnWindowFocus: false } }
|
|
676
|
+
})
|
|
677
|
+
);
|
|
678
|
+
const providerUrl = config.apiUrl || config.demoApiUrl || "https://demo.arbi.local";
|
|
679
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(ArbiProvider, { url: providerUrl, children: /* @__PURE__ */ jsx(ConnectionProvider, { config, children: /* @__PURE__ */ jsx(ConnectionBoundWebSocket, { children }) }) }) });
|
|
680
|
+
}
|
|
681
|
+
function useFirmAiTask() {
|
|
682
|
+
const { assistantConfig } = useConnection();
|
|
683
|
+
const task = useAiTask();
|
|
684
|
+
const kitRun = task.run;
|
|
685
|
+
const run = useCallback(
|
|
686
|
+
(question, docIds, opts) => kitRun(question, docIds, { ...opts, model: opts?.model ?? assistantConfig }),
|
|
687
|
+
[kitRun, assistantConfig]
|
|
688
|
+
);
|
|
689
|
+
return { ...task, run };
|
|
690
|
+
}
|
|
691
|
+
function useWorkspaceDocs() {
|
|
692
|
+
const { workspaceId, fallbackWorkspaceId } = useConnection();
|
|
693
|
+
return useWorkspaceDocs$1(workspaceId ?? fallbackWorkspaceId);
|
|
694
|
+
}
|
|
695
|
+
function useSemanticSearch() {
|
|
696
|
+
const { docIds } = useWorkspaceDocs();
|
|
697
|
+
return useSemanticSearch$1(docIds);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/lib/testid.ts
|
|
701
|
+
function tid(...parts) {
|
|
702
|
+
return parts.filter((p) => p !== void 0 && p !== false && p !== "").map(
|
|
703
|
+
(p) => String(p).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
|
|
704
|
+
).join("-");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/lib/format.ts
|
|
708
|
+
function gbp(value, compact = false) {
|
|
709
|
+
return new Intl.NumberFormat("en-GB", {
|
|
710
|
+
style: "currency",
|
|
711
|
+
currency: "GBP",
|
|
712
|
+
maximumFractionDigits: compact && Math.abs(value) >= 1e3 ? 1 : 0,
|
|
713
|
+
notation: compact ? "compact" : "standard"
|
|
714
|
+
}).format(value);
|
|
715
|
+
}
|
|
716
|
+
function initials(name) {
|
|
717
|
+
const parts = name.trim().split(/\s+/);
|
|
718
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
719
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
720
|
+
}
|
|
721
|
+
function parseIso(iso) {
|
|
722
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(iso);
|
|
723
|
+
const d = new Date(dateOnly ? `${iso}T00:00:00` : iso);
|
|
724
|
+
if (Number.isNaN(d.getTime())) throw new Error(`Invalid ISO date: ${iso}`);
|
|
725
|
+
return d;
|
|
726
|
+
}
|
|
727
|
+
function fmtDate(iso) {
|
|
728
|
+
try {
|
|
729
|
+
return new Intl.DateTimeFormat("en-GB", {
|
|
730
|
+
day: "numeric",
|
|
731
|
+
month: "short",
|
|
732
|
+
year: "numeric"
|
|
733
|
+
}).format(parseIso(iso));
|
|
734
|
+
} catch {
|
|
735
|
+
return iso;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function fmtDateTime(iso) {
|
|
739
|
+
try {
|
|
740
|
+
return new Intl.DateTimeFormat("en-GB", {
|
|
741
|
+
day: "numeric",
|
|
742
|
+
month: "short",
|
|
743
|
+
year: "numeric",
|
|
744
|
+
hour: "2-digit",
|
|
745
|
+
minute: "2-digit",
|
|
746
|
+
hour12: false
|
|
747
|
+
}).format(parseIso(iso));
|
|
748
|
+
} catch {
|
|
749
|
+
return iso;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
var RELATIVE_UNITS = [
|
|
753
|
+
["year", 31536e3],
|
|
754
|
+
["month", 2592e3],
|
|
755
|
+
["week", 604800],
|
|
756
|
+
["day", 86400],
|
|
757
|
+
["hour", 3600],
|
|
758
|
+
["minute", 60],
|
|
759
|
+
["second", 1]
|
|
760
|
+
];
|
|
761
|
+
function fromNow(iso, from = /* @__PURE__ */ new Date()) {
|
|
762
|
+
try {
|
|
763
|
+
const diffSeconds = (parseIso(iso).getTime() - from.getTime()) / 1e3;
|
|
764
|
+
const abs = Math.abs(diffSeconds);
|
|
765
|
+
const rtf = new Intl.RelativeTimeFormat("en-GB", { numeric: "always" });
|
|
766
|
+
for (const [unit, secs] of RELATIVE_UNITS) {
|
|
767
|
+
if (abs >= secs || unit === "second") {
|
|
768
|
+
const value = Math.round(diffSeconds / secs);
|
|
769
|
+
return rtf.format(value, unit);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return rtf.format(0, "second");
|
|
773
|
+
} catch {
|
|
774
|
+
return iso;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function daysUntil(iso, from = /* @__PURE__ */ new Date()) {
|
|
778
|
+
try {
|
|
779
|
+
const target = parseIso(iso);
|
|
780
|
+
const a = Date.UTC(target.getFullYear(), target.getMonth(), target.getDate());
|
|
781
|
+
const b = Date.UTC(from.getFullYear(), from.getMonth(), from.getDate());
|
|
782
|
+
return Math.round((a - b) / 864e5);
|
|
783
|
+
} catch {
|
|
784
|
+
return 0;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function hrs(n) {
|
|
788
|
+
return `${n.toFixed(1)}h`;
|
|
789
|
+
}
|
|
790
|
+
function pct(n) {
|
|
791
|
+
return `${Math.round(n)}%`;
|
|
792
|
+
}
|
|
793
|
+
function EmptyState({
|
|
794
|
+
icon: Icon,
|
|
795
|
+
title,
|
|
796
|
+
description,
|
|
797
|
+
action,
|
|
798
|
+
className,
|
|
799
|
+
testId
|
|
800
|
+
}) {
|
|
801
|
+
return /* @__PURE__ */ jsxs(
|
|
802
|
+
"div",
|
|
803
|
+
{
|
|
804
|
+
className: cn(
|
|
805
|
+
"flex flex-col items-center justify-center rounded-xl border border-dashed border-border bg-muted/30 px-6 py-12 text-center",
|
|
806
|
+
className
|
|
807
|
+
),
|
|
808
|
+
"data-testid": testId,
|
|
809
|
+
children: [
|
|
810
|
+
Icon && /* @__PURE__ */ jsx("div", { className: "mb-3 flex size-11 items-center justify-center rounded-full bg-muted text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { className: "size-5" }) }),
|
|
811
|
+
/* @__PURE__ */ jsx("p", { className: "font-display text-base font-semibold text-ink", children: title }),
|
|
812
|
+
description && /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: description }),
|
|
813
|
+
action && /* @__PURE__ */ jsx("div", { className: "mt-4", children: action })
|
|
814
|
+
]
|
|
815
|
+
}
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
var alignClass = { left: "text-left", right: "text-right", center: "text-center" };
|
|
819
|
+
function DataTable({
|
|
820
|
+
columns,
|
|
821
|
+
rows,
|
|
822
|
+
getRowId,
|
|
823
|
+
onRowClick,
|
|
824
|
+
area,
|
|
825
|
+
emptyTitle = "Nothing to show",
|
|
826
|
+
emptyDescription,
|
|
827
|
+
className
|
|
828
|
+
}) {
|
|
829
|
+
if (rows.length === 0) {
|
|
830
|
+
return /* @__PURE__ */ jsx(EmptyState, { title: emptyTitle, description: emptyDescription, testId: tid(area, "empty") });
|
|
831
|
+
}
|
|
832
|
+
return /* @__PURE__ */ jsx("div", { className: cn("rounded-xl border border-border bg-card", className), children: /* @__PURE__ */ jsxs(Table, { "data-testid": tid(area, "table"), children: [
|
|
833
|
+
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: columns.map((c) => /* @__PURE__ */ jsx(
|
|
834
|
+
TableHead,
|
|
835
|
+
{
|
|
836
|
+
className: cn(c.align && alignClass[c.align], c.headClassName),
|
|
837
|
+
children: c.header
|
|
838
|
+
},
|
|
839
|
+
c.key
|
|
840
|
+
)) }) }),
|
|
841
|
+
/* @__PURE__ */ jsx(TableBody, { children: rows.map((row) => {
|
|
842
|
+
const id = getRowId(row);
|
|
843
|
+
return /* @__PURE__ */ jsx(
|
|
844
|
+
TableRow,
|
|
845
|
+
{
|
|
846
|
+
"data-testid": tid(area, "row", id),
|
|
847
|
+
onClick: onRowClick ? () => onRowClick(row) : void 0,
|
|
848
|
+
className: cn(onRowClick && "cursor-pointer"),
|
|
849
|
+
children: columns.map((c) => /* @__PURE__ */ jsx(
|
|
850
|
+
TableCell,
|
|
851
|
+
{
|
|
852
|
+
className: cn(c.align && alignClass[c.align], c.className),
|
|
853
|
+
children: c.render(row)
|
|
854
|
+
},
|
|
855
|
+
c.key
|
|
856
|
+
))
|
|
857
|
+
},
|
|
858
|
+
id
|
|
859
|
+
);
|
|
860
|
+
}) })
|
|
861
|
+
] }) });
|
|
862
|
+
}
|
|
863
|
+
function DataTableBlock({ columns, rows, caption, emptyText, testId }) {
|
|
864
|
+
const area = testId ?? "data-table-block";
|
|
865
|
+
const tableColumns = columns.map((col, index) => ({
|
|
866
|
+
key: col.field || `col-${index}`,
|
|
867
|
+
header: col.header,
|
|
868
|
+
align: col.align,
|
|
869
|
+
render: (row) => row.cells[col.field] ?? ""
|
|
870
|
+
}));
|
|
871
|
+
const tableRows = rows.map((cells, index) => ({ __id: String(index), cells }));
|
|
872
|
+
return /* @__PURE__ */ jsxs("div", { "data-testid": testId, children: [
|
|
873
|
+
caption ? /* @__PURE__ */ jsx(
|
|
874
|
+
"p",
|
|
875
|
+
{
|
|
876
|
+
className: "mb-2 text-sm text-muted-foreground",
|
|
877
|
+
"data-testid": testId ? `${testId}-caption` : void 0,
|
|
878
|
+
children: caption
|
|
879
|
+
}
|
|
880
|
+
) : null,
|
|
881
|
+
/* @__PURE__ */ jsx(
|
|
882
|
+
DataTable,
|
|
883
|
+
{
|
|
884
|
+
columns: tableColumns,
|
|
885
|
+
rows: tableRows,
|
|
886
|
+
getRowId: (row) => row.__id,
|
|
887
|
+
area,
|
|
888
|
+
emptyTitle: emptyText
|
|
889
|
+
}
|
|
890
|
+
)
|
|
891
|
+
] });
|
|
892
|
+
}
|
|
893
|
+
var arbiTheme = themeQuartz.withParams({
|
|
894
|
+
// Font settings
|
|
895
|
+
fontFamily: {
|
|
896
|
+
googleFont: "Nunito"
|
|
897
|
+
},
|
|
898
|
+
fontSize: 14,
|
|
899
|
+
headerFontSize: 13,
|
|
900
|
+
headerFontWeight: 600,
|
|
901
|
+
// Sizing
|
|
902
|
+
headerHeight: 44,
|
|
903
|
+
rowHeight: 40,
|
|
904
|
+
cellHorizontalPadding: 16,
|
|
905
|
+
// Borders - structural settings only.
|
|
906
|
+
// Row border is a real hairline in the theme border colour (not hardcoded
|
|
907
|
+
// white, which was invisible on the light-mode background and wrong in dark
|
|
908
|
+
// mode) so adjacent rows are clearly separated — important now that rows can
|
|
909
|
+
// be tall and multi-line (e.g. the Abstract column).
|
|
910
|
+
rowBorder: { style: "solid", width: 1, color: "hsl(var(--border))" },
|
|
911
|
+
columnBorder: false,
|
|
912
|
+
// No column borders - using CSS dividers instead
|
|
913
|
+
wrapperBorder: false,
|
|
914
|
+
// No rounded corners for clean look (except checkboxes)
|
|
915
|
+
borderRadius: 0,
|
|
916
|
+
wrapperBorderRadius: 0,
|
|
917
|
+
checkboxBorderRadius: 2
|
|
918
|
+
// Keep checkboxes square
|
|
919
|
+
});
|
|
920
|
+
function GridView({
|
|
921
|
+
items,
|
|
922
|
+
columnDefs,
|
|
923
|
+
getItemId,
|
|
924
|
+
selection,
|
|
925
|
+
emptyState,
|
|
926
|
+
isLoading = false,
|
|
927
|
+
loadingMessage = "Loading...",
|
|
928
|
+
gridOptions,
|
|
929
|
+
onGridReady,
|
|
930
|
+
onCellValueChanged,
|
|
931
|
+
onRowClick,
|
|
932
|
+
persistState = false,
|
|
933
|
+
gridState,
|
|
934
|
+
onGridStateChange,
|
|
935
|
+
initialVisibleColumns,
|
|
936
|
+
className = "w-full",
|
|
937
|
+
height = 500,
|
|
938
|
+
rowHeight,
|
|
939
|
+
testId = "grid-view",
|
|
940
|
+
pinnedBottomRowData,
|
|
941
|
+
pinnedTopRowData,
|
|
942
|
+
pagination = false,
|
|
943
|
+
paginationPageSizeSelector,
|
|
944
|
+
showEmpty
|
|
945
|
+
}) {
|
|
946
|
+
const gridApiRef = useRef(null);
|
|
947
|
+
const initialRowHeightRef = useRef(rowHeight);
|
|
948
|
+
const initialGridStateRef = useRef(gridState);
|
|
949
|
+
const initialState = useMemo(() => {
|
|
950
|
+
if (!persistState) return void 0;
|
|
951
|
+
return initialGridStateRef.current;
|
|
952
|
+
}, [persistState]);
|
|
953
|
+
const saveGridState = useCallback(() => {
|
|
954
|
+
if (!persistState || !onGridStateChange || !gridApiRef.current) return;
|
|
955
|
+
const state = gridApiRef.current.getState();
|
|
956
|
+
onGridStateChange(state);
|
|
957
|
+
}, [persistState, onGridStateChange]);
|
|
958
|
+
const ensureSelectionColumnVisible = useCallback(() => {
|
|
959
|
+
if (!gridApiRef.current || !selection?.mode) return;
|
|
960
|
+
const currentState = gridApiRef.current.getColumnState();
|
|
961
|
+
const selectionColumn = currentState.find((col) => col.colId === "ag-Grid-SelectionColumn");
|
|
962
|
+
const selectionColumnIndex = currentState.findIndex(
|
|
963
|
+
(col) => col.colId === "ag-Grid-SelectionColumn"
|
|
964
|
+
);
|
|
965
|
+
if (!selectionColumn || selectionColumn.hide || selectionColumnIndex !== 0) {
|
|
966
|
+
gridApiRef.current.setColumnsVisible(["ag-Grid-SelectionColumn"], true);
|
|
967
|
+
gridApiRef.current.applyColumnState({
|
|
968
|
+
state: [
|
|
969
|
+
{
|
|
970
|
+
colId: "ag-Grid-SelectionColumn",
|
|
971
|
+
hide: false,
|
|
972
|
+
width: selectionColumn?.width || 48,
|
|
973
|
+
pinned: null
|
|
974
|
+
// Ensure not pinned in wrong position
|
|
975
|
+
}
|
|
976
|
+
],
|
|
977
|
+
defaultState: { hide: void 0 },
|
|
978
|
+
applyOrder: false
|
|
979
|
+
});
|
|
980
|
+
if (selectionColumnIndex !== 0) {
|
|
981
|
+
gridApiRef.current.moveColumns(["ag-Grid-SelectionColumn"], 0);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}, [selection?.mode]);
|
|
985
|
+
const getRowId = useCallback(
|
|
986
|
+
(params) => {
|
|
987
|
+
return getItemId(params.data);
|
|
988
|
+
},
|
|
989
|
+
[getItemId]
|
|
990
|
+
);
|
|
991
|
+
const selectionRef = useRef(selection);
|
|
992
|
+
selectionRef.current = selection;
|
|
993
|
+
const onSelectionChangeRef = useRef(selection?.onSelectionChange);
|
|
994
|
+
onSelectionChangeRef.current = selection?.onSelectionChange;
|
|
995
|
+
const handleSelectionChanged = useCallback(() => {
|
|
996
|
+
if (!onSelectionChangeRef.current || !gridApiRef.current) return;
|
|
997
|
+
const selectedNodes = gridApiRef.current.getSelectedNodes();
|
|
998
|
+
const selectedData = selectedNodes.map((node) => node.data).filter((item) => item !== void 0);
|
|
999
|
+
onSelectionChangeRef.current(selectedData);
|
|
1000
|
+
}, []);
|
|
1001
|
+
const handleRowClicked = useCallback(
|
|
1002
|
+
(event) => {
|
|
1003
|
+
if (!onRowClick || !event.data) return;
|
|
1004
|
+
const target = event.event?.target;
|
|
1005
|
+
if (target?.closest('button, input, a, select, textarea, [role="button"]')) return;
|
|
1006
|
+
onRowClick(event.data);
|
|
1007
|
+
},
|
|
1008
|
+
[onRowClick]
|
|
1009
|
+
);
|
|
1010
|
+
const handleGridReady = useCallback(
|
|
1011
|
+
(event) => {
|
|
1012
|
+
gridApiRef.current = event.api;
|
|
1013
|
+
if (!initialState && initialVisibleColumns?.length) {
|
|
1014
|
+
const currentState = event.api.getColumnState();
|
|
1015
|
+
const stateMap = new Map(currentState.map((col) => [col.colId, col]));
|
|
1016
|
+
const visibleSet = new Set(initialVisibleColumns);
|
|
1017
|
+
const selectionCol = stateMap.get("ag-Grid-SelectionColumn");
|
|
1018
|
+
const orderedState = [
|
|
1019
|
+
// Selection column first - ALWAYS visible, create if not in state
|
|
1020
|
+
selectionCol ? { ...selectionCol, hide: false } : { colId: "ag-Grid-SelectionColumn", hide: false, width: 48 },
|
|
1021
|
+
// Visible columns in specified order
|
|
1022
|
+
...initialVisibleColumns.filter((colId) => colId !== "ag-Grid-SelectionColumn").map((colId) => {
|
|
1023
|
+
const col = stateMap.get(colId);
|
|
1024
|
+
return col ? { ...col, hide: false } : void 0;
|
|
1025
|
+
}),
|
|
1026
|
+
// Hidden columns (everything else)
|
|
1027
|
+
...currentState.filter((col) => col.colId !== "ag-Grid-SelectionColumn" && !visibleSet.has(col.colId)).map((col) => ({ ...col, hide: true }))
|
|
1028
|
+
].filter((col) => col !== void 0);
|
|
1029
|
+
event.api.applyColumnState({ state: orderedState, applyOrder: true });
|
|
1030
|
+
}
|
|
1031
|
+
if (initialState && initialVisibleColumns?.length) {
|
|
1032
|
+
const currentState = event.api.getColumnState();
|
|
1033
|
+
const savedColIds = new Set(
|
|
1034
|
+
initialState.columnOrder?.orderedColIds ?? currentState.map((c) => c.colId)
|
|
1035
|
+
);
|
|
1036
|
+
const newCols = currentState.filter(
|
|
1037
|
+
(col) => !savedColIds.has(col.colId) && col.colId !== "ag-Grid-SelectionColumn"
|
|
1038
|
+
);
|
|
1039
|
+
if (newCols.length > 0) {
|
|
1040
|
+
const defaultVisible = new Set(initialVisibleColumns);
|
|
1041
|
+
event.api.applyColumnState({
|
|
1042
|
+
state: newCols.map((col) => ({
|
|
1043
|
+
...col,
|
|
1044
|
+
hide: !defaultVisible.has(col.colId)
|
|
1045
|
+
}))
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
ensureSelectionColumnVisible();
|
|
1050
|
+
if (onGridReady) {
|
|
1051
|
+
onGridReady({ api: event.api });
|
|
1052
|
+
}
|
|
1053
|
+
const sel = selectionRef.current;
|
|
1054
|
+
if (sel && sel.selected.length > 0) {
|
|
1055
|
+
const selectedIds = new Set(sel.selected.map(getItemId));
|
|
1056
|
+
event.api.forEachNode((node) => {
|
|
1057
|
+
if (node.data && selectedIds.has(getItemId(node.data))) {
|
|
1058
|
+
node.setSelected(true);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
[onGridReady, getItemId, ensureSelectionColumnVisible, initialState, initialVisibleColumns]
|
|
1064
|
+
);
|
|
1065
|
+
const handleFirstDataRendered = useCallback(() => {
|
|
1066
|
+
ensureSelectionColumnVisible();
|
|
1067
|
+
}, [ensureSelectionColumnVisible]);
|
|
1068
|
+
const handleModelUpdated = useCallback(() => {
|
|
1069
|
+
}, []);
|
|
1070
|
+
const prevItemsRef = useRef([]);
|
|
1071
|
+
useEffect(() => {
|
|
1072
|
+
prevItemsRef.current = items;
|
|
1073
|
+
}, [items]);
|
|
1074
|
+
const handleStateUpdated = useCallback(() => {
|
|
1075
|
+
saveGridState();
|
|
1076
|
+
}, [saveGridState]);
|
|
1077
|
+
const defaultOptions = useMemo(() => {
|
|
1078
|
+
return {
|
|
1079
|
+
// Initial row height (stable — captured once). Live changes go through the
|
|
1080
|
+
// imperative effect, not this option, to avoid column-state churn.
|
|
1081
|
+
rowHeight: initialRowHeightRef.current,
|
|
1082
|
+
suppressMenuHide: true,
|
|
1083
|
+
// Prevent hiding columns since we don't have enterprise sidebar
|
|
1084
|
+
maintainColumnOrder: true,
|
|
1085
|
+
// Preserve column order when columns are added/removed
|
|
1086
|
+
// Row virtualization: Render rows as needed for performance
|
|
1087
|
+
// rowBuffer renders extra rows above/below viewport for smoother scrolling
|
|
1088
|
+
rowBuffer: 10,
|
|
1089
|
+
// Render 10 extra rows above/below viewport for smooth scrolling
|
|
1090
|
+
suppressColumnVirtualisation: false,
|
|
1091
|
+
// Keep column virtualization enabled
|
|
1092
|
+
suppressRowVirtualisation: false,
|
|
1093
|
+
// ENABLE row virtualization - only render visible rows
|
|
1094
|
+
animateRows: false,
|
|
1095
|
+
// Disable row animations
|
|
1096
|
+
suppressColumnMoveAnimation: true,
|
|
1097
|
+
// Disable column move/resize animations
|
|
1098
|
+
// Efficient updates with React Query cache
|
|
1099
|
+
// getRowId enables change detection - grid only re-renders changed rows
|
|
1100
|
+
// Works perfectly with React Query's cache updates
|
|
1101
|
+
getRowId,
|
|
1102
|
+
rowSelection: selection?.mode ? {
|
|
1103
|
+
mode: selection.mode === "single" ? "singleRow" : "multiRow",
|
|
1104
|
+
selectAll: "filtered",
|
|
1105
|
+
// Header checkbox selects all filtered rows (across all pages)
|
|
1106
|
+
// In multi-row mode, only checkboxes should toggle selection (clicking row body would replace selection)
|
|
1107
|
+
enableClickSelection: selection.mode === "single",
|
|
1108
|
+
checkboxes: true,
|
|
1109
|
+
headerCheckbox: selection.mode === "multiple"
|
|
1110
|
+
} : void 0,
|
|
1111
|
+
selectionColumnDef: selection?.mode ? {
|
|
1112
|
+
sortable: selection.mode === "multiple",
|
|
1113
|
+
width: 48,
|
|
1114
|
+
lockVisible: true,
|
|
1115
|
+
// Cannot be hidden
|
|
1116
|
+
suppressMovable: true
|
|
1117
|
+
// Cannot be moved
|
|
1118
|
+
} : void 0,
|
|
1119
|
+
// Use normal layout for better performance - autoHeight causes forced reflows
|
|
1120
|
+
domLayout: "normal",
|
|
1121
|
+
// Stop editing when clicking outside the cell/grid
|
|
1122
|
+
stopEditingWhenCellsLoseFocus: true,
|
|
1123
|
+
// Additional performance optimizations
|
|
1124
|
+
suppressCellFocus: true,
|
|
1125
|
+
// Don't focus cells on click (faster)
|
|
1126
|
+
suppressRowHoverHighlight: true,
|
|
1127
|
+
// Disable hover highlight - reduces DOM manipulation during scroll
|
|
1128
|
+
suppressScrollOnNewData: true,
|
|
1129
|
+
// Don't scroll when data updates
|
|
1130
|
+
suppressAggFuncInHeader: true,
|
|
1131
|
+
// Don't show aggregation functions (faster)
|
|
1132
|
+
// Tooltip UX: the AG Grid default show-delay is 2 s which hurts
|
|
1133
|
+
// discoverability for cells where tooltips carry the "reveal full
|
|
1134
|
+
// content" affordance for non-editable users. 300 ms is snappy
|
|
1135
|
+
// without being accidental. tooltipInteraction lets users hover
|
|
1136
|
+
// onto the tooltip itself (e.g. to select / copy long text).
|
|
1137
|
+
// These are AG Grid's native tooltip controls — no React / Radix
|
|
1138
|
+
// provider involved, zero per-row cost; tooltip DOM is created on
|
|
1139
|
+
// demand when hover-delay elapses and destroyed on mouseout.
|
|
1140
|
+
tooltipShowDelay: 300,
|
|
1141
|
+
tooltipHideDelay: 4e3,
|
|
1142
|
+
tooltipInteraction: true,
|
|
1143
|
+
// Pagination (disabled by default)
|
|
1144
|
+
pagination,
|
|
1145
|
+
paginationPageSizeSelector,
|
|
1146
|
+
defaultColDef: {
|
|
1147
|
+
sortable: true,
|
|
1148
|
+
resizable: true,
|
|
1149
|
+
unSortIcon: true,
|
|
1150
|
+
// Show unsort icon
|
|
1151
|
+
sortingOrder: ["asc", "desc", null],
|
|
1152
|
+
// 3-way sorting: asc → desc → unsorted (AG Grid default)
|
|
1153
|
+
wrapText: false,
|
|
1154
|
+
// Disable text wrapping for cleaner display
|
|
1155
|
+
autoHeight: false,
|
|
1156
|
+
// Use fixed row height from theme
|
|
1157
|
+
wrapHeaderText: false,
|
|
1158
|
+
// Disable header text wrapping for cleaner look
|
|
1159
|
+
autoHeaderHeight: false,
|
|
1160
|
+
// Fixed header height
|
|
1161
|
+
filterParams: {
|
|
1162
|
+
maxNumConditions: 1,
|
|
1163
|
+
buttons: ["reset"]
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
}, [selection?.mode, getRowId, pagination, paginationPageSizeSelector]);
|
|
1168
|
+
const combinedOptions = useMemo(
|
|
1169
|
+
() => ({ ...defaultOptions, ...gridOptions }),
|
|
1170
|
+
[defaultOptions, gridOptions]
|
|
1171
|
+
);
|
|
1172
|
+
const containerStyle = useMemo(() => {
|
|
1173
|
+
const style = { height };
|
|
1174
|
+
if (rowHeight) {
|
|
1175
|
+
const LINE_PX = 14 * 1.35;
|
|
1176
|
+
const BORDER = 2;
|
|
1177
|
+
const PREFERRED_PAD = 16;
|
|
1178
|
+
const padTotal = Math.max(0, Math.min(PREFERRED_PAD, rowHeight - BORDER - LINE_PX));
|
|
1179
|
+
const lines = Math.max(1, Math.floor((rowHeight - BORDER - padTotal) / LINE_PX));
|
|
1180
|
+
const vars = style;
|
|
1181
|
+
vars["--abstract-pad-y"] = `${padTotal / 2}px`;
|
|
1182
|
+
vars["--abstract-max-h"] = `${lines * LINE_PX + padTotal + BORDER}px`;
|
|
1183
|
+
}
|
|
1184
|
+
return style;
|
|
1185
|
+
}, [height, rowHeight]);
|
|
1186
|
+
useEffect(() => {
|
|
1187
|
+
const api = gridApiRef.current;
|
|
1188
|
+
if (!api || api.isDestroyed() || !rowHeight) return;
|
|
1189
|
+
api.setGridOption("rowHeight", rowHeight);
|
|
1190
|
+
api.resetRowHeights();
|
|
1191
|
+
}, [rowHeight]);
|
|
1192
|
+
const containerClassName = useMemo(() => {
|
|
1193
|
+
const baseClass = className || "";
|
|
1194
|
+
const shadowClass = "shadow-sm";
|
|
1195
|
+
const alignClass2 = rowHeight ? "ag-cells-centered" : "";
|
|
1196
|
+
return [baseClass, shadowClass, alignClass2].filter(Boolean).join(" ");
|
|
1197
|
+
}, [className, rowHeight]);
|
|
1198
|
+
if (isLoading) {
|
|
1199
|
+
const skeletonRowCount = 12;
|
|
1200
|
+
return /* @__PURE__ */ jsxs(
|
|
1201
|
+
"div",
|
|
1202
|
+
{
|
|
1203
|
+
"data-testid": `${testId}-loading`,
|
|
1204
|
+
className: containerClassName,
|
|
1205
|
+
style: containerStyle,
|
|
1206
|
+
"aria-busy": "true",
|
|
1207
|
+
"aria-label": loadingMessage,
|
|
1208
|
+
children: [
|
|
1209
|
+
/* @__PURE__ */ jsxs("div", { className: "flex h-11 items-center gap-4 border-b border-border/40 px-4", children: [
|
|
1210
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-6 rounded bg-muted animate-pulse" }),
|
|
1211
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-32 rounded bg-muted animate-pulse" }),
|
|
1212
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-24 rounded bg-muted animate-pulse" }),
|
|
1213
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-20 rounded bg-muted animate-pulse" }),
|
|
1214
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-28 rounded bg-muted animate-pulse" }),
|
|
1215
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-16 rounded bg-muted animate-pulse ml-auto" })
|
|
1216
|
+
] }),
|
|
1217
|
+
Array.from({ length: skeletonRowCount }).map((_, rowIdx) => /* @__PURE__ */ jsxs("div", { className: "flex h-10 items-center gap-4 border-b border-border/20 px-4", children: [
|
|
1218
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-4 rounded-sm bg-muted/70 animate-pulse" }),
|
|
1219
|
+
/* @__PURE__ */ jsx(
|
|
1220
|
+
"div",
|
|
1221
|
+
{
|
|
1222
|
+
className: "h-3 rounded bg-muted/70 animate-pulse",
|
|
1223
|
+
style: { width: `${20 + rowIdx * 17 % 40}%` }
|
|
1224
|
+
}
|
|
1225
|
+
),
|
|
1226
|
+
/* @__PURE__ */ jsx(
|
|
1227
|
+
"div",
|
|
1228
|
+
{
|
|
1229
|
+
className: "h-3 rounded bg-muted/60 animate-pulse",
|
|
1230
|
+
style: { width: `${8 + rowIdx * 11 % 12}%` }
|
|
1231
|
+
}
|
|
1232
|
+
),
|
|
1233
|
+
/* @__PURE__ */ jsx(
|
|
1234
|
+
"div",
|
|
1235
|
+
{
|
|
1236
|
+
className: "h-3 rounded bg-muted/60 animate-pulse",
|
|
1237
|
+
style: { width: `${6 + rowIdx * 7 % 10}%` }
|
|
1238
|
+
}
|
|
1239
|
+
),
|
|
1240
|
+
/* @__PURE__ */ jsx(
|
|
1241
|
+
"div",
|
|
1242
|
+
{
|
|
1243
|
+
className: "h-3 rounded bg-muted/50 animate-pulse ml-auto",
|
|
1244
|
+
style: { width: `${5 + rowIdx * 5 % 8}%` }
|
|
1245
|
+
}
|
|
1246
|
+
)
|
|
1247
|
+
] }, rowIdx))
|
|
1248
|
+
]
|
|
1249
|
+
}
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
if ((showEmpty ?? items.length === 0) && emptyState) {
|
|
1253
|
+
return /* @__PURE__ */ jsxs(
|
|
1254
|
+
"div",
|
|
1255
|
+
{
|
|
1256
|
+
"data-testid": `${testId}-empty`,
|
|
1257
|
+
className: "flex flex-col justify-center items-center h-96 text-center p-6",
|
|
1258
|
+
children: [
|
|
1259
|
+
emptyState.icon && /* @__PURE__ */ jsx("div", { className: "text-muted-foreground/60 mb-4", children: emptyState.icon }),
|
|
1260
|
+
emptyState.title && /* @__PURE__ */ jsx("h3", { className: "text-xl font-medium mb-2", children: emptyState.title }),
|
|
1261
|
+
emptyState.message && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground mb-6 max-w-md", children: emptyState.message }),
|
|
1262
|
+
emptyState.action
|
|
1263
|
+
]
|
|
1264
|
+
}
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
return /* @__PURE__ */ jsx("div", { className: containerClassName, style: containerStyle, "data-testid": testId, children: /* @__PURE__ */ jsx(
|
|
1268
|
+
AgGridReact,
|
|
1269
|
+
{
|
|
1270
|
+
theme: arbiTheme,
|
|
1271
|
+
modules: [AllCommunityModule],
|
|
1272
|
+
columnDefs,
|
|
1273
|
+
rowData: items,
|
|
1274
|
+
pinnedBottomRowData,
|
|
1275
|
+
pinnedTopRowData,
|
|
1276
|
+
getRowId,
|
|
1277
|
+
initialState,
|
|
1278
|
+
onGridReady: handleGridReady,
|
|
1279
|
+
onFirstDataRendered: handleFirstDataRendered,
|
|
1280
|
+
onModelUpdated: handleModelUpdated,
|
|
1281
|
+
onSelectionChanged: handleSelectionChanged,
|
|
1282
|
+
onCellValueChanged,
|
|
1283
|
+
onRowClicked: handleRowClicked,
|
|
1284
|
+
onStateUpdated: handleStateUpdated,
|
|
1285
|
+
...combinedOptions
|
|
1286
|
+
}
|
|
1287
|
+
) });
|
|
1288
|
+
}
|
|
1289
|
+
function PageHeader({
|
|
1290
|
+
title,
|
|
1291
|
+
eyebrow,
|
|
1292
|
+
description,
|
|
1293
|
+
actions,
|
|
1294
|
+
className,
|
|
1295
|
+
testId
|
|
1296
|
+
}) {
|
|
1297
|
+
return /* @__PURE__ */ jsxs(
|
|
1298
|
+
"div",
|
|
1299
|
+
{
|
|
1300
|
+
className: cn("flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between", className),
|
|
1301
|
+
"data-testid": testId,
|
|
1302
|
+
children: [
|
|
1303
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
1304
|
+
eyebrow && /* @__PURE__ */ jsx("p", { className: "eyebrow mb-1", children: eyebrow }),
|
|
1305
|
+
/* @__PURE__ */ jsx("h1", { className: "font-display text-2xl font-semibold tracking-tight text-ink", children: title }),
|
|
1306
|
+
description && /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-2xl text-sm text-muted-foreground", children: description })
|
|
1307
|
+
] }),
|
|
1308
|
+
actions && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions })
|
|
1309
|
+
]
|
|
1310
|
+
}
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
function SectionHeading({
|
|
1314
|
+
eyebrow,
|
|
1315
|
+
title,
|
|
1316
|
+
lead,
|
|
1317
|
+
align = "left",
|
|
1318
|
+
className,
|
|
1319
|
+
invert
|
|
1320
|
+
}) {
|
|
1321
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("max-w-3xl", align === "center" && "mx-auto text-center", className), children: [
|
|
1322
|
+
eyebrow && /* @__PURE__ */ jsx("p", { className: "eyebrow mb-3", children: eyebrow }),
|
|
1323
|
+
/* @__PURE__ */ jsx(
|
|
1324
|
+
"h2",
|
|
1325
|
+
{
|
|
1326
|
+
className: cn(
|
|
1327
|
+
"font-display text-3xl font-semibold tracking-tight text-balance sm:text-4xl",
|
|
1328
|
+
invert ? "text-parchment" : "text-ink"
|
|
1329
|
+
),
|
|
1330
|
+
children: title
|
|
1331
|
+
}
|
|
1332
|
+
),
|
|
1333
|
+
lead && /* @__PURE__ */ jsx(
|
|
1334
|
+
"p",
|
|
1335
|
+
{
|
|
1336
|
+
className: cn(
|
|
1337
|
+
"mt-4 text-lg leading-relaxed",
|
|
1338
|
+
invert ? "text-parchment/70" : "text-muted-foreground"
|
|
1339
|
+
),
|
|
1340
|
+
children: lead
|
|
1341
|
+
}
|
|
1342
|
+
)
|
|
1343
|
+
] });
|
|
1344
|
+
}
|
|
1345
|
+
function Toolbar({
|
|
1346
|
+
search,
|
|
1347
|
+
onSearch,
|
|
1348
|
+
searchPlaceholder = "Search\u2026",
|
|
1349
|
+
filters,
|
|
1350
|
+
actions,
|
|
1351
|
+
className,
|
|
1352
|
+
testId
|
|
1353
|
+
}) {
|
|
1354
|
+
return /* @__PURE__ */ jsxs(
|
|
1355
|
+
"div",
|
|
1356
|
+
{
|
|
1357
|
+
className: cn(
|
|
1358
|
+
"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
|
|
1359
|
+
className
|
|
1360
|
+
),
|
|
1361
|
+
"data-testid": testId,
|
|
1362
|
+
children: [
|
|
1363
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-wrap items-center gap-2", children: [
|
|
1364
|
+
onSearch && /* @__PURE__ */ jsxs("div", { className: "relative w-full max-w-xs", children: [
|
|
1365
|
+
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }),
|
|
1366
|
+
/* @__PURE__ */ jsx(
|
|
1367
|
+
Input,
|
|
1368
|
+
{
|
|
1369
|
+
value: search,
|
|
1370
|
+
onChange: (e) => onSearch(e.target.value),
|
|
1371
|
+
placeholder: searchPlaceholder,
|
|
1372
|
+
className: "bg-card text-sm placeholder:text-muted-foreground/70 focus-visible:border-ring/60 focus-visible:ring-2 focus-visible:ring-ring/50 pl-8",
|
|
1373
|
+
"data-testid": testId ? `${testId}-search` : "toolbar-search"
|
|
1374
|
+
}
|
|
1375
|
+
)
|
|
1376
|
+
] }),
|
|
1377
|
+
filters
|
|
1378
|
+
] }),
|
|
1379
|
+
actions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: actions })
|
|
1380
|
+
]
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
function MetricCard({
|
|
1385
|
+
label,
|
|
1386
|
+
value,
|
|
1387
|
+
icon: Icon,
|
|
1388
|
+
hint,
|
|
1389
|
+
delta,
|
|
1390
|
+
accent = "ink",
|
|
1391
|
+
testId
|
|
1392
|
+
}) {
|
|
1393
|
+
const accentColor = accent === "emerald" ? "text-emerald" : accent === "brass" ? "text-[color:var(--color-brass)]" : "text-ink";
|
|
1394
|
+
const deltaGood = delta?.good ?? delta?.direction === "up";
|
|
1395
|
+
return /* @__PURE__ */ jsxs(Card, { variant: "elevated", className: "card-hover rounded-xl p-5", "data-testid": testId, children: [
|
|
1396
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
|
|
1397
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: label }),
|
|
1398
|
+
Icon && /* @__PURE__ */ jsx(Icon, { className: cn("size-4 shrink-0", accentColor) })
|
|
1399
|
+
] }),
|
|
1400
|
+
/* @__PURE__ */ jsx(
|
|
1401
|
+
"p",
|
|
1402
|
+
{
|
|
1403
|
+
className: "mt-2 font-display text-2xl font-semibold tracking-tight",
|
|
1404
|
+
"data-testid": testId && `${testId}-value`,
|
|
1405
|
+
children: value
|
|
1406
|
+
}
|
|
1407
|
+
),
|
|
1408
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-2", children: [
|
|
1409
|
+
delta && /* @__PURE__ */ jsxs(
|
|
1410
|
+
"span",
|
|
1411
|
+
{
|
|
1412
|
+
className: cn(
|
|
1413
|
+
"inline-flex items-center gap-0.5 text-xs font-medium",
|
|
1414
|
+
deltaGood ? "text-success" : "text-destructive"
|
|
1415
|
+
),
|
|
1416
|
+
children: [
|
|
1417
|
+
delta.direction === "up" ? /* @__PURE__ */ jsx(ArrowUpRight, { className: "size-3" }) : /* @__PURE__ */ jsx(ArrowDownRight, { className: "size-3" }),
|
|
1418
|
+
delta.value
|
|
1419
|
+
]
|
|
1420
|
+
}
|
|
1421
|
+
),
|
|
1422
|
+
hint && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: hint })
|
|
1423
|
+
] })
|
|
1424
|
+
] });
|
|
1425
|
+
}
|
|
1426
|
+
var ASPECT = {
|
|
1427
|
+
"16:9": "aspect-video",
|
|
1428
|
+
"4:3": "aspect-[4/3]",
|
|
1429
|
+
"1:1": "aspect-square",
|
|
1430
|
+
"21:9": "aspect-[21/9]",
|
|
1431
|
+
"4:5": "aspect-[4/5]"
|
|
1432
|
+
};
|
|
1433
|
+
var ROUNDED = {
|
|
1434
|
+
none: "rounded-none",
|
|
1435
|
+
md: "rounded-md",
|
|
1436
|
+
lg: "rounded-xl",
|
|
1437
|
+
full: "rounded-full"
|
|
1438
|
+
};
|
|
1439
|
+
function BlockImage({
|
|
1440
|
+
src,
|
|
1441
|
+
alt = "",
|
|
1442
|
+
aspect = "16:9",
|
|
1443
|
+
rounded = "lg",
|
|
1444
|
+
className,
|
|
1445
|
+
testId
|
|
1446
|
+
}) {
|
|
1447
|
+
const shape = cn(ASPECT[aspect] ?? ASPECT["16:9"], ROUNDED[rounded] ?? ROUNDED.lg);
|
|
1448
|
+
if (src) {
|
|
1449
|
+
return /* @__PURE__ */ jsx(
|
|
1450
|
+
"img",
|
|
1451
|
+
{
|
|
1452
|
+
src,
|
|
1453
|
+
alt,
|
|
1454
|
+
className: cn("w-full object-cover", shape, className),
|
|
1455
|
+
"data-testid": testId
|
|
1456
|
+
}
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
return /* @__PURE__ */ jsx(
|
|
1460
|
+
"div",
|
|
1461
|
+
{
|
|
1462
|
+
className: cn(
|
|
1463
|
+
"flex w-full items-center justify-center overflow-hidden border border-border",
|
|
1464
|
+
"bg-gradient-to-br from-muted via-card to-accent text-muted-foreground",
|
|
1465
|
+
shape,
|
|
1466
|
+
className
|
|
1467
|
+
),
|
|
1468
|
+
"data-testid": testId,
|
|
1469
|
+
"aria-label": alt || "Image placeholder",
|
|
1470
|
+
role: "img",
|
|
1471
|
+
children: /* @__PURE__ */ jsx(ImageIcon, { className: "size-8 opacity-60" })
|
|
1472
|
+
}
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
var TONE_SURFACE = {
|
|
1476
|
+
default: "bg-background text-foreground",
|
|
1477
|
+
muted: "bg-muted text-foreground",
|
|
1478
|
+
ink: "bg-foreground text-background"
|
|
1479
|
+
};
|
|
1480
|
+
function Hero({
|
|
1481
|
+
eyebrow,
|
|
1482
|
+
title,
|
|
1483
|
+
subtitle,
|
|
1484
|
+
primaryCta,
|
|
1485
|
+
secondaryCta,
|
|
1486
|
+
align = "left",
|
|
1487
|
+
tone = "default",
|
|
1488
|
+
imageUrl,
|
|
1489
|
+
testId
|
|
1490
|
+
}) {
|
|
1491
|
+
const subtle = tone === "ink" ? "text-background/70" : "text-muted-foreground";
|
|
1492
|
+
const eyebrowClass = tone === "ink" ? "text-background/60" : "text-muted-foreground";
|
|
1493
|
+
const centered = align === "center";
|
|
1494
|
+
const copy = /* @__PURE__ */ jsxs("div", { className: cn("max-w-2xl", centered && "mx-auto text-center"), children: [
|
|
1495
|
+
eyebrow && /* @__PURE__ */ jsx("p", { className: cn("mb-4 text-sm font-semibold uppercase tracking-wider", eyebrowClass), children: eyebrow }),
|
|
1496
|
+
/* @__PURE__ */ jsx("h1", { className: "text-4xl font-semibold tracking-tight text-balance sm:text-5xl", children: title }),
|
|
1497
|
+
subtitle && /* @__PURE__ */ jsx("p", { className: cn("mt-6 text-lg leading-relaxed", subtle), children: subtitle }),
|
|
1498
|
+
(primaryCta?.label || secondaryCta?.label) && /* @__PURE__ */ jsxs(
|
|
1499
|
+
"div",
|
|
1500
|
+
{
|
|
1501
|
+
className: cn("mt-8 flex flex-col gap-3 sm:flex-row", centered && "sm:justify-center"),
|
|
1502
|
+
children: [
|
|
1503
|
+
primaryCta?.label && /* @__PURE__ */ jsx(Button, { asChild: true, size: "lg", children: /* @__PURE__ */ jsxs("a", { href: primaryCta.href || "#", children: [
|
|
1504
|
+
primaryCta.label,
|
|
1505
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "size-4" })
|
|
1506
|
+
] }) }),
|
|
1507
|
+
secondaryCta?.label && /* @__PURE__ */ jsx(Button, { asChild: true, size: "lg", variant: "outline", children: /* @__PURE__ */ jsx("a", { href: secondaryCta.href || "#", children: secondaryCta.label }) })
|
|
1508
|
+
]
|
|
1509
|
+
}
|
|
1510
|
+
)
|
|
1511
|
+
] });
|
|
1512
|
+
return /* @__PURE__ */ jsx("section", { className: cn(TONE_SURFACE[tone] ?? TONE_SURFACE.default), "data-testid": testId, children: /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-7xl px-5 py-20 lg:px-8 lg:py-28", children: centered ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-12", children: [
|
|
1513
|
+
copy,
|
|
1514
|
+
imageUrl && /* @__PURE__ */ jsx(
|
|
1515
|
+
BlockImage,
|
|
1516
|
+
{
|
|
1517
|
+
src: imageUrl,
|
|
1518
|
+
aspect: "16:9",
|
|
1519
|
+
rounded: "lg",
|
|
1520
|
+
className: "w-full max-w-4xl",
|
|
1521
|
+
testId: testId && `${testId}-image`
|
|
1522
|
+
}
|
|
1523
|
+
)
|
|
1524
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "grid items-center gap-12 lg:grid-cols-2", children: [
|
|
1525
|
+
copy,
|
|
1526
|
+
/* @__PURE__ */ jsx(
|
|
1527
|
+
BlockImage,
|
|
1528
|
+
{
|
|
1529
|
+
src: imageUrl,
|
|
1530
|
+
aspect: "4:5",
|
|
1531
|
+
rounded: "lg",
|
|
1532
|
+
testId: testId && `${testId}-image`
|
|
1533
|
+
}
|
|
1534
|
+
)
|
|
1535
|
+
] }) }) });
|
|
1536
|
+
}
|
|
1537
|
+
var COLS = {
|
|
1538
|
+
"2": "sm:grid-cols-2",
|
|
1539
|
+
"3": "sm:grid-cols-2 lg:grid-cols-3"
|
|
1540
|
+
};
|
|
1541
|
+
function FeatureGrid({ columns = "3", items, testId }) {
|
|
1542
|
+
return /* @__PURE__ */ jsx("div", { className: cn("grid gap-5", COLS[columns] ?? COLS["3"]), "data-testid": testId, children: (items ?? []).map((item, i) => {
|
|
1543
|
+
const Icon = item.icon;
|
|
1544
|
+
return /* @__PURE__ */ jsxs(
|
|
1545
|
+
Card,
|
|
1546
|
+
{
|
|
1547
|
+
variant: "outline",
|
|
1548
|
+
className: "h-full p-6",
|
|
1549
|
+
"data-testid": testId && `${testId}-item-${i}`,
|
|
1550
|
+
children: [
|
|
1551
|
+
Icon && /* @__PURE__ */ jsx("span", { className: "mb-4 inline-flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary", children: /* @__PURE__ */ jsx(Icon, { className: "size-5" }) }),
|
|
1552
|
+
/* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold tracking-tight", children: item.title }),
|
|
1553
|
+
item.description && /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm leading-relaxed text-muted-foreground", children: item.description })
|
|
1554
|
+
]
|
|
1555
|
+
},
|
|
1556
|
+
i
|
|
1557
|
+
);
|
|
1558
|
+
}) });
|
|
1559
|
+
}
|
|
1560
|
+
var GRID = {
|
|
1561
|
+
1: "grid-cols-1",
|
|
1562
|
+
2: "grid-cols-2",
|
|
1563
|
+
3: "grid-cols-2 sm:grid-cols-3",
|
|
1564
|
+
4: "grid-cols-2 lg:grid-cols-4"
|
|
1565
|
+
};
|
|
1566
|
+
function StatGroup({ items, testId }) {
|
|
1567
|
+
const list = items ?? [];
|
|
1568
|
+
const cols = GRID[Math.min(list.length, 4)] ?? GRID[4];
|
|
1569
|
+
return /* @__PURE__ */ jsx("dl", { className: cn("grid gap-8", cols), "data-testid": testId, children: list.map((item, i) => /* @__PURE__ */ jsxs("div", { className: "text-center", "data-testid": testId && `${testId}-item-${i}`, children: [
|
|
1570
|
+
/* @__PURE__ */ jsx("dt", { className: "sr-only", children: item.label }),
|
|
1571
|
+
/* @__PURE__ */ jsx("dd", { className: "text-4xl font-semibold tracking-tight sm:text-5xl", children: item.value }),
|
|
1572
|
+
/* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: item.label })
|
|
1573
|
+
] }, i)) });
|
|
1574
|
+
}
|
|
1575
|
+
function Testimonial({ quote, author, role, avatarUrl, testId }) {
|
|
1576
|
+
return /* @__PURE__ */ jsxs(Card, { variant: "outline", className: "mx-auto max-w-3xl p-8 text-center", "data-testid": testId, children: [
|
|
1577
|
+
/* @__PURE__ */ jsx(Quote, { className: "mx-auto size-8 text-primary/40", "aria-hidden": true }),
|
|
1578
|
+
/* @__PURE__ */ jsx("blockquote", { className: "mt-4 text-xl font-medium leading-relaxed text-balance", children: quote }),
|
|
1579
|
+
(author || avatarUrl) && /* @__PURE__ */ jsxs("div", { className: "mt-6 flex items-center justify-center gap-3", children: [
|
|
1580
|
+
(avatarUrl || author) && /* @__PURE__ */ jsxs(Avatar, { children: [
|
|
1581
|
+
avatarUrl && /* @__PURE__ */ jsx(AvatarImage, { src: avatarUrl, alt: author ?? "" }),
|
|
1582
|
+
/* @__PURE__ */ jsx(AvatarFallback, { children: initials(author || "?") })
|
|
1583
|
+
] }),
|
|
1584
|
+
/* @__PURE__ */ jsxs("div", { className: "text-left", children: [
|
|
1585
|
+
author && /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold", children: author }),
|
|
1586
|
+
role && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: role })
|
|
1587
|
+
] })
|
|
1588
|
+
] })
|
|
1589
|
+
] });
|
|
1590
|
+
}
|
|
1591
|
+
var TONE_SURFACE2 = {
|
|
1592
|
+
default: "bg-primary text-primary-foreground",
|
|
1593
|
+
muted: "bg-muted text-foreground",
|
|
1594
|
+
ink: "bg-foreground text-background"
|
|
1595
|
+
};
|
|
1596
|
+
function CTABanner({ title, subtitle, cta, tone = "default", testId }) {
|
|
1597
|
+
const subtle = tone === "default" ? "text-primary-foreground/80" : tone === "ink" ? "text-background/70" : "text-muted-foreground";
|
|
1598
|
+
return /* @__PURE__ */ jsxs(
|
|
1599
|
+
"div",
|
|
1600
|
+
{
|
|
1601
|
+
className: cn(
|
|
1602
|
+
"rounded-2xl px-8 py-14 text-center",
|
|
1603
|
+
TONE_SURFACE2[tone] ?? TONE_SURFACE2.default
|
|
1604
|
+
),
|
|
1605
|
+
"data-testid": testId,
|
|
1606
|
+
children: [
|
|
1607
|
+
/* @__PURE__ */ jsx("h2", { className: "text-3xl font-semibold tracking-tight text-balance sm:text-4xl", children: title }),
|
|
1608
|
+
subtitle && /* @__PURE__ */ jsx("p", { className: cn("mx-auto mt-4 max-w-2xl text-lg", subtle), children: subtitle }),
|
|
1609
|
+
cta?.label && /* @__PURE__ */ jsx("div", { className: "mt-8", children: /* @__PURE__ */ jsx(Button, { asChild: true, size: "lg", variant: tone === "default" ? "secondary" : "default", children: /* @__PURE__ */ jsxs("a", { href: cta.href || "#", children: [
|
|
1610
|
+
cta.label,
|
|
1611
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "size-4" })
|
|
1612
|
+
] }) }) })
|
|
1613
|
+
]
|
|
1614
|
+
}
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
function LogoCloud({ items, testId }) {
|
|
1618
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-center gap-4", "data-testid": testId, children: (items ?? []).map((item, i) => /* @__PURE__ */ jsx(
|
|
1619
|
+
"span",
|
|
1620
|
+
{
|
|
1621
|
+
className: "rounded-md border border-border bg-muted px-5 py-2.5 text-sm font-semibold uppercase tracking-wide text-muted-foreground",
|
|
1622
|
+
"data-testid": testId && `${testId}-item-${i}`,
|
|
1623
|
+
children: item.label
|
|
1624
|
+
},
|
|
1625
|
+
i
|
|
1626
|
+
)) });
|
|
1627
|
+
}
|
|
1628
|
+
function FAQ({ items, testId }) {
|
|
1629
|
+
return /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-3xl divide-y divide-border", "data-testid": testId, children: (items ?? []).map((item, i) => /* @__PURE__ */ jsxs("details", { className: "group py-4", "data-testid": testId && `${testId}-item-${i}`, children: [
|
|
1630
|
+
/* @__PURE__ */ jsxs("summary", { className: "flex cursor-pointer list-none items-center justify-between gap-4 text-left text-base font-medium", children: [
|
|
1631
|
+
item.question,
|
|
1632
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground transition-transform group-open:rotate-180" })
|
|
1633
|
+
] }),
|
|
1634
|
+
item.answer && /* @__PURE__ */ jsx("p", { className: "mt-3 text-sm leading-relaxed text-muted-foreground", children: item.answer })
|
|
1635
|
+
] }, i)) });
|
|
1636
|
+
}
|
|
1637
|
+
function RichTextBlock({ content, testId }) {
|
|
1638
|
+
return /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-3xl", "data-testid": testId, children: /* @__PURE__ */ jsx(AiMarkdown, { children: content ?? "" }) });
|
|
1639
|
+
}
|
|
1640
|
+
var STYLES = {
|
|
1641
|
+
info: { wrap: "border-primary/30 bg-primary/5", icon: "text-primary", Icon: Info },
|
|
1642
|
+
success: { wrap: "border-success/30 bg-success/10", icon: "text-success", Icon: CheckCircle2 },
|
|
1643
|
+
warning: { wrap: "border-warning/40 bg-warning/10", icon: "text-warning", Icon: AlertTriangle },
|
|
1644
|
+
danger: {
|
|
1645
|
+
wrap: "border-destructive/30 bg-destructive/10",
|
|
1646
|
+
icon: "text-destructive",
|
|
1647
|
+
Icon: XCircle
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
function Callout({ variant = "info", title, body, testId }) {
|
|
1651
|
+
const style = STYLES[variant] ?? STYLES.info;
|
|
1652
|
+
const Icon = style.Icon;
|
|
1653
|
+
return /* @__PURE__ */ jsxs(
|
|
1654
|
+
"div",
|
|
1655
|
+
{
|
|
1656
|
+
className: cn("flex gap-3 rounded-lg border p-4 text-foreground", style.wrap),
|
|
1657
|
+
"data-testid": testId,
|
|
1658
|
+
role: "note",
|
|
1659
|
+
children: [
|
|
1660
|
+
/* @__PURE__ */ jsx(Icon, { className: cn("mt-0.5 size-5 shrink-0", style.icon), "aria-hidden": true }),
|
|
1661
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
1662
|
+
title && /* @__PURE__ */ jsx("p", { className: "font-semibold", children: title }),
|
|
1663
|
+
body && /* @__PURE__ */ jsx("p", { className: "text-sm leading-relaxed text-muted-foreground", children: body })
|
|
1664
|
+
] })
|
|
1665
|
+
]
|
|
1666
|
+
}
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
var SIZE = {
|
|
1670
|
+
sm: "size-8 text-xs",
|
|
1671
|
+
md: "size-10 text-sm",
|
|
1672
|
+
lg: "size-14 text-base",
|
|
1673
|
+
xl: "size-20 text-xl"
|
|
1674
|
+
};
|
|
1675
|
+
function AvatarBlock({ src, name = "", size = "md", testId }) {
|
|
1676
|
+
return /* @__PURE__ */ jsxs(Avatar, { className: cn(SIZE[size] ?? SIZE.md), "data-testid": testId, children: [
|
|
1677
|
+
src && /* @__PURE__ */ jsx(AvatarImage, { src, alt: name }),
|
|
1678
|
+
/* @__PURE__ */ jsx(AvatarFallback, { className: "font-semibold", children: initials(name || "?") })
|
|
1679
|
+
] });
|
|
1680
|
+
}
|
|
1681
|
+
function Navbar({ brand, links, cta, testId }) {
|
|
1682
|
+
return /* @__PURE__ */ jsx("header", { className: "border-b border-border bg-background", "data-testid": testId, children: /* @__PURE__ */ jsxs("nav", { className: "mx-auto flex max-w-7xl items-center justify-between gap-6 px-5 py-4 lg:px-8", children: [
|
|
1683
|
+
/* @__PURE__ */ jsx("span", { className: "text-lg font-semibold tracking-tight", children: brand }),
|
|
1684
|
+
/* @__PURE__ */ jsx("div", { className: "hidden items-center gap-6 md:flex", children: (links ?? []).map((link, i) => /* @__PURE__ */ jsx(
|
|
1685
|
+
"a",
|
|
1686
|
+
{
|
|
1687
|
+
href: link.href || "#",
|
|
1688
|
+
className: "text-sm font-medium text-muted-foreground transition-colors hover:text-foreground",
|
|
1689
|
+
"data-testid": testId && `${testId}-link-${i}`,
|
|
1690
|
+
children: link.label
|
|
1691
|
+
},
|
|
1692
|
+
i
|
|
1693
|
+
)) }),
|
|
1694
|
+
cta?.label && /* @__PURE__ */ jsx(Button, { asChild: true, size: "sm", children: /* @__PURE__ */ jsx("a", { href: cta.href || "#", children: cta.label }) })
|
|
1695
|
+
] }) });
|
|
1696
|
+
}
|
|
1697
|
+
function Footer({ columns, copyright, testId }) {
|
|
1698
|
+
return /* @__PURE__ */ jsx("footer", { className: "border-t border-border bg-muted text-foreground", "data-testid": testId, children: /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl px-5 py-14 lg:px-8", children: [
|
|
1699
|
+
/* @__PURE__ */ jsx("div", { className: "grid gap-8 sm:grid-cols-2 lg:grid-cols-4", children: (columns ?? []).map((col, i) => /* @__PURE__ */ jsxs("div", { "data-testid": testId && `${testId}-col-${i}`, children: [
|
|
1700
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold tracking-tight", children: col.heading }),
|
|
1701
|
+
/* @__PURE__ */ jsx("ul", { className: "mt-4 space-y-2", children: (col.links ?? []).map((link, j) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
1702
|
+
"a",
|
|
1703
|
+
{
|
|
1704
|
+
href: link.href || "#",
|
|
1705
|
+
className: "text-sm text-muted-foreground transition-colors hover:text-foreground",
|
|
1706
|
+
children: link.label
|
|
1707
|
+
}
|
|
1708
|
+
) }, j)) })
|
|
1709
|
+
] }, i)) }),
|
|
1710
|
+
copyright && /* @__PURE__ */ jsx("p", { className: "mt-12 border-t border-border pt-6 text-sm text-muted-foreground", children: copyright })
|
|
1711
|
+
] }) });
|
|
1712
|
+
}
|
|
1713
|
+
function ListBlock({ ordered = false, items, testId }) {
|
|
1714
|
+
const list = items ?? [];
|
|
1715
|
+
const className = cn(
|
|
1716
|
+
"mx-auto max-w-3xl space-y-2 pl-5 text-foreground",
|
|
1717
|
+
ordered ? "list-decimal" : "list-disc"
|
|
1718
|
+
);
|
|
1719
|
+
const content = list.map((item, i) => /* @__PURE__ */ jsx("li", { className: "leading-relaxed marker:text-muted-foreground", children: item.text }, i));
|
|
1720
|
+
return ordered ? /* @__PURE__ */ jsx("ol", { className, "data-testid": testId, children: content }) : /* @__PURE__ */ jsx("ul", { className, "data-testid": testId, children: content });
|
|
1721
|
+
}
|
|
1722
|
+
var GRID2 = {
|
|
1723
|
+
2: "sm:grid-cols-2",
|
|
1724
|
+
3: "sm:grid-cols-2 lg:grid-cols-3",
|
|
1725
|
+
4: "sm:grid-cols-2 lg:grid-cols-4"
|
|
1726
|
+
};
|
|
1727
|
+
var GAP = {
|
|
1728
|
+
sm: "gap-3",
|
|
1729
|
+
md: "gap-6",
|
|
1730
|
+
lg: "gap-10"
|
|
1731
|
+
};
|
|
1732
|
+
function Columns({ count = 3, gap = "md", columns, testId }) {
|
|
1733
|
+
return /* @__PURE__ */ jsx(
|
|
1734
|
+
"div",
|
|
1735
|
+
{
|
|
1736
|
+
className: cn("grid grid-cols-1", GRID2[count] ?? GRID2[3], GAP[gap] ?? GAP.md),
|
|
1737
|
+
"data-testid": testId,
|
|
1738
|
+
children: columns.map((col, i) => /* @__PURE__ */ jsx("div", { "data-testid": testId && `${testId}-col-${i}`, children: col }, i))
|
|
1739
|
+
}
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
var SIZE2 = {
|
|
1743
|
+
sm: "h-4",
|
|
1744
|
+
md: "h-8",
|
|
1745
|
+
lg: "h-16",
|
|
1746
|
+
xl: "h-24"
|
|
1747
|
+
};
|
|
1748
|
+
function Spacer({ size = "md", testId }) {
|
|
1749
|
+
return /* @__PURE__ */ jsx("div", { className: cn("w-full", SIZE2[size] ?? SIZE2.md), "data-testid": testId, "aria-hidden": true });
|
|
1750
|
+
}
|
|
1751
|
+
var WIDTH = {
|
|
1752
|
+
narrow: "max-w-3xl",
|
|
1753
|
+
default: "max-w-5xl",
|
|
1754
|
+
wide: "max-w-7xl",
|
|
1755
|
+
full: "max-w-none"
|
|
1756
|
+
};
|
|
1757
|
+
var PADDING = {
|
|
1758
|
+
none: "px-0 py-0",
|
|
1759
|
+
sm: "px-4 py-6",
|
|
1760
|
+
md: "px-5 py-12 lg:px-8",
|
|
1761
|
+
lg: "px-5 py-20 lg:px-8"
|
|
1762
|
+
};
|
|
1763
|
+
function Container({ width = "default", padding = "md", children, testId }) {
|
|
1764
|
+
return /* @__PURE__ */ jsx(
|
|
1765
|
+
"div",
|
|
1766
|
+
{
|
|
1767
|
+
className: cn(
|
|
1768
|
+
"mx-auto w-full",
|
|
1769
|
+
WIDTH[width] ?? WIDTH.default,
|
|
1770
|
+
PADDING[padding] ?? PADDING.md
|
|
1771
|
+
),
|
|
1772
|
+
"data-testid": testId,
|
|
1773
|
+
children
|
|
1774
|
+
}
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
var EMPTY_DATA = { content: [], root: {} };
|
|
1778
|
+
function StudioEditor({
|
|
1779
|
+
config,
|
|
1780
|
+
pages,
|
|
1781
|
+
persistence,
|
|
1782
|
+
themeBundle,
|
|
1783
|
+
defaultTheme,
|
|
1784
|
+
brandLabel,
|
|
1785
|
+
backHref,
|
|
1786
|
+
backLabel = "Back",
|
|
1787
|
+
live,
|
|
1788
|
+
initialSlug,
|
|
1789
|
+
testIdPrefix = "studio"
|
|
1790
|
+
}) {
|
|
1791
|
+
const arbi = useArbi();
|
|
1792
|
+
const [slug, setSlug] = useState(initialSlug ?? pages[0]?.slug ?? "home");
|
|
1793
|
+
const [data, setData] = useState(EMPTY_DATA);
|
|
1794
|
+
const [rev, setRev] = useState(0);
|
|
1795
|
+
const [loading, setLoading] = useState(true);
|
|
1796
|
+
const [themeOpen, setThemeOpen] = useState(false);
|
|
1797
|
+
const [via, setVia] = useState(null);
|
|
1798
|
+
const [saving, setSaving] = useState(false);
|
|
1799
|
+
const latest = useRef(EMPTY_DATA);
|
|
1800
|
+
useEffect(() => {
|
|
1801
|
+
let cancelled = false;
|
|
1802
|
+
setLoading(true);
|
|
1803
|
+
void (async () => {
|
|
1804
|
+
const { data: loaded } = await persistence.loadPage(arbi, live, slug);
|
|
1805
|
+
if (cancelled) return;
|
|
1806
|
+
const next = loaded ?? EMPTY_DATA;
|
|
1807
|
+
setData(next);
|
|
1808
|
+
latest.current = next;
|
|
1809
|
+
setRev((r) => r + 1);
|
|
1810
|
+
setLoading(false);
|
|
1811
|
+
})();
|
|
1812
|
+
return () => {
|
|
1813
|
+
cancelled = true;
|
|
1814
|
+
};
|
|
1815
|
+
}, [slug, live]);
|
|
1816
|
+
const persist4 = useCallback(
|
|
1817
|
+
async (toSave) => {
|
|
1818
|
+
setSaving(true);
|
|
1819
|
+
const where = await persistence.savePage(arbi, live, slug, toSave);
|
|
1820
|
+
setVia(where);
|
|
1821
|
+
setSaving(false);
|
|
1822
|
+
},
|
|
1823
|
+
[arbi, live, slug, persistence]
|
|
1824
|
+
);
|
|
1825
|
+
useEffect(() => {
|
|
1826
|
+
let cancelled = false;
|
|
1827
|
+
void (async () => {
|
|
1828
|
+
const { data: stored } = await persistence.loadTheme(arbi, live);
|
|
1829
|
+
if (cancelled || !stored) return;
|
|
1830
|
+
themeBundle.useStore.setState({
|
|
1831
|
+
colors: { ...defaultTheme.colors, ...stored.colors },
|
|
1832
|
+
fonts: stored.fonts ?? defaultTheme.fonts,
|
|
1833
|
+
radius: stored.radius ?? defaultTheme.radius,
|
|
1834
|
+
activePresetId: null
|
|
1835
|
+
});
|
|
1836
|
+
applyStoredTheme(themeBundle);
|
|
1837
|
+
})();
|
|
1838
|
+
return () => {
|
|
1839
|
+
cancelled = true;
|
|
1840
|
+
};
|
|
1841
|
+
}, [arbi, live]);
|
|
1842
|
+
const saveThemeToArbi = useCallback(async () => {
|
|
1843
|
+
const snapshot = themeBundle.useStore.getState().snapshot();
|
|
1844
|
+
return persistence.saveTheme(arbi, live, snapshot);
|
|
1845
|
+
}, [arbi, live, persistence, themeBundle]);
|
|
1846
|
+
return /* @__PURE__ */ jsxs("div", { className: "al-studio", "data-testid": "page-studio", children: [
|
|
1847
|
+
/* @__PURE__ */ jsxs("header", { className: "flex items-center gap-3 border-b border-border bg-card px-4 py-2", children: [
|
|
1848
|
+
backHref && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1849
|
+
/* @__PURE__ */ jsxs(
|
|
1850
|
+
Link,
|
|
1851
|
+
{
|
|
1852
|
+
to: backHref,
|
|
1853
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground hover:text-emerald",
|
|
1854
|
+
"data-testid": `${testIdPrefix}-back`,
|
|
1855
|
+
children: [
|
|
1856
|
+
/* @__PURE__ */ jsx(ArrowLeft, { className: "size-4" }),
|
|
1857
|
+
" ",
|
|
1858
|
+
backLabel
|
|
1859
|
+
]
|
|
1860
|
+
}
|
|
1861
|
+
),
|
|
1862
|
+
/* @__PURE__ */ jsx("span", { className: "h-4 w-px bg-border" })
|
|
1863
|
+
] }),
|
|
1864
|
+
/* @__PURE__ */ jsx("span", { className: "font-display text-sm font-semibold text-ink", children: brandLabel }),
|
|
1865
|
+
/* @__PURE__ */ jsx(
|
|
1866
|
+
"select",
|
|
1867
|
+
{
|
|
1868
|
+
value: slug,
|
|
1869
|
+
onChange: (e) => setSlug(e.target.value),
|
|
1870
|
+
className: "rounded border border-border bg-background px-2 py-1 text-sm",
|
|
1871
|
+
"data-testid": `${testIdPrefix}-slug`,
|
|
1872
|
+
children: pages.map((p) => /* @__PURE__ */ jsx("option", { value: p.slug, children: p.label }, p.slug))
|
|
1873
|
+
}
|
|
1874
|
+
),
|
|
1875
|
+
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
|
|
1876
|
+
via && /* @__PURE__ */ jsxs(
|
|
1877
|
+
"span",
|
|
1878
|
+
{
|
|
1879
|
+
className: "text-xs text-muted-foreground",
|
|
1880
|
+
"data-testid": `${testIdPrefix}-save-via`,
|
|
1881
|
+
children: [
|
|
1882
|
+
"saved via ",
|
|
1883
|
+
via
|
|
1884
|
+
]
|
|
1885
|
+
}
|
|
1886
|
+
),
|
|
1887
|
+
/* @__PURE__ */ jsxs(
|
|
1888
|
+
"button",
|
|
1889
|
+
{
|
|
1890
|
+
type: "button",
|
|
1891
|
+
onClick: () => setThemeOpen((o) => !o),
|
|
1892
|
+
className: "inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground hover:border-emerald hover:text-emerald",
|
|
1893
|
+
"data-testid": `${testIdPrefix}-theme-toggle`,
|
|
1894
|
+
children: [
|
|
1895
|
+
/* @__PURE__ */ jsx(Palette, { className: "size-4" }),
|
|
1896
|
+
" Theme"
|
|
1897
|
+
]
|
|
1898
|
+
}
|
|
1899
|
+
),
|
|
1900
|
+
/* @__PURE__ */ jsxs(
|
|
1901
|
+
"button",
|
|
1902
|
+
{
|
|
1903
|
+
type: "button",
|
|
1904
|
+
onClick: () => void persist4(latest.current),
|
|
1905
|
+
disabled: saving || loading,
|
|
1906
|
+
className: "inline-flex items-center gap-1.5 rounded-md bg-emerald px-3 py-1.5 text-sm font-medium text-parchment hover:bg-emerald-bright disabled:opacity-60",
|
|
1907
|
+
"data-testid": `${testIdPrefix}-publish`,
|
|
1908
|
+
children: [
|
|
1909
|
+
saving ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx(Rocket, { className: "size-4" }),
|
|
1910
|
+
saving ? "Publishing\u2026" : "Publish"
|
|
1911
|
+
]
|
|
1912
|
+
}
|
|
1913
|
+
)
|
|
1914
|
+
] })
|
|
1915
|
+
] }),
|
|
1916
|
+
/* @__PURE__ */ jsxs("div", { className: "al-studio__puck relative flex", children: [
|
|
1917
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: loading ? /* @__PURE__ */ jsxs(
|
|
1918
|
+
"div",
|
|
1919
|
+
{
|
|
1920
|
+
className: "flex h-full items-center justify-center text-sm text-muted-foreground",
|
|
1921
|
+
"data-testid": `${testIdPrefix}-loading`,
|
|
1922
|
+
children: [
|
|
1923
|
+
/* @__PURE__ */ jsx(Loader2, { className: "mr-2 size-4 animate-spin" }),
|
|
1924
|
+
" Loading \u201C",
|
|
1925
|
+
slug,
|
|
1926
|
+
"\u201D\u2026"
|
|
1927
|
+
]
|
|
1928
|
+
}
|
|
1929
|
+
) : /* @__PURE__ */ jsx(
|
|
1930
|
+
Puck,
|
|
1931
|
+
{
|
|
1932
|
+
config,
|
|
1933
|
+
data,
|
|
1934
|
+
onChange: (d) => {
|
|
1935
|
+
latest.current = d;
|
|
1936
|
+
},
|
|
1937
|
+
onPublish: (d) => void persist4(d)
|
|
1938
|
+
},
|
|
1939
|
+
`${slug}-${rev}`
|
|
1940
|
+
) }),
|
|
1941
|
+
themeOpen && /* @__PURE__ */ jsx(
|
|
1942
|
+
"aside",
|
|
1943
|
+
{
|
|
1944
|
+
className: "w-80 shrink-0 border-l border-border",
|
|
1945
|
+
"data-testid": `${testIdPrefix}-theme-drawer`,
|
|
1946
|
+
children: /* @__PURE__ */ jsx(
|
|
1947
|
+
ThemeEditor,
|
|
1948
|
+
{
|
|
1949
|
+
bundle: themeBundle,
|
|
1950
|
+
variant: "drawer",
|
|
1951
|
+
testIdPrefix: `${testIdPrefix}-theme`,
|
|
1952
|
+
onSave: saveThemeToArbi
|
|
1953
|
+
}
|
|
1954
|
+
)
|
|
1955
|
+
}
|
|
1956
|
+
)
|
|
1957
|
+
] })
|
|
1958
|
+
] });
|
|
1959
|
+
}
|
|
1960
|
+
function PageRenderer({
|
|
1961
|
+
config,
|
|
1962
|
+
persistence,
|
|
1963
|
+
defaultTheme,
|
|
1964
|
+
live,
|
|
1965
|
+
slug: slugProp,
|
|
1966
|
+
emptyState,
|
|
1967
|
+
testIdPrefix = "studio"
|
|
1968
|
+
}) {
|
|
1969
|
+
const params = useParams();
|
|
1970
|
+
const slug = slugProp ?? params.slug ?? "home";
|
|
1971
|
+
const arbi = useArbi();
|
|
1972
|
+
const [data, setData] = useState(null);
|
|
1973
|
+
const [loading, setLoading] = useState(true);
|
|
1974
|
+
useEffect(() => {
|
|
1975
|
+
let cancelled = false;
|
|
1976
|
+
setLoading(true);
|
|
1977
|
+
void (async () => {
|
|
1978
|
+
const [{ data: page }, { data: theme }] = await Promise.all([
|
|
1979
|
+
persistence.loadPage(arbi, live, slug),
|
|
1980
|
+
persistence.loadTheme(arbi, live)
|
|
1981
|
+
]);
|
|
1982
|
+
if (cancelled) return;
|
|
1983
|
+
applyThemeVars(
|
|
1984
|
+
theme ? { ...defaultTheme, ...theme, colors: { ...defaultTheme.colors, ...theme.colors } } : defaultTheme
|
|
1985
|
+
);
|
|
1986
|
+
setData(page);
|
|
1987
|
+
setLoading(false);
|
|
1988
|
+
})();
|
|
1989
|
+
return () => {
|
|
1990
|
+
cancelled = true;
|
|
1991
|
+
};
|
|
1992
|
+
}, [arbi, live, slug, persistence, defaultTheme]);
|
|
1993
|
+
if (loading) {
|
|
1994
|
+
return /* @__PURE__ */ jsx(
|
|
1995
|
+
"div",
|
|
1996
|
+
{
|
|
1997
|
+
className: "flex min-h-[60vh] items-center justify-center text-sm text-muted-foreground",
|
|
1998
|
+
"data-testid": `${testIdPrefix}-render-loading`,
|
|
1999
|
+
children: "Loading page\u2026"
|
|
2000
|
+
}
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
if (!data || data.content.length === 0) {
|
|
2004
|
+
const title = emptyState?.title ?? "Nothing published yet";
|
|
2005
|
+
const body = emptyState?.body ?? `Publish the \u201C${slug}\u201D page to see it here.`;
|
|
2006
|
+
return /* @__PURE__ */ jsxs(
|
|
2007
|
+
"div",
|
|
2008
|
+
{
|
|
2009
|
+
className: "mx-auto max-w-2xl px-5 py-24 text-center",
|
|
2010
|
+
"data-testid": `${testIdPrefix}-render-empty`,
|
|
2011
|
+
children: [
|
|
2012
|
+
/* @__PURE__ */ jsx("h1", { className: "font-display text-2xl font-semibold text-ink", children: title }),
|
|
2013
|
+
/* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: body })
|
|
2014
|
+
]
|
|
2015
|
+
}
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
return /* @__PURE__ */ jsx("main", { "data-testid": `page-${testIdPrefix}-render`, children: /* @__PURE__ */ jsx(Render, { config, data }) });
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// src/studio/persistence.ts
|
|
2022
|
+
function createStudioPersistence(cfg) {
|
|
2023
|
+
const WORKSPACE_ID = cfg.workspaceId;
|
|
2024
|
+
const FOLDER = cfg.folder ?? "site";
|
|
2025
|
+
const pageKey = (slug) => `${cfg.storagePrefix}${slug}`;
|
|
2026
|
+
const THEME_KEY = cfg.themeStorageKey ?? `${cfg.storagePrefix}theme`;
|
|
2027
|
+
const THEME_FILE = cfg.themeFileName ?? "theme.json";
|
|
2028
|
+
const pageFile = cfg.pageFileName ?? ((slug) => `${slug}.json`);
|
|
2029
|
+
function canUseArbi(arbi, live) {
|
|
2030
|
+
return live && !!arbi;
|
|
2031
|
+
}
|
|
2032
|
+
async function uploadJson(arbi, fileName, obj) {
|
|
2033
|
+
await arbi.selectWorkspace(WORKSPACE_ID);
|
|
2034
|
+
const blob = new Blob([JSON.stringify(obj, null, 2)], { type: "application/json" });
|
|
2035
|
+
await arbi.documents.uploadFile(blob, fileName, { folder: FOLDER });
|
|
2036
|
+
}
|
|
2037
|
+
async function downloadJson(arbi, fileName) {
|
|
2038
|
+
await arbi.selectWorkspace(WORKSPACE_ID);
|
|
2039
|
+
const docs = await arbi.documents.list();
|
|
2040
|
+
const match = docs.filter((d) => (d.file_name ?? "").endsWith(fileName) && (d.folder ?? "").includes(FOLDER)).sort((a, b) => a.created_at < b.created_at ? 1 : -1)[0];
|
|
2041
|
+
if (!match) return null;
|
|
2042
|
+
try {
|
|
2043
|
+
const res = await arbi.documents.download(match.external_id);
|
|
2044
|
+
return JSON.parse(await res.text());
|
|
2045
|
+
} catch {
|
|
2046
|
+
const parsed = await arbi.documents.getParsedContent(match.external_id, "content");
|
|
2047
|
+
const raw = typeof parsed?.content === "string" ? parsed.content : JSON.stringify(parsed);
|
|
2048
|
+
return JSON.parse(raw);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
async function savePage(arbi, live, slug, data) {
|
|
2052
|
+
try {
|
|
2053
|
+
localStorage.setItem(pageKey(slug), JSON.stringify(data));
|
|
2054
|
+
} catch {
|
|
2055
|
+
}
|
|
2056
|
+
if (canUseArbi(arbi, live)) {
|
|
2057
|
+
try {
|
|
2058
|
+
await uploadJson(arbi, pageFile(slug), data);
|
|
2059
|
+
console.info(`[studio] saved page "${slug}" \u2192 ARBI (${WORKSPACE_ID})`);
|
|
2060
|
+
return "arbi";
|
|
2061
|
+
} catch (e) {
|
|
2062
|
+
console.warn(`[studio] ARBI save failed for "${slug}", kept localStorage`, e);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
console.info(`[studio] saved page "${slug}" \u2192 localStorage`);
|
|
2066
|
+
return "local";
|
|
2067
|
+
}
|
|
2068
|
+
async function loadPage(arbi, live, slug) {
|
|
2069
|
+
if (canUseArbi(arbi, live)) {
|
|
2070
|
+
try {
|
|
2071
|
+
const data = await downloadJson(arbi, pageFile(slug));
|
|
2072
|
+
if (data) {
|
|
2073
|
+
console.info(`[studio] loaded page "${slug}" \u2190 ARBI`);
|
|
2074
|
+
return { data, via: "arbi" };
|
|
2075
|
+
}
|
|
2076
|
+
} catch (e) {
|
|
2077
|
+
console.warn(`[studio] ARBI load failed for "${slug}", trying localStorage`, e);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
const raw = safeLocalGet(pageKey(slug));
|
|
2081
|
+
console.info(`[studio] loaded page "${slug}" \u2190 localStorage`);
|
|
2082
|
+
return { data: raw ? JSON.parse(raw) : null, via: "local" };
|
|
2083
|
+
}
|
|
2084
|
+
async function saveTheme(arbi, live, theme) {
|
|
2085
|
+
try {
|
|
2086
|
+
localStorage.setItem(THEME_KEY, JSON.stringify(theme));
|
|
2087
|
+
} catch {
|
|
2088
|
+
}
|
|
2089
|
+
if (canUseArbi(arbi, live)) {
|
|
2090
|
+
try {
|
|
2091
|
+
await uploadJson(arbi, THEME_FILE, theme);
|
|
2092
|
+
console.info(`[studio] saved theme \u2192 ARBI (${WORKSPACE_ID})`);
|
|
2093
|
+
return "arbi";
|
|
2094
|
+
} catch (e) {
|
|
2095
|
+
console.warn("[studio] ARBI theme save failed, kept localStorage", e);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
console.info("[studio] saved theme \u2192 localStorage");
|
|
2099
|
+
return "local";
|
|
2100
|
+
}
|
|
2101
|
+
async function loadTheme(arbi, live) {
|
|
2102
|
+
if (canUseArbi(arbi, live)) {
|
|
2103
|
+
try {
|
|
2104
|
+
const data = await downloadJson(arbi, THEME_FILE);
|
|
2105
|
+
if (data) return { data, via: "arbi" };
|
|
2106
|
+
} catch (e) {
|
|
2107
|
+
console.warn("[studio] ARBI theme load failed, trying localStorage", e);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
const raw = safeLocalGet(THEME_KEY);
|
|
2111
|
+
return { data: raw ? JSON.parse(raw) : null, via: "local" };
|
|
2112
|
+
}
|
|
2113
|
+
function safeLocalGet(key) {
|
|
2114
|
+
try {
|
|
2115
|
+
return localStorage.getItem(key);
|
|
2116
|
+
} catch {
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return { savePage, loadPage, saveTheme, loadTheme };
|
|
2121
|
+
}
|
|
2122
|
+
function AiTextField({
|
|
2123
|
+
value,
|
|
2124
|
+
onChange,
|
|
2125
|
+
name,
|
|
2126
|
+
label,
|
|
2127
|
+
multiline,
|
|
2128
|
+
placeholder,
|
|
2129
|
+
kind,
|
|
2130
|
+
workspaceId,
|
|
2131
|
+
buildPrompt,
|
|
2132
|
+
testIdPrefix
|
|
2133
|
+
}) {
|
|
2134
|
+
const { docIds } = useWorkspaceDocs$1(workspaceId);
|
|
2135
|
+
const task = useAiTask();
|
|
2136
|
+
const [active, setActive] = useState(false);
|
|
2137
|
+
const activeRef = useRef(false);
|
|
2138
|
+
useEffect(() => {
|
|
2139
|
+
if (activeRef.current && task.text) onChange(task.text);
|
|
2140
|
+
}, [task.text, onChange]);
|
|
2141
|
+
useEffect(() => {
|
|
2142
|
+
if (active && !task.isRunning) {
|
|
2143
|
+
activeRef.current = false;
|
|
2144
|
+
setActive(false);
|
|
2145
|
+
}
|
|
2146
|
+
}, [active, task.isRunning]);
|
|
2147
|
+
const write = async () => {
|
|
2148
|
+
if (task.isRunning) return;
|
|
2149
|
+
activeRef.current = true;
|
|
2150
|
+
setActive(true);
|
|
2151
|
+
await task.run(buildPrompt({ kind, label, current: value ?? "" }), docIds);
|
|
2152
|
+
};
|
|
2153
|
+
const inputTid = tid(`${testIdPrefix}-field`, name);
|
|
2154
|
+
return /* @__PURE__ */ jsxs("div", { className: "al-ai-field", children: [
|
|
2155
|
+
multiline ? /* @__PURE__ */ jsx(
|
|
2156
|
+
"textarea",
|
|
2157
|
+
{
|
|
2158
|
+
className: "al-ai-field__input",
|
|
2159
|
+
rows: 5,
|
|
2160
|
+
value: value ?? "",
|
|
2161
|
+
placeholder,
|
|
2162
|
+
onChange: (e) => onChange(e.target.value),
|
|
2163
|
+
"data-testid": inputTid
|
|
2164
|
+
}
|
|
2165
|
+
) : /* @__PURE__ */ jsx(
|
|
2166
|
+
"input",
|
|
2167
|
+
{
|
|
2168
|
+
className: "al-ai-field__input",
|
|
2169
|
+
value: value ?? "",
|
|
2170
|
+
placeholder,
|
|
2171
|
+
onChange: (e) => onChange(e.target.value),
|
|
2172
|
+
"data-testid": inputTid
|
|
2173
|
+
}
|
|
2174
|
+
),
|
|
2175
|
+
/* @__PURE__ */ jsxs(
|
|
2176
|
+
"button",
|
|
2177
|
+
{
|
|
2178
|
+
type: "button",
|
|
2179
|
+
className: "al-ai-field__btn",
|
|
2180
|
+
onClick: () => void write(),
|
|
2181
|
+
disabled: task.isRunning,
|
|
2182
|
+
"data-testid": tid(`${testIdPrefix}-ai-write`, name),
|
|
2183
|
+
title: "Draft this field with AI, grounded in the marketing workspace",
|
|
2184
|
+
children: [
|
|
2185
|
+
task.isRunning ? /* @__PURE__ */ jsx(Loader2, { className: "size-3 animate-spin" }) : /* @__PURE__ */ jsx(Sparkles, { className: "size-3" }),
|
|
2186
|
+
task.isRunning ? "Writing\u2026" : "Write with AI"
|
|
2187
|
+
]
|
|
2188
|
+
}
|
|
2189
|
+
)
|
|
2190
|
+
] });
|
|
2191
|
+
}
|
|
2192
|
+
function createAiFields(cfg) {
|
|
2193
|
+
const { workspaceId, buildPrompt } = cfg;
|
|
2194
|
+
const defaultKind = cfg.defaultKind ?? "copy";
|
|
2195
|
+
const testIdPrefix = cfg.testIdPrefix ?? "studio";
|
|
2196
|
+
const aiText = (label, placeholder) => ({
|
|
2197
|
+
type: "custom",
|
|
2198
|
+
label,
|
|
2199
|
+
render: ({ value, onChange, name }) => /* @__PURE__ */ jsx(
|
|
2200
|
+
AiTextField,
|
|
2201
|
+
{
|
|
2202
|
+
value: value ?? "",
|
|
2203
|
+
onChange,
|
|
2204
|
+
name,
|
|
2205
|
+
label,
|
|
2206
|
+
placeholder,
|
|
2207
|
+
kind: defaultKind,
|
|
2208
|
+
workspaceId,
|
|
2209
|
+
buildPrompt,
|
|
2210
|
+
testIdPrefix
|
|
2211
|
+
}
|
|
2212
|
+
)
|
|
2213
|
+
});
|
|
2214
|
+
const aiTextarea = (label, placeholder, kind = defaultKind) => ({
|
|
2215
|
+
type: "custom",
|
|
2216
|
+
label,
|
|
2217
|
+
render: ({ value, onChange, name }) => /* @__PURE__ */ jsx(
|
|
2218
|
+
AiTextField,
|
|
2219
|
+
{
|
|
2220
|
+
value: value ?? "",
|
|
2221
|
+
onChange,
|
|
2222
|
+
name,
|
|
2223
|
+
label,
|
|
2224
|
+
placeholder,
|
|
2225
|
+
kind,
|
|
2226
|
+
workspaceId,
|
|
2227
|
+
buildPrompt,
|
|
2228
|
+
testIdPrefix,
|
|
2229
|
+
multiline: true
|
|
2230
|
+
}
|
|
2231
|
+
)
|
|
2232
|
+
});
|
|
2233
|
+
return { aiText, aiTextarea };
|
|
2234
|
+
}
|
|
2235
|
+
function Section({ children, tone = "default", className = "" }) {
|
|
2236
|
+
const toneClass = tone === "ink" ? "bg-ink text-parchment" : tone === "muted" ? "border-y border-border bg-card/40" : "";
|
|
2237
|
+
return /* @__PURE__ */ jsx("section", { className: `relative ${toneClass}`, children: /* @__PURE__ */ jsx("div", { className: `mx-auto max-w-7xl px-5 py-16 lg:px-8 lg:py-20 ${className}`, children }) });
|
|
2238
|
+
}
|
|
2239
|
+
var iconMap = {
|
|
2240
|
+
FileText,
|
|
2241
|
+
Folder,
|
|
2242
|
+
Users,
|
|
2243
|
+
Briefcase,
|
|
2244
|
+
Calendar,
|
|
2245
|
+
Clock,
|
|
2246
|
+
CheckCircle,
|
|
2247
|
+
AlertCircle,
|
|
2248
|
+
Info: Info,
|
|
2249
|
+
TrendingUp,
|
|
2250
|
+
DollarSign,
|
|
2251
|
+
BarChart3,
|
|
2252
|
+
Activity,
|
|
2253
|
+
Bell,
|
|
2254
|
+
Scale,
|
|
2255
|
+
Gavel,
|
|
2256
|
+
Sparkles: Sparkles,
|
|
2257
|
+
// Marketing set
|
|
2258
|
+
Zap,
|
|
2259
|
+
Shield,
|
|
2260
|
+
Rocket: Rocket,
|
|
2261
|
+
Star,
|
|
2262
|
+
Heart,
|
|
2263
|
+
Globe,
|
|
2264
|
+
Lock,
|
|
2265
|
+
Mail,
|
|
2266
|
+
Phone,
|
|
2267
|
+
MapPin,
|
|
2268
|
+
Award,
|
|
2269
|
+
Target,
|
|
2270
|
+
Layers,
|
|
2271
|
+
Cloud,
|
|
2272
|
+
Code,
|
|
2273
|
+
Cpu,
|
|
2274
|
+
MessageSquare
|
|
2275
|
+
};
|
|
2276
|
+
var ICON_OPTIONS = [
|
|
2277
|
+
{ label: "None", value: "" },
|
|
2278
|
+
...Object.keys(iconMap).map((name) => ({ label: name, value: name }))
|
|
2279
|
+
];
|
|
2280
|
+
function createBlockKit(ai) {
|
|
2281
|
+
return {
|
|
2282
|
+
textField: (label, placeholder) => ai ? ai.aiText(label, placeholder) : { type: "text", label, placeholder },
|
|
2283
|
+
textareaField: (label, placeholder, kind) => ai ? ai.aiTextarea(label, placeholder, kind) : { type: "textarea", label, placeholder },
|
|
2284
|
+
boolRadio: (label) => ({
|
|
2285
|
+
type: "radio",
|
|
2286
|
+
label,
|
|
2287
|
+
options: [
|
|
2288
|
+
{ label: "No", value: false },
|
|
2289
|
+
{ label: "Yes", value: true }
|
|
2290
|
+
]
|
|
2291
|
+
}),
|
|
2292
|
+
iconField: (label = "Icon") => ({ type: "select", label, options: ICON_OPTIONS }),
|
|
2293
|
+
resolveIcon: (name) => name ? iconMap[name] : void 0,
|
|
2294
|
+
blockTestId: (id) => tid("arbi-block", id)
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
var emptySlot = [];
|
|
2298
|
+
var renderSlot = (slot) => {
|
|
2299
|
+
if (typeof slot !== "function") return null;
|
|
2300
|
+
const Slot = slot;
|
|
2301
|
+
return /* @__PURE__ */ jsx(Slot, {});
|
|
2302
|
+
};
|
|
2303
|
+
var toneField = {
|
|
2304
|
+
type: "radio",
|
|
2305
|
+
label: "Tone",
|
|
2306
|
+
options: [
|
|
2307
|
+
{ label: "Default", value: "default" },
|
|
2308
|
+
{ label: "Muted", value: "muted" },
|
|
2309
|
+
{ label: "Ink", value: "ink" }
|
|
2310
|
+
]
|
|
2311
|
+
};
|
|
2312
|
+
function createWebsiteBlocks(kit) {
|
|
2313
|
+
const { textField, textareaField, boolRadio, iconField, resolveIcon, blockTestId } = kit;
|
|
2314
|
+
const ctaField = (label) => ({
|
|
2315
|
+
type: "object",
|
|
2316
|
+
label,
|
|
2317
|
+
objectFields: {
|
|
2318
|
+
label: textField("Label"),
|
|
2319
|
+
href: { type: "text", label: "Link" }
|
|
2320
|
+
}
|
|
2321
|
+
});
|
|
2322
|
+
const ColumnsBlock = {
|
|
2323
|
+
label: "Columns",
|
|
2324
|
+
fields: {
|
|
2325
|
+
count: {
|
|
2326
|
+
type: "radio",
|
|
2327
|
+
label: "Columns",
|
|
2328
|
+
options: [
|
|
2329
|
+
{ label: "2", value: 2 },
|
|
2330
|
+
{ label: "3", value: 3 },
|
|
2331
|
+
{ label: "4", value: 4 }
|
|
2332
|
+
]
|
|
2333
|
+
},
|
|
2334
|
+
gap: {
|
|
2335
|
+
type: "radio",
|
|
2336
|
+
label: "Gap",
|
|
2337
|
+
options: [
|
|
2338
|
+
{ label: "S", value: "sm" },
|
|
2339
|
+
{ label: "M", value: "md" },
|
|
2340
|
+
{ label: "L", value: "lg" }
|
|
2341
|
+
]
|
|
2342
|
+
},
|
|
2343
|
+
col1: { type: "slot" },
|
|
2344
|
+
col2: { type: "slot" },
|
|
2345
|
+
col3: { type: "slot" },
|
|
2346
|
+
col4: { type: "slot" }
|
|
2347
|
+
},
|
|
2348
|
+
defaultProps: {
|
|
2349
|
+
count: 3,
|
|
2350
|
+
gap: "md",
|
|
2351
|
+
col1: emptySlot,
|
|
2352
|
+
col2: emptySlot,
|
|
2353
|
+
col3: emptySlot,
|
|
2354
|
+
col4: emptySlot
|
|
2355
|
+
},
|
|
2356
|
+
render: ({ id, count, gap, col1, col2, col3, col4 }) => {
|
|
2357
|
+
const n = Number(count) || 3;
|
|
2358
|
+
const columns = [col1, col2, col3, col4].slice(0, n).map(renderSlot);
|
|
2359
|
+
return /* @__PURE__ */ jsx(Columns, { count: n, gap, columns, testId: blockTestId(id) });
|
|
2360
|
+
}
|
|
2361
|
+
};
|
|
2362
|
+
const SpacerBlock = {
|
|
2363
|
+
label: "Spacer",
|
|
2364
|
+
fields: {
|
|
2365
|
+
size: {
|
|
2366
|
+
type: "radio",
|
|
2367
|
+
label: "Size",
|
|
2368
|
+
options: [
|
|
2369
|
+
{ label: "S", value: "sm" },
|
|
2370
|
+
{ label: "M", value: "md" },
|
|
2371
|
+
{ label: "L", value: "lg" },
|
|
2372
|
+
{ label: "XL", value: "xl" }
|
|
2373
|
+
]
|
|
2374
|
+
}
|
|
2375
|
+
},
|
|
2376
|
+
defaultProps: { size: "md" },
|
|
2377
|
+
render: ({ id, size }) => /* @__PURE__ */ jsx(Spacer, { size, testId: blockTestId(id) })
|
|
2378
|
+
};
|
|
2379
|
+
const ContainerBlock = {
|
|
2380
|
+
label: "Container",
|
|
2381
|
+
fields: {
|
|
2382
|
+
width: {
|
|
2383
|
+
type: "radio",
|
|
2384
|
+
label: "Width",
|
|
2385
|
+
options: [
|
|
2386
|
+
{ label: "Narrow", value: "narrow" },
|
|
2387
|
+
{ label: "Default", value: "default" },
|
|
2388
|
+
{ label: "Wide", value: "wide" },
|
|
2389
|
+
{ label: "Full", value: "full" }
|
|
2390
|
+
]
|
|
2391
|
+
},
|
|
2392
|
+
padding: {
|
|
2393
|
+
type: "radio",
|
|
2394
|
+
label: "Padding",
|
|
2395
|
+
options: [
|
|
2396
|
+
{ label: "None", value: "none" },
|
|
2397
|
+
{ label: "S", value: "sm" },
|
|
2398
|
+
{ label: "M", value: "md" },
|
|
2399
|
+
{ label: "L", value: "lg" }
|
|
2400
|
+
]
|
|
2401
|
+
},
|
|
2402
|
+
children: { type: "slot" }
|
|
2403
|
+
},
|
|
2404
|
+
defaultProps: { width: "default", padding: "md", children: emptySlot },
|
|
2405
|
+
render: ({ id, width, padding, children }) => /* @__PURE__ */ jsx(Container, { width, padding, testId: blockTestId(id), children: renderSlot(children) })
|
|
2406
|
+
};
|
|
2407
|
+
const HeroBlock = {
|
|
2408
|
+
label: "Hero",
|
|
2409
|
+
fields: {
|
|
2410
|
+
eyebrow: textField("Eyebrow"),
|
|
2411
|
+
title: textField("Title"),
|
|
2412
|
+
subtitle: textareaField("Subtitle", void 0, "lead"),
|
|
2413
|
+
primaryCta: ctaField("Primary CTA"),
|
|
2414
|
+
secondaryCta: ctaField("Secondary CTA"),
|
|
2415
|
+
align: {
|
|
2416
|
+
type: "radio",
|
|
2417
|
+
label: "Align",
|
|
2418
|
+
options: [
|
|
2419
|
+
{ label: "Left", value: "left" },
|
|
2420
|
+
{ label: "Center", value: "center" }
|
|
2421
|
+
]
|
|
2422
|
+
},
|
|
2423
|
+
tone: toneField,
|
|
2424
|
+
imageUrl: { type: "text", label: "Image URL" }
|
|
2425
|
+
},
|
|
2426
|
+
defaultProps: {
|
|
2427
|
+
eyebrow: "Introducing",
|
|
2428
|
+
title: "Build a better website, faster.",
|
|
2429
|
+
subtitle: "A flexible platform that helps your team ship polished, on-brand pages without waiting on engineering.",
|
|
2430
|
+
primaryCta: { label: "Get started", href: "#" },
|
|
2431
|
+
secondaryCta: { label: "Learn more", href: "#" },
|
|
2432
|
+
align: "left",
|
|
2433
|
+
tone: "default",
|
|
2434
|
+
imageUrl: ""
|
|
2435
|
+
},
|
|
2436
|
+
render: ({ id, eyebrow, title, subtitle, primaryCta, secondaryCta, align, tone, imageUrl }) => /* @__PURE__ */ jsx(
|
|
2437
|
+
Hero,
|
|
2438
|
+
{
|
|
2439
|
+
eyebrow,
|
|
2440
|
+
title,
|
|
2441
|
+
subtitle,
|
|
2442
|
+
primaryCta,
|
|
2443
|
+
secondaryCta,
|
|
2444
|
+
align,
|
|
2445
|
+
tone,
|
|
2446
|
+
imageUrl,
|
|
2447
|
+
testId: blockTestId(id)
|
|
2448
|
+
}
|
|
2449
|
+
)
|
|
2450
|
+
};
|
|
2451
|
+
const FeatureGridBlock = {
|
|
2452
|
+
label: "Feature grid",
|
|
2453
|
+
fields: {
|
|
2454
|
+
columns: {
|
|
2455
|
+
type: "radio",
|
|
2456
|
+
label: "Columns",
|
|
2457
|
+
options: [
|
|
2458
|
+
{ label: "2", value: "2" },
|
|
2459
|
+
{ label: "3", value: "3" }
|
|
2460
|
+
]
|
|
2461
|
+
},
|
|
2462
|
+
items: {
|
|
2463
|
+
type: "array",
|
|
2464
|
+
label: "Features",
|
|
2465
|
+
arrayFields: {
|
|
2466
|
+
icon: iconField(),
|
|
2467
|
+
title: textField("Title"),
|
|
2468
|
+
description: textareaField("Description")
|
|
2469
|
+
},
|
|
2470
|
+
defaultItemProps: {
|
|
2471
|
+
icon: "Sparkles",
|
|
2472
|
+
title: "Feature",
|
|
2473
|
+
description: "Describe this feature."
|
|
2474
|
+
},
|
|
2475
|
+
getItemSummary: (item) => item?.title || "Feature"
|
|
2476
|
+
}
|
|
2477
|
+
},
|
|
2478
|
+
defaultProps: {
|
|
2479
|
+
columns: "3",
|
|
2480
|
+
items: [
|
|
2481
|
+
{
|
|
2482
|
+
icon: "Zap",
|
|
2483
|
+
title: "Fast by default",
|
|
2484
|
+
description: "Ship pages in minutes with sensible, on-brand defaults."
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
icon: "Shield",
|
|
2488
|
+
title: "Secure & reliable",
|
|
2489
|
+
description: "Enterprise-grade security baked into every layer."
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
icon: "Layers",
|
|
2493
|
+
title: "Composable",
|
|
2494
|
+
description: "Mix and match blocks to build any layout you need."
|
|
2495
|
+
}
|
|
2496
|
+
]
|
|
2497
|
+
},
|
|
2498
|
+
render: ({ id, columns, items }) => {
|
|
2499
|
+
const list = (items ?? []).map(
|
|
2500
|
+
(it) => ({
|
|
2501
|
+
...it,
|
|
2502
|
+
icon: resolveIcon(it.icon)
|
|
2503
|
+
})
|
|
2504
|
+
);
|
|
2505
|
+
return /* @__PURE__ */ jsx(FeatureGrid, { columns, items: list, testId: blockTestId(id) });
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
const StatGroupBlock = {
|
|
2509
|
+
label: "Stat group",
|
|
2510
|
+
fields: {
|
|
2511
|
+
items: {
|
|
2512
|
+
type: "array",
|
|
2513
|
+
label: "Stats",
|
|
2514
|
+
arrayFields: {
|
|
2515
|
+
value: textField("Value"),
|
|
2516
|
+
label: textField("Label")
|
|
2517
|
+
},
|
|
2518
|
+
defaultItemProps: { value: "100%", label: "Metric" },
|
|
2519
|
+
getItemSummary: (item) => item?.label || "Stat"
|
|
2520
|
+
}
|
|
2521
|
+
},
|
|
2522
|
+
defaultProps: {
|
|
2523
|
+
items: [
|
|
2524
|
+
{ value: "10k+", label: "Active users" },
|
|
2525
|
+
{ value: "99.9%", label: "Uptime" },
|
|
2526
|
+
{ value: "4.9/5", label: "Customer rating" },
|
|
2527
|
+
{ value: "24/7", label: "Support" }
|
|
2528
|
+
]
|
|
2529
|
+
},
|
|
2530
|
+
render: ({ id, items }) => /* @__PURE__ */ jsx(StatGroup, { items, testId: blockTestId(id) })
|
|
2531
|
+
};
|
|
2532
|
+
const TestimonialBlock = {
|
|
2533
|
+
label: "Testimonial",
|
|
2534
|
+
fields: {
|
|
2535
|
+
quote: textareaField("Quote"),
|
|
2536
|
+
author: textField("Author"),
|
|
2537
|
+
role: textField("Role"),
|
|
2538
|
+
avatarUrl: { type: "text", label: "Avatar URL" }
|
|
2539
|
+
},
|
|
2540
|
+
defaultProps: {
|
|
2541
|
+
quote: "This is the best tool our team has adopted in years. It just works.",
|
|
2542
|
+
author: "Alex Morgan",
|
|
2543
|
+
role: "Head of Product, Northwind",
|
|
2544
|
+
avatarUrl: ""
|
|
2545
|
+
},
|
|
2546
|
+
render: ({ id, quote, author, role, avatarUrl }) => /* @__PURE__ */ jsx(
|
|
2547
|
+
Testimonial,
|
|
2548
|
+
{
|
|
2549
|
+
quote,
|
|
2550
|
+
author,
|
|
2551
|
+
role,
|
|
2552
|
+
avatarUrl,
|
|
2553
|
+
testId: blockTestId(id)
|
|
2554
|
+
}
|
|
2555
|
+
)
|
|
2556
|
+
};
|
|
2557
|
+
const CTABannerBlock = {
|
|
2558
|
+
label: "CTA banner",
|
|
2559
|
+
fields: {
|
|
2560
|
+
title: textField("Title"),
|
|
2561
|
+
subtitle: textareaField("Subtitle"),
|
|
2562
|
+
cta: ctaField("Button"),
|
|
2563
|
+
tone: toneField
|
|
2564
|
+
},
|
|
2565
|
+
defaultProps: {
|
|
2566
|
+
title: "Ready to get started?",
|
|
2567
|
+
subtitle: "Join thousands of teams already building with us.",
|
|
2568
|
+
cta: { label: "Start free trial", href: "#" },
|
|
2569
|
+
tone: "default"
|
|
2570
|
+
},
|
|
2571
|
+
render: ({ id, title, subtitle, cta, tone }) => /* @__PURE__ */ jsx(CTABanner, { title, subtitle, cta, tone, testId: blockTestId(id) })
|
|
2572
|
+
};
|
|
2573
|
+
const LogoCloudBlock = {
|
|
2574
|
+
label: "Logo cloud",
|
|
2575
|
+
fields: {
|
|
2576
|
+
items: {
|
|
2577
|
+
type: "array",
|
|
2578
|
+
label: "Logos",
|
|
2579
|
+
arrayFields: { label: textField("Label") },
|
|
2580
|
+
defaultItemProps: { label: "Brand" },
|
|
2581
|
+
getItemSummary: (item) => item?.label || "Logo"
|
|
2582
|
+
}
|
|
2583
|
+
},
|
|
2584
|
+
defaultProps: {
|
|
2585
|
+
items: [
|
|
2586
|
+
{ label: "Northwind" },
|
|
2587
|
+
{ label: "Acme" },
|
|
2588
|
+
{ label: "Globex" },
|
|
2589
|
+
{ label: "Initech" },
|
|
2590
|
+
{ label: "Umbrella" }
|
|
2591
|
+
]
|
|
2592
|
+
},
|
|
2593
|
+
render: ({ id, items }) => /* @__PURE__ */ jsx(LogoCloud, { items, testId: blockTestId(id) })
|
|
2594
|
+
};
|
|
2595
|
+
const FAQBlock = {
|
|
2596
|
+
label: "FAQ",
|
|
2597
|
+
fields: {
|
|
2598
|
+
items: {
|
|
2599
|
+
type: "array",
|
|
2600
|
+
label: "Questions",
|
|
2601
|
+
arrayFields: {
|
|
2602
|
+
question: textField("Question"),
|
|
2603
|
+
answer: textareaField("Answer")
|
|
2604
|
+
},
|
|
2605
|
+
defaultItemProps: { question: "A question?", answer: "The answer." },
|
|
2606
|
+
getItemSummary: (item) => item?.question || "Question"
|
|
2607
|
+
}
|
|
2608
|
+
},
|
|
2609
|
+
defaultProps: {
|
|
2610
|
+
items: [
|
|
2611
|
+
{
|
|
2612
|
+
question: "How does the free trial work?",
|
|
2613
|
+
answer: "You get full access for 14 days, no credit card required."
|
|
2614
|
+
},
|
|
2615
|
+
{
|
|
2616
|
+
question: "Can I change plans later?",
|
|
2617
|
+
answer: "Yes \u2014 upgrade or downgrade at any time from your dashboard."
|
|
2618
|
+
},
|
|
2619
|
+
{
|
|
2620
|
+
question: "Do you offer support?",
|
|
2621
|
+
answer: "Every plan includes 24/7 support from our team."
|
|
2622
|
+
}
|
|
2623
|
+
]
|
|
2624
|
+
},
|
|
2625
|
+
render: ({ id, items }) => /* @__PURE__ */ jsx(FAQ, { items, testId: blockTestId(id) })
|
|
2626
|
+
};
|
|
2627
|
+
const RichTextBlockConfig = {
|
|
2628
|
+
label: "Rich text",
|
|
2629
|
+
fields: {
|
|
2630
|
+
content: textareaField("Content", void 0, "body")
|
|
2631
|
+
},
|
|
2632
|
+
defaultProps: {
|
|
2633
|
+
content: "## About us\n\nWe build tools that help teams do their best work. Our platform combines **power** and _simplicity_ so you can focus on what matters.\n\n- Fast to learn\n- Easy to scale\n- Loved by teams"
|
|
2634
|
+
},
|
|
2635
|
+
render: ({ id, content }) => /* @__PURE__ */ jsx(RichTextBlock, { content, testId: blockTestId(id) })
|
|
2636
|
+
};
|
|
2637
|
+
const CalloutBlock = {
|
|
2638
|
+
label: "Callout",
|
|
2639
|
+
fields: {
|
|
2640
|
+
variant: {
|
|
2641
|
+
type: "radio",
|
|
2642
|
+
label: "Variant",
|
|
2643
|
+
options: [
|
|
2644
|
+
{ label: "Info", value: "info" },
|
|
2645
|
+
{ label: "Success", value: "success" },
|
|
2646
|
+
{ label: "Warning", value: "warning" },
|
|
2647
|
+
{ label: "Danger", value: "danger" }
|
|
2648
|
+
]
|
|
2649
|
+
},
|
|
2650
|
+
title: textField("Title"),
|
|
2651
|
+
body: textareaField("Body")
|
|
2652
|
+
},
|
|
2653
|
+
defaultProps: {
|
|
2654
|
+
variant: "info",
|
|
2655
|
+
title: "Good to know",
|
|
2656
|
+
body: "This is a helpful note that draws attention to important information."
|
|
2657
|
+
},
|
|
2658
|
+
render: ({ id, variant, title, body }) => /* @__PURE__ */ jsx(Callout, { variant, title, body, testId: blockTestId(id) })
|
|
2659
|
+
};
|
|
2660
|
+
const ImageBlock = {
|
|
2661
|
+
label: "Image",
|
|
2662
|
+
fields: {
|
|
2663
|
+
src: { type: "text", label: "Image URL" },
|
|
2664
|
+
alt: textField("Alt text"),
|
|
2665
|
+
aspect: {
|
|
2666
|
+
type: "radio",
|
|
2667
|
+
label: "Aspect",
|
|
2668
|
+
options: [
|
|
2669
|
+
{ label: "16:9", value: "16:9" },
|
|
2670
|
+
{ label: "4:3", value: "4:3" },
|
|
2671
|
+
{ label: "1:1", value: "1:1" },
|
|
2672
|
+
{ label: "21:9", value: "21:9" }
|
|
2673
|
+
]
|
|
2674
|
+
},
|
|
2675
|
+
rounded: {
|
|
2676
|
+
type: "radio",
|
|
2677
|
+
label: "Rounded",
|
|
2678
|
+
options: [
|
|
2679
|
+
{ label: "None", value: "none" },
|
|
2680
|
+
{ label: "M", value: "md" },
|
|
2681
|
+
{ label: "L", value: "lg" },
|
|
2682
|
+
{ label: "Full", value: "full" }
|
|
2683
|
+
]
|
|
2684
|
+
}
|
|
2685
|
+
},
|
|
2686
|
+
defaultProps: { src: "", alt: "Image", aspect: "16:9", rounded: "lg" },
|
|
2687
|
+
render: ({ id, src, alt, aspect, rounded }) => /* @__PURE__ */ jsx(BlockImage, { src, alt, aspect, rounded, testId: blockTestId(id) })
|
|
2688
|
+
};
|
|
2689
|
+
const AvatarBlockConfig = {
|
|
2690
|
+
label: "Avatar",
|
|
2691
|
+
fields: {
|
|
2692
|
+
src: { type: "text", label: "Image URL" },
|
|
2693
|
+
name: textField("Name"),
|
|
2694
|
+
size: {
|
|
2695
|
+
type: "radio",
|
|
2696
|
+
label: "Size",
|
|
2697
|
+
options: [
|
|
2698
|
+
{ label: "S", value: "sm" },
|
|
2699
|
+
{ label: "M", value: "md" },
|
|
2700
|
+
{ label: "L", value: "lg" },
|
|
2701
|
+
{ label: "XL", value: "xl" }
|
|
2702
|
+
]
|
|
2703
|
+
}
|
|
2704
|
+
},
|
|
2705
|
+
defaultProps: { src: "", name: "Jane Doe", size: "md" },
|
|
2706
|
+
render: ({ id, src, name, size }) => /* @__PURE__ */ jsx(AvatarBlock, { src, name, size, testId: blockTestId(id) })
|
|
2707
|
+
};
|
|
2708
|
+
const NavbarBlock = {
|
|
2709
|
+
label: "Navbar",
|
|
2710
|
+
fields: {
|
|
2711
|
+
brand: textField("Brand"),
|
|
2712
|
+
links: {
|
|
2713
|
+
type: "array",
|
|
2714
|
+
label: "Links",
|
|
2715
|
+
arrayFields: {
|
|
2716
|
+
label: textField("Label"),
|
|
2717
|
+
href: { type: "text", label: "Link" }
|
|
2718
|
+
},
|
|
2719
|
+
defaultItemProps: { label: "Link", href: "#" },
|
|
2720
|
+
getItemSummary: (item) => item?.label || "Link"
|
|
2721
|
+
},
|
|
2722
|
+
cta: ctaField("CTA")
|
|
2723
|
+
},
|
|
2724
|
+
defaultProps: {
|
|
2725
|
+
brand: "Acme",
|
|
2726
|
+
links: [
|
|
2727
|
+
{ label: "Product", href: "#" },
|
|
2728
|
+
{ label: "Pricing", href: "#" },
|
|
2729
|
+
{ label: "About", href: "#" },
|
|
2730
|
+
{ label: "Contact", href: "#" }
|
|
2731
|
+
],
|
|
2732
|
+
cta: { label: "Sign up", href: "#" }
|
|
2733
|
+
},
|
|
2734
|
+
render: ({ id, brand, links, cta }) => /* @__PURE__ */ jsx(Navbar, { brand, links, cta, testId: blockTestId(id) })
|
|
2735
|
+
};
|
|
2736
|
+
const FooterBlock = {
|
|
2737
|
+
label: "Footer",
|
|
2738
|
+
fields: {
|
|
2739
|
+
columns: {
|
|
2740
|
+
type: "array",
|
|
2741
|
+
label: "Columns",
|
|
2742
|
+
arrayFields: {
|
|
2743
|
+
heading: textField("Heading"),
|
|
2744
|
+
links: {
|
|
2745
|
+
type: "array",
|
|
2746
|
+
label: "Links",
|
|
2747
|
+
arrayFields: {
|
|
2748
|
+
label: textField("Label"),
|
|
2749
|
+
href: { type: "text", label: "Link" }
|
|
2750
|
+
},
|
|
2751
|
+
defaultItemProps: { label: "Link", href: "#" },
|
|
2752
|
+
getItemSummary: (item) => item?.label || "Link"
|
|
2753
|
+
}
|
|
2754
|
+
},
|
|
2755
|
+
defaultItemProps: { heading: "Column", links: [{ label: "Link", href: "#" }] },
|
|
2756
|
+
getItemSummary: (item) => item?.heading || "Column"
|
|
2757
|
+
},
|
|
2758
|
+
copyright: { type: "text", label: "Copyright" }
|
|
2759
|
+
},
|
|
2760
|
+
defaultProps: {
|
|
2761
|
+
columns: [
|
|
2762
|
+
{
|
|
2763
|
+
heading: "Product",
|
|
2764
|
+
links: [
|
|
2765
|
+
{ label: "Features", href: "#" },
|
|
2766
|
+
{ label: "Pricing", href: "#" }
|
|
2767
|
+
]
|
|
2768
|
+
},
|
|
2769
|
+
{
|
|
2770
|
+
heading: "Company",
|
|
2771
|
+
links: [
|
|
2772
|
+
{ label: "About", href: "#" },
|
|
2773
|
+
{ label: "Careers", href: "#" }
|
|
2774
|
+
]
|
|
2775
|
+
},
|
|
2776
|
+
{
|
|
2777
|
+
heading: "Resources",
|
|
2778
|
+
links: [
|
|
2779
|
+
{ label: "Blog", href: "#" },
|
|
2780
|
+
{ label: "Docs", href: "#" }
|
|
2781
|
+
]
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
heading: "Legal",
|
|
2785
|
+
links: [
|
|
2786
|
+
{ label: "Privacy", href: "#" },
|
|
2787
|
+
{ label: "Terms", href: "#" }
|
|
2788
|
+
]
|
|
2789
|
+
}
|
|
2790
|
+
],
|
|
2791
|
+
copyright: "\xA9 2026 Acme, Inc. All rights reserved."
|
|
2792
|
+
},
|
|
2793
|
+
render: ({ id, columns, copyright }) => /* @__PURE__ */ jsx(Footer, { columns, copyright, testId: blockTestId(id) })
|
|
2794
|
+
};
|
|
2795
|
+
const ListBlockConfig = {
|
|
2796
|
+
label: "List",
|
|
2797
|
+
fields: {
|
|
2798
|
+
ordered: boolRadio("Ordered"),
|
|
2799
|
+
items: {
|
|
2800
|
+
type: "array",
|
|
2801
|
+
label: "Items",
|
|
2802
|
+
arrayFields: { text: textField("Text") },
|
|
2803
|
+
defaultItemProps: { text: "List item" },
|
|
2804
|
+
getItemSummary: (item) => item?.text || "Item"
|
|
2805
|
+
}
|
|
2806
|
+
},
|
|
2807
|
+
defaultProps: {
|
|
2808
|
+
ordered: false,
|
|
2809
|
+
items: [{ text: "First item" }, { text: "Second item" }, { text: "Third item" }]
|
|
2810
|
+
},
|
|
2811
|
+
render: ({ id, ordered, items }) => /* @__PURE__ */ jsx(ListBlock, { ordered, items, testId: blockTestId(id) })
|
|
2812
|
+
};
|
|
2813
|
+
return {
|
|
2814
|
+
Columns: ColumnsBlock,
|
|
2815
|
+
Spacer: SpacerBlock,
|
|
2816
|
+
Container: ContainerBlock,
|
|
2817
|
+
Hero: HeroBlock,
|
|
2818
|
+
FeatureGrid: FeatureGridBlock,
|
|
2819
|
+
StatGroup: StatGroupBlock,
|
|
2820
|
+
Testimonial: TestimonialBlock,
|
|
2821
|
+
CTABanner: CTABannerBlock,
|
|
2822
|
+
LogoCloud: LogoCloudBlock,
|
|
2823
|
+
FAQ: FAQBlock,
|
|
2824
|
+
RichText: RichTextBlockConfig,
|
|
2825
|
+
Callout: CalloutBlock,
|
|
2826
|
+
Image: ImageBlock,
|
|
2827
|
+
Avatar: AvatarBlockConfig,
|
|
2828
|
+
Navbar: NavbarBlock,
|
|
2829
|
+
Footer: FooterBlock,
|
|
2830
|
+
List: ListBlockConfig
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
function createArbiBlocksConfig(opts = {}) {
|
|
2834
|
+
const kit = createBlockKit(opts.aiFields);
|
|
2835
|
+
const { textField, textareaField, boolRadio, iconField, resolveIcon, blockTestId } = kit;
|
|
2836
|
+
const SectionHeadingBlock = {
|
|
2837
|
+
label: "Section heading",
|
|
2838
|
+
fields: {
|
|
2839
|
+
title: textField("Title"),
|
|
2840
|
+
eyebrow: textField("Eyebrow"),
|
|
2841
|
+
lead: textareaField("Lead", void 0, "lead"),
|
|
2842
|
+
align: {
|
|
2843
|
+
type: "radio",
|
|
2844
|
+
label: "Align",
|
|
2845
|
+
options: [
|
|
2846
|
+
{ label: "Left", value: "left" },
|
|
2847
|
+
{ label: "Center", value: "center" }
|
|
2848
|
+
]
|
|
2849
|
+
},
|
|
2850
|
+
invert: boolRadio("Invert (on dark)")
|
|
2851
|
+
},
|
|
2852
|
+
defaultProps: { title: "Section title", align: "left", invert: false },
|
|
2853
|
+
render: ({ id, title, eyebrow, lead, align, invert }) => /* @__PURE__ */ jsx("div", { "data-testid": blockTestId(id), children: /* @__PURE__ */ jsx(SectionHeading, { title, eyebrow, lead, align, invert }) })
|
|
2854
|
+
};
|
|
2855
|
+
const PageHeaderBlock = {
|
|
2856
|
+
label: "Page header",
|
|
2857
|
+
fields: {
|
|
2858
|
+
title: textField("Title"),
|
|
2859
|
+
eyebrow: textField("Eyebrow"),
|
|
2860
|
+
description: textareaField("Description", void 0, "description")
|
|
2861
|
+
},
|
|
2862
|
+
defaultProps: { title: "Page title" },
|
|
2863
|
+
// `actions` (ReactNode) is deferred for v1 — see module notes.
|
|
2864
|
+
render: ({ id, title, eyebrow, description }) => /* @__PURE__ */ jsx(
|
|
2865
|
+
PageHeader,
|
|
2866
|
+
{
|
|
2867
|
+
title,
|
|
2868
|
+
eyebrow,
|
|
2869
|
+
description,
|
|
2870
|
+
testId: blockTestId(id)
|
|
2871
|
+
}
|
|
2872
|
+
)
|
|
2873
|
+
};
|
|
2874
|
+
const EmptyStateBlock = {
|
|
2875
|
+
label: "Empty state",
|
|
2876
|
+
fields: {
|
|
2877
|
+
title: textField("Title"),
|
|
2878
|
+
description: textareaField("Description", void 0, "description"),
|
|
2879
|
+
icon: iconField()
|
|
2880
|
+
},
|
|
2881
|
+
defaultProps: { title: "Nothing here yet" },
|
|
2882
|
+
// `action` (ReactNode) is deferred for v1 — see module notes.
|
|
2883
|
+
render: ({ id, title, description, icon }) => /* @__PURE__ */ jsx(
|
|
2884
|
+
EmptyState,
|
|
2885
|
+
{
|
|
2886
|
+
title,
|
|
2887
|
+
description,
|
|
2888
|
+
icon: resolveIcon(icon),
|
|
2889
|
+
testId: blockTestId(id)
|
|
2890
|
+
}
|
|
2891
|
+
)
|
|
2892
|
+
};
|
|
2893
|
+
const MetricCardBlock = {
|
|
2894
|
+
label: "Metric card",
|
|
2895
|
+
fields: {
|
|
2896
|
+
label: { type: "text", label: "Label" },
|
|
2897
|
+
value: { type: "text", label: "Value" },
|
|
2898
|
+
hint: { type: "text", label: "Hint" },
|
|
2899
|
+
accent: {
|
|
2900
|
+
type: "radio",
|
|
2901
|
+
label: "Accent",
|
|
2902
|
+
options: [
|
|
2903
|
+
{ label: "Ink", value: "ink" },
|
|
2904
|
+
{ label: "Emerald", value: "emerald" },
|
|
2905
|
+
{ label: "Brass", value: "brass" }
|
|
2906
|
+
]
|
|
2907
|
+
},
|
|
2908
|
+
delta: {
|
|
2909
|
+
type: "object",
|
|
2910
|
+
label: "Delta",
|
|
2911
|
+
objectFields: {
|
|
2912
|
+
value: { type: "text", label: "Value" },
|
|
2913
|
+
direction: {
|
|
2914
|
+
type: "radio",
|
|
2915
|
+
label: "Direction",
|
|
2916
|
+
options: [
|
|
2917
|
+
{ label: "Up", value: "up" },
|
|
2918
|
+
{ label: "Down", value: "down" }
|
|
2919
|
+
]
|
|
2920
|
+
},
|
|
2921
|
+
good: boolRadio("Good")
|
|
2922
|
+
}
|
|
2923
|
+
},
|
|
2924
|
+
icon: iconField()
|
|
2925
|
+
},
|
|
2926
|
+
defaultProps: { label: "Metric", value: "0", accent: "ink" },
|
|
2927
|
+
render: ({ id, label, value, hint, accent, delta, icon }) => /* @__PURE__ */ jsx(
|
|
2928
|
+
MetricCard,
|
|
2929
|
+
{
|
|
2930
|
+
label,
|
|
2931
|
+
value,
|
|
2932
|
+
hint,
|
|
2933
|
+
accent,
|
|
2934
|
+
delta,
|
|
2935
|
+
icon: resolveIcon(icon),
|
|
2936
|
+
testId: blockTestId(id)
|
|
2937
|
+
}
|
|
2938
|
+
)
|
|
2939
|
+
};
|
|
2940
|
+
const DataTableBlockConfig = {
|
|
2941
|
+
label: "Data table",
|
|
2942
|
+
fields: {
|
|
2943
|
+
caption: textField("Caption"),
|
|
2944
|
+
columns: {
|
|
2945
|
+
type: "array",
|
|
2946
|
+
label: "Columns",
|
|
2947
|
+
arrayFields: {
|
|
2948
|
+
header: { type: "text", label: "Header" },
|
|
2949
|
+
field: { type: "text", label: "Field key" },
|
|
2950
|
+
align: {
|
|
2951
|
+
type: "radio",
|
|
2952
|
+
label: "Align",
|
|
2953
|
+
options: [
|
|
2954
|
+
{ label: "Left", value: "left" },
|
|
2955
|
+
{ label: "Right", value: "right" },
|
|
2956
|
+
{ label: "Center", value: "center" }
|
|
2957
|
+
]
|
|
2958
|
+
}
|
|
2959
|
+
},
|
|
2960
|
+
defaultItemProps: { header: "Column", field: "field", align: "left" },
|
|
2961
|
+
getItemSummary: (item) => item?.header || "Column"
|
|
2962
|
+
},
|
|
2963
|
+
rows: {
|
|
2964
|
+
type: "array",
|
|
2965
|
+
label: "Rows",
|
|
2966
|
+
arrayFields: {
|
|
2967
|
+
cells: {
|
|
2968
|
+
type: "array",
|
|
2969
|
+
label: "Cells",
|
|
2970
|
+
arrayFields: {
|
|
2971
|
+
value: { type: "text", label: "Value" }
|
|
2972
|
+
},
|
|
2973
|
+
defaultItemProps: { value: "" },
|
|
2974
|
+
getItemSummary: (item) => item?.value || "Cell"
|
|
2975
|
+
}
|
|
2976
|
+
},
|
|
2977
|
+
defaultItemProps: { cells: [{ value: "" }, { value: "" }] },
|
|
2978
|
+
getItemSummary: (_item, i) => `Row ${(i ?? 0) + 1}`
|
|
2979
|
+
},
|
|
2980
|
+
emptyText: textField("Empty text")
|
|
2981
|
+
},
|
|
2982
|
+
defaultProps: {
|
|
2983
|
+
columns: [
|
|
2984
|
+
{ header: "Matter", field: "matter", align: "left" },
|
|
2985
|
+
{ header: "Status", field: "status", align: "left" }
|
|
2986
|
+
],
|
|
2987
|
+
rows: [
|
|
2988
|
+
{ cells: [{ value: "Acme v. Globex" }, { value: "Active" }] },
|
|
2989
|
+
{ cells: [{ value: "Initech Merger" }, { value: "Closed" }] }
|
|
2990
|
+
],
|
|
2991
|
+
emptyText: "No rows yet"
|
|
2992
|
+
},
|
|
2993
|
+
render: ({ id, columns, rows, caption, emptyText }) => {
|
|
2994
|
+
const cols = columns ?? [];
|
|
2995
|
+
const rawRows = rows ?? [];
|
|
2996
|
+
const recordRows = rawRows.map((row) => {
|
|
2997
|
+
const record = {};
|
|
2998
|
+
cols.forEach((col, index) => {
|
|
2999
|
+
record[col.field || `col-${index}`] = row.cells?.[index]?.value ?? "";
|
|
3000
|
+
});
|
|
3001
|
+
return record;
|
|
3002
|
+
});
|
|
3003
|
+
const normalisedCols = cols.map((col, index) => ({
|
|
3004
|
+
...col,
|
|
3005
|
+
field: col.field || `col-${index}`
|
|
3006
|
+
}));
|
|
3007
|
+
return /* @__PURE__ */ jsx(
|
|
3008
|
+
DataTableBlock,
|
|
3009
|
+
{
|
|
3010
|
+
columns: normalisedCols,
|
|
3011
|
+
rows: recordRows,
|
|
3012
|
+
caption,
|
|
3013
|
+
emptyText,
|
|
3014
|
+
testId: blockTestId(id)
|
|
3015
|
+
}
|
|
3016
|
+
);
|
|
3017
|
+
}
|
|
3018
|
+
};
|
|
3019
|
+
const SectionBlock = {
|
|
3020
|
+
label: "Section",
|
|
3021
|
+
fields: {
|
|
3022
|
+
children: { type: "slot" },
|
|
3023
|
+
tone: {
|
|
3024
|
+
type: "radio",
|
|
3025
|
+
label: "Tone",
|
|
3026
|
+
options: [
|
|
3027
|
+
{ label: "Default", value: "default" },
|
|
3028
|
+
{ label: "Muted", value: "muted" },
|
|
3029
|
+
{ label: "Ink", value: "ink" }
|
|
3030
|
+
]
|
|
3031
|
+
}
|
|
3032
|
+
},
|
|
3033
|
+
defaultProps: { tone: "default", children: [] },
|
|
3034
|
+
render: ({ id, tone, children: Children }) => /* @__PURE__ */ jsx("div", { "data-testid": blockTestId(id), children: /* @__PURE__ */ jsx(Section, { tone, children: /* @__PURE__ */ jsx(Children, {}) }) })
|
|
3035
|
+
};
|
|
3036
|
+
const CardBlock = {
|
|
3037
|
+
label: "Card",
|
|
3038
|
+
fields: {
|
|
3039
|
+
variant: {
|
|
3040
|
+
type: "select",
|
|
3041
|
+
label: "Variant",
|
|
3042
|
+
options: [
|
|
3043
|
+
{ label: "Flat", value: "flat" },
|
|
3044
|
+
{ label: "Outline", value: "outline" },
|
|
3045
|
+
{ label: "Elevated", value: "elevated" }
|
|
3046
|
+
]
|
|
3047
|
+
},
|
|
3048
|
+
children: { type: "slot" }
|
|
3049
|
+
},
|
|
3050
|
+
defaultProps: { variant: "flat", children: [] },
|
|
3051
|
+
render: ({ id, variant, children: Children }) => /* @__PURE__ */ jsx(Card, { variant, className: "p-5", "data-testid": blockTestId(id), children: /* @__PURE__ */ jsx(Children, {}) })
|
|
3052
|
+
};
|
|
3053
|
+
const ButtonBlock = {
|
|
3054
|
+
label: "Button",
|
|
3055
|
+
fields: {
|
|
3056
|
+
children: { type: "text", label: "Label" },
|
|
3057
|
+
variant: {
|
|
3058
|
+
type: "select",
|
|
3059
|
+
label: "Variant",
|
|
3060
|
+
options: [
|
|
3061
|
+
{ label: "Default", value: "default" },
|
|
3062
|
+
{ label: "Accent", value: "accent" },
|
|
3063
|
+
{ label: "Destructive", value: "destructive" },
|
|
3064
|
+
{ label: "Outline", value: "outline" },
|
|
3065
|
+
{ label: "Secondary", value: "secondary" },
|
|
3066
|
+
{ label: "Ghost", value: "ghost" },
|
|
3067
|
+
{ label: "Link", value: "link" }
|
|
3068
|
+
]
|
|
3069
|
+
},
|
|
3070
|
+
size: {
|
|
3071
|
+
type: "select",
|
|
3072
|
+
label: "Size",
|
|
3073
|
+
options: [
|
|
3074
|
+
{ label: "Default", value: "default" },
|
|
3075
|
+
{ label: "Small", value: "sm" },
|
|
3076
|
+
{ label: "Large", value: "lg" },
|
|
3077
|
+
{ label: "Icon", value: "icon" }
|
|
3078
|
+
]
|
|
3079
|
+
}
|
|
3080
|
+
},
|
|
3081
|
+
defaultProps: { variant: "default", size: "default", children: "Button" },
|
|
3082
|
+
render: ({ id, children, variant, size }) => /* @__PURE__ */ jsx(Button, { variant, size, "data-testid": blockTestId(id), children })
|
|
3083
|
+
};
|
|
3084
|
+
const BadgeBlock = {
|
|
3085
|
+
label: "Badge",
|
|
3086
|
+
fields: {
|
|
3087
|
+
text: { type: "text", label: "Text" },
|
|
3088
|
+
variant: {
|
|
3089
|
+
type: "select",
|
|
3090
|
+
label: "Variant",
|
|
3091
|
+
options: [
|
|
3092
|
+
{ label: "Default", value: "default" },
|
|
3093
|
+
{ label: "Accent", value: "accent" },
|
|
3094
|
+
{ label: "Secondary", value: "secondary" },
|
|
3095
|
+
{ label: "Destructive", value: "destructive" },
|
|
3096
|
+
{ label: "Outline", value: "outline" }
|
|
3097
|
+
]
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
defaultProps: { text: "Badge", variant: "default" },
|
|
3101
|
+
render: ({ id, text, variant }) => /* @__PURE__ */ jsx(Badge, { variant, "data-testid": blockTestId(id), children: text })
|
|
3102
|
+
};
|
|
3103
|
+
const SeparatorBlock = {
|
|
3104
|
+
label: "Separator",
|
|
3105
|
+
fields: {},
|
|
3106
|
+
defaultProps: {},
|
|
3107
|
+
render: ({ id }) => /* @__PURE__ */ jsx(Separator, { "data-testid": blockTestId(id) })
|
|
3108
|
+
};
|
|
3109
|
+
const website = createWebsiteBlocks(kit);
|
|
3110
|
+
const components = {
|
|
3111
|
+
SectionHeading: SectionHeadingBlock,
|
|
3112
|
+
PageHeader: PageHeaderBlock,
|
|
3113
|
+
EmptyState: EmptyStateBlock,
|
|
3114
|
+
MetricCard: MetricCardBlock,
|
|
3115
|
+
DataTable: DataTableBlockConfig,
|
|
3116
|
+
Section: SectionBlock,
|
|
3117
|
+
Card: CardBlock,
|
|
3118
|
+
Button: ButtonBlock,
|
|
3119
|
+
Badge: BadgeBlock,
|
|
3120
|
+
Separator: SeparatorBlock,
|
|
3121
|
+
...website
|
|
3122
|
+
};
|
|
3123
|
+
const config = {
|
|
3124
|
+
categories: {
|
|
3125
|
+
Layout: {
|
|
3126
|
+
title: "Layout",
|
|
3127
|
+
components: ["Section", "Container", "Columns", "Card", "Spacer"]
|
|
3128
|
+
},
|
|
3129
|
+
Marketing: {
|
|
3130
|
+
title: "Marketing",
|
|
3131
|
+
components: [
|
|
3132
|
+
"Hero",
|
|
3133
|
+
"FeatureGrid",
|
|
3134
|
+
"StatGroup",
|
|
3135
|
+
"Testimonial",
|
|
3136
|
+
"CTABanner",
|
|
3137
|
+
"LogoCloud",
|
|
3138
|
+
"FAQ"
|
|
3139
|
+
]
|
|
3140
|
+
},
|
|
3141
|
+
Content: {
|
|
3142
|
+
title: "Content",
|
|
3143
|
+
components: ["SectionHeading", "PageHeader", "EmptyState", "RichText", "Callout", "List"]
|
|
3144
|
+
},
|
|
3145
|
+
Media: { title: "Media", components: ["Image", "Avatar"] },
|
|
3146
|
+
Navigation: { title: "Navigation", components: ["Navbar", "Footer"] },
|
|
3147
|
+
Data: { title: "Data", components: ["MetricCard", "DataTable"] },
|
|
3148
|
+
Primitives: { title: "Primitives", components: ["Button", "Badge", "Separator"] }
|
|
3149
|
+
},
|
|
3150
|
+
components,
|
|
3151
|
+
root: {
|
|
3152
|
+
render: ({ children }) => /* @__PURE__ */ jsx(
|
|
3153
|
+
"div",
|
|
3154
|
+
{
|
|
3155
|
+
className: "bg-background text-foreground font-sans",
|
|
3156
|
+
"data-testid": "arbi-blocks-page-root",
|
|
3157
|
+
children
|
|
3158
|
+
}
|
|
3159
|
+
)
|
|
3160
|
+
}
|
|
3161
|
+
};
|
|
3162
|
+
return config;
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
// src/builder/modules/registry.ts
|
|
3166
|
+
function createModuleRegistry(appNav) {
|
|
3167
|
+
const DEFAULT_MODULES = appNav.flatMap(
|
|
3168
|
+
(section, sIdx) => section.items.map((item, iIdx) => ({
|
|
3169
|
+
id: item.id,
|
|
3170
|
+
label: item.label,
|
|
3171
|
+
section: section.heading,
|
|
3172
|
+
icon: item.icon,
|
|
3173
|
+
to: item.to,
|
|
3174
|
+
arbi: item.arbi ?? false,
|
|
3175
|
+
end: item.end ?? false,
|
|
3176
|
+
// Global order that preserves section grouping then in-section order.
|
|
3177
|
+
defaultOrder: sIdx * 100 + iIdx
|
|
3178
|
+
}))
|
|
3179
|
+
);
|
|
3180
|
+
const SECTION_ORDER = appNav.map((s) => s.heading);
|
|
3181
|
+
const toNavItem = (m) => ({
|
|
3182
|
+
to: m.to,
|
|
3183
|
+
label: m.label,
|
|
3184
|
+
icon: m.icon,
|
|
3185
|
+
id: m.id,
|
|
3186
|
+
arbi: m.arbi || void 0,
|
|
3187
|
+
end: m.end || void 0
|
|
3188
|
+
});
|
|
3189
|
+
return { DEFAULT_MODULES, SECTION_ORDER, toNavItem };
|
|
3190
|
+
}
|
|
3191
|
+
function createModuleStore({
|
|
3192
|
+
registry,
|
|
3193
|
+
storageKey
|
|
3194
|
+
}) {
|
|
3195
|
+
const { DEFAULT_MODULES, SECTION_ORDER, toNavItem } = registry;
|
|
3196
|
+
function isEnabled(overrides, id2) {
|
|
3197
|
+
return overrides[id2]?.enabled ?? true;
|
|
3198
|
+
}
|
|
3199
|
+
const id = (m) => m.id;
|
|
3200
|
+
function orderOf(overrides, m) {
|
|
3201
|
+
return overrides[id(m)]?.order ?? m.defaultOrder;
|
|
3202
|
+
}
|
|
3203
|
+
const useModuleStore = create()(
|
|
3204
|
+
persist(
|
|
3205
|
+
(set, get) => ({
|
|
3206
|
+
overrides: {},
|
|
3207
|
+
toggle: (moduleId) => {
|
|
3208
|
+
const m = DEFAULT_MODULES.find((x) => x.id === moduleId);
|
|
3209
|
+
if (!m) return;
|
|
3210
|
+
const cur = get().overrides;
|
|
3211
|
+
const prev = cur[moduleId] ?? { enabled: true, order: m.defaultOrder };
|
|
3212
|
+
set({
|
|
3213
|
+
overrides: { ...cur, [moduleId]: { ...prev, enabled: !isEnabled(cur, moduleId) } }
|
|
3214
|
+
});
|
|
3215
|
+
},
|
|
3216
|
+
move: (moduleId, dir) => {
|
|
3217
|
+
const cur = get().overrides;
|
|
3218
|
+
const target = DEFAULT_MODULES.find((x) => x.id === moduleId);
|
|
3219
|
+
if (!target) return;
|
|
3220
|
+
const siblings = DEFAULT_MODULES.filter((x) => x.section === target.section).sort(
|
|
3221
|
+
(a2, b2) => orderOf(cur, a2) - orderOf(cur, b2)
|
|
3222
|
+
);
|
|
3223
|
+
const idx = siblings.findIndex((x) => x.id === moduleId);
|
|
3224
|
+
const swapIdx = dir === "up" ? idx - 1 : idx + 1;
|
|
3225
|
+
if (swapIdx < 0 || swapIdx >= siblings.length) return;
|
|
3226
|
+
const a = siblings[idx];
|
|
3227
|
+
const b = siblings[swapIdx];
|
|
3228
|
+
const next = { ...cur };
|
|
3229
|
+
next[a.id] = { enabled: isEnabled(cur, a.id), order: orderOf(cur, b) };
|
|
3230
|
+
next[b.id] = { enabled: isEnabled(cur, b.id), order: orderOf(cur, a) };
|
|
3231
|
+
set({ overrides: next });
|
|
3232
|
+
},
|
|
3233
|
+
reset: () => set({ overrides: {} })
|
|
3234
|
+
}),
|
|
3235
|
+
{ name: storageKey }
|
|
3236
|
+
)
|
|
3237
|
+
);
|
|
3238
|
+
function useResolvedModules() {
|
|
3239
|
+
const overrides = useModuleStore((s) => s.overrides);
|
|
3240
|
+
return useMemo(() => {
|
|
3241
|
+
return SECTION_ORDER.map((section) => ({
|
|
3242
|
+
section,
|
|
3243
|
+
modules: DEFAULT_MODULES.filter((m) => m.section === section).map((m) => ({ ...m, enabled: isEnabled(overrides, m.id), order: orderOf(overrides, m) })).sort((a, b) => a.order - b.order)
|
|
3244
|
+
}));
|
|
3245
|
+
}, [overrides]);
|
|
3246
|
+
}
|
|
3247
|
+
function useEnabledNav() {
|
|
3248
|
+
const grouped = useResolvedModules();
|
|
3249
|
+
return useMemo(
|
|
3250
|
+
() => grouped.map((g) => ({
|
|
3251
|
+
heading: g.section,
|
|
3252
|
+
items: g.modules.filter((m) => m.enabled).map(toNavItem)
|
|
3253
|
+
})).filter((s) => s.items.length > 0),
|
|
3254
|
+
[grouped]
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
return { useModuleStore, useResolvedModules, useEnabledNav };
|
|
3258
|
+
}
|
|
3259
|
+
var toggle = (list, id) => list.includes(id) ? list.filter((x) => x !== id) : [...list, id];
|
|
3260
|
+
function createAgentSelectionStore({
|
|
3261
|
+
storageKey
|
|
3262
|
+
}) {
|
|
3263
|
+
return create()(
|
|
3264
|
+
persist(
|
|
3265
|
+
(set) => ({
|
|
3266
|
+
configIds: [],
|
|
3267
|
+
agentIds: [],
|
|
3268
|
+
toggleConfig: (id) => set((s) => ({ configIds: toggle(s.configIds, id) })),
|
|
3269
|
+
toggleAgent: (id) => set((s) => ({ agentIds: toggle(s.agentIds, id) })),
|
|
3270
|
+
clear: () => set({ configIds: [], agentIds: [] })
|
|
3271
|
+
}),
|
|
3272
|
+
{ name: storageKey }
|
|
3273
|
+
)
|
|
3274
|
+
);
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// src/builder/export.ts
|
|
3278
|
+
function createVerticalExport(cfg) {
|
|
3279
|
+
const { vertical, themeStore, moduleStore, agentStore, registry } = cfg;
|
|
3280
|
+
const { DEFAULT_MODULES, SECTION_ORDER } = registry;
|
|
3281
|
+
function buildVerticalConfig() {
|
|
3282
|
+
const { colors, fonts, radius } = themeStore.getState();
|
|
3283
|
+
const { overrides } = moduleStore.useModuleStore.getState();
|
|
3284
|
+
const { configIds, agentIds } = agentStore.getState();
|
|
3285
|
+
const modules = DEFAULT_MODULES.map((m) => ({
|
|
3286
|
+
id: m.id,
|
|
3287
|
+
label: m.label,
|
|
3288
|
+
section: m.section,
|
|
3289
|
+
to: m.to,
|
|
3290
|
+
arbi: m.arbi,
|
|
3291
|
+
enabled: overrides[m.id]?.enabled ?? true,
|
|
3292
|
+
order: overrides[m.id]?.order ?? m.defaultOrder
|
|
3293
|
+
})).sort(
|
|
3294
|
+
(a, b) => SECTION_ORDER.indexOf(a.section) - SECTION_ORDER.indexOf(b.section) || a.order - b.order
|
|
3295
|
+
);
|
|
3296
|
+
return {
|
|
3297
|
+
version: 1,
|
|
3298
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3299
|
+
vertical,
|
|
3300
|
+
theme: { colors, fonts, radius },
|
|
3301
|
+
modules,
|
|
3302
|
+
agents: { configIds, agentIds }
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
function serialize(config = buildVerticalConfig()) {
|
|
3306
|
+
return JSON.stringify(config, null, 2);
|
|
3307
|
+
}
|
|
3308
|
+
function download(config = buildVerticalConfig()) {
|
|
3309
|
+
const blob = new Blob([serialize(config)], { type: "application/json" });
|
|
3310
|
+
const url = URL.createObjectURL(blob);
|
|
3311
|
+
const a = document.createElement("a");
|
|
3312
|
+
a.href = url;
|
|
3313
|
+
a.download = `${config.vertical.key}-vertical.json`;
|
|
3314
|
+
document.body.appendChild(a);
|
|
3315
|
+
a.click();
|
|
3316
|
+
a.remove();
|
|
3317
|
+
URL.revokeObjectURL(url);
|
|
3318
|
+
}
|
|
3319
|
+
return { buildVerticalConfig, serialize, download };
|
|
3320
|
+
}
|
|
3321
|
+
function ModuleManager({ store, testIdPrefix = "builder" }) {
|
|
3322
|
+
const grouped = store.useResolvedModules();
|
|
3323
|
+
const toggle2 = store.useModuleStore((s) => s.toggle);
|
|
3324
|
+
const move = store.useModuleStore((s) => s.move);
|
|
3325
|
+
const reset = store.useModuleStore((s) => s.reset);
|
|
3326
|
+
const enabledCount = grouped.reduce((n, g) => n + g.modules.filter((m) => m.enabled).length, 0);
|
|
3327
|
+
const total = grouped.reduce((n, g) => n + g.modules.length, 0);
|
|
3328
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", "data-testid": `${testIdPrefix}-modules`, children: [
|
|
3329
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
3330
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", "data-testid": `${testIdPrefix}-modules-count`, children: [
|
|
3331
|
+
enabledCount,
|
|
3332
|
+
" of ",
|
|
3333
|
+
total,
|
|
3334
|
+
" modules enabled"
|
|
3335
|
+
] }),
|
|
3336
|
+
/* @__PURE__ */ jsxs(
|
|
3337
|
+
Button,
|
|
3338
|
+
{
|
|
3339
|
+
size: "sm",
|
|
3340
|
+
variant: "ghost",
|
|
3341
|
+
onClick: reset,
|
|
3342
|
+
"data-testid": `${testIdPrefix}-modules-reset`,
|
|
3343
|
+
children: [
|
|
3344
|
+
/* @__PURE__ */ jsx(RotateCcw, { className: "size-3.5" }),
|
|
3345
|
+
" Reset"
|
|
3346
|
+
]
|
|
3347
|
+
}
|
|
3348
|
+
)
|
|
3349
|
+
] }),
|
|
3350
|
+
grouped.map((group) => /* @__PURE__ */ jsxs(
|
|
3351
|
+
Card,
|
|
3352
|
+
{
|
|
3353
|
+
variant: "elevated",
|
|
3354
|
+
className: "rounded-xl",
|
|
3355
|
+
"data-testid": tid(`${testIdPrefix}-modules-section`, group.section),
|
|
3356
|
+
children: [
|
|
3357
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: group.section }) }),
|
|
3358
|
+
/* @__PURE__ */ jsx(CardContent, { className: "space-y-1.5", children: group.modules.map((m, idx) => {
|
|
3359
|
+
const Icon = m.icon;
|
|
3360
|
+
return /* @__PURE__ */ jsxs(
|
|
3361
|
+
"div",
|
|
3362
|
+
{
|
|
3363
|
+
className: "flex items-center gap-3 rounded-md border border-border/60 bg-card px-3 py-2",
|
|
3364
|
+
"data-testid": tid(`${testIdPrefix}-module`, m.id),
|
|
3365
|
+
children: [
|
|
3366
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
3367
|
+
/* @__PURE__ */ jsx(
|
|
3368
|
+
Button,
|
|
3369
|
+
{
|
|
3370
|
+
size: "icon",
|
|
3371
|
+
variant: "ghost",
|
|
3372
|
+
className: "size-6",
|
|
3373
|
+
disabled: idx === 0,
|
|
3374
|
+
onClick: () => move(m.id, "up"),
|
|
3375
|
+
"aria-label": `Move ${m.label} up`,
|
|
3376
|
+
"data-testid": tid(`${testIdPrefix}-module-up`, m.id),
|
|
3377
|
+
children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-3.5" })
|
|
3378
|
+
}
|
|
3379
|
+
),
|
|
3380
|
+
/* @__PURE__ */ jsx(
|
|
3381
|
+
Button,
|
|
3382
|
+
{
|
|
3383
|
+
size: "icon",
|
|
3384
|
+
variant: "ghost",
|
|
3385
|
+
className: "size-6",
|
|
3386
|
+
disabled: idx === group.modules.length - 1,
|
|
3387
|
+
onClick: () => move(m.id, "down"),
|
|
3388
|
+
"aria-label": `Move ${m.label} down`,
|
|
3389
|
+
"data-testid": tid(`${testIdPrefix}-module-down`, m.id),
|
|
3390
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5" })
|
|
3391
|
+
}
|
|
3392
|
+
)
|
|
3393
|
+
] }),
|
|
3394
|
+
/* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground" }),
|
|
3395
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
3396
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-ink", children: m.label }),
|
|
3397
|
+
/* @__PURE__ */ jsx("p", { className: "truncate font-mono text-[11px] text-muted-foreground", children: m.to })
|
|
3398
|
+
] }),
|
|
3399
|
+
m.arbi && /* @__PURE__ */ jsxs(Badge, { variant: "accent", className: "text-[10px]", children: [
|
|
3400
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "size-3" }),
|
|
3401
|
+
" ARBI"
|
|
3402
|
+
] }),
|
|
3403
|
+
/* @__PURE__ */ jsx(
|
|
3404
|
+
Switch,
|
|
3405
|
+
{
|
|
3406
|
+
checked: m.enabled,
|
|
3407
|
+
onCheckedChange: () => toggle2(m.id),
|
|
3408
|
+
"aria-label": `Enable ${m.label}`,
|
|
3409
|
+
"data-testid": tid(`${testIdPrefix}-module-toggle`, m.id)
|
|
3410
|
+
}
|
|
3411
|
+
)
|
|
3412
|
+
]
|
|
3413
|
+
},
|
|
3414
|
+
m.id
|
|
3415
|
+
);
|
|
3416
|
+
}) })
|
|
3417
|
+
]
|
|
3418
|
+
},
|
|
3419
|
+
group.section
|
|
3420
|
+
))
|
|
3421
|
+
] });
|
|
3422
|
+
}
|
|
3423
|
+
var TIER_VARIANT = {
|
|
3424
|
+
Premium: "default",
|
|
3425
|
+
Wise: "accent",
|
|
3426
|
+
Fast: "secondary",
|
|
3427
|
+
auto: "secondary"
|
|
3428
|
+
};
|
|
3429
|
+
function ConfigModelBadge({ configId, testIdPrefix }) {
|
|
3430
|
+
const arbi = useArbi();
|
|
3431
|
+
const { data, isLoading, isError } = useQuery({
|
|
3432
|
+
queryKey: ["builder", "config-model", configId],
|
|
3433
|
+
queryFn: async () => (await arbi.agentConfig.get(configId)).Agents.AGENT_MODEL_NAME,
|
|
3434
|
+
staleTime: 6e4
|
|
3435
|
+
});
|
|
3436
|
+
const tier = isLoading ? "\u2026" : isError || !data ? "\u2014" : data;
|
|
3437
|
+
return /* @__PURE__ */ jsxs(
|
|
3438
|
+
Badge,
|
|
3439
|
+
{
|
|
3440
|
+
variant: TIER_VARIANT[tier] ?? "secondary",
|
|
3441
|
+
"data-testid": tid(`${testIdPrefix}-agents-config-tier`, configId),
|
|
3442
|
+
children: [
|
|
3443
|
+
/* @__PURE__ */ jsx(Cpu, { className: "size-3" }),
|
|
3444
|
+
" ",
|
|
3445
|
+
tier
|
|
3446
|
+
]
|
|
3447
|
+
}
|
|
3448
|
+
);
|
|
3449
|
+
}
|
|
3450
|
+
function AgentsPanel({ store, testIdPrefix = "builder" }) {
|
|
3451
|
+
const { data: configs, isLoading: configsLoading } = useConfigs();
|
|
3452
|
+
const { data: agents = [], isLoading: agentsLoading } = useAgents();
|
|
3453
|
+
const configIds = store((s) => s.configIds);
|
|
3454
|
+
const agentIds = store((s) => s.agentIds);
|
|
3455
|
+
const toggleConfig = store((s) => s.toggleConfig);
|
|
3456
|
+
const toggleAgent = store((s) => s.toggleAgent);
|
|
3457
|
+
const versions = configs?.versions ?? [];
|
|
3458
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", "data-testid": `${testIdPrefix}-agents`, children: [
|
|
3459
|
+
/* @__PURE__ */ jsxs(
|
|
3460
|
+
Card,
|
|
3461
|
+
{
|
|
3462
|
+
variant: "elevated",
|
|
3463
|
+
className: "rounded-xl",
|
|
3464
|
+
"data-testid": `${testIdPrefix}-agents-configs`,
|
|
3465
|
+
children: [
|
|
3466
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [
|
|
3467
|
+
/* @__PURE__ */ jsx(BrainCircuit, { className: "size-4 text-emerald" }),
|
|
3468
|
+
" Behaviour configurations",
|
|
3469
|
+
/* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "ml-1", children: versions.length })
|
|
3470
|
+
] }) }),
|
|
3471
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "space-y-1.5", children: [
|
|
3472
|
+
configsLoading && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Loading configurations\u2026" }),
|
|
3473
|
+
!configsLoading && versions.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "No configurations found on this workspace." }),
|
|
3474
|
+
versions.map((v) => /* @__PURE__ */ jsxs(
|
|
3475
|
+
"label",
|
|
3476
|
+
{
|
|
3477
|
+
className: "flex cursor-pointer items-center gap-3 rounded-md border border-border/60 bg-card px-3 py-2",
|
|
3478
|
+
"data-testid": tid(`${testIdPrefix}-agents-config`, v.external_id),
|
|
3479
|
+
children: [
|
|
3480
|
+
/* @__PURE__ */ jsx(
|
|
3481
|
+
Checkbox,
|
|
3482
|
+
{
|
|
3483
|
+
checked: configIds.includes(v.external_id),
|
|
3484
|
+
onCheckedChange: () => toggleConfig(v.external_id),
|
|
3485
|
+
"data-testid": tid(`${testIdPrefix}-agents-config-toggle`, v.external_id)
|
|
3486
|
+
}
|
|
3487
|
+
),
|
|
3488
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
3489
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-ink", children: v.title ?? "Untitled behaviour" }),
|
|
3490
|
+
/* @__PURE__ */ jsx("p", { className: "truncate font-mono text-[11px] text-muted-foreground", children: v.external_id })
|
|
3491
|
+
] }),
|
|
3492
|
+
/* @__PURE__ */ jsx(ConfigModelBadge, { configId: v.external_id, testIdPrefix })
|
|
3493
|
+
]
|
|
3494
|
+
},
|
|
3495
|
+
v.external_id
|
|
3496
|
+
))
|
|
3497
|
+
] })
|
|
3498
|
+
]
|
|
3499
|
+
}
|
|
3500
|
+
),
|
|
3501
|
+
/* @__PURE__ */ jsxs(Card, { variant: "elevated", className: "rounded-xl", "data-testid": `${testIdPrefix}-agents-agents`, children: [
|
|
3502
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [
|
|
3503
|
+
/* @__PURE__ */ jsx(Bot, { className: "size-4 text-emerald" }),
|
|
3504
|
+
" Agents",
|
|
3505
|
+
/* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "ml-1", children: agents.length })
|
|
3506
|
+
] }) }),
|
|
3507
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "space-y-1.5", children: [
|
|
3508
|
+
agentsLoading && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Loading agents\u2026" }),
|
|
3509
|
+
!agentsLoading && agents.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "No agents provisioned." }),
|
|
3510
|
+
agents.map((a) => /* @__PURE__ */ jsxs(
|
|
3511
|
+
"label",
|
|
3512
|
+
{
|
|
3513
|
+
className: "flex cursor-pointer items-center gap-3 rounded-md border border-border/60 bg-card px-3 py-2",
|
|
3514
|
+
"data-testid": tid(`${testIdPrefix}-agents-agent`, a.external_id),
|
|
3515
|
+
children: [
|
|
3516
|
+
/* @__PURE__ */ jsx(
|
|
3517
|
+
Checkbox,
|
|
3518
|
+
{
|
|
3519
|
+
checked: agentIds.includes(a.external_id),
|
|
3520
|
+
onCheckedChange: () => toggleAgent(a.external_id),
|
|
3521
|
+
"data-testid": tid(`${testIdPrefix}-agents-agent-toggle`, a.external_id)
|
|
3522
|
+
}
|
|
3523
|
+
),
|
|
3524
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
3525
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-ink", children: (a.given_name ?? "Agent").replace(/-/g, " ") }),
|
|
3526
|
+
/* @__PURE__ */ jsx("p", { className: "truncate font-mono text-[11px] text-muted-foreground", children: a.external_id })
|
|
3527
|
+
] })
|
|
3528
|
+
]
|
|
3529
|
+
},
|
|
3530
|
+
a.external_id
|
|
3531
|
+
))
|
|
3532
|
+
] })
|
|
3533
|
+
] })
|
|
3534
|
+
] });
|
|
3535
|
+
}
|
|
3536
|
+
function ExportPanel({
|
|
3537
|
+
themeBundle,
|
|
3538
|
+
moduleStore,
|
|
3539
|
+
agentStore,
|
|
3540
|
+
exporter,
|
|
3541
|
+
testIdPrefix
|
|
3542
|
+
}) {
|
|
3543
|
+
const theme = themeBundle.useStore();
|
|
3544
|
+
const modules = moduleStore.useModuleStore();
|
|
3545
|
+
const agents = agentStore();
|
|
3546
|
+
const json = useMemo(() => exporter.serialize(), [theme, modules, agents, exporter]);
|
|
3547
|
+
return /* @__PURE__ */ jsxs(Card, { variant: "elevated", className: "rounded-xl", "data-testid": `${testIdPrefix}-export-panel`, children: [
|
|
3548
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, { className: "flex items-center justify-between text-base", children: [
|
|
3549
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
3550
|
+
/* @__PURE__ */ jsx(FileJson, { className: "size-4 text-emerald" }),
|
|
3551
|
+
" Vertical config"
|
|
3552
|
+
] }),
|
|
3553
|
+
/* @__PURE__ */ jsxs(
|
|
3554
|
+
Button,
|
|
3555
|
+
{
|
|
3556
|
+
size: "sm",
|
|
3557
|
+
variant: "accent",
|
|
3558
|
+
onClick: () => exporter.download(),
|
|
3559
|
+
"data-testid": `${testIdPrefix}-export`,
|
|
3560
|
+
children: [
|
|
3561
|
+
/* @__PURE__ */ jsx(Download, { className: "size-4" }),
|
|
3562
|
+
" Download JSON"
|
|
3563
|
+
]
|
|
3564
|
+
}
|
|
3565
|
+
)
|
|
3566
|
+
] }) }),
|
|
3567
|
+
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx(
|
|
3568
|
+
"pre",
|
|
3569
|
+
{
|
|
3570
|
+
className: "scrollbar-slim max-h-[520px] overflow-auto rounded-md bg-ink p-4 font-mono text-[11px] leading-relaxed text-parchment",
|
|
3571
|
+
"data-testid": `${testIdPrefix}-export-preview`,
|
|
3572
|
+
children: json
|
|
3573
|
+
}
|
|
3574
|
+
) })
|
|
3575
|
+
] });
|
|
3576
|
+
}
|
|
3577
|
+
function VerticalBuilder({
|
|
3578
|
+
themeBundle,
|
|
3579
|
+
moduleStore,
|
|
3580
|
+
agentStore,
|
|
3581
|
+
exporter,
|
|
3582
|
+
themePreview,
|
|
3583
|
+
header,
|
|
3584
|
+
testIdPrefix = "builder"
|
|
3585
|
+
}) {
|
|
3586
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", "data-testid": `page-${testIdPrefix}`, children: [
|
|
3587
|
+
header,
|
|
3588
|
+
/* @__PURE__ */ jsxs(Tabs, { defaultValue: "theme", children: [
|
|
3589
|
+
/* @__PURE__ */ jsxs(TabsList, { "data-testid": `${testIdPrefix}-tabs`, children: [
|
|
3590
|
+
/* @__PURE__ */ jsxs(TabsTrigger, { value: "theme", "data-testid": `${testIdPrefix}-tab-theme`, children: [
|
|
3591
|
+
/* @__PURE__ */ jsx(Palette, { className: "size-4" }),
|
|
3592
|
+
" Theme"
|
|
3593
|
+
] }),
|
|
3594
|
+
/* @__PURE__ */ jsxs(TabsTrigger, { value: "modules", "data-testid": `${testIdPrefix}-tab-modules`, children: [
|
|
3595
|
+
/* @__PURE__ */ jsx(LayoutGrid, { className: "size-4" }),
|
|
3596
|
+
" Modules"
|
|
3597
|
+
] }),
|
|
3598
|
+
/* @__PURE__ */ jsxs(TabsTrigger, { value: "agents", "data-testid": `${testIdPrefix}-tab-agents`, children: [
|
|
3599
|
+
/* @__PURE__ */ jsx(Bot, { className: "size-4" }),
|
|
3600
|
+
" Agents"
|
|
3601
|
+
] }),
|
|
3602
|
+
/* @__PURE__ */ jsxs(TabsTrigger, { value: "export", "data-testid": `${testIdPrefix}-tab-export`, children: [
|
|
3603
|
+
/* @__PURE__ */ jsx(FileJson, { className: "size-4" }),
|
|
3604
|
+
" Export"
|
|
3605
|
+
] })
|
|
3606
|
+
] }),
|
|
3607
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "theme", children: /* @__PURE__ */ jsx(
|
|
3608
|
+
ThemeEditor,
|
|
3609
|
+
{
|
|
3610
|
+
bundle: themeBundle,
|
|
3611
|
+
variant: "panel",
|
|
3612
|
+
testIdPrefix: `${testIdPrefix}-theme`,
|
|
3613
|
+
preview: themePreview
|
|
3614
|
+
}
|
|
3615
|
+
) }),
|
|
3616
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "modules", children: /* @__PURE__ */ jsx(ModuleManager, { store: moduleStore, testIdPrefix }) }),
|
|
3617
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "agents", children: /* @__PURE__ */ jsx(AgentsPanel, { store: agentStore, testIdPrefix }) }),
|
|
3618
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "export", children: /* @__PURE__ */ jsx(
|
|
3619
|
+
ExportPanel,
|
|
3620
|
+
{
|
|
3621
|
+
themeBundle,
|
|
3622
|
+
moduleStore,
|
|
3623
|
+
agentStore,
|
|
3624
|
+
exporter,
|
|
3625
|
+
testIdPrefix
|
|
3626
|
+
}
|
|
3627
|
+
) })
|
|
3628
|
+
] })
|
|
3629
|
+
] });
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
export { AgentsPanel, AvatarBlock, BlockImage, CTABanner, Callout, Columns, ConnectionProvider, Container, DataTable, DataTableBlock, EmptyState, FAQ, FeatureGrid, Footer, GridView, Hero, LegalArbiProvider, ListBlock, LogoCloud, MetricCard, ModuleManager, Navbar, PageHeader, PageRenderer, RichTextBlock, Section, SectionHeading, Spacer, StatGroup, StudioEditor, THEME_STYLE_ID, Testimonial, ThemeEditor, Toolbar, VerticalBuilder, applyStoredTheme, applyThemeVars, clearThemeVars, createAgentSelectionStore, createAiFields, createArbiBlocksConfig, createBlockKit, createModuleRegistry, createModuleStore, createPromptLibrary, createStudioPersistence, createThemeStore, createVerticalExport, createWebsiteBlocks, daysUntil, fmtDate, fmtDateTime, fromNow, gbp, hrs, iconMap, initials, isAuthenticated, matterToContext, pct, tid, toHslTriplet, useConnection, useFirmAiTask, useSemanticSearch, useWorkspaceDocs };
|
|
3633
|
+
//# sourceMappingURL=index.js.map
|
|
3634
|
+
//# sourceMappingURL=index.js.map
|