@k2b/ui 0.22.0 → 0.23.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 (98) hide show
  1. package/dist/browser/content/Calendar.js +1086 -1049
  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 +1571 -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 +199 -171
  12. package/dist/browser/content/DataTable.js.map +1 -1
  13. package/dist/browser/content/calendar-day-scroll.js +98 -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 +1649 -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 +123 -97
  33. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  34. package/dist/browser/layout/PageTitle.js +4 -1
  35. package/dist/browser/layout/PageTitle.js.map +1 -1
  36. package/dist/browser/layout/app-workspace-controller.js +164 -86
  37. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  38. package/dist/browser/surfaces/StatCell.js +18 -6
  39. package/dist/browser/surfaces/StatCell.js.map +1 -1
  40. package/dist/ssr/content/Calendar.js +524 -335
  41. package/dist/ssr/content/Calendar.js.map +1 -1
  42. package/dist/ssr/content/CalendarDayView.js +800 -0
  43. package/dist/ssr/content/CalendarDayView.js.map +1 -0
  44. package/dist/ssr/content/CalendarWeekView.js +1069 -0
  45. package/dist/ssr/content/CalendarWeekView.js.map +1 -0
  46. package/dist/ssr/content/ChartExplorer.js +93 -34
  47. package/dist/ssr/content/ChartExplorer.js.map +1 -1
  48. package/dist/ssr/content/ChartFilterControls.js +49 -7
  49. package/dist/ssr/content/ChartFilterControls.js.map +1 -1
  50. package/dist/ssr/content/DataTable.js +69 -51
  51. package/dist/ssr/content/DataTable.js.map +1 -1
  52. package/dist/ssr/content/calendar-day-scroll.js +98 -0
  53. package/dist/ssr/content/calendar-day-scroll.js.map +1 -0
  54. package/dist/ssr/content/calendar-days.js +408 -0
  55. package/dist/ssr/content/calendar-days.js.map +1 -0
  56. package/dist/ssr/content/calendar-dialogs.js +159 -0
  57. package/dist/ssr/content/calendar-dialogs.js.map +1 -0
  58. package/dist/ssr/content/calendar-messages.js +210 -2
  59. package/dist/ssr/content/calendar-messages.js.map +1 -1
  60. package/dist/ssr/content/calendar-pointer.js +15 -3
  61. package/dist/ssr/content/calendar-pointer.js.map +1 -1
  62. package/dist/ssr/content/calendar-shared.js +261 -0
  63. package/dist/ssr/content/calendar-shared.js.map +1 -0
  64. package/dist/ssr/content/calendar-time-view.js +1306 -0
  65. package/dist/ssr/content/calendar-time-view.js.map +1 -0
  66. package/dist/ssr/content/chart-messages.js +10 -0
  67. package/dist/ssr/content/chart-messages.js.map +1 -1
  68. package/dist/ssr/content/index.js.map +1 -1
  69. package/dist/ssr/inputs/Combobox.js.map +1 -1
  70. package/dist/ssr/layout/AppWorkspace.js +38 -29
  71. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  72. package/dist/ssr/layout/PageTitle.js +4 -1
  73. package/dist/ssr/layout/PageTitle.js.map +1 -1
  74. package/dist/ssr/layout/app-workspace-controller.js +164 -86
  75. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  76. package/dist/ssr/surfaces/StatCell.js +2 -2
  77. package/dist/ssr/surfaces/StatCell.js.map +1 -1
  78. package/dist/styles.css +1 -1
  79. package/dist/types/content/Calendar.d.ts +91 -0
  80. package/dist/types/content/CalendarDayView.d.ts +7 -0
  81. package/dist/types/content/CalendarWeekView.d.ts +7 -0
  82. package/dist/types/content/ChartExplorer.d.ts +36 -1
  83. package/dist/types/content/ChartFilterControls.d.ts +6 -1
  84. package/dist/types/content/DataTable.d.ts +5 -0
  85. package/dist/types/content/calendar-day-scroll.d.ts +42 -0
  86. package/dist/types/content/calendar-days.d.ts +192 -0
  87. package/dist/types/content/calendar-dialogs.d.ts +22 -0
  88. package/dist/types/content/calendar-messages.d.ts +99 -0
  89. package/dist/types/content/calendar-pointer.d.ts +9 -0
  90. package/dist/types/content/calendar-shared.d.ts +69 -0
  91. package/dist/types/content/calendar-time-view.d.ts +252 -0
  92. package/dist/types/content/chart-messages.d.ts +5 -0
  93. package/dist/types/content/index.d.ts +2 -2
  94. package/dist/types/layout/AppWorkspace.d.ts +25 -3
  95. package/dist/types/layout/PageTitle.d.ts +2 -1
  96. package/dist/types/layout/app-workspace-controller.d.ts +9 -1
  97. package/dist/types/surfaces/StatCell.d.ts +5 -0
  98. package/package.json +1 -1
