@zq-silk/yui 2.14.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.
Files changed (70) hide show
  1. package/dist/config/configCatalog.js +1 -1
  2. package/dist/controlPlaneCli.js +1 -1
  3. package/dist/job/projectSync.js +3 -3
  4. package/dist/storage/sqliteSchema.js +12 -0
  5. package/dist/storage/storageVersions.js +1 -1
  6. package/dist/storage/upgrade/projectSyncClockMigration.js +30 -0
  7. package/dist/web/assets/assetManifest.js +23 -1
  8. package/dist/web/assets/client/app/catalog.js +40 -14
  9. package/dist/web/assets/client/app/login.js +156 -0
  10. package/dist/web/assets/client/app/main.js +33 -9
  11. package/dist/web/assets/client/app/manage.js +18 -0
  12. package/dist/web/assets/client/app/selection.js +4 -1
  13. package/dist/web/assets/client/app/taskView.js +22 -5
  14. package/dist/web/assets/client/i18n/en.js +28 -4
  15. package/dist/web/assets/client/i18n/zh-CN.js +28 -4
  16. package/dist/web/assets/client/index.js +8 -0
  17. package/dist/web/assets/client/layout/manageShell.js +30 -8
  18. package/dist/web/assets/client/layout/shortcuts.js +7 -3
  19. package/dist/web/assets/client/layout/sidebarNav.js +147 -0
  20. package/dist/web/assets/client/layout/sizing.js +5 -2
  21. package/dist/web/assets/client/layout/workspace.js +120 -21
  22. package/dist/web/assets/client/lib/api.js +5 -0
  23. package/dist/web/assets/client/lib/motion.js +59 -0
  24. package/dist/web/assets/client/lib/prefs.js +29 -2
  25. package/dist/web/assets/client/lib/session.js +21 -0
  26. package/dist/web/assets/client/lib/theme.js +11 -4
  27. package/dist/web/assets/client/ui/primitives.js +27 -0
  28. package/dist/web/assets/client/views/dock/terminal.js +4 -1
  29. package/dist/web/assets/client/views/overview.js +8 -5
  30. package/dist/web/assets/client/views/task/page.js +10 -10
  31. package/dist/web/assets/shared/brand.js +22 -0
  32. package/dist/web/assets/shared/geometry.js +7 -1
  33. package/dist/web/assets/shared/icons.js +5 -0
  34. package/dist/web/assets/shared/motion.js +74 -0
  35. package/dist/web/assets/shell/appRail.js +14 -0
  36. package/dist/web/assets/shell/dialogs.js +1 -0
  37. package/dist/web/assets/shell/dock.js +19 -6
  38. package/dist/web/assets/shell/index.js +6 -2
  39. package/dist/web/assets/shell/login.js +57 -0
  40. package/dist/web/assets/shell/manage.js +32 -20
  41. package/dist/web/assets/shell/settingsContent.js +1 -1
  42. package/dist/web/assets/shell/sidebar.js +16 -23
  43. package/dist/web/assets/styles/base.js +1 -1
  44. package/dist/web/assets/styles/index.js +4 -0
  45. package/dist/web/assets/styles/layout/responsive.js +63 -19
  46. package/dist/web/assets/styles/layout/workspace.js +53 -16
  47. package/dist/web/assets/styles/motion.js +51 -0
  48. package/dist/web/assets/styles/tokens.js +5 -3
  49. package/dist/web/assets/styles/ui/controls.js +1 -1
  50. package/dist/web/assets/styles/ui/forms.js +1 -1
  51. package/dist/web/assets/styles/ui/surfaces.js +1 -2
  52. package/dist/web/assets/styles/views/conversation.js +1 -1
  53. package/dist/web/assets/styles/views/dock.js +22 -2
  54. package/dist/web/assets/styles/views/login.js +45 -0
  55. package/dist/web/assets/styles/views/manage.js +25 -42
  56. package/dist/web/assets/styles/views/page.js +2 -3
  57. package/dist/web/assets/styles/views/records.js +1 -1
  58. package/dist/web/assets/styles/views/settings.js +1 -1
  59. package/dist/web/assets/styles/views/sidebar.js +25 -20
  60. package/dist/web/assets/styles/views/task.js +9 -15
  61. package/dist/web/assets/styles/views/terminal.js +1 -1
  62. package/dist/web/webAccess.js +129 -9
  63. package/dist/web/webServer.js +170 -14
  64. package/docs/release-workflow.md +67 -0
  65. package/docs/release-workflow.zh-CN.md +49 -0
  66. package/docs/storage-baseline.md +7 -1
  67. package/docs/storage-baseline.zh-CN.md +6 -1
  68. package/docs/testing/verification-levels.md +77 -11
  69. package/docs/testing/verification-levels.zh-CN.md +63 -9
  70. package/package.json +1 -1
