@proveanything/smartlinks 2.0.62 → 2.0.63

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,9 @@
1
+ /**
2
+ * Trim a card function's return value to what a card shows — the same rules SmartLinks applies
3
+ * before caching it (prove services/adminCards normalisePayload), so a preview shows exactly what the
4
+ * dashboard will. Unknown fields are dropped, text is shortened, lists are capped, and only `/paths`
5
+ * and `https:` links survive. Throws when the input isn't an object or the result is over 8KB.
6
+ */
7
+ import type { AdminCardPayload } from './types/adminCards.js';
8
+ export declare const ADMIN_CARD_MAX_BYTES: number;
9
+ export declare function normaliseAdminCardPayload(raw: unknown): AdminCardPayload;
@@ -0,0 +1,61 @@
1
+ export const ADMIN_CARD_MAX_BYTES = 8 * 1024;
2
+ const TONES = ['ok', 'attention', 'neutral', 'error'];
3
+ const DIRECTIONS = ['up', 'down', 'flat'];
4
+ const str = (v, max) => String(v).slice(0, max);
5
+ function action(a) {
6
+ if (!a || typeof a !== 'object' || !a.label)
7
+ return undefined;
8
+ const out = { label: str(a.label, 40) };
9
+ if (typeof a.path === 'string' && a.path.startsWith('/'))
10
+ out.path = a.path.slice(0, 300);
11
+ else if (typeof a.url === 'string' && /^https:\/\//i.test(a.url))
12
+ out.url = a.url.slice(0, 500);
13
+ else
14
+ return undefined;
15
+ return out;
16
+ }
17
+ function byteLength(text) {
18
+ return typeof TextEncoder !== 'undefined' ? new TextEncoder().encode(text).length : text.length;
19
+ }
20
+ export function normaliseAdminCardPayload(raw) {
21
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
22
+ throw new Error('The card function must return an object (an AdminCardPayload).');
23
+ const r = raw;
24
+ const p = {};
25
+ if (r.title != null)
26
+ p.title = str(r.title, 80);
27
+ if (typeof r.value === 'number' && Number.isFinite(r.value))
28
+ p.value = r.value;
29
+ else if (r.value != null)
30
+ p.value = str(r.value, 40);
31
+ if (r.label != null)
32
+ p.label = str(r.label, 80);
33
+ if (r.unit != null)
34
+ p.unit = str(r.unit, 12);
35
+ if (r.trend && DIRECTIONS.includes(r.trend.direction)) {
36
+ p.trend = Object.assign(Object.assign({ direction: r.trend.direction }, (r.trend.label != null ? { label: str(r.trend.label, 60) } : {})), (typeof r.trend.good === 'boolean' ? { good: r.trend.good } : {}));
37
+ }
38
+ if (r.status && TONES.includes(r.status.tone) && r.status.text)
39
+ p.status = { tone: r.status.tone, text: str(r.status.text, 140) };
40
+ if (r.progress && Number.isFinite(r.progress.done) && Number.isFinite(r.progress.total) && r.progress.total > 0) {
41
+ p.progress = Object.assign({ done: r.progress.done, total: r.progress.total }, (r.progress.label != null ? { label: str(r.progress.label, 60) } : {}));
42
+ }
43
+ if (Array.isArray(r.items)) {
44
+ p.items = r.items.filter((i) => i && i.title != null).slice(0, 5).map((i) => (Object.assign(Object.assign(Object.assign({ title: str(i.title, 120) }, (i.subtitle != null ? { subtitle: str(i.subtitle, 120) } : {})), (i.meta != null ? { meta: str(i.meta, 60) } : {})), (typeof i.path === 'string' && i.path.startsWith('/') ? { path: i.path.slice(0, 300) } : {}))));
45
+ }
46
+ if (Array.isArray(r.todo)) {
47
+ p.todo = r.todo.filter((t) => t && t.text).slice(0, 5).map((t) => {
48
+ const a = action(t.action);
49
+ return Object.assign({ text: str(t.text, 140) }, (a ? { action: a } : {}));
50
+ });
51
+ }
52
+ if (Array.isArray(r.actions))
53
+ p.actions = r.actions.map(action).filter(Boolean).slice(0, 2);
54
+ if (r.empty && r.empty.text) {
55
+ const a = action(r.empty.action);
56
+ p.empty = Object.assign({ text: str(r.empty.text, 140) }, (a ? { action: a } : {}));
57
+ }
58
+ if (byteLength(JSON.stringify(p)) > ADMIN_CARD_MAX_BYTES)
59
+ throw new Error(`The card's payload is over ${ADMIN_CARD_MAX_BYTES / 1024}KB — return fewer or shorter items.`);
60
+ return p;
61
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The admin card RENDERER — one look for a card everywhere it's drawn (Studio's dashboard, Forge's
3
+ * preview), so what a developer previews is what the business sees. docs/admin-cards.md.
4
+ *
5
+ * Framework-light on purpose: the host passes its own React, so the SDK has no React dependency and
6
+ * a page never gets a second copy of React.
7
+ *
8
+ * ```tsx
9
+ * import React from 'react'
10
+ * import { createAdminCardViews } from '@proveanything/smartlinks'
11
+ * const { AdminCardView, AdminCardGrid, AdminCardTodo } = createAdminCardViews(React)
12
+ *
13
+ * <AdminCardTodo cards={cards} onAction={go} />
14
+ * <AdminCardGrid>
15
+ * {cards.map((c) => <AdminCardView key={c.key} card={c} onAction={go} onRefresh={refresh} />)}
16
+ * </AdminCardGrid>
17
+ * ```
18
+ *
19
+ * Styling: plain classes (`slc-*`) injected once, coloured by CSS variables that default to the SDK
20
+ * theme tokens (`--sl-color-*`) — set `--slc-*` on a parent to fit any host.
21
+ */
22
+ import type { AdminCard, AdminCardAction } from './types/adminCards.js';
23
+ type Node = any;
24
+ interface ReactLike {
25
+ createElement: (type: any, props?: any, ...children: any[]) => Node;
26
+ Fragment: any;
27
+ useEffect: (fn: () => void | (() => void), deps?: unknown[]) => void;
28
+ }
29
+ export interface AdminCardViewProps {
30
+ card: Pick<AdminCard, 'title' | 'size' | 'state' | 'payload' | 'computedAt'> & Partial<AdminCard>;
31
+ /** A card button or list row was clicked: `path` = open the app's admin there; `url` = open it. */
32
+ onAction?: (action: AdminCardAction, card: AdminCardViewProps['card']) => void;
33
+ /** Show a refresh button that calls this. */
34
+ onRefresh?: (card: AdminCardViewProps['card']) => void;
35
+ /** The refresh is running (spins the button). */
36
+ refreshing?: boolean;
37
+ /** Component cards: mount the app's component (the host loads its bundle). */
38
+ renderComponent?: (card: AdminCardViewProps['card']) => Node;
39
+ /** For number formatting. Defaults to the browser's. */
40
+ lang?: string;
41
+ /** "Now", for the "updated 5 min ago" line (tests, server render). */
42
+ now?: number;
43
+ className?: string;
44
+ }
45
+ export interface AdminCardTodoItem {
46
+ card: AdminCardViewProps['card'];
47
+ text: string;
48
+ tone: 'attention' | 'error';
49
+ action?: AdminCardAction;
50
+ }
51
+ /** The To do strip's items: every card's `todo`, plus attention/error statuses without one. */
52
+ export declare function collectAdminCardTodo(cards: Array<AdminCardViewProps['card']>): AdminCardTodoItem[];
53
+ /** "just now" / "5 min ago" / "3 h ago" / "2 d ago". */
54
+ export declare function adminCardAge(at: number | null | undefined, now?: number): string;
55
+ /** Add the card styles to the page once (the views call it; hosts rendering on the server can too). */
56
+ export declare function injectAdminCardStyles(doc?: Document | undefined): void;
57
+ /** The card stylesheet, for hosts that prefer to ship it themselves. */
58
+ export declare const ADMIN_CARD_CSS = "\n.slc-grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--slc-gap,16px)}\n.slc-grid>*{grid-column:span 12}\n@media (min-width:768px){.slc-grid>.slc-size-small{grid-column:span 6}.slc-grid>.slc-size-medium,.slc-grid>.slc-size-wide{grid-column:span 12}}\n@media (min-width:1100px){.slc-grid>.slc-size-small{grid-column:span 3}.slc-grid>.slc-size-medium{grid-column:span 6}.slc-grid>.slc-size-wide{grid-column:span 8}}\n.slc-card{--_bg:var(--slc-bg,var(--sl-color-surface,#fff));--_fg:var(--slc-fg,var(--sl-color-fg,#0f172a));--_muted:var(--slc-muted,var(--sl-color-muted,#64748b));--_border:var(--slc-border,var(--sl-color-border,#e2e8f0));--_accent:var(--slc-accent,var(--sl-color-accent,#2563eb));--_ok:var(--slc-ok,var(--sl-color-success,#059669));--_warn:var(--slc-attention,#b45309);--_err:var(--slc-error,var(--sl-color-danger,#dc2626));\n box-sizing:border-box;display:flex;flex-direction:column;gap:12px;min-height:148px;padding:16px 18px;border:1px solid var(--_border);border-radius:var(--slc-radius,var(--sl-radius-lg,14px));background:var(--_bg);color:var(--_fg);font:inherit;position:relative}\n.slc-head{display:flex;align-items:center;gap:8px;min-width:0}\n.slc-title{margin:0;font-size:.95rem;font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.slc-age{font-size:.75rem;color:var(--_muted);white-space:nowrap}\n.slc-refresh{all:unset;cursor:pointer;width:24px;height:24px;display:grid;place-items:center;border-radius:6px;color:var(--_muted)}\n.slc-refresh:hover{background:color-mix(in srgb,var(--_muted) 12%,transparent);color:var(--_fg)}\n.slc-refresh:focus-visible{outline:2px solid var(--_accent)}\n.slc-spin{animation:slc-spin 1s linear infinite}\n@keyframes slc-spin{to{transform:rotate(360deg)}}\n.slc-valuerow{display:flex;align-items:baseline;flex-wrap:wrap;gap:6px 10px}\n.slc-value{font-size:2rem;line-height:1.1;font-weight:700;letter-spacing:-.02em;min-width:0;overflow-wrap:anywhere}\n.slc-value.slc-text{font-size:1.375rem;letter-spacing:-.01em}\n.slc-unit{font-size:1rem;font-weight:600;margin-left:2px;color:var(--_muted)}\n.slc-label{font-size:.875rem;color:var(--_muted)}\n.slc-trend{font-size:.75rem;font-weight:600;padding:2px 8px;border-radius:999px;background:color-mix(in srgb,var(--_muted) 12%,transparent);color:var(--_muted)}\n.slc-trend.slc-good{background:color-mix(in srgb,var(--_ok) 14%,transparent);color:var(--_ok)}\n.slc-trend.slc-bad{background:color-mix(in srgb,var(--_err) 12%,transparent);color:var(--_err)}\n.slc-status{display:flex;align-items:flex-start;gap:8px;font-size:.875rem}\n.slc-dot{flex:none;width:8px;height:8px;border-radius:50%;margin-top:6px;background:var(--_muted)}\n.slc-tone-ok .slc-dot{background:var(--_ok)}.slc-tone-attention .slc-dot{background:var(--_warn)}.slc-tone-error .slc-dot{background:var(--_err)}\n.slc-progress{display:flex;flex-direction:column;gap:4px;font-size:.75rem;color:var(--_muted)}\n.slc-bar{height:6px;border-radius:999px;background:color-mix(in srgb,var(--_muted) 18%,transparent);overflow:hidden}\n.slc-bar>span{display:block;height:100%;background:var(--_accent);border-radius:inherit}\n.slc-items{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}\n.slc-item{display:flex;align-items:center;gap:8px;padding:7px 0;border-top:1px solid var(--_border);font-size:.875rem;min-width:0}\n.slc-item:first-child{border-top:0}\n.slc-item-main{flex:1;min-width:0}\n.slc-item-title,.slc-item-sub{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.slc-item-sub,.slc-item-meta{color:var(--_muted);font-size:.75rem}\n.slc-link{all:unset;cursor:pointer}\n.slc-link:hover .slc-item-title{text-decoration:underline}\n.slc-link:focus-visible{outline:2px solid var(--_accent);border-radius:4px}\n.slc-empty{font-size:.875rem;color:var(--_muted)}\n.slc-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:auto}\n.slc-btn{all:unset;cursor:pointer;font-size:.8125rem;font-weight:600;padding:6px 12px;border-radius:8px;border:1px solid var(--_border);color:var(--_fg)}\n.slc-btn:hover{background:color-mix(in srgb,var(--_muted) 10%,transparent)}\n.slc-btn.slc-primary{background:var(--_accent);border-color:var(--_accent);color:var(--slc-on-accent,var(--sl-color-on-accent,#fff))}\n.slc-btn:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}\n.slc-note{font-size:.75rem;color:var(--_muted)}\n.slc-note.slc-err{color:var(--_err)}\n.slc-skel{height:12px;border-radius:6px;background:color-mix(in srgb,var(--_muted) 16%,transparent);animation:slc-pulse 1.4s ease-in-out infinite}\n@keyframes slc-pulse{50%{opacity:.5}}\n.slc-todo{display:flex;flex-direction:column;gap:6px;padding:12px 16px;border-radius:var(--slc-radius,var(--sl-radius-lg,14px));border:1px solid color-mix(in srgb,var(--slc-attention,#b45309) 35%,transparent);background:color-mix(in srgb,var(--slc-attention,#b45309) 7%,var(--slc-bg,var(--sl-color-surface,#fff)));color:var(--slc-fg,var(--sl-color-fg,#0f172a))}\n.slc-todo-title{margin:0 0 2px;font-size:.875rem;font-weight:600}\n.slc-todo-row{display:flex;align-items:center;gap:10px;font-size:.875rem}\n.slc-todo-row .slc-dot{margin-top:0;background:var(--slc-attention,#b45309)}\n.slc-todo-row.slc-tone-error .slc-dot{background:var(--slc-error,var(--sl-color-danger,#dc2626))}\n.slc-todo-text{flex:1;min-width:0}\n";
59
+ /** The card views, bound to the host's React. Call once per app (module scope). */
60
+ export declare function createAdminCardViews(React: ReactLike): {
61
+ AdminCardView: (props: AdminCardViewProps) => any;
62
+ AdminCardGrid: ({ children, className }: {
63
+ children?: any;
64
+ className?: string | undefined;
65
+ }) => any;
66
+ AdminCardTodo: ({ cards, onAction, title }: {
67
+ cards: Array<AdminCardViewProps['card']>;
68
+ onAction?: AdminCardViewProps['onAction'];
69
+ title?: string | undefined;
70
+ }) => any;
71
+ };
72
+ export {};
@@ -0,0 +1,187 @@
1
+ /** The To do strip's items: every card's `todo`, plus attention/error statuses without one. */
2
+ export function collectAdminCardTodo(cards) {
3
+ var _a;
4
+ const out = [];
5
+ for (const card of cards) {
6
+ const p = card.payload;
7
+ if (!p)
8
+ continue;
9
+ for (const t of p.todo || [])
10
+ out.push(Object.assign({ card, text: t.text, tone: ((_a = p.status) === null || _a === void 0 ? void 0 : _a.tone) === 'error' ? 'error' : 'attention' }, (t.action ? { action: t.action } : {})));
11
+ if (!(p.todo || []).length && p.status && (p.status.tone === 'attention' || p.status.tone === 'error')) {
12
+ const action = (p.actions || [])[0];
13
+ out.push(Object.assign({ card, text: `${card.title}: ${p.status.text}`, tone: p.status.tone }, (action ? { action } : {})));
14
+ }
15
+ }
16
+ return out.sort((a, b) => (a.tone === b.tone ? 0 : a.tone === 'error' ? -1 : 1));
17
+ }
18
+ /** "just now" / "5 min ago" / "3 h ago" / "2 d ago". */
19
+ export function adminCardAge(at, now = Date.now()) {
20
+ if (!at)
21
+ return '';
22
+ const s = Math.max(0, Math.round((now - at) / 1000));
23
+ if (s < 60)
24
+ return 'just now';
25
+ if (s < 3600)
26
+ return `${Math.round(s / 60)} min ago`;
27
+ if (s < 86400)
28
+ return `${Math.round(s / 3600)} h ago`;
29
+ return `${Math.round(s / 86400)} d ago`;
30
+ }
31
+ const CSS = `
32
+ .slc-grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--slc-gap,16px)}
33
+ .slc-grid>*{grid-column:span 12}
34
+ @media (min-width:768px){.slc-grid>.slc-size-small{grid-column:span 6}.slc-grid>.slc-size-medium,.slc-grid>.slc-size-wide{grid-column:span 12}}
35
+ @media (min-width:1100px){.slc-grid>.slc-size-small{grid-column:span 3}.slc-grid>.slc-size-medium{grid-column:span 6}.slc-grid>.slc-size-wide{grid-column:span 8}}
36
+ .slc-card{--_bg:var(--slc-bg,var(--sl-color-surface,#fff));--_fg:var(--slc-fg,var(--sl-color-fg,#0f172a));--_muted:var(--slc-muted,var(--sl-color-muted,#64748b));--_border:var(--slc-border,var(--sl-color-border,#e2e8f0));--_accent:var(--slc-accent,var(--sl-color-accent,#2563eb));--_ok:var(--slc-ok,var(--sl-color-success,#059669));--_warn:var(--slc-attention,#b45309);--_err:var(--slc-error,var(--sl-color-danger,#dc2626));
37
+ box-sizing:border-box;display:flex;flex-direction:column;gap:12px;min-height:148px;padding:16px 18px;border:1px solid var(--_border);border-radius:var(--slc-radius,var(--sl-radius-lg,14px));background:var(--_bg);color:var(--_fg);font:inherit;position:relative}
38
+ .slc-head{display:flex;align-items:center;gap:8px;min-width:0}
39
+ .slc-title{margin:0;font-size:.95rem;font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
40
+ .slc-age{font-size:.75rem;color:var(--_muted);white-space:nowrap}
41
+ .slc-refresh{all:unset;cursor:pointer;width:24px;height:24px;display:grid;place-items:center;border-radius:6px;color:var(--_muted)}
42
+ .slc-refresh:hover{background:color-mix(in srgb,var(--_muted) 12%,transparent);color:var(--_fg)}
43
+ .slc-refresh:focus-visible{outline:2px solid var(--_accent)}
44
+ .slc-spin{animation:slc-spin 1s linear infinite}
45
+ @keyframes slc-spin{to{transform:rotate(360deg)}}
46
+ .slc-valuerow{display:flex;align-items:baseline;flex-wrap:wrap;gap:6px 10px}
47
+ .slc-value{font-size:2rem;line-height:1.1;font-weight:700;letter-spacing:-.02em;min-width:0;overflow-wrap:anywhere}
48
+ .slc-value.slc-text{font-size:1.375rem;letter-spacing:-.01em}
49
+ .slc-unit{font-size:1rem;font-weight:600;margin-left:2px;color:var(--_muted)}
50
+ .slc-label{font-size:.875rem;color:var(--_muted)}
51
+ .slc-trend{font-size:.75rem;font-weight:600;padding:2px 8px;border-radius:999px;background:color-mix(in srgb,var(--_muted) 12%,transparent);color:var(--_muted)}
52
+ .slc-trend.slc-good{background:color-mix(in srgb,var(--_ok) 14%,transparent);color:var(--_ok)}
53
+ .slc-trend.slc-bad{background:color-mix(in srgb,var(--_err) 12%,transparent);color:var(--_err)}
54
+ .slc-status{display:flex;align-items:flex-start;gap:8px;font-size:.875rem}
55
+ .slc-dot{flex:none;width:8px;height:8px;border-radius:50%;margin-top:6px;background:var(--_muted)}
56
+ .slc-tone-ok .slc-dot{background:var(--_ok)}.slc-tone-attention .slc-dot{background:var(--_warn)}.slc-tone-error .slc-dot{background:var(--_err)}
57
+ .slc-progress{display:flex;flex-direction:column;gap:4px;font-size:.75rem;color:var(--_muted)}
58
+ .slc-bar{height:6px;border-radius:999px;background:color-mix(in srgb,var(--_muted) 18%,transparent);overflow:hidden}
59
+ .slc-bar>span{display:block;height:100%;background:var(--_accent);border-radius:inherit}
60
+ .slc-items{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
61
+ .slc-item{display:flex;align-items:center;gap:8px;padding:7px 0;border-top:1px solid var(--_border);font-size:.875rem;min-width:0}
62
+ .slc-item:first-child{border-top:0}
63
+ .slc-item-main{flex:1;min-width:0}
64
+ .slc-item-title,.slc-item-sub{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
65
+ .slc-item-sub,.slc-item-meta{color:var(--_muted);font-size:.75rem}
66
+ .slc-link{all:unset;cursor:pointer}
67
+ .slc-link:hover .slc-item-title{text-decoration:underline}
68
+ .slc-link:focus-visible{outline:2px solid var(--_accent);border-radius:4px}
69
+ .slc-empty{font-size:.875rem;color:var(--_muted)}
70
+ .slc-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:auto}
71
+ .slc-btn{all:unset;cursor:pointer;font-size:.8125rem;font-weight:600;padding:6px 12px;border-radius:8px;border:1px solid var(--_border);color:var(--_fg)}
72
+ .slc-btn:hover{background:color-mix(in srgb,var(--_muted) 10%,transparent)}
73
+ .slc-btn.slc-primary{background:var(--_accent);border-color:var(--_accent);color:var(--slc-on-accent,var(--sl-color-on-accent,#fff))}
74
+ .slc-btn:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}
75
+ .slc-note{font-size:.75rem;color:var(--_muted)}
76
+ .slc-note.slc-err{color:var(--_err)}
77
+ .slc-skel{height:12px;border-radius:6px;background:color-mix(in srgb,var(--_muted) 16%,transparent);animation:slc-pulse 1.4s ease-in-out infinite}
78
+ @keyframes slc-pulse{50%{opacity:.5}}
79
+ .slc-todo{display:flex;flex-direction:column;gap:6px;padding:12px 16px;border-radius:var(--slc-radius,var(--sl-radius-lg,14px));border:1px solid color-mix(in srgb,var(--slc-attention,#b45309) 35%,transparent);background:color-mix(in srgb,var(--slc-attention,#b45309) 7%,var(--slc-bg,var(--sl-color-surface,#fff)));color:var(--slc-fg,var(--sl-color-fg,#0f172a))}
80
+ .slc-todo-title{margin:0 0 2px;font-size:.875rem;font-weight:600}
81
+ .slc-todo-row{display:flex;align-items:center;gap:10px;font-size:.875rem}
82
+ .slc-todo-row .slc-dot{margin-top:0;background:var(--slc-attention,#b45309)}
83
+ .slc-todo-row.slc-tone-error .slc-dot{background:var(--slc-error,var(--sl-color-danger,#dc2626))}
84
+ .slc-todo-text{flex:1;min-width:0}
85
+ `;
86
+ let injected = false;
87
+ /** Add the card styles to the page once (the views call it; hosts rendering on the server can too). */
88
+ export function injectAdminCardStyles(doc = typeof document !== 'undefined' ? document : undefined) {
89
+ if (injected || !doc)
90
+ return;
91
+ injected = true;
92
+ const style = doc.createElement('style');
93
+ style.setAttribute('data-smartlinks', 'admin-cards');
94
+ style.textContent = CSS;
95
+ doc.head.appendChild(style);
96
+ }
97
+ /** The card stylesheet, for hosts that prefer to ship it themselves. */
98
+ export const ADMIN_CARD_CSS = CSS;
99
+ const ITEMS_FOR_SIZE = { small: 0, medium: 3, wide: 5, full: 5 };
100
+ /** The card views, bound to the host's React. Call once per app (module scope). */
101
+ export function createAdminCardViews(React) {
102
+ const h = React.createElement;
103
+ function useStyles() {
104
+ React.useEffect(() => injectAdminCardStyles(), []);
105
+ }
106
+ function formatValue(v, lang) {
107
+ if (typeof v === 'number') {
108
+ try {
109
+ return new Intl.NumberFormat(lang, { maximumFractionDigits: 1 }).format(v);
110
+ }
111
+ catch (_a) {
112
+ return String(v);
113
+ }
114
+ }
115
+ return v == null ? '' : String(v);
116
+ }
117
+ function ActionButton({ action, primary, onClick }) {
118
+ return h('button', { type: 'button', className: `slc-btn${primary ? ' slc-primary' : ''}`, onClick }, action.label);
119
+ }
120
+ function RefreshIcon({ spinning }) {
121
+ return h('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', className: spinning ? 'slc-spin' : undefined, 'aria-hidden': true }, h('path', { d: 'M21 12a9 9 0 1 1-2.64-6.36' }), h('path', { d: 'M21 3v6h-6' }));
122
+ }
123
+ /** One card: header (title, age, refresh) and a body drawn from its payload. */
124
+ function AdminCardView(props) {
125
+ var _a;
126
+ useStyles();
127
+ const { card, onAction, onRefresh, refreshing = false, renderComponent, lang, now } = props;
128
+ const p = card.payload;
129
+ const size = card.size || 'medium';
130
+ const act = (a) => onAction && onAction(a, card);
131
+ const title = (p && p.title) || card.title;
132
+ const age = card.state === 'pending' ? 'loading…' : card.state === 'stale' ? 'updating…' : adminCardAge(card.computedAt, now);
133
+ const header = h('header', { className: 'slc-head' }, h('h3', { className: 'slc-title', title }, title), age ? h('span', { className: 'slc-age' }, age) : null, onRefresh ? h('button', { type: 'button', className: 'slc-refresh', 'aria-label': `Refresh ${title}`, title: 'Refresh', disabled: refreshing, onClick: () => onRefresh(card) }, h(RefreshIcon, { spinning: refreshing || card.state === 'stale' })) : null);
134
+ let body;
135
+ if (card.kind === 'component' && renderComponent) {
136
+ body = renderComponent(card);
137
+ }
138
+ else if (!p) {
139
+ body = card.state === 'error'
140
+ ? h('p', { className: 'slc-note slc-err' }, card.error || "This card couldn't load.")
141
+ : h('div', { 'aria-busy': true, style: { display: 'flex', flexDirection: 'column', gap: 8 } }, h('div', { className: 'slc-skel', style: { width: '40%', height: 28 } }), h('div', { className: 'slc-skel', style: { width: '70%' } }));
142
+ }
143
+ else {
144
+ const showEmpty = !!p.empty && (p.value == null || p.value === 0) && !(p.items || []).length;
145
+ const maxItems = (_a = ITEMS_FOR_SIZE[size]) !== null && _a !== void 0 ? _a : 3;
146
+ const items = (p.items || []).slice(0, maxItems);
147
+ const actions = (size === 'small' ? (p.actions || []).slice(0, 1) : p.actions || []);
148
+ const trendClass = p.trend && p.trend.good != null ? (p.trend.good ? ' slc-good' : ' slc-bad') : '';
149
+ const arrow = p.trend ? (p.trend.direction === 'up' ? '▲ ' : p.trend.direction === 'down' ? '▼ ' : '') : '';
150
+ body = h(React.Fragment, null, p.value != null && !showEmpty
151
+ ? h('div', { className: 'slc-valuerow' }, h('span', { className: `slc-value${typeof p.value === 'string' && p.value.length > 8 ? ' slc-text' : ''}` }, formatValue(p.value, lang), p.unit ? h('span', { className: 'slc-unit' }, p.unit) : null), p.label ? h('span', { className: 'slc-label' }, p.label) : null, p.trend ? h('span', { className: `slc-trend${trendClass}` }, arrow + (p.trend.label || '')) : null)
152
+ : null, showEmpty
153
+ ? h('div', { className: 'slc-empty' }, p.empty.text, p.empty.action ? h('div', { className: 'slc-actions', style: { marginTop: 10 } }, h(ActionButton, { action: p.empty.action, primary: true, onClick: () => act(p.empty.action) })) : null)
154
+ : null, p.status ? h('div', { className: `slc-status slc-tone-${p.status.tone}` }, h('span', { className: 'slc-dot', 'aria-hidden': true }), h('span', null, p.status.text)) : null, p.progress
155
+ ? h('div', { className: 'slc-progress' }, h('div', { className: 'slc-bar', role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': p.progress.total, 'aria-valuenow': p.progress.done }, h('span', { style: { width: `${Math.min(100, Math.round((p.progress.done / p.progress.total) * 100))}%` } })), h('span', null, p.progress.label || `${p.progress.done} of ${p.progress.total}`))
156
+ : null, items.length && !showEmpty
157
+ ? h('ul', { className: 'slc-items' }, ...items.map((it, i) => {
158
+ const inner = h(React.Fragment, null, h('span', { className: 'slc-item-main' }, h('span', { className: 'slc-item-title' }, it.title), it.subtitle ? h('span', { className: 'slc-item-sub' }, it.subtitle) : null), it.meta ? h('span', { className: 'slc-item-meta' }, it.meta) : null);
159
+ return h('li', { key: i, className: 'slc-item' }, it.path && onAction
160
+ ? h('button', { type: 'button', className: 'slc-link', style: { display: 'flex', alignItems: 'center', gap: 8, flex: 1, minWidth: 0 }, onClick: () => act({ label: it.title, path: it.path }) }, inner)
161
+ : inner);
162
+ }))
163
+ : null, actions.length && !showEmpty
164
+ ? h('div', { className: 'slc-actions' }, ...actions.map((a, i) => h(ActionButton, { key: i, action: a, primary: i === 0, onClick: () => act(a) })))
165
+ : null, card.state === 'error' && card.error ? h('p', { className: 'slc-note slc-err' }, `Couldn't refresh: ${card.error}`) : null);
166
+ }
167
+ return h('article', {
168
+ className: `slc-card slc-size-${size} slc-state-${card.state}${props.className ? ` ${props.className}` : ''}`,
169
+ 'aria-busy': card.state === 'pending' || card.state === 'stale' || refreshing ? true : undefined,
170
+ 'data-card': card.key,
171
+ }, header, body);
172
+ }
173
+ /** The 12-column dashboard grid (one column on phones); each card spans by its size. */
174
+ function AdminCardGrid({ children, className }) {
175
+ useStyles();
176
+ return h('div', { className: `slc-grid${className ? ` ${className}` : ''}` }, children);
177
+ }
178
+ /** The To do strip: what the cards say needs doing. Renders nothing when there's nothing to do. */
179
+ function AdminCardTodo({ cards, onAction, title = 'To do' }) {
180
+ useStyles();
181
+ const todo = collectAdminCardTodo(cards);
182
+ if (!todo.length)
183
+ return null;
184
+ return h('section', { className: 'slc-todo', 'aria-label': title }, h('h2', { className: 'slc-todo-title' }, title), ...todo.slice(0, 6).map((t, i) => h('div', { key: i, className: `slc-todo-row slc-tone-${t.tone}` }, h('span', { className: 'slc-dot', 'aria-hidden': true }), h('span', { className: 'slc-todo-text' }, t.text), t.action && onAction ? h('button', { type: 'button', className: 'slc-btn', onClick: () => onAction(t.action, t.card) }, t.action.label) : null)));
185
+ }
186
+ return { AdminCardView, AdminCardGrid, AdminCardTodo };
187
+ }
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.62 | Generated: 2026-10-13T20:05:22.753Z
3
+ Version: 2.0.63 | Generated: 2026-10-14T09:04:47.371Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -146,6 +146,26 @@ realtime.onChange({ collectionId }, { type: 'cards' }, () => reload()) // a car
146
146
  await adminCards.refresh(collectionId, 'faq', 'unanswered') // the card's refresh button
147
147
  ```
148
148
 
149
+ Draw them with the SDK's renderer, so every host (Studio, Forge's preview) shows a card the same
150
+ way. Pass your React in; the SDK doesn't bundle React:
151
+
152
+ ```tsx
153
+ import React from 'react'
154
+ import { createAdminCardViews } from '@proveanything/smartlinks'
155
+ const { AdminCardView, AdminCardGrid, AdminCardTodo } = createAdminCardViews(React)
156
+
157
+ <AdminCardTodo cards={cards} onAction={open} />
158
+ <AdminCardGrid>
159
+ {cards.map((c) => <AdminCardView key={c.key} card={c} onAction={open} onRefresh={refresh} />)}
160
+ </AdminCardGrid>
161
+ ```
162
+
163
+ `onAction(action, card)`: `action.path` opens the card's app admin at that path; `action.url` opens a
164
+ link. Colours follow the SDK theme tokens (`--sl-color-*`); override with `--slc-bg`, `--slc-fg`,
165
+ `--slc-muted`, `--slc-border`, `--slc-accent`, `--slc-ok`, `--slc-attention`, `--slc-error`,
166
+ `--slc-radius`. `normaliseAdminCardPayload(raw)` trims a raw function result exactly as the server
167
+ does (for previews).
168
+
149
169
  Each card has `state`: `fresh`, `stale` (last payload, a newer one is coming), `pending` (first
150
170
  computation still running) or `error` (last attempt failed; `payload` is the last good one).
151
171
 
package/dist/index.d.ts CHANGED
@@ -13,6 +13,9 @@ export * as utils from './utils/index.js';
13
13
  export * as seo from './seo.js';
14
14
  export * as site from './site.js';
15
15
  export * as headless from './headless.js';
16
+ export { createAdminCardViews, collectAdminCardTodo, adminCardAge, injectAdminCardStyles, ADMIN_CARD_CSS } from './adminCardsView.js';
17
+ export type { AdminCardViewProps, AdminCardTodoItem } from './adminCardsView.js';
18
+ export { normaliseAdminCardPayload, ADMIN_CARD_MAX_BYTES } from './adminCardPayload.js';
16
19
  export { SHARED_DEPENDENCY_CONTRACT_VERSION, SHARED_DEPENDENCIES, SHARED_DEPENDENCY_SPECIFIERS, importMapPathFor, getHostSharedDependencies, } from './shared-dependencies.js';
17
20
  export type { SharedDependency, HostSharedDependencies } from './shared-dependencies.js';
18
21
  export { AI_TOOL_NAMES, BUILTIN_AI_TOOLS, getBuiltinAiTool } from './ai-tools.js';
package/dist/index.js CHANGED
@@ -20,6 +20,9 @@ import * as site_1 from './site.js';
20
20
  export { site_1 as site };
21
21
  import * as headless_1 from './headless.js';
22
22
  export { headless_1 as headless };
23
+ // Admin cards: the shared renderer (bind it to your React) and the payload trimmer — docs/admin-cards.md
24
+ export { createAdminCardViews, collectAdminCardTodo, adminCardAge, injectAdminCardStyles, ADMIN_CARD_CSS } from './adminCardsView.js';
25
+ export { normaliseAdminCardPayload, ADMIN_CARD_MAX_BYTES } from './adminCardPayload.js';
23
26
  // Shared dependency contract (host↔app) — one source of truth for externalized deps
24
27
  export { SHARED_DEPENDENCY_CONTRACT_VERSION, SHARED_DEPENDENCIES, SHARED_DEPENDENCY_SPECIFIERS, importMapPathFor, getHostSharedDependencies, } from './shared-dependencies.js';
25
28
  // Built-in AI tool catalog (design-time discovery of the core agentic toolset)
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.62 | Generated: 2026-10-13T20:05:22.753Z
3
+ Version: 2.0.63 | Generated: 2026-10-14T09:04:47.371Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -146,6 +146,26 @@ realtime.onChange({ collectionId }, { type: 'cards' }, () => reload()) // a car
146
146
  await adminCards.refresh(collectionId, 'faq', 'unanswered') // the card's refresh button
147
147
  ```
148
148
 
149
+ Draw them with the SDK's renderer, so every host (Studio, Forge's preview) shows a card the same
150
+ way. Pass your React in; the SDK doesn't bundle React:
151
+
152
+ ```tsx
153
+ import React from 'react'
154
+ import { createAdminCardViews } from '@proveanything/smartlinks'
155
+ const { AdminCardView, AdminCardGrid, AdminCardTodo } = createAdminCardViews(React)
156
+
157
+ <AdminCardTodo cards={cards} onAction={open} />
158
+ <AdminCardGrid>
159
+ {cards.map((c) => <AdminCardView key={c.key} card={c} onAction={open} onRefresh={refresh} />)}
160
+ </AdminCardGrid>
161
+ ```
162
+
163
+ `onAction(action, card)`: `action.path` opens the card's app admin at that path; `action.url` opens a
164
+ link. Colours follow the SDK theme tokens (`--sl-color-*`); override with `--slc-bg`, `--slc-fg`,
165
+ `--slc-muted`, `--slc-border`, `--slc-accent`, `--slc-ok`, `--slc-attention`, `--slc-error`,
166
+ `--slc-radius`. `normaliseAdminCardPayload(raw)` trims a raw function result exactly as the server
167
+ does (for previews).
168
+
149
169
  Each card has `state`: `fresh`, `stale` (last payload, a newer one is coming), `pending` (first
150
170
  computation still running) or `error` (last attempt failed; `payload` is the last good one).
151
171
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.0.62",
3
+ "version": "2.0.63",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",