@arbidocs/blocks 0.3.115 → 0.3.117

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.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { create } from 'zustand';
2
2
  import { persist } from 'zustand/middleware';
3
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, Check, ArrowLeft, Loader2, ChevronUp, BrainCircuit, Bot, LayoutGrid, FileJson, Save, Download, ImagePlus, Upload, Link2 } 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 as Tabs$1, TabsList, TabsTrigger, TabsContent, Separator, ArbiWebSocketProvider, useDocuments, useThumbnails } from '@arbidocs/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, Check, X, Menu, ArrowLeft, Loader2, ChevronUp, BrainCircuit, Bot, LayoutGrid, FileJson, Save, Download, Crop, ImagePlus, Upload, Link2, MessagesSquare } 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, NotificationBell, UserMenu, Badge, Switch, useConfigs, useAgents, Checkbox, Tabs as Tabs$1, TabsList, TabsTrigger, TabsContent, Separator, ArbiWebSocketProvider, useDocuments, useThumbnails, ImageCropModal, DEFAULT_ASPECT_OPTIONS, useConversations } from '@arbidocs/react';
6
6
  export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, redlineStats, textToArtifact, toRedlineMarkdown } from '@arbidocs/react';
7
7
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
8
  import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from '@tanstack/react-query';
9
9
  import { AgGridReact } from 'ag-grid-react';
10
10
  import { themeQuartz, AllCommunityModule } from 'ag-grid-community';
11
- import { Link, useParams } from 'react-router-dom';
11
+ import { useLocation, Link, useParams, NavLink } from 'react-router-dom';
12
12
  import { Puck, Render } from '@measured/puck';
13
13
 
14
14
  // src/prompts.ts
@@ -603,7 +603,25 @@ function ConnectionProvider({
603
603
  await arbi.selectWorkspace(target.external_id);
604
604
  setWorkspaceId(target.external_id);
605
605
  }
606
- setUser({ name: email.split("@")[0], email });
606
+ let resolved = { name: email.split("@")[0], email };
607
+ try {
608
+ const me = arbi.user.identity();
609
+ const members = await arbi.workspaces.listUsers();
610
+ const self = members.find((m) => m.user.external_id === me?.externalId)?.user;
611
+ if (self) {
612
+ const fullName = [self.given_name, self.family_name].filter(Boolean).join(" ").trim();
613
+ resolved = {
614
+ name: fullName || email.split("@")[0],
615
+ email: self.email || email,
616
+ externalId: self.external_id,
617
+ picture: self.picture ?? null
618
+ };
619
+ } else if (me?.externalId) {
620
+ resolved.externalId = me.externalId;
621
+ }
622
+ } catch {
623
+ }
624
+ setUser(resolved);
607
625
  setStatus("authenticated");
608
626
  } catch (e) {
609
627
  setError(e instanceof Error ? e.message : "Sign-in failed. Check your credentials.");
@@ -2297,6 +2315,390 @@ function TestimonialGrid({ items, columns = "3", testId }) {
2297
2315
  i
2298
2316
  )) });
2299
2317
  }
