@clawnify/app 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -153,18 +153,22 @@ type HostToAppMessage = {
153
153
  */
154
154
  declare const embedded: boolean;
155
155
  /**
156
- * Open the dashboard's chat, optionally with a prompt in the composer. The
157
- * prompt is a DRAFT: the user reads it and presses send. The host never sends
158
- * text an app wrote, because an app renders data other people typed (a form
159
- * submission, a contact note) and that text must not drive the user's agent.
156
+ * Open the chat, optionally with a prompt in its composer. Inside Clawnify
157
+ * that is the dashboard's chat; outside, the app's own `<AppChat standalone>`
158
+ * panel. The prompt is a DRAFT: the user reads it and presses send. It is
159
+ * never sent for them, because an app renders data other people typed (a form
160
+ * submission, a contact note) and that text must not drive anyone's agent.
160
161
  *
161
- * Returns false when standalone (no dashboard, no chat): hide the button then,
162
- * or check `embedded` before rendering it.
162
+ * Returns false when there is no chat to open (standalone with no `<AppChat>`
163
+ * mounted). Render the button behind `useHasChat()`:
163
164
  *
164
- * {embedded && <button onClick={() => openChat(`Draft a follow-up for ${c.name}`)}>Ask AI</button>}
165
- * {embedded && <button onClick={() => openChat()}>Chat</button>} // no prompt: just open it
165
+ * const hasChat = useHasChat();
166
+ * {hasChat && <button onClick={() => openChat(`Draft a follow-up for ${c.name}`)}>Ask AI</button>}
167
+ * {hasChat && <button onClick={() => openChat()}>Chat</button>} // no prompt: just open it
166
168
  */
167
169
  declare function openChat(prompt?: string): boolean;
170
+ /** Whether `openChat` has somewhere to go: the dashboard, or a mounted `<AppChat>`. */
171
+ declare function useHasChat(): boolean;
168
172
  /**
169
173
  * Tell the dashboard's chat what the user is looking at, so "this contact"
170
174
  * means the one on screen. Call it from the screen that shows the record; the
@@ -174,6 +178,39 @@ declare function openChat(prompt?: string): boolean;
174
178
  * useChatContext({ label: "Contact", record: { type: "contact", id: c.id, label: c.name } });
175
179
  */
176
180
  declare function useChatContext(context: ChatContext | null): void;
181
+ /** What `<AppChat standalone>` hands the app's own chat panel. */
182
+ interface StandaloneChat {
183
+ open: boolean;
184
+ /**
185
+ * The last prompt `openChat` passed, for the composer. A new object on every
186
+ * call (compare by identity, or by `id`), so the same prompt twice prefills
187
+ * twice. Null when opened without one.
188
+ */
189
+ draft: {
190
+ id: number;
191
+ text: string;
192
+ } | null;
193
+ /** What `useChatContext` reports is on screen; send it with each message. */
194
+ context: ChatContext | null;
195
+ close: () => void;
196
+ }
197
+ interface AppChatProps {
198
+ /**
199
+ * The app's own chat, drawn only outside Clawnify. It talks to the app's own
200
+ * endpoint (`/api/chat` with the app's own model key, say). Inside the
201
+ * dashboard nothing renders and `openChat` opens the dashboard's chat.
202
+ */
203
+ standalone: (chat: StandaloneChat) => ReactNode;
204
+ }
205
+ /**
206
+ * Where `openChat` lands when the app runs outside Clawnify. Mount it once,
207
+ * near the root:
208
+ *
209
+ * <AppChat standalone={(chat) => <MyChatPanel {...chat} />} />
210
+ *
211
+ * Mount at most one: the last one mounted receives `openChat`.
212
+ */
213
+ declare function AppChat({ standalone }: AppChatProps): react.JSX.Element | null;
177
214
  /**
178
215
  * Run `onChange` when the dashboard's chat has written this app's data (a
179
216
  * POST, PUT, PATCH or DELETE through your API), with the API paths it called.
@@ -213,8 +250,26 @@ interface AppNavProps {
213
250
  active?: string;
214
251
  /** Called with the item the user chose, from either renderer. */
215
252
  onNavigate: (item: AppNavItem) => void;
216
- /** Rendered at the bottom of the standalone sidebar (a footer action, say). */
253
+ /** Rendered at the bottom of the built-in sidebar (a footer action, say). */
217
254
  children?: ReactNode;
255
+ /**
256
+ * The app's own sidebar, drawn instead of the built-in one outside
257
+ * Clawnify, from the same nav data (so the navigation is still defined
258
+ * once). Inside the dashboard it never renders: the dashboard draws the nav.
259
+ *
260
+ * standalone={(nav) => <MySidebar groups={nav.groups} active={nav.active} onSelect={nav.onNavigate} />}
261
+ */
262
+ standalone?: (nav: StandaloneNav) => ReactNode;
263
+ }
264
+ /** What `<AppNav standalone>` hands the app's own sidebar. */
265
+ interface StandaloneNav {
266
+ title: string;
267
+ icon?: string;
268
+ groups: AppNavGroup[];
269
+ active?: string;
270
+ /** The `home: true` item, which the built-in sidebar opens from its brand row. */
271
+ home?: AppNavItem;
272
+ onNavigate: (item: AppNavItem) => void;
218
273
  }
219
274
  /**
220
275
  * The app's navigation: sidebar when standalone, host-fed when embedded.
@@ -225,6 +280,6 @@ interface AppNavProps {
225
280
  * `--border`, `--primary`…) and falls back to the platform defaults when a
226
281
  * token is missing.
227
282
  */
228
- declare function AppNav({ title, icon, groups, active, onNavigate, children }: AppNavProps): react.JSX.Element | null;
283
+ declare function AppNav({ title, icon, groups, active, onNavigate, children, standalone }: AppNavProps): react.JSX.Element | null;
229
284
 
230
- export { AppNav, type AppNavGroup, type AppNavItem, type AppNavProps, type AppToHostMessage, type ChatContext, HOST_BRIDGE_VERSION, type HostFeature, type HostToAppMessage, NAV_COLORS, type NavColor, TILE_ICONS, TileIcon, type TileIconName, type TileIconProps, embedded, navColorFor, openChat, reportLocation, useChatContext, useHostChanges, useHostNavigate };
285
+ export { AppChat, type AppChatProps, AppNav, type AppNavGroup, type AppNavItem, type AppNavProps, type AppToHostMessage, type ChatContext, HOST_BRIDGE_VERSION, type HostFeature, type HostToAppMessage, NAV_COLORS, type NavColor, type StandaloneChat, type StandaloneNav, TILE_ICONS, TileIcon, type TileIconName, type TileIconProps, embedded, navColorFor, openChat, reportLocation, useChatContext, useHasChat, useHostChanges, useHostNavigate };
@@ -1,7 +1,8 @@
1
1
  // src/client/index.tsx
2
2
  import {
3
3
  useEffect,
4
- useRef
4
+ useRef,
5
+ useState
5
6
  } from "react";
6
7
  import {
7
8
  AddressBook,
@@ -233,26 +234,47 @@ function subscribe(listener, feature) {
233
234
  function isAppPath(path) {
234
235
  return typeof path === "string" && path.length <= 2048 && /^\/(?![/\\])/.test(path);
235
236
  }
237
+ var standaloneOpen = null;
238
+ var chatListeners = /* @__PURE__ */ new Set();
239
+ function notifyChat() {
240
+ for (const l of [...chatListeners]) l();
241
+ }
242
+ function useChatVersion() {
243
+ const [, setVersion] = useState(0);
244
+ useEffect(() => {
245
+ const l = () => setVersion((v) => v + 1);
246
+ chatListeners.add(l);
247
+ return () => {
248
+ chatListeners.delete(l);
249
+ };
250
+ }, []);
251
+ }
236
252
  function openChat(prompt) {
237
- if (!embedded) return false;
238
- post({
239
- source: "clawnify-app",
240
- v: HOST_BRIDGE_VERSION,
241
- type: "chat",
242
- ...prompt ? { prompt: String(prompt).slice(0, 2e3) } : {}
243
- });
253
+ const draft = prompt ? String(prompt).slice(0, 2e3) : null;
254
+ if (!embedded) {
255
+ if (!standaloneOpen) return false;
256
+ standaloneOpen(draft);
257
+ return true;
258
+ }
259
+ post({ source: "clawnify-app", v: HOST_BRIDGE_VERSION, type: "chat", ...draft ? { prompt: draft } : {} });
244
260
  return true;
245
261
  }
262
+ function useHasChat() {
263
+ useChatVersion();
264
+ return embedded || standaloneOpen !== null;
265
+ }
246
266
  function useChatContext(context) {
247
- const key = embedded && context ? JSON.stringify(sanitizeContext(context)) : "";
267
+ const key = context ? JSON.stringify(sanitizeContext(context)) : "";
248
268
  useEffect(() => {
249
- if (!embedded || !key) return;
269
+ if (!key) return;
250
270
  listen();
251
271
  chatContext = JSON.parse(key);
252
272
  post({ source: "clawnify-app", v: HOST_BRIDGE_VERSION, type: "context", context: chatContext });
273
+ notifyChat();
253
274
  return () => {
254
275
  chatContext = null;
255
276
  post({ source: "clawnify-app", v: HOST_BRIDGE_VERSION, type: "context", context: null });
277
+ notifyChat();
256
278
  };
257
279
  }, [key]);
258
280
  }
@@ -268,6 +290,27 @@ function sanitizeContext(c) {
268
290
  } : {}
269
291
  };
270
292
  }
