@hanzo/ui 8.0.13 → 8.0.15

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.
@@ -0,0 +1,1142 @@
1
+ 'use client';
2
+ import { PROVIDERS } from './chunk-ZGTJCJ55.js';
3
+ import { createContext, useContext, useSyncExternalStore, useRef, useEffect, useMemo, useState, useCallback } from 'react';
4
+ import { Card, XStack, Text, Button, YStack, Label, Input, TextArea, Switch, Slider, ScrollView } from '@hanzo/gui';
5
+ import { TriangleAlert, CreditCard, ChevronsUpDown, ChevronUp, ChevronDown, Check, ExternalLink, ArrowRight, X, Calendar, AlertTriangle, CheckCircle2, List, Plus, Link2, RefreshCw, Share2, Send, Pencil, Trash2 } from '@hanzogui/lucide-icons-2';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
+
8
+ var HostCtx = createContext({});
9
+ var HostProvider = ({ actions, children }) => /* @__PURE__ */ jsx(HostCtx.Provider, { value: actions, children });
10
+ var useHost = () => useContext(HostCtx);
11
+ function classifyBackend(e) {
12
+ const status = typeof e?.status === "number" ? e.status : 0;
13
+ const message = e instanceof Error ? e.message : String(e);
14
+ if (status === 503) return { kind: "not-initialized", message };
15
+ if (status === 404 || status === 405) return { kind: "unavailable", message };
16
+ if (status === 501) return { kind: "not-implemented", message };
17
+ if (status === 402) return { kind: "billing", message };
18
+ if (status === 401) return { kind: "signin", message };
19
+ if (status === 403) return { kind: "access", message };
20
+ return { kind: "error", message };
21
+ }
22
+ function classifyRead(e) {
23
+ const s = classifyBackend(e);
24
+ return s.kind === "billing" ? null : s;
25
+ }
26
+ var TITLES = {
27
+ "not-initialized": "Backend not initialized",
28
+ unavailable: "Not available on this deployment yet",
29
+ "not-implemented": "Not yet available",
30
+ access: "Not enabled for your account",
31
+ signin: "Your session expired",
32
+ billing: "Add credits to continue",
33
+ error: "Could not reach the backend"
34
+ };
35
+ var BODIES = {
36
+ "not-initialized": "The /v1 route is mounted but its runtime (or the console API key it proxies to) is not configured on this deployment yet. Real data appears here once it is \u2014 no placeholder data is shown.",
37
+ unavailable: "This endpoint is not mounted at the gateway on this host yet. The view lights up automatically once the route is live.",
38
+ // 501 — a real, planned capability whose body ships in a later phase.
39
+ "not-implemented": "This capability is planned but not wired yet. The backend answers honestly rather than pretending it worked \u2014 nothing here is fabricated, and the view lights up automatically once it ships.",
40
+ // 403 for a SIGNED-IN user — never "sign in".
41
+ access: "You're signed in, but this isn't enabled for your organization on this deployment, or it's an admin-only surface. It appears here automatically once your account has access \u2014 nothing is fabricated.",
42
+ // 401 — the session itself lapsed; re-auth returns to this exact page.
43
+ signin: "Your session has expired or isn\u2019t recognized here. Sign in again to continue where you left off.",
44
+ // Empty → the card shows the backend's own message (the honest "Insufficient
45
+ // balance. Please add credits…" from the gateway billing gate).
46
+ billing: "",
47
+ error: ""
48
+ };
49
+ function BackendStateCard({
50
+ state,
51
+ onRetry,
52
+ hint
53
+ }) {
54
+ const { signIn, addCredits } = useHost();
55
+ return /* @__PURE__ */ jsxs(Card, { borderWidth: 1, borderColor: "$borderColor", p: "$4", gap: "$2", maxWidth: 640, children: [
56
+ /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "center", children: [
57
+ /* @__PURE__ */ jsx(TriangleAlert, { size: 16 }),
58
+ /* @__PURE__ */ jsx(Text, { fontSize: "$4", fontWeight: "700", children: TITLES[state.kind] })
59
+ ] }),
60
+ /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color11", children: BODIES[state.kind] || state.message }),
61
+ hint ? /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color10", children: hint }) : null,
62
+ state.kind === "signin" && signIn ? /* @__PURE__ */ jsx(Button, { size: "$2", theme: "light", self: "flex-start", onPress: signIn, children: "Sign in again" }) : state.kind === "billing" && addCredits ? (
63
+ // 402 — the org has no funded balance. Offer a top-up CTA (and keep Retry so
64
+ // a returning, funded caller can reload in place).
65
+ /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [
66
+ /* @__PURE__ */ jsx(Button, { size: "$2", theme: "light", self: "flex-start", icon: /* @__PURE__ */ jsx(CreditCard, { size: 14 }), onPress: addCredits, children: "Add credits" }),
67
+ onRetry ? /* @__PURE__ */ jsx(Button, { size: "$2", self: "flex-start", onPress: onRetry, children: "Retry" }) : null
68
+ ] })
69
+ ) : onRetry ? /* @__PURE__ */ jsx(Button, { size: "$2", self: "flex-start", onPress: onRetry, children: "Retry" }) : null
70
+ ] });
71
+ }
72
+ function SkeletonRows({ columns, count = 6 }) {
73
+ return /* @__PURE__ */ jsx(YStack, { children: Array.from({ length: count }).map((_, r) => /* @__PURE__ */ jsx(XStack, { py: "$2.5", px: "$3", gap: "$3", borderTopWidth: 1, borderColor: "$borderColor", items: "center", children: columns.map((c, i) => /* @__PURE__ */ jsx(YStack, { width: c.width, flex: c.width ? void 0 : 1, minW: c.width ? void 0 : FLEX_MIN_COL_W, items: c.align === "right" ? "flex-end" : "flex-start", children: /* @__PURE__ */ jsx(
74
+ "div",
75
+ {
76
+ className: "hz-skeleton",
77
+ style: { height: 12, borderRadius: 6, width: `${[62, 40, 54, 34, 48][(i + r) % 5]}%` },
78
+ "aria-hidden": true
79
+ }
80
+ ) }, c.key)) }, r)) });
81
+ }
82
+ function DataTable({
83
+ columns,
84
+ rows,
85
+ loading,
86
+ empty = "Nothing here yet.",
87
+ rowKey,
88
+ onRowPress,
89
+ isRowExpanded,
90
+ renderExpanded,
91
+ sort,
92
+ onSortChange
93
+ }) {
94
+ const minTableW = columns.reduce((sum, c) => sum + (c.width ?? FLEX_MIN_COL_W), 0) + (columns.length + 1) * 12;
95
+ return /* @__PURE__ */ jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$4", overflow: "hidden", children: [
96
+ /* @__PURE__ */ jsx(YStack, { style: { overflowX: "auto", overflowY: "visible" }, children: /* @__PURE__ */ jsxs(YStack, { minW: minTableW, children: [
97
+ /* @__PURE__ */ jsx(XStack, { bg: "$color1", py: "$2.5", px: "$3", gap: "$3", borderBottomWidth: 1, borderColor: "$borderColor", role: "row", children: columns.map((c) => {
98
+ const sortable = c.sortable === true && !!onSortChange;
99
+ const active = sortable && sort?.key === c.key;
100
+ const Caret = !sortable ? null : !active ? ChevronsUpDown : sort?.dir === "asc" ? ChevronUp : ChevronDown;
101
+ return /* @__PURE__ */ jsxs(
102
+ XStack,
103
+ {
104
+ width: c.width,
105
+ flex: c.width ? void 0 : 1,
106
+ minW: c.width ? void 0 : FLEX_MIN_COL_W,
107
+ items: "center",
108
+ gap: "$1",
109
+ justify: c.align === "right" ? "flex-end" : "flex-start",
110
+ role: "columnheader",
111
+ "aria-sort": sortable ? active ? sort?.dir === "asc" ? "ascending" : "descending" : "none" : void 0,
112
+ "aria-label": sortable ? `Sort by ${c.header}` : void 0,
113
+ cursor: sortable ? "pointer" : void 0,
114
+ hoverStyle: sortable ? { opacity: 0.75 } : void 0,
115
+ onPress: sortable ? () => onSortChange?.(c.key) : void 0,
116
+ children: [
117
+ /* @__PURE__ */ jsx(
118
+ Text,
119
+ {
120
+ fontSize: "$1",
121
+ fontWeight: "500",
122
+ color: active ? "$color12" : "$color10",
123
+ text: c.align === "right" ? "right" : "left",
124
+ className: c.mono ? "hz-tnum" : void 0,
125
+ children: c.header
126
+ }
127
+ ),
128
+ Caret ? /* @__PURE__ */ jsx(Caret, { size: 12, color: active ? "$color12" : "$color9" }) : null
129
+ ]
130
+ },
131
+ c.key
132
+ );
133
+ }) }),
134
+ loading ? /* @__PURE__ */ jsx(SkeletonRows, { columns }) : rows.length > 0 ? /* @__PURE__ */ jsx(YStack, { children: rows.map((row) => {
135
+ const expanded = (isRowExpanded?.(row) ?? false) && !!renderExpanded;
136
+ return /* @__PURE__ */ jsxs(YStack, { children: [
137
+ /* @__PURE__ */ jsx(
138
+ XStack,
139
+ {
140
+ className: "hz-row",
141
+ py: "$2.5",
142
+ px: "$3",
143
+ gap: "$3",
144
+ borderTopWidth: 1,
145
+ borderColor: "$borderColor",
146
+ items: "center",
147
+ bg: expanded ? "$color2" : void 0,
148
+ hoverStyle: onRowPress ? { bg: "$color2" } : void 0,
149
+ cursor: onRowPress ? "pointer" : void 0,
150
+ onPress: onRowPress ? () => onRowPress(row) : void 0,
151
+ children: columns.map((c) => {
152
+ const cell = c.render ? c.render(row) : String(row[c.key] ?? "");
153
+ return /* @__PURE__ */ jsx(
154
+ YStack,
155
+ {
156
+ width: c.width,
157
+ flex: c.width ? void 0 : 1,
158
+ minW: c.width ? void 0 : FLEX_MIN_COL_W,
159
+ justify: "center",
160
+ items: c.align === "right" ? "flex-end" : "flex-start",
161
+ children: typeof cell === "string" || typeof cell === "number" ? /* @__PURE__ */ jsx(
162
+ Text,
163
+ {
164
+ fontSize: "$3",
165
+ numberOfLines: 1,
166
+ color: "$color12",
167
+ text: c.align === "right" ? "right" : "left",
168
+ className: c.mono ? "hz-mono" : void 0,
169
+ children: cell
170
+ }
171
+ ) : cell
172
+ },
173
+ c.key
174
+ );
175
+ })
176
+ }
177
+ ),
178
+ expanded && renderExpanded ? /* @__PURE__ */ jsx(YStack, { borderTopWidth: 1, borderColor: "$borderColor", bg: "$color1", children: renderExpanded(row) }) : null
179
+ ] }, rowKey(row));
180
+ }) }) : null
181
+ ] }) }),
182
+ !loading && rows.length === 0 ? /* @__PURE__ */ jsx(YStack, { py: "$8", px: "$4", items: "center", gap: "$1", borderTopWidth: 1, borderColor: "$borderColor", children: /* @__PURE__ */ jsx(Text, { color: "$color10", fontSize: "$3", text: "center", children: empty }) }) : null
183
+ ] });
184
+ }
185
+ var FLEX_MIN_COL_W = 120;
186
+ function isHexColor(v) {
187
+ return typeof v === "string" && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(v.trim());
188
+ }
189
+ function resolveAccent(theme) {
190
+ if (!theme || !theme.isEnabled) return null;
191
+ const hex = (theme.colorPrimary ?? "").trim();
192
+ return isHexColor(hex) ? hex : null;
193
+ }
194
+ function expandHex(hex) {
195
+ const h = hex.trim().toLowerCase();
196
+ if (h.length === 4) return `#${h[1]}${h[1]}${h[2]}${h[2]}${h[3]}${h[3]}`;
197
+ return h;
198
+ }
199
+ function contrastText(hex) {
200
+ if (!isHexColor(hex)) return "#ffffff";
201
+ const h = expandHex(hex);
202
+ const r = parseInt(h.slice(1, 3), 16) / 255;
203
+ const g = parseInt(h.slice(3, 5), 16) / 255;
204
+ const b = parseInt(h.slice(5, 7), 16) / 255;
205
+ const lin = (c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
206
+ const luminance = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
207
+ return luminance > 0.5 ? "#000000" : "#ffffff";
208
+ }
209
+ function accentFor(theme) {
210
+ const hex = resolveAccent(theme);
211
+ return { accent: hex, contrast: hex ? contrastText(hex) : "#ffffff" };
212
+ }
213
+ var DEFAULT = { accent: null, contrast: "#ffffff" };
214
+ var current = DEFAULT;
215
+ var listeners = /* @__PURE__ */ new Set();
216
+ function setOrgAccent(theme) {
217
+ const next = accentFor(theme);
218
+ if (next.accent === current.accent) return;
219
+ current = next.accent ? next : DEFAULT;
220
+ for (const l of listeners) l();
221
+ }
222
+ function subscribe(cb) {
223
+ listeners.add(cb);
224
+ return () => {
225
+ listeners.delete(cb);
226
+ };
227
+ }
228
+ function useAccent() {
229
+ return useSyncExternalStore(
230
+ subscribe,
231
+ () => current,
232
+ () => DEFAULT
233
+ );
234
+ }
235
+ function PrimaryButton(props) {
236
+ const { accent, contrast } = useAccent();
237
+ if (accent) {
238
+ return /* @__PURE__ */ jsx(Button, { style: { backgroundColor: accent, color: contrast, borderColor: accent }, ...props });
239
+ }
240
+ return /* @__PURE__ */ jsx(Button, { theme: "light", ...props });
241
+ }
242
+ function openHref(href) {
243
+ if (typeof window !== "undefined") window.open(href, "_blank", "noopener");
244
+ }
245
+ function EmptyState({
246
+ icon: Icon,
247
+ title,
248
+ description,
249
+ bullets,
250
+ primary,
251
+ secondary
252
+ }) {
253
+ return /* @__PURE__ */ jsxs(
254
+ Card,
255
+ {
256
+ borderWidth: 1,
257
+ borderColor: "$borderColor",
258
+ borderStyle: "dashed",
259
+ p: "$6",
260
+ gap: "$4",
261
+ items: "center",
262
+ maxWidth: 640,
263
+ self: "center",
264
+ width: "100%",
265
+ children: [
266
+ /* @__PURE__ */ jsx(
267
+ YStack,
268
+ {
269
+ width: 48,
270
+ height: 48,
271
+ items: "center",
272
+ justify: "center",
273
+ rounded: "$4",
274
+ bg: "$color3",
275
+ children: /* @__PURE__ */ jsx(Icon, { size: 24 })
276
+ }
277
+ ),
278
+ /* @__PURE__ */ jsxs(YStack, { gap: "$2", items: "center", maxW: 480, children: [
279
+ /* @__PURE__ */ jsx(Text, { fontSize: "$6", fontWeight: "500", text: "center", letterSpacing: -0.3, children: title }),
280
+ /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color11", text: "center", children: description })
281
+ ] }),
282
+ bullets && bullets.length ? /* @__PURE__ */ jsx(YStack, { gap: "$2", self: "stretch", maxW: 420, mx: "auto", children: bullets.map((b) => /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "flex-start", children: [
283
+ /* @__PURE__ */ jsx(YStack, { pt: "$1", children: /* @__PURE__ */ jsx(Check, { size: 14, color: "$color10" }) }),
284
+ /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color11", flex: 1, children: b })
285
+ ] }, b)) }) : null,
286
+ primary || secondary ? /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", justify: "center", children: [
287
+ primary ? /* @__PURE__ */ jsx(
288
+ PrimaryButton,
289
+ {
290
+ icon: primary.icon,
291
+ iconAfter: primary.href ? /* @__PURE__ */ jsx(ExternalLink, { size: 15 }) : /* @__PURE__ */ jsx(ArrowRight, { size: 15 }),
292
+ onPress: () => primary.href ? openHref(primary.href) : primary.onPress?.(),
293
+ children: primary.label
294
+ }
295
+ ) : null,
296
+ secondary ? /* @__PURE__ */ jsx(
297
+ Button,
298
+ {
299
+ chromeless: true,
300
+ icon: secondary.icon,
301
+ iconAfter: secondary.href ? /* @__PURE__ */ jsx(ExternalLink, { size: 14 }) : void 0,
302
+ onPress: () => secondary.href ? openHref(secondary.href) : secondary.onPress?.(),
303
+ children: secondary.label
304
+ }
305
+ ) : null
306
+ ] }) : null
307
+ ]
308
+ }
309
+ );
310
+ }
311
+ function FieldRow({ label, children }) {
312
+ return /* @__PURE__ */ jsxs(XStack, { gap: "$3", items: "flex-start", flexWrap: "wrap", children: [
313
+ /* @__PURE__ */ jsx(Label, { width: "100%", pt: "$2", color: "$color11", fontSize: "$3", $md: { width: 180 }, children: label }),
314
+ /* @__PURE__ */ jsx(YStack, { flex: 1, minW: 0, $md: { minW: 240 }, children })
315
+ ] });
316
+ }
317
+ function FieldText({
318
+ value,
319
+ onChange,
320
+ disabled,
321
+ secure,
322
+ placeholder
323
+ }) {
324
+ return /* @__PURE__ */ jsx(
325
+ Input,
326
+ {
327
+ value,
328
+ onChangeText: onChange,
329
+ disabled,
330
+ secureTextEntry: secure,
331
+ placeholder,
332
+ autoCapitalize: "none"
333
+ }
334
+ );
335
+ }
336
+ function FieldTextArea({
337
+ value,
338
+ onChange,
339
+ disabled,
340
+ rows = 6
341
+ }) {
342
+ return /* @__PURE__ */ jsx(TextArea, { value, onChangeText: onChange, disabled, numberOfLines: rows });
343
+ }
344
+ function FieldSwitch({
345
+ checked,
346
+ onChange,
347
+ disabled
348
+ }) {
349
+ return /* @__PURE__ */ jsx(Switch, { checked, onCheckedChange: onChange, disabled, size: "$3", children: /* @__PURE__ */ jsx(Switch.Thumb, {}) });
350
+ }
351
+ var CHEVRON = `data:image/svg+xml,${encodeURIComponent(
352
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9a9a9a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>'
353
+ )}`;
354
+ function selectStyle(disabled) {
355
+ return {
356
+ width: "100%",
357
+ boxSizing: "border-box",
358
+ appearance: "none",
359
+ WebkitAppearance: "none",
360
+ MozAppearance: "none",
361
+ background: `var(--background) url("${CHEVRON}") no-repeat right 10px center`,
362
+ color: "var(--color12)",
363
+ border: "1px solid var(--borderColor)",
364
+ borderRadius: 9,
365
+ padding: "9px 34px 9px 12px",
366
+ fontSize: 14,
367
+ lineHeight: "20px",
368
+ fontFamily: "inherit",
369
+ height: 40,
370
+ outline: "none",
371
+ cursor: disabled ? "not-allowed" : "pointer",
372
+ opacity: disabled ? 0.5 : 1
373
+ };
374
+ }
375
+ var OPTION_STYLE = { background: "var(--color2)", color: "var(--color12)" };
376
+ function FieldSelect({
377
+ value,
378
+ options,
379
+ onChange,
380
+ disabled,
381
+ placeholder = "Select\u2026"
382
+ }) {
383
+ const showPlaceholder = value === "" || !options.includes(value);
384
+ return /* @__PURE__ */ jsxs(
385
+ "select",
386
+ {
387
+ value: showPlaceholder ? "" : value,
388
+ onChange: (e) => onChange(e.currentTarget.value),
389
+ disabled,
390
+ "aria-label": placeholder,
391
+ style: selectStyle(disabled),
392
+ children: [
393
+ showPlaceholder ? /* @__PURE__ */ jsx("option", { value: "", disabled: true, style: OPTION_STYLE, children: placeholder }) : null,
394
+ options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt, style: OPTION_STYLE, children: opt }, opt))
395
+ ]
396
+ }
397
+ );
398
+ }
399
+ function FieldSlider({
400
+ value,
401
+ min,
402
+ max,
403
+ step,
404
+ onChange,
405
+ disabled
406
+ }) {
407
+ return /* @__PURE__ */ jsxs(XStack, { gap: "$3", items: "center", children: [
408
+ /* @__PURE__ */ jsx(Text, { width: 56, fontSize: "$3", color: "$color11", children: value }),
409
+ /* @__PURE__ */ jsxs(
410
+ Slider,
411
+ {
412
+ flex: 1,
413
+ min,
414
+ max,
415
+ step,
416
+ value: [value],
417
+ onValueChange: (v) => onChange(v[0] ?? min),
418
+ disabled,
419
+ children: [
420
+ /* @__PURE__ */ jsx(Slider.Track, { children: /* @__PURE__ */ jsx(Slider.TrackActive, {}) }),
421
+ /* @__PURE__ */ jsx(Slider.Thumb, { index: 0, circular: true })
422
+ ]
423
+ }
424
+ )
425
+ ] });
426
+ }
427
+ function PageHeader({
428
+ title,
429
+ subtitle,
430
+ actions
431
+ }) {
432
+ return (
433
+ // flexWrap lets the actions drop below the title on narrow screens; flex={1}
434
+ // minW={0} lets the title column shrink so a long subtitle WRAPS instead of
435
+ // running off the viewport (a flex child defaults to min-width:auto = content
436
+ // width, which would overflow on mobile).
437
+ /* @__PURE__ */ jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$3", flexWrap: "wrap", children: [
438
+ /* @__PURE__ */ jsxs(YStack, { gap: "$1", flex: 1, minW: 200, children: [
439
+ /* @__PURE__ */ jsx(Text, { fontSize: "$7", fontWeight: "500", letterSpacing: -0.4, children: title }),
440
+ subtitle ? /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color11", children: subtitle }) : null
441
+ ] }),
442
+ actions ? /* @__PURE__ */ jsx(XStack, { gap: "$2", items: "center", flexWrap: "wrap", width: "100%", $md: { width: "auto", justify: "flex-end" }, children: actions }) : null
443
+ ] })
444
+ );
445
+ }
446
+ var toneOf = (status) => {
447
+ const s = status.toLowerCase();
448
+ if (s === "green") return "green";
449
+ if (s === "yellow") return "yellow";
450
+ if (s === "red") return "red";
451
+ if (["ready", "active", "running", "available", "ok", "live", "succeeded", "connected", "synced", "imported"].includes(s)) return "green";
452
+ if (["creating", "provisioning", "pending", "updating", "attaching", "building", "deploying", "queued", "importing"].includes(s))
453
+ return "yellow";
454
+ if (["error", "failed", "degraded", "down", "canceled", "conflict"].includes(s)) return "red";
455
+ return "neutral";
456
+ };
457
+ var TONE_BG = {
458
+ green: "$color5",
459
+ yellow: "$color4",
460
+ red: "$color4",
461
+ neutral: "$color3"
462
+ };
463
+ var TONE_FG = {
464
+ green: "$color12",
465
+ yellow: "$color12",
466
+ red: "$color12",
467
+ neutral: "$color11"
468
+ };
469
+ function StatusTag({ status }) {
470
+ const tone = toneOf(status ?? "");
471
+ return /* @__PURE__ */ jsx(Text, { fontSize: "$1", px: "$2", py: "$1", rounded: "$2", bg: TONE_BG[tone], color: TONE_FG[tone], children: status || "unknown" });
472
+ }
473
+
474
+ // src/product/color.ts
475
+ var asColor = (hex) => hex;
476
+ var LG = 1024;
477
+ var lockCount = 0;
478
+ var savedOverflow = "";
479
+ function lockScroll() {
480
+ if (typeof document === "undefined") return;
481
+ if (lockCount === 0) {
482
+ savedOverflow = document.body.style.overflow;
483
+ document.body.style.overflow = "hidden";
484
+ }
485
+ lockCount++;
486
+ }
487
+ function unlockScroll() {
488
+ if (typeof document === "undefined") return;
489
+ lockCount = Math.max(0, lockCount - 1);
490
+ if (lockCount === 0) document.body.style.overflow = savedOverflow;
491
+ }
492
+ function SlideOver({
493
+ open,
494
+ onClose,
495
+ side = "right",
496
+ size = 420,
497
+ title,
498
+ icon: Icon,
499
+ iconColor,
500
+ headerRight,
501
+ ariaLabel,
502
+ zIndex = 1e3,
503
+ children
504
+ }) {
505
+ const panelRef = useRef(null);
506
+ const openerRef = useRef(null);
507
+ useEffect(() => {
508
+ if (!open) return;
509
+ openerRef.current = typeof document !== "undefined" ? document.activeElement : null;
510
+ lockScroll();
511
+ const t = window.setTimeout(() => {
512
+ const el = panelRef.current;
513
+ if (el && typeof el.focus === "function") el.focus();
514
+ }, 0);
515
+ return () => {
516
+ window.clearTimeout(t);
517
+ unlockScroll();
518
+ const opener = openerRef.current;
519
+ if (opener && opener instanceof HTMLElement && typeof opener.focus === "function") opener.focus();
520
+ };
521
+ }, [open]);
522
+ useEffect(() => {
523
+ if (!open) return;
524
+ const onKey = (e) => {
525
+ if (e.key === "Escape") {
526
+ e.stopPropagation();
527
+ onClose();
528
+ }
529
+ };
530
+ window.addEventListener("keydown", onKey);
531
+ return () => window.removeEventListener("keydown", onKey);
532
+ }, [open, onClose]);
533
+ const offscreen = side === "right" ? "translateX(100%)" : "translateX(-100%)";
534
+ const edge = side === "right" ? { r: 0 } : { l: 0 };
535
+ return /* @__PURE__ */ jsxs(
536
+ YStack,
537
+ {
538
+ overflow: "hidden",
539
+ pointerEvents: open ? "auto" : "none",
540
+ "aria-hidden": !open,
541
+ style: { position: "fixed", inset: 0, zIndex },
542
+ children: [
543
+ /* @__PURE__ */ jsx(
544
+ YStack,
545
+ {
546
+ position: "absolute",
547
+ t: 0,
548
+ l: 0,
549
+ r: 0,
550
+ b: 0,
551
+ bg: "rgba(0,0,0,0.55)",
552
+ className: "hz-fade",
553
+ style: { opacity: open ? 1 : 0 },
554
+ onPress: onClose
555
+ }
556
+ ),
557
+ /* @__PURE__ */ jsx(
558
+ YStack,
559
+ {
560
+ ref: panelRef,
561
+ tabIndex: -1,
562
+ role: "dialog",
563
+ "aria-modal": open ? true : void 0,
564
+ "aria-label": typeof title === "string" ? title : ariaLabel,
565
+ position: "absolute",
566
+ t: 0,
567
+ b: 0,
568
+ ...edge,
569
+ width: "100%",
570
+ $lg: { width: size, maxW: "100vw" },
571
+ bg: "$color1",
572
+ borderLeftWidth: side === "right" ? 1 : 0,
573
+ borderRightWidth: side === "left" ? 1 : 0,
574
+ borderColor: "$borderColor",
575
+ className: "hz-slide hz-elevation-4",
576
+ style: {
577
+ transform: open ? "translateX(0)" : offscreen,
578
+ height: "100dvh",
579
+ paddingTop: "env(safe-area-inset-top)",
580
+ paddingBottom: "env(safe-area-inset-bottom)"
581
+ },
582
+ children: title !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
583
+ /* @__PURE__ */ jsxs(
584
+ XStack,
585
+ {
586
+ items: "center",
587
+ gap: "$2.5",
588
+ px: "$4",
589
+ height: 56,
590
+ borderBottomWidth: 1,
591
+ borderColor: "$borderColor",
592
+ children: [
593
+ Icon ? /* @__PURE__ */ jsx(Icon, { size: 18, color: iconColor ? asColor(iconColor) : void 0 }) : null,
594
+ /* @__PURE__ */ jsx(Text, { flex: 1, fontSize: "$5", fontWeight: "700", color: "$color12", numberOfLines: 1, children: title }),
595
+ headerRight,
596
+ /* @__PURE__ */ jsx(Button, { chromeless: true, width: 44, height: 44, icon: /* @__PURE__ */ jsx(X, { size: 18 }), onPress: onClose, "aria-label": "Close" })
597
+ ]
598
+ }
599
+ ),
600
+ /* @__PURE__ */ jsx(ScrollView, { flex: 1, children: /* @__PURE__ */ jsx(YStack, { flex: 1, p: "$4", gap: "$3", children }) })
601
+ ] }) : (
602
+ // Bare mode — the caller owns the full panel layout (e.g. the nav drawer
603
+ // renders its own header + scroll + footer).
604
+ children
605
+ )
606
+ }
607
+ )
608
+ ]
609
+ }
610
+ );
611
+ }
612
+
613
+ // src/product/social/format.ts
614
+ function formatPostTime(unix) {
615
+ if (!unix) return "\u2014";
616
+ return new Date(unix * 1e3).toLocaleString(void 0, {
617
+ month: "short",
618
+ day: "numeric",
619
+ hour: "2-digit",
620
+ minute: "2-digit"
621
+ });
622
+ }
623
+ function postDayBucket(unix) {
624
+ const d = new Date(unix * 1e3);
625
+ const key = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
626
+ const label = d.toLocaleDateString(void 0, { weekday: "short", month: "short", day: "numeric" });
627
+ return { key, label };
628
+ }
629
+ function postPreview(s) {
630
+ const t = s.trim();
631
+ return t.length > 72 ? `${t.slice(0, 72)}\u2026` : t || "\u2014";
632
+ }
633
+ function parsePostTime(dt) {
634
+ const ms = Date.parse(dt.trim());
635
+ return Number.isFinite(ms) && ms > 0 ? Math.floor(ms / 1e3) : 0;
636
+ }
637
+ function PostAgenda({ posts, onOpen }) {
638
+ const days = useMemo(() => {
639
+ const timed = posts.filter((p) => (p.scheduleAt ?? 0) > 0).sort((a, b) => (a.scheduleAt ?? 0) - (b.scheduleAt ?? 0));
640
+ const groups = [];
641
+ const index = /* @__PURE__ */ new Map();
642
+ for (const p of timed) {
643
+ const { key, label } = postDayBucket(p.scheduleAt ?? 0);
644
+ let i = index.get(key);
645
+ if (i === void 0) {
646
+ i = groups.length;
647
+ index.set(key, i);
648
+ groups.push({ label, items: [] });
649
+ }
650
+ groups[i].items.push(p);
651
+ }
652
+ return groups;
653
+ }, [posts]);
654
+ if (days.length === 0) {
655
+ return /* @__PURE__ */ jsx(
656
+ EmptyState,
657
+ {
658
+ icon: Calendar,
659
+ title: "Nothing on the calendar",
660
+ description: "Scheduled and timed posts appear here, grouped by day. Compose a post and pick Schedule to plan ahead."
661
+ }
662
+ );
663
+ }
664
+ return /* @__PURE__ */ jsx(YStack, { gap: "$4", children: days.map((d) => /* @__PURE__ */ jsxs(YStack, { gap: "$2", children: [
665
+ /* @__PURE__ */ jsx(Text, { fontSize: "$3", fontWeight: "500", color: "$color11", children: d.label }),
666
+ d.items.map((p) => /* @__PURE__ */ jsxs(
667
+ XStack,
668
+ {
669
+ items: "center",
670
+ justify: "space-between",
671
+ gap: "$3",
672
+ borderWidth: 1,
673
+ borderColor: "$borderColor",
674
+ rounded: "$4",
675
+ px: "$4",
676
+ py: "$3",
677
+ cursor: "pointer",
678
+ hoverStyle: { bg: "$color2" },
679
+ onPress: () => onOpen(p),
680
+ children: [
681
+ /* @__PURE__ */ jsxs(YStack, { gap: "$1", flex: 1, children: [
682
+ /* @__PURE__ */ jsx(Text, { fontSize: "$3", children: postPreview(p.content) }),
683
+ /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "center", children: [
684
+ /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$color10", children: p.channel }),
685
+ /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color10", children: [
686
+ "\xB7 ",
687
+ formatPostTime(p.scheduleAt ?? 0)
688
+ ] })
689
+ ] })
690
+ ] }),
691
+ /* @__PURE__ */ jsx(StatusTag, { status: p.status })
692
+ ]
693
+ },
694
+ p.id
695
+ ))
696
+ ] }, d.label)) });
697
+ }
698
+ var COMPOSE_MODES = ["draft", "schedule", "now"];
699
+ var COMPOSE_LABEL = {
700
+ draft: "Save draft",
701
+ schedule: "Schedule",
702
+ now: "Publish now"
703
+ };
704
+ function PostComposer({
705
+ channels,
706
+ providers,
707
+ onSubmit
708
+ }) {
709
+ const [content, setContent] = useState("");
710
+ const [channel, setChannel] = useState(channels[0] ?? "x");
711
+ const [mode, setMode] = useState("draft");
712
+ const [scheduleAt, setScheduleAt] = useState("");
713
+ const [saving, setSaving] = useState(false);
714
+ const [error, setError] = useState(null);
715
+ const cap = providers.find((p) => p.provider === channel);
716
+ const unconfigured = (mode === "now" || mode === "schedule") && !!cap && !cap.credentialsConfigured;
717
+ const submit = async () => {
718
+ if (!content.trim()) {
719
+ setError("Content is required.");
720
+ return;
721
+ }
722
+ let status = "draft";
723
+ let at = 0;
724
+ if (mode === "schedule") {
725
+ at = parsePostTime(scheduleAt);
726
+ if (at <= Math.floor(Date.now() / 1e3)) {
727
+ setError("Pick a future date and time to schedule.");
728
+ return;
729
+ }
730
+ status = "scheduled";
731
+ } else if (mode === "now") {
732
+ status = "scheduled";
733
+ }
734
+ setSaving(true);
735
+ setError(null);
736
+ const message = await onSubmit({ content: content.trim(), channel, status, scheduleAt: at }, mode);
737
+ if (message) {
738
+ setError(message);
739
+ setSaving(false);
740
+ }
741
+ };
742
+ return /* @__PURE__ */ jsxs(YStack, { gap: "$3", p: "$4", children: [
743
+ /* @__PURE__ */ jsx(FieldRow, { label: "Content", children: /* @__PURE__ */ jsx(FieldTextArea, { value: content, onChange: setContent, disabled: saving }) }),
744
+ /* @__PURE__ */ jsx(FieldRow, { label: "Channel", children: /* @__PURE__ */ jsx(FieldSelect, { value: channel, options: channels, onChange: setChannel, disabled: saving }) }),
745
+ /* @__PURE__ */ jsx(FieldRow, { label: "When", children: /* @__PURE__ */ jsx(
746
+ FieldSelect,
747
+ {
748
+ value: mode,
749
+ options: [...COMPOSE_MODES],
750
+ onChange: (v) => setMode(v),
751
+ disabled: saving
752
+ }
753
+ ) }),
754
+ mode === "schedule" ? /* @__PURE__ */ jsx(FieldRow, { label: "Schedule at", children: /* @__PURE__ */ jsx(
755
+ FieldText,
756
+ {
757
+ value: scheduleAt,
758
+ onChange: setScheduleAt,
759
+ placeholder: "2026-07-15 09:00",
760
+ disabled: saving
761
+ }
762
+ ) }) : null,
763
+ unconfigured ? /* @__PURE__ */ jsxs(XStack, { items: "flex-start", gap: "$2", children: [
764
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, color: "var(--yellow10)" }),
765
+ /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color10", children: [
766
+ channel,
767
+ " isn\u2019t configured to publish yet \u2014 needs ",
768
+ cap?.missingCredentials.join(", "),
769
+ ". The post is saved and marked failed on publish until credentials are supplied."
770
+ ] })
771
+ ] }) : null,
772
+ error ? /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$red10", children: error }) : null,
773
+ /* @__PURE__ */ jsx(PrimaryButton, { onPress: submit, disabled: saving, children: saving ? "Working\u2026" : COMPOSE_LABEL[mode] })
774
+ ] });
775
+ }
776
+ function ProviderReadinessList({ providers }) {
777
+ return /* @__PURE__ */ jsxs(YStack, { gap: "$2", children: [
778
+ /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color10", children: "Network publish-readiness" }),
779
+ providers.map((p) => /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", py: "$1", children: [
780
+ /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
781
+ p.credentialsConfigured ? /* @__PURE__ */ jsx(CheckCircle2, { size: 14, color: "var(--green10)" }) : /* @__PURE__ */ jsx(AlertTriangle, { size: 14, color: "var(--yellow10)" }),
782
+ /* @__PURE__ */ jsx(Text, { fontSize: "$2", children: p.provider })
783
+ ] }),
784
+ /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$color10", children: p.credentialsConfigured ? "Ready" : `needs ${p.missingCredentials.join(", ")}` })
785
+ ] }, p.provider))
786
+ ] });
787
+ }
788
+ function SocialSummaryBar({ summary }) {
789
+ const cells = [
790
+ { label: "Posts", value: summary.posts },
791
+ { label: "Scheduled", value: summary.scheduled },
792
+ { label: "Published", value: summary.published },
793
+ { label: "Accounts", value: summary.accounts }
794
+ ];
795
+ return /* @__PURE__ */ jsx(XStack, { gap: "$3", flexWrap: "wrap", children: cells.map((c) => /* @__PURE__ */ jsxs(
796
+ YStack,
797
+ {
798
+ gap: "$1",
799
+ borderWidth: 1,
800
+ borderColor: "$borderColor",
801
+ rounded: "$4",
802
+ px: "$4",
803
+ py: "$3",
804
+ minW: 140,
805
+ children: [
806
+ /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$color10", children: c.label }),
807
+ /* @__PURE__ */ jsx(Text, { fontSize: "$6", fontWeight: "500", className: "hz-tnum", children: c.value })
808
+ ]
809
+ },
810
+ c.label
811
+ )) });
812
+ }
813
+ var OPTIONS = [
814
+ { id: "list", label: "List", icon: List },
815
+ { id: "calendar", label: "Calendar", icon: Calendar }
816
+ ];
817
+ function ViewToggle({
818
+ view,
819
+ onChange
820
+ }) {
821
+ return /* @__PURE__ */ jsx(XStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$4", overflow: "hidden", children: OPTIONS.map((o) => {
822
+ const Icon = o.icon;
823
+ const active = view === o.id;
824
+ return /* @__PURE__ */ jsxs(
825
+ XStack,
826
+ {
827
+ items: "center",
828
+ gap: "$2",
829
+ px: "$3",
830
+ py: "$2",
831
+ cursor: "pointer",
832
+ bg: active ? "$color4" : "transparent",
833
+ hoverStyle: { bg: active ? "$color4" : "$color2" },
834
+ onPress: () => onChange(o.id),
835
+ children: [
836
+ /* @__PURE__ */ jsx(Icon, { size: 14 }),
837
+ /* @__PURE__ */ jsx(Text, { fontSize: "$2", fontWeight: active ? "500" : "400", children: o.label })
838
+ ]
839
+ },
840
+ o.id
841
+ );
842
+ }) });
843
+ }
844
+ var POST_COLUMNS = [
845
+ { key: "content", header: "Post", render: (p) => postPreview(p.content) },
846
+ { key: "channel", header: "Channel", render: (p) => p.channel || "\u2014" },
847
+ { key: "status", header: "Status", render: (p) => /* @__PURE__ */ jsx(StatusTag, { status: p.status }) },
848
+ { key: "scheduleAt", header: "Scheduled", render: (p) => formatPostTime(p.scheduleAt) }
849
+ ];
850
+ var ACCOUNT_COLUMNS = [
851
+ { key: "handle", header: "Account", render: (a) => a.handle || "\u2014" },
852
+ { key: "provider", header: "Network", render: (a) => a.provider || "\u2014" },
853
+ { key: "status", header: "Status", render: (a) => /* @__PURE__ */ jsx(StatusTag, { status: a.status }) }
854
+ ];
855
+ function PostDetail({ api, post, onChanged }) {
856
+ const [publishing, setPublishing] = useState(false);
857
+ const [error, setError] = useState(null);
858
+ const canPublish = post.status === "draft" || post.status === "scheduled" || post.status === "failed";
859
+ const publish = async () => {
860
+ setPublishing(true);
861
+ setError(null);
862
+ try {
863
+ await api.posts.publish(post.id);
864
+ onChanged();
865
+ } catch (e) {
866
+ setError(classifyBackend(e).message || "Publish failed.");
867
+ } finally {
868
+ setPublishing(false);
869
+ }
870
+ };
871
+ return /* @__PURE__ */ jsxs(YStack, { gap: "$3", p: "$4", children: [
872
+ /* @__PURE__ */ jsx(FieldRow, { label: "Content", children: /* @__PURE__ */ jsx(Text, { fontSize: "$3", children: post.content || "\u2014" }) }),
873
+ /* @__PURE__ */ jsx(FieldRow, { label: "Channel", children: /* @__PURE__ */ jsx(Text, { fontSize: "$3", children: post.channel }) }),
874
+ /* @__PURE__ */ jsx(FieldRow, { label: "Status", children: /* @__PURE__ */ jsx(StatusTag, { status: post.status }) }),
875
+ post.scheduleAt > 0 ? /* @__PURE__ */ jsx(FieldRow, { label: "Scheduled", children: /* @__PURE__ */ jsx(Text, { fontSize: "$3", children: formatPostTime(post.scheduleAt) }) }) : null,
876
+ post.media.length > 0 ? /* @__PURE__ */ jsx(FieldRow, { label: "Media", children: /* @__PURE__ */ jsx(YStack, { gap: "$1", children: post.media.map((m) => /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color11", children: m }, m)) }) }) : null,
877
+ post.externalId ? /* @__PURE__ */ jsx(FieldRow, { label: "External id", children: /* @__PURE__ */ jsx(Text, { fontSize: "$3", className: "hz-tnum", children: post.externalId }) }) : null,
878
+ post.error ? /* @__PURE__ */ jsx(FieldRow, { label: "Last error", children: /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$red10", children: post.error }) }) : null,
879
+ error ? /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$red10", children: error }) : null,
880
+ canPublish ? /* @__PURE__ */ jsx(PrimaryButton, { onPress: publish, disabled: publishing, icon: /* @__PURE__ */ jsx(Send, { size: 16 }), children: publishing ? "Publishing\u2026" : "Publish now" }) : null
881
+ ] });
882
+ }
883
+ function ConnectAccount({
884
+ api,
885
+ providers,
886
+ onCreated
887
+ }) {
888
+ const [provider, setProvider] = useState("x");
889
+ const [handle, setHandle] = useState("");
890
+ const [saving, setSaving] = useState(false);
891
+ const [error, setError] = useState(null);
892
+ const submit = async () => {
893
+ setSaving(true);
894
+ setError(null);
895
+ try {
896
+ await api.accounts.create({ provider, handle: handle.trim(), status: "connected" });
897
+ onCreated();
898
+ } catch (e) {
899
+ setError(classifyBackend(e).message || "Failed to connect account.");
900
+ } finally {
901
+ setSaving(false);
902
+ }
903
+ };
904
+ return /* @__PURE__ */ jsxs(YStack, { gap: "$4", p: "$4", children: [
905
+ /* @__PURE__ */ jsx(ProviderReadinessList, { providers }),
906
+ /* @__PURE__ */ jsxs(YStack, { gap: "$3", children: [
907
+ /* @__PURE__ */ jsx(FieldRow, { label: "Network", children: /* @__PURE__ */ jsx(FieldSelect, { value: provider, options: [...PROVIDERS], onChange: setProvider, disabled: saving }) }),
908
+ /* @__PURE__ */ jsx(FieldRow, { label: "Handle", children: /* @__PURE__ */ jsx(FieldText, { value: handle, onChange: setHandle, placeholder: "@hanzo", disabled: saving }) }),
909
+ error ? /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$red10", children: error }) : null,
910
+ /* @__PURE__ */ jsx(PrimaryButton, { onPress: submit, disabled: saving, children: saving ? "Connecting\u2026" : "Connect account" })
911
+ ] })
912
+ ] });
913
+ }
914
+ function SocialResource({
915
+ api,
916
+ title = "Publish",
917
+ subtitle = "Compose, schedule and publish your content across networks \u2014 per org, over the native /v1/social engine."
918
+ }) {
919
+ const [state, setState] = useState({ phase: "loading" });
920
+ const [view, setView] = useState("list");
921
+ const [composing, setComposing] = useState(false);
922
+ const [connecting, setConnecting] = useState(false);
923
+ const [openPost, setOpenPost] = useState(null);
924
+ const load = useCallback(async () => {
925
+ setState({ phase: "loading" });
926
+ try {
927
+ const [summary, posts2, accounts2, providers2] = await Promise.all([
928
+ api.summary(),
929
+ api.posts.list(),
930
+ api.accounts.list(),
931
+ api.providers()
932
+ ]);
933
+ setState({ phase: "ready", data: { summary, posts: posts2, accounts: accounts2, providers: providers2 } });
934
+ } catch (e) {
935
+ setState({ phase: "error", error: classifyBackend(e) });
936
+ }
937
+ }, [api]);
938
+ useEffect(() => {
939
+ void load();
940
+ }, [load]);
941
+ const providers = state.phase === "ready" ? state.data.providers : [];
942
+ const posts = state.phase === "ready" ? state.data.posts : [];
943
+ const accounts = state.phase === "ready" ? state.data.accounts : [];
944
+ const empty = state.phase === "ready" && posts.length === 0 && accounts.length === 0;
945
+ const submitDraft = async (draft, mode) => {
946
+ try {
947
+ const created = await api.posts.create(draft);
948
+ if (mode === "now" && created.status === "failed") return created.error || "Publish failed.";
949
+ } catch (e) {
950
+ return classifyBackend(e).message || "Failed to create post.";
951
+ }
952
+ setComposing(false);
953
+ void load();
954
+ return null;
955
+ };
956
+ return /* @__PURE__ */ jsxs(YStack, { gap: "$4", p: "$4", children: [
957
+ /* @__PURE__ */ jsx(
958
+ PageHeader,
959
+ {
960
+ title,
961
+ subtitle,
962
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [
963
+ /* @__PURE__ */ jsx(PrimaryButton, { onPress: () => setComposing(true), icon: /* @__PURE__ */ jsx(Plus, { size: 16 }), children: "New post" }),
964
+ /* @__PURE__ */ jsx(PrimaryButton, { onPress: () => setConnecting(true), icon: /* @__PURE__ */ jsx(Link2, { size: 16 }), children: "Connect account" }),
965
+ /* @__PURE__ */ jsx(PrimaryButton, { onPress: () => void load(), icon: /* @__PURE__ */ jsx(RefreshCw, { size: 16 }), children: "Refresh" })
966
+ ] })
967
+ }
968
+ ),
969
+ state.phase === "error" ? /* @__PURE__ */ jsx(BackendStateCard, { state: state.error, onRetry: () => void load() }) : empty ? /* @__PURE__ */ jsxs(YStack, { gap: "$4", children: [
970
+ state.phase === "ready" ? /* @__PURE__ */ jsx(SocialSummaryBar, { summary: state.data.summary }) : null,
971
+ /* @__PURE__ */ jsx(
972
+ EmptyState,
973
+ {
974
+ icon: Share2,
975
+ title: "No posts or accounts yet",
976
+ description: "Connect a social account, then compose, schedule and publish across X, Instagram, LinkedIn, TikTok and more.",
977
+ primary: { label: "New post", onPress: () => setComposing(true) }
978
+ }
979
+ )
980
+ ] }) : /* @__PURE__ */ jsxs(YStack, { gap: "$5", children: [
981
+ state.phase === "ready" ? /* @__PURE__ */ jsx(SocialSummaryBar, { summary: state.data.summary }) : null,
982
+ /* @__PURE__ */ jsxs(YStack, { gap: "$3", children: [
983
+ /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", flexWrap: "wrap", children: [
984
+ /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
985
+ /* @__PURE__ */ jsx(Send, { size: 16 }),
986
+ /* @__PURE__ */ jsx(Text, { fontSize: "$5", fontWeight: "500", children: "Posts" })
987
+ ] }),
988
+ /* @__PURE__ */ jsx(ViewToggle, { view, onChange: setView })
989
+ ] }),
990
+ view === "list" ? /* @__PURE__ */ jsx(
991
+ DataTable,
992
+ {
993
+ columns: POST_COLUMNS,
994
+ rows: posts,
995
+ loading: state.phase === "loading",
996
+ empty: "No posts yet.",
997
+ rowKey: (p) => p.id,
998
+ onRowPress: (p) => setOpenPost(p)
999
+ }
1000
+ ) : /* @__PURE__ */ jsx(PostAgenda, { posts, onOpen: (p) => setOpenPost(p) })
1001
+ ] }),
1002
+ /* @__PURE__ */ jsxs(YStack, { gap: "$2", children: [
1003
+ /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
1004
+ /* @__PURE__ */ jsx(Link2, { size: 16 }),
1005
+ /* @__PURE__ */ jsx(Text, { fontSize: "$5", fontWeight: "500", children: "Accounts" })
1006
+ ] }),
1007
+ /* @__PURE__ */ jsx(
1008
+ DataTable,
1009
+ {
1010
+ columns: ACCOUNT_COLUMNS,
1011
+ rows: accounts,
1012
+ loading: state.phase === "loading",
1013
+ empty: "No accounts connected yet.",
1014
+ rowKey: (a) => a.id
1015
+ }
1016
+ )
1017
+ ] })
1018
+ ] }),
1019
+ /* @__PURE__ */ jsx(SlideOver, { open: composing, onClose: () => setComposing(false), title: "New post", icon: Send, ariaLabel: "New post", children: /* @__PURE__ */ jsx(PostComposer, { channels: [...PROVIDERS], providers, onSubmit: submitDraft }) }),
1020
+ /* @__PURE__ */ jsx(
1021
+ SlideOver,
1022
+ {
1023
+ open: connecting,
1024
+ onClose: () => setConnecting(false),
1025
+ title: "Connect account",
1026
+ icon: Link2,
1027
+ ariaLabel: "Connect account",
1028
+ children: /* @__PURE__ */ jsx(
1029
+ ConnectAccount,
1030
+ {
1031
+ api,
1032
+ providers,
1033
+ onCreated: () => {
1034
+ setConnecting(false);
1035
+ void load();
1036
+ }
1037
+ }
1038
+ )
1039
+ }
1040
+ ),
1041
+ /* @__PURE__ */ jsx(SlideOver, { open: openPost !== null, onClose: () => setOpenPost(null), title: "Post", icon: Send, ariaLabel: "Post detail", children: openPost ? /* @__PURE__ */ jsx(
1042
+ PostDetail,
1043
+ {
1044
+ api,
1045
+ post: openPost,
1046
+ onChanged: () => {
1047
+ setOpenPost(null);
1048
+ void load();
1049
+ }
1050
+ }
1051
+ ) : null })
1052
+ ] });
1053
+ }
1054
+ var META = {
1055
+ x: { label: "X", bg: "#0f0f12", fg: "#e6e6ea", mark: "\u{1D54F}" },
1056
+ facebook: { label: "Facebook", bg: "#1877f2", fg: "#ffffff", mark: "f" },
1057
+ instagram: { label: "Instagram", bg: "#e1306c", fg: "#ffffff", mark: "IG" },
1058
+ linkedin: { label: "LinkedIn", bg: "#0a66c2", fg: "#ffffff", mark: "in" },
1059
+ tiktok: { label: "TikTok", bg: "#111114", fg: "#25f4ee", mark: "TT" },
1060
+ youtube: { label: "YouTube", bg: "#ff0000", fg: "#ffffff", mark: "\u25B6" },
1061
+ threads: { label: "Threads", bg: "#111114", fg: "#e6e6ea", mark: "@" }
1062
+ };
1063
+ function ChannelBadge({
1064
+ channel,
1065
+ showLabel = false,
1066
+ size = 22
1067
+ }) {
1068
+ const m = META[channel] ?? META.x;
1069
+ return /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
1070
+ /* @__PURE__ */ jsx(
1071
+ YStack,
1072
+ {
1073
+ width: size,
1074
+ height: size,
1075
+ items: "center",
1076
+ justify: "center",
1077
+ rounded: "$2",
1078
+ bg: m.bg,
1079
+ children: /* @__PURE__ */ jsx(Text, { fontSize: "$1", fontWeight: "800", color: m.fg, children: m.mark })
1080
+ }
1081
+ ),
1082
+ showLabel ? /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color12", children: m.label }) : null
1083
+ ] });
1084
+ }
1085
+ var money = (cents) => `$${Math.round((cents || 0) / 100).toLocaleString()}`;
1086
+ function CampaignCard({ campaign }) {
1087
+ const pct = campaign.budget > 0 ? Math.min(100, campaign.spend / campaign.budget * 100) : 0;
1088
+ return /* @__PURE__ */ jsxs(Card, { p: "$4", gap: "$3", borderWidth: 1, borderColor: "$borderColor", width: "100%", children: [
1089
+ /* @__PURE__ */ jsxs(XStack, { items: "flex-start", justify: "space-between", gap: "$2", children: [
1090
+ /* @__PURE__ */ jsxs(YStack, { gap: "$1", children: [
1091
+ /* @__PURE__ */ jsx(Text, { fontSize: "$4", fontWeight: "700", children: campaign.name }),
1092
+ /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color11", children: campaign.channel })
1093
+ ] }),
1094
+ /* @__PURE__ */ jsx(StatusTag, { status: campaign.status })
1095
+ ] }),
1096
+ campaign.objective ? /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color11", children: campaign.objective }) : null,
1097
+ /* @__PURE__ */ jsxs(YStack, { gap: "$1.5", children: [
1098
+ /* @__PURE__ */ jsxs(XStack, { justify: "space-between", children: [
1099
+ /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color11", children: [
1100
+ money(campaign.spend),
1101
+ " spent"
1102
+ ] }),
1103
+ /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color11", children: [
1104
+ money(campaign.budget),
1105
+ " budget"
1106
+ ] })
1107
+ ] }),
1108
+ /* @__PURE__ */ jsx(YStack, { height: 6, rounded: "$2", bg: "$color3", overflow: "hidden", children: /* @__PURE__ */ jsx(YStack, { height: 6, rounded: "$2", bg: "$color9", width: `${pct}%` }) })
1109
+ ] })
1110
+ ] });
1111
+ }
1112
+ function PostCard({
1113
+ post,
1114
+ onEdit,
1115
+ onDelete
1116
+ }) {
1117
+ const mediaCount = post.media?.length ?? 0;
1118
+ return /* @__PURE__ */ jsxs(Card, { p: "$3", gap: "$2.5", borderWidth: 1, borderColor: "$borderColor", width: "100%", children: [
1119
+ /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [
1120
+ /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
1121
+ /* @__PURE__ */ jsx(ChannelBadge, { channel: post.channel }),
1122
+ /* @__PURE__ */ jsx(StatusTag, { status: post.status })
1123
+ ] }),
1124
+ /* @__PURE__ */ jsxs(XStack, { gap: "$1", children: [
1125
+ onEdit ? /* @__PURE__ */ jsx(Button, { size: "$2", chromeless: true, icon: /* @__PURE__ */ jsx(Pencil, { size: 15 }), onPress: () => onEdit(post) }) : null,
1126
+ onDelete ? /* @__PURE__ */ jsx(Button, { size: "$2", chromeless: true, icon: /* @__PURE__ */ jsx(Trash2, { size: 15 }), onPress: () => onDelete(post) }) : null
1127
+ ] })
1128
+ ] }),
1129
+ /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color12", children: post.content }),
1130
+ /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [
1131
+ post.scheduleAt ? /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$color11", children: formatPostTime(post.scheduleAt) }) : /* @__PURE__ */ jsx(YStack, {}),
1132
+ mediaCount ? /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color10", children: [
1133
+ mediaCount,
1134
+ " media"
1135
+ ] }) : null
1136
+ ] })
1137
+ ] });
1138
+ }
1139
+
1140
+ export { BackendStateCard, COMPOSE_MODES, CampaignCard, ChannelBadge, DataTable, EmptyState, FieldRow, FieldSelect, FieldSlider, FieldSwitch, FieldText, FieldTextArea, HostProvider, LG, PageHeader, PostAgenda, PostCard, PostComposer, PrimaryButton, ProviderReadinessList, SlideOver, SocialResource, SocialSummaryBar, StatusTag, ViewToggle, accentFor, asColor, classifyBackend, classifyRead, contrastText, formatPostTime, isHexColor, parsePostTime, postDayBucket, postPreview, resolveAccent, setOrgAccent, useAccent, useHost };
1141
+ //# sourceMappingURL=chunk-23V6BPHG.js.map
1142
+ //# sourceMappingURL=chunk-23V6BPHG.js.map