@k2b/ui 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/dist/base.css +2 -0
  2. package/dist/browser/actions/Dropdown.js +159 -99
  3. package/dist/browser/actions/Dropdown.js.map +1 -1
  4. package/dist/browser/actions/FilterChip.js +113 -56
  5. package/dist/browser/actions/FilterChip.js.map +1 -1
  6. package/dist/browser/actions/ViewTabs.js +8 -0
  7. package/dist/browser/actions/ViewTabs.js.map +1 -1
  8. package/dist/browser/actions/filter-messages.js +26 -2
  9. package/dist/browser/actions/filter-messages.js.map +1 -1
  10. package/dist/browser/actions/menu-rows.js +23 -3
  11. package/dist/browser/actions/menu-rows.js.map +1 -1
  12. package/dist/browser/actions/menu-sheet.js +2 -7
  13. package/dist/browser/actions/menu-sheet.js.map +1 -1
  14. package/dist/browser/chat/ChatComposer.js +480 -403
  15. package/dist/browser/chat/ChatComposer.js.map +1 -1
  16. package/dist/browser/chat/chat-messages.js +14 -0
  17. package/dist/browser/chat/chat-messages.js.map +1 -1
  18. package/dist/browser/chat/index.js.map +1 -1
  19. package/dist/browser/content/Calendar.js +39 -29
  20. package/dist/browser/content/Calendar.js.map +1 -1
  21. package/dist/browser/content/CalendarDayView.js +159 -165
  22. package/dist/browser/content/CalendarDayView.js.map +1 -1
  23. package/dist/browser/content/Chart.js +104 -55
  24. package/dist/browser/content/Chart.js.map +1 -1
  25. package/dist/browser/content/ChartExplorer.js +5 -3
  26. package/dist/browser/content/ChartExplorer.js.map +1 -1
  27. package/dist/browser/content/MapView.js +4 -4
  28. package/dist/browser/content/MapView.js.map +1 -1
  29. package/dist/browser/content/MessageRow.js +607 -343
  30. package/dist/browser/content/MessageRow.js.map +1 -1
  31. package/dist/browser/content/ReactionBar.js +17 -0
  32. package/dist/browser/content/ReactionBar.js.map +1 -1
  33. package/dist/browser/content/chart-inspection-core.js +4 -1
  34. package/dist/browser/content/chart-inspection-core.js.map +1 -1
  35. package/dist/browser/content/code-highlight.js +66 -0
  36. package/dist/browser/content/code-highlight.js.map +1 -1
  37. package/dist/browser/content/index.js +2 -1
  38. package/dist/browser/content/index.js.map +1 -1
  39. package/dist/browser/content/map-view-live.js +47 -10
  40. package/dist/browser/content/map-view-live.js.map +1 -1
  41. package/dist/browser/content/map-view-types.js.map +1 -1
  42. package/dist/browser/feedback/dialog-core.js +6 -0
  43. package/dist/browser/feedback/dialog-core.js.map +1 -1
  44. package/dist/browser/inputs/AutocompleteEditor.js +167 -104
  45. package/dist/browser/inputs/AutocompleteEditor.js.map +1 -1
  46. package/dist/browser/inputs/RichSelect.js +1474 -0
  47. package/dist/browser/inputs/RichSelect.js.map +1 -0
  48. package/dist/browser/inputs/TextInput.js +99 -61
  49. package/dist/browser/inputs/TextInput.js.map +1 -1
  50. package/dist/browser/inputs/choice.js +19 -8
  51. package/dist/browser/inputs/choice.js.map +1 -1
  52. package/dist/browser/inputs/index.js +1 -0
  53. package/dist/browser/inputs/index.js.map +1 -1
  54. package/dist/browser/inputs/rich-select-messages.js +46 -0
  55. package/dist/browser/inputs/rich-select-messages.js.map +1 -0
  56. package/dist/browser/intl/shortcut-messages.js +4 -0
  57. package/dist/browser/intl/shortcut-messages.js.map +1 -1
  58. package/dist/browser/intl/shortcut.js +4 -0
  59. package/dist/browser/intl/shortcut.js.map +1 -1
  60. package/dist/browser/layout/AppWorkspace.js +624 -565
  61. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  62. package/dist/browser/layout/DetailPanel.js +10 -1
  63. package/dist/browser/layout/DetailPanel.js.map +1 -1
  64. package/dist/browser/layout/MobileShell.js +213 -38
  65. package/dist/browser/layout/MobileShell.js.map +1 -1
  66. package/dist/browser/layout/Navigation.js +14 -5
  67. package/dist/browser/layout/Navigation.js.map +1 -1
  68. package/dist/browser/layout/PageTitle.js +9 -3
  69. package/dist/browser/layout/PageTitle.js.map +1 -1
  70. package/dist/browser/layout/ResizeSeparator.js +8 -2
  71. package/dist/browser/layout/ResizeSeparator.js.map +1 -1
  72. package/dist/browser/layout/app-workspace-controller.js +192 -31
  73. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  74. package/dist/browser/layout/index.js +1 -0
  75. package/dist/browser/layout/index.js.map +1 -1
  76. package/dist/browser/layout/navigation-model.js.map +1 -1
  77. package/dist/browser/layout/resize-separator.js +25 -1
  78. package/dist/browser/layout/resize-separator.js.map +1 -1
  79. package/dist/browser/layout/scroll-fade.js +32 -5
  80. package/dist/browser/layout/scroll-fade.js.map +1 -1
  81. package/dist/browser/surfaces/NoticeCard.js +46 -30
  82. package/dist/browser/surfaces/NoticeCard.js.map +1 -1
  83. package/dist/browser/surfaces/Skeleton.js +14 -0
  84. package/dist/browser/surfaces/Skeleton.js.map +1 -0
  85. package/dist/browser/surfaces/index.js +1 -0
  86. package/dist/browser/surfaces/index.js.map +1 -1
  87. package/dist/browser/surfaces/resource-messages.js +2 -2
  88. package/dist/browser/surfaces/resource-messages.js.map +1 -1
  89. package/dist/ssr/actions/Dropdown.js +51 -27
  90. package/dist/ssr/actions/Dropdown.js.map +1 -1
  91. package/dist/ssr/actions/FilterChip.js +67 -16
  92. package/dist/ssr/actions/FilterChip.js.map +1 -1
  93. package/dist/ssr/actions/ViewTabs.js +8 -0
  94. package/dist/ssr/actions/ViewTabs.js.map +1 -1
  95. package/dist/ssr/actions/filter-messages.js +26 -2
  96. package/dist/ssr/actions/filter-messages.js.map +1 -1
  97. package/dist/ssr/actions/menu-rows.js +23 -3
  98. package/dist/ssr/actions/menu-rows.js.map +1 -1
  99. package/dist/ssr/actions/menu-sheet.js +2 -7
  100. package/dist/ssr/actions/menu-sheet.js.map +1 -1
  101. package/dist/ssr/chat/ChatComposer.js +96 -67
  102. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  103. package/dist/ssr/chat/chat-messages.js +14 -0
  104. package/dist/ssr/chat/chat-messages.js.map +1 -1
  105. package/dist/ssr/chat/index.js.map +1 -1
  106. package/dist/ssr/content/Calendar.js +40 -31
  107. package/dist/ssr/content/Calendar.js.map +1 -1
  108. package/dist/ssr/content/CalendarDayView.js +40 -48
  109. package/dist/ssr/content/CalendarDayView.js.map +1 -1
  110. package/dist/ssr/content/Chart.js +27 -7
  111. package/dist/ssr/content/Chart.js.map +1 -1
  112. package/dist/ssr/content/ChartExplorer.js +5 -3
  113. package/dist/ssr/content/ChartExplorer.js.map +1 -1
  114. package/dist/ssr/content/MapView.js +4 -4
  115. package/dist/ssr/content/MapView.js.map +1 -1
  116. package/dist/ssr/content/MessageRow.js +264 -91
  117. package/dist/ssr/content/MessageRow.js.map +1 -1
  118. package/dist/ssr/content/ReactionBar.js +17 -0
  119. package/dist/ssr/content/ReactionBar.js.map +1 -1
  120. package/dist/ssr/content/chart-inspection-core.js +4 -1
  121. package/dist/ssr/content/chart-inspection-core.js.map +1 -1
  122. package/dist/ssr/content/code-highlight.js +66 -0
  123. package/dist/ssr/content/code-highlight.js.map +1 -1
  124. package/dist/ssr/content/index.js +2 -1
  125. package/dist/ssr/content/index.js.map +1 -1
  126. package/dist/ssr/content/map-view-types.js.map +1 -1
  127. package/dist/ssr/feedback/dialog-core.js +6 -0
  128. package/dist/ssr/feedback/dialog-core.js.map +1 -1
  129. package/dist/ssr/inputs/AutocompleteEditor.js +64 -24
  130. package/dist/ssr/inputs/AutocompleteEditor.js.map +1 -1
  131. package/dist/ssr/inputs/RichSelect.js +950 -0
  132. package/dist/ssr/inputs/RichSelect.js.map +1 -0
  133. package/dist/ssr/inputs/TextInput.js +44 -17
  134. package/dist/ssr/inputs/TextInput.js.map +1 -1
  135. package/dist/ssr/inputs/choice.js +19 -8
  136. package/dist/ssr/inputs/choice.js.map +1 -1
  137. package/dist/ssr/inputs/index.js +1 -0
  138. package/dist/ssr/inputs/index.js.map +1 -1
  139. package/dist/ssr/inputs/rich-select-messages.js +46 -0
  140. package/dist/ssr/inputs/rich-select-messages.js.map +1 -0
  141. package/dist/ssr/intl/shortcut-messages.js +4 -0
  142. package/dist/ssr/intl/shortcut-messages.js.map +1 -1
  143. package/dist/ssr/intl/shortcut.js +4 -0
  144. package/dist/ssr/intl/shortcut.js.map +1 -1
  145. package/dist/ssr/layout/AppWorkspace.js +166 -125
  146. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  147. package/dist/ssr/layout/DetailPanel.js +1 -1
  148. package/dist/ssr/layout/DetailPanel.js.map +1 -1
  149. package/dist/ssr/layout/MobileShell.js +100 -20
  150. package/dist/ssr/layout/MobileShell.js.map +1 -1
  151. package/dist/ssr/layout/Navigation.js +1 -1
  152. package/dist/ssr/layout/Navigation.js.map +1 -1
  153. package/dist/ssr/layout/PageTitle.js +9 -3
  154. package/dist/ssr/layout/PageTitle.js.map +1 -1
  155. package/dist/ssr/layout/ResizeSeparator.js +4 -1
  156. package/dist/ssr/layout/ResizeSeparator.js.map +1 -1
  157. package/dist/ssr/layout/app-workspace-controller.js +192 -31
  158. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  159. package/dist/ssr/layout/index.js +1 -0
  160. package/dist/ssr/layout/index.js.map +1 -1
  161. package/dist/ssr/layout/navigation-model.js.map +1 -1
  162. package/dist/ssr/layout/resize-separator.js +25 -1
  163. package/dist/ssr/layout/resize-separator.js.map +1 -1
  164. package/dist/ssr/layout/scroll-fade.js +32 -5
  165. package/dist/ssr/layout/scroll-fade.js.map +1 -1
  166. package/dist/ssr/surfaces/NoticeCard.js +21 -12
  167. package/dist/ssr/surfaces/NoticeCard.js.map +1 -1
  168. package/dist/ssr/surfaces/Skeleton.js +8 -0
  169. package/dist/ssr/surfaces/Skeleton.js.map +1 -0
  170. package/dist/ssr/surfaces/index.js +1 -0
  171. package/dist/ssr/surfaces/index.js.map +1 -1
  172. package/dist/ssr/surfaces/resource-messages.js +2 -2
  173. package/dist/ssr/surfaces/resource-messages.js.map +1 -1
  174. package/dist/styles.css +1 -1
  175. package/dist/types/actions/Dropdown.d.ts +14 -0
  176. package/dist/types/actions/FilterChip.d.ts +11 -0
  177. package/dist/types/actions/filter-messages.d.ts +12 -0
  178. package/dist/types/actions/menu-rows.d.ts +7 -2
  179. package/dist/types/chat/ChatComposer.d.ts +13 -0
  180. package/dist/types/chat/chat-messages.d.ts +7 -0
  181. package/dist/types/chat/index.d.ts +1 -1
  182. package/dist/types/content/Calendar.d.ts +7 -0
  183. package/dist/types/content/Chart.d.ts +4 -2
  184. package/dist/types/content/ChartExplorer.d.ts +0 -1
  185. package/dist/types/content/MessageRow.d.ts +64 -2
  186. package/dist/types/content/code-highlight.d.ts +3 -1
  187. package/dist/types/content/index.d.ts +3 -2
  188. package/dist/types/content/map-view-live.d.ts +1 -1
  189. package/dist/types/content/map-view-types.d.ts +2 -0
  190. package/dist/types/feedback/dialog-core.d.ts +3 -0
  191. package/dist/types/inputs/AutocompleteEditor.d.ts +6 -1
  192. package/dist/types/inputs/RichSelect.d.ts +93 -0
  193. package/dist/types/inputs/TextInput.d.ts +5 -2
  194. package/dist/types/inputs/choice.d.ts +24 -4
  195. package/dist/types/inputs/index.d.ts +2 -0
  196. package/dist/types/inputs/rich-select-messages.d.ts +21 -0
  197. package/dist/types/intl/shortcut-messages.d.ts +2 -0
  198. package/dist/types/layout/AppWorkspace.d.ts +9 -0
  199. package/dist/types/layout/DetailPanel.d.ts +2 -0
  200. package/dist/types/layout/MobileShell.d.ts +52 -5
  201. package/dist/types/layout/app-workspace-controller.d.ts +2 -0
  202. package/dist/types/layout/index.d.ts +2 -1
  203. package/dist/types/layout/navigation-model.d.ts +2 -0
  204. package/dist/types/layout/resize-separator.d.ts +1 -0
  205. package/dist/types/layout/scroll-fade.d.ts +5 -0
  206. package/dist/types/surfaces/NoticeCard.d.ts +3 -0
  207. package/dist/types/surfaces/Skeleton.d.ts +6 -0
  208. package/dist/types/surfaces/index.d.ts +1 -0
  209. package/package.json +4 -4
