@k2b/ui 0.31.0 → 0.33.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 (209) hide show
  1. package/dist/base.css +2 -0
  2. package/dist/browser/actions/Dropdown.js +159 -99
  3. package/dist/browser/actions/Dropdown.js.map +1 -1
  4. package/dist/browser/actions/FilterChip.js +113 -56
  5. package/dist/browser/actions/FilterChip.js.map +1 -1
  6. package/dist/browser/actions/ViewTabs.js +8 -0
  7. package/dist/browser/actions/ViewTabs.js.map +1 -1
  8. package/dist/browser/actions/filter-messages.js +26 -2
  9. package/dist/browser/actions/filter-messages.js.map +1 -1
  10. package/dist/browser/actions/menu-rows.js +23 -3
  11. package/dist/browser/actions/menu-rows.js.map +1 -1
  12. package/dist/browser/actions/menu-sheet.js +2 -7
  13. package/dist/browser/actions/menu-sheet.js.map +1 -1
  14. package/dist/browser/chat/ChatComposer.js +480 -403
  15. package/dist/browser/chat/ChatComposer.js.map +1 -1
  16. package/dist/browser/chat/chat-messages.js +14 -0
  17. package/dist/browser/chat/chat-messages.js.map +1 -1
  18. package/dist/browser/chat/index.js.map +1 -1
  19. package/dist/browser/content/Calendar.js +39 -29
  20. package/dist/browser/content/Calendar.js.map +1 -1
  21. package/dist/browser/content/CalendarDayView.js +159 -165
  22. package/dist/browser/content/CalendarDayView.js.map +1 -1
  23. package/dist/browser/content/Chart.js +104 -55
  24. package/dist/browser/content/Chart.js.map +1 -1
  25. package/dist/browser/content/ChartExplorer.js +5 -3
  26. package/dist/browser/content/ChartExplorer.js.map +1 -1
  27. package/dist/browser/content/MapView.js +4 -4
  28. package/dist/browser/content/MapView.js.map +1 -1
  29. package/dist/browser/content/MessageRow.js +607 -343
  30. package/dist/browser/content/MessageRow.js.map +1 -1
  31. package/dist/browser/content/ReactionBar.js +17 -0
  32. package/dist/browser/content/ReactionBar.js.map +1 -1
  33. package/dist/browser/content/chart-inspection-core.js +4 -1
  34. package/dist/browser/content/chart-inspection-core.js.map +1 -1
  35. package/dist/browser/content/code-highlight.js +66 -0
  36. package/dist/browser/content/code-highlight.js.map +1 -1
  37. package/dist/browser/content/index.js +2 -1
  38. package/dist/browser/content/index.js.map +1 -1
  39. package/dist/browser/content/map-view-live.js +47 -10
  40. package/dist/browser/content/map-view-live.js.map +1 -1
  41. package/dist/browser/content/map-view-types.js.map +1 -1
  42. package/dist/browser/feedback/dialog-core.js +6 -0
  43. package/dist/browser/feedback/dialog-core.js.map +1 -1
  44. package/dist/browser/inputs/AutocompleteEditor.js +167 -104
  45. package/dist/browser/inputs/AutocompleteEditor.js.map +1 -1
  46. package/dist/browser/inputs/RichSelect.js +1474 -0
  47. package/dist/browser/inputs/RichSelect.js.map +1 -0
  48. package/dist/browser/inputs/TextInput.js +99 -61
  49. package/dist/browser/inputs/TextInput.js.map +1 -1
  50. package/dist/browser/inputs/choice.js +19 -8
  51. package/dist/browser/inputs/choice.js.map +1 -1
  52. package/dist/browser/inputs/index.js +1 -0
  53. package/dist/browser/inputs/index.js.map +1 -1
  54. package/dist/browser/inputs/rich-select-messages.js +46 -0
  55. package/dist/browser/inputs/rich-select-messages.js.map +1 -0
  56. package/dist/browser/intl/shortcut-messages.js +4 -0
  57. package/dist/browser/intl/shortcut-messages.js.map +1 -1
  58. package/dist/browser/intl/shortcut.js +4 -0
  59. package/dist/browser/intl/shortcut.js.map +1 -1
  60. package/dist/browser/layout/AppWorkspace.js +624 -565
  61. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  62. package/dist/browser/layout/DetailPanel.js +10 -1
  63. package/dist/browser/layout/DetailPanel.js.map +1 -1
  64. package/dist/browser/layout/MobileShell.js +213 -38
  65. package/dist/browser/layout/MobileShell.js.map +1 -1
  66. package/dist/browser/layout/Navigation.js +14 -5
  67. package/dist/browser/layout/Navigation.js.map +1 -1
  68. package/dist/browser/layout/PageTitle.js +9 -3
  69. package/dist/browser/layout/PageTitle.js.map +1 -1
  70. package/dist/browser/layout/ResizeSeparator.js +8 -2
  71. package/dist/browser/layout/ResizeSeparator.js.map +1 -1
  72. package/dist/browser/layout/app-workspace-controller.js +192 -31
  73. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  74. package/dist/browser/layout/index.js +1 -0
  75. package/dist/browser/layout/index.js.map +1 -1
  76. package/dist/browser/layout/navigation-model.js.map +1 -1
  77. package/dist/browser/layout/resize-separator.js +25 -1
  78. package/dist/browser/layout/resize-separator.js.map +1 -1
  79. package/dist/browser/layout/scroll-fade.js +32 -5
  80. package/dist/browser/layout/scroll-fade.js.map +1 -1
  81. package/dist/browser/surfaces/NoticeCard.js +46 -30
  82. package/dist/browser/surfaces/NoticeCard.js.map +1 -1
  83. package/dist/browser/surfaces/Skeleton.js +14 -0
  84. package/dist/browser/surfaces/Skeleton.js.map +1 -0
  85. package/dist/browser/surfaces/index.js +1 -0
  86. package/dist/browser/surfaces/index.js.map +1 -1
  87. package/dist/browser/surfaces/resource-messages.js +2 -2
  88. package/dist/browser/surfaces/resource-messages.js.map +1 -1
  89. package/dist/ssr/actions/Dropdown.js +51 -27
  90. package/dist/ssr/actions/Dropdown.js.map +1 -1
  91. package/dist/ssr/actions/FilterChip.js +67 -16
  92. package/dist/ssr/actions/FilterChip.js.map +1 -1
  93. package/dist/ssr/actions/ViewTabs.js +8 -0
  94. package/dist/ssr/actions/ViewTabs.js.map +1 -1
  95. package/dist/ssr/actions/filter-messages.js +26 -2
  96. package/dist/ssr/actions/filter-messages.js.map +1 -1
  97. package/dist/ssr/actions/menu-rows.js +23 -3
  98. package/dist/ssr/actions/menu-rows.js.map +1 -1
  99. package/dist/ssr/actions/menu-sheet.js +2 -7
  100. package/dist/ssr/actions/menu-sheet.js.map +1 -1
  101. package/dist/ssr/chat/ChatComposer.js +96 -67
  102. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  103. package/dist/ssr/chat/chat-messages.js +14 -0
  104. package/dist/ssr/chat/chat-messages.js.map +1 -1
  105. package/dist/ssr/chat/index.js.map +1 -1
  106. package/dist/ssr/content/Calendar.js +40 -31
  107. package/dist/ssr/content/Calendar.js.map +1 -1
  108. package/dist/ssr/content/CalendarDayView.js +40 -48
  109. package/dist/ssr/content/CalendarDayView.js.map +1 -1
  110. package/dist/ssr/content/Chart.js +27 -7
  111. package/dist/ssr/content/Chart.js.map +1 -1
  112. package/dist/ssr/content/ChartExplorer.js +5 -3
  113. package/dist/ssr/content/ChartExplorer.js.map +1 -1
  114. package/dist/ssr/content/MapView.js +4 -4
  115. package/dist/ssr/content/MapView.js.map +1 -1
  116. package/dist/ssr/content/MessageRow.js +264 -91
  117. package/dist/ssr/content/MessageRow.js.map +1 -1
  118. package/dist/ssr/content/ReactionBar.js +17 -0
  119. package/dist/ssr/content/ReactionBar.js.map +1 -1
  120. package/dist/ssr/content/chart-inspection-core.js +4 -1
  121. package/dist/ssr/content/chart-inspection-core.js.map +1 -1
  122. package/dist/ssr/content/code-highlight.js +66 -0
  123. package/dist/ssr/content/code-highlight.js.map +1 -1
  124. package/dist/ssr/content/index.js +2 -1
  125. package/dist/ssr/content/index.js.map +1 -1
  126. package/dist/ssr/content/map-view-types.js.map +1 -1
  127. package/dist/ssr/feedback/dialog-core.js +6 -0
  128. package/dist/ssr/feedback/dialog-core.js.map +1 -1
  129. package/dist/ssr/inputs/AutocompleteEditor.js +64 -24
  130. package/dist/ssr/inputs/AutocompleteEditor.js.map +1 -1
  131. package/dist/ssr/inputs/RichSelect.js +950 -0
  132. package/dist/ssr/inputs/RichSelect.js.map +1 -0
  133. package/dist/ssr/inputs/TextInput.js +44 -17
  134. package/dist/ssr/inputs/TextInput.js.map +1 -1
  135. package/dist/ssr/inputs/choice.js +19 -8
  136. package/dist/ssr/inputs/choice.js.map +1 -1
  137. package/dist/ssr/inputs/index.js +1 -0
  138. package/dist/ssr/inputs/index.js.map +1 -1
  139. package/dist/ssr/inputs/rich-select-messages.js +46 -0
  140. package/dist/ssr/inputs/rich-select-messages.js.map +1 -0
  141. package/dist/ssr/intl/shortcut-messages.js +4 -0
  142. package/dist/ssr/intl/shortcut-messages.js.map +1 -1
  143. package/dist/ssr/intl/shortcut.js +4 -0
  144. package/dist/ssr/intl/shortcut.js.map +1 -1
  145. package/dist/ssr/layout/AppWorkspace.js +166 -125
  146. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  147. package/dist/ssr/layout/DetailPanel.js +1 -1
  148. package/dist/ssr/layout/DetailPanel.js.map +1 -1
  149. package/dist/ssr/layout/MobileShell.js +100 -20
  150. package/dist/ssr/layout/MobileShell.js.map +1 -1
  151. package/dist/ssr/layout/Navigation.js +1 -1
  152. package/dist/ssr/layout/Navigation.js.map +1 -1
  153. package/dist/ssr/layout/PageTitle.js +9 -3
  154. package/dist/ssr/layout/PageTitle.js.map +1 -1
  155. package/dist/ssr/layout/ResizeSeparator.js +4 -1
  156. package/dist/ssr/layout/ResizeSeparator.js.map +1 -1
  157. package/dist/ssr/layout/app-workspace-controller.js +192 -31
  158. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  159. package/dist/ssr/layout/index.js +1 -0
  160. package/dist/ssr/layout/index.js.map +1 -1
  161. package/dist/ssr/layout/navigation-model.js.map +1 -1
  162. package/dist/ssr/layout/resize-separator.js +25 -1
  163. package/dist/ssr/layout/resize-separator.js.map +1 -1
  164. package/dist/ssr/layout/scroll-fade.js +32 -5
  165. package/dist/ssr/layout/scroll-fade.js.map +1 -1
  166. package/dist/ssr/surfaces/NoticeCard.js +21 -12
  167. package/dist/ssr/surfaces/NoticeCard.js.map +1 -1
  168. package/dist/ssr/surfaces/Skeleton.js +8 -0
  169. package/dist/ssr/surfaces/Skeleton.js.map +1 -0
  170. package/dist/ssr/surfaces/index.js +1 -0
  171. package/dist/ssr/surfaces/index.js.map +1 -1
  172. package/dist/ssr/surfaces/resource-messages.js +2 -2
  173. package/dist/ssr/surfaces/resource-messages.js.map +1 -1
  174. package/dist/styles.css +1 -1
  175. package/dist/types/actions/Dropdown.d.ts +14 -0
  176. package/dist/types/actions/FilterChip.d.ts +11 -0
  177. package/dist/types/actions/filter-messages.d.ts +12 -0
  178. package/dist/types/actions/menu-rows.d.ts +7 -2
  179. package/dist/types/chat/ChatComposer.d.ts +13 -0
  180. package/dist/types/chat/chat-messages.d.ts +7 -0
  181. package/dist/types/chat/index.d.ts +1 -1
  182. package/dist/types/content/Calendar.d.ts +7 -0
  183. package/dist/types/content/Chart.d.ts +4 -2
  184. package/dist/types/content/ChartExplorer.d.ts +0 -1
  185. package/dist/types/content/MessageRow.d.ts +64 -2
  186. package/dist/types/content/code-highlight.d.ts +3 -1
  187. package/dist/types/content/index.d.ts +3 -2
  188. package/dist/types/content/map-view-live.d.ts +1 -1
  189. package/dist/types/content/map-view-types.d.ts +2 -0
  190. package/dist/types/feedback/dialog-core.d.ts +3 -0
  191. package/dist/types/inputs/AutocompleteEditor.d.ts +6 -1
  192. package/dist/types/inputs/RichSelect.d.ts +93 -0
  193. package/dist/types/inputs/TextInput.d.ts +5 -2
  194. package/dist/types/inputs/choice.d.ts +24 -4
  195. package/dist/types/inputs/index.d.ts +2 -0
  196. package/dist/types/inputs/rich-select-messages.d.ts +21 -0
  197. package/dist/types/intl/shortcut-messages.d.ts +2 -0
  198. package/dist/types/layout/AppWorkspace.d.ts +9 -0
  199. package/dist/types/layout/DetailPanel.d.ts +2 -0
  200. package/dist/types/layout/MobileShell.d.ts +52 -5
  201. package/dist/types/layout/app-workspace-controller.d.ts +2 -0
  202. package/dist/types/layout/index.d.ts +2 -1
  203. package/dist/types/layout/navigation-model.d.ts +2 -0
  204. package/dist/types/layout/resize-separator.d.ts +1 -0
  205. package/dist/types/layout/scroll-fade.d.ts +5 -0
  206. package/dist/types/surfaces/NoticeCard.d.ts +3 -0
  207. package/dist/types/surfaces/Skeleton.d.ts +6 -0
  208. package/dist/types/surfaces/index.d.ts +1 -0
  209. package/package.json +4 -4