2318
+ function NavItemLink({ item, onNavigate }) {
2319
+ return /* @__PURE__ */ jsxs(
2320
+ NavLink,
2321
+ {
2322
+ to: item.to,
2323
+ end: item.end,
2324
+ onClick: onNavigate,
2325
+ "data-testid": tid("sidebar-nav", item.id),
2326
+ className: ({ isActive }) => cn(
2327
+ "group flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
2328
+ isActive ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground"
2329
+ ),
2330
+ children: [
2331
+ /* @__PURE__ */ jsx(item.icon, { className: "size-[18px] shrink-0" }),
2332
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: item.label }),
2333
+ item.arbi && /* @__PURE__ */ jsx(Sparkles, { className: "size-3.5 text-sidebar-primary", "aria-label": "Powered by ARBI" })
2334
+ ]
2335
+ }
2336
+ );
2337
+ }
2338
+ function SidebarBody({
2339
+ sections,
2340
+ footerItems,
2341
+ logo,
2342
+ logoHref,
2343
+ logoLabel,
2344
+ sidebarFooter,
2345
+ onNavigate
2346
+ }) {
2347
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2348
+ logo && /* @__PURE__ */ jsx("div", { className: "flex h-16 items-center border-b border-sidebar-border px-5", children: logoHref ? /* @__PURE__ */ jsx(Link, { to: logoHref, "aria-label": logoLabel, onClick: onNavigate, children: logo }) : logo }),
2349
+ /* @__PURE__ */ jsx("nav", { className: "flex-1 space-y-6 overflow-y-auto px-3 py-5", children: sections.map((section) => /* @__PURE__ */ jsxs("div", { children: [
2350
+ /* @__PURE__ */ jsx("p", { className: "px-3 pb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-sidebar-foreground/55", children: section.heading }),
2351
+ /* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: section.items.map((item) => /* @__PURE__ */ jsx(NavItemLink, { item, onNavigate }, item.id)) })
2352
+ ] }, section.heading)) }),
2353
+ (footerItems?.length || sidebarFooter) && /* @__PURE__ */ jsxs("div", { className: "border-t border-sidebar-border px-3 py-3", children: [
2354
+ footerItems?.map((item) => /* @__PURE__ */ jsx(NavItemLink, { item, onNavigate }, item.id)),
2355
+ sidebarFooter
2356
+ ] })
2357
+ ] });
2358
+ }
2359
+ function AppShell({
2360
+ sections,
2361
+ footerItems,
2362
+ logo,
2363
+ logoHref,
2364
+ logoLabel,
2365
+ sidebarFooter,
2366
+ topbar,
2367
+ rightRail,
2368
+ children,
2369
+ mainClassName,
2370
+ testIdPrefix = "app"
2371
+ }) {
2372
+ const [open, setOpen] = useState(false);
2373
+ const location = useLocation();
2374
+ useEffect(() => {
2375
+ setOpen(false);
2376
+ }, [location.pathname]);
2377
+ const body = (onNavigate) => /* @__PURE__ */ jsx(
2378
+ SidebarBody,
2379
+ {
2380
+ sections,
2381
+ footerItems,
2382
+ logo,
2383
+ logoHref,
2384
+ logoLabel,
2385
+ sidebarFooter,
2386
+ onNavigate
2387
+ }
2388
+ );
2389
+ return /* @__PURE__ */ jsxs(
2390
+ "div",
2391
+ {
2392
+ className: "flex h-screen overflow-hidden bg-background",
2393
+ "data-testid": `${testIdPrefix}-shell`,
2394
+ children: [
2395
+ /* @__PURE__ */ jsx(
2396
+ "aside",
2397
+ {
2398
+ className: "hidden h-full w-64 shrink-0 flex-col border-r border-sidebar-border bg-sidebar lg:flex",
2399
+ "data-testid": `${testIdPrefix}-sidebar`,
2400
+ children: body()
2401
+ }
2402
+ ),
2403
+ open && /* @__PURE__ */ jsxs(
2404
+ "div",
2405
+ {
2406
+ className: "fixed inset-0 z-50 lg:hidden",
2407
+ "data-testid": `${testIdPrefix}-sidebar-mobile`,
2408
+ children: [
2409
+ /* @__PURE__ */ jsx(
2410
+ "div",
2411
+ {
2412
+ className: "absolute inset-0 bg-black/60 backdrop-blur-sm",
2413
+ onClick: () => setOpen(false),
2414
+ "aria-hidden": true
2415
+ }
2416
+ ),
2417
+ /* @__PURE__ */ jsxs("aside", { className: "absolute inset-y-0 left-0 flex w-64 max-w-[82%] flex-col border-r border-sidebar-border bg-sidebar shadow-xl", children: [
2418
+ /* @__PURE__ */ jsx(
2419
+ "button",
2420
+ {
2421
+ type: "button",
2422
+ onClick: () => setOpen(false),
2423
+ className: "absolute right-3 top-4 z-10 flex size-8 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground",
2424
+ "aria-label": "Close menu",
2425
+ "data-testid": `${testIdPrefix}-sidebar-close`,
2426
+ children: /* @__PURE__ */ jsx(X, { className: "size-5" })
2427
+ }
2428
+ ),
2429
+ body(() => setOpen(false))
2430
+ ] })
2431
+ ]
2432
+ }
2433
+ ),
2434
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [
2435
+ /* @__PURE__ */ jsxs(
2436
+ "header",
2437
+ {
2438
+ className: "flex h-16 shrink-0 items-center gap-4 border-b border-border bg-card/80 px-6 backdrop-blur",
2439
+ "data-testid": `${testIdPrefix}-topbar`,
2440
+ children: [
2441
+ /* @__PURE__ */ jsx(
2442
+ "button",
2443
+ {
2444
+ type: "button",
2445
+ onClick: () => setOpen(true),
2446
+ className: "flex size-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground lg:hidden",
2447
+ "data-testid": `${testIdPrefix}-nav-toggle`,
2448
+ "aria-label": "Open menu",
2449
+ children: /* @__PURE__ */ jsx(Menu, { className: "size-5" })
2450
+ }
2451
+ ),
2452
+ topbar
2453
+ ]
2454
+ }
2455
+ ),
2456
+ /* @__PURE__ */ jsx(
2457
+ "main",
2458
+ {
2459
+ className: cn("flex-1 overflow-y-auto", mainClassName),
2460
+ "data-testid": `${testIdPrefix}-main`,
2461
+ children
2462
+ }
2463
+ )
2464
+ ] }),
2465
+ rightRail
2466
+ ]
2467
+ }
2468
+ );
2469
+ }
2470
+ function AppHeader({
2471
+ onSearch,
2472
+ onAsk,
2473
+ searchPlaceholder = "Ask AI or search\u2026",
2474
+ searchKbd = "\u2318K",
2475
+ askLabel = "Ask AI",
2476
+ leading,
2477
+ actions,
2478
+ user,
2479
+ userItems,
2480
+ onLogout,
2481
+ onNotificationNavigate,
2482
+ notificationTones,
2483
+ classNames,
2484
+ testIdPrefix = "app"
2485
+ }) {
2486
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2487
+ /* @__PURE__ */ jsxs(
2488
+ "button",
2489
+ {
2490
+ type: "button",
2491
+ onClick: onSearch,
2492
+ className: classNames?.search ?? "relative hidden max-w-md flex-1 items-center rounded-md border border-input bg-background py-2 pl-9 pr-3 text-left text-sm text-muted-foreground transition-colors hover:bg-accent md:flex",
2493
+ "data-testid": tid(testIdPrefix, "global-search"),
2494
+ children: [
2495
+ /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }),
2496
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: searchPlaceholder }),
2497
+ /* @__PURE__ */ jsx("kbd", { className: "rounded border border-border px-1.5 py-0.5 text-[10px]", children: searchKbd })
2498
+ ]
2499
+ }
2500
+ ),
2501
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-3", children: [
2502
+ leading,
2503
+ /* @__PURE__ */ jsxs(
2504
+ Button,
2505
+ {
2506
+ size: "sm",
2507
+ onClick: onAsk ?? onSearch,
2508
+ className: classNames?.askButton,
2509
+ "data-testid": tid(testIdPrefix, "ask-ai"),
2510
+ children: [
2511
+ /* @__PURE__ */ jsx(Sparkles, { className: "size-4" }),
2512
+ askLabel
2513
+ ]
2514
+ }
2515
+ ),
2516
+ actions,
2517
+ /* @__PURE__ */ jsx(
2518
+ NotificationBell,
2519
+ {
2520
+ testId: tid(testIdPrefix, "notifications"),
2521
+ onNavigate: onNotificationNavigate,
2522
+ toneClassNames: notificationTones
2523
+ }
2524
+ ),
2525
+ /* @__PURE__ */ jsx(
2526
+ UserMenu,
2527
+ {
2528
+ user,
2529
+ items: userItems,
2530
+ onLogout,
2531
+ testId: tid(testIdPrefix, "user-menu"),
2532
+ logoutTestId: tid(testIdPrefix, "logout")
2533
+ }
2534
+ )
2535
+ ] })
2536
+ ] });
2537
+ }
2538
+ var LiveDataContext = createContext(false);
2539
+ function useLiveDataActive() {
2540
+ return useContext(LiveDataContext);
2541
+ }
2542
+ function LiveDataProvider({ children }) {
2543
+ return /* @__PURE__ */ jsx(LiveDataContext.Provider, { value: true, children });
2544
+ }
2545
+ function WidgetPlaceholder({
2546
+ label,
2547
+ icon: Icon = Sparkles,
2548
+ testId
2549
+ }) {
2550
+ return /* @__PURE__ */ jsxs(
2551
+ "div",
2552
+ {
2553
+ className: "flex items-center gap-3 rounded-xl border border-dashed border-border bg-muted/30 px-5 py-6 text-sm text-muted-foreground",
2554
+ "data-testid": testId,
2555
+ children: [
2556
+ /* @__PURE__ */ jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { className: "size-4" }) }),
2557
+ /* @__PURE__ */ jsxs("span", { children: [
2558
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: label }),
2559
+ /* @__PURE__ */ jsx("span", { className: "ml-1", children: "\xB7 live in the app" })
2560
+ ] })
2561
+ ]
2562
+ }
2563
+ );
2564
+ }
2565
+ function useActiveWorkspaceId() {
2566
+ const { workspaceId, fallbackWorkspaceId } = useConnection();
2567
+ return workspaceId ?? fallbackWorkspaceId ?? void 0;
2568
+ }
2569
+ function timeAgo(iso) {
2570
+ if (!iso) return "";
2571
+ const then = new Date(iso).getTime();
2572
+ if (Number.isNaN(then)) return "";
2573
+ const s = Math.max(0, Math.round((Date.now() - then) / 1e3));
2574
+ if (s < 60) return "just now";
2575
+ const m = Math.round(s / 60);
2576
+ if (m < 60) return `${m}m ago`;
2577
+ const h = Math.round(m / 60);
2578
+ if (h < 24) return `${h}h ago`;
2579
+ return `${Math.round(h / 24)}d ago`;
2580
+ }
2581
+ var METRIC_SOURCES = {
2582
+ documents: { label: "Documents", icon: FileText },
2583
+ conversations: { label: "Agent threads", icon: Bot }
2584
+ };
2585
+ var asMetricSource = (s) => s in METRIC_SOURCES ? s : "documents";
2586
+ function AgentActivityLive({
2587
+ title,
2588
+ limit,
2589
+ testId
2590
+ }) {
2591
+ const wsId = useActiveWorkspaceId();
2592
+ const { data, isLoading } = useConversations(wsId);
2593
+ const rows = [...data ?? []].sort((a, b) => +new Date(b.updated_at ?? 0) - +new Date(a.updated_at ?? 0)).slice(0, limit);
2594
+ return /* @__PURE__ */ jsxs(Card, { variant: "elevated", className: "rounded-xl p-5", "data-testid": testId, children: [
2595
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center gap-2", children: [
2596
+ /* @__PURE__ */ jsx(Bot, { className: "size-4 text-muted-foreground" }),
2597
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-foreground", children: title })
2598
+ ] }),
2599
+ isLoading ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", "data-testid": `${testId}-loading`, children: "Loading agent threads\u2026" }) : rows.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", "data-testid": `${testId}-empty`, children: "No agent threads yet." }) : /* @__PURE__ */ jsx("ul", { className: "space-y-2", "data-testid": `${testId}-list`, children: rows.map((c, i) => /* @__PURE__ */ jsxs(
2600
+ "li",
2601
+ {
2602
+ className: "flex items-center justify-between gap-3",
2603
+ "data-testid": `${testId}-item-${i}`,
2604
+ children: [
2605
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 truncate", children: [
2606
+ /* @__PURE__ */ jsx(MessagesSquare, { className: "size-3.5 shrink-0 text-muted-foreground" }),
2607
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-foreground", children: c.title?.trim() || "Untitled thread" })
2608
+ ] }),
2609
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: timeAgo(c.updated_at) })
2610
+ ]
2611
+ },
2612
+ c.external_id ?? i
2613
+ )) })
2614
+ ] });
2615
+ }
2616
+ function LiveMetricInner({
2617
+ label,
2618
+ source,
2619
+ accent,
2620
+ testId
2621
+ }) {
2622
+ const wsId = useActiveWorkspaceId();
2623
+ const docs = useDocuments(wsId, { enabled: source === "documents" });
2624
+ const convos = useConversations(wsId, { enabled: source === "conversations" });
2625
+ const query = source === "documents" ? docs : convos;
2626
+ const count = Array.isArray(query.data) ? query.data.length : 0;
2627
+ return /* @__PURE__ */ jsx(
2628
+ MetricCard,
2629
+ {
2630
+ label: label || METRIC_SOURCES[source].label,
2631
+ value: query.isLoading ? "\u2014" : String(count),
2632
+ accent,
2633
+ icon: METRIC_SOURCES[source].icon,
2634
+ testId
2635
+ }
2636
+ );
2637
+ }
2638
+ function AgentActivityBlock({
2639
+ title,
2640
+ limit,
2641
+ testId
2642
+ }) {
2643
+ return useLiveDataActive() ? /* @__PURE__ */ jsx(AgentActivityLive, { title, limit, testId }) : /* @__PURE__ */ jsx(WidgetPlaceholder, { label: String(title || "Agent activity"), icon: Bot, testId });
2644
+ }
2645
+ function LiveMetricBlock({
2646
+ label,
2647
+ source,
2648
+ accent,
2649
+ testId
2650
+ }) {
2651
+ const src = asMetricSource(source);
2652
+ return useLiveDataActive() ? /* @__PURE__ */ jsx(LiveMetricInner, { label, source: src, accent, testId }) : /* @__PURE__ */ jsx(
2653
+ WidgetPlaceholder,
2654
+ {
2655
+ label: String(label || METRIC_SOURCES[src].label),
2656
+ icon: METRIC_SOURCES[src].icon,
2657
+ testId
2658
+ }
2659
+ );
2660
+ }
2661
+ function createLiveWidgets(kit) {
2662
+ const { blockTestId } = kit;
2663
+ const AgentActivityWidget = {
2664
+ label: "Agent activity (live)",
2665
+ fields: {
2666
+ title: { type: "text", label: "Title" },
2667
+ limit: { type: "number", label: "Rows", min: 1, max: 8 }
2668
+ },
2669
+ defaultProps: { title: "Recent agent activity", limit: 4 },
2670
+ render: ({ id, title, limit }) => /* @__PURE__ */ jsx(AgentActivityBlock, { title, limit, testId: blockTestId(id) })
2671
+ };
2672
+ const LiveMetricWidget = {
2673
+ label: "Live metric",
2674
+ fields: {
2675
+ label: { type: "text", label: "Label" },
2676
+ source: {
2677
+ type: "select",
2678
+ label: "Source",
2679
+ options: [
2680
+ { label: "Documents", value: "documents" },
2681
+ { label: "Agent threads", value: "conversations" }
2682
+ ]
2683
+ },
2684
+ accent: {
2685
+ type: "radio",
2686
+ label: "Accent",
2687
+ options: [
2688
+ { label: "Default", value: "default" },
2689
+ { label: "Primary", value: "primary" },
2690
+ { label: "Success", value: "success" }
2691
+ ]
2692
+ }
2693
+ },
2694
+ defaultProps: { label: "", source: "documents", accent: "primary" },
2695
+ render: ({ id, label, source, accent }) => /* @__PURE__ */ jsx(LiveMetricBlock, { label, source, accent, testId: blockTestId(id) })
2696
+ };
2697
+ return {
2698
+ AgentActivity: AgentActivityWidget,
2699
+ LiveMetric: LiveMetricWidget
2700
+ };
2701
+ }
2300
2702
 
