@unoverse-platform/studio 0.1.68 → 0.1.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/local/{sample-template/chat/states/conversation.yaml → sample-component/text-chat/layouts/chat-active.yaml} +0 -1
  2. package/local/{sample-template/chat/states/welcome.yaml → sample-component/text-chat/layouts/chat-welcome.yaml} +0 -1
  3. package/local/sample-component/text-chat/layouts/chat.yaml +77 -0
  4. package/local/sample-component/text-chat/manifest.yaml +10 -0
  5. package/local/sample-component/text-chat/text-chat.yaml +59 -0
  6. package/local/sample-template/chat/components/chat-region.yaml +10 -0
  7. package/local/sample-template/chat/components/core-focus.yaml +3 -31
  8. package/local/sample-template/chat/components/core.yaml +1 -28
  9. package/local/sample-template/chat/manifest.yaml +7 -12
  10. package/local/scaffold.mjs +8 -3
  11. package/package.json +1 -1
  12. package/screens/ComponentsView.tsx +8 -1
  13. package/vendor/sdk/appEngine.tsx +1 -1
  14. package/vendor/skills/unoverse-create/references/brand.md +1 -1
  15. package/vendor/skills/unoverse-create/references/component.md +21 -1
  16. package/vendor/skills/unoverse-create/references/template.md +1 -1
  17. /package/local/{sample-template/chat → sample-component/text-chat}/components/assistant-turn.yaml +0 -0
  18. /package/local/{sample-template/chat → sample-component/text-chat}/components/header.yaml +0 -0
  19. /package/local/{sample-template/chat → sample-component/text-chat}/components/suggestions.yaml +0 -0
  20. /package/local/{sample-template/chat → sample-component/text-chat}/components/thinking-dots.yaml +0 -0
  21. /package/local/{sample-template/chat → sample-component/text-chat}/components/user-turn.yaml +0 -0
@@ -1,5 +1,4 @@
1
1
  type: Box
2
- visibleWhen: hasMessages
3
2
  autoScroll: true
4
3
  style:
5
4
  flex: 1
@@ -1,5 +1,4 @@
1
1
  type: Box
2
- visibleWhen: isEmpty
3
2
  style:
4
3
  direction: column
5
4
  flex: 1