@@ -1,6 +1,6 @@
1
1
  import { suppressTextSelection } from "../internal/text-selection";
2
2
  import { prefersReducedMotion } from "../motion";
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";
3
+ import { APP_WORKSPACE_DETAIL_DEFAULT, APP_WORKSPACE_DETAIL_MAX, APP_WORKSPACE_DETAIL_MIN, APP_WORKSPACE_DRAWER_DEFAULT, 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_DEFAULT, APP_WORKSPACE_SIDEBAR_MAX, APP_WORKSPACE_SIDEBAR_MIN, appWorkspacePanelVariable, appWorkspaceResizeLimits, fitAppWorkspacePaneSizes, normalizeAppWorkspaceLayoutState, resolveAppWorkspaceSidebarWidth, safeAppWorkspacePanelId, shouldCollapseAppWorkspaceSidebar } from "./app-workspace-state";
4
4
  import { readAppWorkspaceWidth, resolveAppWorkspaceWidth } from "./app-workspace-width";
5
5
  import { installScrollFades } from "./scroll-fade";
6
6
  const HANDLE_SELECTOR = "[data-app-workspace-resize]";
@@ -26,7 +26,7 @@ const parkDetailDrawer = (aside, focus = false) => {
26
26
  detailDrawerContent(aside)?.style.removeProperty("--k2b-detail-space");
27
27
  const toggle = detailToggle(aside);
28
28
  toggle?.setAttribute("aria-expanded", "false");
29
- if (focus) toggle?.focus({
29
+ if (focus) detailReturnControl(aside)?.focus({
30
30
  preventScroll: true
31
31
  });
32
32
  };
@@ -66,6 +66,8 @@ const sidebarHideable = root => sidebarElement(root)?.dataset.workspaceHideable
66
66
  * stays application state, so the controller only requests the change.
67
67
  */
68
68
  export const APP_WORKSPACE_SIDEBAR_HIDE_EVENT = "k2b-app-workspace-sidebar-hide";
69
+ /** Asks the owning Detail to reopen its current available object. */
70
+ export const APP_WORKSPACE_DETAIL_OPEN_EVENT = "k2b-app-workspace-detail-open";
69
71
  /** Asks the owning `AppWorkspace.Detail` to close; its `onClose` decides. */
70
72
  export const APP_WORKSPACE_DETAIL_CLOSE_EVENT = "k2b-app-workspace-detail-close";
71
73
  /**
@@ -80,6 +82,15 @@ const previewSidebarHidden = (root, hidden) => {
80
82
  if (hidden) sidebar.dataset.workspaceHidePreview = "true";else delete sidebar.dataset.workspaceHidePreview;
81
83
  };
82
84
  const isVisible = element => Boolean(element && !element.hidden && getComputedStyle(element).display !== "none");
85
+ // A pane-bound head can be absent on a phone. The visible control in Main
86
+ // that names this same detail remains its return target, within the same root.
87
+ const detailReturnControl = aside => {
88
+ const root = workspaceRoot(aside);
89
+ if (!root) return undefined;
90
+ const shared = detailToggle(aside);
91
+ if (isVisible(shared ?? null) && shared && !shared.matches(":disabled")) return shared;
92
+ return rootElements(root, DETAIL_REQUEST_CONTROL_SELECTOR).find(control => control.matches(FOCUSABLE_SELECTOR) && !aside.contains(control) && !control.matches(":disabled, [inert]") && !control.closest("[inert]") && control.getAttribute("aria-controls")?.split(/\s+/).includes(aside.id) && control.getClientRects().length > 0);
93
+ };
83
94
  const elementSize = (element, kind) => {
84
95
  if (!isVisible(element)) return 0;
85
96
  const rect = element.getBoundingClientRect();
@@ -336,9 +347,20 @@ export const installAppWorkspaceController = (options = {}) => {
336
347
  if (detailButton) {
337
348
  const root = workspaceRoot(detailButton);
338
349
  const aside = root ? controlledPanel(root, detailButton) : null;
339
- if (!aside || aside.hidden || !detailWaiting(aside) || claimedToggleClicks.has(event)) return;
350
+ if (!aside || detailButton.matches(":disabled") || claimedToggleClicks.has(event)) return;
351
+ const reopenable = aside.dataset.workspaceDetailReopen === "true";
352
+ if (!reopenable && (aside.hidden || !detailWaiting(aside))) return;
340
353
  claimedToggleClicks.add(event);
341
- if (detailDrawer(aside)) parkDetailDrawer(aside);else {
354
+ if (aside.hidden) {
355
+ aside.dispatchEvent(new CustomEvent(APP_WORKSPACE_DETAIL_OPEN_EVENT));
356
+ // The app may reject opening (for example while navigating). Never
357
+ // override hidden ourselves or reveal retained, unavailable contents.
358
+ if (aside.hidden) return;
359
+ if (wide?.matches && detailWaiting(aside)) openDetailDrawer(aside);
360
+ if (event.detail === 0 || phone?.matches) requestTitle(aside);else detailButton.focus({
361
+ preventScroll: true
362
+ });
363
+ } else if (reopenable && (!wide?.matches || !detailWaiting(aside))) requestClose(aside);else if (detailDrawer(aside)) parkDetailDrawer(aside);else {
342
364
  openDetailDrawer(aside);
343
365
  if (event.detail === 0) requestTitle(aside);else detailButton.focus({
344
366
  preventScroll: true
@@ -435,6 +457,15 @@ export const installAppWorkspaceController = (options = {}) => {
435
457
  return rootElements(root, detailSelector).find(aside => !aside.hidden && ids?.includes(aside.id) && detailWaiting(aside)) ?? null;
436
458
  };
437
459
  const syncDetailRequestControls = () => {
460
+ for (const root of allRoots()) {
461
+ for (const aside of rootElements(root, detailSelector)) {
462
+ const toggle = detailToggle(aside);
463
+ if (toggle?.dataset.workspaceDetailReopen !== "true") continue;
464
+ const expanded = !aside.hidden && (!wide?.matches || !detailWaiting(aside) || detailDrawer(aside));
465
+ const value = String(expanded);
466
+ if (toggle.getAttribute("aria-expanded") !== value) toggle.setAttribute("aria-expanded", value);
467
+ }
468
+ }
438
469
  const waitingControls = new Map();
439
470
  for (const root of allRoots()) {
440
471
  const waitingDetails = wide?.matches ? rootElements(root, detailSelector).filter(aside => !aside.hidden && detailWaiting(aside)) : [];
@@ -509,7 +540,63 @@ export const installAppWorkspaceController = (options = {}) => {
509
540
  if (args[2] === "sidebar") scheduleHostSync();
510
541
  return size;
511
542
  };
543
+ // A responsive application control and the shared control can name the same
544
+ // Detail. Remember actual focus, not an old selection, before CSS hides it.
545
+ let detailControlFocus = null;
546
+ const controlledDetail = control => {
547
+ if (!control.matches(FOCUSABLE_SELECTOR) || control.matches(HANDLE_SELECTOR) || !eventRoot.contains(control)) return null;
548
+ const root = workspaceRoot(control);
549
+ if (!root) return null;
550
+ const ids = control.getAttribute("aria-controls")?.split(/\s+/);
551
+ const aside = rootElements(root, detailSelector).find(aside => ids?.includes(aside.id));
552
+ return aside ? {
553
+ control,
554
+ aside,
555
+ root
556
+ } : null;
557
+ };
558
+ const detailControlVisible = control => control.isConnected && control.getClientRects().length > 0 && getComputedStyle(control).visibility === "visible";
559
+ const detailControlUsable = control => detailControlVisible(control) && !control.matches(":disabled, [aria-disabled='true']") && !control.closest("[inert], [hidden], [aria-hidden='true']");
560
+ const onDetailControlFocusOut = () => {
561
+ // Explicit blur of a visible control is not a responsive focus loss.
562
+ if (detailControlFocus && detailControlVisible(detailControlFocus.control)) detailControlFocus = null;
563
+ };
564
+ const rescueDetailControlFocus = () => {
565
+ const previous = detailControlFocus;
566
+ if (!previous) return;
567
+ const {
568
+ control,
569
+ aside,
570
+ root
571
+ } = previous;
572
+ const active = ownerDocument.activeElement;
573
+ if (!control.isConnected || !aside.isConnected || workspaceRoot(control) !== root || workspaceRoot(aside) !== root || active !== ownerDocument.body && !control.contains(active)) {
574
+ detailControlFocus = null;
575
+ return;
576
+ }
577
+ const shared = detailToggle(aside);
578
+ if (shared?.dataset.workspaceDetailReopen === "true" && phone?.matches && phonePage(aside) && !aside.hidden && !aside.contains(control)) {
579
+ // WebKit can update matchMedia after resize has already focused the now
580
+ // visible application control. Move into the page before Main becomes inert.
581
+ detailControlFocus = null;
582
+ requestTitle(aside);
583
+ return;
584
+ }
585
+ if (detailControlVisible(control)) return;
586
+ detailControlFocus = null;
587
+ if (shared?.dataset.workspaceDetailReopen !== "true") {
588
+ // Selection-only details keep their existing returning-column title target.
589
+ if (control === shared && !aside.hidden && !detailWaiting(aside)) requestTitle(aside);
590
+ return;
591
+ }
592
+ const candidates = shared ? [shared, ...rootElements(root, "[aria-controls]")] : rootElements(root, "[aria-controls]");
593
+ const replacement = candidates.find(candidate => candidate !== control && detailControlUsable(candidate) && controlledDetail(candidate)?.aside === aside);
594
+ replacement?.focus({
595
+ preventScroll: true
596
+ });
597
+ };
512
598
  const syncHostState = () => {
599
+ rescueDetailControlFocus();
513
600
  if (pendingDetail && !canOpenPendingDetail(pendingDetail.aside)) cancelPendingDetail();
514
601
  if (overlay && sidebarForm(overlay.root) !== "overlay") closeOverlay("none");
515
602
  for (const root of allRoots()) {
@@ -525,11 +612,13 @@ export const installAppWorkspaceController = (options = {}) => {
525
612
  const before = detailWasWaiting.get(aside);
526
613
  detailWasWaiting.set(aside, waiting);
527
614
  if (!waiting) {
528
- // The Details button hides with the waiting; its focus moves to the returning column's title.
529
- const toggleFocused = detailToggle(aside)?.contains(ownerDocument.activeElement) ?? false;
615
+ // Rescue focus only when the legacy waiting-only button disappears.
616
+ // A persistent head control stays a valid focus target after resizing.
617
+ const toggle = detailToggle(aside);
618
+ const toggleFocused = toggle?.contains(ownerDocument.activeElement) ?? false;
530
619
  parkDetailDrawer(aside);
531
- if (toggleFocused) focusTitle(aside);
532
- } else if (detailDrawer(aside)) syncDetailDrawerSpace(aside);else if (before !== true && aside.contains(ownerDocument.activeElement)) detailToggle(aside)?.focus({
620
+ if (toggle && toggleFocused && !isVisible(toggle)) focusTitle(aside);
621
+ } else if (detailDrawer(aside)) syncDetailDrawerSpace(aside);else if (before !== true && aside.contains(ownerDocument.activeElement)) detailReturnControl(aside)?.focus({
533
622
  preventScroll: true
534
623
  });
535
624
  }
@@ -648,6 +737,8 @@ export const installAppWorkspaceController = (options = {}) => {
648
737
  const root = target?.closest(ROOT_SELECTOR);
649
738
  if (!target || !root || !observedRoots.has(root)) return;
650
739
  lastFocus.set(root, target);
740
+ const control = target.closest("[aria-controls]");
741
+ detailControlFocus = control ? controlledDetail(control) : null;
651
742
  const previous = remembered.get(root);
652
743
  // Pointer focus follows pointerdown; keeping that snapshot also keeps the
653
744
  // interaction that opened the panel rather than treating focus as a new one.
@@ -694,6 +785,7 @@ export const installAppWorkspaceController = (options = {}) => {
694
785
  // that opens later than that does not take the drawer or the focus.
695
786
  let lastChosen = null;
696
787
  const cancelTitleOnInteraction = event => {
788
+ if (detailControlFocus && !(event.target instanceof Node && detailControlFocus.control.contains(event.target))) detailControlFocus = null;
697
789
  if (event.type === "pointerdown" || event.type === "keydown") clearActivation();
698
790
  if (lastChosen && !(event.target instanceof Node && lastChosen.item.contains(event.target))) {
699
791
  lastChosen.chosen = false;
@@ -955,7 +1047,13 @@ export const installAppWorkspaceController = (options = {}) => {
955
1047
  pendingAnchors.set(root, cancel);
956
1048
  } else anchorItem(root, aside, snapshot);
957
1049
  const focused = ownerDocument.activeElement;
958
- if (snapshot?.item.isConnected && (aside.contains(lastFocus.get(root) ?? null) || aside.contains(focused) || focused === ownerDocument.body)) snapshot.item.focus({
1050
+ const toggle = detailToggle(aside);
1051
+ const returnControl = detailReturnControl(aside);
1052
+ if (toggle?.dataset.workspaceDetailReopen === "true" && returnControl && (aside.contains(focused) || focused === ownerDocument.body && aside.contains(lastFocus.get(root) ?? null))) {
1053
+ returnControl.focus({
1054
+ preventScroll: true
1055
+ });
1056
+ } else if (snapshot?.item.isConnected && (aside.contains(lastFocus.get(root) ?? null) || aside.contains(focused) || focused === ownerDocument.body)) snapshot.item.focus({
959
1057
  preventScroll: true
960
1058
  });
961
1059
  clearDrag(aside);
@@ -1232,34 +1330,86 @@ export const installAppWorkspaceController = (options = {}) => {
1232
1330
  const handle = resizeHandle(event);
1233
1331
  const root = handle?.closest(ROOT_SELECTOR);
1234
1332
  const kind = handle ? resizeKind(handle) : null;
1235
- if (!handle || !root || kind !== "detail" && kind !== "pane" || !workspaceResizable(root) || !controlsResizableRegion(root, handle, kind)) return;
1236
- const key = kind === "pane" ? "paneWidths" : "detailWidths";
1237
- const widths = {
1238
- ...layoutState[key]
1239
- };
1240
- delete widths[panelId(handle)];
1241
- layoutState = {
1242
- ...layoutState,
1243
- [key]: widths
1244
- };
1245
- const variable = appWorkspacePanelVariable(kind, panelId(handle));
1246
- root.style.removeProperty(variable);
1247
- // An SSR ancestor may still supply the old preference until the next request.
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");
1333
+ if (!handle || !root || !kind || !workspaceResizable(root) || !controlsResizableRegion(root, handle, kind)) return;
1334
+ if (active) return;
1335
+ if (kind === "sidebar") {
1336
+ const {
1337
+ sidebarWidth: _width,
1338
+ sidebarCollapsed: _collapsed,
1339
+ ...remaining
1340
+ } = layoutState;
1341
+ layoutState = remaining;
1342
+ } else {
1343
+ const key = kind === "pane" ? "paneWidths" : kind === "drawer" ? "drawerHeights" : "detailWidths";
1344
+ const widths = {
1345
+ ...layoutState[key]
1346
+ };
1347
+ delete widths[panelId(handle)];
1348
+ layoutState = {
1349
+ ...layoutState,
1350
+ [key]: widths
1351
+ };
1253
1352
  }
1254
- // The siblings fit again around the default width, also in a Main without levels.
1255
- if (kind === "pane") reconcilePanes(root, layoutState);
1353
+ const regions = resetRegions.get(root) ?? new Map();
1354
+ regions.set(resizeVariable(handle, kind), {
1355
+ kind,
1356
+ id: panelId(handle),
1357
+ configuration: handle
1358
+ });
1359
+ resetRegions.set(root, regions);
1360
+ writeState();
1361
+ if (kind === "sidebar" || kind === "pane") reconcilePanes(root, layoutState);
1256
1362
  updateHandleValue(root, handle, kind, currentSize(root, handle, kind));
1257
- options.writeState?.(layoutState);
1258
1363
  };
1259
1364
  const stopScrollFades = installScrollFades(eventRoot);
1260
1365
  let layoutState = normalizeAppWorkspaceLayoutState(options.readState?.()) ?? {
1261
1366
  version: 2
1262
1367
  };
1368
+ const resetRegions = new WeakMap();
1369
+ const resizeVariable = (handle, kind) => kind === "sidebar" ? "--k2b-workspace-sidebar-width" : appWorkspacePanelVariable(kind, panelId(handle));
1370
+ const defaultSize = (handle, kind) => readAppWorkspaceWidth(handle.dataset.workspaceDefaultSize) ?? (kind === "sidebar" ? APP_WORKSPACE_SIDEBAR_DEFAULT : kind === "pane" ? APP_WORKSPACE_PANE_DEFAULT : kind === "drawer" ? APP_WORKSPACE_DRAWER_DEFAULT : APP_WORKSPACE_DETAIL_DEFAULT);
1371
+ const restoreResetDefaults = root => {
1372
+ for (const [variable, region] of resetRegions.get(root) ?? []) {
1373
+ const {
1374
+ kind,
1375
+ id
1376
+ } = region;
1377
+ const persisted = kind === "sidebar" ? layoutState.sidebarWidth : layoutState[kind === "pane" ? "paneWidths" : kind === "drawer" ? "drawerHeights" : "detailWidths"]?.[id];
1378
+ if (persisted !== undefined || kind === "sidebar" && layoutState.sidebarCollapsed) continue;
1379
+ const handle = rootElements(root, HANDLE_SELECTOR).find(handle => resizeKind(handle) === kind && panelId(handle) === id);
1380
+ // Keep the latest handle's configuration across unmounts. Limits still
1381
+ // measure today's workspace, so a missing handle cannot freeze its fit.
1382
+ if (handle) region.configuration = handle;
1383
+ const spec = defaultSize(region.configuration, kind);
1384
+ // Store the configured default, not a temporary fit. CSS/relayout owns
1385
+ // the available space, so a numeric level can grow back when room returns.
1386
+ if (typeof spec === "number") {
1387
+ // Sidebar and drawer have no CSS fit. Keep their configured default
1388
+ // in the map, but render within today's bounds and recover it later.
1389
+ if (kind === "sidebar" || kind === "drawer") {
1390
+ const {
1391
+ min,
1392
+ max
1393
+ } = sizeLimits(root, region.configuration, kind);
1394
+ root.style.setProperty(variable, `${clamp(spec, min, max)}px`);
1395
+ } else root.style.setProperty(variable, `${spec}px`);
1396
+ } else {
1397
+ root.style.removeProperty(variable);
1398
+ if (getComputedStyle(root).getPropertyValue(variable).trim()) root.style.setProperty(variable, "initial");
1399
+ }
1400
+ if (kind === "sidebar") delete root.dataset.sidebarCollapsed;
1401
+ }
1402
+ };
1403
+ const writeState = () => {
1404
+ options.writeState?.(layoutState);
1405
+ // Keep reset defaults through reactive writes and temporary handle unmounts,
1406
+ // without saving a width that would mask a changed application default.
1407
+ for (const root of roots()) {
1408
+ if (!workspaceResizable(root)) continue;
1409
+ restoreResetDefaults(root);
1410
+ reconcilePanes(root, layoutState);
1411
+ }
1412
+ };
1263
1413
  let active = null;
1264
1414
  let resizeFrame = null;
1265
1415
  let pendingClient = null;
@@ -1281,7 +1431,9 @@ export const installAppWorkspaceController = (options = {}) => {
1281
1431
  }
1282
1432
  };
1283
1433
  }
1284
- options.writeState?.(layoutState);
1434
+ const root = workspaceRoot(handle);
1435
+ if (root) resetRegions.get(root)?.delete(resizeVariable(handle, kind));
1436
+ writeState();
1285
1437
  };
1286
1438
 
1287
1439
  // A level resize starts from what is on screen. When the stored widths do
@@ -1422,6 +1574,11 @@ export const installAppWorkspaceController = (options = {}) => {
1422
1574
  const kind = handle ? resizeKind(handle) : null;
1423
1575
  const root = handle ? workspaceRoot(handle) : null;
1424
1576
  if (!handle || !kind || !root || !workspaceResizable(root) || !controlsResizableRegion(root, handle, kind)) return;
1577
+ if (event.key === "Enter" && !event.defaultPrevented && !event.isComposing && !event.altKey && !event.ctrlKey && !event.metaKey && !active) {
1578
+ event.preventDefault();
1579
+ onPanelDoubleClick(event);
1580
+ return;
1581
+ }
1425
1582
  const current = currentSize(root, handle, kind);
1426
1583
  const {
1427
1584
  min,
@@ -1500,6 +1657,7 @@ export const installAppWorkspaceController = (options = {}) => {
1500
1657
  // resizing; without this the same workspace still had persisted sizes
1501
1658
  // forced onto it on load and on every window resize.
1502
1659
  if (!workspaceResizable(root)) return;
1660
+ restoreResetDefaults(root);
1503
1661
  const handles = rootElements(root, HANDLE_SELECTOR);
1504
1662
  // Chrome establishes the space available to the work area. Auxiliary
1505
1663
  // panes are then fitted together into what remains, so DOM order cannot
@@ -1654,6 +1812,7 @@ export const installAppWorkspaceController = (options = {}) => {
1654
1812
  ownerDocument.addEventListener("keydown", onOverlayKeyDown);
1655
1813
  ownerWindow.addEventListener("keydown", onDetailKeyDown);
1656
1814
  ownerDocument.addEventListener("focusin", cancelTitleOnInteraction, true);
1815
+ ownerDocument.addEventListener("focusout", onDetailControlFocusOut, true);
1657
1816
  ownerDocument.addEventListener("pointerdown", cancelTitleOnInteraction, true);
1658
1817
  ownerDocument.addEventListener("keydown", cancelTitleOnInteraction, true);
1659
1818
  ownerDocument.addEventListener("click", onDetailRequestClick, true);
@@ -1714,6 +1873,8 @@ export const installAppWorkspaceController = (options = {}) => {
1714
1873
  titleTabIndices.forEach(title => title.removeAttribute("tabindex"));
1715
1874
  ownerWindow.removeEventListener("keydown", onDetailKeyDown);
1716
1875
  ownerDocument.removeEventListener("focusin", cancelTitleOnInteraction, true);
1876
+ ownerDocument.removeEventListener("focusout", onDetailControlFocusOut, true);
1877
+ detailControlFocus = null;
1717
1878
  ownerDocument.removeEventListener("pointerdown", cancelTitleOnInteraction, true);
1718
1879
  ownerDocument.removeEventListener("keydown", cancelTitleOnInteraction, true);
1719
1880
  clearActivation();