@k2b/ui 0.45.0 → 0.47.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 (121) hide show
  1. package/dist/browser/actions/Dropdown.js +8 -1
  2. package/dist/browser/actions/Dropdown.js.map +1 -1
  3. package/dist/browser/actions/FilterChip.js +11 -5
  4. package/dist/browser/actions/FilterChip.js.map +1 -1
  5. package/dist/browser/actions/ViewTabs.js +225 -93
  6. package/dist/browser/actions/ViewTabs.js.map +1 -1
  7. package/dist/browser/chat/ChatDecision.js +10 -6
  8. package/dist/browser/chat/ChatDecision.js.map +1 -1
  9. package/dist/browser/content/FileGrid.js +125 -99
  10. package/dist/browser/content/FileGrid.js.map +1 -1
  11. package/dist/browser/content/VirtualFeed.js +26 -21
  12. package/dist/browser/content/VirtualFeed.js.map +1 -1
  13. package/dist/browser/inputs/Combobox.js +14 -6
  14. package/dist/browser/inputs/Combobox.js.map +1 -1
  15. package/dist/browser/inputs/choice.js +36 -9
  16. package/dist/browser/inputs/choice.js.map +1 -1
  17. package/dist/browser/layout/AnchoredPopover.js +2 -1
  18. package/dist/browser/layout/AnchoredPopover.js.map +1 -1
  19. package/dist/browser/layout/AppWorkspace.js +348 -444
  20. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  21. package/dist/browser/layout/AppWorkspaceSidebarItemStatus.js +113 -0
  22. package/dist/browser/layout/AppWorkspaceSidebarItemStatus.js.map +1 -0
  23. package/dist/browser/layout/DetailPanel.js +130 -108
  24. package/dist/browser/layout/DetailPanel.js.map +1 -1
  25. package/dist/browser/layout/DetailSections.js +1123 -0
  26. package/dist/browser/layout/DetailSections.js.map +1 -0
  27. package/dist/browser/layout/DetailsToggle.js +32 -0
  28. package/dist/browser/layout/DetailsToggle.js.map +1 -0
  29. package/dist/browser/layout/Discussion.js +3 -0
  30. package/dist/browser/layout/Discussion.js.map +1 -1
  31. package/dist/browser/layout/MobileShell.js +141 -102
  32. package/dist/browser/layout/MobileShell.js.map +1 -1
  33. package/dist/browser/layout/PageTitle.js +33 -10
  34. package/dist/browser/layout/PageTitle.js.map +1 -1
  35. package/dist/browser/layout/app-workspace-controller.js +12 -0
  36. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  37. package/dist/browser/layout/detail-sections-messages.js +127 -0
  38. package/dist/browser/layout/detail-sections-messages.js.map +1 -0
  39. package/dist/browser/layout/index.js +3 -0
  40. package/dist/browser/layout/index.js.map +1 -1
  41. package/dist/browser/surfaces/Avatar.js +3 -1
  42. package/dist/browser/surfaces/Avatar.js.map +1 -1
  43. package/dist/browser/surfaces/AvatarStack.js +95 -61
  44. package/dist/browser/surfaces/AvatarStack.js.map +1 -1
  45. package/dist/browser/surfaces/ChangeList.js +77 -0
  46. package/dist/browser/surfaces/ChangeList.js.map +1 -0
  47. package/dist/browser/surfaces/index.js +2 -1
  48. package/dist/browser/surfaces/index.js.map +1 -1
  49. package/dist/browser/text-editor/TextEditor.js +19 -8
  50. package/dist/browser/text-editor/TextEditor.js.map +1 -1
  51. package/dist/ssr/actions/Dropdown.js +6 -0
  52. package/dist/ssr/actions/Dropdown.js.map +1 -1
  53. package/dist/ssr/actions/FilterChip.js +1 -1
  54. package/dist/ssr/actions/FilterChip.js.map +1 -1
  55. package/dist/ssr/actions/ViewTabs.js +144 -37
  56. package/dist/ssr/actions/ViewTabs.js.map +1 -1
  57. package/dist/ssr/chat/ChatDecision.js +10 -6
  58. package/dist/ssr/chat/ChatDecision.js.map +1 -1
  59. package/dist/ssr/content/FileGrid.js +36 -22
  60. package/dist/ssr/content/FileGrid.js.map +1 -1
  61. package/dist/ssr/content/VirtualFeed.js +26 -21
  62. package/dist/ssr/content/VirtualFeed.js.map +1 -1
  63. package/dist/ssr/inputs/Combobox.js +6 -3
  64. package/dist/ssr/inputs/Combobox.js.map +1 -1
  65. package/dist/ssr/inputs/choice.js +36 -9
  66. package/dist/ssr/inputs/choice.js.map +1 -1
  67. package/dist/ssr/layout/AnchoredPopover.js +2 -1
  68. package/dist/ssr/layout/AnchoredPopover.js.map +1 -1
  69. package/dist/ssr/layout/AppWorkspace.js +66 -132
  70. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  71. package/dist/ssr/layout/AppWorkspaceSidebarItemStatus.js +76 -0
  72. package/dist/ssr/layout/AppWorkspaceSidebarItemStatus.js.map +1 -0
  73. package/dist/ssr/layout/DetailPanel.js +18 -1
  74. package/dist/ssr/layout/DetailPanel.js.map +1 -1
  75. package/dist/ssr/layout/DetailSections.js +838 -0
  76. package/dist/ssr/layout/DetailSections.js.map +1 -0
  77. package/dist/ssr/layout/DetailsToggle.js +29 -0
  78. package/dist/ssr/layout/DetailsToggle.js.map +1 -0
  79. package/dist/ssr/layout/Discussion.js +3 -0
  80. package/dist/ssr/layout/Discussion.js.map +1 -1
  81. package/dist/ssr/layout/MobileShell.js +60 -29
  82. package/dist/ssr/layout/MobileShell.js.map +1 -1
  83. package/dist/ssr/layout/PageTitle.js +29 -6
  84. package/dist/ssr/layout/PageTitle.js.map +1 -1
  85. package/dist/ssr/layout/app-workspace-controller.js +12 -0
  86. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  87. package/dist/ssr/layout/detail-sections-messages.js +127 -0
  88. package/dist/ssr/layout/detail-sections-messages.js.map +1 -0
  89. package/dist/ssr/layout/index.js +3 -0
  90. package/dist/ssr/layout/index.js.map +1 -1
  91. package/dist/ssr/surfaces/Avatar.js +3 -1
  92. package/dist/ssr/surfaces/Avatar.js.map +1 -1
  93. package/dist/ssr/surfaces/AvatarStack.js +61 -32
  94. package/dist/ssr/surfaces/AvatarStack.js.map +1 -1
  95. package/dist/ssr/surfaces/ChangeList.js +37 -0
  96. package/dist/ssr/surfaces/ChangeList.js.map +1 -0
  97. package/dist/ssr/surfaces/index.js +2 -1
  98. package/dist/ssr/surfaces/index.js.map +1 -1
  99. package/dist/ssr/text-editor/TextEditor.js +19 -8
  100. package/dist/ssr/text-editor/TextEditor.js.map +1 -1
  101. package/dist/styles.css +1 -1
  102. package/dist/types/actions/FilterChip.d.ts +4 -0
  103. package/dist/types/actions/ViewTabs.d.ts +11 -1
  104. package/dist/types/chat/ChatDecision.d.ts +5 -0
  105. package/dist/types/content/FileGrid.d.ts +6 -0
  106. package/dist/types/inputs/Combobox.d.ts +2 -0
  107. package/dist/types/inputs/choice.d.ts +9 -3
  108. package/dist/types/layout/AppWorkspace.d.ts +2 -28
  109. package/dist/types/layout/AppWorkspaceSidebarItemStatus.d.ts +30 -0
  110. package/dist/types/layout/DetailPanel.d.ts +9 -0
  111. package/dist/types/layout/DetailSections.d.ts +186 -0
  112. package/dist/types/layout/DetailsToggle.d.ts +13 -0
  113. package/dist/types/layout/MobileShell.d.ts +12 -0
  114. package/dist/types/layout/PageTitle.d.ts +7 -0
  115. package/dist/types/layout/detail-sections-messages.d.ts +61 -0
  116. package/dist/types/layout/index.d.ts +5 -1
  117. package/dist/types/surfaces/Avatar.d.ts +3 -0
  118. package/dist/types/surfaces/AvatarStack.d.ts +20 -1
  119. package/dist/types/surfaces/ChangeList.d.ts +20 -0
  120. package/dist/types/surfaces/index.d.ts +5 -3
  121. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["chat/ChatDecision.tsx"],"sourcesContent":["import {\n createContext,\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n For,\n type JSX,\n on,\n onCleanup,\n onMount,\n Show,\n untrack,\n useContext,\n} from \"solid-js\";\nimport { Button } from \"../actions/Button\";\nimport { Avatar } from \"../surfaces/Avatar\";\nimport { useChatMessages } from \"./chat-messages\";\nimport type {\n ChatDecisionAnswer,\n ChatDecisionReason,\n ChatDecisionState,\n ChatOutcomeLine,\n ChatOutcomeTone,\n ChatPreviewHeaderRow,\n ChatPreviewPerson,\n} from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Decision\n// ---------------------------------------------------------------------------\n\nexport type ChatDecisionProps = {\n /** `open` shows the question; every other state shows `outcome` in its place. */\n state: ChatDecisionState;\n /** The question as one sentence, usually with `ReferenceChip`s. */\n sentence: JSX.Element;\n /** Why this question is asked, in the platform's words. */\n reason: ChatDecisionReason;\n /** The thing itself, usually `Chat.Preview`. */\n preview?: JSX.Element;\n /** The main answer, named by verb and target (\"Send to Lea\"). `danger` for deletions, `primary` for requests. */\n confirm: ChatDecisionAnswer & { variant?: \"ai\" | \"primary\" | \"danger\" };\n /** The other answer (\"Don't send\"). */\n decline: ChatDecisionAnswer;\n /** An optional quiet third answer after the buttons, such as \"Yes, and don't ask again in this chat\". */\n remember?: ChatDecisionAnswer;\n /** Turns `confirm` and `remember` off; this reason replaces `reason` in the same place. */\n unavailable?: ChatDecisionReason;\n /** The result line shown in every state but `open`, usually `Chat.Outcome`. */\n outcome?: JSX.Element;\n /** Receives a rejected promise from an answer's `onSelect`. */\n onAnswerError?: (error: unknown) => void;\n anchorId?: string | number;\n class?: string;\n};\n\n/** What an outcome inside a question needs from it: whether it was just answered here, and a link press inside it. */\nconst DecisionScope = createContext<{ answered: () => boolean; pressed: () => void; released: () => void }>();\n\nconst reasonIcon = (reason: ChatDecisionReason) => reason.icon ?? \"ti ti-info-circle\";\n\n/**\n * A question the person answers in the conversation: one sentence, why it is asked, the thing itself, and two answers.\n * An answer collapses it at once to its outcome line, which takes the focus and is announced.\n */\nexport function ChatDecision(props: ChatDecisionProps): JSX.Element {\n const id = createUniqueId();\n const sentenceId = `k2b-chat-decision-${id}-sentence`;\n const reasonId = `k2b-chat-decision-${id}-reason`;\n let root: HTMLDivElement | undefined;\n let sentence: HTMLParagraphElement | undefined;\n // An answer given here moves the focus to the outcome; a click in the outcome that reopens the question moves it\n // back to the sentence. A state change from elsewhere moves no focus.\n const [answeredHere, setAnsweredHere] = createSignal(false);\n const [arrive, setArrive] = createSignal(false);\n let reopenedHere = false;\n // A press on the sentence focuses it too; only a focus moved there from elsewhere, such as a jump, lights it up.\n // The browser also focuses the sentence again when its window comes back; that is no arrival either.\n let sentencePressed = false;\n let sentenceHeldAcrossWindow = false;\n const windowLeft = () => {\n if (sentence && document.activeElement === sentence) sentenceHeldAcrossWindow = true;\n };\n onMount(() => {\n window.addEventListener(\"blur\", windowLeft);\n onCleanup(() => window.removeEventListener(\"blur\", windowLeft));\n });\n // A press counts only while its action runs: a reopen from elsewhere later must not take the focus.\n const scope = {\n answered: answeredHere,\n pressed: () => {\n reopenedHere = true;\n },\n released: () => {\n reopenedHere = false;\n },\n };\n\n // Read once: an object literal from the host builds its JSX again on every read.\n const reason = createMemo(() => props.unavailable ?? props.reason);\n const open = () => props.state === \"open\";\n\n const answer = (choice: ChatDecisionAnswer, enabled: boolean) => {\n if (!enabled || !open() || answeredHere()) return;\n setAnsweredHere(true);\n try {\n const result = choice.onSelect();\n if (result instanceof Promise) result.catch((error: unknown) => props.onAnswerError?.(error));\n } catch (error) {\n props.onAnswerError?.(error);\n }\n // A host that keeps the question open (it could not take the answer) gets working buttons back.\n queueMicrotask(() => {\n if (open()) setAnsweredHere(false);\n });\n };\n\n createEffect(\n on(\n () => props.state,\n (state, previous) => {\n if (state !== \"open\" && previous === \"open\" && answeredHere()) {\n root?.querySelector<HTMLElement>(\".k2b-chat-outcome\")?.focus({ preventScroll: true });\n }\n if (state === \"open\" && previous !== undefined && previous !== \"open\") {\n // A host that could not take an answer given here returns the question from `sending`; the outcome that held\n // the focus is gone, so the focus goes back to the sentence instead of the page.\n const active = document.activeElement;\n const failedHere = answeredHere() && previous === \"sending\" && (!active || active === document.body);\n setAnsweredHere(false);\n if (reopenedHere || failedHere) {\n sentence?.focus({ preventScroll: true });\n setArrive(true);\n }\n // Handled. Until then the marker holds through states in between, such as `sending` while a reopen loads.\n reopenedHere = false;\n }\n },\n ),\n );\n\n return (\n <div\n ref={root}\n class={`k2b-chat-decision ${props.class ?? \"\"}`}\n data-state={props.state}\n data-chat-anchor={props.anchorId !== undefined ? String(props.anchorId) : undefined}\n >\n <Show when={open()} fallback={<DecisionScope.Provider value={scope}>{props.outcome}</DecisionScope.Provider>}>\n <section class=\"k2b-chat-decision__card\" aria-labelledby={sentenceId} aria-describedby={reasonId}>\n <p\n ref={sentence}\n id={sentenceId}\n class=\"k2b-chat-decision__sentence\"\n tabindex=\"-1\"\n data-arrive={arrive() ? \"true\" : undefined}\n onPointerDown={() => {\n sentencePressed = true;\n }}\n onPointerUp={() => {\n sentencePressed = false;\n }}\n onPointerCancel={() => {\n sentencePressed = false;\n }}\n onFocus={() => {\n if (!sentencePressed && !sentenceHeldAcrossWindow) setArrive(true);\n sentencePressed = false;\n sentenceHeldAcrossWindow = false;\n }}\n onAnimationEnd={() => setArrive(false)}\n >\n {props.sentence}\n </p>\n <p id={reasonId} class=\"k2b-chat-line k2b-chat-decision__reason\" data-tone={reason().tone ?? \"neutral\"}>\n <i class={`k2b-chat-line__icon ${reasonIcon(reason())}`} aria-hidden=\"true\" />\n <span class=\"k2b-chat-line__text\">{reason().text}</span>\n </p>\n {props.preview}\n <div class=\"k2b-chat-decision__answers\">\n <Button\n class=\"k2b-chat-decision__answer\"\n wrap\n variant={props.confirm.variant ?? \"ai\"}\n disabled={Boolean(props.unavailable)}\n onClick={() => answer(props.confirm, !props.unavailable)}\n >\n <Show when={props.confirm.icon}>{(icon) => <i class={icon()} aria-hidden=\"true\" />}</Show>\n {props.confirm.label}\n </Button>\n <Button class=\"k2b-chat-decision__answer\" wrap variant=\"subtle\" onClick={() => answer(props.decline, true)}>\n <Show when={props.decline.icon}>{(icon) => <i class={icon()} aria-hidden=\"true\" />}</Show>\n {props.decline.label}\n </Button>\n <Show when={props.remember}>\n {(remember) => (\n <Button\n class=\"k2b-chat-decision__remember\"\n wrap\n variant=\"text\"\n disabled={Boolean(props.unavailable)}\n onClick={() => answer(remember(), !props.unavailable)}\n >\n {remember().label}\n </Button>\n )}\n </Show>\n </div>\n </section>\n </Show>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Outcome\n// ---------------------------------------------------------------------------\n\nexport type ChatOutcomeProps = {\n /**\n * Every line one flow can show, such as running and done. All of them share one cell, so the line is as high as\n * the longest and switching between them moves nothing.\n */\n lines: Readonly<Record<string, ChatOutcomeLine>>;\n /** The key of the visible line. */\n current: string;\n /** Receives an error thrown or rejected by a line's `action`. */\n onActionError?: (error: unknown) => void;\n anchorId?: string | number;\n class?: string;\n};\n\nconst OUTCOME_ICONS: Record<ChatOutcomeTone, string> = {\n running: \"ti ti-loader-2 k2b-spin\",\n done: \"ti ti-check\",\n off: \"ti ti-circle-off\",\n waiting: \"ti ti-clock\",\n warning: \"ti ti-alert-triangle\",\n error: \"ti ti-alert-circle\",\n};\n\n/** The visible words of a line, without what screen readers do not hear. */\nconst spokenText = (element: Element): string => {\n const copy = element.cloneNode(true) as Element;\n // Buttons under the line are controls, not part of what happened.\n for (const hidden of copy.querySelectorAll('[aria-hidden=\"true\"], .k2b-chat-outcome__actions')) hidden.remove();\n // The time and the link stand apart by margin only; spoken, they need a space.\n for (const part of copy.querySelectorAll(\".k2b-chat-outcome__time, .k2b-chat-outcome__link\")) part.before(\" \");\n return (copy.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n};\n\n/**\n * One line that says what happened: running, done, off, waiting, warning, or error. Changes are announced through a\n * polite live region; the line itself can take the focus.\n */\nexport function ChatOutcome(props: ChatOutcomeProps): JSX.Element {\n const scope = useContext(DecisionScope);\n // Only a line that replaces a question just answered here fades in; history renders final.\n const entering = untrack(() => scope?.answered() ?? false);\n const [live, setLive] = createSignal(\"\");\n let root: HTMLDivElement | undefined;\n let cell: HTMLDivElement | undefined;\n let pending = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n // Read once: an object literal from the host builds its JSX again on every read.\n const lines = createMemo(() => props.lines);\n const line = () => lines()[props.current];\n const tone = () => line()?.tone ?? \"off\";\n const keys = () => {\n const all = Object.keys(lines());\n return all.includes(props.current) ? all : [props.current, ...all];\n };\n\n // Empty the region first, so the same words spoken again are a change too.\n const announce = () => {\n setLive(\"\");\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n const current = cell?.querySelector('[data-current=\"true\"]');\n setLive(current ? spokenText(current) : \"\");\n }, 40);\n };\n\n onMount(() => {\n if (untrack(() => scope?.answered())) announce();\n });\n createEffect(\n on(\n () => props.current,\n () => {\n // A link pressed in a line that is now hidden would leave the focus on an invisible button, which the browser\n // then drops to the page. The line itself keeps it instead.\n const active = document.activeElement;\n const hidden = active && cell?.contains(active) && !active.closest('[data-current=\"true\"]');\n if (hidden || (pending && active === document.body)) root?.focus({ preventScroll: true });\n announce();\n },\n { defer: true },\n ),\n );\n onCleanup(() => {\n if (timer) clearTimeout(timer);\n });\n\n return (\n <div\n ref={root}\n class={`k2b-chat-outcome ${props.class ?? \"\"}`}\n data-tone={tone()}\n data-chat-anchor={props.anchorId !== undefined ? String(props.anchorId) : undefined}\n data-enter={entering ? \"true\" : undefined}\n tabindex=\"-1\"\n >\n <span class=\"k2b-chat-outcome__icon\" aria-hidden=\"true\">\n <i class={line()?.icon ?? OUTCOME_ICONS[tone()]} />\n </span>\n {/* Busy is set on the words only: a live region inside a busy subtree would be held back. */}\n <div ref={cell} class=\"k2b-chat-outcome__cell\" aria-busy={tone() === \"running\" ? \"true\" : undefined}>\n <For each={keys()}>\n {(key) => {\n const entry = () => lines()[key];\n const current = () => key === props.current;\n return (\n <Show when={entry()}>\n {(item) => (\n <div\n class=\"k2b-chat-outcome__text\"\n data-current={current() ? \"true\" : undefined}\n aria-hidden={current() ? undefined : \"true\"}\n >\n {item().text}\n <Show when={item().detail}>\n {(detail) => (\n <>\n {\" \"}\n <span class=\"k2b-chat-outcome__detail\">{detail()}</span>\n </>\n )}\n </Show>\n <Show when={item().time}>{(time) => <span class=\"k2b-chat-outcome__time\">{time()}</span>}</Show>\n <Show when={item().action}>\n {(action) => (\n <Button\n class=\"k2b-chat-outcome__link\"\n wrap\n variant=\"text\"\n tabIndex={current() ? undefined : -1}\n onClick={() => {\n // A second press while the first is still running would repeat its work.\n if (pending) return;\n scope?.pressed();\n try {\n const result = action().onSelect();\n if (result instanceof Promise) {\n pending = true;\n result\n .catch((error: unknown) => props.onActionError?.(error))\n .finally(() => {\n pending = false;\n scope?.released();\n });\n return;\n }\n } catch (error) {\n props.onActionError?.(error);\n }\n scope?.released();\n }}\n >\n {action().label}\n </Button>\n )}\n </Show>\n {/* Every line keeps its buttons, hidden with its text, so the cell also reserves their row. */}\n <Show when={item().actions}>{(actions) => <div class=\"k2b-chat-outcome__actions\">{actions()}</div>}</Show>\n </div>\n )}\n </Show>\n );\n }}\n </For>\n </div>\n <span class=\"k2b-sr-only\" role=\"status\" aria-live=\"polite\">\n {live()}\n </span>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Preview\n// ---------------------------------------------------------------------------\n\nexport type ChatPreviewProps = { children: JSX.Element; class?: string };\n\n/** The thing a question is about, as calm text on the sentence's edge: no surface, no column. */\nfunction ChatPreviewRoot(props: ChatPreviewProps): JSX.Element {\n return <div class={`k2b-chat-preview ${props.class ?? \"\"}`}>{props.children}</div>;\n}\n\nexport type ChatPreviewHeaderProps = {\n /** One row per field, such as From, To, Cc, and Bcc; the label stands right before its value. */\n rows: readonly ChatPreviewHeaderRow[];\n /** A last line, such as when the message goes out. */\n note?: JSX.Element;\n};\n\nconst PreviewPeople = (props: { people: readonly ChatPreviewPerson[] }) => (\n <For each={props.people}>\n {(person, index) => (\n <>\n {index() > 0 ? \", \" : \"\"}\n <span class=\"k2b-chat-preview__person\">\n <Show when={person.name}>{(name) => <b>{name()}</b>}</Show>\n <Show when={person.address}>\n {(address) => (\n <span class=\"k2b-chat-preview__address\" data-alone={person.name ? undefined : \"true\"}>\n {address()}\n </span>\n )}\n </Show>\n </span>\n </>\n )}\n </For>\n);\n\nfunction ChatPreviewHeader(props: ChatPreviewHeaderProps): JSX.Element {\n return (\n <div class=\"k2b-chat-preview__header\">\n <dl>\n <For each={props.rows}>\n {(row) => (\n <div data-emphasis={row.emphasis ? \"true\" : undefined}>\n <dt>{row.label}</dt>\n <dd>\n <Show when={row.people}>{(people) => <PreviewPeople people={people()} />}</Show>\n {row.value}\n </dd>\n </div>\n )}\n </For>\n </dl>\n <Show when={props.note}>{(note) => <p>{note()}</p>}</Show>\n </div>\n );\n}\n\nexport type ChatPreviewTitleProps = { children: JSX.Element; meta?: JSX.Element };\n\n/** The thing's name in the text's own face, with optional quiet facts after it. */\nfunction ChatPreviewTitle(props: ChatPreviewTitleProps): JSX.Element {\n return (\n <p class=\"k2b-chat-preview__title-line\">\n <span class=\"k2b-chat-preview__title\">{props.children}</span>\n <Show when={props.meta}>\n {(meta) => (\n <>\n {\" \"}\n <span class=\"k2b-chat-preview__meta\">{meta()}</span>\n </>\n )}\n </Show>\n </p>\n );\n}\n\n/** A rule or instruction as one sentence, such as what a routine does; chips and marks may stand inside. */\nfunction ChatPreviewSentence(props: { children: JSX.Element }): JSX.Element {\n return <p class=\"k2b-chat-preview__sentence\">{props.children}</p>;\n}\n\nexport type ChatPreviewExcerptProps = {\n children: JSX.Element;\n /** Shows the whole text in place instead of the first lines, such as after `Chat.Preview.ShowAll`. */\n expanded?: boolean;\n id?: string;\n};\n\n/** The first lines of a longer text. It keeps its line breaks, shows about three lines, and fades out. */\nfunction ChatPreviewExcerpt(props: ChatPreviewExcerptProps): JSX.Element {\n return (\n <p\n class=\"k2b-chat-preview__excerpt\"\n id={props.id}\n data-expanded={props.expanded ? \"true\" : undefined}\n tabindex={props.expanded ? -1 : undefined}\n >\n {props.children}\n </p>\n );\n}\n\nexport type ChatPreviewShowAllProps = {\n /** The id of the `Chat.Preview.Excerpt` this button opens. */\n controls: string;\n onShow: () => void;\n children: JSX.Element;\n};\n\n/**\n * Shows the rest of an excerpt in place, for a text with no page of its own to open. It looks like `ReadMore`; the\n * caller removes it once the text is shown, and the focus moves to the text, so reading goes on where it was.\n */\nfunction ChatPreviewShowAll(props: ChatPreviewShowAllProps): JSX.Element {\n const show = () => {\n props.onShow();\n queueMicrotask(() => document.getElementById(props.controls)?.focus({ preventScroll: true }));\n };\n return (\n <button type=\"button\" class=\"k2b-chat-preview__more\" aria-controls={props.controls} aria-expanded=\"false\" onClick={show}>\n {props.children}\n </button>\n );\n}\n\nexport type ChatPreviewReadMoreProps = {\n href: string;\n children: JSX.Element;\n /** Tooltip, such as \"Opens exactly this version of the draft\". */\n title?: string;\n};\n\n/** Opens the whole thing at exactly the version the question shows. */\nfunction ChatPreviewReadMore(props: ChatPreviewReadMoreProps): JSX.Element {\n return (\n <a class=\"k2b-chat-preview__more\" href={props.href} title={props.title}>\n {props.children}\n </a>\n );\n}\n\nconst InList = createContext(false);\n\n/** A list of lines with a name, such as \"What the routine may do\". */\nfunction ChatPreviewLines(props: { label?: string; children: JSX.Element }): JSX.Element {\n return (\n <ul class=\"k2b-chat-preview__lines\" aria-label={props.label}>\n <InList.Provider value={true}>{props.children}</InList.Provider>\n </ul>\n );\n}\n\nexport type ChatPreviewLineProps = {\n /** Icon class; the text after it starts 1.5rem from the edge. */\n icon: string;\n /** Icon colour, such as the PDF colour of an attachment. */\n accent?: string;\n /** `muted` for limits and notes, `warning` for an application's own warning. */\n tone?: \"default\" | \"muted\" | \"warning\";\n /** Marks the whole line as new or removed. */\n mark?: \"added\" | \"removed\";\n /** Quiet facts after the text, such as a file size. */\n meta?: JSX.Element;\n children: JSX.Element;\n};\n\n/** One line led by an icon: a consequence, a right, an attachment, or a warning. */\nfunction ChatPreviewLine(props: ChatPreviewLineProps): JSX.Element {\n const inList = useContext(InList);\n const messages = useChatMessages();\n const content = () => (\n <>\n <i class={`k2b-chat-line__icon ${props.icon}`} style={props.accent ? { color: props.accent } : undefined} aria-hidden=\"true\" />\n <span class=\"k2b-chat-line__text\">\n {/* One stable wrapper, so marking a line later never moves its mounted children. */}\n <span class=\"k2b-chat-mark\" data-change={props.mark}>\n <Show when={props.mark}>\n {(mark) => <span class=\"k2b-sr-only\">{mark() === \"added\" ? messages().markAdded : messages().markRemoved} </span>}\n </Show>\n {props.children}\n </span>\n <Show when={props.meta}>\n {(meta) => (\n <>\n {\" \"}\n <span class=\"k2b-chat-preview__meta\">{meta()}</span>\n </>\n )}\n </Show>\n </span>\n </>\n );\n const attributes = () => ({\n class: \"k2b-chat-line k2b-chat-preview__line\",\n \"data-tone\": props.tone && props.tone !== \"default\" ? props.tone : undefined,\n });\n return inList ? <li {...attributes()}>{content()}</li> : <p {...attributes()}>{content()}</p>;\n}\n\nexport type ChatPreviewPeopleProps = {\n /** The first few people; their avatars overlap at the start of the line. */\n people: readonly { name: string; src?: string | null }[];\n /** The names as text, such as \"Lea Berg, Tom Weber and 3 others\". */\n children: JSX.Element;\n};\n\nfunction ChatPreviewPeople(props: ChatPreviewPeopleProps): JSX.Element {\n return (\n <p class=\"k2b-chat-preview__people\">\n <span class=\"k2b-chat-preview__faces\" aria-hidden=\"true\">\n <For each={props.people}>{(person) => <Avatar name={person.name} src={person.src} size=\"xs\" />}</For>\n </span>\n <span>{props.children}</span>\n </p>\n );\n}\n\nexport const ChatPreview = Object.assign(ChatPreviewRoot, {\n Header: ChatPreviewHeader,\n Title: ChatPreviewTitle,\n Sentence: ChatPreviewSentence,\n Excerpt: ChatPreviewExcerpt,\n ReadMore: ChatPreviewReadMore,\n ShowAll: ChatPreviewShowAll,\n Lines: ChatPreviewLines,\n Line: ChatPreviewLine,\n People: ChatPreviewPeople,\n});\n\n// ---------------------------------------------------------------------------\n// Mark\n// ---------------------------------------------------------------------------\n\nexport type ChatMarkProps = { change: \"added\" | \"removed\"; children: JSX.Element };\n\n/** Marks new or removed words; screen readers hear \"new:\" or \"removed:\" first. */\nexport function ChatMark(props: ChatMarkProps): JSX.Element {\n const messages = useChatMessages();\n return (\n <Show\n when={props.change === \"removed\"}\n fallback={\n <mark class=\"k2b-chat-mark\" data-change=\"added\">\n <span class=\"k2b-sr-only\">{messages().markAdded} </span>\n {props.children}\n </mark>\n }\n >\n <del class=\"k2b-chat-mark\" data-change=\"removed\">\n <span class=\"k2b-sr-only\">{messages().markRemoved} </span>\n {props.children}\n </del>\n </Show>\n );\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["chat/ChatDecision.tsx"],"sourcesContent":["import {\n createContext,\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n For,\n type JSX,\n on,\n onCleanup,\n onMount,\n Show,\n untrack,\n useContext,\n} from \"solid-js\";\nimport { Button } from \"../actions/Button\";\nimport { Avatar } from \"../surfaces/Avatar\";\nimport { useChatMessages } from \"./chat-messages\";\nimport type {\n ChatDecisionAnswer,\n ChatDecisionReason,\n ChatDecisionState,\n ChatOutcomeLine,\n ChatOutcomeTone,\n ChatPreviewHeaderRow,\n ChatPreviewPerson,\n} from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Decision\n// ---------------------------------------------------------------------------\n\nexport type ChatDecisionProps = {\n /** `open` shows the question; every other state shows `outcome` in its place. */\n state: ChatDecisionState;\n /** The question as one sentence, usually with `ReferenceChip`s. */\n sentence: JSX.Element;\n /** Why this question is asked, in the platform's words. */\n reason: ChatDecisionReason;\n /** The thing itself, usually `Chat.Preview`. */\n preview?: JSX.Element;\n /** The main answer, named by verb and target (\"Send to Lea\"). `danger` for deletions, `primary` for requests. */\n confirm: ChatDecisionAnswer & { variant?: \"ai\" | \"primary\" | \"danger\" };\n /** The other answer (\"Don't send\"). */\n decline: ChatDecisionAnswer;\n /** An optional quiet third answer after the buttons, such as \"Yes, and don't ask again in this chat\". */\n remember?: ChatDecisionAnswer;\n /** Turns `confirm` and `remember` off; this reason replaces `reason` in the same place. */\n unavailable?: ChatDecisionReason;\n /**\n * Turns every answer off until the reason clears, such as no connection, so no answer is lost on the way; this\n * reason replaces `reason` and `unavailable` in the same place.\n */\n waiting?: ChatDecisionReason;\n /** The result line shown in every state but `open`, usually `Chat.Outcome`. */\n outcome?: JSX.Element;\n /** Receives a rejected promise from an answer's `onSelect`. */\n onAnswerError?: (error: unknown) => void;\n anchorId?: string | number;\n class?: string;\n};\n\n/** What an outcome inside a question needs from it: whether it was just answered here, and a link press inside it. */\nconst DecisionScope = createContext<{ answered: () => boolean; pressed: () => void; released: () => void }>();\n\nconst reasonIcon = (reason: ChatDecisionReason) => reason.icon ?? \"ti ti-info-circle\";\n\n/**\n * A question the person answers in the conversation: one sentence, why it is asked, the thing itself, and two answers.\n * An answer collapses it at once to its outcome line, which takes the focus and is announced.\n */\nexport function ChatDecision(props: ChatDecisionProps): JSX.Element {\n const id = createUniqueId();\n const sentenceId = `k2b-chat-decision-${id}-sentence`;\n const reasonId = `k2b-chat-decision-${id}-reason`;\n let root: HTMLDivElement | undefined;\n let sentence: HTMLParagraphElement | undefined;\n // An answer given here moves the focus to the outcome; a click in the outcome that reopens the question moves it\n // back to the sentence. A state change from elsewhere moves no focus.\n const [answeredHere, setAnsweredHere] = createSignal(false);\n const [arrive, setArrive] = createSignal(false);\n let reopenedHere = false;\n // A press on the sentence focuses it too; only a focus moved there from elsewhere, such as a jump, lights it up.\n // The browser also focuses the sentence again when its window comes back; that is no arrival either.\n let sentencePressed = false;\n let sentenceHeldAcrossWindow = false;\n const windowLeft = () => {\n if (sentence && document.activeElement === sentence) sentenceHeldAcrossWindow = true;\n };\n onMount(() => {\n window.addEventListener(\"blur\", windowLeft);\n onCleanup(() => window.removeEventListener(\"blur\", windowLeft));\n });\n // A press counts only while its action runs: a reopen from elsewhere later must not take the focus.\n const scope = {\n answered: answeredHere,\n pressed: () => {\n reopenedHere = true;\n },\n released: () => {\n reopenedHere = false;\n },\n };\n\n // Read once: an object literal from the host builds its JSX again on every read.\n const reason = createMemo(() => props.waiting ?? props.unavailable ?? props.reason);\n const confirmOff = () => Boolean(props.waiting ?? props.unavailable);\n const open = () => props.state === \"open\";\n\n const answer = (choice: ChatDecisionAnswer, enabled: boolean) => {\n if (!enabled || !open() || answeredHere()) return;\n setAnsweredHere(true);\n try {\n const result = choice.onSelect();\n if (result instanceof Promise) result.catch((error: unknown) => props.onAnswerError?.(error));\n } catch (error) {\n props.onAnswerError?.(error);\n }\n // A host that keeps the question open (it could not take the answer) gets working buttons back.\n queueMicrotask(() => {\n if (open()) setAnsweredHere(false);\n });\n };\n\n createEffect(\n on(\n () => props.state,\n (state, previous) => {\n if (state !== \"open\" && previous === \"open\" && answeredHere()) {\n root?.querySelector<HTMLElement>(\".k2b-chat-outcome\")?.focus({ preventScroll: true });\n }\n if (state === \"open\" && previous !== undefined && previous !== \"open\") {\n // A host that could not take an answer given here returns the question from `sending`; the outcome that held\n // the focus is gone, so the focus goes back to the sentence instead of the page.\n const active = document.activeElement;\n const failedHere = answeredHere() && previous === \"sending\" && (!active || active === document.body);\n setAnsweredHere(false);\n if (reopenedHere || failedHere) {\n sentence?.focus({ preventScroll: true });\n setArrive(true);\n }\n // Handled. Until then the marker holds through states in between, such as `sending` while a reopen loads.\n reopenedHere = false;\n }\n },\n ),\n );\n\n return (\n <div\n ref={root}\n class={`k2b-chat-decision ${props.class ?? \"\"}`}\n data-state={props.state}\n data-chat-anchor={props.anchorId !== undefined ? String(props.anchorId) : undefined}\n >\n <Show when={open()} fallback={<DecisionScope.Provider value={scope}>{props.outcome}</DecisionScope.Provider>}>\n <section class=\"k2b-chat-decision__card\" aria-labelledby={sentenceId} aria-describedby={reasonId}>\n <p\n ref={sentence}\n id={sentenceId}\n class=\"k2b-chat-decision__sentence\"\n tabindex=\"-1\"\n data-arrive={arrive() ? \"true\" : undefined}\n onPointerDown={() => {\n sentencePressed = true;\n }}\n onPointerUp={() => {\n sentencePressed = false;\n }}\n onPointerCancel={() => {\n sentencePressed = false;\n }}\n onFocus={() => {\n if (!sentencePressed && !sentenceHeldAcrossWindow) setArrive(true);\n sentencePressed = false;\n sentenceHeldAcrossWindow = false;\n }}\n onAnimationEnd={() => setArrive(false)}\n >\n {props.sentence}\n </p>\n <p id={reasonId} class=\"k2b-chat-line k2b-chat-decision__reason\" data-tone={reason().tone ?? \"neutral\"}>\n <i class={`k2b-chat-line__icon ${reasonIcon(reason())}`} aria-hidden=\"true\" />\n <span class=\"k2b-chat-line__text\">{reason().text}</span>\n </p>\n {props.preview}\n <div class=\"k2b-chat-decision__answers\">\n <Button\n class=\"k2b-chat-decision__answer\"\n wrap\n variant={props.confirm.variant ?? \"ai\"}\n disabled={confirmOff()}\n onClick={() => answer(props.confirm, !confirmOff())}\n >\n <Show when={props.confirm.icon}>{(icon) => <i class={icon()} aria-hidden=\"true\" />}</Show>\n {props.confirm.label}\n </Button>\n <Button\n class=\"k2b-chat-decision__answer\"\n wrap\n variant=\"subtle\"\n disabled={Boolean(props.waiting)}\n onClick={() => answer(props.decline, !props.waiting)}\n >\n <Show when={props.decline.icon}>{(icon) => <i class={icon()} aria-hidden=\"true\" />}</Show>\n {props.decline.label}\n </Button>\n <Show when={props.remember}>\n {(remember) => (\n <Button\n class=\"k2b-chat-decision__remember\"\n wrap\n variant=\"text\"\n disabled={confirmOff()}\n onClick={() => answer(remember(), !confirmOff())}\n >\n {remember().label}\n </Button>\n )}\n </Show>\n </div>\n </section>\n </Show>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Outcome\n// ---------------------------------------------------------------------------\n\nexport type ChatOutcomeProps = {\n /**\n * Every line one flow can show, such as running and done. All of them share one cell, so the line is as high as\n * the longest and switching between them moves nothing.\n */\n lines: Readonly<Record<string, ChatOutcomeLine>>;\n /** The key of the visible line. */\n current: string;\n /** Receives an error thrown or rejected by a line's `action`. */\n onActionError?: (error: unknown) => void;\n anchorId?: string | number;\n class?: string;\n};\n\nconst OUTCOME_ICONS: Record<ChatOutcomeTone, string> = {\n running: \"ti ti-loader-2 k2b-spin\",\n done: \"ti ti-check\",\n off: \"ti ti-circle-off\",\n waiting: \"ti ti-clock\",\n warning: \"ti ti-alert-triangle\",\n error: \"ti ti-alert-circle\",\n};\n\n/** The visible words of a line, without what screen readers do not hear. */\nconst spokenText = (element: Element): string => {\n const copy = element.cloneNode(true) as Element;\n // Buttons under the line are controls, not part of what happened.\n for (const hidden of copy.querySelectorAll('[aria-hidden=\"true\"], .k2b-chat-outcome__actions')) hidden.remove();\n // The time and the link stand apart by margin only; spoken, they need a space.\n for (const part of copy.querySelectorAll(\".k2b-chat-outcome__time, .k2b-chat-outcome__link\")) part.before(\" \");\n return (copy.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n};\n\n/**\n * One line that says what happened: running, done, off, waiting, warning, or error. Changes are announced through a\n * polite live region; the line itself can take the focus.\n */\nexport function ChatOutcome(props: ChatOutcomeProps): JSX.Element {\n const scope = useContext(DecisionScope);\n // Only a line that replaces a question just answered here fades in; history renders final.\n const entering = untrack(() => scope?.answered() ?? false);\n const [live, setLive] = createSignal(\"\");\n let root: HTMLDivElement | undefined;\n let cell: HTMLDivElement | undefined;\n let pending = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n // Read once: an object literal from the host builds its JSX again on every read.\n const lines = createMemo(() => props.lines);\n const line = () => lines()[props.current];\n const tone = () => line()?.tone ?? \"off\";\n const keys = () => {\n const all = Object.keys(lines());\n return all.includes(props.current) ? all : [props.current, ...all];\n };\n\n // Empty the region first, so the same words spoken again are a change too.\n const announce = () => {\n setLive(\"\");\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n const current = cell?.querySelector('[data-current=\"true\"]');\n setLive(current ? spokenText(current) : \"\");\n }, 40);\n };\n\n onMount(() => {\n if (untrack(() => scope?.answered())) announce();\n });\n createEffect(\n on(\n () => props.current,\n () => {\n // A link pressed in a line that is now hidden would leave the focus on an invisible button, which the browser\n // then drops to the page. The line itself keeps it instead.\n const active = document.activeElement;\n const hidden = active && cell?.contains(active) && !active.closest('[data-current=\"true\"]');\n if (hidden || (pending && active === document.body)) root?.focus({ preventScroll: true });\n announce();\n },\n { defer: true },\n ),\n );\n onCleanup(() => {\n if (timer) clearTimeout(timer);\n });\n\n return (\n <div\n ref={root}\n class={`k2b-chat-outcome ${props.class ?? \"\"}`}\n data-tone={tone()}\n data-chat-anchor={props.anchorId !== undefined ? String(props.anchorId) : undefined}\n data-enter={entering ? \"true\" : undefined}\n tabindex=\"-1\"\n >\n <span class=\"k2b-chat-outcome__icon\" aria-hidden=\"true\">\n <i class={line()?.icon ?? OUTCOME_ICONS[tone()]} />\n </span>\n {/* Busy is set on the words only: a live region inside a busy subtree would be held back. */}\n <div ref={cell} class=\"k2b-chat-outcome__cell\" aria-busy={tone() === \"running\" ? \"true\" : undefined}>\n <For each={keys()}>\n {(key) => {\n const entry = () => lines()[key];\n const current = () => key === props.current;\n return (\n <Show when={entry()}>\n {(item) => (\n <div\n class=\"k2b-chat-outcome__text\"\n data-current={current() ? \"true\" : undefined}\n aria-hidden={current() ? undefined : \"true\"}\n >\n {item().text}\n <Show when={item().detail}>\n {(detail) => (\n <>\n {\" \"}\n <span class=\"k2b-chat-outcome__detail\">{detail()}</span>\n </>\n )}\n </Show>\n <Show when={item().time}>{(time) => <span class=\"k2b-chat-outcome__time\">{time()}</span>}</Show>\n <Show when={item().action}>\n {(action) => (\n <Button\n class=\"k2b-chat-outcome__link\"\n wrap\n variant=\"text\"\n tabIndex={current() ? undefined : -1}\n onClick={() => {\n // A second press while the first is still running would repeat its work.\n if (pending) return;\n scope?.pressed();\n try {\n const result = action().onSelect();\n if (result instanceof Promise) {\n pending = true;\n result\n .catch((error: unknown) => props.onActionError?.(error))\n .finally(() => {\n pending = false;\n scope?.released();\n });\n return;\n }\n } catch (error) {\n props.onActionError?.(error);\n }\n scope?.released();\n }}\n >\n {action().label}\n </Button>\n )}\n </Show>\n {/* Every line keeps its buttons, hidden with its text, so the cell also reserves their row. */}\n <Show when={item().actions}>{(actions) => <div class=\"k2b-chat-outcome__actions\">{actions()}</div>}</Show>\n </div>\n )}\n </Show>\n );\n }}\n </For>\n </div>\n <span class=\"k2b-sr-only\" role=\"status\" aria-live=\"polite\">\n {live()}\n </span>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Preview\n// ---------------------------------------------------------------------------\n\nexport type ChatPreviewProps = { children: JSX.Element; class?: string };\n\n/** The thing a question is about, as calm text on the sentence's edge: no surface, no column. */\nfunction ChatPreviewRoot(props: ChatPreviewProps): JSX.Element {\n return <div class={`k2b-chat-preview ${props.class ?? \"\"}`}>{props.children}</div>;\n}\n\nexport type ChatPreviewHeaderProps = {\n /** One row per field, such as From, To, Cc, and Bcc; the label stands right before its value. */\n rows: readonly ChatPreviewHeaderRow[];\n /** A last line, such as when the message goes out. */\n note?: JSX.Element;\n};\n\nconst PreviewPeople = (props: { people: readonly ChatPreviewPerson[] }) => (\n <For each={props.people}>\n {(person, index) => (\n <>\n {index() > 0 ? \", \" : \"\"}\n <span class=\"k2b-chat-preview__person\">\n <Show when={person.name}>{(name) => <b>{name()}</b>}</Show>\n <Show when={person.address}>\n {(address) => (\n <span class=\"k2b-chat-preview__address\" data-alone={person.name ? undefined : \"true\"}>\n {address()}\n </span>\n )}\n </Show>\n </span>\n </>\n )}\n </For>\n);\n\nfunction ChatPreviewHeader(props: ChatPreviewHeaderProps): JSX.Element {\n return (\n <div class=\"k2b-chat-preview__header\">\n <dl>\n <For each={props.rows}>\n {(row) => (\n <div data-emphasis={row.emphasis ? \"true\" : undefined}>\n <dt>{row.label}</dt>\n <dd>\n <Show when={row.people}>{(people) => <PreviewPeople people={people()} />}</Show>\n {row.value}\n </dd>\n </div>\n )}\n </For>\n </dl>\n <Show when={props.note}>{(note) => <p>{note()}</p>}</Show>\n </div>\n );\n}\n\nexport type ChatPreviewTitleProps = { children: JSX.Element; meta?: JSX.Element };\n\n/** The thing's name in the text's own face, with optional quiet facts after it. */\nfunction ChatPreviewTitle(props: ChatPreviewTitleProps): JSX.Element {\n return (\n <p class=\"k2b-chat-preview__title-line\">\n <span class=\"k2b-chat-preview__title\">{props.children}</span>\n <Show when={props.meta}>\n {(meta) => (\n <>\n {\" \"}\n <span class=\"k2b-chat-preview__meta\">{meta()}</span>\n </>\n )}\n </Show>\n </p>\n );\n}\n\n/** A rule or instruction as one sentence, such as what a routine does; chips and marks may stand inside. */\nfunction ChatPreviewSentence(props: { children: JSX.Element }): JSX.Element {\n return <p class=\"k2b-chat-preview__sentence\">{props.children}</p>;\n}\n\nexport type ChatPreviewExcerptProps = {\n children: JSX.Element;\n /** Shows the whole text in place instead of the first lines, such as after `Chat.Preview.ShowAll`. */\n expanded?: boolean;\n id?: string;\n};\n\n/** The first lines of a longer text. It keeps its line breaks, shows about three lines, and fades out. */\nfunction ChatPreviewExcerpt(props: ChatPreviewExcerptProps): JSX.Element {\n return (\n <p\n class=\"k2b-chat-preview__excerpt\"\n id={props.id}\n data-expanded={props.expanded ? \"true\" : undefined}\n tabindex={props.expanded ? -1 : undefined}\n >\n {props.children}\n </p>\n );\n}\n\nexport type ChatPreviewShowAllProps = {\n /** The id of the `Chat.Preview.Excerpt` this button opens. */\n controls: string;\n onShow: () => void;\n children: JSX.Element;\n};\n\n/**\n * Shows the rest of an excerpt in place, for a text with no page of its own to open. It looks like `ReadMore`; the\n * caller removes it once the text is shown, and the focus moves to the text, so reading goes on where it was.\n */\nfunction ChatPreviewShowAll(props: ChatPreviewShowAllProps): JSX.Element {\n const show = () => {\n props.onShow();\n queueMicrotask(() => document.getElementById(props.controls)?.focus({ preventScroll: true }));\n };\n return (\n <button type=\"button\" class=\"k2b-chat-preview__more\" aria-controls={props.controls} aria-expanded=\"false\" onClick={show}>\n {props.children}\n </button>\n );\n}\n\nexport type ChatPreviewReadMoreProps = {\n href: string;\n children: JSX.Element;\n /** Tooltip, such as \"Opens exactly this version of the draft\". */\n title?: string;\n};\n\n/** Opens the whole thing at exactly the version the question shows. */\nfunction ChatPreviewReadMore(props: ChatPreviewReadMoreProps): JSX.Element {\n return (\n <a class=\"k2b-chat-preview__more\" href={props.href} title={props.title}>\n {props.children}\n </a>\n );\n}\n\nconst InList = createContext(false);\n\n/** A list of lines with a name, such as \"What the routine may do\". */\nfunction ChatPreviewLines(props: { label?: string; children: JSX.Element }): JSX.Element {\n return (\n <ul class=\"k2b-chat-preview__lines\" aria-label={props.label}>\n <InList.Provider value={true}>{props.children}</InList.Provider>\n </ul>\n );\n}\n\nexport type ChatPreviewLineProps = {\n /** Icon class; the text after it starts 1.5rem from the edge. */\n icon: string;\n /** Icon colour, such as the PDF colour of an attachment. */\n accent?: string;\n /** `muted` for limits and notes, `warning` for an application's own warning. */\n tone?: \"default\" | \"muted\" | \"warning\";\n /** Marks the whole line as new or removed. */\n mark?: \"added\" | \"removed\";\n /** Quiet facts after the text, such as a file size. */\n meta?: JSX.Element;\n children: JSX.Element;\n};\n\n/** One line led by an icon: a consequence, a right, an attachment, or a warning. */\nfunction ChatPreviewLine(props: ChatPreviewLineProps): JSX.Element {\n const inList = useContext(InList);\n const messages = useChatMessages();\n const content = () => (\n <>\n <i class={`k2b-chat-line__icon ${props.icon}`} style={props.accent ? { color: props.accent } : undefined} aria-hidden=\"true\" />\n <span class=\"k2b-chat-line__text\">\n {/* One stable wrapper, so marking a line later never moves its mounted children. */}\n <span class=\"k2b-chat-mark\" data-change={props.mark}>\n <Show when={props.mark}>\n {(mark) => <span class=\"k2b-sr-only\">{mark() === \"added\" ? messages().markAdded : messages().markRemoved} </span>}\n </Show>\n {props.children}\n </span>\n <Show when={props.meta}>\n {(meta) => (\n <>\n {\" \"}\n <span class=\"k2b-chat-preview__meta\">{meta()}</span>\n </>\n )}\n </Show>\n </span>\n </>\n );\n const attributes = () => ({\n class: \"k2b-chat-line k2b-chat-preview__line\",\n \"data-tone\": props.tone && props.tone !== \"default\" ? props.tone : undefined,\n });\n return inList ? <li {...attributes()}>{content()}</li> : <p {...attributes()}>{content()}</p>;\n}\n\nexport type ChatPreviewPeopleProps = {\n /** The first few people; their avatars overlap at the start of the line. */\n people: readonly { name: string; src?: string | null }[];\n /** The names as text, such as \"Lea Berg, Tom Weber and 3 others\". */\n children: JSX.Element;\n};\n\nfunction ChatPreviewPeople(props: ChatPreviewPeopleProps): JSX.Element {\n return (\n <p class=\"k2b-chat-preview__people\">\n <span class=\"k2b-chat-preview__faces\" aria-hidden=\"true\">\n <For each={props.people}>{(person) => <Avatar name={person.name} src={person.src} size=\"xs\" />}</For>\n </span>\n <span>{props.children}</span>\n </p>\n );\n}\n\nexport const ChatPreview = Object.assign(ChatPreviewRoot, {\n Header: ChatPreviewHeader,\n Title: ChatPreviewTitle,\n Sentence: ChatPreviewSentence,\n Excerpt: ChatPreviewExcerpt,\n ReadMore: ChatPreviewReadMore,\n ShowAll: ChatPreviewShowAll,\n Lines: ChatPreviewLines,\n Line: ChatPreviewLine,\n People: ChatPreviewPeople,\n});\n\n// ---------------------------------------------------------------------------\n// Mark\n// ---------------------------------------------------------------------------\n\nexport type ChatMarkProps = { change: \"added\" | \"removed\"; children: JSX.Element };\n\n/** Marks new or removed words; screen readers hear \"new:\" or \"removed:\" first. */\nexport function ChatMark(props: ChatMarkProps): JSX.Element {\n const messages = useChatMessages();\n return (\n <Show\n when={props.change === \"removed\"}\n fallback={\n <mark class=\"k2b-chat-mark\" data-change=\"added\">\n <span class=\"k2b-sr-only\">{messages().markAdded} </span>\n {props.children}\n </mark>\n }\n >\n <del class=\"k2b-chat-mark\" data-change=\"removed\">\n <span class=\"k2b-sr-only\">{messages().markRemoved} </span>\n {props.children}\n </del>\n </Show>\n );\n}\n"],"mappings":"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","ignoreList":[]}
