@fluid-app/fluid-cli-portal 0.1.53 → 0.1.55
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/{backend-dev-plugin-B9-S4Is-.mjs → backend-dev-plugin-CY3yxgJ1.mjs} +1 -1
- package/dist/{backend-dev-plugin-B9-S4Is-.mjs.map → backend-dev-plugin-CY3yxgJ1.mjs.map} +1 -1
- package/dist/index.d.mts +622 -421
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +719 -1815
- package/dist/index.mjs.map +1 -1
- package/dist/{portal-dev-plugin-ByYelgFg.mjs → portal-dev-plugin-CHlxpXc5.mjs} +1 -1
- package/dist/{portal-dev-plugin-ByYelgFg.mjs.map → portal-dev-plugin-CHlxpXc5.mjs.map} +1 -1
- package/dist/portal-widget-dev-plugin-B53Lf9Ke.mjs +144 -0
- package/dist/portal-widget-dev-plugin-B53Lf9Ke.mjs.map +1 -0
- package/dist/pull-CUR8tbW9.mjs +1633 -0
- package/dist/pull-CUR8tbW9.mjs.map +1 -0
- package/dist/sdk.gen-DuPhfdxN.mjs +469 -0
- package/dist/sdk.gen-DuPhfdxN.mjs.map +1 -0
- package/dist/src-CpH5SpDH.mjs +2157 -0
- package/dist/src-CpH5SpDH.mjs.map +1 -0
- package/dist/vite-plugin.d.mts +4 -1
- package/dist/vite-plugin.d.mts.map +1 -1
- package/dist/vite-plugin.mjs +5 -3
- package/package.json +6 -4
- package/templates/base/AGENTS.md +4 -0
- package/templates/base/skills/fluid-portal-authoring/SKILL.md +113 -1
- package/dist/pull-C3yj9L07.mjs +0 -1232
- package/dist/pull-C3yj9L07.mjs.map +0 -1
|
@@ -60,6 +60,38 @@ Guidelines:
|
|
|
60
60
|
- Do not hand-edit `.portal-sync/`; it is sync metadata, not source content.
|
|
61
61
|
- Do not invent unsupported fields. Match the shapes produced by `pnpm pull`.
|
|
62
62
|
|
|
63
|
+
## Screen structure: every screen needs a container root
|
|
64
|
+
|
|
65
|
+
**Rule: a screen's `component_tree` must hold exactly one container node at the top, and every other widget must be inside its `props.children`.**
|
|
66
|
+
|
|
67
|
+
The container types are `ContainerWidget`, `LayoutWidget`, and `CardWidget`. `ContainerWidget` is the default choice for a page root; the admin builder writes one on every screen it authors.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"name": "UGC",
|
|
72
|
+
"component_tree": [
|
|
73
|
+
{
|
|
74
|
+
"id": "ContainerWidget-ugc-root",
|
|
75
|
+
"type": "ContainerWidget",
|
|
76
|
+
"props": {
|
|
77
|
+
"gapSize": "md",
|
|
78
|
+
"padding": 4,
|
|
79
|
+
"children": [{ "id": "…", "type": "…", "columnIndex": 0, "props": {} }]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A screen with bare top-level widgets and no container root **renders correctly in preview and in the live portal**, so nothing you can see locally will tell you it is wrong. What breaks is the admin visual builder: only container widgets receive the edit-mode child-management callbacks (`onAddChild`, `onWidgetSelect`, child delete/duplicate), so a screen without a container root offers **no drop zones**. A human opening that screen in the builder cannot drag anything onto it.
|
|
87
|
+
|
|
88
|
+
This is the single most likely defect in an agent-authored screen, because the failure is invisible from every surface an agent can check.
|
|
89
|
+
|
|
90
|
+
- Put a container root on every screen you create, without exception.
|
|
91
|
+
- Only these three types are containers by default. A `NestedWidget`, `SpacerWidget`, or any other widget at the root does not satisfy this rule.
|
|
92
|
+
- Containers nest. A column layout inside the root is `LayoutWidget` in the root's `children`.
|
|
93
|
+
- When mirroring an existing screen, copy its root container rather than lifting its children out.
|
|
94
|
+
|
|
63
95
|
## Add a new page and put it in the menu
|
|
64
96
|
|
|
65
97
|
A page = a **screen** (a `component_tree` of widgets) + a **navigation item** that points at it. A screen with no nav item is unreachable; a nav item whose screen reference doesn't resolve is silently dropped in local preview and refused at push. Both live in `portal/`.
|
|
@@ -173,6 +205,46 @@ If you create `portal/themes/<slug>.json`, it must contain the complete
|
|
|
173
205
|
pulled theme's full `config` object and then change its `id`, `name`, and token
|
|
174
206
|
values; do not invent a partial config.
|
|
175
207
|
|
|
208
|
+
## Decide first: portal widget, or Mist app?
|
|
209
|
+
|
|
210
|
+
Before building anything custom, work out which system the feature belongs to. This is the most expensive decision to get wrong in portal work, and the deciding question is not "internal or external data" — it is **does this need a server that can hold a secret.**
|
|
211
|
+
|
|
212
|
+
### A company portal widget cannot reach the network
|
|
213
|
+
|
|
214
|
+
Portal widgets run as Remote DOM packages inside a locked-down Web Worker. `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`, `localStorage`, `indexedDB` and more are removed before widget code runs, and no capability re-grants them. A portal widget's data comes from exactly three places:
|
|
215
|
+
|
|
216
|
+
- props written into the screen JSON
|
|
217
|
+
- built-in host capabilities — `account`, `store`, `products`, `content`, `mySite`, `todos`, `calendar`, `points`, `localization`, and friends
|
|
218
|
+
- **data sources**, which the host resolves and passes into props (`api` with a Fluid preset, `custom` for hand-picked Fluid resources, `static` for literal data)
|
|
219
|
+
|
|
220
|
+
### A Mist app is the app; the droplet is its identity
|
|
221
|
+
|
|
222
|
+
A Mist app is a hosted application with its own backend and a `public_url`. It can hold secrets, call any external API, receive webhooks, and do server-side work. Fluid surfaces it through integration records that **all point at that same `public_url`**:
|
|
223
|
+
|
|
224
|
+
| Record | What it is | Per mist |
|
|
225
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
|
226
|
+
| **Droplet** | Identity and credentials — `FLUID_DROPLET_UUID` / `_SECRET` / `_WEBHOOK_AUTH_TOKEN`, OAuth scopes, install lifecycle and webhooks | one |
|
|
227
|
+
| **Mobile embed** | A placement in the mobile app: `embed_url` plus a required cover image and height | many |
|
|
228
|
+
| **Drop zone** | A placement in an admin page/zone slot | many |
|
|
229
|
+
|
|
230
|
+
**The droplet is not the app.** It is the app's identity and permission record. The Mist app is the thing that runs, and it is what an embed points at.
|
|
231
|
+
|
|
232
|
+
### The decision
|
|
233
|
+
|
|
234
|
+
1. **Needs an API key, OAuth, webhooks, background jobs, or any server-side work** → build a **Mist app**. Attach a droplet for credentials and install lifecycle, add a placement surface, and embed the **Mist's public URL**.
|
|
235
|
+
2. **Presents Fluid's own data** (orders, products, account, shares, subscriptions, content) → build a **company portal widget** and read it through a capability or a preset data source. No hosting, no secrets, no deploy pipeline.
|
|
236
|
+
3. **Presents public, unauthenticated, CORS-enabled JSON and nothing more** → a portal widget with an `api` data source pointing at an absolute endpoint works. The host fetches it, so it runs in the browser and cannot hold a credential — the moment authentication enters, this becomes case 1.
|
|
237
|
+
|
|
238
|
+
Ask the user which of these their feature is when it is not obvious from the request. A feature that "pulls in data from <third-party service>" is case 1 essentially every time.
|
|
239
|
+
|
|
240
|
+
### Embedding a Mist app in a portal screen
|
|
241
|
+
|
|
242
|
+
Point the embed at the **Mist's public URL**, or at a purpose-built widget route the Mist app serves. Those routes are built to be iframed and need no installation parameter.
|
|
243
|
+
|
|
244
|
+
**Do not point an embed at the droplet's `/embed` route.** That is the admin dashboard surface and requires a `?dri=` installation parameter; without one it renders "Missing installation" or a blank frame.
|
|
245
|
+
|
|
246
|
+
If the Mist app already publishes registered widget types (see below), prefer those over an iframe embed — they compose properly with the screen and the builder.
|
|
247
|
+
|
|
176
248
|
## Choosing a widget (recommend built-ins before custom code)
|
|
177
249
|
|
|
178
250
|
Use the `type` value in a `component_tree` node. Reach for what already exists:
|
|
@@ -182,13 +254,46 @@ Use the `type` value in a `component_tree` node. Reach for what already exists:
|
|
|
182
254
|
- Content: `TextWidget`, `BulletListWidget`, `CardWidget`, `AlertWidget`, `TableWidget`, `ChartWidget`, `CalendarWidget`
|
|
183
255
|
- Commerce/member: `ShopWidget`, `PointsWidget` (rewards balance), `RecentActivityWidget`, `ToDoWidget`
|
|
184
256
|
- Links/sharing: `LinkWidget`, `QuickLinksWidget`, `QuickShareWidget`, `ListWidget`
|
|
185
|
-
- Platform: `EmbedWidget` (
|
|
257
|
+
- Platform: `EmbedWidget` (iframes an arbitrary URL into a screen), `MySiteWidget`
|
|
186
258
|
|
|
187
259
|
`TextWidget` renders its `title` and `description` as plain text. Do not put
|
|
188
260
|
HTML in either field; markup is escaped and displayed literally.
|
|
189
261
|
|
|
190
262
|
An unregistered `type` renders nothing. Data-driven UI a built-in can't express (a member-specific dashboard) needs a custom widget package — a separate concern from this definition-editing workflow; note it to the user rather than hand-rolling code here.
|
|
191
263
|
|
|
264
|
+
### Droplet widgets are real widget types, not embeds
|
|
265
|
+
|
|
266
|
+
An installed droplet (UGC, and others) contributes **registered widget types**, addressed exactly like any other widget:
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"id": "…",
|
|
271
|
+
"type": "droplet.ugc.drp_fuwamfg3licz1l4yocpkjos12t9vhcrh.MakeAVideoCta",
|
|
272
|
+
"props": {
|
|
273
|
+
"headline": "…",
|
|
274
|
+
"eyebrow": "…",
|
|
275
|
+
"ctaLabel": "…",
|
|
276
|
+
"dri": "",
|
|
277
|
+
"apiBaseUrl": "…"
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The shape is `droplet.<scope>.<dropletId>.<WidgetName>`, and the props are ordinary JSON the builder writes.
|
|
283
|
+
|
|
284
|
+
**Do not reach for `EmbedWidget` to render a droplet's UI.** `EmbedWidget` iframes a URL, and a droplet's `/embed` route is the _admin dashboard_ surface, which requires a `?dri=` installation parameter — pointing an `EmbedWidget` at it renders "Missing installation" or a blank frame. That mistake looks reasonable and fails quietly.
|
|
285
|
+
|
|
286
|
+
**Absence from an API is not evidence a widget type does not exist.** These endpoints are scoped differently and will each come back empty or 404 for a droplet widget that is installed and working:
|
|
287
|
+
|
|
288
|
+
| Endpoint | Why it looks empty |
|
|
289
|
+
| ---------------------------------- | -------------------------------------------------------- |
|
|
290
|
+
| `/api/droplets` (`app_extensions`) | Does not list contributed widget types |
|
|
291
|
+
| `/__widget-packages__` | Dev-server route for _unpublished company_ packages only |
|
|
292
|
+
| `/api/app/widget-packages` | Portal-session scoped; 404s for a CLI token |
|
|
293
|
+
| `/api/company/mobile_widgets` | **This one lists installed droplet widgets** |
|
|
294
|
+
|
|
295
|
+
**When you cannot confirm a widget's type or prop shape from an API, ask the human to drag it onto a scratch screen in the admin builder and then `pnpm pull`.** The pulled JSON is ground truth for both the `type` string and the exact props. Do this instead of inferring from absence, and do it early — it costs one message and replaces a chain of confident guesses.
|
|
296
|
+
|
|
192
297
|
## System screens and how to reach them
|
|
193
298
|
|
|
194
299
|
The portal ships built-in screens addressed by nav slug: `profile` (alias `account`), `orders`, `subscriptions`, `messaging`, `contacts`, `shop`, `customers`, `my-site`, `share/*`, `app-download`. To expose one, add a navigation item with that slug to the profile — `messaging` and `contacts` only appear when a nav item includes them, and `messages`/`my-site` are rep-only (a non-rep member sees the fallback). These are core surfaces the admin builder keeps; author your own pages alongside them in `portal/` and let the definition stay the source of truth so drag-and-drop editing keeps working.
|
|
@@ -295,6 +400,8 @@ Work these in order and report which one failed:
|
|
|
295
400
|
6. Push refused? Read the cross-reference validation errors — they name the file and the missing slug.
|
|
296
401
|
7. Push partially failed? Fix the reported phase error and rerun; the snapshot only advanced for files that succeeded.
|
|
297
402
|
8. Users do not see the change? Push updates the draft only — create/activate a version.
|
|
403
|
+
9. Screen renders fine but the admin builder shows no drop zones on it? The screen is missing its container root. See "Screen structure: every screen needs a container root".
|
|
404
|
+
10. A widget renders blank or says "Missing installation"? You likely used `EmbedWidget` against a droplet's `/embed` route instead of the droplet's registered widget `type`.
|
|
298
405
|
|
|
299
406
|
## Widget work inside a portal project
|
|
300
407
|
|
|
@@ -325,4 +432,9 @@ Before considering portal work complete:
|
|
|
325
432
|
- [ ] Kept portal structure, routes, and content changes in pulled Fluid OS JSON under `portal/`.
|
|
326
433
|
- [ ] Ran typecheck/lint/build or the closest available checks.
|
|
327
434
|
- [ ] Ran push only when the local `portal/` diff was understood.
|
|
435
|
+
- [ ] **Every screen created or modified has a single container root (`ContainerWidget` / `LayoutWidget` / `CardWidget`) with all other widgets in its `props.children`.** Preview cannot detect this; check the JSON.
|
|
436
|
+
- [ ] Chose portal widget vs Mist app deliberately — anything needing a secret, OAuth, webhooks, or server-side work is a Mist app, not a portal widget.
|
|
437
|
+
- [ ] Any embed points at a Mist's public URL or a widget route, never at a droplet's `?dri=`-gated `/embed`.
|
|
438
|
+
- [ ] Used registered widget `type` values for droplet/company widgets, not an `EmbedWidget` pointed at a droplet route.
|
|
439
|
+
- [ ] Confirmed any uncertain widget `type` or prop shape against builder-authored JSON rather than inferring it.
|
|
328
440
|
- [ ] Created/activated a version only when the definition should go live.
|