@qilin-agent/client-ui-layout 0.0.0-stage → 3.1.4
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/LICENSE +21 -0
- package/README.i18n.yaml +40 -0
- package/README.md +97 -2
- package/README.zh.md +98 -0
- package/lib/client.js +658 -0
- package/lib/index.js +6 -0
- package/lib/types/client/AppFrame.d.ts +7 -0
- package/lib/types/client/DocumentTitle.d.ts +14 -0
- package/lib/types/client/columns.d.ts +51 -0
- package/lib/types/client/index.d.ts +129 -0
- package/lib/types/client/service.d.ts +76 -0
- package/lib/types/client/shortcut-locales.d.ts +7 -0
- package/lib/types/client/stores.d.ts +72 -0
- package/lib/types/client/theme-presenter.d.ts +49 -0
- package/lib/types/index.d.ts +4 -0
- package/package.json +66 -3
package/lib/client.js
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
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
|
+
/** Applies theme snapshots to the document; one instance per plugin fiber. */
|
|
477
|
+
var ThemePresenter = class {
|
|
478
|
+
/** Token names this presenter wrote in the last apply (its retraction set). */
|
|
479
|
+
appliedTokens = [];
|
|
480
|
+
/** The single metadata node this presenter inserts and removes. */
|
|
481
|
+
themeColorMeta;
|
|
482
|
+
/** Create the presenter-owned metadata node before the first snapshot arrives. */
|
|
483
|
+
constructor() {
|
|
484
|
+
this.themeColorMeta = document.createElement("meta");
|
|
485
|
+
this.themeColorMeta.name = "theme-color";
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Project a snapshot onto the document: set root `color-scheme` and the body
|
|
489
|
+
* palette attribute from `active.colorScheme` (never the id — `system` is
|
|
490
|
+
* resolved upstream), publish the content font-size axis, then
|
|
491
|
+
* replace the previously applied token variables with `active.tokens`.
|
|
492
|
+
* Browser theme-color metadata follows the computed body background after
|
|
493
|
+
* those writes, so the rendered palette remains the color authority.
|
|
494
|
+
* @param snapshot - resolved theme snapshot from ctx.theme.
|
|
495
|
+
*/
|
|
496
|
+
apply(snapshot) {
|
|
497
|
+
const scheme = snapshot.active.colorScheme;
|
|
498
|
+
document.documentElement.style.colorScheme = scheme;
|
|
499
|
+
document.documentElement.setAttribute(THEME_SOURCE_ATTRIBUTE, snapshot.preference === "system" ? "system" : scheme);
|
|
500
|
+
const body = document.body;
|
|
501
|
+
if (scheme === "dark") body.setAttribute(DARK_ATTRIBUTE, "");
|
|
502
|
+
else body.removeAttribute(DARK_ATTRIBUTE);
|
|
503
|
+
body.style.setProperty(CONTENT_FONT_SIZE_VARIABLE, `${snapshot.fontSize}px`);
|
|
504
|
+
for (const name of this.appliedTokens) body.style.removeProperty(name);
|
|
505
|
+
this.appliedTokens = [];
|
|
506
|
+
for (const [name, value] of Object.entries(snapshot.active.tokens)) {
|
|
507
|
+
body.style.setProperty(name, value);
|
|
508
|
+
this.appliedTokens.push(name);
|
|
509
|
+
}
|
|
510
|
+
this.themeColorMeta.content = getComputedStyle(body).backgroundColor;
|
|
511
|
+
if (!this.themeColorMeta.isConnected) document.head.append(this.themeColorMeta);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Retract root color-scheme, the theme-source attribute, the palette attribute, token variables,
|
|
515
|
+
* the content axes, and the owned metadata node.
|
|
516
|
+
*/
|
|
517
|
+
dispose() {
|
|
518
|
+
document.documentElement.style.removeProperty("color-scheme");
|
|
519
|
+
document.documentElement.removeAttribute(THEME_SOURCE_ATTRIBUTE);
|
|
520
|
+
const body = document.body;
|
|
521
|
+
body.removeAttribute(DARK_ATTRIBUTE);
|
|
522
|
+
body.style.removeProperty(CONTENT_FONT_SIZE_VARIABLE);
|
|
523
|
+
for (const name of this.appliedTokens) body.style.removeProperty(name);
|
|
524
|
+
this.appliedTokens = [];
|
|
525
|
+
this.themeColorMeta.remove();
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region lib/types/client/index.js
|
|
530
|
+
/** Required services (cordis fiber inject — the loader passes all module exports as an object plugin). */
|
|
531
|
+
const inject = [
|
|
532
|
+
"slots",
|
|
533
|
+
"theme",
|
|
534
|
+
"locale",
|
|
535
|
+
"shortcuts"
|
|
536
|
+
];
|
|
537
|
+
/**
|
|
538
|
+
* Client plugin body: provide ctx.layout, then one register() call — AppFrame
|
|
539
|
+
* into 'root' with the four child-slot declarations, the layout store seat,
|
|
540
|
+
* and the shared root instance supplying commands and the panel-info source.
|
|
541
|
+
* @param ctx - client root context.
|
|
542
|
+
*/
|
|
543
|
+
function apply(ctx) {
|
|
544
|
+
ctx.effect(() => ctx.locale.register("shortcuts.layout", {
|
|
545
|
+
zh,
|
|
546
|
+
en
|
|
547
|
+
}), "layout: command labels");
|
|
548
|
+
const t = ctx.locale.bind("shortcuts.layout");
|
|
549
|
+
ctx.effect(() => {
|
|
550
|
+
const handle = createLayoutStore();
|
|
551
|
+
const instance = handle.create();
|
|
552
|
+
const store = {
|
|
553
|
+
...handle,
|
|
554
|
+
create: () => instance
|
|
555
|
+
};
|
|
556
|
+
const retainMainPanels = () => {
|
|
557
|
+
instance.actions.retainMainPanels(ctx.slots.entries("main").flatMap((entry) => entry.options.key === void 0 ? [] : [entry.options.key]));
|
|
558
|
+
};
|
|
559
|
+
const layout = new LayoutController(instance.actions, (id) => ctx.slots.entries("main").some((entry) => entry.options.key === id), {
|
|
560
|
+
getSnapshot: () => instance.getSnapshot().panelInfo,
|
|
561
|
+
subscribe: (listener) => instance.subscribe(listener)
|
|
562
|
+
});
|
|
563
|
+
const disposePanelInfo = ctx.slots.provideRoot({ hooks: { panelInfo: layout.panelInfo } });
|
|
564
|
+
const disposeService = ctx.reflect.provide("layout", layout);
|
|
565
|
+
const disposeRegistration = ctx.slots.register({
|
|
566
|
+
name: "root",
|
|
567
|
+
locale: "common",
|
|
568
|
+
children: {
|
|
569
|
+
"sidebar": {
|
|
570
|
+
kind: "single",
|
|
571
|
+
scope: "root"
|
|
572
|
+
},
|
|
573
|
+
"main": {
|
|
574
|
+
kind: "keyed",
|
|
575
|
+
scope: "root"
|
|
576
|
+
},
|
|
577
|
+
"rightbar": {
|
|
578
|
+
kind: "single",
|
|
579
|
+
scope: "root"
|
|
580
|
+
},
|
|
581
|
+
"shell.bottom": {
|
|
582
|
+
kind: "single",
|
|
583
|
+
scope: "root"
|
|
584
|
+
},
|
|
585
|
+
"shell.overlay": {
|
|
586
|
+
kind: "list",
|
|
587
|
+
scope: "root"
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
store
|
|
591
|
+
}, AppFrame);
|
|
592
|
+
const disposeShortcut = ctx.shortcuts.register({
|
|
593
|
+
id: "sidebar.left.toggle",
|
|
594
|
+
label: () => t("toggle"),
|
|
595
|
+
aliases: ["sidebar", "toggle left sidebar"],
|
|
596
|
+
defaults: {
|
|
597
|
+
"desktop:macos": {
|
|
598
|
+
code: "KeyB",
|
|
599
|
+
modifiers: ["primary"]
|
|
600
|
+
},
|
|
601
|
+
"desktop:windows": {
|
|
602
|
+
code: "KeyB",
|
|
603
|
+
modifiers: ["primary"]
|
|
604
|
+
},
|
|
605
|
+
"desktop:linux": {
|
|
606
|
+
code: "KeyB",
|
|
607
|
+
modifiers: ["primary"]
|
|
608
|
+
},
|
|
609
|
+
"web:macos": {
|
|
610
|
+
code: "KeyB",
|
|
611
|
+
modifiers: ["primary", "alt"]
|
|
612
|
+
},
|
|
613
|
+
"web:windows": {
|
|
614
|
+
code: "KeyB",
|
|
615
|
+
modifiers: ["primary", "alt"]
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
regions: ["page", "editable"],
|
|
619
|
+
modals: [],
|
|
620
|
+
resolve: () => ({
|
|
621
|
+
status: "handled",
|
|
622
|
+
run: () => {
|
|
623
|
+
layout.toggleSidebar();
|
|
624
|
+
}
|
|
625
|
+
})
|
|
626
|
+
});
|
|
627
|
+
const disposePanels = ctx.slots.subscribe("main", retainMainPanels);
|
|
628
|
+
retainMainPanels();
|
|
629
|
+
return () => {
|
|
630
|
+
disposeShortcut();
|
|
631
|
+
layout.dispose();
|
|
632
|
+
disposePanels();
|
|
633
|
+
disposeRegistration();
|
|
634
|
+
disposePanelInfo();
|
|
635
|
+
disposeService();
|
|
636
|
+
};
|
|
637
|
+
}, "ui-layout: service + root registration");
|
|
638
|
+
ctx.effect(() => {
|
|
639
|
+
const presenter = new ThemePresenter();
|
|
640
|
+
presenter.apply(ctx.theme.getTheme());
|
|
641
|
+
const off = ctx.on("theme/change", (snapshot) => {
|
|
642
|
+
presenter.apply(snapshot);
|
|
643
|
+
});
|
|
644
|
+
return () => {
|
|
645
|
+
off();
|
|
646
|
+
presenter.dispose();
|
|
647
|
+
};
|
|
648
|
+
}, "ui-layout: theme presenter");
|
|
649
|
+
}
|
|
650
|
+
//#endregion
|
|
651
|
+
exports.LayoutController = LayoutController;
|
|
652
|
+
exports.apply = apply;
|
|
653
|
+
exports.inject = inject;
|
|
654
|
+
return module.exports;
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
//# sourceMappingURL=client.js.map
|
package/lib/index.js
ADDED
|
@@ -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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PropsRuntime } from '@qilin-agent/client-ui-slots';
|
|
2
|
+
/** Props for the browser title projection. */
|
|
3
|
+
export type DocumentTitleProps = Pick<PropsRuntime<'root'>, 'useSessions' | 'usePanelInfo'> & {
|
|
4
|
+
/** Build-configured or localized product title. */
|
|
5
|
+
productTitle: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Project the selected durable session title into the browser title and
|
|
9
|
+
* restore the build-selected product title when unmounted.
|
|
10
|
+
* @param props - Selected session title projection.
|
|
11
|
+
* @returns No rendered content.
|
|
12
|
+
*/
|
|
13
|
+
export declare function DocumentTitle({ useSessions, usePanelInfo, productTitle }: DocumentTitleProps): null;
|
|
14
|
+
//# sourceMappingURL=DocumentTitle.d.ts.map
|