@k2b/ui 0.29.0 → 0.31.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 (243) hide show
  1. package/README.md +2 -2
  2. package/dist/base.css +2 -2
  3. package/dist/browser/actions/Dropdown.js +6 -3
  4. package/dist/browser/actions/Dropdown.js.map +1 -1
  5. package/dist/browser/actions/FilterChip.js +137 -7
  6. package/dist/browser/actions/FilterChip.js.map +1 -1
  7. package/dist/browser/actions/SegmentedControl.js +5 -9
  8. package/dist/browser/actions/SegmentedControl.js.map +1 -1
  9. package/dist/browser/actions/SpotlightSearch.js +6 -3
  10. package/dist/browser/actions/SpotlightSearch.js.map +1 -1
  11. package/dist/browser/actions/Tabs.js +5 -2
  12. package/dist/browser/actions/Tabs.js.map +1 -1
  13. package/dist/browser/actions/Toolbar.js +2 -0
  14. package/dist/browser/actions/Toolbar.js.map +1 -1
  15. package/dist/browser/actions/ViewTabs.js +294 -0
  16. package/dist/browser/actions/ViewTabs.js.map +1 -0
  17. package/dist/browser/actions/filter-messages.js +6 -2
  18. package/dist/browser/actions/filter-messages.js.map +1 -1
  19. package/dist/browser/actions/index.js +2 -1
  20. package/dist/browser/actions/index.js.map +1 -1
  21. package/dist/browser/chat/ChatComposer.js +1108 -604
  22. package/dist/browser/chat/ChatComposer.js.map +1 -1
  23. package/dist/browser/chat/ChatDecision.js +146 -84
  24. package/dist/browser/chat/ChatDecision.js.map +1 -1
  25. package/dist/browser/chat/ChatPrimitives.js +292 -159
  26. package/dist/browser/chat/ChatPrimitives.js.map +1 -1
  27. package/dist/browser/chat/chat-messages.js +8 -0
  28. package/dist/browser/chat/chat-messages.js.map +1 -1
  29. package/dist/browser/chat/command-search.js +26 -15
  30. package/dist/browser/chat/command-search.js.map +1 -1
  31. package/dist/browser/chat/index.js +4 -2
  32. package/dist/browser/chat/index.js.map +1 -1
  33. package/dist/browser/content/Calendar.js +143 -116
  34. package/dist/browser/content/Calendar.js.map +1 -1
  35. package/dist/browser/content/CalendarWeekView.js +291 -266
  36. package/dist/browser/content/CalendarWeekView.js.map +1 -1
  37. package/dist/browser/content/DataTable.js +224 -155
  38. package/dist/browser/content/DataTable.js.map +1 -1
  39. package/dist/browser/content/MapAttribution.js +27 -0
  40. package/dist/browser/content/MapAttribution.js.map +1 -0
  41. package/dist/browser/content/MapView.js +553 -0
  42. package/dist/browser/content/MapView.js.map +1 -0
  43. package/dist/browser/content/MapViewFrame.js +395 -0
  44. package/dist/browser/content/MapViewFrame.js.map +1 -0
  45. package/dist/browser/content/data-table-messages.js +4 -2
  46. package/dist/browser/content/data-table-messages.js.map +1 -1
  47. package/dist/browser/content/index.js +5 -0
  48. package/dist/browser/content/index.js.map +1 -1
  49. package/dist/browser/content/map-palette.js +39 -0
  50. package/dist/browser/content/map-palette.js.map +1 -0
  51. package/dist/browser/content/map-style.js +276 -0
  52. package/dist/browser/content/map-style.js.map +1 -0
  53. package/dist/browser/content/map-view-geometry.js +269 -0
  54. package/dist/browser/content/map-view-geometry.js.map +1 -0
  55. package/dist/browser/content/map-view-live.js +28 -0
  56. package/dist/browser/content/map-view-live.js.map +1 -0
  57. package/dist/browser/content/map-view-messages.js +53 -0
  58. package/dist/browser/content/map-view-messages.js.map +1 -0
  59. package/dist/browser/content/map-view-types.js +2 -0
  60. package/dist/browser/content/map-view-types.js.map +1 -0
  61. package/dist/browser/content/map-view-worker.js +10 -0
  62. package/dist/browser/feedback/MovedHint.js +197 -0
  63. package/dist/browser/feedback/MovedHint.js.map +1 -0
  64. package/dist/browser/feedback/dialog-core.js +85 -9
  65. package/dist/browser/feedback/dialog-core.js.map +1 -1
  66. package/dist/browser/feedback/index.js +1 -0
  67. package/dist/browser/feedback/index.js.map +1 -1
  68. package/dist/browser/feedback/prompts.js +6 -1
  69. package/dist/browser/feedback/prompts.js.map +1 -1
  70. package/dist/browser/inputs/DatePicker.js +337 -212
  71. package/dist/browser/inputs/DatePicker.js.map +1 -1
  72. package/dist/browser/inputs/date-picker.js +59 -5
  73. package/dist/browser/inputs/date-picker.js.map +1 -1
  74. package/dist/browser/intl/messages.js +12 -0
  75. package/dist/browser/intl/messages.js.map +1 -1
  76. package/dist/browser/intl/weekend.js +39 -0
  77. package/dist/browser/intl/weekend.js.map +1 -0
  78. package/dist/browser/layout/AppOverview.js +0 -1
  79. package/dist/browser/layout/AppOverview.js.map +1 -1
  80. package/dist/browser/layout/AppWorkspace.js +701 -476
  81. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  82. package/dist/browser/layout/PageTitle.js +3 -2
  83. package/dist/browser/layout/PageTitle.js.map +1 -1
  84. package/dist/browser/layout/SettingsModal.js +2 -2
  85. package/dist/browser/layout/SettingsModal.js.map +1 -1
  86. package/dist/browser/layout/SidebarSwitcher.js +13 -12
  87. package/dist/browser/layout/SidebarSwitcher.js.map +1 -1
  88. package/dist/browser/layout/TabBar.js +1 -1
  89. package/dist/browser/layout/TabBar.js.map +1 -1
  90. package/dist/browser/layout/app-host-width.js +44 -0
  91. package/dist/browser/layout/app-host-width.js.map +1 -0
  92. package/dist/browser/layout/app-workspace-controller.js +331 -45
  93. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  94. package/dist/browser/layout/app-workspace-width.js +26 -0
  95. package/dist/browser/layout/app-workspace-width.js.map +1 -0
  96. package/dist/browser/layout/index.js +2 -1
  97. package/dist/browser/layout/index.js.map +1 -1
  98. package/dist/browser/surfaces/Placeholder.js +36 -13
  99. package/dist/browser/surfaces/Placeholder.js.map +1 -1
  100. package/dist/licenses/maplibre-gl.txt +116 -0
  101. package/dist/ssr/actions/Dropdown.js +5 -2
  102. package/dist/ssr/actions/Dropdown.js.map +1 -1
  103. package/dist/ssr/actions/FilterChip.js +59 -7
  104. package/dist/ssr/actions/FilterChip.js.map +1 -1
  105. package/dist/ssr/actions/SegmentedControl.js +1 -2
  106. package/dist/ssr/actions/SegmentedControl.js.map +1 -1
  107. package/dist/ssr/actions/SpotlightSearch.js +2 -2
  108. package/dist/ssr/actions/SpotlightSearch.js.map +1 -1
  109. package/dist/ssr/actions/Tabs.js +2 -2
  110. package/dist/ssr/actions/Tabs.js.map +1 -1
  111. package/dist/ssr/actions/Toolbar.js +2 -0
  112. package/dist/ssr/actions/Toolbar.js.map +1 -1
  113. package/dist/ssr/actions/ViewTabs.js +211 -0
  114. package/dist/ssr/actions/ViewTabs.js.map +1 -0
  115. package/dist/ssr/actions/filter-messages.js +6 -2
  116. package/dist/ssr/actions/filter-messages.js.map +1 -1
  117. package/dist/ssr/actions/index.js +2 -1
  118. package/dist/ssr/actions/index.js.map +1 -1
  119. package/dist/ssr/chat/ChatComposer.js +546 -131
  120. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  121. package/dist/ssr/chat/ChatDecision.js +44 -19
  122. package/dist/ssr/chat/ChatDecision.js.map +1 -1
  123. package/dist/ssr/chat/ChatPrimitives.js +95 -24
  124. package/dist/ssr/chat/ChatPrimitives.js.map +1 -1
  125. package/dist/ssr/chat/chat-messages.js +8 -0
  126. package/dist/ssr/chat/chat-messages.js.map +1 -1
  127. package/dist/ssr/chat/command-search.js +26 -15
  128. package/dist/ssr/chat/command-search.js.map +1 -1
  129. package/dist/ssr/chat/index.js +4 -2
  130. package/dist/ssr/chat/index.js.map +1 -1
  131. package/dist/ssr/content/Calendar.js +109 -100
  132. package/dist/ssr/content/Calendar.js.map +1 -1
  133. package/dist/ssr/content/CalendarWeekView.js +42 -28
  134. package/dist/ssr/content/CalendarWeekView.js.map +1 -1
  135. package/dist/ssr/content/DataTable.js +78 -31
  136. package/dist/ssr/content/DataTable.js.map +1 -1
  137. package/dist/ssr/content/MapAttribution.js +19 -0
  138. package/dist/ssr/content/MapAttribution.js.map +1 -0
  139. package/dist/ssr/content/MapView.js +474 -0
  140. package/dist/ssr/content/MapView.js.map +1 -0
  141. package/dist/ssr/content/MapViewFrame.js +193 -0
  142. package/dist/ssr/content/MapViewFrame.js.map +1 -0
  143. package/dist/ssr/content/data-table-messages.js +4 -2
  144. package/dist/ssr/content/data-table-messages.js.map +1 -1
  145. package/dist/ssr/content/index.js +5 -0
  146. package/dist/ssr/content/index.js.map +1 -1
  147. package/dist/ssr/content/map-palette.js +39 -0
  148. package/dist/ssr/content/map-palette.js.map +1 -0
  149. package/dist/ssr/content/map-style.js +276 -0
  150. package/dist/ssr/content/map-style.js.map +1 -0
  151. package/dist/ssr/content/map-view-geometry.js +269 -0
  152. package/dist/ssr/content/map-view-geometry.js.map +1 -0
  153. package/dist/ssr/content/map-view-live.js +1 -0
  154. package/dist/ssr/content/map-view-messages.js +53 -0
  155. package/dist/ssr/content/map-view-messages.js.map +1 -0
  156. package/dist/ssr/content/map-view-types.js +2 -0
  157. package/dist/ssr/content/map-view-types.js.map +1 -0
  158. package/dist/ssr/feedback/MovedHint.js +166 -0
  159. package/dist/ssr/feedback/MovedHint.js.map +1 -0
  160. package/dist/ssr/feedback/dialog-core.js +85 -9
  161. package/dist/ssr/feedback/dialog-core.js.map +1 -1
  162. package/dist/ssr/feedback/index.js +1 -0
  163. package/dist/ssr/feedback/index.js.map +1 -1
  164. package/dist/ssr/feedback/prompts.js +6 -1
  165. package/dist/ssr/feedback/prompts.js.map +1 -1
  166. package/dist/ssr/inputs/DatePicker.js +138 -69
  167. package/dist/ssr/inputs/DatePicker.js.map +1 -1
  168. package/dist/ssr/inputs/date-picker.js +59 -5
  169. package/dist/ssr/inputs/date-picker.js.map +1 -1
  170. package/dist/ssr/intl/messages.js +12 -0
  171. package/dist/ssr/intl/messages.js.map +1 -1
  172. package/dist/ssr/intl/weekend.js +39 -0
  173. package/dist/ssr/intl/weekend.js.map +1 -0
  174. package/dist/ssr/layout/AppOverview.js +0 -1
  175. package/dist/ssr/layout/AppOverview.js.map +1 -1
  176. package/dist/ssr/layout/AppWorkspace.js +216 -44
  177. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  178. package/dist/ssr/layout/PageTitle.js +3 -2
  179. package/dist/ssr/layout/PageTitle.js.map +1 -1
  180. package/dist/ssr/layout/SettingsModal.js +2 -2
  181. package/dist/ssr/layout/SettingsModal.js.map +1 -1
  182. package/dist/ssr/layout/SidebarSwitcher.js +13 -12
  183. package/dist/ssr/layout/SidebarSwitcher.js.map +1 -1
  184. package/dist/ssr/layout/TabBar.js +1 -1
  185. package/dist/ssr/layout/TabBar.js.map +1 -1
  186. package/dist/ssr/layout/app-host-width.js +44 -0
  187. package/dist/ssr/layout/app-host-width.js.map +1 -0
  188. package/dist/ssr/layout/app-workspace-controller.js +331 -45
  189. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  190. package/dist/ssr/layout/app-workspace-width.js +26 -0
  191. package/dist/ssr/layout/app-workspace-width.js.map +1 -0
  192. package/dist/ssr/layout/index.js +2 -1
  193. package/dist/ssr/layout/index.js.map +1 -1
  194. package/dist/ssr/surfaces/Placeholder.js +24 -7
  195. package/dist/ssr/surfaces/Placeholder.js.map +1 -1
  196. package/dist/styles.css +1 -1
  197. package/dist/types/actions/Dropdown.d.ts +4 -0
  198. package/dist/types/actions/FilterChip.d.ts +22 -0
  199. package/dist/types/actions/ViewTabs.d.ts +30 -0
  200. package/dist/types/actions/filter-messages.d.ts +2 -0
  201. package/dist/types/actions/index.d.ts +4 -2
  202. package/dist/types/chat/ChatComposer.d.ts +52 -0
  203. package/dist/types/chat/ChatDecision.d.ts +18 -2
  204. package/dist/types/chat/ChatPrimitives.d.ts +10 -0
  205. package/dist/types/chat/chat-messages.d.ts +4 -0
  206. package/dist/types/chat/command-search.d.ts +2 -0
  207. package/dist/types/chat/index.d.ts +9 -8
  208. package/dist/types/content/DataTable.d.ts +3 -0
  209. package/dist/types/content/MapAttribution.d.ts +6 -0
  210. package/dist/types/content/MapView.d.ts +5 -0
  211. package/dist/types/content/MapViewFrame.d.ts +31 -0
  212. package/dist/types/content/data-table-messages.d.ts +1 -0
  213. package/dist/types/content/index.d.ts +6 -0
  214. package/dist/types/content/map-palette.d.ts +58 -0
  215. package/dist/types/content/map-style.d.ts +29 -0
  216. package/dist/types/content/map-view-geometry.d.ts +55 -0
  217. package/dist/types/content/map-view-live.d.ts +2 -0
  218. package/dist/types/content/map-view-messages.d.ts +21 -0
  219. package/dist/types/content/map-view-types.d.ts +61 -0
  220. package/dist/types/feedback/MovedHint.d.ts +25 -0
  221. package/dist/types/feedback/dialog-core.d.ts +6 -2
  222. package/dist/types/feedback/index.d.ts +2 -0
  223. package/dist/types/feedback/prompts.d.ts +2 -0
  224. package/dist/types/inputs/DatePicker.d.ts +7 -0
  225. package/dist/types/inputs/date-picker.d.ts +25 -1
  226. package/dist/types/intl/messages.d.ts +6 -0
  227. package/dist/types/intl/weekend.d.ts +12 -0
  228. package/dist/types/layout/AppWorkspace.d.ts +48 -8
  229. package/dist/types/layout/app-host-width.d.ts +6 -0
  230. package/dist/types/layout/app-workspace-width.d.ts +13 -0
  231. package/dist/types/layout/index.d.ts +3 -2
  232. package/dist/types/surfaces/Placeholder.d.ts +2 -1
  233. package/map-glyphs/IBM Plex Sans Medium/0-255.pbf +0 -0
  234. package/map-glyphs/IBM Plex Sans Medium/1024-1279.pbf +0 -0
  235. package/map-glyphs/IBM Plex Sans Medium/256-511.pbf +0 -0
  236. package/map-glyphs/IBM Plex Sans Medium/512-767.pbf +0 -0
  237. package/map-glyphs/IBM Plex Sans Medium/768-1023.pbf +0 -0
  238. package/map-glyphs/IBM Plex Sans Medium/7680-7935.pbf +0 -0
  239. package/map-glyphs/IBM Plex Sans Medium/8192-8447.pbf +0 -0
  240. package/map-glyphs/IBM Plex Sans Medium/8448-8703.pbf +0 -0
  241. package/map-glyphs/IBM Plex Sans Medium/8704-8959.pbf +0 -0
  242. package/map-glyphs/OFL.txt +93 -0
  243. package/package.json +11 -6
