@proveanything/smartlinks 1.8.3 → 1.8.5
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/api/appConfiguration.d.ts +34 -0
- package/dist/api/appConfiguration.js +80 -0
- package/dist/docs/API_SUMMARY.md +48 -5
- package/dist/docs/app-manifest.md +32 -0
- package/dist/docs/deep-link-discovery.md +28 -0
- package/dist/docs/manifests.md +5 -1
- package/dist/docs/widgets.md +135 -0
- package/dist/http.js +50 -3
- package/dist/openapi.yaml +61 -5
- package/dist/types/appConfiguration.d.ts +24 -0
- package/dist/types/appManifest.d.ts +10 -4
- package/docs/API_SUMMARY.md +48 -5
- package/docs/app-manifest.md +32 -0
- package/docs/deep-link-discovery.md +28 -0
- package/docs/manifests.md +5 -1
- package/docs/widgets.md +135 -0
- package/openapi.yaml +61 -5
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CollectionWidgetsResponse, GetCollectionWidgetsOptions } from "../types/appManifest";
|
|
2
|
+
import type { GetWidgetInstanceOptions, WidgetInstance, WidgetInstanceSummary } from "../types/appConfiguration";
|
|
2
3
|
/**
|
|
3
4
|
* Options for collection/product-scoped app configuration.
|
|
4
5
|
* This data is set by admins and applies to all users within the scope.
|
|
@@ -193,6 +194,39 @@ export declare namespace appConfiguration {
|
|
|
193
194
|
* ```
|
|
194
195
|
*/
|
|
195
196
|
function getConfig(opts: AppConfigOptions): Promise<any>;
|
|
197
|
+
/**
|
|
198
|
+
* Resolve a configured widget instance by ID from an app's stored config.
|
|
199
|
+
* This is a thin convenience wrapper over `getConfig()` that reads `config.widgets[widgetId]`.
|
|
200
|
+
*
|
|
201
|
+
* @param opts - Scope options plus the widget instance ID
|
|
202
|
+
* @returns The configured widget instance
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const widget = await appConfiguration.getWidgetInstance({
|
|
207
|
+
* collectionId: 'my-collection',
|
|
208
|
+
* appId: 'widget-toolkit',
|
|
209
|
+
* widgetId: 'launch-countdown'
|
|
210
|
+
* })
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
function getWidgetInstance<TWidget = any>(opts: GetWidgetInstanceOptions): Promise<WidgetInstance<TWidget>>;
|
|
214
|
+
/**
|
|
215
|
+
* List configured widget instances for an app.
|
|
216
|
+
* Useful for picker UIs, setup schemas, and widget-to-widget references.
|
|
217
|
+
*
|
|
218
|
+
* @param opts - App config scope options
|
|
219
|
+
* @returns Array of widget instance summaries
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* const widgets = await appConfiguration.listWidgetInstances({
|
|
224
|
+
* collectionId: 'my-collection',
|
|
225
|
+
* appId: 'widget-toolkit'
|
|
226
|
+
* })
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
function listWidgetInstances(opts: Omit<GetWidgetInstanceOptions, 'widgetId'>): Promise<WidgetInstanceSummary[]>;
|
|
196
230
|
/**
|
|
197
231
|
* Set app configuration for a collection/product scope.
|
|
198
232
|
* Requires admin authentication.
|
|
@@ -1,5 +1,24 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
1
12
|
// src/api/appConfiguration.ts
|
|
2
13
|
import { request, post, del } from "../http";
|
|
14
|
+
function getWidgetsMap(config) {
|
|
15
|
+
if (!config || typeof config !== 'object' || Array.isArray(config))
|
|
16
|
+
return {};
|
|
17
|
+
const widgets = config.widgets;
|
|
18
|
+
if (!widgets || typeof widgets !== 'object' || Array.isArray(widgets))
|
|
19
|
+
return {};
|
|
20
|
+
return widgets;
|
|
21
|
+
}
|
|
3
22
|
function buildAppPath(opts, type) {
|
|
4
23
|
const base = opts.admin ? "admin" : "public";
|
|
5
24
|
let path = `/${base}`;
|
|
@@ -228,6 +247,67 @@ export var appConfiguration;
|
|
|
228
247
|
return request(path);
|
|
229
248
|
}
|
|
230
249
|
appConfiguration.getConfig = getConfig;
|
|
250
|
+
/**
|
|
251
|
+
* Resolve a configured widget instance by ID from an app's stored config.
|
|
252
|
+
* This is a thin convenience wrapper over `getConfig()` that reads `config.widgets[widgetId]`.
|
|
253
|
+
*
|
|
254
|
+
* @param opts - Scope options plus the widget instance ID
|
|
255
|
+
* @returns The configured widget instance
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* const widget = await appConfiguration.getWidgetInstance({
|
|
260
|
+
* collectionId: 'my-collection',
|
|
261
|
+
* appId: 'widget-toolkit',
|
|
262
|
+
* widgetId: 'launch-countdown'
|
|
263
|
+
* })
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
async function getWidgetInstance(opts) {
|
|
267
|
+
const { widgetId } = opts, configOpts = __rest(opts, ["widgetId"]);
|
|
268
|
+
const config = await getConfig(configOpts);
|
|
269
|
+
const widgets = getWidgetsMap(config);
|
|
270
|
+
const instance = widgets[widgetId];
|
|
271
|
+
if (!instance || typeof instance !== 'object' || Array.isArray(instance)) {
|
|
272
|
+
throw new Error(`Widget instance \"${widgetId}\" not found for app \"${opts.appId}\"`);
|
|
273
|
+
}
|
|
274
|
+
return instance;
|
|
275
|
+
}
|
|
276
|
+
appConfiguration.getWidgetInstance = getWidgetInstance;
|
|
277
|
+
/**
|
|
278
|
+
* List configured widget instances for an app.
|
|
279
|
+
* Useful for picker UIs, setup schemas, and widget-to-widget references.
|
|
280
|
+
*
|
|
281
|
+
* @param opts - App config scope options
|
|
282
|
+
* @returns Array of widget instance summaries
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```typescript
|
|
286
|
+
* const widgets = await appConfiguration.listWidgetInstances({
|
|
287
|
+
* collectionId: 'my-collection',
|
|
288
|
+
* appId: 'widget-toolkit'
|
|
289
|
+
* })
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
async function listWidgetInstances(opts) {
|
|
293
|
+
const config = await getConfig(opts);
|
|
294
|
+
const widgets = getWidgetsMap(config);
|
|
295
|
+
return Object.entries(widgets).map(([id, instance]) => {
|
|
296
|
+
var _a;
|
|
297
|
+
const widgetInstance = instance && typeof instance === 'object' && !Array.isArray(instance)
|
|
298
|
+
? instance
|
|
299
|
+
: {};
|
|
300
|
+
const resolvedId = typeof widgetInstance.id === 'string' && widgetInstance.id.trim()
|
|
301
|
+
? widgetInstance.id
|
|
302
|
+
: id;
|
|
303
|
+
return Object.assign(Object.assign({}, widgetInstance), { id: resolvedId, name: typeof widgetInstance.name === 'string' && widgetInstance.name.trim()
|
|
304
|
+
? widgetInstance.name
|
|
305
|
+
: resolvedId, type: typeof ((_a = widgetInstance.widget) === null || _a === void 0 ? void 0 : _a.type) === 'string'
|
|
306
|
+
? widgetInstance.widget.type
|
|
307
|
+
: undefined });
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
appConfiguration.listWidgetInstances = listWidgetInstances;
|
|
231
311
|
/**
|
|
232
312
|
* Set app configuration for a collection/product scope.
|
|
233
313
|
* Requires admin authentication.
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.5 | Generated: 2026-03-14T14:43:55.897Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -1283,6 +1283,39 @@ interface AppConfigurationResponse {
|
|
|
1283
1283
|
}
|
|
1284
1284
|
```
|
|
1285
1285
|
|
|
1286
|
+
**GetWidgetInstanceOptions** (interface)
|
|
1287
|
+
```typescript
|
|
1288
|
+
interface GetWidgetInstanceOptions {
|
|
1289
|
+
appId: string
|
|
1290
|
+
collectionId?: string
|
|
1291
|
+
productId?: string
|
|
1292
|
+
variantId?: string
|
|
1293
|
+
batchId?: string
|
|
1294
|
+
admin?: boolean
|
|
1295
|
+
widgetId: string
|
|
1296
|
+
}
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
**WidgetInstance<TWidget = any>** (interface)
|
|
1300
|
+
```typescript
|
|
1301
|
+
interface WidgetInstance<TWidget = any> {
|
|
1302
|
+
id: string
|
|
1303
|
+
name?: string
|
|
1304
|
+
widget?: TWidget
|
|
1305
|
+
[key: string]: any
|
|
1306
|
+
}
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
**WidgetInstanceSummary** (interface)
|
|
1310
|
+
```typescript
|
|
1311
|
+
interface WidgetInstanceSummary {
|
|
1312
|
+
id: string
|
|
1313
|
+
name: string
|
|
1314
|
+
type?: string
|
|
1315
|
+
[key: string]: any
|
|
1316
|
+
}
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1286
1319
|
### appManifest
|
|
1287
1320
|
|
|
1288
1321
|
**AppBundle** (interface)
|
|
@@ -1324,6 +1357,16 @@ interface AppWidgetComponent {
|
|
|
1324
1357
|
}
|
|
1325
1358
|
```
|
|
1326
1359
|
|
|
1360
|
+
**AppManifestWidgets** (interface)
|
|
1361
|
+
```typescript
|
|
1362
|
+
interface AppManifestWidgets {
|
|
1363
|
+
files: AppManifestFiles;
|
|
1364
|
+
components: AppWidgetComponent[];
|
|
1365
|
+
instanceResolution?: boolean;
|
|
1366
|
+
instanceParam?: string;
|
|
1367
|
+
}
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1327
1370
|
**AppContainerComponent** (interface)
|
|
1328
1371
|
```typescript
|
|
1329
1372
|
interface AppContainerComponent {
|
|
@@ -1525,10 +1568,7 @@ interface AppManifest {
|
|
|
1525
1568
|
* (setup questions, import schema, tunable fields, metrics definitions).
|
|
1526
1569
|
* Absent when the app has no admin UI.
|
|
1527
1570
|
admin?: string;
|
|
1528
|
-
widgets?:
|
|
1529
|
-
files: AppManifestFiles;
|
|
1530
|
-
components: AppWidgetComponent[];
|
|
1531
|
-
};
|
|
1571
|
+
widgets?: AppManifestWidgets;
|
|
1532
1572
|
containers?: {
|
|
1533
1573
|
files: AppManifestFiles;
|
|
1534
1574
|
components: AppContainerComponent[];
|
|
@@ -6016,6 +6056,9 @@ Get related threads and records for a case (admin only) GET /cases/:caseId/relat
|
|
|
6016
6056
|
**getConfig**(opts: AppConfigOptions) → `Promise<any>`
|
|
6017
6057
|
Get app configuration for a collection/product scope. ```typescript const config = await appConfiguration.getConfig({ appId: 'warranty-portal', collectionId: 'my-collection' }); ```
|
|
6018
6058
|
|
|
6059
|
+
**listWidgetInstances**(opts: Omit<GetWidgetInstanceOptions, 'widgetId'>) → `Promise<WidgetInstanceSummary[]>`
|
|
6060
|
+
List configured widget instances for an app. Useful for picker UIs, setup schemas, and widget-to-widget references. ```typescript const widgets = await appConfiguration.listWidgetInstances({ collectionId: 'my-collection', appId: 'widget-toolkit' }) ```
|
|
6061
|
+
|
|
6019
6062
|
**setConfig**(opts: AppConfigOptions) → `Promise<any>`
|
|
6020
6063
|
Set app configuration for a collection/product scope. Requires admin authentication. ```typescript await appConfiguration.setConfig({ appId: 'warranty-portal', collectionId: 'my-collection', admin: true, config: { warrantyPeriod: 24, supportEmail: 'support@example.com' } }); ```
|
|
6021
6064
|
|
|
@@ -46,6 +46,8 @@ The manifest is loaded automatically by the platform for every collection page.
|
|
|
46
46
|
"admin": "app.admin.json",
|
|
47
47
|
|
|
48
48
|
"widgets": {
|
|
49
|
+
"instanceResolution": true,
|
|
50
|
+
"instanceParam": "widgetId",
|
|
49
51
|
"files": {
|
|
50
52
|
"js": {
|
|
51
53
|
"umd": "dist/widgets.umd.js",
|
|
@@ -127,8 +129,36 @@ Declares the widget bundle. Omit if the app has no widget component.
|
|
|
127
129
|
| `files.js.umd` | UMD bundle path — used for dynamic `<script>` loading |
|
|
128
130
|
| `files.js.esm` | ESM bundle path — used for `import()` / native ES modules (optional but recommended) |
|
|
129
131
|
| `files.css` | CSS bundle path — omit if the widget ships no styles |
|
|
132
|
+
| `instanceResolution` | Optional boolean. When `true`, this app supports resolving configured widget instances by ID from app config |
|
|
133
|
+
| `instanceParam` | Optional string. Query/hash param used for instance lookup. Defaults to `"widgetId"` |
|
|
130
134
|
| `components[]` | One entry per exported widget component (see below) |
|
|
131
135
|
|
|
136
|
+
**Widget instance resolution**
|
|
137
|
+
|
|
138
|
+
Apps such as widget toolkits often store reusable widget instances in collection-scoped app config, for example under `config.widgets.launch-countdown`. When your widget bundle can self-configure from one of those stored instances, declare that capability in the manifest:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
"widgets": {
|
|
142
|
+
"instanceResolution": true,
|
|
143
|
+
"instanceParam": "widgetId",
|
|
144
|
+
"files": {
|
|
145
|
+
"js": {
|
|
146
|
+
"umd": "dist/widgets.umd.js",
|
|
147
|
+
"esm": "dist/widgets.es.js"
|
|
148
|
+
},
|
|
149
|
+
"css": null
|
|
150
|
+
},
|
|
151
|
+
"components": [
|
|
152
|
+
{
|
|
153
|
+
"name": "WidgetToolkitResolver",
|
|
154
|
+
"description": "Resolves and renders a configured widget instance by ID."
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This tells the platform and other apps that they can deep-link into a stored widget instance using a URL or embed context such as `?appId=widget-toolkit&widgetId=launch-countdown`.
|
|
161
|
+
|
|
132
162
|
**Component fields:**
|
|
133
163
|
|
|
134
164
|
| Field | Type | Description |
|
|
@@ -281,6 +311,8 @@ Fetched only by the admin UI and AI-assisted setup flows — never loaded on the
|
|
|
281
311
|
}
|
|
282
312
|
```
|
|
283
313
|
|
|
314
|
+
> `dynamic-select` widget pickers are a reasonable future extension for admin schemas, but they are not a built-in question type in the SDK today. For now, treat widget-instance selection as an app-level UI convention powered by `appConfiguration.listWidgetInstances()`.
|
|
315
|
+
|
|
284
316
|
### Field Reference
|
|
285
317
|
|
|
286
318
|
#### `aiGuide`
|
|
@@ -35,6 +35,8 @@ Deep-linkable states come from **two sources** depending on their nature:
|
|
|
35
35
|
| **App manifest** (`app.manifest.json`) | Fixed routes built into the app | Only when the app itself is updated |
|
|
36
36
|
| **App config** (`appConfig.linkable`) | Content-driven entries that vary by collection | When admins create, remove, or rename content |
|
|
37
37
|
|
|
38
|
+
Widget-oriented apps can also use the same deep-link model with `widgetId` rather than `pageId`: the app config stores reusable widget instances, and the URL or embed context selects a specific instance to render.
|
|
39
|
+
|
|
38
40
|
Consumers merge both sources to get the full set of navigable states for an app.
|
|
39
41
|
|
|
40
42
|
```text
|
|
@@ -232,6 +234,7 @@ The parent platform constructs the final navigation URL from an entry by combini
|
|
|
232
234
|
| Entry | Resolved URL |
|
|
233
235
|
|-------|--------------|
|
|
234
236
|
| `{ title: "About Us", params: { pageId: "about-us" } }` | `https://app.example.com/#/?collectionId=abc&appId=my-app&pageId=about-us` |
|
|
237
|
+
| `{ title: "Launch Countdown", params: { widgetId: "launch-countdown" } }` | `https://app.example.com/#/?collectionId=abc&appId=widget-toolkit&widgetId=launch-countdown` |
|
|
235
238
|
| `{ title: "Advanced Settings", path: "/settings", params: { tab: "advanced" } }` | `https://app.example.com/admin.html#/settings?collectionId=abc&appId=my-app&tab=advanced` |
|
|
236
239
|
| `{ title: "Gallery", path: "/gallery" }` | `https://app.example.com/#/gallery?collectionId=abc&appId=my-app` |
|
|
237
240
|
| `{ title: "Home" }` | `https://app.example.com/#/?collectionId=abc&appId=my-app` |
|
|
@@ -259,6 +262,31 @@ This section only applies to **dynamic links stored in `appConfig.linkable`**. S
|
|
|
259
262
|
|
|
260
263
|
Apps **MUST** update `appConfig.linkable` whenever the set of dynamic navigable states changes:
|
|
261
264
|
|
|
265
|
+
For widget toolkits, that usually means syncing entries from your stored widget instance map:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const widgets = await SL.appConfiguration.listWidgetInstances({
|
|
269
|
+
collectionId,
|
|
270
|
+
appId: 'widget-toolkit',
|
|
271
|
+
admin: true,
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const linkable = widgets.map(widget => ({
|
|
275
|
+
title: widget.name,
|
|
276
|
+
params: { widgetId: widget.id },
|
|
277
|
+
}))
|
|
278
|
+
|
|
279
|
+
const current = await SL.appConfiguration.getConfig({ collectionId, appId: 'widget-toolkit', admin: true }) ?? {}
|
|
280
|
+
await SL.appConfiguration.setConfig({
|
|
281
|
+
collectionId,
|
|
282
|
+
appId: 'widget-toolkit',
|
|
283
|
+
admin: true,
|
|
284
|
+
config: { ...current, linkable },
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
This gives the portal and AI orchestrators the same discoverability for widget instances that page-driven apps already get for `pageId` content.
|
|
289
|
+
|
|
262
290
|
- A page is created, deleted, published, or unpublished
|
|
263
291
|
- A page's `deepLinkable` flag is toggled
|
|
264
292
|
- A page title changes
|
package/dist/docs/manifests.md
CHANGED
|
@@ -19,7 +19,7 @@ app.admin.json ← loaded on-demand (setup wizards, import, AI config flow
|
|
|
19
19
|
|---------|---------|-------------|
|
|
20
20
|
| `meta` | App identity, version, appId, SEO priority | All workflows |
|
|
21
21
|
| `admin` | Pointer to `app.admin.json` | Admin orchestrators |
|
|
22
|
-
| `widgets` | Bundle files + component definitions + settings schemas | Widget Builder |
|
|
22
|
+
| `widgets` | Bundle files + component definitions + settings schemas; can also declare widget-instance resolution via `widgetId` | Widget Builder |
|
|
23
23
|
| `containers` | Bundle files + component definitions | Container Loader |
|
|
24
24
|
| `executor` | Bundle files, factory name, exports list | Server / AI |
|
|
25
25
|
| `linkable` | Static deep-link routes | Portal menus / AI nav |
|
|
@@ -29,6 +29,8 @@ app.admin.json ← loaded on-demand (setup wizards, import, AI config flow
|
|
|
29
29
|
"meta": { "name": "My App", "appId": "my-app", "version": "1.0.0" },
|
|
30
30
|
"admin": "app.admin.json",
|
|
31
31
|
"widgets": {
|
|
32
|
+
"instanceResolution": true,
|
|
33
|
+
"instanceParam": "widgetId",
|
|
32
34
|
"files": {
|
|
33
35
|
"js": { "umd": "dist/widgets.umd.js", "esm": "dist/widgets.es.js" },
|
|
34
36
|
"css": null
|
|
@@ -62,6 +64,8 @@ app.admin.json ← loaded on-demand (setup wizards, import, AI config flow
|
|
|
62
64
|
}
|
|
63
65
|
```
|
|
64
66
|
|
|
67
|
+
When `instanceResolution` is enabled, the app is declaring that consumers can pass a widget instance identifier such as `?appId=widget-toolkit&widgetId=launch-countdown`, and the widget bundle will resolve its stored configuration from app config.
|
|
68
|
+
|
|
65
69
|
> ⚠️ **`css` is `null` by default.** Most widgets and containers use Tailwind/shadcn classes inherited from the parent and produce **no CSS output file**. Set `"css": null` in the manifest. Only set it to a filename if your widget/container ships a custom CSS file that actually exists in `dist/`. The parent portal checks this value before injecting a `<link>` tag — a non-null value pointing to a missing file will cause a 404.
|
|
66
70
|
|
|
67
71
|
---
|
package/dist/docs/widgets.md
CHANGED
|
@@ -202,6 +202,141 @@ Widgets support two navigation patterns:
|
|
|
202
202
|
|
|
203
203
|
## Building a Widget
|
|
204
204
|
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Widget Instance Resolution
|
|
208
|
+
|
|
209
|
+
Some apps expose a **widget resolver** instead of a single hard-coded widget. The resolver receives a `widgetId`, looks up a stored instance in app config, and renders the correct widget with its saved settings.
|
|
210
|
+
|
|
211
|
+
This pattern is useful for widget toolkits, promo blocks, countdown libraries, CTA collections, and any app where admins create multiple reusable widget instances.
|
|
212
|
+
|
|
213
|
+
### URL and embed convention
|
|
214
|
+
|
|
215
|
+
Use `widgetId` the same way page-oriented apps use `pageId`:
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
?appId=widget-toolkit&widgetId=launch-countdown
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
This works naturally in:
|
|
222
|
+
|
|
223
|
+
- direct container or iframe links
|
|
224
|
+
- widget-to-widget references
|
|
225
|
+
- content slots that need to embed a preconfigured widget instance
|
|
226
|
+
- platform deep-link routing when the manifest declares widget instance resolution
|
|
227
|
+
|
|
228
|
+
### Manifest declaration
|
|
229
|
+
|
|
230
|
+
Declare support in `app.manifest.json`:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"widgets": {
|
|
235
|
+
"instanceResolution": true,
|
|
236
|
+
"instanceParam": "widgetId",
|
|
237
|
+
"files": {
|
|
238
|
+
"js": {
|
|
239
|
+
"umd": "dist/widgets.umd.js",
|
|
240
|
+
"esm": "dist/widgets.es.js"
|
|
241
|
+
},
|
|
242
|
+
"css": null
|
|
243
|
+
},
|
|
244
|
+
"components": [
|
|
245
|
+
{
|
|
246
|
+
"name": "WidgetToolkitResolver",
|
|
247
|
+
"description": "Resolves and renders widget instances by ID."
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### SDK helpers
|
|
255
|
+
|
|
256
|
+
The SDK now includes two thin helpers on `SL.appConfiguration`:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
const widget = await SL.appConfiguration.getWidgetInstance({
|
|
260
|
+
collectionId,
|
|
261
|
+
appId: 'widget-toolkit',
|
|
262
|
+
widgetId: 'launch-countdown',
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const widgets = await SL.appConfiguration.listWidgetInstances({
|
|
266
|
+
collectionId,
|
|
267
|
+
appId: 'widget-toolkit',
|
|
268
|
+
})
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`getWidgetInstance()` is intentionally just a wrapper over `getConfig()` that reads `config.widgets[widgetId]`. That keeps the integration simple today while giving the platform freedom to optimize the lookup later.
|
|
272
|
+
|
|
273
|
+
### Stored config shape
|
|
274
|
+
|
|
275
|
+
The recommended storage shape is:
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{
|
|
279
|
+
"widgets": {
|
|
280
|
+
"launch-countdown": {
|
|
281
|
+
"id": "launch-countdown",
|
|
282
|
+
"name": "Launch Countdown",
|
|
283
|
+
"widget": {
|
|
284
|
+
"type": "countdown",
|
|
285
|
+
"targetDate": "2026-06-01T00:00:00Z",
|
|
286
|
+
"label": "Launching in..."
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Resolver pattern
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const WidgetToolkitResolver: React.FC<SmartLinksWidgetProps & { widgetId?: string }> = ({
|
|
297
|
+
collectionId,
|
|
298
|
+
appId,
|
|
299
|
+
widgetId,
|
|
300
|
+
SL,
|
|
301
|
+
...props
|
|
302
|
+
}) => {
|
|
303
|
+
const [instance, setInstance] = React.useState<any | null>(null)
|
|
304
|
+
|
|
305
|
+
React.useEffect(() => {
|
|
306
|
+
if (!widgetId) return
|
|
307
|
+
SL.appConfiguration.getWidgetInstance({ collectionId, appId, widgetId })
|
|
308
|
+
.then(setInstance)
|
|
309
|
+
.catch(console.error)
|
|
310
|
+
}, [collectionId, appId, widgetId, SL])
|
|
311
|
+
|
|
312
|
+
if (!widgetId || !instance) return null
|
|
313
|
+
|
|
314
|
+
switch (instance.widget?.type) {
|
|
315
|
+
case 'countdown':
|
|
316
|
+
return <CountdownWidget {...props} {...instance.widget} />
|
|
317
|
+
case 'cta-button':
|
|
318
|
+
return <CtaButtonWidget {...props} {...instance.widget} />
|
|
319
|
+
default:
|
|
320
|
+
return null
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Listing widget instances
|
|
326
|
+
|
|
327
|
+
`listWidgetInstances()` is useful for picker UIs and cross-app references:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
const options = await SL.appConfiguration.listWidgetInstances({
|
|
331
|
+
collectionId,
|
|
332
|
+
appId: 'widget-toolkit',
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// [{ id: 'launch-countdown', name: 'Launch Countdown', type: 'countdown' }]
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
This is a good foundation for future admin question types or dynamic selects, but the SDK does not currently define a built-in `dynamic-select` schema field.
|
|
339
|
+
|
|
205
340
|
### 1. Create the Widget Component
|
|
206
341
|
|
|
207
342
|
```typescript
|
package/dist/http.js
CHANGED
|
@@ -718,7 +718,7 @@ async function proxyStreamRequest(method, path, body, headers) {
|
|
|
718
718
|
ensureProxyListener();
|
|
719
719
|
const payloadBody = (typeof FormData !== 'undefined' && body instanceof FormData)
|
|
720
720
|
? serializeFormDataForProxy(body)
|
|
721
|
-
: body;
|
|
721
|
+
: sanitizeForPostMessage(body);
|
|
722
722
|
const id = generateProxyId();
|
|
723
723
|
const streamController = createProxyStreamIterable(id);
|
|
724
724
|
proxyStreamPending.set(id, streamController);
|
|
@@ -749,6 +749,53 @@ function serializeFormDataForProxy(fd) {
|
|
|
749
749
|
});
|
|
750
750
|
return { _isFormData: true, entries };
|
|
751
751
|
}
|
|
752
|
+
function isAbortSignalLike(value) {
|
|
753
|
+
return !!value
|
|
754
|
+
&& typeof value === 'object'
|
|
755
|
+
&& 'aborted' in value
|
|
756
|
+
&& 'addEventListener' in value
|
|
757
|
+
&& 'removeEventListener' in value;
|
|
758
|
+
}
|
|
759
|
+
function sanitizeForPostMessage(value, seen = new WeakSet()) {
|
|
760
|
+
if (value == null)
|
|
761
|
+
return value;
|
|
762
|
+
const valueType = typeof value;
|
|
763
|
+
if (valueType === 'function' || valueType === 'symbol') {
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
if (valueType !== 'object') {
|
|
767
|
+
return value;
|
|
768
|
+
}
|
|
769
|
+
if (typeof Blob !== 'undefined' && value instanceof Blob) {
|
|
770
|
+
return value;
|
|
771
|
+
}
|
|
772
|
+
if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
|
|
773
|
+
return value;
|
|
774
|
+
}
|
|
775
|
+
if (ArrayBuffer.isView(value)) {
|
|
776
|
+
return value;
|
|
777
|
+
}
|
|
778
|
+
if (isAbortSignalLike(value)) {
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
if (seen.has(value)) {
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
seen.add(value);
|
|
785
|
+
if (Array.isArray(value)) {
|
|
786
|
+
return value
|
|
787
|
+
.map(item => sanitizeForPostMessage(item, seen))
|
|
788
|
+
.filter(item => item !== undefined);
|
|
789
|
+
}
|
|
790
|
+
const proto = Object.getPrototypeOf(value);
|
|
791
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
792
|
+
return value;
|
|
793
|
+
}
|
|
794
|
+
const sanitizedEntries = Object.entries(value)
|
|
795
|
+
.map(([key, entryValue]) => [key, sanitizeForPostMessage(entryValue, seen)])
|
|
796
|
+
.filter(([, entryValue]) => entryValue !== undefined);
|
|
797
|
+
return Object.fromEntries(sanitizedEntries);
|
|
798
|
+
}
|
|
752
799
|
function sanitizeProxyRequestOptions(options) {
|
|
753
800
|
if (!options)
|
|
754
801
|
return undefined;
|
|
@@ -766,7 +813,7 @@ async function proxyRequest(method, path, body, headers, options) {
|
|
|
766
813
|
ensureProxyListener();
|
|
767
814
|
const payloadBody = (typeof FormData !== 'undefined' && body instanceof FormData)
|
|
768
815
|
? serializeFormDataForProxy(body)
|
|
769
|
-
: body;
|
|
816
|
+
: sanitizeForPostMessage(body);
|
|
770
817
|
const id = generateProxyId();
|
|
771
818
|
const msg = {
|
|
772
819
|
_smartlinksProxyRequest: true,
|
|
@@ -1443,7 +1490,7 @@ export async function sendCustomProxyMessage(request, params) {
|
|
|
1443
1490
|
_smartlinksCustomProxyRequest: true,
|
|
1444
1491
|
id,
|
|
1445
1492
|
request,
|
|
1446
|
-
params,
|
|
1493
|
+
params: sanitizeForPostMessage(params),
|
|
1447
1494
|
};
|
|
1448
1495
|
logDebug('[smartlinks] proxy:custom postMessage', { id, request, params: safeBodyPreview(params) });
|
|
1449
1496
|
return new Promise((resolve, reject) => {
|
package/dist/openapi.yaml
CHANGED
|
@@ -11433,6 +11433,49 @@ components:
|
|
|
11433
11433
|
required:
|
|
11434
11434
|
- id
|
|
11435
11435
|
- name
|
|
11436
|
+
GetWidgetInstanceOptions:
|
|
11437
|
+
type: object
|
|
11438
|
+
properties:
|
|
11439
|
+
appId:
|
|
11440
|
+
type: string
|
|
11441
|
+
collectionId:
|
|
11442
|
+
type: string
|
|
11443
|
+
productId:
|
|
11444
|
+
type: string
|
|
11445
|
+
variantId:
|
|
11446
|
+
type: string
|
|
11447
|
+
batchId:
|
|
11448
|
+
type: string
|
|
11449
|
+
admin:
|
|
11450
|
+
type: boolean
|
|
11451
|
+
widgetId:
|
|
11452
|
+
type: string
|
|
11453
|
+
required:
|
|
11454
|
+
- appId
|
|
11455
|
+
- widgetId
|
|
11456
|
+
WidgetInstance:
|
|
11457
|
+
type: object
|
|
11458
|
+
properties:
|
|
11459
|
+
id:
|
|
11460
|
+
type: string
|
|
11461
|
+
name:
|
|
11462
|
+
type: string
|
|
11463
|
+
widget:
|
|
11464
|
+
$ref: "#/components/schemas/TWidget"
|
|
11465
|
+
required:
|
|
11466
|
+
- id
|
|
11467
|
+
WidgetInstanceSummary:
|
|
11468
|
+
type: object
|
|
11469
|
+
properties:
|
|
11470
|
+
id:
|
|
11471
|
+
type: string
|
|
11472
|
+
name:
|
|
11473
|
+
type: string
|
|
11474
|
+
type:
|
|
11475
|
+
type: string
|
|
11476
|
+
required:
|
|
11477
|
+
- id
|
|
11478
|
+
- name
|
|
11436
11479
|
AppBundle:
|
|
11437
11480
|
type: object
|
|
11438
11481
|
properties:
|
|
@@ -11493,6 +11536,22 @@ components:
|
|
|
11493
11536
|
additionalProperties: true
|
|
11494
11537
|
required:
|
|
11495
11538
|
- name
|
|
11539
|
+
AppManifestWidgets:
|
|
11540
|
+
type: object
|
|
11541
|
+
properties:
|
|
11542
|
+
files:
|
|
11543
|
+
$ref: "#/components/schemas/AppManifestFiles"
|
|
11544
|
+
components:
|
|
11545
|
+
type: array
|
|
11546
|
+
items:
|
|
11547
|
+
$ref: "#/components/schemas/AppWidgetComponent"
|
|
11548
|
+
instanceResolution:
|
|
11549
|
+
type: boolean
|
|
11550
|
+
instanceParam:
|
|
11551
|
+
type: string
|
|
11552
|
+
required:
|
|
11553
|
+
- files
|
|
11554
|
+
- components
|
|
11496
11555
|
AppContainerComponent:
|
|
11497
11556
|
type: object
|
|
11498
11557
|
properties:
|
|
@@ -11785,6 +11844,8 @@ components:
|
|
|
11785
11844
|
admin:
|
|
11786
11845
|
type: string
|
|
11787
11846
|
widgets:
|
|
11847
|
+
$ref: "#/components/schemas/AppManifestWidgets"
|
|
11848
|
+
containers:
|
|
11788
11849
|
type: object
|
|
11789
11850
|
additionalProperties: true
|
|
11790
11851
|
files:
|
|
@@ -11793,9 +11854,6 @@ components:
|
|
|
11793
11854
|
type: array
|
|
11794
11855
|
items:
|
|
11795
11856
|
$ref: "#/components/schemas/AppContainerComponent"
|
|
11796
|
-
containers:
|
|
11797
|
-
type: object
|
|
11798
|
-
additionalProperties: true
|
|
11799
11857
|
linkable:
|
|
11800
11858
|
type: array
|
|
11801
11859
|
items:
|
|
@@ -11809,8 +11867,6 @@ components:
|
|
|
11809
11867
|
- function
|
|
11810
11868
|
- files
|
|
11811
11869
|
- components
|
|
11812
|
-
- files
|
|
11813
|
-
- components
|
|
11814
11870
|
CollectionAppWidget:
|
|
11815
11871
|
type: object
|
|
11816
11872
|
properties:
|
|
@@ -9,3 +9,27 @@ export interface AppConfigurationResponse {
|
|
|
9
9
|
/** Key-value pairs representing configuration settings */
|
|
10
10
|
settings?: Record<string, any>;
|
|
11
11
|
}
|
|
12
|
+
/** Options for resolving a configured widget instance from app config. */
|
|
13
|
+
export interface GetWidgetInstanceOptions {
|
|
14
|
+
appId: string;
|
|
15
|
+
collectionId?: string;
|
|
16
|
+
productId?: string;
|
|
17
|
+
variantId?: string;
|
|
18
|
+
batchId?: string;
|
|
19
|
+
admin?: boolean;
|
|
20
|
+
widgetId: string;
|
|
21
|
+
}
|
|
22
|
+
/** A configured widget instance stored in `appConfig.widgets`. */
|
|
23
|
+
export interface WidgetInstance<TWidget = any> {
|
|
24
|
+
id: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
widget?: TWidget;
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
}
|
|
29
|
+
/** Minimal summary shape for picker UIs and instance selectors. */
|
|
30
|
+
export interface WidgetInstanceSummary {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
type?: string;
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
}
|
|
@@ -51,6 +51,15 @@ export interface AppWidgetComponent {
|
|
|
51
51
|
/** JSON-Schema-style settings the widget accepts */
|
|
52
52
|
settings?: Record<string, any>;
|
|
53
53
|
}
|
|
54
|
+
/** Widget bundle declaration in `app.manifest.json`. */
|
|
55
|
+
export interface AppManifestWidgets {
|
|
56
|
+
files: AppManifestFiles;
|
|
57
|
+
components: AppWidgetComponent[];
|
|
58
|
+
/** Whether this app supports resolving configured widget instances by ID. */
|
|
59
|
+
instanceResolution?: boolean;
|
|
60
|
+
/** Query/hash parameter name used for instance resolution. Defaults to `widgetId`. */
|
|
61
|
+
instanceParam?: string;
|
|
62
|
+
}
|
|
54
63
|
/** A single container component defined in the manifest */
|
|
55
64
|
export interface AppContainerComponent {
|
|
56
65
|
name: string;
|
|
@@ -301,10 +310,7 @@ export interface AppManifest {
|
|
|
301
310
|
*/
|
|
302
311
|
admin?: string;
|
|
303
312
|
/** Widget bundle definition. Presence means a widget bundle exists for this app. */
|
|
304
|
-
widgets?:
|
|
305
|
-
files: AppManifestFiles;
|
|
306
|
-
components: AppWidgetComponent[];
|
|
307
|
-
};
|
|
313
|
+
widgets?: AppManifestWidgets;
|
|
308
314
|
/** Container bundle definition. Presence means a container bundle exists. */
|
|
309
315
|
containers?: {
|
|
310
316
|
files: AppManifestFiles;
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.5 | Generated: 2026-03-14T14:43:55.897Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -1283,6 +1283,39 @@ interface AppConfigurationResponse {
|
|
|
1283
1283
|
}
|
|
1284
1284
|
```
|
|
1285
1285
|
|
|
1286
|
+
**GetWidgetInstanceOptions** (interface)
|
|
1287
|
+
```typescript
|
|
1288
|
+
interface GetWidgetInstanceOptions {
|
|
1289
|
+
appId: string
|
|
1290
|
+
collectionId?: string
|
|
1291
|
+
productId?: string
|
|
1292
|
+
variantId?: string
|
|
1293
|
+
batchId?: string
|
|
1294
|
+
admin?: boolean
|
|
1295
|
+
widgetId: string
|
|
1296
|
+
}
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
**WidgetInstance<TWidget = any>** (interface)
|
|
1300
|
+
```typescript
|
|
1301
|
+
interface WidgetInstance<TWidget = any> {
|
|
1302
|
+
id: string
|
|
1303
|
+
name?: string
|
|
1304
|
+
widget?: TWidget
|
|
1305
|
+
[key: string]: any
|
|
1306
|
+
}
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
**WidgetInstanceSummary** (interface)
|
|
1310
|
+
```typescript
|
|
1311
|
+
interface WidgetInstanceSummary {
|
|
1312
|
+
id: string
|
|
1313
|
+
name: string
|
|
1314
|
+
type?: string
|
|
1315
|
+
[key: string]: any
|
|
1316
|
+
}
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1286
1319
|
### appManifest
|
|
1287
1320
|
|
|
1288
1321
|
**AppBundle** (interface)
|
|
@@ -1324,6 +1357,16 @@ interface AppWidgetComponent {
|
|
|
1324
1357
|
}
|
|
1325
1358
|
```
|
|
1326
1359
|
|
|
1360
|
+
**AppManifestWidgets** (interface)
|
|
1361
|
+
```typescript
|
|
1362
|
+
interface AppManifestWidgets {
|
|
1363
|
+
files: AppManifestFiles;
|
|
1364
|
+
components: AppWidgetComponent[];
|
|
1365
|
+
instanceResolution?: boolean;
|
|
1366
|
+
instanceParam?: string;
|
|
1367
|
+
}
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1327
1370
|
**AppContainerComponent** (interface)
|
|
1328
1371
|
```typescript
|
|
1329
1372
|
interface AppContainerComponent {
|
|
@@ -1525,10 +1568,7 @@ interface AppManifest {
|
|
|
1525
1568
|
* (setup questions, import schema, tunable fields, metrics definitions).
|
|
1526
1569
|
* Absent when the app has no admin UI.
|
|
1527
1570
|
admin?: string;
|
|
1528
|
-
widgets?:
|
|
1529
|
-
files: AppManifestFiles;
|
|
1530
|
-
components: AppWidgetComponent[];
|
|
1531
|
-
};
|
|
1571
|
+
widgets?: AppManifestWidgets;
|
|
1532
1572
|
containers?: {
|
|
1533
1573
|
files: AppManifestFiles;
|
|
1534
1574
|
components: AppContainerComponent[];
|
|
@@ -6016,6 +6056,9 @@ Get related threads and records for a case (admin only) GET /cases/:caseId/relat
|
|
|
6016
6056
|
**getConfig**(opts: AppConfigOptions) → `Promise<any>`
|
|
6017
6057
|
Get app configuration for a collection/product scope. ```typescript const config = await appConfiguration.getConfig({ appId: 'warranty-portal', collectionId: 'my-collection' }); ```
|
|
6018
6058
|
|
|
6059
|
+
**listWidgetInstances**(opts: Omit<GetWidgetInstanceOptions, 'widgetId'>) → `Promise<WidgetInstanceSummary[]>`
|
|
6060
|
+
List configured widget instances for an app. Useful for picker UIs, setup schemas, and widget-to-widget references. ```typescript const widgets = await appConfiguration.listWidgetInstances({ collectionId: 'my-collection', appId: 'widget-toolkit' }) ```
|
|
6061
|
+
|
|
6019
6062
|
**setConfig**(opts: AppConfigOptions) → `Promise<any>`
|
|
6020
6063
|
Set app configuration for a collection/product scope. Requires admin authentication. ```typescript await appConfiguration.setConfig({ appId: 'warranty-portal', collectionId: 'my-collection', admin: true, config: { warrantyPeriod: 24, supportEmail: 'support@example.com' } }); ```
|
|
6021
6064
|
|
package/docs/app-manifest.md
CHANGED
|
@@ -46,6 +46,8 @@ The manifest is loaded automatically by the platform for every collection page.
|
|
|
46
46
|
"admin": "app.admin.json",
|
|
47
47
|
|
|
48
48
|
"widgets": {
|
|
49
|
+
"instanceResolution": true,
|
|
50
|
+
"instanceParam": "widgetId",
|
|
49
51
|
"files": {
|
|
50
52
|
"js": {
|
|
51
53
|
"umd": "dist/widgets.umd.js",
|
|
@@ -127,8 +129,36 @@ Declares the widget bundle. Omit if the app has no widget component.
|
|
|
127
129
|
| `files.js.umd` | UMD bundle path — used for dynamic `<script>` loading |
|
|
128
130
|
| `files.js.esm` | ESM bundle path — used for `import()` / native ES modules (optional but recommended) |
|
|
129
131
|
| `files.css` | CSS bundle path — omit if the widget ships no styles |
|
|
132
|
+
| `instanceResolution` | Optional boolean. When `true`, this app supports resolving configured widget instances by ID from app config |
|
|
133
|
+
| `instanceParam` | Optional string. Query/hash param used for instance lookup. Defaults to `"widgetId"` |
|
|
130
134
|
| `components[]` | One entry per exported widget component (see below) |
|
|
131
135
|
|
|
136
|
+
**Widget instance resolution**
|
|
137
|
+
|
|
138
|
+
Apps such as widget toolkits often store reusable widget instances in collection-scoped app config, for example under `config.widgets.launch-countdown`. When your widget bundle can self-configure from one of those stored instances, declare that capability in the manifest:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
"widgets": {
|
|
142
|
+
"instanceResolution": true,
|
|
143
|
+
"instanceParam": "widgetId",
|
|
144
|
+
"files": {
|
|
145
|
+
"js": {
|
|
146
|
+
"umd": "dist/widgets.umd.js",
|
|
147
|
+
"esm": "dist/widgets.es.js"
|
|
148
|
+
},
|
|
149
|
+
"css": null
|
|
150
|
+
},
|
|
151
|
+
"components": [
|
|
152
|
+
{
|
|
153
|
+
"name": "WidgetToolkitResolver",
|
|
154
|
+
"description": "Resolves and renders a configured widget instance by ID."
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This tells the platform and other apps that they can deep-link into a stored widget instance using a URL or embed context such as `?appId=widget-toolkit&widgetId=launch-countdown`.
|
|
161
|
+
|
|
132
162
|
**Component fields:**
|
|
133
163
|
|
|
134
164
|
| Field | Type | Description |
|
|
@@ -281,6 +311,8 @@ Fetched only by the admin UI and AI-assisted setup flows — never loaded on the
|
|
|
281
311
|
}
|
|
282
312
|
```
|
|
283
313
|
|
|
314
|
+
> `dynamic-select` widget pickers are a reasonable future extension for admin schemas, but they are not a built-in question type in the SDK today. For now, treat widget-instance selection as an app-level UI convention powered by `appConfiguration.listWidgetInstances()`.
|
|
315
|
+
|
|
284
316
|
### Field Reference
|
|
285
317
|
|
|
286
318
|
#### `aiGuide`
|
|
@@ -35,6 +35,8 @@ Deep-linkable states come from **two sources** depending on their nature:
|
|
|
35
35
|
| **App manifest** (`app.manifest.json`) | Fixed routes built into the app | Only when the app itself is updated |
|
|
36
36
|
| **App config** (`appConfig.linkable`) | Content-driven entries that vary by collection | When admins create, remove, or rename content |
|
|
37
37
|
|
|
38
|
+
Widget-oriented apps can also use the same deep-link model with `widgetId` rather than `pageId`: the app config stores reusable widget instances, and the URL or embed context selects a specific instance to render.
|
|
39
|
+
|
|
38
40
|
Consumers merge both sources to get the full set of navigable states for an app.
|
|
39
41
|
|
|
40
42
|
```text
|
|
@@ -232,6 +234,7 @@ The parent platform constructs the final navigation URL from an entry by combini
|
|
|
232
234
|
| Entry | Resolved URL |
|
|
233
235
|
|-------|--------------|
|
|
234
236
|
| `{ title: "About Us", params: { pageId: "about-us" } }` | `https://app.example.com/#/?collectionId=abc&appId=my-app&pageId=about-us` |
|
|
237
|
+
| `{ title: "Launch Countdown", params: { widgetId: "launch-countdown" } }` | `https://app.example.com/#/?collectionId=abc&appId=widget-toolkit&widgetId=launch-countdown` |
|
|
235
238
|
| `{ title: "Advanced Settings", path: "/settings", params: { tab: "advanced" } }` | `https://app.example.com/admin.html#/settings?collectionId=abc&appId=my-app&tab=advanced` |
|
|
236
239
|
| `{ title: "Gallery", path: "/gallery" }` | `https://app.example.com/#/gallery?collectionId=abc&appId=my-app` |
|
|
237
240
|
| `{ title: "Home" }` | `https://app.example.com/#/?collectionId=abc&appId=my-app` |
|
|
@@ -259,6 +262,31 @@ This section only applies to **dynamic links stored in `appConfig.linkable`**. S
|
|
|
259
262
|
|
|
260
263
|
Apps **MUST** update `appConfig.linkable` whenever the set of dynamic navigable states changes:
|
|
261
264
|
|
|
265
|
+
For widget toolkits, that usually means syncing entries from your stored widget instance map:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const widgets = await SL.appConfiguration.listWidgetInstances({
|
|
269
|
+
collectionId,
|
|
270
|
+
appId: 'widget-toolkit',
|
|
271
|
+
admin: true,
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const linkable = widgets.map(widget => ({
|
|
275
|
+
title: widget.name,
|
|
276
|
+
params: { widgetId: widget.id },
|
|
277
|
+
}))
|
|
278
|
+
|
|
279
|
+
const current = await SL.appConfiguration.getConfig({ collectionId, appId: 'widget-toolkit', admin: true }) ?? {}
|
|
280
|
+
await SL.appConfiguration.setConfig({
|
|
281
|
+
collectionId,
|
|
282
|
+
appId: 'widget-toolkit',
|
|
283
|
+
admin: true,
|
|
284
|
+
config: { ...current, linkable },
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
This gives the portal and AI orchestrators the same discoverability for widget instances that page-driven apps already get for `pageId` content.
|
|
289
|
+
|
|
262
290
|
- A page is created, deleted, published, or unpublished
|
|
263
291
|
- A page's `deepLinkable` flag is toggled
|
|
264
292
|
- A page title changes
|
package/docs/manifests.md
CHANGED
|
@@ -19,7 +19,7 @@ app.admin.json ← loaded on-demand (setup wizards, import, AI config flow
|
|
|
19
19
|
|---------|---------|-------------|
|
|
20
20
|
| `meta` | App identity, version, appId, SEO priority | All workflows |
|
|
21
21
|
| `admin` | Pointer to `app.admin.json` | Admin orchestrators |
|
|
22
|
-
| `widgets` | Bundle files + component definitions + settings schemas | Widget Builder |
|
|
22
|
+
| `widgets` | Bundle files + component definitions + settings schemas; can also declare widget-instance resolution via `widgetId` | Widget Builder |
|
|
23
23
|
| `containers` | Bundle files + component definitions | Container Loader |
|
|
24
24
|
| `executor` | Bundle files, factory name, exports list | Server / AI |
|
|
25
25
|
| `linkable` | Static deep-link routes | Portal menus / AI nav |
|
|
@@ -29,6 +29,8 @@ app.admin.json ← loaded on-demand (setup wizards, import, AI config flow
|
|
|
29
29
|
"meta": { "name": "My App", "appId": "my-app", "version": "1.0.0" },
|
|
30
30
|
"admin": "app.admin.json",
|
|
31
31
|
"widgets": {
|
|
32
|
+
"instanceResolution": true,
|
|
33
|
+
"instanceParam": "widgetId",
|
|
32
34
|
"files": {
|
|
33
35
|
"js": { "umd": "dist/widgets.umd.js", "esm": "dist/widgets.es.js" },
|
|
34
36
|
"css": null
|
|
@@ -62,6 +64,8 @@ app.admin.json ← loaded on-demand (setup wizards, import, AI config flow
|
|
|
62
64
|
}
|
|
63
65
|
```
|
|
64
66
|
|
|
67
|
+
When `instanceResolution` is enabled, the app is declaring that consumers can pass a widget instance identifier such as `?appId=widget-toolkit&widgetId=launch-countdown`, and the widget bundle will resolve its stored configuration from app config.
|
|
68
|
+
|
|
65
69
|
> ⚠️ **`css` is `null` by default.** Most widgets and containers use Tailwind/shadcn classes inherited from the parent and produce **no CSS output file**. Set `"css": null` in the manifest. Only set it to a filename if your widget/container ships a custom CSS file that actually exists in `dist/`. The parent portal checks this value before injecting a `<link>` tag — a non-null value pointing to a missing file will cause a 404.
|
|
66
70
|
|
|
67
71
|
---
|
package/docs/widgets.md
CHANGED
|
@@ -202,6 +202,141 @@ Widgets support two navigation patterns:
|
|
|
202
202
|
|
|
203
203
|
## Building a Widget
|
|
204
204
|
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Widget Instance Resolution
|
|
208
|
+
|
|
209
|
+
Some apps expose a **widget resolver** instead of a single hard-coded widget. The resolver receives a `widgetId`, looks up a stored instance in app config, and renders the correct widget with its saved settings.
|
|
210
|
+
|
|
211
|
+
This pattern is useful for widget toolkits, promo blocks, countdown libraries, CTA collections, and any app where admins create multiple reusable widget instances.
|
|
212
|
+
|
|
213
|
+
### URL and embed convention
|
|
214
|
+
|
|
215
|
+
Use `widgetId` the same way page-oriented apps use `pageId`:
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
?appId=widget-toolkit&widgetId=launch-countdown
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
This works naturally in:
|
|
222
|
+
|
|
223
|
+
- direct container or iframe links
|
|
224
|
+
- widget-to-widget references
|
|
225
|
+
- content slots that need to embed a preconfigured widget instance
|
|
226
|
+
- platform deep-link routing when the manifest declares widget instance resolution
|
|
227
|
+
|
|
228
|
+
### Manifest declaration
|
|
229
|
+
|
|
230
|
+
Declare support in `app.manifest.json`:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"widgets": {
|
|
235
|
+
"instanceResolution": true,
|
|
236
|
+
"instanceParam": "widgetId",
|
|
237
|
+
"files": {
|
|
238
|
+
"js": {
|
|
239
|
+
"umd": "dist/widgets.umd.js",
|
|
240
|
+
"esm": "dist/widgets.es.js"
|
|
241
|
+
},
|
|
242
|
+
"css": null
|
|
243
|
+
},
|
|
244
|
+
"components": [
|
|
245
|
+
{
|
|
246
|
+
"name": "WidgetToolkitResolver",
|
|
247
|
+
"description": "Resolves and renders widget instances by ID."
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### SDK helpers
|
|
255
|
+
|
|
256
|
+
The SDK now includes two thin helpers on `SL.appConfiguration`:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
const widget = await SL.appConfiguration.getWidgetInstance({
|
|
260
|
+
collectionId,
|
|
261
|
+
appId: 'widget-toolkit',
|
|
262
|
+
widgetId: 'launch-countdown',
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const widgets = await SL.appConfiguration.listWidgetInstances({
|
|
266
|
+
collectionId,
|
|
267
|
+
appId: 'widget-toolkit',
|
|
268
|
+
})
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`getWidgetInstance()` is intentionally just a wrapper over `getConfig()` that reads `config.widgets[widgetId]`. That keeps the integration simple today while giving the platform freedom to optimize the lookup later.
|
|
272
|
+
|
|
273
|
+
### Stored config shape
|
|
274
|
+
|
|
275
|
+
The recommended storage shape is:
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{
|
|
279
|
+
"widgets": {
|
|
280
|
+
"launch-countdown": {
|
|
281
|
+
"id": "launch-countdown",
|
|
282
|
+
"name": "Launch Countdown",
|
|
283
|
+
"widget": {
|
|
284
|
+
"type": "countdown",
|
|
285
|
+
"targetDate": "2026-06-01T00:00:00Z",
|
|
286
|
+
"label": "Launching in..."
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Resolver pattern
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const WidgetToolkitResolver: React.FC<SmartLinksWidgetProps & { widgetId?: string }> = ({
|
|
297
|
+
collectionId,
|
|
298
|
+
appId,
|
|
299
|
+
widgetId,
|
|
300
|
+
SL,
|
|
301
|
+
...props
|
|
302
|
+
}) => {
|
|
303
|
+
const [instance, setInstance] = React.useState<any | null>(null)
|
|
304
|
+
|
|
305
|
+
React.useEffect(() => {
|
|
306
|
+
if (!widgetId) return
|
|
307
|
+
SL.appConfiguration.getWidgetInstance({ collectionId, appId, widgetId })
|
|
308
|
+
.then(setInstance)
|
|
309
|
+
.catch(console.error)
|
|
310
|
+
}, [collectionId, appId, widgetId, SL])
|
|
311
|
+
|
|
312
|
+
if (!widgetId || !instance) return null
|
|
313
|
+
|
|
314
|
+
switch (instance.widget?.type) {
|
|
315
|
+
case 'countdown':
|
|
316
|
+
return <CountdownWidget {...props} {...instance.widget} />
|
|
317
|
+
case 'cta-button':
|
|
318
|
+
return <CtaButtonWidget {...props} {...instance.widget} />
|
|
319
|
+
default:
|
|
320
|
+
return null
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Listing widget instances
|
|
326
|
+
|
|
327
|
+
`listWidgetInstances()` is useful for picker UIs and cross-app references:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
const options = await SL.appConfiguration.listWidgetInstances({
|
|
331
|
+
collectionId,
|
|
332
|
+
appId: 'widget-toolkit',
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// [{ id: 'launch-countdown', name: 'Launch Countdown', type: 'countdown' }]
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
This is a good foundation for future admin question types or dynamic selects, but the SDK does not currently define a built-in `dynamic-select` schema field.
|
|
339
|
+
|
|
205
340
|
### 1. Create the Widget Component
|
|
206
341
|
|
|
207
342
|
```typescript
|
package/openapi.yaml
CHANGED
|
@@ -11433,6 +11433,49 @@ components:
|
|
|
11433
11433
|
required:
|
|
11434
11434
|
- id
|
|
11435
11435
|
- name
|
|
11436
|
+
GetWidgetInstanceOptions:
|
|
11437
|
+
type: object
|
|
11438
|
+
properties:
|
|
11439
|
+
appId:
|
|
11440
|
+
type: string
|
|
11441
|
+
collectionId:
|
|
11442
|
+
type: string
|
|
11443
|
+
productId:
|
|
11444
|
+
type: string
|
|
11445
|
+
variantId:
|
|
11446
|
+
type: string
|
|
11447
|
+
batchId:
|
|
11448
|
+
type: string
|
|
11449
|
+
admin:
|
|
11450
|
+
type: boolean
|
|
11451
|
+
widgetId:
|
|
11452
|
+
type: string
|
|
11453
|
+
required:
|
|
11454
|
+
- appId
|
|
11455
|
+
- widgetId
|
|
11456
|
+
WidgetInstance:
|
|
11457
|
+
type: object
|
|
11458
|
+
properties:
|
|
11459
|
+
id:
|
|
11460
|
+
type: string
|
|
11461
|
+
name:
|
|
11462
|
+
type: string
|
|
11463
|
+
widget:
|
|
11464
|
+
$ref: "#/components/schemas/TWidget"
|
|
11465
|
+
required:
|
|
11466
|
+
- id
|
|
11467
|
+
WidgetInstanceSummary:
|
|
11468
|
+
type: object
|
|
11469
|
+
properties:
|
|
11470
|
+
id:
|
|
11471
|
+
type: string
|
|
11472
|
+
name:
|
|
11473
|
+
type: string
|
|
11474
|
+
type:
|
|
11475
|
+
type: string
|
|
11476
|
+
required:
|
|
11477
|
+
- id
|
|
11478
|
+
- name
|
|
11436
11479
|
AppBundle:
|
|
11437
11480
|
type: object
|
|
11438
11481
|
properties:
|
|
@@ -11493,6 +11536,22 @@ components:
|
|
|
11493
11536
|
additionalProperties: true
|
|
11494
11537
|
required:
|
|
11495
11538
|
- name
|
|
11539
|
+
AppManifestWidgets:
|
|
11540
|
+
type: object
|
|
11541
|
+
properties:
|
|
11542
|
+
files:
|
|
11543
|
+
$ref: "#/components/schemas/AppManifestFiles"
|
|
11544
|
+
components:
|
|
11545
|
+
type: array
|
|
11546
|
+
items:
|
|
11547
|
+
$ref: "#/components/schemas/AppWidgetComponent"
|
|
11548
|
+
instanceResolution:
|
|
11549
|
+
type: boolean
|
|
11550
|
+
instanceParam:
|
|
11551
|
+
type: string
|
|
11552
|
+
required:
|
|
11553
|
+
- files
|
|
11554
|
+
- components
|
|
11496
11555
|
AppContainerComponent:
|
|
11497
11556
|
type: object
|
|
11498
11557
|
properties:
|
|
@@ -11785,6 +11844,8 @@ components:
|
|
|
11785
11844
|
admin:
|
|
11786
11845
|
type: string
|
|
11787
11846
|
widgets:
|
|
11847
|
+
$ref: "#/components/schemas/AppManifestWidgets"
|
|
11848
|
+
containers:
|
|
11788
11849
|
type: object
|
|
11789
11850
|
additionalProperties: true
|
|
11790
11851
|
files:
|
|
@@ -11793,9 +11854,6 @@ components:
|
|
|
11793
11854
|
type: array
|
|
11794
11855
|
items:
|
|
11795
11856
|
$ref: "#/components/schemas/AppContainerComponent"
|
|
11796
|
-
containers:
|
|
11797
|
-
type: object
|
|
11798
|
-
additionalProperties: true
|
|
11799
11857
|
linkable:
|
|
11800
11858
|
type: array
|
|
11801
11859
|
items:
|
|
@@ -11809,8 +11867,6 @@ components:
|
|
|
11809
11867
|
- function
|
|
11810
11868
|
- files
|
|
11811
11869
|
- components
|
|
11812
|
-
- files
|
|
11813
|
-
- components
|
|
11814
11870
|
CollectionAppWidget:
|
|
11815
11871
|
type: object
|
|
11816
11872
|
properties:
|