@hanzo/ui 8.0.12 → 8.0.13

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.
@@ -4,7 +4,7 @@ export { AgnosticThemeToggle, ThemeToggle, ThemeToggleNext } from '../chunk-42CU
4
4
  import { createContext, useRef, useState, useEffect, useCallback, useLayoutEffect, useMemo, cloneElement, useContext, useSyncExternalStore } from 'react';
5
5
  import { useTheme, XStack, Text, YStack, Card, Theme, useThemeName, Portal, Input, Button, Spinner, Popover, Separator, useControllableState, Label, TextArea, Switch, Slider, ScrollView } from '@hanzo/gui';
6
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
- import { Check, ChevronRight, ChevronDown, RefreshCw, Grip, CreditCard, Users, Bot, MessageCircle, AppWindow, LayoutGrid, Sparkles, UserRound, Settings2, LogOut, ChevronsUpDown, Plus, Search, TriangleAlert, ChevronUp, Trash2, ExternalLink, ArrowRight, Cpu, Globe, Server, X, Pencil, Calendar, AlertTriangle, CheckCircle2, List, Info, CircleX, CircleCheck } from '@hanzogui/lucide-icons-2';
7
+ import { Check, ChevronRight, ChevronDown, RefreshCw, Grip, CreditCard, Users, Bot, MessageCircle, AppWindow, LayoutGrid, Sparkles, UserRound, Settings2, LogOut, ChevronsUpDown, Plus, Search, TriangleAlert, ChevronUp, Trash2, ExternalLink, ArrowRight, Cpu, Globe, Server, X, Calendar, AlertTriangle, CheckCircle2, List, Link2, Share2, Send, Pencil, Info, CircleX, CircleCheck } from '@hanzogui/lucide-icons-2';
8
8
  export { Boxes as CustomModelMark } from '@hanzogui/lucide-icons-2';
9
9
  import { surfaceById } from '@hanzo/products';
10
10
  import { HanzoLogo } from '@hanzo/logo/react';
@@ -1076,7 +1076,7 @@ function OrgSwitcher({ scope, orgs, pageSize = 20, current: given, create, picke
1076
1076
  const [open, setOpen] = useState(false);
1077
1077
  const [query, setQuery] = useState("");
1078
1078
  const [debounced, setDebounced] = useState("");
1079
- const [rows, setRows] = useState([]);
1079
+ const [rows2, setRows] = useState([]);
1080
1080
  const [loading, setLoading] = useState(false);
1081
1081
  const [loadingMore, setLoadingMore] = useState(false);
1082
1082
  const [hasMore, setHasMore] = useState(false);
@@ -1128,13 +1128,13 @@ function OrgSwitcher({ scope, orgs, pageSize = 20, current: given, create, picke
1128
1128
  void fetchPage(pageRef.current + 1, debounced, true);
1129
1129
  }, [loading, loadingMore, hasMore, debounced, fetchPage]);
1130
1130
  const visible = useMemo(() => {
1131
- if (orgs) return filterOrgs(rows, query);
1131
+ if (orgs) return filterOrgs(rows2, query);
1132
1132
  return [{ name: currentId, displayName: titleCase(currentId) }];
1133
- }, [orgs, rows, query, currentId]);
1133
+ }, [orgs, rows2, query, currentId]);
1134
1134
  const current2 = useMemo(() => {
1135
1135
  if (given && given.name === currentId) return given;
1136
- return rows.find((o) => o.name === currentId) ?? { name: currentId, displayName: titleCase(currentId) };
1137
- }, [given, rows, currentId]);
1136
+ return rows2.find((o) => o.name === currentId) ?? { name: currentId, displayName: titleCase(currentId) };
1137
+ }, [given, rows2, currentId]);
1138
1138
  const currentLabel = current2.displayName || titleCase(current2.name);
1139
1139
  const select = useCallback(
1140
1140
  (org) => {
@@ -1684,7 +1684,7 @@ function SkeletonRows({ columns, count = 6 }) {
1684
1684
  }
1685
1685
  function DataTable({
1686
1686
  columns,
1687
- rows,
1687
+ rows: rows2,
1688
1688
  loading,
1689
1689
  empty = "Nothing here yet.",
1690
1690
  rowKey,
@@ -1734,7 +1734,7 @@ function DataTable({
1734
1734
  c.key
1735
1735
  );
1736
1736
  }) }),
1737
- loading ? /* @__PURE__ */ jsx(SkeletonRows, { columns }) : rows.length > 0 ? /* @__PURE__ */ jsx(YStack, { children: rows.map((row) => {
1737
+ loading ? /* @__PURE__ */ jsx(SkeletonRows, { columns }) : rows2.length > 0 ? /* @__PURE__ */ jsx(YStack, { children: rows2.map((row) => {
1738
1738
  const expanded = (isRowExpanded?.(row) ?? false) && !!renderExpanded;
1739
1739
  return /* @__PURE__ */ jsxs(YStack, { children: [
1740
1740
  /* @__PURE__ */ jsx(
@@ -1782,7 +1782,7 @@ function DataTable({
1782
1782
  ] }, rowKey(row));
1783
1783
  }) }) : null
1784
1784
  ] }) }),
1785
- !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
1785
+ !loading && rows2.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
1786
1786
  ] });