@@ -1 +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, 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 // A link focused in the server title keeps focus in the live one: same kind, then same address.\n const active = document.activeElement;\n const focused =\n active && element.contains(active)\n ? {\n kind: [\"k2b-page-title__up\", \"k2b-page-title__back\"].find((name) => active.classList.contains(name)),\n href: active.getAttribute(\"href\"),\n }\n : 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 const upgraded = place.previousSibling;\n if (focused && upgraded instanceof HTMLElement && upgraded.classList.contains(\"k2b-page-title\")) {\n const links = focused.kind ? [...upgraded.querySelectorAll<HTMLElement>(`a.${focused.kind}`)] : [];\n const link =\n links.find((candidate) => candidate.getAttribute(\"href\") === focused.href) ??\n links[0] ??\n upgraded.querySelector<HTMLElement>(\"a[data-k2b-page-trail-up]\");\n link?.focus({ preventScroll: true });\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. A\n // workspace head holds its title in its lead.\n for (const row of root.querySelectorAll<HTMLElement>(\n \"[data-trail]:has(> .k2b-page-title[data-fallback]), [data-trail]:has(> .k2b-app-workspace__head-lead > .k2b-page-title[data-fallback])\",\n ))\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 = new Map<Text, () => void>();\n const upgrade = () => {\n for (const [place, dispose] of disposers) {\n if (place.isConnected) continue;\n dispose();\n disposers.delete(place);\n }\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 // A link focused in the server title keeps focus in the live one: same kind, then same address.\n const active = document.activeElement;\n const focused =\n active && element.contains(active)\n ? {\n kind: [\"k2b-page-title__up\", \"k2b-page-title__back\"].find((name) => active.classList.contains(name)),\n href: active.getAttribute(\"href\"),\n }\n : 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.set(\n place,\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 const upgraded = place.previousSibling;\n if (focused && upgraded instanceof HTMLElement && upgraded.classList.contains(\"k2b-page-title\")) {\n const links = focused.kind ? [...upgraded.querySelectorAll<HTMLElement>(`a.${focused.kind}`)] : [];\n const link =\n links.find((candidate) => candidate.getAttribute(\"href\") === focused.href) ??\n links[0] ??\n upgraded.querySelector<HTMLElement>(\"a[data-k2b-page-trail-up]\");\n link?.focus({ preventScroll: true });\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. A\n // workspace head holds its title in its lead.\n for (const row of root.querySelectorAll<HTMLElement>(\n \"[data-trail]:has(> .k2b-page-title[data-fallback]), [data-trail]:has(> .k2b-app-workspace__head-lead > .k2b-page-title[data-fallback])\",\n ))\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.values()) dispose();\n disposers.clear();\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAAA,SAAS,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,GAAG,EAAY,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,QAAQ,UAAU;AACvI,SAAS,OAAO,EAAE,MAAM,EAAE,QAAQ,QAAQ,cAAc;AACxD,SAAS,cAAc,QAAQ,mBAAmB;AAClD,SAAS,0BAA0B,EAAE,+BAA+B,QAAQ,yBAAyB;AACrG,SAAS,oBAAoB,QAAQ,sBAAsB;;AAE3D;;AAEA;;AAkBA;AACA,OAAO,MAAM,gBAAgB,GAAG,gBAAgB;AAChD,MAAM,eAAe,GAAG,6BAA6B;AAErD,MAAM,OAAO,GAAI,KAAc,IAA8B;EAC3D,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,OAAO,KAAK;EACrD,MAAM,KAAK,GAAG,KAAgC;EAC9C,OACE,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,IAC/B,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,IAC9B,KAAK,CAAC,SAAS,CAAC,KAAK,CAAE,IAAI,IAAK,OAAO,IAAI,EAAE,KAAK,KAAK,QAAQ,IAAI,OAAO,IAAI,EAAE,IAAI,KAAK,QAAQ,CAAC;AAEtG,CAAC;;AAED;AACA,OAAO,MAAM,aAAa,GAAG,MAAkC;EAC7D,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,OAAO,SAAS;EACrD,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAC;EACtD,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,SAAS;EAC1C,IAAI;IACF,MAAM,KAAc,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC;IACrD,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,SAAS;EAC3C,CAAC,CAAC,MAAM;IACN,OAAO,SAAS;EAClB;AACF,CAAC;;AAED;AACA,MAAM,cAAc,GAAI,KAAqB,IAAa,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC;;AAExG;AACA;AACA;AACA;AACA;AACA,OAAO,MAAM,YAAY,GAAI,KAAqB,IAAW;EAC3D,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE;EACrC,IAAI,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAC;EACpD,IAAI,CAAC,MAAM,EAAE;IACX,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC;IACzC,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,kBAAkB,CAAC;IAC/C,MAAM,CAAC,YAAY,CAAC,qBAAqB,EAAE,EAAE,CAAC;IAC9C,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;EAC9B;EACA,MAAM,CAAC,WAAW,GAAG,cAAc,CAAC,KAAK,CAAC;EAC1C,MAAM,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,gBAAgB,GAAG,aAAa,CAAmC,CAAC;;AAE1E;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAAS,iBAAiB,CAAC,KAAmE,EAAe;EAClH,yBACG,gBAAgB,CAAC,QAAQ;IAAC,KAAK,EAAE,MAAM,KAAK,CAAC,KAAK;IAAA;MAAA,0BAChD,IAAI;QAAA,IAAC,IAAI;UAAA,OAAE,KAAK,CAAC,KAAK;QAAA;QAAA,UACnB,KAAK,kBAAuE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;MAAI,IAE1G,KAAK,CAAC,QAAQ;IAAA;EAAA;AAGrB;;AAEA;AACA,OAAO,MAAM,YAAY,GAAG,MAA0C;EACpE,MAAM,OAAO,GAAG,UAAU,CAAC,gBAAgB,CAAC;EAC5C,IAAI,OAAO,EAAE,OAAO,OAAO;EAC3B,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,OAAO,MAAM,SAAS;EACzD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC;EACvD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC;EAC9C,MAAM,CAAC,gBAAgB,CAAC,gBAAgB,EAAE,MAAM,CAAC;EACjD,SAAS,CAAC,MAAM,MAAM,CAAC,mBAAmB,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;EACrE,OAAO,KAAK;AACd,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAAS,SAAS,CAAC,KAAqB,EAAe;EAC5D,MAAM,QAAQ,GAAG,oBAAoB,CAAC,CAAC;EACvC;EACA,MAAM,IAAI,GAAG,QAAQ,GAAG,UAAU,CAAC,gBAAgB,CAAC,GAAG,SAAS;EAChE,MAAM,MAAM,GAAG,uBAAuB,cAAc,CAAC,CAAC,EAAE;EACxD,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,SAAS;EAC7C,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;EACvC,IAAI,IAAgC;EACpC;EACA;EACA,OAAO,CAAC,MAAM;IACZ,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,+BAA+B,GAAG,CAAC,EAAE;IACnE,SAAS,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC;EAC7C,CAAC,CAAC;EACF,sBAGW,2BAAkB,KAAK,CAAC,KAAK,IAAI,EAAE,SAAE,wCAChC,SAAS,CAAC,CAAC,CAAC,MAAM,kDACf,KAAK,CAAC,QAAQ,GAAG,EAAE,YAAG,SAAS,oDAC7B,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,GAAG,CAAC,GAAG,EAAE,YAAG,SAAS,4CAErE,IAAI;IAAA,IAAC,IAAI;MAAA,OAAE,MAAM,CAAC,CAAC;IAAA;IAAA,UAChB,EAAE,sBACD,cAAc;MAAA;MAAA,IAEb,IAAI;QAAA,OAAE,EAAE,CAAC,CAAC,CAAC,IAAI;MAAA;MAAA,IACf,KAAK;QAAA,OAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;UAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;QAAM,CAAC,CAAC;MAAA;MAAA;MAAA;MAAA;QAAA;MAAA;IAAA;EAMjD,gCAGA,IAAI;IAAA,IAAC,IAAI;MAAA,OAAE,sBAAS,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,OAAI,MAAM,CAAC,CAAC;IAAA;IAAA,UAC5C,EAAE,yDAC6C,QAAQ,CAAC,CAAC,CAAC,IAAI,iDAGpD,EAAE,CAAC,CAAC,CAAC,IAAI,wDACH,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;MAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;IAAM,CAAC,CAAC,0BAKd,EAAE,CAAC,CAAC,CAAC,KAAK;EAGpD,gCAEF,IAAI;IAAA,IAAC,IAAI;MAAA,OAAE,SAAS,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC;IAAA;IAAA;MAAA,4DACe,QAAQ,CAAC,CAAC,CAAC,IAAI,4CAEzD,GAAG;QAAA,IAAC,IAAI;UAAA,OAAE,SAAS,CAAC,CAAC;QAAA;QAAA,UACnB,CAAC,IAAI,EAAE,KAAK,wBAER,IAAI;UAAA,IAAC,IAAI;YAAA,OAAE,KAAK,CAAC,CAAC,KAAK,CAAC;UAAA;UAAA;YAAA,+DAGoD,MAAM,wDAAc,QAAQ,CAAC,CAAC,CAAC,UAAU;UAAA;QAAA,yDAKxE,KAAK,CAAC,CAAC,4CAClD,IAAI;UAAA,IAAC,IAAI;YAAA,OAAE,KAAK,CAAC,CAAC,GAAG,CAAC;UAAA;UAAA;YAAA;UAAA;QAAA,qCAIf,IAAI,CAAC,IAAI,0DAES,KAAK,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,EAAE,YAAG,SAAS,0BAG1E,IAAI,CAAC,KAAK;MAIlB,mCAGI,MAAM,4CACZ,GAAG;QAAA,IAAC,IAAI;UAAA,OAAE,SAAS,CAAC,CAAC;QAAA;QAAA,UACnB,CAAC,IAAI,EAAE,KAAK,oDACyB,IAAI,CAAC,IAAI,wDAAc,KAAK,CAAC,CAAC,0BAC/D,IAAI,CAAC,KAAK;MAEd;IAAA;EAAA,gCAKR,OAAO;IAAA,IAAC,SAAS;MAAA,OAAE,KAAK,CAAC,EAAE,IAAI,IAAI;IAAA;IAAA;IAAA;MAAA,gCACE,KAAK,CAAC,KAAK,CAAC,KAAK,sBACpD,IAAI;QAAA,IAAC,IAAI;UAAA,OAAE,KAAK,CAAC,QAAQ;QAAA;QAAA;UAAA,+BACiB,KAAK,CAAC,QAAQ;QAAA;MAAA;IAAA;EAAA;AAMnE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAM,eAAe,GAAG,CAAC,IAAgB,GAAG,QAAQ,KAAmB;EAC5E,MAAM,SAAS,GAAG,IAAI,GAAG,CAAmB,CAAC;EAC7C,MAAM,OAAO,GAAG,MAAM;IACpB,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,SAAS,EAAE;MACxC,IAAI,KAAK,CAAC,WAAW,EAAE;MACvB,OAAO,CAAC,CAAC;MACT,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC;IACzB;IACA,MAAM,KAAK,GAAG,aAAa,CAAC,CAAC;IAC7B,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,gBAAgB,CAAc,kCAAkC,CAAC,EAAE;MAC5F,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa;MACjC,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,4BAA4B,CAAC,EAAE;MAC3D,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,wBAAwB,CAAC,EAAE,SAAS;MACtE,MAAM,EAAE,GAAG,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,IAAI,GAAG,GAAG,GAAG,IAAI;MACnD,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,eAAe,CAAC;MACtD,MAAM,KAAK,GAAG,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,MAAM,CAAE,IAAI,IAAK,IAAI,KAAK,gBAAgB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS;MACvG,MAAM,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAC,2BAA2B,CAAC;MACnE,MAAM,aAAa,GAAG,QAAQ,GAAG,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,GAAG,SAAS;MACrE;MACA,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa;MACrC,MAAM,OAAO,GACX,MAAM,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,GAC9B;QACE,IAAI,EAAE,CAAC,oBAAoB,EAAE,sBAAsB,CAAC,CAAC,IAAI,CAAE,IAAI,IAAK,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpG,IAAI,EAAE,MAAM,CAAC,YAAY,CAAC,MAAM;MAClC,CAAC,GACD,SAAS;MACf;MACA,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;MACzC,GAAG,CAAC,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC;MAChC,OAAO,CAAC,MAAM,CAAC,CAAC;MAChB,SAAS,CAAC,GAAG,CACX,KAAK,EACL,UAAU,CAAE,OAAO,IAAK;QACtB,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC;QAC3B,MAAM,CACJ,GAAG,EACH,wBACG,IAAI;UAAA,IAAC,IAAI;YAAA,OAAE,IAAI,CAAC,CAAC;UAAA;UAAA,UACd,OAAO,sBAAM,SAAS;YAAA,IAAC,KAAK;cAAA,OAAE,OAAO,CAAC,CAAC;YAAA;YAAE,EAAE,EAAE,EAAE;YAAE,QAAQ,EAAE,QAAQ;YAAA,SAAS,KAAK;YAAE,QAAQ,EAAE;UAAa;QAAI,EAEnH,EACD,KACF,CAAC;QACD,OAAO,OAAO;MAChB,CAAC,CACH,CAAC;MACD,MAAM,QAAQ,GAAG,KAAK,CAAC,eAAe;MACtC,IAAI,OAAO,IAAI,QAAQ,YAAY,WAAW,IAAI,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE;QAC/F,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,GAAG,CAAC,GAAG,QAAQ,CAAC,gBAAgB,CAAc,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE;QAClG,MAAM,IAAI,GACR,KAAK,CAAC,IAAI,CAAE,SAAS,IAAK,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,IAC1E,KAAK,CAAC,CAAC,CAAC,IACR,QAAQ,CAAC,aAAa,CAAc,2BAA2B,CAAC;QAClE,IAAI,EAAE,KAAK,CAAC;UAAE,aAAa,EAAE;QAAK,CAAC,CAAC;MACtC;IACF;IACA;IACA,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,gBAAgB,CAAc,8CAA8C,CAAC,EACtG,IAAI,KAAK,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,4BAA4B,CAAC,EAAE,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,KAAK;IAChG;IACA;IACA,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,gBAAgB,CACrC,wIACF,CAAC,EACC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,4BAA4B,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,GAAG,KAAK,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,GAAG,OAAO;EAChH,CAAC;EACD,MAAM,CAAC,gBAAgB,CAAC,gBAAgB,EAAE,OAAO,CAAC;EAClD,OAAO,MAAM;IACX,MAAM,CAAC,mBAAmB,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACrD,KAAK,MAAM,OAAO,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACnD,SAAS,CAAC,KAAK,CAAC,CAAC;EACnB,CAAC;AACH,CAAC","ignoreList":[]}
@@ -32,6 +32,9 @@ export default function ResizeSeparator(props) {
32
32
  },
