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