@proveanything/smartlinks 1.11.1 → 1.11.2

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,15 +1,21 @@
1
1
  # SmartLinks UI Utils (`@proveanything/smartlinks-utils-ui`)
2
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.
3
+ > Companion React component library for the SmartLinks SDK. Ships the heavy,
4
+ > opinionated admin UI pieces that almost every SmartLinks microapp ends up
5
+ > needing — built once, theme-able, tree-shakeable, and wired straight into
6
+ > the SmartLinks SDK.
4
7
  >
5
- > Package: `@proveanything/smartlinks-utils-ui`
6
- > Tracks: `@proveanything/smartlinks ≥ 1.9`
8
+ > Package: `@proveanything/smartlinks-utils-ui`
9
+ > Tracks: `@proveanything/smartlinks ≥ 1.9` (some hooks require ≥ 1.10)
7
10
 
8
11
  ---
9
12
 
10
13
  ## What is this module for?
11
14
 
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.
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.
13
19
 
14
20
  **When do you need it?**
15
21
 
@@ -17,11 +23,15 @@
17
23
  - You need a media asset picker, icon picker, or font picker in an admin panel
18
24
  - You need a recursive rule/conditions editor for targeting or audience logic
19
25
  - You want the standard inheritance/override editor for scoped records
20
- - You need the `useResolvedRecord` hook on the public widget side
26
+ - You need the `useResolvedRecord` / `useCollectedRecords` hooks on the public widget side
21
27
 
22
- You do **not** need it for apps that only use `appConfiguration`, basic widgets without scoped data, or executor bundles.
28
+ You do **not** need it for apps that only use `appConfiguration`, basic widgets
29
+ without scoped data, or executor bundles.
23
30
 
24
- > **Admin-only**: all components call the SDK with `admin: true`. Do not render them in public-facing views.
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.
25
35
 
26
36
  ---
27
37
 
@@ -34,18 +44,21 @@ npm install @proveanything/smartlinks-utils-ui
34
44
  Peer dependencies (you already have these in a SmartLinks app):
35
45
 
36
46
  ```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
47
+ npm install react react-dom @proveanything/smartlinks @tanstack/react-query
40
48
  ```
41
49
 
50
+ `@tanstack/react-query` is required by every hook and by `RecordsAdminShell`
51
+ (caching, pagination, optimistic save). Wrap your app in a
52
+ `<QueryClientProvider>` somewhere up the tree.
53
+
42
54
  Import the compiled styles **once** in your app entry (e.g. `main.tsx`):
43
55
 
44
56
  ```tsx
45
57
  import '@proveanything/smartlinks-utils-ui/styles.css';
46
58
  ```
47
59
 
48
- Components inherit your shadcn-compatible CSS variables (`--primary`, `--background`, `--border`, …) so they pick up your theme automatically.
60
+ Components inherit your shadcn-compatible CSS variables (`--primary`,
61
+ `--background`, `--border`, …) so they pick up your theme automatically.
49
62
 
50
63
  ---
51
64
 
