@proveanything/smartlinks 1.9.20 → 1.9.22

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,315 @@
1
+ # SmartLinks Records-Based Admin Pattern
2
+
3
+ > Canonical guide for building admin UIs in microapps that store **per-product**, **per-facet**, **per-variant** or **per-batch** data.
4
+ >
5
+ > Audience: microapp developers (nutrition, allergy, ingredients, cooking-guide, warranty, provenance, …).
6
+ >
7
+ > Status: **standard** — new admin apps in this category MUST follow this contract. Existing apps SHOULD migrate.
8
+
9
+ ---
10
+
11
+ ## 1. Why a shared pattern?
12
+
13
+ Many SmartLinks microapps store **structured data that varies along one or more product axes**:
14
+
15
+ | App | Varies by |
16
+ |------------------|------------------------------------------------|
17
+ | Nutrition | product, facet (bread type, region), batch |
18
+ | Allergy | product, facet (recipe family) |
19
+ | Ingredients | product, variant (size), batch (production run)|
20
+ | Cooking guide | product, facet (cut of meat) |
21
+ | Warranty | product, variant, batch |
22
+ | Provenance | batch |
23
+
24
+ Without a shared pattern, every app reinvents:
25
+
26
+ - the left-rail "browse + select" list
27
+ - the per-axis precedence rules
28
+ - the inheritance/override UI
29
+ - the "no data yet" empty state
30
+ - CSV import/export
31
+ - bulk operations
32
+
33
+ The result is drift: each app feels different and admins have to re-learn the model. This guide locks the model down at the SDK level so the matching UI primitives in `@proveanything/ui-utils` (see the [companion guide](ui-utils.md)) can stay simple.
34
+
35
+ ---
36
+
37
+ ## 2. Storage model: `app.records`
38
+
39
+ **Do not** stuff per-axis data into `appConfiguration.products[productId]`. Use `app.records` (recordType-keyed) so that data is queryable, paginatable and survives schema changes.
40
+
41
+ ```ts
42
+ import { app } from '@proveanything/smartlinks';
43
+
44
+ await app.records.create(collectionId, appId, {
45
+ recordType: 'nutrition', // app-defined, stable
46
+ ref: 'product:prod_abc', // see §3
47
+ data: { /* domain payload */ },
48
+ }, /* admin= */ true);
49
+ ```
50
+
51
+ Each record has:
52
+
53
+ | Field | Purpose |
54
+ |--------------|-------------------------------------------------------------|
55
+ | `recordType` | Namespaces records inside the app. One app may have several (`nutrition`, `cooking_steps`). |
56
+ | `ref` | Encodes the **scope** of the record. See §3. |
57
+ | `data` | The domain payload. Free-form per app. |
58
+ | `meta` | Reserved for system fields (timestamps, author). |
59
+
60
+ > **Rule:** `(appId, recordType, ref)` is unique. Treat it as the natural key.
61
+
62
+ ---
63
+
64
+ ## 3. The `ref` convention (REQUIRED)
65
+
66
+ `ref` is a colon-delimited string that encodes which scope a record applies to. Standardising it is what makes the shared admin shell possible.
67
+
68
+ ```
69
+ product:<productId>
70
+ variant:<productId>:<variantId>
71
+ batch:<productId>:<batchId>
72
+ proof:<proofId>
73
+ facet:<facetKey>:<valueKey>
74
+ default
75
+ ```
76
+
77
+ Notes:
78
+
79
+ - Variants and batches are **always nested under a product** in the SDK, so their refs include `productId`.
80
+ - `facet:` refs are **collection-wide** — facets cross products by design.
81
+ - `default` is a single record per (app, recordType) used as the global fallback.
82
+ - Refs are opaque to the SDK. Apps parse them. A helper module (`@proveanything/smartlinks-utils-ui/records-admin`) exports `parseRef`/`buildRef` so all apps agree on syntax. See Appendix A for the full implementation.
83
+
84
+ ### Adding scopes later
85
+
86
+ If a new axis appears (e.g. `region:eu`), pick a new prefix and document it. Never reuse a prefix with different semantics.
87
+
88
+ ---
89
+
90
+ ## 4. Resolution order (REQUIRED)
91
+
92
+ When the **public** side of an app needs "the data that applies to this proof / product / context", it walks the chain from most-specific to least-specific and returns the first match:
93
+
94
+ ```
95
+ proof → batch → variant → product → facet(*) → default
96
+ ```
97
+
98
+ `facet(*)` means: walk every facet attached to the product in a deterministic order (alphabetical by `facetKey`, then `valueKey`) and use the first matching facet record.
99
+
100
+ > **Rule:** Resolution is **first-match-wins, not merge**. If you need field-level merging, build it on top with explicit `inheritsFrom` markers in the payload — but the default for shared infra is whole-record replacement, because it is far easier to reason about.
101
+
102
+ The canonical resolver lives in `@proveanything/smartlinks-utils-ui/records-admin` so every app behaves identically:
103
+
104
+ ```ts
105
+ import { resolveRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
106
+
107
+ const resolved = await resolveRecord({
108
+ appId,
109
+ recordType: 'nutrition',
110
+ scope: { collectionId, productId, variantId, batchId, proofId },
111
+ });
112
+ // → { record, source: 'variant' | 'product' | 'facet' | … } | null
113
+ ```
114
+
115
+ Apps that only support a subset of scopes pass `supportedScopes: ['product', 'facet']` and the resolver skips the rest.
116
+
117
+ ---
118
+
119
+ ## 5. Scope capabilities (declared per app)
120
+
121
+ An app declares which scopes it accepts records for in `app.manifest.json`. This drives the admin UI and avoids dead tabs:
122
+
123
+ ```json
124
+ // app.manifest.json (extension)
125
+ {
126
+ "records": {
127
+ "nutrition": {
128
+ "scopes": ["product", "facet", "batch"],
129
+ "defaultScope": "facet",
130
+ "label": "Nutrition info"
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ | Field | Meaning |
137
+ |----------------|----------------------------------------------------------------|
138
+ | `scopes` | Allowed scope kinds, in **resolution order**. |
139
+ | `defaultScope` | Where the "Create new" button lands in the admin shell. |
140
+ | `label` | Human-readable label for the record type (used in headings). |
141
+
142
+ The shared admin shell reads this manifest entry and renders only the relevant tabs. Apps don't hard-code tab lists.
143
+
144
+ See [app-manifest.md](app-manifest.md) for the full schema reference.
145
+
146
+ ---
147
+
148
+ ## 6. Discovering whether variants / batches are in use
149
+
150
+ The `Collection` object exposes top-level `variants: boolean` and `batches: boolean` flags that indicate whether the collection has these features enabled. Read them directly rather than probing by listing:
151
+
152
+ ```ts
153
+ import { appConfiguration } from '@proveanything/smartlinks';
154
+
155
+ const collection = await appConfiguration.getCollection(collectionId);
156
+
157
+ const showVariantTab = collection.variants && scopeConfig?.scopes.includes('variant') === true;
158
+ const showBatchTab = collection.batches && scopeConfig?.scopes.includes('batch') === true;
159
+ ```
160
+
161
+ `scopeConfig` is the parsed manifest `records` entry for this `recordType` — e.g. `manifest.records?.nutrition`.
162
+
163
+ Rules:
164
+
165
+ 1. If the collection has the feature **and** the app **declares** support for the scope, show the tab and offer "Add variant" / "Add batch" affordances.
166
+ 2. If the app does **not** declare support, hide the tab entirely even if `collection.variants` is true (another app created them).
167
+ 3. If the collection does not have the feature enabled, hide the tab even if the app declares support.
168
+
169
+ ---
170
+
171
+ ## 7. Inheritance & overrides
172
+
173
+ Because resolution is first-match-wins, the admin UI must make inheritance **visible**:
174
+
175
+ - When editing a **variant** record, show the **product** record as the inherited baseline.
176
+ - Each field in the editor displays a small **↩ "Inherited"** marker when its value matches the parent and **● "Override"** when it differs.
177
+ - A row-level "Reset to inherited" action removes the override (deletes the record at the current scope if all overrides are reset).
178
+
179
+ Apps don't have to implement this themselves — the `<RecordEditor>` primitive in `@proveanything/smartlinks-utils-ui` does it given the resolved parent payload.
180
+
181
+ ---
182
+
183
+ ## 8. Bulk operations
184
+
185
+ Standard verbs every shell should expose:
186
+
187
+ | Verb | Behaviour |
188
+ |-------------------|----------------------------------------------------------------------|
189
+ | **Apply to many** | Take the current record's payload, write it to N selected products / variants. |
190
+ | **Copy from** | Pick a source scope, copy its payload to the current scope. |
191
+ | **Clear** | Delete records at the current scope (children unaffected). |
192
+
193
+ Use the `bulkUpsert` / `bulkDelete` helpers from `@proveanything/smartlinks-utils-ui/records-admin`, which handle batching and error collection for you:
194
+
195
+ ```ts
196
+ import { bulkUpsert, bulkDelete } from '@proveanything/smartlinks-utils-ui/records-admin';
197
+
198
+ // Apply current payload to many refs
199
+ await bulkUpsert({ SL, collectionId, appId, recordType, refs: targetRefs, data: payload });
200
+
201
+ // Clear records at selected refs
202
+ await bulkDelete({ SL, collectionId, appId, recordType, refs: targetRefs });
203
+ ```
204
+
205
+ If you are using `<RecordsAdminShell>`, the bulk actions menu is included and wired automatically via the `onTelemetry` hook — you don't call these directly unless you're building a custom shell.
206
+
207
+ ---
208
+
209
+ ## 9. CSV import / export
210
+
211
+ Adopt this column shape across all records-based apps:
212
+
213
+ ```
214
+ scope,scopeRef,<field1>,<field2>,...
215
+ product,prod_abc,250,12.5,...
216
+ variant,prod_abc/var_500ml,260,12.5,...
217
+ batch,prod_abc/B-2024-03,255,12.5,...
218
+ facet,bread_type/sourdough,240,11.0,...
219
+ ```
220
+
221
+ - `scope` is the kind; `scopeRef` is the human-readable reference (the shell maps it to the canonical `ref`).
222
+ - Validation errors return a downloadable annotated CSV with an `error` column appended.
223
+ - Round-tripping (export → reimport unchanged) MUST be a no-op.
224
+
225
+ ---
226
+
227
+ ## 10. Public-side hook contract
228
+
229
+ To keep widgets consistent across apps, expose one hook per record type (implemented in `@proveanything/ui-utils`):
230
+
231
+ ```ts
232
+ import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
233
+
234
+ const { data, source, isLoading } = useResolvedRecord({
235
+ appId,
236
+ recordType: 'nutrition',
237
+ // any combination of these — the hook walks the chain:
238
+ collectionId, productId, variantId, batchId, proofId,
239
+ });
240
+ ```
241
+
242
+ `source` is one of `'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'default' | null`. UI can show a badge ("Showing batch-specific values") when useful.
243
+
244
+ ---
245
+
246
+ ## 11. Telemetry
247
+
248
+ All admin shells emit these events. The `<RecordsAdminShell>` from `@proveanything/ui-utils` wires them automatically using `interactions.appendEvent` from the SDK — apps don't call this themselves.
249
+
250
+ | Event | Props |
251
+ |------------------------|------------------------------------------------------|
252
+ | `record.opened` | `appId, recordType, ref, source` |
253
+ | `record.saved` | `appId, recordType, ref, fieldsChanged` |
254
+ | `record.deleted` | `appId, recordType, ref` |
255
+ | `record.bulkApplied` | `appId, recordType, sourceRef, targetCount` |
256
+ | `record.imported` | `appId, recordType, rows, errors` |
257
+
258
+ ---
259
+
260
+ ## 12. Required reading for app authors
261
+
262
+ 1. This document.
263
+ 2. The companion **[UI utils guide](ui-utils.md)** — explains the React primitives (`<RecordsAdminShell>`, `useResolvedRecord`, etc.) that implement this pattern.
264
+ 3. [PRODUCT_FACETS_SDK.md](PRODUCT_FACETS_SDK.md) — facet model.
265
+ 4. [app-data-storage.md](app-data-storage.md) — `app.records` surface.
266
+
267
+ ## 13. Migration checklist for existing apps
268
+
269
+ - [ ] Stop writing per-product data into `appConfiguration`.
270
+ - [ ] Move to `app.records` with `recordType` + `ref`.
271
+ - [ ] Adopt the `ref` syntax in §3.
272
+ - [ ] Add a `records` block to `app.manifest.json`.
273
+ - [ ] Replace bespoke admin browser with `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui`.
274
+ - [ ] Replace bespoke public hook with `useResolvedRecord`.
275
+ - [ ] Remove any "is variants enabled?" config — use `collection.variants` / `collection.batches` flags instead (§6).
276
+
277
+ ---
278
+
279
+ ## Appendix A — `ref` parser reference
280
+
281
+ This is the canonical implementation. Copy it into your app or import from `@proveanything/ui-utils/records`.
282
+
283
+ ```ts
284
+ type ParsedRef =
285
+ | { kind: 'default' }
286
+ | { kind: 'product'; productId: string }
287
+ | { kind: 'variant'; productId: string; variantId: string }
288
+ | { kind: 'batch'; productId: string; batchId: string }
289
+ | { kind: 'proof'; proofId: string }
290
+ | { kind: 'facet'; facetKey: string; valueKey: string };
291
+
292
+ export const buildRef = (p: ParsedRef): string => {
293
+ switch (p.kind) {
294
+ case 'default': return 'default';
295
+ case 'product': return `product:${p.productId}`;
296
+ case 'variant': return `variant:${p.productId}:${p.variantId}`;
297
+ case 'batch': return `batch:${p.productId}:${p.batchId}`;
298
+ case 'proof': return `proof:${p.proofId}`;
299
+ case 'facet': return `facet:${p.facetKey}:${p.valueKey}`;
300
+ }
301
+ };
302
+
303
+ export const parseRef = (ref: string): ParsedRef | null => {
304
+ if (ref === 'default') return { kind: 'default' };
305
+ const [head, ...rest] = ref.split(':');
306
+ switch (head) {
307
+ case 'product': return rest.length === 1 ? { kind: 'product', productId: rest[0] } : null;
308
+ case 'variant': return rest.length === 2 ? { kind: 'variant', productId: rest[0], variantId: rest[1] } : null;
309
+ case 'batch': return rest.length === 2 ? { kind: 'batch', productId: rest[0], batchId: rest[1] } : null;
310
+ case 'proof': return rest.length === 1 ? { kind: 'proof', proofId: rest[0] } : null;
311
+ case 'facet': return rest.length >= 2 ? { kind: 'facet', facetKey: rest[0], valueKey: rest.slice(1).join(':') } : null;
312
+ default: return null;
313
+ }
314
+ };
315
+ ```
@@ -0,0 +1,294 @@
1
+ # SmartLinks UI Utils (`@proveanything/smartlinks-utils-ui`)
2
+
3
+ > Companion React component library for the SmartLinks SDK. Ships the heavy, opinionated admin UI pieces that almost every SmartLinks microapp ends up needing — built once, theme-able, tree-shakeable, and wired straight into the SmartLinks SDK.
4
+ >
5
+ > Package: `@proveanything/smartlinks-utils-ui`
6
+ > Tracks: `@proveanything/smartlinks ≥ 1.9`
7
+
8
+ ---
9
+
10
+ ## What is this module for?
11
+
12
+ `@proveanything/smartlinks-utils-ui` sits on top of `@proveanything/smartlinks`. The core SDK handles data — records, configurations, interactions. This module handles **UI** — the shared React components, hooks, and admin shells that translate SDK data into consistent admin interfaces.
13
+
14
+ **When do you need it?**
15
+
16
+ - You are building an admin UI for a records-based microapp (see [records-admin-pattern.md](records-admin-pattern.md))
17
+ - You need a media asset picker, icon picker, or font picker in an admin panel
18
+ - You need a recursive rule/conditions editor for targeting or audience logic
19
+ - You want the standard inheritance/override editor for scoped records
20
+ - You need the `useResolvedRecord` hook on the public widget side
21
+
22
+ You do **not** need it for apps that only use `appConfiguration`, basic widgets without scoped data, or executor bundles.
23
+
24
+ > **Admin-only**: all components call the SDK with `admin: true`. Do not render them in public-facing views.
25
+
26
+ ---
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install @proveanything/smartlinks-utils-ui
32
+ ```
33
+
34
+ Peer dependencies (you already have these in a SmartLinks app):
35
+
36
+ ```bash
37
+ npm install react react-dom @proveanything/smartlinks
38
+ # Recommended — enables caching and pagination in Records Admin Shell:
39
+ npm install @tanstack/react-query
40
+ ```
41
+
42
+ Import the compiled styles **once** in your app entry (e.g. `main.tsx`):
43
+
44
+ ```tsx
45
+ import '@proveanything/smartlinks-utils-ui/styles.css';
46
+ ```
47
+
48
+ Components inherit your shadcn-compatible CSS variables (`--primary`, `--background`, `--border`, …) so they pick up your theme automatically.
49
+
50
+ ---
51
+
52
+ ## What's in the box
53
+
54
+ | Module | What it is | When to reach for it |
55
+ |--------|------------|----------------------|
56
+ | [Records Admin Shell](#records-admin-shell) | Full admin UI for `app.records` with scope inheritance | Per-product / per-variant / per-batch / per-facet config tools |
57
+ | [Asset Picker](#asset-picker) | Browse / upload / paste / URL-import images and files | Any time the admin needs to pick or upload media |
58
+ | [Icon Picker](#icon-picker) | Searchable Font Awesome 7 Pro picker | Configurable buttons, badges, menus, tiles |
59
+ | [Font Picker](#font-picker) | Google Fonts + custom uploaded fonts | Theme editors, brand customisation panels |
60
+ | [Conditions Editor](#conditions-editor) | Recursive AND/OR rule builder for 12 condition types | Targeting, gating, audience rules, segmentation |
61
+
62
+ ---
63
+
64
+ ## Records Admin Shell
65
+
66
+ The primary export. A complete admin UI for managing `app.records` — typed JSON blobs attached to facets, products, variants, and batches — with scope inheritance built in. You provide the form for one record; the shell handles everything else.
67
+
68
+ ```tsx
69
+ import {
70
+ RecordsAdminShell,
71
+ InheritanceMarker,
72
+ type EditorContext,
73
+ } from '@proveanything/smartlinks-utils-ui/records-admin';
74
+ import * as SL from '@proveanything/smartlinks';
75
+
76
+ <RecordsAdminShell<NutritionData>
77
+ SL={SL}
78
+ collectionId={collectionId}
79
+ appId={appId}
80
+ recordType="nutrition"
81
+ label="Nutrition info"
82
+ scopes={['facet', 'product', 'variant', 'batch']}
83
+ contextScope={{ productId, variantId, batchId }} // from iframe URL — optional
84
+ defaultData={() => ({})}
85
+ csvSchema={{ columns: [/* ... */] }}
86
+ renderEditor={(ctx) => <NutritionForm ctx={ctx} />}
87
+ renderPreview={({ resolved }) => <pre>{JSON.stringify(resolved, null, 2)}</pre>}
88
+ />
89
+ ```
90
+
91
+ **What it handles:**
92
+
93
+ - **Browser pane** with scope tabs (Facet / Product / Variant / Batch), search, and status filter pills (All / Configured / Partial / Empty)
94
+ - **Editor pane** with sticky save / discard / delete footer
95
+ - **Per-field `<InheritanceMarker>`** showing whether a value is the record's own or inherited from a parent scope, with one-click revert-to-inherited
96
+ - **Inheritance resolver** walks `batch → variant → product → facet` and returns both the resolved and parent values
97
+ - **Collection-aware tabs**: calls `collection.get` and hides Variants / Batches tabs unless `collection.variants` / `collection.batches` are true — no flicker
98
+ - **Server-side pagination** via `useInfiniteQuery` — handles thousands of products with a "Load more" button
99
+ - **Context-aware**: pass `contextScope` from your iframe URL (`productId` / `variantId` / `batchId`) and the browser is constrained to that subtree with the right tab auto-selected
100
+ - **CSV import / export** with a schema you define; failed rows come back as an annotated CSV
101
+ - **Bulk actions menu** (apply-to-many, copy-from, clear) via `bulkUpsert` / `bulkDelete`
102
+ - **Telemetry hook** (`onTelemetry`) emits typed events for save, delete, scope change, CSV import/export, bulk apply
103
+ - **i18n strings** fully overridable
104
+
105
+ > Requires a `<QueryClientProvider>` from `@tanstack/react-query` somewhere up the tree.
106
+
107
+ ### Lower-level pieces (advanced use)
108
+
109
+ ```tsx
110
+ import {
111
+ // Hooks
112
+ useRecordList, useRecordEditor, useResolvedRecord, useScopeProbe,
113
+ // Data helpers
114
+ parseRef, buildRef, resolutionChain,
115
+ listRecords, getRecordByRef, upsertRecord, deleteRecord,
116
+ bulkUpsert, bulkDelete,
117
+ exportCsv, importCsv, downloadBlob,
118
+ // UI pieces
119
+ RecordBrowser, RecordEditor, ScopeBreadcrumb,
120
+ InheritanceMarker, ResolvedPreview, BulkActionsMenu,
121
+ } from '@proveanything/smartlinks-utils-ui/records-admin';
122
+ ```
123
+
124
+ ### `useResolvedRecord` hook
125
+
126
+ ```ts
127
+ import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
128
+
129
+ const { data, source, isLoading } = useResolvedRecord({
130
+ SL,
131
+ appId,
132
+ recordType: 'nutrition',
133
+ collectionId,
134
+ productId,
135
+ variantId, // optional
136
+ batchId, // optional
137
+ proofId, // optional
138
+ });
139
+ // source: 'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'default' | null
140
+ ```
141
+
142
+ Walks the resolution chain defined in [records-admin-pattern.md §4](records-admin-pattern.md#4-resolution-order-required). Use this on the **public widget side** to read the correct value for a given context.
143
+
144
+ ### `parseRef` / `buildRef` utilities
145
+
146
+ ```ts
147
+ import { parseRef, buildRef } from '@proveanything/smartlinks-utils-ui/records-admin';
148
+
149
+ const parsed = parseRef('variant:prod_abc:var_500ml');
150
+ // → { kind: 'variant', productId: 'prod_abc', variantId: 'var_500ml' }
151
+
152
+ const ref = buildRef({ kind: 'facet', facetKey: 'bread_type', valueKey: 'sourdough' });
153
+ // → 'facet:bread_type:sourdough'
154
+ ```
155
+
156
+ See [records-admin-pattern.md Appendix A](records-admin-pattern.md#appendix-a--ref-parser-reference) for the full `ParsedRef` type.
157
+
158
+ ---
159
+
160
+ ## Asset Picker
161
+
162
+ ```tsx
163
+ import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
164
+
165
+ <AssetPicker
166
+ scope={{ type: 'collection', collectionId: 'abc123' }}
167
+ mode="dialog" // or "inline"
168
+ allowUpload
169
+ accept={['image/*']} // MIME filtering
170
+ onSelect={(asset) => setHeroUrl(asset.url)}
171
+ trigger={<Button>Choose image</Button>}
172
+ />
173
+ ```
174
+
175
+ **What it does:**
176
+ - Browses assets at **collection** or **product** scope (dual-scope tabs)
177
+ - Four ingest paths: file upload, URL import, clipboard paste (with rename preview), and selection from existing assets
178
+ - Inline mode for embedding in a panel; dialog mode for modal pickers
179
+ - Double-click a tile to confirm instantly
180
+
181
+ ---
182
+
183
+ ## Icon Picker
184
+
185
+ ```tsx
186
+ import { IconPicker } from '@proveanything/smartlinks-utils-ui/icon-picker';
187
+
188
+ <IconPicker
189
+ mode="dialog"
190
+ value="fa-solid fa-heart"
191
+ onSelect={(icon) => setIcon(icon.name)}
192
+ trigger={<Button>Pick icon</Button>}
193
+ />
194
+ ```
195
+
196
+ **What it does:**
197
+ - Searches **Font Awesome 7 Pro** (uses the shared kit `75493b59b3`)
198
+ - Family hierarchy: Classic (Solid / Regular / Light), Duotone, and Brands
199
+ - Search-first with a background catalogue crawler — first results appear instantly, the full index fills in behind
200
+ - Auto-switches families intelligently (e.g. brand searches surface brand icons even when "Classic" is selected)
201
+
202
+ > Requires the FA kit script to be present on the host page.
203
+
204
+ ---
205
+
206
+ ## Font Picker
207
+
208
+ ```tsx
209
+ import { FontPicker } from '@proveanything/smartlinks-utils-ui/font-picker';
210
+
211
+ <FontPicker
212
+ mode="dialog"
213
+ value="Inter"
214
+ showPreview
215
+ onSelect={(font) => {
216
+ console.log(font.family); // "Inter"
217
+ console.log(font.cssFontFamily); // "'Inter', ui-sans-serif, system-ui, sans-serif"
218
+ console.log(font.loadSnippet); // <link href="..." rel="stylesheet">
219
+ }}
220
+ />
221
+
222
+ {/* With custom fonts uploaded into the collection */}
223
+ <FontPicker
224
+ mode="dialog"
225
+ showCustomFonts
226
+ scope={{ collectionId: 'abc123' }}
227
+ onSelect={(font) => /* ... */}
228
+ />
229
+ ```
230
+
231
+ **What it does:**
232
+ - Full **Google Fonts** catalogue plus any **custom fonts** uploaded for the brand (stored via `appConfiguration` under `customFonts`)
233
+ - Upload zone auto-detects weight/style from the filename (e.g. `MyFont-BoldItalic.woff2`)
234
+ - Returns a `FontSelection` with `family`, `cssFontFamily`, and a ready-to-inject `loadSnippet` (`<link>` for Google fonts, `@font-face` CSS for custom uploads)
235
+ - Lazy-loads previews via `IntersectionObserver`; includes a management UI for editing custom font definitions
236
+
237
+ ---
238
+
239
+ ## Conditions Editor
240
+
241
+ ```tsx
242
+ import { ConditionsEditor } from '@proveanything/smartlinks-utils-ui/conditions-editor';
243
+
244
+ <ConditionsEditor
245
+ value={rules}
246
+ onChange={setRules}
247
+ collectionId={collectionId} // auto-loads facet definitions
248
+ versions={[{ title: 'Default', value: '' }]}
249
+ tags={['featured', 'new']}
250
+ />
251
+ ```
252
+
253
+ **What it does:**
254
+ - Recursive AND / OR group builder — nest conditions to any depth
255
+ - **12 condition types:** Version, Country, Value, User, Date, Device, Tag, Facet, Geofence, Product, Item Status, Condition Reference
256
+ - **Facet condition** auto-fetches definitions from `facets.publicList(collectionId, { includeValues: true })` when only `collectionId` is passed (SDK ≥ 1.9.20)
257
+ - **Country picker** is a searchable multi-select with removable ISO 3166-1 chips and a "Use regions" toggle
258
+ - Renders correctly inside iframe contexts — avoids `overflow-hidden` so dropdowns escape their cards
259
+
260
+ ---
261
+
262
+ ## Tree shaking
263
+
264
+ Each component has its own subpath export:
265
+
266
+ ```tsx
267
+ // Bundles only the Asset Picker
268
+ import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
269
+
270
+ // Barrel import — bundler tree-shakes the rest
271
+ import { AssetPicker } from '@proveanything/smartlinks-utils-ui';
272
+ ```
273
+
274
+ If you use subpath imports, import `styles.css` separately — subpaths do not pull it in automatically.
275
+
276
+ ---
277
+
278
+ ## Relationship to the core SDK
279
+
280
+ ```
281
+ @proveanything/smartlinks ← data layer (records, config, interactions, …)
282
+
283
+ @proveanything/smartlinks-utils-ui ← UI layer (components, hooks, admin shells)
284
+
285
+ your microapp ← domain logic and custom forms
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Further reading
291
+
292
+ - [records-admin-pattern.md](records-admin-pattern.md) — the data contract that `RecordsAdminShell` implements
293
+ - [building-react-components.md](building-react-components.md) — dual-mode rendering rules that apply to all components
294
+ - [app-manifest.md](app-manifest.md) — the `records` manifest block that drives tab generation
package/openapi.yaml CHANGED
@@ -17134,6 +17134,10 @@ components:
17134
17134
  type: string
17135
17135
  allowAutoGenerateClaims:
17136
17136
  type: boolean
17137
+ variants:
17138
+ type: boolean
17139
+ batches:
17140
+ type: boolean
17137
17141
  defaultAuthKitId:
17138
17142
  type: string
17139
17143
  required:
@@ -17159,6 +17163,8 @@ components:
17159
17163
  - supported
17160
17164
  - roles
17161
17165
  - shortId
17166
+ - variants
17167
+ - batches
17162
17168
  - defaultAuthKitId
17163
17169
  AppConfig:
17164
17170
  type: object
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.9.20",
3
+ "version": "1.9.22",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",