@@ -199,7 +199,8 @@ export function PageTitle(props) {
199
199
 
200
200
  /**
201
201
  * Keeps server-rendered titles of the host's trail current: a workspace head
202
- * that never hydrates, and an app's own header title from the host's trail.
202
+ * that never hydrates, its heading, and an app's own header title from the
203
+ * host's trail.
203
204
  * When the host publishes a new trail, for example after `setPageTrail()`,
204
205
  * each such title is replaced in place by a live one with the same element,
205
206
  * class, and subtitle, and a head learns whether it now has a page above it.
@@ -241,6 +242,8 @@ export const followPageTrail = (root = document) => {
241
242
  return dispose;
242
243
  }));
243
244
  }
245
+ // A server-rendered workspace heading carries only the title.
246
+ for (const heading of root.querySelectorAll("[data-k2b-page-trail-title][data-k2b-server]")) if (trail && !heading.closest("solid-island, solid-client")) heading.textContent = trail.title;
244
247
  // The row's `data-trail` decides whether it shows; it follows the depth.
245
248
  for (const row of root.querySelectorAll("[data-trail]:has(> .k2b-page-title[data-fallback])")) if (!row.closest("solid-island, solid-client")) row.dataset.trail = trail?.ancestors.length ? "up" : "title";
246
249
  };
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["layout/PageTitle.tsx"],"sourcesContent":["import { createContext, createRoot, createSignal, createUniqueId, For, type JSX, onCleanup, onMount, Show, useContext } from \"solid-js\";\nimport { Dynamic, insert, isServer } from \"solid-js/web\";\nimport { IconButtonLink } from \"../actions/Button\";\nimport { installPageTitleController, PAGE_TITLE_CONTROLLER_ATTRIBUTE } from \"./page-title-controller\";\nimport { useWorkspaceMessages } from \"./workspace-messages\";\n\n/** A page above the current one. Only pages someone can go back to are levels, so each has a link. */\nexport type PageTrailLink = { title: string; href: string };\n/** The current page and the pages above it, nearest last. */\nexport type PageTrailState = { title: string; ancestors: PageTrailLink[] };\n\nexport type PageTitleProps = {\n trail: PageTrailState;\n /** Quiet text after the title, for example a count. */\n subtitle?: JSX.Element;\n /** Defaults to `\"h1\"`. Chrome that repeats a title the page already shows uses `\"p\"`. */\n as?: \"h1\" | \"h2\" | \"p\";\n /**\n * Marks a title drawn by surrounding chrome, such as the workspace head or a\n * host shell. It steps back when the page renders its own `PageTitle`, so a\n * page has one title with one way up.\n */\n fallback?: boolean;\n class?: string;\n};\n\n/** Window event a host dispatches after publishing a new page trail. */\nexport const PAGE_TRAIL_EVENT = \"k2b:page-trail\";\nconst SOURCE_SELECTOR = \"script[data-k2b-page-trail]\";\n\nconst isTrail = (value: unknown): value is PageTrailState => {\n if (!value || typeof value !== \"object\") return false;\n const trail = value as Partial<PageTrailState>;\n return (\n typeof trail.title === \"string\" &&\n Array.isArray(trail.ancestors) &&\n trail.ancestors.every((link) => typeof link?.title === \"string\" && typeof link?.href === \"string\")\n );\n};\n\n/** The trail a host published for this document, or `undefined` outside a browser or without one. */\nexport const readPageTrail = (): PageTrailState | undefined => {\n if (typeof document === \"undefined\") return undefined;\n const source = document.querySelector(SOURCE_SELECTOR);\n if (!source?.textContent) return undefined;\n try {\n const value: unknown = JSON.parse(source.textContent);\n return isTrail(value) ? value : undefined;\n } catch {\n return undefined;\n }\n};\n\n/** `<` is escaped so a title can never end the script element it travels in. */\nconst serializeTrail = (trail: PageTrailState): string => JSON.stringify(trail).replace(/</g, \"\\\\u003c\");\n\n/**\n * Publishes a new trail for the document in the browser, for example after an\n * in-page navigation. Workspace heads and page titles that read the host's\n * trail follow it.\n */\nexport const setPageTrail = (trail: PageTrailState): void => {\n if (typeof document === \"undefined\") return;\n let source = document.querySelector(SOURCE_SELECTOR);\n if (!source) {\n source = document.createElement(\"script\");\n source.setAttribute(\"type\", \"application/json\");\n source.setAttribute(\"data-k2b-page-trail\", \"\");\n document.body.append(source);\n }\n source.textContent = serializeTrail(trail);\n window.dispatchEvent(new Event(PAGE_TRAIL_EVENT));\n};\n\nconst PageTrailContext = createContext<() => PageTrailState | undefined>();\n\n/**\n * Gives a host's page trail to everything below it. During server rendering\n * the context carries it; the provider also publishes it into the document,\n * where browser-rendered parts such as islands read it. Below a provider that\n * renders in the browser, its `trail` prop is the source: update the prop.\n */\nexport function PageTrailProvider(props: { trail: PageTrailState | undefined; children: JSX.Element }): JSX.Element {\n return (\n <PageTrailContext.Provider value={() => props.trail}>\n <Show when={props.trail}>\n {(trail) => <script type=\"application/json\" data-k2b-page-trail=\"\" innerHTML={serializeTrail(trail())} />}\n </Show>\n {props.children}\n </PageTrailContext.Provider>\n );\n}\n\n/** The host's page trail: from the provider while rendering below it, otherwise from the document. */\nexport const usePageTrail = (): (() => PageTrailState | undefined) => {\n const context = useContext(PageTrailContext);\n if (context) return context;\n if (typeof window === \"undefined\") return () => undefined;\n const [trail, setTrail] = createSignal(readPageTrail());\n const update = () => setTrail(readPageTrail());\n window.addEventListener(PAGE_TRAIL_EVENT, update);\n onCleanup(() => window.removeEventListener(PAGE_TRAIL_EVENT, update));\n return trail;\n};\n\n/**\n * A page title with its way up. The number of pages above decides the form:\n * none shows the title alone; one puts \"‹ Parent\" above it; more put a path\n * row above it whose middle collapses into \"…\" only when it does not fit.\n * Both rows share the same height, so the title never moves between depths.\n * On phones a back arrow left of the title replaces the row above.\n */\nexport function PageTitle(props: PageTitleProps): JSX.Element {\n const messages = useWorkspaceMessages();\n // A title rendered on the server from the host's trail follows the host's later trails (see `followPageTrail`).\n const host = isServer ? useContext(PageTrailContext) : undefined;\n const menuId = `k2b-page-title-menu-${createUniqueId()}`;\n const ancestors = () => props.trail.ancestors;\n const parent = () => ancestors().at(-1);\n let root: HTMLDivElement | undefined;\n // A host may control every title in the document at once; a title outside\n // one still collapses its path.\n onMount(() => {\n if (!root || root.closest(`[${PAGE_TITLE_CONTROLLER_ATTRIBUTE}]`)) return;\n onCleanup(installPageTitleController(root));\n });\n return (\n <div\n ref={root}\n class={`k2b-page-title ${props.class ?? \"\"}`}\n data-depth={ancestors().length}\n data-fallback={props.fallback ? \"\" : undefined}\n data-k2b-server={isServer && props.trail === host?.() ? \"\" : undefined}\n >\n <Show when={parent()}>\n {(up) => (\n <IconButtonLink\n class=\"k2b-page-title__back\"\n href={up().href}\n label={messages().backTo({ page: up().title })}\n data-k2b-page-trail-link=\"\"\n data-k2b-page-trail-up=\"\"\n >\n <i class=\"ti ti-chevron-left\" aria-hidden=\"true\" />\n </IconButtonLink>\n )}\n </Show>\n <div class=\"k2b-page-title__copy\">\n <Show when={ancestors().length === 1 && parent()}>\n {(up) => (\n <nav class=\"k2b-page-title__trail\" aria-label={messages().path}>\n <a\n class=\"k2b-page-title__up\"\n href={up().href}\n aria-label={messages().backTo({ page: up().title })}\n data-k2b-page-trail-link=\"\"\n data-k2b-page-trail-up=\"\"\n >\n <i class=\"ti ti-chevron-left\" aria-hidden=\"true\" />\n <span class=\"k2b-page-title__label\">{up().title}</span>\n </a>\n </nav>\n )}\n </Show>\n <Show when={ancestors().length > 1}>\n <nav class=\"k2b-page-title__trail\" aria-label={messages().path}>\n <ol class=\"k2b-page-title__path\">\n <For each={ancestors()}>\n {(link, index) => (\n <>\n <Show when={index() === 1}>\n <li class=\"k2b-page-title__more\" hidden>\n <i class=\"ti ti-chevron-right\" aria-hidden=\"true\" />\n <button type=\"button\" class=\"k2b-page-title__more-button\" popovertarget={menuId} aria-label={messages().moreLevels}>\n …\n </button>\n </li>\n </Show>\n <li class=\"k2b-page-title__crumb\" data-index={index()}>\n <Show when={index() > 0}>\n <i class=\"ti ti-chevron-right\" aria-hidden=\"true\" />\n </Show>\n <a\n href={link.href}\n data-k2b-page-trail-link=\"\"\n data-k2b-page-trail-up={index() === ancestors().length - 1 ? \"\" : undefined}\n class=\"k2b-page-title__label\"\n >\n {link.title}\n </a>\n </li>\n </>\n )}\n </For>\n </ol>\n <div id={menuId} popover=\"auto\" class=\"k2b-dropdown__menu k2b-page-title__menu\">\n <For each={ancestors()}>\n {(link, index) => (\n <a class=\"k2b-dropdown__item\" href={link.href} data-index={index()} data-k2b-page-trail-link=\"\" hidden>\n {link.title}\n </a>\n )}\n </For>\n </div>\n </nav>\n </Show>\n <Dynamic component={props.as ?? \"h1\"} class=\"k2b-page-title__title\">\n <span class=\"k2b-page-title__text\">{props.trail.title}</span>\n <Show when={props.subtitle}>\n <small class=\"k2b-page-title__subtitle\">{props.subtitle}</small>\n </Show>\n </Dynamic>\n </div>\n </div>\n );\n}\n\n/**\n * Keeps server-rendered titles of the host's trail current: a workspace head\n * that never hydrates, and an app's own header title from the host's trail.\n * When the host publishes a new trail, for example after `setPageTrail()`,\n * each such title is replaced in place by a live one with the same element,\n * class, and subtitle, and a head learns whether it now has a page above it.\n * Titles inside islands render in the browser anyway. Returns the disposer.\n */\nexport const followPageTrail = (root: ParentNode = document): (() => void) => {\n const disposers: Array<() => void> = [];\n const upgrade = () => {\n const trail = readPageTrail();\n for (const element of root.querySelectorAll<HTMLElement>(\".k2b-page-title[data-k2b-server]\")) {\n const row = element.parentElement;\n if (!row || element.closest(\"solid-island, solid-client\")) continue;\n const tag = element.querySelector(\".k2b-page-title__title\")?.localName;\n const as = tag === \"p\" || tag === \"h2\" ? tag : \"h1\";\n const fallback = element.hasAttribute(\"data-fallback\");\n const extra = [...element.classList].filter((name) => name !== \"k2b-page-title\").join(\" \") || undefined;\n const subtitle = element.querySelector(\".k2b-page-title__subtitle\");\n const subtitleNodes = subtitle ? [...subtitle.childNodes] : undefined;\n // The live title takes the server title's place, before whatever follows it in the row.\n const place = document.createTextNode(\"\");\n row.insertBefore(place, element);\n element.remove();\n disposers.push(\n createRoot((dispose) => {\n const live = usePageTrail();\n insert(\n row,\n () => (\n <Show when={live()}>\n {(current) => <PageTitle trail={current()} as={as} fallback={fallback} class={extra} subtitle={subtitleNodes} />}\n </Show>\n ),\n place,\n );\n return dispose;\n }),\n );\n }\n // The row's `data-trail` decides whether it shows; it follows the depth.\n for (const row of root.querySelectorAll<HTMLElement>(\"[data-trail]:has(> .k2b-page-title[data-fallback])\"))\n if (!row.closest(\"solid-island, solid-client\")) row.dataset.trail = trail?.ancestors.length ? \"up\" : \"title\";\n };\n window.addEventListener(PAGE_TRAIL_EVENT, upgrade);\n return () => {\n window.removeEventListener(PAGE_TRAIL_EVENT, upgrade);\n for (const dispose of disposers.splice(0)) dispose();\n };\n};\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["layout/PageTitle.tsx"],"sourcesContent":["import { createContext, createRoot, createSignal, createUniqueId, For, type JSX, onCleanup, onMount, Show, useContext } from \"solid-js\";\nimport { Dynamic, insert, isServer } from \"solid-js/web\";\nimport { IconButtonLink } from \"../actions/Button\";\nimport { installPageTitleController, PAGE_TITLE_CONTROLLER_ATTRIBUTE } from \"./page-title-controller\";\nimport { useWorkspaceMessages } from \"./workspace-messages\";\n\n/** A page above the current one. Only pages someone can go back to are levels, so each has a link. */\nexport type PageTrailLink = { title: string; href: string };\n/** The current page and the pages above it, nearest last. */\nexport type PageTrailState = { title: string; ancestors: PageTrailLink[] };\n\nexport type PageTitleProps = {\n trail: PageTrailState;\n /** Quiet text after the title, for example a count. */\n subtitle?: JSX.Element;\n /** Defaults to `\"h1\"`. Chrome that repeats a title the page already shows uses `\"p\"`. */\n as?: \"h1\" | \"h2\" | \"p\";\n /**\n * Marks a title drawn by surrounding chrome, such as the workspace head or a\n * host shell. It steps back when the page renders its own `PageTitle`, so a\n * page has one title with one way up.\n */\n fallback?: boolean;\n class?: string;\n};\n\n/** Window event a host dispatches after publishing a new page trail. */\nexport const PAGE_TRAIL_EVENT = \"k2b:page-trail\";\nconst SOURCE_SELECTOR = \"script[data-k2b-page-trail]\";\n\nconst isTrail = (value: unknown): value is PageTrailState => {\n if (!value || typeof value !== \"object\") return false;\n const trail = value as Partial<PageTrailState>;\n return (\n typeof trail.title === \"string\" &&\n Array.isArray(trail.ancestors) &&\n trail.ancestors.every((link) => typeof link?.title === \"string\" && typeof link?.href === \"string\")\n );\n};\n\n/** The trail a host published for this document, or `undefined` outside a browser or without one. */\nexport const readPageTrail = (): PageTrailState | undefined => {\n if (typeof document === \"undefined\") return undefined;\n const source = document.querySelector(SOURCE_SELECTOR);\n if (!source?.textContent) return undefined;\n try {\n const value: unknown = JSON.parse(source.textContent);\n return isTrail(value) ? value : undefined;\n } catch {\n return undefined;\n }\n};\n\n/** `<` is escaped so a title can never end the script element it travels in. */\nconst serializeTrail = (trail: PageTrailState): string => JSON.stringify(trail).replace(/</g, \"\\\\u003c\");\n\n/**\n * Publishes a new trail for the document in the browser, for example after an\n * in-page navigation. Workspace heads and page titles that read the host's\n * trail follow it.\n */\nexport const setPageTrail = (trail: PageTrailState): void => {\n if (typeof document === \"undefined\") return;\n let source = document.querySelector(SOURCE_SELECTOR);\n if (!source) {\n source = document.createElement(\"script\");\n source.setAttribute(\"type\", \"application/json\");\n source.setAttribute(\"data-k2b-page-trail\", \"\");\n document.body.append(source);\n }\n source.textContent = serializeTrail(trail);\n window.dispatchEvent(new Event(PAGE_TRAIL_EVENT));\n};\n\nconst PageTrailContext = createContext<() => PageTrailState | undefined>();\n\n/**\n * Gives a host's page trail to everything below it. During server rendering\n * the context carries it; the provider also publishes it into the document,\n * where browser-rendered parts such as islands read it. Below a provider that\n * renders in the browser, its `trail` prop is the source: update the prop.\n */\nexport function PageTrailProvider(props: { trail: PageTrailState | undefined; children: JSX.Element }): JSX.Element {\n return (\n <PageTrailContext.Provider value={() => props.trail}>\n <Show when={props.trail}>\n {(trail) => <script type=\"application/json\" data-k2b-page-trail=\"\" innerHTML={serializeTrail(trail())} />}\n </Show>\n {props.children}\n </PageTrailContext.Provider>\n );\n}\n\n/** The host's page trail: from the provider while rendering below it, otherwise from the document. */\nexport const usePageTrail = (): (() => PageTrailState | undefined) => {\n const context = useContext(PageTrailContext);\n if (context) return context;\n if (typeof window === \"undefined\") return () => undefined;\n const [trail, setTrail] = createSignal(readPageTrail());\n const update = () => setTrail(readPageTrail());\n window.addEventListener(PAGE_TRAIL_EVENT, update);\n onCleanup(() => window.removeEventListener(PAGE_TRAIL_EVENT, update));\n return trail;\n};\n\n/**\n * A page title with its way up. The number of pages above decides the form:\n * none shows the title alone; one puts \"‹ Parent\" above it; more put a path\n * row above it whose middle collapses into \"…\" only when it does not fit.\n * Both rows share the same height, so the title never moves between depths.\n * On phones a back arrow left of the title replaces the row above.\n */\nexport function PageTitle(props: PageTitleProps): JSX.Element {\n const messages = useWorkspaceMessages();\n // A title rendered on the server from the host's trail follows the host's later trails (see `followPageTrail`).\n const host = isServer ? useContext(PageTrailContext) : undefined;\n const menuId = `k2b-page-title-menu-${createUniqueId()}`;\n const ancestors = () => props.trail.ancestors;\n const parent = () => ancestors().at(-1);\n let root: HTMLDivElement | undefined;\n // A host may control every title in the document at once; a title outside\n // one still collapses its path.\n onMount(() => {\n if (!root || root.closest(`[${PAGE_TITLE_CONTROLLER_ATTRIBUTE}]`)) return;\n onCleanup(installPageTitleController(root));\n });\n return (\n <div\n ref={root}\n class={`k2b-page-title ${props.class ?? \"\"}`}\n data-depth={ancestors().length}\n data-fallback={props.fallback ? \"\" : undefined}\n data-k2b-server={isServer && props.trail === host?.() ? \"\" : undefined}\n >\n <Show when={parent()}>\n {(up) => (\n <IconButtonLink\n class=\"k2b-page-title__back\"\n href={up().href}\n label={messages().backTo({ page: up().title })}\n data-k2b-page-trail-link=\"\"\n data-k2b-page-trail-up=\"\"\n >\n <i class=\"ti ti-chevron-left\" aria-hidden=\"true\" />\n </IconButtonLink>\n )}\n </Show>\n <div class=\"k2b-page-title__copy\">\n <Show when={ancestors().length === 1 && parent()}>\n {(up) => (\n <nav class=\"k2b-page-title__trail\" aria-label={messages().path}>\n <a\n class=\"k2b-page-title__up\"\n href={up().href}\n aria-label={messages().backTo({ page: up().title })}\n data-k2b-page-trail-link=\"\"\n data-k2b-page-trail-up=\"\"\n >\n <i class=\"ti ti-chevron-left\" aria-hidden=\"true\" />\n <span class=\"k2b-page-title__label\">{up().title}</span>\n </a>\n </nav>\n )}\n </Show>\n <Show when={ancestors().length > 1}>\n <nav class=\"k2b-page-title__trail\" aria-label={messages().path}>\n <ol class=\"k2b-page-title__path\">\n <For each={ancestors()}>\n {(link, index) => (\n <>\n <Show when={index() === 1}>\n <li class=\"k2b-page-title__more\" hidden>\n <i class=\"ti ti-chevron-right\" aria-hidden=\"true\" />\n <button type=\"button\" class=\"k2b-page-title__more-button\" popovertarget={menuId} aria-label={messages().moreLevels}>\n …\n </button>\n </li>\n </Show>\n <li class=\"k2b-page-title__crumb\" data-index={index()}>\n <Show when={index() > 0}>\n <i class=\"ti ti-chevron-right\" aria-hidden=\"true\" />\n </Show>\n <a\n href={link.href}\n data-k2b-page-trail-link=\"\"\n data-k2b-page-trail-up={index() === ancestors().length - 1 ? \"\" : undefined}\n class=\"k2b-page-title__label\"\n >\n {link.title}\n </a>\n </li>\n </>\n )}\n </For>\n </ol>\n <div id={menuId} popover=\"auto\" class=\"k2b-dropdown__menu k2b-page-title__menu\">\n <For each={ancestors()}>\n {(link, index) => (\n <a class=\"k2b-dropdown__item\" href={link.href} data-index={index()} data-k2b-page-trail-link=\"\" hidden>\n {link.title}\n </a>\n )}\n </For>\n </div>\n </nav>\n </Show>\n <Dynamic component={props.as ?? \"h1\"} class=\"k2b-page-title__title\">\n <span class=\"k2b-page-title__text\">{props.trail.title}</span>\n <Show when={props.subtitle}>\n <small class=\"k2b-page-title__subtitle\">{props.subtitle}</small>\n </Show>\n </Dynamic>\n </div>\n </div>\n );\n}\n\n/**\n * Keeps server-rendered titles of the host's trail current: a workspace head\n * that never hydrates, its heading, and an app's own header title from the\n * host's trail.\n * When the host publishes a new trail, for example after `setPageTrail()`,\n * each such title is replaced in place by a live one with the same element,\n * class, and subtitle, and a head learns whether it now has a page above it.\n * Titles inside islands render in the browser anyway. Returns the disposer.\n */\nexport const followPageTrail = (root: ParentNode = document): (() => void) => {\n const disposers: Array<() => void> = [];\n const upgrade = () => {\n const trail = readPageTrail();\n for (const element of root.querySelectorAll<HTMLElement>(\".k2b-page-title[data-k2b-server]\")) {\n const row = element.parentElement;\n if (!row || element.closest(\"solid-island, solid-client\")) continue;\n const tag = element.querySelector(\".k2b-page-title__title\")?.localName;\n const as = tag === \"p\" || tag === \"h2\" ? tag : \"h1\";\n const fallback = element.hasAttribute(\"data-fallback\");\n const extra = [...element.classList].filter((name) => name !== \"k2b-page-title\").join(\" \") || undefined;\n const subtitle = element.querySelector(\".k2b-page-title__subtitle\");\n const subtitleNodes = subtitle ? [...subtitle.childNodes] : undefined;\n // The live title takes the server title's place, before whatever follows it in the row.\n const place = document.createTextNode(\"\");\n row.insertBefore(place, element);\n element.remove();\n disposers.push(\n createRoot((dispose) => {\n const live = usePageTrail();\n insert(\n row,\n () => (\n <Show when={live()}>\n {(current) => <PageTitle trail={current()} as={as} fallback={fallback} class={extra} subtitle={subtitleNodes} />}\n </Show>\n ),\n place,\n );\n return dispose;\n }),\n );\n }\n // A server-rendered workspace heading carries only the title.\n for (const heading of root.querySelectorAll<HTMLElement>(\"[data-k2b-page-trail-title][data-k2b-server]\"))\n if (trail && !heading.closest(\"solid-island, solid-client\")) heading.textContent = trail.title;\n // The row's `data-trail` decides whether it shows; it follows the depth.\n for (const row of root.querySelectorAll<HTMLElement>(\"[data-trail]:has(> .k2b-page-title[data-fallback])\"))\n if (!row.closest(\"solid-island, solid-client\")) row.dataset.trail = trail?.ancestors.length ? \"up\" : \"title\";\n };\n window.addEventListener(PAGE_TRAIL_EVENT, upgrade);\n return () => {\n window.removeEventListener(PAGE_TRAIL_EVENT, upgrade);\n for (const dispose of disposers.splice(0)) dispose();\n };\n};\n"],"mappings":"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","ignoreList":[]}
@@ -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,94 @@ 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
+ // a step can also land above Main's top or beside it, as in a board.
525
+ const page = ownerDocument.scrollingElement;
526
+ const vertical = verticalScroller(target, main) ?? page;
527
+ const horizontal = scrollerOn("x", target, main) ?? page;
528
+ const bounds = scroller => scroller && scroller !== page ? scroller.getBoundingClientRect() : null;
529
+ const box = target.getBoundingClientRect();
530
+ const top = Math.max(0, bounds(vertical)?.top ?? 0);
531
+ const left = Math.max(0, bounds(horizontal)?.left ?? 0);
532
+ const right = Math.min(ownerWindow.innerWidth, bounds(horizontal)?.right ?? Infinity);
533
+ const dy = box.top < top ? box.top - top - 8 : Math.max(0, box.bottom - (ownerWindow.innerHeight - aside.offsetHeight) + 8);
534
+ // As with scrollIntoView's "nearest", an item past both edges stays put
535
+ // and one wider than the view keeps its start in view.
536
+ 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;
537
+ if (vertical && vertical === horizontal) {
538
+ if (dy || dx) vertical.scrollBy({
539
+ top: dy,
540
+ left: dx,
541
+ behavior
542
+ });
543
+ } else {
544
+ if (dy) vertical?.scrollBy({
545
+ top: dy,
546
+ behavior
547
+ });
548
+ if (dx) horizontal?.scrollBy({
549
+ left: dx,
550
+ behavior
551
+ });
552
+ }
553
+ } else target.scrollIntoView({
554
+ block: "nearest",
555
+ inline: "nearest",
556
+ behavior
557
+ });
558
+ }
559
+ }));
560
+ };
498
561
  const detailTransition = (aside, wasHidden) => {
499
562
  const root = aside.closest(ROOT_SELECTOR);
500
563
  detailHidden.set(aside, Boolean(aside.hidden));
501
564
  if (!root || !observedRoots.has(root) || Boolean(aside.hidden) === wasHidden) return;
565
+ pendingAnchors.get(root)?.();
566
+ slide(root, aside);
502
567
  touchedDetails.add(aside);
503
568
  const pending = revealFrames.get(root);
504
569
  if (pending !== undefined) cancelAnimationFrame(pending);
@@ -508,18 +573,25 @@ export const installAppWorkspaceController = (options = {}) => {
508
573
  if (snapshot) snapshot.fresh = false;
509
574
  if (aside.hidden) {
510
575
  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);
576
+ if (panelStep === aside) panelStep = null;
577
+ if (root.hasAttribute("data-workspace-motion") && !reducedMotion?.matches && wide?.matches) {
578
+ // Closing holds the detail column until the slide ends; anchor after Main widens.
579
+ const cancel = () => {
580
+ root.removeEventListener("transitionend", onTransitionEnd);
581
+ clearTimeout(timer);
582
+ pendingAnchors.delete(root);
583
+ };
584
+ const finish = () => {
585
+ cancel();
586
+ if (aside.hidden && aside.isConnected) anchorItem(root, aside, snapshot);
587
+ };
588
+ const onTransitionEnd = event => {
589
+ if (event.target === root && event.propertyName === "--k2b-workspace-detail-track") finish();
590
+ };
591
+ const timer = setTimeout(finish, 300);
592
+ root.addEventListener("transitionend", onTransitionEnd);
593
+ pendingAnchors.set(root, cancel);
594
+ } else anchorItem(root, aside, snapshot);
523
595
  const focused = ownerDocument.activeElement;
524
596
  if (snapshot?.item.isConnected && (aside.contains(lastFocus.get(root) ?? null) || aside.contains(focused) || focused === ownerDocument.body)) snapshot.item.focus({
525
597
  preventScroll: true
@@ -528,30 +600,10 @@ export const installAppWorkspaceController = (options = {}) => {
528
600
  if (detailDrag?.aside === aside) finishDetailDrag(true);
529
601
  return;
530
602
  }
531
- clearHolds(root);
532
603
  anchorItem(root, aside, snapshot);
533
604
  if (phone?.matches) setDetent(aside, false);
534
- rememberTrailPadding(root);
535
605
  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
- }));
606
+ revealItem(root, aside, snapshot);
555
607
  if (snapshot.interaction === "keyboard" || phone?.matches) requestTitle(aside);
556
608
  }