@@ -66,6 +66,9 @@ export const EN = {
66
66
  "jobs.category.ordinary": "Ordinary Tasks",
67
67
  "jobs.category.job": "Job execution Tasks",
68
68
  "jobs.category.all": "All Tasks",
69
+ "jobs.categoryShort.ordinary": "Ordinary",
70
+ "jobs.categoryShort.job": "Jobs",
71
+ "jobs.categoryShort.all": "All",
69
72
  "jobs.recentSuccess": "Most recent successful Run",
70
73
  "jobs.diagnostics": "Execution diagnostics",
71
74
  "jobs.artifacts": "Registered fixed artifacts",
@@ -146,6 +149,9 @@ export const EN = {
146
149
  "models.window.monthly": "Monthly",
147
150
  "manage.nav": "Main navigation",
148
151
  "manage.workbench": "Workbench",
152
+ "manage.sections": "Sections",
153
+ "manage.collapseSections": "Collapse sections",
154
+ "manage.showSections": "Show sections",
149
155
  "settings.section.preferences": "Preferences",
150
156
  "settings.section.skills": "Skills",
151
157
  "settings.section.server": "Agents & server",
@@ -444,6 +450,8 @@ export const EN = {
444
450
  "sidebar.workItems": "Work items",
445
451
  "sidebar.resize": "Resize task list",
446
452
  "sidebar.resizeHint": "Drag to resize · double-click to reset",
453
+ "sidebar.collapse": "Collapse task list",
454
+ "sidebar.show": "Show task list",
447
455
  "search.label": "Search tasks",
448
456
  "bodySearch.title": "Search task contents",
449
457
  "bodySearch.query": "Text to find",
@@ -505,7 +513,6 @@ export const EN = {
505
513
  "catalog.help.executionSignals": "Live runs, open work and recovery signals to inspect; not failures",
506
514
  "actions.refresh": "Refresh",
507
515
  "actions.operator": "Operator",
508
- "actions.back": "Back to tasks",
509
516
  "actions.close": "Close",
510
517
  "actions.answer": "Answer",
511
518
  "actions.save": "Save",
@@ -558,6 +565,7 @@ export const EN = {
558
565
  "shortcut.refresh": "Refresh",
559
566
  "shortcut.dock": "Show or hide the session panel",
560
567
  "shortcut.operator": "Operator session",
568
+ "shortcut.sidebar": "Show or hide the task list",
561
569
  "shortcut.tabs": "Switch task sections",
562
570
  "shortcut.escape": "Close panel / leave task",
563
571
  "loading.dashboard": "Reading local state…",
@@ -572,7 +580,6 @@ export const EN = {
572
580
  "common.notReported": "not reported",
573
581
  "detail.unobserved": "unknown",
574
582
  "detail.partial": "partial",
575
- "overview.kicker": "Workspace",
576
583
  "overview.title": "Overview",
577
584
  "overview.sub": "{total} tasks · {active} active",
578
585
  "overview.inbox": "Waiting on you",
@@ -604,7 +611,6 @@ export const EN = {
604
611
  "metrics.completed": "Completed",
605
612
  "metrics.total": "Total",
606
613
  "crumbs.label": "Breadcrumb",
607
- "crumbs.tasks": "Tasks",
608
614
  "task.updated": "Updated",
609
615
  "tabs.label": "Task sections",
610
616
  "tabs.overview": "Overview",
@@ -1000,6 +1006,7 @@ export const EN = {
1000
1006
  "terminal.cliHint": "Full terminal",
1001
1007
  "terminal.historyLimited": "This Role has a {current}-line tmux history. Exit and re-enter it to use {target} lines.",
1002
1008
  "global.open": "Global input",
1009
+ "global.openRole": "Message another Role…",
1003
1010
  "global.title": "Global Role input",
1004
1011
  "global.help": "Queue is the default. A cancel request does not prove the Turn stopped.",
1005
1012
  "global.role": "Role",
@@ -1226,5 +1233,22 @@ export const EN = {
1226
1233
  "discussion.older": "Show {count} earlier messages",
1227
1234
  "discussion.olderLoading": "Reading earlier messages…",
1228
1235
  "record.raw": "Raw record",
1229
- "record.reading": "Reading the exact record…"
1236
+ "record.reading": "Reading the exact record…",
1237
+ "login.title": "Sign in · Yui",
1238
+ "login.heading": "Sign in to Yui",
1239
+ "login.subtitle": "Enter the Web access password from this listener's private credential file.",
1240
+ "login.password": "Password",
1241
+ "login.show": "Show password",
1242
+ "login.hide": "Hide password",
1243
+ "login.submit": "Sign in",
1244
+ "login.submitting": "Signing in…",
1245
+ "login.error.empty": "Enter the password.",
1246
+ "login.error.invalid": "Incorrect password. Check the credential file and try again.",
1247
+ "login.error.throttled": "Too many failed attempts. Try again in {seconds} s.",
1248
+ "login.error.origin": "This address is not accepted by the Web listener. Open one of the URLs printed by yui web.",
1249
+ "login.error.network": "Cannot reach Yui Web. Check that the listener is running, then try again.",
1250
+ "login.notice.expired": "Your session has expired. Sign in again to continue.",
1251
+ "login.notice.signedOut": "You have signed out.",
1252
+ "login.httpWarning": "This connection uses plain HTTP and is not encrypted. Sign in only on a trusted network.",
1253
+ "login.noscript": "JavaScript is required to sign in."
1230
1254
  };
@@ -64,6 +64,9 @@ export const ZH_CN = {
64
64
  "jobs.category.ordinary": "普通任务",
65
65
  "jobs.category.job": "Job 执行任务",
66
66
  "jobs.category.all": "全部任务",
67
+ "jobs.categoryShort.ordinary": "普通",
68
+ "jobs.categoryShort.job": "Job",
69
+ "jobs.categoryShort.all": "全部",
67
70
  "jobs.recentSuccess": "最近成功的 Run",
68
71
  "jobs.diagnostics": "执行诊断",
69
72
  "jobs.artifacts": "已登记固定产物",
@@ -144,6 +147,9 @@ export const ZH_CN = {
144
147
  "models.window.monthly": "每月",
145
148
  "manage.nav": "主导航",
146
149
  "manage.workbench": "工作台",
150
+ "manage.sections": "分区",
151
+ "manage.collapseSections": "收起分区列表",
152
+ "manage.showSections": "展开分区列表",
147
153
  "settings.section.preferences": "偏好设置",
148
154
  "settings.section.skills": "Skill",
149
155
  "settings.section.server": "Agent 与服务",
@@ -440,6 +446,8 @@ export const ZH_CN = {
440
446
  "sidebar.workItems": "工作项",
441
447
  "sidebar.resize": "调整任务列表宽度",
442
448
  "sidebar.resizeHint": "拖拽调整宽度 · 双击恢复默认",
449
+ "sidebar.collapse": "收起任务列表",
450
+ "sidebar.show": "展开任务列表",
443
451
  "search.label": "搜索任务",
444
452
  "bodySearch.title": "搜索任务正文",
445
453
  "bodySearch.query": "查找正文",
@@ -501,7 +509,6 @@ export const ZH_CN = {
501
509
  "catalog.help.executionSignals": "需要检查的运行、未完成工作和恢复信号;不代表失败",
502
510
  "actions.refresh": "刷新",
503
511
  "actions.operator": "Operator",
504
- "actions.back": "返回任务列表",
505
512
  "actions.close": "关闭",
506
513
  "actions.answer": "回答",
507
514
  "actions.save": "保存",
@@ -554,6 +561,7 @@ export const ZH_CN = {
554
561
  "shortcut.refresh": "刷新",
555
562
  "shortcut.dock": "显示或隐藏会话面板",
556
563
  "shortcut.operator": "Operator 会话",
564
+ "shortcut.sidebar": "显示或隐藏任务列表",
557
565
  "shortcut.tabs": "切换任务分区",
558
566
  "shortcut.escape": "关闭面板/离开任务",
559
567
  "loading.dashboard": "正在读取本地状态…",
@@ -568,7 +576,6 @@ export const ZH_CN = {
568
576
  "common.notReported": "未上报",
569
577
  "detail.unobserved": "未知",
570
578
  "detail.partial": "部分",
571
- "overview.kicker": "工作区",
572
579
  "overview.title": "总览",
573
580
  "overview.sub": "{total} 个任务 · {active} 个进行中",
574
581
  "overview.inbox": "等待你回答",
@@ -600,7 +607,6 @@ export const ZH_CN = {
600
607
  "metrics.completed": "已完成",
601
608
  "metrics.total": "总数",
602
609
  "crumbs.label": "路径",
603
- "crumbs.tasks": "任务",
604
610
  "task.updated": "更新于",
605
611
  "tabs.label": "任务分区",
606
612
  "tabs.overview": "概览",
@@ -996,6 +1002,7 @@ export const ZH_CN = {
996
1002
  "terminal.cliHint": "完整终端",
997
1003
  "terminal.historyLimited": "这个 Role 的 tmux 历史只有 {current} 行;退出并重新进入后可使用 {target} 行。",
998
1004
  "global.open": "全局消息",
1005
+ "global.openRole": "给其他 Role 发消息…",
999
1006
  "global.title": "全局 Role 消息",
1000
1007
  "global.help": "默认排队;取消请求不代表执行已停止。",
1001
1008
  "global.role": "Role",
@@ -1222,5 +1229,22 @@ export const ZH_CN = {
1222
1229
  "discussion.older": "显示更早的 {count} 条消息",
1223
1230
  "discussion.olderLoading": "正在读取更早的消息…",
1224
1231
  "record.raw": "原始记录",
1225
- "record.reading": "正在读取完整记录…"
1232
+ "record.reading": "正在读取完整记录…",
1233
+ "login.title": "登录 · Yui",
1234
+ "login.heading": "登录 Yui",
1235
+ "login.subtitle": "输入此 Web 监听所用私有凭证文件中的访问密码。",
1236
+ "login.password": "密码",
1237
+ "login.show": "显示密码",
1238
+ "login.hide": "隐藏密码",
1239
+ "login.submit": "登录",
1240
+ "login.submitting": "正在登录…",
1241
+ "login.error.empty": "请输入密码。",
1242
+ "login.error.invalid": "密码不正确。请核对凭证文件后重试。",
1243
+ "login.error.throttled": "失败次数过多,请在 {seconds} 秒后重试。",
1244
+ "login.error.origin": "Web 监听不接受当前地址。请打开 yui web 输出的访问地址。",
1245
+ "login.error.network": "无法连接 Yui Web。请确认监听仍在运行后重试。",
1246
+ "login.notice.expired": "会话已过期,请重新登录以继续。",
1247
+ "login.notice.signedOut": "你已退出登录。",
1248
+ "login.httpWarning": "当前连接为未加密的 HTTP。请仅在可信网络中登录。",
1249
+ "login.noscript": "需要启用 JavaScript 才能登录。"
1226
1250
  };
@@ -16,7 +16,9 @@ import { DOM_SCRIPT } from "./lib/dom.js";
16
16
  import { FORMAT_SCRIPT } from "./lib/format.js";
17
17
  import { I18N_SCRIPT } from "./lib/i18n.js";
18
18
  import { MARKDOWN_SCRIPT } from "./lib/markdown.js";
19
+ import { MOTION_SCRIPT } from "./lib/motion.js";
19
20
  import { PREFS_SCRIPT } from "./lib/prefs.js";
21
+ import { SESSION_SCRIPT } from "./lib/session.js";
20
22
  import { THEME_SCRIPT } from "./lib/theme.js";
21
23
  import { CONTAINERS_SCRIPT } from "./ui/containers.js";
22
24
  import { CONTROLS_SCRIPT } from "./ui/controls.js";
@@ -54,6 +56,7 @@ import { RESIZER_SCRIPT } from "./layout/resizer.js";
54
56
  import { ROUTER_SCRIPT } from "./layout/router.js";
55
57
  import { SHORTCUTS_SCRIPT } from "./layout/shortcuts.js";
56
58
  import { SIZING_SCRIPT } from "./layout/sizing.js";
59
+ import { SIDEBAR_NAV_SCRIPT } from "./layout/sidebarNav.js";
57
60
  import { WORKSPACE_SCRIPT } from "./layout/workspace.js";
58
61
  import { CATALOG_SCRIPT } from "./app/catalog.js";
59
62
  import { SELECTION_SCRIPT } from "./app/selection.js";
@@ -66,6 +69,7 @@ import { MODELS_VIEW_SCRIPT } from "./views/models.js";
66
69
  import { MODELS_SCRIPT } from "./app/models.js";
67
70
  import { MANAGE_SHELL_SCRIPT } from "./layout/manageShell.js";
68
71
  import { MANAGE_SCRIPT } from "./app/manage.js";
72
+ import { LOGIN_SCRIPT } from "./app/login.js";
69
73
  export { APP_SCRIPT } from "./app/main.js";
70
74
  export const CLIENT_MODULES = Object.freeze({
71
75
  "views/skills": SKILLS_SCRIPT,
@@ -75,9 +79,11 @@ export const CLIENT_MODULES = Object.freeze({
75
79
  "lib/dom": DOM_SCRIPT,
76
80
  "lib/format": FORMAT_SCRIPT,
77
81
  "lib/prefs": PREFS_SCRIPT,
82
+ "lib/motion": MOTION_SCRIPT,
78
83
  "lib/i18n": I18N_SCRIPT,
79
84
  "lib/theme": THEME_SCRIPT,
80
85
  "lib/api": API_SCRIPT,
86
+ "lib/session": SESSION_SCRIPT,
81
87
  "lib/markdown": MARKDOWN_SCRIPT,
82
88
  "ui/primitives": PRIMITIVES_SCRIPT,
83
89
  "ui/containers": CONTAINERS_SCRIPT,
@@ -113,6 +119,7 @@ export const CLIENT_MODULES = Object.freeze({
113
119
  "views/task/files": TASK_FILES_SCRIPT,
114
120
  "layout/resizer": RESIZER_SCRIPT,
115
121
  "layout/sizing": SIZING_SCRIPT,
122
+ "layout/sidebarNav": SIDEBAR_NAV_SCRIPT,
116
123
  "layout/workspace": WORKSPACE_SCRIPT,
117
124
  "layout/router": ROUTER_SCRIPT,
118
125
  "layout/shortcuts": SHORTCUTS_SCRIPT,
@@ -121,6 +128,7 @@ export const CLIENT_MODULES = Object.freeze({
121
128
  "app/taskView": TASK_VIEW_SCRIPT,
122
129
  "app/settings": SETTINGS_SCRIPT,
123
130
  "app/manage": MANAGE_SCRIPT,
131
+ "app/login": LOGIN_SCRIPT,
124
132
  "app/models": MODELS_SCRIPT,
125
133
  "app/selection": SELECTION_SCRIPT
126
134
  });
@@ -1,6 +1,8 @@
1
1
  import { MANAGE_SECTIONS } from "../../shared/manageNav.js";
2
2
  export const MANAGE_SHELL_SCRIPT = String.raw `
3
3
  import { createI18n } from "/assets/js/lib/i18n.js";
4
+ import { rememberPlace, linkPlaces } from "/assets/js/lib/prefs.js";
5
+ import { enter } from "/assets/js/lib/motion.js";
4
6
  const SECTIONS = ${JSON.stringify(MANAGE_SECTIONS)};
5
7
  export const i18n = createI18n([document.querySelector("#locale-select"), document.querySelector("#settings-locale")]);
6
8
  const pages = new Map();
@@ -34,6 +36,8 @@ export function manageHref(next) {
34
36
  return "/" + manageArea(next.page, next.section) + "/" + next.section +
35
37
  (next.section === "quota" && next.source === "command-code" ? "?source=command-code" : "");
36
38
  }
39
+ // The center column scrolls, not the document (the frame is the workbench's).
40
+ function scroller() { return document.querySelector("#manage-scroll"); }
37
41
  function render() {
38
42
  if (!started) return;
39
43
  const rail = document.querySelector(".manage-rail");
@@ -43,29 +47,45 @@ function render() {
43
47
  if (active) link.setAttribute("aria-current", "page"); else link.removeAttribute("aria-current");
44
48
  if (dirty[link.dataset.page]?.[link.dataset.section]) link.dataset.dirty = "true"; else delete link.dataset.dirty;
45
49
  });
46
- rail.querySelectorAll("a[data-area]").forEach(link => {
47
- if (link.dataset.area === area) link.setAttribute("aria-current", "true"); else link.removeAttribute("aria-current");
50
+ // The global rail marks the current area.
51
+ const app = document.querySelector(".manage-app");
52
+ app.querySelectorAll("a[data-area]").forEach(link => {
53
+ if (link.dataset.area === area) link.setAttribute("aria-current", "page"); else link.removeAttribute("aria-current");
48
54
  });
55
+ // The list head and the top bar name where the reader is.
56
+ const areaTitle = i18n.t(area + ".title");
57
+ const sectionTitle = i18n.t((route.page === "models" ? "models" : "settings") + ".section." + route.section);
58
+ document.querySelector("#manage-area-title").textContent = areaTitle;
59
+ document.querySelector("#manage-crumb-area").textContent = areaTitle;
60
+ document.querySelector("#manage-crumb-section").textContent = sectionTitle;
49
61
  rail.querySelectorAll("[data-area-group]").forEach(group => {
50
62
  if (group.dataset.areaGroup === area) group.dataset.active = "true"; else delete group.dataset.active;
51
63
  });
52
- document.title = i18n.t((route.page === "models" ? "models" : "settings") + ".section." + route.section) +
53
- " · " + i18n.t(area + ".title") + " · Yui";
64
+ linkPlaces(app);
65
+ document.title = sectionTitle + " · " + areaTitle + " · Yui";
54
66
  }
55
67
  function apply(next) {
56
68
  route = next;
69
+ rememberPlace(manageArea(next.page, next.section), manageHref(next));
70
+ rememberPlace("manage", manageHref(next));
57
71
  document.querySelector("#models-root").hidden = next.page !== "models";
58
72
  document.querySelector("#settings-root").hidden = next.page !== "settings";
59
73
  pages.get(next.page).show(next);
60
74
  render();
61
75
  }
76
+ // A section the reader switched to rises in (never on the first paint).
77
+ function revealSection() {
78
+ if (typeof enter !== "function") return;
79
+ enter(document.querySelector("#models-root:not([hidden]), #settings-root:not([hidden])"), "rise");
80
+ }
62
81
  export function navigateManage(next) {
63
82
  const normalized = readManageRoute(new URL(manageHref(next), location.origin));
64
83
  if (manageHref(normalized) === location.pathname + location.search) return;
65
- history.replaceState({ ...history.state, scroll: window.scrollY }, "");
84
+ history.replaceState({ ...history.state, scroll: scroller().scrollTop }, "");
66
85
  history.pushState({ scroll: 0 }, "", manageHref(normalized));
67
86
  apply(normalized);
68
- window.scrollTo(0, 0);
87
+ scroller().scrollTop = 0;
88
+ revealSection();
69
89
  }
70
90
  export function createManageShell(options) {
71
91
  return {
@@ -79,7 +99,7 @@ export function registerManagePage(name, controller) { pages.set(name, controlle
79
99
  export function startManagement() {
80
100
  started = true;
81
101
  route = readManageRoute(new URL(location.href));
82
- history.replaceState({ ...history.state, scroll: window.scrollY }, "", manageHref(route));
102
+ history.replaceState({ ...history.state, scroll: scroller().scrollTop }, "", manageHref(route));
83
103
  history.scrollRestoration = "manual";
84
104
  apply(route);
85
105
  document.querySelector(".manage-app").hidden = false;
@@ -96,8 +116,10 @@ export function startManagement() {
96
116
  });
97
117
  window.addEventListener("popstate", () => {
98
118
  apply(readManageRoute(new URL(location.href)));
99
- window.scrollTo(0, history.state?.scroll || 0);
119
+ scroller().scrollTop = history.state?.scroll || 0;
120
+ revealSection();
100
121
  });
122
+ window.addEventListener("pageshow", render);
101
123
  i18n.subscribe(render);
102
124
  }
103
125
  `;
@@ -6,11 +6,12 @@ import { isEditing } from "/assets/js/lib/dom.js";
6
6
 
7
7
  const TABS = ${JSON.stringify(TASK_TABS)};
8
8
 
9
- // deps: { el, state, globalInput, workspace, selection, catalog, taskView }
9
+ // deps: { el, state, globalInput, workspace, sidebarNav, selection, catalog, taskView }
10
10
  // el #search, #settings-dialog and #terminal-host
11
11
  // state read-only here: state.selected, state.detail
12
12
  // globalInput views/globalInput (its dialog blocks shortcuts while open)
13
13
  // workspace layout/workspace: the dock and the operator Session
14
+ // sidebarNav layout/sidebarNav: "[" shows or hides the task list
14
15
  // selection app/selection: Escape leaves the Task
15
16
  // catalog app/catalog: "r" refreshes
16
17
  // taskView app/taskView: 1–4 switch tabs
@@ -26,10 +27,12 @@ export function bindShortcuts(deps) {
26
27
  });
27
28
  }
28
29
 
29
- // Escape unwinds one level: leave a field, close the dock, leave the Task.
30
+ // Escape unwinds one level: leave a field, close the task-list drawer, close
31
+ // the dock, leave the Task.
30
32
  function escape(deps, active, typing, inTerminal) {
31
33
  if (inTerminal) return;
32
34
  if (typing) { active.blur(); return; }
35
+ if (deps.sidebarNav.drawerOpen()) { deps.sidebarNav.closeDrawer(); return; }
33
36
  if (deps.workspace.dockVisible()) { deps.workspace.setDockOpen(false); return; }
34
37
  if (deps.state.selected) deps.selection.clearSelection();
35
38
  }
@@ -37,8 +40,9 @@ function escape(deps, active, typing, inTerminal) {
37
40
  function shortcut(deps, event) {
38
41
  const key = event.key.toLowerCase();
39
42
  if (key === "/") { event.preventDefault(); deps.el.search.focus(); deps.el.search.select(); }
43
+ else if (key === "[") deps.sidebarNav.toggle();
40
44
  else if (key === "r") deps.catalog.refresh();
41
- else if (key === "o") deps.workspace.openOperator();
45
+ else if (key === "o") deps.workspace.toggleOperator();
42
46
  else if (key === "d") deps.workspace.toggleDock();
43
47
  else if (/^[1-9]$/.test(key) && Number(key) <= TABS.length && deps.state.detail) deps.taskView.switchTab(TABS[Number(key) - 1]);
44
48
  }
@@ -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
- const aside = window.innerWidth > G.narrow ? el.sidebar.offsetWidth : 0;
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() {