1787
1787
  }
1788
1788
  var FLEX_MIN_COL_W = 120;
@@ -2232,9 +2232,9 @@ function FieldTextArea({
2232
2232
  value,
2233
2233
  onChange,
2234
2234
  disabled,
2235
- rows = 6
2235
+ rows: rows2 = 6
2236
2236
  }) {
2237
- return /* @__PURE__ */ jsx(TextArea, { value, onChangeText: onChange, disabled, numberOfLines: rows });
2237
+ return /* @__PURE__ */ jsx(TextArea, { value, onChangeText: onChange, disabled, numberOfLines: rows2 });
2238
2238
  }
2239
2239
  function FieldSwitch({
2240
2240
  checked,
@@ -2784,63 +2784,94 @@ function useToast() {
2784
2784
  if (!ctx) throw new Error("useToast must be used within <ToastProvider>");
2785
2785
  return ctx;
2786
2786
  }
2787
- var META = {
2788
- x: { label: "X", bg: "#0f0f12", fg: "#e6e6ea", mark: "\u{1D54F}" },
2789
- facebook: { label: "Facebook", bg: "#1877f2", fg: "#ffffff", mark: "f" },
2790
- instagram: { label: "Instagram", bg: "#e1306c", fg: "#ffffff", mark: "IG" },
2791
- linkedin: { label: "LinkedIn", bg: "#0a66c2", fg: "#ffffff", mark: "in" },
2792
- tiktok: { label: "TikTok", bg: "#111114", fg: "#25f4ee", mark: "TT" },
2793
- youtube: { label: "YouTube", bg: "#ff0000", fg: "#ffffff", mark: "\u25B6" },
2794
- threads: { label: "Threads", bg: "#111114", fg: "#e6e6ea", mark: "@" }
2787
+
2788
+ // src/product/social/api.ts
2789
+ var enc = encodeURIComponent;
2790
+ var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
2791
+ var num = (v) => {
2792
+ if (typeof v === "number" && Number.isFinite(v)) return v;
2793
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
2794
+ return 0;
2795
2795
  };
2796
- function ChannelBadge({
2797
- channel,
2798
- showLabel = false,
2799
- size = 22
2800
- }) {
2801
- const m = META[channel] ?? META.x;
2802
- return /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
2803
- /* @__PURE__ */ jsx(
2804
- YStack,
2805
- {
2806
- width: size,
2807
- height: size,
2808
- items: "center",
2809
- justify: "center",
2810
- rounded: "$2",
2811
- bg: m.bg,
2812
- children: /* @__PURE__ */ jsx(Text, { fontSize: "$1", fontWeight: "800", color: m.fg, children: m.mark })
2813
- }
2814
- ),
2815
- showLabel ? /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color12", children: m.label }) : null
2816
- ] });
2796
+ var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
2797
+ var asRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
2798
+ var rows = (payload) => {
2799
+ if (Array.isArray(payload)) return payload.filter((x) => x && typeof x === "object");
2800
+ if (payload && typeof payload === "object") {
2801
+ for (const k of ["data", "items", "rows"]) {
2802
+ const v = payload[k];
2803
+ if (Array.isArray(v)) return v.filter((x) => x && typeof x === "object");
2804
+ }
2805
+ }
2806
+ return [];
2807
+ };
2808
+ var PROVIDERS = ["x", "facebook", "instagram", "linkedin", "tiktok", "youtube", "threads"];
2809
+ var ACCOUNT_STATUSES = ["connected", "disconnected", "error"];
2810
+ var POST_STATUSES = ["draft", "scheduled", "published", "failed"];
2811
+ function normalizeAccount(raw) {
2812
+ const r = asRecord(raw);
2813
+ return {
2814
+ id: str(r.id),
2815
+ provider: str(r.provider) || "x",
2816
+ handle: str(r.handle),
2817
+ status: str(r.status) || "connected",
2818
+ createdAt: num(r.createdAt),
2819
+ updatedAt: num(r.updatedAt)
2820
+ };
2817
2821
  }
2818
- var money = (cents) => `$${Math.round((cents || 0) / 100).toLocaleString()}`;
2819
- function CampaignCard({ campaign }) {
2820
- const pct = campaign.budget > 0 ? Math.min(100, campaign.spend / campaign.budget * 100) : 0;
2821
- return /* @__PURE__ */ jsxs(Card, { p: "$4", gap: "$3", borderWidth: 1, borderColor: "$borderColor", width: "100%", children: [
2822
- /* @__PURE__ */ jsxs(XStack, { items: "flex-start", justify: "space-between", gap: "$2", children: [
2823
- /* @__PURE__ */ jsxs(YStack, { gap: "$1", children: [
2824
- /* @__PURE__ */ jsx(Text, { fontSize: "$4", fontWeight: "700", children: campaign.name }),
2825
- /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color11", children: campaign.channel })
2826
- ] }),
2827
- /* @__PURE__ */ jsx(StatusTag, { status: campaign.status })
2828
- ] }),
2829
- campaign.objective ? /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color11", children: campaign.objective }) : null,
2830
- /* @__PURE__ */ jsxs(YStack, { gap: "$1.5", children: [
2831
- /* @__PURE__ */ jsxs(XStack, { justify: "space-between", children: [
2832
- /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color11", children: [
2833
- money(campaign.spend),
2834
- " spent"
2835
- ] }),
2836
- /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color11", children: [
2837
- money(campaign.budget),
2838
- " budget"
2839
- ] })
2840
- ] }),
2841
- /* @__PURE__ */ jsx(YStack, { height: 6, rounded: "$2", bg: "$color3", overflow: "hidden", children: /* @__PURE__ */ jsx(YStack, { height: 6, rounded: "$2", bg: "$color9", width: `${pct}%` }) })
2842
- ] })
2843
- ] });
2822
+ function normalizePost(raw) {
2823
+ const r = asRecord(raw);
2824
+ return {
2825
+ id: str(r.id),
2826
+ content: str(r.content),
2827
+ channel: str(r.channel) || "x",
2828
+ status: str(r.status) || "draft",
2829
+ scheduleAt: num(r.scheduleAt),
2830
+ media: strs(r.media),
2831
+ accountId: str(r.accountId) || void 0,
2832
+ externalId: str(r.externalId) || void 0,
2833
+ error: str(r.error) || void 0,
2834
+ createdAt: num(r.createdAt),
2835
+ updatedAt: num(r.updatedAt)
2836
+ };
2837
+ }
2838
+ function normalizeSummary(raw) {
2839
+ const r = asRecord(raw);
2840
+ return { posts: num(r.posts), scheduled: num(r.scheduled), published: num(r.published), accounts: num(r.accounts) };
2841
+ }
2842
+ function normalizeProviderCapability(raw) {
2843
+ const r = asRecord(raw);
2844
+ return {
2845
+ provider: str(r.provider),
2846
+ credentialsConfigured: Boolean(r.credentialsConfigured),
2847
+ missingCredentials: strs(r.missingCredentials)
2848
+ };
2849
+ }
2850
+ var normalizeAccounts = (p) => rows(p).map(normalizeAccount).filter((a) => a.id);
2851
+ var normalizePosts = (p) => rows(p).map(normalizePost).filter((x) => x.id);
2852
+ var normalizeProviders = (p) => rows(p).map(normalizeProviderCapability).filter((c) => c.provider);
2853
+ function createSocialApi(rest) {
2854
+ return {
2855
+ summary: () => rest.get("summary").then(normalizeSummary),
2856
+ /** Publish-readiness per network (+ the exact missing OAuth-app credentials). */
2857
+ providers: () => rest.get("providers").then(normalizeProviders),
2858
+ accounts: {
2859
+ list: (provider) => rest.get(`accounts${provider ? `?provider=${enc(provider)}` : ""}`).then(normalizeAccounts),
2860
+ get: (id) => rest.get(`accounts/${enc(id)}`).then(normalizeAccount),
2861
+ create: (body) => rest.post("accounts", body).then(normalizeAccount),
2862
+ update: (id, body) => rest.put(`accounts/${enc(id)}`, body).then(normalizeAccount),
2863
+ remove: (id) => rest.del(`accounts/${enc(id)}`)
2864
+ },
2865
+ posts: {
2866
+ list: (status) => rest.get(`posts${status ? `?status=${enc(status)}` : ""}`).then(normalizePosts),
2867
+ get: (id) => rest.get(`posts/${enc(id)}`).then(normalizePost),
2868
+ create: (body) => rest.post("posts", body).then(normalizePost),
2869
+ update: (id, body) => rest.put(`posts/${enc(id)}`, body).then(normalizePost),
2870
+ remove: (id) => rest.del(`posts/${enc(id)}`),
2871
+ /** Publish a post NOW to its channel's connected accounts. */
2872
+ publish: (id) => rest.post(`posts/${enc(id)}/publish`).then(normalizePost)
2873
+ }
2874
+ };
2844
2875
  }