33
33
  onResize: value => props.onResize(value),
34
34
  onResizeEnd: value => props.onResizeEnd?.(value),
35
+ get onReset() {
36
+ return props.onReset;
37
+ },
35
38
  onCollapseIntent: armed => props.onCollapseIntent?.(armed),
36
39
  onCollapse: () => props.onCollapse?.(),
37
40
  onDragChange: dragging => props.onDragChange?.(dragging)
@@ -40,6 +43,6 @@ export default function ResizeSeparator(props) {
40
43
  createEffect(() => {
41
44
  if (element) element.setAttribute("aria-valuenow", String(Math.round(props.value * 100) / 100));
42
45
  });
43
- return _$ssr(_tmpl$, `k2b-resize-separator${props.class ? ` ${_$escape(props.class, true)}` : ""}`, _$ssrAttribute("aria-orientation", _$escape(props.orientation, true), false) + _$ssrAttribute("aria-valuemin", _$escape(props.min, true), false) + _$ssrAttribute("aria-valuemax", _$escape(props.max, true), false) + _$ssrAttribute("aria-valuenow", _$escape(Math.round(props.value * 100), true) / 100, false) + _$ssrAttribute("aria-label", _$escape(props.label, true), false) + _$ssrAttribute("aria-controls", _$escape(props.controls, true), false) + _$ssrAttribute("data-orientation", _$escape(props.orientation, true), false));
46
+ return _$ssr(_tmpl$, `k2b-resize-separator${props.class ? ` ${_$escape(props.class, true)}` : ""}`, _$ssrAttribute("aria-orientation", _$escape(props.orientation, true), false) + _$ssrAttribute("aria-valuemin", _$escape(props.min, true), false) + _$ssrAttribute("aria-valuemax", _$escape(props.max, true), false) + _$ssrAttribute("aria-valuenow", _$escape(Math.round(props.value * 100), true) / 100, false) + _$ssrAttribute("aria-label", _$escape(props.label, true), false) + _$ssrAttribute("aria-controls", _$escape(props.controls, true), false) + _$ssrAttribute("data-orientation", _$escape(props.orientation, true), false) + _$ssrAttribute("data-resize-edge", props.grow === "start" ? "start" : "end", false));
44
47
  }