293
+ function AppChat({ standalone }) {
294
+ const [open, setOpen] = useState(false);
295
+ const [draft, setDraft] = useState(null);
296
+ const nextId = useRef(0);
297
+ useChatVersion();
298
+ useEffect(() => {
299
+ if (embedded) return;
300
+ const handler = (text) => {
301
+ setDraft(text ? { id: ++nextId.current, text } : null);
302
+ setOpen(true);
303
+ };
304
+ standaloneOpen = handler;
305
+ notifyChat();
306
+ return () => {
307
+ if (standaloneOpen === handler) standaloneOpen = null;
308
+ notifyChat();
309
+ };
310
+ }, []);
311
+ if (embedded) return null;
312
+ return /* @__PURE__ */ jsx(Fragment, { children: standalone({ open, draft, context: chatContext, close: () => setOpen(false) }) });
313
+ }
271
314
  function useHostChanges(onChange) {
272
315
  const ref = useRef(onChange);
273
316
  ref.current = onChange;
@@ -300,7 +343,7 @@ function reportLocation(path) {
300
343
  }
301
344
  post({ source: "clawnify-app", v: HOST_BRIDGE_VERSION, type: "location", path: clean });
302
345
  }
303
- function AppNav({ title, icon, groups, active, onNavigate, children }) {
346
+ function AppNav({ title, icon, groups, active, onNavigate, children, standalone }) {
304
347
  const onNavigateRef = useRef(onNavigate);
305
348
  onNavigateRef.current = onNavigate;
306
349
  const groupsRef = useRef(groups);
@@ -331,6 +374,7 @@ function AppNav({ title, icon, groups, active, onNavigate, children }) {
331
374
  );
332
375
  if (embedded) return null;
333
376
  const home = groups.flatMap((g) => g.items).find((i) => i.home);
377
+ if (standalone) return /* @__PURE__ */ jsx(Fragment, { children: standalone({ title, icon, groups, active, home, onNavigate }) });
334
378
  const brand = /* @__PURE__ */ jsxs(Fragment, { children: [
335
379
  (() => {
336
380
  const I = iconFor(icon);
@@ -505,6 +549,7 @@ a.cn-nav-brand:hover{background:var(--sunken,#f1f5f9)}
505
549
  }
506
550
  `;
507
551
  export {
552
+ AppChat,
508
553
  AppNav,
509
554
  HOST_BRIDGE_VERSION,
510
555
  NAV_COLORS,
@@ -515,6 +560,7 @@ export {
515
560
  openChat,
516
561
  reportLocation,
517
562
  useChatContext,
563
+ useHasChat,
518
564
  useHostChanges,
519
565
  useHostNavigate
520
566
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/index.tsx"],"sourcesContent":["/**\n * @clawnify/app/client — the host bridge for a Clawnify app's browser side.\n *\n * A Clawnify app is a full page on its own origin. Opened directly (local\n * `pnpm dev`, or `<slug>.apps.clawnify.com`) it renders its own sidebar.\n * Opened inside the Clawnify dashboard it sits in an iframe, and the dashboard\n * wants to render that same navigation in ITS sidebar so the user sees one\n * nav, not two.\n *\n * `<AppNav>` is the one definition of the app's navigation that serves both:\n *\n * • standalone → it draws the app's sidebar (desktop rail, mobile strip)\n * • embedded → it draws nothing and posts the nav tree to the host over\n * `postMessage`; the host renders it and posts `navigate`\n * back when the user clicks.\n *\n * Nothing about the app changes between the two: same router, same screens,\n * same data. Only who paints the list of links.\n *\n * import { AppNav, reportLocation } from \"@clawnify/app/client\";\n *\n * <AppNav\n * title=\"OpenDialer\"\n * icon=\"phone\"\n * groups={[{ label: \"Work\", items: [\n * { id: \"dashboard\", label: \"Dashboard\", href: \"/\", home: true },\n * { id: \"leads\", label: \"Leads\", href: \"/leads\", icon: \"users\", count: 12 },\n * ]}]}\n * active=\"leads\"\n * onNavigate={(item) => navigate(item.href!)}\n * />\n *\n * Re-render with a new `groups` / `active` and the host sidebar updates in\n * the same tick — counts are just a field, so \"Leads 12\" becomes \"Leads 11\"\n * the moment your own state does. No polling, no endpoint.\n *\n * Embedding is detected from the `clawnify_host=1` query parameter the\n * dashboard appends to the iframe URL, remembered in sessionStorage so a\n * full-page navigation inside the iframe stays in embedded mode. A page that\n * is iframed by anything else, or opened directly, is \"standalone\".\n *\n * The same channel connects the app to the dashboard's chat (the AG-UI idea:\n * the agent and the screen share one loop):\n *\n * openChat(\"Draft a follow-up for Mario\") opens the chat, text as a draft\n * setChatContext({ label, record }) what the user is looking at\n * useHostChanges((paths) => refetch()) the chat just wrote your data\n * useHostNavigate((path) => navigate(path)) the chat opens one of your pages\n *\n * Protocol (versioned, additive):\n *\n * app → host { source: \"clawnify-app\", v: 1, type: \"ready\", features? }\n * { source: \"clawnify-app\", v: 1, type: \"nav\", title, icon?, groups, active? }\n * { source: \"clawnify-app\", v: 1, type: \"location\", path }\n * { source: \"clawnify-app\", v: 1, type: \"context\", context }\n * { source: \"clawnify-app\", v: 1, type: \"chat\", prompt? }\n * host → app { source: \"clawnify-host\", v: 1, type: \"hello\" }\n * { source: \"clawnify-host\", v: 1, type: \"navigate\", id }\n * { source: \"clawnify-host\", v: 1, type: \"navigate\", path }\n * { source: \"clawnify-host\", v: 1, type: \"changed\", paths }\n *\n * `features` tells the host which of its messages the app handles, so it can\n * fall back for the ones it does not: without \"changes\" the host reloads the\n * iframe after the chat writes data, without \"path\" it reloads it at the new\n * path instead of asking the app's router.\n */\nimport {\n useEffect,\n useRef,\n type MouseEvent,\n type ReactNode,\n} from \"react\";\nimport {\n AddressBook,\n Archive,\n Bell,\n BookOpen,\n Briefcase,\n Building,\n Calendar,\n CalendarDots,\n Camera,\n ChartBar,\n ChartPie,\n ChatCircle,\n CheckSquare,\n ClipboardText,\n Clock,\n ClockCounterClockwise,\n Code,\n CreditCard,\n CurrencyDollar,\n Database,\n Envelope,\n File,\n FileText,\n Folder,\n Gear,\n Globe,\n GridFour,\n Hash,\n House,\n Image,\n Kanban,\n Lightning,\n Link,\n List,\n ListChecks,\n MagnifyingGlass,\n MapTrifold,\n Microphone,\n Package,\n PaperPlaneTilt,\n Phone,\n Play,\n Pulse,\n Robot,\n ShoppingCart,\n Sparkle,\n SquaresFour,\n Stack,\n Star,\n Table,\n Tag,\n Target,\n Tray,\n Truck,\n Upload,\n User,\n VideoCamera,\n Wallet,\n type Icon as PhosphorIcon,\n} from \"@phosphor-icons/react\";\n\nexport const HOST_BRIDGE_VERSION = 1 as const;\n\n/** One row in the navigation. `icon` is a lucide icon name (kebab-case). */\nexport interface AppNavItem {\n /** Stable identifier the host hands back on `navigate`. */\n id: string;\n label: string;\n /**\n * In-app path this item opens. Optional: a state-driven view can omit it.\n * When present the host uses it to restore the view on reload and to make\n * the dashboard URL shareable.\n */\n href?: string;\n /** Icon name from TILE_ICONS, e.g. \"layout-dashboard\". Unknown names render a dot. */\n icon?: string;\n /** Live badge, e.g. items waiting. Omit for none. */\n count?: number;\n /**\n * The app's home screen. Not listed as a row: standalone the brand row\n * opens it, and in the dashboard the app's own header does. One per app.\n */\n home?: boolean;\n /**\n * Tile colour for this section. Sections are the records inside an app,\n * so they draw as a small coloured tile with a white glyph (the app itself\n * is navigation and stays an outline glyph). One of `NAV_COLORS`; omitted\n * = picked from the palette by `id`, stable across renders and hosts.\n */\n color?: NavColor;\n}\n\n/** The section tile palette, shared by the standalone sidebar and the dashboard. */\nexport const NAV_COLORS = {\n red: \"#ef4444\",\n orange: \"#f97514\",\n amber: \"#f59e0b\",\n green: \"#22c55e\",\n sky: \"#17bde9\",\n blue: \"#266df0\",\n violet: \"#8b5cf6\",\n pink: \"#ec4899\",\n} as const;\nexport type NavColor = keyof typeof NAV_COLORS;\n\n/** Deterministic palette pick for an item without an explicit colour. */\nexport function navColorFor(id: string): NavColor {\n const names = Object.keys(NAV_COLORS) as NavColor[];\n let h = 0;\n for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;\n return names[h % names.length];\n}\n\n/**\n * The icon library: the names the Clawnify dashboard can draw for an app\n * and its sections (resolved to the Phosphor set in both sidebars). Pick\n * from these; the apps AGENTS.md lists them by meaning.\n */\nexport const TILE_ICONS = [\n \"activity\",\n \"archive\",\n \"bar-chart-3\",\n \"bell\",\n \"book-open\",\n \"bot\",\n \"briefcase\",\n \"building-2\",\n \"calendar\",\n \"calendar-days\",\n \"camera\",\n \"check-square\",\n \"clipboard-list\",\n \"clock\",\n \"code-2\",\n \"contact\",\n \"credit-card\",\n \"database\",\n \"dollar-sign\",\n \"file\",\n \"file-text\",\n \"folder\",\n \"globe\",\n \"hash\",\n \"history\",\n \"home\",\n \"image\",\n \"inbox\",\n \"kanban\",\n \"layers\",\n \"layout-dashboard\",\n \"layout-grid\",\n \"link\",\n \"list\",\n \"list-checks\",\n \"mail\",\n \"map\",\n \"message-square\",\n \"mic\",\n \"package\",\n \"phone\",\n \"pie-chart\",\n \"play\",\n \"search\",\n \"send\",\n \"settings\",\n \"shopping-cart\",\n \"sparkles\",\n \"star\",\n \"table\",\n \"tag\",\n \"target\",\n \"truck\",\n \"upload\",\n \"user\",\n \"users\",\n \"video\",\n \"wallet\",\n \"zap\",\n] as const;\nexport type TileIconName = (typeof TILE_ICONS)[number];\n\nexport interface TileIconProps {\n /** Icon name from TILE_ICONS. Unknown or missing = a small dot. */\n icon?: string;\n /** Palette colour. Omitted = picked from `seed` (or `icon`) by hash. */\n color?: NavColor;\n /** Stable string the colour is derived from when `color` is not given. */\n seed?: string;\n /** Tile edge in px. The glyph scales with it. Default 16. */\n size?: number;\n className?: string;\n title?: string;\n}\n\n/**\n * The record tile: a small rounded square in a palette colour with a white\n * lucide glyph. The sidebar draws every app section with it; apps can use\n * the same tile anywhere a record type needs an identity mark (table rows,\n * cards, pickers) so the two surfaces stay one system. Self-contained inline\n * styles, so it works outside <AppNav> and in any stylesheet setup.\n *\n * <TileIcon icon=\"users\" color=\"blue\" />\n * <TileIcon icon=\"phone\" seed={lead.id} size={20} />\n */\nexport function TileIcon({ icon, color, seed, size = 16, className, title }: TileIconProps) {\n const fill = NAV_COLORS[color ?? navColorFor(seed ?? icon ?? \"\")];\n // The glyph box is 0.7 of the tile: Phosphor draws on ~224 of its 256\n // units, so the visible glyph lands near 60% of the tile with clear\n // padding around it, solid white — Attio's record-tile proportion.\n const glyph = Math.round(size * 0.7);\n return (\n <span\n className={className}\n title={title}\n aria-hidden={title ? undefined : true}\n style={{\n display: \"inline-flex\",\n width: size,\n height: size,\n flexShrink: 0,\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"30%\",\n border: \"1px solid rgb(0 0 0 / 0.06)\",\n background: fill,\n color: \"#fff\",\n }}\n >\n {iconFor(icon) ? (\n (() => { const I = iconFor(icon)!; return <I size={glyph} weight=\"fill\" aria-hidden />; })()\n ) : (\n <span style={{ width: glyph * 0.6, height: glyph * 0.6, borderRadius: 9999, background: \"currentColor\" }} />\n )}\n </span>\n );\n}\n\nexport interface AppNavGroup {\n /** Eyebrow shown above the items. Omit for an unlabelled group. */\n label?: string;\n items: AppNavItem[];\n}\n\n/** Host messages this app handles itself. The host falls back for the rest. */\nexport type HostFeature = \"changes\" | \"path\";\n\n/**\n * What the user is looking at, for the dashboard's chat. It lets them say\n * \"this contact\" and be understood. Labels and ids only: the chat reads the\n * record itself through your API if it needs more.\n */\nexport interface ChatContext {\n /** The screen, e.g. \"Contact\" or \"Pipeline\". */\n label?: string;\n /** The record on screen, when there is one. */\n record?: { type: string; id: string; label?: string };\n}\n\nexport type AppToHostMessage =\n | { source: \"clawnify-app\"; v: 1; type: \"ready\"; features?: HostFeature[] }\n | {\n source: \"clawnify-app\";\n v: 1;\n type: \"nav\";\n title: string;\n /** lucide icon name for the app itself. */\n icon?: string;\n groups: AppNavGroup[];\n active?: string;\n }\n | { source: \"clawnify-app\"; v: 1; type: \"location\"; path: string }\n | { source: \"clawnify-app\"; v: 1; type: \"context\"; context: ChatContext | null }\n | { source: \"clawnify-app\"; v: 1; type: \"chat\"; prompt?: string };\n\nexport type HostToAppMessage =\n | { source: \"clawnify-host\"; v: 1; type: \"hello\" }\n | { source: \"clawnify-host\"; v: 1; type: \"navigate\"; id: string }\n | { source: \"clawnify-host\"; v: 1; type: \"navigate\"; path: string }\n | { source: \"clawnify-host\"; v: 1; type: \"changed\"; paths: string[] };\n\nconst HOST_PARAM = \"clawnify_host\";\nconst STORAGE_KEY = \"clawnify_host\";\n\n/**\n * Whether this page is running inside the Clawnify dashboard. Computed once at\n * module load — the query parameter is only on the initial URL — and remembered\n * for the tab so in-iframe full navigations keep the mode.\n */\nexport const embedded: boolean = (() => {\n if (typeof window === \"undefined\") return false;\n if (window.parent === window) return false;\n let flagged = false;\n try {\n flagged = new URLSearchParams(window.location.search).get(HOST_PARAM) === \"1\";\n if (flagged) window.sessionStorage.setItem(STORAGE_KEY, \"1\");\n else flagged = window.sessionStorage.getItem(STORAGE_KEY) === \"1\";\n } catch {\n // Storage can throw in a partitioned or locked-down iframe; the query\n // parameter alone still decides for this document.\n }\n return flagged;\n})();\n\nfunction post(message: AppToHostMessage) {\n if (!embedded) return;\n // The dashboard may live on app.clawnify.com or a white-label domain, so the\n // app cannot pin the target origin. The payload is navigation labels, a\n // path, record labels and a draft prompt — nothing secret — and the host\n // verifies the sender is its own iframe.\n window.parent.postMessage(message, \"*\");\n}\n\n// ── One channel for every SDK surface ──────────────────────────────────────\n// <AppNav>, the chat context and the host hooks share one window listener, so\n// an app can use any of them without the others (a single-screen app has no\n// <AppNav> but can still refresh when the chat writes its data).\n\ntype HostListener = (message: HostToAppMessage) => void;\nconst hostListeners = new Set<HostListener>();\n/** How many mounted hooks handle each feature; announced while above zero. */\nconst featureCounts = new Map<HostFeature, number>();\nlet listening = false;\nlet chatContext: ChatContext | null = null;\n\nfunction announce() {\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"ready\", features: [...featureCounts.keys()] });\n}\n\nfunction listen() {\n if (listening || !embedded) return;\n listening = true;\n window.addEventListener(\"message\", (e: MessageEvent) => {\n if (e.source !== window.parent) return;\n const d = e.data as HostToAppMessage | undefined;\n if (!d || d.source !== \"clawnify-host\" || d.v !== HOST_BRIDGE_VERSION) return;\n // Host (re)mounted its listener: it has lost what we told it.\n if (d.type === \"hello\" && chatContext) {\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"context\", context: chatContext });\n }\n for (const listener of [...hostListeners]) listener(d);\n });\n announce();\n}\n\nfunction subscribe(listener: HostListener, feature?: HostFeature): () => void {\n if (!embedded) return () => {};\n listen();\n hostListeners.add(listener);\n if (feature) {\n const n = featureCounts.get(feature) ?? 0;\n featureCounts.set(feature, n + 1);\n if (n === 0) announce();\n }\n return () => {\n hostListeners.delete(listener);\n if (!feature) return;\n const n = (featureCounts.get(feature) ?? 1) - 1;\n if (n > 0) featureCounts.set(feature, n);\n else {\n featureCounts.delete(feature);\n announce();\n }\n };\n}\n\n/** An in-app path the host may hand us: absolute, same origin. */\nfunction isAppPath(path: unknown): path is string {\n return typeof path === \"string\" && path.length <= 2048 && /^\\/(?![/\\\\])/.test(path);\n}\n\n/**\n * Open the dashboard's chat, optionally with a prompt in the composer. The\n * prompt is a DRAFT: the user reads it and presses send. The host never sends\n * text an app wrote, because an app renders data other people typed (a form\n * submission, a contact note) and that text must not drive the user's agent.\n *\n * Returns false when standalone (no dashboard, no chat): hide the button then,\n * or check `embedded` before rendering it.\n *\n * {embedded && <button onClick={() => openChat(`Draft a follow-up for ${c.name}`)}>Ask AI</button>}\n * {embedded && <button onClick={() => openChat()}>Chat</button>} // no prompt: just open it\n */\nexport function openChat(prompt?: string): boolean {\n if (!embedded) return false;\n post({\n source: \"clawnify-app\",\n v: HOST_BRIDGE_VERSION,\n type: \"chat\",\n ...(prompt ? { prompt: String(prompt).slice(0, 2000) } : {}),\n });\n return true;\n}\n\n/**\n * Tell the dashboard's chat what the user is looking at, so \"this contact\"\n * means the one on screen. Call it from the screen that shows the record; the\n * context clears when that screen unmounts. Labels and ids only, never the\n * record's content: the chat reads that through your API when it needs to.\n *\n * useChatContext({ label: \"Contact\", record: { type: \"contact\", id: c.id, label: c.name } });\n */\nexport function useChatContext(context: ChatContext | null) {\n const key = embedded && context ? JSON.stringify(sanitizeContext(context)) : \"\";\n useEffect(() => {\n if (!embedded || !key) return;\n listen();\n chatContext = JSON.parse(key) as ChatContext;\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"context\", context: chatContext });\n return () => {\n chatContext = null;\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"context\", context: null });\n };\n }, [key]);\n}\n\nfunction sanitizeContext(c: ChatContext): ChatContext {\n return {\n ...(c.label ? { label: String(c.label).slice(0, 60) } : {}),\n ...(c.record && c.record.type && c.record.id\n ? {\n record: {\n type: String(c.record.type).slice(0, 40),\n id: String(c.record.id).slice(0, 80),\n ...(c.record.label ? { label: String(c.record.label).slice(0, 80) } : {}),\n },\n }\n : {}),\n };\n}\n\n/**\n * Run `onChange` when the dashboard's chat has written this app's data (a\n * POST, PUT, PATCH or DELETE through your API), with the API paths it called.\n * Refetch what is on screen. While no mounted component uses this hook, the\n * dashboard reloads the whole iframe after such a write instead.\n *\n * useHostChanges(() => { void reload(); });\n */\nexport function useHostChanges(onChange: (paths: string[]) => void) {\n const ref = useRef(onChange);\n ref.current = onChange;\n useEffect(\n () =>\n subscribe((d) => {\n if (d.type !== \"changed\" || !Array.isArray(d.paths)) return;\n ref.current(d.paths.filter((p): p is string => typeof p === \"string\").slice(0, 20));\n }, \"changes\"),\n [],\n );\n}\n\n/**\n * Let the dashboard (its chat, a deep link) open any page of the app through\n * your router, without reloading the iframe. `path` is an in-app path such as\n * \"/contacts/42?tab=deals\". While no mounted component uses this hook, the\n * dashboard reloads the iframe at that path instead.\n *\n * useHostNavigate((path) => navigate(path));\n */\nexport function useHostNavigate(onNavigate: (path: string) => void) {\n const ref = useRef(onNavigate);\n ref.current = onNavigate;\n useEffect(\n () =>\n subscribe((d) => {\n if (d.type === \"navigate\" && \"path\" in d && isAppPath(d.path)) ref.current(d.path);\n }, \"path\"),\n [],\n );\n}\n\n/**\n * Tell the host where the app is, so the dashboard URL can restore this view\n * on reload. Call it whenever your router's location changes; a no-op when\n * standalone.\n */\nexport function reportLocation(path: string) {\n // The initial URL carries the dashboard's own parameters (the app token\n // and the host flag). They are not part of the app's location and must\n // never land in a URL the dashboard writes, so strip them here as well.\n let clean = path;\n try {\n const u = new URL(path, \"http://app.invalid\");\n u.searchParams.delete(\"token\");\n u.searchParams.delete(HOST_PARAM);\n clean = u.pathname + u.search + u.hash;\n } catch {\n // Not a path we can parse: send as-is, the host validates its shape.\n }\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"location\", path: clean });\n}\n\nexport interface AppNavProps {\n /** App name, shown in the brand row standalone and in the host's switcher. */\n title: string;\n /**\n * The app's own glyph, a lucide icon name (kebab-case). Drawn as an\n * outline icon with no background: in the standalone brand row, and by the\n * dashboard on the app's sidebar item. Tiles are for records, not for\n * navigation, so the app's coloured tile icon stays out of the sidebar.\n */\n icon?: string;\n groups: AppNavGroup[];\n /** `id` of the active item. */\n active?: string;\n /** Called with the item the user chose, from either renderer. */\n onNavigate: (item: AppNavItem) => void;\n /** Rendered at the bottom of the standalone sidebar (a footer action, say). */\n children?: ReactNode;\n}\n\n/**\n * The app's navigation: sidebar when standalone, host-fed when embedded.\n *\n * Standalone layout: the sidebar is a 260px column at ≥768px and a horizontal\n * strip below that, so put it first inside a `flex flex-col md:flex-row`\n * container. It paints with the app's own theme tokens (`--surface`,\n * `--border`, `--primary`…) and falls back to the platform defaults when a\n * token is missing.\n */\nexport function AppNav({ title, icon, groups, active, onNavigate, children }: AppNavProps) {\n // Always the latest callback, so the host listener never calls a stale one.\n const onNavigateRef = useRef(onNavigate);\n onNavigateRef.current = onNavigate;\n const groupsRef = useRef(groups);\n groupsRef.current = groups;\n\n // Embedded: publish the tree whenever it changes. Serialising is the cheap\n // way to dedupe — apps build `groups` inline every render.\n const payload = embedded\n ? JSON.stringify({ title, icon, groups: sanitize(groups), active })\n : \"\";\n const lastSent = useRef(\"\");\n useEffect(() => {\n if (!embedded || payload === lastSent.current) return;\n lastSent.current = payload;\n const { title, icon, groups, active } = JSON.parse(payload) as {\n title: string;\n icon?: string;\n groups: AppNavGroup[];\n active?: string;\n };\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"nav\", title, icon, groups, active });\n }, [payload]);\n\n // Embedded: handshake + listen for the host's clicks.\n useEffect(\n () =>\n subscribe((d) => {\n if (d.type === \"hello\") {\n // Host (re)mounted its listener after we first published: resend.\n lastSent.current = \"\";\n const g = sanitize(groupsRef.current);\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"nav\", title, icon, groups: g, active });\n } else if (d.type === \"navigate\" && \"id\" in d && typeof d.id === \"string\") {\n const item = groupsRef.current.flatMap((g) => g.items).find((i) => i.id === d.id);\n if (item) onNavigateRef.current(item);\n }\n }),\n // title/active are read from the closure only on `hello`; the publish\n // effect above covers their normal updates.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n );\n\n if (embedded) return null;\n\n const home = groups.flatMap((g) => g.items).find((i) => i.home);\n const brand = (\n <>\n {(() => { const I = iconFor(icon); return I ? <I size={16} aria-hidden /> : null; })()}\n <span>{title}</span>\n </>\n );\n\n return (\n <aside className=\"cn-nav\">\n <style>{CSS}</style>\n {home ? (\n <a\n href={home.href ?? \"/\"}\n className=\"cn-nav-brand\"\n data-active={home.id === active || undefined}\n aria-current={home.id === active ? \"page\" : undefined}\n onClick={(e: MouseEvent) => {\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;\n e.preventDefault();\n onNavigate(home);\n }}\n >\n {brand}\n </a>\n ) : (\n <div className=\"cn-nav-brand\">{brand}</div>\n )}\n <nav className=\"cn-nav-groups\" aria-label=\"Primary\">\n {groups.map((g, gi) => (\n <div className=\"cn-nav-group\" key={g.label ?? gi}>\n {g.label && <p className=\"cn-nav-eyebrow\">{g.label}</p>}\n {g.items\n .filter((item) => !item.home)\n .map((item) => (\n <NavRow key={item.id} item={item} active={item.id === active} onNavigate={onNavigate} />\n ))}\n </div>\n ))}\n </nav>\n {children && <div className=\"cn-nav-footer\">{children}</div>}\n </aside>\n );\n}\n\nfunction NavRow({\n item,\n active,\n onNavigate,\n}: {\n item: AppNavItem;\n active: boolean;\n onNavigate: (item: AppNavItem) => void;\n}) {\n const body = (\n <>\n <TileIcon icon={item.icon} color={item.color} seed={item.id} />\n <span className=\"cn-nav-label\">{item.label}</span>\n {typeof item.count === \"number\" && <span className=\"cn-nav-count\">{item.count}</span>}\n </>\n );\n // A real link when there is an href — middle-click and copy-link keep\n // working — but the click itself goes through the app's router.\n if (item.href) {\n return (\n <a\n href={item.href}\n className=\"cn-nav-item\"\n data-active={active || undefined}\n aria-current={active ? \"page\" : undefined}\n onClick={(e: MouseEvent) => {\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;\n e.preventDefault();\n onNavigate(item);\n }}\n >\n {body}\n </a>\n );\n }\n return (\n <button\n type=\"button\"\n className=\"cn-nav-item\"\n data-active={active || undefined}\n aria-current={active ? \"page\" : undefined}\n onClick={() => onNavigate(item)}\n >\n {body}\n </button>\n );\n}\n\n/** Our names → Phosphor components; the same map the dashboard holds. */\nconst ICONS: Record<string, PhosphorIcon> = {\n \"activity\": Pulse,\n \"archive\": Archive,\n \"bar-chart-3\": ChartBar,\n \"bell\": Bell,\n \"book-open\": BookOpen,\n \"bot\": Robot,\n \"briefcase\": Briefcase,\n \"building-2\": Building,\n \"calendar\": Calendar,\n \"calendar-days\": CalendarDots,\n \"camera\": Camera,\n \"check-square\": CheckSquare,\n \"clipboard-list\": ClipboardText,\n \"clock\": Clock,\n \"code-2\": Code,\n \"contact\": AddressBook,\n \"credit-card\": CreditCard,\n \"database\": Database,\n \"dollar-sign\": CurrencyDollar,\n \"file\": File,\n \"file-text\": FileText,\n \"folder\": Folder,\n \"globe\": Globe,\n \"hash\": Hash,\n \"history\": ClockCounterClockwise,\n \"home\": House,\n \"image\": Image,\n \"inbox\": Tray,\n \"kanban\": Kanban,\n \"layers\": Stack,\n \"layout-dashboard\": SquaresFour,\n \"layout-grid\": GridFour,\n \"link\": Link,\n \"list\": List,\n \"list-checks\": ListChecks,\n \"mail\": Envelope,\n \"map\": MapTrifold,\n \"message-square\": ChatCircle,\n \"mic\": Microphone,\n \"package\": Package,\n \"phone\": Phone,\n \"pie-chart\": ChartPie,\n \"play\": Play,\n \"search\": MagnifyingGlass,\n \"send\": PaperPlaneTilt,\n \"settings\": Gear,\n \"shopping-cart\": ShoppingCart,\n \"sparkles\": Sparkle,\n \"star\": Star,\n \"table\": Table,\n \"tag\": Tag,\n \"target\": Target,\n \"truck\": Truck,\n \"upload\": Upload,\n \"user\": User,\n \"users\": User,\n \"video\": VideoCamera,\n \"wallet\": Wallet,\n \"zap\": Lightning,\n};\nfunction iconFor(name?: string): PhosphorIcon | undefined {\n return name ? ICONS[name] : undefined;\n}\n\n/** Only the fields the protocol defines, with sane bounds, cross the bridge. */\nfunction sanitize(groups: AppNavGroup[]): AppNavGroup[] {\n return groups.slice(0, 20).map((g) => ({\n ...(g.label ? { label: String(g.label).slice(0, 40) } : {}),\n items: g.items.slice(0, 100).map((i) => ({\n id: String(i.id),\n label: String(i.label).slice(0, 60),\n ...(i.href ? { href: String(i.href) } : {}),\n ...(i.icon ? { icon: String(i.icon) } : {}),\n ...(typeof i.count === \"number\" && Number.isFinite(i.count) ? { count: i.count } : {}),\n ...(i.home ? { home: true } : {}),\n ...(i.color && i.color in NAV_COLORS ? { color: i.color } : {}),\n })),\n }));\n}\n\n// Scoped by class prefix; tokens follow apps/DESIGN.md with its light defaults\n// as fallbacks, so an app with a partial theme still gets a readable sidebar.\nconst CSS = `\n.cn-nav{width:16.25rem;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border,#e2e8f0);background:var(--surface,#fff);color:var(--foreground,#1a202c)}\n.cn-nav-brand{height:3.5rem;flex-shrink:0;display:flex;align-items:center;gap:.5rem;padding:0 1.25rem;border-bottom:1px solid var(--border,#e2e8f0);font-size:.875rem;font-weight:600;color:inherit;text-decoration:none}\na.cn-nav-brand:hover{background:var(--sunken,#f1f5f9)}\n.cn-nav-brand>svg{color:var(--primary,#dd5164)}\n.cn-nav-groups{flex:1;overflow-y:auto;padding:.75rem}\n.cn-nav-group+.cn-nav-group{margin-top:1rem}\n.cn-nav-eyebrow{margin:0 0 .25rem;padding:0 .625rem;font-size:.75rem;font-weight:500;color:var(--muted,#475569)}\n.cn-nav-item{display:flex;align-items:center;gap:.5rem;width:100%;margin:0 0 .125rem;padding:.4375rem .625rem;border:0;border-radius:.375rem;background:none;color:inherit;font:inherit;font-size:.875rem;text-align:left;text-decoration:none;cursor:pointer}\n.cn-nav-item:hover{background:var(--sunken,#f1f5f9)}\n.cn-nav-item[data-active]{background:color-mix(in srgb,var(--primary,#dd5164) 12%,transparent);color:var(--primary,#dd5164);font-weight:600}\n.cn-nav-item:focus-visible{outline:2px solid var(--ring,#2563eb);outline-offset:-2px}\n.cn-nav-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.cn-nav-count{font-size:.75rem;font-variant-numeric:tabular-nums;color:var(--muted,#475569)}\n.cn-nav-item[data-active] .cn-nav-count{color:inherit}\n.cn-nav-footer{padding:.75rem;border-top:1px solid var(--border,#e2e8f0)}\n@media (max-width:767px){\n.cn-nav{width:100%;flex-direction:row;border-right:0;border-bottom:1px solid var(--border,#e2e8f0)}\n.cn-nav-brand,.cn-nav-eyebrow,.cn-nav-footer{display:none}\n.cn-nav-groups{display:flex;gap:.25rem;padding:.5rem .75rem;overflow-x:auto;overflow-y:hidden}\n.cn-nav-group{display:flex;gap:.25rem}\n.cn-nav-group+.cn-nav-group{margin:0}\n.cn-nav-item{width:auto;margin:0;white-space:nowrap}\n}\n`;\n"],"mappings":"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","names":["title","icon","groups","active"]}
1
+ {"version":3,"sources":["../../src/client/index.tsx"],"sourcesContent":["/**\n * @clawnify/app/client — the host bridge for a Clawnify app's browser side.\n *\n * A Clawnify app is a full page on its own origin. Opened directly (local\n * `pnpm dev`, or `<slug>.apps.clawnify.com`) it renders its own sidebar.\n * Opened inside the Clawnify dashboard it sits in an iframe, and the dashboard\n * wants to render that same navigation in ITS sidebar so the user sees one\n * nav, not two.\n *\n * `<AppNav>` is the one definition of the app's navigation that serves both:\n *\n * • standalone → it draws the app's sidebar (desktop rail, mobile strip)\n * • embedded → it draws nothing and posts the nav tree to the host over\n * `postMessage`; the host renders it and posts `navigate`\n * back when the user clicks.\n *\n * Nothing about the app changes between the two: same router, same screens,\n * same data. Only who paints the list of links.\n *\n * import { AppNav, reportLocation } from \"@clawnify/app/client\";\n *\n * <AppNav\n * title=\"OpenDialer\"\n * icon=\"phone\"\n * groups={[{ label: \"Work\", items: [\n * { id: \"dashboard\", label: \"Dashboard\", href: \"/\", home: true },\n * { id: \"leads\", label: \"Leads\", href: \"/leads\", icon: \"users\", count: 12 },\n * ]}]}\n * active=\"leads\"\n * onNavigate={(item) => navigate(item.href!)}\n * />\n *\n * Re-render with a new `groups` / `active` and the host sidebar updates in\n * the same tick — counts are just a field, so \"Leads 12\" becomes \"Leads 11\"\n * the moment your own state does. No polling, no endpoint.\n *\n * Embedding is detected from the `clawnify_host=1` query parameter the\n * dashboard appends to the iframe URL, remembered in sessionStorage so a\n * full-page navigation inside the iframe stays in embedded mode. A page that\n * is iframed by anything else, or opened directly, is \"standalone\".\n *\n * The same channel connects the app to the dashboard's chat (the AG-UI idea:\n * the agent and the screen share one loop):\n *\n * openChat(\"Draft a follow-up for Mario\") opens the chat, text as a draft\n * setChatContext({ label, record }) what the user is looking at\n * useHostChanges((paths) => refetch()) the chat just wrote your data\n * useHostNavigate((path) => navigate(path)) the chat opens one of your pages\n *\n * Protocol (versioned, additive):\n *\n * app → host { source: \"clawnify-app\", v: 1, type: \"ready\", features? }\n * { source: \"clawnify-app\", v: 1, type: \"nav\", title, icon?, groups, active? }\n * { source: \"clawnify-app\", v: 1, type: \"location\", path }\n * { source: \"clawnify-app\", v: 1, type: \"context\", context }\n * { source: \"clawnify-app\", v: 1, type: \"chat\", prompt? }\n * host → app { source: \"clawnify-host\", v: 1, type: \"hello\" }\n * { source: \"clawnify-host\", v: 1, type: \"navigate\", id }\n * { source: \"clawnify-host\", v: 1, type: \"navigate\", path }\n * { source: \"clawnify-host\", v: 1, type: \"changed\", paths }\n *\n * Outside Clawnify there is no dashboard, so both surfaces fall back to the\n * app's own: `<AppNav standalone>` draws the app's own sidebar from the same\n * nav data, and `<AppChat standalone>` is where `openChat` and\n * `useChatContext` land, a panel the app wires to its own chat endpoint.\n *\n * `features` tells the host which of its messages the app handles, so it can\n * fall back for the ones it does not: without \"changes\" the host reloads the\n * iframe after the chat writes data, without \"path\" it reloads it at the new\n * path instead of asking the app's router.\n */\nimport {\n useEffect,\n useRef,\n useState,\n type MouseEvent,\n type ReactNode,\n} from \"react\";\nimport {\n AddressBook,\n Archive,\n Bell,\n BookOpen,\n Briefcase,\n Building,\n Calendar,\n CalendarDots,\n Camera,\n ChartBar,\n ChartPie,\n ChatCircle,\n CheckSquare,\n ClipboardText,\n Clock,\n ClockCounterClockwise,\n Code,\n CreditCard,\n CurrencyDollar,\n Database,\n Envelope,\n File,\n FileText,\n Folder,\n Gear,\n Globe,\n GridFour,\n Hash,\n House,\n Image,\n Kanban,\n Lightning,\n Link,\n List,\n ListChecks,\n MagnifyingGlass,\n MapTrifold,\n Microphone,\n Package,\n PaperPlaneTilt,\n Phone,\n Play,\n Pulse,\n Robot,\n ShoppingCart,\n Sparkle,\n SquaresFour,\n Stack,\n Star,\n Table,\n Tag,\n Target,\n Tray,\n Truck,\n Upload,\n User,\n VideoCamera,\n Wallet,\n type Icon as PhosphorIcon,\n} from \"@phosphor-icons/react\";\n\nexport const HOST_BRIDGE_VERSION = 1 as const;\n\n/** One row in the navigation. `icon` is a lucide icon name (kebab-case). */\nexport interface AppNavItem {\n /** Stable identifier the host hands back on `navigate`. */\n id: string;\n label: string;\n /**\n * In-app path this item opens. Optional: a state-driven view can omit it.\n * When present the host uses it to restore the view on reload and to make\n * the dashboard URL shareable.\n */\n href?: string;\n /** Icon name from TILE_ICONS, e.g. \"layout-dashboard\". Unknown names render a dot. */\n icon?: string;\n /** Live badge, e.g. items waiting. Omit for none. */\n count?: number;\n /**\n * The app's home screen. Not listed as a row: standalone the brand row\n * opens it, and in the dashboard the app's own header does. One per app.\n */\n home?: boolean;\n /**\n * Tile colour for this section. Sections are the records inside an app,\n * so they draw as a small coloured tile with a white glyph (the app itself\n * is navigation and stays an outline glyph). One of `NAV_COLORS`; omitted\n * = picked from the palette by `id`, stable across renders and hosts.\n */\n color?: NavColor;\n}\n\n/** The section tile palette, shared by the standalone sidebar and the dashboard. */\nexport const NAV_COLORS = {\n red: \"#ef4444\",\n orange: \"#f97514\",\n amber: \"#f59e0b\",\n green: \"#22c55e\",\n sky: \"#17bde9\",\n blue: \"#266df0\",\n violet: \"#8b5cf6\",\n pink: \"#ec4899\",\n} as const;\nexport type NavColor = keyof typeof NAV_COLORS;\n\n/** Deterministic palette pick for an item without an explicit colour. */\nexport function navColorFor(id: string): NavColor {\n const names = Object.keys(NAV_COLORS) as NavColor[];\n let h = 0;\n for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;\n return names[h % names.length];\n}\n\n/**\n * The icon library: the names the Clawnify dashboard can draw for an app\n * and its sections (resolved to the Phosphor set in both sidebars). Pick\n * from these; the apps AGENTS.md lists them by meaning.\n */\nexport const TILE_ICONS = [\n \"activity\",\n \"archive\",\n \"bar-chart-3\",\n \"bell\",\n \"book-open\",\n \"bot\",\n \"briefcase\",\n \"building-2\",\n \"calendar\",\n \"calendar-days\",\n \"camera\",\n \"check-square\",\n \"clipboard-list\",\n \"clock\",\n \"code-2\",\n \"contact\",\n \"credit-card\",\n \"database\",\n \"dollar-sign\",\n \"file\",\n \"file-text\",\n \"folder\",\n \"globe\",\n \"hash\",\n \"history\",\n \"home\",\n \"image\",\n \"inbox\",\n \"kanban\",\n \"layers\",\n \"layout-dashboard\",\n \"layout-grid\",\n \"link\",\n \"list\",\n \"list-checks\",\n \"mail\",\n \"map\",\n \"message-square\",\n \"mic\",\n \"package\",\n \"phone\",\n \"pie-chart\",\n \"play\",\n \"search\",\n \"send\",\n \"settings\",\n \"shopping-cart\",\n \"sparkles\",\n \"star\",\n \"table\",\n \"tag\",\n \"target\",\n \"truck\",\n \"upload\",\n \"user\",\n \"users\",\n \"video\",\n \"wallet\",\n \"zap\",\n] as const;\nexport type TileIconName = (typeof TILE_ICONS)[number];\n\nexport interface TileIconProps {\n /** Icon name from TILE_ICONS. Unknown or missing = a small dot. */\n icon?: string;\n /** Palette colour. Omitted = picked from `seed` (or `icon`) by hash. */\n color?: NavColor;\n /** Stable string the colour is derived from when `color` is not given. */\n seed?: string;\n /** Tile edge in px. The glyph scales with it. Default 16. */\n size?: number;\n className?: string;\n title?: string;\n}\n\n/**\n * The record tile: a small rounded square in a palette colour with a white\n * lucide glyph. The sidebar draws every app section with it; apps can use\n * the same tile anywhere a record type needs an identity mark (table rows,\n * cards, pickers) so the two surfaces stay one system. Self-contained inline\n * styles, so it works outside <AppNav> and in any stylesheet setup.\n *\n * <TileIcon icon=\"users\" color=\"blue\" />\n * <TileIcon icon=\"phone\" seed={lead.id} size={20} />\n */\nexport function TileIcon({ icon, color, seed, size = 16, className, title }: TileIconProps) {\n const fill = NAV_COLORS[color ?? navColorFor(seed ?? icon ?? \"\")];\n // The glyph box is 0.7 of the tile: Phosphor draws on ~224 of its 256\n // units, so the visible glyph lands near 60% of the tile with clear\n // padding around it, solid white — Attio's record-tile proportion.\n const glyph = Math.round(size * 0.7);\n return (\n <span\n className={className}\n title={title}\n aria-hidden={title ? undefined : true}\n style={{\n display: \"inline-flex\",\n width: size,\n height: size,\n flexShrink: 0,\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"30%\",\n border: \"1px solid rgb(0 0 0 / 0.06)\",\n background: fill,\n color: \"#fff\",\n }}\n >\n {iconFor(icon) ? (\n (() => { const I = iconFor(icon)!; return <I size={glyph} weight=\"fill\" aria-hidden />; })()\n ) : (\n <span style={{ width: glyph * 0.6, height: glyph * 0.6, borderRadius: 9999, background: \"currentColor\" }} />\n )}\n </span>\n );\n}\n\nexport interface AppNavGroup {\n /** Eyebrow shown above the items. Omit for an unlabelled group. */\n label?: string;\n items: AppNavItem[];\n}\n\n/** Host messages this app handles itself. The host falls back for the rest. */\nexport type HostFeature = \"changes\" | \"path\";\n\n/**\n * What the user is looking at, for the dashboard's chat. It lets them say\n * \"this contact\" and be understood. Labels and ids only: the chat reads the\n * record itself through your API if it needs more.\n */\nexport interface ChatContext {\n /** The screen, e.g. \"Contact\" or \"Pipeline\". */\n label?: string;\n /** The record on screen, when there is one. */\n record?: { type: string; id: string; label?: string };\n}\n\nexport type AppToHostMessage =\n | { source: \"clawnify-app\"; v: 1; type: \"ready\"; features?: HostFeature[] }\n | {\n source: \"clawnify-app\";\n v: 1;\n type: \"nav\";\n title: string;\n /** lucide icon name for the app itself. */\n icon?: string;\n groups: AppNavGroup[];\n active?: string;\n }\n | { source: \"clawnify-app\"; v: 1; type: \"location\"; path: string }\n | { source: \"clawnify-app\"; v: 1; type: \"context\"; context: ChatContext | null }\n | { source: \"clawnify-app\"; v: 1; type: \"chat\"; prompt?: string };\n\nexport type HostToAppMessage =\n | { source: \"clawnify-host\"; v: 1; type: \"hello\" }\n | { source: \"clawnify-host\"; v: 1; type: \"navigate\"; id: string }\n | { source: \"clawnify-host\"; v: 1; type: \"navigate\"; path: string }\n | { source: \"clawnify-host\"; v: 1; type: \"changed\"; paths: string[] };\n\nconst HOST_PARAM = \"clawnify_host\";\nconst STORAGE_KEY = \"clawnify_host\";\n\n/**\n * Whether this page is running inside the Clawnify dashboard. Computed once at\n * module load — the query parameter is only on the initial URL — and remembered\n * for the tab so in-iframe full navigations keep the mode.\n */\nexport const embedded: boolean = (() => {\n if (typeof window === \"undefined\") return false;\n if (window.parent === window) return false;\n let flagged = false;\n try {\n flagged = new URLSearchParams(window.location.search).get(HOST_PARAM) === \"1\";\n if (flagged) window.sessionStorage.setItem(STORAGE_KEY, \"1\");\n else flagged = window.sessionStorage.getItem(STORAGE_KEY) === \"1\";\n } catch {\n // Storage can throw in a partitioned or locked-down iframe; the query\n // parameter alone still decides for this document.\n }\n return flagged;\n})();\n\nfunction post(message: AppToHostMessage) {\n if (!embedded) return;\n // The dashboard may live on app.clawnify.com or a white-label domain, so the\n // app cannot pin the target origin. The payload is navigation labels, a\n // path, record labels and a draft prompt — nothing secret — and the host\n // verifies the sender is its own iframe.\n window.parent.postMessage(message, \"*\");\n}\n\n// ── One channel for every SDK surface ──────────────────────────────────────\n// <AppNav>, the chat context and the host hooks share one window listener, so\n// an app can use any of them without the others (a single-screen app has no\n// <AppNav> but can still refresh when the chat writes its data).\n\ntype HostListener = (message: HostToAppMessage) => void;\nconst hostListeners = new Set<HostListener>();\n/** How many mounted hooks handle each feature; announced while above zero. */\nconst featureCounts = new Map<HostFeature, number>();\nlet listening = false;\nlet chatContext: ChatContext | null = null;\n\nfunction announce() {\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"ready\", features: [...featureCounts.keys()] });\n}\n\nfunction listen() {\n if (listening || !embedded) return;\n listening = true;\n window.addEventListener(\"message\", (e: MessageEvent) => {\n if (e.source !== window.parent) return;\n const d = e.data as HostToAppMessage | undefined;\n if (!d || d.source !== \"clawnify-host\" || d.v !== HOST_BRIDGE_VERSION) return;\n // Host (re)mounted its listener: it has lost what we told it.\n if (d.type === \"hello\" && chatContext) {\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"context\", context: chatContext });\n }\n for (const listener of [...hostListeners]) listener(d);\n });\n announce();\n}\n\nfunction subscribe(listener: HostListener, feature?: HostFeature): () => void {\n if (!embedded) return () => {};\n listen();\n hostListeners.add(listener);\n if (feature) {\n const n = featureCounts.get(feature) ?? 0;\n featureCounts.set(feature, n + 1);\n if (n === 0) announce();\n }\n return () => {\n hostListeners.delete(listener);\n if (!feature) return;\n const n = (featureCounts.get(feature) ?? 1) - 1;\n if (n > 0) featureCounts.set(feature, n);\n else {\n featureCounts.delete(feature);\n announce();\n }\n };\n}\n\n/** An in-app path the host may hand us: absolute, same origin. */\nfunction isAppPath(path: unknown): path is string {\n return typeof path === \"string\" && path.length <= 2048 && /^\\/(?![/\\\\])/.test(path);\n}\n\n// ── Standalone chat: where openChat lands outside Clawnify ────────────────\n// One mounted <AppChat standalone> registers itself here. Listeners keep\n// useHasChat and the panel's context in step without a React provider, so the\n// hooks work from any component, however deep.\n\ntype StandaloneOpen = (draft: string | null) => void;\nlet standaloneOpen: StandaloneOpen | null = null;\nconst chatListeners = new Set<() => void>();\nfunction notifyChat() {\n for (const l of [...chatListeners]) l();\n}\nfunction useChatVersion() {\n const [, setVersion] = useState(0);\n useEffect(() => {\n const l = () => setVersion((v) => v + 1);\n chatListeners.add(l);\n return () => {\n chatListeners.delete(l);\n };\n }, []);\n}\n\n/**\n * Open the chat, optionally with a prompt in its composer. Inside Clawnify\n * that is the dashboard's chat; outside, the app's own `<AppChat standalone>`\n * panel. The prompt is a DRAFT: the user reads it and presses send. It is\n * never sent for them, because an app renders data other people typed (a form\n * submission, a contact note) and that text must not drive anyone's agent.\n *\n * Returns false when there is no chat to open (standalone with no `<AppChat>`\n * mounted). Render the button behind `useHasChat()`:\n *\n * const hasChat = useHasChat();\n * {hasChat && <button onClick={() => openChat(`Draft a follow-up for ${c.name}`)}>Ask AI</button>}\n * {hasChat && <button onClick={() => openChat()}>Chat</button>} // no prompt: just open it\n */\nexport function openChat(prompt?: string): boolean {\n const draft = prompt ? String(prompt).slice(0, 2000) : null;\n if (!embedded) {\n if (!standaloneOpen) return false;\n standaloneOpen(draft);\n return true;\n }\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"chat\", ...(draft ? { prompt: draft } : {}) });\n return true;\n}\n\n/** Whether `openChat` has somewhere to go: the dashboard, or a mounted `<AppChat>`. */\nexport function useHasChat(): boolean {\n useChatVersion();\n return embedded || standaloneOpen !== null;\n}\n\n/**\n * Tell the dashboard's chat what the user is looking at, so \"this contact\"\n * means the one on screen. Call it from the screen that shows the record; the\n * context clears when that screen unmounts. Labels and ids only, never the\n * record's content: the chat reads that through your API when it needs to.\n *\n * useChatContext({ label: \"Contact\", record: { type: \"contact\", id: c.id, label: c.name } });\n */\nexport function useChatContext(context: ChatContext | null) {\n const key = context ? JSON.stringify(sanitizeContext(context)) : \"\";\n useEffect(() => {\n if (!key) return;\n listen();\n chatContext = JSON.parse(key) as ChatContext;\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"context\", context: chatContext });\n notifyChat();\n return () => {\n chatContext = null;\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"context\", context: null });\n notifyChat();\n };\n }, [key]);\n}\n\nfunction sanitizeContext(c: ChatContext): ChatContext {\n return {\n ...(c.label ? { label: String(c.label).slice(0, 60) } : {}),\n ...(c.record && c.record.type && c.record.id\n ? {\n record: {\n type: String(c.record.type).slice(0, 40),\n id: String(c.record.id).slice(0, 80),\n ...(c.record.label ? { label: String(c.record.label).slice(0, 80) } : {}),\n },\n }\n : {}),\n };\n}\n\n/** What `<AppChat standalone>` hands the app's own chat panel. */\nexport interface StandaloneChat {\n open: boolean;\n /**\n * The last prompt `openChat` passed, for the composer. A new object on every\n * call (compare by identity, or by `id`), so the same prompt twice prefills\n * twice. Null when opened without one.\n */\n draft: { id: number; text: string } | null;\n /** What `useChatContext` reports is on screen; send it with each message. */\n context: ChatContext | null;\n close: () => void;\n}\n\nexport interface AppChatProps {\n /**\n * The app's own chat, drawn only outside Clawnify. It talks to the app's own\n * endpoint (`/api/chat` with the app's own model key, say). Inside the\n * dashboard nothing renders and `openChat` opens the dashboard's chat.\n */\n standalone: (chat: StandaloneChat) => ReactNode;\n}\n\n/**\n * Where `openChat` lands when the app runs outside Clawnify. Mount it once,\n * near the root:\n *\n * <AppChat standalone={(chat) => <MyChatPanel {...chat} />} />\n *\n * Mount at most one: the last one mounted receives `openChat`.\n */\nexport function AppChat({ standalone }: AppChatProps) {\n const [open, setOpen] = useState(false);\n const [draft, setDraft] = useState<StandaloneChat[\"draft\"]>(null);\n const nextId = useRef(0);\n useChatVersion();\n useEffect(() => {\n if (embedded) return;\n const handler: StandaloneOpen = (text) => {\n setDraft(text ? { id: ++nextId.current, text } : null);\n setOpen(true);\n };\n standaloneOpen = handler;\n notifyChat();\n return () => {\n if (standaloneOpen === handler) standaloneOpen = null;\n notifyChat();\n };\n }, []);\n if (embedded) return null;\n return <>{standalone({ open, draft, context: chatContext, close: () => setOpen(false) })}</>;\n}\n\n/**\n * Run `onChange` when the dashboard's chat has written this app's data (a\n * POST, PUT, PATCH or DELETE through your API), with the API paths it called.\n * Refetch what is on screen. While no mounted component uses this hook, the\n * dashboard reloads the whole iframe after such a write instead.\n *\n * useHostChanges(() => { void reload(); });\n */\nexport function useHostChanges(onChange: (paths: string[]) => void) {\n const ref = useRef(onChange);\n ref.current = onChange;\n useEffect(\n () =>\n subscribe((d) => {\n if (d.type !== \"changed\" || !Array.isArray(d.paths)) return;\n ref.current(d.paths.filter((p): p is string => typeof p === \"string\").slice(0, 20));\n }, \"changes\"),\n [],\n );\n}\n\n/**\n * Let the dashboard (its chat, a deep link) open any page of the app through\n * your router, without reloading the iframe. `path` is an in-app path such as\n * \"/contacts/42?tab=deals\". While no mounted component uses this hook, the\n * dashboard reloads the iframe at that path instead.\n *\n * useHostNavigate((path) => navigate(path));\n */\nexport function useHostNavigate(onNavigate: (path: string) => void) {\n const ref = useRef(onNavigate);\n ref.current = onNavigate;\n useEffect(\n () =>\n subscribe((d) => {\n if (d.type === \"navigate\" && \"path\" in d && isAppPath(d.path)) ref.current(d.path);\n }, \"path\"),\n [],\n );\n}\n\n/**\n * Tell the host where the app is, so the dashboard URL can restore this view\n * on reload. Call it whenever your router's location changes; a no-op when\n * standalone.\n */\nexport function reportLocation(path: string) {\n // The initial URL carries the dashboard's own parameters (the app token\n // and the host flag). They are not part of the app's location and must\n // never land in a URL the dashboard writes, so strip them here as well.\n let clean = path;\n try {\n const u = new URL(path, \"http://app.invalid\");\n u.searchParams.delete(\"token\");\n u.searchParams.delete(HOST_PARAM);\n clean = u.pathname + u.search + u.hash;\n } catch {\n // Not a path we can parse: send as-is, the host validates its shape.\n }\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"location\", path: clean });\n}\n\nexport interface AppNavProps {\n /** App name, shown in the brand row standalone and in the host's switcher. */\n title: string;\n /**\n * The app's own glyph, a lucide icon name (kebab-case). Drawn as an\n * outline icon with no background: in the standalone brand row, and by the\n * dashboard on the app's sidebar item. Tiles are for records, not for\n * navigation, so the app's coloured tile icon stays out of the sidebar.\n */\n icon?: string;\n groups: AppNavGroup[];\n /** `id` of the active item. */\n active?: string;\n /** Called with the item the user chose, from either renderer. */\n onNavigate: (item: AppNavItem) => void;\n /** Rendered at the bottom of the built-in sidebar (a footer action, say). */\n children?: ReactNode;\n /**\n * The app's own sidebar, drawn instead of the built-in one outside\n * Clawnify, from the same nav data (so the navigation is still defined\n * once). Inside the dashboard it never renders: the dashboard draws the nav.\n *\n * standalone={(nav) => <MySidebar groups={nav.groups} active={nav.active} onSelect={nav.onNavigate} />}\n */\n standalone?: (nav: StandaloneNav) => ReactNode;\n}\n\n/** What `<AppNav standalone>` hands the app's own sidebar. */\nexport interface StandaloneNav {\n title: string;\n icon?: string;\n groups: AppNavGroup[];\n active?: string;\n /** The `home: true` item, which the built-in sidebar opens from its brand row. */\n home?: AppNavItem;\n onNavigate: (item: AppNavItem) => void;\n}\n\n/**\n * The app's navigation: sidebar when standalone, host-fed when embedded.\n *\n * Standalone layout: the sidebar is a 260px column at ≥768px and a horizontal\n * strip below that, so put it first inside a `flex flex-col md:flex-row`\n * container. It paints with the app's own theme tokens (`--surface`,\n * `--border`, `--primary`…) and falls back to the platform defaults when a\n * token is missing.\n */\nexport function AppNav({ title, icon, groups, active, onNavigate, children, standalone }: AppNavProps) {\n // Always the latest callback, so the host listener never calls a stale one.\n const onNavigateRef = useRef(onNavigate);\n onNavigateRef.current = onNavigate;\n const groupsRef = useRef(groups);\n groupsRef.current = groups;\n\n // Embedded: publish the tree whenever it changes. Serialising is the cheap\n // way to dedupe — apps build `groups` inline every render.\n const payload = embedded\n ? JSON.stringify({ title, icon, groups: sanitize(groups), active })\n : \"\";\n const lastSent = useRef(\"\");\n useEffect(() => {\n if (!embedded || payload === lastSent.current) return;\n lastSent.current = payload;\n const { title, icon, groups, active } = JSON.parse(payload) as {\n title: string;\n icon?: string;\n groups: AppNavGroup[];\n active?: string;\n };\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"nav\", title, icon, groups, active });\n }, [payload]);\n\n // Embedded: handshake + listen for the host's clicks.\n useEffect(\n () =>\n subscribe((d) => {\n if (d.type === \"hello\") {\n // Host (re)mounted its listener after we first published: resend.\n lastSent.current = \"\";\n const g = sanitize(groupsRef.current);\n post({ source: \"clawnify-app\", v: HOST_BRIDGE_VERSION, type: \"nav\", title, icon, groups: g, active });\n } else if (d.type === \"navigate\" && \"id\" in d && typeof d.id === \"string\") {\n const item = groupsRef.current.flatMap((g) => g.items).find((i) => i.id === d.id);\n if (item) onNavigateRef.current(item);\n }\n }),\n // title/active are read from the closure only on `hello`; the publish\n // effect above covers their normal updates.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n );\n\n if (embedded) return null;\n\n const home = groups.flatMap((g) => g.items).find((i) => i.home);\n if (standalone) return <>{standalone({ title, icon, groups, active, home, onNavigate })}</>;\n const brand = (\n <>\n {(() => { const I = iconFor(icon); return I ? <I size={16} aria-hidden /> : null; })()}\n <span>{title}</span>\n </>\n );\n\n return (\n <aside className=\"cn-nav\">\n <style>{CSS}</style>\n {home ? (\n <a\n href={home.href ?? \"/\"}\n className=\"cn-nav-brand\"\n data-active={home.id === active || undefined}\n aria-current={home.id === active ? \"page\" : undefined}\n onClick={(e: MouseEvent) => {\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;\n e.preventDefault();\n onNavigate(home);\n }}\n >\n {brand}\n </a>\n ) : (\n <div className=\"cn-nav-brand\">{brand}</div>\n )}\n <nav className=\"cn-nav-groups\" aria-label=\"Primary\">\n {groups.map((g, gi) => (\n <div className=\"cn-nav-group\" key={g.label ?? gi}>\n {g.label && <p className=\"cn-nav-eyebrow\">{g.label}</p>}\n {g.items\n .filter((item) => !item.home)\n .map((item) => (\n <NavRow key={item.id} item={item} active={item.id === active} onNavigate={onNavigate} />\n ))}\n </div>\n ))}\n </nav>\n {children && <div className=\"cn-nav-footer\">{children}</div>}\n </aside>\n );\n}\n\nfunction NavRow({\n item,\n active,\n onNavigate,\n}: {\n item: AppNavItem;\n active: boolean;\n onNavigate: (item: AppNavItem) => void;\n}) {\n const body = (\n <>\n <TileIcon icon={item.icon} color={item.color} seed={item.id} />\n <span className=\"cn-nav-label\">{item.label}</span>\n {typeof item.count === \"number\" && <span className=\"cn-nav-count\">{item.count}</span>}\n </>\n );\n // A real link when there is an href — middle-click and copy-link keep\n // working — but the click itself goes through the app's router.\n if (item.href) {\n return (\n <a\n href={item.href}\n className=\"cn-nav-item\"\n data-active={active || undefined}\n aria-current={active ? \"page\" : undefined}\n onClick={(e: MouseEvent) => {\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;\n e.preventDefault();\n onNavigate(item);\n }}\n >\n {body}\n </a>\n );\n }\n return (\n <button\n type=\"button\"\n className=\"cn-nav-item\"\n data-active={active || undefined}\n aria-current={active ? \"page\" : undefined}\n onClick={() => onNavigate(item)}\n >\n {body}\n </button>\n );\n}\n\n/** Our names → Phosphor components; the same map the dashboard holds. */\nconst ICONS: Record<string, PhosphorIcon> = {\n \"activity\": Pulse,\n \"archive\": Archive,\n \"bar-chart-3\": ChartBar,\n \"bell\": Bell,\n \"book-open\": BookOpen,\n \"bot\": Robot,\n \"briefcase\": Briefcase,\n \"building-2\": Building,\n \"calendar\": Calendar,\n \"calendar-days\": CalendarDots,\n \"camera\": Camera,\n \"check-square\": CheckSquare,\n \"clipboard-list\": ClipboardText,\n \"clock\": Clock,\n \"code-2\": Code,\n \"contact\": AddressBook,\n \"credit-card\": CreditCard,\n \"database\": Database,\n \"dollar-sign\": CurrencyDollar,\n \"file\": File,\n \"file-text\": FileText,\n \"folder\": Folder,\n \"globe\": Globe,\n \"hash\": Hash,\n \"history\": ClockCounterClockwise,\n \"home\": House,\n \"image\": Image,\n \"inbox\": Tray,\n \"kanban\": Kanban,\n \"layers\": Stack,\n \"layout-dashboard\": SquaresFour,\n \"layout-grid\": GridFour,\n \"link\": Link,\n \"list\": List,\n \"list-checks\": ListChecks,\n \"mail\": Envelope,\n \"map\": MapTrifold,\n \"message-square\": ChatCircle,\n \"mic\": Microphone,\n \"package\": Package,\n \"phone\": Phone,\n \"pie-chart\": ChartPie,\n \"play\": Play,\n \"search\": MagnifyingGlass,\n \"send\": PaperPlaneTilt,\n \"settings\": Gear,\n \"shopping-cart\": ShoppingCart,\n \"sparkles\": Sparkle,\n \"star\": Star,\n \"table\": Table,\n \"tag\": Tag,\n \"target\": Target,\n \"truck\": Truck,\n \"upload\": Upload,\n \"user\": User,\n \"users\": User,\n \"video\": VideoCamera,\n \"wallet\": Wallet,\n \"zap\": Lightning,\n};\nfunction iconFor(name?: string): PhosphorIcon | undefined {\n return name ? ICONS[name] : undefined;\n}\n\n/** Only the fields the protocol defines, with sane bounds, cross the bridge. */\nfunction sanitize(groups: AppNavGroup[]): AppNavGroup[] {\n return groups.slice(0, 20).map((g) => ({\n ...(g.label ? { label: String(g.label).slice(0, 40) } : {}),\n items: g.items.slice(0, 100).map((i) => ({\n id: String(i.id),\n label: String(i.label).slice(0, 60),\n ...(i.href ? { href: String(i.href) } : {}),\n ...(i.icon ? { icon: String(i.icon) } : {}),\n ...(typeof i.count === \"number\" && Number.isFinite(i.count) ? { count: i.count } : {}),\n ...(i.home ? { home: true } : {}),\n ...(i.color && i.color in NAV_COLORS ? { color: i.color } : {}),\n })),\n }));\n}\n\n// Scoped by class prefix; tokens follow apps/DESIGN.md with its light defaults\n// as fallbacks, so an app with a partial theme still gets a readable sidebar.\nconst CSS = `\n.cn-nav{width:16.25rem;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border,#e2e8f0);background:var(--surface,#fff);color:var(--foreground,#1a202c)}\n.cn-nav-brand{height:3.5rem;flex-shrink:0;display:flex;align-items:center;gap:.5rem;padding:0 1.25rem;border-bottom:1px solid var(--border,#e2e8f0);font-size:.875rem;font-weight:600;color:inherit;text-decoration:none}\na.cn-nav-brand:hover{background:var(--sunken,#f1f5f9)}\n.cn-nav-brand>svg{color:var(--primary,#dd5164)}\n.cn-nav-groups{flex:1;overflow-y:auto;padding:.75rem}\n.cn-nav-group+.cn-nav-group{margin-top:1rem}\n.cn-nav-eyebrow{margin:0 0 .25rem;padding:0 .625rem;font-size:.75rem;font-weight:500;color:var(--muted,#475569)}\n.cn-nav-item{display:flex;align-items:center;gap:.5rem;width:100%;margin:0 0 .125rem;padding:.4375rem .625rem;border:0;border-radius:.375rem;background:none;color:inherit;font:inherit;font-size:.875rem;text-align:left;text-decoration:none;cursor:pointer}\n.cn-nav-item:hover{background:var(--sunken,#f1f5f9)}\n.cn-nav-item[data-active]{background:color-mix(in srgb,var(--primary,#dd5164) 12%,transparent);color:var(--primary,#dd5164);font-weight:600}\n.cn-nav-item:focus-visible{outline:2px solid var(--ring,#2563eb);outline-offset:-2px}\n.cn-nav-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.cn-nav-count{font-size:.75rem;font-variant-numeric:tabular-nums;color:var(--muted,#475569)}\n.cn-nav-item[data-active] .cn-nav-count{color:inherit}\n.cn-nav-footer{padding:.75rem;border-top:1px solid var(--border,#e2e8f0)}\n@media (max-width:767px){\n.cn-nav{width:100%;flex-direction:row;border-right:0;border-bottom:1px solid var(--border,#e2e8f0)}\n.cn-nav-brand,.cn-nav-eyebrow,.cn-nav-footer{display:none}\n.cn-nav-groups{display:flex;gap:.25rem;padding:.5rem .75rem;overflow-x:auto;overflow-y:hidden}\n.cn-nav-group{display:flex;gap:.25rem}\n.cn-nav-group+.cn-nav-group{margin:0}\n.cn-nav-item{width:auto;margin:0;white-space:nowrap}\n}\n`;\n"],"mappings":"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","names":["title","icon","groups","active"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawnify/app",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "The standard Clawnify app skeleton in one call. createApp() returns a fully-wired OpenAPIHono with D1/Storage init + /api/openapi.json + /llms.txt discovery pre-baked, so an app is just imports + routes + export.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",