2301
2703
  // src/studio/publish.ts
2302
2704
  function blobToDataUrl(blob) {
@@ -2571,34 +2973,36 @@ function PageRenderer({
2571
2973
  live,
2572
2974
  slug: slugProp,
2573
2975
  emptyState,
2574
- testIdPrefix = "studio"
2976
+ testIdPrefix = "studio",
2977
+ liveData = false,
2978
+ initialData,
2979
+ applyTheme = true
2575
2980
  }) {
2576
2981
  const params = useParams();
2577
2982
  const slug = slugProp ?? params.slug ?? "home";
2578
2983
  const arbi = useArbi();
2579
- const [data, setData] = useState(null);
2580
- const [loading, setLoading] = useState(true);
2984
+ const [data, setData] = useState(initialData ?? null);
2985
+ const [loading, setLoading] = useState(!initialData);
2581
2986
  useEffect(() => {
2582
2987
  let cancelled = false;
2583
- setLoading(true);
2988
+ setLoading(!initialData);
2584
2989
  void (async () => {
2585
- const [{ data: published }, { data: theme }] = await Promise.all([
2586
- persistence.loadPublished(arbi, live, slug),
2587
- persistence.loadTheme(arbi, live)
2588
- ]);
2990
+ const theme = applyTheme ? (await persistence.loadTheme(arbi, live)).data : void 0;
2589
2991
  if (cancelled) return;
2590
- const page = published ?? (await persistence.loadPage(arbi, live, slug)).data;
2992
+ const page = initialData ?? (await persistence.loadPublished(arbi, live, slug)).data ?? (await persistence.loadPage(arbi, live, slug)).data;
2591
2993
  if (cancelled) return;
2592
- applyThemeVars(
2593
- theme ? { ...defaultTheme, ...theme, colors: { ...defaultTheme.colors, ...theme.colors } } : defaultTheme
2594
- );
2994
+ if (applyTheme) {
2995
+ applyThemeVars(
2996
+ theme ? { ...defaultTheme, ...theme, colors: { ...defaultTheme.colors, ...theme.colors } } : defaultTheme
2997
+ );
2998
+ }
2595
2999
  setData(page);
2596
3000
  setLoading(false);
2597
3001
  })();
2598
3002
  return () => {
2599
3003
  cancelled = true;
2600
3004
  };
2601
- }, [arbi, live, slug, persistence, defaultTheme]);
3005
+ }, [arbi, live, slug, persistence, defaultTheme, initialData, applyTheme]);
2602
3006
  if (loading) {
2603
3007
  return /* @__PURE__ */ jsx(
2604
3008
  "div",
@@ -2624,7 +3028,8 @@ function PageRenderer({
2624
3028
  }
2625
3029
  );
2626
3030
  }
2627
- return /* @__PURE__ */ jsx("main", { "data-testid": `page-${testIdPrefix}-render`, children: /* @__PURE__ */ jsx(Render, { config, data }) });
3031
+ const rendered = /* @__PURE__ */ jsx(Render, { config, data });
3032
+ return /* @__PURE__ */ jsx("main", { "data-testid": `page-${testIdPrefix}-render`, children: liveData ? /* @__PURE__ */ jsx(LiveDataProvider, { children: rendered }) : rendered });
2628
3033
  }