557
609
  };
@@ -575,11 +627,45 @@ export const installAppWorkspaceController = (options = {}) => {
575
627
  }
576
628
  const owningRoot = aside?.closest(ROOT_SELECTOR);
577
629
  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: {
630
+ if (event.key === "Escape" && closable(aside)) requestClose(aside);else if ((event.key.toLowerCase() === "j" || event.key.toLowerCase() === "k") && aside.hasAttribute("data-workspace-detail-steppable")) {
631
+ const main = mainElement(owningRoot);
632
+ const inMain = remembered.get(owningRoot)?.item.contains(target) || main?.contains(target);
633
+ const inPanel = aside.contains(target) || target === ownerDocument.body && panelStep === aside;
634
+ panelStep = inPanel ? aside : null;
635
+ const previousTitle = inPanel ? titleIn(aside) : null;
636
+ const previousText = previousTitle?.textContent;
637
+ const detail = {
580
638
  direction: event.key.toLowerCase() === "j" ? 1 : -1
639
+ };
640
+ aside.dispatchEvent(new CustomEvent(APP_WORKSPACE_DETAIL_STEP_EVENT, {
641
+ detail
642
+ }));
643
+ const shown = detail.item;
644
+ const item = shown instanceof HTMLElement && main?.contains(shown) && shown.closest(ROOT_SELECTOR) === owningRoot ? shown : null;
645
+ if (item && main) {
646
+ const snapshot = {
647
+ item,
648
+ cell: null,
649
+ interaction: "keyboard",
650
+ scroller: verticalScroller(item, main),
651
+ scrollTop: 0,
652
+ top: 0,
653
+ fresh: false
654
+ };
655
+ refreshItem(snapshot);
656
+ remembered.set(owningRoot, snapshot);
657
+ revealItem(owningRoot, aside, snapshot);
581
658
  }
582
- }));else if (event.key === "F6") {
659
+ const title = titleIn(aside);
660
+ if (item && inMain) item.focus({
661
+ preventScroll: true
662
+ });
663
+ // A panel that swapped its title during the event has the new one now.
664
+ else if (item && inPanel && title && (title !== previousTitle || title.textContent !== previousText)) focusTitle(aside);
665
+ // This key's capture cancelled an earlier step's title request, so a
666
+ // step from the body, even past the last item, asks again.
667
+ else if (inPanel && (item || target === ownerDocument.body)) requestTitle(aside, Boolean(item));
668
+ } else if (event.key === "F6") {
583
669
  if (aside.contains(target)) {
584
670
  const item = remembered.get(owningRoot)?.item;
585
671
  const main = mainElement(owningRoot);
@@ -781,7 +867,6 @@ export const installAppWorkspaceController = (options = {}) => {
781
867
  root.style.removeProperty(variable);
782
868
  if (getComputedStyle(root).getPropertyValue(variable).trim()) root.style.setProperty(variable, `${numberData(handle, "workspaceDefaultSize", APP_WORKSPACE_DETAIL_DEFAULT)}px`);
783
869
  updateHandleValue(root, handle, "detail", currentSize(root, handle, "detail"));
784
- rememberTrailPadding(root);
785
870
  options.writeState?.(layoutState);
786
871
  };
787
872
  const stopScrollFades = installScrollFades(eventRoot);
@@ -863,7 +948,6 @@ export const installAppWorkspaceController = (options = {}) => {
863
948
  const size = finished.kind === "pane" ? finished.liveSize : applySize(finished.root, finished.handle, finished.kind, finished.liveSize);
864
949
  persistSize(finished.handle, finished.kind, size);
865
950
  if (finished.kind !== "drawer") reconcilePanes(finished.root, layoutState);
866
- if (finished.kind === "detail") rememberTrailPadding(finished.root);
867
951
  }
868
952
  if (finished.handle.hasPointerCapture?.(finished.pointerId)) finished.handle.releasePointerCapture(finished.pointerId);
869
953
  finished.releaseTextSelection();
@@ -929,7 +1013,6 @@ export const installAppWorkspaceController = (options = {}) => {
929
1013
  event.preventDefault();
930
1014
  persistSize(handle, kind, applySize(root, handle, kind, requested));
931
1015
  if (kind === "sidebar" || kind === "detail") reconcilePanes(root, layoutState);
932
- if (kind === "detail") rememberTrailPadding(root);
933
1016
  };
934
1017
  const allRoots = () => {
935
1018
  const nested = Array.from(eventRoot.querySelectorAll(ROOT_SELECTOR));
@@ -949,7 +1032,6 @@ export const installAppWorkspaceController = (options = {}) => {
949
1032
  if (aside.dataset.workspaceDetailSheet === "full") setDetent(aside, false);
950
1033
  }
951
1034
  }
952
- rememberTrailPadding(root);
953
1035
  });
