@k2b/ui 0.29.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 (173) hide show
  1. package/README.md +1 -1
  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/ViewTabs.js +294 -0
  14. package/dist/browser/actions/ViewTabs.js.map +1 -0
  15. package/dist/browser/actions/filter-messages.js +6 -2
  16. package/dist/browser/actions/filter-messages.js.map +1 -1
  17. package/dist/browser/actions/index.js +2 -1
  18. package/dist/browser/actions/index.js.map +1 -1
  19. package/dist/browser/chat/ChatComposer.js +1108 -604
  20. package/dist/browser/chat/ChatComposer.js.map +1 -1
  21. package/dist/browser/chat/ChatDecision.js +120 -84
  22. package/dist/browser/chat/ChatDecision.js.map +1 -1
  23. package/dist/browser/chat/ChatPrimitives.js +292 -159
  24. package/dist/browser/chat/ChatPrimitives.js.map +1 -1
  25. package/dist/browser/chat/chat-messages.js +8 -0
  26. package/dist/browser/chat/chat-messages.js.map +1 -1
  27. package/dist/browser/chat/command-search.js +26 -15
  28. package/dist/browser/chat/command-search.js.map +1 -1
  29. package/dist/browser/chat/index.js +4 -2
  30. package/dist/browser/chat/index.js.map +1 -1
  31. package/dist/browser/content/Calendar.js +143 -116
  32. package/dist/browser/content/Calendar.js.map +1 -1
  33. package/dist/browser/content/CalendarWeekView.js +291 -266
  34. package/dist/browser/content/CalendarWeekView.js.map +1 -1
  35. package/dist/browser/content/DataTable.js +224 -155
  36. package/dist/browser/content/DataTable.js.map +1 -1
  37. package/dist/browser/content/data-table-messages.js +4 -2
  38. package/dist/browser/content/data-table-messages.js.map +1 -1
  39. package/dist/browser/feedback/MovedHint.js +197 -0
  40. package/dist/browser/feedback/MovedHint.js.map +1 -0
  41. package/dist/browser/feedback/dialog-core.js +66 -9
  42. package/dist/browser/feedback/dialog-core.js.map +1 -1
  43. package/dist/browser/feedback/index.js +1 -0
  44. package/dist/browser/feedback/index.js.map +1 -1
  45. package/dist/browser/feedback/prompts.js +6 -1
  46. package/dist/browser/feedback/prompts.js.map +1 -1
  47. package/dist/browser/inputs/DatePicker.js +337 -212
  48. package/dist/browser/inputs/DatePicker.js.map +1 -1
  49. package/dist/browser/inputs/date-picker.js +59 -5
  50. package/dist/browser/inputs/date-picker.js.map +1 -1
  51. package/dist/browser/intl/messages.js +12 -0
  52. package/dist/browser/intl/messages.js.map +1 -1
  53. package/dist/browser/intl/weekend.js +39 -0
  54. package/dist/browser/intl/weekend.js.map +1 -0
  55. package/dist/browser/layout/AppOverview.js +0 -1
  56. package/dist/browser/layout/AppOverview.js.map +1 -1
  57. package/dist/browser/layout/AppWorkspace.js +688 -471
  58. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  59. package/dist/browser/layout/PageTitle.js +3 -2
  60. package/dist/browser/layout/PageTitle.js.map +1 -1
  61. package/dist/browser/layout/SettingsModal.js +2 -2
  62. package/dist/browser/layout/SettingsModal.js.map +1 -1
  63. package/dist/browser/layout/SidebarSwitcher.js +13 -12
  64. package/dist/browser/layout/SidebarSwitcher.js.map +1 -1
  65. package/dist/browser/layout/TabBar.js +1 -1
  66. package/dist/browser/layout/TabBar.js.map +1 -1
  67. package/dist/browser/layout/app-host-width.js +44 -0
  68. package/dist/browser/layout/app-host-width.js.map +1 -0
  69. package/dist/browser/layout/app-workspace-controller.js +219 -39
  70. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  71. package/dist/browser/layout/index.js +1 -0
  72. package/dist/browser/layout/index.js.map +1 -1
  73. package/dist/browser/surfaces/Placeholder.js +36 -13
  74. package/dist/browser/surfaces/Placeholder.js.map +1 -1
  75. package/dist/ssr/actions/Dropdown.js +5 -2
  76. package/dist/ssr/actions/Dropdown.js.map +1 -1
  77. package/dist/ssr/actions/FilterChip.js +59 -7
  78. package/dist/ssr/actions/FilterChip.js.map +1 -1
  79. package/dist/ssr/actions/SegmentedControl.js +1 -2
  80. package/dist/ssr/actions/SegmentedControl.js.map +1 -1
  81. package/dist/ssr/actions/SpotlightSearch.js +2 -2
  82. package/dist/ssr/actions/SpotlightSearch.js.map +1 -1
  83. package/dist/ssr/actions/Tabs.js +2 -2
  84. package/dist/ssr/actions/Tabs.js.map +1 -1
  85. package/dist/ssr/actions/ViewTabs.js +211 -0
  86. package/dist/ssr/actions/ViewTabs.js.map +1 -0
  87. package/dist/ssr/actions/filter-messages.js +6 -2
  88. package/dist/ssr/actions/filter-messages.js.map +1 -1
  89. package/dist/ssr/actions/index.js +2 -1
  90. package/dist/ssr/actions/index.js.map +1 -1
  91. package/dist/ssr/chat/ChatComposer.js +546 -131
  92. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  93. package/dist/ssr/chat/ChatDecision.js +33 -19
  94. package/dist/ssr/chat/ChatDecision.js.map +1 -1
  95. package/dist/ssr/chat/ChatPrimitives.js +95 -24
  96. package/dist/ssr/chat/ChatPrimitives.js.map +1 -1
  97. package/dist/ssr/chat/chat-messages.js +8 -0
  98. package/dist/ssr/chat/chat-messages.js.map +1 -1
  99. package/dist/ssr/chat/command-search.js +26 -15
  100. package/dist/ssr/chat/command-search.js.map +1 -1
  101. package/dist/ssr/chat/index.js +4 -2
  102. package/dist/ssr/chat/index.js.map +1 -1
  103. package/dist/ssr/content/Calendar.js +109 -100
  104. package/dist/ssr/content/Calendar.js.map +1 -1
  105. package/dist/ssr/content/CalendarWeekView.js +42 -28
  106. package/dist/ssr/content/CalendarWeekView.js.map +1 -1
  107. package/dist/ssr/content/DataTable.js +78 -31
  108. package/dist/ssr/content/DataTable.js.map +1 -1
  109. package/dist/ssr/content/data-table-messages.js +4 -2
  110. package/dist/ssr/content/data-table-messages.js.map +1 -1
  111. package/dist/ssr/feedback/MovedHint.js +166 -0
  112. package/dist/ssr/feedback/MovedHint.js.map +1 -0
  113. package/dist/ssr/feedback/dialog-core.js +66 -9
  114. package/dist/ssr/feedback/dialog-core.js.map +1 -1
  115. package/dist/ssr/feedback/index.js +1 -0
  116. package/dist/ssr/feedback/index.js.map +1 -1
  117. package/dist/ssr/feedback/prompts.js +6 -1
  118. package/dist/ssr/feedback/prompts.js.map +1 -1
  119. package/dist/ssr/inputs/DatePicker.js +138 -69
  120. package/dist/ssr/inputs/DatePicker.js.map +1 -1
  121. package/dist/ssr/inputs/date-picker.js +59 -5
  122. package/dist/ssr/inputs/date-picker.js.map +1 -1
  123. package/dist/ssr/intl/messages.js +12 -0
  124. package/dist/ssr/intl/messages.js.map +1 -1
  125. package/dist/ssr/intl/weekend.js +39 -0
  126. package/dist/ssr/intl/weekend.js.map +1 -0
  127. package/dist/ssr/layout/AppOverview.js +0 -1
  128. package/dist/ssr/layout/AppOverview.js.map +1 -1
  129. package/dist/ssr/layout/AppWorkspace.js +203 -39
  130. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  131. package/dist/ssr/layout/PageTitle.js +3 -2
  132. package/dist/ssr/layout/PageTitle.js.map +1 -1
  133. package/dist/ssr/layout/SettingsModal.js +2 -2
  134. package/dist/ssr/layout/SettingsModal.js.map +1 -1
  135. package/dist/ssr/layout/SidebarSwitcher.js +13 -12
  136. package/dist/ssr/layout/SidebarSwitcher.js.map +1 -1
  137. package/dist/ssr/layout/TabBar.js +1 -1
  138. package/dist/ssr/layout/TabBar.js.map +1 -1
  139. package/dist/ssr/layout/app-host-width.js +44 -0
  140. package/dist/ssr/layout/app-host-width.js.map +1 -0
  141. package/dist/ssr/layout/app-workspace-controller.js +219 -39
  142. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  143. package/dist/ssr/layout/index.js +1 -0
  144. package/dist/ssr/layout/index.js.map +1 -1
  145. package/dist/ssr/surfaces/Placeholder.js +24 -7
  146. package/dist/ssr/surfaces/Placeholder.js.map +1 -1
  147. package/dist/styles.css +1 -1
  148. package/dist/types/actions/Dropdown.d.ts +4 -0
  149. package/dist/types/actions/FilterChip.d.ts +22 -0
  150. package/dist/types/actions/ViewTabs.d.ts +30 -0
  151. package/dist/types/actions/filter-messages.d.ts +2 -0
  152. package/dist/types/actions/index.d.ts +4 -2
  153. package/dist/types/chat/ChatComposer.d.ts +52 -0
  154. package/dist/types/chat/ChatDecision.d.ts +18 -2
  155. package/dist/types/chat/ChatPrimitives.d.ts +10 -0
  156. package/dist/types/chat/chat-messages.d.ts +4 -0
  157. package/dist/types/chat/command-search.d.ts +2 -0
  158. package/dist/types/chat/index.d.ts +9 -8
  159. package/dist/types/content/DataTable.d.ts +3 -0
  160. package/dist/types/content/data-table-messages.d.ts +1 -0
  161. package/dist/types/feedback/MovedHint.d.ts +25 -0
  162. package/dist/types/feedback/dialog-core.d.ts +5 -2
  163. package/dist/types/feedback/index.d.ts +2 -0
  164. package/dist/types/feedback/prompts.d.ts +2 -0
  165. package/dist/types/inputs/DatePicker.d.ts +7 -0
  166. package/dist/types/inputs/date-picker.d.ts +25 -1
  167. package/dist/types/intl/messages.d.ts +6 -0
  168. package/dist/types/intl/weekend.d.ts +12 -0
  169. package/dist/types/layout/AppWorkspace.d.ts +43 -7
  170. package/dist/types/layout/app-host-width.d.ts +6 -0
  171. package/dist/types/layout/index.d.ts +2 -1
  172. package/dist/types/surfaces/Placeholder.d.ts +2 -1
  173. package/package.json +4 -4