@@ -0,0 +1,77 @@
1
+ # THE SHELL of the one public state: the FULL chat CX — the header, the mood region
2
+ # (welcome hero / live timeline), the suggestion chips over its foot, and the composer.
3
+ # The steps switch on chatState (the declared axis): Studio writes it when you step
4
+ # Welcome/Active. Live, nothing writes it, so the Switch falls to its default case,
5
+ # where the projected conversation facts pick the mood.
6
+ type: Box
7
+ style:
8
+ direction: column
9
+ # `flex: 1` fills a flex column (the template's core); `height: full` fills a block
10
+ # parent with a real height (the standalone preview).
11
+ flex: 1
12
+ height: full
13
+ minHeight: "0"
14
+ children:
15
+ - $include: components/header
16
+ - type: Box
17
+ style:
18
+ direction: column
19
+ flex: 1
20
+ minHeight: "0"
21
+ position: relative
22
+ when:
23
+ - field: hasMessages
24
+ eq: true
25
+ apply:
26
+ background: surface.base
27
+ radiusTopLeft: 2xl
28
+ overflow: hidden
29
+ - field: chatState
30
+ eq: active
31
+ apply:
32
+ background: surface.base
33
+ radiusTopLeft: 2xl
34
+ overflow: hidden
35
+ # chatState is only ever set by Studio's stepper, so this height is
36
+ # preview-only: live, the template's flex sizes the region.
37
+ minHeight: "120"
38
+ children:
39
+ - type: Switch
40
+ on: chatState
41
+ cases:
42
+ welcome: { $include: layouts/chat-welcome }
43
+ active: { $include: layouts/chat-active }
44
+ default:
45
+ type: Box
46
+ style:
47
+ direction: column
48
+ flex: 1
49
+ minHeight: "0"
50
+ children:
51
+ - type: Box
52
+ visibleWhen: isEmpty
53
+ style:
54
+ direction: column
55
+ flex: 1
56
+ minHeight: "0"
57
+ children:
58
+ - $include: layouts/chat-welcome
59
+ - type: Box
60
+ visibleWhen: hasMessages
61
+ style:
62
+ direction: column
63
+ flex: 1
64
+ minHeight: "0"
65
+ children:
66
+ - $include: layouts/chat-active
67
+ - type: Box
68
+ style:
69
+ position: absolute
70
+ bottom: "0"
71
+ left: "0"
72
+ right: "0"
73
+ direction: column
74
+ children:
75
+ - $include: components/suggestions
76
+ - type: Ref
77
+ ref: composer-bar
@@ -0,0 +1,10 @@
1
+ title: Text Chat
2
+ description: The conversation surface, a welcome hero on an empty chat and the live message timeline once it begins.
3
+ whenToUse: Start talking to the assistant and see the conversation itself, the welcome screen to begin from, then the running exchange of messages once the chat is underway. Not for typing a message and not a page or card of its own, it is the chat surface inside your own apps.
4
+ category: Communication
5
+ version: 1.0.0
6
+
7
+ # The app-level LOAD MODE: absent, a component app would load in `focus`. Arrival
8
+ # inside a template is the tree's first declared state (chat); this key keeps the app
9
+ # load honest to it.
10
+ defaultState: chat
@@ -0,0 +1,59 @@
1
+ # THE CHAT SURFACE — a worked example component, shipped with Studio so the chat CX
2
+ # lives in ONE place your apps can share. It is yours: rename it, restyle it (it uses
3
+ # only foundation tokens, so your project's theme restyles it whole), or delete it.
4
+ # Guide: https://docs.unoverse.ai/design/components.md
5
+ unoverse: "1.0"
6
+ kind: component
7
+ name: text-chat
8
+ category: Communication
9
+ nodeSize:
10
+ width: 480
11
+ height: 400
12
+ props:
13
+ # `input: false` on all three — no workflow fills them, so none of them join the
14
+ # node's config contract. They exist so the preview has something to draw.
15
+ #
16
+ # chatState is THE PREVIEW AXIS: Studio writes it when you step Welcome/Active. The
17
+ # default is EMPTY on purpose — live, nothing writes it, the Switch falls to its
18
+ # default case and the conversation facts pick the mood. A non-empty default would
19
+ # pin the mood forever.
20
+ chatState:
21
+ type: string
22
+ input: false
23
+ default: ""
24
+ enum: ["", welcome, active]
25
+ # Preview defaults for the live fallback's guards (the conversation facts the
26
+ # platform projects for real).
27
+ isEmpty:
28
+ type: boolean
29
+ input: false
30
+ default: true
31
+ hasMessages:
32
+ type: boolean
33
+ input: false
34
+ default: false
35
+ # THE STATE TREE (State Model v2): ONE public state — the single-state permanent
36
+ # component: it lives in a template's core and templates never rearrange around it.
37
+ # The chat's two moods NEST beneath it as PRIVATE substates (the rearrange rule: the
38
+ # template wouldn't move, so they nest), which is also what makes them steppable in
39
+ # Studio. The mood is DERIVED from the conversation, so the shell's default case keeps
40
+ # its own conditions. The template has the state; this component just reacts.
41
+ state:
42
+ view:
43
+ states:
44
+ chat:
45
+ on: chatState
46
+ states:
47
+ welcome:
48
+ layout: chat-welcome
49
+ active:
50
+ layout: chat-active
51
+ root:
52
+ type: Switch
53
+ on: view
54
+ cases:
55
+ chat: { $include: layouts/chat }
56
+ # Ref placement: embedded in a template's core the component has no spawned
57
+ # instance slice, so `view` is absent from scope — the chat is still the one
58
+ # thing to draw.
59
+ default: { $include: layouts/chat }
@@ -0,0 +1,10 @@
1
+ # THE CHAT REGION — one shared component (text-chat) owns the whole chat CX: the
2
+ # welcome hero on an empty chat, the live timeline once messages exist, the suggestion
3
+ # chips and the composer. The switching rides the projected conversation facts inside
4
+ # the component; nothing is wired here. Shared by core and core-focus.
5
+ #
6
+ # A BARE ref resolves your own project's component: a ref inside a project's tree
7
+ # matches that project first. Qualify it (`<project>/text-chat`) only if you ship two
8
+ # projects that both carry the name.
9
+ type: Ref
10
+ ref: text-chat
@@ -1,7 +1,6 @@
1
1
  # THE CHAT COLUMN WITH A FOCUSED COMPONENT OVER IT — core, plus the focus surface
