@k2b/ui 0.22.0 → 0.24.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 (117) hide show
  1. package/dist/browser/content/Calendar.js +1122 -1065
  2. package/dist/browser/content/Calendar.js.map +1 -1
  3. package/dist/browser/content/CalendarDayView.js +1187 -0
  4. package/dist/browser/content/CalendarDayView.js.map +1 -0
  5. package/dist/browser/content/CalendarWeekView.js +1593 -0
  6. package/dist/browser/content/CalendarWeekView.js.map +1 -0
  7. package/dist/browser/content/ChartExplorer.js +174 -82
  8. package/dist/browser/content/ChartExplorer.js.map +1 -1
  9. package/dist/browser/content/ChartFilterControls.js +90 -33
  10. package/dist/browser/content/ChartFilterControls.js.map +1 -1
  11. package/dist/browser/content/DataTable.js +235 -197
  12. package/dist/browser/content/DataTable.js.map +1 -1
  13. package/dist/browser/content/calendar-day-scroll.js +111 -0
  14. package/dist/browser/content/calendar-day-scroll.js.map +1 -0
  15. package/dist/browser/content/calendar-days.js +408 -0
  16. package/dist/browser/content/calendar-days.js.map +1 -0
  17. package/dist/browser/content/calendar-dialogs.js +236 -0
  18. package/dist/browser/content/calendar-dialogs.js.map +1 -0
  19. package/dist/browser/content/calendar-messages.js +210 -2
  20. package/dist/browser/content/calendar-messages.js.map +1 -1
  21. package/dist/browser/content/calendar-pointer.js +15 -3
  22. package/dist/browser/content/calendar-pointer.js.map +1 -1
  23. package/dist/browser/content/calendar-shared.js +328 -0
  24. package/dist/browser/content/calendar-shared.js.map +1 -0
  25. package/dist/browser/content/calendar-time-view.js +1654 -0
  26. package/dist/browser/content/calendar-time-view.js.map +1 -0
  27. package/dist/browser/content/chart-messages.js +10 -0
  28. package/dist/browser/content/chart-messages.js.map +1 -1
  29. package/dist/browser/content/index.js.map +1 -1
  30. package/dist/browser/inputs/Combobox.js +0 -1
  31. package/dist/browser/inputs/Combobox.js.map +1 -1
  32. package/dist/browser/layout/AppWorkspace.js +125 -97
  33. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  34. package/dist/browser/layout/Navigation.js +18 -3
  35. package/dist/browser/layout/Navigation.js.map +1 -1
  36. package/dist/browser/layout/PageTitle.js +18 -1
  37. package/dist/browser/layout/PageTitle.js.map +1 -1
  38. package/dist/browser/layout/SidebarSwitcher.js +505 -0
  39. package/dist/browser/layout/SidebarSwitcher.js.map +1 -0
  40. package/dist/browser/layout/app-workspace-controller.js +174 -86
  41. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  42. package/dist/browser/layout/index.js +1 -0
  43. package/dist/browser/layout/index.js.map +1 -1
  44. package/dist/browser/layout/navigation-model.js +1 -0
  45. package/dist/browser/layout/navigation-model.js.map +1 -1
  46. package/dist/browser/surfaces/StatCell.js +18 -6
  47. package/dist/browser/surfaces/StatCell.js.map +1 -1
  48. package/dist/ssr/content/Calendar.js +553 -341
  49. package/dist/ssr/content/Calendar.js.map +1 -1
  50. package/dist/ssr/content/CalendarDayView.js +800 -0
  51. package/dist/ssr/content/CalendarDayView.js.map +1 -0
  52. package/dist/ssr/content/CalendarWeekView.js +1090 -0
  53. package/dist/ssr/content/CalendarWeekView.js.map +1 -0
  54. package/dist/ssr/content/ChartExplorer.js +93 -34
  55. package/dist/ssr/content/ChartExplorer.js.map +1 -1
  56. package/dist/ssr/content/ChartFilterControls.js +49 -7
  57. package/dist/ssr/content/ChartFilterControls.js.map +1 -1
  58. package/dist/ssr/content/DataTable.js +88 -66
  59. package/dist/ssr/content/DataTable.js.map +1 -1
  60. package/dist/ssr/content/calendar-day-scroll.js +111 -0
  61. package/dist/ssr/content/calendar-day-scroll.js.map +1 -0
  62. package/dist/ssr/content/calendar-days.js +408 -0
  63. package/dist/ssr/content/calendar-days.js.map +1 -0
  64. package/dist/ssr/content/calendar-dialogs.js +159 -0
  65. package/dist/ssr/content/calendar-dialogs.js.map +1 -0
  66. package/dist/ssr/content/calendar-messages.js +210 -2
  67. package/dist/ssr/content/calendar-messages.js.map +1 -1
  68. package/dist/ssr/content/calendar-pointer.js +15 -3
  69. package/dist/ssr/content/calendar-pointer.js.map +1 -1
  70. package/dist/ssr/content/calendar-shared.js +261 -0
  71. package/dist/ssr/content/calendar-shared.js.map +1 -0
  72. package/dist/ssr/content/calendar-time-view.js +1311 -0
  73. package/dist/ssr/content/calendar-time-view.js.map +1 -0
  74. package/dist/ssr/content/chart-messages.js +10 -0
  75. package/dist/ssr/content/chart-messages.js.map +1 -1
  76. package/dist/ssr/content/index.js.map +1 -1
  77. package/dist/ssr/inputs/Combobox.js.map +1 -1
  78. package/dist/ssr/layout/AppWorkspace.js +40 -29
  79. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  80. package/dist/ssr/layout/Navigation.js +12 -2
  81. package/dist/ssr/layout/Navigation.js.map +1 -1
  82. package/dist/ssr/layout/PageTitle.js +18 -1
  83. package/dist/ssr/layout/PageTitle.js.map +1 -1
  84. package/dist/ssr/layout/SidebarSwitcher.js +271 -0
  85. package/dist/ssr/layout/SidebarSwitcher.js.map +1 -0
  86. package/dist/ssr/layout/app-workspace-controller.js +174 -86
  87. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  88. package/dist/ssr/layout/index.js +1 -0
  89. package/dist/ssr/layout/index.js.map +1 -1
  90. package/dist/ssr/layout/navigation-model.js +1 -0
  91. package/dist/ssr/layout/navigation-model.js.map +1 -1
  92. package/dist/ssr/surfaces/StatCell.js +2 -2
  93. package/dist/ssr/surfaces/StatCell.js.map +1 -1
  94. package/dist/styles.css +1 -1
  95. package/dist/types/content/Calendar.d.ts +95 -1
  96. package/dist/types/content/CalendarDayView.d.ts +7 -0
  97. package/dist/types/content/CalendarWeekView.d.ts +7 -0
  98. package/dist/types/content/ChartExplorer.d.ts +36 -1
  99. package/dist/types/content/ChartFilterControls.d.ts +6 -1
  100. package/dist/types/content/DataTable.d.ts +14 -0
  101. package/dist/types/content/calendar-day-scroll.d.ts +47 -0
  102. package/dist/types/content/calendar-days.d.ts +192 -0
  103. package/dist/types/content/calendar-dialogs.d.ts +22 -0
  104. package/dist/types/content/calendar-messages.d.ts +99 -0
  105. package/dist/types/content/calendar-pointer.d.ts +9 -0
  106. package/dist/types/content/calendar-shared.d.ts +69 -0
  107. package/dist/types/content/calendar-time-view.d.ts +252 -0
  108. package/dist/types/content/chart-messages.d.ts +5 -0
  109. package/dist/types/content/index.d.ts +2 -2
  110. package/dist/types/layout/AppWorkspace.d.ts +27 -3
  111. package/dist/types/layout/PageTitle.d.ts +2 -1
  112. package/dist/types/layout/SidebarSwitcher.d.ts +65 -0
  113. package/dist/types/layout/app-workspace-controller.d.ts +9 -1
  114. package/dist/types/layout/index.d.ts +1 -0
  115. package/dist/types/layout/navigation-model.d.ts +4 -0
  116. package/dist/types/surfaces/StatCell.d.ts +5 -0
  117. package/package.json +3 -3