@@ -4,10 +4,8 @@ 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
- /** Touch-tablet gestures remain viewport decisions. */
10
- const TOUCH_DRAWER_QUERY = "(min-width: 48rem) and (max-width: 63.999rem) and (pointer: coarse)";
11
9
  const DETAIL_TOGGLE_SELECTOR = "[data-app-workspace-detail-toggle]";
12
10
  const DETAIL_DRAWER_SELECTOR = '.k2b-app-workspace__detail[data-workspace-detail-drawer="open"]';
13
11
  const cssState = (element, name) => getComputedStyle(element).getPropertyValue(name).trim();
@@ -40,6 +38,12 @@ const FOCUSABLE_SELECTOR = ["a[href]", "button:not([disabled])", "input:not([dis
40
38
  /** Text entry, menus, dialogs, and open popups keep their own keys. */
41
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']";
42
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]";
43
47
  const clamp = (value, min, max) => Math.round(Math.min(max, Math.max(min, value)));
44
48
  const eventElement = event => event.target instanceof Element ? event.target : null;
45
49
  const resizeHandle = event => eventElement(event)?.closest(HANDLE_SELECTOR) ?? null;
@@ -52,6 +56,7 @@ const workspaceRoot = handle => handle.closest(ROOT_SELECTOR);
52
56
  const workspaceResizable = root => root.dataset.workspaceResizable !== "false";
