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