@proveanything/smartlinks 2.0.15 → 2.0.17

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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.15 | Generated: 2026-09-22T15:00:20.418Z
3
+ Version: 2.0.17 | Generated: 2026-09-24T09:42:39.649Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -2183,6 +2183,8 @@ interface AppManifest {
2183
2183
  moduleFormat?: 'umd' | 'esm' | 'dual';
2184
2184
  sharedDependencies?: string;
2185
2185
  cssBaseline?: string;
2186
+ respectsHostTheme?: boolean;
2187
+ themeTokens?: string;
2186
2188
  globals?: Record<string, string>;
2187
2189
  seo?: {
2188
2190
  strategy?: 'executor' | string;
@@ -7,7 +7,7 @@
7
7
  > Status: **standard**. New apps MUST follow this contract; existing apps SHOULD migrate.
8
8
  >
9
9
  > SDK: `@proveanything/smartlinks` ≥ **2.0** (R5).
10
- > Admin shell (React only): `@proveanything/smartlinks-utils-ui` ≥ **0.7.6** — required for the admin side if using the React shell; not needed in public widgets.
10
+ > Admin shell (React only): `@proveanything/smartlinks-utils-ui` ≥ **2.0** — required for the admin side if using the React shell; not needed in public widgets.
11
11
 
12
12
  ---
13
13
 
@@ -34,14 +34,24 @@ bundle. Two small additions to your build, both non-secret:
34
34
  // app.manifest.json
35
35
  "build": { "hash": "a1b2c3d4", "at": "2026-09-20T10:00:00Z" }
36
36
  ```
37
- 2. **Ping us on publish** (a `postbuild` step — `SMARTLINKS_APP_ID` is an *identifier, not a
38
- secret*, so it's fine in the repo):
37
+ 2. **Ping us on publish** (a `postbuild` step — the app id is an *identifier, not a secret*, so it's
38
+ fine in the repo). **Prefer the tiny node wrapper** the example app ships
39
+ (`scripts/smartlinks-deploy.mjs` in `smartlinks-app-example` — copy it): its
40
+ no-key path does exactly this ping and, crucially, **resolves the app id from
41
+ `SMARTLINKS_APP_ID` *or* the manifest's `meta.appId`**, so it works even when no env var is set:
42
+ ```js
43
+ // the id-resolution the wrapper uses — env var first, manifest fallback
44
+ const appId = process.env.SMARTLINKS_APP_ID
45
+ || JSON.parse(fs.readFileSync('public/app.manifest.json')).meta.appId
46
+ ```
47
+ ```jsonc
48
+ // package.json — wrapper (recommended)
49
+ "scripts": { "postbuild": "node scripts/smartlinks-deploy.mjs" }
50
+ ```
51
+ A bare `curl` works too, **but only if `$SMARTLINKS_APP_ID` is actually set in the build env** —
52
+ it has no manifest fallback, so an unset var makes the ping a silent no-op (a common miss):
39
53
  ```jsonc
40
- // package.json
41
- "scripts": {
42
- "build": "vite build && … && node scripts/hash-bundles.mjs", // your existing hashing step
43
- "postbuild": "curl -fsS -X POST \"$SMARTLINKS_API/api/v1/apps/$SMARTLINKS_APP_ID/refresh-dev\" -H 'content-type: application/json' -d \"{\\\"hash\\\":\\\"$BUILD_HASH\\\"}\""
44
- }
54
+ "postbuild": "curl -fsS -X POST \"$SMARTLINKS_API/api/v1/apps/$SMARTLINKS_APP_ID/refresh-dev\" -H 'content-type: application/json' -d \"{\\\"hash\\\":\\\"$BUILD_HASH\\\"}\""
45
55
  ```
46
56
 
47
57
  That's it — no deploy key. When the ping arrives, SmartLinks looks up your app's published URL
@@ -51,6 +61,14 @@ and registers the dev release (the hash becomes the version). `POST /apps/{appId
51
61
  authorises nothing on its own — worst case it re-fetches your app's own public bundle — so it needs
52
62
  no secret; it's rate-limited and de-duped per app.
53
63
 
64
+ **Where your app learns its own id:** either `SMARTLINKS_APP_ID` (a build-env var) or the manifest's
65
+ `meta.appId`. **Whichever you use must equal your authoritative platform id** — the one in the
66
+ catalog (the `appModules` handle the CDN + every collection's config bind to). On the keyed
67
+ `/releases` path `meta.appId` is *ignored* in favour of the URL id, but for `refresh-dev` the id you
68
+ ping with **is** the lookup key — so if `meta.appId` is blank or has drifted from your platform id,
69
+ the ping resolves the wrong app (or `NO_DEV_URL`). **Stamp `meta.appId` with your real platform id**
70
+ (don't invent one) and the manifest fallback is reliable.
71
+
54
72
  **One-time setup:** the app must exist in the catalog with its **id** and its **Lovable URL**
55
73
  recorded (that's how we know what to fetch, and the `id` you ping with must match). Ask the platform
56
74
  owner to register the app once; after that, every Publish auto-updates dev.
@@ -0,0 +1,239 @@
1
+ # SmartLinks Theme Tokens — host theming contract
2
+
3
+ Status: **v1 (draft) — FORTHCOMING, not yet live host-side.**
4
+ > ⚠️ **Do not build apps against this yet.** The host (Hub/Portal) does not yet set `--sl-*` tokens
5
+ > or send `smartlinks:root-state`. Until it does, apps follow the **current** theming — see
6
+ > [`theme.system.md`](./theme.system.md) (`useSmartLinksTheme()` + shadcn vars `--primary`/`--background`,
7
+ > the `?theme=` base64 payload). This document is the target model that will supersede it once the host
8
+ > serves it; it exists so the SDK preset (`theme.css`), the iframe bootstrap (`theme-boot.js`) and the
9
+ > Hub theme engine can be built against a fixed contract.
10
+
11
+ Applies to: `@proveanything/smartlinks` ^2.0 (R5), container + widget embeds
12
+ Companion to: [`css-baseline.md`](./css-baseline.md) (mechanics, live) — this doc is **brand** (colour, shape, type)
13
+
14
+ ---
15
+
16
+ ## The one idea
17
+
18
+ `sl-baseline` gives apps guaranteed **mechanics** (layout, spacing, type scale) with no colour or brand.
19
+ Theme tokens give apps the host's **brand** — colour, corner shape, fonts — as a small set of
20
+ **semantic CSS custom properties** the host sets and the app reads. An app that binds to these
21
+ follows any host theme automatically, forever, with no code change.
22
+
23
+ The durable thing here is the **token contract** (names + meanings), not any theme. A *theme* is just
24
+ a set of token *values* — disposable data, hand-authored or AI-generated. The contract is small,
25
+ semantic, and versioned; themes churn freely on top of it.
26
+
27
+ ```
28
+ host sets --sl-color-accent: #b4531f (a theme value)
29
+ │
30
+ ▼
31
+ SDK theme.css maps it onto Tailwind's token → bg-primary, text-primary, …
32
+ │
33
+ ▼
34
+ app renders in the host's brand — no per-app work
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 1. Design rules (why it lasts)
40
+
41
+ 1. **Semantic, never literal.** Tokens name a *role* (`--sl-color-accent`, `--sl-color-surface`),
42
+ never a colour (`--sl-blue-600`). Roles survive redesigns; literals rot.
43
+ 2. **Small guaranteed core.** The app-facing contract is ~12 tokens. Richer, Hub-specific styling
44
+ (button weight, elevation, density, imagery) lives in **host-internal** component tokens and is
45
+ NOT part of this contract — so it can evolve without breaking apps or binding other surfaces.
46
+ 3. **Additive, versioned, retained.** New tokens are added within a version with sane fallbacks; a
47
+ removal or rename is a new version, old retained. Same discipline as the shared-dependency
48
+ contract — never hard-remove.
49
+ 4. **Each surface declares the subset it honours.** The token set is a **superset**; Hub honours all
50
+ of it, Portal a subset. Apps read defensively (every token has a fallback), so one brand renders
51
+ consistently across surfaces and missing tokens degrade gracefully.
52
+ 5. **Tokens only — never templated CSS.** Themes are declarative values, always previewable and
53
+ validatable. (Liquid/templating belongs to content, not styling.)
54
+
55
+ ---
56
+
57
+ ## 2. The v1 token set
58
+
59
+ All tokens are CSS custom properties read from the **nearest scoping element** (the app's mount
60
+ root), so different embeds on one page can theme differently. Every token has a fallback, so an app
61
+ renders correctly even against a host that sets none of them.
62
+
63
+ ### Core (v1 — guaranteed; every host honours these)
64
+
65
+ | Token | Role | Example value |
66
+ |---|---|---|
67
+ | `--sl-color-bg` | app / page background | `#ffffff` |
68
+ | `--sl-color-surface` | card / panel background | `#f7f7f8` |
69
+ | `--sl-color-fg` | primary text (on bg/surface) | `#18181b` |
70
+ | `--sl-color-muted` | secondary / muted text | `#71717a` |
71
+ | `--sl-color-border` | borders, dividers, input outlines | `#e4e4e7` |
72
+ | `--sl-color-accent` | brand / primary action | `#4f46e5` |
73
+ | `--sl-color-on-accent` | text / icon on an accent fill | `#ffffff` |
74
+ | `--sl-radius-sm` | small corner radius | `4px` |
75
+ | `--sl-radius-md` | default corner radius | `8px` |
76
+ | `--sl-radius-lg` | large corner radius | `16px` |
77
+ | `--sl-font-heading` | heading font stack | `'Inter', sans-serif` |
78
+ | `--sl-font-body` | body font stack | `'Inter', sans-serif` |
79
+
80
+ ### Extended (v1 — optional; Hub may set, Portal may not; read defensively)
81
+
82
+ | Token | Role | Fallback |
83
+ |---|---|---|
84
+ | `--sl-color-surface-raised` | elevated surface (popover, modal) | `--sl-color-surface` |
85
+ | `--sl-color-accent-soft` | soft accent fill (badges, hovers) | derived from `--sl-color-accent` |
86
+ | `--sl-color-success` | positive status | `#16a34a` |
87
+ | `--sl-color-danger` | negative / destructive status | `#dc2626` |
88
+ | `--sl-shadow-sm` / `--sl-shadow-md` | elevation | none |
89
+
90
+ > Everything richer than this — button weight (solid/soft/outline), density/compactness, image
91
+ > treatment, elevation scale — is a **host-internal component token**, driven by Hub's theme engine.
92
+ > It is deliberately NOT in this contract, so Hub can be as skinnable as it likes without it becoming
93
+ > a forever-obligation on every app and surface.
94
+
95
+ Dark mode is a *theme* (a different value-set), not a separate token set — the host swaps the values.
96
+
97
+ ---
98
+
99
+ ## 3. Theme value-sets (what the AI emits, what the lookbook stores)
100
+
101
+ A theme is data: a token version + a flat map of values. The host expands each `values` key to
102
+ `--sl-<key>` on the mount root.
103
+
104
+ ```jsonc
105
+ {
106
+ "tokensVersion": "v1",
107
+ "name": "Warm Editorial",
108
+ "values": {
109
+ "color-bg": "#faf7f2",
110
+ "color-surface": "#ffffff",
111
+ "color-fg": "#1a1a1a",
112
+ "color-muted": "#6b6b6b",
113
+ "color-border": "#e6e0d8",
114
+ "color-accent": "#b4531f",
115
+ "color-on-accent": "#ffffff",
116
+ "radius-sm": "4px", "radius-md": "10px", "radius-lg": "18px",
117
+ "font-heading": "'Fraunces', serif",
118
+ "font-body": "'Inter', sans-serif"
119
+ }
120
+ }
121
+ ```
122
+
123
+ - **Lookbook** = a gallery of these value-sets shipped as starting points; AI can generate more.
124
+ - **Refine** = AI edits `values` conversationally, previewed live in the real host, always inside the
125
+ contract → always safe, previewable, validatable.
126
+ - **Brand on-ramp** = ingest a site/brand guide → AI emits a `values` set → refine.
127
+ - **Validation** = a value-set must pass contrast (WCAG AA) for `fg`/`bg`, `muted`/`bg`,
128
+ `on-accent`/`accent` before it is offered or saved. AI output is checked, not trusted.
129
+
130
+ Store per collection as `{ tokensVersion, name, values }` — portable, diffable, exportable.
131
+
132
+ ---
133
+
134
+ ## 4. Host obligations vs app obligations
135
+
136
+ **Host (Hub / Portal):**
137
+ - Applies the active theme's `--sl-*` tokens where the embed will read them — `:root`/`<body>` for the
138
+ simple one-brand page (container/widget apps inherit for free), or the app's mount root when embeds
139
+ must theme independently or you want leak isolation for untrusted apps.
140
+ - For **iframe** apps, hands the initial state in via the URL fragment and posts live viewer-pref
141
+ updates as `smartlinks:root-state` (§6.2). Brand-theme changes = reload the embed.
142
+ - Declares the token version it serves and which subset (Hub = full, Portal = core).
143
+
144
+ **App:**
145
+ - `@import "@proveanything/smartlinks/theme.css";` after Tailwind (see §5).
146
+ - Uses semantic utilities (`bg-primary`, `text-foreground`, `border-border`, `rounded-md`,
147
+ `font-heading`) — never hardcoded palette utilities (`bg-blue-600`, `#rrggbb`).
148
+ - Authors CSS against the viewer-pref hooks (§6.1): the `.dark` class, `[data-contrast="high"]`,
149
+ `--sl-font-scale`, and the native `@media (prefers-contrast|prefers-reduced-motion)` queries.
150
+ **Never writes a message handler** — the SDK bootstrap flips the hooks.
151
+ - Iframe/standalone entries only: include `@proveanything/smartlinks/theme-boot.js` in `<head>`
152
+ (§6.2). Container/widget apps don't need it.
153
+ - Declares intent in the manifest:
154
+
155
+ ```jsonc
156
+ { "meta": { "respectsHostTheme": true, "themeTokens": "v1" } }
157
+ ```
158
+
159
+ `smartlinks-doctor` warns (not errors) on hardcoded colour utilities / hex in component source when
160
+ `respectsHostTheme` is `true`.
161
+
162
+ ---
163
+
164
+ ## 5. The SDK preset (`theme.css`)
165
+
166
+ Ships from the SDK. A Tailwind 4 `@theme` block that maps Tailwind's tokens onto the `--sl-*`
167
+ contract, with fallbacks so it is safe even where the host sets nothing:
168
+
169
+ ```css
170
+ @import "tailwindcss";
171
+ @import "@proveanything/smartlinks/theme.css";
172
+ ```
173
+
174
+ Requires Tailwind 4 (`@theme`). Apps still on Tailwind 3 stay as they are until they migrate
175
+ (step 16); the preset is opt-in and additive.
176
+
177
+ ---
178
+
179
+ ## 6. Viewer preferences, accessibility & iframe delivery
180
+
181
+ Two different kinds of "theme-ish" state, handled differently:
182
+
183
+ | | Brand theme | Viewer / accessibility prefs |
184
+ |---|---|---|
185
+ | What | accent, fonts, radius (the `--sl-*` tokens) | light/dark, contrast, font size, reduced-motion, language |
186
+ | Set by | the operator (rarely, in an editor) | the **viewer** (any time, mid-session) |
187
+ | Change model | **reload** the embed with new values | **live**, no reload (a reload on an a11y toggle *is* an a11y failure) |
188
+
189
+ **The app never writes a message handler.** It authors **CSS** against a small, fixed set of root
190
+ hooks; a generic SDK bootstrap (§6.2) flips those hooks. That's the whole obligation.
191
+
192
+ ### 6.1 The viewer-pref hooks (fixed vocabulary)
193
+
194
+ - **Dark mode** — the `.dark` class (Tailwind/shadcn convention) *and* `[data-theme="dark"|"light"]`.
195
+ - **Contrast** — `[data-contrast="high"]`. Also honour `@media (prefers-contrast: more)` — the browser
196
+ propagates the OS setting into iframes natively, so you get that slice for free.
197
+ - **Reduced motion** — honour `@media (prefers-reduced-motion: reduce)` (native, free).
198
+ - **Font size** — `--sl-font-scale` (a number, e.g. `1.25`); scale your base/rem type off it.
199
+ - **Language** — the `lang` attribute. (A language change is *content*, not just CSS — re-render/
200
+ re-fetch is the app's own i18n job; the hook just flags it. A heavy content swap may reload.)
201
+
202
+ Most of accessibility is therefore **free**: the OS-level `prefers-contrast` / `prefers-reduced-motion`
203
+ / browser zoom reach the iframe with no passing at all — just respect the standard media queries.
204
+
205
+ ### 6.2 Iframe delivery (`theme-boot.js`)
206
+
207
+ Container/widget apps inherit the host `:root` and need none of this. **Iframe/standalone** apps
208
+ (their own document — the security boundary for untrusted apps) include the SDK bootstrap
209
+ `@proveanything/smartlinks/theme-boot.js` in `<head>` (inline is best — zero flash):
210
+
211
+ - **Boot (before first paint):** reads the initial state from the URL fragment
212
+ `#slt=<base64url(JSON)>` (`{ tokensVersion, values, theme, contrast, fontScale, lang }`), plus
213
+ convenience params `?theme=&contrast=&fontScale=&lang=`, and applies it to `:root` synchronously.
214
+ This *replaces* the legacy base64-17-keys scheme — same idea (correct on first paint), but a
215
+ structured, versioned payload.
216
+ - **Live:** applies generic root-state updates the host posts as
217
+ `{ type: 'smartlinks:root-state', values?, theme?, contrast?, fontScale?, lang?, attrs? }`. This is
218
+ what makes accessibility toggles instant. The app carries none of this logic — the bootstrap is
219
+ generic and SDK-owned; the app only wrote CSS.
220
+
221
+ Payload stays small (the reason the fixed vocabulary matters): the token set + a handful of mode keys
222
+ fit comfortably in a URL fragment, no compression needed.
223
+
224
+ ---
225
+
226
+ ## 7. Versioning
227
+
228
+ - `themeTokens: "vN"` — the version an app targets.
229
+ - Within a version: additive only (new tokens get fallbacks). Never remove a token in-version.
230
+ - A removal/rename → `v(N+1)`, `vN` retained; hosts may serve several; apps declare which they target.
231
+ - Surfaces declare their honoured subset; the "mapping" between Hub and Portal is *which keys each
232
+ honours*, not a translation layer.
233
+
234
+ ## 8. Change log
235
+
236
+ - **v1 (draft, 2026-09-23):** initial semantic core (12) + extended (optional) set; value-set schema;
237
+ host mount-root scoping; manifest declaration + doctor warn; viewer-pref/accessibility hooks
238
+ (`.dark`/`data-theme`, `data-contrast`, `--sl-font-scale`, `lang`, native `prefers-*`); iframe
239
+ `theme-boot.js` (URL-boot + generic live root-state); brand change = reload, prefs = live.
@@ -1,104 +1,121 @@
1
- # UI Utils — `@proveanything/smartlinks-utils-ui`
2
-
3
- > **This page is a pointer.** Detailed, up-to-date docs for every component
4
- > live in the UI Utils package itself. This SDK doc only summarises *what's
5
- > in the box* and *when to reach for it* — so we don't have to update the
6
- > SDK every time a UI Utils prop changes.
7
-
8
- - **NPM:** [`@proveanything/smartlinks-utils-ui`](https://www.npmjs.com/package/@proveanything/smartlinks-utils-ui)
9
- - **Source & docs:** [`smartlinks-ui` repo](https://github.com/proveanything/smartlinks-ui)
10
- - **Per-component reference:** [`packages/smartlinks-ui/docs/`](https://github.com/proveanything/smartlinks-ui/tree/main/packages/smartlinks-ui/docs)
11
- - **Tracks:** `@proveanything/smartlinks ≥ 1.13` (peer dep)
12
-
13
- ## Install
14
-
15
- ```bash
16
- npm install @proveanything/smartlinks-utils-ui
17
- # Peer deps you probably already have:
18
- npm install react react-dom @proveanything/smartlinks @tanstack/react-query
19
- ```
20
-
21
- Import the styles **once** in your app entry (typically `src/admin-main.tsx`):
22
-
23
- ```ts
24
- import '@proveanything/smartlinks-utils-ui/styles.css';
25
- ```
26
-
27
- Components inherit your shadcn-compatible CSS variables (`--primary`,
28
- `--background`, `--border`, …) — no extra theming required.
29
-
30
- > **Admin-only.** Every component in this package calls the SDK with
31
- > `admin: true` somewhere (save, upload, etc.). Never import it from a
32
- > public widget or from `MobileAdminContainer`.
33
-
34
- ## What's in the box
35
-
36
- | Module | Subpath import | Use it for |
37
- |---|---|---|
38
- | **RecordsAdminShell** | `/records-admin` | Full admin UI for the `app.records` pattern. Top-level scopes: **Global** (`'collection'`), **Rule** (`'rule'`, AND-of-OR over facets), and **Product** — with automatic variant & batch drill-down where the collection enables them. Browser pane, editor pane with sticky save/discard/delete, optimistic save, deep-linking, lifecycle hooks, conflict handling. Decide [cardinality](https://github.com/proveanything/smartlinks-ui/blob/main/packages/smartlinks-ui/docs/records-admin-shell.md#-choosing-cardinality-read-this-first) up front: `'singleton'` (one winning record per scope — warranty, nutrition) vs `'list'` (many records per scope — auction items, FAQs, gallery). |
39
- | **Records hooks** | `/records-admin` | `useResolvedRecord`, `useMergedRecord`, `useCollectedRecords`, `useResolveAllRecords`, `useRulePreview`, … for the **public widget** side. |
40
- | **AdminPageHeader** | (root) | Standardised title / subtitle / icon / help / actions header. **Required** for any admin app that doesn't mount `RecordsAdminShell` (which embeds it). No bespoke admin chrome. |
41
- | **AssetPicker** | `/asset-picker` | Pick / upload / paste / URL-import / AI-generate / stock-search / crop media assets. MIME filtering, scope-aware, tag editor. |
42
- | **IconPicker** | `/icon-picker` | Searchable Font Awesome 7 Pro picker (requires the FA kit script on the host page). |
43
- | **FontPicker** | `/font-picker` | Google Fonts catalogue + custom uploaded font families with live previews. |
44
- | **ConditionsEditor** | `/conditions-editor` | Recursive AND/OR rule builder (12 condition types, facet-aware). |
45
- | **FacetRuleEditor** | `/facet-rule-editor` | Author server-side facet rules (AND-of-OR) for record targeting. |
46
- | **LinkPicker** | `/link-picker` | Universal navigation picker: external URL, an installed app, or a deep link inside an app. Stores a `LinkTarget` discriminated union — never a resolved URL. Ships with `resolveLink` and `useLinkTargets`. |
47
- | **Hints** | (root) | `useHintsPreference`, `useIntroState`, `HintsPreferenceToggle` — global "show/hide intro hints" preference shared across admin apps. |
48
-
49
- Each module has a per-subpath export so bundlers tree-shake the rest:
50
-
51
- ```ts
52
- import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
53
- import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
54
- ```
55
-
56
- ## Minimal examples
57
-
58
- ### Records admin (most apps need this)
59
-
60
- ```tsx
61
- import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
62
- import * as SL from '@proveanything/smartlinks';
63
-
64
- <RecordsAdminShell
65
- SL={SL}
66
- collectionId={collectionId}
67
- appId={appId}
68
- recordType="nutrition"
69
- label="Nutrition info"
70
- scopes={['collection', 'rule', 'product']} // canonical top-level tabs
71
- // items={{ cardinality: 'list' }} // ← opt in for multi-item apps
72
- defaultData={() => ({})}
73
- renderEditor={(ctx) => <NutritionForm ctx={ctx} />}
74
- />
75
- ```
76
-
77
- ### Public widget — read the resolved value
78
-
79
- ```tsx
80
- import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
81
-
82
- const { data, source } = useResolvedRecord({
83
- SL, appId, recordType: 'nutrition',
84
- collectionId, productId, variantId, batchId,
85
- });
86
- // source: 'proof' | 'batch' | 'variant' | 'product' | 'rule' | 'collection' | null
87
- ```
88
-
89
- For multi-item (`cardinality: 'list'`) apps, use `useResolveAllRecords` /
90
- `app.records.resolveAll()` instead — it returns every applicable record, not
91
- just the winner.
92
-
93
- For everything else (props, slots, cardinality decisions, deep-link adapters,
94
- lifecycle hooks, conflict handling, design tokens) → **read the package docs**.
95
-
96
- ## Prerequisites
97
-
98
- All components assume `SL.initializeApi()` has already run.
99
-
100
- ## Where to go next
101
-
102
- - Component reference: <https://github.com/proveanything/smartlinks-ui/tree/main/packages/smartlinks-ui/docs>
103
- - Records pattern (concepts, scope inheritance, resolution order): [`records-admin-pattern.md`](./records-admin-pattern.md)
104
- - Public consumption (how widgets/executors fetch the right records at runtime): the package's [`public-consumption.md`](https://github.com/proveanything/smartlinks-ui/blob/main/packages/smartlinks-ui/docs/public-consumption.md)
1
+ # UI Utils — `@proveanything/smartlinks-utils-ui`
2
+
3
+ The standardised, **admin-only** React toolkit for SmartLinks microapps. This page
4
+ is self-contained: the table and examples below are enough to build with. The
5
+ exhaustive per-component reference (props, slots, hooks, design tokens) ships
6
+ **inside the package** — read it from `node_modules/@proveanything/smartlinks-utils-ui/docs/`
7
+ after install. Nothing here depends on an external site.
8
+
9
+ - **NPM:** [`@proveanything/smartlinks-utils-ui`](https://www.npmjs.com/package/@proveanything/smartlinks-utils-ui)
10
+ - **Per-component reference:** `node_modules/@proveanything/smartlinks-utils-ui/docs/*.md`
11
+ (`records-admin-shell.md`, `admin-page-header.md`, `asset-picker.md`, `conditions-editor.md`,
12
+ `facet-rule-editor.md`, `link-picker.md`, `records-admin-hooks.md`, `public-consumption.md`, …)
13
+ - **Tracks:** `@proveanything/smartlinks ≥ 2.0` (peer dep)
14
+
15
+ ## Admin is standardised; the consumer surface is bespoke
16
+
17
+ Two different design mandates, and the split is deliberate:
18
+
19
+ - **Admin surfaces** (the desktop `admin.html` a brand operator uses) are a
20
+ **product family** — like Salesforce: consistent, predictable chrome across every
21
+ app an operator opens. Do **not** hand-roll admin tables, editors or page headers.
22
+ Mount `RecordsAdminShell` for anything record-shaped; otherwise wrap the page in
23
+ `AdminPageHeader` and use the pickers. That consistency *is* the product.
24
+ - **Consumer / public surfaces** (the container, widgets, public pages the
25
+ end-customer sees) are **bespoke and brand-led** — entirely your design. utils-ui
26
+ belongs here only as read-side hooks (`useResolvedRecord`, `useResolveAllRecords`),
27
+ never as UI.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install @proveanything/smartlinks-utils-ui
33
+ # Peer deps you probably already have:
34
+ npm install react react-dom @proveanything/smartlinks @tanstack/react-query
35
+ ```
36
+
37
+ Import the styles **once** in your admin entry (typically `src/admin-main.tsx`):
38
+
39
+ ```ts
40
+ import '@proveanything/smartlinks-utils-ui/styles.css';
41
+ ```
42
+
43
+ Components inherit your shadcn-compatible CSS variables (`--primary`,
44
+ `--background`, `--border`, …) — no extra theming required.
45
+
46
+ > **Admin-only.** Every component in this package calls the SDK with
47
+ > `admin: true` somewhere (save, upload, etc.). Never import it from a
48
+ > public widget or from `MobileAdminContainer`.
49
+
50
+ ## What's in the box
51
+
52
+ | Module | Subpath import | Use it for |
53
+ |---|---|---|
54
+ | **RecordsAdminShell** | `/records-admin` | Full admin UI for the `app.records` pattern. Top-level scopes: **Global** (`'collection'`), **Rule** (`'rule'`, AND-of-OR over facets), and **Product** — with automatic variant & batch drill-down where the collection enables them. Browser pane, editor pane with sticky save/discard/delete, optimistic save, deep-linking, lifecycle hooks, conflict handling. Decide **cardinality** up front: `'singleton'` (one winning record per scope — warranty, nutrition) vs `'list'` (many records per scope — auction items, FAQs, gallery). See `docs/records-admin-shell.md`. |
55
+ | **Records hooks** | `/records-admin` | `useResolvedRecord`, `useMergedRecord`, `useCollectedRecords`, `useResolveAllRecords`, `useRulePreview`, … for the **public widget** side. See `docs/records-admin-hooks.md`. |
56
+ | **AdminPageHeader** | (root) | Standardised title / subtitle / icon / help / actions header. **Required** for any admin app that doesn't mount `RecordsAdminShell` (which embeds it). No bespoke admin chrome. |
57
+ | **AssetPicker** | `/asset-picker` | Pick / upload / paste / URL-import / AI-generate / stock-search / crop media assets. MIME filtering, scope-aware, tag editor. |
58
+ | **IconPicker** | `/icon-picker` | Searchable Font Awesome 7 Pro picker (requires the FA kit script on the host page). |
59
+ | **FontPicker** | `/font-picker` | Google Fonts catalogue + custom uploaded font families with live previews. |
60
+ | **ConditionsEditor** | `/conditions-editor` | Recursive AND/OR rule builder (12 condition types, facet-aware). |
61
+ | **FacetRuleEditor** | `/facet-rule-editor` | Author server-side facet rules (AND-of-OR) for record targeting. |
62
+ | **LinkPicker** | `/link-picker` | Universal navigation picker: external URL, an installed app, or a deep link inside an app. Stores a `LinkTarget` discriminated union — never a resolved URL. Ships with `resolveLink` and `useLinkTargets`. |
63
+ | **Liquid editors** | `/liquid-editor` | Author Liquid templates with a code or rich (TipTap) editor, variable autocomplete. |
64
+ | **Hints** | (root) | `useHintsPreference`, `useIntroState`, `HintsPreferenceToggle` — global "show/hide intro hints" preference shared across admin apps. |
65
+
66
+ Each module has a per-subpath export so bundlers tree-shake the rest:
67
+
68
+ ```ts
69
+ import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
70
+ import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
71
+ ```
72
+
73
+ ## Minimal examples
74
+
75
+ ### Records admin (most apps need this)
76
+
77
+ ```tsx
78
+ import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
79
+ import * as SL from '@proveanything/smartlinks';
80
+
81
+ <RecordsAdminShell
82
+ SL={SL}
83
+ collectionId={collectionId}
84
+ appId={appId}
85
+ recordType="nutrition"
86
+ label="Nutrition info"
87
+ scopes={['collection', 'rule', 'product']} // canonical top-level tabs
88
+ // items={{ cardinality: 'list' }} // ← opt in for multi-item apps
89
+ defaultData={() => ({})}
90
+ renderEditor={(ctx) => <NutritionForm ctx={ctx} />}
91
+ />
92
+ ```
93
+
94
+ ### Public widget — read the resolved value
95
+
96
+ ```tsx
97
+ import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
98
+
99
+ const { data, source } = useResolvedRecord({
100
+ SL, appId, recordType: 'nutrition',
101
+ collectionId, productId, variantId, batchId,
102
+ });
103
+ // source: 'proof' | 'batch' | 'variant' | 'product' | 'rule' | 'collection' | null
104
+ ```
105
+
106
+ For multi-item (`cardinality: 'list'`) apps, use `useResolveAllRecords` /
107
+ `app.records.resolveAll()` instead — it returns every applicable record, not
108
+ just the winner.
109
+
110
+ For everything else (props, slots, cardinality decisions, deep-link adapters,
111
+ lifecycle hooks, conflict handling, design tokens) → **read the matching file in
112
+ `node_modules/@proveanything/smartlinks-utils-ui/docs/`**.
113
+
114
+ ## Prerequisites
115
+
116
+ All components assume `SL.initializeApi()` has already run.
117
+
118
+ ## Where to go next
119
+
120
+ - Per-component reference (shipped with the package): `node_modules/@proveanything/smartlinks-utils-ui/docs/`
121
+ - Records pattern (concepts, scope inheritance, resolution order): [`app-records-pattern.md`](./app-records-pattern.md)