53
57
  const rootElements = (root, selector) => Array.from(root.querySelectorAll(selector)).filter(element => element.closest(ROOT_SELECTOR) === root);
54
58
  const sidebarElement = root => rootElements(root, ".k2b-app-workspace__sidebar")[0] ?? null;
59
+ const sidebarForm = root => cssState(sidebarElement(root) ?? root, "--k2b-workspace-sidebar-form");
55
60
  const mainElement = root => rootElements(root, ".k2b-app-workspace__main")[0] ?? null;
56
61
  const sidebarCollapsible = root => sidebarElement(root)?.dataset.workspaceCollapsible === "true";
57
62
  const sidebarHideable = root => sidebarElement(root)?.dataset.workspaceHideable === "true";
@@ -101,7 +106,7 @@ const numberData = (handle, key, fallback) => {
101
106
  const value = Number(handle.dataset[key]);
102
107
  return Number.isFinite(value) ? value : fallback;
103
108
  };
104
- const sizeLimits = (root, handle, kind) => {
109
+ const sizeLimits = (root, handle, kind, reserveMainMinimum = true) => {
105
110
  const controlled = controlledPanel(root, handle);
106
111
  const main = kind === "pane" ? handle.closest(".k2b-app-workspace__main") : mainElement(root);
107
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)) : [];
@@ -116,14 +121,23 @@ const sizeLimits = (root, handle, kind) => {
116
121
  const content = kind === "detail" ? controlled?.closest(".k2b-app-workspace__content") : null;
117
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;
118
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;
119
- 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({
120
128
  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,
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,
123
132
  min: numberData(handle, "workspaceMinSize", defaultMin),
124
133
  max: numberData(handle, "workspaceMaxSize", defaultMax),
125
134
  sidebarCollapsible: kind === "sidebar" && sidebarCollapsible(root)
126
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;
127
141
  };
128
142
  const currentSize = (root, handle, kind) => kind === "sidebar" ? elementSize(sidebarElement(root), kind) : elementSize(controlledPanel(root, handle), kind);
129
143
  const updateHandleValue = (root, handle, kind, size, limits = sizeLimits(root, handle, kind)) => {
@@ -135,7 +149,7 @@ const updateHandleValue = (root, handle, kind, size, limits = sizeLimits(root, h
135
149
  handle.setAttribute("aria-valuemax", String(max));
136
150
  handle.setAttribute("aria-valuenow", String(clamp(size, min, max)));
137
151
  };
138
- const applySize = (root, handle, kind, requestedSize,
152
+ const applyRegionSize = (root, handle, kind, requestedSize,
139
153
  /**
140
154
  * `snapSidebar: false` tracks the pointer continuously and only *previews*
141
155
  * the collapsed state. Snapping (the default, and what keyboard steps and
@@ -146,7 +160,7 @@ options = {}) => {
146
160
  const {
147
161
  min,
148
162
  max
149
- } = sizeLimits(root, handle, kind);
163
+ } = sizeLimits(root, handle, kind, options.reserveMainMinimum);
150
164
  if (kind === "sidebar") {
151
165
  const collapsible = sidebarCollapsible(root);
152
166
  const sidebar = options.snapSidebar === false ? {
@@ -157,10 +171,8 @@ options = {}) => {
157
171
  // Cloud removes the attribute rather than writing "false", so that a
158
172
  // presence selector (`[data-sidebar-collapsed]`) means "collapsed".
159
173
  if (sidebar.collapsed) root.dataset.sidebarCollapsed = "true";else delete root.dataset.sidebarCollapsed;
160
- updateHandleValue(root, handle, kind, sidebar.width, {
161
- min,
162
- max
163
- });
174
+ // A restored width may require an overlay; the handle still advertises interactive limits.
175
+ updateHandleValue(root, handle, kind, sidebar.width);
164
176
  return sidebar.width;
165
177
  }
166
178
  const size = clamp(requestedSize, min, max);
@@ -229,7 +241,7 @@ const claimedToggleClicks = new WeakSet();
229
241
  const claimedDetailGestures = new WeakSet();
230
242
 
231
243
  /**
232
- * 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
233
245
  * from the button in the workspace's head. While it is open the rest of
234
246
  * the workspace is inert, Tab stays in the sidebar, and Escape, the scrim,
235
247
  * choosing an item, hiding the sidebar, or leaving the width range close it
@@ -304,9 +316,7 @@ export const installAppWorkspaceController = (options = {}) => {
304
316
  if (!aside || aside.hidden || !detailWaiting(aside) || claimedToggleClicks.has(event)) return;
305
317
  claimedToggleClicks.add(event);
306
318
  if (detailDrawer(aside)) parkDetailDrawer(aside);else {
307
- aside.dataset.workspaceDetailDrawer = "open";
308
- detailButton.setAttribute("aria-expanded", "true");
309
- syncDetailDrawerSpace(aside);
319
+ openDetailDrawer(aside);
310
320
  if (event.detail === 0) requestTitle(aside);else detailButton.focus({
311
321
  preventScroll: true
312
322
  });
@@ -318,7 +328,7 @@ export const installAppWorkspaceController = (options = {}) => {
318
328
  const next = overlayTarget(toggle);
319
329
  if (!next || claimedToggleClicks.has(event)) return;
320
330
  claimedToggleClicks.add(event);
321
- if (overlay?.sidebar === next.sidebar) closeOverlay("button");else if (cssState(next.root, "--k2b-workspace-sidebar-form") === "overlay") openOverlay(next);
331
+ if (overlay?.sidebar === next.sidebar) closeOverlay("button");else if (sidebarForm(next.root) === "overlay") openOverlay(next);
322
332
  return;
323
333
  }
324
334
  if (!overlay || !target) return;
@@ -386,8 +396,29 @@ export const installAppWorkspaceController = (options = {}) => {
386
396
  // Whether each open detail waited at the last check, so a column that starts
387
397
  // waiting under focus hands that focus to its Details button.
388
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
+ };
389
419
  const syncHostState = () => {
390
- if (overlay && cssState(overlay.root, "--k2b-workspace-sidebar-form") !== "overlay") closeOverlay("none");
420
+ if (pendingDetail && !canOpenPendingDetail(pendingDetail.aside)) cancelPendingDetail();
421
+ if (overlay && sidebarForm(overlay.root) !== "overlay") closeOverlay("none");
391
422
  for (const root of allRoots()) {
392
423
  // A drawer removed with its panel leaves no space behind in Content.
393
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");
@@ -413,7 +444,6 @@ export const installAppWorkspaceController = (options = {}) => {
413
444
  };
414
445
  const phone = typeof window.matchMedia === "function" ? window.matchMedia("(max-width: 47.999rem)") : null;
415
446
  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
447
  const detailSelector = ".k2b-app-workspace__detail";
418
448
  const grabberSelector = ".k2b-app-workspace__detail-grabber";
419
449
  const detailHidden = new WeakMap();
@@ -447,6 +477,36 @@ export const installAppWorkspaceController = (options = {}) => {
447
477
  snapshot.top = snapshot.item.getBoundingClientRect().top;
448
478
  snapshot.scrollTop = snapshot.scroller?.scrollTop ?? 0;
449
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
+ };
450
510
  // A table row can exceed its scroller's width, so keep the clicked cell for horizontal reveal.
451
511
  const rememberInteraction = event => {
452
512
  const target = eventElement(event);
@@ -455,24 +515,37 @@ export const installAppWorkspaceController = (options = {}) => {
455
515
  const previous = remembered.get(root);
456
516
  if (previous && openDetails(root).length) refreshItem(previous);
457
517
  const main = mainElement(root);
458
- const item = target.closest(FOCUSABLE_SELECTOR);
459
- 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;
460
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;
461
524
  const cell = target.closest("td, th");
462
525
  const snapshot = {
463
526
  item,
464
527
  cell: cell && main.contains(cell) && cell.closest(ROOT_SELECTOR) === root ? cell : null,
465
- interaction: event instanceof KeyboardEvent ? "keyboard" : "pointer",
528
+ interaction: event instanceof KeyboardEvent || assistive ? "keyboard" : "pointer",
466
529
  scroller: verticalScroller(item, main),
467
530
  scrollTop: 0,
468
531
  top: 0,
469
- 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']")
470
538
  };
471
539
  refreshItem(snapshot);
472
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
+ }
473
546
  // 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);
547
+ if (snapshot.chosen && snapshot.interaction === "keyboard") {
548
+ const aside = openDetails(root).find(aside => isVisible(aside) && (!detailWaiting(aside) || detailDrawer(aside)));
476
549
  if (aside) requestTitle(aside, true);
477
550
  }
478
551
  };
@@ -523,7 +596,18 @@ export const installAppWorkspaceController = (options = {}) => {
523
596
  // content, so focus falls to the body. The panel keeps owning the steps until
524
597
  // focus or a pointer goes elsewhere, so a quick second J still ends on the title.
525
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;
526
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();
527
611
  if (event.type !== "keydown" && !(event.target instanceof Node && panelStep?.contains(event.target))) panelStep = null;
528
612
  if (event.type !== "focusin" || event.target !== focusingTitle) cancelTitle();
529
613
  };
@@ -573,14 +657,13 @@ export const installAppWorkspaceController = (options = {}) => {
573
657
  delete aside.dataset.workspaceDetailDragging;
574
658
  };
575
659
  const anchorItem = (root, aside, snapshot) => {
576
- if (!wide?.matches || touchDrawer?.matches && closable(aside) || detailDrawer(aside) || !snapshot?.item.isConnected) return;
660
+ if (!wide?.matches || detailDrawer(aside) || !snapshot?.item.isConnected) return;
577
661
  if (!mainElement(root)?.contains(snapshot.item) || snapshot.item.closest(ROOT_SELECTOR) !== root) return;
578
662
  const scroller = snapshot.scroller;
579
663
  if (scroller?.isConnected && scroller.scrollTop === snapshot.scrollTop) scroller.scrollTop += snapshot.item.getBoundingClientRect().top - snapshot.top;
580
664
  };
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).
665
+ // A moving Detail would cross a framed workspace's edge, so the workspace
666
+ // clips while it slides (see the stylesheet).
584
667
  const slide = (root, aside) => {
585
668
  slides.get(root)?.();
586
669
  if (!root.hasAttribute("data-workspace-motion") || prefersReducedMotion() || !wide?.matches) return;
@@ -665,6 +748,82 @@ export const installAppWorkspaceController = (options = {}) => {
665
748
  }
666
749
  }));
667
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
+ };
668
827
  const detailTransition = (aside, wasHidden) => {
669
828
  const root = aside.closest(ROOT_SELECTOR);
670
829
  detailHidden.set(aside, Boolean(aside.hidden));
@@ -679,6 +838,7 @@ export const installAppWorkspaceController = (options = {}) => {
679
838
  const followedInteraction = snapshot?.fresh ?? false;
680
839
  if (snapshot) snapshot.fresh = false;
681
840
  if (aside.hidden) {
841
+ if (pendingDetail?.aside === aside) cancelPendingDetail();
682
842
  parkDetailDrawer(aside);
683
843
  if (pendingTitle?.aside === aside) cancelTitle();
684
844
  if (panelStep === aside) panelStep = null;
@@ -708,7 +868,11 @@ export const installAppWorkspaceController = (options = {}) => {
708
868
  if (detailDrag?.aside === aside) finishDetailDrag(true);
709
869
  return;
710
870
  }
711
- if (detailWaiting(aside) && !detailDrawer(aside)) return;
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
+ }
712
876
  anchorItem(root, aside, snapshot);
713
877
  if (phone?.matches) setDetent(aside, false);
714
878
  if (followedInteraction && snapshot) {
@@ -722,7 +886,8 @@ export const installAppWorkspaceController = (options = {}) => {
722
886
  if (!target || target.closest(KEY_OWNER_SELECTOR)) return;
723
887
  if (ownerDocument.querySelector("dialog[open], [aria-modal='true']")) return;
724
888
  try {
725
- 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;
726
891
  } catch {
727
892
  // Older engines have no popover selector; the other popup guards still apply.
728
893
  }
@@ -764,7 +929,8 @@ export const installAppWorkspaceController = (options = {}) => {
764
929
  scroller: verticalScroller(item, main),
765
930
  scrollTop: 0,
766
931
  top: 0,
767
- fresh: false
932
+ fresh: false,
933
+ chosen: false
768
934
  };
769
935
  refreshItem(snapshot);
770
936
  remembered.set(owningRoot, snapshot);
@@ -829,7 +995,7 @@ export const installAppWorkspaceController = (options = {}) => {
829
995
  clearDrag(drag.aside);
830
996
  if (cancelled || !drag.moved) return;
831
997
  if (drag.source === "drawer") {
832
- 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));
833
999
  } else if (drag.dy > 60) {
834
1000
  setDetent(drag.aside, false);
835
1001
  if (!drag.full && closable(drag.aside)) requestClose(drag.aside);
@@ -918,7 +1084,7 @@ export const installAppWorkspaceController = (options = {}) => {
918
1084
  if (phone?.matches && phonePage(aside)) return;
919
1085
  if (phone?.matches && (verticalScroller(target, aside) ?? aside).scrollTop <= 0) {
920
1086
  if (!ownsTouchGesture(target, aside, "sheet")) beginDetailDrag(aside, target, "sheet", touch.clientX, touch.clientY, touch.identifier);
921
- } else if (touchDrawer?.matches && closable(aside)) {
1087
+ } else if (wide?.matches && detailDrawer(aside)) {
922
1088
  if (!ownsTouchGesture(target, aside, "drawer")) beginDetailDrag(aside, target, "drawer", touch.clientX, touch.clientY, touch.identifier);
923
1089
  }
924
1090
  if (detailDrag) claimedDetailGestures.add(event);
@@ -1221,7 +1387,7 @@ export const installAppWorkspaceController = (options = {}) => {
1221
1387
  return allRoots().filter(root => getComputedStyle(root).display !== "none" && root.getBoundingClientRect().width > 0);
1222
1388
  };
1223
1389
  const reconcile = () => {
1224
- 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);
1225
1391
  allRoots().forEach(root => {
1226
1392
  if (!phone?.matches) {
1227
1393
  for (const aside of rootElements(root, detailSelector)) {
@@ -1248,7 +1414,9 @@ export const installAppWorkspaceController = (options = {}) => {
1248
1414
  return;
1249
1415
  }
1250
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)];
1251
- 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));
1252
1420
  });
1253
1421
  reconcilePanes(root, layoutState, active?.root === root ? active : null);
1254
1422
  });
@@ -1363,6 +1531,7 @@ export const installAppWorkspaceController = (options = {}) => {
1363
1531
  const mutationObserver = typeof MutationObserver === "undefined" ? null : new MutationObserver(records => {
1364
1532
  syncPhonePages();
1365
1533
  if (pendingTitle && (pendingTitle.aside.hidden || !pendingTitle.aside.isConnected)) cancelTitle();
1534
+ if (pendingDetail && (!canOpenPendingDetail(pendingDetail.aside) || !pendingDetail.snapshot.chosen || !pendingDetail.snapshot.item.isConnected)) cancelPendingDetail();
1366
1535
  // Hiding or removing the sidebar, or its button, ends the overlay.
1367
1536
  if (overlay && (overlay.sidebar.hidden || !overlay.sidebar.isConnected || !overlay.toggle.isConnected)) closeOverlay("kept");
1368
1537
  const details = new Set();
@@ -1376,7 +1545,7 @@ export const installAppWorkspaceController = (options = {}) => {
1376
1545
  if (!changed) return;
1377
1546
  syncObservedRoots();
1378
1547
  // 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();
1548
+ if (records.some(record => record.type === "childList" || record.attributeName === "data-width" || record.attributeName === "data-workspace-main-min")) syncHostState();
1380
1549
  scheduleReconcile();
1381
1550
  });
1382
1551
  eventRoot.addEventListener("pointerdown", onPointerDown);
@@ -1386,6 +1555,10 @@ export const installAppWorkspaceController = (options = {}) => {
1386
1555
  ownerDocument.addEventListener("focusin", cancelTitleOnInteraction, true);
1387
1556
  ownerDocument.addEventListener("pointerdown", cancelTitleOnInteraction, true);
1388
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);
1389
1562
  eventRoot.addEventListener("pointerdown", rememberInteraction, true);
1390
1563
  eventRoot.addEventListener("keydown", rememberInteraction, true);
1391
1564
  eventRoot.addEventListener("focusin", onDetailFocusIn);
@@ -1411,7 +1584,7 @@ export const installAppWorkspaceController = (options = {}) => {
1411
1584
  mutationObserver?.observe(eventRoot, {
1412
1585
  attributes: true,
1413
1586
  attributeOldValue: true,
1414
- attributeFilter: ["hidden", "data-workspace-level-shown", "data-width"],
1587
+ attributeFilter: ["hidden", "data-workspace-level-shown", "data-width", "data-workspace-main-min"],
1415
1588
  childList: true,
1416
1589
  subtree: true
1417
1590
  });
@@ -1421,6 +1594,7 @@ export const installAppWorkspaceController = (options = {}) => {
1421
1594
  phonePageInerted.clear();
1422
1595
  phone?.removeEventListener("change", syncPhonePages);
1423
1596
  cancelTitle();
1597
+ cancelPendingDetail();
1424
1598
  pendingAnchors.forEach(cancel => cancel());
1425
1599
  slides.forEach(end => end());
1426
1600
  stopScrollFades();
@@ -1439,6 +1613,11 @@ export const installAppWorkspaceController = (options = {}) => {
1439
1613
  ownerDocument.removeEventListener("focusin", cancelTitleOnInteraction, true);
1440
1614
  ownerDocument.removeEventListener("pointerdown", cancelTitleOnInteraction, true);
1441
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);
1442
1621
  eventRoot.removeEventListener("pointerdown", rememberInteraction, true);
1443
1622
  eventRoot.removeEventListener("keydown", rememberInteraction, true);
1444
1623
  eventRoot.removeEventListener("focusin", onDetailFocusIn);
@@ -1457,6 +1636,7 @@ export const installAppWorkspaceController = (options = {}) => {
1457
1636
  // torn down, so unmounting during a drag cannot lose the final size.
1458
1637
  stopResize();
1459
1638
  if (reconcileFrame !== null) cancelAnimationFrame(reconcileFrame);
1639
+ if (hostSyncFrame !== null) cancelAnimationFrame(hostSyncFrame);
1460
1640
  mutationObserver?.disconnect();
1461
1641
  resizeObserver?.disconnect();
1462
1642
  observedRoots.clear();