954
1036
  roots().forEach(root => {
955
1037
  // `data-workspace-resizable="false"` already refuses pointer and keyboard
@@ -974,7 +1056,6 @@ export const installAppWorkspaceController = (options = {}) => {
974
1056
  });
975
1057
  reconcilePanes(root, layoutState, active?.root === root ? active : null);
976
1058
  });
977
- heldTrails.forEach(shrinkHold);
978
1059
  };
979
1060
  /**
980
1061
  * A handle's limits move whenever anything around it does — opening a detail
@@ -1036,6 +1117,8 @@ export const installAppWorkspaceController = (options = {}) => {
1036
1117
  const next = new Set(allRoots());
1037
1118
  observedRoots.forEach(root => {
1038
1119
  if (!next.has(root)) {
1120
+ pendingAnchors.get(root)?.();
1121
+ slides.get(root)?.();
1039
1122
  resizeObserver?.unobserve(root);
1040
1123
  const frame = motionFrames.get(root);
1041
1124
  if (frame !== undefined) cancelAnimationFrame(frame);
@@ -1069,7 +1152,6 @@ export const installAppWorkspaceController = (options = {}) => {
1069
1152
  }));
1070
1153
  }));
1071
1154
  }
1072
- rememberTrailPadding(root);
1073
1155
  });
1074
1156
  };
1075
1157
  const layoutNode = node => node instanceof Element && (node.matches(LAYOUT_REGION_SELECTOR) || Boolean(node.querySelector(LAYOUT_REGION_SELECTOR)));
@@ -1099,10 +1181,6 @@ export const installAppWorkspaceController = (options = {}) => {
1099
1181
  eventRoot.addEventListener("pointerdown", rememberInteraction, true);
1100
1182
  eventRoot.addEventListener("keydown", rememberInteraction, true);
1101
1183
  eventRoot.addEventListener("focusin", onDetailFocusIn);
1102
- eventRoot.addEventListener("scroll", onTrailScroll, {
1103
- passive: true,
1104
- capture: true
1105
- });
1106
1184
  eventRoot.addEventListener("pointerdown", onSheetPointerDown);
1107
1185
  eventRoot.addEventListener("click", onGrabberClick);
1108
1186
  eventRoot.addEventListener("dblclick", onDetailDoubleClick);
@@ -1131,13 +1209,14 @@ export const installAppWorkspaceController = (options = {}) => {
1131
1209
  scheduleReconcile();
1132
1210
  return () => {
1133
1211
  cancelTitle();
1212
+ pendingAnchors.forEach(cancel => cancel());
1213
+ slides.forEach(end => end());
1134
1214
  stopScrollFades();
1135
1215
  finishDetailDrag(true);
1136
1216
  if (suppressedClickTimer !== null) clearTimeout(suppressedClickTimer);
1137
1217
  motionFrames.forEach(cancelAnimationFrame);
1138
1218
  revealFrames.forEach(cancelAnimationFrame);
1139
1219
  motionRoots.forEach(root => delete root.dataset.workspaceMotion);
1140
- heldTrails.forEach(trail => trail.style.removeProperty("--k2b-detail-trail-hold"));
1141
1220
  touchedDetails.forEach(aside => {
1142
1221
  clearDrag(aside);
1143
1222
  delete aside.dataset.workspaceDetailSheet;
@@ -1151,7 +1230,6 @@ export const installAppWorkspaceController = (options = {}) => {
1151
1230
  eventRoot.removeEventListener("pointerdown", rememberInteraction, true);
1152
1231
  eventRoot.removeEventListener("keydown", rememberInteraction, true);
1153
1232
  eventRoot.removeEventListener("focusin", onDetailFocusIn);
1154
- eventRoot.removeEventListener("scroll", onTrailScroll, true);
1155
1233
  eventRoot.removeEventListener("pointerdown", onSheetPointerDown);
1156
1234
  eventRoot.removeEventListener("click", onGrabberClick);
1157
1235
  eventRoot.removeEventListener("dblclick", onDetailDoubleClick);