@unoverse-platform/studio 0.1.85 → 0.1.86
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/ComponentsView.tsx +4 -3
- package/screens/TemplatesLibraryView.tsx +142 -82
- package/screens/TemplatesView.tsx +49 -8
- package/screens/states.ts +25 -7
- package/src/host.ts +1 -1
- package/vendor/sdk/lib/UnoverseComponent.tsx +49 -3
- package/vendor/sdk/lib/app.tsx +121 -31
- package/vendor/sdk/lib/core/actions.ts +2 -2
- package/vendor/sdk/lib/core/apps.ts +14 -8
- package/vendor/sdk/lib/core/conditions.ts +15 -3
- package/vendor/sdk/lib/core/connection.ts +17 -1
- package/vendor/sdk/lib/core/store.ts +104 -21
- package/vendor/sdk/lib/transition/MountArrangement.tsx +71 -0
- package/vendor/sdk/lib/transition/decide.ts +53 -0
- package/vendor/sdk/lib/transition/guestHold.ts +34 -0
- package/vendor/sdk/lib/transition/index.ts +9 -0
- package/vendor/sdk/lib/transition/viewTransition.ts +29 -0
- package/vendor/skills/unoverse-create/SKILL.md +11 -5
- package/vendor/skills/unoverse-create/references/app.md +35 -21
- package/vendor/skills/unoverse-create/references/component.md +55 -54
- package/vendor/skills/unoverse-create/references/node.md +3 -1
- package/vendor/skills/unoverse-create/references/template.md +133 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commit decision — the Transition Model's pure heart (UNOVERSE_TEMPLATE_MODEL §13).
|
|
3
|
+
*
|
|
4
|
+
* The mount double-buffers its arrangement and asks ONE question per render:
|
|
5
|
+
* may the committed arrangement change now? The answer depends only on the two
|
|
6
|
+
* arrangements — no store, no DOM, no time — so the contract is a pure function
|
|
7
|
+
* under conformance tests and can never regress silently.
|
|
8
|
+
*
|
|
9
|
+
* - "immediate": commit with no ceremony (first paint; the guest's own
|
|
10
|
+
* navigation changed the layout; an un-directed mount — no
|
|
11
|
+
* verdict governs it, so its behaviour is exactly pre-contract).
|
|
12
|
+
* - "transition": a BOUNDARY happened — the governing verdict changed (settle
|
|
13
|
+
* or clear) — commit as the one designed, animatable change.
|
|
14
|
+
* - "hold": membership moved with no boundary (a data ripple). The shelf
|
|
15
|
+
* does not move. The caller owns the fail-open grace (§13.5).
|
|
16
|
+
* - "none": nothing changed.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface Arrangement {
|
|
20
|
+
/** The template's active layout (shape) name. */
|
|
21
|
+
layoutName: string | null;
|
|
22
|
+
/** Held pointers, in display order. */
|
|
23
|
+
held: string[];
|
|
24
|
+
/** Identity of the governing verdict (sorted held-pointer join), null when no
|
|
25
|
+
* verdict governs — the un-directed mount. */
|
|
26
|
+
verdictSig: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type CommitDecision = "immediate" | "transition" | "hold" | "none";
|
|
30
|
+
|
|
31
|
+
const sameHeld = (a: string[], b: string[]) => a.length === b.length && a.every((p, i) => p === b[i]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `directed` = the placement grants a director, so every delivery WILL settle.
|
|
35
|
+
* A directed mount's membership therefore moves only at the settle — including
|
|
36
|
+
* the very first batch, whose cards arrive as separate messages before any
|
|
37
|
+
* verdict exists (committing each arrival painted the shelf in six pops, the
|
|
38
|
+
* first-appearance flicker observed live). Un-directed mounts keep committing
|
|
39
|
+
* immediately: no settle is ever coming for them.
|
|
40
|
+
*/
|
|
41
|
+
export function decideCommit(committed: Arrangement | null, next: Arrangement, directed: boolean): CommitDecision {
|
|
42
|
+
if (!committed) return "immediate";
|
|
43
|
+
// The guest's own navigation (name-sync layout change) is never held back.
|
|
44
|
+
if (next.layoutName !== committed.layoutName) return "immediate";
|
|
45
|
+
if (sameHeld(next.held, committed.held)) return "none";
|
|
46
|
+
// Membership moved. A changed governing verdict IS the boundary (settle/clear).
|
|
47
|
+
if (next.verdictSig !== committed.verdictSig) return "transition";
|
|
48
|
+
// Directed: the settle is coming — hold until it lands (grace fails open).
|
|
49
|
+
if (directed) return "hold";
|
|
50
|
+
// No verdict governs an un-directed mount: commit as it always did.
|
|
51
|
+
if (next.verdictSig === null) return "immediate";
|
|
52
|
+
return "hold";
|
|
53
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE GUEST'S HAND OUTRANKS THE DIRECTOR (UNOVERSE_TEMPLATE_MODEL §6b rule 3,
|
|
3
|
+
* state priority): a pointer whose LATEST write is the guest's own interaction,
|
|
4
|
+
* holding a non-base face the mount claims (an open Detail View), stays in the
|
|
5
|
+
* mount's scope regardless of deliveries and settles. Observed live without
|
|
6
|
+
* this: a new batch settled and yanked the guest out of an open detail page.
|
|
7
|
+
*
|
|
8
|
+
* The hold releases exactly as the model says: the guest's ✕ writes the face
|
|
9
|
+
* back to the base — no longer a non-base guest hold — and the current
|
|
10
|
+
* arrangement takes over. A re-delivered instance is an ARRIVAL (its merge
|
|
11
|
+
* out-touches the guest write) and may be re-faced, per §4 level 2.
|
|
12
|
+
*
|
|
13
|
+
* Pure over reads so the priority law is conformance-testable.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface GuestHoldReads {
|
|
17
|
+
touchOf(p: string): number;
|
|
18
|
+
interactionTouchOf(p: string): number;
|
|
19
|
+
viewOf(p: string): string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function guestHeldPointers(
|
|
23
|
+
pointers: readonly string[],
|
|
24
|
+
reads: GuestHoldReads,
|
|
25
|
+
claimedLayouts: readonly string[],
|
|
26
|
+
baseLayout: string | null,
|
|
27
|
+
): string[] {
|
|
28
|
+
return pointers.filter((p) => {
|
|
29
|
+
const it = reads.interactionTouchOf(p);
|
|
30
|
+
if (!it || it < reads.touchOf(p)) return false; // latest write is not the guest's
|
|
31
|
+
const v = reads.viewOf(p);
|
|
32
|
+
return !!v && v !== baseLayout && claimedLayouts.includes(v);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Transition Model (UNOVERSE_TEMPLATE_MODEL §13) — the render layer's
|
|
3
|
+
* designed way of moving interfaces in and out of a mounted template:
|
|
4
|
+
* `decide.ts` (the pure commit decision), `MountArrangement.tsx` (the
|
|
5
|
+
* double-buffer), `viewTransition.ts` (the theme-gated animated commit).
|
|
6
|
+
*/
|
|
7
|
+
export { decideCommit, type Arrangement, type CommitDecision } from "./decide";
|
|
8
|
+
export { MountArrangement } from "./MountArrangement";
|
|
9
|
+
export { commitArrangement } from "./viewTransition";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commit IS the animation (UNOVERSE_TEMPLATE_MODEL §13.4): when the theme
|
|
3
|
+
* asks for motion, the boundary commit is wrapped in the browser's View
|
|
4
|
+
* Transitions API — the browser diffs old and new and animates; the THEME's CSS
|
|
5
|
+
* owns every duration and ease. The SDK contributes only this generic behaviour
|
|
6
|
+
* and NO styles. Engines without the API, and themes that author no motion, get
|
|
7
|
+
* the instant commit — today's behaviour, gracefully.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { flushSync } from "react-dom";
|
|
11
|
+
|
|
12
|
+
type WithViewTransition = { startViewTransition?: (cb: () => void) => unknown };
|
|
13
|
+
|
|
14
|
+
export function commitArrangement(commit: () => void, motion: boolean): void {
|
|
15
|
+
const doc = typeof document !== "undefined" ? (document as unknown as WithViewTransition) : undefined;
|
|
16
|
+
if (!motion || typeof doc?.startViewTransition !== "function") {
|
|
17
|
+
commit();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// From a microtask, outside React's lifecycle, so flushSync is legal: the
|
|
21
|
+
// API snapshots, the commit flushes the new DOM, the browser animates.
|
|
22
|
+
queueMicrotask(() => {
|
|
23
|
+
try {
|
|
24
|
+
doc.startViewTransition!(() => flushSync(commit));
|
|
25
|
+
} catch {
|
|
26
|
+
commit(); // fail open — the change lands even if the ceremony breaks
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: unoverse-create
|
|
3
|
-
description: "Create or edit Unoverse platform artifacts: UI components (including briefed/AI-filled components), apps, atoms and styles (design/), brand and org packs (themes, tokens, rebranding, cloning a universe for a new client), agent skills and prompt blocks (prompts/), custom workflow nodes (nodes/), and workflows built live on the Canvas via the builder MCP. Use whenever the user wants to create, add, build, or modify a component, app, card, page, theme, brand, org pack, agent skill, prompt block, custom node, or workflow in an Unoverse project, even if they don't say 'Unoverse' (they'll describe UI like 'a card with a button and an image' or 'a page the AI fills in')."
|
|
3
|
+
description: "Create or edit Unoverse platform artifacts: UI components (including briefed/AI-filled components), apps, templates (arrangements with open parts a delivery fills — shelf grids, emails, comparison pages), atoms and styles (design/), brand and org packs (themes, tokens, rebranding, cloning a universe for a new client), agent skills and prompt blocks (prompts/), custom workflow nodes (nodes/), and workflows built live on the Canvas via the builder MCP. Use whenever the user wants to create, add, build, or modify a component, app, card, page, theme, brand, org pack, agent skill, prompt block, custom node, or workflow in an Unoverse project, even if they don't say 'Unoverse' (they'll describe UI like 'a card with a button and an image' or 'a page the AI fills in')."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Creating Unoverse Artifacts
|
|
@@ -30,8 +30,10 @@ that is the workspace folder itself; in the platform monorepo it is `apps/unover
|
|
|
30
30
|
([components](https://github.com/unoverse-platform/marketplace/tree/main/definitions/components), [atoms](https://github.com/unoverse-platform/marketplace/tree/main/definitions/atoms), [styles](https://github.com/unoverse-platform/marketplace/tree/main/definitions/styles)).
|
|
31
31
|
Study the closest exemplar there, then compose those atoms and reference those
|
|
32
32
|
components; never hand-roll a shape the base already ships. Everything is plain
|
|
33
|
-
YAML files
|
|
33
|
+
YAML files: no connection, no running platform needed. (In the platform monorepo
|
|
34
34
|
the same material is on disk at `design/marketplace/`.)
|
|
35
|
+
[The design system](https://docs.unoverse.ai/design/design-system.md) is the guide to
|
|
36
|
+
what ships and how to use it.
|
|
35
37
|
|
|
36
38
|
## Step 1: Identify the artifact, read its playbook
|
|
37
39
|
|
|
@@ -42,6 +44,7 @@ anything**:
|
|
|
42
44
|
|---|---|
|
|
43
45
|
| A UI component or shared atom, incl. a page/card the AI fills with content (briefed) | `references/component.md` (§Briefs for AI-filled) |
|
|
44
46
|
| An app (chat surface, wizard shell) | `references/app.md` |
|
|
47
|
+
| A template: an arrangement with open parts a delivery fills (a shelf grid, an email, a comparison page) | `references/template.md` |
|
|
45
48
|
| A style / theme / token change on ONE component | `references/component.md` §Tokens |
|
|
46
49
|
| A brand: an org pack, a rebrand, a new client universe, cloning an org | `references/brand.md` |
|
|
47
50
|
| An agent skill (behavior guide the AI follows) | `references/agent-skill.md` |
|
|
@@ -59,15 +62,18 @@ If it's ambiguous (e.g. "add a card that shows weather"), it's usually a **compo
|
|
|
59
62
|
**New to design artifacts?** The guided learning journey at
|
|
60
63
|
[docs.unoverse.ai/design](https://docs.unoverse.ai/design/overview.md)
|
|
61
64
|
([quick-start](https://docs.unoverse.ai/design/quick-start.md) →
|
|
65
|
+
[essentials](https://docs.unoverse.ai/design/essentials.md) →
|
|
66
|
+
[interface-data](https://docs.unoverse.ai/design/interface-data.md) →
|
|
62
67
|
[components](https://docs.unoverse.ai/design/components.md) →
|
|
63
68
|
[state](https://docs.unoverse.ai/design/state.md) →
|
|
69
|
+
[templates](https://docs.unoverse.ai/design/templates.md) →
|
|
64
70
|
[apps](https://docs.unoverse.ai/design/apps.md) →
|
|
65
71
|
[styles-and-tokens](https://docs.unoverse.ai/design/styles-and-tokens.md) →
|
|
66
72
|
[studio](https://docs.unoverse.ai/design/studio.md) →
|
|
67
73
|
[validate-and-ship](https://docs.unoverse.ai/design/validate-and-ship.md) →
|
|
68
|
-
[troubleshooting](https://docs.unoverse.ai/design/troubleshooting.md)) teaches
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
[troubleshooting](https://docs.unoverse.ai/design/troubleshooting.md)) teaches the
|
|
75
|
+
shared grammar every artifact uses, the four kinds, the state model (incl. locked
|
|
76
|
+
voice/native state), tokens, **studio**, and validation in order;
|
|
71
77
|
[design/CLAUDE.md](https://docs.unoverse.ai/design/CLAUDE.md) is the condensed agent
|
|
72
78
|
rulebook. The playbooks here assume that material. Append `.md` to any docs URL to
|
|
73
79
|
fetch it as raw markdown.
|
|
@@ -16,15 +16,16 @@ already in the workspace. (Monorepo: `design/sab/apps/sab-chat`.)
|
|
|
16
16
|
A component is a state tree whose public states are its whole interface; an app
|
|
17
17
|
declares its OWN state tree in the manifest and reacts to components by name:
|
|
18
18
|
|
|
19
|
-
> A component enters a public
|
|
19
|
+
> A component enters a public state → the app asks "do I have a state with that
|
|
20
20
|
> name?" Match: the app enters that state and its layout draws. No match: the
|
|
21
21
|
> component renders inline in the flow. Always.
|
|
22
22
|
|
|
23
23
|
Derived off the store, never stored, no wiring. The component never knows apps
|
|
24
24
|
exist, and post-spawn the app NEVER writes a component's state. Name-match is the
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
ONLY mechanism: an app state reacts because its name matches, so give the app the
|
|
26
|
+
name the component writes. (`reactsTo:` is DESIGN-ONLY: nothing in the runtime
|
|
27
|
+
reads it, so never author it.) **Rule of thumb: component causes it = a reaction
|
|
28
|
+
state; app knows it = a base substate or app state.**
|
|
28
29
|
|
|
29
30
|
## The manifest states tree: one declaration, everything follows
|
|
30
31
|
|
|
@@ -33,13 +34,21 @@ The canonical shape (a chat layout: a base arrangement plus a reaction ladder):
|
|
|
33
34
|
```yaml
|
|
34
35
|
states:
|
|
35
36
|
main: # the base arrangement, FIRST
|
|
37
|
+
layout: layouts/main
|
|
36
38
|
states:
|
|
37
|
-
welcome:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
welcome: # contained substate; first substate = the landing default
|
|
40
|
+
layout: layouts/main-welcome
|
|
41
|
+
focus: # then the reaction ladder, in priority order
|
|
42
|
+
layout: layouts/focus
|
|
43
|
+
products:
|
|
44
|
+
layout: layouts/products
|
|
45
|
+
detail:
|
|
46
|
+
layout: layouts/detail
|
|
41
47
|
```
|
|
42
48
|
|
|
49
|
+
Every state DECLARES its drawing as a path relative to the app folder (2026-08-29
|
|
50
|
+
ruling): no same-name default, no assumed folder, `{}` is not a complete state.
|
|
51
|
+
|
|
43
52
|
- **Top-level order IS the priority ladder**, base first. The app can be in ONE
|
|
44
53
|
state at a time: it walks the list top-down and enters the first of its states that
|
|
45
54
|
ANY hosted component currently matches. The ladder is DERIVED from the tree; never
|
|
@@ -59,19 +68,22 @@ states:
|
|
|
59
68
|
- **A higher state CANCELS the claims below it**: while active, hosted components in
|
|
60
69
|
lower-ranked states retract to inline (durable `lifetime: "conversation"` slices opt
|
|
61
70
|
out), so releasing it lands on the BASE arrangement, never a stale lower one.
|
|
62
|
-
- Ties (two components in the same
|
|
71
|
+
- Ties (two components in the same state): most recent write wins (`limit: 1`).
|
|
63
72
|
- **No word is special.** `focus` outranks `products` by list position only; the engine
|
|
64
73
|
matches names and walks the list. Even priority is data.
|
|
65
74
|
- The app's own condition-guarded moods (a welcome hero on an empty conversation)
|
|
66
75
|
are private substates of the base, never ladder entries: the app does not
|
|
67
76
|
rearrange between them.
|
|
68
77
|
|
|
69
|
-
## The anatomy:
|
|
78
|
+
## The anatomy: ONE FOLDER GRAMMAR (2026-08-29 — apps match components/templates)
|
|
70
79
|
|
|
71
80
|
```
|
|
72
81
|
design/<project>/apps/<name>/
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
<name>.yaml # THE ENVELOPE: type: app + the states tree (what the app IS);
|
|
83
|
+
# each state declares its layout path and carries its preview
|
|
84
|
+
manifest.yaml # THE MCP FACE: description/whenToUse/inputSchema/binding/
|
|
85
|
+
# makeMCP/assetDomains — how the outside finds and calls it
|
|
86
|
+
layouts/ # the ARRANGEMENTS: one per state, each DECLARED by path in the tree
|
|
75
87
|
main.yaml # the base: the core alone
|
|
76
88
|
products.yaml # state "products" → core + cards rail
|
|
77
89
|
detail.yaml # state "detail" → core + full record page
|
|
@@ -84,7 +96,7 @@ design/<project>/apps/<name>/
|
|
|
84
96
|
Each layout = a complete arrangement: `$include: components/core` + that state's
|
|
85
97
|
surface. Shared chrome lives ONCE in `components/`; every layout includes it. The tree
|
|
86
98
|
claims the instance, the layout's surface renders it: a reaction state's layout holds a
|
|
87
|
-
ComponentSlot selecting the matching
|
|
99
|
+
ComponentSlot selecting the matching state.
|
|
88
100
|
|
|
89
101
|
## manifest.yaml (the app contract)
|
|
90
102
|
|
|
@@ -121,27 +133,29 @@ ComponentSlot. Lint-enforced:
|
|
|
121
133
|
## Surfaces: apps REACT, they never own
|
|
122
134
|
|
|
123
135
|
One instance → one placeholder: the flow is the placeholder for `inline`; a reaction
|
|
124
|
-
state's layout (via its inner surface) is the placeholder for a matched
|
|
136
|
+
state's layout (via its inner surface) is the placeholder for a matched state. The SDK
|
|
125
137
|
keeps a claimed instance out of the flow; never hide a flow copy yourself.
|
|
126
138
|
|
|
127
139
|
- **Flow slot**: `{ type: ComponentSlot, select: {} }`, always present.
|
|
128
|
-
- **Reaction surface**: select by the public `
|
|
140
|
+
- **Reaction surface**: select by the public `state`, never by type/id:
|
|
129
141
|
```yaml
|
|
130
142
|
type: ComponentSlot
|
|
131
143
|
appWidth: panel
|
|
132
144
|
select:
|
|
133
145
|
from: all
|
|
134
|
-
where:
|
|
146
|
+
where:
|
|
147
|
+
field: state
|
|
148
|
+
eq: page
|
|
135
149
|
limit: 1
|
|
136
150
|
frame: # chrome; an inner bare ComponentSlot marks the placement
|
|
137
151
|
```
|
|
138
|
-
❌ `select: { type: [...] }`, ❌ `where.field` ≠ `
|
|
139
|
-
writing a component's slice, all lint-flagged. (`defaultState`
|
|
140
|
-
|
|
152
|
+
❌ `select: { type: [...] }`, ❌ `where.field` ≠ `state`, ❌ app chrome
|
|
153
|
+
writing a component's slice, all lint-flagged. (`view` and `defaultState` are the
|
|
154
|
+
legacy spellings of the select field.)
|
|
141
155
|
- **Input tools are app chrome** (unchanged rule): a composer, an edit form, a
|
|
142
156
|
picker is owned by the app, never a streamed component state; chrome never
|
|
143
157
|
"arrives". A card's Edit stages its data into the app's one input tool.
|
|
144
|
-
- **App state** (`
|
|
158
|
+
- **App state** (`setAppValue`) is ONLY for the app's own chrome (a
|
|
145
159
|
disclosure, the composer `draft`). "Is something focused" is DERIVED, never stored.
|
|
146
160
|
- **Chrome reacts via `surfacedView`** (SDK-projected active view name, `""` = all
|
|
147
161
|
inline): `visibleWhen { field: surfacedView, in: ["", "products"] }`.
|
|
@@ -180,5 +194,5 @@ whole, see brand.md). Addresses: apps
|
|
|
180
194
|
4. **See it**: in Studio, the state switcher reads the tree (base first, ladder order),
|
|
181
195
|
base substates beneath, freely combinable; acting inside the preview transitions
|
|
182
196
|
like the runtime. Then run it live: the bound workflow streams components, a card
|
|
183
|
-
entering a public
|
|
197
|
+
entering a public state walks the ladder and rearranges the app end-to-end,
|
|
184
198
|
and its ✕ hands the lower state back.
|
|
@@ -16,10 +16,11 @@ folder shape. (In the platform monorepo the richest multi-state exemplar is on d
|
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
design/<project>/components/<name>/
|
|
19
|
-
<name>.yaml #
|
|
19
|
+
<name>.yaml # the envelope: name/category/nodeSize/outputs/props + `states:` + `values:` (root only as a real shell)
|
|
20
20
|
manifest.yaml # RENDER CONTRACT + discovery meta (title / description ≤120 /
|
|
21
|
-
# whenToUse utterance-shaped).
|
|
22
|
-
#
|
|
21
|
+
# whenToUse utterance-shaped). REQUIRED on every artifact EXCEPT
|
|
22
|
+
# a flat file nothing discovers (2026-08-30); the envelope NEVER
|
|
23
|
+
# duplicates the meta.
|
|
23
24
|
layouts/ # the DRAWINGS: one file per state that owns an arrangement
|
|
24
25
|
components/ # component-local shared partials (earned: 2+ layouts share a shape)
|
|
25
26
|
```
|
|
@@ -32,14 +33,14 @@ manifest, no folders. Structure is EARNED; start flat.
|
|
|
32
33
|
A component is **data plus a state tree**; each state owns the layout(s) that draw it.
|
|
33
34
|
The root's `state:` block IS the design, readable top to bottom.
|
|
34
35
|
|
|
35
|
-
> **THE GATE
|
|
36
|
+
> **THE GATE, answer this before adding ANY state to the tree:**
|
|
36
37
|
> *"Would a hosting app rearrange the page to show this?"*
|
|
37
38
|
> **No → it's a private step**: nest it under `on:` inside an existing public state.
|
|
38
39
|
> **Yes → public.**
|
|
39
40
|
> ⚠ "The user asked for N states" never means N public states. In-place swaps
|
|
40
41
|
> (confirmation, success, empty, error, loading, a form's next step) are almost always
|
|
41
|
-
> PRIVATE. Privacy is the default; promotion to
|
|
42
|
-
> app consequence.
|
|
42
|
+
> PRIVATE. Privacy is the default; promotion to a PUBLIC state is a deliberate act
|
|
43
|
+
> with an app consequence.
|
|
43
44
|
|
|
44
45
|
The canonical shape (a product-card component). Note where each state landed and why:
|
|
45
46
|
`products` and `detail` are PUBLIC because an app genuinely rearranges for each (a
|
|
@@ -47,35 +48,30 @@ grid rail vs a full page); `detail`'s inner `detail`/`apply` are PRIVATE steps b
|
|
|
47
48
|
applying swaps content in place — the page does not move:
|
|
48
49
|
|
|
49
50
|
```yaml
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
states: # TOP LEVEL (2026-08-29), like every kind; the word is
|
|
52
|
+
products: # `state` everywhere (setValue/Switch on/select field)
|
|
53
|
+
layout: layouts/products # REQUIRED path: nothing assumed
|
|
54
|
+
detail: # the full page
|
|
55
|
+
layout: layouts/product
|
|
56
|
+
on: step # a PRIVATE axis: the steps inside detail
|
|
53
57
|
states:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
initial: detail
|
|
59
|
-
states:
|
|
60
|
-
detail: { layout: product-detail }
|
|
61
|
-
apply: { layout: product-apply }
|
|
62
|
-
root:
|
|
63
|
-
type: Box
|
|
64
|
-
children:
|
|
65
|
-
- type: Switch
|
|
66
|
-
on: view # the root switches on `view`, ONE case active
|
|
67
|
-
cases:
|
|
68
|
-
products: { $include: layouts/products }
|
|
69
|
-
detail: { $include: layouts/product }
|
|
58
|
+
detail: { layout: layouts/product-detail }
|
|
59
|
+
apply: { layout: layouts/product-apply }
|
|
60
|
+
values: # STARTING VALUES of keys the component writes (scalars)
|
|
61
|
+
progressPct: 10%
|
|
70
62
|
```
|
|
71
63
|
|
|
64
|
+
**No `root`**: the compiler synthesizes the `Switch on state` from the tree. Author
|
|
65
|
+
a root ONLY as a real shell (chrome beyond the plain Switch); a restated Switch is
|
|
66
|
+
linted as redundant.
|
|
67
|
+
|
|
72
68
|
The rules, in the order they bite:
|
|
73
69
|
|
|
74
|
-
- **The
|
|
75
|
-
The root is a `Switch on
|
|
76
|
-
`setValue { values: [{ key:
|
|
70
|
+
- **The word is `state`** (ruled 2026-08-29). The component's public state lives in
|
|
71
|
+
its slice under `state`. The root is a `Switch on state`; buttons transition with
|
|
72
|
+
`setValue { values: [{ key: state, value: detail }] }`. That write is the ENTIRE
|
|
77
73
|
transition machinery: no guards, no events, no per-component machines.
|
|
78
|
-
- **Public states are the top level of
|
|
74
|
+
- **Public states are the top level of `states:`** and they are the app's
|
|
79
75
|
WHOLE vocabulary: everything apps can react to, nothing more. Steps nest
|
|
80
76
|
privately (`on: <key>` + `initial` + `states`), invisible outside the component.
|
|
81
77
|
The placement rule is THE GATE above: **if the app must rearrange to show it,
|
|
@@ -84,11 +80,11 @@ The rules, in the order they bite:
|
|
|
84
80
|
**The tell in Studio:** non-initial public states preview FULL-BLEED (no stage
|
|
85
81
|
padding), precisely because they are app-liftable surfaces. If a state you
|
|
86
82
|
meant as an in-place swap suddenly renders edge-to-edge, you promoted a private
|
|
87
|
-
step to
|
|
88
|
-
- **Each state owns its layout(s)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
state selects it. Name states for what they MEAN (`detail`), never for their drawing. **Substate drawings are axis-prefixed**: a step's file carries its axis key
|
|
83
|
+
step to the public menu — move it into the nest.
|
|
84
|
+
- **Each state owns its layout(s), DECLARED as a path** (`layout: layouts/<file>`,
|
|
85
|
+
REQUIRED — 2026-08-29; no same-name default, `{}` is not complete; the one
|
|
86
|
+
exception is a FLAT component with no layouts/ folder). A layout is never
|
|
87
|
+
addressable on its own; only its owning state selects it. Name states for what they MEAN (`detail`), never for their drawing. **Substate drawings are axis-prefixed**: a step's file carries its axis key
|
|
92
88
|
(`step-register`, `callState-idle`) and is declared via `layout:`; unprefixed
|
|
93
89
|
files are arrangements (exemplars: `course-application`, `voice-chat`).
|
|
94
90
|
- **Shell vs steps.** A state's own layout is its SHELL, always on while the state is
|
|
@@ -106,20 +102,20 @@ The rules, in the order they bite:
|
|
|
106
102
|
inline. The server compiles the tree to the public menu + initials it serves
|
|
107
103
|
(`publicStates`, `initialView`). Post-spawn, the app NEVER writes a component's
|
|
108
104
|
state.
|
|
109
|
-
- **Unmatched
|
|
105
|
+
- **Unmatched state = inline. Always.** An ignored public state does not stay put, it
|
|
110
106
|
FALLS INLINE in the flow. A step that must never move the page belongs in the nest,
|
|
111
107
|
not on the public menu.
|
|
112
|
-
- Legacy: v1's `defaultState`
|
|
113
|
-
|
|
108
|
+
- Legacy: `view` (the v2 intermediate) and v1's `defaultState` are read-only
|
|
109
|
+
aliases; layout-name-sync is superseded. New authoring writes `state`.
|
|
114
110
|
|
|
115
111
|
## The rules (Studio's publish lint enforces all of these; 0 errors required)
|
|
116
112
|
|
|
117
113
|
1. **Three homes for everything it shows** (AUTHORING §3):
|
|
118
114
|
- static content → **hardcoded literals** in the layout (`value`, literal `items: []` on
|
|
119
115
|
Each, `src`), never props, never `state`;
|
|
120
|
-
-
|
|
121
|
-
`progressPct`): an array/object/URL
|
|
122
|
-
static → hardcode);
|
|
116
|
+
- starting values of keys it writes → the **`values:` block, SCALARS ONLY**
|
|
117
|
+
(`step`, `phase`, `progressPct`; `state:` scalars = legacy): an array/object/URL
|
|
118
|
+
there is slop (workflow data → props; static → hardcode);
|
|
123
119
|
- workflow-fed data → **`props` with `input: true`** (the `default` is the preview mock).
|
|
124
120
|
Usually empty.
|
|
125
121
|
- **Projected-field preview rule:** any LOCKED/projected field a
|
|
@@ -127,7 +123,7 @@ The rules, in the order they bite:
|
|
|
127
123
|
a prop with a preview default: Studio's bare component preview supplies ONLY prop
|
|
128
124
|
defaults, so an undeclared projected read renders NOTHING there; live, the projected
|
|
129
125
|
value wins and the default never applies. A Ref-embedded component's root Switch
|
|
130
|
-
likewise needs a `default:` case (`
|
|
126
|
+
likewise needs a `default:` case (`state` is absent without a spawned slice).
|
|
131
127
|
- **Who owns a mood: the DRIVER decides.** Ask what WRITES the discriminant:
|
|
132
128
|
(1) the component's own buttons (`setValue`) → component substates on a private axis,
|
|
133
129
|
names from the design (`course-application`); (2) a service projecting a NAMED value
|
|
@@ -143,12 +139,15 @@ The rules, in the order they bite:
|
|
|
143
139
|
- **Prop names = the data contract. Never invent them.** Source data hydrates props
|
|
144
140
|
BY NAME (as-is, no mapping); a name the source doesn't carry silently renders the
|
|
145
141
|
preview default (tell: title streams, image stays mock). Content-attached cards use
|
|
146
|
-
the writer vocabulary
|
|
147
|
-
|
|
142
|
+
the writer vocabulary, listed layer by layer in
|
|
143
|
+
[design/interface-data](https://docs.unoverse.ai/design/interface-data.md)
|
|
144
|
+
(`title` `tagline` `description` `bodyCopy` `primaryImage` `images` …). Law:
|
|
148
145
|
`UNOVERSE_MCP_APP_PROTOCOL.md` §Content-attached cards (platform spec; a
|
|
149
146
|
guard test in the platform server enforces it).
|
|
150
|
-
2. **
|
|
151
|
-
|
|
147
|
+
2. **NO root: the compiler synthesizes the `Switch on state`** from `states:` (the tree
|
|
148
|
+
above), so a hand-written root that merely restates the tree is lint-flagged as
|
|
149
|
+
redundant. Author a `root:` ONLY for real always-on chrome beyond that Switch.
|
|
150
|
+
Keep an `inline` state for any component that lives in a
|
|
152
151
|
conversation flow: the flow is its placeholder there. One EXCEPTION is the
|
|
153
152
|
**single-state (permanent) component**: exactly ONE public state, NO inline. It
|
|
154
153
|
arrives in its surface, cannot leave it (no ✕, no other state), and is enriched in
|
|
@@ -164,14 +163,14 @@ The rules, in the order they bite:
|
|
|
164
163
|
takes over. Default `"turn"`: the instance returns to inline / retires on the next
|
|
165
164
|
user turn. Values are a closed set: `turn | conversation` (lint-checked;
|
|
166
165
|
[design/components](https://docs.unoverse.ai/design/components.md)).
|
|
167
|
-
3. **
|
|
166
|
+
3. **Private state is local; the public `state` is the interface** (STATE_MODEL §5). Private axes
|
|
168
167
|
(`step`, `phase`, …) are the component's own business; the app never sees them.
|
|
169
|
-
The only thing that crosses is the public `
|
|
170
|
-
slice (`setValue`): expand = `setValue {
|
|
168
|
+
The only thing that crosses is the public `state`. The component writes ONLY its own
|
|
169
|
+
slice (`setValue`): expand = `setValue { state: "page" }`; its expanded layout
|
|
171
170
|
carries its own ✕ back down. When a hosting app has a state of that name, the
|
|
172
171
|
instance **lifts out of the flow into that state's slot**. The SDK renders each
|
|
173
172
|
instance in exactly ONE placeholder, so you never hide a flow copy yourself (no
|
|
174
|
-
`hideBelow`, no overlay hack). ❌ NEVER `
|
|
173
|
+
`hideBelow`, no overlay hack). ❌ NEVER `setAppValue` to open a surface
|
|
175
174
|
(the deprecated bridge, linted). ❌ Never guard a layout against its own state:
|
|
176
175
|
the tree owns "which state is active"; a layout re-checking its discriminant is
|
|
177
176
|
the double-guard smell the model removes.
|
|
@@ -182,7 +181,7 @@ The rules, in the order they bite:
|
|
|
182
181
|
5. **Atoms** (the base design system's shared shapes, used via `Ref`; browse them at
|
|
183
182
|
[marketplace/definitions/atoms](https://github.com/unoverse-platform/marketplace/tree/main/definitions/atoms), or on disk at `design/marketplace/atoms/` in the
|
|
184
183
|
platform monorepo) are for shapes shared across components, authoring-time
|
|
185
|
-
only (the server expands them before serving; no Studio
|
|
184
|
+
only (the server expands them before serving; no Studio entry). `Ref props` remaps FIELDS;
|
|
186
185
|
**`Ref with` passes LITERALS**: `{ "type": "Ref", "ref": "button", "with": { "label":
|
|
187
186
|
"Learn more", "icon": "arrowRight" }, "action": { … } }`. A bind whose field is a `with`
|
|
188
187
|
key becomes a hardcoded attribute, a truthy `with` key drops a matching `visibleWhen`
|
|
@@ -225,7 +224,7 @@ The rules, in the order they bite:
|
|
|
225
224
|
`onEnterView` only. Omit it and every state fires.
|
|
226
225
|
- **Name the handler for what it DOES, never for the phase** (`fetchPlaceDetails`, not
|
|
227
226
|
`onEnterView`): the phase already said the moment.
|
|
228
|
-
- **Nothing to wire up.** A component changing its own
|
|
227
|
+
- **Nothing to wire up.** A component changing its own face writes `state`, and that
|
|
229
228
|
write IS the event the hook reacts to. No signal to author, no action to chain.
|
|
230
229
|
- **Prefer a platform-ready handler.** `getDetail` (zero code) reads the instance's
|
|
231
230
|
`universal_id` and merges the stored record's fields in by name, so authored copy
|
|
@@ -311,9 +310,11 @@ component. Its key nouns/verbs must appear in `whenToUse`'s first sentence.
|
|
|
311
310
|
1. Study the closest published exemplar ([marketplace/definitions/components](https://github.com/unoverse-platform/marketplace/tree/main/definitions/components);
|
|
312
311
|
monorepo: `design/sab/components/product-card`); copy its folder shape.
|
|
313
312
|
2. Declare the state tree first: public states on top, steps nested, each state owning
|
|
314
|
-
its layout
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
its layout as a root-relative path (`layout: layouts/<file>`, required; a FLAT
|
|
314
|
+
component with no layouts/ folder declares face-only states with no layout).
|
|
315
|
+
3. Write the envelope + manifest + layouts; the root is synthesized from `states:`; every
|
|
316
|
+
shown thing in its ONE home; transitions are `setValue` writes to `state` (or a private
|
|
317
|
+
axis).
|
|
317
318
|
4. Publish from Studio; its lint must report 0 errors (the rules are v2-aware and cite
|
|
318
319
|
the doc for every rule).
|
|
319
320
|
5. Restart unoverse → Studio: mock (prop defaults + the state switcher reads the tree:
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
1. [nodes/CLAUDE.md](https://docs.unoverse.ai/nodes/CLAUDE.md): the authoritative agent
|
|
6
6
|
guide. The folder, the eight non-negotiable rules, every file with an example, and
|
|
7
7
|
the lint-error-to-fix table.
|
|
8
|
-
2. [nodes/manifest-nodes](https://docs.unoverse.ai/nodes/manifest-nodes.md): the format in full
|
|
8
|
+
2. [nodes/manifest-nodes](https://docs.unoverse.ai/nodes/manifest-nodes.md): the format in full.
|
|
9
|
+
[nodes/expressions](https://docs.unoverse.ai/nodes/expressions.md) is the template and
|
|
10
|
+
expression grammar, the sandbox, and every root the run context carries.
|
|
9
11
|
3. [nodes/node-discoverability](https://docs.unoverse.ai/nodes/node-discoverability.md): **read before writing `whenToUse`**.
|
|
10
12
|
4. As needed: [credentials](https://docs.unoverse.ai/nodes/credentials.md),
|
|
11
13
|
[who-can-run-it](https://docs.unoverse.ai/nodes/who-can-run-it.md),
|