2629
3034
 
2630
3035
  // src/studio/persistence.ts
@@ -2899,6 +3304,9 @@ function useImageGen() {
2899
3304
  reset: task.reset
2900
3305
  };
2901
3306
  }
3307
+ function toJpgName(name) {
3308
+ return name.replace(/\.[^.]+$/, "") + ".jpg";
3309
+ }
2902
3310
  var IMAGE_EXT = /\.(png|jpe?g|webp|gif|avif|svg)$/i;
2903
3311
  function ImageFieldControl({
2904
3312
  value,
@@ -2912,9 +3320,12 @@ function ImageFieldControl({
2912
3320
  const [url, setUrl] = useState("");
2913
3321
  const [prompt, setPrompt] = useState("");
2914
3322
  const [busy, setBusy] = useState(false);
3323
+ const [cropSrc, setCropSrc] = useState(null);
3324
+ const [cropName, setCropName] = useState("image.jpg");
2915
3325
  const arbi = useArbi();
2916
3326
  const gen = useImageGen();
2917
3327
  const queryClient = useQueryClient();
3328
+ const currentAssetImage = useAssetImage(isAssetRef(value) ? assetRefId(value) : "");
2918
3329
  const tp = (...q) => tid(`${testIdPrefix}-image`, name, ...q);
2919
3330
  const refreshAssets = () => queryClient.invalidateQueries({ queryKey: ["arbi", "documents", workspaceId] });
2920
3331
  const { data: docs } = useDocuments(workspaceId);
@@ -2929,13 +3340,24 @@ function ImageFieldControl({
2929
3340
  [imageDocs]
2930
3341
  );
2931
3342
  const { data: thumbs } = useThumbnails(imageIds);
2932
- const onUpload = async (e) => {
3343
+ const onUpload = (e) => {
2933
3344
  const file = e.target.files?.[0];
2934
3345
  if (!file) return;
3346
+ setCropName(toJpgName(file.name));
3347
+ setCropSrc(URL.createObjectURL(file));
3348
+ e.target.value = "";
3349
+ };
3350
+ const openReframe = () => {
3351
+ if (!currentAssetImage) return;
3352
+ setCropName("reframe.jpg");
3353
+ setCropSrc(currentAssetImage);
3354
+ };
3355
+ const uploadBlobAsAsset = async (blob, name2) => {
2935
3356
  setBusy(true);
2936
3357
  try {
2937
3358
  await arbi.selectWorkspace(workspaceId);
2938
- const res = await arbi.documents.uploadFile(file, file.name, { folder });
3359
+ const file = new File([blob], name2, { type: blob.type });
3360
+ const res = await arbi.documents.uploadFile(file, name2, { folder });
2939
3361
  const id = res.doc_ext_ids?.[0];
2940
3362
  if (id) {
2941
3363
  onChange(asAssetRef(id));
@@ -2945,6 +3367,12 @@ function ImageFieldControl({
2945
3367
  setBusy(false);
2946
3368
  }
2947
3369
  };
3370
+ const onCropComplete = async (result) => {
3371
+ const src = cropSrc;
3372
+ setCropSrc(null);
3373
+ await uploadBlobAsAsset(result.blob, cropName);
3374
+ if (src?.startsWith("blob:")) URL.revokeObjectURL(src);
3375
+ };
2948
3376
  const onGenerate = async () => {
2949
3377
  if (!prompt.trim() || gen.isGenerating) return;
2950
3378
  const id = await gen.generate(prompt);
@@ -2962,7 +3390,22 @@ function ImageFieldControl({
2962
3390
  const inputClass = "w-full rounded-md border border-border bg-card px-2.5 py-1.5 text-sm text-foreground outline-none focus:border-primary";
2963
3391
  const tabClass = (on) => `inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium ${on ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"}`;
2964
3392
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-testid": tp(), children: [
2965
- /* @__PURE__ */ jsx(BlockImage, { src: value, aspect: "16:9", rounded: "md", testId: tp("preview") }),
3393
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
3394
+ /* @__PURE__ */ jsx(BlockImage, { src: value, aspect: "16:9", rounded: "md", testId: tp("preview") }),
3395
+ currentAssetImage ? /* @__PURE__ */ jsxs(
3396
+ "button",
3397
+ {
3398
+ type: "button",
3399
+ onClick: openReframe,
3400
+ className: "absolute right-1.5 top-1.5 inline-flex items-center gap-1 rounded-md bg-background/85 px-2 py-1 text-xs font-medium text-foreground shadow-sm backdrop-blur hover:bg-background",
3401
+ "data-testid": tp("reframe"),
3402
+ children: [
3403
+ /* @__PURE__ */ jsx(Crop, { className: "size-3" }),
3404
+ " Crop"
3405
+ ]
3406
+ }
3407
+ ) : null
3408
+ ] }),
2966
3409
  /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: MODES.map((m) => /* @__PURE__ */ jsxs(
2967
3410
  "button",
2968
3411
  {
@@ -3079,6 +3522,19 @@ function ImageFieldControl({
3079
3522
  "data-testid": tp("clear"),
3080
3523
  children: "Clear"
3081
3524
  }
3525
+ ),
3526
+ /* @__PURE__ */ jsx(
3527
+ ImageCropModal,
3528
+ {
3529
+ open: !!cropSrc,
3530
+ image: cropSrc,
3531
+ title: "Frame your image",
3532
+ aspectOptions: DEFAULT_ASPECT_OPTIONS,
3533
+ cropShape: "rect",
3534
+ output: { maxSize: 1600 },
3535
+ onComplete: onCropComplete,
3536
+ onCancel: () => setCropSrc(null)
3537
+ }
3082
3538
  )
3083
3539
  ] });
3084
3540
  }
@@ -3156,7 +3612,7 @@ function Section({
3156
3612
  ] });
3157
3613
  }
3158
3614
  var iconMap = {
3159
- FileText,
3615
+ FileText: FileText,
3160
3616
  Folder,
3161
3617
  Users,
3162
3618
  Briefcase,
@@ -4232,6 +4688,13 @@ function createSiteBlocks(kit) {
4232
4688
  TestimonialGrid: TestimonialGridBlock
4233
4689
  };
4234
4690
  }
4691
+ var emptySlot2 = [];
4692
+ var METRIC_COLS = {
4693
+ "2": "sm:grid-cols-2",
4694
+ "3": "sm:grid-cols-2 lg:grid-cols-3",
4695
+ "4": "sm:grid-cols-2 lg:grid-cols-4"
4696
+ };
4697
+ var METRIC_GAP = { sm: "gap-3", md: "gap-4", lg: "gap-6" };
4235
4698
  function createArbiBlocksConfig(opts = {}) {
4236
4699
  const kit = createBlockKit(opts.aiFields, opts.imageField, opts.eyebrowClassName);
4237
4700
  const { textField, textareaField, boolRadio, iconField, resolveIcon, blockTestId } = kit;
@@ -4269,16 +4732,19 @@ function createArbiBlocksConfig(opts = {}) {
4269
4732
  fields: {
4270
4733
  title: textField("Title"),
4271
4734
  eyebrow: textField("Eyebrow"),
4272
- description: textareaField("Description", void 0, "description")
4735
+ description: textareaField("Description", void 0, "description"),
4736
+ // `actions` is a slot so the top band can host real buttons/badges authored
4737
+ // as blocks (drop a Button in), while the interactive body stays bespoke.
4738
+ actions: { type: "slot", allow: ["Button", "Badge"] }
4273
4739
  },
4274
- defaultProps: { title: "Page title" },
4275
- // `actions` (ReactNode) is deferred for v1 — see module notes.
4276
- render: ({ id, title, eyebrow, description }) => /* @__PURE__ */ jsx(
4740
+ defaultProps: { title: "Page title", actions: emptySlot2 },
4741
+ render: ({ id, title, eyebrow, description, actions: Actions }) => /* @__PURE__ */ jsx(
4277
4742
  PageHeader,
4278
4743
  {
4279
4744
  title,
4280
4745
  eyebrow,
4281
4746
  description,
4747
+ actions: /* @__PURE__ */ jsx(Actions, { className: "flex items-center gap-2" }),
4282
4748
  testId: blockTestId(id)
4283
4749
  }
4284
4750
  )
@@ -4288,16 +4754,18 @@ function createArbiBlocksConfig(opts = {}) {
4288
4754
  fields: {
4289
4755
  title: textField("Title"),
4290
4756
  description: textareaField("Description", void 0, "description"),
4291
- icon: iconField()
4757
+ icon: iconField(),
4758
+ // `action` is a slot so an empty state can offer a real call-to-action button.
4759
+ action: { type: "slot", allow: ["Button"] }
4292
4760
  },
4293
- defaultProps: { title: "Nothing here yet" },
4294
- // `action` (ReactNode) is deferred for v1 — see module notes.
4295
- render: ({ id, title, description, icon }) => /* @__PURE__ */ jsx(
4761
+ defaultProps: { title: "Nothing here yet", action: emptySlot2 },
4762
+ render: ({ id, title, description, icon, action: Action }) => /* @__PURE__ */ jsx(
4296
4763
  EmptyState,
4297
4764
  {
4298
4765
  title,
4299
4766
  description,
4300
4767
  icon: resolveIcon(icon),
4768
+ action: /* @__PURE__ */ jsx(Action, {}),
4301
4769
  testId: blockTestId(id)
4302
4770
  }
4303
4771
  )
@@ -4350,6 +4818,42 @@ function createArbiBlocksConfig(opts = {}) {
4350
4818
  }
4351
4819
  )
4352
4820
  };
4821
+ const MetricRowBlock = {
4822
+ label: "Metric row",
4823
+ fields: {
4824
+ columns: {
4825
+ type: "radio",
4826
+ label: "Columns",
4827
+ options: [
4828
+ { label: "2", value: "2" },
4829
+ { label: "3", value: "3" },
4830
+ { label: "4", value: "4" }
4831
+ ]
4832
+ },
4833
+ gap: {
4834
+ type: "radio",
4835
+ label: "Gap",
4836
+ options: [
4837
+ { label: "S", value: "sm" },
4838
+ { label: "M", value: "md" },
4839
+ { label: "L", value: "lg" }
4840
+ ]
4841
+ },
4842
+ // Static KPI tiles and (when registered) live metric widgets both belong in a band.
4843
+ items: { type: "slot", allow: ["MetricCard", "LiveMetric"] }
4844
+ },
4845
+ defaultProps: { columns: "3", gap: "md", items: emptySlot2 },
4846
+ render: ({ id, columns, gap, items: Items }) => /* @__PURE__ */ jsx("div", { "data-testid": blockTestId(id), children: /* @__PURE__ */ jsx(
4847
+ Items,
4848
+ {
4849
+ className: cn(
4850
+ "grid grid-cols-1",
4851
+ METRIC_COLS[columns] ?? METRIC_COLS["3"],
4852
+ METRIC_GAP[gap] ?? METRIC_GAP.md
4853
+ )
4854
+ }
4855
+ ) })
4856
+ };
4353
4857
  const DataTableBlockConfig = {
4354
4858
  label: "Data table",
4355
4859
  fields: {
@@ -4548,11 +5052,13 @@ function createArbiBlocksConfig(opts = {}) {
4548
5052
  };
4549
5053
  const website = createWebsiteBlocks(kit);
4550
5054
  const site = createSiteBlocks(kit);
5055
+ const widgets = opts.liveWidgets ? createLiveWidgets(kit) : {};
4551
5056
  const components = {
4552
5057
  SectionHeading: SectionHeadingBlock,
4553
5058
  PageHeader: PageHeaderBlock,
4554
5059
  EmptyState: EmptyStateBlock,
4555
5060
  MetricCard: MetricCardBlock,
5061
+ MetricRow: MetricRowBlock,
4556
5062
  DataTable: DataTableBlockConfig,
4557
5063
  Section: SectionBlock,
4558
5064
  Card: CardBlock,
@@ -4560,7 +5066,8 @@ function createArbiBlocksConfig(opts = {}) {
4560
5066
  Badge: BadgeBlock,
4561
5067
  Separator: SeparatorBlock,
4562
5068
  ...website,
4563
- ...site
5069
+ ...site,
5070
+ ...widgets
4564
5071
  };
4565
5072
  const config = {
4566
5073
  categories: {
@@ -4594,7 +5101,8 @@ function createArbiBlocksConfig(opts = {}) {
4594
5101
  Commerce: { title: "Commerce", components: ["Pricing"] },
4595
5102
  Forms: { title: "Forms", components: ["ContactForm"] },
4596
5103
  Navigation: { title: "Navigation", components: ["Navbar", "Footer"] },
4597
- Data: { title: "Data", components: ["MetricCard", "DataTable"] },
5104
+ Data: { title: "Data", components: ["MetricRow", "MetricCard", "DataTable"] },
5105
+ ...opts.liveWidgets ? { Widgets: { title: "Widgets", components: ["AgentActivity", "LiveMetric"] } } : {},
4598
5106
  Primitives: { title: "Primitives", components: ["Button", "Badge", "Separator"] }
4599
5107
  },
4600
5108
  components,
@@ -5079,6 +5587,6 @@ function VerticalBuilder({
5079
5587
  ] });
5080
5588
  }
5081
5589
 
5082
- export { ASSET_REF_PREFIX, Accordion, AgentsPanel, AssetResolverProvider, AvatarBlock, Banner, BlockImage, CTABanner, Callout, Columns, ConnectionProvider, ContactForm, Container, DataTable, DataTableBlock, EmptyState, FAQ, FeatureGrid, Footer, Gallery, GridView, Hero, LegalArbiProvider, ListBlock, LogoCloud, MediaText, MetricCard, ModuleManager, Navbar, PageHeader, PageRenderer, PricingTable, RichTextBlock, Section, SectionHeading, Spacer, StatGroup, Steps, StudioEditor, THEME_STYLE_ID, Tabs, TeamGrid, Testimonial, TestimonialGrid, ThemeEditor, Toolbar, VerticalBuilder, VideoEmbed, applyStoredTheme, applyThemeVars, asAssetRef, assetRefId, clearThemeVars, createAgentSelectionStore, createAiFields, createArbiBlocksConfig, createArbiMaterializer, createBlockKit, createImageField, createModuleRegistry, createModuleStore, createPromptLibrary, createSiteBlocks, createStudioPersistence, createThemeStore, createVerticalExport, createWebsiteBlocks, daysUntil, fmtDate, fmtDateTime, fromNow, gbp, hrs, iconMap, initials, isAssetRef, isAuthenticated, materializePageData, matterToContext, pct, tid, toEmbedUrl, toHslTriplet, useAssetResolverActive, useConnection, useFirmAiTask, useImageGen, useSemanticSearch, useWorkspaceDocs };
5590
+ export { ASSET_REF_PREFIX, Accordion, AgentsPanel, AppHeader, AppShell, AssetResolverProvider, AvatarBlock, Banner, BlockImage, CTABanner, Callout, Columns, ConnectionProvider, ContactForm, Container, DataTable, DataTableBlock, EmptyState, FAQ, FeatureGrid, Footer, Gallery, GridView, Hero, LegalArbiProvider, ListBlock, LiveDataProvider, LogoCloud, MediaText, MetricCard, ModuleManager, Navbar, PageHeader, PageRenderer, PricingTable, RichTextBlock, Section, SectionHeading, Spacer, StatGroup, Steps, StudioEditor, THEME_STYLE_ID, Tabs, TeamGrid, Testimonial, TestimonialGrid, ThemeEditor, Toolbar, VerticalBuilder, VideoEmbed, WidgetPlaceholder, applyStoredTheme, applyThemeVars, asAssetRef, assetRefId, clearThemeVars, createAgentSelectionStore, createAiFields, createArbiBlocksConfig, createArbiMaterializer, createBlockKit, createImageField, createLiveWidgets, createModuleRegistry, createModuleStore, createPromptLibrary, createSiteBlocks, createStudioPersistence, createThemeStore, createVerticalExport, createWebsiteBlocks, daysUntil, fmtDate, fmtDateTime, fromNow, gbp, hrs, iconMap, initials, isAssetRef, isAuthenticated, materializePageData, matterToContext, pct, tid, toEmbedUrl, toHslTriplet, useAssetResolverActive, useConnection, useFirmAiTask, useImageGen, useLiveDataActive, useSemanticSearch, useWorkspaceDocs };
5083
5591
  //# sourceMappingURL=index.js.map
5084
5592
  //# sourceMappingURL=index.js.map