@@ -35,7 +35,11 @@ const sidebarHideable = root => sidebarElement(root)?.dataset.workspaceHideable
35
35
  export const APP_WORKSPACE_SIDEBAR_HIDE_EVENT = "k2b-app-workspace-sidebar-hide";
36
36
  /** Asks the owning `AppWorkspace.Detail` to close; its `onClose` decides. */
37
37
  export const APP_WORKSPACE_DETAIL_CLOSE_EVENT = "k2b-app-workspace-detail-close";
38
- /** Asks the owning `AppWorkspace.Detail` for the previous or next item, as `detail.direction` of `-1` or `1`. */
38
+ /**
39
+ * Asks the owning Detail to step by `detail.direction` (`-1` or `1`). It writes
40
+ * the Main element showing the selection to `detail.item`, read synchronously
41
+ * by the controller after dispatch.
42
+ */
39
43
  export const APP_WORKSPACE_DETAIL_STEP_EVENT = "k2b-app-workspace-detail-step";
40
44
  const previewSidebarHidden = (root, hidden) => {
41
45
  const sidebar = sidebarElement(root);
@@ -64,16 +68,17 @@ const sizeLimits = (root, handle, kind) => {
64
68
  const controlled = controlledPanel(root, handle);
65
69
  const main = kind === "pane" ? handle.closest(".k2b-app-workspace__main") : mainElement(root);
66
70
  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") && isVisible(pane)) : [];
67
- // A detail shares the content area with the main region, as the stylesheet
68
- // fits it. The area already leaves out a sidebar in its own column; as the
69
- // compact overlay or stacked above the content, the sidebar takes none of it.
71
+ // Content ends before the detail track, so its width alone would shrink a
72
+ // persisted panel again on every reconciliation. Measure from Content's
73
+ // start to the workspace's inner end: this includes the detail column and
74
+ // leaves out a desktop sidebar, while an overlay sidebar takes no width.
70
75
  const content = kind === "detail" ? controlled?.closest(".k2b-app-workspace__content") : null;
71
76
  const defaultMin = kind === "sidebar" ? APP_WORKSPACE_SIDEBAR_MIN : kind === "pane" ? APP_WORKSPACE_PANE_MIN : kind === "detail" ? APP_WORKSPACE_DETAIL_MIN : APP_WORKSPACE_DRAWER_MIN;
72
77
  const defaultMax = kind === "sidebar" ? APP_WORKSPACE_SIDEBAR_MAX : kind === "pane" ? APP_WORKSPACE_PANE_MAX : kind === "detail" ? APP_WORKSPACE_DETAIL_MAX : APP_WORKSPACE_DRAWER_MAX;
73
78
  return appWorkspaceResizeLimits({
74
79
  kind,
75
- workspaceSize: kind === "drawer" ? root.getBoundingClientRect().height : kind === "pane" && main ? main.getBoundingClientRect().width : (content ?? root).getBoundingClientRect().width,
76
- reservedSize: kind === "detail" ? 16 : kind === "pane" ? panes.reduce((total, pane) => total + (pane === controlled ? 0 : elementSize(pane, "pane")), 0) : 0,
80
+ workspaceSize: kind === "drawer" ? root.getBoundingClientRect().height : kind === "pane" && main ? main.getBoundingClientRect().width : content ? root.getBoundingClientRect().right - root.clientLeft - content.getBoundingClientRect().left : root.getBoundingClientRect().width,
81
+ reservedSize: kind === "detail" ? 8 : kind === "pane" ? panes.reduce((total, pane) => total + (pane === controlled ? 0 : elementSize(pane, "pane")), 0) : 0,
77
82
  min: numberData(handle, "workspaceMinSize", defaultMin),
78
83
  max: numberData(handle, "workspaceMaxSize", defaultMax),
79
84
  sidebarCollapsible: kind === "sidebar" && sidebarCollapsible(root)
@@ -317,18 +322,17 @@ export const installAppWorkspaceController = (options = {}) => {
317
322
  const reducedMotion = typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
318
323
  const detailSelector = ".k2b-app-workspace__detail";
319
324
  const grabberSelector = ".k2b-app-workspace__detail-grabber";
320
- const trailSelector = ".k2b-detail-trail";
321
325
  const detailHidden = new WeakMap();
322
326
  const remembered = new WeakMap();
323
327
  const lastFocus = new WeakMap();
324
- const trailPadding = new WeakMap();
325
- const heldTrails = new Set();
326
328
  const touchedDetails = new Set();
327
329
  // Only a panel blocks scrolling for its touch gestures; a document-wide
328
330
  // non-passive listener would make every scroll on the page wait for script.
329
331
  const touchMoveAsides = new Set();
330
332
  const titleTabIndices = new Set();
331
333
  const motionFrames = new Map();
334
+ const pendingAnchors = new Map();
335
+ const slides = new Map();
332
336
  const motionRoots = new Set();
333
337
  const revealFrames = new Map();
334
338
  const openDetails = root => rootElements(root, detailSelector).filter(aside => !aside.hidden);
@@ -337,17 +341,19 @@ export const installAppWorkspaceController = (options = {}) => {
337
341
  const requestClose = aside => {
338
342
  if (aside.hasAttribute("data-workspace-detail-closable")) aside.dispatchEvent(new CustomEvent(APP_WORKSPACE_DETAIL_CLOSE_EVENT));else closeControl(aside)?.click();
339
343
  };
340
- const verticalScroller = (target, boundary) => {
344
+ const scrollerOn = (axis, target, boundary) => {
341
345
  for (let element = target; element && boundary.contains(element); element = element.parentElement) {
342
- if (element instanceof HTMLElement && /^(auto|scroll)$/.test(getComputedStyle(element).overflowY) && element.scrollHeight > element.clientHeight) return element;
346
+ if (element instanceof HTMLElement && /^(auto|scroll)$/.test(axis === "y" ? getComputedStyle(element).overflowY : getComputedStyle(element).overflowX) && (axis === "y" ? element.scrollHeight > element.clientHeight : element.scrollWidth > element.clientWidth)) return element;
343
347
  if (element === boundary) break;
344
348
  }
345
349
  return null;
346
350
  };
351
+ const verticalScroller = (target, boundary) => scrollerOn("y", target, boundary);
347
352
  const refreshItem = snapshot => {
348
353
  snapshot.top = snapshot.item.getBoundingClientRect().top;
349
354
  snapshot.scrollTop = snapshot.scroller?.scrollTop ?? 0;
350
355
  };
356
+ // A table row can exceed its scroller's width, so keep the clicked cell for horizontal reveal.
351
357
  const rememberInteraction = event => {
352
358
  const target = eventElement(event);
353
359
  const root = target?.closest(ROOT_SELECTOR);
@@ -358,8 +364,10 @@ export const installAppWorkspaceController = (options = {}) => {
358
364
  const item = target.closest(FOCUSABLE_SELECTOR);
359
365
  if (!main || !item || !main.contains(item) || item.closest(ROOT_SELECTOR) !== root) return;
360
366
  if (event instanceof KeyboardEvent && event.key !== "Enter" && event.key !== " ") return;
367
+ const cell = target.closest("td, th");
361
368
  const snapshot = {
362
369
  item,
370
+ cell: cell && main.contains(cell) && cell.closest(ROOT_SELECTOR) === root ? cell : null,
363
371
  interaction: event instanceof KeyboardEvent ? "keyboard" : "pointer",
364
372
  scroller: verticalScroller(item, main),
365
373
  scrollTop: 0,
@@ -417,7 +425,12 @@ export const installAppWorkspaceController = (options = {}) => {
417
425
  pendingTitle?.observer.disconnect();
418
426
  pendingTitle = null;
419
427
  };
428
+ // A step from inside the panel can remove the focused element with the old
429
+ // content, so focus falls to the body. The panel keeps owning the steps until
430
+ // focus or a pointer goes elsewhere, so a quick second J still ends on the title.
431
+ let panelStep = null;
420
432
  const cancelTitleOnInteraction = event => {
433
+ if (event.type !== "keydown" && !(event.target instanceof Node && panelStep?.contains(event.target))) panelStep = null;
421
434
  if (event.type !== "focusin" || event.target !== focusingTitle) cancelTitle();
422
435
  };
423
436
  const requestTitle = (aside, waitForChange = false) => {
@@ -463,42 +476,104 @@ export const installAppWorkspaceController = (options = {}) => {
463
476
  aside.style.removeProperty("transform");
464
477
  delete aside.dataset.workspaceDetailDragging;
465
478
  };
466
- const trailsIn = root => {
467
- const main = mainElement(root);
468
- return main ? rootElements(root, trailSelector).filter(trail => main.contains(trail)) : [];
469
- };
470
- const clearHolds = root => {
471
- for (const trail of trailsIn(root)) {
472
- trail.style.removeProperty("--k2b-detail-trail-hold");
473
- heldTrails.delete(trail);
474
- }
475
- };
476
- const rememberTrailPadding = root => {
477
- if (!openDetails(root).some(isVisible)) return;
478
- for (const trail of trailsIn(root)) trailPadding.set(trail, Number.parseFloat(getComputedStyle(trail).paddingInlineEnd) || 0);
479
- };
480
- const shrinkHold = trail => {
481
- const hold = Number.parseFloat(trail.style.getPropertyValue("--k2b-detail-trail-hold")) || 0;
482
- const visible = trail.scrollWidth <= trail.clientWidth || trail.scrollLeft < 1 ? 0 : Math.min(hold, Math.max(0, Math.ceil(trail.scrollLeft + trail.clientWidth - (trail.scrollWidth - hold))));
483
- if (visible > 0) trail.style.setProperty("--k2b-detail-trail-hold", `${visible}px`);else {
484
- trail.style.removeProperty("--k2b-detail-trail-hold");
485
- heldTrails.delete(trail);
486
- }
487
- };
488
- const onTrailScroll = event => {
489
- const target = eventElement(event);
490
- if (target instanceof HTMLElement && heldTrails.has(target)) shrinkHold(target);
491
- };
492
479
  const anchorItem = (root, aside, snapshot) => {
493
- if (!wide?.matches || touchDrawer?.matches && closable(aside) || trailsIn(root).length || !snapshot?.item.isConnected) return;
480
+ if (!wide?.matches || touchDrawer?.matches && closable(aside) || !snapshot?.item.isConnected) return;
494
481
  if (!mainElement(root)?.contains(snapshot.item) || snapshot.item.closest(ROOT_SELECTOR) !== root) return;
495
482
  const scroller = snapshot.scroller;
496
483
  if (scroller?.isConnected && scroller.scrollTop === snapshot.scrollTop) scroller.scrollTop += snapshot.item.getBoundingClientRect().top - snapshot.top;
497
484
  };
485
+ // From 64 rem the workspace does not clip, so the panel's shadow shows on
486
+ // every edge. A panel sliding in or out would cross a framed workspace's
487
+ // edge, so the workspace clips while the panel moves (see the stylesheet).
488
+ const slide = (root, aside) => {
489
+ slides.get(root)?.();
490
+ if (!root.hasAttribute("data-workspace-motion") || reducedMotion?.matches || !wide?.matches) return;
491
+ const end = () => {
492
+ aside.removeEventListener("transitionend", onEnd);
493
+ aside.removeEventListener("transitioncancel", onEnd);
494
+ cancelAnimationFrame(frame);
495
+ slides.delete(root);
496
+ delete root.dataset.workspaceDetailSliding;
497
+ };
498
+ const onEnd = event => {
499
+ if (event.target === aside && event.propertyName === "transform") end();
500
+ };
501
+ aside.addEventListener("transitionend", onEnd);
502
+ aside.addEventListener("transitioncancel", onEnd);
503
+ // A slow first frame must not end the clip early, so only a panel that
504
+ // does not slide at all ends it in that frame.
505
+ const frame = requestAnimationFrame(() => {
506
+ const sliding = aside.getAnimations?.().some(animation => "transitionProperty" in animation && animation.transitionProperty === "transform");
507
+ if (!sliding) end();
508
+ });
509
+ slides.set(root, end);
510
+ root.dataset.workspaceDetailSliding = "";
511
+ };
512
+ const revealItem = (root, aside, snapshot) => {
513
+ const pending = revealFrames.get(root);
514
+ if (pending !== undefined) cancelAnimationFrame(pending);
515
+ revealFrames.set(root, requestAnimationFrame(() => {
516
+ revealFrames.delete(root);
517
+ if (!aside.hidden && aside.isConnected && snapshot.item.isConnected && mainElement(root)?.contains(snapshot.item) && snapshot.item.closest(ROOT_SELECTOR) === root) {
518
+ const behavior = reducedMotion?.matches ? "auto" : "smooth";
519
+ const main = mainElement(root);
520
+ const cell = snapshot.cell;
521
+ const target = cell?.isConnected && main?.contains(cell) ? cell : snapshot.item;
522
+ if (phone?.matches && main) {
523
+ // The sheet covers Main's lower part, so the item ends 8px above it;
524
+ // the other edges include the scroller's scroll padding, like scrollIntoView.
525
+ const page = ownerDocument.scrollingElement;
526
+ const vertical = verticalScroller(target, main) ?? page;
527
+ const horizontal = scrollerOn("x", target, main) ?? page;
528
+ const bounds = scroller => {
529
+ if (!scroller || scroller === page) return null;
530
+ const rect = scroller.getBoundingClientRect();
531
+ const style = ownerWindow.getComputedStyle(scroller);
532
+ const padding = value => Number.parseFloat(value) || 0;
533
+ return {
534
+ top: rect.top + padding(style.scrollPaddingTop),
535
+ left: rect.left + padding(style.scrollPaddingLeft),
536
+ right: rect.right - padding(style.scrollPaddingRight)
537
+ };
538
+ };
539
+ const box = target.getBoundingClientRect();
540
+ const top = Math.max(0, bounds(vertical)?.top ?? 0);
541
+ const left = Math.max(0, bounds(horizontal)?.left ?? 0);
542
+ const right = Math.min(ownerWindow.innerWidth, bounds(horizontal)?.right ?? Infinity);
543
+ const dy = box.top < top ? box.top - top - 8 : Math.max(0, box.bottom - (ownerWindow.innerHeight - aside.offsetHeight) + 8);
544
+ // As with scrollIntoView's "nearest", an item past both edges stays put
545
+ // and one wider than the view keeps its start in view.
546
+ const dx = box.left < left && box.right > right ? 0 : box.left < left ? box.left - left - 8 : box.right > right ? Math.min(box.right - right + 8, Math.max(0, box.left - left - 8)) : 0;
547
+ if (vertical && vertical === horizontal) {
548
+ if (dy || dx) vertical.scrollBy({
549
+ top: dy,
550
+ left: dx,
551
+ behavior
552
+ });
553
+ } else {
554
+ if (dy) vertical?.scrollBy({
555
+ top: dy,
556
+ behavior
557
+ });
558
+ if (dx) horizontal?.scrollBy({
559
+ left: dx,
560
+ behavior
561
+ });
562
+ }
563
+ } else target.scrollIntoView({
564
+ block: "nearest",
565
+ inline: "nearest",
566
+ behavior
567
+ });
568
+ }
569
+ }));
570
+ };
498
571
  const detailTransition = (aside, wasHidden) => {
499
572
  const root = aside.closest(ROOT_SELECTOR);
500
573
  detailHidden.set(aside, Boolean(aside.hidden));
501
574
  if (!root || !observedRoots.has(root) || Boolean(aside.hidden) === wasHidden) return;
575
+ pendingAnchors.get(root)?.();
576
+ slide(root, aside);
502
577
  touchedDetails.add(aside);
503
578
  const pending = revealFrames.get(root);
504
579
  if (pending !== undefined) cancelAnimationFrame(pending);
@@ -508,18 +583,25 @@ export const installAppWorkspaceController = (options = {}) => {
508
583
  if (snapshot) snapshot.fresh = false;
509
584
  if (aside.hidden) {
510
585
  if (pendingTitle?.aside === aside) cancelTitle();
511
- const trails = trailsIn(root);
512
- // Restore all padding before the first layout read, or the browser can
513
- // clamp scrollLeft as soon as the open panel's trailing space disappears.
514
- for (const trail of trails) {
515
- const hold = trailPadding.get(trail) ?? 0;
516
- if (hold > 0) {
517
- trail.style.setProperty("--k2b-detail-trail-hold", `${hold}px`);
518
- heldTrails.add(trail);
519
- }
520
- }
521
- trails.forEach(shrinkHold);
522
- anchorItem(root, aside, snapshot);
586
+ if (panelStep === aside) panelStep = null;
587
+ if (root.hasAttribute("data-workspace-motion") && !reducedMotion?.matches && wide?.matches) {
588
+ // Closing holds the detail column until the slide ends; anchor after Main widens.
589
+ const cancel = () => {
590
+ root.removeEventListener("transitionend", onTransitionEnd);
591
+ clearTimeout(timer);
592
+ pendingAnchors.delete(root);
593
+ };
594
+ const finish = () => {
595
+ cancel();
596
+ if (aside.hidden && aside.isConnected) anchorItem(root, aside, snapshot);
597
+ };
598
+ const onTransitionEnd = event => {
599
+ if (event.target === root && event.propertyName === "--k2b-workspace-detail-track") finish();
600
+ };
601
+ const timer = setTimeout(finish, 300);
602
+ root.addEventListener("transitionend", onTransitionEnd);
603
+ pendingAnchors.set(root, cancel);
604
+ } else anchorItem(root, aside, snapshot);
523
605
  const focused = ownerDocument.activeElement;
524
606
  if (snapshot?.item.isConnected && (aside.contains(lastFocus.get(root) ?? null) || aside.contains(focused) || focused === ownerDocument.body)) snapshot.item.focus({
525
607
  preventScroll: true
@@ -528,30 +610,10 @@ export const installAppWorkspaceController = (options = {}) => {
528
610
  if (detailDrag?.aside === aside) finishDetailDrag(true);
529
611
  return;
530
612
  }
531
- clearHolds(root);
532
613
  anchorItem(root, aside, snapshot);
533
614
  if (phone?.matches) setDetent(aside, false);
534
- rememberTrailPadding(root);
535
615
  if (followedInteraction && snapshot) {
536
- revealFrames.set(root, requestAnimationFrame(() => {
537
- revealFrames.delete(root);
538
- if (!aside.hidden && aside.isConnected && snapshot.item.isConnected && mainElement(root)?.contains(snapshot.item) && snapshot.item.closest(ROOT_SELECTOR) === root) {
539
- const behavior = reducedMotion?.matches ? "auto" : "smooth";
540
- if (phone?.matches) {
541
- const sheetTop = ownerWindow.innerHeight - aside.offsetHeight;
542
- const overlap = snapshot.item.getBoundingClientRect().bottom - sheetTop + 8;
543
- const main = mainElement(root);
544
- if (overlap > 0 && main) (verticalScroller(snapshot.item, main) ?? ownerDocument.scrollingElement)?.scrollBy({
545
- top: overlap,
546
- behavior
547
- });
548
- } else snapshot.item.scrollIntoView({
549
- block: "nearest",
550
- inline: "nearest",
551
- behavior
552
- });
553
- }
554
- }));
616
+ revealItem(root, aside, snapshot);
555
617
  if (snapshot.interaction === "keyboard" || phone?.matches) requestTitle(aside);
556
618
  }
557
619
  };
@@ -575,11 +637,45 @@ export const installAppWorkspaceController = (options = {}) => {
575
637
  }
576
638
  const owningRoot = aside?.closest(ROOT_SELECTOR);
577
639
  if (!aside || !owningRoot) return;
578
- if (event.key === "Escape" && closable(aside)) requestClose(aside);else if ((event.key.toLowerCase() === "j" || event.key.toLowerCase() === "k") && aside.hasAttribute("data-workspace-detail-steppable")) aside.dispatchEvent(new CustomEvent(APP_WORKSPACE_DETAIL_STEP_EVENT, {
579
- detail: {
640
+ if (event.key === "Escape" && closable(aside)) requestClose(aside);else if ((event.key.toLowerCase() === "j" || event.key.toLowerCase() === "k") && aside.hasAttribute("data-workspace-detail-steppable")) {
641
+ const main = mainElement(owningRoot);
642
+ const inMain = remembered.get(owningRoot)?.item.contains(target) || main?.contains(target);
643
+ const inPanel = aside.contains(target) || target === ownerDocument.body && panelStep === aside;
644
+ panelStep = inPanel ? aside : null;
645
+ const previousTitle = inPanel ? titleIn(aside) : null;
646
+ const previousText = previousTitle?.textContent;
647
+ const detail = {
580
648
  direction: event.key.toLowerCase() === "j" ? 1 : -1
649
+ };
650
+ aside.dispatchEvent(new CustomEvent(APP_WORKSPACE_DETAIL_STEP_EVENT, {
651
+ detail
652
+ }));
653
+ const shown = detail.item;
654
+ const item = shown instanceof HTMLElement && main?.contains(shown) && shown.closest(ROOT_SELECTOR) === owningRoot ? shown : null;
655
+ if (item && main) {
656
+ const snapshot = {
657
+ item,
658
+ cell: null,
659
+ interaction: "keyboard",
660
+ scroller: verticalScroller(item, main),
661
+ scrollTop: 0,
662
+ top: 0,
663
+ fresh: false
664
+ };
665
+ refreshItem(snapshot);
666
+ remembered.set(owningRoot, snapshot);
667
+ revealItem(owningRoot, aside, snapshot);
581
668
  }
582
- }));else if (event.key === "F6") {
669
+ const title = titleIn(aside);
670
+ if (item && inMain) item.focus({
671
+ preventScroll: true
672
+ });
673
+ // A panel that swapped its title during the event has the new one now.
674
+ else if (item && inPanel && title && (title !== previousTitle || title.textContent !== previousText)) focusTitle(aside);
675
+ // This key's capture cancelled an earlier step's title request, so a
676
+ // step from the body, even past the last item, asks again.
677
+ else if (inPanel && (item || target === ownerDocument.body)) requestTitle(aside, Boolean(item));
678
+ } else if (event.key === "F6") {
583
679
  if (aside.contains(target)) {
584
680
  const item = remembered.get(owningRoot)?.item;
585
681
  const main = mainElement(owningRoot);
@@ -781,7 +877,6 @@ export const installAppWorkspaceController = (options = {}) => {
781
877
  root.style.removeProperty(variable);
782
878
  if (getComputedStyle(root).getPropertyValue(variable).trim()) root.style.setProperty(variable, `${numberData(handle, "workspaceDefaultSize", APP_WORKSPACE_DETAIL_DEFAULT)}px`);
783
879
  updateHandleValue(root, handle, "detail", currentSize(root, handle, "detail"));
784
- rememberTrailPadding(root);
785
880
  options.writeState?.(layoutState);
786
881
  };
787
882
  const stopScrollFades = installScrollFades(eventRoot);
@@ -863,7 +958,6 @@ export const installAppWorkspaceController = (options = {}) => {
863
958
  const size = finished.kind === "pane" ? finished.liveSize : applySize(finished.root, finished.handle, finished.kind, finished.liveSize);
864
959
  persistSize(finished.handle, finished.kind, size);
865
960
  if (finished.kind !== "drawer") reconcilePanes(finished.root, layoutState);
866
- if (finished.kind === "detail") rememberTrailPadding(finished.root);
867
961
  }
868
962
  if (finished.handle.hasPointerCapture?.(finished.pointerId)) finished.handle.releasePointerCapture(finished.pointerId);
869
963
  finished.releaseTextSelection();
@@ -929,7 +1023,6 @@ export const installAppWorkspaceController = (options = {}) => {
929
1023
  event.preventDefault();
930
1024
  persistSize(handle, kind, applySize(root, handle, kind, requested));
931
1025
  if (kind === "sidebar" || kind === "detail") reconcilePanes(root, layoutState);
932
- if (kind === "detail") rememberTrailPadding(root);
933
1026
  };
934
1027
  const allRoots = () => {
935
1028
  const nested = Array.from(eventRoot.querySelectorAll(ROOT_SELECTOR));
@@ -949,7 +1042,6 @@ export const installAppWorkspaceController = (options = {}) => {
949
1042
  if (aside.dataset.workspaceDetailSheet === "full") setDetent(aside, false);
950
1043
  }
951
1044
  }
952
- rememberTrailPadding(root);
953
1045
  });
954
1046
  roots().forEach(root => {
955
1047
  // `data-workspace-resizable="false"` already refuses pointer and keyboard
@@ -974,7 +1066,6 @@ export const installAppWorkspaceController = (options = {}) => {
974
1066
  });
975
1067
  reconcilePanes(root, layoutState, active?.root === root ? active : null);
976
1068
  });
977
- heldTrails.forEach(shrinkHold);
978
1069
  };
979
1070
  /**
980
1071
  * A handle's limits move whenever anything around it does — opening a detail
@@ -1036,6 +1127,8 @@ export const installAppWorkspaceController = (options = {}) => {
1036
1127
  const next = new Set(allRoots());
1037
1128
  observedRoots.forEach(root => {
1038
1129
  if (!next.has(root)) {
1130
+ pendingAnchors.get(root)?.();
1131
+ slides.get(root)?.();
1039
1132
  resizeObserver?.unobserve(root);
1040
1133
  const frame = motionFrames.get(root);
1041
1134
  if (frame !== undefined) cancelAnimationFrame(frame);
@@ -1069,7 +1162,6 @@ export const installAppWorkspaceController = (options = {}) => {
1069
1162
  }));
1070
1163
  }));
1071
1164
  }
1072
- rememberTrailPadding(root);
1073
1165
  });
1074
1166
  };
1075
1167
  const layoutNode = node => node instanceof Element && (node.matches(LAYOUT_REGION_SELECTOR) || Boolean(node.querySelector(LAYOUT_REGION_SELECTOR)));
@@ -1099,10 +1191,6 @@ export const installAppWorkspaceController = (options = {}) => {
1099
1191
  eventRoot.addEventListener("pointerdown", rememberInteraction, true);
1100
1192
  eventRoot.addEventListener("keydown", rememberInteraction, true);
1101
1193
  eventRoot.addEventListener("focusin", onDetailFocusIn);
1102
- eventRoot.addEventListener("scroll", onTrailScroll, {
1103
- passive: true,
1104
- capture: true
1105
- });
1106
1194
  eventRoot.addEventListener("pointerdown", onSheetPointerDown);
1107
1195
  eventRoot.addEventListener("click", onGrabberClick);
1108
1196
  eventRoot.addEventListener("dblclick", onDetailDoubleClick);
@@ -1131,13 +1219,14 @@ export const installAppWorkspaceController = (options = {}) => {
1131
1219
  scheduleReconcile();
1132
1220
  return () => {
1133
1221
  cancelTitle();
1222
+ pendingAnchors.forEach(cancel => cancel());
1223
+ slides.forEach(end => end());
1134
1224
  stopScrollFades();
1135
1225
  finishDetailDrag(true);
1136
1226
  if (suppressedClickTimer !== null) clearTimeout(suppressedClickTimer);
1137
1227
  motionFrames.forEach(cancelAnimationFrame);
1138
1228
  revealFrames.forEach(cancelAnimationFrame);
1139
1229
  motionRoots.forEach(root => delete root.dataset.workspaceMotion);
1140
- heldTrails.forEach(trail => trail.style.removeProperty("--k2b-detail-trail-hold"));
1141
1230
  touchedDetails.forEach(aside => {
1142
1231
  clearDrag(aside);
1143
1232
  delete aside.dataset.workspaceDetailSheet;
@@ -1151,7 +1240,6 @@ export const installAppWorkspaceController = (options = {}) => {
1151
1240
  eventRoot.removeEventListener("pointerdown", rememberInteraction, true);
1152
1241
  eventRoot.removeEventListener("keydown", rememberInteraction, true);
1153
1242
  eventRoot.removeEventListener("focusin", onDetailFocusIn);
1154
- eventRoot.removeEventListener("scroll", onTrailScroll, true);
1155
1243
  eventRoot.removeEventListener("pointerdown", onSheetPointerDown);
1156
1244
  eventRoot.removeEventListener("click", onGrabberClick);
1157
1245
  eventRoot.removeEventListener("dblclick", onDetailDoubleClick);