@k2b/ui 0.28.0 → 0.30.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 (278) hide show
  1. package/README.md +1 -1
  2. package/dist/base.css +2 -2
  3. package/dist/browser/actions/ContextMenu.js +5 -0
  4. package/dist/browser/actions/ContextMenu.js.map +1 -1
  5. package/dist/browser/actions/DragAndDrop.js +89 -0
  6. package/dist/browser/actions/DragAndDrop.js.map +1 -0
  7. package/dist/browser/actions/Dropdown.js +6 -3
  8. package/dist/browser/actions/Dropdown.js.map +1 -1
  9. package/dist/browser/actions/FilterChip.js +137 -7
  10. package/dist/browser/actions/FilterChip.js.map +1 -1
  11. package/dist/browser/actions/GestureMenu.js +26 -3
  12. package/dist/browser/actions/GestureMenu.js.map +1 -1
  13. package/dist/browser/actions/SegmentedControl.js +5 -9
  14. package/dist/browser/actions/SegmentedControl.js.map +1 -1
  15. package/dist/browser/actions/SpotlightSearch.js +6 -3
  16. package/dist/browser/actions/SpotlightSearch.js.map +1 -1
  17. package/dist/browser/actions/Tabs.js +5 -2
  18. package/dist/browser/actions/Tabs.js.map +1 -1
  19. package/dist/browser/actions/ViewTabs.js +294 -0
  20. package/dist/browser/actions/ViewTabs.js.map +1 -0
  21. package/dist/browser/actions/drag-and-drop-messages.js +75 -0
  22. package/dist/browser/actions/drag-and-drop-messages.js.map +1 -0
  23. package/dist/browser/actions/drag-and-drop.js +692 -0
  24. package/dist/browser/actions/drag-and-drop.js.map +1 -0
  25. package/dist/browser/actions/filter-messages.js +6 -2
  26. package/dist/browser/actions/filter-messages.js.map +1 -1
  27. package/dist/browser/actions/index.js +3 -1
  28. package/dist/browser/actions/index.js.map +1 -1
  29. package/dist/browser/chat/ChatComposer.js +1229 -605
  30. package/dist/browser/chat/ChatComposer.js.map +1 -1
  31. package/dist/browser/chat/ChatDecision.js +814 -0
  32. package/dist/browser/chat/ChatDecision.js.map +1 -0
  33. package/dist/browser/chat/ChatPrimitives.js +292 -153
  34. package/dist/browser/chat/ChatPrimitives.js.map +1 -1
  35. package/dist/browser/chat/ChatTimeline.js.map +1 -1
  36. package/dist/browser/chat/chat-messages.js +14 -0
  37. package/dist/browser/chat/chat-messages.js.map +1 -1
  38. package/dist/browser/chat/command-search.js +35 -17
  39. package/dist/browser/chat/command-search.js.map +1 -1
  40. package/dist/browser/chat/index.js +10 -3
  41. package/dist/browser/chat/index.js.map +1 -1
  42. package/dist/browser/chat/types.js.map +1 -1
  43. package/dist/browser/content/Calendar.js +146 -119
  44. package/dist/browser/content/Calendar.js.map +1 -1
  45. package/dist/browser/content/CalendarWeekView.js +291 -266
  46. package/dist/browser/content/CalendarWeekView.js.map +1 -1
  47. package/dist/browser/content/Chart.js +54 -71
  48. package/dist/browser/content/Chart.js.map +1 -1
  49. package/dist/browser/content/DataTable.js +224 -155
  50. package/dist/browser/content/DataTable.js.map +1 -1
  51. package/dist/browser/content/FileBrowser.js +2 -2
  52. package/dist/browser/content/FileBrowser.js.map +1 -1
  53. package/dist/browser/content/FileTree.js +42 -36
  54. package/dist/browser/content/FileTree.js.map +1 -1
  55. package/dist/browser/content/FileTypeIcon.js +72 -0
  56. package/dist/browser/content/FileTypeIcon.js.map +1 -0
  57. package/dist/browser/content/FileView.js +2 -2
  58. package/dist/browser/content/FileView.js.map +1 -1
  59. package/dist/browser/content/ReferenceChip.js +162 -0
  60. package/dist/browser/content/ReferenceChip.js.map +1 -0
  61. package/dist/browser/content/calendar-pointer.js +9 -0
  62. package/dist/browser/content/calendar-pointer.js.map +1 -1
  63. package/dist/browser/content/calendar-time-view.js +6 -3
  64. package/dist/browser/content/calendar-time-view.js.map +1 -1
  65. package/dist/browser/content/data-table-messages.js +4 -2
  66. package/dist/browser/content/data-table-messages.js.map +1 -1
  67. package/dist/browser/content/index.js +2 -0
  68. package/dist/browser/content/index.js.map +1 -1
  69. package/dist/browser/feedback/MovedHint.js +197 -0
  70. package/dist/browser/feedback/MovedHint.js.map +1 -0
  71. package/dist/browser/feedback/dialog-core.js +66 -9
  72. package/dist/browser/feedback/dialog-core.js.map +1 -1
  73. package/dist/browser/feedback/index.js +1 -0
  74. package/dist/browser/feedback/index.js.map +1 -1
  75. package/dist/browser/feedback/prompts.js +6 -1
  76. package/dist/browser/feedback/prompts.js.map +1 -1
  77. package/dist/browser/inputs/DatePicker.js +337 -212
  78. package/dist/browser/inputs/DatePicker.js.map +1 -1
  79. package/dist/browser/inputs/date-picker.js +59 -5
  80. package/dist/browser/inputs/date-picker.js.map +1 -1
  81. package/dist/browser/intl/messages.js +12 -0
  82. package/dist/browser/intl/messages.js.map +1 -1
  83. package/dist/browser/intl/weekend.js +39 -0
  84. package/dist/browser/intl/weekend.js.map +1 -0
  85. package/dist/browser/layout/AppOverview.js +1 -2
  86. package/dist/browser/layout/AppOverview.js.map +1 -1
  87. package/dist/browser/layout/AppWorkspace.js +947 -531
  88. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  89. package/dist/browser/layout/PageTitle.js +3 -2
  90. package/dist/browser/layout/PageTitle.js.map +1 -1
  91. package/dist/browser/layout/PanelDialog.js +88 -86
  92. package/dist/browser/layout/PanelDialog.js.map +1 -1
  93. package/dist/browser/layout/ResizeSeparator.js +81 -0
  94. package/dist/browser/layout/ResizeSeparator.js.map +1 -0
  95. package/dist/browser/layout/SettingsModal.js +2 -2
  96. package/dist/browser/layout/SettingsModal.js.map +1 -1
  97. package/dist/browser/layout/SidebarSwitcher.js +19 -11
  98. package/dist/browser/layout/SidebarSwitcher.js.map +1 -1
  99. package/dist/browser/layout/TabBar.js +16 -13
  100. package/dist/browser/layout/TabBar.js.map +1 -1
  101. package/dist/browser/layout/app-host-width.js +44 -0
  102. package/dist/browser/layout/app-host-width.js.map +1 -0
  103. package/dist/browser/layout/app-workspace-controller.js +337 -44
  104. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  105. package/dist/browser/layout/index.js +4 -1
  106. package/dist/browser/layout/index.js.map +1 -1
  107. package/dist/browser/layout/resize-separator.js +90 -0
  108. package/dist/browser/layout/resize-separator.js.map +1 -0
  109. package/dist/browser/layout/workspace-messages.js +12 -2
  110. package/dist/browser/layout/workspace-messages.js.map +1 -1
  111. package/dist/browser/surfaces/LinkCard.js +1 -1
  112. package/dist/browser/surfaces/LinkCard.js.map +1 -1
  113. package/dist/browser/surfaces/Placeholder.js +45 -11
  114. package/dist/browser/surfaces/Placeholder.js.map +1 -1
  115. package/dist/browser/surfaces/StatCell.js +23 -0
  116. package/dist/browser/surfaces/StatCell.js.map +1 -1
  117. package/dist/browser/widgets/Widget.js +1 -1
  118. package/dist/browser/widgets/Widget.js.map +1 -1
  119. package/dist/ssr/actions/ContextMenu.js +5 -0
  120. package/dist/ssr/actions/ContextMenu.js.map +1 -1
  121. package/dist/ssr/actions/DragAndDrop.js +63 -0
  122. package/dist/ssr/actions/DragAndDrop.js.map +1 -0
  123. package/dist/ssr/actions/Dropdown.js +5 -2
  124. package/dist/ssr/actions/Dropdown.js.map +1 -1
  125. package/dist/ssr/actions/FilterChip.js +59 -7
  126. package/dist/ssr/actions/FilterChip.js.map +1 -1
  127. package/dist/ssr/actions/GestureMenu.js +26 -3
  128. package/dist/ssr/actions/GestureMenu.js.map +1 -1
  129. package/dist/ssr/actions/SegmentedControl.js +1 -2
  130. package/dist/ssr/actions/SegmentedControl.js.map +1 -1
  131. package/dist/ssr/actions/SpotlightSearch.js +2 -2
  132. package/dist/ssr/actions/SpotlightSearch.js.map +1 -1
  133. package/dist/ssr/actions/Tabs.js +2 -2
  134. package/dist/ssr/actions/Tabs.js.map +1 -1
  135. package/dist/ssr/actions/ViewTabs.js +211 -0
  136. package/dist/ssr/actions/ViewTabs.js.map +1 -0
  137. package/dist/ssr/actions/drag-and-drop-messages.js +75 -0
  138. package/dist/ssr/actions/drag-and-drop-messages.js.map +1 -0
  139. package/dist/ssr/actions/drag-and-drop.js +692 -0
  140. package/dist/ssr/actions/drag-and-drop.js.map +1 -0
  141. package/dist/ssr/actions/filter-messages.js +6 -2
  142. package/dist/ssr/actions/filter-messages.js.map +1 -1
  143. package/dist/ssr/actions/index.js +3 -1
  144. package/dist/ssr/actions/index.js.map +1 -1
  145. package/dist/ssr/chat/ChatComposer.js +668 -136
  146. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  147. package/dist/ssr/chat/ChatDecision.js +479 -0
  148. package/dist/ssr/chat/ChatDecision.js.map +1 -0
  149. package/dist/ssr/chat/ChatPrimitives.js +95 -24
  150. package/dist/ssr/chat/ChatPrimitives.js.map +1 -1
  151. package/dist/ssr/chat/ChatTimeline.js.map +1 -1
  152. package/dist/ssr/chat/chat-messages.js +14 -0
  153. package/dist/ssr/chat/chat-messages.js.map +1 -1
  154. package/dist/ssr/chat/command-search.js +35 -17
  155. package/dist/ssr/chat/command-search.js.map +1 -1
  156. package/dist/ssr/chat/index.js +10 -3
  157. package/dist/ssr/chat/index.js.map +1 -1
  158. package/dist/ssr/chat/types.js.map +1 -1
  159. package/dist/ssr/content/Calendar.js +112 -103
  160. package/dist/ssr/content/Calendar.js.map +1 -1
  161. package/dist/ssr/content/CalendarWeekView.js +42 -28
  162. package/dist/ssr/content/CalendarWeekView.js.map +1 -1
  163. package/dist/ssr/content/Chart.js +19 -23
  164. package/dist/ssr/content/Chart.js.map +1 -1
  165. package/dist/ssr/content/DataTable.js +78 -31
  166. package/dist/ssr/content/DataTable.js.map +1 -1
  167. package/dist/ssr/content/FileBrowser.js +2 -2
  168. package/dist/ssr/content/FileBrowser.js.map +1 -1
  169. package/dist/ssr/content/FileTree.js +21 -14
  170. package/dist/ssr/content/FileTree.js.map +1 -1
  171. package/dist/ssr/content/FileTypeIcon.js +50 -0
  172. package/dist/ssr/content/FileTypeIcon.js.map +1 -0
  173. package/dist/ssr/content/FileView.js +2 -2
  174. package/dist/ssr/content/FileView.js.map +1 -1
  175. package/dist/ssr/content/ReferenceChip.js +121 -0
  176. package/dist/ssr/content/ReferenceChip.js.map +1 -0
  177. package/dist/ssr/content/calendar-pointer.js +9 -0
  178. package/dist/ssr/content/calendar-pointer.js.map +1 -1
  179. package/dist/ssr/content/calendar-time-view.js +6 -3
  180. package/dist/ssr/content/calendar-time-view.js.map +1 -1
  181. package/dist/ssr/content/data-table-messages.js +4 -2
  182. package/dist/ssr/content/data-table-messages.js.map +1 -1
  183. package/dist/ssr/content/index.js +2 -0
  184. package/dist/ssr/content/index.js.map +1 -1
  185. package/dist/ssr/feedback/MovedHint.js +166 -0
  186. package/dist/ssr/feedback/MovedHint.js.map +1 -0
  187. package/dist/ssr/feedback/dialog-core.js +66 -9
  188. package/dist/ssr/feedback/dialog-core.js.map +1 -1
  189. package/dist/ssr/feedback/index.js +1 -0
  190. package/dist/ssr/feedback/index.js.map +1 -1
  191. package/dist/ssr/feedback/prompts.js +6 -1
  192. package/dist/ssr/feedback/prompts.js.map +1 -1
  193. package/dist/ssr/inputs/DatePicker.js +138 -69
  194. package/dist/ssr/inputs/DatePicker.js.map +1 -1
  195. package/dist/ssr/inputs/date-picker.js +59 -5
  196. package/dist/ssr/inputs/date-picker.js.map +1 -1
  197. package/dist/ssr/intl/messages.js +12 -0
  198. package/dist/ssr/intl/messages.js.map +1 -1
  199. package/dist/ssr/intl/weekend.js +39 -0
  200. package/dist/ssr/intl/weekend.js.map +1 -0
  201. package/dist/ssr/layout/AppOverview.js +1 -2
  202. package/dist/ssr/layout/AppOverview.js.map +1 -1
  203. package/dist/ssr/layout/AppWorkspace.js +457 -116
  204. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  205. package/dist/ssr/layout/PageTitle.js +3 -2
  206. package/dist/ssr/layout/PageTitle.js.map +1 -1
  207. package/dist/ssr/layout/PanelDialog.js +7 -6
  208. package/dist/ssr/layout/PanelDialog.js.map +1 -1
  209. package/dist/ssr/layout/ResizeSeparator.js +45 -0
  210. package/dist/ssr/layout/ResizeSeparator.js.map +1 -0
  211. package/dist/ssr/layout/SettingsModal.js +2 -2
  212. package/dist/ssr/layout/SettingsModal.js.map +1 -1
  213. package/dist/ssr/layout/SidebarSwitcher.js +19 -11
  214. package/dist/ssr/layout/SidebarSwitcher.js.map +1 -1
  215. package/dist/ssr/layout/TabBar.js +2 -2
  216. package/dist/ssr/layout/TabBar.js.map +1 -1
  217. package/dist/ssr/layout/app-host-width.js +44 -0
  218. package/dist/ssr/layout/app-host-width.js.map +1 -0
  219. package/dist/ssr/layout/app-workspace-controller.js +337 -44
  220. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  221. package/dist/ssr/layout/index.js +4 -1
  222. package/dist/ssr/layout/index.js.map +1 -1
  223. package/dist/ssr/layout/resize-separator.js +90 -0
  224. package/dist/ssr/layout/resize-separator.js.map +1 -0
  225. package/dist/ssr/layout/workspace-messages.js +12 -2
  226. package/dist/ssr/layout/workspace-messages.js.map +1 -1
  227. package/dist/ssr/surfaces/LinkCard.js +1 -1
  228. package/dist/ssr/surfaces/LinkCard.js.map +1 -1
  229. package/dist/ssr/surfaces/Placeholder.js +25 -6
  230. package/dist/ssr/surfaces/Placeholder.js.map +1 -1
  231. package/dist/ssr/surfaces/StatCell.js +23 -0
  232. package/dist/ssr/surfaces/StatCell.js.map +1 -1
  233. package/dist/ssr/widgets/Widget.js +1 -1
  234. package/dist/ssr/widgets/Widget.js.map +1 -1
  235. package/dist/styles.css +1 -1
  236. package/dist/types/actions/ContextMenu.d.ts +1 -0
  237. package/dist/types/actions/DragAndDrop.d.ts +29 -0
  238. package/dist/types/actions/Dropdown.d.ts +4 -0
  239. package/dist/types/actions/FilterChip.d.ts +22 -0
  240. package/dist/types/actions/ViewTabs.d.ts +30 -0
  241. package/dist/types/actions/drag-and-drop-messages.d.ts +35 -0
  242. package/dist/types/actions/drag-and-drop.d.ts +36 -0
  243. package/dist/types/actions/filter-messages.d.ts +2 -0
  244. package/dist/types/actions/index.d.ts +6 -2
  245. package/dist/types/chat/ChatComposer.d.ts +71 -1
  246. package/dist/types/chat/ChatDecision.d.ts +149 -0
  247. package/dist/types/chat/ChatPrimitives.d.ts +14 -1
  248. package/dist/types/chat/ChatTimeline.d.ts +2 -1
  249. package/dist/types/chat/chat-messages.d.ts +7 -0
  250. package/dist/types/chat/command-search.d.ts +5 -0
  251. package/dist/types/chat/index.d.ts +28 -5
  252. package/dist/types/chat/types.d.ts +54 -1
  253. package/dist/types/content/Chart.d.ts +9 -3
  254. package/dist/types/content/DataTable.d.ts +3 -0
  255. package/dist/types/content/FileTypeIcon.d.ts +27 -0
  256. package/dist/types/content/ReferenceChip.d.ts +49 -0
  257. package/dist/types/content/calendar-pointer.d.ts +6 -0
  258. package/dist/types/content/data-table-messages.d.ts +1 -0
  259. package/dist/types/content/index.d.ts +3 -0
  260. package/dist/types/feedback/MovedHint.d.ts +25 -0
  261. package/dist/types/feedback/dialog-core.d.ts +5 -2
  262. package/dist/types/feedback/index.d.ts +2 -0
  263. package/dist/types/feedback/prompts.d.ts +2 -0
  264. package/dist/types/inputs/DatePicker.d.ts +7 -0
  265. package/dist/types/inputs/date-picker.d.ts +25 -1
  266. package/dist/types/intl/messages.d.ts +6 -0
  267. package/dist/types/intl/weekend.d.ts +12 -0
  268. package/dist/types/layout/AppWorkspace.d.ts +48 -10
  269. package/dist/types/layout/ResizeSeparator.d.ts +9 -0
  270. package/dist/types/layout/TabBar.d.ts +7 -0
  271. package/dist/types/layout/app-host-width.d.ts +6 -0
  272. package/dist/types/layout/app-workspace-controller.d.ts +2 -0
  273. package/dist/types/layout/index.d.ts +5 -2
  274. package/dist/types/layout/resize-separator.d.ts +18 -0
  275. package/dist/types/layout/workspace-messages.d.ts +5 -0
  276. package/dist/types/surfaces/Placeholder.d.ts +2 -1
  277. package/dist/types/surfaces/StatCell.d.ts +6 -0
  278. package/package.json +5 -5