@@ -11,17 +11,19 @@ var _tmpl$ = ["<div class=\"", "\"", " role=\"grid\"", ">", "</div>"],
11
11
  _tmpl$4 = ["<div class=\"k2b-file-grid__meta\">", "</div>"],
12
12
  _tmpl$5 = ["<div class=\"k2b-file-grid__actions\">", "</div>"],
13
13
  _tmpl$6 = ["<div role=\"row\" class=\"k2b-file-grid__row\">", "</div>"],
14
- _tmpl$7 = ["<div class=\"", "\"", " data-open-items=\"true\" role=\"list\"", ">", "</div>"],
15
- _tmpl$8 = ["<button type=\"button\" class=\"k2b-file-grid__open\" aria-labelledby=\"", "\"", "></button>"],
16
- _tmpl$9 = ["<div class=\"k2b-file-grid__name\" id=\"", "\">", "</div>"],
17
- _tmpl$0 = ["<div class=\"k2b-file-grid__meta\" id=\"", "\">", "</div>"],
18
- _tmpl$1 = ["<div role=\"listitem\" class=\"k2b-file-grid__row\">", "</div>"],
19
- _tmpl$10 = ["<div class=\"k2b-file-grid-stack__toolbar\">", "</div>"],
20
- _tmpl$11 = ["<button type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"true\"><span>", "</span><i class=\"ti ti-chevron-up\" aria-hidden=\"true\"></i></button>"],
21
- _tmpl$12 = ["<div class=\"k2b-file-grid-stack\"", ">", "", "", "</div>"],
22
- _tmpl$13 = ["<div class=\"k2b-file-grid-stack__group\"><div class=\"k2b-file-grid-stack__label\"", ">", "<span class=\"k2b-file-grid-stack__count\">", "</span></div>", "</div>"],
23
- _tmpl$14 = ["<span class=\"k2b-file-grid-stack__more-meta\">", "</span>"],
24
- _tmpl$15 = ["<button type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"false\"><span>", "</span><i class=\"ti ti-chevron-down\" aria-hidden=\"true\"></i>", "</button>"];
14
+ _tmpl$7 = ["<span class=\"k2b-file-grid__check-box\"", "><i class=\"ti ti-check\"></i></span>"],
15
+ _tmpl$8 = ["<span class=\"k2b-file-grid__check\" aria-hidden=\"true\">", "</span>"],
16
+ _tmpl$9 = ["<div class=\"", "\"", " data-open-items=\"true\" role=\"list\"", ">", "</div>"],
17
+ _tmpl$0 = ["<button type=\"button\" class=\"k2b-file-grid__open\" aria-labelledby=\"", "\"", "></button>"],
18
+ _tmpl$1 = ["<div class=\"k2b-file-grid__name\" id=\"", "\">", "</div>"],
19
+ _tmpl$10 = ["<div class=\"k2b-file-grid__meta\" id=\"", "\">", "</div>"],
20
+ _tmpl$11 = ["<div role=\"listitem\" class=\"k2b-file-grid__row\">", "</div>"],
21
+ _tmpl$12 = ["<div class=\"k2b-file-grid-stack__toolbar\">", "</div>"],
22
+ _tmpl$13 = ["<button type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"true\"><span>", "</span><i class=\"ti ti-chevron-up\" aria-hidden=\"true\"></i></button>"],
23
+ _tmpl$14 = ["<div class=\"k2b-file-grid-stack\"", ">", "", "", "</div>"],
24
+ _tmpl$15 = ["<div class=\"k2b-file-grid-stack__group\"><div class=\"k2b-file-grid-stack__label\"", ">", "<span class=\"k2b-file-grid-stack__count\">", "</span></div>", "</div>"],
25
+ _tmpl$16 = ["<span class=\"k2b-file-grid-stack__more-meta\">", "</span>"],
26
+ _tmpl$17 = ["<button type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"false\"><span>", "</span><i class=\"ti ti-chevron-down\" aria-hidden=\"true\"></i>", "</button>"];
25
27
  import { createEffect, createMemo, createSignal, createUniqueId, For, Index, on, Show } from "solid-js";