2845
2876
 
2846
2877
  // src/product/social/format.ts
@@ -2867,33 +2898,6 @@ function parsePostTime(dt) {
2867
2898
  const ms = Date.parse(dt.trim());
2868
2899
  return Number.isFinite(ms) && ms > 0 ? Math.floor(ms / 1e3) : 0;
2869
2900
  }
2870
- function PostCard({
2871
- post,
2872
- onEdit,
2873
- onDelete
2874
- }) {
2875
- const mediaCount = post.media?.length ?? 0;
2876
- return /* @__PURE__ */ jsxs(Card, { p: "$3", gap: "$2.5", borderWidth: 1, borderColor: "$borderColor", width: "100%", children: [
2877
- /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [
2878
- /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
2879
- /* @__PURE__ */ jsx(ChannelBadge, { channel: post.channel }),
2880
- /* @__PURE__ */ jsx(StatusTag, { status: post.status })
2881
- ] }),
2882
- /* @__PURE__ */ jsxs(XStack, { gap: "$1", children: [
2883
- onEdit ? /* @__PURE__ */ jsx(Button, { size: "$2", chromeless: true, icon: /* @__PURE__ */ jsx(Pencil, { size: 15 }), onPress: () => onEdit(post) }) : null,
2884
- onDelete ? /* @__PURE__ */ jsx(Button, { size: "$2", chromeless: true, icon: /* @__PURE__ */ jsx(Trash2, { size: 15 }), onPress: () => onDelete(post) }) : null
2885
- ] })
2886
- ] }),
2887
- /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color12", children: post.content }),
2888
- /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [
2889
- post.scheduleAt ? /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$color11", children: formatPostTime(post.scheduleAt) }) : /* @__PURE__ */ jsx(YStack, {}),
2890
- mediaCount ? /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color10", children: [
2891
- mediaCount,
2892
- " media"
2893
- ] }) : null
2894
- ] })
2895
- ] });
2896
- }
2897
2901
  function PostAgenda({ posts, onOpen }) {
2898
2902
  const days = useMemo(() => {
2899
2903
  const timed = posts.filter((p) => (p.scheduleAt ?? 0) > 0).sort((a, b) => (a.scheduleAt ?? 0) - (b.scheduleAt ?? 0));
@@ -3101,7 +3105,302 @@ function ViewToggle({
3101
3105
  );
3102
3106
  }) });
