@fluid-app/fluid-cli-portal 0.1.61 → 0.1.63

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.
Files changed (26) hide show
  1. package/authoring/commands.md +242 -0
  2. package/dist/{backend-dev-plugin-20fLk-4t.mjs → backend-dev-plugin-z9IZCHaY.mjs} +2 -2
  3. package/dist/{backend-dev-plugin-20fLk-4t.mjs.map → backend-dev-plugin-z9IZCHaY.mjs.map} +1 -1
  4. package/dist/index.d.mts +571 -423
  5. package/dist/index.d.mts.map +1 -1
  6. package/dist/index.mjs +17 -15
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/{portal-dev-plugin-5BbRo9QT.mjs → portal-dev-plugin-2jZODDsX.mjs} +2 -2
  9. package/dist/{portal-dev-plugin-5BbRo9QT.mjs.map → portal-dev-plugin-2jZODDsX.mjs.map} +1 -1
  10. package/dist/{portal-widget-dev-plugin-NBHcnvFJ.mjs → portal-widget-dev-plugin-Dyp2bVy7.mjs} +3 -3
  11. package/dist/{portal-widget-dev-plugin-NBHcnvFJ.mjs.map → portal-widget-dev-plugin-Dyp2bVy7.mjs.map} +1 -1
  12. package/dist/{pull-BlG6pMwC.mjs → pull-BWnby_pY.mjs} +6287 -2908
  13. package/dist/{pull-BlG6pMwC.mjs.map → pull-BWnby_pY.mjs.map} +1 -1
  14. package/dist/{src-2znVq55R.mjs → src-CQ24GVe6.mjs} +7 -1
  15. package/dist/src-CQ24GVe6.mjs.map +1 -0
  16. package/dist/vite-plugin.mjs +4 -4
  17. package/package.json +6 -5
  18. package/templates/base/.gitignore.template +3 -0
  19. package/templates/base/AGENTS.md +40 -41
  20. package/templates/base/skills/fluid-portal-authoring/SKILL.md +37 -428
  21. package/templates/base/skills/fluid-portal-authoring/references/portal-json.md +46 -0
  22. package/templates/base/skills/fluid-portal-authoring/references/widgets-and-runtime.md +57 -0
  23. package/templates/base/skills/fluid-portal-authoring/references/workflows.md +56 -0
  24. package/templates/starter/.env.example +9 -0
  25. package/templates/starter/README.md.template +99 -169
  26. package/dist/src-2znVq55R.mjs.map +0 -1
