@qilin-agent/client-ui-layout 0.0.0-stage → 3.1.3

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/lib/client.js ADDED
@@ -0,0 +1,662 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@qilin-agent/client-ui-layout",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_jsx_runtime = require("react/jsx-runtime");
8
+ let react = require("react");
9
+ let _qilin_agent_client_store = require("@qilin-agent/client-store");
10
+ /** Viewport width below which the sidebar auto-collapses to the rail (deepsuite
11
+ * LG breakpoint); a manual toggle below it re-expands over the squeezed center
12
+ * (stores.ts narrowExpanded). */
13
+ const SIDEBAR_AUTO_COLLAPSE = 1024;
14
+ /** Maximum normal right panel width as a fraction of the frame. */
15
+ const RIGHTBAR_MAX_RATIO = .7;
16
+ /** First-open right panel preference as a fraction of the frame. */
17
+ const RIGHTBAR_DEFAULT_RATIO = .35;
18
+ /**
19
+ * Clamp a panel width into its contract range.
20
+ * @param px - requested width.
21
+ * @param min - range lower bound.
22
+ * @param max - range upper bound.
23
+ * @returns the clamped width.
24
+ */
25
+ function clampWidth(px, min, max) {
26
+ return Math.min(max, Math.max(min, Math.round(px)));
27
+ }
28
+ /**
29
+ * Solve the three column widths for one viewport frame.
30
+ * @param viewport - available frame width in px.
31
+ * @param sidebar - sidebar width preference in px (0 = closed).
32
+ * @param rightbar - requested right panel width in px (0 = no track).
33
+ * @param collapsedWidth - track width of the closed sidebar; the default keeps
34
+ * the icon rail, 0 hides the column entirely (macOS desktop).
35
+ * @returns actual widths after shrinking or removing the right track; only
36
+ * without that track may the center fall below its minimum, down to zero.
37
+ */
38
+ function computeColumns(viewport, sidebar, rightbar, collapsedWidth = 56) {
39
+ const s = sidebar === 0 ? collapsedWidth : clampWidth(sidebar, 264, 420);
40
+ const available = viewport - s - 400;
41
+ const r = rightbar === 0 || available < 300 ? 0 : Math.min(available, clampWidth(rightbar, 300, viewport * RIGHTBAR_MAX_RATIO));
42
+ return {
43
+ sidebar: s,
44
+ center: Math.max(0, viewport - s - r),
45
+ rightbar: r
46
+ };
47
+ }
48
+ //#endregion
49
+ //#region lib/types/client/DocumentTitle.js
50
+ /** Browser title selection follows the active main panel without subscribing the frame. */
51
+ /**
52
+ * Project the selected durable session title into the browser title and
53
+ * restore the build-selected product title when unmounted.
54
+ * @param props - Selected session title projection.
55
+ * @returns No rendered content.
56
+ */
57
+ function DocumentTitle({ useSessions, usePanelInfo, productTitle }) {
58
+ const showSessionTitle = usePanelInfo((info) => info.activePanelId === null);
59
+ const title = useSessions((state) => {
60
+ const current = Object.values(state.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
61
+ return !showSessionTitle || current === void 0 ? void 0 : state.byId[current]?.title;
62
+ });
63
+ (0, react.useEffect)(() => {
64
+ document.title = title === void 0 ? productTitle : `${title} — ${productTitle}`;
65
+ return () => {
66
+ document.title = productTitle;
67
+ };
68
+ }, [productTitle, title]);
69
+ return null;
70
+ }
71
+ //#endregion
72
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-layout/src/client/AppFrame.module.css.mjs
73
+ const css = ".WW6k1q_frame{background:var(--qilin-alias-bg-base);height:100%;transition:grid-template-columns var(--ds-transition-duration-slow) var(--ds-ease-in-out);grid-template-rows:minmax(0,1fr) auto;display:grid;position:relative;overflow:hidden}.WW6k1q_frame[data-dragging]{transition:none}[data-windows-titlebar]{--dsh-frame-top-clearance:var(--dsh-windows-titlebar-height);--dsh-frame-chrome-top:var(--dsh-windows-titlebar-height)}[data-platform=darwin],[data-windows-titlebar]{--dsh-frame-overlay-top:calc(var(--dsh-frame-top-clearance,0px) + 20px)}[data-fullscreen]{--dsh-frame-overlay-top:20px;--dsh-frame-chrome-top:0px}[data-windows-titlebar] .WW6k1q_frame{--dsh-windows-content-radius:16px;box-sizing:border-box;padding-top:var(--dsh-windows-titlebar-height);background:var(--qilin-specific-sidebar-fill);grid-template-rows:minmax(0,1fr)}[data-windows-titlebar] .WW6k1q_centerCol{background:var(--qilin-alias-bg-base);border-radius:var(--dsh-windows-content-radius) 0 0 0;corner-shape:round}[data-windows-titlebar] .WW6k1q_frame:before{content:\"\";height:var(--dsh-windows-titlebar-height);background:var(--qilin-specific-sidebar-fill);-webkit-app-region:drag;position:absolute;inset:0 0 auto}[data-windows-titlebar] .WW6k1q_sidebarCol{border-right:none}[data-windows-titlebar] .WW6k1q_handle{top:var(--dsh-windows-titlebar-height)}@media (prefers-reduced-motion:reduce){.WW6k1q_frame{transition:none}}.WW6k1q_sidebarCol{background:var(--qilin-specific-sidebar-fill);border-right:.5px solid var(--qilin-alias-border-l3);min-width:0;overflow:hidden}.WW6k1q_centerCol{flex-direction:column;min-width:0;display:flex;overflow:hidden}[data-platform=darwin] .WW6k1q_frame{background:0 0}[data-platform=darwin] .WW6k1q_sidebarCol{background:color-mix(in srgb, var(--qilin-specific-sidebar-fill) 60%, transparent);border-right:none}[data-platform=darwin] .WW6k1q_centerCol{background:var(--qilin-alias-bg-base);border-left:.5px solid var(--qilin-alias-border-l3)}[data-platform=darwin] [data-sidebar-collapsed] .WW6k1q_centerCol{border-left:none}.WW6k1q_handle{cursor:col-resize;z-index:11;touch-action:none;width:8px;transition:left var(--ds-transition-duration-slow) var(--ds-ease-in-out);margin-left:-4px;position:absolute;top:0;bottom:0}.WW6k1q_handle[data-side=rightbar]:after{content:\"\";corner-shape:round;background:var(--qilin-alias-border-l4);opacity:0;width:4px;height:32px;transition:opacity var(--ds-transition-duration-fast) var(--ds-ease-in-out);border-radius:999px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.WW6k1q_handle[data-side=rightbar]:hover:after,.WW6k1q_handle[data-side=rightbar][data-dragging]:after{opacity:1}.WW6k1q_frame[data-dragging] .WW6k1q_handle,.WW6k1q_frame[data-rightbar-fullscreen],.WW6k1q_frame[data-rightbar-fullscreen] .WW6k1q_handle,.WW6k1q_frame[data-rightbar-instant],.WW6k1q_frame[data-rightbar-instant] .WW6k1q_handle{transition:none}@media (prefers-reduced-motion:reduce){.WW6k1q_handle{transition:none}}.WW6k1q_rightbarCol{min-width:0;position:relative;overflow:visible}.WW6k1q_bottomRow{background:var(--qilin-alias-bg-base);grid-column:1/-1;min-width:0;min-height:0;overflow:auto}.WW6k1q_overlayLayer{z-index:20;pointer-events:none;position:absolute;inset:0}.WW6k1q_overlayLayer>*{pointer-events:auto}";
74
+ const tagId = "@qilin-agent/client-ui-layout/AppFrame.module.css";
75
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
76
+ const tag = document.createElement("style");
77
+ tag.dataset.plugin = "@qilin-agent/client-ui-layout";
78
+ tag.dataset.pluginCss = tagId;
79
+ tag.textContent = css;
80
+ document.head.appendChild(tag);
81
+ }
82
+ var AppFrame_module_css_default = {
83
+ "bottomRow": "WW6k1q_bottomRow",
84
+ "centerCol": "WW6k1q_centerCol",
85
+ "frame": "WW6k1q_frame",
86
+ "handle": "WW6k1q_handle",
87
+ "overlayLayer": "WW6k1q_overlayLayer",
88
+ "rightbarCol": "WW6k1q_rightbarCol",
89
+ "sidebarCol": "WW6k1q_sidebarCol"
90
+ };
91
+ //#endregion
92
+ //#region lib/types/client/AppFrame.js
93
+ /**
94
+ * Three-column shell frame, registered into the built-in 'root' slot (the web
95
+ * shell renders only 'root'). Owns the grid tracks (sidebar | center |
96
+ * rightbar), the drag handles (pointer capture + rAF throttle), the column
97
+ * solve (columns.ts), and the child-slot render decisions: the sidebar slot
98
+ * receives live parameters from that solve. The root-scoped main slot selects
99
+ * the Conversation or a global panel. Each column occupant owns its Session
100
+ * binding and reports the geometry it needs.
101
+ *
102
+ * The right column is a track, not a box: its occupant draws its panel anchored
103
+ * to the frame's right edge at the resolved normal width, and the
104
+ * track only decides whether the centre makes room for it. The occupant reports
105
+ * shown/track/fullscreen through `ctx.layout`; fullscreen keeps the reported
106
+ * track but hides the outer resize handle. Everything arrives through the framework
107
+ * shares — zero cordis or framework imports, zero self-made hooks.
108
+ */
109
+ /** Center column grid item (session-body building block). */
110
+ function CenterColumn(props) {
111
+ return (0, react_jsx_runtime.jsx)("div", {
112
+ className: AppFrame_module_css_default.centerCol,
113
+ children: props.children
114
+ });
115
+ }
116
+ /** Subscribe to the main key without subscribing the column frame to each panel id. */
117
+ function MainPanel({ usePanelInfo, renderSlot }) {
118
+ return renderSlot("main", {}, { entryKey: usePanelInfo((info) => info.activePanelId) ?? "conversation" });
119
+ }
120
+ /**
121
+ * Right column grid item. Zero-width unless the occupant asked for a track; the
122
+ * occupant's panel is positioned against the column's right edge, which never
123
+ * moves, so it can hang over the centre when there is no track.
124
+ */
125
+ function RightbarColumn(props) {
126
+ return (0, react_jsx_runtime.jsx)("div", {
127
+ className: AppFrame_module_css_default.rightbarCol,
128
+ "data-rightbar-col": true,
129
+ children: props.children
130
+ });
131
+ }
132
+ /**
133
+ * One drag handle: pointer capture, rAF-throttled dx reports against the drag-start origin.
134
+ * `side` keys the hover-reveal CSS to the owning column.
135
+ */
136
+ function DragHandle(props) {
137
+ const [dragging, setDragging] = (0, react.useState)(false);
138
+ const origin = (0, react.useRef)(0);
139
+ const latest = (0, react.useRef)(0);
140
+ const frame = (0, react.useRef)(null);
141
+ const capture = (0, react.useRef)(null);
142
+ const callbacks = (0, react.useRef)({
143
+ onStart: props.onStart,
144
+ onDrag: props.onDrag,
145
+ onEnd: props.onEnd
146
+ });
147
+ callbacks.current = {
148
+ onStart: props.onStart,
149
+ onDrag: props.onDrag,
150
+ onEnd: props.onEnd
151
+ };
152
+ const endDrag = (0, react.useCallback)(() => {
153
+ const active = capture.current;
154
+ if (active === null) return;
155
+ capture.current = null;
156
+ if (frame.current !== null) {
157
+ cancelAnimationFrame(frame.current);
158
+ frame.current = null;
159
+ }
160
+ if (active.element.hasPointerCapture(active.id)) active.element.releasePointerCapture(active.id);
161
+ setDragging(false);
162
+ callbacks.current.onEnd();
163
+ }, []);
164
+ (0, react.useEffect)(() => endDrag, [endDrag]);
165
+ const onPointerDown = (0, react.useCallback)((e) => {
166
+ if (e.button !== 0 || capture.current !== null) return;
167
+ e.preventDefault();
168
+ e.currentTarget.setPointerCapture(e.pointerId);
169
+ capture.current = {
170
+ element: e.currentTarget,
171
+ id: e.pointerId
172
+ };
173
+ origin.current = e.clientX;
174
+ latest.current = e.clientX;
175
+ callbacks.current.onStart();
176
+ setDragging(true);
177
+ }, []);
178
+ const onPointerMove = (0, react.useCallback)((e) => {
179
+ if (capture.current?.id !== e.pointerId) return;
180
+ latest.current = e.clientX;
181
+ frame.current ??= requestAnimationFrame(() => {
182
+ frame.current = null;
183
+ callbacks.current.onDrag(latest.current - origin.current);
184
+ });
185
+ }, []);
186
+ const onPointerUp = (0, react.useCallback)((e) => {
187
+ if (capture.current?.id !== e.pointerId) return;
188
+ callbacks.current.onDrag(e.clientX - origin.current);
189
+ endDrag();
190
+ }, [endDrag]);
191
+ const onPointerCancel = (0, react.useCallback)((e) => {
192
+ if (capture.current?.id === e.pointerId) endDrag();
193
+ }, [endDrag]);
194
+ return (0, react_jsx_runtime.jsx)("div", {
195
+ className: AppFrame_module_css_default.handle,
196
+ style: { left: props.left },
197
+ "data-side": props.side,
198
+ "data-dragging": dragging || void 0,
199
+ onPointerDown,
200
+ onPointerMove,
201
+ onPointerUp,
202
+ onPointerCancel,
203
+ onLostPointerCapture: onPointerCancel
204
+ });
205
+ }
206
+ /** The three-column frame (see module doc). */
207
+ function AppFrame({ useStore, useSessions, usePanelInfo, actions, renderSlot, t }) {
208
+ const layoutInfo = useStore((state) => state.layoutInfo);
209
+ const frameRef = (0, react.useRef)(null);
210
+ const viewport = layoutInfo.viewportWidth;
211
+ (0, react.useLayoutEffect)(() => {
212
+ const el = frameRef.current;
213
+ /* v8 ignore next -- the ref is always attached by effect time: the frame div renders unconditionally. */
214
+ if (el === null) return;
215
+ let raf = null;
216
+ let disposed = false;
217
+ const measure = () => {
218
+ const width = el.getBoundingClientRect().width;
219
+ if (width > 0) actions.setViewportWidth(width);
220
+ };
221
+ measure();
222
+ const observer = new ResizeObserver(() => {
223
+ if (disposed) return;
224
+ raf ??= requestAnimationFrame(() => {
225
+ raf = null;
226
+ measure();
227
+ });
228
+ });
229
+ observer.observe(el);
230
+ return () => {
231
+ disposed = true;
232
+ observer.disconnect();
233
+ if (raf !== null) cancelAnimationFrame(raf);
234
+ };
235
+ }, [actions]);
236
+ const narrow = viewport < SIDEBAR_AUTO_COLLAPSE;
237
+ const sidebarCollapsed = narrow ? !layoutInfo.narrowExpanded : layoutInfo.sidebar === 0;
238
+ const sidebarPreference = sidebarCollapsed ? 0 : layoutInfo.sidebar === 0 ? 280 : layoutInfo.sidebar;
239
+ const rightbarPreference = layoutInfo.rightbar ?? viewport * .35;
240
+ const collapsedWidth = document.documentElement.dataset.platform === "darwin" || document.documentElement.hasAttribute("data-windows-titlebar") ? 0 : 56;
241
+ const normal = computeColumns(viewport, !layoutInfo.rightbarShown && narrow ? 0 : sidebarPreference, rightbarPreference, collapsedWidth);
242
+ const cols = computeColumns(viewport, sidebarPreference, layoutInfo.rightbarTrack ? rightbarPreference : 0, collapsedWidth);
243
+ const colsRef = (0, react.useRef)(cols);
244
+ colsRef.current = cols;
245
+ const rightbarWidth = (0, react.useRef)(normal.rightbar);
246
+ rightbarWidth.current = normal.rightbar;
247
+ const sidebarBase = (0, react.useRef)(0);
248
+ const rightbarBase = (0, react.useRef)(0);
249
+ const [dragging, setDragging] = (0, react.useState)(false);
250
+ const onDragEnd = (0, react.useCallback)(() => {
251
+ setDragging(false);
252
+ }, []);
253
+ const onSidebarStart = (0, react.useCallback)(() => {
254
+ sidebarBase.current = colsRef.current.sidebar;
255
+ setDragging(true);
256
+ }, []);
257
+ const onSidebarDrag = (0, react.useCallback)((dx) => {
258
+ actions.setSidebar(sidebarBase.current + dx);
259
+ }, [actions]);
260
+ const onRightbarStart = (0, react.useCallback)(() => {
261
+ rightbarBase.current = rightbarWidth.current;
262
+ setDragging(true);
263
+ }, []);
264
+ const onRightbarDrag = (0, react.useCallback)((dx) => {
265
+ actions.setRightbar(rightbarBase.current - dx);
266
+ }, [actions]);
267
+ const productTitle = "QiLin";
268
+ const sidebar = (0, react.useMemo)(() => renderSlot("sidebar", {
269
+ collapsed: sidebarCollapsed,
270
+ width: cols.sidebar
271
+ }), [
272
+ renderSlot,
273
+ sidebarCollapsed,
274
+ cols.sidebar
275
+ ]);
276
+ const main = (0, react.useMemo)(() => (0, react_jsx_runtime.jsx)(MainPanel, {
277
+ usePanelInfo,
278
+ renderSlot
279
+ }), [usePanelInfo, renderSlot]);
280
+ const overlays = (0, react.useMemo)(() => renderSlot("shell.overlay", {}), [renderSlot]);
281
+ return (0, react_jsx_runtime.jsxs)("div", {
282
+ ref: frameRef,
283
+ className: AppFrame_module_css_default.frame,
284
+ "data-app-frame": "",
285
+ style: {
286
+ ...document.documentElement.hasAttribute("data-windows-titlebar") ? { "--qilin-windows-sidebar-width": `${cols.sidebar}px` } : {},
287
+ gridTemplateColumns: `${cols.sidebar}px minmax(0, 1fr) ${cols.rightbar}px`
288
+ },
289
+ "data-sidebar-collapsed": sidebarCollapsed || void 0,
290
+ "data-rightbar-collapsed": cols.rightbar === 0 || void 0,
291
+ "data-rightbar-fullscreen": layoutInfo.rightbarFullscreen || void 0,
292
+ "data-rightbar-instant": layoutInfo.rightbarInstant || void 0,
293
+ "data-dragging": dragging || void 0,
294
+ children: [
295
+ (0, react_jsx_runtime.jsx)(DocumentTitle, {
296
+ productTitle,
297
+ useSessions,
298
+ usePanelInfo
299
+ }),
300
+ (0, react_jsx_runtime.jsx)("div", {
301
+ className: AppFrame_module_css_default.sidebarCol,
302
+ children: sidebar
303
+ }),
304
+ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(CenterColumn, { children: main }), (0, react_jsx_runtime.jsx)(RightbarColumn, { children: renderSlot("rightbar", {
305
+ width: normal.rightbar,
306
+ viewportWidth: viewport,
307
+ canShow: normal.rightbar > 0
308
+ }) })] }),
309
+ (0, react_jsx_runtime.jsx)("div", {
310
+ className: AppFrame_module_css_default.bottomRow,
311
+ "data-shell-bottom": true,
312
+ children: renderSlot("shell.bottom", {})
313
+ }),
314
+ (0, react_jsx_runtime.jsx)("div", {
315
+ className: AppFrame_module_css_default.overlayLayer,
316
+ "data-shell-overlay": true,
317
+ children: overlays
318
+ }),
319
+ !sidebarCollapsed && (0, react_jsx_runtime.jsx)(DragHandle, {
320
+ side: "sidebar",
321
+ left: cols.sidebar,
322
+ onStart: onSidebarStart,
323
+ onDrag: onSidebarDrag,
324
+ onEnd: onDragEnd
325
+ }),
326
+ layoutInfo.rightbarShown && !layoutInfo.rightbarFullscreen && normal.rightbar > 0 && (0, react_jsx_runtime.jsx)(DragHandle, {
327
+ side: "rightbar",
328
+ left: viewport - normal.rightbar,
329
+ onStart: onRightbarStart,
330
+ onDrag: onRightbarDrag,
331
+ onEnd: onDragEnd
332
+ })
333
+ ]
334
+ });
335
+ }
336
+ //#endregion
337
+ //#region lib/types/client/stores.js
338
+ /**
339
+ * Root-owned frame measurement, panel preferences, and presentation reports.
340
+ * The registration supplies a fresh store and binds its actions to ctx.layout.
341
+ */
342
+ /**
343
+ * Create the layout panel store handle. For the sidebar the preference IS the
344
+ * width, so closing it forgets its drag width — reopening restores the contract
345
+ * default. The right panel initializes at 35% of the frame on first opening
346
+ * and keeps that px preference across resizes and close. Drag writes clamp to
347
+ * the current frame's range. Narrow sidebar toggles change only the expansion
348
+ * override; opening the right panel clears that override.
349
+ * @returns the store handle (spec + type + identity + factory in one).
350
+ */
351
+ function createLayoutStore() {
352
+ return (0, _qilin_agent_client_store.defineStore)({
353
+ init: () => ({
354
+ panelInfo: { activePanelId: null },
355
+ layoutInfo: {
356
+ sidebar: 280,
357
+ viewportWidth: window.innerWidth,
358
+ narrowExpanded: false,
359
+ rightbar: null,
360
+ rightbarShown: false,
361
+ rightbarTrack: false,
362
+ rightbarFullscreen: false,
363
+ rightbarInstant: false
364
+ }
365
+ }),
366
+ actions: {
367
+ selectPanel: (d, panelId) => {
368
+ d.panelInfo.activePanelId = panelId;
369
+ },
370
+ retainMainPanels: (d, panelIds) => {
371
+ if (d.panelInfo.activePanelId !== null && !panelIds.includes(d.panelInfo.activePanelId)) d.panelInfo.activePanelId = null;
372
+ },
373
+ setSidebar: (d, px) => {
374
+ d.layoutInfo.rightbarInstant = false;
375
+ d.layoutInfo.sidebar = clampWidth(px, 264, 420);
376
+ },
377
+ toggleSidebar: (d) => {
378
+ d.layoutInfo.rightbarInstant = false;
379
+ if (d.layoutInfo.viewportWidth < 1024) d.layoutInfo.narrowExpanded = !d.layoutInfo.narrowExpanded;
380
+ else d.layoutInfo.sidebar = d.layoutInfo.sidebar === 0 ? 280 : 0;
381
+ },
382
+ setViewportWidth: (d, width) => {
383
+ if (d.layoutInfo.viewportWidth === width) return;
384
+ d.layoutInfo.rightbarInstant = false;
385
+ if (d.layoutInfo.viewportWidth < 1024 !== width < 1024) d.layoutInfo.narrowExpanded = false;
386
+ d.layoutInfo.viewportWidth = width;
387
+ },
388
+ setRightbar: (d, px) => {
389
+ d.layoutInfo.rightbarInstant = false;
390
+ d.layoutInfo.rightbar = clampWidth(px, 300, Math.max(300, d.layoutInfo.viewportWidth * RIGHTBAR_MAX_RATIO));
391
+ },
392
+ openRightbar: (d, track, fullscreen) => {
393
+ if (!d.layoutInfo.rightbarShown || d.layoutInfo.rightbarTrack !== track || d.layoutInfo.rightbarFullscreen !== fullscreen) d.layoutInfo.rightbarInstant = d.layoutInfo.rightbarFullscreen && !fullscreen;
394
+ if (!d.layoutInfo.rightbarShown && d.layoutInfo.viewportWidth < 1024) d.layoutInfo.narrowExpanded = false;
395
+ d.layoutInfo.rightbar ??= Math.max(300, Math.round(d.layoutInfo.viewportWidth * RIGHTBAR_DEFAULT_RATIO));
396
+ d.layoutInfo.rightbarShown = true;
397
+ d.layoutInfo.rightbarTrack = track;
398
+ d.layoutInfo.rightbarFullscreen = fullscreen;
399
+ },
400
+ closeRightbar: (d) => {
401
+ if (d.layoutInfo.rightbarShown) d.layoutInfo.rightbarInstant = d.layoutInfo.rightbarFullscreen;
402
+ d.layoutInfo.rightbarShown = false;
403
+ d.layoutInfo.rightbarTrack = false;
404
+ d.layoutInfo.rightbarFullscreen = false;
405
+ }
406
+ }
407
+ });
408
+ }
409
+ //#endregion
410
+ //#region lib/types/client/service.js
411
+ /** Cross-plugin panel-action face (ctx.layout). */
412
+ var LayoutController = class {
413
+ panels;
414
+ hasMainPanel;
415
+ panelInfo;
416
+ navigation = new AbortController();
417
+ /**
418
+ * @param panels - actions of the instance shared with the root entry.
419
+ * @param hasMainPanel - checks the live main-slot registry for a panel id.
420
+ * @param panelInfo - root store's shared central-panel selection source.
421
+ */
422
+ constructor(panels, hasMainPanel, panelInfo) {
423
+ this.panels = panels;
424
+ this.hasMainPanel = hasMainPanel;
425
+ this.panelInfo = panelInfo;
426
+ }
427
+ /** Select a global panel or return to the Conversation. */
428
+ selectPanel(panelId) {
429
+ if (panelId !== null && !this.hasMainPanel(panelId)) throw new Error(`layout.selectPanel: main panel "${panelId}" is not registered`);
430
+ this.navigation.abort();
431
+ this.panels.selectPanel(panelId);
432
+ }
433
+ /** @returns the new pending navigation's cancellation signal. */
434
+ beginNavigation() {
435
+ this.navigation.abort();
436
+ this.navigation = new AbortController();
437
+ return this.navigation.signal;
438
+ }
439
+ /** Invalidate pending navigations when the layout owner is unloaded. */
440
+ dispose() {
441
+ this.navigation.abort();
442
+ }
443
+ /** Toggle the sidebar panel (closed ⟷ contract default width). */
444
+ toggleSidebar() {
445
+ this.panels.toggleSidebar();
446
+ }
447
+ /** Report the right panel's track and fullscreen presentation. */
448
+ openRightbar(track, fullscreen) {
449
+ this.panels.openRightbar(track, fullscreen);
450
+ }
451
+ /** Report the right panel as hidden: no track, no handle. */
452
+ closeRightbar() {
453
+ this.panels.closeRightbar();
454
+ }
455
+ };
456
+ //#endregion
457
+ //#region lib/types/client/shortcut-locales.js
458
+ /** Layout command labels. */
459
+ const zh = { toggle: "展开/收起左侧栏" };
460
+ /** English labels for the same layout commands. */
461
+ const en = { toggle: "Toggle left sidebar" };
462
+ //#endregion
463
+ //#region lib/types/client/theme-presenter.js
464
+ /** Body attribute selecting the dark base palette in the token stylesheets. */
465
+ const DARK_ATTRIBUTE = "data-ds-dark-theme";
466
+ /**
467
+ * Root attribute publishing the theme source (`light`, `dark`, or `system`)
468
+ * for host shells that mirror it into native window chrome (the Electron
469
+ * preload forwards it to `nativeTheme.themeSource` so macOS vibrancy follows
470
+ * the app theme). `system` only when the preference is `system`; a fixed
471
+ * preference (including registered theme ids) publishes its resolved scheme.
472
+ */
473
+ const THEME_SOURCE_ATTRIBUTE = "data-ds-theme-source";
474
+ /** Body variable carrying the user's content font size in px. */
475
+ const CONTENT_FONT_SIZE_VARIABLE = "--qilin-content-font-size";
476
+ /** Body variable carrying the user's content leading adjustment in px. */
477
+ const CONTENT_LEADING_VARIABLE = "--qilin-content-leading";
478
+ /** Applies theme snapshots to the document; one instance per plugin fiber. */
479
+ var ThemePresenter = class {
480
+ /** Token names this presenter wrote in the last apply (its retraction set). */
481
+ appliedTokens = [];
482
+ /** The single metadata node this presenter inserts and removes. */
483
+ themeColorMeta;
484
+ /** Create the presenter-owned metadata node before the first snapshot arrives. */
485
+ constructor() {
486
+ this.themeColorMeta = document.createElement("meta");
487
+ this.themeColorMeta.name = "theme-color";
488
+ }
489
+ /**
490
+ * Project a snapshot onto the document: set root `color-scheme` and the body
491
+ * palette attribute from `active.colorScheme` (never the id — `system` is
492
+ * resolved upstream), publish the content font-size and leading axes, then
493
+ * replace the previously applied token variables with `active.tokens`.
494
+ * Browser theme-color metadata follows the computed body background after
495
+ * those writes, so the rendered palette remains the color authority.
496
+ * @param snapshot - resolved theme snapshot from ctx.theme.
497
+ */
498
+ apply(snapshot) {
499
+ const scheme = snapshot.active.colorScheme;
500
+ document.documentElement.style.colorScheme = scheme;
501
+ document.documentElement.setAttribute(THEME_SOURCE_ATTRIBUTE, snapshot.preference === "system" ? "system" : scheme);
502
+ const body = document.body;
503
+ if (scheme === "dark") body.setAttribute(DARK_ATTRIBUTE, "");
504
+ else body.removeAttribute(DARK_ATTRIBUTE);
505
+ body.style.setProperty(CONTENT_FONT_SIZE_VARIABLE, `${snapshot.fontSize}px`);
506
+ body.style.setProperty(CONTENT_LEADING_VARIABLE, `${snapshot.leading}px`);
507
+ for (const name of this.appliedTokens) body.style.removeProperty(name);
508
+ this.appliedTokens = [];
509
+ for (const [name, value] of Object.entries(snapshot.active.tokens)) {
510
+ body.style.setProperty(name, value);
511
+ this.appliedTokens.push(name);
512
+ }
513
+ this.themeColorMeta.content = getComputedStyle(body).backgroundColor;
514
+ if (!this.themeColorMeta.isConnected) document.head.append(this.themeColorMeta);
515
+ }
516
+ /**
517
+ * Retract root color-scheme, the theme-source attribute, the palette attribute, token variables,
518
+ * the content axes, and the owned metadata node.
519
+ */
520
+ dispose() {
521
+ document.documentElement.style.removeProperty("color-scheme");
522
+ document.documentElement.removeAttribute(THEME_SOURCE_ATTRIBUTE);
523
+ const body = document.body;
524
+ body.removeAttribute(DARK_ATTRIBUTE);
525
+ body.style.removeProperty(CONTENT_FONT_SIZE_VARIABLE);
526
+ body.style.removeProperty(CONTENT_LEADING_VARIABLE);
527
+ for (const name of this.appliedTokens) body.style.removeProperty(name);
528
+ this.appliedTokens = [];
529
+ this.themeColorMeta.remove();
530
+ }
531
+ };
532
+ //#endregion
533
+ //#region lib/types/client/index.js
534
+ /** Required services (cordis fiber inject — the loader passes all module exports as an object plugin). */
535
+ const inject = [
536
+ "slots",
537
+ "theme",
538
+ "locale",
539
+ "shortcuts"
540
+ ];
541
+ /**
542
+ * Client plugin body: provide ctx.layout, then one register() call — AppFrame
543
+ * into 'root' with the four child-slot declarations, the layout store seat,
544
+ * and the shared root instance supplying commands and the panel-info source.
545
+ * @param ctx - client root context.
546
+ */
547
+ function apply(ctx) {
548
+ ctx.effect(() => ctx.locale.register("shortcuts.layout", {
549
+ zh,
550
+ en
551
+ }), "layout: command labels");
552
+ const t = ctx.locale.bind("shortcuts.layout");
553
+ ctx.effect(() => {
554
+ const handle = createLayoutStore();
555
+ const instance = handle.create();
556
+ const store = {
557
+ ...handle,
558
+ create: () => instance
559
+ };
560
+ const retainMainPanels = () => {
561
+ instance.actions.retainMainPanels(ctx.slots.entries("main").flatMap((entry) => entry.options.key === void 0 ? [] : [entry.options.key]));
562
+ };
563
+ const layout = new LayoutController(instance.actions, (id) => ctx.slots.entries("main").some((entry) => entry.options.key === id), {
564
+ getSnapshot: () => instance.getSnapshot().panelInfo,
565
+ subscribe: (listener) => instance.subscribe(listener)
566
+ });
567
+ const disposePanelInfo = ctx.slots.provideRoot({ hooks: { panelInfo: layout.panelInfo } });
568
+ const disposeService = ctx.reflect.provide("layout", layout);
569
+ const disposeRegistration = ctx.slots.register({
570
+ name: "root",
571
+ locale: "common",
572
+ children: {
573
+ "sidebar": {
574
+ kind: "single",
575
+ scope: "root"
576
+ },
577
+ "main": {
578
+ kind: "keyed",
579
+ scope: "root"
580
+ },
581
+ "rightbar": {
582
+ kind: "single",
583
+ scope: "root"
584
+ },
585
+ "shell.bottom": {
586
+ kind: "single",
587
+ scope: "root"
588
+ },
589
+ "shell.overlay": {
590
+ kind: "list",
591
+ scope: "root"
592
+ }
593
+ },
594
+ store
595
+ }, AppFrame);
596
+ const disposeShortcut = ctx.shortcuts.register({
597
+ id: "sidebar.left.toggle",
598
+ label: () => t("toggle"),
599
+ aliases: ["sidebar", "toggle left sidebar"],
600
+ defaults: {
601
+ "desktop:macos": {
602
+ code: "KeyB",
603
+ modifiers: ["primary"]
604
+ },
605
+ "desktop:windows": {
606
+ code: "KeyB",
607
+ modifiers: ["primary"]
608
+ },
609
+ "desktop:linux": {
610
+ code: "KeyB",
611
+ modifiers: ["primary"]
612
+ },
613
+ "web:macos": {
614
+ code: "KeyB",
615
+ modifiers: ["primary", "alt"]
616
+ },
617
+ "web:windows": {
618
+ code: "KeyB",
619
+ modifiers: ["primary", "alt"]
620
+ }
621
+ },
622
+ regions: ["page", "editable"],
623
+ modals: [],
624
+ resolve: () => ({
625
+ status: "handled",
626
+ run: () => {
627
+ layout.toggleSidebar();
628
+ }
629
+ })
630
+ });
631
+ const disposePanels = ctx.slots.subscribe("main", retainMainPanels);
632
+ retainMainPanels();
633
+ return () => {
634
+ disposeShortcut();
635
+ layout.dispose();
636
+ disposePanels();
637
+ disposeRegistration();
638
+ disposePanelInfo();
639
+ disposeService();
640
+ };
641
+ }, "ui-layout: service + root registration");
642
+ ctx.effect(() => {
643
+ const presenter = new ThemePresenter();
644
+ presenter.apply(ctx.theme.getTheme());
645
+ const off = ctx.on("theme/change", (snapshot) => {
646
+ presenter.apply(snapshot);
647
+ });
648
+ return () => {
649
+ off();
650
+ presenter.dispose();
651
+ };
652
+ }, "ui-layout: theme presenter");
653
+ }
654
+ //#endregion
655
+ exports.LayoutController = LayoutController;
656
+ exports.apply = apply;
657
+ exports.inject = inject;
658
+ return module.exports;
659
+ }
660
+ });
661
+
662
+ //# sourceMappingURL=client.js.map
package/lib/index.js ADDED
@@ -0,0 +1,6 @@
1
+ //#region lib/types/index.js
2
+ /** Host loader entry for the browser-only layout plugin. */
3
+ /** Provides no host-side behavior. */
4
+ function apply() {}
5
+ //#endregion
6
+ export { apply };
@@ -0,0 +1,7 @@
1
+ import type { PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore } from '@qilin-agent/client-ui-slots';
2
+ import type { createLayoutStore } from './stores.ts';
3
+ /** Full composed props: runtime share + child-slot render share + store share. */
4
+ export type AppFrameProps = PropsRuntime<'root'> & PropsRenderSlots<'sidebar' | 'main' | 'rightbar' | 'shell.bottom' | 'shell.overlay'> & PropsStore<ReturnType<typeof createLayoutStore>> & PropsLocale<'common'>;
5
+ /** The three-column frame (see module doc). */
6
+ export declare function AppFrame({ useStore, useSessions, usePanelInfo, actions, renderSlot, t, }: AppFrameProps): import("react").JSX.Element;
7
+ //# sourceMappingURL=AppFrame.d.ts.map