45
48
  //# sourceMappingURL=ResizeSeparator.js.map
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["layout/ResizeSeparator.tsx"],"sourcesContent":["import { createEffect, onCleanup, onMount } from \"solid-js\";\nimport { attachResizeSeparator, type ResizeSeparatorOptions } from \"./resize-separator\";\n\nexport type ResizeSeparatorProps = Omit<ResizeSeparatorOptions, \"value\"> & {\n label: string;\n orientation: \"vertical\" | \"horizontal\";\n value: number;\n controls?: string;\n class?: string;\n};\n\nexport default function ResizeSeparator(props: ResizeSeparatorProps) {\n let element: HTMLDivElement | undefined;\n onMount(() => {\n if (element)\n onCleanup(\n attachResizeSeparator(element, {\n get min() {\n return props.min;\n },\n get max() {\n return props.max;\n },\n value: () => props.value,\n get unit() {\n return props.unit;\n },\n get step() {\n return props.step;\n },\n get largeStep() {\n return props.largeStep;\n },\n get grow() {\n return props.grow;\n },\n get collapseBelow() {\n return props.collapseBelow;\n },\n onResize: (value) => props.onResize(value),\n onResizeEnd: (value) => props.onResizeEnd?.(value),\n onCollapseIntent: (armed) => props.onCollapseIntent?.(armed),\n onCollapse: () => props.onCollapse?.(),\n onDragChange: (dragging) => props.onDragChange?.(dragging),\n }),\n );\n });\n createEffect(() => {\n if (element) element.setAttribute(\"aria-valuenow\", String(Math.round(props.value * 100) / 100));\n });\n return (\n <div\n ref={element}\n role=\"separator\"\n tabindex=\"0\"\n class={`k2b-resize-separator${props.class ? ` ${props.class}` : \"\"}`}\n aria-orientation={props.orientation}\n aria-valuemin={props.min}\n aria-valuemax={props.max}\n aria-valuenow={Math.round(props.value * 100) / 100}\n aria-label={props.label}\n aria-controls={props.controls}\n data-orientation={props.orientation}\n >\n <span aria-hidden=\"true\" />\n </div>\n );\n}\n"],"mappings":";;;;AAAA,SAAS,YAAY,EAAE,SAAS,EAAE,OAAO,QAAQ,UAAU;AAC3D,SAAS,qBAAqB,QAAqC,oBAAoB;AAUvF,eAAe,SAAS,eAAe,CAAC,KAA2B,EAAE;EACnE,IAAI,OAAmC;EACvC,OAAO,CAAC,MAAM;IACZ,IAAI,OAAO,EACT,SAAS,CACP,qBAAqB,CAAC,OAAO,EAAE;MAC7B,IAAI,GAAG,GAAG;QACR,OAAO,KAAK,CAAC,GAAG;MAClB,CAAC;MACD,IAAI,GAAG,GAAG;QACR,OAAO,KAAK,CAAC,GAAG;MAClB,CAAC;MACD,KAAK,EAAE,MAAM,KAAK,CAAC,KAAK;MACxB,IAAI,IAAI,GAAG;QACT,OAAO,KAAK,CAAC,IAAI;MACnB,CAAC;MACD,IAAI,IAAI,GAAG;QACT,OAAO,KAAK,CAAC,IAAI;MACnB,CAAC;MACD,IAAI,SAAS,GAAG;QACd,OAAO,KAAK,CAAC,SAAS;MACxB,CAAC;MACD,IAAI,IAAI,GAAG;QACT,OAAO,KAAK,CAAC,IAAI;MACnB,CAAC;MACD,IAAI,aAAa,GAAG;QAClB,OAAO,KAAK,CAAC,aAAa;MAC5B,CAAC;MACD,QAAQ,EAAG,KAAK,IAAK,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC;MAC1C,WAAW,EAAG,KAAK,IAAK,KAAK,CAAC,WAAW,GAAG,KAAK,CAAC;MAClD,gBAAgB,EAAG,KAAK,IAAK,KAAK,CAAC,gBAAgB,GAAG,KAAK,CAAC;MAC5D,UAAU,EAAE,MAAM,KAAK,CAAC,UAAU,GAAG,CAAC;MACtC,YAAY,EAAG,QAAQ,IAAK,KAAK,CAAC,YAAY,GAAG,QAAQ;IAC3D,CAAC,CACH,CAAC;EACL,CAAC,CAAC;EACF,YAAY,CAAC,MAAM;IACjB,IAAI,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;EACjG,CAAC,CAAC;EACF,qBAKW,uBAAuB,KAAK,CAAC,KAAK,GAAG,aAAI,KAAK,CAAC,KAAK,SAAE,GAAG,EAAE,EAAE,8CAClD,KAAK,CAAC,WAAW,2DACpB,KAAK,CAAC,GAAG,2DACT,KAAK,CAAC,GAAG,kDACT,aAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,UAAG,GAAG,iDACtC,KAAK,CAAC,KAAK,2DACR,KAAK,CAAC,QAAQ,8DACX,KAAK,CAAC,WAAW;AAKzC","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["layout/ResizeSeparator.tsx"],"sourcesContent":["import { createEffect, onCleanup, onMount } from \"solid-js\";\nimport { attachResizeSeparator, type ResizeSeparatorOptions } from \"./resize-separator\";\n\nexport type ResizeSeparatorProps = Omit<ResizeSeparatorOptions, \"value\"> & {\n label: string;\n orientation: \"vertical\" | \"horizontal\";\n value: number;\n controls?: string;\n class?: string;\n};\n\nexport default function ResizeSeparator(props: ResizeSeparatorProps) {\n let element: HTMLDivElement | undefined;\n onMount(() => {\n if (element)\n onCleanup(\n attachResizeSeparator(element, {\n get min() {\n return props.min;\n },\n get max() {\n return props.max;\n },\n value: () => props.value,\n get unit() {\n return props.unit;\n },\n get step() {\n return props.step;\n },\n get largeStep() {\n return props.largeStep;\n },\n get grow() {\n return props.grow;\n },\n get collapseBelow() {\n return props.collapseBelow;\n },\n onResize: (value) => props.onResize(value),\n onResizeEnd: (value) => props.onResizeEnd?.(value),\n get onReset() {\n return props.onReset;\n },\n onCollapseIntent: (armed) => props.onCollapseIntent?.(armed),\n onCollapse: () => props.onCollapse?.(),\n onDragChange: (dragging) => props.onDragChange?.(dragging),\n }),\n );\n });\n createEffect(() => {\n if (element) element.setAttribute(\"aria-valuenow\", String(Math.round(props.value * 100) / 100));\n });\n return (\n <div\n ref={element}\n role=\"separator\"\n tabindex=\"0\"\n class={`k2b-resize-separator${props.class ? ` ${props.class}` : \"\"}`}\n aria-orientation={props.orientation}\n aria-valuemin={props.min}\n aria-valuemax={props.max}\n aria-valuenow={Math.round(props.value * 100) / 100}\n aria-label={props.label}\n aria-controls={props.controls}\n data-orientation={props.orientation}\n // A separator that grows toward the start sits on the start edge of the region it resizes; the drag cue shades that region.\n data-resize-edge={props.grow === \"start\" ? \"start\" : \"end\"}\n >\n <span aria-hidden=\"true\" />\n </div>\n );\n}\n"],"mappings":";;;;AAAA,SAAS,YAAY,EAAE,SAAS,EAAE,OAAO,QAAQ,UAAU;AAC3D,SAAS,qBAAqB,QAAqC,oBAAoB;AAUvF,eAAe,SAAS,eAAe,CAAC,KAA2B,EAAE;EACnE,IAAI,OAAmC;EACvC,OAAO,CAAC,MAAM;IACZ,IAAI,OAAO,EACT,SAAS,CACP,qBAAqB,CAAC,OAAO,EAAE;MAC7B,IAAI,GAAG,GAAG;QACR,OAAO,KAAK,CAAC,GAAG;MAClB,CAAC;MACD,IAAI,GAAG,GAAG;QACR,OAAO,KAAK,CAAC,GAAG;MAClB,CAAC;MACD,KAAK,EAAE,MAAM,KAAK,CAAC,KAAK;MACxB,IAAI,IAAI,GAAG;QACT,OAAO,KAAK,CAAC,IAAI;MACnB,CAAC;MACD,IAAI,IAAI,GAAG;QACT,OAAO,KAAK,CAAC,IAAI;MACnB,CAAC;MACD,IAAI,SAAS,GAAG;QACd,OAAO,KAAK,CAAC,SAAS;MACxB,CAAC;MACD,IAAI,IAAI,GAAG;QACT,OAAO,KAAK,CAAC,IAAI;MACnB,CAAC;MACD,IAAI,aAAa,GAAG;QAClB,OAAO,KAAK,CAAC,aAAa;MAC5B,CAAC;MACD,QAAQ,EAAG,KAAK,IAAK,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC;MAC1C,WAAW,EAAG,KAAK,IAAK,KAAK,CAAC,WAAW,GAAG,KAAK,CAAC;MAClD,IAAI,OAAO,GAAG;QACZ,OAAO,KAAK,CAAC,OAAO;MACtB,CAAC;MACD,gBAAgB,EAAG,KAAK,IAAK,KAAK,CAAC,gBAAgB,GAAG,KAAK,CAAC;MAC5D,UAAU,EAAE,MAAM,KAAK,CAAC,UAAU,GAAG,CAAC;MACtC,YAAY,EAAG,QAAQ,IAAK,KAAK,CAAC,YAAY,GAAG,QAAQ;IAC3D,CAAC,CACH,CAAC;EACL,CAAC,CAAC;EACF,YAAY,CAAC,MAAM;IACjB,IAAI,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;EACjG,CAAC,CAAC;EACF,qBAKW,uBAAuB,KAAK,CAAC,KAAK,GAAG,aAAI,KAAK,CAAC,KAAK,SAAE,GAAG,EAAE,EAAE,8CAClD,KAAK,CAAC,WAAW,2DACpB,KAAK,CAAC,GAAG,2DACT,KAAK,CAAC,GAAG,kDACT,aAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,UAAG,GAAG,iDACtC,KAAK,CAAC,KAAK,2DACR,KAAK,CAAC,QAAQ,8DACX,KAAK,CAAC,WAAW,qDAEjB,KAAK,CAAC,IAAI,KAAK,OAAO,GAAG,OAAO,GAAG,KAAK;AAKhE","ignoreList":[]}
@@ -1,6 +1,6 @@
1
1
  import { suppressTextSelection } from "../internal/text-selection";
