@unoverse-platform/studio 0.1.26 → 0.1.28
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/package.json +1 -1
- package/screens/AtomsView.tsx +40 -2
- package/screens/ComponentsView.tsx +112 -21
- package/screens/NativeAppHost.tsx +8 -4
- package/screens/TemplatesView.tsx +51 -21
- package/screens/states.ts +27 -4
- package/vendor/sdk/appEngine.tsx +20 -4
- package/vendor/sdk/lib/UnoverseComponent.tsx +8 -2
- package/vendor/sdk/lib/core/actions.ts +10 -6
- package/vendor/sdk/lib/core/client.ts +39 -12
- package/vendor/sdk/lib/core/index.ts +21 -0
- package/vendor/sdk/lib/core/refs.ts +117 -0
- package/vendor/sdk/lib/core/store.ts +65 -38
- package/vendor/sdk/lib/core/templates.ts +270 -0
- package/vendor/sdk/lib/core/types.ts +27 -7
- package/vendor/sdk/lib/orb/ThinkingOrb.tsx +131 -0
- package/vendor/sdk/lib/orb/engine/braid.ts +50 -0
- package/vendor/sdk/lib/orb/engine/core.ts +167 -0
- package/vendor/sdk/lib/orb/engine/lattice.ts +205 -0
- package/vendor/sdk/lib/orb/engine/morph.ts +138 -0
- package/vendor/sdk/lib/orb/engine/orbits.ts +80 -0
- package/vendor/sdk/lib/orb/engine/profiles.ts +162 -0
- package/vendor/sdk/lib/orb/engine/registry.ts +31 -0
- package/vendor/sdk/lib/orb/engine/ribbon.ts +93 -0
- package/vendor/sdk/lib/orb/engine/rings.ts +239 -0
- package/vendor/sdk/lib/orb/engine/types.ts +14 -0
- package/vendor/sdk/lib/orb/engine/web.ts +98 -0
- package/vendor/sdk/lib/orb/index.ts +20 -0
- package/vendor/sdk/lib/orb/presets.ts +181 -0
- package/vendor/sdk/lib/orb/theme.ts +83 -0
- package/vendor/sdk/lib/orb/types.ts +84 -0
- package/vendor/sdk/lib/primitives.tsx +116 -2
- package/vendor/sdk/lib/render.tsx +32 -8
- package/vendor/sdk/lib/style.ts +6 -2
- package/vendor/sdk/lib/template.tsx +86 -224
- package/vendor/skills/unoverse-create/SKILL.md +18 -18
- package/vendor/skills/unoverse-create/references/agent-skill.md +12 -12
- package/vendor/skills/unoverse-create/references/block.md +5 -5
- package/vendor/skills/unoverse-create/references/component.md +150 -75
- package/vendor/skills/unoverse-create/references/node.md +1 -1
- package/vendor/skills/unoverse-create/references/template.md +142 -82
- package/vendor/skills/unoverse-create/references/workflow.md +15 -15
|
@@ -1,116 +1,186 @@
|
|
|
1
|
-
# Playbook
|
|
1
|
+
# Playbook: Components & Atoms (rx data)
|
|
2
2
|
|
|
3
3
|
**Read first:** `docs/design/components.md` (the microapp anatomy) + `docs/design/state.md`
|
|
4
|
-
(the
|
|
5
|
-
`UNOVERSE_STATE_MODEL.md` §
|
|
6
|
-
|
|
4
|
+
(the state model). Deep law: [UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3 (three homes),
|
|
5
|
+
`UNOVERSE_STATE_MODEL.md` §5 (the six rules), `UNOVERSE_LAYERS.md` (states own layouts).
|
|
6
|
+
**Study the exemplar before writing**: `rx/sab/components/product-card` is the canonical
|
|
7
|
+
State Model v2 component; mirror it exactly.
|
|
7
8
|
|
|
8
9
|
## The anatomy
|
|
9
10
|
|
|
10
11
|
```
|
|
11
|
-
apps/unoverse/rx
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
states/ # the component's PRIVATE steps (wizard questions) — listed in stateOrder
|
|
19
|
-
components/ # component-local shared partials (earned: 2+ states share a shape)
|
|
12
|
+
apps/unoverse/rx/<project>/components/<name>/
|
|
13
|
+
<name>.yaml # ROOT: envelope (name/category/nodeSize/outputs/props) + the STATE TREE + root
|
|
14
|
+
manifest.yaml # RENDER CONTRACT + discovery meta (title / description ≤120 /
|
|
15
|
+
# whenToUse utterance-shaped). Presence = spatially discoverable;
|
|
16
|
+
# the envelope NEVER duplicates the meta.
|
|
17
|
+
layouts/ # the DRAWINGS: one file per state that owns an arrangement
|
|
18
|
+
components/ # component-local shared partials (earned: 2+ layouts share a shape)
|
|
20
19
|
```
|
|
21
20
|
|
|
22
|
-
A **flat component** (simple card/chart) is just `<name>.
|
|
23
|
-
no folders. Structure is EARNED; start flat.
|
|
21
|
+
A **flat component** (simple card/chart) is just `<name>.yaml` + `root`: one drawing, no
|
|
22
|
+
manifest, no folders. Structure is EARNED; start flat.
|
|
24
23
|
|
|
25
|
-
## The
|
|
24
|
+
## The state model (v2): data + a state tree
|
|
25
|
+
|
|
26
|
+
A component is **data plus a state tree**; each state owns the layout(s) that draw it.
|
|
27
|
+
The root's `state:` block IS the design, readable top to bottom. The canonical shape
|
|
28
|
+
(`rx/sab/components/product-card/product-card.yaml`):
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
state:
|
|
32
|
+
view: # the PUBLIC axis
|
|
33
|
+
initial: products
|
|
34
|
+
states:
|
|
35
|
+
products: {} # no layout key: same-name default, {} is complete
|
|
36
|
+
detail: # the full page
|
|
37
|
+
layout: product # declared ONLY because the filename differs
|
|
38
|
+
on: step # a PRIVATE axis: the steps inside detail
|
|
39
|
+
initial: detail
|
|
40
|
+
states:
|
|
41
|
+
detail: { layout: product-detail }
|
|
42
|
+
apply: { layout: product-apply }
|
|
43
|
+
root:
|
|
44
|
+
type: Box
|
|
45
|
+
children:
|
|
46
|
+
- type: Switch
|
|
47
|
+
on: view # the root switches on `view`, ONE case active
|
|
48
|
+
cases:
|
|
49
|
+
products: { $include: layouts/products }
|
|
50
|
+
detail: { $include: layouts/product }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The rules, in the order they bite:
|
|
54
|
+
|
|
55
|
+
- **The axis is `view`.** The component's public state lives in its slice under `view`.
|
|
56
|
+
The root is a `Switch on view`; buttons transition with
|
|
57
|
+
`setValue { values: [{ key: view, value: detail }] }`. That write is the ENTIRE
|
|
58
|
+
transition machinery: no guards, no events, no per-component machines.
|
|
59
|
+
- **Public states are the top level of the `view` axis** and they are the template's
|
|
60
|
+
WHOLE vocabulary: everything templates can react to, nothing more. Steps nest
|
|
61
|
+
privately (`on: <key>` + `initial` + `states`), invisible outside the component.
|
|
62
|
+
The placement rule: **if the template must rearrange to show it, make it public;
|
|
63
|
+
if the template wouldn't move, nest it privately.** Privacy is the default;
|
|
64
|
+
promotion is deliberate.
|
|
65
|
+
- **Each state owns its layout(s).** `layout:` is OPTIONAL: a state with no declaration
|
|
66
|
+
draws the layout of its own name, and `{}` is a complete state. Write `layout:` only
|
|
67
|
+
when the filename differs. A layout is never addressable on its own; only its owning
|
|
68
|
+
state selects it. Name states for what they MEAN (`detail`), never for their drawing.
|
|
69
|
+
- **Shell vs steps.** A state's own layout is its SHELL, always on while the state is
|
|
70
|
+
active. Its nested substates are the STEPS, the only choices inside it, addressed by
|
|
71
|
+
state names (layout filenames are plumbing).
|
|
72
|
+
- **Variants**: one meaning, several arrangements:
|
|
73
|
+
`focused: { layouts: { horizontal: focused-h, vertical: focused-v } }`. Variants bind
|
|
74
|
+
the SAME fields; anything that changes meaning or bound data is a second state
|
|
75
|
+
(lint-enforced). The component picks its variant by the space it finds itself in
|
|
76
|
+
(container queries); templates influence it only by controlling the container.
|
|
77
|
+
- **Arrival: nobody sends a state.** The host template places the spawned actor: it
|
|
78
|
+
honors the component's declared `initial` when it has a matching state; otherwise it
|
|
79
|
+
scans its own states in declared order and takes the first name on the component's
|
|
80
|
+
public menu; no overlap and the component wakes in its own initial chain and renders
|
|
81
|
+
inline. The server compiles the tree to the public menu + initials it serves
|
|
82
|
+
(`publicStates`, `initialView`). Post-spawn, the template NEVER writes a component's
|
|
83
|
+
state.
|
|
84
|
+
- **Unmatched view = inline. Always.** An ignored public state does not stay put, it
|
|
85
|
+
FALLS INLINE in the flow. A step that must never move the page belongs in the nest,
|
|
86
|
+
not on the public menu.
|
|
87
|
+
- Legacy: v1's `defaultState` field and layout-name-sync are superseded; the SDK reads
|
|
88
|
+
`defaultState` only as a migration alias for `view`. New authoring writes `view`.
|
|
89
|
+
|
|
90
|
+
## The rules (Studio's publish lint enforces all of these; 0 errors required)
|
|
26
91
|
|
|
27
92
|
1. **Three homes for everything it shows** (AUTHORING §3):
|
|
28
93
|
- static content → **hardcoded literals** in the layout (`value`, literal `items: []` on
|
|
29
|
-
Each, `src`)
|
|
94
|
+
Each, `src`), never props, never `state`;
|
|
30
95
|
- internal view-state → the **`state` block, SCALARS ONLY** (`step`, `phase`,
|
|
31
|
-
`progressPct`)
|
|
96
|
+
`progressPct`): an array/object/URL in `state` is slop (workflow data → props;
|
|
32
97
|
static → hardcode);
|
|
33
98
|
- workflow-fed data → **`props` with `input: true`** (the `default` is the preview mock).
|
|
34
99
|
Usually empty.
|
|
35
|
-
- **Prop names = the data contract
|
|
100
|
+
- **Prop names = the data contract. Never invent them.** Source data hydrates props
|
|
36
101
|
BY NAME (as-is, no mapping); a name the source doesn't carry silently renders the
|
|
37
102
|
preview default (tell: title streams, image stays mock). Content-attached cards use
|
|
38
103
|
the writer vocabulary: `title` `tagline` `description` `bodyCopy` `introParagraph`
|
|
39
104
|
`primaryImage` `images` `link` `callToAction`. Guard:
|
|
40
105
|
`server/src/runtime/content-card-hydration.test.ts`; law:
|
|
41
106
|
`UNOVERSE_MCP_TEMPLATE_PROTOCOL.md` §Content-attached cards.
|
|
42
|
-
2. **Root = `Switch on
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
**Lifetime (OPTIONAL manifest flag)** — `"lifetime": "conversation"` marks a durable,
|
|
107
|
+
2. **Root = `Switch on view`**, cases per PUBLIC state, each `$include` of the state's
|
|
108
|
+
layout (the tree above). Keep an `inline` state for any component that lives in a
|
|
109
|
+
conversation flow: the flow is its placeholder there. One EXCEPTION is the
|
|
110
|
+
**single-state (permanent) component**: exactly ONE public state, NO inline. It
|
|
111
|
+
arrives in its surface, cannot leave it (no ✕, no other state), and is enriched in
|
|
112
|
+
place via data merges to one stable instance, e.g. a continuously-updated page
|
|
113
|
+
living in a template's focus state. Permanence is by construction, never by a
|
|
114
|
+
pinning rule.
|
|
115
|
+
**Lifetime (OPTIONAL manifest flag).** `"lifetime": "conversation"` marks a durable,
|
|
52
116
|
conversation-scoped surface (a cart, an itinerary, a composed page): the platform keys
|
|
53
|
-
its instance by the CONVERSATION (every re-call hydrates the SAME slice
|
|
117
|
+
its instance by the CONVERSATION (every re-call hydrates the SAME slice: merge, not
|
|
54
118
|
re-place) and the new-turn reset skips it, so it stays on screen until it is replaced,
|
|
55
119
|
closes itself, or a NEW TEMPLATE loads (the template swap is the hard refresh boundary).
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
120
|
+
Durable slices also opt out of `cancelBelow` retraction when a higher template state
|
|
121
|
+
takes over. Default `"turn"`: the instance returns to inline / retires on the next
|
|
122
|
+
user turn. Values are a closed set: `turn | conversation` (lint-checked; docs/design/04).
|
|
123
|
+
3. **State is local; the `view` is the interface** (STATE_MODEL §5). Private axes
|
|
124
|
+
(`step`, `phase`, …) are the component's own business; the template never sees them.
|
|
125
|
+
The only thing that crosses is the public `view`. The component writes ONLY its own
|
|
126
|
+
slice (`setValue`): expand = `setValue { view: "detail" }`; its expanded layout
|
|
127
|
+
carries its own ✕ back down. When a hosting template has a state of that name, the
|
|
128
|
+
instance **lifts out of the flow into that state's slot**. The SDK renders each
|
|
129
|
+
instance in exactly ONE placeholder, so you never hide a flow copy yourself (no
|
|
130
|
+
`hideBelow`, no overlay hack). ❌ NEVER `setTemplateValue` to open a surface
|
|
131
|
+
(the deprecated bridge, linted). ❌ Never guard a layout against its own state:
|
|
132
|
+
the tree owns "which state is active"; a layout re-checking its discriminant is
|
|
133
|
+
the double-guard smell the model removes.
|
|
134
|
+
4. **A new layout file is earned only by a different arrangement** (or a different set
|
|
135
|
+
of bound fields). Same shape, different values = ONE state, data-driven: seven wizard
|
|
136
|
+
questions sharing one shape are one `question` state whose data changes, never seven
|
|
137
|
+
files (LAYERS §4, §7).
|
|
138
|
+
5. **Atoms** (`rx/atoms/`, via `Ref`) are for shapes shared across components, authoring-time
|
|
69
139
|
only (the server expands them before serving; no Studio view). `Ref props` remaps FIELDS;
|
|
70
140
|
**`Ref with` passes LITERALS**: `{ "type": "Ref", "ref": "button", "with": { "label":
|
|
71
|
-
"Learn more", "icon": "arrowRight" }, "action": { … } }
|
|
141
|
+
"Learn more", "icon": "arrowRight" }, "action": { … } }`. A bind whose field is a `with`
|
|
72
142
|
key becomes a hardcoded attribute, a truthy `with` key drops a matching `visibleWhen`
|
|
73
143
|
guard (unprovided ⇒ that piece stays hidden), `{{key}}` style bindings take the value.
|
|
74
144
|
⚠ Icon: literal = `icon: "phone"`; bound = `bind: { name: field }`.
|
|
75
|
-
6. **Briefs
|
|
76
|
-
sits ON the node that renders what it describes, next to its `bind
|
|
145
|
+
6. **Briefs: components an AI fills** (docs/design/03 §Briefed components; deep law: [UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3b). A `brief`
|
|
146
|
+
sits ON the node that renders what it describes, next to its `bind`, never in the
|
|
77
147
|
manifest, never a separate file:
|
|
78
148
|
- bound element → `"brief": { "description": "…", "maxLength": 60 }`
|
|
79
149
|
- Each → `"brief": { "description": "…", "minItems": 3, "maxItems": 5 }` (its template's
|
|
80
|
-
binds define the item shape
|
|
81
|
-
-
|
|
150
|
+
binds define the item shape; brief those fields too)
|
|
151
|
+
- layout/partial ROOT (no bind) → a plain-string brief = composition context
|
|
82
152
|
(ordering, refinement rules like "update, never discard").
|
|
83
153
|
The platform compiles briefs into the component's MCP tool schema (keys pass through
|
|
84
|
-
verbatim
|
|
85
|
-
spatial search results
|
|
154
|
+
verbatim; the brief uses JSON Schema's OWN vocabulary) and injects the grounding law (fill ONLY from
|
|
155
|
+
spatial search results, never invent). The server REFEREES (invalid/empty args →
|
|
86
156
|
instructive result, no render) and MIRRORS (success result returns the rendered page
|
|
87
|
-
for surgical refinement). **The schema IS the instruction channel
|
|
88
|
-
|
|
89
|
-
closed + linted. Pairs with the single-
|
|
157
|
+
for surgical refinement). **The schema IS the instruction channel.**
|
|
158
|
+
Write briefs, never side-channel prompts/skills for filling a component. Shape is
|
|
159
|
+
closed + linted. Pairs with the single-state permanent pattern (rule 2) for
|
|
90
160
|
continuously-enriched pages; exemplar: `rx/components/perfectday`.
|
|
91
161
|
7. **Tokens only** (LAW 1) + closed style keys + real space-scale steps
|
|
92
|
-
(`docs/design/06`)
|
|
93
|
-
8. **Lifecycle handlers
|
|
162
|
+
(`docs/design/06`): an invented step is silently broken CSS.
|
|
163
|
+
8. **Lifecycle handlers: a component's own server-side code** (deep law:
|
|
94
164
|
[UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3c). When a component needs live data from an
|
|
95
165
|
API or a computation (not from an AI = brief, not from the workflow = `input` props), it
|
|
96
166
|
may bring a small server-side script that runs when the component renders. It's for DATA,
|
|
97
167
|
not styling or logic-in-the-def.
|
|
98
168
|
- **Opt in via the manifest** (authorizes execution; auditable): `"lifecycle":
|
|
99
|
-
["onStart"]` in
|
|
100
|
-
- **The code is a sibling file named for the lifecycle
|
|
101
|
-
beside
|
|
169
|
+
["onStart"]` in the manifest. A handler file with no opt-in is INERT.
|
|
170
|
+
- **The code is a sibling file named for the lifecycle**: `onStart` → `onstart.js`,
|
|
171
|
+
beside the root file. Its **default export** is the handler.
|
|
102
172
|
- **`onStart`** is the only lifecycle today; it runs SERVER-SIDE when the instance is
|
|
103
173
|
created (the MCP render path), and whatever it returns is **merged into the instance
|
|
104
|
-
by prop name** (unknown keys inert
|
|
174
|
+
by prop name** (unknown keys inert, same contract as workflow-fed props). Return only
|
|
105
175
|
the fields you're filling; **never overwrite a curated prop** you didn't intend to.
|
|
106
176
|
- **`ctx` is a bounded sandbox**: `ctx.props` (the instance's current data),
|
|
107
177
|
`ctx.instanceId`, `ctx.env(name)` (server config). The **API key comes from
|
|
108
|
-
`ctx.env`, NEVER hardcoded
|
|
178
|
+
`ctx.env`, NEVER hardcoded**: `rx/` is a shared design folder; a secret in it leaks.
|
|
109
179
|
- Self-contained (the external call lives in the handler); errors never break the render.
|
|
110
|
-
Exemplar: `rx/
|
|
180
|
+
Exemplar: `rx/yasisland/components/restaurantcard/onstart.js` (Google Maps details).
|
|
111
181
|
|
|
112
182
|
```js
|
|
113
|
-
// onstart.js
|
|
183
|
+
// onstart.js: manifest opts in with "lifecycle": ["onStart"]
|
|
114
184
|
export default async function onStart(ctx) {
|
|
115
185
|
const key = ctx.env("SEARCHAPI_KEY"); // secret from server config
|
|
116
186
|
const res = await fetch(`https://…&q=${encodeURIComponent(ctx.props.title)}&api_key=${key}`);
|
|
@@ -119,34 +189,39 @@ no folders. Structure is EARNED; start flat.
|
|
|
119
189
|
}
|
|
120
190
|
```
|
|
121
191
|
|
|
122
|
-
## Discovery meta
|
|
192
|
+
## Discovery meta: write it to RANK (docs/nodes/node-discoverability.md)
|
|
123
193
|
|
|
124
194
|
A component with a manifest is an **MCP app**: spatial embeds `` `title. whenToUse||description
|
|
125
195
|
[category]` `` and ranks it against the USER'S OWN WORDS (`findIntent`). The meta is the
|
|
126
|
-
selection text, not documentation
|
|
196
|
+
selection text, not documentation. Weak meta makes a working component invisible. The
|
|
127
197
|
strict contract (doc 14, applies verbatim):
|
|
128
198
|
|
|
129
|
-
- **`title`** = the thing itself, human display name
|
|
130
|
-
- **`description`** = what it IS, ONE line ≤120 chars
|
|
199
|
+
- **`title`** = the thing itself, human display name, no mechanism, no org prefix.
|
|
200
|
+
- **`description`** = what it IS, ONE line ≤120 chars, the listing subtitle; never
|
|
131
201
|
"use when…" inside it.
|
|
132
202
|
- **`whenToUse`** = the selection text, **utterance-shaped**: the words a user would
|
|
133
|
-
actually say, outcome first
|
|
203
|
+
actually say, outcome first; the OPENING words dominate the embedding. Never
|
|
134
204
|
selector-shaped dev framing ("Pick when the user asks…"), never layout/mechanism first
|
|
135
|
-
("A single-
|
|
205
|
+
("A single-state page that…" ranks near rendering concepts and loses every intent query).
|
|
136
206
|
- **Disqualify by PROPERTY, never by naming a sibling** ("Not for answering a single
|
|
137
|
-
question, not for one item on its own")
|
|
207
|
+
question, not for one item on its own"). Naming rivals dates and tangles; the ranking
|
|
138
208
|
surfaces the alternative. A fallback/home surface never enumerates siblings' jobs
|
|
139
209
|
(the generalist trap).
|
|
140
210
|
- **`category`** = the domain of the JOB (Travel, Payments…), not the implementation.
|
|
141
211
|
- Meta embeds **AS-IS** (no LLM rewrite); editing `whenToUse` re-embeds on the next train.
|
|
142
212
|
|
|
143
213
|
Self-test before shipping: write the sentence the user would actually say to want this
|
|
144
|
-
component
|
|
214
|
+
component. Its key nouns/verbs must appear in `whenToUse`'s first sentence.
|
|
145
215
|
|
|
146
216
|
## Workflow
|
|
147
217
|
|
|
148
|
-
1. Study
|
|
149
|
-
2.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
218
|
+
1. Study `rx/sab/components/product-card`; copy its folder shape.
|
|
219
|
+
2. Declare the state tree first: public states on top, steps nested, each state owning
|
|
220
|
+
its layout (same-name default, `layout:` only when the filename differs).
|
|
221
|
+
3. Write the envelope + manifest + layouts; root = `Switch on view`; every shown thing
|
|
222
|
+
in its ONE home; transitions are `setValue` writes to `view` (or a private axis).
|
|
223
|
+
4. Publish from Studio; its lint must report 0 errors (the rules are v2-aware and cite
|
|
224
|
+
the doc for every rule).
|
|
225
|
+
5. Restart unoverse → Studio: mock (prop defaults + the state switcher reads the tree:
|
|
226
|
+
public states on top, steps beneath by state name), then live. Debug: stream log →
|
|
227
|
+
state inspector → definition; never guess.
|
|
@@ -161,7 +161,7 @@ the manifest describes the real service rather than something merely coherent.
|
|
|
161
161
|
returns: "return response.items.map(helpers.row)"
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
-
A helper sees ONLY its arguments and its sibling helpers
|
|
164
|
+
A helper sees ONLY its arguments and its sibling helpers, never `config`, `credentials`
|
|
165
165
|
or the caller's scope. Same sandbox, no extra authority.
|
|
166
166
|
|
|
167
167
|
## Do not
|
|
@@ -1,115 +1,175 @@
|
|
|
1
|
-
# Playbook
|
|
1
|
+
# Playbook: Templates (MCP apps)
|
|
2
2
|
|
|
3
3
|
**Read first:** `docs/design/templates.md` (layouts + manifest-as-envelope) +
|
|
4
|
-
`docs/design/state.md` (the
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
`docs/design/state.md` (the state model). Deep law: `UNOVERSE_STATE_MODEL.md` §5
|
|
5
|
+
(the six rules), `UNOVERSE_LAYERS.md`. Everything in the component playbook (closed
|
|
6
|
+
vocabulary, tokens, scale steps) applies here too. **Study the exemplar before
|
|
7
|
+
writing**: `rx/sab/templates/sab-chat-layout` is the canonical State Model v2 template.
|
|
8
|
+
|
|
9
|
+
## THE model: the component drives; the template reacts by name
|
|
10
|
+
|
|
11
|
+
A component is a state tree whose public states are its whole interface; a template
|
|
12
|
+
declares its OWN state tree in the manifest and reacts to components by name:
|
|
13
|
+
|
|
14
|
+
> A component enters a public view → the template asks "do I have a state with that
|
|
15
|
+
> name?" Match: the template enters that state and its layout draws. No match: the
|
|
16
|
+
> component renders inline in the flow. Always.
|
|
17
|
+
|
|
18
|
+
Derived off the store, never stored, no wiring. The component never knows templates
|
|
19
|
+
exist, and post-spawn the template NEVER writes a component's state. Name-match is the
|
|
20
|
+
default convention (`reactsTo:` on a template state is the rare escape hatch for
|
|
21
|
+
vocabulary mismatches). **Rule of thumb: component causes it = a reaction state;
|
|
22
|
+
template knows it = a base substate or template state.**
|
|
23
|
+
|
|
24
|
+
## The manifest states tree: one declaration, everything follows
|
|
25
|
+
|
|
26
|
+
The canonical shape (`rx/sab/templates/sab-chat-layout/manifest.yaml`):
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
states:
|
|
30
|
+
main: # the base arrangement, FIRST
|
|
31
|
+
states:
|
|
32
|
+
welcome: {} # contained substate; first substate = the landing default
|
|
33
|
+
focus: {} # then the reaction ladder, in priority order
|
|
34
|
+
products: {}
|
|
35
|
+
detail: {}
|
|
36
|
+
```
|
|
18
37
|
|
|
19
|
-
|
|
38
|
+
- **Top-level order IS the priority ladder**, base first. The template can be in ONE
|
|
39
|
+
state at a time: it walks the list top-down and enters the first of its states that
|
|
40
|
+
ANY hosted component currently matches. The ladder is DERIVED from the tree; never
|
|
41
|
+
author `stateOrder` (that field survives only as the legacy form for templates
|
|
42
|
+
without a tree).
|
|
43
|
+
- **Nesting IS containment, enforced by compilation.** A declared base substate
|
|
44
|
+
(`welcome`) is stripped from every non-base arrangement; no hand-written guard ever
|
|
45
|
+
polices it. The substate's file keeps its own condition (`visibleWhen: isEmpty`).
|
|
46
|
+
- **The shell rule.** Base content drawn in every arrangement is NOT declared as a
|
|
47
|
+
substate. sab's conversation timeline is main's own shell, included by the shared
|
|
48
|
+
chat chrome and legitimately drawn beside the rail, the page and under focus;
|
|
49
|
+
declaring it would contain it to `main` and empty the chat column in every reaction
|
|
50
|
+
state. Only states the template must scope get declared.
|
|
51
|
+
- **Arrival honors the component's declared initial** when the template has a matching
|
|
52
|
+
state; otherwise the template scans its own states in declared order and takes the
|
|
53
|
+
first name on the component's public menu; no overlap and the component rides inline.
|
|
54
|
+
- **A higher state CANCELS the claims below it**: while active, hosted components in
|
|
55
|
+
lower-ranked states retract to inline (durable `lifetime: "conversation"` slices opt
|
|
56
|
+
out), so releasing it lands on the BASE arrangement, never a stale lower one.
|
|
57
|
+
- Ties (two components in the same view): most recent write wins (`limit: 1`).
|
|
58
|
+
- **No word is special.** `focus` outranks `products` by list position only; the engine
|
|
59
|
+
matches names and walks the list. Even priority is data.
|
|
60
|
+
- The template's own condition-guarded moods (a welcome hero on an empty conversation)
|
|
61
|
+
are private substates of the base, never ladder entries: the template does not
|
|
62
|
+
rearrange between them.
|
|
63
|
+
|
|
64
|
+
## The anatomy: manifest-only (NO `<name>.yaml` envelope)
|
|
20
65
|
|
|
21
66
|
```
|
|
22
|
-
apps/unoverse/rx
|
|
23
|
-
manifest.
|
|
24
|
-
layouts/ # the ARRANGEMENTS
|
|
25
|
-
|
|
26
|
-
products.
|
|
27
|
-
|
|
67
|
+
apps/unoverse/rx/<project>/templates/<name>/
|
|
68
|
+
manifest.yaml # THE ENVELOPE: the whole app contract, incl. the states tree
|
|
69
|
+
layouts/ # the ARRANGEMENTS: one per state that owns one (same-name default)
|
|
70
|
+
main.yaml # the base: the core alone
|
|
71
|
+
products.yaml # state "products" → core + cards rail
|
|
72
|
+
detail.yaml # state "detail" → core + full record page
|
|
28
73
|
components/ # template-local partials (core, header, composer-bar, turns…)
|
|
29
|
-
states/ #
|
|
30
|
-
#
|
|
74
|
+
states/ # base substates' drawings (welcome): included plainly by the
|
|
75
|
+
# chrome, own condition inside, containment compiled from the tree
|
|
31
76
|
```
|
|
32
77
|
|
|
33
|
-
Each layout = a complete arrangement:
|
|
34
|
-
surface. Shared chrome lives ONCE in `components/`; every layout includes it.
|
|
35
|
-
|
|
36
|
-
|
|
78
|
+
Each layout = a complete arrangement: `$include: components/core` + that state's
|
|
79
|
+
surface. Shared chrome lives ONCE in `components/`; every layout includes it. The tree
|
|
80
|
+
claims the instance, the layout's surface renders it: a reaction state's layout holds a
|
|
81
|
+
ComponentSlot selecting the matching view.
|
|
37
82
|
|
|
38
|
-
## manifest.
|
|
83
|
+
## manifest.yaml (the app contract)
|
|
39
84
|
|
|
40
|
-
Required: `name` · `description` (what it IS, ≤120) · `whenToUse` (utterance-shaped
|
|
41
|
-
USER's words; findIntent ranks on it
|
|
85
|
+
Required: `name` · `description` (what it IS, ≤120) · `whenToUse` (utterance-shaped: the
|
|
86
|
+
USER's words; findIntent ranks on it, the OPENING words carry the ranking; disqualify by
|
|
42
87
|
property, never by naming siblings; full contract: docs/design/05 §Naming) ·
|
|
43
|
-
`category` · `defaultState
|
|
44
|
-
|
|
45
|
-
`binding` (`workflow` + `trigger
|
|
46
|
-
is not done). Optional: `preview` (the per-
|
|
47
|
-
`{ "products": ["
|
|
48
|
-
`autoTrigger`, `expose`, `service` (`"voice"
|
|
49
|
-
service, which projects `callState` into scope).
|
|
88
|
+
`category` · `defaultState: "template"` (the app-level LOAD state, a protocol field,
|
|
89
|
+
nothing to do with component views) · `inputSchema` · `layout` (the BASE layout name) ·
|
|
90
|
+
`states` (the tree above) · `binding` (`workflow` + `trigger`: the app OWNS its
|
|
91
|
+
workflow; without a real binding it is not done). Optional: `preview` (the per-state
|
|
92
|
+
mock map for Studio: `{ "products": ["product-card", "product-card"] }`; repeated
|
|
93
|
+
name = several instances), `autoTrigger`, `expose`, `service` (`"voice"`: the channel
|
|
94
|
+
instantiates the native service, which projects `callState` into scope).
|
|
50
95
|
|
|
51
|
-
**The generalist trap:** a home/fallback surface must NOT enumerate its siblings' jobs
|
|
52
|
-
|
|
96
|
+
**The generalist trap:** a home/fallback surface must NOT enumerate its siblings' jobs.
|
|
97
|
+
It owns general help and cedes specific jobs by property, naming none.
|
|
53
98
|
|
|
54
|
-
## Sizing
|
|
99
|
+
## Sizing: each layout owns its widths
|
|
55
100
|
|
|
56
101
|
The app = the ACTIVE layout's total, nothing else ever. Inside a layout: the core panel
|
|
57
102
|
is always open (`appWidth` on the core box), the surface panel declares its width on the
|
|
58
103
|
ComponentSlot. Lint-enforced:
|
|
59
|
-
- `appWidth` = a NAMED size from the
|
|
60
|
-
(`chat` · `chat-slim` · `rail` · `panel`)
|
|
61
|
-
- ONE declaration per panel
|
|
104
|
+
- `appWidth` = a NAMED size from the project's `styles/semantic/app-sizes`
|
|
105
|
+
(`chat` · `chat-slim` · `rail` · `panel` · `flex`). Raw CSS is an ERROR.
|
|
106
|
+
- ONE declaration per panel; the frame never declares width/flex.
|
|
62
107
|
- NEVER on a layout root; NEVER `visibleWhen`-guarded (a conditional arrangement is a
|
|
63
|
-
|
|
64
|
-
- An OVERLAY surface (`inset: 0` over the core) declares NO appWidth and needs NO
|
|
65
|
-
of its own
|
|
66
|
-
- Nothing in the manifest sizes the app (`width`/`focusWidth` dead
|
|
108
|
+
STATE with its own layout).
|
|
109
|
+
- An OVERLAY surface (`inset: 0` over the core) declares NO appWidth and needs NO state
|
|
110
|
+
of its own. Reach for a state when the ARRANGEMENT changes.
|
|
111
|
+
- Nothing in the manifest sizes the app (`width`/`focusWidth` dead, a lint error).
|
|
67
112
|
- Layout root: `overflow: hidden`.
|
|
68
113
|
|
|
69
|
-
## Surfaces
|
|
70
|
-
|
|
71
|
-
One instance → one placeholder: the flow is the placeholder for `inline`; a
|
|
72
|
-
|
|
73
|
-
claimed instance out of the flow
|
|
74
|
-
|
|
75
|
-
- **Flow slot**: `{
|
|
76
|
-
- **Reaction surface**: select by the
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
114
|
+
## Surfaces: templates REACT, they never own
|
|
115
|
+
|
|
116
|
+
One instance → one placeholder: the flow is the placeholder for `inline`; a reaction
|
|
117
|
+
state's layout (via its inner surface) is the placeholder for a matched view. The SDK
|
|
118
|
+
keeps a claimed instance out of the flow; never hide a flow copy yourself.
|
|
119
|
+
|
|
120
|
+
- **Flow slot**: `{ type: ComponentSlot, select: {} }`, always present.
|
|
121
|
+
- **Reaction surface**: select by the public `view`, never by type/id:
|
|
122
|
+
```yaml
|
|
123
|
+
type: ComponentSlot
|
|
124
|
+
appWidth: panel
|
|
125
|
+
select:
|
|
126
|
+
from: all
|
|
127
|
+
where: { field: view, eq: detail }
|
|
128
|
+
limit: 1
|
|
129
|
+
frame: # chrome; an inner bare ComponentSlot marks the placement
|
|
81
130
|
```
|
|
82
|
-
❌ `select: { type: [...] }`, ❌ `where.field` ≠ `
|
|
83
|
-
writing a component's slice
|
|
131
|
+
❌ `select: { type: [...] }`, ❌ `where.field` ≠ `view`, ❌ template chrome
|
|
132
|
+
writing a component's slice, all lint-flagged. (`defaultState` as the select field
|
|
133
|
+
is the v1 legacy spelling.)
|
|
134
|
+
- **Input tools are template chrome** (unchanged rule): a composer, an edit form, a
|
|
135
|
+
picker is owned by the template, never a streamed component state; chrome never
|
|
136
|
+
"arrives". A card's Edit stages its data into the template's one input tool.
|
|
84
137
|
- **Template state** (`setTemplateValue`) is ONLY for the template's own chrome (a
|
|
85
|
-
disclosure, the composer `draft`). "Is something focused" is DERIVED
|
|
138
|
+
disclosure, the composer `draft`). "Is something focused" is DERIVED, never stored.
|
|
86
139
|
- **Chrome reacts via `surfacedView`** (SDK-projected active view name, `""` = all
|
|
87
|
-
inline): `visibleWhen {
|
|
88
|
-
- **A `limit: 1` surface's occupant FILLS it** automatically
|
|
89
|
-
`minHeight: full` through
|
|
140
|
+
inline): `visibleWhen { field: surfacedView, in: ["", "products"] }`.
|
|
141
|
+
- **A `limit: 1` surface's occupant FILLS it** automatically; never sprinkle
|
|
142
|
+
`minHeight: full` through component layouts. Rails (no limit) keep content-sized cards.
|
|
90
143
|
- A full layer caps its height and scrolls its BODY only (`flex: 1` + `minHeight: 0` +
|
|
91
144
|
`overflow: auto`; header/footer pinned).
|
|
145
|
+
- `pinned: true` on a reaction layout keeps late-arriving components from yanking the
|
|
146
|
+
page from under a reading guest; the guest's own tap still moves it.
|
|
92
147
|
|
|
93
148
|
## Voice templates
|
|
94
149
|
|
|
95
|
-
`"service": "voice"` in the manifest. Call phases (
|
|
96
|
-
|
|
97
|
-
slim core (`chat-slim`) in the card layouts. Cards during a call
|
|
98
|
-
exactly as in chat. Audio is never wired in a definition.
|
|
150
|
+
`"service": "voice"` in the manifest. Call phases (idle…user-speaking) branch off the
|
|
151
|
+
projected `callState` discriminant inside the base; typically a wide core in the base
|
|
152
|
+
layout and a slim core (`chat-slim`) in the card layouts. Cards during a call match
|
|
153
|
+
template states by name exactly as in chat. Audio is never wired in a definition.
|
|
99
154
|
|
|
100
|
-
## New
|
|
155
|
+
## New project?
|
|
101
156
|
|
|
102
|
-
`rx
|
|
103
|
-
unique across tiers) + a **complete self-contained** `styles/` (copy
|
|
104
|
-
`
|
|
105
|
-
`unoverse://apps/<
|
|
106
|
-
themes `unoverse://theme/<
|
|
157
|
+
`rx/<project>/` = `templates/` + optional `components/` (project-private microapps,
|
|
158
|
+
names unique across tiers) + a **complete self-contained** `styles/` (copy an existing
|
|
159
|
+
project's `styles/` and re-value the base scales). Addresses: apps
|
|
160
|
+
`unoverse://apps/<project>/<name>`, project components
|
|
161
|
+
`unoverse://components/<project>/<name>`, themes `unoverse://theme/<project>/<theme>`.
|
|
107
162
|
|
|
108
163
|
## Validate & ship
|
|
109
164
|
|
|
110
|
-
1.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
165
|
+
1. Manifest declares the states tree: base first, reaction states in priority order,
|
|
166
|
+
base-only moods nested inside the base, shell content NOT declared. No authored
|
|
167
|
+
`stateOrder`, no hand-written containment guards.
|
|
168
|
+
2. Publish from Studio; its lint must report 0 errors (the rules are v2-aware:
|
|
169
|
+
state/layout ownership, widths, reaction rules, tokens).
|
|
170
|
+
3. Restart unoverse; the node re-synthesizes.
|
|
171
|
+
4. **See it**: in Studio, the state switcher reads the tree (base first, ladder order),
|
|
172
|
+
base substates beneath, freely combinable; acting inside the preview transitions
|
|
173
|
+
like the runtime. Then run it live: the bound workflow streams components, a card
|
|
174
|
+
entering a public view walks the ladder and rearranges the template end-to-end,
|
|
175
|
+
and its ✕ hands the lower state back.
|