3103
3107
  }
3108
+ var POST_COLUMNS = [
3109
+ { key: "content", header: "Post", render: (p) => postPreview(p.content) },
3110
+ { key: "channel", header: "Channel", render: (p) => p.channel || "\u2014" },
3111
+ { key: "status", header: "Status", render: (p) => /* @__PURE__ */ jsx(StatusTag, { status: p.status }) },
3112
+ { key: "scheduleAt", header: "Scheduled", render: (p) => formatPostTime(p.scheduleAt) }
3113
+ ];
3114
+ var ACCOUNT_COLUMNS = [
3115
+ { key: "handle", header: "Account", render: (a) => a.handle || "\u2014" },
3116
+ { key: "provider", header: "Network", render: (a) => a.provider || "\u2014" },
3117
+ { key: "status", header: "Status", render: (a) => /* @__PURE__ */ jsx(StatusTag, { status: a.status }) }
3118
+ ];
3119
+ function PostDetail({ api, post, onChanged }) {
3120
+ const [publishing, setPublishing] = useState(false);
3121
+ const [error, setError] = useState(null);
3122
+ const canPublish = post.status === "draft" || post.status === "scheduled" || post.status === "failed";
3123
+ const publish = async () => {
3124
+ setPublishing(true);
3125
+ setError(null);
3126
+ try {
3127
+ await api.posts.publish(post.id);
3128
+ onChanged();
3129
+ } catch (e) {
3130
+ setError(classifyBackend(e).message || "Publish failed.");
3131
+ } finally {
3132
+ setPublishing(false);
3133
+ }
3134
+ };
3135
+ return /* @__PURE__ */ jsxs(YStack, { gap: "$3", p: "$4", children: [
3136
+ /* @__PURE__ */ jsx(FieldRow, { label: "Content", children: /* @__PURE__ */ jsx(Text, { fontSize: "$3", children: post.content || "\u2014" }) }),
3137
+ /* @__PURE__ */ jsx(FieldRow, { label: "Channel", children: /* @__PURE__ */ jsx(Text, { fontSize: "$3", children: post.channel }) }),
3138
+ /* @__PURE__ */ jsx(FieldRow, { label: "Status", children: /* @__PURE__ */ jsx(StatusTag, { status: post.status }) }),
3139
+ post.scheduleAt > 0 ? /* @__PURE__ */ jsx(FieldRow, { label: "Scheduled", children: /* @__PURE__ */ jsx(Text, { fontSize: "$3", children: formatPostTime(post.scheduleAt) }) }) : null,
3140
+ 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,
3141
+ post.externalId ? /* @__PURE__ */ jsx(FieldRow, { label: "External id", children: /* @__PURE__ */ jsx(Text, { fontSize: "$3", className: "hz-tnum", children: post.externalId }) }) : null,
3142
+ post.error ? /* @__PURE__ */ jsx(FieldRow, { label: "Last error", children: /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$red10", children: post.error }) }) : null,
3143
+ error ? /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$red10", children: error }) : null,
3144
+ canPublish ? /* @__PURE__ */ jsx(PrimaryButton, { onPress: publish, disabled: publishing, icon: /* @__PURE__ */ jsx(Send, { size: 16 }), children: publishing ? "Publishing\u2026" : "Publish now" }) : null
3145
+ ] });
3146
+ }
3147
+ function ConnectAccount({
3148
+ api,
3149
+ providers,
3150
+ onCreated
3151
+ }) {
3152
+ const [provider, setProvider] = useState("x");
3153
+ const [handle, setHandle] = useState("");
3154
+ const [saving, setSaving] = useState(false);
3155
+ const [error, setError] = useState(null);
3156
+ const submit = async () => {
3157
+ setSaving(true);
3158
+ setError(null);
3159
+ try {
3160
+ await api.accounts.create({ provider, handle: handle.trim(), status: "connected" });
3161
+ onCreated();
3162
+ } catch (e) {
3163
+ setError(classifyBackend(e).message || "Failed to connect account.");
3164
+ } finally {
3165
+ setSaving(false);
3166
+ }
3167
+ };
3168
+ return /* @__PURE__ */ jsxs(YStack, { gap: "$4", p: "$4", children: [
3169
+ /* @__PURE__ */ jsx(ProviderReadinessList, { providers }),
3170
+ /* @__PURE__ */ jsxs(YStack, { gap: "$3", children: [
3171
+ /* @__PURE__ */ jsx(FieldRow, { label: "Network", children: /* @__PURE__ */ jsx(FieldSelect, { value: provider, options: [...PROVIDERS], onChange: setProvider, disabled: saving }) }),
3172
+ /* @__PURE__ */ jsx(FieldRow, { label: "Handle", children: /* @__PURE__ */ jsx(FieldText, { value: handle, onChange: setHandle, placeholder: "@hanzo", disabled: saving }) }),
3173
+ error ? /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$red10", children: error }) : null,
3174
+ /* @__PURE__ */ jsx(PrimaryButton, { onPress: submit, disabled: saving, children: saving ? "Connecting\u2026" : "Connect account" })
3175
+ ] })
3176
+ ] });
3177
+ }
3178
+ function SocialResource({
3179
+ api,
3180
+ title = "Publish",
3181
+ subtitle = "Compose, schedule and publish your content across networks \u2014 per org, over the native /v1/social engine."
3182
+ }) {
3183
+ const [state, setState] = useState({ phase: "loading" });
3184
+ const [view, setView] = useState("list");
3185
+ const [composing, setComposing] = useState(false);
3186
+ const [connecting, setConnecting] = useState(false);
3187
+ const [openPost, setOpenPost] = useState(null);
3188
+ const load = useCallback(async () => {
3189
+ setState({ phase: "loading" });
3190
+ try {
3191
+ const [summary, posts2, accounts2, providers2] = await Promise.all([
3192
+ api.summary(),
3193
+ api.posts.list(),
3194
+ api.accounts.list(),
3195
+ api.providers()
3196
+ ]);
3197
+ setState({ phase: "ready", data: { summary, posts: posts2, accounts: accounts2, providers: providers2 } });
3198
+ } catch (e) {
3199
+ setState({ phase: "error", error: classifyBackend(e) });
3200
+ }
3201
+ }, [api]);
3202
+ useEffect(() => {
3203
+ void load();
3204
+ }, [load]);
3205
+ const providers = state.phase === "ready" ? state.data.providers : [];
3206
+ const posts = state.phase === "ready" ? state.data.posts : [];
3207
+ const accounts = state.phase === "ready" ? state.data.accounts : [];
3208
+ const empty = state.phase === "ready" && posts.length === 0 && accounts.length === 0;
3209
+ const submitDraft = async (draft, mode) => {
3210
+ try {
3211
+ const created = await api.posts.create(draft);
3212
+ if (mode === "now" && created.status === "failed") return created.error || "Publish failed.";
3213
+ } catch (e) {
3214
+ return classifyBackend(e).message || "Failed to create post.";
3215
+ }
3216
+ setComposing(false);
3217
+ void load();
3218
+ return null;
3219
+ };
3220
+ return /* @__PURE__ */ jsxs(YStack, { gap: "$4", p: "$4", children: [
3221
+ /* @__PURE__ */ jsx(
3222
+ PageHeader,
3223
+ {
3224
+ title,
3225
+ subtitle,
3226
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [
3227
+ /* @__PURE__ */ jsx(PrimaryButton, { onPress: () => setComposing(true), icon: /* @__PURE__ */ jsx(Plus, { size: 16 }), children: "New post" }),
3228
+ /* @__PURE__ */ jsx(PrimaryButton, { onPress: () => setConnecting(true), icon: /* @__PURE__ */ jsx(Link2, { size: 16 }), children: "Connect account" }),
3229
+ /* @__PURE__ */ jsx(PrimaryButton, { onPress: () => void load(), icon: /* @__PURE__ */ jsx(RefreshCw, { size: 16 }), children: "Refresh" })
3230
+ ] })
3231
+ }
3232
+ ),
3233
+ state.phase === "error" ? /* @__PURE__ */ jsx(BackendStateCard, { state: state.error, onRetry: () => void load() }) : empty ? /* @__PURE__ */ jsxs(YStack, { gap: "$4", children: [
3234
+ state.phase === "ready" ? /* @__PURE__ */ jsx(SocialSummaryBar, { summary: state.data.summary }) : null,
3235
+ /* @__PURE__ */ jsx(
3236
+ EmptyState,
3237
+ {
3238
+ icon: Share2,
3239
+ title: "No posts or accounts yet",
3240
+ description: "Connect a social account, then compose, schedule and publish across X, Instagram, LinkedIn, TikTok and more.",
3241
+ primary: { label: "New post", onPress: () => setComposing(true) }
3242
+ }
3243
+ )
3244
+ ] }) : /* @__PURE__ */ jsxs(YStack, { gap: "$5", children: [
3245
+ state.phase === "ready" ? /* @__PURE__ */ jsx(SocialSummaryBar, { summary: state.data.summary }) : null,
3246
+ /* @__PURE__ */ jsxs(YStack, { gap: "$3", children: [
3247
+ /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", flexWrap: "wrap", children: [
3248
+ /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
3249
+ /* @__PURE__ */ jsx(Send, { size: 16 }),
3250
+ /* @__PURE__ */ jsx(Text, { fontSize: "$5", fontWeight: "500", children: "Posts" })
3251
+ ] }),
3252
+ /* @__PURE__ */ jsx(ViewToggle, { view, onChange: setView })
3253
+ ] }),
3254
+ view === "list" ? /* @__PURE__ */ jsx(
3255
+ DataTable,
3256
+ {
3257
+ columns: POST_COLUMNS,
3258
+ rows: posts,
3259
+ loading: state.phase === "loading",
3260
+ empty: "No posts yet.",
3261
+ rowKey: (p) => p.id,
3262
+ onRowPress: (p) => setOpenPost(p)
3263
+ }
3264
+ ) : /* @__PURE__ */ jsx(PostAgenda, { posts, onOpen: (p) => setOpenPost(p) })
3265
+ ] }),
3266
+ /* @__PURE__ */ jsxs(YStack, { gap: "$2", children: [
3267
+ /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
3268
+ /* @__PURE__ */ jsx(Link2, { size: 16 }),
3269
+ /* @__PURE__ */ jsx(Text, { fontSize: "$5", fontWeight: "500", children: "Accounts" })
3270
+ ] }),
3271
+ /* @__PURE__ */ jsx(
3272
+ DataTable,
3273
+ {
3274
+ columns: ACCOUNT_COLUMNS,
3275
+ rows: accounts,
3276
+ loading: state.phase === "loading",
3277
+ empty: "No accounts connected yet.",
3278
+ rowKey: (a) => a.id
3279
+ }
3280
+ )
3281
+ ] })
3282
+ ] }),
3283
+ /* @__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 }) }),
3284
+ /* @__PURE__ */ jsx(
3285
+ SlideOver,
3286
+ {
3287
+ open: connecting,
3288
+ onClose: () => setConnecting(false),
3289
+ title: "Connect account",
3290
+ icon: Link2,
3291
+ ariaLabel: "Connect account",
3292
+ children: /* @__PURE__ */ jsx(
3293
+ ConnectAccount,
3294
+ {
3295
+ api,
3296
+ providers,
3297
+ onCreated: () => {
3298
+ setConnecting(false);
3299
+ void load();
3300
+ }
3301
+ }
3302
+ )
3303
+ }
3304
+ ),
3305
+ /* @__PURE__ */ jsx(SlideOver, { open: openPost !== null, onClose: () => setOpenPost(null), title: "Post", icon: Send, ariaLabel: "Post detail", children: openPost ? /* @__PURE__ */ jsx(
3306
+ PostDetail,
3307
+ {
3308
+ api,
3309
+ post: openPost,
3310
+ onChanged: () => {
3311
+ setOpenPost(null);
3312
+ void load();
3313
+ }
3314
+ }
3315
+ ) : null })
3316
+ ] });
3317
+ }
3318
+ var META = {
3319
+ x: { label: "X", bg: "#0f0f12", fg: "#e6e6ea", mark: "\u{1D54F}" },
3320
+ facebook: { label: "Facebook", bg: "#1877f2", fg: "#ffffff", mark: "f" },
3321
+ instagram: { label: "Instagram", bg: "#e1306c", fg: "#ffffff", mark: "IG" },
3322
+ linkedin: { label: "LinkedIn", bg: "#0a66c2", fg: "#ffffff", mark: "in" },
3323
+ tiktok: { label: "TikTok", bg: "#111114", fg: "#25f4ee", mark: "TT" },
3324
+ youtube: { label: "YouTube", bg: "#ff0000", fg: "#ffffff", mark: "\u25B6" },
3325
+ threads: { label: "Threads", bg: "#111114", fg: "#e6e6ea", mark: "@" }
3326
+ };
3327
+ function ChannelBadge({
3328
+ channel,
3329
+ showLabel = false,
3330
+ size = 22
3331
+ }) {
3332
+ const m = META[channel] ?? META.x;
3333
+ return /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
3334
+ /* @__PURE__ */ jsx(
3335
+ YStack,
3336
+ {
3337
+ width: size,
3338
+ height: size,
3339
+ items: "center",
3340
+ justify: "center",
3341
+ rounded: "$2",
3342
+ bg: m.bg,
3343
+ children: /* @__PURE__ */ jsx(Text, { fontSize: "$1", fontWeight: "800", color: m.fg, children: m.mark })
3344
+ }
3345
+ ),
3346
+ showLabel ? /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color12", children: m.label }) : null
3347
+ ] });
3348
+ }
3349
+ var money = (cents) => `$${Math.round((cents || 0) / 100).toLocaleString()}`;
3350
+ function CampaignCard({ campaign }) {
3351
+ const pct = campaign.budget > 0 ? Math.min(100, campaign.spend / campaign.budget * 100) : 0;
3352
+ return /* @__PURE__ */ jsxs(Card, { p: "$4", gap: "$3", borderWidth: 1, borderColor: "$borderColor", width: "100%", children: [
3353
+ /* @__PURE__ */ jsxs(XStack, { items: "flex-start", justify: "space-between", gap: "$2", children: [
3354
+ /* @__PURE__ */ jsxs(YStack, { gap: "$1", children: [
3355
+ /* @__PURE__ */ jsx(Text, { fontSize: "$4", fontWeight: "700", children: campaign.name }),
3356
+ /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color11", children: campaign.channel })
3357
+ ] }),
3358
+ /* @__PURE__ */ jsx(StatusTag, { status: campaign.status })
3359
+ ] }),
3360
+ campaign.objective ? /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color11", children: campaign.objective }) : null,
3361
+ /* @__PURE__ */ jsxs(YStack, { gap: "$1.5", children: [
3362
+ /* @__PURE__ */ jsxs(XStack, { justify: "space-between", children: [
3363
+ /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color11", children: [
3364
+ money(campaign.spend),
3365
+ " spent"
3366
+ ] }),
3367
+ /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color11", children: [
3368
+ money(campaign.budget),
3369
+ " budget"
3370
+ ] })
3371
+ ] }),
3372
+ /* @__PURE__ */ jsx(YStack, { height: 6, rounded: "$2", bg: "$color3", overflow: "hidden", children: /* @__PURE__ */ jsx(YStack, { height: 6, rounded: "$2", bg: "$color9", width: `${pct}%` }) })
3373
+ ] })
3374
+ ] });
3375
+ }
3376
+ function PostCard({
3377
+ post,
3378
+ onEdit,
3379
+ onDelete
3380
+ }) {
3381
+ const mediaCount = post.media?.length ?? 0;
3382
+ return /* @__PURE__ */ jsxs(Card, { p: "$3", gap: "$2.5", borderWidth: 1, borderColor: "$borderColor", width: "100%", children: [
3383
+ /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [
3384
+ /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", children: [
3385
+ /* @__PURE__ */ jsx(ChannelBadge, { channel: post.channel }),
3386
+ /* @__PURE__ */ jsx(StatusTag, { status: post.status })
3387
+ ] }),
3388
+ /* @__PURE__ */ jsxs(XStack, { gap: "$1", children: [
3389
+ onEdit ? /* @__PURE__ */ jsx(Button, { size: "$2", chromeless: true, icon: /* @__PURE__ */ jsx(Pencil, { size: 15 }), onPress: () => onEdit(post) }) : null,
3390
+ onDelete ? /* @__PURE__ */ jsx(Button, { size: "$2", chromeless: true, icon: /* @__PURE__ */ jsx(Trash2, { size: 15 }), onPress: () => onDelete(post) }) : null
3391
+ ] })
3392
+ ] }),
3393
+ /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color12", children: post.content }),
3394
+ /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [
3395
+ post.scheduleAt ? /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$color11", children: formatPostTime(post.scheduleAt) }) : /* @__PURE__ */ jsx(YStack, {}),
3396
+ mediaCount ? /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color10", children: [
3397
+ mediaCount,
3398
+ " media"
3399
+ ] }) : null
3400
+ ] })
3401
+ ] });
3402
+ }
3104
3403
 
