@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.
- package/dist/adminCardPayload.d.ts +9 -0
- package/dist/adminCardPayload.js +61 -0
- package/dist/adminCardsView.d.ts +72 -0
- package/dist/adminCardsView.js +187 -0
- package/dist/docs/API_SUMMARY.md +3 -1
- package/dist/docs/admin-cards.md +20 -0
- package/dist/docs/connectors.md +1 -1
- package/dist/docs/server-functions.md +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/openapi.yaml +10 -0
- package/dist/types/appManifest.d.ts +11 -0
- package/docs/API_SUMMARY.md +3 -1
- package/docs/admin-cards.md +20 -0
- package/docs/connectors.md +1 -1
- package/docs/server-functions.md +1 -0
- package/openapi.yaml +10 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.
|
|
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
|
```
|
package/dist/docs/admin-cards.md
CHANGED
|
@@ -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/docs/connectors.md
CHANGED
|
@@ -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
|
}
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.
|
|
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
|
```
|
package/docs/admin-cards.md
CHANGED
|
@@ -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/docs/connectors.md
CHANGED
|
@@ -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:**
|
package/docs/server-functions.md
CHANGED
|
@@ -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:
|