2
- # laid over the conversation region. A core VARIANT, not a copy: header, states and
3
- # composer are the same shared files core.yaml includes; the only difference is the
4
- # overlay slot.
2
+ # laid over it. A core VARIANT, not a copy: chat-region is the same shared file
3
+ # core.yaml includes; the only difference is the overlay slot.
5
4
  type: Box
6
5
  appWidth: chat
7
6
  style:
@@ -15,32 +14,5 @@ children:
15
14
  minHeight: "0"
16
15
  position: relative
17
16
  children:
18
- - $include: components/header
19
- - type: Box
20
- style:
21
- direction: column
22
- flex: 1
23
- minHeight: "0"
24
- position: relative
25
- when:
26
- - field: hasMessages
27
- eq: true
28
- apply:
29
- background: surface.base
30
- radiusTopLeft: 2xl
31
- overflow: hidden
32
- children:
33
- - $include: states/welcome
34
- - $include: states/conversation
35
- - type: Box
36
- style:
37
- position: absolute
38
- bottom: "0"
39
- left: "0"
40
- right: "0"
41
- direction: column
42
- children:
43
- - $include: components/suggestions
17
+ - $include: components/chat-region
44
18
  - $include: components/focus-overlay
45
- - type: Ref
46
- ref: composer-bar
@@ -11,31 +11,4 @@ children:
11
11
  minHeight: "0"
12
12
  position: relative
13
13
  children:
14
- - $include: components/header
15
- - type: Box
16
- style:
17
- direction: column
18
- flex: 1
19
- minHeight: "0"
20
- position: relative
21
- when:
22
- - field: hasMessages
23
- eq: true
24
- apply:
25
- background: surface.base
26
- radiusTopLeft: 2xl
27
- overflow: hidden
28
- children:
29
- - $include: states/welcome
30
- - $include: states/conversation
31
- - type: Box
32
- style:
33
- position: absolute
34
- bottom: "0"
35
- left: "0"
36
- right: "0"
37
- direction: column
38
- children:
39
- - $include: components/suggestions
40
- - type: Ref
41
- ref: composer-bar
14
+ - $include: components/chat-region
@@ -16,22 +16,17 @@ inputSchema:
16
16
  description: The user's request
17
17
 
18
18
  # THE TEMPLATE TREE (State Model v2): one declaration, everything follows.
19
- # Top-level order IS the priority ladder (base first, then reaction states). NESTING
20
- # is containment: welcome exists only inside standard — the compiler strips it from
21
- # every other arrangement — and it is the landing default. No stateOrder, no
22
- # hand-written guards.
23
- # NOTE: the conversation timeline is NOT declared as a substate — it is standard's own
24
- # shell content (states/conversation), included by the shared chrome (components/core)
25
- # and legitimately drawn under the focus overlay too. Declaring it would contain it to
26
- # standard and empty the chat column in every reaction state.
19
+ # Top-level order IS the priority ladder (base first, then reaction states). No
20
+ # stateOrder, no hand-written guards.
21
+ # NOTE: the chat itself is not a state here. Its two moods (welcome hero / live
22
+ # timeline) live inside the text-chat component, which both cores draw via
23
+ # components/chat-region. The component defaults to welcome on an empty conversation
24
+ # and hands over to the timeline the moment messages exist.
27
25
  states:
