@proveanything/smartlinks 1.13.3 → 1.13.4
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/docs/API_SUMMARY.md +1 -1
- package/dist/docs/ui-utils.md +56 -462
- package/docs/API_SUMMARY.md +1 -1
- package/docs/ui-utils.md +56 -462
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
package/dist/docs/ui-utils.md
CHANGED
|
@@ -1,510 +1,104 @@
|
|
|
1
|
-
#
|
|
1
|
+
# UI Utils — `@proveanything/smartlinks-utils-ui`
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
> Package: `@proveanything/smartlinks-utils-ui`
|
|
9
|
-
> Tracks: `@proveanything/smartlinks ≥ 1.9` (some hooks require ≥ 1.10)
|
|
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.
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`@proveanything/smartlinks-utils-ui` sits on top of `@proveanything/smartlinks`.
|
|
16
|
-
The core SDK handles data — records, configurations, interactions. This module
|
|
17
|
-
handles **UI** — the shared React components, hooks, and admin shells that
|
|
18
|
-
translate SDK data into consistent admin interfaces.
|
|
19
|
-
|
|
20
|
-
**When do you need it?**
|
|
21
|
-
|
|
22
|
-
- You are building an admin UI for a records-based microapp (see [app-records-pattern.md](app-records-pattern.md))
|
|
23
|
-
- You need a media asset picker, icon picker, or font picker in an admin panel
|
|
24
|
-
- You need a recursive rule/conditions editor for targeting or audience logic
|
|
25
|
-
- You want the standard inheritance/override editor for scoped records
|
|
26
|
-
- You need the `useResolvedRecord` / `useCollectedRecords` hooks on the public widget side
|
|
27
|
-
|
|
28
|
-
You do **not** need it for apps that only use `appConfiguration`, basic widgets
|
|
29
|
-
without scoped data, or executor bundles.
|
|
30
|
-
|
|
31
|
-
> **Admin components are admin-only**: `RecordsAdminShell`, `AssetPicker` upload,
|
|
32
|
-
> `FacetRuleEditor`, etc. call the SDK with `admin: true`. Do not render them in
|
|
33
|
-
> public-facing views. The public-side hooks (`useResolvedRecord`,
|
|
34
|
-
> `useCollectedRecords`) are safe in widgets.
|
|
35
|
-
|
|
36
|
-
---
|
|
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)
|
|
37
12
|
|
|
38
13
|
## Install
|
|
39
14
|
|
|
40
15
|
```bash
|
|
41
16
|
npm install @proveanything/smartlinks-utils-ui
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Peer dependencies (you already have these in a SmartLinks app):
|
|
45
|
-
|
|
46
|
-
```bash
|
|
17
|
+
# Peer deps you probably already have:
|
|
47
18
|
npm install react react-dom @proveanything/smartlinks @tanstack/react-query
|
|
48
19
|
```
|
|
49
20
|
|
|
50
|
-
|
|
51
|
-
(caching, pagination, optimistic save). Wrap your app in a
|
|
52
|
-
`<QueryClientProvider>` somewhere up the tree.
|
|
53
|
-
|
|
54
|
-
Import the compiled styles **once** in your app entry (e.g. `main.tsx`):
|
|
21
|
+
Import the styles **once** in your app entry (typically `src/admin-main.tsx`):
|
|
55
22
|
|
|
56
|
-
```
|
|
23
|
+
```ts
|
|
57
24
|
import '@proveanything/smartlinks-utils-ui/styles.css';
|
|
58
25
|
```
|
|
59
26
|
|
|
60
27
|
Components inherit your shadcn-compatible CSS variables (`--primary`,
|
|
61
|
-
`--background`, `--border`, …)
|
|
28
|
+
`--background`, `--border`, …) — no extra theming required.
|
|
62
29
|
|
|
63
|
-
|
|
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`.
|
|
64
33
|
|
|
65
34
|
## What's in the box
|
|
66
35
|
|
|
67
|
-
| Module |
|
|
68
|
-
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
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:
|
|
75
50
|
|
|
76
|
-
|
|
51
|
+
```ts
|
|
52
|
+
import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
53
|
+
import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
|
|
54
|
+
```
|
|
77
55
|
|
|
78
|
-
##
|
|
56
|
+
## Minimal examples
|
|
79
57
|
|
|
80
|
-
|
|
81
|
-
JSON blobs attached to facets, products, variants, batches, the collection
|
|
82
|
-
root, or matched via facet rules — with scope inheritance built in. You
|
|
83
|
-
provide the form for one record; the shell handles everything else.
|
|
58
|
+
### Records admin (most apps need this)
|
|
84
59
|
|
|
85
60
|
```tsx
|
|
86
|
-
import {
|
|
87
|
-
RecordsAdminShell,
|
|
88
|
-
InheritanceMarker,
|
|
89
|
-
type EditorContext,
|
|
90
|
-
} from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
61
|
+
import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
91
62
|
import * as SL from '@proveanything/smartlinks';
|
|
92
63
|
|
|
93
|
-
<RecordsAdminShell
|
|
64
|
+
<RecordsAdminShell
|
|
94
65
|
SL={SL}
|
|
95
66
|
collectionId={collectionId}
|
|
96
67
|
appId={appId}
|
|
97
68
|
recordType="nutrition"
|
|
98
69
|
label="Nutrition info"
|
|
99
|
-
scopes={['
|
|
100
|
-
|
|
70
|
+
scopes={['collection', 'rule', 'product']} // canonical top-level tabs
|
|
71
|
+
// items={{ cardinality: 'list' }} // ← opt in for multi-item apps
|
|
101
72
|
defaultData={() => ({})}
|
|
102
|
-
csvSchema={{ columns: [/* ... */] }} // optional — omit to disable CSV
|
|
103
73
|
renderEditor={(ctx) => <NutritionForm ctx={ctx} />}
|
|
104
|
-
renderPreview={({ resolved }) => (
|
|
105
|
-
<pre>{JSON.stringify(resolved, null, 2)}</pre>
|
|
106
|
-
)}
|
|
107
74
|
/>
|
|
108
75
|
```
|
|
109
76
|
|
|
110
|
-
###
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
type ScopeKind = 'collection' | 'product' | 'facet' | 'variant' | 'batch' | 'rule';
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
- `'collection'` — terminal default (one record for the whole brand)
|
|
117
|
-
- `'facet'` — anchored to a facet value (e.g. `bagel-type=white`)
|
|
118
|
-
- `'product'` / `'variant'` / `'batch'` — anchored to that node
|
|
119
|
-
- `'rule'` — synthetic UI scope: records targeted via a `facetRule`
|
|
120
|
-
(AND-of-OR over facets) rather than pinned to a node. Their refs start
|
|
121
|
-
with `rule:`, and the shell renders the `<FacetRuleEditor>` inline when the
|
|
122
|
-
user opens one of these records.
|
|
123
|
-
|
|
124
|
-
### What it handles
|
|
125
|
-
|
|
126
|
-
- **Browser pane** with scope tabs (Facet / Product / Variant / Batch / Rule), search, and status filter pills
|
|
127
|
-
- **Editor pane** with sticky save / discard / delete footer, optimistic save, and an unsaved-drafts tray
|
|
128
|
-
- **Per-field `<InheritanceMarker>`** showing whether a value is the record's own or inherited from a parent scope, with one-click revert-to-inherited
|
|
129
|
-
- **Inheritance resolver** walks `proof → batch → variant → product → rule → facet → collection` server-side via SDK 1.10 `match()`
|
|
130
|
-
- **Collection-aware tabs**: calls `collection.get` and hides Variants / Batches tabs unless `collection.variants` / `collection.batches` are true — no flicker
|
|
131
|
-
- **Server-side pagination** via `useInfiniteQuery` — handles thousands of products with a "Load more" button
|
|
132
|
-
- **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
|
|
133
|
-
- **Cardinality**: `cardinality="singleton"` (one record per scope) or `"collection"` (many — FAQs, recipes, SOPs). Collection mode adds an item-list view (`itemViews: ['table' | 'cards' | 'gallery']`) and Back / prev / next nav
|
|
134
|
-
- **Multiple presentations** for the rail (`presentations: ['list' | 'compact']`) and right pane
|
|
135
|
-
- **CSV import / export** (optional — provide `csvSchema` to enable); failed rows come back as an annotated CSV
|
|
136
|
-
- **Bulk actions menu** (apply-to-many, copy-from, clear) via `bulkUpsert` / `bulkDelete`
|
|
137
|
-
- **Clipboard** — copy a record's value, paste onto another scope
|
|
138
|
-
- **Telemetry hook** (`onTelemetry`) emits typed events for save, delete, scope change, CSV import/export, bulk apply
|
|
139
|
-
- **i18n strings** fully overridable via the `i18n` prop
|
|
140
|
-
|
|
141
|
-
> Requires a `<QueryClientProvider>` from `@tanstack/react-query` somewhere up the tree.
|
|
142
|
-
|
|
143
|
-
### Lower-level pieces (advanced use)
|
|
77
|
+
### Public widget — read the resolved value
|
|
144
78
|
|
|
145
79
|
```tsx
|
|
146
|
-
import {
|
|
147
|
-
// Hooks
|
|
148
|
-
useRecordList, useRecordEditor, useResolvedRecord, useCollectedRecords,
|
|
149
|
-
useResolveAllRecords, useRulePreview, useScopeProbe,
|
|
150
|
-
// Data helpers
|
|
151
|
-
parseRef, buildRef, resolutionChain,
|
|
152
|
-
listRecords, getRecordById, createRecord, upsertRecord,
|
|
153
|
-
removeRecord, restoreRecord, matchRecords,
|
|
154
|
-
bulkUpsert, bulkDelete,
|
|
155
|
-
exportCsv, importCsv, downloadBlob,
|
|
156
|
-
// UI pieces
|
|
157
|
-
RecordBrowser, RecordEditor, ScopeBreadcrumb,
|
|
158
|
-
InheritanceMarker, ResolvedPreview, BulkActionsMenu,
|
|
159
|
-
// Drafts / unsaved state
|
|
160
|
-
DirtyDraftProvider, useDirtyDrafts, useUnsavedGuard,
|
|
161
|
-
} from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Note the names: it's `getRecordById` (not `getRecordByRef`) and `removeRecord`
|
|
165
|
-
(not `deleteRecord`). Records are addressed by UUID `id` internally; refs
|
|
166
|
-
(`product:abc123`) are only used for display / breadcrumb / URL purposes.
|
|
167
|
-
|
|
168
|
-
### `useResolvedRecord` hook
|
|
169
|
-
|
|
170
|
-
Use on the **public widget side** when the app shows one answer for the
|
|
171
|
-
current product (singleton cardinality). Walks the inheritance chain
|
|
172
|
-
server-side and returns the first match.
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
80
|
import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
176
81
|
|
|
177
|
-
const { data, source
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
appId,
|
|
181
|
-
recordType: 'nutrition',
|
|
182
|
-
collectionId,
|
|
183
|
-
productId,
|
|
184
|
-
variantId, // optional
|
|
185
|
-
batchId, // optional
|
|
186
|
-
proofId, // optional
|
|
187
|
-
// recordId, // optional — direct UUID lookup, bypasses inheritance
|
|
188
|
-
});
|
|
189
|
-
// source: 'self' | 'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'universal' | 'empty'
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
- `source` — which scope the winning record came from. `'self'` is returned
|
|
193
|
-
when you passed an explicit `recordId`; `'empty'` when nothing matched.
|
|
194
|
-
- `sourceRef` — the ref of the matched record (e.g. `product:abc123`).
|
|
195
|
-
- `recordId` — the UUID of the matched record.
|
|
196
|
-
- `facetRule` — present when the match came from a rule-targeted record.
|
|
197
|
-
|
|
198
|
-
Pass `recordId` directly when you already know the UUID (deep links, rule
|
|
199
|
-
records) — the hook will skip the inheritance walk entirely.
|
|
200
|
-
|
|
201
|
-
### `useCollectedRecords` hook
|
|
202
|
-
|
|
203
|
-
Use on the **public widget side** when the app shows many answers
|
|
204
|
-
(collection cardinality — FAQs, recipes, care tips). Returns every matching
|
|
205
|
-
record across the chain, most-specific first.
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
import { useCollectedRecords } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
209
|
-
|
|
210
|
-
const { items, isLoading, error } = useCollectedRecords<FaqEntry>({
|
|
211
|
-
SL, appId, collectionId,
|
|
212
|
-
recordType: 'faq',
|
|
213
|
-
productId,
|
|
214
|
-
// sort: { kind: 'field', field: 'order', direction: 'asc' },
|
|
215
|
-
});
|
|
216
|
-
// items: CollectedRecord<FaqEntry>[] — each has { data, scope, ref, depth }
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
`depth: 0` is the most-specific match. Default sort is by specificity
|
|
220
|
-
descending; pass `{ kind: 'field', field, direction }` to sort by a payload
|
|
221
|
-
field instead (with specificity as a stable tiebreak).
|
|
222
|
-
|
|
223
|
-
### `useResolveAllRecords` hook
|
|
224
|
-
|
|
225
|
-
When you need every record of every declared type that applies to a context —
|
|
226
|
-
rare, mostly used in executors and SEO surfaces.
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
import { useResolveAllRecords } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
230
|
-
|
|
231
|
-
const { entries, isLoading } = useResolveAllRecords({
|
|
232
|
-
SL, collectionId, appId,
|
|
233
|
-
context: { productId, facets: { brand: 'acme' } },
|
|
82
|
+
const { data, source } = useResolvedRecord({
|
|
83
|
+
SL, appId, recordType: 'nutrition',
|
|
84
|
+
collectionId, productId, variantId, batchId,
|
|
234
85
|
});
|
|
86
|
+
// source: 'proof' | 'batch' | 'variant' | 'product' | 'rule' | 'collection' | null
|
|
235
87
|
```
|
|
236
88
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"matches N products" count as the rule is edited. Debounced — safe to call
|
|
241
|
-
on every keystroke.
|
|
242
|
-
|
|
243
|
-
```ts
|
|
244
|
-
import { useRulePreview } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
245
|
-
|
|
246
|
-
const preview = useRulePreview({
|
|
247
|
-
SL, collectionId, appId,
|
|
248
|
-
rule, // FacetRule | null
|
|
249
|
-
// limit: 20,
|
|
250
|
-
// debounceMs: 350,
|
|
251
|
-
});
|
|
252
|
-
// preview: {
|
|
253
|
-
// totalMatches: number | null;
|
|
254
|
-
// sampleProductIds: string[];
|
|
255
|
-
// isLoading: boolean;
|
|
256
|
-
// isStale: boolean;
|
|
257
|
-
// error: Error | null;
|
|
258
|
-
// }
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Pass `preview` straight into `<FacetRuleEditor preview={preview} />`.
|
|
262
|
-
|
|
263
|
-
### `useScopeProbe` hook
|
|
89
|
+
For multi-item (`cardinality: 'list'`) apps, use `useResolveAllRecords` /
|
|
90
|
+
`app.records.resolveAll()` instead — it returns every applicable record, not
|
|
91
|
+
just the winner.
|
|
264
92
|
|
|
265
|
-
|
|
266
|
-
|
|
93
|
+
For everything else (props, slots, cardinality decisions, deep-link adapters,
|
|
94
|
+
lifecycle hooks, conflict handling, design tokens) → **read the package docs**.
|
|
267
95
|
|
|
268
|
-
|
|
269
|
-
import { useScopeProbe } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
270
|
-
|
|
271
|
-
const { hasVariants, hasBatches, isLoading } = useScopeProbe({
|
|
272
|
-
SL, collectionId,
|
|
273
|
-
});
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
It's a thin wrapper around `SL.collection.get(collectionId).variants /.batches`
|
|
277
|
-
(SDK ≥ 1.9). It does **not** report per-scope record status — that's handled
|
|
278
|
-
internally by the shell's status pills.
|
|
279
|
-
|
|
280
|
-
### `parseRef` / `buildRef` utilities
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
import { parseRef, buildRef } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
284
|
-
|
|
285
|
-
const parsed = parseRef('variant:prod_abc:var_500ml');
|
|
286
|
-
// → { kind: 'variant', productId: 'prod_abc', variantId: 'var_500ml', raw: '...' }
|
|
287
|
-
|
|
288
|
-
const ref = buildRef({ kind: 'product', productId: 'prod_abc', raw: '' });
|
|
289
|
-
// → 'product:prod_abc'
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
Refs are for display, breadcrumbs, and URLs. Records are addressed by UUID
|
|
293
|
-
`id` everywhere internally.
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
|
-
## Facet Rule Editor
|
|
298
|
-
|
|
299
|
-
A standalone facet-rule builder. The `<RecordsAdminShell>` embeds this
|
|
300
|
-
automatically when `'rule'` is in `scopes` and the user opens a rule-targeted
|
|
301
|
-
record — reach for it directly only when you need rule editing elsewhere
|
|
302
|
-
(e.g. a settings page, a conditions sidebar).
|
|
303
|
-
|
|
304
|
-
```tsx
|
|
305
|
-
import { FacetRuleEditor } from '@proveanything/smartlinks-utils-ui/facet-rule-editor';
|
|
306
|
-
import { useRulePreview } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
307
|
-
|
|
308
|
-
const preview = useRulePreview({ SL, collectionId, appId, rule });
|
|
309
|
-
|
|
310
|
-
<FacetRuleEditor
|
|
311
|
-
value={rule}
|
|
312
|
-
onChange={setRule}
|
|
313
|
-
collectionId={collectionId} // lazy-fetches facet definitions via SL.facets.publicList
|
|
314
|
-
preview={preview} // optional — wire from useRulePreview
|
|
315
|
-
onClear={() => setRule(null)} // optional — renders a "Remove rule" affordance
|
|
316
|
-
/>
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
Props:
|
|
320
|
-
|
|
321
|
-
- `value: FacetRule | null` / `onChange: (next: FacetRule | null) => void` — controlled
|
|
322
|
-
- `facets?: FacetOption[]` — supply directly, **or** pass `collectionId` and the editor lazy-fetches via `SL.facets.publicList`
|
|
323
|
-
- `getFacets?: (collectionId: string) => Promise<FacetOption[]>` — override the lazy-fetcher
|
|
324
|
-
- `preview?` — `{ totalMatches, sampleProductIds?, isLoading?, isStale?, error? }` (matches the `useRulePreview` return shape)
|
|
325
|
-
- `readOnly?`, `onClear?`, `title?`, `description?`, `className?`
|
|
326
|
-
|
|
327
|
-
Free-text facet entry is **not** supported — admins must pick from defined facets.
|
|
328
|
-
|
|
329
|
-
See [app-records-pattern.md §4](app-records-pattern.md#4-admin-side----recordsadminshell-the-only-thing-you-should-be-writing) for the standalone usage example.
|
|
330
|
-
|
|
331
|
-
---
|
|
332
|
-
|
|
333
|
-
## Asset Picker
|
|
334
|
-
|
|
335
|
-
```tsx
|
|
336
|
-
import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
|
|
337
|
-
|
|
338
|
-
<AssetPicker
|
|
339
|
-
scope={{ type: 'collection', collectionId: 'abc123' }}
|
|
340
|
-
mode="dialog" // or "inline"
|
|
341
|
-
allowUpload
|
|
342
|
-
accept={['image/*']} // MIME filtering
|
|
343
|
-
onSelect={(asset) => setHeroUrl(asset.url)}
|
|
344
|
-
trigger={<Button>Choose image</Button>}
|
|
345
|
-
/>
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Scope shape:
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
type AssetScope =
|
|
352
|
-
| { type: 'collection'; collectionId: string }
|
|
353
|
-
| { type: 'product'; collectionId: string; productId: string }
|
|
354
|
-
| { type: 'proof'; collectionId: string; productId: string; proofId: string };
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
**What it does:**
|
|
358
|
-
|
|
359
|
-
- Browses assets at **collection**, **product**, or **proof** scope
|
|
360
|
-
- Optional `productScope` prop adds a second tab so users can pick from product-level assets while editing at collection scope (or vice versa)
|
|
361
|
-
- Optional `appId` prop stamps every upload with the owning app and adds "This app" / "All in collection" pill tabs with provenance badges on assets owned by other apps
|
|
362
|
-
- Four ingest paths: file upload, URL import, clipboard paste (with rename preview), and selection from existing assets
|
|
363
|
-
- Inline mode for embedding in a panel; dialog mode for modal pickers
|
|
364
|
-
- Double-click a tile to confirm instantly
|
|
365
|
-
|
|
366
|
-
---
|
|
367
|
-
|
|
368
|
-
## Icon Picker
|
|
369
|
-
|
|
370
|
-
```tsx
|
|
371
|
-
import { IconPicker } from '@proveanything/smartlinks-utils-ui/icon-picker';
|
|
372
|
-
|
|
373
|
-
<IconPicker
|
|
374
|
-
mode="dialog"
|
|
375
|
-
value="fa-solid fa-heart"
|
|
376
|
-
onSelect={(icon) => setIcon(icon.name)}
|
|
377
|
-
trigger={<Button>Pick icon</Button>}
|
|
378
|
-
/>
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
`onSelect` receives:
|
|
382
|
-
|
|
383
|
-
```ts
|
|
384
|
-
interface IconSelection {
|
|
385
|
-
name: string; // full CSS class, e.g. 'fa-solid fa-heart'
|
|
386
|
-
family: 'classic' | 'duotone' | 'brands';
|
|
387
|
-
style: 'solid' | 'regular' | 'light' | null; // null for brands
|
|
388
|
-
label?: string;
|
|
389
|
-
}
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
**What it does:**
|
|
393
|
-
|
|
394
|
-
- Searches **Font Awesome 7 Pro** (uses the shared kit)
|
|
395
|
-
- Family hierarchy: Classic (Solid / Regular / Light), Duotone, and Brands
|
|
396
|
-
- Search-first with a background catalogue crawler — first results appear instantly, the full index fills in behind
|
|
397
|
-
- Auto-switches families intelligently (e.g. brand searches surface brand icons even when "Classic" is selected)
|
|
398
|
-
|
|
399
|
-
> Requires the FA kit script to be present on the host page.
|
|
400
|
-
|
|
401
|
-
---
|
|
402
|
-
|
|
403
|
-
## Font Picker
|
|
404
|
-
|
|
405
|
-
```tsx
|
|
406
|
-
import { FontPicker } from '@proveanything/smartlinks-utils-ui/font-picker';
|
|
407
|
-
|
|
408
|
-
<FontPicker
|
|
409
|
-
mode="dialog"
|
|
410
|
-
value="Inter"
|
|
411
|
-
showPreview
|
|
412
|
-
onSelect={(font) => {
|
|
413
|
-
console.log(font.family); // "Inter"
|
|
414
|
-
console.log(font.cssFontFamily); // "'Inter', ui-sans-serif, system-ui, sans-serif"
|
|
415
|
-
console.log(font.loadSnippet); // <link href="..." rel="stylesheet">
|
|
416
|
-
}}
|
|
417
|
-
/>
|
|
418
|
-
|
|
419
|
-
{/* With custom fonts uploaded into the collection */}
|
|
420
|
-
<FontPicker
|
|
421
|
-
mode="dialog"
|
|
422
|
-
showCustomFonts
|
|
423
|
-
scope={{ collectionId: 'abc123' }}
|
|
424
|
-
onSelect={(font) => /* ... */}
|
|
425
|
-
/>
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
**What it does:**
|
|
429
|
-
|
|
430
|
-
- Full **Google Fonts** catalogue plus any **custom fonts** uploaded for the brand (stored via `appConfiguration` under `customFonts`)
|
|
431
|
-
- Upload zone auto-detects weight/style from the filename (e.g. `MyFont-BoldItalic.woff2`)
|
|
432
|
-
- Returns a `FontSelection` with `family`, `cssFontFamily`, and a ready-to-inject `loadSnippet` (`<link>` for Google fonts, `@font-face` CSS for custom uploads)
|
|
433
|
-
- Lazy-loads previews via `IntersectionObserver`; includes a management UI for editing custom font definitions
|
|
434
|
-
|
|
435
|
-
---
|
|
436
|
-
|
|
437
|
-
## Conditions Editor
|
|
438
|
-
|
|
439
|
-
```tsx
|
|
440
|
-
import { ConditionsEditor } from '@proveanything/smartlinks-utils-ui/conditions-editor';
|
|
441
|
-
|
|
442
|
-
<ConditionsEditor
|
|
443
|
-
value={rules}
|
|
444
|
-
onChange={setRules}
|
|
445
|
-
collectionId={collectionId} // auto-loads facet definitions
|
|
446
|
-
versions={[{ title: 'Default', value: '' }]}
|
|
447
|
-
tags={['featured', 'new']}
|
|
448
|
-
/>
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
**What it does:**
|
|
452
|
-
|
|
453
|
-
- Recursive AND / OR group builder — nest conditions to any depth
|
|
454
|
-
- **12 condition types:** Version, Country, Value, User, Date, Device, Tag, Facet, Geofence, Product, Item Status, Condition Reference
|
|
455
|
-
- **Facet condition** auto-fetches definitions from `facets.publicList(collectionId, { includeValues: true })` when only `collectionId` is passed (SDK ≥ 1.9.20)
|
|
456
|
-
- **Country picker** is a searchable multi-select with removable ISO 3166-1 chips and a "Use regions" toggle
|
|
457
|
-
- Renders correctly inside iframe contexts — avoids `overflow-hidden` so dropdowns escape their cards
|
|
458
|
-
|
|
459
|
-
> `ConditionsEditor` and `FacetRuleEditor` solve different problems.
|
|
460
|
-
> `FacetRuleEditor` is a focused AND-of-OR rule over facets only, used to
|
|
461
|
-
> target which records apply to which products. `ConditionsEditor` is the
|
|
462
|
-
> full recursive logical builder over 12 condition types, used for runtime
|
|
463
|
-
> gating, audience segmentation, and version targeting.
|
|
464
|
-
|
|
465
|
-
---
|
|
466
|
-
|
|
467
|
-
## Tree shaking
|
|
468
|
-
|
|
469
|
-
Each component has its own subpath export:
|
|
470
|
-
|
|
471
|
-
```tsx
|
|
472
|
-
// Bundles only the Asset Picker
|
|
473
|
-
import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
|
|
474
|
-
|
|
475
|
-
// Barrel import — bundler tree-shakes the rest
|
|
476
|
-
import { AssetPicker } from '@proveanything/smartlinks-utils-ui';
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
If you use subpath imports, import `styles.css` separately — subpaths do not
|
|
480
|
-
pull it in automatically.
|
|
481
|
-
|
|
482
|
-
Available subpaths:
|
|
483
|
-
|
|
484
|
-
- `/records-admin`
|
|
485
|
-
- `/facet-rule-editor`
|
|
486
|
-
- `/asset-picker`
|
|
487
|
-
- `/icon-picker`
|
|
488
|
-
- `/font-picker`
|
|
489
|
-
- `/conditions-editor`
|
|
490
|
-
- `/styles.css`
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## Relationship to the core SDK
|
|
495
|
-
|
|
496
|
-
```text
|
|
497
|
-
@proveanything/smartlinks ← data layer (records, config, interactions, …)
|
|
498
|
-
↑
|
|
499
|
-
@proveanything/smartlinks-utils-ui ← UI layer (components, hooks, admin shells)
|
|
500
|
-
↑
|
|
501
|
-
your microapp ← domain logic and custom forms
|
|
502
|
-
```
|
|
96
|
+
## Prerequisites
|
|
503
97
|
|
|
504
|
-
|
|
98
|
+
All components assume `SL.initializeApi()` has already run.
|
|
505
99
|
|
|
506
|
-
##
|
|
100
|
+
## Where to go next
|
|
507
101
|
|
|
508
|
-
-
|
|
509
|
-
- [
|
|
510
|
-
-
|
|
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)
|
package/docs/API_SUMMARY.md
CHANGED
package/docs/ui-utils.md
CHANGED
|
@@ -1,510 +1,104 @@
|
|
|
1
|
-
#
|
|
1
|
+
# UI Utils — `@proveanything/smartlinks-utils-ui`
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
> Package: `@proveanything/smartlinks-utils-ui`
|
|
9
|
-
> Tracks: `@proveanything/smartlinks ≥ 1.9` (some hooks require ≥ 1.10)
|
|
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.
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`@proveanything/smartlinks-utils-ui` sits on top of `@proveanything/smartlinks`.
|
|
16
|
-
The core SDK handles data — records, configurations, interactions. This module
|
|
17
|
-
handles **UI** — the shared React components, hooks, and admin shells that
|
|
18
|
-
translate SDK data into consistent admin interfaces.
|
|
19
|
-
|
|
20
|
-
**When do you need it?**
|
|
21
|
-
|
|
22
|
-
- You are building an admin UI for a records-based microapp (see [app-records-pattern.md](app-records-pattern.md))
|
|
23
|
-
- You need a media asset picker, icon picker, or font picker in an admin panel
|
|
24
|
-
- You need a recursive rule/conditions editor for targeting or audience logic
|
|
25
|
-
- You want the standard inheritance/override editor for scoped records
|
|
26
|
-
- You need the `useResolvedRecord` / `useCollectedRecords` hooks on the public widget side
|
|
27
|
-
|
|
28
|
-
You do **not** need it for apps that only use `appConfiguration`, basic widgets
|
|
29
|
-
without scoped data, or executor bundles.
|
|
30
|
-
|
|
31
|
-
> **Admin components are admin-only**: `RecordsAdminShell`, `AssetPicker` upload,
|
|
32
|
-
> `FacetRuleEditor`, etc. call the SDK with `admin: true`. Do not render them in
|
|
33
|
-
> public-facing views. The public-side hooks (`useResolvedRecord`,
|
|
34
|
-
> `useCollectedRecords`) are safe in widgets.
|
|
35
|
-
|
|
36
|
-
---
|
|
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)
|
|
37
12
|
|
|
38
13
|
## Install
|
|
39
14
|
|
|
40
15
|
```bash
|
|
41
16
|
npm install @proveanything/smartlinks-utils-ui
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Peer dependencies (you already have these in a SmartLinks app):
|
|
45
|
-
|
|
46
|
-
```bash
|
|
17
|
+
# Peer deps you probably already have:
|
|
47
18
|
npm install react react-dom @proveanything/smartlinks @tanstack/react-query
|
|
48
19
|
```
|
|
49
20
|
|
|
50
|
-
|
|
51
|
-
(caching, pagination, optimistic save). Wrap your app in a
|
|
52
|
-
`<QueryClientProvider>` somewhere up the tree.
|
|
53
|
-
|
|
54
|
-
Import the compiled styles **once** in your app entry (e.g. `main.tsx`):
|
|
21
|
+
Import the styles **once** in your app entry (typically `src/admin-main.tsx`):
|
|
55
22
|
|
|
56
|
-
```
|
|
23
|
+
```ts
|
|
57
24
|
import '@proveanything/smartlinks-utils-ui/styles.css';
|
|
58
25
|
```
|
|
59
26
|
|
|
60
27
|
Components inherit your shadcn-compatible CSS variables (`--primary`,
|
|
61
|
-
`--background`, `--border`, …)
|
|
28
|
+
`--background`, `--border`, …) — no extra theming required.
|
|
62
29
|
|
|
63
|
-
|
|
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`.
|
|
64
33
|
|
|
65
34
|
## What's in the box
|
|
66
35
|
|
|
67
|
-
| Module |
|
|
68
|
-
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
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:
|
|
75
50
|
|
|
76
|
-
|
|
51
|
+
```ts
|
|
52
|
+
import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
53
|
+
import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
|
|
54
|
+
```
|
|
77
55
|
|
|
78
|
-
##
|
|
56
|
+
## Minimal examples
|
|
79
57
|
|
|
80
|
-
|
|
81
|
-
JSON blobs attached to facets, products, variants, batches, the collection
|
|
82
|
-
root, or matched via facet rules — with scope inheritance built in. You
|
|
83
|
-
provide the form for one record; the shell handles everything else.
|
|
58
|
+
### Records admin (most apps need this)
|
|
84
59
|
|
|
85
60
|
```tsx
|
|
86
|
-
import {
|
|
87
|
-
RecordsAdminShell,
|
|
88
|
-
InheritanceMarker,
|
|
89
|
-
type EditorContext,
|
|
90
|
-
} from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
61
|
+
import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
91
62
|
import * as SL from '@proveanything/smartlinks';
|
|
92
63
|
|
|
93
|
-
<RecordsAdminShell
|
|
64
|
+
<RecordsAdminShell
|
|
94
65
|
SL={SL}
|
|
95
66
|
collectionId={collectionId}
|
|
96
67
|
appId={appId}
|
|
97
68
|
recordType="nutrition"
|
|
98
69
|
label="Nutrition info"
|
|
99
|
-
scopes={['
|
|
100
|
-
|
|
70
|
+
scopes={['collection', 'rule', 'product']} // canonical top-level tabs
|
|
71
|
+
// items={{ cardinality: 'list' }} // ← opt in for multi-item apps
|
|
101
72
|
defaultData={() => ({})}
|
|
102
|
-
csvSchema={{ columns: [/* ... */] }} // optional — omit to disable CSV
|
|
103
73
|
renderEditor={(ctx) => <NutritionForm ctx={ctx} />}
|
|
104
|
-
renderPreview={({ resolved }) => (
|
|
105
|
-
<pre>{JSON.stringify(resolved, null, 2)}</pre>
|
|
106
|
-
)}
|
|
107
74
|
/>
|
|
108
75
|
```
|
|
109
76
|
|
|
110
|
-
###
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
type ScopeKind = 'collection' | 'product' | 'facet' | 'variant' | 'batch' | 'rule';
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
- `'collection'` — terminal default (one record for the whole brand)
|
|
117
|
-
- `'facet'` — anchored to a facet value (e.g. `bagel-type=white`)
|
|
118
|
-
- `'product'` / `'variant'` / `'batch'` — anchored to that node
|
|
119
|
-
- `'rule'` — synthetic UI scope: records targeted via a `facetRule`
|
|
120
|
-
(AND-of-OR over facets) rather than pinned to a node. Their refs start
|
|
121
|
-
with `rule:`, and the shell renders the `<FacetRuleEditor>` inline when the
|
|
122
|
-
user opens one of these records.
|
|
123
|
-
|
|
124
|
-
### What it handles
|
|
125
|
-
|
|
126
|
-
- **Browser pane** with scope tabs (Facet / Product / Variant / Batch / Rule), search, and status filter pills
|
|
127
|
-
- **Editor pane** with sticky save / discard / delete footer, optimistic save, and an unsaved-drafts tray
|
|
128
|
-
- **Per-field `<InheritanceMarker>`** showing whether a value is the record's own or inherited from a parent scope, with one-click revert-to-inherited
|
|
129
|
-
- **Inheritance resolver** walks `proof → batch → variant → product → rule → facet → collection` server-side via SDK 1.10 `match()`
|
|
130
|
-
- **Collection-aware tabs**: calls `collection.get` and hides Variants / Batches tabs unless `collection.variants` / `collection.batches` are true — no flicker
|
|
131
|
-
- **Server-side pagination** via `useInfiniteQuery` — handles thousands of products with a "Load more" button
|
|
132
|
-
- **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
|
|
133
|
-
- **Cardinality**: `cardinality="singleton"` (one record per scope) or `"collection"` (many — FAQs, recipes, SOPs). Collection mode adds an item-list view (`itemViews: ['table' | 'cards' | 'gallery']`) and Back / prev / next nav
|
|
134
|
-
- **Multiple presentations** for the rail (`presentations: ['list' | 'compact']`) and right pane
|
|
135
|
-
- **CSV import / export** (optional — provide `csvSchema` to enable); failed rows come back as an annotated CSV
|
|
136
|
-
- **Bulk actions menu** (apply-to-many, copy-from, clear) via `bulkUpsert` / `bulkDelete`
|
|
137
|
-
- **Clipboard** — copy a record's value, paste onto another scope
|
|
138
|
-
- **Telemetry hook** (`onTelemetry`) emits typed events for save, delete, scope change, CSV import/export, bulk apply
|
|
139
|
-
- **i18n strings** fully overridable via the `i18n` prop
|
|
140
|
-
|
|
141
|
-
> Requires a `<QueryClientProvider>` from `@tanstack/react-query` somewhere up the tree.
|
|
142
|
-
|
|
143
|
-
### Lower-level pieces (advanced use)
|
|
77
|
+
### Public widget — read the resolved value
|
|
144
78
|
|
|
145
79
|
```tsx
|
|
146
|
-
import {
|
|
147
|
-
// Hooks
|
|
148
|
-
useRecordList, useRecordEditor, useResolvedRecord, useCollectedRecords,
|
|
149
|
-
useResolveAllRecords, useRulePreview, useScopeProbe,
|
|
150
|
-
// Data helpers
|
|
151
|
-
parseRef, buildRef, resolutionChain,
|
|
152
|
-
listRecords, getRecordById, createRecord, upsertRecord,
|
|
153
|
-
removeRecord, restoreRecord, matchRecords,
|
|
154
|
-
bulkUpsert, bulkDelete,
|
|
155
|
-
exportCsv, importCsv, downloadBlob,
|
|
156
|
-
// UI pieces
|
|
157
|
-
RecordBrowser, RecordEditor, ScopeBreadcrumb,
|
|
158
|
-
InheritanceMarker, ResolvedPreview, BulkActionsMenu,
|
|
159
|
-
// Drafts / unsaved state
|
|
160
|
-
DirtyDraftProvider, useDirtyDrafts, useUnsavedGuard,
|
|
161
|
-
} from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Note the names: it's `getRecordById` (not `getRecordByRef`) and `removeRecord`
|
|
165
|
-
(not `deleteRecord`). Records are addressed by UUID `id` internally; refs
|
|
166
|
-
(`product:abc123`) are only used for display / breadcrumb / URL purposes.
|
|
167
|
-
|
|
168
|
-
### `useResolvedRecord` hook
|
|
169
|
-
|
|
170
|
-
Use on the **public widget side** when the app shows one answer for the
|
|
171
|
-
current product (singleton cardinality). Walks the inheritance chain
|
|
172
|
-
server-side and returns the first match.
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
80
|
import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
176
81
|
|
|
177
|
-
const { data, source
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
appId,
|
|
181
|
-
recordType: 'nutrition',
|
|
182
|
-
collectionId,
|
|
183
|
-
productId,
|
|
184
|
-
variantId, // optional
|
|
185
|
-
batchId, // optional
|
|
186
|
-
proofId, // optional
|
|
187
|
-
// recordId, // optional — direct UUID lookup, bypasses inheritance
|
|
188
|
-
});
|
|
189
|
-
// source: 'self' | 'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'universal' | 'empty'
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
- `source` — which scope the winning record came from. `'self'` is returned
|
|
193
|
-
when you passed an explicit `recordId`; `'empty'` when nothing matched.
|
|
194
|
-
- `sourceRef` — the ref of the matched record (e.g. `product:abc123`).
|
|
195
|
-
- `recordId` — the UUID of the matched record.
|
|
196
|
-
- `facetRule` — present when the match came from a rule-targeted record.
|
|
197
|
-
|
|
198
|
-
Pass `recordId` directly when you already know the UUID (deep links, rule
|
|
199
|
-
records) — the hook will skip the inheritance walk entirely.
|
|
200
|
-
|
|
201
|
-
### `useCollectedRecords` hook
|
|
202
|
-
|
|
203
|
-
Use on the **public widget side** when the app shows many answers
|
|
204
|
-
(collection cardinality — FAQs, recipes, care tips). Returns every matching
|
|
205
|
-
record across the chain, most-specific first.
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
import { useCollectedRecords } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
209
|
-
|
|
210
|
-
const { items, isLoading, error } = useCollectedRecords<FaqEntry>({
|
|
211
|
-
SL, appId, collectionId,
|
|
212
|
-
recordType: 'faq',
|
|
213
|
-
productId,
|
|
214
|
-
// sort: { kind: 'field', field: 'order', direction: 'asc' },
|
|
215
|
-
});
|
|
216
|
-
// items: CollectedRecord<FaqEntry>[] — each has { data, scope, ref, depth }
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
`depth: 0` is the most-specific match. Default sort is by specificity
|
|
220
|
-
descending; pass `{ kind: 'field', field, direction }` to sort by a payload
|
|
221
|
-
field instead (with specificity as a stable tiebreak).
|
|
222
|
-
|
|
223
|
-
### `useResolveAllRecords` hook
|
|
224
|
-
|
|
225
|
-
When you need every record of every declared type that applies to a context —
|
|
226
|
-
rare, mostly used in executors and SEO surfaces.
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
import { useResolveAllRecords } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
230
|
-
|
|
231
|
-
const { entries, isLoading } = useResolveAllRecords({
|
|
232
|
-
SL, collectionId, appId,
|
|
233
|
-
context: { productId, facets: { brand: 'acme' } },
|
|
82
|
+
const { data, source } = useResolvedRecord({
|
|
83
|
+
SL, appId, recordType: 'nutrition',
|
|
84
|
+
collectionId, productId, variantId, batchId,
|
|
234
85
|
});
|
|
86
|
+
// source: 'proof' | 'batch' | 'variant' | 'product' | 'rule' | 'collection' | null
|
|
235
87
|
```
|
|
236
88
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"matches N products" count as the rule is edited. Debounced — safe to call
|
|
241
|
-
on every keystroke.
|
|
242
|
-
|
|
243
|
-
```ts
|
|
244
|
-
import { useRulePreview } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
245
|
-
|
|
246
|
-
const preview = useRulePreview({
|
|
247
|
-
SL, collectionId, appId,
|
|
248
|
-
rule, // FacetRule | null
|
|
249
|
-
// limit: 20,
|
|
250
|
-
// debounceMs: 350,
|
|
251
|
-
});
|
|
252
|
-
// preview: {
|
|
253
|
-
// totalMatches: number | null;
|
|
254
|
-
// sampleProductIds: string[];
|
|
255
|
-
// isLoading: boolean;
|
|
256
|
-
// isStale: boolean;
|
|
257
|
-
// error: Error | null;
|
|
258
|
-
// }
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Pass `preview` straight into `<FacetRuleEditor preview={preview} />`.
|
|
262
|
-
|
|
263
|
-
### `useScopeProbe` hook
|
|
89
|
+
For multi-item (`cardinality: 'list'`) apps, use `useResolveAllRecords` /
|
|
90
|
+
`app.records.resolveAll()` instead — it returns every applicable record, not
|
|
91
|
+
just the winner.
|
|
264
92
|
|
|
265
|
-
|
|
266
|
-
|
|
93
|
+
For everything else (props, slots, cardinality decisions, deep-link adapters,
|
|
94
|
+
lifecycle hooks, conflict handling, design tokens) → **read the package docs**.
|
|
267
95
|
|
|
268
|
-
|
|
269
|
-
import { useScopeProbe } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
270
|
-
|
|
271
|
-
const { hasVariants, hasBatches, isLoading } = useScopeProbe({
|
|
272
|
-
SL, collectionId,
|
|
273
|
-
});
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
It's a thin wrapper around `SL.collection.get(collectionId).variants /.batches`
|
|
277
|
-
(SDK ≥ 1.9). It does **not** report per-scope record status — that's handled
|
|
278
|
-
internally by the shell's status pills.
|
|
279
|
-
|
|
280
|
-
### `parseRef` / `buildRef` utilities
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
import { parseRef, buildRef } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
284
|
-
|
|
285
|
-
const parsed = parseRef('variant:prod_abc:var_500ml');
|
|
286
|
-
// → { kind: 'variant', productId: 'prod_abc', variantId: 'var_500ml', raw: '...' }
|
|
287
|
-
|
|
288
|
-
const ref = buildRef({ kind: 'product', productId: 'prod_abc', raw: '' });
|
|
289
|
-
// → 'product:prod_abc'
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
Refs are for display, breadcrumbs, and URLs. Records are addressed by UUID
|
|
293
|
-
`id` everywhere internally.
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
|
-
## Facet Rule Editor
|
|
298
|
-
|
|
299
|
-
A standalone facet-rule builder. The `<RecordsAdminShell>` embeds this
|
|
300
|
-
automatically when `'rule'` is in `scopes` and the user opens a rule-targeted
|
|
301
|
-
record — reach for it directly only when you need rule editing elsewhere
|
|
302
|
-
(e.g. a settings page, a conditions sidebar).
|
|
303
|
-
|
|
304
|
-
```tsx
|
|
305
|
-
import { FacetRuleEditor } from '@proveanything/smartlinks-utils-ui/facet-rule-editor';
|
|
306
|
-
import { useRulePreview } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
307
|
-
|
|
308
|
-
const preview = useRulePreview({ SL, collectionId, appId, rule });
|
|
309
|
-
|
|
310
|
-
<FacetRuleEditor
|
|
311
|
-
value={rule}
|
|
312
|
-
onChange={setRule}
|
|
313
|
-
collectionId={collectionId} // lazy-fetches facet definitions via SL.facets.publicList
|
|
314
|
-
preview={preview} // optional — wire from useRulePreview
|
|
315
|
-
onClear={() => setRule(null)} // optional — renders a "Remove rule" affordance
|
|
316
|
-
/>
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
Props:
|
|
320
|
-
|
|
321
|
-
- `value: FacetRule | null` / `onChange: (next: FacetRule | null) => void` — controlled
|
|
322
|
-
- `facets?: FacetOption[]` — supply directly, **or** pass `collectionId` and the editor lazy-fetches via `SL.facets.publicList`
|
|
323
|
-
- `getFacets?: (collectionId: string) => Promise<FacetOption[]>` — override the lazy-fetcher
|
|
324
|
-
- `preview?` — `{ totalMatches, sampleProductIds?, isLoading?, isStale?, error? }` (matches the `useRulePreview` return shape)
|
|
325
|
-
- `readOnly?`, `onClear?`, `title?`, `description?`, `className?`
|
|
326
|
-
|
|
327
|
-
Free-text facet entry is **not** supported — admins must pick from defined facets.
|
|
328
|
-
|
|
329
|
-
See [app-records-pattern.md §4](app-records-pattern.md#4-admin-side----recordsadminshell-the-only-thing-you-should-be-writing) for the standalone usage example.
|
|
330
|
-
|
|
331
|
-
---
|
|
332
|
-
|
|
333
|
-
## Asset Picker
|
|
334
|
-
|
|
335
|
-
```tsx
|
|
336
|
-
import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
|
|
337
|
-
|
|
338
|
-
<AssetPicker
|
|
339
|
-
scope={{ type: 'collection', collectionId: 'abc123' }}
|
|
340
|
-
mode="dialog" // or "inline"
|
|
341
|
-
allowUpload
|
|
342
|
-
accept={['image/*']} // MIME filtering
|
|
343
|
-
onSelect={(asset) => setHeroUrl(asset.url)}
|
|
344
|
-
trigger={<Button>Choose image</Button>}
|
|
345
|
-
/>
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Scope shape:
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
type AssetScope =
|
|
352
|
-
| { type: 'collection'; collectionId: string }
|
|
353
|
-
| { type: 'product'; collectionId: string; productId: string }
|
|
354
|
-
| { type: 'proof'; collectionId: string; productId: string; proofId: string };
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
**What it does:**
|
|
358
|
-
|
|
359
|
-
- Browses assets at **collection**, **product**, or **proof** scope
|
|
360
|
-
- Optional `productScope` prop adds a second tab so users can pick from product-level assets while editing at collection scope (or vice versa)
|
|
361
|
-
- Optional `appId` prop stamps every upload with the owning app and adds "This app" / "All in collection" pill tabs with provenance badges on assets owned by other apps
|
|
362
|
-
- Four ingest paths: file upload, URL import, clipboard paste (with rename preview), and selection from existing assets
|
|
363
|
-
- Inline mode for embedding in a panel; dialog mode for modal pickers
|
|
364
|
-
- Double-click a tile to confirm instantly
|
|
365
|
-
|
|
366
|
-
---
|
|
367
|
-
|
|
368
|
-
## Icon Picker
|
|
369
|
-
|
|
370
|
-
```tsx
|
|
371
|
-
import { IconPicker } from '@proveanything/smartlinks-utils-ui/icon-picker';
|
|
372
|
-
|
|
373
|
-
<IconPicker
|
|
374
|
-
mode="dialog"
|
|
375
|
-
value="fa-solid fa-heart"
|
|
376
|
-
onSelect={(icon) => setIcon(icon.name)}
|
|
377
|
-
trigger={<Button>Pick icon</Button>}
|
|
378
|
-
/>
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
`onSelect` receives:
|
|
382
|
-
|
|
383
|
-
```ts
|
|
384
|
-
interface IconSelection {
|
|
385
|
-
name: string; // full CSS class, e.g. 'fa-solid fa-heart'
|
|
386
|
-
family: 'classic' | 'duotone' | 'brands';
|
|
387
|
-
style: 'solid' | 'regular' | 'light' | null; // null for brands
|
|
388
|
-
label?: string;
|
|
389
|
-
}
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
**What it does:**
|
|
393
|
-
|
|
394
|
-
- Searches **Font Awesome 7 Pro** (uses the shared kit)
|
|
395
|
-
- Family hierarchy: Classic (Solid / Regular / Light), Duotone, and Brands
|
|
396
|
-
- Search-first with a background catalogue crawler — first results appear instantly, the full index fills in behind
|
|
397
|
-
- Auto-switches families intelligently (e.g. brand searches surface brand icons even when "Classic" is selected)
|
|
398
|
-
|
|
399
|
-
> Requires the FA kit script to be present on the host page.
|
|
400
|
-
|
|
401
|
-
---
|
|
402
|
-
|
|
403
|
-
## Font Picker
|
|
404
|
-
|
|
405
|
-
```tsx
|
|
406
|
-
import { FontPicker } from '@proveanything/smartlinks-utils-ui/font-picker';
|
|
407
|
-
|
|
408
|
-
<FontPicker
|
|
409
|
-
mode="dialog"
|
|
410
|
-
value="Inter"
|
|
411
|
-
showPreview
|
|
412
|
-
onSelect={(font) => {
|
|
413
|
-
console.log(font.family); // "Inter"
|
|
414
|
-
console.log(font.cssFontFamily); // "'Inter', ui-sans-serif, system-ui, sans-serif"
|
|
415
|
-
console.log(font.loadSnippet); // <link href="..." rel="stylesheet">
|
|
416
|
-
}}
|
|
417
|
-
/>
|
|
418
|
-
|
|
419
|
-
{/* With custom fonts uploaded into the collection */}
|
|
420
|
-
<FontPicker
|
|
421
|
-
mode="dialog"
|
|
422
|
-
showCustomFonts
|
|
423
|
-
scope={{ collectionId: 'abc123' }}
|
|
424
|
-
onSelect={(font) => /* ... */}
|
|
425
|
-
/>
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
**What it does:**
|
|
429
|
-
|
|
430
|
-
- Full **Google Fonts** catalogue plus any **custom fonts** uploaded for the brand (stored via `appConfiguration` under `customFonts`)
|
|
431
|
-
- Upload zone auto-detects weight/style from the filename (e.g. `MyFont-BoldItalic.woff2`)
|
|
432
|
-
- Returns a `FontSelection` with `family`, `cssFontFamily`, and a ready-to-inject `loadSnippet` (`<link>` for Google fonts, `@font-face` CSS for custom uploads)
|
|
433
|
-
- Lazy-loads previews via `IntersectionObserver`; includes a management UI for editing custom font definitions
|
|
434
|
-
|
|
435
|
-
---
|
|
436
|
-
|
|
437
|
-
## Conditions Editor
|
|
438
|
-
|
|
439
|
-
```tsx
|
|
440
|
-
import { ConditionsEditor } from '@proveanything/smartlinks-utils-ui/conditions-editor';
|
|
441
|
-
|
|
442
|
-
<ConditionsEditor
|
|
443
|
-
value={rules}
|
|
444
|
-
onChange={setRules}
|
|
445
|
-
collectionId={collectionId} // auto-loads facet definitions
|
|
446
|
-
versions={[{ title: 'Default', value: '' }]}
|
|
447
|
-
tags={['featured', 'new']}
|
|
448
|
-
/>
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
**What it does:**
|
|
452
|
-
|
|
453
|
-
- Recursive AND / OR group builder — nest conditions to any depth
|
|
454
|
-
- **12 condition types:** Version, Country, Value, User, Date, Device, Tag, Facet, Geofence, Product, Item Status, Condition Reference
|
|
455
|
-
- **Facet condition** auto-fetches definitions from `facets.publicList(collectionId, { includeValues: true })` when only `collectionId` is passed (SDK ≥ 1.9.20)
|
|
456
|
-
- **Country picker** is a searchable multi-select with removable ISO 3166-1 chips and a "Use regions" toggle
|
|
457
|
-
- Renders correctly inside iframe contexts — avoids `overflow-hidden` so dropdowns escape their cards
|
|
458
|
-
|
|
459
|
-
> `ConditionsEditor` and `FacetRuleEditor` solve different problems.
|
|
460
|
-
> `FacetRuleEditor` is a focused AND-of-OR rule over facets only, used to
|
|
461
|
-
> target which records apply to which products. `ConditionsEditor` is the
|
|
462
|
-
> full recursive logical builder over 12 condition types, used for runtime
|
|
463
|
-
> gating, audience segmentation, and version targeting.
|
|
464
|
-
|
|
465
|
-
---
|
|
466
|
-
|
|
467
|
-
## Tree shaking
|
|
468
|
-
|
|
469
|
-
Each component has its own subpath export:
|
|
470
|
-
|
|
471
|
-
```tsx
|
|
472
|
-
// Bundles only the Asset Picker
|
|
473
|
-
import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
|
|
474
|
-
|
|
475
|
-
// Barrel import — bundler tree-shakes the rest
|
|
476
|
-
import { AssetPicker } from '@proveanything/smartlinks-utils-ui';
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
If you use subpath imports, import `styles.css` separately — subpaths do not
|
|
480
|
-
pull it in automatically.
|
|
481
|
-
|
|
482
|
-
Available subpaths:
|
|
483
|
-
|
|
484
|
-
- `/records-admin`
|
|
485
|
-
- `/facet-rule-editor`
|
|
486
|
-
- `/asset-picker`
|
|
487
|
-
- `/icon-picker`
|
|
488
|
-
- `/font-picker`
|
|
489
|
-
- `/conditions-editor`
|
|
490
|
-
- `/styles.css`
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## Relationship to the core SDK
|
|
495
|
-
|
|
496
|
-
```text
|
|
497
|
-
@proveanything/smartlinks ← data layer (records, config, interactions, …)
|
|
498
|
-
↑
|
|
499
|
-
@proveanything/smartlinks-utils-ui ← UI layer (components, hooks, admin shells)
|
|
500
|
-
↑
|
|
501
|
-
your microapp ← domain logic and custom forms
|
|
502
|
-
```
|
|
96
|
+
## Prerequisites
|
|
503
97
|
|
|
504
|
-
|
|
98
|
+
All components assume `SL.initializeApi()` has already run.
|
|
505
99
|
|
|
506
|
-
##
|
|
100
|
+
## Where to go next
|
|
507
101
|
|
|
508
|
-
-
|
|
509
|
-
- [
|
|
510
|
-
-
|
|
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)
|