2
2
  import { prefersReducedMotion } from "../motion";
3
- import { APP_WORKSPACE_DETAIL_DEFAULT, APP_WORKSPACE_DETAIL_MAX, APP_WORKSPACE_DETAIL_MIN, APP_WORKSPACE_DRAWER_MAX, APP_WORKSPACE_DRAWER_MIN, APP_WORKSPACE_MAIN_MIN, APP_WORKSPACE_PANE_DEFAULT, APP_WORKSPACE_PANE_MAX, APP_WORKSPACE_PANE_MIN, APP_WORKSPACE_SIDEBAR_COLLAPSED, APP_WORKSPACE_SIDEBAR_MAX, APP_WORKSPACE_SIDEBAR_MIN, appWorkspacePanelVariable, appWorkspaceResizeLimits, fitAppWorkspacePaneSizes, normalizeAppWorkspaceLayoutState, resolveAppWorkspaceSidebarWidth, safeAppWorkspacePanelId, shouldCollapseAppWorkspaceSidebar } from "./app-workspace-state";
3
+ import { APP_WORKSPACE_DETAIL_DEFAULT, APP_WORKSPACE_DETAIL_MAX, APP_WORKSPACE_DETAIL_MIN, APP_WORKSPACE_DRAWER_DEFAULT, APP_WORKSPACE_DRAWER_MAX, APP_WORKSPACE_DRAWER_MIN, APP_WORKSPACE_MAIN_MIN, APP_WORKSPACE_PANE_DEFAULT, APP_WORKSPACE_PANE_MAX, APP_WORKSPACE_PANE_MIN, APP_WORKSPACE_SIDEBAR_COLLAPSED, APP_WORKSPACE_SIDEBAR_DEFAULT, APP_WORKSPACE_SIDEBAR_MAX, APP_WORKSPACE_SIDEBAR_MIN, appWorkspacePanelVariable, appWorkspaceResizeLimits, fitAppWorkspacePaneSizes, normalizeAppWorkspaceLayoutState, resolveAppWorkspaceSidebarWidth, safeAppWorkspacePanelId, shouldCollapseAppWorkspaceSidebar } from "./app-workspace-state";
4
4
  import { readAppWorkspaceWidth, resolveAppWorkspaceWidth } from "./app-workspace-width";
