@unoverse-platform/studio 0.1.0
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/bin/studio.mjs +124 -0
- package/index.html +12 -0
- package/local/catalog.ts +105 -0
- package/local/keys.ts +118 -0
- package/local/nodes.ts +77 -0
- package/local/plugin.ts +401 -0
- package/local/scaffold.d.mts +13 -0
- package/local/scaffold.mjs +85 -0
- package/package.json +43 -0
- package/public/logo.png +0 -0
- package/screens/AtomsView.tsx +206 -0
- package/screens/ComponentsView.tsx +551 -0
- package/screens/ConfigForm.tsx +217 -0
- package/screens/DesignSystemView.tsx +324 -0
- package/screens/IdentityHeader.tsx +88 -0
- package/screens/PromptBlocksView.tsx +137 -0
- package/screens/SkillsView.tsx +167 -0
- package/screens/TemplatesView.tsx +1005 -0
- package/screens/index.ts +26 -0
- package/screens/states.ts +44 -0
- package/src/App.tsx +252 -0
- package/src/ConnectUniverse.tsx +232 -0
- package/src/NewProject.tsx +89 -0
- package/src/NodeKeys.tsx +98 -0
- package/src/NodesView.tsx +443 -0
- package/src/PublishDialog.tsx +309 -0
- package/src/UniverseSession.tsx +121 -0
- package/src/host.ts +165 -0
- package/src/index.css +7 -0
- package/src/main.tsx +11 -0
- package/src/universes.ts +210 -0
- package/vendor/sdk/appEngine.tsx +219 -0
- package/vendor/sdk/lib/UnoverseComponent.tsx +120 -0
- package/vendor/sdk/lib/connection.tsx +302 -0
- package/vendor/sdk/lib/core/actions.ts +84 -0
- package/vendor/sdk/lib/core/client.ts +134 -0
- package/vendor/sdk/lib/core/conditions.ts +43 -0
- package/vendor/sdk/lib/core/connection.ts +142 -0
- package/vendor/sdk/lib/core/index.ts +32 -0
- package/vendor/sdk/lib/core/store.ts +455 -0
- package/vendor/sdk/lib/core/types.ts +194 -0
- package/vendor/sdk/lib/index.ts +59 -0
- package/vendor/sdk/lib/isolate.tsx +70 -0
- package/vendor/sdk/lib/primitives.tsx +453 -0
- package/vendor/sdk/lib/realtime/audioUtils.ts +40 -0
- package/vendor/sdk/lib/realtime/types.ts +45 -0
- package/vendor/sdk/lib/realtime/useAudioCapture.ts +237 -0
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +145 -0
- package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +164 -0
- package/vendor/sdk/lib/render.tsx +178 -0
- package/vendor/sdk/lib/streamed.tsx +65 -0
- package/vendor/sdk/lib/style.ts +227 -0
- package/vendor/sdk/lib/template.tsx +521 -0
- package/vendor/sdk/lib/theme.ts +55 -0
- package/vendor/sdk/lib/voice.tsx +311 -0
- package/vendor/sdk/main.tsx +96 -0
- package/vite.config.ts +70 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unoverse/core — shared neutral types.
|
|
3
|
+
*
|
|
4
|
+
* The neutral definition shape, the interaction vocabulary, and the resolved-theme
|
|
5
|
+
* SHAPE (values live on the server). Pure types — no runtime, no imports.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---- The neutral definition shape (mirrors apps/unoverse/definitions/*.json) ----
|
|
9
|
+
|
|
10
|
+
export interface UnoverseNode {
|
|
11
|
+
type: string; // Box | Stack | Row | Column | Text | Image | Button | ComponentSlot | Skeleton | ...
|
|
12
|
+
style?: Record<string, unknown>;
|
|
13
|
+
bind?: Record<string, string>; // target prop -> data field
|
|
14
|
+
visibleWhen?: VisibleWhen; // render only when the condition holds (truthy field, or {field,eq/ne})
|
|
15
|
+
action?: string | ActionSpec; // dispatched on interaction (bare string → server, or a setValue envelope)
|
|
16
|
+
placeholder?: string; // Input: empty-field copy (content, not style)
|
|
17
|
+
/** Box: keep the scroll pinned to the newest content — smooth-scroll to the end when
|
|
18
|
+
* content grows (new turn or streaming text), unless the user has scrolled up. Generic
|
|
19
|
+
* scroll-container behavior, opt-in via data (e.g. the conversation state's scroll box). */
|
|
20
|
+
autoScroll?: boolean;
|
|
21
|
+
/** Box: a sticky bar (its own `position: sticky` + `top` in rx) that stays HIDDEN while
|
|
22
|
+
* the element before it (its previous sibling — e.g. a hero image) is on screen, then
|
|
23
|
+
* fades in once that sibling scrolls away. Overlays the content (no flow gap). Generic
|
|
24
|
+
* scroll-reveal behavior, opt-in via data. */
|
|
25
|
+
revealOnStick?: boolean;
|
|
26
|
+
/** THE PROTOCOL'S LOADING SEMANTIC: while this node's bound field carries no data yet,
|
|
27
|
+
* render its Skeleton ghost in place (variant inferred from the node type; look fully
|
|
28
|
+
* theme-driven). One word gives any progressively-composed page (briefed components)
|
|
29
|
+
* honest loading states — each platform SDK realizes it with its native idiom. */
|
|
30
|
+
skeleton?: boolean;
|
|
31
|
+
/** TEMPLATE LAYOUT ROOT ONLY — the guest-control law: a pinned layout, once active,
|
|
32
|
+
* changes only by a GUEST write (tap, its ✕) — background arrivals cannot displace
|
|
33
|
+
* it. Yields also when its component leaves the view or the template swaps. */
|
|
34
|
+
pinned?: boolean;
|
|
35
|
+
children?: UnoverseNode[];
|
|
36
|
+
// --- template primitives (UNOVERSE_SPEC.md §2e) ---
|
|
37
|
+
/** ComponentSlot: which components to pull from the store timeline.
|
|
38
|
+
* `where` selects by COMPONENT STATE (the reaction contract, STATE_MODEL §5b): any
|
|
39
|
+
* component whose slice matches, newest state-write first — NEVER by type/id (a
|
|
40
|
+
* template naming a component type was the violation; the vocabulary has no way to
|
|
41
|
+
* say it). The field/value are the template author's data; core knows no state names. */
|
|
42
|
+
select?: { from?: "latest" | "all"; limit?: number; where?: { field: string; eq?: unknown; ne?: unknown } };
|
|
43
|
+
/** ComponentSlot: rendered when the slot resolves to zero components. */
|
|
44
|
+
fallback?: UnoverseNode;
|
|
45
|
+
/** ComponentSlot: chrome rendered AROUND the matches — only when the slot resolved
|
|
46
|
+
* (the non-empty twin of `fallback`). Inside it, a bare `{ "type": "ComponentSlot" }`
|
|
47
|
+
* (no select) receives the outer slot's matches. This is how a template surface
|
|
48
|
+
* EXISTS only while a component is in its state (§5b: derived, not stored) — e.g. a
|
|
49
|
+
* focus overlay that appears because something is focused and vanishes when it isn't. */
|
|
50
|
+
frame?: UnoverseNode;
|
|
51
|
+
/** Skeleton: placeholder variant. */
|
|
52
|
+
variant?: "text" | "card" | "image";
|
|
53
|
+
/** Timeline: data sub-tree rendered for a user turn (binds `text`). ALL bubble UX lives here, not the SDK. */
|
|
54
|
+
user?: UnoverseNode;
|
|
55
|
+
/** Timeline: data sub-tree rendered for an assistant turn (its ComponentSlot is scoped to that turn). */
|
|
56
|
+
assistant?: UnoverseNode;
|
|
57
|
+
/** Each: the per-item subtree, rendered once per element of the bound array (`bind.items`),
|
|
58
|
+
* with that element as its data scope. The node's own `style` is the list container. */
|
|
59
|
+
template?: UnoverseNode;
|
|
60
|
+
/** Literal content — components are microapps: static copy/lists/images are HARDCODED
|
|
61
|
+
* in the def; props exist only for state (UNOVERSE_AUTHORING.md). `bind` wins when set. */
|
|
62
|
+
value?: unknown; // Text
|
|
63
|
+
items?: unknown[]; // Each
|
|
64
|
+
src?: string; // Image
|
|
65
|
+
alt?: string; // Image
|
|
66
|
+
/** Timeline: static author data merged into every assistant turn's data scope
|
|
67
|
+
* (e.g. avatarUrl, thinkingText). The SDK forwards it verbatim — it interprets
|
|
68
|
+
* nothing chat-specific; the turn STATE flags (streaming/thinking) are added on top. */
|
|
69
|
+
assistantData?: Record<string, unknown>;
|
|
70
|
+
/** Timeline: static author data merged into every user turn's data scope. */
|
|
71
|
+
userData?: Record<string, unknown>;
|
|
72
|
+
/** Icon: the served glyph NAME (literal) — or bind a data field via `bind.name`. */
|
|
73
|
+
icon?: string;
|
|
74
|
+
/** Disable the element (Input/Button) when this data field is truthy; the node's
|
|
75
|
+
* `style.disabled` is merged while disabled. */
|
|
76
|
+
disabledWhen?: string;
|
|
77
|
+
/** Switch: the data field whose value selects which `cases` branch renders. */
|
|
78
|
+
on?: string;
|
|
79
|
+
/** Switch: a value→branch map (string-keyed). The branch for `data[on]` renders;
|
|
80
|
+
* if none matches, `cases.default` renders; if neither, nothing renders. The
|
|
81
|
+
* declarative whole-view swap (a wizard step, inline↔focused) — one place, mutually
|
|
82
|
+
* exclusive, instead of a `visibleWhen` repeated on every branch. Each branch is a
|
|
83
|
+
* normal subtree (commonly a `{ "$include": "step-x" }`, expanded at load). */
|
|
84
|
+
cases?: Record<string, UnoverseNode>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface UnoverseDefinition {
|
|
88
|
+
unoverse: string;
|
|
89
|
+
kind: "component" | "template";
|
|
90
|
+
name: string;
|
|
91
|
+
/** For a TEMPLATE: the client org it belongs to (server-injected). Names the org's
|
|
92
|
+
* theme (`unoverse://theme/{org}/light`). Universal components carry no org. */
|
|
93
|
+
org?: string;
|
|
94
|
+
description?: string;
|
|
95
|
+
/** EXTERNAL contract — the rare workflow-fed inputs (`input: true`). A contained
|
|
96
|
+
* microapp has an EMPTY (or absent) props block. */
|
|
97
|
+
props?: Record<string, unknown>;
|
|
98
|
+
/** INTERNAL state + initial values — the keys the component's own actions write
|
|
99
|
+
* (step, phase, its defaultState signal). Never external, never workbench controls;
|
|
100
|
+
* merged into the render scope beneath the live slice. */
|
|
101
|
+
state?: Record<string, unknown>;
|
|
102
|
+
root: UnoverseNode;
|
|
103
|
+
/** TEMPLATE LAYOUTS (name-sync, docs/design/05): present only when the template
|
|
104
|
+
* ships multiple layouts/ files. Each is a full arrangement; the active one is
|
|
105
|
+
* the layout whose NAME matches the latest surfaced component view, falling back
|
|
106
|
+
* to `defaultLayout`. Absent → classic single-layout behavior via `root`. */
|
|
107
|
+
layouts?: Record<string, UnoverseNode>;
|
|
108
|
+
defaultLayout?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- The interaction vocabulary (UNOVERSE_SPEC.md §2e-3) ----
|
|
112
|
+
//
|
|
113
|
+
// A small, declarative layer — NOT a programming language. Stac-inspired
|
|
114
|
+
// (set_value + Multi Action + visible-if): an `action` is a bare string (→ the
|
|
115
|
+
// workflow) or a `setValue` envelope (a LOCAL store write, the ported `updateData`);
|
|
116
|
+
// `visibleWhen` is a truthy field name or an equality test.
|
|
117
|
+
|
|
118
|
+
/** One `setValue` patch. `value` is a literal OR a `{{field}}` / `{{a.b}}` binding
|
|
119
|
+
* resolved against the action's data scope (e.g. the tapped item inside an `Each`). */
|
|
120
|
+
export interface ActionValue {
|
|
121
|
+
key: string;
|
|
122
|
+
value: unknown;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A declarative action. `setValue` merges `values` into THIS component's
|
|
126
|
+
* `chatId:nodeId` slice (no round-trip); any other `type` routes to the workflow
|
|
127
|
+
* (optionally narrowed by `send`). `then` chains a follow-up (Stac Multi Action). */
|
|
128
|
+
export interface ActionSpec {
|
|
129
|
+
// The SDK hardcodes no UI concept — only generic writes + a server route:
|
|
130
|
+
type: string; // "setValue" (component slice) | "setTemplateValue" (template state) | "input" | "submit"/custom (→ server)
|
|
131
|
+
values?: ActionValue[]; // setValue / setTemplateValue patches (the dev's chosen keys)
|
|
132
|
+
send?: string[]; // server route: which scope fields to include (default: all)
|
|
133
|
+
then?: ActionSpec | string; // optional chained follow-up
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A data condition, evaluated against the render data scope. Pick ONE test:
|
|
137
|
+
* `eq` (equals), `ne` (not-equals), `in` (one of a list) — or, with none of them, a
|
|
138
|
+
* truthy test on `field`. Shared by `visibleWhen`, the `Switch` node, and `style.when`,
|
|
139
|
+
* so the three "react to state" mechanisms speak ONE predicate. NOT a programming
|
|
140
|
+
* language — no `and`/`or`/arithmetic; derive anything richer in the producing node. */
|
|
141
|
+
export type Condition = { field: string; eq?: unknown; ne?: unknown; in?: unknown[] };
|
|
142
|
+
|
|
143
|
+
/** `visibleWhen`: a data field name (truthy; an empty array counts as empty) OR a
|
|
144
|
+
* `Condition`. The element renders only when it holds. */
|
|
145
|
+
export type VisibleWhen = string | Condition;
|
|
146
|
+
|
|
147
|
+
/** A data-driven style variant — the generalization of `style.disabled`. When its
|
|
148
|
+
* `Condition` holds against the data scope, `apply` (ordinary style vocab) is merged
|
|
149
|
+
* OVER the base style (later variants win). Authored as `style.when: StyleVariant[]`.
|
|
150
|
+
* This is render-time (data) styling; interaction styling (hover/active/disabled) stays
|
|
151
|
+
* in the injected pseudo-state keys. Lets one element restyle by state instead of
|
|
152
|
+
* cloning it under opposite `visibleWhen`s. */
|
|
153
|
+
export type StyleVariant = Condition & { apply: Record<string, unknown> };
|
|
154
|
+
|
|
155
|
+
// ---- The resolved theme (SHAPE only — values live on the server) ----
|
|
156
|
+
//
|
|
157
|
+
// The MCP server OWNS THE VISUAL: it resolves the design tokens (apps/unoverse/
|
|
158
|
+
// rx/styles) live and serves this shape at `unoverse://theme/{name}`. This is the
|
|
159
|
+
// token SHAPE, never the token VALUES — the SDK fetches the values (readTheme),
|
|
160
|
+
// it does NOT bundle them. A brand change in rx/styles is therefore refresh-only,
|
|
161
|
+
// exactly like a definition change (UNOVERSE_SPEC §2d-1).
|
|
162
|
+
export interface ResolvedTheme {
|
|
163
|
+
color: Record<string, string>;
|
|
164
|
+
/** Webfont stylesheet URLs (served `fonts.stylesheets` data) — injected into
|
|
165
|
+
* document.head by the SDK (shadow roots can't register @font-face). */
|
|
166
|
+
fonts?: string[];
|
|
167
|
+
/** Dimension scale (theme.space ← rx/styles/base/spacing.json) — used for BOTH spacing
|
|
168
|
+
* (gap/padding/margin) AND element sizes (width/height), Tailwind-style. */
|
|
169
|
+
space: Record<string, string>;
|
|
170
|
+
radius: Record<string, string>;
|
|
171
|
+
shadow: Record<string, string>;
|
|
172
|
+
borderWidth: Record<string, string>;
|
|
173
|
+
/** Line-height tokens (theme.lineHeight ← font.lineHeight) — tight/snug/normal/relaxed. */
|
|
174
|
+
lineHeight: Record<string, string | number>;
|
|
175
|
+
text: Record<string, Record<string, string | number>>;
|
|
176
|
+
skeleton: Record<string, unknown>;
|
|
177
|
+
prose: Record<string, unknown>;
|
|
178
|
+
/** STANDARD APP SIZES (theme.appSize ← rx/styles/semantic/app-sizes.json) — the named
|
|
179
|
+
* width blocks `appWidth` references ("chat"/"rail"/"panel" → raw host-facing CSS).
|
|
180
|
+
* Resolved by the SDK like every other token — one pipeline. */
|
|
181
|
+
appSize?: Record<string, string>;
|
|
182
|
+
/** Named keyframes (theme.keyframes ← rx/styles/semantic/keyframes.json), injected
|
|
183
|
+
* once per render root. Lets any definition animate via `style.animation` — no SDK edit. */
|
|
184
|
+
keyframes: Record<string, Record<string, Record<string, string>>>;
|
|
185
|
+
/** Named icon glyphs as SERVED DATA. The SERVER sources these from an icon pack
|
|
186
|
+
* (control plane) → `{ viewBox, attrs, children }` where children are raw SVG
|
|
187
|
+
* elements `[tag, attrs]`. The generic `Icon` leaf renders them, sized/coloured by
|
|
188
|
+
* node.style. The SDK never bundles the pack — icons are served like any token. */
|
|
189
|
+
icons: Record<string, { viewBox?: string; attrs?: Record<string, unknown>; children?: [string, Record<string, unknown>][] }>;
|
|
190
|
+
/** Base render-root settings (theme.root ← rx/styles/semantic/root.json) — a CSS
|
|
191
|
+
* object the SDK spreads onto its outermost wrapper (e.g. font-smoothing). Served,
|
|
192
|
+
* not authored: a whole-tree rendering normalization, like legacy styles/base.css. */
|
|
193
|
+
root: Record<string, unknown>;
|
|
194
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export { UnoverseComponent } from "./UnoverseComponent";
|
|
2
|
+
export type { UnoverseComponentProps } from "./UnoverseComponent";
|
|
3
|
+
export { StreamedUnoverseComponent, useUnoverseInstance } from "./streamed";
|
|
4
|
+
export type { StreamedUnoverseComponentProps } from "./streamed";
|
|
5
|
+
export { StreamedUnoverseTemplate, selectPointers, computeAppWidth, resolveActiveLayout } from "./template";
|
|
6
|
+
export type { StreamedUnoverseTemplateProps } from "./template";
|
|
7
|
+
export { renderNode, styleToCss, keyframesCss } from "./render";
|
|
8
|
+
export type { ActionHandler, SlotResolver } from "./render";
|
|
9
|
+
export { useUnoverseTheme } from "./theme";
|
|
10
|
+
export { IsolatedRoot } from "./isolate";
|
|
11
|
+
export { useUnoverseConnection } from "./connection";
|
|
12
|
+
export type { UnoverseConnectionConfig, UnoverseSessionParams, UnoverseConnection } from "./connection";
|
|
13
|
+
// The `voice` native service (UNOVERSE_SPEC §2e-1 Tier-3). Channel-instantiated; its
|
|
14
|
+
// neutral state is spread into a `service: "voice"` template's scope, its actions
|
|
15
|
+
// dispatched from the template. Audio rides the /ws/gravity WS lane (§5), not MCP.
|
|
16
|
+
export { useVoiceService } from "./voice";
|
|
17
|
+
export type { UseVoiceServiceConfig, VoiceService, VoiceServiceState, VoiceSession, VoiceConnectionStatus } from "./voice";
|
|
18
|
+
// Re-export core's FULL public API. core is BUNDLED into this package (tsup noExternal), so
|
|
19
|
+
// consumers import EVERYTHING from `@unoverse/sdk` and never depend on a
|
|
20
|
+
// separate `-core` install — eliminating react↔core version/export drift entirely. EXPLICIT
|
|
21
|
+
// named re-exports (NOT `export *`): tsup's .d.ts bundler inlines `export *` from a bundled
|
|
22
|
+
// dep as LOCAL declarations WITHOUT re-exporting them, so consumers' type imports break
|
|
23
|
+
// ("declares X locally, but it is not exported"). Named re-exports emit correctly.
|
|
24
|
+
export {
|
|
25
|
+
ComponentStore,
|
|
26
|
+
UnoverseClient,
|
|
27
|
+
applyServerMessage,
|
|
28
|
+
dispatchAction,
|
|
29
|
+
resolveValue,
|
|
30
|
+
matches,
|
|
31
|
+
visible,
|
|
32
|
+
selectCase,
|
|
33
|
+
} from "./core";
|
|
34
|
+
export type {
|
|
35
|
+
ResolvedTheme,
|
|
36
|
+
UnoverseDefinition,
|
|
37
|
+
UnoverseNode,
|
|
38
|
+
ActionSpec,
|
|
39
|
+
ActionValue,
|
|
40
|
+
ActionContext,
|
|
41
|
+
Condition,
|
|
42
|
+
VisibleWhen,
|
|
43
|
+
StyleVariant,
|
|
44
|
+
LifecycleState,
|
|
45
|
+
Pointer,
|
|
46
|
+
Turn,
|
|
47
|
+
UserTurn,
|
|
48
|
+
AssistantTurn,
|
|
49
|
+
UnoverseEvent,
|
|
50
|
+
ComponentInitEvent,
|
|
51
|
+
ComponentDataEvent,
|
|
52
|
+
UnoverseClientOptions,
|
|
53
|
+
ServerMessage,
|
|
54
|
+
ServerComponentInit,
|
|
55
|
+
ServerComponentData,
|
|
56
|
+
ServerWorkflowState,
|
|
57
|
+
ServerTemplateData,
|
|
58
|
+
ServerSessionReady,
|
|
59
|
+
} from "./core";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <IsolatedRoot> — mounts `children` inside an open Shadow DOM for complete CSS isolation.
|
|
3
|
+
*
|
|
4
|
+
* The SDK is the renderer for EVERY consumer (the canvas node preview, the live channel,
|
|
5
|
+
* the workbench, external host apps), so the isolation boundary lives HERE — once — rather
|
|
6
|
+
* than being bolted onto each host (today only the canvas wraps; the channel renders
|
|
7
|
+
* unprotected, so page styles and component styles leak both ways).
|
|
8
|
+
*
|
|
9
|
+
* Why a portal, not a nested React root: `createPortal` renders into the shadow root while
|
|
10
|
+
* keeping the SAME React tree/context, so the SDK's client/store/theme context still flows
|
|
11
|
+
* to the rendered components. A separate `createRoot` would sever that.
|
|
12
|
+
*
|
|
13
|
+
* Web-only — this file lives in `unoverse-react`, never in the framework-agnostic core
|
|
14
|
+
* (Flutter/Swift have no DOM). Token VALUES are already resolved inline by the renderer, so
|
|
15
|
+
* concrete styles work unchanged inside the shadow root; only inherited typography needs the
|
|
16
|
+
* reset below, after which the served `theme.root` is the sole typographic baseline.
|
|
17
|
+
*/
|
|
18
|
+
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
|
19
|
+
import { createPortal } from "react-dom";
|
|
20
|
+
|
|
21
|
+
// Shadow DOM blocks style SELECTORS but NOT inheritance: inherited properties (font, colour,
|
|
22
|
+
// line-height, letter-spacing) still pierce the boundary, and <button>/<input> fall back to
|
|
23
|
+
// the UA font. `:host { all: initial }` neutralizes the inherited cascade; `display: contents`
|
|
24
|
+
// keeps the host out of layout (the consumer owns the outer box). The themed child the SDK
|
|
25
|
+
// passes in (theme.root) then re-establishes the design-system typography.
|
|
26
|
+
const RESET_CSS = `
|
|
27
|
+
:host { all: initial; display: contents; }
|
|
28
|
+
.uno-isolate-root { box-sizing: border-box; }
|
|
29
|
+
.uno-isolate-root *, .uno-isolate-root *::before, .uno-isolate-root *::after { box-sizing: border-box; }
|
|
30
|
+
.uno-isolate-root button, .uno-isolate-root input, .uno-isolate-root textarea, .uno-isolate-root select {
|
|
31
|
+
font: inherit; letter-spacing: inherit; color: inherit; margin: 0;
|
|
32
|
+
}
|
|
33
|
+
/* Scrollbars hidden by default across every scroll container — scrolling still works,
|
|
34
|
+
the bar is just not painted. Generic chrome that rx can't author per-node (pseudo-
|
|
35
|
+
elements), so it lives with the structural reset. */
|
|
36
|
+
.uno-isolate-root * { scrollbar-width: none; }
|
|
37
|
+
.uno-isolate-root *::-webkit-scrollbar { width: 0; height: 0; display: none; }
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export function IsolatedRoot({ children, rootStyle }: { children: ReactNode; rootStyle?: CSSProperties }) {
|
|
41
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const [shadow, setShadow] = useState<ShadowRoot | null>(null);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const host = hostRef.current;
|
|
46
|
+
if (!host) return;
|
|
47
|
+
// Reuse an existing shadow root across re-mounts (attachShadow throws if called twice).
|
|
48
|
+
setShadow(host.shadowRoot ?? host.attachShadow({ mode: "open" }));
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div ref={hostRef} style={{ display: "contents" }}>
|
|
53
|
+
{shadow &&
|
|
54
|
+
createPortal(
|
|
55
|
+
<>
|
|
56
|
+
<style>{RESET_CSS}</style>
|
|
57
|
+
{/* Apply the served DS render-root (theme.root: font family/size/weight/line-height/
|
|
58
|
+
colour + smoothing) DIRECTLY to this REAL container — not a `display:contents`
|
|
59
|
+
wrapper, whose inherited-font cascade is unreliable inside a shadow root. This is
|
|
60
|
+
the Unoverse equivalent of the legacy `.gravity-component` baseline, so every node
|
|
61
|
+
in the tree inherits the design-system typography. */}
|
|
62
|
+
<div className="uno-isolate-root" style={rootStyle}>
|
|
63
|
+
{children}
|
|
64
|
+
</div>
|
|
65
|
+
</>,
|
|
66
|
+
shadow as unknown as Element,
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|