@@ -53,7 +66,8 @@ Components inherit your shadcn-compatible CSS variables (`--primary`, `--backgro
53
66
 
54
67
  | Module | What it is | When to reach for it |
55
68
  |--------|------------|----------------------|
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 |
69
+ | [Records Admin Shell](#records-admin-shell) | Full admin UI for `app.records` with scope inheritance, cardinality, and rule editing | Per-product / per-variant / per-batch / per-facet config tools |
70
+ | [FacetRuleEditor](#facet-rule-editor) | Standalone facet-rule builder with live preview | When you need rule editing outside the admin shell |
57
71
  | [Asset Picker](#asset-picker) | Browse / upload / paste / URL-import images and files | Any time the admin needs to pick or upload media |
58
72
  | [Icon Picker](#icon-picker) | Searchable Font Awesome 7 Pro picker | Configurable buttons, badges, menus, tiles |
59
73
  | [Font Picker](#font-picker) | Google Fonts + custom uploaded fonts | Theme editors, brand customisation panels |
@@ -63,7 +77,10 @@ Components inherit your shadcn-compatible CSS variables (`--primary`, `--backgro
63
77
 
64
78
  ## Records Admin Shell
65
79
 
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.
80
+ The primary export. A complete admin UI for managing `app.records` — typed
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.
67
84
 
68
85
  ```tsx
69
86
  import {
@@ -82,25 +99,44 @@ import * as SL from '@proveanything/smartlinks';
82
99
  scopes={['facet', 'product', 'variant', 'batch']}
83
100
  contextScope={{ productId, variantId, batchId }} // from iframe URL — optional
84
101
  defaultData={() => ({})}
85
- csvSchema={{ columns: [/* ... */] }} // optional — omit to disable CSV import/export
102
+ csvSchema={{ columns: [/* ... */] }} // optional — omit to disable CSV
86
103
  renderEditor={(ctx) => <NutritionForm ctx={ctx} />}
87
- renderPreview={({ resolved }) => <pre>{JSON.stringify(resolved, null, 2)}</pre>}
104
+ renderPreview={({ resolved }) => (
105
+ <pre>{JSON.stringify(resolved, null, 2)}</pre>
106
+ )}
88
107
  />
89
108
  ```
90
109
 
91
- **What it handles:**
110
+ ### Valid `ScopeKind` values
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.
92
123
 
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
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
95
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
96
- - **Inheritance resolver** walks `batch → variant → product → facet` and returns both the resolved and parent values
129
+ - **Inheritance resolver** walks `proof → batch → variant → product → rule → facet collection` server-side via SDK 1.10 `match()`
97
130
  - **Collection-aware tabs**: calls `collection.get` and hides Variants / Batches tabs unless `collection.variants` / `collection.batches` are true — no flicker
98
131
  - **Server-side pagination** via `useInfiniteQuery` — handles thousands of products with a "Load more" button
99
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
100
135
  - **CSV import / export** (optional — provide `csvSchema` to enable); failed rows come back as an annotated CSV
101
136
  - **Bulk actions menu** (apply-to-many, copy-from, clear) via `bulkUpsert` / `bulkDelete`
137
+ - **Clipboard** — copy a record's value, paste onto another scope
102
138
  - **Telemetry hook** (`onTelemetry`) emits typed events for save, delete, scope change, CSV import/export, bulk apply
103
- - **i18n strings** fully overridable
139
+ - **i18n strings** fully overridable via the `i18n` prop
104
140
 
105
141
  > Requires a `<QueryClientProvider>` from `@tanstack/react-query` somewhere up the tree.
106
142
 
@@ -109,37 +145,137 @@ import * as SL from '@proveanything/smartlinks';
109
145
  ```tsx
110
146
  import {
111
147
  // Hooks
112
- useRecordList, useRecordEditor, useResolvedRecord, useScopeProbe,
148
+ useRecordList, useRecordEditor, useResolvedRecord, useCollectedRecords,
149
+ useResolveAllRecords, useRulePreview, useScopeProbe,
113
150
  // Data helpers
114
151
  parseRef, buildRef, resolutionChain,
115
- listRecords, getRecordByRef, upsertRecord, deleteRecord,
152
+ listRecords, getRecordById, createRecord, upsertRecord,
153
+ removeRecord, restoreRecord, matchRecords,
116
154
  bulkUpsert, bulkDelete,
117
155
  exportCsv, importCsv, downloadBlob,
118
156
  // UI pieces
119
157
  RecordBrowser, RecordEditor, ScopeBreadcrumb,
120
158
  InheritanceMarker, ResolvedPreview, BulkActionsMenu,
159
+ // Drafts / unsaved state
160
+ DirtyDraftProvider, useDirtyDrafts, useUnsavedGuard,
121
161
  } from '@proveanything/smartlinks-utils-ui/records-admin';
122
162
  ```
123
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
+
124
168
  ### `useResolvedRecord` hook
125
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
+
126
174
  ```ts
127
175
  import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
128
176
 
129
- const { data, source, isLoading } = useResolvedRecord({
130
- SL,
131
- appId,
132
- recordType: 'nutrition',
133
- collectionId,
177
+ const { data, source, sourceRef, recordId, facetRule, isLoading, error } =
178
+ useResolvedRecord<NutritionData>({
179
+ SL,
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',
134
213
  productId,
135
- variantId, // optional
136
- batchId, // optional
137
- proofId, // optional
214
+ // sort: { kind: 'field', field: 'order', direction: 'asc' },
138
215
  });
139
- // source: 'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'universal' | null
216
+ // items: CollectedRecord<FaqEntry>[] each has { data, scope, ref, depth }
140
217
  ```
141
218
 
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.
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' } },
234
+ });
235
+ ```
236
+
237
+ ### `useRulePreview` hook
238
+
239
+ Wire into `<FacetRuleEditor>` (or any custom rule UI) to show a live
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
264
+
265
+ Reports whether a collection has variants/batches enabled, so the shell
266
+ (or your own UI) can hide the corresponding tabs without flicker.
267
+
268
+ ```ts
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.
143
279
 
