@proveanything/smartlinks 2.0.62 → 2.0.64

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.64 | Generated: 2026-10-14T09:37:13.261Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -3095,6 +3095,8 @@ interface ConnectorShapeResult {
3095
3095
  interface ConnectorSerializeInput {
3096
3096
  installation: ConnectorInstallation
3097
3097
  documents: Array<Record<string, unknown>>
3098
+ entities?: Array<{ id: string | null; name?: string | null; deleted?: boolean }>
3099
+ targetOptions?: Record<string, Record<string, unknown>>
3098
3100
  eventType?: string
3099
3101
  }
3100
3102
  ```
@@ -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
 
@@ -110,7 +110,7 @@ declare the provider's hosts (`network:<host>`) and the secrets it reads (`secre
110
110
  | `sample` | Mapper, AI assist, dry run | `{ installation, limit }` | `ConnectorRecords` `{ records }`: real provider records |
111
111
  | `fetch` | Every inbound run | `ConnectorFetchInput` `{ installation, cursor, limit }` | `ConnectorRecords` `{ records, nextCursor }`: one page per call |
112
112
  | `shape` | Every inbound run (and sample dry runs) | `ConnectorShapeInput` `{ installation, records }` | `ConnectorShapeResult` `{ documents, skipped }`: one provider-neutral document per record, same order; `null` skips one |
113
- | `serialize` | Every outbound run | `ConnectorSerializeInput` `{ installation, documents, eventType? }`: documents core mapped to `describeTarget` paths; a deletion arrives as `{ id, _deleted: true }` | `ConnectorSerializeResult` `{ payloads, skipped }`: one provider payload per document, same order; `null` skips one |
113
+ | `serialize` | Every outbound run | `ConnectorSerializeInput` `{ installation, documents, entities, targetOptions, eventType? }`: documents core mapped to `describeTarget` paths; `entities` = the SmartLinks record each came from (id, name); `targetOptions` = per-field options the mapping carries (e.g. locale, unit); a deletion arrives as `{ id, _deleted: true }` | `ConnectorSerializeResult` `{ payloads, skipped }`: one provider payload per document, same order; `null` skips one |
114
114
  | `send` | Every outbound run | `ConnectorSendInput` `{ installation, payloads }` | `ConnectorSendResult` `{ results: [{ index, ok, status, error, externalId }] }` |
115
115
 
116
116
  **How a run flows:**
@@ -167,6 +167,7 @@ Declare the minimum you need.
167
167
  | `sl:<resource>:read` / `sl:<resource>:write` | SDK access to that resource, e.g. `sl:products:read`, `sl:records:write`, `sl:attestations:write`, `sl:contacts:write`. |
168
168
  | `network` | `ctx.fetch` to any host. |
169
169
  | `network:<host>` | `ctx.fetch` to that host only (repeat for several). Prefer this over blanket `network`. |
170
+ | `network:*.<domain>` | `ctx.fetch` to any subdomain of `<domain>` (not `<domain>` itself), for a customer's own endpoint at a provider, e.g. `network:*.servicebus.windows.net`. `<domain>` needs at least two labels (`*.com` is not honoured). |
170
171
  | `secrets:<name>` | `ctx.secrets.get('<name>')` for that one secret (see [Secrets](#secrets--api-keys-per-collection)). |
171
172
 
172
173
  If you don't declare `network`, `ctx.fetch` is absent. Reading a secret you didn't declare with
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)
package/dist/openapi.yaml CHANGED
@@ -20172,6 +20172,16 @@ components:
20172
20172
  items:
20173
20173
  type: object
20174
20174
  additionalProperties: true
20175
+ entities:
20176
+ type: array
20177
+ items:
20178
+ type: object
20179
+ additionalProperties: true
20180
+ targetOptions:
20181
+ type: object
20182
+ additionalProperties:
20183
+ type: object
20184
+ additionalProperties: true
20175
20185
  eventType:
20176
20186
  type: string
20177
20187
  required:
@@ -1010,6 +1010,17 @@ export interface ConnectorShapeResult {
1010
1010
  export interface ConnectorSerializeInput {
1011
1011
  installation: ConnectorInstallation;
1012
1012
  documents: Array<Record<string, unknown>>;
1013
+ /** The SmartLinks record each document came from (same order): its id and name, e.g. for an envelope's own id. */
1014
+ entities?: Array<{
1015
+ id: string | null;
1016
+ name?: string | null;
1017
+ deleted?: boolean;
1018
+ }>;
1019
+ /**
1020
+ * Options the flow's mapping carries per target field (keyed by target path), beyond the mapping
1021
+ * itself — e.g. { "brand": { "locale": "en-GB", "uom": "kg" } }. Data for the wire format, not mapping logic.
1022
+ */
1023
+ targetOptions?: Record<string, Record<string, unknown>>;
1013
1024
  /** The event that triggered the run, e.g. 'product.updated' (absent for manual / scheduled runs). */
1014
1025
  eventType?: string;
1015
1026
  }
@@ -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.64 | Generated: 2026-10-14T09:37:13.261Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -3095,6 +3095,8 @@ interface ConnectorShapeResult {
3095
3095
  interface ConnectorSerializeInput {
3096
3096
  installation: ConnectorInstallation
3097
3097
  documents: Array<Record<string, unknown>>
3098
+ entities?: Array<{ id: string | null; name?: string | null; deleted?: boolean }>
3099
+ targetOptions?: Record<string, Record<string, unknown>>
3098
3100
  eventType?: string
3099
3101
  }
3100
3102
  ```
@@ -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
 
@@ -110,7 +110,7 @@ declare the provider's hosts (`network:<host>`) and the secrets it reads (`secre
110
110
  | `sample` | Mapper, AI assist, dry run | `{ installation, limit }` | `ConnectorRecords` `{ records }`: real provider records |
111
111
  | `fetch` | Every inbound run | `ConnectorFetchInput` `{ installation, cursor, limit }` | `ConnectorRecords` `{ records, nextCursor }`: one page per call |
112
112
  | `shape` | Every inbound run (and sample dry runs) | `ConnectorShapeInput` `{ installation, records }` | `ConnectorShapeResult` `{ documents, skipped }`: one provider-neutral document per record, same order; `null` skips one |
113
- | `serialize` | Every outbound run | `ConnectorSerializeInput` `{ installation, documents, eventType? }`: documents core mapped to `describeTarget` paths; a deletion arrives as `{ id, _deleted: true }` | `ConnectorSerializeResult` `{ payloads, skipped }`: one provider payload per document, same order; `null` skips one |
113
+ | `serialize` | Every outbound run | `ConnectorSerializeInput` `{ installation, documents, entities, targetOptions, eventType? }`: documents core mapped to `describeTarget` paths; `entities` = the SmartLinks record each came from (id, name); `targetOptions` = per-field options the mapping carries (e.g. locale, unit); a deletion arrives as `{ id, _deleted: true }` | `ConnectorSerializeResult` `{ payloads, skipped }`: one provider payload per document, same order; `null` skips one |
114
114
  | `send` | Every outbound run | `ConnectorSendInput` `{ installation, payloads }` | `ConnectorSendResult` `{ results: [{ index, ok, status, error, externalId }] }` |
115
115
 
116
116
  **How a run flows:**
@@ -167,6 +167,7 @@ Declare the minimum you need.
167
167
  | `sl:<resource>:read` / `sl:<resource>:write` | SDK access to that resource, e.g. `sl:products:read`, `sl:records:write`, `sl:attestations:write`, `sl:contacts:write`. |
168
168
  | `network` | `ctx.fetch` to any host. |
169
169
  | `network:<host>` | `ctx.fetch` to that host only (repeat for several). Prefer this over blanket `network`. |
170
+ | `network:*.<domain>` | `ctx.fetch` to any subdomain of `<domain>` (not `<domain>` itself), for a customer's own endpoint at a provider, e.g. `network:*.servicebus.windows.net`. `<domain>` needs at least two labels (`*.com` is not honoured). |
170
171
  | `secrets:<name>` | `ctx.secrets.get('<name>')` for that one secret (see [Secrets](#secrets--api-keys-per-collection)). |
171
172
 
172
173
  If you don't declare `network`, `ctx.fetch` is absent. Reading a secret you didn't declare with
package/openapi.yaml CHANGED
@@ -20172,6 +20172,16 @@ components:
20172
20172
  items:
20173
20173
  type: object
20174
20174
  additionalProperties: true
20175
+ entities:
20176
+ type: array
20177
+ items:
20178
+ type: object
20179
+ additionalProperties: true
20180
+ targetOptions:
20181
+ type: object
20182
+ additionalProperties:
20183
+ type: object
20184
+ additionalProperties: true
20175
20185
  eventType:
20176
20186
  type: string
20177
20187
  required:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.0.62",
3
+ "version": "2.0.64",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",