@@ -1,13 +1,12 @@
1
1
  import { suppressTextSelection } from "../internal/text-selection";
2
2
  import { prefersReducedMotion } from "../motion";
3
3
  import { APP_WORKSPACE_DETAIL_DEFAULT, APP_WORKSPACE_DETAIL_MAX, APP_WORKSPACE_DETAIL_MIN, APP_WORKSPACE_DRAWER_MAX, APP_WORKSPACE_DRAWER_MIN, APP_WORKSPACE_MAIN_MIN, APP_WORKSPACE_PANE_DEFAULT, APP_WORKSPACE_PANE_MAX, APP_WORKSPACE_PANE_MIN, APP_WORKSPACE_SIDEBAR_COLLAPSED, APP_WORKSPACE_SIDEBAR_MAX, APP_WORKSPACE_SIDEBAR_MIN, appWorkspacePanelVariable, appWorkspaceResizeLimits, fitAppWorkspacePaneSizes, normalizeAppWorkspaceLayoutState, resolveAppWorkspaceSidebarWidth, safeAppWorkspacePanelId, shouldCollapseAppWorkspaceSidebar } from "./app-workspace-state";
4
+ import { readAppWorkspaceWidth, resolveAppWorkspaceWidth } from "./app-workspace-width";
4
5
  import { installScrollFades } from "./scroll-fade";