144
280
  ### `parseRef` / `buildRef` utilities
145
281
 
@@ -147,13 +283,50 @@ Walks the resolution chain defined in [records-admin-pattern.md §4](records-adm
147
283
  import { parseRef, buildRef } from '@proveanything/smartlinks-utils-ui/records-admin';
148
284
 
149
285
  const parsed = parseRef('variant:prod_abc:var_500ml');
150
- // → { kind: 'variant', productId: 'prod_abc', variantId: 'var_500ml' }
286
+ // → { kind: 'variant', productId: 'prod_abc', variantId: 'var_500ml', raw: '...' }
151
287
 
152
- const ref = buildRef({ kind: 'facet', facetKey: 'bread_type', valueKey: 'sourdough' });
153
- // → 'facet:bread_type:sourdough'
288
+ const ref = buildRef({ kind: 'product', productId: 'prod_abc', raw: '' });
289
+ // → 'product:prod_abc'
154
290
  ```
155
291
 
156
- See [records-admin-pattern.md Appendix A](records-admin-pattern.md#appendix-a--ref-parser-reference) for the full `ParsedRef` type.
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 [records-admin-pattern.md §4](records-admin-pattern.md#4-admin-side----recordsadminshell-the-only-thing-you-should-be-writing) for the standalone usage example.
157
330
 
158
331
  ---
159
332
 
@@ -172,8 +345,20 @@ import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
172
345
  />
173
346
  ```
174
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
+
175
357
  **What it does:**
176
- - Browses assets at **collection** or **product** scope (dual-scope tabs)
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
177
362
  - Four ingest paths: file upload, URL import, clipboard paste (with rename preview), and selection from existing assets
178
363
  - Inline mode for embedding in a panel; dialog mode for modal pickers
179
364
  - Double-click a tile to confirm instantly
@@ -193,8 +378,20 @@ import { IconPicker } from '@proveanything/smartlinks-utils-ui/icon-picker';
193
378
  />
194
379
  ```
195
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
+
196
392
  **What it does:**
197
- - Searches **Font Awesome 7 Pro** (uses the shared kit `75493b59b3`)
393
+
394
+ - Searches **Font Awesome 7 Pro** (uses the shared kit)
198
395
  - Family hierarchy: Classic (Solid / Regular / Light), Duotone, and Brands
199
396
  - Search-first with a background catalogue crawler — first results appear instantly, the full index fills in behind
200
397
  - Auto-switches families intelligently (e.g. brand searches surface brand icons even when "Classic" is selected)
@@ -229,6 +426,7 @@ import { FontPicker } from '@proveanything/smartlinks-utils-ui/font-picker';
229
426
  ```
230
427
 
231
428
  **What it does:**
429
+
232
430
  - Full **Google Fonts** catalogue plus any **custom fonts** uploaded for the brand (stored via `appConfiguration` under `customFonts`)
233
431
  - Upload zone auto-detects weight/style from the filename (e.g. `MyFont-BoldItalic.woff2`)
234
432
  - Returns a `FontSelection` with `family`, `cssFontFamily`, and a ready-to-inject `loadSnippet` (`<link>` for Google fonts, `@font-face` CSS for custom uploads)
@@ -251,12 +449,19 @@ import { ConditionsEditor } from '@proveanything/smartlinks-utils-ui/conditions-
251
449
  ```
252
450
 
253
451
  **What it does:**
452
+
254
453
  - Recursive AND / OR group builder — nest conditions to any depth
255
454
  - **12 condition types:** Version, Country, Value, User, Date, Device, Tag, Facet, Geofence, Product, Item Status, Condition Reference
256
455
  - **Facet condition** auto-fetches definitions from `facets.publicList(collectionId, { includeValues: true })` when only `collectionId` is passed (SDK ≥ 1.9.20)
257
456
  - **Country picker** is a searchable multi-select with removable ISO 3166-1 chips and a "Use regions" toggle
258
457
  - Renders correctly inside iframe contexts — avoids `overflow-hidden` so dropdowns escape their cards
259
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
+
260
465
  ---
261
466
 
262
467
  ## Tree shaking
@@ -271,13 +476,24 @@ import { AssetPicker } from '@proveanything/smartlinks-utils-ui/asset-picker';
271
476
  import { AssetPicker } from '@proveanything/smartlinks-utils-ui';
272
477
  ```
273
478
 
274
- If you use subpath imports, import `styles.css` separately — subpaths do not pull it in automatically.
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`
275
491
 
276
492
  ---
277
493
 
278
494
  ## Relationship to the core SDK
279
495
 
280
- ```
496
+ ```text
281
497
  @proveanything/smartlinks ← data layer (records, config, interactions, …)
282
498
 
283
499
  @proveanything/smartlinks-utils-ui ← UI layer (components, hooks, admin shells)
@@ -291,4 +507,4 @@ your microapp ← domain logic and custom forms
291
507
 
292
508
  - [records-admin-pattern.md](records-admin-pattern.md) — the data contract that `RecordsAdminShell` implements
293
509
  - [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
510
+ - [app-manifest.md](app-manifest.md) — the `records` manifest block that drives tab generation
package/dist/index.d.ts CHANGED
@@ -23,3 +23,5 @@ export type { Collection, CollectionResponse, CollectionCreateRequest, Collectio
23
23
  export type { Proof, ProofResponse, ProofCreateRequest, ProofUpdateRequest, ProofClaimRequest, } from "./types/proof";
24
24
  export type { QrShortCodeLookupResponse, } from "./types/qr";
25
25
  export type { ReverseTagLookupParams, ReverseTagLookupResponse, } from "./types/tags";
26
+ export type { AdminMobileCapability, AdminMobileHostId, AdminMobileEvent, ScannerEventSubscriber, AdminMobileHostContext, MobileAdminComponentManifest, MobileAdminBundleManifest, } from './mobile-admin/types';
27
+ export { HostCapabilityUnavailableError, HostPermissionDeniedError, HostTimeoutError, } from './mobile-admin/errors';
package/dist/index.js CHANGED
@@ -11,3 +11,4 @@ export { cache_1 as cache };
11
11
  export { IframeResponder, isAdminFromRoles, buildIframeSrc, } from './iframeResponder';
12
12
  import * as utils_1 from './utils';
13
13
  export { utils_1 as utils };
14
+ export { HostCapabilityUnavailableError, HostPermissionDeniedError, HostTimeoutError, } from './mobile-admin/errors';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Structured errors thrown by `AdminMobileHostContext.actions`.
3
+ * Catch these by name or with `instanceof` to give users actionable feedback.
4
+ *
5
+ * All three classes call `Object.setPrototypeOf(this, new.target.prototype)` so
6
+ * `instanceof` works correctly when transpiled to ES5.
7
+ */
8
+ /**
9
+ * Thrown when a container requests a hardware action that the current host
10
+ * does not support (e.g. calling `requestNfcTap` on a `'pwa'` host).
11
+ *
12
+ * @example
13
+ * try {
14
+ * const uid = await host.actions.requestNfcTap();
15
+ * } catch (err) {
16
+ * if (err instanceof HostCapabilityUnavailableError) {
17
+ * host.ui.toast?.({ title: `NFC not available on this device`, variant: 'destructive' });
18
+ * }
19
+ * }
20
+ */
21
+ export declare class HostCapabilityUnavailableError extends Error {
22
+ /** The capability that was requested but is unavailable. */
23
+ capability: 'nfc' | 'rfid' | 'qr' | 'camera';
24
+ /** `AdminMobileHostId` string of the host that rejected the request. */
25
+ host: string;
26
+ constructor(capability: HostCapabilityUnavailableError['capability'], host: string);
27
+ }
28
+ /**
29
+ * Thrown when the user denies a runtime permission request (e.g. camera or
30
+ * NFC access) during a host action.
31
+ *
32
+ * @example
33
+ * try {
34
+ * const photo = await host.actions.requestCameraPhoto();
35
+ * } catch (err) {
36
+ * if (err instanceof HostPermissionDeniedError) {
37
+ * host.ui.toast?.({ title: 'Camera permission denied', variant: 'destructive' });
38
+ * }
39
+ * }
40
+ */
41
+ export declare class HostPermissionDeniedError extends Error {
42
+ /** The capability for which permission was denied. */
43
+ capability: 'nfc' | 'rfid' | 'qr' | 'camera';
44
+ constructor(capability: HostPermissionDeniedError['capability']);
45
+ }
46
+ /**
47
+ * Thrown when a time-bounded host action (NFC tap, QR scan) exceeds its
48
+ * allowed duration.
49
+ *
50
+ * @example
51
+ * try {
52
+ * const { uid } = await host.actions.requestNfcTap(5000);
53
+ * } catch (err) {
54
+ * if (err instanceof HostTimeoutError) {
55
+ * console.warn(`Timed out after ${err.timeoutMs}ms`);
56
+ * }
57
+ * }
58
+ */
59
+ export declare class HostTimeoutError extends Error {
60
+ /** The capability that timed out. */
61
+ capability: 'nfc' | 'qr';
62
+ /** The timeout threshold in milliseconds. */
63
+ timeoutMs: number;
64
+ constructor(capability: HostTimeoutError['capability'], timeoutMs: number);
65
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Structured errors thrown by `AdminMobileHostContext.actions`.
3
+ * Catch these by name or with `instanceof` to give users actionable feedback.
4
+ *
5
+ * All three classes call `Object.setPrototypeOf(this, new.target.prototype)` so
6
+ * `instanceof` works correctly when transpiled to ES5.
7
+ */
8
+ /**
9
+ * Thrown when a container requests a hardware action that the current host
10
+ * does not support (e.g. calling `requestNfcTap` on a `'pwa'` host).
11
+ *
12
+ * @example
13
+ * try {
14
+ * const uid = await host.actions.requestNfcTap();
15
+ * } catch (err) {
16
+ * if (err instanceof HostCapabilityUnavailableError) {
17
+ * host.ui.toast?.({ title: `NFC not available on this device`, variant: 'destructive' });
18
+ * }
19
+ * }
20
+ */
21
+ export class HostCapabilityUnavailableError extends Error {
22
+ constructor(capability, host) {
23
+ super(`Capability '${capability}' is unavailable on host '${host}'`);
24
+ this.name = 'HostCapabilityUnavailableError';
25
+ this.capability = capability;
26
+ this.host = host;
27
+ Object.setPrototypeOf(this, new.target.prototype);
28
+ }
29
+ }
30
+ /**
31
+ * Thrown when the user denies a runtime permission request (e.g. camera or
32
+ * NFC access) during a host action.
33
+ *
34
+ * @example
35
+ * try {
36
+ * const photo = await host.actions.requestCameraPhoto();
37
+ * } catch (err) {
38
+ * if (err instanceof HostPermissionDeniedError) {
39
+ * host.ui.toast?.({ title: 'Camera permission denied', variant: 'destructive' });
40
+ * }
41
+ * }
42
+ */
43
+ export class HostPermissionDeniedError extends Error {
44
+ constructor(capability) {
45
+ super(`Permission denied for capability '${capability}'`);
46
+ this.name = 'HostPermissionDeniedError';
47
+ this.capability = capability;
48
+ Object.setPrototypeOf(this, new.target.prototype);
49
+ }
50
+ }
51
+ /**
52
+ * Thrown when a time-bounded host action (NFC tap, QR scan) exceeds its
53
+ * allowed duration.
54
+ *
55
+ * @example
56
+ * try {
57
+ * const { uid } = await host.actions.requestNfcTap(5000);
58
+ * } catch (err) {
59
+ * if (err instanceof HostTimeoutError) {
60
+ * console.warn(`Timed out after ${err.timeoutMs}ms`);
61
+ * }
62
+ * }
63
+ */
64
+ export class HostTimeoutError extends Error {
65
+ constructor(capability, timeoutMs) {
66
+ super(`Capability '${capability}' timed out after ${timeoutMs}ms`);
67
+ this.name = 'HostTimeoutError';
68
+ this.capability = capability;
69
+ this.timeoutMs = timeoutMs;
70
+ Object.setPrototypeOf(this, new.target.prototype);
71
+ }
72
+ }