@unoverse-platform/studio 0.1.103 → 0.1.104
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 +4 -2
- package/screens/AtomsView.tsx +2 -1
- package/screens/ComponentsView.tsx +2 -1
- package/screens/NativeAppHost.tsx +1 -2
- package/screens/TemplatesLibraryView.tsx +14 -1
- package/screens/TemplatesView.tsx +2 -1
- package/screens/index.ts +4 -0
- package/screens/react/unoverse.tsx +281 -0
- package/vendor/base/src/agent-mcp/index.ts +1 -1
- package/vendor/base/src/agent-mcp/invoke.ts +6 -2
- package/vendor/base/src/agent-mcp/tools.ts +14 -5
- package/vendor/base/src/agent-mcp/types.ts +6 -0
- package/vendor/base/src/auth/auth.ts +90 -23
- package/vendor/base/src/definitions/load/compile/sections.ts +79 -0
- package/vendor/base/src/definitions/load/compile/template.ts +23 -2
- package/vendor/base/src/definitions/model.ts +9 -0
- package/vendor/base/src/definitions/registry/catalog.ts +6 -7
- package/vendor/base/src/executor.ts +25 -1
- package/vendor/base/src/manifests/executor/toolBridge.ts +25 -19
- package/vendor/base/src/manifests/runtime/channels/final.ts +4 -1
- package/vendor/base/src/manifests/runtime/duplex/session.ts +3 -9
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +28 -25
- package/vendor/base/src/platform/serviceCalls.ts +13 -0
- package/vendor/sdk/appEngine.ts +363 -0
- package/vendor/sdk/lib/actions.ts +36 -0
- package/vendor/sdk/lib/citations.ts +48 -0
- package/vendor/sdk/lib/core/apps.ts +18 -0
- package/vendor/sdk/lib/core/client.ts +3 -3
- package/vendor/sdk/lib/core/store.ts +6 -0
- package/vendor/sdk/lib/dom/app.ts +546 -0
- package/vendor/sdk/lib/dom/chrome.ts +125 -0
- package/vendor/sdk/lib/dom/component.ts +333 -0
- package/vendor/sdk/lib/dom/connection.ts +309 -0
- package/vendor/sdk/lib/dom/element.ts +69 -0
- package/vendor/sdk/lib/dom/isolate.ts +37 -0
- package/vendor/sdk/lib/dom/markdown.ts +388 -0
- package/vendor/sdk/lib/dom/markdownParse.ts +440 -0
- package/vendor/sdk/lib/dom/mounted.ts +29 -0
- package/vendor/sdk/lib/dom/primitives.ts +689 -0
- package/vendor/sdk/lib/dom/region.ts +195 -0
- package/vendor/sdk/lib/dom/render.ts +177 -0
- package/vendor/sdk/lib/dom/theme.ts +38 -0
- package/vendor/sdk/lib/dom/types.ts +36 -0
- package/vendor/sdk/lib/index.ts +26 -17
- package/vendor/sdk/lib/orb/canvas.ts +139 -0
- package/vendor/sdk/lib/orb/index.ts +8 -18
- package/vendor/sdk/lib/orb/presets.ts +1 -1
- package/vendor/sdk/lib/orb/{types.ts → states.ts} +0 -24
- package/vendor/sdk/lib/realtime/capture.ts +152 -0
- package/vendor/sdk/lib/realtime/playback.ts +129 -0
- package/vendor/sdk/lib/realtime/socket.ts +117 -0
- package/vendor/sdk/lib/style.ts +45 -37
- package/vendor/sdk/lib/voiceService.ts +344 -0
- package/vendor/sdk/main.ts +107 -0
- package/vendor/sdk/mcpAppBridge.ts +125 -102
- package/vendor/sdk/pageInterfaces.ts +452 -0
- package/vendor/sdk/responseView.ts +193 -0
- package/vite.config.ts +3 -3
- package/vendor/sdk/appEngine.tsx +0 -395
- package/vendor/sdk/lib/UnoverseComponent.tsx +0 -325
- package/vendor/sdk/lib/app.tsx +0 -730
- package/vendor/sdk/lib/connection.tsx +0 -433
- package/vendor/sdk/lib/isolate.tsx +0 -70
- package/vendor/sdk/lib/orb/ThinkingOrb.tsx +0 -131
- package/vendor/sdk/lib/orb/theme.ts +0 -83
- package/vendor/sdk/lib/primitives.tsx +0 -943
- package/vendor/sdk/lib/realtime/useAudioCapture.ts +0 -237
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +0 -157
- package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +0 -164
- package/vendor/sdk/lib/render.tsx +0 -244
- package/vendor/sdk/lib/streamed.tsx +0 -30
- package/vendor/sdk/lib/theme.ts +0 -55
- package/vendor/sdk/lib/transition/MountArrangement.tsx +0 -71
- package/vendor/sdk/lib/transition/index.ts +0 -9
- package/vendor/sdk/lib/transition/viewTransition.ts +0 -29
- package/vendor/sdk/lib/voice.tsx +0 -422
- package/vendor/sdk/main.tsx +0 -125
- package/vendor/sdk/pageInterfaces.tsx +0 -388
- package/vendor/sdk/responseView.tsx +0 -82
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AN APP, in plain DOM: draws an app or template definition and fills its slots, places,
|
|
3
|
+
* mounted templates and conversation from the store.
|
|
4
|
+
*
|
|
5
|
+
* A template owns nothing (UNOVERSE_SPEC.md §2e-0): it reads the shared state and arranges
|
|
6
|
+
* what it finds. Each instance it places is drawn by the same component everywhere
|
|
7
|
+
* (./component), which follows its own slice. The app draws again only when the store's
|
|
8
|
+
* STRUCTURE moves (turns placed, app state, lifecycle), never per streamed chunk.
|
|
9
|
+
*/
|
|
10
|
+
import type { ResolvedTheme, UnoverseDefinition, UnoverseNode } from "../core/types";
|
|
11
|
+
import type { ComponentStore } from "../core/store";
|
|
12
|
+
import { dispatchAction } from "../core/actions";
|
|
13
|
+
import {
|
|
14
|
+
collectSurfacedViews,
|
|
15
|
+
cssWidth,
|
|
16
|
+
formatRelative,
|
|
17
|
+
isViewField,
|
|
18
|
+
mountAliases,
|
|
19
|
+
orderedClaims,
|
|
20
|
+
placeWidthStyle,
|
|
21
|
+
propDefaults,
|
|
22
|
+
resolveActiveLayout,
|
|
23
|
+
resolveActiveView,
|
|
24
|
+
selectPointers,
|
|
25
|
+
viewOf,
|
|
26
|
+
} from "../core/apps";
|
|
27
|
+
import { decideCommit, type Arrangement } from "../transition/decide";
|
|
28
|
+
import { guestHeldPointers } from "../transition/guestHold";
|
|
29
|
+
import { keyframesCss, styleToCss, type CssStyle } from "../style";
|
|
30
|
+
import type { ActionHandler } from "../actions";
|
|
31
|
+
import { mountComponent, type ComponentMount, type DefinitionSource } from "./component";
|
|
32
|
+
import { setChildren, setStyle } from "./element";
|
|
33
|
+
import { isolatedRoot } from "./isolate";
|
|
34
|
+
import { mountQueue } from "./mounted";
|
|
35
|
+
import { regionView, type Leaf, type Region, type SlotCompute } from "./region";
|
|
36
|
+
import { ensureFontStylesheets, fetchTheme } from "./theme";
|
|
37
|
+
import type { Data } from "./types";
|
|
38
|
+
|
|
39
|
+
/** The voice native service's three verbs (UNOVERSE_SPEC §2e-1), answered here for every host. */
|
|
40
|
+
export interface VoiceVerbs {
|
|
41
|
+
startCall(): void;
|
|
42
|
+
endCall(): void;
|
|
43
|
+
toggleMute(): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AppOptions {
|
|
47
|
+
client: DefinitionSource;
|
|
48
|
+
store: ComponentStore;
|
|
49
|
+
/** e.g. "unoverse://apps/acme/KeyService/definition" */
|
|
50
|
+
uri: string;
|
|
51
|
+
onAction?: ActionHandler;
|
|
52
|
+
theme?: ResolvedTheme;
|
|
53
|
+
/** Host overrides for the definition's declared props (per-tenant logo, brand name). */
|
|
54
|
+
props?: Data;
|
|
55
|
+
/** Draw inside a shadow root (default true). */
|
|
56
|
+
isolate?: boolean;
|
|
57
|
+
/** DEV CHROME ON A CONTAINER: drawn above a Container's content (Studio's state tabs). */
|
|
58
|
+
templateChrome?: (info: { name: string; states: string[]; active?: string; set: (state: string) => void }) => Node | null;
|
|
59
|
+
/** The voice service, for an app whose buttons start and end a call. */
|
|
60
|
+
voice?: VoiceVerbs;
|
|
61
|
+
doc?: Document;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AppMount {
|
|
65
|
+
readonly nodes: Node[];
|
|
66
|
+
update(next: Partial<AppOptions>): void;
|
|
67
|
+
destroy(): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A held-back change commits anyway after this long without a boundary (§13.5). */
|
|
71
|
+
const GRACE_MS = 4000;
|
|
72
|
+
|
|
73
|
+
/** Every pointer a mounted template layout's slots currently select: its occupancy. */
|
|
74
|
+
function collectSlotPointers(node: unknown, store: ComponentStore, scope?: Set<string>): string[] {
|
|
75
|
+
const out: string[] = [];
|
|
76
|
+
const walk = (n: unknown): void => {
|
|
77
|
+
if (!n || typeof n !== "object") return;
|
|
78
|
+
const un = n as UnoverseNode & { children?: unknown[]; item?: unknown; frame?: unknown; cases?: Record<string, unknown> };
|
|
79
|
+
if (un.type === "ComponentSlot") out.push(...selectPointers(store, un, scope));
|
|
80
|
+
(un.children ?? []).forEach(walk);
|
|
81
|
+
if (un.item) walk(un.item);
|
|
82
|
+
if (un.frame) walk(un.frame);
|
|
83
|
+
if (un.cases) Object.values(un.cases).forEach(walk);
|
|
84
|
+
};
|
|
85
|
+
walk(node);
|
|
86
|
+
return [...new Set(out)];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The commit IS the animation (§13.4): with theme motion, the boundary commit runs inside the
|
|
90
|
+
* browser's View Transitions API; otherwise, and on failure, it lands at once. */
|
|
91
|
+
function commitArrangement(doc: Document, commit: () => void, motion: boolean): void {
|
|
92
|
+
const start = (doc as unknown as { startViewTransition?: (cb: () => void) => unknown }).startViewTransition;
|
|
93
|
+
if (!motion || typeof start !== "function") return commit();
|
|
94
|
+
queueMicrotask(() => {
|
|
95
|
+
try {
|
|
96
|
+
start.call(doc, commit);
|
|
97
|
+
} catch {
|
|
98
|
+
commit();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function mountApp(options: AppOptions): AppMount {
|
|
104
|
+
let o: AppOptions = { ...options };
|
|
105
|
+
// Isolated unless a host says `false`: a prop left out arrives as `undefined` and keeps the default.
|
|
106
|
+
const isolated = () => o.isolate !== false;
|
|
107
|
+
const doc = o.doc ?? document;
|
|
108
|
+
let def: UnoverseDefinition | null = null;
|
|
109
|
+
let error: string | null = null;
|
|
110
|
+
let mounted: Record<string, UnoverseDefinition> = {};
|
|
111
|
+
let fetched: ResolvedTheme | null = null;
|
|
112
|
+
let themeName: string | null = null;
|
|
113
|
+
let alive = true;
|
|
114
|
+
let version = -1;
|
|
115
|
+
let loadToken = 0;
|
|
116
|
+
// THE DOUBLE BUFFER per mounted template place (§13.3): the committed arrangement and the
|
|
117
|
+
// fail-open grace timer. Render-layer state, deliberately: never in the store.
|
|
118
|
+
const buffers = new Map<string, { committed: Arrangement | null; grace: ReturnType<typeof setTimeout> | null }>();
|
|
119
|
+
const fetching = new Set<string>();
|
|
120
|
+
|
|
121
|
+
let nodes: Node[] = [];
|
|
122
|
+
const iso = isolatedRoot(doc);
|
|
123
|
+
const plain = doc.createElement("div");
|
|
124
|
+
const keyframes = doc.createElement("style");
|
|
125
|
+
const onPage = mountQueue(() => (isolated() ? iso.host.isConnected : keyframes.isConnected));
|
|
126
|
+
const tree = regionView(doc, onPage);
|
|
127
|
+
|
|
128
|
+
const show = (next: Node[]) => {
|
|
129
|
+
const at = nodes[0];
|
|
130
|
+
const parent = at?.parentNode;
|
|
131
|
+
if (parent && (next.length !== nodes.length || next.some((n, i) => n !== nodes[i]))) {
|
|
132
|
+
for (const n of next) parent.insertBefore(n, at);
|
|
133
|
+
for (const n of nodes) if (!next.includes(n)) n.parentNode?.removeChild(n);
|
|
134
|
+
}
|
|
135
|
+
nodes = next;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const load = () => {
|
|
139
|
+
const token = ++loadToken;
|
|
140
|
+
def = null;
|
|
141
|
+
error = null;
|
|
142
|
+
mounted = {};
|
|
143
|
+
o.client
|
|
144
|
+
.readDefinition(o.uri)
|
|
145
|
+
.then((d) => {
|
|
146
|
+
if (!alive || token !== loadToken) return;
|
|
147
|
+
def = d;
|
|
148
|
+
// MOUNTED TEMPLATES: an entry naming a template definition, fetched once per app.
|
|
149
|
+
for (const entry of Object.values((d as { templates?: Record<string, { template?: string }> }).templates ?? {})) {
|
|
150
|
+
const use = entry?.template;
|
|
151
|
+
if (!use) continue;
|
|
152
|
+
const turi = use.includes("://") ? use : `unoverse://templates/${use}`;
|
|
153
|
+
// A failed fetch may be a transient (a dev-server restart): retry once, never silently.
|
|
154
|
+
o.client
|
|
155
|
+
.readDefinition(turi)
|
|
156
|
+
.catch(() => new Promise((r) => setTimeout(r, 1500)).then(() => o.client.readDefinition(turi)))
|
|
157
|
+
.then((t) => {
|
|
158
|
+
if (!alive || token !== loadToken || !t) return;
|
|
159
|
+
mounted = { ...mounted, [use]: t };
|
|
160
|
+
draw();
|
|
161
|
+
})
|
|
162
|
+
.catch((e) => console.warn(`[unoverse] mounted template "${use}" failed to load:`, e));
|
|
163
|
+
}
|
|
164
|
+
draw();
|
|
165
|
+
})
|
|
166
|
+
.catch((e) => {
|
|
167
|
+
if (!alive || token !== loadToken) return;
|
|
168
|
+
error = String(e?.message ?? e);
|
|
169
|
+
draw();
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const loadTheme = () => {
|
|
174
|
+
if (o.theme || !def) return;
|
|
175
|
+
const name = def.org ? `${def.org}/light` : "light";
|
|
176
|
+
if (name === themeName) return;
|
|
177
|
+
themeName = name;
|
|
178
|
+
fetchTheme(o.client, name)
|
|
179
|
+
.then((t) => {
|
|
180
|
+
if (!alive || themeName !== name) return;
|
|
181
|
+
fetched = t;
|
|
182
|
+
draw();
|
|
183
|
+
})
|
|
184
|
+
.catch(() => {});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** The store work a draw leads to, run after it (as the old renderer's effects were). */
|
|
188
|
+
const afterDraw = () => {
|
|
189
|
+
const store = o.store;
|
|
190
|
+
// A PAGE THAT STREAMED IN: its address names its template; learn what it claims.
|
|
191
|
+
for (const p of store.getResponses().flatMap((r) => r.components)) {
|
|
192
|
+
const { chatId: c, nodeId: n } = store.split(p);
|
|
193
|
+
const url = store.getComponentUrl(c, n);
|
|
194
|
+
if (!url || !url.includes("://templates/")) continue;
|
|
195
|
+
const key = url.replace(/^unoverse:\/\/templates\//, "");
|
|
196
|
+
if (mounted[key] || fetching.has(key)) continue;
|
|
197
|
+
fetching.add(key);
|
|
198
|
+
o.client
|
|
199
|
+
.readDefinition(url)
|
|
200
|
+
.then((t) => {
|
|
201
|
+
fetching.delete(key);
|
|
202
|
+
if (!alive || !t || mounted[key]) return;
|
|
203
|
+
mounted = { ...mounted, [key]: t };
|
|
204
|
+
draw();
|
|
205
|
+
})
|
|
206
|
+
.catch((e) => {
|
|
207
|
+
fetching.delete(key);
|
|
208
|
+
console.warn(`[unoverse] streamed template "${key}" failed to load:`, e);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (!def || def.places?.length) return;
|
|
212
|
+
// ARRIVAL PLACEMENT (STATE_MODEL v2 §5 rule 3): the host places each arrival once, in
|
|
213
|
+
// its declared initial if this template has it, else the first shared state.
|
|
214
|
+
const order = def.stateOrder;
|
|
215
|
+
if (order?.length)
|
|
216
|
+
for (const turn of store.getResponses())
|
|
217
|
+
for (const p of turn.components) {
|
|
218
|
+
const { chatId, nodeId } = store.split(p);
|
|
219
|
+
const slice = store.get(chatId, nodeId) as Data | undefined;
|
|
220
|
+
if (!slice || viewOf(slice) !== undefined) continue;
|
|
221
|
+
const menu = Array.isArray(slice.publicStates) ? (slice.publicStates as unknown[]) : undefined;
|
|
222
|
+
if (!menu) continue;
|
|
223
|
+
const initial = typeof slice.initialView === "string" ? slice.initialView : undefined;
|
|
224
|
+
const face = initial && order.includes(initial) ? initial : order.find((s) => menu.includes(s));
|
|
225
|
+
if (face) store.apply({ type: "COMPONENT_DATA", chatId, nodeId, data: { state: face } });
|
|
226
|
+
}
|
|
227
|
+
// RULE 4: a winner a DELIVERY put there clears the states below it.
|
|
228
|
+
const ladder = resolveActiveView(def, store, surfacedViews());
|
|
229
|
+
if (ladder.view && !ladder.byGuest) {
|
|
230
|
+
const claims = orderedClaims(def);
|
|
231
|
+
store.cancelBelow(ladder.view, claims, mountAliases(def, new Set(claims)));
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/** The states this app reacts to (claims), so a claimed instance is kept out of the flow. */
|
|
236
|
+
const surfacedViews = (): Set<string> => {
|
|
237
|
+
const views = new Set<string>(orderedClaims(def));
|
|
238
|
+
const activeRoot = def ? resolveActiveLayout(def, o.store) : undefined;
|
|
239
|
+
if (activeRoot) collectSurfacedViews(activeRoot, views, def?.defs as Record<string, unknown> | undefined);
|
|
240
|
+
for (const tpl of Object.values(mounted)) {
|
|
241
|
+
for (const name of Object.keys(tpl.layouts ?? {})) views.add(name);
|
|
242
|
+
// A TEMPLATE CLAIMS WHAT ITS SECTIONS CLAIM, not just its state names.
|
|
243
|
+
for (const secs of Object.values((tpl as { sections?: Record<string, { face?: string }[]> }).sections ?? {}))
|
|
244
|
+
for (const sec of secs) if (sec?.face) views.add(sec.face);
|
|
245
|
+
}
|
|
246
|
+
// `inline` IS NEVER A CLAIM: the universal fallback face, drawn in the flow.
|
|
247
|
+
views.delete("inline");
|
|
248
|
+
return views;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// ONE action interpreter for the whole tree; template chrome has no component slice.
|
|
252
|
+
const dispatch: ActionHandler = (action, data, meta) => {
|
|
253
|
+
const store = o.store;
|
|
254
|
+
// VOICE NATIVE-SERVICE actions are answered by the shared service, never forwarded.
|
|
255
|
+
if (typeof action === "string" && o.voice) {
|
|
256
|
+
if (action === "startCall") return void o.voice.startCall();
|
|
257
|
+
if (action === "endCall") return void o.voice.endCall();
|
|
258
|
+
if (action === "toggleMute") return void o.voice.toggleMute();
|
|
259
|
+
if (action === "backToChat") o.voice.endCall();
|
|
260
|
+
}
|
|
261
|
+
// GUEST CLOSE from TEMPLATE CHROME: a ✕ on the layout itself un-surfaces the current view.
|
|
262
|
+
if (
|
|
263
|
+
action &&
|
|
264
|
+
typeof action === "object" &&
|
|
265
|
+
(action as { type?: string }).type === "setValue" &&
|
|
266
|
+
(action as { values?: { key?: string; value?: unknown }[] }).values?.some((v) => isViewField(v.key) && v.value === "inline")
|
|
267
|
+
) {
|
|
268
|
+
store.closeSurfaces();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
dispatchAction(action as Parameters<typeof dispatchAction>[0], (data ?? {}) as Data, {
|
|
272
|
+
store,
|
|
273
|
+
chatId: "",
|
|
274
|
+
nodeId: "",
|
|
275
|
+
sendToServer: o.onAction ? (t, payload) => o.onAction!(t, payload, meta) : undefined,
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const draw = () => {
|
|
280
|
+
if (!alive) return;
|
|
281
|
+
const store = o.store;
|
|
282
|
+
version = store.getStructureVersion();
|
|
283
|
+
loadTheme();
|
|
284
|
+
const theme = o.theme ?? fetched;
|
|
285
|
+
ensureFontStylesheets(theme, doc);
|
|
286
|
+
const say = (text: string) => {
|
|
287
|
+
tree.destroy();
|
|
288
|
+
const d = doc.createElement("div");
|
|
289
|
+
d.textContent = text;
|
|
290
|
+
show([d]);
|
|
291
|
+
};
|
|
292
|
+
if (error) return say(`Unoverse error: ${error}`);
|
|
293
|
+
if (!def || !theme) return say(`Loading ${o.uri}…`);
|
|
294
|
+
const app = def;
|
|
295
|
+
const views = surfacedViews();
|
|
296
|
+
const surfacedView = resolveActiveView(app, store, views).view;
|
|
297
|
+
const ctx = { theme, onAction: dispatch, defs: app.defs };
|
|
298
|
+
|
|
299
|
+
const leaf = (pointer: string, extraData?: Data, fill?: boolean) => {
|
|
300
|
+
const { chatId, nodeId } = store.split(pointer);
|
|
301
|
+
const opts = { client: o.client, store, chatId, nodeId, onAction: dispatch, theme, extraData, fill, doc };
|
|
302
|
+
// Keyed by the INSTANCE: one re-delivered later keeps its elements (no flicker).
|
|
303
|
+
return { key: nodeId, make: (): Leaf => mountComponent(opts), refresh: (l: Leaf) => (l as ComponentMount).update(opts) };
|
|
304
|
+
};
|
|
305
|
+
// THE STATE THE SCREEN PLACED IT IN: a place draws each interface in that face.
|
|
306
|
+
const placed = (p: string): Data | undefined => {
|
|
307
|
+
const state = store.placedState(p);
|
|
308
|
+
return state ? { state } : undefined;
|
|
309
|
+
};
|
|
310
|
+
const fallback = (node: UnoverseNode): Region =>
|
|
311
|
+
node.fallback ? { kind: "tree", key: "fallback", node: node.fallback, data: {}, ctx } : { kind: "none" };
|
|
312
|
+
|
|
313
|
+
// WHAT A SLOT OR A PLACE HOLDS: its fallback when empty; else its frame around the
|
|
314
|
+
// instances, a single occupant filling the frame; else the instances as a list.
|
|
315
|
+
const drawPointers = (node: UnoverseNode, pointers: string[], fill: boolean): Region => {
|
|
316
|
+
if (!pointers.length) return fallback(node);
|
|
317
|
+
if (node.frame) {
|
|
318
|
+
// A panel declares its width ONCE (core/apps placeWidthStyle).
|
|
319
|
+
const placeStyle = placeWidthStyle((node as { appWidth?: string }).appWidth, theme.appSize);
|
|
320
|
+
const frame = placeStyle ? ({ ...node.frame, style: { ...(node.frame.style ?? {}), ...placeStyle } } as UnoverseNode) : node.frame;
|
|
321
|
+
const inner: SlotCompute = () =>
|
|
322
|
+
fill
|
|
323
|
+
? { kind: "list", key: "fill", flip: false, items: pointers.map((p) => leaf(p, placed(p), fill)) }
|
|
324
|
+
: { kind: "list", key: "flip", flip: true, items: pointers.map((p) => leaf(p, placed(p))) };
|
|
325
|
+
return { kind: "tree", key: "frame", node: frame, data: store.getAppState() as Data, ctx, slot: inner };
|
|
326
|
+
}
|
|
327
|
+
return { kind: "list", key: "flip", flip: true, items: pointers.map((p) => leaf(p, placed(p))) };
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const makeSlot = (scope?: Set<string>): SlotCompute => {
|
|
331
|
+
const slot: SlotCompute = (node) => {
|
|
332
|
+
// A PLACE draws what the screen document put in it.
|
|
333
|
+
if (node.type === "Place") return drawPointers(node, [...store.placeItems(String((node as { name?: string }).name ?? ""))], node.holds === "one");
|
|
334
|
+
if (node.type === "Template") {
|
|
335
|
+
const cname = (node as { name?: string }).name ?? "";
|
|
336
|
+
const spec = (app as { templates?: Record<string, { template?: string; states?: Record<string, { interface?: string; width?: string }>; interface?: string; director?: unknown }> }).templates?.[cname];
|
|
337
|
+
// THE MOUNT: the place names a template definition, whose own machinery runs here.
|
|
338
|
+
if (spec?.template) return mountedTemplate(node, cname, spec, mounted[spec.template]);
|
|
339
|
+
const stateNames = spec?.states ? Object.keys(spec.states) : [];
|
|
340
|
+
const appState = store.getAppState() as Data;
|
|
341
|
+
const active = (appState[`template:${cname}`] as string | undefined) ?? stateNames[0];
|
|
342
|
+
const heldInterface = spec?.states ? spec.states[active ?? ""]?.interface : spec?.interface;
|
|
343
|
+
// AN EMPTY STATE HOLDS NOTHING (`closed: {}` is a collapsed drawer).
|
|
344
|
+
const emptyState = Boolean(spec?.states) && !heldInterface;
|
|
345
|
+
const holder = {
|
|
346
|
+
...node,
|
|
347
|
+
type: "ComponentSlot",
|
|
348
|
+
select: heldInterface ? { type: heldInterface, limit: 1 } : emptyState ? { limit: 0 } : (node.select ?? {}),
|
|
349
|
+
} as UnoverseNode;
|
|
350
|
+
if (o.templateChrome && spec) {
|
|
351
|
+
const set = (state: string) => store.mergeAppState({ [`template:${cname}`]: state });
|
|
352
|
+
const declaredW = spec.states?.[active ?? ""]?.width;
|
|
353
|
+
const stateW = declaredW ? (cssWidth(theme.appSize?.[declaredW] ?? declaredW) ?? declaredW) : undefined;
|
|
354
|
+
const chrome = o.templateChrome({ name: cname, states: stateNames, active, set });
|
|
355
|
+
return {
|
|
356
|
+
kind: "box",
|
|
357
|
+
key: `chrome:${cname}`,
|
|
358
|
+
style: { display: "flex", flexDirection: "column", minHeight: 0, height: "100%", ...(stateW ? { width: stateW, flex: "0 0 auto", overflow: "hidden" } : {}) },
|
|
359
|
+
parts: [
|
|
360
|
+
chrome ? { kind: "nodes", key: "chrome", nodes: [chrome] } : { kind: "none" },
|
|
361
|
+
{ kind: "box", key: "inner", style: { flex: 1, minHeight: 0, overflow: "auto" }, parts: [slot(holder, {})] },
|
|
362
|
+
],
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
node = holder;
|
|
366
|
+
}
|
|
367
|
+
// DECLARE = REARRANGE, CLAIM = FILL: a claim on a DECLARED reaction state fills only
|
|
368
|
+
// while that view is the app's active state; an undeclared claim is a container.
|
|
369
|
+
const claim = node.select?.where;
|
|
370
|
+
const declared = typeof claim?.eq === "string" && (app.stateOrder ?? []).includes(claim.eq);
|
|
371
|
+
if (isViewField(claim?.field) && typeof claim?.eq === "string" && declared && claim.eq !== surfacedView) return fallback(node);
|
|
372
|
+
return drawPointers(node, selectPointers(store, node, scope), node.select?.limit === 1);
|
|
373
|
+
};
|
|
374
|
+
return slot;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/** A mounted template: its own layouts and slots over the shared store, double-buffered. */
|
|
378
|
+
const mountedTemplate = (node: UnoverseNode, cname: string, spec: { director?: unknown }, tpl: UnoverseDefinition | undefined): Region => {
|
|
379
|
+
if (!tpl) return { kind: "none" };
|
|
380
|
+
// HOLD: LATEST: the current delivery, narrowed by a judged verdict; the guest's open
|
|
381
|
+
// detail stays in scope through deliveries (the guest's hand outranks the settle).
|
|
382
|
+
const active = resolveActiveLayout(tpl, store);
|
|
383
|
+
const delivery = store.currentDeliveryPointers();
|
|
384
|
+
const dirScope = store.directorScope(delivery);
|
|
385
|
+
const tplLayouts = (tpl as { layouts?: Record<string, UnoverseNode> }).layouts ?? {};
|
|
386
|
+
const layoutNames = Object.keys(tplLayouts);
|
|
387
|
+
const baseLayout = (tpl as { defaultLayout?: string }).defaultLayout ?? layoutNames[0] ?? null;
|
|
388
|
+
const guestHeld = guestHeldPointers(
|
|
389
|
+
store.getResponses().flatMap((r) => r.components),
|
|
390
|
+
{
|
|
391
|
+
touchOf: (p) => store.touchOf(p),
|
|
392
|
+
interactionTouchOf: (p) => store.interactionTouchOf(p),
|
|
393
|
+
viewOf: (p) => {
|
|
394
|
+
const { chatId, nodeId } = store.split(p);
|
|
395
|
+
return viewOf(store.get(chatId, nodeId) as Data | undefined);
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
layoutNames,
|
|
399
|
+
baseLayout,
|
|
400
|
+
);
|
|
401
|
+
const scope = guestHeld.length ? new Set([...(dirScope ?? delivery), ...guestHeld]) : (dirScope ?? delivery);
|
|
402
|
+
const next: Arrangement = {
|
|
403
|
+
layoutName: active ? (Object.entries(tplLayouts).find(([, t]) => t === active)?.[0] ?? null) : null,
|
|
404
|
+
held: active ? collectSlotPointers(active, store, scope) : [],
|
|
405
|
+
verdictSig: dirScope ? [...dirScope].sort().join("|") : null,
|
|
406
|
+
};
|
|
407
|
+
const directed =
|
|
408
|
+
Object.values((tpl as { sections?: Record<string, { kind?: string }[]> }).sections ?? {}).flat().some((s) => s?.kind === "director") ||
|
|
409
|
+
Boolean(spec.director);
|
|
410
|
+
const motion = ((theme as { motion?: { mount?: string } }).motion?.mount ?? "") === "transition";
|
|
411
|
+
// THE TRANSITION MODEL (§13): commit the next arrangement only at a boundary.
|
|
412
|
+
const buf = buffers.get(cname) ?? { committed: null, grace: null };
|
|
413
|
+
buffers.set(cname, buf);
|
|
414
|
+
const decision = decideCommit(buf.committed, next, directed);
|
|
415
|
+
if (decision === "hold") {
|
|
416
|
+
buf.grace ??= setTimeout(() => {
|
|
417
|
+
buf.grace = null;
|
|
418
|
+
buf.committed = next;
|
|
419
|
+
draw();
|
|
420
|
+
}, GRACE_MS);
|
|
421
|
+
} else if (decision !== "none") {
|
|
422
|
+
if (buf.grace) clearTimeout(buf.grace);
|
|
423
|
+
buf.grace = null;
|
|
424
|
+
if (decision === "transition")
|
|
425
|
+
commitArrangement(doc, () => {
|
|
426
|
+
buf.committed = next;
|
|
427
|
+
draw();
|
|
428
|
+
}, motion);
|
|
429
|
+
else buf.committed = next;
|
|
430
|
+
}
|
|
431
|
+
const arr = buf.committed ?? next;
|
|
432
|
+
if (!arr.held.length) return { kind: "none" };
|
|
433
|
+
const arrActive = (arr.layoutName ? tplLayouts[arr.layoutName] : undefined) ?? active;
|
|
434
|
+
if (!arrActive) return { kind: "none" };
|
|
435
|
+
const inner: Region = { kind: "tree", key: `tpl:${arr.layoutName}`, node: arrActive, data: store.getAppState() as Data, ctx: { theme, onAction: dispatch, defs: tpl.defs }, slot: makeSlot(new Set(arr.held)) };
|
|
436
|
+
// A PLACE DECLARES ITS WIDTH ONCE (core/apps placeWidthStyle).
|
|
437
|
+
const placeStyle = placeWidthStyle((node as { appWidth?: string }).appWidth, theme.appSize);
|
|
438
|
+
const rawFrame = (node as { frame?: UnoverseNode }).frame;
|
|
439
|
+
if (rawFrame) {
|
|
440
|
+
const frame = placeStyle ? ({ ...rawFrame, style: { ...(rawFrame.style ?? {}), ...placeStyle } } as UnoverseNode) : rawFrame;
|
|
441
|
+
return { kind: "tree", key: `frame:${cname}`, node: frame, data: store.getAppState() as Data, ctx, slot: () => inner };
|
|
442
|
+
}
|
|
443
|
+
return { kind: "box", key: `mount:${cname}`, style: { display: "flex", flexDirection: "column", minHeight: 0, ...((placeStyle ?? {}) as CssStyle) }, parts: [inner] };
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Timeline: a GENERIC iterator over the turns, drawing the `user` / `assistant` subtree
|
|
447
|
+
// per turn. The assistant subtree's slot holds that turn's UNCLAIMED components.
|
|
448
|
+
const timeline = (node: UnoverseNode): Region => {
|
|
449
|
+
const inChat = app.places?.length ? new Set(store.placeItems("chat")) : null;
|
|
450
|
+
const turns: Region[] = store.getTimeline().map((turn) => {
|
|
451
|
+
const sub = turn.role === "user" ? node.user : node.assistant;
|
|
452
|
+
if (!sub) return { kind: "none" };
|
|
453
|
+
// One instance, one placeholder: an instance a surface shows is not drawn here too.
|
|
454
|
+
const shown =
|
|
455
|
+
turn.role === "assistant"
|
|
456
|
+
? turn.components.filter((p) => {
|
|
457
|
+
if (inChat) return inChat.has(p);
|
|
458
|
+
const { chatId, nodeId } = store.split(p);
|
|
459
|
+
const view = viewOf(store.get(chatId, nodeId) as Data | undefined);
|
|
460
|
+
return !view || !views.has(view);
|
|
461
|
+
})
|
|
462
|
+
: [];
|
|
463
|
+
const streaming = turn.role === "assistant" && turn.streamingState === "streaming";
|
|
464
|
+
const turnSlot: SlotCompute = (slotNode) => {
|
|
465
|
+
let ptrs = [...shown];
|
|
466
|
+
if (slotNode.select?.limit != null) ptrs = ptrs.slice(0, slotNode.select.limit);
|
|
467
|
+
if (!ptrs.length) return fallback(slotNode);
|
|
468
|
+
return { kind: "list", key: "turn", flip: false, items: ptrs.map((p) => leaf(p, { streaming, ...placed(p) })) };
|
|
469
|
+
};
|
|
470
|
+
// The turn's scope: the author's static data and NEUTRAL facts, never UX policy.
|
|
471
|
+
const time = formatRelative(turn.createdAt);
|
|
472
|
+
const data =
|
|
473
|
+
turn.role === "user"
|
|
474
|
+
? { ...(node.userData ?? {}), text: turn.text, time }
|
|
475
|
+
: {
|
|
476
|
+
...(node.assistantData ?? {}),
|
|
477
|
+
time,
|
|
478
|
+
streaming,
|
|
479
|
+
empty: shown.length === 0,
|
|
480
|
+
active: turn.streamingState === "streaming" || shown.length > 0,
|
|
481
|
+
};
|
|
482
|
+
return { kind: "tree", key: `turn:${turn.id}:${turn.role}`, node: sub, data, ctx, slot: turnSlot };
|
|
483
|
+
});
|
|
484
|
+
return { kind: "box", key: "timeline", style: styleToCss(node.style, theme), parts: turns };
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const appSlot = makeSlot();
|
|
488
|
+
const resolve: SlotCompute = (node, data) => (node.type === "Timeline" ? timeline(node) : appSlot(node, data));
|
|
489
|
+
|
|
490
|
+
// Root scope: declared prop defaults, the host's overrides, the app's own state, and
|
|
491
|
+
// conversation facts read off the store, never stored.
|
|
492
|
+
const isEmpty = store.getTimeline().length === 0;
|
|
493
|
+
const lifecycle = store.getLifecycle();
|
|
494
|
+
const rootData: Data = {
|
|
495
|
+
...propDefaults(app.props),
|
|
496
|
+
...((app as { state?: Data }).state ?? {}),
|
|
497
|
+
...(o.props ?? {}),
|
|
498
|
+
...store.getAppState(),
|
|
499
|
+
isEmpty,
|
|
500
|
+
hasMessages: !isEmpty,
|
|
501
|
+
surfacedView,
|
|
502
|
+
lifecycle,
|
|
503
|
+
isThinking: lifecycle === "thinking",
|
|
504
|
+
isStreaming: lifecycle === "thinking" || lifecycle === "streaming",
|
|
505
|
+
};
|
|
506
|
+
const rootNode = (resolveActiveLayout(app, store) ?? app.root) as UnoverseNode;
|
|
507
|
+
tree.set({ kind: "tree", key: "app", node: rootNode, data: rootData, ctx, slot: resolve });
|
|
508
|
+
keyframes.textContent = keyframesCss(theme);
|
|
509
|
+
// A template is a SURFACE: its root fills the box the host gives it.
|
|
510
|
+
const surfaceRoot = { ...(theme.root as Record<string, string>), height: "100%" };
|
|
511
|
+
if (isolated()) {
|
|
512
|
+
setStyle(iso.root, surfaceRoot);
|
|
513
|
+
setChildren(iso.root, [keyframes, ...tree.nodes]);
|
|
514
|
+
show([iso.host]);
|
|
515
|
+
} else {
|
|
516
|
+
setStyle(plain, { display: "contents", ...surfaceRoot });
|
|
517
|
+
setChildren(plain, tree.nodes);
|
|
518
|
+
show([keyframes, plain]);
|
|
519
|
+
}
|
|
520
|
+
queueMicrotask(() => alive && afterDraw());
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const unsubscribe = o.store.subscribe(() => {
|
|
524
|
+
if (o.store.getStructureVersion() !== version) draw();
|
|
525
|
+
});
|
|
526
|
+
load();
|
|
527
|
+
draw();
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
get nodes() {
|
|
531
|
+
return nodes;
|
|
532
|
+
},
|
|
533
|
+
update(next) {
|
|
534
|
+
const reload = next.uri !== undefined && next.uri !== o.uri;
|
|
535
|
+
o = { ...o, ...next };
|
|
536
|
+
if (reload) load();
|
|
537
|
+
draw();
|
|
538
|
+
},
|
|
539
|
+
destroy() {
|
|
540
|
+
alive = false;
|
|
541
|
+
unsubscribe();
|
|
542
|
+
for (const b of buffers.values()) if (b.grace) clearTimeout(b.grace);
|
|
543
|
+
tree.destroy();
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-element "chrome" shared by the interactive primitives: the base inline style, the
|
|
3
|
+
* interaction-state rules (hover, active), the container-query rules (hideBelow, hideAbove,
|
|
4
|
+
* span stacking, a grid's own stackBelow, a columns map) and the data-driven disabled state.
|
|
5
|
+
* Inline styles cannot express pseudo-classes or container queries, so those become scoped
|
|
6
|
+
* rules in `<style>` elements. Every value is served; this file authors none.
|
|
7
|
+
*/
|
|
8
|
+
import type { ResolvedTheme, UnoverseNode } from "../core/types";
|
|
9
|
+
import { styleToCss, cssDecls, type CssStyle } from "../style";
|
|
10
|
+
import type { Data } from "./types";
|
|
11
|
+
|
|
12
|
+
/** Stable class name from a string (identical specs share one rule). */
|
|
13
|
+
export function hashStr(s: string): string {
|
|
14
|
+
let h = 0;
|
|
15
|
+
for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
|
16
|
+
return (h >>> 0).toString(36);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const INTERACTION_STATES: [string, string][] = [
|
|
20
|
+
["hover", ":hover"],
|
|
21
|
+
["active", ":active"],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// A state rule is a pure function of (spec, theme), and specs come from cached definitions.
|
|
25
|
+
const stateRuleCache = new WeakMap<ResolvedTheme, WeakMap<object, { cls: string; rule: string }>>();
|
|
26
|
+
function stateRule(theme: ResolvedTheme, spec: Record<string, unknown>, key: string, pseudo: string) {
|
|
27
|
+
let bySpec = stateRuleCache.get(theme);
|
|
28
|
+
if (!bySpec) {
|
|
29
|
+
bySpec = new WeakMap();
|
|
30
|
+
stateRuleCache.set(theme, bySpec);
|
|
31
|
+
}
|
|
32
|
+
let hit = bySpec.get(spec);
|
|
33
|
+
if (!hit) {
|
|
34
|
+
// !important: the base style is inline, which beats a class selector otherwise.
|
|
35
|
+
const decls = cssDecls(styleToCss(spec, theme), true);
|
|
36
|
+
const cls = `uno-${key}-${hashStr(decls)}`;
|
|
37
|
+
hit = { cls, rule: `.${cls}${pseudo}{${decls}}` };
|
|
38
|
+
bySpec.set(spec, hit);
|
|
39
|
+
}
|
|
40
|
+
return hit;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Chrome {
|
|
44
|
+
className: string | undefined;
|
|
45
|
+
rules: string[];
|
|
46
|
+
style: CssStyle;
|
|
47
|
+
disabled: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function nodeChrome(node: UnoverseNode, data: Data, theme: ResolvedTheme): Chrome {
|
|
51
|
+
const base = styleToCss(node.style, theme, data);
|
|
52
|
+
const disabled = node.disabledWhen ? !!data[node.disabledWhen] : false;
|
|
53
|
+
const style = disabled && node.style?.disabled ? { ...base, ...styleToCss(node.style.disabled as Record<string, unknown>, theme) } : base;
|
|
54
|
+
const classes: string[] = [];
|
|
55
|
+
const rules: string[] = [];
|
|
56
|
+
const s = node.style as Record<string, unknown> | undefined;
|
|
57
|
+
for (const [key, pseudo] of INTERACTION_STATES) {
|
|
58
|
+
const spec = s?.[key];
|
|
59
|
+
if (!spec) continue;
|
|
60
|
+
const { cls, rule } = stateRule(theme, spec as Record<string, unknown>, key, pseudo);
|
|
61
|
+
classes.push(cls);
|
|
62
|
+
rules.push(rule);
|
|
63
|
+
}
|
|
64
|
+
// `hideBelow`: hidden while the nearest container is narrower than a served width.
|
|
65
|
+
if (s?.hideBelow) {
|
|
66
|
+
const w = theme.space[s.hideBelow as string] ?? theme.layout?.[s.hideBelow as string] ?? (s.hideBelow as string);
|
|
67
|
+
const cls = `uno-hidebelow-${hashStr(String(w))}`;
|
|
68
|
+
classes.push(cls);
|
|
69
|
+
rules.push(`@container (max-width:${w}){.${cls}{display:none !important}}`);
|
|
70
|
+
}
|
|
71
|
+
// `span` stacks: below the served threshold a spanning child takes the whole row of its grid.
|
|
72
|
+
if (s?.span) {
|
|
73
|
+
const raw = theme.grid?.stackBelow ?? "";
|
|
74
|
+
const w = theme.space[raw] ?? theme.layout?.[raw] ?? raw;
|
|
75
|
+
if (w) {
|
|
76
|
+
const cls = `uno-stack-${hashStr(String(w))}`;
|
|
77
|
+
classes.push(cls);
|
|
78
|
+
rules.push(`@container (max-width:${w}){.${cls}:not(.uno-gridstack > *){grid-column:1 / -1 !important}}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// A grid's own `stackBelow`: below it every child takes the whole row.
|
|
82
|
+
if (s?.columns && s.stackBelow) {
|
|
83
|
+
const raw = s.stackBelow as string;
|
|
84
|
+
const w = theme.space[raw] ?? theme.layout?.[raw] ?? raw;
|
|
85
|
+
const cls = `uno-gridstack-${hashStr(String(w))}`;
|
|
86
|
+
classes.push("uno-gridstack", cls);
|
|
87
|
+
rules.push(`@container (max-width:${w}){.${cls} > *{grid-column:1 / -1 !important}}`);
|
|
88
|
+
}
|
|
89
|
+
// `columns` as a breakpoint map: column counts per served band, by container width.
|
|
90
|
+
if (s?.columns && typeof s.columns === "object") {
|
|
91
|
+
const map = s.columns as Record<string, unknown>;
|
|
92
|
+
const first = map.mobile ?? map.tablet ?? map.desktop ?? map.full;
|
|
93
|
+
if (first != null) {
|
|
94
|
+
const cls = `uno-cols-${hashStr(JSON.stringify(map))}`;
|
|
95
|
+
let css = `.${cls}{display:grid;grid-template-columns:repeat(${String(first)},minmax(0,1fr));align-content:start;container-type:inline-size}`;
|
|
96
|
+
for (const band of ["tablet", "desktop", "full"] as const) {
|
|
97
|
+
const w = theme.breakpoint?.[band];
|
|
98
|
+
if (map[band] != null && w) css += `@container (min-width:${w}){.${cls}{grid-template-columns:repeat(${String(map[band])},minmax(0,1fr))}}`;
|
|
99
|
+
}
|
|
100
|
+
classes.push(cls);
|
|
101
|
+
rules.push(css);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// `hideAbove`: the mirror, hidden while the container is wider than the width.
|
|
105
|
+
if (s?.hideAbove) {
|
|
106
|
+
const w = theme.space[s.hideAbove as string] ?? theme.layout?.[s.hideAbove as string] ?? (s.hideAbove as string);
|
|
107
|
+
const cls = `uno-hideabove-${hashStr(String(w))}`;
|
|
108
|
+
classes.push(cls);
|
|
109
|
+
rules.push(`@container (min-width:${w}){.${cls}{display:none !important}}`);
|
|
110
|
+
}
|
|
111
|
+
return { className: classes.join(" ") || undefined, rules, style, disabled };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The `<style>` elements an element carries for its rules, reused as the rules change. */
|
|
115
|
+
export function styleEls(doc: Document) {
|
|
116
|
+
const els: HTMLStyleElement[] = [];
|
|
117
|
+
return (rules: string[]): Node[] => {
|
|
118
|
+
while (els.length > rules.length) els.pop();
|
|
119
|
+
rules.forEach((rule, i) => {
|
|
120
|
+
const el = (els[i] ??= doc.createElement("style"));
|
|
121
|
+
if (el.textContent !== rule) el.textContent = rule;
|
|
122
|
+
});
|
|
123
|
+
return els.slice();
|
|
124
|
+
};
|
|
125
|
+
}
|