@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.
- package/dist/browser/actions/Dropdown.js +8 -1
- package/dist/browser/actions/Dropdown.js.map +1 -1
- package/dist/browser/actions/FilterChip.js +11 -5
- package/dist/browser/actions/FilterChip.js.map +1 -1
- package/dist/browser/actions/ViewTabs.js +225 -93
- package/dist/browser/actions/ViewTabs.js.map +1 -1
- package/dist/browser/chat/ChatDecision.js +10 -6
- package/dist/browser/chat/ChatDecision.js.map +1 -1
- package/dist/browser/content/FileGrid.js +125 -99
- package/dist/browser/content/FileGrid.js.map +1 -1
- package/dist/browser/content/VirtualFeed.js +26 -21
- package/dist/browser/content/VirtualFeed.js.map +1 -1
- package/dist/browser/inputs/Combobox.js +14 -6
- package/dist/browser/inputs/Combobox.js.map +1 -1
- package/dist/browser/inputs/choice.js +36 -9
- package/dist/browser/inputs/choice.js.map +1 -1
- package/dist/browser/layout/AnchoredPopover.js +2 -1
- package/dist/browser/layout/AnchoredPopover.js.map +1 -1
- package/dist/browser/layout/AppWorkspace.js +348 -444
- package/dist/browser/layout/AppWorkspace.js.map +1 -1
- package/dist/browser/layout/AppWorkspaceSidebarItemStatus.js +113 -0
- package/dist/browser/layout/AppWorkspaceSidebarItemStatus.js.map +1 -0
- package/dist/browser/layout/DetailPanel.js +130 -108
- package/dist/browser/layout/DetailPanel.js.map +1 -1
- package/dist/browser/layout/DetailSections.js +1123 -0
- package/dist/browser/layout/DetailSections.js.map +1 -0
- package/dist/browser/layout/DetailsToggle.js +32 -0
- package/dist/browser/layout/DetailsToggle.js.map +1 -0
- package/dist/browser/layout/Discussion.js +3 -0
- package/dist/browser/layout/Discussion.js.map +1 -1
- package/dist/browser/layout/MobileShell.js +141 -102
- package/dist/browser/layout/MobileShell.js.map +1 -1
- package/dist/browser/layout/PageTitle.js +33 -10
- package/dist/browser/layout/PageTitle.js.map +1 -1
- package/dist/browser/layout/app-workspace-controller.js +12 -0
- package/dist/browser/layout/app-workspace-controller.js.map +1 -1
- package/dist/browser/layout/detail-sections-messages.js +127 -0
- package/dist/browser/layout/detail-sections-messages.js.map +1 -0
- package/dist/browser/layout/index.js +3 -0
- package/dist/browser/layout/index.js.map +1 -1
- package/dist/browser/surfaces/Avatar.js +3 -1
- package/dist/browser/surfaces/Avatar.js.map +1 -1
- package/dist/browser/surfaces/AvatarStack.js +95 -61
- package/dist/browser/surfaces/AvatarStack.js.map +1 -1
- package/dist/browser/surfaces/ChangeList.js +77 -0
- package/dist/browser/surfaces/ChangeList.js.map +1 -0
- package/dist/browser/surfaces/index.js +2 -1
- package/dist/browser/surfaces/index.js.map +1 -1
- package/dist/browser/text-editor/TextEditor.js +19 -8
- package/dist/browser/text-editor/TextEditor.js.map +1 -1
- package/dist/ssr/actions/Dropdown.js +6 -0
- package/dist/ssr/actions/Dropdown.js.map +1 -1
- package/dist/ssr/actions/FilterChip.js +1 -1
- package/dist/ssr/actions/FilterChip.js.map +1 -1
- package/dist/ssr/actions/ViewTabs.js +144 -37
- package/dist/ssr/actions/ViewTabs.js.map +1 -1
- package/dist/ssr/chat/ChatDecision.js +10 -6
- package/dist/ssr/chat/ChatDecision.js.map +1 -1
- package/dist/ssr/content/FileGrid.js +36 -22
- package/dist/ssr/content/FileGrid.js.map +1 -1
- package/dist/ssr/content/VirtualFeed.js +26 -21
- package/dist/ssr/content/VirtualFeed.js.map +1 -1
- package/dist/ssr/inputs/Combobox.js +6 -3
- package/dist/ssr/inputs/Combobox.js.map +1 -1
- package/dist/ssr/inputs/choice.js +36 -9
- package/dist/ssr/inputs/choice.js.map +1 -1
- package/dist/ssr/layout/AnchoredPopover.js +2 -1
- package/dist/ssr/layout/AnchoredPopover.js.map +1 -1
- package/dist/ssr/layout/AppWorkspace.js +66 -132
- package/dist/ssr/layout/AppWorkspace.js.map +1 -1
- package/dist/ssr/layout/AppWorkspaceSidebarItemStatus.js +76 -0
- package/dist/ssr/layout/AppWorkspaceSidebarItemStatus.js.map +1 -0
- package/dist/ssr/layout/DetailPanel.js +18 -1
- package/dist/ssr/layout/DetailPanel.js.map +1 -1
- package/dist/ssr/layout/DetailSections.js +838 -0
- package/dist/ssr/layout/DetailSections.js.map +1 -0
- package/dist/ssr/layout/DetailsToggle.js +29 -0
- package/dist/ssr/layout/DetailsToggle.js.map +1 -0
- package/dist/ssr/layout/Discussion.js +3 -0
- package/dist/ssr/layout/Discussion.js.map +1 -1
- package/dist/ssr/layout/MobileShell.js +60 -29
- package/dist/ssr/layout/MobileShell.js.map +1 -1
- package/dist/ssr/layout/PageTitle.js +29 -6
- package/dist/ssr/layout/PageTitle.js.map +1 -1
- package/dist/ssr/layout/app-workspace-controller.js +12 -0
- package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
- package/dist/ssr/layout/detail-sections-messages.js +127 -0
- package/dist/ssr/layout/detail-sections-messages.js.map +1 -0
- package/dist/ssr/layout/index.js +3 -0
- package/dist/ssr/layout/index.js.map +1 -1
- package/dist/ssr/surfaces/Avatar.js +3 -1
- package/dist/ssr/surfaces/Avatar.js.map +1 -1
- package/dist/ssr/surfaces/AvatarStack.js +61 -32
- package/dist/ssr/surfaces/AvatarStack.js.map +1 -1
- package/dist/ssr/surfaces/ChangeList.js +37 -0
- package/dist/ssr/surfaces/ChangeList.js.map +1 -0
- package/dist/ssr/surfaces/index.js +2 -1
- package/dist/ssr/surfaces/index.js.map +1 -1
- package/dist/ssr/text-editor/TextEditor.js +19 -8
- package/dist/ssr/text-editor/TextEditor.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/actions/FilterChip.d.ts +4 -0
- package/dist/types/actions/ViewTabs.d.ts +11 -1
- package/dist/types/chat/ChatDecision.d.ts +5 -0
- package/dist/types/content/FileGrid.d.ts +6 -0
- package/dist/types/inputs/Combobox.d.ts +2 -0
- package/dist/types/inputs/choice.d.ts +9 -3
- package/dist/types/layout/AppWorkspace.d.ts +2 -28
- package/dist/types/layout/AppWorkspaceSidebarItemStatus.d.ts +30 -0
- package/dist/types/layout/DetailPanel.d.ts +9 -0
- package/dist/types/layout/DetailSections.d.ts +186 -0
- package/dist/types/layout/DetailsToggle.d.ts +13 -0
- package/dist/types/layout/MobileShell.d.ts +12 -0
- package/dist/types/layout/PageTitle.d.ts +7 -0
- package/dist/types/layout/detail-sections-messages.d.ts +61 -0
- package/dist/types/layout/index.d.ts +5 -1
- package/dist/types/surfaces/Avatar.d.ts +3 -0
- package/dist/types/surfaces/AvatarStack.d.ts +20 -1
- package/dist/types/surfaces/ChangeList.d.ts +20 -0
- package/dist/types/surfaces/index.d.ts +5 -3
- package/package.json +3 -3
|
@@ -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":[]}
|
|
@@ -128,6 +128,7 @@ export function VirtualFeed(props) {
|
|
|
128
128
|
let focusIndex = -1;
|
|
129
129
|
let fresh = [];
|
|
130
130
|
let restoring = false;
|
|
131
|
+
let restoreVersion = 0;
|
|
131
132
|
let restoreAgain = false;
|
|
132
133
|
let pendingAnnouncement = [];
|
|
133
134
|
let announceTimer;
|
|
@@ -206,30 +207,30 @@ export function VirtualFeed(props) {
|
|
|
206
207
|
* not: rows moved down would show empty space above the first item, and rows moved up would put the first items
|
|
207
208
|
* out of the scroll range's reach. The margin is the overscan, so the write lands before the engine draws there.
|
|
208
209
|
*/
|
|
209
|
-
const mayDefer = top => deferring() && Math.min(viewport.scrollTop, top) > OVERSCAN;
|
|
210
|
+
const mayDefer = (top, physicalTop) => deferring() && Math.min(physicalTop ?? viewport.scrollTop, top) > OVERSCAN;
|
|
210
211
|
/** Scrolls to `target`, within what the layout can reach, and returns the position the reader gets. */
|
|
211
|
-
const writeTop = (target, immediate) => {
|
|
212
|
-
const next = Math.min(Math.max(0, total() -
|
|
213
|
-
|
|
212
|
+
const writeTop = (target, immediate, height) => {
|
|
213
|
+
const next = Math.min(Math.max(0, total() - height), Math.max(0, target));
|
|
214
|
+
const from = viewport.scrollTop;
|
|
215
|
+
if (!immediate && mayDefer(next, from)) {
|
|
214
216
|
// The rows move instead; the scroll range stays the same, so the engine clamps nothing.
|
|
215
|
-
lastTop =
|
|
217
|
+
lastTop = from;
|
|
216
218
|
deferred = next - lastTop;
|
|
217
219
|
feed.style.height = `${total() - deferred}px`;
|
|
218
220
|
return next;
|
|
219
221
|
}
|
|
220
222
|
deferred = 0;
|
|
221
223
|
feed.style.height = `${total()}px`;
|
|
222
|
-
const from = viewport.scrollTop;
|
|
223
224
|
if (Math.abs(from - next) > 0.5) viewport.scrollTop = next;
|
|
224
225
|
lastTop = viewport.scrollTop;
|
|
225
226
|
if (Math.abs(lastTop - from) > 0.5) owedUntil = performance.now() + END_WAIT_MS;
|
|
226
227
|
return next;
|
|
227
228
|
};
|
|
228
229
|
let visibleKeys;
|
|
229
|
-
const reportVisible = () => {
|
|
230
|
+
const reportVisible = (top, height) => {
|
|
230
231
|
if (!props.onVisibleChange) return;
|
|
231
|
-
|
|
232
|
-
const bottom = top + viewport.clientHeight;
|
|
232
|
+
top ??= logicalTop();
|
|
233
|
+
const bottom = top + (height ?? viewport.clientHeight);
|
|
233
234
|
let first = -1;
|
|
234
235
|
let last = -1;
|
|
235
236
|
if (bottom > top && hi > lo) for (let index = indexAt(top); index < hi && offsetOf(index) < bottom; index++) {
|
|
@@ -252,9 +253,7 @@ export function VirtualFeed(props) {
|
|
|
252
253
|
last: list[last]
|
|
253
254
|
});
|
|
254
255
|
};
|
|
255
|
-
const updateRange = () => {
|
|
256
|
-
const top = logicalTop();
|
|
257
|
-
const height = viewport.clientHeight;
|
|
256
|
+
const updateRange = (top = logicalTop(), height = viewport.clientHeight) => {
|
|
258
257
|
const indices = [];
|
|
259
258
|
if (hi > lo) {
|
|
260
259
|
const start = indexAt(top - OVERSCAN);
|
|
@@ -268,7 +267,7 @@ export function VirtualFeed(props) {
|
|
|
268
267
|
renderRows(indices);
|
|
269
268
|
setTabStop(focusIndex >= lo && focusIndex < hi ? focusIndex : hi > lo ? indexAt(top + height - 1) : -1);
|
|
270
269
|
// Newly mounted rows still have estimates; report only after their heights are measured.
|
|
271
|
-
if (!restoring && !fresh.some(node => node.isConnected)) reportVisible();
|
|
270
|
+
if (!restoring && !fresh.some(node => node.isConnected)) reportVisible(top, height);
|
|
272
271
|
};
|
|
273
272
|
const measure = measured => {
|
|
274
273
|
let changed = false;
|
|
@@ -422,6 +421,8 @@ export function VirtualFeed(props) {
|
|
|
422
421
|
return;
|
|
423
422
|
}
|
|
424
423
|
restoring = true;
|
|
424
|
+
const restored = ++restoreVersion;
|
|
425
|
+
const height = viewport.clientHeight;
|
|
425
426
|
let target = 0;
|
|
426
427
|
let top = 0;
|
|
427
428
|
// Following the end shows the end of all items from now on, so later moves are judged against it.
|
|
@@ -431,9 +432,10 @@ export function VirtualFeed(props) {
|
|
|
431
432
|
restoreAgain = false;
|
|
432
433
|
recompute();
|
|
433
434
|
target = stick ? total() : offsetOf(anchor.index) - anchor.delta;
|
|
434
|
-
top =
|
|
435
|
-
|
|
436
|
-
|
|
435
|
+
top = Math.min(Math.max(0, total() - height), Math.max(0, target));
|
|
436
|
+
// Row heights do not depend on their translation or the feed's scroll position. Mount and measure the
|
|
437
|
+
// logical range first; writing the final geometry once avoids a layout flush on every correction pass.
|
|
438
|
+
updateRange(top, height);
|
|
437
439
|
if (pass === MEASURE_PASSES) break;
|
|
438
440
|
// Rows mounted here reach the ResizeObserver only next frame (its loop limit), so measure them now.
|
|
439
441
|
const mounted = fresh.filter(node => node.isConnected);
|
|
@@ -441,6 +443,8 @@ export function VirtualFeed(props) {
|
|
|
441
443
|
if (measure(mounted.map(node => [node, node.getBoundingClientRect().height]))) restoreAgain = true;
|
|
442
444
|
if (!restoreAgain) break;
|
|
443
445
|
}
|
|
446
|
+
top = writeTop(target, immediate, height);
|
|
447
|
+
setVersion(value => value + 1);
|
|
444
448
|
// A position the layout cannot reach, above its start or past its end, becomes the one the reader sees, so a
|
|
445
449
|
// page that loads there later keeps it instead of moving to it.
|
|
446
450
|
if (!stick && Math.abs(top - target) > 0.5) anchor = {
|
|
@@ -450,8 +454,9 @@ export function VirtualFeed(props) {
|
|
|
450
454
|
} finally {
|
|
451
455
|
restoring = false;
|
|
452
456
|
}
|
|
453
|
-
checkEdges();
|
|
454
|
-
|
|
457
|
+
checkEdges(lastTop + deferred, height);
|
|
458
|
+
// A synchronous edge load can restore another window; that restore already reports its final geometry.
|
|
459
|
+
if (restored === restoreVersion) reportVisible(lastTop + deferred, height);
|
|
455
460
|
};
|
|
456
461
|
|
|
457
462
|
/**
|
|
@@ -623,10 +628,10 @@ export function VirtualFeed(props) {
|
|
|
623
628
|
};
|
|
624
629
|
|
|
625
630
|
/** Grows the window or asks for more items when the reader nears an edge. */
|
|
626
|
-
const checkEdges = () => {
|
|
631
|
+
const checkEdges = (top, height) => {
|
|
627
632
|
if (hi <= lo || restoring) return;
|
|
628
|
-
|
|
629
|
-
|
|
633
|
+
top ??= logicalTop();
|
|
634
|
+
height ??= viewport.clientHeight;
|
|
630
635
|
// Rows measured taller than estimated can grow the window past the cap without any edge moving.
|
|
631
636
|
if (total() > MAX_WINDOW && trimWindow(top + height / 2 > total() / 2 ? "start" : "end")) {
|
|
632
637
|
restore(true);
|