28
- standard:
29
- states:
30
- welcome: {}
26
+ standard: {}
31
27
  focused: {}
32
28
 
33
29
  preview:
34
- conversation: [ markdown-renderer ]
35
30
  focused: [ card ]
36
31
 
37
32
  autoTrigger: false
@@ -86,13 +86,18 @@ export function createProject(home, name) {
86
86
  cpSync(sampleStyles, join(root, "styles"), { recursive: true });
87
87
  }
88
88
 
89
- // A worked example TEMPLATE (a generic chat: welcome hero, suggestions, focus
90
- // overlay — the bpp-chat-layout shape, neutralized to foundation tokens), so
91
- // templates/ is never an empty folder and the first app is a rename away. Same
89
+ // A worked example TEMPLATE (a generic chat: focus overlay + the chat region),
90
+ // so templates/ is never an empty folder and the first app is a rename away. Same
92
91
  // rule as the styles: copied only into an empty folder, then the developer's.
93
92
  const sampleTemplate = join(dirname(fileURLToPath(import.meta.url)), "sample-template/chat");
94
93
  if (existsSync(sampleTemplate) && readdirSync(join(root, "templates")).length === 0) {
95
94
  cpSync(sampleTemplate, join(root, "templates/chat"), { recursive: true });
95
+ // Its other half: the chat CX itself (welcome hero, timeline, suggestions,
96
+ // composer) is a COMPONENT the template draws via `Ref`, so the surface lives once
97
+ // and every app of this project shares it. Copied with the template, never without
98
+ // it — a template whose chat-region resolves nothing renders an empty column.
99
+ const sampleComponent = join(dirname(fileURLToPath(import.meta.url)), "sample-component/text-chat");
100
+ if (existsSync(sampleComponent)) cpSync(sampleComponent, join(root, "components/text-chat"), { recursive: true });
96
101
  }
97
102
  return root;
98
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -715,9 +715,16 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
715
715
  </>
716
716
  )}
717
717
  <div className={isFocused ? "min-h-0 flex-1 overflow-auto" : "min-w-0 flex-1 overflow-auto p-10"}>
718
+ {/* THE STAGE OFFERS ITS HEIGHT; the component decides whether to take it.
719
+ A single-row GRID, not a flex column: the SDK's shadow host is
720
+ `display: contents`, so `.uno-isolate-root` is the item here, and a grid
721
+ item stretches on BOTH axes by default — a flex item only stretches
722
+ across. That gives the isolate root a real height to resolve against,
723
+ so a full surface (chat, voice) fills and a card still sizes to its
724
+ content. */}
718
725
  <div
719
726
  key={`${masterState}:${previewNonce}`}
720
- className={`mx-auto max-w-full ${isFocused ? "h-full" : "transition-[width] duration-200"}`}
727
+ className={`mx-auto max-w-full ${isFocused ? "h-full" : "grid h-full grid-rows-1 transition-[width] duration-200"}`}
721
728
  style={{ width: sourceOpen ? "100%" : (stageWidth ?? "100%"), ...(isFocused ? { animation: "wb-focusFadeIn 0.2s ease-out" } : {}) }}
722
729
  >
723
730
  {selected && <UnoverseComponent client={client} store={store} chatId={CHAT_ID} nodeId={selected} theme={theme} onAction={(a) => setLastAction(typeof a === "string" ? a : a?.type ?? null)} />}