5
5
  import { installScrollFades } from "./scroll-fade";
6
6
  const HANDLE_SELECTOR = "[data-app-workspace-resize]";
@@ -26,7 +26,7 @@ const parkDetailDrawer = (aside, focus = false) => {
26
26
  detailDrawerContent(aside)?.style.removeProperty("--k2b-detail-space");
27
27
  const toggle = detailToggle(aside);
28
28
  toggle?.setAttribute("aria-expanded", "false");
29
- if (focus) toggle?.focus({
29
+ if (focus) detailReturnControl(aside)?.focus({
30
30
  preventScroll: true
31
31
  });
32
32
  };
@@ -66,6 +66,8 @@ const sidebarHideable = root => sidebarElement(root)?.dataset.workspaceHideable
66
66
  * stays application state, so the controller only requests the change.
67
67
  */
68
68
  export const APP_WORKSPACE_SIDEBAR_HIDE_EVENT = "k2b-app-workspace-sidebar-hide";
69
+ /** Asks the owning Detail to reopen its current available object. */
70
+ export const APP_WORKSPACE_DETAIL_OPEN_EVENT = "k2b-app-workspace-detail-open";
69
71
  /** Asks the owning `AppWorkspace.Detail` to close; its `onClose` decides. */
70
72
  export const APP_WORKSPACE_DETAIL_CLOSE_EVENT = "k2b-app-workspace-detail-close";
71
73
  /**
@@ -80,6 +82,15 @@ const previewSidebarHidden = (root, hidden) => {
80
82
  if (hidden) sidebar.dataset.workspaceHidePreview = "true";else delete sidebar.dataset.workspaceHidePreview;
81
83
  };
82
84
  const isVisible = element => Boolean(element && !element.hidden && getComputedStyle(element).display !== "none");
85
+ // A pane-bound head can be absent on a phone. The visible control in Main
86
+ // that names this same detail remains its return target, within the same root.
87
+ const detailReturnControl = aside => {
88
+ const root = workspaceRoot(aside);
89
+ if (!root) return undefined;
90
+ const shared = detailToggle(aside);
91
+ if (isVisible(shared ?? null) && shared && !shared.matches(":disabled")) return shared;
92
+ return rootElements(root, DETAIL_REQUEST_CONTROL_SELECTOR).find(control => control.matches(FOCUSABLE_SELECTOR) && !aside.contains(control) && !control.matches(":disabled, [inert]") && !control.closest("[inert]") && control.getAttribute("aria-controls")?.split(/\s+/).includes(aside.id) && control.getClientRects().length > 0);
93
+ };
83
94
  const elementSize = (element, kind) => {
84
95
  if (!isVisible(element)) return 0;
85
96
  const rect = element.getBoundingClientRect();
@@ -336,9 +347,20 @@ export const installAppWorkspaceController = (options = {}) => {
336
347
  if (detailButton) {
337
348
  const root = workspaceRoot(detailButton);
338
349
  const aside = root ? controlledPanel(root, detailButton) : null;
339
- if (!aside || aside.hidden || !detailWaiting(aside) || claimedToggleClicks.has(event)) return;
350
+ if (!aside || detailButton.matches(":disabled") || claimedToggleClicks.has(event)) return;
351
+ const reopenable = aside.dataset.workspaceDetailReopen === "true";
352
+ if (!reopenable && (aside.hidden || !detailWaiting(aside))) return;
340
353
  claimedToggleClicks.add(event);
341
- if (detailDrawer(aside)) parkDetailDrawer(aside);else {
354
+ if (aside.hidden) {
355
+ aside.dispatchEvent(new CustomEvent(APP_WORKSPACE_DETAIL_OPEN_EVENT));
356
+ // The app may reject opening (for example while navigating). Never
357
+ // override hidden ourselves or reveal retained, unavailable contents.
358
+ if (aside.hidden) return;
359
+ if (wide?.matches && detailWaiting(aside)) openDetailDrawer(aside);
360
+ if (event.detail === 0 || phone?.matches) requestTitle(aside);else detailButton.focus({
361
+ preventScroll: true
362
+ });
363
+ } else if (reopenable && (!wide?.matches || !detailWaiting(aside))) requestClose(aside);else if (detailDrawer(aside)) parkDetailDrawer(aside);else {
342
364
  openDetailDrawer(aside);
343
365
  if (event.detail === 0) requestTitle(aside);else detailButton.focus({
344
366
  preventScroll: true
@@ -435,6 +457,15 @@ export const installAppWorkspaceController = (options = {}) => {
435
457
  return rootElements(root, detailSelector).find(aside => !aside.hidden && ids?.includes(aside.id) && detailWaiting(aside)) ?? null;
436
458
  };
437
459
  const syncDetailRequestControls = () => {
460
+ for (const root of allRoots()) {
461
+ for (const aside of rootElements(root, detailSelector)) {
462
+ const toggle = detailToggle(aside);
463
+ if (toggle?.dataset.workspaceDetailReopen !== "true") continue;
464
+ const expanded = !aside.hidden && (!wide?.matches || !detailWaiting(aside) || detailDrawer(aside));
465
+ const value = String(expanded);
466
+ if (toggle.getAttribute("aria-expanded") !== value) toggle.setAttribute("aria-expanded", value);
467
+ }
468
+ }
438
469
  const waitingControls = new Map();
439
470
  for (const root of allRoots()) {
440
471
  const waitingDetails = wide?.matches ? rootElements(root, detailSelector).filter(aside => !aside.hidden && detailWaiting(aside)) : [];
@@ -509,7 +540,63 @@ export const installAppWorkspaceController = (options = {}) => {
509
540
  if (args[2] === "sidebar") scheduleHostSync();
510
541
  return size;
511
542
  };
543
+ // A responsive application control and the shared control can name the same
544
+ // Detail. Remember actual focus, not an old selection, before CSS hides it.
545
+ let detailControlFocus = null;
546
+ const controlledDetail = control => {
547
+ if (!control.matches(FOCUSABLE_SELECTOR) || control.matches(HANDLE_SELECTOR) || !eventRoot.contains(control)) return null;
548
+ const root = workspaceRoot(control);
549
+ if (!root) return null;
550
+ const ids = control.getAttribute("aria-controls")?.split(/\s+/);
551
+ const aside = rootElements(root, detailSelector).find(aside => ids?.includes(aside.id));
552
+ return aside ? {
553
+ control,
554
+ aside,
555
+ root
556
+ } : null;
557
+ };
558
+ const detailControlVisible = control => control.isConnected && control.getClientRects().length > 0 && getComputedStyle(control).visibility === "visible";
559
+ const detailControlUsable = control => detailControlVisible(control) && !control.matches(":disabled, [aria-disabled='true']") && !control.closest("[inert], [hidden], [aria-hidden='true']");
560
+ const onDetailControlFocusOut = () => {
561
+ // Explicit blur of a visible control is not a responsive focus loss.
562
+ if (detailControlFocus && detailControlVisible(detailControlFocus.control)) detailControlFocus = null;
563
+ };
564
+ const rescueDetailControlFocus = () => {
565
+ const previous = detailControlFocus;
566
+ if (!previous) return;
567
+ const {
568
+ control,
569
+ aside,
570
+ root
571
+ } = previous;
572
+ const active = ownerDocument.activeElement;
573
+ if (!control.isConnected || !aside.isConnected || workspaceRoot(control) !== root || workspaceRoot(aside) !== root || active !== ownerDocument.body && !control.contains(active)) {
574
+ detailControlFocus = null;
575
+ return;
576
+ }
577
+ const shared = detailToggle(aside);
578
+ if (shared?.dataset.workspaceDetailReopen === "true" && phone?.matches && phonePage(aside) && !aside.hidden && !aside.contains(control)) {
579
+ // WebKit can update matchMedia after resize has already focused the now
580
+ // visible application control. Move into the page before Main becomes inert.
581
+ detailControlFocus = null;
582
+ requestTitle(aside);
583
+ return;
584
+ }
585
+ if (detailControlVisible(control)) return;
586
+ detailControlFocus = null;
587
+ if (shared?.dataset.workspaceDetailReopen !== "true") {
588
+ // Selection-only details keep their existing returning-column title target.
589
+ if (control === shared && !aside.hidden && !detailWaiting(aside)) requestTitle(aside);
590
+ return;
591
+ }
592
+ const candidates = shared ? [shared, ...rootElements(root, "[aria-controls]")] : rootElements(root, "[aria-controls]");
593
+ const replacement = candidates.find(candidate => candidate !== control && detailControlUsable(candidate) && controlledDetail(candidate)?.aside === aside);
594
+ replacement?.focus({
595
+ preventScroll: true
596
+ });
597
+ };
512
598
  const syncHostState = () => {
599
+ rescueDetailControlFocus();
513
600
  if (pendingDetail && !canOpenPendingDetail(pendingDetail.aside)) cancelPendingDetail();
514
601
  if (overlay && sidebarForm(overlay.root) !== "overlay") closeOverlay("none");
515
602
  for (const root of allRoots()) {
@@ -525,11 +612,13 @@ export const installAppWorkspaceController = (options = {}) => {
525
612
  const before = detailWasWaiting.get(aside);
526
613
  detailWasWaiting.set(aside, waiting);
527
614
  if (!waiting) {
528
- // The Details button hides with the waiting; its focus moves to the returning column's title.
529
- const toggleFocused = detailToggle(aside)?.contains(ownerDocument.activeElement) ?? false;
615
+ // Rescue focus only when the legacy waiting-only button disappears.
616
+ // A persistent head control stays a valid focus target after resizing.
617
+ const toggle = detailToggle(aside);
618
+ const toggleFocused = toggle?.contains(ownerDocument.activeElement) ?? false;
530
619
  parkDetailDrawer(aside);
531
- if (toggleFocused) focusTitle(aside);
532
- } else if (detailDrawer(aside)) syncDetailDrawerSpace(aside);else if (before !== true && aside.contains(ownerDocument.activeElement)) detailToggle(aside)?.focus({
620
+ if (toggle && toggleFocused && !isVisible(toggle)) focusTitle(aside);
621
+ } else if (detailDrawer(aside)) syncDetailDrawerSpace(aside);else if (before !== true && aside.contains(ownerDocument.activeElement)) detailReturnControl(aside)?.focus({
533
622
  preventScroll: true
534
623
  });
535
624
  }
@@ -648,6 +737,8 @@ export const installAppWorkspaceController = (options = {}) => {
648
737
  const root = target?.closest(ROOT_SELECTOR);
649
738
  if (!target || !root || !observedRoots.has(root)) return;
650
739
  lastFocus.set(root, target);
740
+ const control = target.closest("[aria-controls]");
741
+ detailControlFocus = control ? controlledDetail(control) : null;
651
742
  const previous = remembered.get(root);
652
743
  // Pointer focus follows pointerdown; keeping that snapshot also keeps the
653
744
  // interaction that opened the panel rather than treating focus as a new one.
@@ -694,6 +785,7 @@ export const installAppWorkspaceController = (options = {}) => {
694
785
  // that opens later than that does not take the drawer or the focus.
695
786
  let lastChosen = null;
696
787
  const cancelTitleOnInteraction = event => {
788
+ if (detailControlFocus && !(event.target instanceof Node && detailControlFocus.control.contains(event.target))) detailControlFocus = null;
697
789
  if (event.type === "pointerdown" || event.type === "keydown") clearActivation();
698
790
  if (lastChosen && !(event.target instanceof Node && lastChosen.item.contains(event.target))) {
699
791
  lastChosen.chosen = false;
@@ -955,7 +1047,13 @@ export const installAppWorkspaceController = (options = {}) => {
955
1047
  pendingAnchors.set(root, cancel);
956
1048
  } else anchorItem(root, aside, snapshot);
957
1049
  const focused = ownerDocument.activeElement;
958
- if (snapshot?.item.isConnected && (aside.contains(lastFocus.get(root) ?? null) || aside.contains(focused) || focused === ownerDocument.body)) snapshot.item.focus({
1050
+ const toggle = detailToggle(aside);
1051
+ const returnControl = detailReturnControl(aside);
1052
+ if (toggle?.dataset.workspaceDetailReopen === "true" && returnControl && (aside.contains(focused) || focused === ownerDocument.body && aside.contains(lastFocus.get(root) ?? null))) {
1053
+ returnControl.focus({
1054
+ preventScroll: true
1055
+ });
1056
+ } else if (snapshot?.item.isConnected && (aside.contains(lastFocus.get(root) ?? null) || aside.contains(focused) || focused === ownerDocument.body)) snapshot.item.focus({
959
1057
  preventScroll: true
960
1058
  });
961
1059
  clearDrag(aside);
@@ -1232,34 +1330,86 @@ export const installAppWorkspaceController = (options = {}) => {
1232
1330
  const handle = resizeHandle(event);
1233
1331
  const root = handle?.closest(ROOT_SELECTOR);
1234
1332
  const kind = handle ? resizeKind(handle) : null;
1235
- if (!handle || !root || kind !== "detail" && kind !== "pane" || !workspaceResizable(root) || !controlsResizableRegion(root, handle, kind)) return;
1236
- const key = kind === "pane" ? "paneWidths" : "detailWidths";
1237
- const widths = {
1238
- ...layoutState[key]
1239
- };
1240
- delete widths[panelId(handle)];
1241
- layoutState = {
1242
- ...layoutState,
1243
- [key]: widths
1244
- };
1245
- const variable = appWorkspacePanelVariable(kind, panelId(handle));
1246
- root.style.removeProperty(variable);
1247
- // An SSR ancestor may still supply the old preference until the next request.
1248
- if (getComputedStyle(root).getPropertyValue(variable).trim()) {
1249
- const spec = readAppWorkspaceWidth(handle.dataset.workspaceDefaultSize) ?? (kind === "pane" ? APP_WORKSPACE_PANE_DEFAULT : APP_WORKSPACE_DETAIL_DEFAULT);
1250
- // A share must resolve at the pane, where the workspace length exists.
1251
- // `initial` clears an inherited SSR preference and restores its CSS fallback.
1252
- root.style.setProperty(variable, typeof spec === "number" ? `${spec}px` : "initial");
1333
+ if (!handle || !root || !kind || !workspaceResizable(root) || !controlsResizableRegion(root, handle, kind)) return;
1334
+ if (active) return;
1335
+ if (kind === "sidebar") {
1336
+ const {
1337
+ sidebarWidth: _width,
1338
+ sidebarCollapsed: _collapsed,
1339
+ ...remaining
1340
+ } = layoutState;
1341
+ layoutState = remaining;
1342
+ } else {
1343
+ const key = kind === "pane" ? "paneWidths" : kind === "drawer" ? "drawerHeights" : "detailWidths";
1344
+ const widths = {
1345
+ ...layoutState[key]
1346
+ };
1347
+ delete widths[panelId(handle)];
1348
+ layoutState = {
1349
+ ...layoutState,
1350
+ [key]: widths
1351
+ };
1253
1352
  }
1254
- // The siblings fit again around the default width, also in a Main without levels.
1255
- if (kind === "pane") reconcilePanes(root, layoutState);
1353
+ const regions = resetRegions.get(root) ?? new Map();
1354
+ regions.set(resizeVariable(handle, kind), {
1355
+ kind,
1356
+ id: panelId(handle),
1357
+ configuration: handle
1358
+ });
1359
+ resetRegions.set(root, regions);
1360
+ writeState();
1361
+ if (kind === "sidebar" || kind === "pane") reconcilePanes(root, layoutState);
1256
1362
  updateHandleValue(root, handle, kind, currentSize(root, handle, kind));
1257
- options.writeState?.(layoutState);
1258
1363
  };
1259
1364
  const stopScrollFades = installScrollFades(eventRoot);
1260
1365
  let layoutState = normalizeAppWorkspaceLayoutState(options.readState?.()) ?? {
1261
1366
  version: 2
1262
1367
  };
1368
+ const resetRegions = new WeakMap();
1369
+ const resizeVariable = (handle, kind) => kind === "sidebar" ? "--k2b-workspace-sidebar-width" : appWorkspacePanelVariable(kind, panelId(handle));
1370
+ const defaultSize = (handle, kind) => readAppWorkspaceWidth(handle.dataset.workspaceDefaultSize) ?? (kind === "sidebar" ? APP_WORKSPACE_SIDEBAR_DEFAULT : kind === "pane" ? APP_WORKSPACE_PANE_DEFAULT : kind === "drawer" ? APP_WORKSPACE_DRAWER_DEFAULT : APP_WORKSPACE_DETAIL_DEFAULT);
1371
+ const restoreResetDefaults = root => {
1372
+ for (const [variable, region] of resetRegions.get(root) ?? []) {
1373
+ const {
1374
+ kind,
1375
+ id
1376
+ } = region;
1377
+ const persisted = kind === "sidebar" ? layoutState.sidebarWidth : layoutState[kind === "pane" ? "paneWidths" : kind === "drawer" ? "drawerHeights" : "detailWidths"]?.[id];
1378
+ if (persisted !== undefined || kind === "sidebar" && layoutState.sidebarCollapsed) continue;
1379
+ const handle = rootElements(root, HANDLE_SELECTOR).find(handle => resizeKind(handle) === kind && panelId(handle) === id);
1380
+ // Keep the latest handle's configuration across unmounts. Limits still
1381
+ // measure today's workspace, so a missing handle cannot freeze its fit.
1382
+ if (handle) region.configuration = handle;
1383
+ const spec = defaultSize(region.configuration, kind);
1384
+ // Store the configured default, not a temporary fit. CSS/relayout owns
1385
+ // the available space, so a numeric level can grow back when room returns.
1386
+ if (typeof spec === "number") {
1387
+ // Sidebar and drawer have no CSS fit. Keep their configured default
1388
+ // in the map, but render within today's bounds and recover it later.
1389
+ if (kind === "sidebar" || kind === "drawer") {
1390
+ const {
1391
+ min,
1392
+ max
1393
+ } = sizeLimits(root, region.configuration, kind);
1394
+ root.style.setProperty(variable, `${clamp(spec, min, max)}px`);
1395
+ } else root.style.setProperty(variable, `${spec}px`);
1396
+ } else {
1397
+ root.style.removeProperty(variable);
1398
+ if (getComputedStyle(root).getPropertyValue(variable).trim()) root.style.setProperty(variable, "initial");
1399
+ }
1400
+ if (kind === "sidebar") delete root.dataset.sidebarCollapsed;
1401
+ }
1402
+ };
1403
+ const writeState = () => {
1404
+ options.writeState?.(layoutState);
1405
+ // Keep reset defaults through reactive writes and temporary handle unmounts,
1406
+ // without saving a width that would mask a changed application default.
1407
+ for (const root of roots()) {
1408
+ if (!workspaceResizable(root)) continue;
1409
+ restoreResetDefaults(root);
1410
+ reconcilePanes(root, layoutState);
1411
+ }
1412
+ };
1263
1413
  let active = null;
1264
1414
  let resizeFrame = null;
1265
1415
  let pendingClient = null;
@@ -1281,7 +1431,9 @@ export const installAppWorkspaceController = (options = {}) => {
1281
1431
  }
1282
1432
  };
1283
1433
  }
1284
- options.writeState?.(layoutState);
1434
+ const root = workspaceRoot(handle);
1435
+ if (root) resetRegions.get(root)?.delete(resizeVariable(handle, kind));
1436
+ writeState();
1285
1437
  };
1286
1438
 
1287
1439
  // A level resize starts from what is on screen. When the stored widths do
@@ -1422,6 +1574,11 @@ export const installAppWorkspaceController = (options = {}) => {
1422
1574
  const kind = handle ? resizeKind(handle) : null;
1423
1575
  const root = handle ? workspaceRoot(handle) : null;
1424
1576
  if (!handle || !kind || !root || !workspaceResizable(root) || !controlsResizableRegion(root, handle, kind)) return;
1577
+ if (event.key === "Enter" && !event.defaultPrevented && !event.isComposing && !event.altKey && !event.ctrlKey && !event.metaKey && !active) {
1578
+ event.preventDefault();
1579
+ onPanelDoubleClick(event);
1580
+ return;
1581
+ }
1425
1582
  const current = currentSize(root, handle, kind);
1426
1583
  const {
1427
1584
  min,
@@ -1500,6 +1657,7 @@ export const installAppWorkspaceController = (options = {}) => {
1500
1657
  // resizing; without this the same workspace still had persisted sizes
1501
1658
  // forced onto it on load and on every window resize.
1502
1659
  if (!workspaceResizable(root)) return;
1660
+ restoreResetDefaults(root);
1503
1661
  const handles = rootElements(root, HANDLE_SELECTOR);
1504
1662
  // Chrome establishes the space available to the work area. Auxiliary
1505
1663
  // panes are then fitted together into what remains, so DOM order cannot
@@ -1654,6 +1812,7 @@ export const installAppWorkspaceController = (options = {}) => {
1654
1812
  ownerDocument.addEventListener("keydown", onOverlayKeyDown);
1655
1813
  ownerWindow.addEventListener("keydown", onDetailKeyDown);
1656
1814
  ownerDocument.addEventListener("focusin", cancelTitleOnInteraction, true);
1815
+ ownerDocument.addEventListener("focusout", onDetailControlFocusOut, true);
1657
1816
  ownerDocument.addEventListener("pointerdown", cancelTitleOnInteraction, true);
1658
1817
  ownerDocument.addEventListener("keydown", cancelTitleOnInteraction, true);
1659
1818
  ownerDocument.addEventListener("click", onDetailRequestClick, true);
@@ -1714,6 +1873,8 @@ export const installAppWorkspaceController = (options = {}) => {
1714
1873
  titleTabIndices.forEach(title => title.removeAttribute("tabindex"));
1715
1874
  ownerWindow.removeEventListener("keydown", onDetailKeyDown);
1716
1875
  ownerDocument.removeEventListener("focusin", cancelTitleOnInteraction, true);
1876
+ ownerDocument.removeEventListener("focusout", onDetailControlFocusOut, true);
1877
+ detailControlFocus = null;
1717
1878
  ownerDocument.removeEventListener("pointerdown", cancelTitleOnInteraction, true);
1718
1879
  ownerDocument.removeEventListener("keydown", cancelTitleOnInteraction, true);
1719
1880
  clearActivation();