@zq-silk/yui 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/assets/client/app/catalog.js +40 -14
- package/dist/web/assets/client/app/main.js +33 -9
- package/dist/web/assets/client/app/manage.js +18 -0
- package/dist/web/assets/client/app/selection.js +4 -1
- package/dist/web/assets/client/app/taskView.js +22 -5
- package/dist/web/assets/client/i18n/en.js +10 -3
- package/dist/web/assets/client/i18n/zh-CN.js +10 -3
- package/dist/web/assets/client/index.js +4 -0
- package/dist/web/assets/client/layout/manageShell.js +30 -8
- package/dist/web/assets/client/layout/shortcuts.js +7 -3
- package/dist/web/assets/client/layout/sidebarNav.js +147 -0
- package/dist/web/assets/client/layout/sizing.js +5 -2
- package/dist/web/assets/client/layout/workspace.js +120 -21
- package/dist/web/assets/client/lib/motion.js +59 -0
- package/dist/web/assets/client/lib/prefs.js +29 -2
- package/dist/web/assets/client/lib/theme.js +11 -4
- package/dist/web/assets/client/ui/primitives.js +27 -0
- package/dist/web/assets/client/views/overview.js +8 -5
- package/dist/web/assets/client/views/task/page.js +10 -10
- package/dist/web/assets/shared/brand.js +22 -0
- package/dist/web/assets/shared/geometry.js +7 -1
- package/dist/web/assets/shared/icons.js +4 -0
- package/dist/web/assets/shared/motion.js +74 -0
- package/dist/web/assets/shell/appRail.js +14 -0
- package/dist/web/assets/shell/dialogs.js +1 -0
- package/dist/web/assets/shell/dock.js +19 -6
- package/dist/web/assets/shell/index.js +6 -2
- package/dist/web/assets/shell/login.js +9 -8
- package/dist/web/assets/shell/manage.js +32 -20
- package/dist/web/assets/shell/settingsContent.js +1 -1
- package/dist/web/assets/shell/sidebar.js +16 -23
- package/dist/web/assets/styles/base.js +1 -1
- package/dist/web/assets/styles/index.js +2 -0
- package/dist/web/assets/styles/layout/responsive.js +63 -19
- package/dist/web/assets/styles/layout/workspace.js +53 -16
- package/dist/web/assets/styles/motion.js +51 -0
- package/dist/web/assets/styles/tokens.js +5 -3
- package/dist/web/assets/styles/ui/controls.js +1 -1
- package/dist/web/assets/styles/ui/forms.js +1 -1
- package/dist/web/assets/styles/ui/surfaces.js +1 -2
- package/dist/web/assets/styles/views/conversation.js +1 -1
- package/dist/web/assets/styles/views/dock.js +22 -2
- package/dist/web/assets/styles/views/login.js +16 -13
- package/dist/web/assets/styles/views/manage.js +25 -42
- package/dist/web/assets/styles/views/page.js +2 -3
- package/dist/web/assets/styles/views/records.js +1 -1
- package/dist/web/assets/styles/views/settings.js +1 -1
- package/dist/web/assets/styles/views/sidebar.js +25 -20
- package/dist/web/assets/styles/views/task.js +9 -15
- package/dist/web/assets/styles/views/terminal.js +1 -1
- package/docs/release-workflow.md +22 -0
- package/docs/release-workflow.zh-CN.md +15 -0
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { LAYOUT_GEOMETRY } from "../../shared/geometry.js";
|
|
2
|
+
export const SIDEBAR_NAV_SCRIPT = String.raw `
|
|
3
|
+
// Task list visibility. On wide screens the list is a column the user can
|
|
4
|
+
// collapse (remembered in this browser). Where it cannot sit beside the
|
|
5
|
+
// workspace — narrow screens, or a medium screen with the dock open — it
|
|
6
|
+
// opens as a drawer over the workspace instead, and closes on a choice,
|
|
7
|
+
// on the scrim, on Escape or by dragging it away. Every [data-sidebar-toggle]
|
|
8
|
+
// reports the state. Drawer motion lives in CSS (layout/responsive); a drag
|
|
9
|
+
// only moves the panels with the finger and hands the release back to CSS.
|
|
10
|
+
// Collapsing the inline column glides through lib/motion.
|
|
11
|
+
import { readPreference, writePreference } from "/assets/js/lib/prefs.js";
|
|
12
|
+
import { layoutTransition } from "/assets/js/lib/motion.js";
|
|
13
|
+
|
|
14
|
+
const G = ${JSON.stringify(LAYOUT_GEOMETRY)};
|
|
15
|
+
|
|
16
|
+
// deps: { el: { sidebar, rail?, scrim, collapse }, relayout }
|
|
17
|
+
// rail the global rail, which travels with the list on narrow screens
|
|
18
|
+
// relayout() re-applies column widths after the list appears or hides
|
|
19
|
+
export function createSidebarNav(deps) {
|
|
20
|
+
const body = document.body;
|
|
21
|
+
const state = { collapsed: readPreference("yui.sidebar.collapsed", "0") === "1" };
|
|
22
|
+
|
|
23
|
+
// Whether the list can sit in the grid at the current width and dock state.
|
|
24
|
+
function inlineAvailable() {
|
|
25
|
+
const width = window.innerWidth;
|
|
26
|
+
if (width <= G.narrow) return false;
|
|
27
|
+
return !(width <= G.focusMax && body.classList.contains("dock-open"));
|
|
28
|
+
}
|
|
29
|
+
function drawerOpen() { return body.classList.contains("sidebar-drawer"); }
|
|
30
|
+
function visible() { return drawerOpen() || (inlineAvailable() && !state.collapsed); }
|
|
31
|
+
function sync() {
|
|
32
|
+
const expanded = String(visible());
|
|
33
|
+
document.querySelectorAll("[data-sidebar-toggle]").forEach(function (button) {
|
|
34
|
+
if (button.getAttribute("aria-expanded") !== expanded) button.setAttribute("aria-expanded", expanded);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function setCollapsed(collapsed) {
|
|
38
|
+
state.collapsed = collapsed;
|
|
39
|
+
writePreference("yui.sidebar.collapsed", collapsed ? "1" : "0");
|
|
40
|
+
// The columns glide to their new widths (View Transition, desktop only).
|
|
41
|
+
const update = function () {
|
|
42
|
+
body.classList.toggle("sidebar-collapsed", collapsed);
|
|
43
|
+
deps.relayout();
|
|
44
|
+
sync();
|
|
45
|
+
};
|
|
46
|
+
if (typeof layoutTransition === "function") layoutTransition(update);
|
|
47
|
+
else update();
|
|
48
|
+
}
|
|
49
|
+
function setDrawer(open) {
|
|
50
|
+
if (open === drawerOpen()) return;
|
|
51
|
+
body.classList.toggle("sidebar-drawer", open);
|
|
52
|
+
if (open) deps.el.collapse.focus({ preventScroll: true });
|
|
53
|
+
sync();
|
|
54
|
+
}
|
|
55
|
+
function reveal() {
|
|
56
|
+
if (!inlineAvailable()) setDrawer(true);
|
|
57
|
+
else if (state.collapsed) setCollapsed(false);
|
|
58
|
+
}
|
|
59
|
+
function hide() {
|
|
60
|
+
if (drawerOpen()) setDrawer(false);
|
|
61
|
+
else if (inlineAvailable()) setCollapsed(true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
body.classList.toggle("sidebar-collapsed", state.collapsed);
|
|
65
|
+
deps.el.collapse.addEventListener("click", hide);
|
|
66
|
+
deps.el.scrim.addEventListener("click", function () { setDrawer(false); });
|
|
67
|
+
// A choice made in the drawer hands the screen back to the workspace.
|
|
68
|
+
deps.el.sidebar.addEventListener("click", function (event) {
|
|
69
|
+
if (drawerOpen() && event.target.closest(".task-row, .manage-link, #create-task, #body-search-open")) setDrawer(false);
|
|
70
|
+
});
|
|
71
|
+
bindDrag(deps.el, drawerOpen, function () { setDrawer(false); });
|
|
72
|
+
window.addEventListener("resize", function () { if (drawerOpen() && inlineAvailable()) setDrawer(false); sync(); });
|
|
73
|
+
// The dock opening or closing changes whether the list fits.
|
|
74
|
+
new MutationObserver(sync).observe(body, { attributes: true, attributeFilter: ["class"] });
|
|
75
|
+
sync();
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
toggle: function () { if (visible()) hide(); else reveal(); },
|
|
79
|
+
reveal: reveal,
|
|
80
|
+
hide: hide,
|
|
81
|
+
closeDrawer: function () { setDrawer(false); },
|
|
82
|
+
drawerOpen: drawerOpen,
|
|
83
|
+
sync: sync
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Drag to dismiss, as native drawers do: once a touch moves mostly sideways,
|
|
88
|
+
// the panels follow the finger (leftward only) and the scrim thins with them.
|
|
89
|
+
// Releasing past a third of the width, or with a quick flick, closes; any
|
|
90
|
+
// other release springs back. Either way CSS animates from the current spot.
|
|
91
|
+
function bindDrag(el, isOpen, close) {
|
|
92
|
+
const panels = [el.sidebar, el.rail].filter(Boolean);
|
|
93
|
+
let drag = null;
|
|
94
|
+
function place(dx) {
|
|
95
|
+
const offset = "translate3d(" + dx + "px,0,0)";
|
|
96
|
+
panels.forEach(function (panel) { panel.style.transform = offset; });
|
|
97
|
+
el.scrim.style.opacity = String(Math.max(0, 1 + dx / drag.width));
|
|
98
|
+
}
|
|
99
|
+
function release() {
|
|
100
|
+
panels.forEach(function (panel) { panel.style.transform = ""; });
|
|
101
|
+
el.scrim.style.opacity = "";
|
|
102
|
+
document.body.classList.remove("drawer-dragging");
|
|
103
|
+
}
|
|
104
|
+
function start(event) {
|
|
105
|
+
if (!isOpen() || event.touches.length !== 1) return;
|
|
106
|
+
const touch = event.touches[0];
|
|
107
|
+
const width = panels.reduce(function (sum, panel) { return sum + panel.getBoundingClientRect().width; }, 0);
|
|
108
|
+
drag = { x: touch.clientX, y: touch.clientY, width: width || 1, active: false, dx: 0, at: event.timeStamp, v: 0 };
|
|
109
|
+
}
|
|
110
|
+
function move(event) {
|
|
111
|
+
if (!drag) return;
|
|
112
|
+
const touch = event.touches[0];
|
|
113
|
+
const dx = touch.clientX - drag.x;
|
|
114
|
+
const dy = touch.clientY - drag.y;
|
|
115
|
+
if (!drag.active) {
|
|
116
|
+
if (Math.abs(dx) < 8 && Math.abs(dy) < 8) return;
|
|
117
|
+
// A vertical start is a scroll; leave it alone.
|
|
118
|
+
if (Math.abs(dy) >= Math.abs(dx) || dx > 0) { drag = null; return; }
|
|
119
|
+
drag.active = true;
|
|
120
|
+
document.body.classList.add("drawer-dragging");
|
|
121
|
+
}
|
|
122
|
+
const next = Math.min(0, dx);
|
|
123
|
+
const elapsed = event.timeStamp - drag.at;
|
|
124
|
+
if (elapsed > 0) drag.v = (next - drag.dx) / elapsed;
|
|
125
|
+
drag.dx = next;
|
|
126
|
+
drag.at = event.timeStamp;
|
|
127
|
+
place(next);
|
|
128
|
+
}
|
|
129
|
+
function end() {
|
|
130
|
+
if (!drag) return;
|
|
131
|
+
const finished = drag;
|
|
132
|
+
drag = null;
|
|
133
|
+
if (!finished.active) return;
|
|
134
|
+
// Removing the inline offset in the same frame as the class change lets
|
|
135
|
+
// the CSS transition continue from where the finger let go.
|
|
136
|
+
const dismiss = finished.dx < -finished.width / 3 || finished.v < -0.45;
|
|
137
|
+
release();
|
|
138
|
+
if (dismiss) close();
|
|
139
|
+
}
|
|
140
|
+
[el.sidebar, el.rail, el.scrim].filter(Boolean).forEach(function (node) {
|
|
141
|
+
node.addEventListener("touchstart", start, { passive: true });
|
|
142
|
+
node.addEventListener("touchmove", move, { passive: true });
|
|
143
|
+
node.addEventListener("touchend", end, { passive: true });
|
|
144
|
+
node.addEventListener("touchcancel", end, { passive: true });
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
@@ -18,7 +18,10 @@ export function createSizing(el, dock, sidebar, dockVisible) {
|
|
|
18
18
|
const root = document.documentElement.style;
|
|
19
19
|
|
|
20
20
|
function maxDockWidth() {
|
|
21
|
-
|
|
21
|
+
// The global rail and an inline task list both take room from the
|
|
22
|
+
// workspace; a drawer floats over it and takes none.
|
|
23
|
+
const inline = !document.body.classList.contains("sidebar-drawer");
|
|
24
|
+
const aside = window.innerWidth > G.narrow ? G.rail + (inline ? el.sidebar.offsetWidth : 0) : 0;
|
|
22
25
|
return Math.max(G.dock.min, Math.min(G.dock.max, window.innerWidth - aside - G.centerMin));
|
|
23
26
|
}
|
|
24
27
|
function setDockWidth(width, persist) {
|
|
@@ -30,7 +33,7 @@ export function createSizing(el, dock, sidebar, dockVisible) {
|
|
|
30
33
|
}
|
|
31
34
|
function maxSidebarWidth() {
|
|
32
35
|
// An open dock can shrink to its minimum, so it reserves only that much.
|
|
33
|
-
const reserve = dockVisible() ? G.dock.min + G.divider : 0;
|
|
36
|
+
const reserve = G.rail + (dockVisible() ? G.dock.min + G.divider : 0);
|
|
34
37
|
return Math.max(G.sidebar.min, Math.min(G.sidebar.max, window.innerWidth - reserve - G.centerMin));
|
|
35
38
|
}
|
|
36
39
|
function applySidebarWidth() {
|
|
@@ -5,15 +5,17 @@ export const WORKSPACE_SCRIPT = String.raw `
|
|
|
5
5
|
// in the center), below the narrow breakpoint a full-screen sheet opened
|
|
6
6
|
// explicitly per Task. Everything here is view state and browser preference.
|
|
7
7
|
import { readPreference, writePreference, readSessionAccessMode } from "/assets/js/lib/prefs.js";
|
|
8
|
-
import { markSelected } from "/assets/js/lib/dom.js";
|
|
8
|
+
import { markSelected, h, icon, clear } from "/assets/js/lib/dom.js";
|
|
9
9
|
import { createSizing } from "/assets/js/layout/sizing.js";
|
|
10
|
+
import { layoutTransition, leave } from "/assets/js/lib/motion.js";
|
|
10
11
|
|
|
11
12
|
const NARROW_QUERY = "(max-width: ${BREAKPOINTS.narrow}px)";
|
|
12
13
|
const DOCK_INITIAL = ${DOCK.initial};
|
|
13
14
|
|
|
14
|
-
// deps: { el, state, terminal, sessionOpened }
|
|
15
|
-
// el the shell elements (dock, dividers, sidebar,
|
|
16
|
-
// and the
|
|
15
|
+
// deps: { el, state, t, terminal, conversation, preview, sessionOpened }
|
|
16
|
+
// el the shell elements (dock, dividers, sidebar, the dock's target
|
|
17
|
+
// menu, and the page's [data-dock-toggle] / [data-operator-toggle]
|
|
18
|
+
// buttons inside el.detail)
|
|
17
19
|
// state read-only here: state.selected
|
|
18
20
|
// terminal the dock's native Session (views/dock/terminal)
|
|
19
21
|
// sessionOpened() a Session was just opened in the dock; the caller
|
|
@@ -36,8 +38,8 @@ export function createWorkspace(deps) {
|
|
|
36
38
|
dockVisible: function () { return dockVisible(ws); },
|
|
37
39
|
updateLayout: function () { updateLayout(ws); },
|
|
38
40
|
syncDockButtons: function () { syncDockButtons(ws); },
|
|
39
|
-
setDockOpen: function (open) { setDockOpen(ws, open); },
|
|
40
|
-
toggleDock: function (mode) { toggleDock(ws, mode); },
|
|
41
|
+
setDockOpen: function (open) { glide(function () { setDockOpen(ws, open); }); },
|
|
42
|
+
toggleDock: function (mode) { glide(function () { toggleDock(ws, mode); }); },
|
|
41
43
|
openSession: function (target) { openSession(ws, target); },
|
|
42
44
|
openConversation: function (target, materials) {
|
|
43
45
|
ws.deps.terminal.close();
|
|
@@ -47,12 +49,8 @@ export function createWorkspace(deps) {
|
|
|
47
49
|
ws.deps.conversation.open(target, { current: true, materials: materials });
|
|
48
50
|
syncDockButtons(ws);
|
|
49
51
|
},
|
|
50
|
-
openOperator: function () { openOperator(ws); },
|
|
51
|
-
|
|
52
|
-
if (!ws.deps.state.selected && ws.dock.open && !ws.narrow.matches && preferredMode() === "session") {
|
|
53
|
-
openSession(ws, { scope: "global", roleName: "operator" });
|
|
54
|
-
}
|
|
55
|
-
},
|
|
52
|
+
openOperator: function () { glide(function () { openOperator(ws); }); },
|
|
53
|
+
toggleOperator: function () { glide(function () { toggleOperator(ws); }); },
|
|
56
54
|
// A newly selected Task starts with the narrow-screen sheet closed.
|
|
57
55
|
enterTask: function () {
|
|
58
56
|
ws.deps.terminal.close();
|
|
@@ -60,17 +58,27 @@ export function createWorkspace(deps) {
|
|
|
60
58
|
ws.dock.sheet = false;
|
|
61
59
|
ws.dock.mode = preferredMode();
|
|
62
60
|
ws.deps.conversation.open({ scope: "task", taskId: ws.deps.state.selected, roleName: "leader" });
|
|
61
|
+
pauseHiddenConversation(ws);
|
|
63
62
|
if (ws.dock.mode === "session" && !ws.narrow.matches && ws.dock.open) {
|
|
64
63
|
ws.deps.terminal.open({ scope: "task", taskId: ws.deps.state.selected, roleName: "leader" });
|
|
65
64
|
}
|
|
65
|
+
syncDockButtons(ws);
|
|
66
66
|
},
|
|
67
67
|
leaveTask: function () { leaveTask(ws); }
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// User-initiated dock changes (open, close, swap sides) glide the columns to
|
|
72
|
+
// their new widths; internal re-layouts stay immediate.
|
|
73
|
+
function glide(update) {
|
|
74
|
+
if (typeof layoutTransition === "function") layoutTransition(update);
|
|
75
|
+
else update();
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
function readDockState() {
|
|
72
79
|
return {
|
|
73
|
-
|
|
80
|
+
// The panel starts closed on every load and opens only on request.
|
|
81
|
+
open: false,
|
|
74
82
|
mode: "conversation",
|
|
75
83
|
center: readPreference("yui.dock.side", "end") === "center",
|
|
76
84
|
width: Number(readPreference("yui.dock.width", String(DOCK_INITIAL))) || DOCK_INITIAL,
|
|
@@ -100,7 +108,7 @@ function updateLayout(ws) {
|
|
|
100
108
|
document.body.classList.toggle("task-open", selected);
|
|
101
109
|
document.body.classList.toggle("dock-open", visible);
|
|
102
110
|
document.body.classList.toggle("dock-center", dock.center);
|
|
103
|
-
|
|
111
|
+
revealDock(ws, visible);
|
|
104
112
|
el.divider.hidden = !visible;
|
|
105
113
|
el.discussion.hidden = dock.mode !== "discussion";
|
|
106
114
|
el.session.hidden = dock.mode !== "session";
|
|
@@ -119,13 +127,88 @@ function updateLayout(ws) {
|
|
|
119
127
|
syncDockButtons(ws);
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
// On narrow screens the dock is a full-screen sheet: CSS slides it up on
|
|
131
|
+
// open, and closing slides it back down before it hides (an immediate close
|
|
132
|
+
// would cut the motion off). Wider screens change the grid; user-initiated
|
|
133
|
+
// changes there glide through glide().
|
|
134
|
+
function revealDock(ws, visible) {
|
|
135
|
+
const dockEl = ws.deps.el.dock;
|
|
136
|
+
if (ws.closingDock) { ws.closingDock.cancel(); ws.closingDock = null; }
|
|
137
|
+
if (visible || dockEl.hidden || !ws.narrow.matches || typeof leave !== "function") { dockEl.hidden = !visible; return; }
|
|
138
|
+
const closing = leave(dockEl, "sheetDown");
|
|
139
|
+
if (!closing.animation) { dockEl.hidden = true; return; }
|
|
140
|
+
ws.closingDock = closing;
|
|
141
|
+
closing.finished.then(function () {
|
|
142
|
+
if (ws.closingDock !== closing) return;
|
|
143
|
+
ws.closingDock = null;
|
|
144
|
+
dockEl.hidden = true;
|
|
145
|
+
closing.cancel();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
122
149
|
function syncDockButtons(ws) {
|
|
123
150
|
const el = ws.deps.el;
|
|
124
151
|
const visible = dockVisible(ws);
|
|
125
152
|
markSelected(el.detail.querySelectorAll("[data-dock-toggle]"), "aria-pressed", function (button) {
|
|
126
153
|
return visible && ws.dock.mode === button.dataset.dockToggle;
|
|
127
154
|
});
|
|
128
|
-
el.
|
|
155
|
+
markSelected(el.detail.querySelectorAll("[data-operator-toggle]"), "aria-pressed", function () { return operatorShown(ws); });
|
|
156
|
+
drawTarget(ws);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Conversation target -----------------------------------------------------
|
|
160
|
+
// The dock header names who the panel talks to. Operator is always offered;
|
|
161
|
+
// a selected Task adds its Leader. Other Roles use the global message dialog.
|
|
162
|
+
function currentTarget(ws) {
|
|
163
|
+
const mode = ws.dock.mode;
|
|
164
|
+
const selected = ws.deps.state.selected;
|
|
165
|
+
if (mode === "session") return ws.deps.terminal.current() || ws.terminalTarget || null;
|
|
166
|
+
if (mode === "conversation") return ws.deps.conversation.current();
|
|
167
|
+
return selected ? { scope: "task", taskId: selected, roleName: "leader" } : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function targetLabel(ws, target) {
|
|
171
|
+
if (!target) return ws.deps.t("actions.operator");
|
|
172
|
+
if (target.scope === "global") return target.roleName === "operator" ? ws.deps.t("actions.operator") : target.roleName;
|
|
173
|
+
return target.roleName + " · " + target.taskId;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sameTarget(a, b) {
|
|
177
|
+
return !!a && !!b && a.scope === b.scope && a.roleName === b.roleName && (a.scope !== "task" || a.taskId === b.taskId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function drawTarget(ws) {
|
|
181
|
+
const el = ws.deps.el;
|
|
182
|
+
const current = currentTarget(ws);
|
|
183
|
+
el.dockTargetLabel.textContent = targetLabel(ws, current);
|
|
184
|
+
el.dockTargetIcon.replaceChildren(icon(current && current.scope === "task" ? "target" : "broadcast"));
|
|
185
|
+
const options = [{ scope: "global", roleName: "operator" }];
|
|
186
|
+
if (ws.deps.state.selected) options.push({ scope: "task", taskId: ws.deps.state.selected, roleName: "leader" });
|
|
187
|
+
if (current && !options.some(function (option) { return sameTarget(option, current); })) options.push(current);
|
|
188
|
+
const signature = JSON.stringify([options, current, ws.deps.t("actions.operator")]);
|
|
189
|
+
if (el.dockTargetOptions.dataset.signature === signature) return;
|
|
190
|
+
el.dockTargetOptions.dataset.signature = signature;
|
|
191
|
+
clear(el.dockTargetOptions);
|
|
192
|
+
options.forEach(function (target) {
|
|
193
|
+
el.dockTargetOptions.append(h("button.dock-target-item", {
|
|
194
|
+
type: "button", "aria-pressed": String(sameTarget(target, current)),
|
|
195
|
+
onclick: function () { chooseTarget(ws, target); }
|
|
196
|
+
}, icon(target.scope === "task" ? "target" : "broadcast"), h("span", null, targetLabel(ws, target)),
|
|
197
|
+
sameTarget(target, current) ? icon("check", "icon-sm dock-target-check") : null));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Keep the current view (live terminal or structured conversation) and switch
|
|
202
|
+
// only its owner; Task-only views fall back to the conversation.
|
|
203
|
+
function chooseTarget(ws, target) {
|
|
204
|
+
ws.deps.el.dockTarget.open = false;
|
|
205
|
+
if (ws.dock.mode === "session") { openSession(ws, target); return; }
|
|
206
|
+
ws.deps.terminal.close();
|
|
207
|
+
showDock(ws, true);
|
|
208
|
+
ws.dock.mode = "conversation";
|
|
209
|
+
updateLayout(ws);
|
|
210
|
+
ws.deps.conversation.open(target);
|
|
211
|
+
syncDockButtons(ws);
|
|
129
212
|
}
|
|
130
213
|
|
|
131
214
|
// The dock currently shows the global operator Session.
|
|
@@ -139,7 +222,6 @@ function showDock(ws, open) {
|
|
|
139
222
|
if (ws.narrow.matches) ws.dock.sheet = open;
|
|
140
223
|
else {
|
|
141
224
|
ws.dock.open = open;
|
|
142
|
-
writePreference("yui.dock.open", String(open));
|
|
143
225
|
}
|
|
144
226
|
}
|
|
145
227
|
|
|
@@ -176,6 +258,7 @@ function setDockMode(ws, mode) {
|
|
|
176
258
|
ws.deps.conversation.open(target ? { scope: target.scope, ...(target.scope === "task" ? { taskId: target.taskId } : {}), roleName: target.roleName } : ws.deps.state.selected
|
|
177
259
|
? { scope: "task", taskId: ws.deps.state.selected, roleName: "leader" }
|
|
178
260
|
: { scope: "global", roleName: "operator" });
|
|
261
|
+
syncDockButtons(ws);
|
|
179
262
|
return;
|
|
180
263
|
}
|
|
181
264
|
if (mode === "session") {
|
|
@@ -209,6 +292,11 @@ function openSession(ws, target) {
|
|
|
209
292
|
ws.deps.sessionOpened();
|
|
210
293
|
}
|
|
211
294
|
|
|
295
|
+
function toggleOperator(ws) {
|
|
296
|
+
if (operatorShown(ws)) setDockOpen(ws, false);
|
|
297
|
+
else openOperator(ws);
|
|
298
|
+
}
|
|
299
|
+
|
|
212
300
|
function openOperator(ws) {
|
|
213
301
|
ws.deps.terminal.close();
|
|
214
302
|
showDock(ws, true);
|
|
@@ -224,6 +312,14 @@ function leaveTask(ws) {
|
|
|
224
312
|
ws.deps.terminal.close();
|
|
225
313
|
ws.dock.mode = "conversation";
|
|
226
314
|
ws.deps.conversation.open({ scope: "global", roleName: "operator" });
|
|
315
|
+
pauseHiddenConversation(ws);
|
|
316
|
+
syncDockButtons(ws);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// The conversation keeps its owner while the panel is hidden but stops
|
|
320
|
+
// polling; opening the panel reconnects it.
|
|
321
|
+
function pauseHiddenConversation(ws) {
|
|
322
|
+
if (!dockVisible(ws) || ws.dock.mode !== "conversation") ws.deps.conversation.close();
|
|
227
323
|
}
|
|
228
324
|
|
|
229
325
|
function bindDockControls(ws) {
|
|
@@ -233,15 +329,18 @@ function bindDockControls(ws) {
|
|
|
233
329
|
el.dockTabConversation.addEventListener("click", function () { setDockMode(ws, "conversation"); });
|
|
234
330
|
el.dockTabSession.addEventListener("click", function () { setDockMode(ws, "session"); });
|
|
235
331
|
el.dockTabPreview.addEventListener("click", function () { setDockMode(ws, "preview"); });
|
|
236
|
-
el.dockClose.addEventListener("click", function () { setDockOpen(ws, false); });
|
|
332
|
+
el.dockClose.addEventListener("click", function () { glide(function () { setDockOpen(ws, false); }); });
|
|
237
333
|
el.dockSwap.addEventListener("click", function () {
|
|
238
334
|
dock.center = !dock.center;
|
|
239
335
|
writePreference("yui.dock.side", dock.center ? "center" : "end");
|
|
240
|
-
updateLayout(ws);
|
|
336
|
+
glide(function () { updateLayout(ws); });
|
|
337
|
+
});
|
|
338
|
+
el.globalInputOpen.addEventListener("click", function () { el.dockTarget.open = false; });
|
|
339
|
+
document.addEventListener("click", function (event) {
|
|
340
|
+
if (el.dockTarget.open && !el.dockTarget.contains(event.target)) el.dockTarget.open = false;
|
|
241
341
|
});
|
|
242
|
-
el.
|
|
243
|
-
if (
|
|
244
|
-
else openOperator(ws);
|
|
342
|
+
el.dockTarget.addEventListener("keydown", function (event) {
|
|
343
|
+
if (event.key === "Escape" && el.dockTarget.open) { event.stopPropagation(); el.dockTarget.open = false; el.dockTarget.querySelector("summary").focus(); }
|
|
245
344
|
});
|
|
246
345
|
}
|
|
247
346
|
`;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BREAKPOINTS } from "../../shared/geometry.js";
|
|
2
|
+
import { MOTION } from "../../shared/motion.js";
|
|
3
|
+
export const MOTION_SCRIPT = String.raw `
|
|
4
|
+
// Script-side motion, on the same durations, curves and presets as the
|
|
5
|
+
// stylesheets (shared/motion.ts). Every helper degrades to an immediate,
|
|
6
|
+
// synchronous change when the browser lacks the API, the user prefers
|
|
7
|
+
// reduced motion, or (for layout transitions) the screen is narrow, where
|
|
8
|
+
// drawers and sheets carry their own CSS motion.
|
|
9
|
+
const M = ${JSON.stringify(MOTION)};
|
|
10
|
+
const NARROW = ${BREAKPOINTS.narrow};
|
|
11
|
+
|
|
12
|
+
export const motion = M;
|
|
13
|
+
|
|
14
|
+
export function reducedMotion() {
|
|
15
|
+
return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function options(preset, extra) {
|
|
19
|
+
return Object.assign({ duration: M.durations[preset.duration], easing: M.easings[preset.easing] }, extra || {});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function canAnimate(el) { return Boolean(el) && typeof el.animate === "function" && !reducedMotion(); }
|
|
23
|
+
|
|
24
|
+
// Plays a preset on an element that has just appeared. Returns the
|
|
25
|
+
// Animation, or null when nothing was played.
|
|
26
|
+
export function enter(el, name) {
|
|
27
|
+
const preset = M.presets[name || "rise"];
|
|
28
|
+
if (!preset || !canAnimate(el)) return null;
|
|
29
|
+
return el.animate(preset.frames, options(preset));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Plays an exit preset and resolves once it has finished (or at once when
|
|
33
|
+
// motion is unavailable). The final frame is held until the caller hides
|
|
34
|
+
// the element; call cancel() on the returned animation to abort.
|
|
35
|
+
export function leave(el, name) {
|
|
36
|
+
const preset = M.presets[name || "fade"];
|
|
37
|
+
if (!preset || !canAnimate(el)) return { finished: Promise.resolve(), cancel() {} , animation: null };
|
|
38
|
+
const animation = el.animate(preset.frames, options(preset, { fill: "forwards" }));
|
|
39
|
+
return { finished: animation.finished.then(function () {}, function () {}), cancel() { animation.cancel(); }, animation };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Runs a DOM update that moves the workspace columns (sidebar collapse,
|
|
43
|
+
// dock open or close) inside a View Transition, so the columns glide to
|
|
44
|
+
// their new widths. Without support the update runs synchronously.
|
|
45
|
+
let layoutRunning = 0;
|
|
46
|
+
export function layoutTransition(update) {
|
|
47
|
+
const supported = typeof document !== "undefined" && typeof document.startViewTransition === "function" &&
|
|
48
|
+
typeof window !== "undefined" && window.innerWidth > NARROW && !reducedMotion() && !document.hidden;
|
|
49
|
+
if (!supported) { update(); return; }
|
|
50
|
+
// The column names exist only during the transition (styles/motion).
|
|
51
|
+
const root = document.documentElement;
|
|
52
|
+
const done = function () { if (--layoutRunning === 0) root.classList.remove("vt-layout"); };
|
|
53
|
+
layoutRunning += 1;
|
|
54
|
+
root.classList.add("vt-layout");
|
|
55
|
+
let transition;
|
|
56
|
+
try { transition = document.startViewTransition(update); } catch { done(); update(); return; }
|
|
57
|
+
transition.finished.finally(done);
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
@@ -14,11 +14,38 @@ export function clearPreference(key) {
|
|
|
14
14
|
try { localStorage.removeItem(key); return true; } catch { return false; }
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Per-tab return points between the workbench and the management areas, so
|
|
18
|
+
// switching back restores the filters, Task or section the user left. Only
|
|
19
|
+
// same-origin paths of the expected area are accepted; anything else falls back.
|
|
20
|
+
const PLACES = { workbench: /^\/(?:\?|$)/, models: /^\/models(?:[/?]|$)/, settings: /^\/settings(?:[/?]|$)/,
|
|
21
|
+
manage: /^\/(?:models|settings)(?:[/?]|$)/ };
|
|
22
|
+
|
|
23
|
+
export function rememberPlace(area, href) {
|
|
24
|
+
if (!PLACES[area] || !PLACES[area].test(href)) return;
|
|
25
|
+
try { sessionStorage.setItem("yui.nav." + area, href); } catch { /* storage unavailable: links keep defaults */ }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function recallPlace(area, fallback) {
|
|
29
|
+
try {
|
|
30
|
+
const value = sessionStorage.getItem("yui.nav." + area);
|
|
31
|
+
return value && PLACES[area] && PLACES[area].test(value) ? value : fallback;
|
|
32
|
+
} catch { return fallback; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Point every a[data-nav-area] inside root at its area's return point.
|
|
36
|
+
export function linkPlaces(root) {
|
|
37
|
+
root.querySelectorAll("a[data-nav-area]").forEach(function (link) {
|
|
38
|
+
if (!link.dataset.navDefault) link.dataset.navDefault = link.getAttribute("href");
|
|
39
|
+
link.setAttribute("href", recallPlace(link.dataset.navArea, link.dataset.navDefault));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
// Shared with session access consumers: no server configuration or live
|
|
18
44
|
// Session is changed by this browser-local preference.
|
|
19
45
|
export function readSessionAccessMode() {
|
|
20
|
-
|
|
21
|
-
|
|
46
|
+
// The structured conversation is the default; the native terminal is opt-in.
|
|
47
|
+
const value = readPreference("yui.session.accessMode", "structured");
|
|
48
|
+
return value === "native" ? "native" : "structured";
|
|
22
49
|
}
|
|
23
50
|
|
|
24
51
|
export function writeSessionAccessMode(value) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BRAND_ICONS } from "../../shared/brand.js";
|
|
1
2
|
import { DEFAULT_DARK_THEME, DEFAULT_LIGHT_THEME, THEME_OPTIONS } from "../../shared/themes.js";
|
|
2
3
|
// Shared by the parser-blocking head script and the later settings controller.
|
|
3
4
|
// Both resolve the same browser preference without publishing global state.
|
|
@@ -16,15 +17,21 @@ function storedPreference() {
|
|
|
16
17
|
function resolveTheme(preference, media) {
|
|
17
18
|
return preference === "system" ? (media.matches ? DEFAULT_LIGHT : DEFAULT_DARK) : preference;
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
// Stamp the resolved theme on <html> and swap in its pre-coloured favicon.
|
|
22
|
+
const BRAND_ICONS = ${JSON.stringify(BRAND_ICONS)};
|
|
23
|
+
function stampTheme(id) {
|
|
24
|
+
document.documentElement.dataset.theme = id;
|
|
25
|
+
const icon = document.querySelector('link[rel="icon"]');
|
|
26
|
+
if (icon && BRAND_ICONS[id] && icon.getAttribute("href") !== BRAND_ICONS[id]) icon.setAttribute("href", BRAND_ICONS[id]);
|
|
27
|
+
}
|
|
19
28
|
`;
|
|
20
29
|
// A same-origin classic script runs before styles/body, unlike the deferred
|
|
21
30
|
// application module graph. No inline script or weaker CSP is needed.
|
|
22
31
|
export const THEME_BOOTSTRAP_SCRIPT = String.raw `
|
|
23
32
|
(function () {
|
|
24
33
|
${THEME_PREFERENCE_SCRIPT}
|
|
25
|
-
|
|
26
|
-
storedPreference(), window.matchMedia("(prefers-color-scheme: light)")
|
|
27
|
-
);
|
|
34
|
+
stampTheme(resolveTheme(storedPreference(), window.matchMedia("(prefers-color-scheme: light)")));
|
|
28
35
|
})();
|
|
29
36
|
`;
|
|
30
37
|
export const THEME_SCRIPT = String.raw `
|
|
@@ -60,7 +67,7 @@ export function createThemeController(container, t) {
|
|
|
60
67
|
return resolveTheme(preference, media);
|
|
61
68
|
}
|
|
62
69
|
function apply() {
|
|
63
|
-
|
|
70
|
+
stampTheme(resolved());
|
|
64
71
|
if (container) markSelected(container.querySelectorAll("[data-theme-option]"), "aria-pressed", function (button) {
|
|
65
72
|
return button.dataset.themeOption === preference;
|
|
66
73
|
});
|
|
@@ -42,6 +42,33 @@ export function button(text, options) {
|
|
|
42
42
|
return element;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// The page header's Operator switch: the same place on the overview and on
|
|
46
|
+
// every Task page; layout/workspace marks it pressed while the dock shows it.
|
|
47
|
+
export function operatorToggle(t, onClick) {
|
|
48
|
+
const element = h("button.btn.btn-ghost.dock-toggle.operator-toggle", {
|
|
49
|
+
type: "button", title: t("actions.operator") + " · O", "aria-pressed": "false", onclick: onClick
|
|
50
|
+
}, icon("broadcast"), h("span", null, t("actions.operator")));
|
|
51
|
+
element.dataset.operatorToggle = "true";
|
|
52
|
+
return element;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The task-list switch at the start of the page header. It shows only while
|
|
56
|
+
// the list is out of view (collapsed, or a drawer on smaller screens);
|
|
57
|
+
// layout/sidebarNav keeps aria-expanded current.
|
|
58
|
+
export function sidebarToggle(t, onClick) {
|
|
59
|
+
const element = h("button.icon-btn.side-expand", {
|
|
60
|
+
type: "button", "aria-label": t("sidebar.show"), title: t("sidebar.show") + " · [", "aria-expanded": "false", onclick: onClick
|
|
61
|
+
}, h("span.i-sidebar", null, icon("sidebar")), h("span.i-menu", null, icon("menu")));
|
|
62
|
+
element.dataset.sidebarToggle = "true";
|
|
63
|
+
return element;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The top bar every page starts with (the management shell renders the same
|
|
67
|
+
// markup statically): the list switch, the location, then page actions.
|
|
68
|
+
export function topBar(t, revealSidebar, location, actions) {
|
|
69
|
+
return h("header.top-bar", null, sidebarToggle(t, revealSidebar), location, h("span.spacer"), actions || []);
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
export function note(text, toneName) {
|
|
46
73
|
return h("p.note" + (toneName ? ".tone-" + toneName : ""), null, text);
|
|
47
74
|
}
|
|
@@ -4,7 +4,7 @@ export const OVERVIEW_SCRIPT = String.raw `
|
|
|
4
4
|
// then catalog-wide signals and the explicit Session read.
|
|
5
5
|
import { h, icon, clear } from "/assets/js/lib/dom.js";
|
|
6
6
|
import { byNewest, fill, formatDateTime, relativeTime } from "/assets/js/lib/format.js";
|
|
7
|
-
import { badge, button, dot, emptyState, note } from "/assets/js/ui/primitives.js";
|
|
7
|
+
import { badge, button, dot, emptyState, note, operatorToggle, topBar } from "/assets/js/ui/primitives.js";
|
|
8
8
|
import { card } from "/assets/js/ui/containers.js";
|
|
9
9
|
import { metricTile } from "/assets/js/ui/metrics.js";
|
|
10
10
|
import { label, sessionBadges, tone } from "/assets/js/domain/vocab.js";
|
|
@@ -16,6 +16,12 @@ export function renderOverview(container, state, t, locale, actions) {
|
|
|
16
16
|
const active = tasks.filter(function (task) { return task.status === "active"; });
|
|
17
17
|
const recent = tasks.filter(function (task) { return task.status !== "archived"; }).slice().sort(byNewest).slice(0, 8);
|
|
18
18
|
const page = h("div.page.overview");
|
|
19
|
+
container.append(topBar(t, actions.revealSidebar,
|
|
20
|
+
h("nav.crumbs", { "aria-label": t("crumbs.label") },
|
|
21
|
+
h("span.crumb", null, t("manage.workbench")),
|
|
22
|
+
h("span.crumb-sep", { "aria-hidden": "true" }, "/"),
|
|
23
|
+
h("span.crumb-current", null, t("overview.title"))),
|
|
24
|
+
[operatorToggle(t, actions.toggleOperator)]));
|
|
19
25
|
page.append(overviewHead(state.counts, t, actions), metricStrip(state.counts, t));
|
|
20
26
|
page.append(h("div.card-stack", null,
|
|
21
27
|
inboxCard(state, t, actions),
|
|
@@ -30,13 +36,10 @@ export function renderOverview(container, state, t, locale, actions) {
|
|
|
30
36
|
function overviewHead(counts, t, actions) {
|
|
31
37
|
return h("header.page-head", null,
|
|
32
38
|
h("div.page-head-main", null,
|
|
33
|
-
h("p.kicker", null, t("overview.kicker")),
|
|
34
39
|
h("h1.page-title", null, t("overview.title")),
|
|
35
40
|
h("p.page-sub", null, counts
|
|
36
41
|
? fill(t("overview.sub"), { total: counts.total, active: counts.active || 0 })
|
|
37
|
-
: t("loading.dashboard")))
|
|
38
|
-
h("div.page-head-actions", null,
|
|
39
|
-
button(t("actions.operator"), { icon: "terminal", variant: "ghost", onClick: actions.openOperator })));
|
|
42
|
+
: t("loading.dashboard"))));
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
function metricStrip(counts, t) {
|