26
28
  import { useFileListMessages } from "./file-list-messages";
27
29
 
@@ -61,7 +63,7 @@ function SelectableFileGrid(props) {
61
63
  };
62
64
  const columns = () => grid ? Math.max(1, getComputedStyle(grid).gridTemplateColumns.split(" ").length) : 1;
63
65
  const nested = event => event.target instanceof Element && !!event.target.closest("button,a,input,label,select,textarea,summary,[contenteditable],[role=button],[role=menu]");
64
- return _$ssr(_tmpl$, `k2b-file-grid ${_$escape(props.class ?? "", true)}`, _$ssrAttribute("data-size", _$escape(props.size ?? "md", true), false) + _$ssrAttribute("data-layout", _$escape(props.layout ?? "grid", true), false) + _$ssrAttribute("data-action-visibility", _$escape(props.actionVisibility ?? "always", true), false), _$ssrAttribute("aria-label", _$escape(props.label, true), false) + _$ssrAttribute("aria-multiselectable", _$escape(props.selection.multiple, true), false), _$escape(_$createComponent(For, {
66
+ return _$ssr(_tmpl$, `k2b-file-grid ${_$escape(props.class ?? "", true)}`, _$ssrAttribute("data-size", _$escape(props.size ?? "md", true), false) + _$ssrAttribute("data-layout", _$escape(props.layout ?? "grid", true), false) + _$ssrAttribute("data-action-visibility", _$escape(props.actionVisibility ?? "always", true), false) + _$ssrAttribute("data-checks", props.checks ? "true" : _$escape(undefined, true), false), _$ssrAttribute("aria-label", _$escape(props.label, true), false) + _$ssrAttribute("aria-multiselectable", _$escape(props.selection.multiple, true), false), _$escape(_$createComponent(For, {
65
67
  get each() {
66
68
  return props.rows;
67
69
  },
@@ -80,7 +82,19 @@ function SelectableFileGrid(props) {
80
82
  get tabIndex() {
81
83
  return props.selection.focused() === id() ? 0 : -1;
82
84
  }
83
- }), [_$ssr(_tmpl$2, _$escape(props.renderPreview(row))), _$ssr(_tmpl$3, _$escape(props.renderLabel(row))), _$ssr(_tmpl$4, _$escape(props.renderMeta?.(row))), _$ssr(_tmpl$5, _$escape(props.renderActions?.(row)))], false));
85
+ }), [_$escape(_$createComponent(Show, {
86
+ get when() {
87
+ return props.checks;
88
+ },
89
+ children: checks => _$ssr(_tmpl$8, _$escape(_$createComponent(Show, {
90
+ get when() {
91
+ return checks()(row);
92
+ },
93
+ get children() {
94
+ return _$ssr(_tmpl$7, _$ssrAttribute("data-shape", props.selection.multiple ? "box" : "circle", false));
95
+ }
96
+ })))
97
+ })), _$ssr(_tmpl$2, _$escape(props.renderPreview(row))), _$ssr(_tmpl$3, _$escape(props.renderLabel(row))), _$ssr(_tmpl$4, _$escape(props.renderMeta?.(row))), _$ssr(_tmpl$5, _$escape(props.renderActions?.(row)))], false));
84
98
  }