@@ -151,7 +151,7 @@ export function AppEngine(cfg: AppConfig) {
151
151
  // the ordinary case (one org, one `chat` size), and the incoming app then reported
152
152
  // nothing: its frame sat at the host default (100%) while its core panel held its
153
153
  // declared width, leaving a dead band beside it (observed live 2026-08-13,
154
- // bpp-voice-layout). Clearing the guard here makes the first report after an app
154
+ // bpp-voice). Clearing the guard here makes the first report after an app
155
155
  // switch unconditional; the guard still suppresses repeats within one app.
156
156
  lastWidthRef.current = undefined;
157
157
  const report = () => {
@@ -133,7 +133,7 @@ place. Work through them in order.
133
133
 
134
134
  1. **Copy the folder.** `design/<source>/` → `design/<neworg>/`, keeping `styles/`, `components/`,
135
135
  `templates/`.
136
- 2. **Rename the templates.** Folder AND the id inside the manifest. `bpp-chat-layout`
136
+ 2. **Rename the templates.** Folder AND the id inside the manifest. `bpp-chat`
137
137
  becomes `lyceum-chat-layout` in both places. Template ids are org-qualified and they
138
138
  are referenced by string.
139
139
  3. **Component names stay as copied.** Names are unique per org, not across orgs, so a
@@ -88,7 +88,9 @@ The rules, in the order they bite:
88
88
  - **Each state owns its layout(s).** `layout:` is OPTIONAL: a state with no declaration
89
89
  draws the layout of its own name, and `{}` is a complete state. Write `layout:` only
90
90
  when the filename differs. A layout is never addressable on its own; only its owning
91
- state selects it. Name states for what they MEAN (`detail`), never for their drawing.
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
92
+ (`step-register`, `callState-idle`) and is declared via `layout:`; unprefixed
93
+ files are arrangements (exemplars: `course-application`, `voice-chat`).
92
94
  - **Shell vs steps.** A state's own layout is its SHELL, always on while the state is
93
95
  active. Its nested substates are the STEPS, the only choices inside it, addressed by
94
96
  state names (layout filenames are plumbing).
@@ -120,6 +122,24 @@ The rules, in the order they bite:
120
122
  static → hardcode);
121
123
  - workflow-fed data → **`props` with `input: true`** (the `default` is the preview mock).
122
124
  Usually empty.
125
+ - **Projected-field preview rule:** any LOCKED/projected field a
126
+ component branches on (`callState`, `isEmpty`, `hasMessages`) must ALSO be declared as
127
+ a prop with a preview default: Studio's bare component preview supplies ONLY prop
128
+ defaults, so an undeclared projected read renders NOTHING there; live, the projected
129
+ value wins and the default never applies. A Ref-embedded component's root Switch
130
+ likewise needs a `default:` case (`view` is absent without a spawned slice).
131
+ - **Who owns a mood: the DRIVER decides.** Ask what WRITES the discriminant:
132
+ (1) the component's own buttons (`setValue`) → component substates on a private axis,
133
+ names from the design (`course-application`); (2) a service projecting a NAMED value
134
+ (`callState`) → component substates named for those values + a preview prop
135
+ (`voice-chat`); (3) DERIVED from conversation facts (`isEmpty`/`hasMessages`) → the
136
+ TEMPLATE tier owns it: a condition-guarded mood, or guarded drawings inside an
137
+ embedded component reacting to the same facts — the template has the state,
138
+ components just react. NEVER model a derived mood as component named substates, and
139
+ never borrow another field's values (`lifecycle`) as state names (bpp `text-chat`). Declare the axis as a prop with enum + default
140
+ (preview only; live the projection wins), give the root Switch a `default:` case for
141
+ Ref placement, and prefix the step drawings with the axis (`callState-idle`,
142
+ `lifecycle-active`). Exemplars: `voice-chat`, bpp `text-chat`.
123
143
  - **Prop names = the data contract. Never invent them.** Source data hydrates props
124
144
  BY NAME (as-is, no mapping); a name the source doesn't carry silently renders the
125
145
  preview default (tell: title streams, image stays mock). Content-attached cards use
@@ -9,7 +9,7 @@ Everything in the component playbook (closed vocabulary, tokens, scale steps) ap
9
9
  here too. **Study an exemplar before writing**: templates are always a project's own
10
10
  (none ship in the base set), so your references are the canonical manifest tree inlined
11
11
  below, [design/templates](https://docs.unoverse.ai/design/templates.md), and any template
12
- already in the workspace. (Monorepo: `design/sab/templates/sab-chat-layout`.)
12
+ already in the workspace. (Monorepo: `design/sab/templates/sab-chat`.)
13
13
 
14
14
  ## THE model: the component drives; the template reacts by name
15
15