@@ -1,41 +1,40 @@
1
- # AGENTS.md
2
-
3
- Guidance for AI coding tools working in this generated Fluid portal project.
4
-
5
- This project is primarily authored through the Fluid OS portal definition sync workflow. Author portal structure, routes, and content through pulled Fluid OS JSON under `portal/` unless a human explicitly asks for a different architecture.
6
-
7
- ## Source of truth
8
-
9
- - Use `.agents/skills/fluid-portal-authoring/SKILL.md` for portal definition work.
10
- - Use `.agents/skills/fluid-widget-authoring/SKILL.md` for company Remote DOM widget creation with `pnpm widget:create`, package descriptors, property schemas, runtime CSS, validation, build, and publish work.
11
- - `.claude/skills/...` contains the same generated skills for Claude-compatible tools.
12
-
13
- ## Portal workflow
14
-
15
- 1. Run `pnpm pull` to sync the remote Fluid OS definition into `portal/`.
16
- 2. Edit the pulled JSON under `portal/`.
17
- 3. Validate locally with `pnpm typecheck`, `pnpm lint`, and `pnpm build` when applicable.
18
- 4. Run `pnpm push` to sync local `portal/` JSON back to the remote working/draft definition.
19
- 5. Run `pnpm exec fluid portal version create --activate` only when the pushed definition should become live.
20
-
21
- Do not edit `.portal-sync/` by hand. It is generated sync metadata.
22
-
23
- ## Command boundaries
24
-
25
- - `pnpm pull` / `fluid portal pull`: downloads the portal definition into `portal/`.
26
- - `pnpm push` / `fluid portal push`: updates the remote working/draft definition from local JSON.
27
- - `fluid portal version create --activate`: publishes the remote working/draft definition as the live version.
28
- - `pnpm build`: builds the hosted portal shell assets into `dist/`.
29
- - `fluid portal deploy`: publishes company-owned widget runtime artifacts, not portal JSON and not shell assets.
30
-
31
- ## Quality bar
32
-
33
- - **Every screen needs a container root.** A screen's `component_tree` must hold exactly one `ContainerWidget` / `LayoutWidget` / `CardWidget` at the top, with all other widgets inside its `props.children`. Without it the screen still renders, but the admin visual builder has no drop zones and a human cannot drag anything onto the page. See the `fluid-portal-authoring` skill.
34
- - **Choose the right system before building.** A company portal widget runs in a locked-down worker with no network access, so it can only present Fluid's own data (via capabilities or data sources) or props. Anything needing an API key, OAuth, webhooks, or server-side work is a **Mist app** — a hosted app with its own `public_url`, surfaced through a droplet (identity/credentials) plus a placement record, and embedded by **the Mist's public URL**. The droplet is the app's identity, not the app. See the `fluid-portal-authoring` skill.
35
- - **Droplet widgets are registered widget types, not embeds.** Use `droplet.<scope>.<dropletId>.<Name>` with real props. Do not point an `EmbedWidget` at a droplet's `/embed` route — that is the `?dri=`-gated admin surface and will render blank.
36
- - **Do not infer that a widget type does not exist because an API did not list it.** Widget-related endpoints are scoped differently and legitimately return empty. When unsure of a `type` or prop shape, ask the human to drag the widget onto a scratch screen in the admin builder, then `pnpm pull` and read the truth.
37
- - Keep portal JSON valid and references consistent.
38
- - Preserve stable IDs and slugs unless the change intentionally renames them.
39
- - Keep the portal shell thin; do not fork SDK internals into this app.
40
- - Keep custom page registration in `src/portal.config.ts` and widget package source in `src/widgets.config.ts`.
41
- - For widget changes, use `pnpm widget:create <name>` / `fluid portal widget create <name>` and follow the copied `fluid-widget-authoring` skill before editing package definitions or schemas.
1
+ # Fluid portal project guidance
2
+
3
+ ## Task routing
4
+
5
+ - Use `.agents/skills/fluid-portal-authoring/SKILL.md` for portal JSON, custom
6
+ pages, preview, pull, diff, push, versions, activation, and shell deployment.
7
+ - Use `.agents/skills/fluid-widget-authoring/SKILL.md` for company or Droplet
8
+ Remote DOM widgets, property schemas, theme compliance, portal functions,
9
+ capabilities, and widget publication.
10
+
11
+ ## Sources of truth
12
+
13
+ - Each file under `portal/` follows its versioned `$schema`. The schema owns
14
+ exact fields, widget props, validation constraints, defaults, and examples.
15
+ - Installed SDK declarations and `node_modules/@fluid-app/portal-sdk/authoring/`
16
+ own exact public APIs.
17
+ - Installed CLI references own command syntax, options, and defaults.
18
+ - The skills provide task procedure, safety boundaries, verification, failure
19
+ handling, and recovery. Do not replace that guidance with copied field lists.
20
+ - `.portal-sync/`, `.fluid/`, and `dist/` are generated. Never edit them.
21
+
22
+ ## Repository and remote-state rules
23
+
24
+ - Preserve existing work. Inspect `git status` before pull, scaffold, format,
25
+ or generation commands.
26
+ - Keep screen, navigation, profile, and theme file slugs consistent with every
27
+ cross-resource reference.
28
+ - Pull can overwrite local portal JSON only when its force behavior is
29
+ explicitly selected. Preserve local work before resolving remote drift.
30
+ - Push changes the remote working definition. It does not make that definition
31
+ live unless activation is also requested.
32
+ - Activation, version creation, widget publication, and CDN deployment are
33
+ separate remote changes. Run only the operation authorized by the task.
34
+ - `--yes` skips ordinary confirmation. It does not approve network-enabled
35
+ widgets. Non-interactive approval requires `--allow-network-widgets`.
36
+ - Treat a multi-phase operation as partially complete until every phase result
37
+ is known. Never assume an earlier successful phase rolled back.
38
+ - Follow the portal theme for custom pages and widgets. Widget authors must use
39
+ semantic theme tokens and `colorSelect`; the legacy `color` field is
40
+ deprecated.
@@ -1,444 +1,53 @@
1
1
  ---