85
99
  })));
86
100
  }
@@ -112,23 +126,23 @@ function OpenFileGrid(props) {
112
126
  label: undefined,
113
127
  rows: full() || !limited() ? props.rows : props.rows.slice(0, props.limit)
114
128
  }]);
115
- const list = (group, labelId) => _$ssr(_tmpl$7, `k2b-file-grid ${_$escape(props.class ?? "", true)}`, _$ssrAttribute("data-size", _$escape(props.size ?? "md", true), false) + _$ssrAttribute("data-layout", group().layout === "tiles" ? "tiles" : _$escape(props.layout ?? "grid", true), false) + _$ssrAttribute("data-action-visibility", _$escape(props.actionVisibility ?? "always", true), false), _$ssrAttribute("aria-label", labelId ? _$escape(undefined, true) : _$escape(props.label, true), false) + _$ssrAttribute("aria-labelledby", _$escape(labelId, true), false), _$escape(_$createComponent(For, {
129
+ const list = (group, labelId) => _$ssr(_tmpl$9, `k2b-file-grid ${_$escape(props.class ?? "", true)}`, _$ssrAttribute("data-size", _$escape(props.size ?? "md", true), false) + _$ssrAttribute("data-layout", group().layout === "tiles" ? "tiles" : _$escape(props.layout ?? "grid", true), false) + _$ssrAttribute("data-action-visibility", _$escape(props.actionVisibility ?? "always", true), false), _$ssrAttribute("aria-label", labelId ? _$escape(undefined, true) : _$escape(props.label, true), false) + _$ssrAttribute("aria-labelledby", _$escape(labelId, true), false), _$escape(_$createComponent(For, {
116
130
  get each() {
117
131
  return group().rows;
118
132
  },
119
133
  children: row => {
120
134
  const id = `k2b-file-grid-${createUniqueId()}`;
121
- return _$ssr(_tmpl$1, _$ssrElement("div", _$mergeProps(() => props.itemProps?.(row), {
135
+ return _$ssr(_tmpl$11, _$ssrElement("div", _$mergeProps(() => props.itemProps?.(row), {
122
136
  "class": "k2b-file-grid__item"
123
- }), [_$ssr(_tmpl$8, `${_$escape(id, true)}-name`, _$ssrAttribute("aria-describedby", props.renderMeta ? `${_$escape(id, true)}-meta` : _$escape(undefined, true), false)), _$ssr(_tmpl$2, _$escape(props.renderPreview(row))), _$ssr(_tmpl$9, `${_$escape(id, true)}-name`, _$escape(props.renderLabel(row))), _$ssr(_tmpl$0, `${_$escape(id, true)}-meta`, _$escape(props.renderMeta?.(row))), _$ssr(_tmpl$5, _$escape(props.renderActions?.(row)))], false));
137
+ }), [_$ssr(_tmpl$0, `${_$escape(id, true)}-name`, _$ssrAttribute("aria-describedby", props.renderMeta ? `${_$escape(id, true)}-meta` : _$escape(undefined, true), false)), _$ssr(_tmpl$2, _$escape(props.renderPreview(row))), _$ssr(_tmpl$1, `${_$escape(id, true)}-name`, _$escape(props.renderLabel(row))), _$ssr(_tmpl$10, `${_$escape(id, true)}-meta`, _$escape(props.renderMeta?.(row))), _$ssr(_tmpl$5, _$escape(props.renderActions?.(row)))], false));
124
138
  }
125
139
  })));
126
- return _$ssr(_tmpl$12, _$ssrAttribute("data-expanded", full() ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
140
+ return _$ssr(_tmpl$14, _$ssrAttribute("data-expanded", full() ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
127
141
  get when() {
128
142
  return _$memo(() => !!full())() && props.toolbar;
129
143
  },
130
144
  get children() {
131
- return _$ssr(_tmpl$10, _$escape(props.toolbar));
145
+ return _$ssr(_tmpl$12, _$escape(props.toolbar));
132
146
  }
133
147
  })), _$escape(_$createComponent(Index, {
134
148
  get each() {
@@ -144,7 +158,7 @@ function OpenFileGrid(props) {
144
158
  return list(group);
145
159
  },
146
160
  get children() {
147
- return _$ssr(_tmpl$13, _$ssrAttribute("id", _$escape(labelId, true), false), _$escape(group().label), _$escape(group().rows.length), _$escape(list(group, labelId)));
161
+ return _$ssr(_tmpl$15, _$ssrAttribute("id", _$escape(labelId, true), false), _$escape(group().label), _$escape(group().rows.length), _$escape(list(group, labelId)));
148
162
  }
149
163
  });
150
164
  }
@@ -158,19 +172,19 @@ function OpenFileGrid(props) {
158
172
  return full();
159
173
  },
160
174
  get fallback() {
161
- return _$ssr(_tmpl$15, _$escape(messages().showAll({
175
+ return _$ssr(_tmpl$17, _$escape(messages().showAll({
162
176
  count: props.rows.length
163
177
  })), _$escape(_$createComponent(Show, {
164
178
  get when() {
165
179
  return props.moreMeta;
166
180
  },
167
181
  get children() {
168
- return _$ssr(_tmpl$14, _$escape(props.moreMeta));
182
+ return _$ssr(_tmpl$16, _$escape(props.moreMeta));
169
183
  }
170
184
  })));
171
185
  },
172
186
  get children() {
173
- return _$ssr(_tmpl$11, _$escape(messages().showLess));
187
+ return _$ssr(_tmpl$13, _$escape(messages().showLess));
174
188
  }
175
189
  });
176
190
  }
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["content/FileGrid.tsx"],"sourcesContent":["import { createEffect, createMemo, createSignal, createUniqueId, For, Index, type JSX, on, Show } from \"solid-js\";\nimport type { CollectionSelection } from \"./collection-selection\";\nimport { useFileListMessages } from \"./file-list-messages\";\n\n/** One part of an open file list, such as its files as rows and its images as tiles. */\nexport type FileGridGroup<T> = {\n id: string;\n label: JSX.Element;\n rows: readonly T[];\n /** `tiles` fills each item with its preview and lays the name over it. Defaults to `list`. */\n layout?: \"list\" | \"tiles\";\n};\n\ntype FileGridBaseProps<T> = {\n rows: readonly T[];\n getRowId: (row: T) => string;\n label: string;\n size?: \"sm\" | \"md\" | \"lg\";\n /** `list` shows one compact row per item: preview, name, and meta beside each other. Defaults to `grid`. */\n layout?: \"grid\" | \"list\";\n renderPreview: (row: T) => JSX.Element;\n renderLabel: (row: T) => JSX.Element;\n renderMeta?: (row: T) => JSX.Element;\n renderActions?: (row: T) => JSX.Element;\n /**\n * `progressive` shows the actions on hover, keyboard focus, or an open menu with a mouse or trackpad, and always\n * on touch. Their slot is reserved in both states, so nothing moves. Defaults to `always`.\n */\n actionVisibility?: \"always\" | \"progressive\";\n onOpen: (row: T) => void;\n onContextMenu?: (row: T) => void;\n /** Extra attributes for one tile, for example drag-and-drop handlers or data attributes the host styles. */\n itemProps?: (row: T) => JSX.HTMLAttributes<HTMLDivElement>;\n class?: string;\n};\n\n/** A browser or picker: items are selected, and Enter or a double click opens them. */\nexport type FileGridSelectableProps<T> = FileGridBaseProps<T> & {\n selection: CollectionSelection;\n onRowClick?: (row: T) => void;\n limit?: never;\n expanded?: never;\n onExpandedChange?: never;\n moreMeta?: never;\n toolbar?: never;\n groups?: never;\n};\n\n/** A list without selection, as in a details panel: one click on an item opens it. */\nexport type FileGridOpenProps<T> = FileGridBaseProps<T> & {\n selection?: never;\n onRowClick?: never;\n /** Shows the first `limit` items, then \"Show all N\", which opens the full list in place. Without it, the list is always full. */\n limit?: number;\n /** Controls whether the full list is open. Without it, the list keeps its own state. */\n expanded?: boolean;\n onExpandedChange?: (expanded: boolean) => void;\n /** A quiet note at the end of \"Show all N\", such as how many duplicates were merged. */\n moreMeta?: JSX.Element;\n /** Shown above the full list, such as a count, an arrangement switch, or \"Download all\"; not in the short list. */\n toolbar?: JSX.Element;\n /** How the full list arranges its items, such as files as rows, then images as tiles; the short list stays one list. */\n groups?: (rows: readonly T[]) => readonly FileGridGroup<T>[];\n};\n\nexport type FileGridProps<T> = FileGridSelectableProps<T> | FileGridOpenProps<T>;\n\n/**\n * File presentation only; the host owns data, menus, leases and permissions. Items the selection marks with\n * `isDisabled` stay reachable by keyboard but cannot be selected or opened; say why in `renderMeta`.\n */\nexport function FileGrid<T>(props: FileGridProps<T>) {\n return (\n <Show when={props.selection} fallback={<OpenFileGrid {...(props as FileGridOpenProps<T>)} />}>\n {(selection) => <SelectableFileGrid {...(props as FileGridSelectableProps<T>)} selection={selection()} />}\n </Show>\n );\n}\n\nfunction SelectableFileGrid<T>(props: FileGridSelectableProps<T>) {\n let grid: HTMLDivElement | undefined;\n // Both clicks of a double-click belong to the item that took the first one. When that click replaced the rows,\n // for example by opening a folder, the item now under the pointer ignores the rest of the gesture.\n let pressed: EventTarget | null | undefined;\n const ownsClick = (event: MouseEvent) => {\n if (event.detail > 1 && pressed !== event.currentTarget) pressed = null;\n else pressed = event.currentTarget;\n return pressed !== null;\n };\n const columns = () => (grid ? Math.max(1, getComputedStyle(grid).gridTemplateColumns.split(\" \").length) : 1);\n const nested = (event: Event) =>\n event.target instanceof Element &&\n !!event.target.closest(\"button,a,input,label,select,textarea,summary,[contenteditable],[role=button],[role=menu]\");\n return (\n <div\n ref={grid}\n class={`k2b-file-grid ${props.class ?? \"\"}`}\n data-size={props.size ?? \"md\"}\n data-layout={props.layout ?? \"grid\"}\n data-action-visibility={props.actionVisibility ?? \"always\"}\n role=\"grid\"\n aria-label={props.label}\n aria-multiselectable={props.selection.multiple}\n >\n <For each={props.rows}>\n {(row) => {\n const id = () => props.getRowId(row);\n const disabled = () => props.selection.isDisabled(id());\n return (\n <div role=\"row\" class=\"k2b-file-grid__row\">\n <div\n {...props.itemProps?.(row)}\n role=\"gridcell\"\n class=\"k2b-file-grid__item\"\n aria-selected={props.selection.selected().has(id())}\n aria-disabled={disabled() || undefined}\n ref={(element) => props.selection.register(id(), element)}\n tabIndex={props.selection.focused() === id() ? 0 : -1}\n onFocus={() => props.selection.markFocused(id())}\n onClick={(event) => {\n if (!nested(event) && ownsClick(event) && !disabled()) {\n props.selection.select(id(), event);\n props.onRowClick?.(row);\n }\n }}\n onDblClick={(event) => {\n if (!nested(event) && pressed !== null && !disabled()) props.onOpen(row);\n }}\n onContextMenu={() => props.onContextMenu?.(row)}\n onKeyDown={(event) => {\n if (nested(event)) return;\n if (props.selection.keyDown(event, id(), columns())) return;\n if (event.key === \"Enter\") {\n event.preventDefault();\n if (!disabled()) props.onOpen(row);\n }\n }}\n >\n <div class=\"k2b-file-grid__preview\">{props.renderPreview(row)}</div>\n <div class=\"k2b-file-grid__name\">{props.renderLabel(row)}</div>\n <div class=\"k2b-file-grid__meta\">{props.renderMeta?.(row)}</div>\n <div class=\"k2b-file-grid__actions\">{props.renderActions?.(row)}</div>\n </div>\n </div>\n );\n }}\n </For>\n </div>\n );\n}\n\nfunction OpenFileGrid<T>(props: FileGridOpenProps<T>) {\n const messages = useFileListMessages();\n const [ownExpanded, setOwnExpanded] = createSignal(false);\n const limited = () => props.limit !== undefined && props.rows.length > props.limit;\n // Without a limit the list is always complete, so its toolbar and groups always apply.\n const full = () => props.limit === undefined || (limited() && (props.expanded ?? ownExpanded()));\n let more: HTMLButtonElement | undefined;\n let less: HTMLButtonElement | undefined;\n let moveFocus = false;\n const toggle = (next: boolean) => {\n moveFocus = true;\n if (props.expanded === undefined) setOwnExpanded(next);\n props.onExpandedChange?.(next);\n };\n // The pressed button leaves with the state it showed; once the new state is there, focus moves to its\n // counterpart, also when a controlling host applies it later.\n createEffect(\n on(\n full,\n (open) => {\n if (!moveFocus) return;\n moveFocus = false;\n queueMicrotask(() => (open ? less : more)?.focus());\n },\n { defer: true },\n ),\n );\n const groups = createMemo((): readonly FileGridGroup<T>[] =>\n full() && props.groups\n ? props.groups(props.rows).filter((group) => group.rows.length > 0)\n : [{ id: \"rows\", label: undefined, rows: full() || !limited() ? props.rows : props.rows.slice(0, props.limit) }],\n );\n\n const list = (group: () => FileGridGroup<T>, labelId?: string) => (\n <div\n class={`k2b-file-grid ${props.class ?? \"\"}`}\n data-size={props.size ?? \"md\"}\n data-layout={group().layout === \"tiles\" ? \"tiles\" : (props.layout ?? \"grid\")}\n data-action-visibility={props.actionVisibility ?? \"always\"}\n data-open-items=\"true\"\n role=\"list\"\n aria-label={labelId ? undefined : props.label}\n aria-labelledby={labelId}\n >\n {/* Keyed by the row itself: new data keeps every existing item, its focus, and its state. */}\n <For each={group().rows}>\n {(row) => {\n const id = `k2b-file-grid-${createUniqueId()}`;\n return (\n <div role=\"listitem\" class=\"k2b-file-grid__row\">\n <div {...props.itemProps?.(row)} class=\"k2b-file-grid__item\">\n {/* The whole item opens the file; its actions lie above this layer in their own slot. */}\n <button\n type=\"button\"\n class=\"k2b-file-grid__open\"\n aria-labelledby={`${id}-name`}\n aria-describedby={props.renderMeta ? `${id}-meta` : undefined}\n onClick={() => props.onOpen(row)}\n onContextMenu={() => props.onContextMenu?.(row)}\n />\n <div class=\"k2b-file-grid__preview\">{props.renderPreview(row)}</div>\n <div class=\"k2b-file-grid__name\" id={`${id}-name`}>\n {props.renderLabel(row)}\n </div>\n <div class=\"k2b-file-grid__meta\" id={`${id}-meta`}>\n {props.renderMeta?.(row)}\n </div>\n <div class=\"k2b-file-grid__actions\">{props.renderActions?.(row)}</div>\n </div>\n </div>\n );\n }}\n </For>\n </div>\n );\n\n return (\n <div class=\"k2b-file-grid-stack\" data-expanded={full() ? \"true\" : undefined}>\n <Show when={full() && props.toolbar}>\n <div class=\"k2b-file-grid-stack__toolbar\">{props.toolbar}</div>\n </Show>\n {/* By position: a group that only changes its rows keeps its list, so new data remounts nothing. */}\n <Index each={groups()}>\n {(group) => {\n const labelId = `k2b-file-grid-group-${createUniqueId()}`;\n return (\n <Show when={group().label !== undefined} fallback={list(group)}>\n <div class=\"k2b-file-grid-stack__group\">\n <div class=\"k2b-file-grid-stack__label\" id={labelId}>\n {group().label}\n <span class=\"k2b-file-grid-stack__count\">{group().rows.length}</span>\n </div>\n {list(group, labelId)}\n </div>\n </Show>\n );\n }}\n </Index>\n <Show when={limited()}>\n <Show\n when={full()}\n fallback={\n <button ref={more} type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"false\" onClick={() => toggle(true)}>\n <span>{messages().showAll({ count: props.rows.length })}</span>\n <i class=\"ti ti-chevron-down\" aria-hidden=\"true\" />\n <Show when={props.moreMeta}>\n <span class=\"k2b-file-grid-stack__more-meta\">{props.moreMeta}</span>\n </Show>\n </button>\n }\n >\n <button ref={less} type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"true\" onClick={() => toggle(false)}>\n <span>{messages().showLess}</span>\n <i class=\"ti ti-chevron-up\" aria-hidden=\"true\" />\n </button>\n </Show>\n </Show>\n </div>\n );\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["content/FileGrid.tsx"],"sourcesContent":["import { createEffect, createMemo, createSignal, createUniqueId, For, Index, type JSX, on, Show } from \"solid-js\";\nimport type { CollectionSelection } from \"./collection-selection\";\nimport { useFileListMessages } from \"./file-list-messages\";\n\n/** One part of an open file list, such as its files as rows and its images as tiles. */\nexport type FileGridGroup<T> = {\n id: string;\n label: JSX.Element;\n rows: readonly T[];\n /** `tiles` fills each item with its preview and lays the name over it. Defaults to `list`. */\n layout?: \"list\" | \"tiles\";\n};\n\ntype FileGridBaseProps<T> = {\n rows: readonly T[];\n getRowId: (row: T) => string;\n label: string;\n size?: \"sm\" | \"md\" | \"lg\";\n /** `list` shows one compact row per item: preview, name, and meta beside each other. Defaults to `grid`. */\n layout?: \"grid\" | \"list\";\n renderPreview: (row: T) => JSX.Element;\n renderLabel: (row: T) => JSX.Element;\n renderMeta?: (row: T) => JSX.Element;\n renderActions?: (row: T) => JSX.Element;\n /**\n * `progressive` shows the actions on hover, keyboard focus, or an open menu with a mouse or trackpad, and always\n * on touch. Their slot is reserved in both states, so nothing moves. Defaults to `always`.\n */\n actionVisibility?: \"always\" | \"progressive\";\n onOpen: (row: T) => void;\n onContextMenu?: (row: T) => void;\n /** Extra attributes for one tile, for example drag-and-drop handlers or data attributes the host styles. */\n itemProps?: (row: T) => JSX.HTMLAttributes<HTMLDivElement>;\n class?: string;\n};\n\n/** A browser or picker: items are selected, and Enter or a double click opens them. */\nexport type FileGridSelectableProps<T> = FileGridBaseProps<T> & {\n selection: CollectionSelection;\n onRowClick?: (row: T) => void;\n /**\n * Shows a check box before each item that `checks` returns `true` for, or a circle when only one item may be\n * selected. The check only mirrors the selection; items without one keep its slot, so names stay in line.\n */\n checks?: (row: T) => boolean;\n limit?: never;\n expanded?: never;\n onExpandedChange?: never;\n moreMeta?: never;\n toolbar?: never;\n groups?: never;\n};\n\n/** A list without selection, as in a details panel: one click on an item opens it. */\nexport type FileGridOpenProps<T> = FileGridBaseProps<T> & {\n selection?: never;\n onRowClick?: never;\n checks?: never;\n /** Shows the first `limit` items, then \"Show all N\", which opens the full list in place. Without it, the list is always full. */\n limit?: number;\n /** Controls whether the full list is open. Without it, the list keeps its own state. */\n expanded?: boolean;\n onExpandedChange?: (expanded: boolean) => void;\n /** A quiet note at the end of \"Show all N\", such as how many duplicates were merged. */\n moreMeta?: JSX.Element;\n /** Shown above the full list, such as a count, an arrangement switch, or \"Download all\"; not in the short list. */\n toolbar?: JSX.Element;\n /** How the full list arranges its items, such as files as rows, then images as tiles; the short list stays one list. */\n groups?: (rows: readonly T[]) => readonly FileGridGroup<T>[];\n};\n\nexport type FileGridProps<T> = FileGridSelectableProps<T> | FileGridOpenProps<T>;\n\n/**\n * File presentation only; the host owns data, menus, leases and permissions. Items the selection marks with\n * `isDisabled` stay reachable by keyboard but cannot be selected or opened; say why in `renderMeta`.\n */\nexport function FileGrid<T>(props: FileGridProps<T>) {\n return (\n <Show when={props.selection} fallback={<OpenFileGrid {...(props as FileGridOpenProps<T>)} />}>\n {(selection) => <SelectableFileGrid {...(props as FileGridSelectableProps<T>)} selection={selection()} />}\n </Show>\n );\n}\n\nfunction SelectableFileGrid<T>(props: FileGridSelectableProps<T>) {\n let grid: HTMLDivElement | undefined;\n // Both clicks of a double-click belong to the item that took the first one. When that click replaced the rows,\n // for example by opening a folder, the item now under the pointer ignores the rest of the gesture.\n let pressed: EventTarget | null | undefined;\n const ownsClick = (event: MouseEvent) => {\n if (event.detail > 1 && pressed !== event.currentTarget) pressed = null;\n else pressed = event.currentTarget;\n return pressed !== null;\n };\n const columns = () => (grid ? Math.max(1, getComputedStyle(grid).gridTemplateColumns.split(\" \").length) : 1);\n const nested = (event: Event) =>\n event.target instanceof Element &&\n !!event.target.closest(\"button,a,input,label,select,textarea,summary,[contenteditable],[role=button],[role=menu]\");\n return (\n <div\n ref={grid}\n class={`k2b-file-grid ${props.class ?? \"\"}`}\n data-size={props.size ?? \"md\"}\n data-layout={props.layout ?? \"grid\"}\n data-action-visibility={props.actionVisibility ?? \"always\"}\n data-checks={props.checks ? \"true\" : undefined}\n role=\"grid\"\n aria-label={props.label}\n aria-multiselectable={props.selection.multiple}\n >\n <For each={props.rows}>\n {(row) => {\n const id = () => props.getRowId(row);\n const disabled = () => props.selection.isDisabled(id());\n return (\n <div role=\"row\" class=\"k2b-file-grid__row\">\n <div\n {...props.itemProps?.(row)}\n role=\"gridcell\"\n class=\"k2b-file-grid__item\"\n aria-selected={props.selection.selected().has(id())}\n aria-disabled={disabled() || undefined}\n ref={(element) => props.selection.register(id(), element)}\n tabIndex={props.selection.focused() === id() ? 0 : -1}\n onFocus={() => props.selection.markFocused(id())}\n onClick={(event) => {\n if (!nested(event) && ownsClick(event) && !disabled()) {\n props.selection.select(id(), event);\n props.onRowClick?.(row);\n }\n }}\n onDblClick={(event) => {\n if (!nested(event) && pressed !== null && !disabled()) props.onOpen(row);\n }}\n onContextMenu={() => props.onContextMenu?.(row)}\n onKeyDown={(event) => {\n if (nested(event)) return;\n if (props.selection.keyDown(event, id(), columns())) return;\n if (event.key === \"Enter\") {\n event.preventDefault();\n if (!disabled()) props.onOpen(row);\n }\n }}\n >\n <Show when={props.checks}>\n {(checks) => (\n <span class=\"k2b-file-grid__check\" aria-hidden=\"true\">\n <Show when={checks()(row)}>\n <span class=\"k2b-file-grid__check-box\" data-shape={props.selection.multiple ? \"box\" : \"circle\"}>\n <i class=\"ti ti-check\" />\n </span>\n </Show>\n </span>\n )}\n </Show>\n <div class=\"k2b-file-grid__preview\">{props.renderPreview(row)}</div>\n <div class=\"k2b-file-grid__name\">{props.renderLabel(row)}</div>\n <div class=\"k2b-file-grid__meta\">{props.renderMeta?.(row)}</div>\n <div class=\"k2b-file-grid__actions\">{props.renderActions?.(row)}</div>\n </div>\n </div>\n );\n }}\n </For>\n </div>\n );\n}\n\nfunction OpenFileGrid<T>(props: FileGridOpenProps<T>) {\n const messages = useFileListMessages();\n const [ownExpanded, setOwnExpanded] = createSignal(false);\n const limited = () => props.limit !== undefined && props.rows.length > props.limit;\n // Without a limit the list is always complete, so its toolbar and groups always apply.\n const full = () => props.limit === undefined || (limited() && (props.expanded ?? ownExpanded()));\n let more: HTMLButtonElement | undefined;\n let less: HTMLButtonElement | undefined;\n let moveFocus = false;\n const toggle = (next: boolean) => {\n moveFocus = true;\n if (props.expanded === undefined) setOwnExpanded(next);\n props.onExpandedChange?.(next);\n };\n // The pressed button leaves with the state it showed; once the new state is there, focus moves to its\n // counterpart, also when a controlling host applies it later.\n createEffect(\n on(\n full,\n (open) => {\n if (!moveFocus) return;\n moveFocus = false;\n queueMicrotask(() => (open ? less : more)?.focus());\n },\n { defer: true },\n ),\n );\n const groups = createMemo((): readonly FileGridGroup<T>[] =>\n full() && props.groups\n ? props.groups(props.rows).filter((group) => group.rows.length > 0)\n : [{ id: \"rows\", label: undefined, rows: full() || !limited() ? props.rows : props.rows.slice(0, props.limit) }],\n );\n\n const list = (group: () => FileGridGroup<T>, labelId?: string) => (\n <div\n class={`k2b-file-grid ${props.class ?? \"\"}`}\n data-size={props.size ?? \"md\"}\n data-layout={group().layout === \"tiles\" ? \"tiles\" : (props.layout ?? \"grid\")}\n data-action-visibility={props.actionVisibility ?? \"always\"}\n data-open-items=\"true\"\n role=\"list\"\n aria-label={labelId ? undefined : props.label}\n aria-labelledby={labelId}\n >\n {/* Keyed by the row itself: new data keeps every existing item, its focus, and its state. */}\n <For each={group().rows}>\n {(row) => {\n const id = `k2b-file-grid-${createUniqueId()}`;\n return (\n <div role=\"listitem\" class=\"k2b-file-grid__row\">\n <div {...props.itemProps?.(row)} class=\"k2b-file-grid__item\">\n {/* The whole item opens the file; its actions lie above this layer in their own slot. */}\n <button\n type=\"button\"\n class=\"k2b-file-grid__open\"\n aria-labelledby={`${id}-name`}\n aria-describedby={props.renderMeta ? `${id}-meta` : undefined}\n onClick={() => props.onOpen(row)}\n onContextMenu={() => props.onContextMenu?.(row)}\n />\n <div class=\"k2b-file-grid__preview\">{props.renderPreview(row)}</div>\n <div class=\"k2b-file-grid__name\" id={`${id}-name`}>\n {props.renderLabel(row)}\n </div>\n <div class=\"k2b-file-grid__meta\" id={`${id}-meta`}>\n {props.renderMeta?.(row)}\n </div>\n <div class=\"k2b-file-grid__actions\">{props.renderActions?.(row)}</div>\n </div>\n </div>\n );\n }}\n </For>\n </div>\n );\n\n return (\n <div class=\"k2b-file-grid-stack\" data-expanded={full() ? \"true\" : undefined}>\n <Show when={full() && props.toolbar}>\n <div class=\"k2b-file-grid-stack__toolbar\">{props.toolbar}</div>\n </Show>\n {/* By position: a group that only changes its rows keeps its list, so new data remounts nothing. */}\n <Index each={groups()}>\n {(group) => {\n const labelId = `k2b-file-grid-group-${createUniqueId()}`;\n return (\n <Show when={group().label !== undefined} fallback={list(group)}>\n <div class=\"k2b-file-grid-stack__group\">\n <div class=\"k2b-file-grid-stack__label\" id={labelId}>\n {group().label}\n <span class=\"k2b-file-grid-stack__count\">{group().rows.length}</span>\n </div>\n {list(group, labelId)}\n </div>\n </Show>\n );\n }}\n </Index>\n <Show when={limited()}>\n <Show\n when={full()}\n fallback={\n <button ref={more} type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"false\" onClick={() => toggle(true)}>\n <span>{messages().showAll({ count: props.rows.length })}</span>\n <i class=\"ti ti-chevron-down\" aria-hidden=\"true\" />\n <Show when={props.moreMeta}>\n <span class=\"k2b-file-grid-stack__more-meta\">{props.moreMeta}</span>\n </Show>\n </button>\n }\n >\n <button ref={less} type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"true\" onClick={() => toggle(false)}>\n <span>{messages().showLess}</span>\n <i class=\"ti ti-chevron-up\" aria-hidden=\"true\" />\n </button>\n </Show>\n </Show>\n </div>\n );\n}\n"],"mappings":"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","ignoreList":[]}
@@ -121,6 +121,7 @@ export function VirtualFeed(props) {
121
121
  let focusIndex = -1;
122
122
  let fresh = [];
123
123
  let restoring = false;
124
+ let restoreVersion = 0;
124
125
  let restoreAgain = false;
125
126
  let pendingAnnouncement = [];
126
127
  let announceTimer;
@@ -199,30 +200,30 @@ export function VirtualFeed(props) {
199
200
  * not: rows moved down would show empty space above the first item, and rows moved up would put the first items
200
201
  * out of the scroll range's reach. The margin is the overscan, so the write lands before the engine draws there.
201
202
  */
202
- const mayDefer = top => deferring() && Math.min(viewport.scrollTop, top) > OVERSCAN;
203
+ const mayDefer = (top, physicalTop) => deferring() && Math.min(physicalTop ?? viewport.scrollTop, top) > OVERSCAN;
203
204
  /** Scrolls to `target`, within what the layout can reach, and returns the position the reader gets. */
204
- const writeTop = (target, immediate) => {
205
- const next = Math.min(Math.max(0, total() - viewport.clientHeight), Math.max(0, target));
206
- if (!immediate && mayDefer(next)) {
205
+ const writeTop = (target, immediate, height) => {
206
+ const next = Math.min(Math.max(0, total() - height), Math.max(0, target));
207
+ const from = viewport.scrollTop;
208
+ if (!immediate && mayDefer(next, from)) {
207
209
  // The rows move instead; the scroll range stays the same, so the engine clamps nothing.
208
- lastTop = viewport.scrollTop;
210
+ lastTop = from;
209
211
  deferred = next - lastTop;
210
212
  feed.style.height = `${total() - deferred}px`;
211
213
  return next;
212
214
  }
213
215
  deferred = 0;
214
216
  feed.style.height = `${total()}px`;
215
- const from = viewport.scrollTop;
216
217
  if (Math.abs(from - next) > 0.5) viewport.scrollTop = next;
217
218
  lastTop = viewport.scrollTop;
218
219
  if (Math.abs(lastTop - from) > 0.5) owedUntil = performance.now() + END_WAIT_MS;
219
220
  return next;
220
221
  };
221
222
  let visibleKeys;
222
- const reportVisible = () => {
223
+ const reportVisible = (top, height) => {
223
224
  if (!props.onVisibleChange) return;
224
- const top = logicalTop();
225
- const bottom = top + viewport.clientHeight;
225
+ top ??= logicalTop();
226
+ const bottom = top + (height ?? viewport.clientHeight);
226
227
  let first = -1;
227
228
  let last = -1;
228
229
  if (bottom > top && hi > lo) for (let index = indexAt(top); index < hi && offsetOf(index) < bottom; index++) {
@@ -245,9 +246,7 @@ export function VirtualFeed(props) {
245
246
  last: list[last]
246
247
  });
247
248
  };
248
- const updateRange = () => {
249
- const top = logicalTop();
250
- const height = viewport.clientHeight;
249
+ const updateRange = (top = logicalTop(), height = viewport.clientHeight) => {
251
250
  const indices = [];
252
251
  if (hi > lo) {
253
252
  const start = indexAt(top - OVERSCAN);
@@ -261,7 +260,7 @@ export function VirtualFeed(props) {
261
260
  renderRows(indices);
262
261
  setTabStop(focusIndex >= lo && focusIndex < hi ? focusIndex : hi > lo ? indexAt(top + height - 1) : -1);
263
262
  // Newly mounted rows still have estimates; report only after their heights are measured.
264
- if (!restoring && !fresh.some(node => node.isConnected)) reportVisible();
263
+ if (!restoring && !fresh.some(node => node.isConnected)) reportVisible(top, height);
265
264
  };
266
265
  const measure = measured => {
267
266
  let changed = false;
@@ -369,6 +368,8 @@ export function VirtualFeed(props) {
369
368
  return;
370
369
  }
371
370
  restoring = true;
371
+ const restored = ++restoreVersion;
372
+ const height = viewport.clientHeight;
372
373
  let target = 0;
373
374
  let top = 0;
374
375
  // Following the end shows the end of all items from now on, so later moves are judged against it.
@@ -378,9 +379,10 @@ export function VirtualFeed(props) {
378
379
  restoreAgain = false;
379
380
  recompute();
380
381
  target = stick ? total() : offsetOf(anchor.index) - anchor.delta;
381
- top = writeTop(target, immediate);
382
- updateRange();
383
- setVersion(value => value + 1);
382
+ top = Math.min(Math.max(0, total() - height), Math.max(0, target));
383
+ // Row heights do not depend on their translation or the feed's scroll position. Mount and measure the
384
+ // logical range first; writing the final geometry once avoids a layout flush on every correction pass.
385
+ updateRange(top, height);
384
386
  if (pass === MEASURE_PASSES) break;
385
387
  // Rows mounted here reach the ResizeObserver only next frame (its loop limit), so measure them now.
386
388
  const mounted = fresh.filter(node => node.isConnected);
@@ -388,6 +390,8 @@ export function VirtualFeed(props) {
388
390
  if (measure(mounted.map(node => [node, node.getBoundingClientRect().height]))) restoreAgain = true;
389
391
  if (!restoreAgain) break;
390
392
  }
393
+ top = writeTop(target, immediate, height);
394
+ setVersion(value => value + 1);
391
395
  // A position the layout cannot reach, above its start or past its end, becomes the one the reader sees, so a
392
396
  // page that loads there later keeps it instead of moving to it.
393
397
  if (!stick && Math.abs(top - target) > 0.5) anchor = {
@@ -397,8 +401,9 @@ export function VirtualFeed(props) {
397
401
  } finally {
398
402
  restoring = false;
399
403
  }
400
- checkEdges();
401
- reportVisible();
404
+ checkEdges(lastTop + deferred, height);
405
+ // A synchronous edge load can restore another window; that restore already reports its final geometry.
406
+ if (restored === restoreVersion) reportVisible(lastTop + deferred, height);
402
407
  };
403
408
 
404
409
  /**
@@ -570,10 +575,10 @@ export function VirtualFeed(props) {
570
575
  };
571
576
 
572
577
  /** Grows the window or asks for more items when the reader nears an edge. */
573
- const checkEdges = () => {
578
+ const checkEdges = (top, height) => {
574
579
  if (hi <= lo || restoring) return;
575
- const top = logicalTop();
576
- const height = viewport.clientHeight;
580
+ top ??= logicalTop();
581
+ height ??= viewport.clientHeight;
577
582
  // Rows measured taller than estimated can grow the window past the cap without any edge moving.
578
583
  if (total() > MAX_WINDOW && trimWindow(top + height / 2 > total() / 2 ? "start" : "end")) {
579
584
  restore(true);