3105
- export { AnimatedLogo, AppHeader, BRANDS, BackendStateCard, BarChart, BarRows, BrandMark, CHART_OTHER, CHART_PALETTE, COMPOSE_MODES, CampaignCard, ChannelBadge, ComboBox, CommerceResource, ConfirmDelete, ContextMenu, DataTable, Donut, Donut2 as DonutRing, DropdownMenu, EmptyState, FadeIn, FieldRow, FieldSelect, FieldSlider, FieldSwitch, FieldText, FieldTextArea, FloatingMenu, GenericLogo, HANZO, HANZO_MARK_CONTENT, HanzoMark, HintButton, HostProvider, LUX, LegendDot, LineChart, MenuItemView, MenuLabelView, MenuPanel, MenuSeparatorView, MetricCard, Sparkline2 as MetricSparkline, MiniBars, OrgMark, OrgSwitcher, PARS, PageHeader, Panel, PortalTheme, PostAgenda, PostCard, PostComposer, PrimaryButton, ProductIcon, ProviderLogo, ProviderReadinessList, Reorder, SERIES, LG as SLIDEOVER_LG, SURFACES, SearchInput, Segmented, SelectMenu, SiteFooter, SiteNav, SlideOver, SocialSummaryBar, Sparkline, StatusTag, ToastProvider, UtilBar, ViewToggle, ZOO, accentFor, asColor, classifyBackend, classifyRead, colorForIndex, contrastText, filterOptions, filterOrgs, formatPostTime, isHexColor, isKnownOption, monogram, orgScope, otherSurfaces, parsePostTime, postDayBucket, postPreview, providerInitials, renderMenuItems, resolveAccent, resolveBrand, rowShift, setOrgAccent, targetIndex, useAccent, useContainerWidth, useHost, useToast, utilColor };
3404
+ export { ACCOUNT_STATUSES, AnimatedLogo, AppHeader, BRANDS, BackendStateCard, BarChart, BarRows, BrandMark, CHART_OTHER, CHART_PALETTE, COMPOSE_MODES, CampaignCard, ChannelBadge, ComboBox, CommerceResource, ConfirmDelete, ContextMenu, DataTable, Donut, Donut2 as DonutRing, DropdownMenu, EmptyState, FadeIn, FieldRow, FieldSelect, FieldSlider, FieldSwitch, FieldText, FieldTextArea, FloatingMenu, GenericLogo, HANZO, HANZO_MARK_CONTENT, HanzoMark, HintButton, HostProvider, LUX, LegendDot, LineChart, MenuItemView, MenuLabelView, MenuPanel, MenuSeparatorView, MetricCard, Sparkline2 as MetricSparkline, MiniBars, OrgMark, OrgSwitcher, PARS, POST_STATUSES, PROVIDERS, PageHeader, Panel, PortalTheme, PostAgenda, PostCard, PostComposer, PrimaryButton, ProductIcon, ProviderLogo, ProviderReadinessList, Reorder, SERIES, LG as SLIDEOVER_LG, SURFACES, SearchInput, Segmented, SelectMenu, SiteFooter, SiteNav, SlideOver, SocialResource, SocialSummaryBar, Sparkline, StatusTag, ToastProvider, UtilBar, ViewToggle, ZOO, accentFor, asColor, classifyBackend, classifyRead, colorForIndex, contrastText, createSocialApi, filterOptions, filterOrgs, formatPostTime, isHexColor, isKnownOption, monogram, normalizeAccount, normalizeAccounts, normalizePost, normalizePosts, normalizeProviderCapability, normalizeProviders, normalizeSummary, orgScope, otherSurfaces, parsePostTime, postDayBucket, postPreview, providerInitials, renderMenuItems, resolveAccent, resolveBrand, rowShift, setOrgAccent, targetIndex, useAccent, useContainerWidth, useHost, useToast, utilColor };
3106
3405
  //# sourceMappingURL=index.js.map
3107
3406
  //# sourceMappingURL=index.js.map