@@ -4,14 +4,46 @@ import { APP_WORKSPACE_DETAIL_DEFAULT, APP_WORKSPACE_DETAIL_MAX, APP_WORKSPACE_D
4
4
  import { installScrollFades } from "./scroll-fade";
5
5
  const HANDLE_SELECTOR = "[data-app-workspace-resize]";
6
6
  const ROOT_SELECTOR = "[data-k2b-app-workspace]";
7
- const LAYOUT_REGION_SELECTOR = [ROOT_SELECTOR, ".k2b-app-workspace__sidebar", ".k2b-app-workspace__main-pane", ".k2b-app-workspace__detail", ".k2b-app-workspace__drawer", HANDLE_SELECTOR].join(",");
7
+ const LAYOUT_REGION_SELECTOR = [ROOT_SELECTOR, ".k2b-app-workspace__sidebar", ".k2b-app-workspace__main", ".k2b-app-workspace__main-pane", ".k2b-app-workspace__detail", ".k2b-app-workspace__drawer", HANDLE_SELECTOR].join(",");
8
8
  const TOGGLE_SELECTOR = "[data-app-workspace-sidebar-toggle]";
9
- /** The width range in which the sidebar is an overlay; it matches the compact media query in the stylesheet. */
10
- const COMPACT_QUERY = "(min-width: 48rem) and (max-width: 63.999rem)";
9
+ const DETAIL_TOGGLE_SELECTOR = "[data-app-workspace-detail-toggle]";
10
+ const DETAIL_DRAWER_SELECTOR = '.k2b-app-workspace__detail[data-workspace-detail-drawer="open"]';
11
+ const cssState = (element, name) => getComputedStyle(element).getPropertyValue(name).trim();
12
+ const detailWaiting = aside => cssState(aside, "--k2b-workspace-detail-waiting") === "1";
13
+ const detailDrawer = aside => aside.dataset.workspaceDetailDrawer === "open";
14
+ const detailToggle = aside => {
15
+ const root = workspaceRoot(aside);
16
+ return rootElements(root ?? aside, DETAIL_TOGGLE_SELECTOR).find(toggle => toggle.getAttribute("aria-controls") === aside.id);
17
+ };
18
+ // The trail's registered property accepts a length, not the drawer's percentage
19
+ // cap. Only a user-opened drawer needs this measurement; the first frame stays CSS-owned.
20
+ const detailDrawerContent = aside => aside.closest(".k2b-app-workspace__content");
21
+ const syncDetailDrawerSpace = aside => detailDrawerContent(aside)?.style.setProperty("--k2b-detail-space", `${aside.getBoundingClientRect().width}px`);
22
+ const parkDetailDrawer = (aside, focus = false) => {
23
+ if (!detailDrawer(aside)) return;
24
+ delete aside.dataset.workspaceDetailDrawer;
25
+ detailDrawerContent(aside)?.style.removeProperty("--k2b-detail-space");
26
+ const toggle = detailToggle(aside);
27
+ toggle?.setAttribute("aria-expanded", "false");
28
+ if (focus) toggle?.focus({
29
+ preventScroll: true
30
+ });
31
+ };
32
+
33
+ /** Parks open waiting-detail drawers; focus follows only when it was inside one. */
34
+ export const parkWorkspaceDetailDrawers = (root = document) => {
35
+ for (const aside of root.querySelectorAll(DETAIL_DRAWER_SELECTOR)) parkDetailDrawer(aside, aside.contains(aside.ownerDocument.activeElement));
36
+ };
11
37
  const FOCUSABLE_SELECTOR = ["a[href]", "button:not([disabled])", "input:not([disabled]):not([type='hidden'])", "select:not([disabled])", "textarea:not([disabled])", "[tabindex]:not([tabindex='-1'])"].join(",");
12
38
  /** Text entry, menus, dialogs, and open popups keep their own keys. */
13
39
  const KEY_OWNER_SELECTOR = "input, textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox'], [role='combobox'], [role='searchbox'], [role='spinbutton'], [role='menu'], [role='listbox'], [role='dialog'], dialog, [aria-haspopup]:not([aria-haspopup='false'])[aria-expanded='true']";
14
40
  const ITEM_SELECTOR = ".k2b-app-workspace__sidebar-item";
41
+ /** Controls inside a selected row that act on their own instead of choosing the row. */
42
+ const NESTED_CONTROL_SELECTOR = "[aria-expanded], [aria-haspopup]:not([aria-haspopup='false']), [aria-pressed], [role='checkbox'], [role='switch'], [role='menuitem'], input, select, textarea, summary";
43
+ const DETAIL_REQUEST_CONTROL_SELECTOR = "[aria-pressed], [aria-expanded], [aria-controls]";
44
+ const SELECTION_MARK_SELECTOR = '[aria-selected="true"], [aria-current]:not([aria-current="false"]):not([aria-current="date"]):not([aria-current="time"]), [data-selected="true"]';
45
+ // Calendar day marks select a date range, not the Detail's subject.
46
+ const IGNORED_SELECTION_MARK_SELECTOR = "[data-calendar-day-key]";
15
47
  const clamp = (value, min, max) => Math.round(Math.min(max, Math.max(min, value)));
16
48
  const eventElement = event => event.target instanceof Element ? event.target : null;
17
49
  const resizeHandle = event => eventElement(event)?.closest(HANDLE_SELECTOR) ?? null;
@@ -24,6 +56,7 @@ const workspaceRoot = handle => handle.closest(ROOT_SELECTOR);
24
56
  const workspaceResizable = root => root.dataset.workspaceResizable !== "false";
25
57
  const rootElements = (root, selector) => Array.from(root.querySelectorAll(selector)).filter(element => element.closest(ROOT_SELECTOR) === root);
26
58
  const sidebarElement = root => rootElements(root, ".k2b-app-workspace__sidebar")[0] ?? null;
59
+ const sidebarForm = root => cssState(sidebarElement(root) ?? root, "--k2b-workspace-sidebar-form");
27
60
  const mainElement = root => rootElements(root, ".k2b-app-workspace__main")[0] ?? null;
28
61
  const sidebarCollapsible = root => sidebarElement(root)?.dataset.workspaceCollapsible === "true";
29
62
  const sidebarHideable = root => sidebarElement(root)?.dataset.workspaceHideable === "true";
@@ -59,8 +92,9 @@ const controlledPanel = (root, handle) => {
59
92
  };
60
93
  const flexibleLevel = pane => {
61
94
  if (!pane.hasAttribute("data-workspace-level")) return false;
62
- const widthClass = window.matchMedia("(min-width: 80rem)").matches ? "3" : window.matchMedia("(min-width: 48rem)").matches ? "2" : "1";
63
- return pane.dataset.workspaceLevelLast?.split(" ").includes(widthClass) ?? false;
95
+ const root = workspaceRoot(pane);
96
+ const widthClass = root ? cssState(root, "--k2b-workspace-level-class") : "";
97
+ return Boolean(widthClass && pane.dataset.workspaceLevelLast?.split(" ").includes(widthClass));
64
98
  };
65
99
  const levelsMain = handle => handle.closest(".k2b-app-workspace__main")?.hasAttribute("data-workspace-levels") ?? false;
66
100
  const controlsResizableRegion = (root, handle, kind) => {
@@ -72,7 +106,7 @@ const numberData = (handle, key, fallback) => {
72
106
  const value = Number(handle.dataset[key]);
73
107
  return Number.isFinite(value) ? value : fallback;
74
108
  };
75
- const sizeLimits = (root, handle, kind) => {
109
+ const sizeLimits = (root, handle, kind, reserveMainMinimum = true) => {
76
110
  const controlled = controlledPanel(root, handle);
77
111
  const main = kind === "pane" ? handle.closest(".k2b-app-workspace__main") : mainElement(root);
78
112
  const panes = main ? Array.from(main.querySelectorAll(".k2b-app-workspace__main-pane")).filter(pane => pane.closest(".k2b-app-workspace__main") === main && !pane.classList.contains("is-primary") && !flexibleLevel(pane) && isVisible(pane)) : [];
@@ -87,14 +121,23 @@ const sizeLimits = (root, handle, kind) => {
87
121
  const content = kind === "detail" ? controlled?.closest(".k2b-app-workspace__content") : null;
88
122
  const defaultMin = kind === "sidebar" ? APP_WORKSPACE_SIDEBAR_MIN : kind === "pane" ? APP_WORKSPACE_PANE_MIN : kind === "detail" ? APP_WORKSPACE_DETAIL_MIN : APP_WORKSPACE_DRAWER_MIN;
89
123
  const defaultMax = kind === "sidebar" ? APP_WORKSPACE_SIDEBAR_MAX : kind === "pane" ? APP_WORKSPACE_PANE_MAX : kind === "detail" ? APP_WORKSPACE_DETAIL_MAX : APP_WORKSPACE_DRAWER_MAX;
90
- return appWorkspaceResizeLimits({
124
+ const mainMinValue = cssState(root, "--k2b-workspace-main-min");
125
+ const mainMin = mainMinValue.endsWith("px") ? Number.parseFloat(mainMinValue) : APP_WORKSPACE_MAIN_MIN;
126
+ const mainExtra = Math.max(0, (Number.isFinite(mainMin) ? mainMin : APP_WORKSPACE_MAIN_MIN) - APP_WORKSPACE_MAIN_MIN);
127
+ const limits = appWorkspaceResizeLimits({
91
128
  kind,
92
- workspaceSize: kind === "drawer" ? root.getBoundingClientRect().height : kind === "pane" && main ? main.getBoundingClientRect().width : content ? root.getBoundingClientRect().right - root.clientLeft - content.getBoundingClientRect().left : root.getBoundingClientRect().width,
93
- reservedSize: kind === "detail" ? 8 : kind === "pane" ? panes.reduce((total, pane) => total + (pane === controlled ? 0 : elementSize(pane, "pane")), 0) + separatorSize : 0,
129
+ workspaceSize: kind === "drawer" ? root.getBoundingClientRect().height : kind === "pane" && main ? main.getBoundingClientRect().width : content ? root.getBoundingClientRect().right - root.clientLeft - content.getBoundingClientRect().left : kind === "sidebar" ? root.getBoundingClientRect().width - 2 * root.clientLeft : root.getBoundingClientRect().width,
130
+ // Interactive sizing keeps Main's declared minimum beside the sidebar and the Detail.
131
+ reservedSize: kind === "sidebar" && reserveMainMinimum ? mainExtra : kind === "detail" ? 8 + (reserveMainMinimum ? mainExtra : 0) : kind === "pane" ? panes.reduce((total, pane) => total + (pane === controlled ? 0 : elementSize(pane, "pane")), 0) + separatorSize : 0,
94
132
  min: numberData(handle, "workspaceMinSize", defaultMin),
95
133
  max: numberData(handle, "workspaceMaxSize", defaultMax),
96
134
  sidebarCollapsible: kind === "sidebar" && sidebarCollapsible(root)
97
135
  });
136
+ // Widths are applied as whole pixels; rounding up must not cross the fit boundary.
137
+ return kind === "sidebar" ? {
138
+ min: limits.min,
139
+ max: Math.max(limits.min, Math.floor(limits.max))
140
+ } : limits;
98
141
  };
99
142
  const currentSize = (root, handle, kind) => kind === "sidebar" ? elementSize(sidebarElement(root), kind) : elementSize(controlledPanel(root, handle), kind);
100
143
  const updateHandleValue = (root, handle, kind, size, limits = sizeLimits(root, handle, kind)) => {
@@ -106,7 +149,7 @@ const updateHandleValue = (root, handle, kind, size, limits = sizeLimits(root, h
106
149
  handle.setAttribute("aria-valuemax", String(max));
107
150
  handle.setAttribute("aria-valuenow", String(clamp(size, min, max)));
108
151
  };
109
- const applySize = (root, handle, kind, requestedSize,
152
+ const applyRegionSize = (root, handle, kind, requestedSize,
110
153
  /**
111
154
  * `snapSidebar: false` tracks the pointer continuously and only *previews*
112
155
  * the collapsed state. Snapping (the default, and what keyboard steps and
@@ -117,7 +160,7 @@ options = {}) => {
117
160
  const {
118
161
  min,
119
162
  max
120
- } = sizeLimits(root, handle, kind);
163
+ } = sizeLimits(root, handle, kind, options.reserveMainMinimum);
121
164
  if (kind === "sidebar") {
122
165
  const collapsible = sidebarCollapsible(root);
123
166
  const sidebar = options.snapSidebar === false ? {
@@ -128,10 +171,8 @@ options = {}) => {
128
171
  // Cloud removes the attribute rather than writing "false", so that a
129
172
  // presence selector (`[data-sidebar-collapsed]`) means "collapsed".
130
173
  if (sidebar.collapsed) root.dataset.sidebarCollapsed = "true";else delete root.dataset.sidebarCollapsed;
131
- updateHandleValue(root, handle, kind, sidebar.width, {
132
- min,
133
- max
134
- });
174
+ // A restored width may require an overlay; the handle still advertises interactive limits.
175
+ updateHandleValue(root, handle, kind, sidebar.width);
135
176
  return sidebar.width;
136
177
  }
137
178
  const size = clamp(requestedSize, min, max);
@@ -200,7 +241,7 @@ const claimedToggleClicks = new WeakSet();
200
241
  const claimedDetailGestures = new WeakSet();
201
242
 
202
243
  /**
203
- * The compact overlay: from 48 to 64 rem the sidebar opens over the work area
244
+ * The overlay sidebar opens over the work area when its column does not fit
204
245
  * from the button in the workspace's head. While it is open the rest of
205
246
  * the workspace is inert, Tab stays in the sidebar, and Escape, the scrim,
206
247
  * choosing an item, hiding the sidebar, or leaving the width range close it
@@ -228,7 +269,6 @@ export const installAppWorkspaceController = (options = {}) => {
228
269
  const ownerDocument = eventRoot.ownerDocument ?? document;
229
270
  const ownerWindow = ownerDocument.defaultView ?? window;
230
271
  let overlay = null;
231
- const compact = typeof window.matchMedia === "function" ? window.matchMedia(COMPACT_QUERY) : null;
232
272
  // A server-rendered head names the sidebar before the sidebar renders; the button learns its real id now.
233
273
  for (const toggle of eventRoot.querySelectorAll(TOGGLE_SELECTOR)) overlayTarget(toggle);
234
274
  const openOverlay = target => {
@@ -269,12 +309,26 @@ export const installAppWorkspaceController = (options = {}) => {
269
309
  };
270
310
  const onClick = event => {
271
311
  const target = eventElement(event);
312
+ const detailButton = target?.closest(DETAIL_TOGGLE_SELECTOR);
313
+ if (detailButton) {
314
+ const root = workspaceRoot(detailButton);
315
+ const aside = root ? controlledPanel(root, detailButton) : null;
316
+ if (!aside || aside.hidden || !detailWaiting(aside) || claimedToggleClicks.has(event)) return;
317
+ claimedToggleClicks.add(event);
318
+ if (detailDrawer(aside)) parkDetailDrawer(aside);else {
319
+ openDetailDrawer(aside);
320
+ if (event.detail === 0) requestTitle(aside);else detailButton.focus({
321
+ preventScroll: true
322
+ });
323
+ }
324
+ return;
325
+ }
272
326
  const toggle = target?.closest(TOGGLE_SELECTOR);
273
327
  if (toggle) {
274
328
  const next = overlayTarget(toggle);
275
329
  if (!next || claimedToggleClicks.has(event)) return;
276
330
  claimedToggleClicks.add(event);
277
- if (overlay?.sidebar === next.sidebar) closeOverlay("button");else if (compact?.matches) openOverlay(next);
331
+ if (overlay?.sidebar === next.sidebar) closeOverlay("button");else if (sidebarForm(next.root) === "overlay") openOverlay(next);
278
332
  return;
279
333
  }
280
334
  if (!overlay || !target) return;
@@ -310,6 +364,11 @@ export const installAppWorkspaceController = (options = {}) => {
310
364
  // Dialogs and popovers keep their own Tab order.
311
365
  const page = phone?.matches ? allRoots().flatMap(openDetails).find(phonePage) : undefined;
312
366
  if (!page || ownerDocument.querySelector("dialog[open], [aria-modal='true']") || target.closest("[popover]")) return;
367
+ if (target !== ownerDocument.body) {
368
+ const rect = target.getBoundingClientRect();
369
+ const hit = ownerDocument.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
370
+ if (hit && target.contains(hit)) return;
371
+ }
313
372
  const stops = tabStopsIn(page);
314
373
  event.preventDefault();
315
374
  (event.shiftKey ? stops.at(-1) : stops[0])?.focus();
@@ -334,12 +393,57 @@ export const installAppWorkspaceController = (options = {}) => {
334
393
  first.focus();
335
394
  }
336
395
  };
337
- const onCompactChange = () => {
338
- if (!compact?.matches) closeOverlay("none");
396
+ // Whether each open detail waited at the last check, so a column that starts
397
+ // waiting under focus hands that focus to its Details button.
398
+ const detailWasWaiting = new WeakMap();
399
+ const openDetailDrawer = aside => {
400
+ aside.dataset.workspaceDetailDrawer = "open";
401
+ detailToggle(aside)?.setAttribute("aria-expanded", "true");
402
+ syncDetailDrawerSpace(aside);
403
+ };
404
+ // A sidebar drag changes the room beside it without resizing the workspace,
405
+ // so it rechecks waiting once per frame.
406
+ let hostSyncFrame = null;
407
+ const scheduleHostSync = () => {
408
+ if (hostSyncFrame !== null) return;
409
+ hostSyncFrame = requestAnimationFrame(() => {
410
+ hostSyncFrame = null;
411
+ syncHostState();
412
+ });
413
+ };
414
+ const applySize = (...args) => {
415
+ const size = applyRegionSize(...args);
416
+ if (args[2] === "sidebar") scheduleHostSync();
417
+ return size;
418
+ };
419
+ const syncHostState = () => {
420
+ if (pendingDetail && !canOpenPendingDetail(pendingDetail.aside)) cancelPendingDetail();
421
+ if (overlay && sidebarForm(overlay.root) !== "overlay") closeOverlay("none");
422
+ for (const root of allRoots()) {
423
+ // A drawer removed with its panel leaves no space behind in Content.
424
+ for (const content of rootElements(root, ".k2b-app-workspace__content")) if (content.style.getPropertyValue("--k2b-detail-space") && !content.querySelector(DETAIL_DRAWER_SELECTOR)) content.style.removeProperty("--k2b-detail-space");
425
+ for (const aside of rootElements(root, detailSelector)) {
426
+ if (aside.hidden) {
427
+ detailWasWaiting.delete(aside);
428
+ parkDetailDrawer(aside);
429
+ continue;
430
+ }
431
+ const waiting = detailWaiting(aside);
432
+ const before = detailWasWaiting.get(aside);
433
+ detailWasWaiting.set(aside, waiting);
434
+ if (!waiting) {
435
+ // The Details button hides with the waiting; its focus moves to the returning column's title.
436
+ const toggleFocused = detailToggle(aside)?.contains(ownerDocument.activeElement) ?? false;
437
+ parkDetailDrawer(aside);
438
+ if (toggleFocused) focusTitle(aside);
439
+ } else if (detailDrawer(aside)) syncDetailDrawerSpace(aside);else if (before !== true && aside.contains(ownerDocument.activeElement)) detailToggle(aside)?.focus({
440
+ preventScroll: true
441
+ });
442
+ }
443
+ }
339
444
  };
340
445
  const phone = typeof window.matchMedia === "function" ? window.matchMedia("(max-width: 47.999rem)") : null;
341
446
  const wide = typeof window.matchMedia === "function" ? window.matchMedia("(min-width: 48rem)") : null;
342
- const touchDrawer = typeof window.matchMedia === "function" ? window.matchMedia(`${COMPACT_QUERY} and (pointer: coarse)`) : null;
343
447
  const detailSelector = ".k2b-app-workspace__detail";
344
448
  const grabberSelector = ".k2b-app-workspace__detail-grabber";
345
449
  const detailHidden = new WeakMap();
@@ -373,6 +477,36 @@ export const installAppWorkspaceController = (options = {}) => {
373
477
  snapshot.top = snapshot.item.getBoundingClientRect().top;
374
478
  snapshot.scrollTop = snapshot.scroller?.scrollTop ?? 0;
375
479
  };
480
+ // Native Enter/Space clicks and pointer clicks continue their initiating event.
481
+ let activationItem = null;
482
+ let activationFrame = null;
483
+ const clearActivation = () => {
484
+ if (activationFrame !== null) cancelAnimationFrame(activationFrame);
485
+ activationFrame = null;
486
+ activationItem = null;
487
+ };
488
+ const endActivation = () => {
489
+ if (!activationItem) return;
490
+ // Space's native click follows keyup before the next frame.
491
+ if (activationFrame !== null) cancelAnimationFrame(activationFrame);
492
+ activationFrame = requestAnimationFrame(clearActivation);
493
+ };
494
+ const cancelActivation = () => {
495
+ // A cancelled gesture must not open a Detail that appears later.
496
+ if (lastChosen?.interaction === "pointer" && lastChosen.item === activationItem) {
497
+ lastChosen.chosen = false;
498
+ lastChosen = null;
499
+ }
500
+ clearActivation();
501
+ };
502
+ const onChoiceClick = event => {
503
+ const preceded = event.target instanceof Node && Boolean(activationItem?.contains(event.target));
504
+ clearActivation();
505
+ if (event.detail === 0 && !preceded) {
506
+ cancelTitleOnInteraction(event);
507
+ rememberInteraction(event);
508
+ } else if (!(event.target instanceof Node && lastChosen?.item.contains(event.target))) cancelTitleOnInteraction(event);
509
+ };
376
510
  // A table row can exceed its scroller's width, so keep the clicked cell for horizontal reveal.
377
511
  const rememberInteraction = event => {
378
512
  const target = eventElement(event);
@@ -381,24 +515,37 @@ export const installAppWorkspaceController = (options = {}) => {
381
515
  const previous = remembered.get(root);
382
516
  if (previous && openDetails(root).length) refreshItem(previous);
383
517
  const main = mainElement(root);
384
- const item = target.closest(FOCUSABLE_SELECTOR);
385
- if (!main || !item || !main.contains(item) || item.closest(ROOT_SELECTOR) !== root) return;
518
+ // Collection rows with a roving tab index are items too, though no tab stops.
519
+ const item = target.closest(`${FOCUSABLE_SELECTOR}, [tabindex]`);
520
+ if (!main || !item || item === main || !main.contains(item) || item.closest(ROOT_SELECTOR) !== root) return;
386
521
  if (event instanceof KeyboardEvent && event.key !== "Enter" && event.key !== " ") return;
522
+ if (event.type === "pointerdown" || event.type === "keydown") activationItem = item;
523
+ const assistive = event instanceof MouseEvent && event.type === "click" && event.detail === 0 && event.button === 0;
387
524
  const cell = target.closest("td, th");
388
525
  const snapshot = {
389
526
  item,
390
527
  cell: cell && main.contains(cell) && cell.closest(ROOT_SELECTOR) === root ? cell : null,
391
- interaction: event instanceof KeyboardEvent ? "keyboard" : "pointer",
528
+ interaction: event instanceof KeyboardEvent || assistive ? "keyboard" : "pointer",
392
529
  scroller: verticalScroller(item, main),
393
530
  scrollTop: 0,
394
531
  top: 0,
395
- fresh: true
532
+ fresh: true,
533
+ // Text entry and focus alone do not choose an item.
534
+ // A modified activation, such as Ctrl/Cmd-click opening a new tab, chooses nothing here.
535
+ chosen: (event instanceof PointerEvent && event.type === "pointerdown" && event.button === 0 || event instanceof KeyboardEvent || assistive) && !(event instanceof MouseEvent || event instanceof KeyboardEvent ? event.ctrlKey || event.metaKey || event.shiftKey || event.altKey : false) &&
536
+ // Space scrolls on native links; it does not activate them.
537
+ !(event instanceof KeyboardEvent && event.key === " " && item.matches("a[href], area[href]") && (!item.hasAttribute("role") || item.getAttribute("role") === "link")) && !target.closest(KEY_OWNER_SELECTOR) && !item.closest("[role='tab'], [role='tablist']")
396
538
  };
397
539
  refreshItem(snapshot);
398
540
  remembered.set(root, snapshot);
541
+ if (snapshot.chosen) lastChosen = snapshot;
542
+ if (snapshot.chosen) {
543
+ const aside = openDetails(root).find(canOpenPendingDetail);
544
+ if (aside) checkPendingDetail(root, aside, snapshot, event instanceof KeyboardEvent && event.key === " ");
545
+ }
399
546
  // Enter in an editor or a field types; only an item that chooses something moves on to its title.
400
- if (event instanceof KeyboardEvent && !target.closest(KEY_OWNER_SELECTOR)) {
401
- const aside = openDetails(root).find(isVisible);
547
+ if (snapshot.chosen && snapshot.interaction === "keyboard") {
548
+ const aside = openDetails(root).find(aside => isVisible(aside) && (!detailWaiting(aside) || detailDrawer(aside)));
402
549
  if (aside) requestTitle(aside, true);
403
550
  }
404
551
  };
@@ -449,7 +596,18 @@ export const installAppWorkspaceController = (options = {}) => {
449
596
  // content, so focus falls to the body. The panel keeps owning the steps until
450
597
  // focus or a pointer goes elsewhere, so a quick second J still ends on the title.
451
598
  let panelStep = null;
599
+ // The last choice in Main, until another interaction replaces it: a Detail
600
+ // that opens later than that does not take the drawer or the focus.
601
+ let lastChosen = null;
452
602
  const cancelTitleOnInteraction = event => {
603
+ if (event.type === "pointerdown" || event.type === "keydown") clearActivation();
604
+ if (lastChosen && !(event.target instanceof Node && lastChosen.item.contains(event.target))) {
605
+ lastChosen.chosen = false;
606
+ lastChosen = null;
607
+ }
608
+ if (pendingDetail &&
609
+ // Native pointer focus on the chosen item continues its activation.
610
+ !(event.type === "focusin" && event.target instanceof Node && pendingDetail.snapshot.item.contains(event.target))) cancelPendingDetail();
453
611
  if (event.type !== "keydown" && !(event.target instanceof Node && panelStep?.contains(event.target))) panelStep = null;
454
612
  if (event.type !== "focusin" || event.target !== focusingTitle) cancelTitle();
455
613
  };
@@ -499,14 +657,13 @@ export const installAppWorkspaceController = (options = {}) => {
499
657
  delete aside.dataset.workspaceDetailDragging;
500
658
  };
501
659
  const anchorItem = (root, aside, snapshot) => {
502
- if (!wide?.matches || touchDrawer?.matches && closable(aside) || !snapshot?.item.isConnected) return;
660
+ if (!wide?.matches || detailDrawer(aside) || !snapshot?.item.isConnected) return;
503
661
  if (!mainElement(root)?.contains(snapshot.item) || snapshot.item.closest(ROOT_SELECTOR) !== root) return;
504
662
  const scroller = snapshot.scroller;
505
663
  if (scroller?.isConnected && scroller.scrollTop === snapshot.scrollTop) scroller.scrollTop += snapshot.item.getBoundingClientRect().top - snapshot.top;
506
664
  };
507
- // From 64 rem the workspace does not clip, so the panel's shadow shows on
508
- // every edge. A panel sliding in or out would cross a framed workspace's
509
- // edge, so the workspace clips while the panel moves (see the stylesheet).
665
+ // A moving Detail would cross a framed workspace's edge, so the workspace
666
+ // clips while it slides (see the stylesheet).
510
667
  const slide = (root, aside) => {
511
668
  slides.get(root)?.();
512
669
  if (!root.hasAttribute("data-workspace-motion") || prefersReducedMotion() || !wide?.matches) return;
@@ -591,6 +748,82 @@ export const installAppWorkspaceController = (options = {}) => {
591
748
  }
592
749
  }));
593
750
  };
751
+ let pendingDetail = null;
752
+ const canOpenPendingDetail = aside => aside.isConnected && eventRoot.contains(aside) && !aside.hidden && detailWaiting(aside) && !detailDrawer(aside) && aside.dataset.workspaceDetailChoice !== "request";
753
+ const cancelPendingDetail = () => {
754
+ pendingDetail?.stop();
755
+ pendingDetail = null;
756
+ };
757
+ // Check a completed choice after two frames. If its selection mark arrives
758
+ // later, keep waiting until another interaction replaces the choice.
759
+ const checkPendingDetail = (root, aside, snapshot, space = false) => {
760
+ cancelPendingDetail();
761
+ let frame = null;
762
+ let observer = null;
763
+ const settle = () => {
764
+ ownerDocument.removeEventListener("click", onClick, true);
765
+ ownerDocument.removeEventListener("keyup", onKeyUp, true);
766
+ if (frame !== null) cancelAnimationFrame(frame);
767
+ frame = requestAnimationFrame(() => {
768
+ frame = requestAnimationFrame(() => {
769
+ frame = null;
770
+ const main = mainElement(root);
771
+ if (!canOpenPendingDetail(aside) || !snapshot.chosen || !snapshot.item.isConnected || !main?.contains(snapshot.item) || snapshot.item.closest(ROOT_SELECTOR) !== root) {
772
+ cancelPendingDetail();
773
+ return;
774
+ }
775
+ // ARIA marks, and `data-selected` as DataTable and Calendar mark their selection.
776
+ let selected = snapshot.item.closest(SELECTION_MARK_SELECTOR);
777
+ while (selected?.matches(IGNORED_SELECTION_MARK_SELECTOR)) selected = selected.parentElement?.closest(SELECTION_MARK_SELECTOR) ?? null;
778
+ if (!selected || selected === main || !main.contains(selected)) {
779
+ if (!observer) {
780
+ observer = new MutationObserver(settle);
781
+ observer.observe(main, {
782
+ subtree: true,
783
+ attributes: true,
784
+ attributeFilter: ["aria-selected", "aria-current", "data-selected"]
785
+ });
786
+ }
787
+ return;
788
+ }
789
+ cancelPendingDetail();
790
+ // A control inside a selected row that discloses, toggles, or opens
791
+ // a menu does its own job and selects nothing.
792
+ if (selected !== snapshot.item && snapshot.item.matches(NESTED_CONTROL_SELECTOR)) return;
793
+ snapshot.fresh = false;
794
+ openDetailDrawer(aside);
795
+ revealItem(root, aside, snapshot);
796
+ // The same item keeps its title; another one may still be loading,
797
+ // so focus follows the title that replaces the current one.
798
+ if (snapshot.interaction === "keyboard") {
799
+ focusTitle(aside);
800
+ requestTitle(aside, true);
801
+ }
802
+ });
803
+ });
804
+ };
805
+ const onClick = event => {
806
+ if (event.button === 0 && event.target instanceof Node && snapshot.item.contains(event.target)) settle();else cancelPendingDetail();
807
+ };
808
+ // Space activates a button on release, so its check waits for keyup.
809
+ const onKeyUp = event => {
810
+ if (event.key === " ") settle();
811
+ };
812
+ const stop = () => {
813
+ if (frame !== null) cancelAnimationFrame(frame);
814
+ observer?.disconnect();
815
+ ownerDocument.removeEventListener("click", onClick, true);
816
+ ownerDocument.removeEventListener("keyup", onKeyUp, true);
817
+ ownerDocument.removeEventListener("pointercancel", cancelPendingDetail, true);
818
+ };
819
+ pendingDetail = {
820
+ aside,
821
+ snapshot,
822
+ stop
823
+ };
824
+ ownerDocument.addEventListener("pointercancel", cancelPendingDetail, true);
825
+ if (space) ownerDocument.addEventListener("keyup", onKeyUp, true);else if (snapshot.interaction === "keyboard") settle();else ownerDocument.addEventListener("click", onClick, true);
826
+ };
594
827
  const detailTransition = (aside, wasHidden) => {
595
828
  const root = aside.closest(ROOT_SELECTOR);
596
829
  detailHidden.set(aside, Boolean(aside.hidden));
@@ -605,6 +838,8 @@ export const installAppWorkspaceController = (options = {}) => {
605
838
  const followedInteraction = snapshot?.fresh ?? false;
606
839
  if (snapshot) snapshot.fresh = false;
607
840
  if (aside.hidden) {
841
+ if (pendingDetail?.aside === aside) cancelPendingDetail();
842
+ parkDetailDrawer(aside);
608
843
  if (pendingTitle?.aside === aside) cancelTitle();
609
844
  if (panelStep === aside) panelStep = null;
610
845
  if (root.hasAttribute("data-workspace-motion") && !prefersReducedMotion() && wide?.matches) {
@@ -633,6 +868,11 @@ export const installAppWorkspaceController = (options = {}) => {
633
868
  if (detailDrag?.aside === aside) finishDetailDrag(true);
634
869
  return;
635
870
  }
871
+ if (detailWaiting(aside) && !detailDrawer(aside)) {
872
+ if (!followedInteraction || !snapshot?.chosen) return;
873
+ if (aside.dataset.workspaceDetailChoice === "request" && !snapshot.item.matches(DETAIL_REQUEST_CONTROL_SELECTOR)) return;
874
+ openDetailDrawer(aside);
875
+ }
636
876
  anchorItem(root, aside, snapshot);
637
877
  if (phone?.matches) setDetent(aside, false);
638
878
  if (followedInteraction && snapshot) {
@@ -646,20 +886,26 @@ export const installAppWorkspaceController = (options = {}) => {
646
886
  if (!target || target.closest(KEY_OWNER_SELECTOR)) return;
647
887
  if (ownerDocument.querySelector("dialog[open], [aria-modal='true']")) return;
648
888
  try {
649
- if (ownerDocument.querySelector(":popover-open")) return;
889
+ // A passive hint does not own the keyboard.
890
+ if (ownerDocument.querySelector(":popover-open:not(.k2b-moved-hint)")) return;
650
891
  } catch {
651
892
  // Older engines have no popover selector; the other popup guards still apply.
652
893
  }
653
894
  const root = target.closest(ROOT_SELECTOR);
654
895
  let aside = target.closest(detailSelector);
655
896
  if (aside?.hidden || aside && (!eventRoot.contains(aside) || aside.closest(ROOT_SELECTOR) !== root)) aside = null;
656
- if (!aside && root && observedRoots.has(root) && mainElement(root)?.contains(target)) aside = openDetails(root)[0] ?? null;
897
+ if (!aside && root && observedRoots.has(root) && (mainElement(root)?.contains(target) || target.closest(DETAIL_TOGGLE_SELECTOR))) aside = openDetails(root).find(isVisible) ?? null;
657
898
  if (!aside && target === ownerDocument.body) {
658
- const open = allRoots().flatMap(openDetails);
899
+ const open = allRoots().flatMap(openDetails).filter(isVisible);
659
900
  if (open.length === 1) aside = open[0];
660
901
  }
661
902
  const owningRoot = aside?.closest(ROOT_SELECTOR);
662
- if (!aside || !owningRoot) return;
903
+ if (!aside || !owningRoot || !isVisible(aside)) return;
904
+ if (event.key === "Escape" && detailDrawer(aside)) {
905
+ parkDetailDrawer(aside, true);
906
+ event.preventDefault();
907
+ return;
908
+ }
663
909
  if (event.key === "Escape" && closable(aside)) requestClose(aside);else if ((event.key.toLowerCase() === "j" || event.key.toLowerCase() === "k") && aside.hasAttribute("data-workspace-detail-steppable")) {
664
910
  const main = mainElement(owningRoot);
665
911
  const inMain = remembered.get(owningRoot)?.item.contains(target) || main?.contains(target);
@@ -683,7 +929,8 @@ export const installAppWorkspaceController = (options = {}) => {
683
929
  scroller: verticalScroller(item, main),
684
930
  scrollTop: 0,
685
931
  top: 0,
686
- fresh: false
932
+ fresh: false,
933
+ chosen: false
687
934
  };
688
935
  refreshItem(snapshot);
689
936
  remembered.set(owningRoot, snapshot);
@@ -748,7 +995,7 @@ export const installAppWorkspaceController = (options = {}) => {
748
995
  clearDrag(drag.aside);
749
996
  if (cancelled || !drag.moved) return;
750
997
  if (drag.source === "drawer") {
751
- if (drag.dx > 80 || drag.dx > 30 && drag.velocity > 0.5) requestClose(drag.aside);
998
+ if (drag.dx > 80 || drag.dx > 30 && drag.velocity > 0.5) parkDetailDrawer(drag.aside, drag.aside.contains(ownerDocument.activeElement));
752
999
  } else if (drag.dy > 60) {
753
1000
  setDetent(drag.aside, false);
754
1001
  if (!drag.full && closable(drag.aside)) requestClose(drag.aside);
@@ -837,7 +1084,7 @@ export const installAppWorkspaceController = (options = {}) => {
837
1084
  if (phone?.matches && phonePage(aside)) return;
838
1085
  if (phone?.matches && (verticalScroller(target, aside) ?? aside).scrollTop <= 0) {
839
1086
  if (!ownsTouchGesture(target, aside, "sheet")) beginDetailDrag(aside, target, "sheet", touch.clientX, touch.clientY, touch.identifier);
840
- } else if (touchDrawer?.matches && closable(aside)) {
1087
+ } else if (wide?.matches && detailDrawer(aside)) {
841
1088
  if (!ownsTouchGesture(target, aside, "drawer")) beginDetailDrag(aside, target, "drawer", touch.clientX, touch.clientY, touch.identifier);
842
1089
  }
843
1090
  if (detailDrag) claimedDetailGestures.add(event);
@@ -1140,7 +1387,7 @@ export const installAppWorkspaceController = (options = {}) => {
1140
1387
  return allRoots().filter(root => getComputedStyle(root).display !== "none" && root.getBoundingClientRect().width > 0);
1141
1388
  };
1142
1389
  const reconcile = () => {
1143
- if (detailDrag && (detailDrag.source === "drawer" ? !touchDrawer?.matches : !phone?.matches)) finishDetailDrag(true);
1390
+ if (detailDrag && (detailDrag.source === "drawer" ? !wide?.matches || !detailDrawer(detailDrag.aside) : !phone?.matches)) finishDetailDrag(true);
1144
1391
  allRoots().forEach(root => {
1145
1392
  if (!phone?.matches) {
1146
1393
  for (const aside of rootElements(root, detailSelector)) {
@@ -1167,7 +1414,9 @@ export const installAppWorkspaceController = (options = {}) => {
1167
1414
  return;
1168
1415
  }
1169
1416
  const persisted = kind === "sidebar" ? layoutState.sidebarCollapsed && sidebarCollapsible(root) ? APP_WORKSPACE_SIDEBAR_COLLAPSED : layoutState.sidebarWidth : kind === "detail" ? layoutState.detailWidths?.[panelId(handle)] : layoutState.drawerHeights?.[panelId(handle)];
1170
- if (persisted !== undefined) applySize(root, handle, kind, persisted);else updateHandleValue(root, handle, kind, currentSize(root, handle, kind));
1417
+ if (persisted !== undefined) applySize(root, handle, kind, persisted, {
1418
+ reserveMainMinimum: false
1419
+ });else updateHandleValue(root, handle, kind, currentSize(root, handle, kind));
1171
1420
  });
1172
1421
  reconcilePanes(root, layoutState, active?.root === root ? active : null);
1173
1422
  });
@@ -1196,14 +1445,43 @@ export const installAppWorkspaceController = (options = {}) => {
1196
1445
  reconcileFrame = requestAnimationFrame(() => {
1197
1446
  reconcileFrame = null;
1198
1447
  reconcile();
1448
+ // Reconciling can restore a drawer's persisted width; its trail space follows.
1449
+ for (const root of allRoots()) for (const aside of rootElements(root, DETAIL_DRAWER_SELECTOR)) syncDetailDrawerSpace(aside);
1199
1450
  });
1200
1451
  };
1201
- const onResize = () => scheduleReconcile();
1452
+ const onResize = () => {
1453
+ syncHostState();
1454
+ scheduleReconcile();
1455
+ };
1202
1456
  const observedRoots = new Set();
1203
1457
  const resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
1458
+ syncHostState();
1204
1459
  scheduleReconcile();
1205
1460
  });
1461
+ // A host can change its width while a workspace with a fixed width keeps
1462
+ // its own, so the controller also watches each root's nearest named host.
1463
+ const observedHosts = new Set();
1464
+ // Found once per root: reading styles on every mutation would cost a style pass.
1465
+ const rootHosts = new WeakMap();
1466
+ const hostOf = root => {
1467
+ if (rootHosts.has(root)) return rootHosts.get(root);
1468
+ let host = null;
1469
+ for (let element = root.parentElement; element && !host; element = element.parentElement) if (getComputedStyle(element).containerName.split(/\s+/).includes("k2b-app-area")) host = element;
1470
+ rootHosts.set(root, host);
1471
+ return host;
1472
+ };
1206
1473
  const syncObservedRoots = () => {
1474
+ const hosts = new Set(allRoots().map(hostOf).filter(host => host !== null));
1475
+ observedHosts.forEach(host => {
1476
+ if (hosts.has(host)) return;
1477
+ resizeObserver?.unobserve(host);
1478
+ observedHosts.delete(host);
1479
+ });
1480
+ hosts.forEach(host => {
1481
+ if (observedHosts.has(host)) return;
1482
+ observedHosts.add(host);
1483
+ resizeObserver?.observe(host);
1484
+ });
1207
1485
  for (const elements of [touchedDetails, titleTabIndices, motionRoots]) {
1208
1486
  elements.forEach(element => {
1209
1487
  if (!element.isConnected) elements.delete(element);
@@ -1253,6 +1531,7 @@ export const installAppWorkspaceController = (options = {}) => {
1253
1531
  const mutationObserver = typeof MutationObserver === "undefined" ? null : new MutationObserver(records => {
1254
1532
  syncPhonePages();
1255
1533
  if (pendingTitle && (pendingTitle.aside.hidden || !pendingTitle.aside.isConnected)) cancelTitle();
1534
+ if (pendingDetail && (!canOpenPendingDetail(pendingDetail.aside) || !pendingDetail.snapshot.chosen || !pendingDetail.snapshot.item.isConnected)) cancelPendingDetail();
1256
1535
  // Hiding or removing the sidebar, or its button, ends the overlay.
1257
1536
  if (overlay && (overlay.sidebar.hidden || !overlay.sidebar.isConnected || !overlay.toggle.isConnected)) closeOverlay("kept");
1258
1537
  const details = new Set();
@@ -1265,6 +1544,8 @@ export const installAppWorkspaceController = (options = {}) => {
1265
1544
  const changed = records.some(record => record.type === "attributes" ? record.target instanceof Element && record.target.matches(LAYOUT_REGION_SELECTOR) : [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)].some(layoutNode));
1266
1545
  if (!changed) return;
1267
1546
  syncObservedRoots();
1547
+ // A new width token or a removed panel can end waiting without any box resizing.
1548
+ if (records.some(record => record.type === "childList" || record.attributeName === "data-width" || record.attributeName === "data-workspace-main-min")) syncHostState();
1268
1549
  scheduleReconcile();
1269
1550
  });
1270
1551
  eventRoot.addEventListener("pointerdown", onPointerDown);
@@ -1274,6 +1555,10 @@ export const installAppWorkspaceController = (options = {}) => {
1274
1555
  ownerDocument.addEventListener("focusin", cancelTitleOnInteraction, true);
1275
1556
  ownerDocument.addEventListener("pointerdown", cancelTitleOnInteraction, true);
1276
1557
  ownerDocument.addEventListener("keydown", cancelTitleOnInteraction, true);
1558
+ ownerDocument.addEventListener("click", onChoiceClick, true);
1559
+ ownerDocument.addEventListener("keyup", endActivation, true);
1560
+ ownerDocument.addEventListener("pointerup", endActivation, true);
1561
+ ownerDocument.addEventListener("pointercancel", cancelActivation, true);
1277
1562
  eventRoot.addEventListener("pointerdown", rememberInteraction, true);
1278
1563
  eventRoot.addEventListener("keydown", rememberInteraction, true);
1279
1564
  eventRoot.addEventListener("focusin", onDetailFocusIn);
@@ -1289,7 +1574,7 @@ export const installAppWorkspaceController = (options = {}) => {
1289
1574
  });
1290
1575
  eventRoot.addEventListener("touchend", onDetailTouchEnd);
1291
1576
  eventRoot.addEventListener("touchcancel", onDetailTouchEnd);
1292
- compact?.addEventListener("change", onCompactChange);
1577
+ phone?.addEventListener("change", syncHostState);
1293
1578
  phone?.addEventListener("change", syncPhonePages);
1294
1579
  eventRoot.addEventListener("keydown", onKeyDown);
1295
1580
  eventRoot.addEventListener("focusin", onFocusIn);
@@ -1299,7 +1584,7 @@ export const installAppWorkspaceController = (options = {}) => {
1299
1584
  mutationObserver?.observe(eventRoot, {
1300
1585
  attributes: true,
1301
1586
  attributeOldValue: true,
1302
- attributeFilter: ["hidden", "data-workspace-level-shown"],
1587
+ attributeFilter: ["hidden", "data-workspace-level-shown", "data-width", "data-workspace-main-min"],
1303
1588
  childList: true,
1304
1589
  subtree: true
1305
1590
  });
@@ -1309,6 +1594,7 @@ export const installAppWorkspaceController = (options = {}) => {
1309
1594
  phonePageInerted.clear();
1310
1595
  phone?.removeEventListener("change", syncPhonePages);
1311
1596
  cancelTitle();
1597
+ cancelPendingDetail();
1312
1598
  pendingAnchors.forEach(cancel => cancel());
1313
1599
  slides.forEach(end => end());
1314
1600
  stopScrollFades();
@@ -1327,6 +1613,11 @@ export const installAppWorkspaceController = (options = {}) => {
1327
1613
  ownerDocument.removeEventListener("focusin", cancelTitleOnInteraction, true);
1328
1614
  ownerDocument.removeEventListener("pointerdown", cancelTitleOnInteraction, true);
1329
1615
  ownerDocument.removeEventListener("keydown", cancelTitleOnInteraction, true);
1616
+ clearActivation();
1617
+ ownerDocument.removeEventListener("click", onChoiceClick, true);
1618
+ ownerDocument.removeEventListener("keyup", endActivation, true);
1619
+ ownerDocument.removeEventListener("pointerup", endActivation, true);
1620
+ ownerDocument.removeEventListener("pointercancel", cancelActivation, true);
1330
1621
  eventRoot.removeEventListener("pointerdown", rememberInteraction, true);
1331
1622
  eventRoot.removeEventListener("keydown", rememberInteraction, true);
1332
1623
  eventRoot.removeEventListener("focusin", onDetailFocusIn);
@@ -1345,14 +1636,16 @@ export const installAppWorkspaceController = (options = {}) => {
1345
1636
  // torn down, so unmounting during a drag cannot lose the final size.
1346
1637
  stopResize();
1347
1638
  if (reconcileFrame !== null) cancelAnimationFrame(reconcileFrame);
1639
+ if (hostSyncFrame !== null) cancelAnimationFrame(hostSyncFrame);
1348
1640
  mutationObserver?.disconnect();
1349
1641
  resizeObserver?.disconnect();
1350
1642
  observedRoots.clear();
1643
+ observedHosts.clear();
1351
1644
  closeOverlay("none");
1352
1645
  eventRoot.removeEventListener("pointerdown", onPointerDown);
1353
1646
  eventRoot.removeEventListener("click", onClick);
1354
1647
  ownerDocument.removeEventListener("keydown", onOverlayKeyDown);
1355
- compact?.removeEventListener("change", onCompactChange);
1648
+ phone?.removeEventListener("change", syncHostState);
1356
1649
  eventRoot.removeEventListener("keydown", onKeyDown);
1357
1650
  eventRoot.removeEventListener("focusin", onFocusIn);
1358
1651
  window.removeEventListener("resize", onResize);