2
2
  name: fluid-portal-authoring
3
- description: Use when modifying this generated Fluid portal project through the supported pull/edit/push/version workflow, including portal/ JSON, themes, navigations, screens, profiles, local validation, and distinguishing portal definition sync from widget artifact deployment.
3
+ description: Use when changing Fluid portal screens, navigation, profiles, themes, definition JSON, preview behavior, draft state, or live versions in this project.
4
4
  ---
5
5
 
6
- # Fluid Portal Authoring
6
+ # Fluid portal authoring
7
7
 
8
- Use this skill before changing a generated Fluid portal project.
8
+ ## Decide the remote effect
9
9
 
10
- ## Supported authoring model
10
+ - Local only: edit, preview, validate, diff, and build.
11
+ - Working definition: pull reads remote state; push writes the remote draft.
12
+ - Live definition: version activation changes what end users receive.
13
+ - Runtime artifacts: portal deploy publishes company widget code.
14
+ - Hosted shell: the generated GitHub workflow uploads `dist/` to the CDN.
11
15
 
12
- This template is a Fluid portal shell plus a local copy of the Fluid OS portal definition.
13
- The supported workflow is:
16
+ These operations are independent. Do not substitute one for another.
14
17
 
15
- 1. Pull the remote portal definition into `portal/`.
16
- 2. Edit the pulled JSON files locally.
17
- 3. Validate and preview locally.
18
- 4. Push the JSON changes back to the remote working/draft definition.
19
- 5. Create and activate a version when the pushed definition should go live.
18
+ ## Workflow
20
19
 
21
- In generated portal projects, routes, portal structure, screens, themes, profiles, and definition metadata are owned by Fluid OS JSON under `portal/` after pull.
20
+ 1. Inspect `git status` and preserve existing work.
21
+ 2. Run `pnpm pull` unless the task intentionally starts from newer local JSON.
22
+ 3. Inspect the affected files and their versioned `$schema` values.
23
+ 4. Edit only the required resources under `portal/`.
24
+ 5. Validate cross-resource slugs, network grants, and theme compatibility. Run
25
+ the checks that cover the change, then inspect the diff.
26
+ 6. Run `pnpm push` only when remote draft changes are authorized.
27
+ 7. Create or activate a version only when a live release is authorized.
22
28
 
23
- ## Important files and directories
29
+ Do not edit `.portal-sync/`. Keep IDs, file slugs, navigation targets, theme
30
+ names, and profile references consistent.
24
31
 
25
- - `src/main.tsx`: portal shell bootstrap. Keep this small.
26
- - `src/portal.config.ts`: custom page registration for the portal SDK.
27
- - `src/widgets.config.ts`: company Remote DOM widget package source.
28
- - `src/index.css`: app-level CSS imports and global styles.
29
- - `portal/`: pulled Fluid OS definition JSON. This is the primary editing surface for portal content.
30
- - `.portal-sync/`: generated sync metadata used by pull/push diffing. Do not edit by hand.
31
- - `.fluidrc`: generated CLI profile binding for this portal project.
32
- - `.agents/skills/fluid-widget-authoring/SKILL.md`: Remote DOM widget authoring guidance when this portal owns widget packages.
32
+ Push is phased and can partially succeed. Read the reported phase and inspect
33
+ remote state before retrying. `--yes` does not approve widget network access;
34
+ use interactive approval or `--allow-network-widgets`. Activate only after all
35
+ push phases succeed and only when a live release is authorized.
33
36
 
34
- ## Pull before editing
37
+ ## References
35
38
 
36
- Run:
39
+ - Follow each portal JSON file's `$schema` for resource fields and widget props.
40
+ - [Portal JSON and resource relationships](references/portal-json.md)
41
+ - [Widgets and runtime choices](references/widgets-and-runtime.md)
42
+ - [Preview, synchronization, deployment, and recovery](references/workflows.md)
43
+ - [Installed portal API](../../../node_modules/@fluid-app/portal-sdk/authoring/portal-api/api.md)
44
+ - [Installed portal commands](../../../node_modules/@fluid-app/fluid-cli-portal/authoring/commands.md)
37
45
 
