@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.
- package/authoring/commands.md +242 -0
- package/dist/{backend-dev-plugin-20fLk-4t.mjs → backend-dev-plugin-z9IZCHaY.mjs} +2 -2
- package/dist/{backend-dev-plugin-20fLk-4t.mjs.map → backend-dev-plugin-z9IZCHaY.mjs.map} +1 -1
- package/dist/index.d.mts +571 -423
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +17 -15
- package/dist/index.mjs.map +1 -1
- package/dist/{portal-dev-plugin-5BbRo9QT.mjs → portal-dev-plugin-2jZODDsX.mjs} +2 -2
- package/dist/{portal-dev-plugin-5BbRo9QT.mjs.map → portal-dev-plugin-2jZODDsX.mjs.map} +1 -1
- package/dist/{portal-widget-dev-plugin-NBHcnvFJ.mjs → portal-widget-dev-plugin-Dyp2bVy7.mjs} +3 -3
- package/dist/{portal-widget-dev-plugin-NBHcnvFJ.mjs.map → portal-widget-dev-plugin-Dyp2bVy7.mjs.map} +1 -1
- package/dist/{pull-BlG6pMwC.mjs → pull-BWnby_pY.mjs} +6287 -2908
- package/dist/{pull-BlG6pMwC.mjs.map → pull-BWnby_pY.mjs.map} +1 -1
- package/dist/{src-2znVq55R.mjs → src-CQ24GVe6.mjs} +7 -1
- package/dist/src-CQ24GVe6.mjs.map +1 -0
- package/dist/vite-plugin.mjs +4 -4
- package/package.json +6 -5
- package/templates/base/.gitignore.template +3 -0
- package/templates/base/AGENTS.md +40 -41
- package/templates/base/skills/fluid-portal-authoring/SKILL.md +37 -428
- package/templates/base/skills/fluid-portal-authoring/references/portal-json.md +46 -0
- package/templates/base/skills/fluid-portal-authoring/references/widgets-and-runtime.md +57 -0
- package/templates/base/skills/fluid-portal-authoring/references/workflows.md +56 -0
- package/templates/starter/.env.example +9 -0
- package/templates/starter/README.md.template +99 -169
- package/dist/src-2znVq55R.mjs.map +0 -1
package/templates/base/AGENTS.md
CHANGED
|
@@ -1,41 +1,40 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
|
6
|
+
# Fluid portal authoring
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## Decide the remote effect
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
The supported workflow is:
|
|
16
|
+
These operations are independent. Do not substitute one for another.
|
|
14
17
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
+
Do not edit `.portal-sync/`. Keep IDs, file slugs, navigation targets, theme
|
|
30
|
+
names, and profile references consistent.
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
##
|
|
37
|
+
## References
|
|
35
38
|
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
49
|
+
Use `.agents/skills/fluid-widget-authoring/SKILL.md` when the task changes a
|
|
50
|
+
widget package rather than portal JSON.
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
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.
|