5
6
  const HANDLE_SELECTOR = "[data-app-workspace-resize]";
6
7
  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(",");
8
+ 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
9
  const TOGGLE_SELECTOR = "[data-app-workspace-sidebar-toggle]";
9
- /** Touch-tablet gestures remain viewport decisions. */
10
- const TOUCH_DRAWER_QUERY = "(min-width: 48rem) and (max-width: 63.999rem) and (pointer: coarse)";
11
10
  const DETAIL_TOGGLE_SELECTOR = "[data-app-workspace-detail-toggle]";
12
11
  const DETAIL_DRAWER_SELECTOR = '.k2b-app-workspace__detail[data-workspace-detail-drawer="open"]';
13
12
  const cssState = (element, name) => getComputedStyle(element).getPropertyValue(name).trim();
@@ -40,6 +39,12 @@ const FOCUSABLE_SELECTOR = ["a[href]", "button:not([disabled])", "input:not([dis
40
39
  /** Text entry, menus, dialogs, and open popups keep their own keys. */
41
40
  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']";
42
41
  const ITEM_SELECTOR = ".k2b-app-workspace__sidebar-item";
42
+ /** Controls inside a selected row that act on their own instead of choosing the row. */
43
+ const NESTED_CONTROL_SELECTOR = "[aria-expanded], [aria-haspopup]:not([aria-haspopup='false']), [aria-pressed], [role='checkbox'], [role='switch'], [role='menuitem'], input, select, textarea, summary";
44
+ const DETAIL_REQUEST_CONTROL_SELECTOR = "[aria-pressed], [aria-expanded], [aria-controls]";
45
+ const SELECTION_MARK_SELECTOR = '[aria-selected="true"], [aria-current]:not([aria-current="false"]):not([aria-current="date"]):not([aria-current="time"]), [data-selected="true"]';
46
+ // Calendar day marks select a date range, not the Detail's subject.
47
+ const IGNORED_SELECTION_MARK_SELECTOR = "[data-calendar-day-key]";
43
48
  const clamp = (value, min, max) => Math.round(Math.min(max, Math.max(min, value)));
44
49
  const eventElement = event => event.target instanceof Element ? event.target : null;
45
50
  const resizeHandle = event => eventElement(event)?.closest(HANDLE_SELECTOR) ?? null;
@@ -52,6 +57,7 @@ const workspaceRoot = handle => handle.closest(ROOT_SELECTOR);
52
57
  const workspaceResizable = root => root.dataset.workspaceResizable !== "false";
53
58
  const rootElements = (root, selector) => Array.from(root.querySelectorAll(selector)).filter(element => element.closest(ROOT_SELECTOR) === root);
54
59
  const sidebarElement = root => rootElements(root, ".k2b-app-workspace__sidebar")[0] ?? null;
60
+ const sidebarForm = root => cssState(sidebarElement(root) ?? root, "--k2b-workspace-sidebar-form");
55
61
  const mainElement = root => rootElements(root, ".k2b-app-workspace__main")[0] ?? null;
56
62
  const sidebarCollapsible = root => sidebarElement(root)?.dataset.workspaceCollapsible === "true";
57
63
  const sidebarHideable = root => sidebarElement(root)?.dataset.workspaceHideable === "true";
@@ -101,7 +107,7 @@ const numberData = (handle, key, fallback) => {
101
107
  const value = Number(handle.dataset[key]);
102
108
  return Number.isFinite(value) ? value : fallback;
103
109
  };
104
- const sizeLimits = (root, handle, kind) => {
110
+ const sizeLimits = (root, handle, kind, reserveMainMinimum = true) => {
105
111
  const controlled = controlledPanel(root, handle);
106
112
  const main = kind === "pane" ? handle.closest(".k2b-app-workspace__main") : mainElement(root);
107
113
  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)) : [];
@@ -116,14 +122,24 @@ const sizeLimits = (root, handle, kind) => {
116
122
  const content = kind === "detail" ? controlled?.closest(".k2b-app-workspace__content") : null;
117
123
  const defaultMin = kind === "sidebar" ? APP_WORKSPACE_SIDEBAR_MIN : kind === "pane" ? APP_WORKSPACE_PANE_MIN : kind === "detail" ? APP_WORKSPACE_DETAIL_MIN : APP_WORKSPACE_DRAWER_MIN;
118
124
  const defaultMax = kind === "sidebar" ? APP_WORKSPACE_SIDEBAR_MAX : kind === "pane" ? APP_WORKSPACE_PANE_MAX : kind === "detail" ? APP_WORKSPACE_DETAIL_MAX : APP_WORKSPACE_DRAWER_MAX;
119
- return appWorkspaceResizeLimits({
125
+ const mainSpec = readAppWorkspaceWidth(mainElement(root)?.dataset.workspaceMainMin);
126
+ const mainMinValue = cssState(root, "--k2b-workspace-main-min");
127
+ const mainMin = typeof mainSpec === "object" ? resolveAppWorkspaceWidth(mainSpec, root.getBoundingClientRect().width) : mainMinValue.endsWith("px") ? Number.parseFloat(mainMinValue) : APP_WORKSPACE_MAIN_MIN;
128
+ const mainExtra = Math.max(0, (Number.isFinite(mainMin) ? mainMin : APP_WORKSPACE_MAIN_MIN) - APP_WORKSPACE_MAIN_MIN);
129
+ const limits = appWorkspaceResizeLimits({
120
130
  kind,
121
- workspaceSize: kind === "drawer" ? root.getBoundingClientRect().height : kind === "pane" && main ? main.getBoundingClientRect().width : content ? root.getBoundingClientRect().right - root.clientLeft - content.getBoundingClientRect().left : root.getBoundingClientRect().width,
122
- reservedSize: kind === "detail" ? 8 : kind === "pane" ? panes.reduce((total, pane) => total + (pane === controlled ? 0 : elementSize(pane, "pane")), 0) + separatorSize : 0,
131
+ 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,
132
+ // Interactive sizing keeps Main's declared minimum beside the sidebar and the Detail.
133
+ 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,
123
134
  min: numberData(handle, "workspaceMinSize", defaultMin),
124
135
  max: numberData(handle, "workspaceMaxSize", defaultMax),
125
136
  sidebarCollapsible: kind === "sidebar" && sidebarCollapsible(root)
126
137
  });
138
+ // Widths are applied as whole pixels; rounding up must not cross the fit boundary.
139
+ return kind === "sidebar" ? {
140
+ min: limits.min,
141
+ max: Math.max(limits.min, Math.floor(limits.max))
142
+ } : limits;
127
143
  };
128
144
  const currentSize = (root, handle, kind) => kind === "sidebar" ? elementSize(sidebarElement(root), kind) : elementSize(controlledPanel(root, handle), kind);
129
145
  const updateHandleValue = (root, handle, kind, size, limits = sizeLimits(root, handle, kind)) => {
@@ -135,7 +151,7 @@ const updateHandleValue = (root, handle, kind, size, limits = sizeLimits(root, h
135
151
  handle.setAttribute("aria-valuemax", String(max));
136
152
  handle.setAttribute("aria-valuenow", String(clamp(size, min, max)));
137
153
  };
138
- const applySize = (root, handle, kind, requestedSize,
154
+ const applyRegionSize = (root, handle, kind, requestedSize,
139
155
  /**
140
156
  * `snapSidebar: false` tracks the pointer continuously and only *previews*
141
157
  * the collapsed state. Snapping (the default, and what keyboard steps and
@@ -146,7 +162,7 @@ options = {}) => {
146
162
  const {
147
163
  min,
148
164
  max
149
- } = sizeLimits(root, handle, kind);
165
+ } = sizeLimits(root, handle, kind, options.reserveMainMinimum);
150
166
  if (kind === "sidebar") {
151
167
  const collapsible = sidebarCollapsible(root);
152
168
  const sidebar = options.snapSidebar === false ? {
@@ -157,10 +173,8 @@ options = {}) => {
157
173
  // Cloud removes the attribute rather than writing "false", so that a
158
174
  // presence selector (`[data-sidebar-collapsed]`) means "collapsed".
159
175
  if (sidebar.collapsed) root.dataset.sidebarCollapsed = "true";else delete root.dataset.sidebarCollapsed;
160
- updateHandleValue(root, handle, kind, sidebar.width, {
161
- min,
162
- max
163
- });
176
+ // A restored width may require an overlay; the handle still advertises interactive limits.
177
+ updateHandleValue(root, handle, kind, sidebar.width);
164
178
  return sidebar.width;
165
179
  }
166
180
  const size = clamp(requestedSize, min, max);
@@ -201,18 +215,39 @@ const reconcilePanes = (root, layoutState, active = null) => {
201
215
  if (!isVisible(panel)) return [];
202
216
  const min = numberData(handle, "workspaceMinSize", APP_WORKSPACE_PANE_MIN);
203
217
  const max = Math.max(min, numberData(handle, "workspaceMaxSize", APP_WORKSPACE_PANE_MAX));
204
- const preferred = active?.moved && active.root === root && active.kind === "pane" && active.handle === handle ? active.liveSize : layoutState.paneWidths?.[panelId(handle)] ?? numberData(handle, "workspaceDefaultSize", elementSize(panel, "pane"));
205
- const desired = clamp(preferred, min, max);
218
+ const spec = readAppWorkspaceWidth(handle.dataset.workspaceDefaultSize) ?? elementSize(panel, "pane");
219
+ const preferred = active?.moved && active.root === root && active.kind === "pane" && active.handle === handle ? active.liveSize : layoutState.paneWidths?.[panelId(handle)];
220
+ const cssDefault = preferred === undefined && typeof spec !== "number";
221
+ if (cssDefault && root.style.getPropertyValue(appWorkspacePanelVariable("pane", panelId(handle))))
222
+ // Drop a previous live width (or an inherited SSR preference after reset).
223
+ root.style.setProperty(appWorkspacePanelVariable("pane", panelId(handle)), "initial");
224
+ // CSS has already applied this default and the pane-fit clamp, including
225
+ // subpixels. Reserve exactly that box when fitting other preferences;
226
+ // measuring must not turn a responsive default into a stored px width.
227
+ const requested = preferred ?? resolveAppWorkspaceWidth(spec, root.getBoundingClientRect().width);
228
+ const desired = cssDefault ? elementSize(panel, "pane") : clamp(requested, min, max);
206
229
  return [{
207
230
  desired,
208
231
  handle,
209
- min
232
+ min: cssDefault ? desired : min,
233
+ cssDefault,
234
+ requested: clamp(requested, min, max),
235
+ configuredMin: min
210
236
  }];
211
237
  });
212
238
  if (!fits.length) return;
213
- const sizes = fitAppWorkspacePaneSizes(fits, main.getBoundingClientRect().width - fixedWidth - APP_WORKSPACE_MAIN_MIN);
239
+ const budget = main.getBoundingClientRect().width - fixedWidth - APP_WORKSPACE_MAIN_MIN;
240
+ // Each share default clamps only against Main, not its resizable or non-resizable siblings. When panes
241
+ // together leave Main's primary region less than its minimum, fit them like stored widths, allowing subpixels.
242
+ if (fits.reduce((total, fit) => total + fit.desired, 0) > budget + 0.5) for (const fit of fits) if (fit.cssDefault) Object.assign(fit, {
243
+ cssDefault: false,
244
+ desired: fit.requested,
245
+ min: fit.configuredMin
246
+ });
247
+ const sizes = fitAppWorkspacePaneSizes(fits, budget);
214
248
  fits.forEach((fit, index) => {
215
- root.style.setProperty(appWorkspacePanelVariable("pane", panelId(fit.handle)), `${sizes[index]}px`);
249
+ // Defaults stay CSS-owned; fit only saved or live preferences around them.
250
+ if (!fit.cssDefault) root.style.setProperty(appWorkspacePanelVariable("pane", panelId(fit.handle)), `${sizes[index]}px`);
216
251
  });
217
252
  fits.forEach((fit, index) => updateHandleValue(root, fit.handle, "pane", sizes[index]));
218
253
  };
@@ -229,7 +264,7 @@ const claimedToggleClicks = new WeakSet();
229
264
  const claimedDetailGestures = new WeakSet();
230
265
 
231
266
  /**
232
- * The compact overlay: from 48 to 64 rem the sidebar opens over the work area
267
+ * The overlay sidebar opens over the work area when its column does not fit
233
268
  * from the button in the workspace's head. While it is open the rest of
234
269
  * the workspace is inert, Tab stays in the sidebar, and Escape, the scrim,
235
270
  * choosing an item, hiding the sidebar, or leaving the width range close it
@@ -304,13 +339,12 @@ export const installAppWorkspaceController = (options = {}) => {
304
339
  if (!aside || aside.hidden || !detailWaiting(aside) || claimedToggleClicks.has(event)) return;
305
340
  claimedToggleClicks.add(event);
306
341
  if (detailDrawer(aside)) parkDetailDrawer(aside);else {
307
- aside.dataset.workspaceDetailDrawer = "open";
308
- detailButton.setAttribute("aria-expanded", "true");
309
- syncDetailDrawerSpace(aside);
342
+ openDetailDrawer(aside);
310
343
  if (event.detail === 0) requestTitle(aside);else detailButton.focus({
311
344
  preventScroll: true
312
345
  });
313
346
  }
347
+ syncDetailRequestControls();
314
348
  return;
315
349
  }
316
350
  const toggle = target?.closest(TOGGLE_SELECTOR);
@@ -318,7 +352,7 @@ export const installAppWorkspaceController = (options = {}) => {
318
352
  const next = overlayTarget(toggle);
319
353
  if (!next || claimedToggleClicks.has(event)) return;
320
354
  claimedToggleClicks.add(event);
321
- if (overlay?.sidebar === next.sidebar) closeOverlay("button");else if (cssState(next.root, "--k2b-workspace-sidebar-form") === "overlay") openOverlay(next);
355
+ if (overlay?.sidebar === next.sidebar) closeOverlay("button");else if (sidebarForm(next.root) === "overlay") openOverlay(next);
322
356
  return;
323
357
  }
324
358
  if (!overlay || !target) return;
@@ -386,8 +420,98 @@ export const installAppWorkspaceController = (options = {}) => {
386
420
  // Whether each open detail waited at the last check, so a column that starts
387
421
  // waiting under focus hands that focus to its Details button.
388
422
  const detailWasWaiting = new WeakMap();
423
+ const detailRequestExpanded = new Map();
424
+ const restoreDetailRequestExpanded = (control, state) => {
425
+ // A newer application render already owns the current value.
426
+ if (control.getAttribute("aria-expanded") !== state.written) return;
427
+ if (state.rendered === null) control.removeAttribute("aria-expanded");else control.setAttribute("aria-expanded", state.aside.hidden || !state.aside.isConnected ? "false" : state.rendered);
428
+ };
429
+ const requestedWaitingDetail = control => {
430
+ if (!wide?.matches || control.matches(DETAIL_TOGGLE_SELECTOR) || !eventRoot.contains(control)) return null;
431
+ const root = workspaceRoot(control);
432
+ const region = control.closest(".k2b-app-workspace__main, .k2b-app-workspace__detail");
433
+ if (!root || !region || region.closest(ROOT_SELECTOR) !== root) return null;
434
+ const ids = control.getAttribute("aria-controls")?.split(/\s+/);
435
+ return rootElements(root, detailSelector).find(aside => !aside.hidden && ids?.includes(aside.id) && detailWaiting(aside)) ?? null;
436
+ };
437
+ const syncDetailRequestControls = () => {
438
+ const waitingControls = new Map();
439
+ for (const root of allRoots()) {
440
+ const waitingDetails = wide?.matches ? rootElements(root, detailSelector).filter(aside => !aside.hidden && detailWaiting(aside)) : [];
441
+ if (!waitingDetails.length) continue;
442
+ // Only a control that names a Detail can request it.
443
+ for (const control of rootElements(root, "[aria-controls]")) {
444
+ if (control.matches(DETAIL_TOGGLE_SELECTOR) || !eventRoot.contains(control)) continue;
445
+ const region = control.closest(".k2b-app-workspace__main, .k2b-app-workspace__detail");
446
+ if (!region || region.closest(ROOT_SELECTOR) !== root) continue;
447
+ const ids = control.getAttribute("aria-controls")?.split(/\s+/);
448
+ const aside = waitingDetails.find(aside => ids?.includes(aside.id));
449
+ if (aside) waitingControls.set(control, aside);
450
+ }
451
+ }
452
+ detailRequestExpanded.forEach((state, control) => {
453
+ if (waitingControls.has(control)) return;
454
+ restoreDetailRequestExpanded(control, state);
455
+ detailRequestExpanded.delete(control);
456
+ });
457
+ waitingControls.forEach((aside, control) => {
458
+ const current = control.getAttribute("aria-expanded");
459
+ const state = detailRequestExpanded.get(control);
460
+ const written = String(detailDrawer(aside));
461
+ // Keep the application's latest value to restore when waiting ends.
462
+ const rendered = !state || current !== state.written ? current : state.rendered;
463
+ detailRequestExpanded.set(control, {
464
+ aside,
465
+ rendered,
466
+ written
467
+ });
468
+ if (current !== written) control.setAttribute("aria-expanded", written);
469
+ });
470
+ };
471
+ const onDetailRequestClick = event => {
472
+ if (event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
473
+ const control = eventElement(event)?.closest(DETAIL_REQUEST_CONTROL_SELECTOR);
474
+ const aside = control ? requestedWaitingDetail(control) : null;
475
+ if (!control || !aside) return;
476
+ // The application keeps its Detail open; only its waiting drawer changes.
477
+ event.preventDefault();
478
+ event.stopImmediatePropagation();
479
+ cancelPendingDetail();
480
+ cancelTitle();
481
+ clearActivation();
482
+ // A control inside the drawer leaves with it, so focus goes to Details.
483
+ if (detailDrawer(aside)) parkDetailDrawer(aside, aside.contains(ownerDocument.activeElement));else {
484
+ openDetailDrawer(aside);
485
+ if (event.detail === 0) requestTitle(aside);else control.focus({
486
+ preventScroll: true
487
+ });
488
+ }
489
+ syncDetailRequestControls();
490
+ };
491
+ const openDetailDrawer = aside => {
492
+ aside.dataset.workspaceDetailDrawer = "open";
493
+ detailToggle(aside)?.setAttribute("aria-expanded", "true");
494
+ syncDetailDrawerSpace(aside);
495
+ syncDetailRequestControls();
496
+ };
497
+ // A sidebar drag changes the room beside it without resizing the workspace,
498
+ // so it rechecks waiting once per frame.
499
+ let hostSyncFrame = null;
500
+ const scheduleHostSync = () => {
501
+ if (hostSyncFrame !== null) return;
502
+ hostSyncFrame = requestAnimationFrame(() => {
503
+ hostSyncFrame = null;
504
+ syncHostState();
505
+ });
506
+ };
507
+ const applySize = (...args) => {
508
+ const size = applyRegionSize(...args);
509
+ if (args[2] === "sidebar") scheduleHostSync();
510
+ return size;
511
+ };
389
512
  const syncHostState = () => {
390
- if (overlay && cssState(overlay.root, "--k2b-workspace-sidebar-form") !== "overlay") closeOverlay("none");
513
+ if (pendingDetail && !canOpenPendingDetail(pendingDetail.aside)) cancelPendingDetail();
514
+ if (overlay && sidebarForm(overlay.root) !== "overlay") closeOverlay("none");
391
515
  for (const root of allRoots()) {
392
516
  // A drawer removed with its panel leaves no space behind in Content.
393
517
  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");
@@ -410,10 +534,10 @@ export const installAppWorkspaceController = (options = {}) => {
410
534
  });
411
535
  }
412
536
  }
537
+ syncDetailRequestControls();
413
538
  };
414
539
  const phone = typeof window.matchMedia === "function" ? window.matchMedia("(max-width: 47.999rem)") : null;
415
540
  const wide = typeof window.matchMedia === "function" ? window.matchMedia("(min-width: 48rem)") : null;
416
- const touchDrawer = typeof window.matchMedia === "function" ? window.matchMedia(TOUCH_DRAWER_QUERY) : null;
417
541
  const detailSelector = ".k2b-app-workspace__detail";
418
542
  const grabberSelector = ".k2b-app-workspace__detail-grabber";
419
543
  const detailHidden = new WeakMap();
@@ -447,6 +571,36 @@ export const installAppWorkspaceController = (options = {}) => {
447
571
  snapshot.top = snapshot.item.getBoundingClientRect().top;
448
572
  snapshot.scrollTop = snapshot.scroller?.scrollTop ?? 0;
449
573
  };
574
+ // Native Enter/Space clicks and pointer clicks continue their initiating event.
575
+ let activationItem = null;
576
+ let activationFrame = null;
577
+ const clearActivation = () => {
578
+ if (activationFrame !== null) cancelAnimationFrame(activationFrame);
579
+ activationFrame = null;
580
+ activationItem = null;
581
+ };
582
+ const endActivation = () => {
583
+ if (!activationItem) return;
584
+ // Space's native click follows keyup before the next frame.
585
+ if (activationFrame !== null) cancelAnimationFrame(activationFrame);
586
+ activationFrame = requestAnimationFrame(clearActivation);
587
+ };
588
+ const cancelActivation = () => {
589
+ // A cancelled gesture must not open a Detail that appears later.
590
+ if (lastChosen?.interaction === "pointer" && lastChosen.item === activationItem) {
591
+ lastChosen.chosen = false;
592
+ lastChosen = null;
593
+ }
594
+ clearActivation();
595
+ };
596
+ const onChoiceClick = event => {
597
+ const preceded = event.target instanceof Node && Boolean(activationItem?.contains(event.target));
598
+ clearActivation();
599
+ if (event.detail === 0 && !preceded) {
600
+ cancelTitleOnInteraction(event);
601
+ rememberInteraction(event);
602
+ } else if (!(event.target instanceof Node && lastChosen?.item.contains(event.target))) cancelTitleOnInteraction(event);
603
+ };
450
604
  // A table row can exceed its scroller's width, so keep the clicked cell for horizontal reveal.
451
605
  const rememberInteraction = event => {
452
606
  const target = eventElement(event);
@@ -455,24 +609,37 @@ export const installAppWorkspaceController = (options = {}) => {
455
609
  const previous = remembered.get(root);
456
610
  if (previous && openDetails(root).length) refreshItem(previous);
457
611
  const main = mainElement(root);
458
- const item = target.closest(FOCUSABLE_SELECTOR);
459
- if (!main || !item || !main.contains(item) || item.closest(ROOT_SELECTOR) !== root) return;
612
+ // Collection rows with a roving tab index are items too, though no tab stops.
613
+ const item = target.closest(`${FOCUSABLE_SELECTOR}, [tabindex]`);
614
+ if (!main || !item || item === main || !main.contains(item) || item.closest(ROOT_SELECTOR) !== root) return;
460
615
  if (event instanceof KeyboardEvent && event.key !== "Enter" && event.key !== " ") return;
616
+ if (event.type === "pointerdown" || event.type === "keydown") activationItem = item;
617
+ const assistive = event instanceof MouseEvent && event.type === "click" && event.detail === 0 && event.button === 0;
461
618
  const cell = target.closest("td, th");
462
619
  const snapshot = {
463
620
  item,
464
621
  cell: cell && main.contains(cell) && cell.closest(ROOT_SELECTOR) === root ? cell : null,
465
- interaction: event instanceof KeyboardEvent ? "keyboard" : "pointer",
622
+ interaction: event instanceof KeyboardEvent || assistive ? "keyboard" : "pointer",
466
623
  scroller: verticalScroller(item, main),
467
624
  scrollTop: 0,
468
625
  top: 0,
469
- fresh: true
626
+ fresh: true,
627
+ // Text entry and focus alone do not choose an item.
628
+ // A modified activation, such as Ctrl/Cmd-click opening a new tab, chooses nothing here.
629
+ 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) &&
630
+ // Space scrolls on native links; it does not activate them.
631
+ !(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']")
470
632
  };
471
633
  refreshItem(snapshot);
472
634
  remembered.set(root, snapshot);
635
+ if (snapshot.chosen) lastChosen = snapshot;
636
+ if (snapshot.chosen) {
637
+ const aside = openDetails(root).find(canOpenPendingDetail);
638
+ if (aside) checkPendingDetail(root, aside, snapshot, event instanceof KeyboardEvent && event.key === " ");
639
+ }
473
640
  // Enter in an editor or a field types; only an item that chooses something moves on to its title.
474
- if (event instanceof KeyboardEvent && !target.closest(KEY_OWNER_SELECTOR)) {
475
- const aside = openDetails(root).find(isVisible);
641
+ if (snapshot.chosen && snapshot.interaction === "keyboard") {
642
+ const aside = openDetails(root).find(aside => isVisible(aside) && (!detailWaiting(aside) || detailDrawer(aside)));
476
643
  if (aside) requestTitle(aside, true);
477
644
  }
478
645
  };
@@ -523,7 +690,18 @@ export const installAppWorkspaceController = (options = {}) => {
523
690
  // content, so focus falls to the body. The panel keeps owning the steps until
524
691
  // focus or a pointer goes elsewhere, so a quick second J still ends on the title.
525
692
  let panelStep = null;
693
+ // The last choice in Main, until another interaction replaces it: a Detail
694
+ // that opens later than that does not take the drawer or the focus.
695
+ let lastChosen = null;
526
696
  const cancelTitleOnInteraction = event => {
697
+ if (event.type === "pointerdown" || event.type === "keydown") clearActivation();
698
+ if (lastChosen && !(event.target instanceof Node && lastChosen.item.contains(event.target))) {
699
+ lastChosen.chosen = false;
700
+ lastChosen = null;
701
+ }
702
+ if (pendingDetail &&
703
+ // Native pointer focus on the chosen item continues its activation.
704
+ !(event.type === "focusin" && event.target instanceof Node && pendingDetail.snapshot.item.contains(event.target))) cancelPendingDetail();
527
705
  if (event.type !== "keydown" && !(event.target instanceof Node && panelStep?.contains(event.target))) panelStep = null;
528
706
  if (event.type !== "focusin" || event.target !== focusingTitle) cancelTitle();
529
707
  };
@@ -573,14 +751,13 @@ export const installAppWorkspaceController = (options = {}) => {
573
751
  delete aside.dataset.workspaceDetailDragging;
574
752
  };
575
753
  const anchorItem = (root, aside, snapshot) => {
576
- if (!wide?.matches || touchDrawer?.matches && closable(aside) || detailDrawer(aside) || !snapshot?.item.isConnected) return;
754
+ if (!wide?.matches || detailDrawer(aside) || !snapshot?.item.isConnected) return;
577
755
  if (!mainElement(root)?.contains(snapshot.item) || snapshot.item.closest(ROOT_SELECTOR) !== root) return;
578
756
  const scroller = snapshot.scroller;
579
757
  if (scroller?.isConnected && scroller.scrollTop === snapshot.scrollTop) scroller.scrollTop += snapshot.item.getBoundingClientRect().top - snapshot.top;
580
758
  };
581
- // From 64 rem the workspace does not clip, so the panel's shadow shows on
582
- // every edge. A panel sliding in or out would cross a framed workspace's
583
- // edge, so the workspace clips while the panel moves (see the stylesheet).
759
+ // A moving Detail would cross a framed workspace's edge, so the workspace
760
+ // clips while it slides (see the stylesheet).
584
761
  const slide = (root, aside) => {
585
762
  slides.get(root)?.();
586
763
  if (!root.hasAttribute("data-workspace-motion") || prefersReducedMotion() || !wide?.matches) return;
@@ -665,6 +842,82 @@ export const installAppWorkspaceController = (options = {}) => {
665
842
  }
666
843
  }));
667
844
  };
845
+ let pendingDetail = null;
846
+ const canOpenPendingDetail = aside => aside.isConnected && eventRoot.contains(aside) && !aside.hidden && detailWaiting(aside) && !detailDrawer(aside) && aside.dataset.workspaceDetailChoice !== "request";
847
+ const cancelPendingDetail = () => {
848
+ pendingDetail?.stop();
849
+ pendingDetail = null;
850
+ };
851
+ // Check a completed choice after two frames. If its selection mark arrives
852
+ // later, keep waiting until another interaction replaces the choice.
853
+ const checkPendingDetail = (root, aside, snapshot, space = false) => {
854
+ cancelPendingDetail();
855
+ let frame = null;
856
+ let observer = null;
857
+ const settle = () => {
858
+ ownerDocument.removeEventListener("click", onClick, true);
859
+ ownerDocument.removeEventListener("keyup", onKeyUp, true);
860
+ if (frame !== null) cancelAnimationFrame(frame);
861
+ frame = requestAnimationFrame(() => {
862
+ frame = requestAnimationFrame(() => {
863
+ frame = null;
864
+ const main = mainElement(root);
865
+ if (!canOpenPendingDetail(aside) || !snapshot.chosen || !snapshot.item.isConnected || !main?.contains(snapshot.item) || snapshot.item.closest(ROOT_SELECTOR) !== root) {
866
+ cancelPendingDetail();
867
+ return;
868
+ }
869
+ // ARIA marks, and `data-selected` as DataTable and Calendar mark their selection.
870
+ let selected = snapshot.item.closest(SELECTION_MARK_SELECTOR);
871
+ while (selected?.matches(IGNORED_SELECTION_MARK_SELECTOR)) selected = selected.parentElement?.closest(SELECTION_MARK_SELECTOR) ?? null;
872
+ if (!selected || selected === main || !main.contains(selected)) {
873
+ if (!observer) {
874
+ observer = new MutationObserver(settle);
875
+ observer.observe(main, {
876
+ subtree: true,
877
+ attributes: true,
878
+ attributeFilter: ["aria-selected", "aria-current", "data-selected"]
879
+ });
880
+ }
881
+ return;
882
+ }
883
+ cancelPendingDetail();
884
+ // A control inside a selected row that discloses, toggles, or opens
885
+ // a menu does its own job and selects nothing.
886
+ if (selected !== snapshot.item && snapshot.item.matches(NESTED_CONTROL_SELECTOR)) return;
887
+ snapshot.fresh = false;
888
+ openDetailDrawer(aside);
889
+ revealItem(root, aside, snapshot);
890
+ // The same item keeps its title; another one may still be loading,
891
+ // so focus follows the title that replaces the current one.
892
+ if (snapshot.interaction === "keyboard") {
893
+ focusTitle(aside);
894
+ requestTitle(aside, true);
895
+ }
896
+ });
897
+ });
898
+ };
899
+ const onClick = event => {
900
+ if (event.button === 0 && event.target instanceof Node && snapshot.item.contains(event.target)) settle();else cancelPendingDetail();
901
+ };
902
+ // Space activates a button on release, so its check waits for keyup.
903
+ const onKeyUp = event => {
904
+ if (event.key === " ") settle();
905
+ };
906
+ const stop = () => {
907
+ if (frame !== null) cancelAnimationFrame(frame);
908
+ observer?.disconnect();
909
+ ownerDocument.removeEventListener("click", onClick, true);
910
+ ownerDocument.removeEventListener("keyup", onKeyUp, true);
911
+ ownerDocument.removeEventListener("pointercancel", cancelPendingDetail, true);
912
+ };
913
+ pendingDetail = {
914
+ aside,
915
+ snapshot,
916
+ stop
917
+ };
918
+ ownerDocument.addEventListener("pointercancel", cancelPendingDetail, true);
919
+ if (space) ownerDocument.addEventListener("keyup", onKeyUp, true);else if (snapshot.interaction === "keyboard") settle();else ownerDocument.addEventListener("click", onClick, true);
920
+ };
668
921
  const detailTransition = (aside, wasHidden) => {
669
922
  const root = aside.closest(ROOT_SELECTOR);
670
923
  detailHidden.set(aside, Boolean(aside.hidden));
@@ -679,6 +932,7 @@ export const installAppWorkspaceController = (options = {}) => {
679
932
  const followedInteraction = snapshot?.fresh ?? false;
680
933
  if (snapshot) snapshot.fresh = false;
681
934
  if (aside.hidden) {
935
+ if (pendingDetail?.aside === aside) cancelPendingDetail();
682
936
  parkDetailDrawer(aside);
683
937
  if (pendingTitle?.aside === aside) cancelTitle();
684
938
  if (panelStep === aside) panelStep = null;
@@ -708,7 +962,11 @@ export const installAppWorkspaceController = (options = {}) => {
708
962
  if (detailDrag?.aside === aside) finishDetailDrag(true);
709
963
  return;
710
964
  }
711
- if (detailWaiting(aside) && !detailDrawer(aside)) return;
965
+ if (detailWaiting(aside) && !detailDrawer(aside)) {
966
+ if (!followedInteraction || !snapshot?.chosen) return;
967
+ if (aside.dataset.workspaceDetailChoice === "request" && !snapshot.item.matches(DETAIL_REQUEST_CONTROL_SELECTOR)) return;
968
+ openDetailDrawer(aside);
969
+ }
712
970
  anchorItem(root, aside, snapshot);
713
971
  if (phone?.matches) setDetent(aside, false);
714
972
  if (followedInteraction && snapshot) {
@@ -722,7 +980,8 @@ export const installAppWorkspaceController = (options = {}) => {
722
980
  if (!target || target.closest(KEY_OWNER_SELECTOR)) return;
723
981
  if (ownerDocument.querySelector("dialog[open], [aria-modal='true']")) return;
724
982
  try {
725
- if (ownerDocument.querySelector(":popover-open")) return;
983
+ // A passive hint does not own the keyboard.
984
+ if (ownerDocument.querySelector(":popover-open:not(.k2b-moved-hint)")) return;
726
985
  } catch {
727
986
  // Older engines have no popover selector; the other popup guards still apply.
728
987
  }
@@ -738,6 +997,7 @@ export const installAppWorkspaceController = (options = {}) => {
738
997
  if (!aside || !owningRoot || !isVisible(aside)) return;
739
998
  if (event.key === "Escape" && detailDrawer(aside)) {
740
999
  parkDetailDrawer(aside, true);
1000
+ syncDetailRequestControls();
741
1001
  event.preventDefault();
742
1002
  return;
743
1003
  }
@@ -764,7 +1024,8 @@ export const installAppWorkspaceController = (options = {}) => {
764
1024
  scroller: verticalScroller(item, main),
765
1025
  scrollTop: 0,
766
1026
  top: 0,
767
- fresh: false
1027
+ fresh: false,
1028
+ chosen: false
768
1029
  };
769
1030
  refreshItem(snapshot);
770
1031
  remembered.set(owningRoot, snapshot);
@@ -829,7 +1090,7 @@ export const installAppWorkspaceController = (options = {}) => {
829
1090
  clearDrag(drag.aside);
830
1091
  if (cancelled || !drag.moved) return;
831
1092
  if (drag.source === "drawer") {
832
- if (drag.dx > 80 || drag.dx > 30 && drag.velocity > 0.5) requestClose(drag.aside);
1093
+ if (drag.dx > 80 || drag.dx > 30 && drag.velocity > 0.5) parkDetailDrawer(drag.aside, drag.aside.contains(ownerDocument.activeElement));
833
1094
  } else if (drag.dy > 60) {
834
1095
  setDetent(drag.aside, false);
835
1096
  if (!drag.full && closable(drag.aside)) requestClose(drag.aside);
@@ -918,7 +1179,7 @@ export const installAppWorkspaceController = (options = {}) => {
918
1179
  if (phone?.matches && phonePage(aside)) return;
919
1180
  if (phone?.matches && (verticalScroller(target, aside) ?? aside).scrollTop <= 0) {
920
1181
  if (!ownsTouchGesture(target, aside, "sheet")) beginDetailDrag(aside, target, "sheet", touch.clientX, touch.clientY, touch.identifier);
921
- } else if (touchDrawer?.matches && closable(aside)) {
1182
+ } else if (wide?.matches && detailDrawer(aside)) {
922
1183
  if (!ownsTouchGesture(target, aside, "drawer")) beginDetailDrag(aside, target, "drawer", touch.clientX, touch.clientY, touch.identifier);
923
1184
  }
924
1185
  if (detailDrag) claimedDetailGestures.add(event);
@@ -984,7 +1245,12 @@ export const installAppWorkspaceController = (options = {}) => {
984
1245
  const variable = appWorkspacePanelVariable(kind, panelId(handle));
985
1246
  root.style.removeProperty(variable);
986
1247
  // An SSR ancestor may still supply the old preference until the next request.
987
- if (getComputedStyle(root).getPropertyValue(variable).trim()) root.style.setProperty(variable, `${numberData(handle, "workspaceDefaultSize", kind === "pane" ? APP_WORKSPACE_PANE_DEFAULT : APP_WORKSPACE_DETAIL_DEFAULT)}px`);
1248
+ if (getComputedStyle(root).getPropertyValue(variable).trim()) {
1249
+ const spec = readAppWorkspaceWidth(handle.dataset.workspaceDefaultSize) ?? (kind === "pane" ? APP_WORKSPACE_PANE_DEFAULT : APP_WORKSPACE_DETAIL_DEFAULT);
1250
+ // A share must resolve at the pane, where the workspace length exists.
1251
+ // `initial` clears an inherited SSR preference and restores its CSS fallback.
1252
+ root.style.setProperty(variable, typeof spec === "number" ? `${spec}px` : "initial");
1253
+ }
988
1254
  // The siblings fit again around the default width, also in a Main without levels.
989
1255
  if (kind === "pane") reconcilePanes(root, layoutState);
990
1256
  updateHandleValue(root, handle, kind, currentSize(root, handle, kind));
@@ -1221,7 +1487,7 @@ export const installAppWorkspaceController = (options = {}) => {
1221
1487
  return allRoots().filter(root => getComputedStyle(root).display !== "none" && root.getBoundingClientRect().width > 0);
1222
1488
  };
1223
1489
  const reconcile = () => {
1224
- if (detailDrag && (detailDrag.source === "drawer" ? !touchDrawer?.matches : !phone?.matches)) finishDetailDrag(true);
1490
+ if (detailDrag && (detailDrag.source === "drawer" ? !wide?.matches || !detailDrawer(detailDrag.aside) : !phone?.matches)) finishDetailDrag(true);
1225
1491
  allRoots().forEach(root => {
1226
1492
  if (!phone?.matches) {
1227
1493
  for (const aside of rootElements(root, detailSelector)) {
@@ -1248,7 +1514,9 @@ export const installAppWorkspaceController = (options = {}) => {
1248
1514
  return;
1249
1515
  }
1250
1516
  const persisted = kind === "sidebar" ? layoutState.sidebarCollapsed && sidebarCollapsible(root) ? APP_WORKSPACE_SIDEBAR_COLLAPSED : layoutState.sidebarWidth : kind === "detail" ? layoutState.detailWidths?.[panelId(handle)] : layoutState.drawerHeights?.[panelId(handle)];
1251
- if (persisted !== undefined) applySize(root, handle, kind, persisted);else updateHandleValue(root, handle, kind, currentSize(root, handle, kind));
1517
+ if (persisted !== undefined) applySize(root, handle, kind, persisted, {
1518
+ reserveMainMinimum: false
1519
+ });else updateHandleValue(root, handle, kind, currentSize(root, handle, kind));
1252
1520
  });
1253
1521
  reconcilePanes(root, layoutState, active?.root === root ? active : null);
1254
1522
  });
@@ -1361,8 +1629,10 @@ export const installAppWorkspaceController = (options = {}) => {
1361
1629
  };
1362
1630
  const layoutNode = node => node instanceof Element && (node.matches(LAYOUT_REGION_SELECTOR) || Boolean(node.querySelector(LAYOUT_REGION_SELECTOR)));
1363
1631
  const mutationObserver = typeof MutationObserver === "undefined" ? null : new MutationObserver(records => {
1632
+ if (records.some(record => record.type === "childList" || ["hidden", "aria-controls", "aria-expanded", "data-workspace-detail-drawer"].includes(record.attributeName ?? ""))) syncDetailRequestControls();
1364
1633
  syncPhonePages();
1365
1634
  if (pendingTitle && (pendingTitle.aside.hidden || !pendingTitle.aside.isConnected)) cancelTitle();
1635
+ if (pendingDetail && (!canOpenPendingDetail(pendingDetail.aside) || !pendingDetail.snapshot.chosen || !pendingDetail.snapshot.item.isConnected)) cancelPendingDetail();
1366
1636
  // Hiding or removing the sidebar, or its button, ends the overlay.
1367
1637
  if (overlay && (overlay.sidebar.hidden || !overlay.sidebar.isConnected || !overlay.toggle.isConnected)) closeOverlay("kept");
1368
1638
  const details = new Set();
@@ -1376,7 +1646,7 @@ export const installAppWorkspaceController = (options = {}) => {
1376
1646
  if (!changed) return;
1377
1647
  syncObservedRoots();
1378
1648
  // A new width token or a removed panel can end waiting without any box resizing.
1379
- if (records.some(record => record.type === "childList" || record.attributeName === "data-width")) syncHostState();
1649
+ if (records.some(record => record.type === "childList" || record.attributeName === "data-width" || record.attributeName === "data-workspace-main-min")) syncHostState();
1380
1650
  scheduleReconcile();
1381
1651
  });
1382
1652
  eventRoot.addEventListener("pointerdown", onPointerDown);
@@ -1386,6 +1656,11 @@ export const installAppWorkspaceController = (options = {}) => {
1386
1656
  ownerDocument.addEventListener("focusin", cancelTitleOnInteraction, true);
1387
1657
  ownerDocument.addEventListener("pointerdown", cancelTitleOnInteraction, true);
1388
1658
  ownerDocument.addEventListener("keydown", cancelTitleOnInteraction, true);
1659
+ ownerDocument.addEventListener("click", onDetailRequestClick, true);
1660
+ ownerDocument.addEventListener("click", onChoiceClick, true);
1661
+ ownerDocument.addEventListener("keyup", endActivation, true);
1662
+ ownerDocument.addEventListener("pointerup", endActivation, true);
1663
+ ownerDocument.addEventListener("pointercancel", cancelActivation, true);
1389
1664
  eventRoot.addEventListener("pointerdown", rememberInteraction, true);
1390
1665
  eventRoot.addEventListener("keydown", rememberInteraction, true);
1391
1666
  eventRoot.addEventListener("focusin", onDetailFocusIn);
@@ -1408,10 +1683,11 @@ export const installAppWorkspaceController = (options = {}) => {
1408
1683
  window.addEventListener("resize", onResize);
1409
1684
  syncPhonePages();
1410
1685
  syncObservedRoots();
1686
+ syncDetailRequestControls();
1411
1687
  mutationObserver?.observe(eventRoot, {
1412
1688
  attributes: true,
1413
1689
  attributeOldValue: true,
1414
- attributeFilter: ["hidden", "data-workspace-level-shown", "data-width"],
1690
+ attributeFilter: ["hidden", "aria-controls", "aria-expanded", "data-workspace-detail-drawer", "data-workspace-level-shown", "data-width", "data-workspace-main-min", "data-workspace-default-size"],
1415
1691
  childList: true,
1416
1692
  subtree: true
1417
1693
  });
@@ -1421,6 +1697,7 @@ export const installAppWorkspaceController = (options = {}) => {
1421
1697
  phonePageInerted.clear();
1422
1698
  phone?.removeEventListener("change", syncPhonePages);
1423
1699
  cancelTitle();
1700
+ cancelPendingDetail();
1424
1701
  pendingAnchors.forEach(cancel => cancel());
1425
1702
  slides.forEach(end => end());
1426
1703
  stopScrollFades();
@@ -1439,6 +1716,11 @@ export const installAppWorkspaceController = (options = {}) => {
1439
1716
  ownerDocument.removeEventListener("focusin", cancelTitleOnInteraction, true);
1440
1717
  ownerDocument.removeEventListener("pointerdown", cancelTitleOnInteraction, true);
1441
1718
  ownerDocument.removeEventListener("keydown", cancelTitleOnInteraction, true);
1719
+ clearActivation();
1720
+ ownerDocument.removeEventListener("click", onChoiceClick, true);
1721
+ ownerDocument.removeEventListener("keyup", endActivation, true);
1722
+ ownerDocument.removeEventListener("pointerup", endActivation, true);
1723
+ ownerDocument.removeEventListener("pointercancel", cancelActivation, true);
1442
1724
  eventRoot.removeEventListener("pointerdown", rememberInteraction, true);
1443
1725
  eventRoot.removeEventListener("keydown", rememberInteraction, true);
1444
1726
  eventRoot.removeEventListener("focusin", onDetailFocusIn);
@@ -1457,7 +1739,11 @@ export const installAppWorkspaceController = (options = {}) => {
1457
1739
  // torn down, so unmounting during a drag cannot lose the final size.
1458
1740
  stopResize();
1459
1741
  if (reconcileFrame !== null) cancelAnimationFrame(reconcileFrame);
1742
+ if (hostSyncFrame !== null) cancelAnimationFrame(hostSyncFrame);
1460
1743
  mutationObserver?.disconnect();
1744
+ detailRequestExpanded.forEach((state, control) => restoreDetailRequestExpanded(control, state));
1745
+ detailRequestExpanded.clear();
1746
+ ownerDocument.removeEventListener("click", onDetailRequestClick, true);
1461
1747
  resizeObserver?.disconnect();
1462
1748
  observedRoots.clear();
1463
1749
  observedHosts.clear();