38
- ```bash
39
- pnpm pull
40
- ```
46
+ If an LSP is unavailable, use the installed API reference. The installed
47
+ reference and declarations match the SDK version in this project.
41
48
 
42
- Expected result:
49
+ Use `.agents/skills/fluid-widget-authoring/SKILL.md` when the task changes a
50
+ widget package rather than portal JSON.
43
51
 
44
- - `portal/` contains local JSON for Fluid OS resources such as screens, themes, navigations, profiles, and definition metadata.
45
- - `.portal-sync/` contains sync state used to compute future diffs.
46
- - `.fluidrc` pins the CLI profile when the project was created with a profile.
47
-
48
- Pull before making changes unless you intentionally want to overwrite local work. If local JSON and remote state may both have changed, inspect the diff before pushing.
49
-
50
- ## Edit pulled portal JSON
51
-
52
- Work inside `portal/` for portal definition changes.
53
-
54
- Guidelines:
55
-
56
- - Keep JSON valid and deterministic.
57
- - Preserve stable IDs, slugs, and cross-resource references unless intentionally changing them.
58
- - Update references together. If a navigation item points to a screen/theme/profile slug or ID, make sure the target exists in `portal/`.
59
- - Prefer small, reviewable edits. One portal content change per PR is easier to validate.
60
- - Do not hand-edit `.portal-sync/`; it is sync metadata, not source content.
61
- - Do not invent unsupported fields. Match the shapes produced by `pnpm pull`.
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
-
95
- ## Add a new page and put it in the menu
96
-
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/`.
98
-
99
- 1. Create `portal/screens/<slug>.json`: `{ "name": "Rewards", "component_tree": [ ... ] }`. Give every node an `id` and a widget `type` from the catalog below. The **filename is the resource slug**; an inline `slug` field is optional and is not what cross-resource resolution uses.
100
- 2. Add a navigation item to each profile that should see it. Local JSON uses the screen **slug**, not a numeric backend id: `{ "label": "Rewards", "slug": "rewards", "screen": "rewards", "source": "user", "position": 2, "children": [] }`. Use `children[]` for a nested group. Add it to the profile's `mobile_navigation` too for mobile.
101
- 3. Link to it with a `LinkWidget` in screen mode. Use the screen slug rather
102
- than a URL path so the portal shell handles navigation. `LinkWidget` props for the example above:
103
-
104
- ```json
105
- {
106
- "linkType": "screen",
107
- "screenSlug": "rewards"
108
- }
109
- ```
110
-
111
- Carousel slides use their separate `buttonLink` property; set that to the
112
- path `/rewards`.
113
-
114
- 4. Preview on the profile local dev serves (default/first), then push and activate a version to go live.
115
-
116
- ### Bootstrapping a brand-new empty definition
117
-
118
- `fluid portal pull` legitimately returns zero screens/themes/navigations/profiles for a newly created portal. Do not search for hidden starter resources and do not move the routes into `src/`. Create a complete minimal definition locally:
119
-
120
- `portal/screens/home.json`
121
-
122
- ```json
123
- {
124
- "name": "Home",
125
- "component_tree": [
126
- {
127
- "id": "ContainerWidget-home-root",
128
- "type": "ContainerWidget",
129
- "props": {
130
- "background": {
131
- "type": "solid",
132
- "color": "background"
133
- },
134
- "gapSize": "md",
135
- "padding": 4,
136
- "children": [
137
- {
138
- "id": "TextWidget-home-welcome",
139
- "type": "TextWidget",
140
- "columnIndex": 0,
141
- "props": {
142
- "title": "Welcome",
143
- "titleColor": "foreground",
144
- "description": "Your portal is ready.",
145
- "descriptionColor": "foreground",
146
- "background": {
147
- "type": "solid",
148
- "color": "background"
149
- },
150
- "padding": 4,
151
- "borderRadius": "md"
152
- }
153
- }
154
- ]
155
- }
156
- }
157
- ]
158
- }
159
- ```
160
-
161
- `portal/navigations/main.json` (create a parallel `mobile.json` with `"platform": "mobile"`)
162
-
163
- ```json
164
- {
165
- "name": "Main Navigation",
166
- "platform": "web",
167
- "navigation_items": [
168
- {
169
- "label": "Home",
170
- "slug": "home",
171
- "icon": "home",
172
- "position": 1,
173
- "screen": "home",
174
- "source": "user",
175
- "parent_id": null,
176
- "children": []
177
- }
178
- ]
179
- }
180
- ```
181
-
182
- `portal/profiles/default.json`
183
-
184
- ```json
185
- {
186
- "name": "Default Profile",
187
- "default": true,
188
- "permissions": {
189
- "countries": [],
190
- "ranks": [],
191
- "roles": [],
192
- "platform": []
193
- },
194
- "navigation": "main",
195
- "mobile_navigation": "mobile",
196
- "themes": []
197
- }
198
- ```
199
-
200
- Create additional screen files and add their slug to both navigations before previewing. `fluid portal push` creates files listed as new, records their backend mappings, then creates navigations and profiles in dependency order. “No mapping found” applies to a file incorrectly treated as changed/deleted, not to a valid new file.
201
-
202
- If you create `portal/themes/<slug>.json`, it must contain the complete
203
- `{ "name", "active", "config" }` shape. A theme with only `name` and
204
- `active` can appear harmless in local preview but the API rejects it. Copy a
205
- pulled theme's full `config` object and then change its `id`, `name`, and token
206
- values; do not invent a partial config.
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 has no network access by default
213
-
214
- Portal widgets run as Remote DOM packages inside a locked-down Web Worker. A widget can use standard worker `fetch` only when it declares `uses: [networkAccess]` and the portal author approves the warning. The grant is stored on the widget node and bound to the current package and capability versions. Existing network-enabled widgets retain consent when those versions change; adding network access to a widget that did not previously have it requires review.
215
-
216
- Fluid does not add credentials, tokens, cookies, or headers. Direct requests to `fluid.app`, the current portal origin, loopback, and private-network addresses are blocked. Native redirects are not inspected, and worker execution does not isolate the reputation of `*.fluid.app`; approval is therefore a package-trust decision. WebSocket, EventSource, WebTransport, and streaming-specific APIs remain unavailable.
217
-
218
- Without that declaration and grant, a portal widget's data comes from exactly three places:
219
-
220
- - props written into the screen JSON
221
- - built-in host capabilities — `account`, `store`, `products`, `content`, `mySite`, `todos`, `calendar`, `points`, `localization`, and friends
222
- - **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)
223
-
224
- ### A Mist app is the app; the droplet is its identity
225
-
226
- 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`**:
227
-
228
- | Record | What it is | Per mist |
229
- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------- |
230
- | **Droplet** | Identity and credentials — `FLUID_DROPLET_UUID` / `_SECRET` / `_WEBHOOK_AUTH_TOKEN`, OAuth scopes, install lifecycle and webhooks | one |
231
- | **Mobile embed** | A placement in the mobile app: `embed_url` plus a required cover image and height | many |
232
- | **Drop zone** | A placement in an admin page/zone slot | many |
233
-
234
- **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.
235
-
236
- ### The decision
237
-
238
- 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**.
239
- 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.
240
- 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.
241
-
242
- 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.
243
-
244
- ### Embedding a Mist app in a portal screen
245
-
246
- 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.
247
-
248
- **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.
249
-
250
- 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.
251
-
252
- ## Choosing a widget (recommend built-ins before custom code)
253
-
254
- Use the `type` value in a `component_tree` node. Reach for what already exists:
255
-
256
- - Layout: `ContainerWidget`, `LayoutWidget` (columns/grid), `NestedWidget`, `SpacerWidget`, `SeparatorWidget`
257
- - Hero/media: `CarouselWidget` (rotating hero slides w/ CTA), `ImageWidget`, `VideoWidget`
258
- - Content: `TextWidget`, `BulletListWidget`, `CardWidget`, `AlertWidget`, `TableWidget`, `ChartWidget`, `CalendarWidget`
259
- - Commerce/member: `ShopWidget`, `PointsWidget` (rewards balance), `RecentActivityWidget`, `ToDoWidget`
260
- - Links/sharing: `LinkWidget`, `QuickLinksWidget`, `QuickShareWidget`, `ListWidget`
261
- - Platform: `EmbedWidget` (iframes an arbitrary URL into a screen), `MySiteWidget`
262
-
263
- `TextWidget` renders its `title` and `description` as plain text. Do not put
264
- HTML in either field; markup is escaped and displayed literally.
265
-
266
- 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.
267
-
268
- ### Droplet widgets are real widget types, not embeds
269
-
270
- An installed droplet (UGC, and others) contributes **registered widget types**, addressed exactly like any other widget:
271
-
272
- ```json
273
- {
274
- "id": "…",
275
- "type": "droplet.ugc.drp_fuwamfg3licz1l4yocpkjos12t9vhcrh.MakeAVideoCta",
276
- "props": {
277
- "headline": "…",
278
- "eyebrow": "…",
279
- "ctaLabel": "…",
280
- "dri": "",
281
- "apiBaseUrl": "…"
282
- }
283
- }
284
- ```
285
-
286
- The shape is `droplet.<scope>.<dropletId>.<WidgetName>`, and the props are ordinary JSON the builder writes.
287
-
288
- **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.
289
-
290
- **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:
291
-
292
- | Endpoint | Why it looks empty |
293
- | ---------------------------------- | -------------------------------------------------------- |
294
- | `/api/droplets` (`app_extensions`) | Does not list contributed widget types |
295
- | `/__widget-packages__` | Dev-server route for _unpublished company_ packages only |
296
- | `/api/app/widget-packages` | Portal-session scoped; 404s for a CLI token |
297
- | `/api/company/mobile_widgets` | **This one lists installed droplet widgets** |
298
-
299
- **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.
300
-
301
- ## System screens and how to reach them
302
-
303
- 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.
304
-
305
- ## Validate locally
306
-
307
- Run the checks that match the change:
308
-
309
- ```bash
310
- pnpm typecheck
311
- pnpm lint
312
- pnpm build
313
- ```
314
-
315
- For portal JSON changes, know that validation is asymmetric:
316
-
317
- - **Local preview is lenient.** The dev server builds its manifest from `portal/` and silently skips malformed JSON files and silently drops navigation items whose `screen` reference does not resolve. A screen that "just vanished" from the preview with no error usually means broken JSON or a broken reference in the file you last edited.
318
- - **Push is strict.** `pnpm push` validates cross-references (navigation → screen, profile → navigation/theme) before writing anything and refuses the entire push if any reference is broken.
319
-
320
- Use local preview for visual/content changes:
321
-
322
- ```bash
323
- pnpm dev
324
- ```
325
-
326
- Expected result: the portal CLI pulls the definition when `portal/` is missing, then starts the shell against the local portal JSON. Use `pnpm dev` instead of bare `vite` so the pull and manifest preflight run.
327
-
328
- Two local-preview rules that read as bugs if you do not know them:
329
-
330
- - `fluid portal dev` injects `portalDevPlugin()` and `backendDevPlugin()` programmatically. Do not add either plugin to `vite.config.ts`; doing so creates scaffold drift and is unnecessary. If the command falls back to bare Vite, fix the dependency/config error that triggered the fallback instead of permanently wiring the plugin.
331
- - With no `VITE_API_URL` override, the CLI resolves the signed-in company and proxies `/api` to `https://<subdomain>.portal.fluid.app`. Portal member endpoints do **not** live on `api.fluid.app` or `<subdomain>.fluid.app`; repeated 404s from those hosts are a proxy-target error.
332
- - The local manifest always serves the profile with `default: true` (or the first profile file if none is default). Profile `permissions` are **not** evaluated locally — edits to any other profile will never appear in the local preview. Permission matching only happens on the deployed portal against the real logged-in member.
333
- - A direct localhost preview does **not** have a real portal-member session. The tenant BFF authenticates with an HttpOnly `portal_tenant_user_id` cookie created by the production/Rails handoff, not the Fluid CLI token. Local custom pages, navigation, and definition content still work, but Shop, Orders, Contacts, rep-only screens, and other member-data surfaces may return 401 or remain unauthenticated. A host fix that merely changes an error boundary into permanent skeletons is not successful authentication. Verify signed-in behavior through a real tenant handoff environment.
334
-
335
- ## Push definition changes
336
-
337
- Run:
338
-
339
- ```bash
340
- pnpm push
341
- ```
342
-
343
- What push does, in order (it stops at the first failing gate):
344
-
345
- 1. **Invisible git sync.** Push auto-commits the whole working tree (`git add -A`; `.gitignore` is the only exclusion boundary — keep secrets in `.env`, never in tracked files) and pushes to the portal's Fluid-provisioned git repository. A "Skipped git sync — <reason>" note is non-fatal and the content push continues. A _failed_ git push aborts the whole command with your changes committed locally — reconcile (usually `git pull --rebase`) and rerun.
346
- 2. **Snapshot diff.** Compares `portal/` against `.portal-sync/` state. "Nothing to push" means no local edits since the last pull/push.
347
- 3. **Cross-reference validation.** Refuses the entire push, before any write, if a navigation item references a missing screen or a profile references a missing navigation/theme.
348
- 4. **Phased write.** Screens and themes first, then navigations, then profiles. A failed phase skips later phases, and only successfully pushed files advance the snapshot — fix the error and rerun to push the remainder. "No mapping found for <type> slug" means the resource was never pulled/created remotely; re-pull.
349
-
350
- What push does not do:
351
-
352
- - It does not create a live Fluid OS version by itself.
353
- - It does not upload hosted portal shell assets from `dist/`.
354
- - It does not publish widget runtime artifacts.
355
-
356
- After a successful push, inspect the remote portal definition if possible and run the app locally or against the target environment.
357
-
358
- ## Publish a live portal version
359
-
360
- After pushing and verifying the working/draft definition, create and activate a version when the change should become live:
361
-
362
- ```bash
363
- pnpm exec fluid portal version create --activate
364
- ```
365
-
366
- Use this only when the pushed definition is ready for users. If activation should be coordinated with a release or content review, stop and ask the project owner before running it.
367
-
368
- For an explicitly approved non-interactive release, use
369
- `fluid portal push --yes --activate`. It performs the same two stages but
370
- activates only after every push phase succeeds; malformed or partially-pushed
371
- definitions exit non-zero and are not activated.
372
-
373
- ## Distinguish the three deploy/sync paths
374
-
375
- Do not mix these up:
376
-
377
- - `pnpm push` / `fluid portal push`: syncs `portal/` JSON to the remote working/draft Fluid OS definition.
378
- - `fluid portal version create --activate`: snapshots the remote working/draft definition and makes it live.
379
- - GitHub Actions or hosting deployment: uploads the built portal shell assets from `dist/`.
380
- - `fluid portal deploy`: publishes company-owned widget runtime artifacts. It is not the portal JSON push command and it is not the hosted shell asset deployment.
381
-
382
- ## Work on this portal from another machine
383
-
384
- The project's source lives in a Fluid-provisioned git repository (kept in sync by push). To continue work elsewhere:
385
-
386
- ```bash
387
- fluid login
388
- fluid portal clone <app-name>
389
- cd <app-name>
390
- fluid portal pull
391
- ```
392
-
393
- Clone mints its own short-lived git credentials — do not hand out raw repository URLs or tokens.
394
-
395
- ## Debug order when something looks wrong
396
-
397
- Work these in order and report which one failed:
398
-
399
- 1. Logged in? Auth errors say `Run fluid login first`.
400
- 2. Right directory? Push errors about missing `portal/` or `.portal-sync/` mean you are not in the pulled project (or never pulled).
401
- 3. Built-in screens returning 404? Confirm the proxy target is `<subdomain>.portal.fluid.app`, not `api.fluid.app` or `<subdomain>.fluid.app`.
402
- 4. Built-in screens returning 401 or permanent skeletons? Localhost lacks the portal handoff cookie; use a real tenant handoff environment for authenticated-member verification.
403
- 5. Local preview showing stale/missing definition content? Check for malformed JSON or broken references in the file you last edited (local preview drops them silently), and confirm you edited the profile the local preview serves (`default: true`).
404
- 6. Push refused? Read the cross-reference validation errors — they name the file and the missing slug.
405
- 7. Push partially failed? Fix the reported phase error and rerun; the snapshot only advanced for files that succeeded.
406
- 8. Users do not see the change? Push updates the draft only — create/activate a version.
407
- 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".
408
- 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`.
409
-
410
- ## Widget work inside a portal project
411
-
412
- Company-owned portal widgets are still supported. Use the widget scaffold command, then follow the copied `fluid-widget-authoring` skill for detailed manifest, property schema, theme token, validation, build, and publish rules.
413
-
414
- ```bash
415
- pnpm widget:create my-widget
416
- # or
417
- pnpm exec fluid portal widget create my-widget
418
- ```
419
-
420
- Short version:
421
-
422
- - Keep widget code under the scaffolded `src/widgets/<name>/` directory.
423
- - Keep widget metadata serializable.
424
- - Keep `defaultProps` aligned with property schema defaults.
425
- - Use semantic theme tokens.
426
- - Import runtime CSS from the widget build graph.
427
- - Validate/build widget artifacts before publishing them.
428
-
429
- ## Preflight checklist
430
-
431
- Before considering portal work complete:
432
-
433
- - [ ] Pulled the latest remote definition or intentionally worked from current local JSON.
434
- - [ ] Edited only supported files for the change.
435
- - [ ] Preserved JSON validity and cross-resource references.
436
- - [ ] Kept portal structure, routes, and content changes in pulled Fluid OS JSON under `portal/`.
437
- - [ ] Ran typecheck/lint/build or the closest available checks.
438
- - [ ] Ran push only when the local `portal/` diff was understood.
439
- - [ ] **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.
440
- - [ ] 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.
441
- - [ ] Any embed points at a Mist's public URL or a widget route, never at a droplet's `?dri=`-gated `/embed`.
442
- - [ ] Used registered widget `type` values for droplet/company widgets, not an `EmbedWidget` pointed at a droplet route.
443
- - [ ] Confirmed any uncertain widget `type` or prop shape against builder-authored JSON rather than inferring it.
444
- - [ ] Created/activated a version only when the definition should go live.
52
+ Use the project scripts and installed command reference to select checks for
53
+ the change.
@@ -0,0 +1,46 @@
1
+ # Portal JSON
2
+
3
+ ## Resource graph
4
+
5
+ Portal resources live under `portal/`. The file name is the resource slug used
6
+ by cross-resource references. An inline display name does not replace it.
7
+
8
+ - `screens/<slug>.json` defines a route and its widget tree.
9
+ - `navigations/<slug>.json` defines ordered web or mobile entries.
10
+ - `profiles/<slug>.json` selects navigation resources, themes, and member
11
+ permission filters.
12
+ - `themes/<slug>.json` contains the complete structured theme configuration.
13
+
14
+ A screen can contain zero or one root widget. For a populated screen that must
15
+ support builder child placement, select a registered container from the
16
+ current schema or builder catalog. Use the schema for its exact type and child
17
+ property; do not copy those contracts into this guide.
18
+
19
+ ## Navigation
20
+
21
+ A navigation entry uses the screen file slug. Follow the navigation file's
22
+ `$schema` for its exact fields, values, and examples.
23
+
24
+ Navigation controls discoverability; it is not the only route resolver. A
25
+ valid screen can still be opened directly by its slug when its route and
26
+ profile gates allow it. Preview preserves an unresolved navigation entry but
27
+ cannot attach its screen ID. Push rejects invalid references.
28
+
29
+ Do not maintain a system-screen slug list in guidance. Select system screens
30
+ from the current builder or generated screen-picker contract, then pull the
31
+ definition and preserve the emitted slug and access rules.
32
+
33
+ ## Widgets
34
+
35
+ The widget schema owns the complete node contract, including the distinction
36
+ between built-in and third-party widget properties. Confirm a widget and its
37
+ properties from the current schema, catalog, or builder-authored JSON instead
38
+ of copying any part of that contract here or inferring it from an empty API
39
+ response.
40
+
41
+ ## Empty definitions and themes
42
+
43
+ An empty definition can have no screens, themes, navigations, or profiles.
44
+ Build a complete resource graph before push. Copy a pulled theme when the full
45
+ current shape is needed; a partial config is invalid. The theme file's schema
46
+ owns its exact fields and token structure.