@eifi1/ui-kit 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/admin/admin-sections.d.ts +8 -0
- package/dist/admin/admin-sections.js +2 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/auth/verify-email.d.ts +9 -1
- package/dist/auth/verify-email.js +22 -13
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/components/alert-banner.d.ts +29 -1
- package/dist/components/alert-banner.js +84 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/button-look.d.ts +4 -0
- package/dist/components/button-look.js +2 -0
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/combobox-core.js +5 -1
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +6 -1
- package/dist/components/data-table-column-settings.js +12 -2
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +44 -0
- package/dist/components/data-table-fit.js +103 -0
- package/dist/components/data-table-fit.js.map +1 -0
- package/dist/components/data-table-groups.d.ts +95 -0
- package/dist/components/data-table-groups.js +165 -0
- package/dist/components/data-table-groups.js.map +1 -0
- package/dist/components/data-table-head.js +3 -1
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +7 -0
- package/dist/components/data-table-labels.js +3 -0
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-leading-row.d.ts +17 -0
- package/dist/components/data-table-leading-row.js +41 -0
- package/dist/components/data-table-leading-row.js.map +1 -0
- package/dist/components/data-table-resize.js +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table.d.ts +78 -2
- package/dist/components/data-table.js +348 -257
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/disclosure.d.ts +3 -1
- package/dist/components/disclosure.js +10 -4
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +8 -1
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/floating-panel.d.ts +14 -1
- package/dist/components/floating-panel.js +7 -2
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/hover-menu.js +5 -1
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +2 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/popover.js +5 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/table-skeleton.d.ts +35 -0
- package/dist/components/table-skeleton.js +75 -0
- package/dist/components/table-skeleton.js.map +1 -0
- package/dist/components/tabs.d.ts +10 -12
- package/dist/components/tabs.js +20 -10
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/use-table-state.d.ts +8 -7
- package/dist/components/use-table-state.js +37 -3
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +1 -0
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-table.d.ts +5 -0
- package/dist/feedback/feedback-table.js +30 -7
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +30 -1
- package/dist/hooks/use-dismiss.js +30 -2
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/i18n/german.js +6 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +4 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +6 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +6 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +6 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +6 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +6 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +6 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/shared-labels.js +3 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/float-clearance.d.ts +8 -0
- package/dist/lib/float-clearance.js +29 -0
- package/dist/lib/float-clearance.js.map +1 -0
- package/dist/settings/settings-catalogue.d.ts +10 -0
- package/dist/settings/settings-catalogue.js.map +1 -1
- package/dist/settings/settings-layout.js +10 -3
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/app-shell.d.ts +5 -0
- package/dist/shell/app-shell.js +5 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.d.ts +2 -0
- package/dist/shell/option-switcher-menu.js +6 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +2 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +5 -3
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/tour/tour.js +1 -0
- package/dist/tour/tour.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-sections.tsx +8 -0
- package/src/auth/verify-email.tsx +34 -9
- package/src/components/alert-banner.tsx +131 -7
- package/src/components/button-look.ts +6 -0
- package/src/components/combobox-core.tsx +7 -1
- package/src/components/data-table-column-settings.tsx +23 -2
- package/src/components/data-table-fit.ts +187 -0
- package/src/components/data-table-groups.tsx +275 -0
- package/src/components/data-table-head.tsx +4 -1
- package/src/components/data-table-labels.ts +13 -0
- package/src/components/data-table-leading-row.tsx +55 -0
- package/src/components/data-table-resize.ts +2 -1
- package/src/components/data-table.tsx +246 -43
- package/src/components/disclosure.tsx +22 -5
- package/src/components/dropdown.tsx +11 -1
- package/src/components/floating-panel.tsx +24 -1
- package/src/components/hover-menu.tsx +6 -1
- package/src/components/icon-button.tsx +3 -3
- package/src/components/popover.tsx +7 -1
- package/src/components/table-skeleton.tsx +133 -0
- package/src/components/tabs.tsx +67 -37
- package/src/components/use-table-state.ts +54 -3
- package/src/data-table.ts +2 -0
- package/src/feedback/feedback-table.tsx +33 -1
- package/src/hooks/use-dismiss.ts +65 -3
- package/src/i18n/german.ts +5 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/es.ts +5 -0
- package/src/i18n/locales/fr.ts +5 -0
- package/src/i18n/locales/hu.ts +5 -0
- package/src/i18n/locales/it.ts +5 -0
- package/src/i18n/locales/zh.ts +5 -0
- package/src/i18n/shared-labels.ts +2 -0
- package/src/index.ts +3 -0
- package/src/lib/float-clearance.ts +54 -0
- package/src/settings/settings-catalogue.ts +10 -0
- package/src/settings/settings-layout.tsx +18 -4
- package/src/shell/app-shell.tsx +17 -3
- package/src/shell/option-switcher-menu.tsx +16 -4
- package/src/shell/topbar-action-menu.tsx +22 -10
- package/src/shell/topbar-controls.tsx +18 -12
- package/src/tour/tour.tsx +5 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/data-table-head.tsx"],"sourcesContent":["import type { Dispatch, SetStateAction } from \"react\";\nimport { ArrowDown, ArrowUp, ArrowUpDown, Filter } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { defaultFilterState, isFilterActive, resolveFilter } from \"./data-table-filters\";\nimport type { FilterValue } from \"./data-table-filters\";\nimport type { SortState } from \"./data-table-sort\";\nimport { FilterPopover } from \"./data-table-filter-popover\";\nimport { Popover } from \"./popover\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport { isTableOwnColumn } from \"./data-table-row-actions\";\nimport { columnLabel, logicalAlign, splitCellProps } from \"./data-table-shared\";\nimport type { DataTableColumn, DataTableProps, FilterState } from \"./data-table\";\n\n// DataTable's `<thead>` on a wide screen: the select-all box, and per column the sort\n// button, the filter popover and the resize handle. Moved out of data-table.tsx whole\n// (refactor audit 2026-10, MOD-3 step 5); the state it shows and changes stays the\n// table's.\n\n/** Whether a column is end-aligned — the header then puts its filter button first. */\nconst END_ALIGN = /(^|\\s)text-end(?=\\s|$)/;\n\n/** The wide-screen table's header row. */\nexport function DataTableHead<T>({\n visibleColumns,\n selection,\n compact,\n labels,\n locale,\n isServer,\n hasSortCallback,\n hasFilterCallback,\n sorts,\n filters,\n selectOptionsByKey,\n multiSort,\n resizable,\n widths,\n setWidths,\n headRefs,\n toggleSort,\n setFilterValue,\n clearFilter,\n startResize,\n moveResize,\n finishResize,\n}: {\n visibleColumns: DataTableColumn<T>[];\n selection: DataTableProps<T>[\"selection\"];\n compact: boolean;\n labels: DataTableLabels;\n locale?: string;\n isServer: boolean;\n /** The caller owns the sort (`onSortsChange`), so a server table can offer it. */\n hasSortCallback: boolean;\n /** The caller owns the filters (`onFiltersChange`), likewise. */\n hasFilterCallback: boolean;\n sorts: SortState[];\n filters: FilterState;\n selectOptionsByKey: Record<string, { value: string; label: string }[]>;\n multiSort: boolean;\n resizable: boolean;\n widths: Record<string, number>;\n setWidths: Dispatch<SetStateAction<Record<string, number>>>;\n /** Filled here, one `<th>` per column — what a resize and \"Auto-size\" measure. */\n headRefs: Map<string, HTMLTableCellElement>;\n toggleSort: (key: string, shiftKey: boolean) => void;\n setFilterValue: (key: string, value: FilterValue) => void;\n clearFilter: (key: string) => void;\n startResize: (e: React.PointerEvent<HTMLElement>, key: string) => void;\n moveResize: (e: React.PointerEvent<HTMLElement>) => void;\n finishResize: (e: React.PointerEvent<HTMLElement>, commit: boolean) => void;\n}) {\n return (\n <thead className=\"bg-surface-2 text-secondary\">\n <tr className=\"text-start\">\n {selection && (\n <th\n scope=\"col\"\n className={cn(\n \"sticky top-0 z-10 bg-surface-2 align-middle backdrop-blur-sm\",\n compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\",\n )}\n >\n <input\n type=\"checkbox\"\n className={cn(\"align-middle accent-indigo-600\", compact ? \"size-3.5\" : \"size-4\")}\n checked={selection.allSelected}\n ref={(el) => {\n if (el) el.indeterminate = selection.someSelected && !selection.allSelected;\n }}\n onChange={(e) => selection.onToggleAll(e.target.checked)}\n aria-label={labels.selectAllRows}\n />\n </th>\n )}\n {visibleColumns.map((col) => {\n // In server mode the utilities stay hidden unless the caller\n // takes them over and feeds them back into the server query.\n const sortable = (!isServer || hasSortCallback) && !!col.sortBy;\n const filter = !isServer || hasFilterCallback ? resolveFilter(col) : null;\n const filterState = filters[col.key] ?? (filter ? defaultFilterState(filter) : undefined);\n const sortIdx = sorts.findIndex((s) => s.key === col.key);\n const active = sortIdx !== -1;\n const Icon = active ? (sorts[sortIdx].dir === \"asc\" ? ArrowUp : ArrowDown) : ArrowUpDown;\n const filterActive = filterState && isFilterActive(filterState);\n // Logical, and a legacy `text-right` still counts — see `logicalAlign`.\n const headClass = logicalAlign(col.headClassName);\n const isEndAligned = !!headClass && END_ALIGN.test(headClass);\n const width = widths[col.key];\n const ownHead = splitCellProps(col.headProps);\n const pinnedHead = width ? { width, minWidth: width, maxWidth: width } : undefined;\n return (\n <th\n {...ownHead.attrs}\n key={col.key}\n ref={(el) => {\n if (el) headRefs.set(col.key, el);\n else headRefs.delete(col.key);\n }}\n scope=\"col\"\n // Only a header the user can actually sort says anything: an\n // `aria-sort` on a plain column is a control that is not there,\n // and a persisted or URL sort can name a column with no `sortBy`\n // (the sort chain drops it silently) — which would otherwise\n // claim a sort the table is not applying.\n //\n // \"none\" on the rest is the half that was missing. Without it a\n // sortable header is indistinguishable from a plain one until the\n // user presses it. Secondary criteria read \"none\" too: ARIA allows\n // exactly one sorted column, and the priority is carried by the\n // badge and by what the header button announces on press.\n aria-sort={\n sortable\n ? sortIdx === 0\n ? sorts[0].dir === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : \"none\"\n : undefined\n }\n style={ownHead.style || pinnedHead ? { ...ownHead.style, ...pinnedHead } : undefined}\n className={cn(\n \"relative font-medium align-middle whitespace-nowrap\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n // Keep the header visible while the user scrolls the page.\n // Each <th> carries its own bg so the row doesn't render\n // transparent over the data rows underneath.\n \"sticky top-0 z-10 bg-surface-2 backdrop-blur-sm\",\n logicalAlign(ownHead.className),\n headClass,\n )}\n >\n <div\n className={cn(\n \"flex items-center gap-1\",\n isEndAligned && \"flex-row-reverse\",\n )}\n >\n <Tooltip label={sortable && multiSort ? labels.sortHint : undefined} portal>\n <button\n type=\"button\"\n onClick={(e) => sortable && toggleSort(col.key, e.shiftKey)}\n disabled={!sortable}\n className={cn(\n \"flex items-center gap-1 text-start\",\n sortable && \"hover:text-primary\",\n )}\n >\n <span>{col.header}</span>\n {sortable && (\n <Icon className={cn(\"size-3\", active ? \"opacity-100\" : \"opacity-40\")} />\n )}\n {/* Priority badge, only meaningful with 2+ sort keys */}\n {active && sorts.length > 1 && (\n <span className=\"text-micro font-semibold leading-none text-brand\">\n {sortIdx + 1}\n </span>\n )}\n </button>\n </Tooltip>\n {filter && (\n <Popover\n // 420 px at Normal, in rem so the calendar grows with its\n // text (§3.2); Popover caps it at the viewport.\n width={filter.type === \"date\" ? \"26.25rem\" : undefined}\n // The panel is THIS column's filter, and says so. Unnamed it was\n // announced as the kit-wide fallback, \"Popover\"; then as \"Filter\"\n // in every column alike (round 0.34 Wave 2, BUG-7c).\n labels={{ panel: labels.filterColumn(columnLabel(col, labels), false) }}\n trigger={({ open, toggle, ref }) => (\n <button\n type=\"button\"\n ref={ref}\n onClick={toggle}\n // Named for its column, and for being filtered: the tint\n // alone said that to nobody who cannot see it (BUG-7c).\n aria-label={labels.filterColumn(columnLabel(col, labels), Boolean(filterActive))}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className={cn(\n \"rounded p-1 transition-colors\",\n filterActive\n ? \"bg-brand-soft text-brand-muted\"\n : \"text-placeholder hover:text-secondary hover:bg-hover\",\n )}\n >\n <Filter className=\"size-3\" />\n </button>\n )}\n >\n {() => (\n <FilterPopover\n column={col}\n state={filterState ?? defaultFilterState(filter)}\n onChange={(next) => setFilterValue(col.key, next)}\n onClear={() => clearFilter(col.key)}\n selectOptions={selectOptionsByKey[col.key] ?? []}\n labels={labels}\n locale={locale}\n />\n )}\n </Popover>\n )}\n </div>\n {resizable && !isTableOwnColumn(col.key) && (\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a\n pointer-only drag handle for a visual preference (column width); it is\n deliberately not a tab stop, and no content or action depends on it. */\n <span\n role=\"separator\"\n aria-orientation=\"vertical\"\n onPointerDown={(e) => startResize(e, col.key)}\n onPointerMove={moveResize}\n onPointerUp={(e) => finishResize(e, true)}\n onPointerCancel={(e) => finishResize(e, false)}\n onDoubleClick={(e) => {\n e.preventDefault();\n setWidths((w) => {\n const next = { ...w };\n delete next[col.key];\n return next;\n });\n }}\n // `touch-none`: without it the browser claims a finger drag for\n // scrolling before the first `pointermove` ever arrives.\n // `end-0` and a translate that flips with it: the handle sits on the column's\n // trailing edge, which is its left one in a right-to-left table.\n className=\"absolute end-0 top-0 z-10 h-full w-1.5 -translate-x-1/2 rtl:translate-x-1/2 cursor-col-resize select-none touch-none hover:bg-brand-soft-hover\"\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/data-table-head.tsx"],"sourcesContent":["import type { Dispatch, SetStateAction } from \"react\";\nimport { ArrowDown, ArrowUp, ArrowUpDown, Filter } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { defaultFilterState, isFilterActive, resolveFilter } from \"./data-table-filters\";\nimport type { FilterValue } from \"./data-table-filters\";\nimport type { SortState } from \"./data-table-sort\";\nimport { FilterPopover } from \"./data-table-filter-popover\";\nimport { Popover } from \"./popover\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport { isTableOwnColumn } from \"./data-table-row-actions\";\nimport { columnLabel, logicalAlign, splitCellProps } from \"./data-table-shared\";\nimport { GROW_HEAD_CLASS } from \"./data-table-fit\";\nimport type { DataTableColumn, DataTableProps, FilterState } from \"./data-table\";\n\n// DataTable's `<thead>` on a wide screen: the select-all box, and per column the sort\n// button, the filter popover and the resize handle. Moved out of data-table.tsx whole\n// (refactor audit 2026-10, MOD-3 step 5); the state it shows and changes stays the\n// table's.\n\n/** Whether a column is end-aligned — the header then puts its filter button first. */\nconst END_ALIGN = /(^|\\s)text-end(?=\\s|$)/;\n\n/** The wide-screen table's header row. */\nexport function DataTableHead<T>({\n visibleColumns,\n selection,\n compact,\n labels,\n locale,\n isServer,\n hasSortCallback,\n hasFilterCallback,\n sorts,\n filters,\n selectOptionsByKey,\n multiSort,\n resizable,\n widths,\n setWidths,\n headRefs,\n toggleSort,\n setFilterValue,\n clearFilter,\n startResize,\n moveResize,\n finishResize,\n}: {\n visibleColumns: DataTableColumn<T>[];\n selection: DataTableProps<T>[\"selection\"];\n compact: boolean;\n labels: DataTableLabels;\n locale?: string;\n isServer: boolean;\n /** The caller owns the sort (`onSortsChange`), so a server table can offer it. */\n hasSortCallback: boolean;\n /** The caller owns the filters (`onFiltersChange`), likewise. */\n hasFilterCallback: boolean;\n sorts: SortState[];\n filters: FilterState;\n selectOptionsByKey: Record<string, { value: string; label: string }[]>;\n multiSort: boolean;\n resizable: boolean;\n widths: Record<string, number>;\n setWidths: Dispatch<SetStateAction<Record<string, number>>>;\n /** Filled here, one `<th>` per column — what a resize and \"Auto-size\" measure. */\n headRefs: Map<string, HTMLTableCellElement>;\n toggleSort: (key: string, shiftKey: boolean) => void;\n setFilterValue: (key: string, value: FilterValue) => void;\n clearFilter: (key: string) => void;\n startResize: (e: React.PointerEvent<HTMLElement>, key: string) => void;\n moveResize: (e: React.PointerEvent<HTMLElement>) => void;\n finishResize: (e: React.PointerEvent<HTMLElement>, commit: boolean) => void;\n}) {\n return (\n <thead className=\"bg-surface-2 text-secondary\">\n <tr className=\"text-start\">\n {selection && (\n <th\n scope=\"col\"\n className={cn(\n \"sticky top-0 z-10 bg-surface-2 align-middle backdrop-blur-sm\",\n compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\",\n )}\n >\n <input\n type=\"checkbox\"\n className={cn(\"align-middle accent-indigo-600\", compact ? \"size-3.5\" : \"size-4\")}\n checked={selection.allSelected}\n ref={(el) => {\n if (el) el.indeterminate = selection.someSelected && !selection.allSelected;\n }}\n onChange={(e) => selection.onToggleAll(e.target.checked)}\n aria-label={labels.selectAllRows}\n />\n </th>\n )}\n {visibleColumns.map((col) => {\n // In server mode the utilities stay hidden unless the caller\n // takes them over and feeds them back into the server query.\n const sortable = (!isServer || hasSortCallback) && !!col.sortBy;\n const filter = !isServer || hasFilterCallback ? resolveFilter(col) : null;\n const filterState = filters[col.key] ?? (filter ? defaultFilterState(filter) : undefined);\n const sortIdx = sorts.findIndex((s) => s.key === col.key);\n const active = sortIdx !== -1;\n const Icon = active ? (sorts[sortIdx].dir === \"asc\" ? ArrowUp : ArrowDown) : ArrowUpDown;\n const filterActive = filterState && isFilterActive(filterState);\n // Logical, and a legacy `text-right` still counts — see `logicalAlign`.\n const headClass = logicalAlign(col.headClassName);\n const isEndAligned = !!headClass && END_ALIGN.test(headClass);\n const width = widths[col.key];\n const ownHead = splitCellProps(col.headProps);\n const pinnedHead = width ? { width, minWidth: width, maxWidth: width } : undefined;\n return (\n <th\n {...ownHead.attrs}\n key={col.key}\n ref={(el) => {\n if (el) headRefs.set(col.key, el);\n else headRefs.delete(col.key);\n }}\n scope=\"col\"\n // Only a header the user can actually sort says anything: an\n // `aria-sort` on a plain column is a control that is not there,\n // and a persisted or URL sort can name a column with no `sortBy`\n // (the sort chain drops it silently) — which would otherwise\n // claim a sort the table is not applying.\n //\n // \"none\" on the rest is the half that was missing. Without it a\n // sortable header is indistinguishable from a plain one until the\n // user presses it. Secondary criteria read \"none\" too: ARIA allows\n // exactly one sorted column, and the priority is carried by the\n // badge and by what the header button announces on press.\n aria-sort={\n sortable\n ? sortIdx === 0\n ? sorts[0].dir === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : \"none\"\n : undefined\n }\n style={ownHead.style || pinnedHead ? { ...ownHead.style, ...pinnedHead } : undefined}\n className={cn(\n \"relative font-medium align-middle whitespace-nowrap\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n // Keep the header visible while the user scrolls the page.\n // Each <th> carries its own bg so the row doesn't render\n // transparent over the data rows underneath.\n \"sticky top-0 z-10 bg-surface-2 backdrop-blur-sm\",\n col.grow && GROW_HEAD_CLASS,\n logicalAlign(ownHead.className),\n headClass,\n )}\n >\n <div\n className={cn(\n \"flex items-center gap-1\",\n isEndAligned && \"flex-row-reverse\",\n )}\n >\n <Tooltip label={sortable && multiSort ? labels.sortHint : undefined} portal>\n <button\n type=\"button\"\n onClick={(e) => sortable && toggleSort(col.key, e.shiftKey)}\n disabled={!sortable}\n className={cn(\n \"flex items-center gap-1 text-start\",\n sortable && \"hover:text-primary\",\n )}\n >\n <span>{col.header}</span>\n {sortable && (\n <Icon className={cn(\"size-3\", active ? \"opacity-100\" : \"opacity-40\")} />\n )}\n {/* Priority badge, only meaningful with 2+ sort keys */}\n {active && sorts.length > 1 && (\n <span className=\"text-micro font-semibold leading-none text-brand\">\n {sortIdx + 1}\n </span>\n )}\n </button>\n </Tooltip>\n {filter && (\n <Popover\n // 420 px at Normal, in rem so the calendar grows with its\n // text (§3.2); Popover caps it at the viewport.\n width={filter.type === \"date\" ? \"26.25rem\" : undefined}\n // The panel is THIS column's filter, and says so. Unnamed it was\n // announced as the kit-wide fallback, \"Popover\"; then as \"Filter\"\n // in every column alike (round 0.34 Wave 2, BUG-7c).\n labels={{ panel: labels.filterColumn(columnLabel(col, labels), false) }}\n trigger={({ open, toggle, ref }) => (\n <button\n type=\"button\"\n ref={ref}\n onClick={toggle}\n // Named for its column, and for being filtered: the tint\n // alone said that to nobody who cannot see it (BUG-7c).\n aria-label={labels.filterColumn(columnLabel(col, labels), Boolean(filterActive))}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className={cn(\n \"rounded p-1 transition-colors\",\n filterActive\n ? \"bg-brand-soft text-brand-muted\"\n : \"text-placeholder hover:text-secondary hover:bg-hover\",\n )}\n >\n <Filter className=\"size-3\" />\n </button>\n )}\n >\n {() => (\n <FilterPopover\n column={col}\n state={filterState ?? defaultFilterState(filter)}\n onChange={(next) => setFilterValue(col.key, next)}\n onClear={() => clearFilter(col.key)}\n selectOptions={selectOptionsByKey[col.key] ?? []}\n labels={labels}\n locale={locale}\n />\n )}\n </Popover>\n )}\n </div>\n {/* A `grow` column takes the rest: no width of its own to drag (0.48, KS N2). */}\n {resizable && !isTableOwnColumn(col.key) && !col.grow && (\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a\n pointer-only drag handle for a visual preference (column width); it is\n deliberately not a tab stop, and no content or action depends on it. */\n <span\n role=\"separator\"\n aria-orientation=\"vertical\"\n onPointerDown={(e) => startResize(e, col.key)}\n onPointerMove={moveResize}\n onPointerUp={(e) => finishResize(e, true)}\n onPointerCancel={(e) => finishResize(e, false)}\n onDoubleClick={(e) => {\n e.preventDefault();\n setWidths((w) => {\n const next = { ...w };\n delete next[col.key];\n return next;\n });\n }}\n // `touch-none`: without it the browser claims a finger drag for\n // scrolling before the first `pointermove` ever arrives.\n // `end-0` and a translate that flips with it: the handle sits on the column's\n // trailing edge, which is its left one in a right-to-left table.\n className=\"absolute end-0 top-0 z-10 h-full w-1.5 -translate-x-1/2 rtl:translate-x-1/2 cursor-col-resize select-none touch-none hover:bg-brand-soft-hover\"\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -23,6 +23,9 @@ export interface DataTableLabels extends DataTableSortLabels {
|
|
|
23
23
|
*/
|
|
24
24
|
rowDetails: string;
|
|
25
25
|
autoSize: string;
|
|
26
|
+
/** Beside a column in the Columns panel that the table's width leaves out
|
|
27
|
+
* (`fitPriority`, 0.48): "hidden for width". */
|
|
28
|
+
hiddenForWidth: string;
|
|
26
29
|
loading: string;
|
|
27
30
|
filters: string;
|
|
28
31
|
clearAll: string;
|
|
@@ -84,5 +87,9 @@ export interface DataTableLabels extends DataTableSortLabels {
|
|
|
84
87
|
booleanTrue: string;
|
|
85
88
|
booleanFalse: string;
|
|
86
89
|
booleanUnset: string;
|
|
90
|
+
/** The `loadMore` footer's button when the app names nothing ("Load August 2026"). */
|
|
91
|
+
loadMore: string;
|
|
92
|
+
/** The `loadMore` footer once nothing is left, unless the app says `endLabel`. */
|
|
93
|
+
allLoaded: string;
|
|
87
94
|
}
|
|
88
95
|
export declare const DEFAULT_DATA_TABLE_LABELS: DataTableLabels;
|
|
@@ -10,6 +10,7 @@ const DEFAULT_DATA_TABLE_LABELS = {
|
|
|
10
10
|
cancelSelection: "Cancel",
|
|
11
11
|
rowDetails: "Details",
|
|
12
12
|
autoSize: "Auto-size columns",
|
|
13
|
+
hiddenForWidth: "hidden for width",
|
|
13
14
|
loading: "Loading\u2026",
|
|
14
15
|
filters: "Filters",
|
|
15
16
|
clearAll: "Clear all",
|
|
@@ -59,6 +60,8 @@ const DEFAULT_DATA_TABLE_LABELS = {
|
|
|
59
60
|
booleanTrue: "Yes",
|
|
60
61
|
booleanFalse: "No",
|
|
61
62
|
booleanUnset: "Not set",
|
|
63
|
+
loadMore: "Load more",
|
|
64
|
+
allLoaded: "All rows loaded",
|
|
62
65
|
// The phone sort control (0.22.0). The same words as `DEFAULT_DATA_TABLE_SORT_LABELS`
|
|
63
66
|
// in data-table.tsx, written out rather than spread in: importing that VALUE here
|
|
64
67
|
// would make data-table.tsx and this module import each other (data-table.tsx reads
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/data-table-labels.ts"],"sourcesContent":["// All user-facing strings for the DataTable system — the `dataTable` namespace of\n// `<UiKitProvider labels>`, and per table the `labels` prop over that. The kit's seven\n// catalogues carry the translations; the English defaults below make the table usable\n// with no wiring.\n\n// A type-only import: erased at build time, so this module stays free of the table\n// itself (and of react-router), which `i18n/defaults` relies on.\nimport type { DataTableSortLabels } from \"./data-table\";\n\n/** The phone sort control's four keys (`sortBy`, `sortDefault`, `sortAscending`,\n * `sortDescending`, 0.22.0) are {@link DataTableSortLabels}', declared beside the\n * control and part of this namespace like every other key. */\nexport interface DataTableLabels extends DataTableSortLabels {\n // Table chrome\n columns: string;\n selectAllRows: string;\n sortHint: string;\n filter: string;\n close: string;\n selectRow: string;\n /** The phone card list's way into selection mode for a keyboard and a screen reader\n * (0.40, #421) — a button, visually hidden until focused; a long press is the touch way in. */\n selectRows: string;\n /** Leaves the phone card list's selection mode, clearing the selection (0.40, #421). */\n cancelSelection: string;\n /**\n * The name of an opening row's disclosure button on desktop — visually hidden, in the\n * row's first cell, carrying `aria-expanded` and `aria-controls` (round 0.34, the\n * showcase's axe check: ARIA allows `aria-expanded` on a row only in a treegrid, and\n * the `<tr>` wore it). The same words whether the row is open or shut: the state is\n * the button's to say.\n */\n rowDetails: string;\n autoSize: string;\n loading: string;\n filters: string;\n clearAll: string;\n done: string;\n // Pagination\n pageSize: string;\n pageSizeAll: string;\n prevPage: string;\n nextPage: string;\n // Filter popover\n clearFilter: string;\n filterPlaceholder: string;\n selectFilter: string;\n selectAll: string;\n selectNone: string;\n dateFrom: string;\n dateTo: string;\n numberMin: string;\n numberMax: string;\n numberAbs: string;\n // Date-range preset labels, keyed by preset key (see dateRangePresets()).\n presets: Record<string, string>;\n\n /**\n * The table's own accessible name. Deliberately a label rather than a second,\n * parallel `tableLabel` prop: the audit already records \"three different prop\n * names for the accessible name\" as a finding, and a table that is not \"Data\n * table\" says so with `labels={{ table: \"Transactions\" }}` at the call site.\n */\n table: string;\n\n // ---- Announcements ----\n // Spoken through the live region, never rendered. Each takes its numbers as\n // ARGUMENTS: a count glued into an English template literal is untranslatable,\n // and the grammar around it moves with the number in most languages.\n\n /** After a filter changes: \"12 of 137 rows\". */\n filterResults: (shown: number, total: number) => string;\n /** After a header is activated: \"Sorted by Name, ascending\". */\n sortedAscending: (column: string) => string;\n sortedDescending: (column: string) => string;\n /** The third click, which removes the column from the sort entirely. */\n sortCleared: (column: string) => string;\n /** After paging: \"Page 3 of 14\". `page` is 1-BASED, as spoken. */\n pageChanged: (page: number, totalPages: number) => string;\n\n // ---- Counted chrome ----\n // Visible text that interpolates numbers. These were template literals until the\n // audit flagged them: a consumer could translate every key in this file and still\n // be left with English (and with ASCII digit grouping) in the footer.\n\n /** The pagination range summary: \"1–25 / 137\". */\n pageRange: (from: number, to: number, total: number) => string;\n /** The same summary when the page size is \"all\" — just the row count. */\n rowCount: (total: number) => string;\n /** The column-settings rail and its heading: \"Columns (4/9)\". */\n columnsCount: (visible: number, total: number) => string;\n /**\n * A column's filter button and panel: \"Filter Amount\", and \"Filter Amount (active)\"\n * while the column is filtered (round 0.34 Wave 2, BUG-7c). Every column's used to be\n * \"Filter\" alike, and \"filtered\" was a colour. An app that translated only `filter`\n * gets `\"<filter>: <column>\"` from it: DataTable derives the one from the other.\n */\n filterColumn: (column: string, active: boolean) => string;\n\n // ---- Empty state, row actions, boolean cells (0.12.0) ----\n\n /** The body of a table with no rows and no `empty` prop of its own. A sentence, not\n * \"—\": a dash in the only row of a table reads as one row with a missing value. */\n empty: string;\n /** The accessible name of the `rowActions` column (its header is visually empty). */\n actions: string;\n /** The `kind: \"edit\"` row action's name — its tooltip and its swipe label. */\n edit: string;\n /** The `kind: \"delete\"` row action's name. */\n delete: string;\n /** What {@link BooleanMark} says to a screen reader for `true` / `false` / nothing. */\n booleanTrue: string;\n booleanFalse: string;\n booleanUnset: string;\n}\n\nexport const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {\n columns: \"Columns\",\n selectAllRows: \"Select all rows\",\n sortHint: \"Click to sort · Shift-click to add a sort\",\n filter: \"Filter\",\n close: \"Close\",\n selectRow: \"Select row\",\n selectRows: \"Select rows\",\n cancelSelection: \"Cancel\",\n rowDetails: \"Details\",\n autoSize: \"Auto-size columns\",\n loading: \"Loading…\",\n filters: \"Filters\",\n clearAll: \"Clear all\",\n done: \"Done\",\n pageSize: \"Rows per page\",\n pageSizeAll: \"All\",\n prevPage: \"Previous page\",\n nextPage: \"Next page\",\n clearFilter: \"Clear filter\",\n filterPlaceholder: \"Filter…\",\n selectFilter: \"Select\",\n selectAll: \"All\",\n selectNone: \"None\",\n dateFrom: \"From\",\n dateTo: \"To\",\n numberMin: \"Min\",\n numberMax: \"Max\",\n numberAbs: \"Absolute value\",\n presets: {\n today: \"Today\",\n yesterday: \"Yesterday\",\n this_week: \"This week\",\n last_week: \"Last week\",\n last_7_days: \"Last 7 days\",\n last_30_days: \"Last 30 days\",\n this_month: \"This month\",\n last_month: \"Last month\",\n last_3_months: \"Last 3 months\",\n ytd: \"Year to date\",\n last_year: \"Last year\",\n },\n table: \"Data table\",\n filterResults: (shown, total) => `${shown} of ${total} rows`,\n sortedAscending: (column) => `Sorted by ${column}, ascending`,\n sortedDescending: (column) => `Sorted by ${column}, descending`,\n sortCleared: (column) => `Sort cleared on ${column}`,\n pageChanged: (page, totalPages) => `Page ${page} of ${totalPages}`,\n // The separators are the ones that shipped, so adding the seam changes no screen.\n pageRange: (from, to, total) => `${from}–${to} / ${total}`,\n rowCount: (total) => `${total}`,\n columnsCount: (visible, total) => `Columns (${visible}/${total})`,\n filterColumn: (column, active) => (active ? `Filter ${column} (active)` : `Filter ${column}`),\n empty: \"No entries\",\n actions: \"Actions\",\n edit: \"Edit\",\n delete: \"Delete\",\n booleanTrue: \"Yes\",\n booleanFalse: \"No\",\n booleanUnset: \"Not set\",\n // The phone sort control (0.22.0). The same words as `DEFAULT_DATA_TABLE_SORT_LABELS`\n // in data-table.tsx, written out rather than spread in: importing that VALUE here\n // would make data-table.tsx and this module import each other (data-table.tsx reads\n // these defaults from here), and would pull the whole table into `i18n/defaults`.\n sortBy: \"Sort by\",\n sortDefault: \"Default order\",\n sortAscending: \"Ascending\",\n sortDescending: \"Descending\",\n};\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/components/data-table-labels.ts"],"sourcesContent":["// All user-facing strings for the DataTable system — the `dataTable` namespace of\n// `<UiKitProvider labels>`, and per table the `labels` prop over that. The kit's seven\n// catalogues carry the translations; the English defaults below make the table usable\n// with no wiring.\n\n// A type-only import: erased at build time, so this module stays free of the table\n// itself (and of react-router), which `i18n/defaults` relies on.\nimport type { DataTableSortLabels } from \"./data-table\";\n\n/** The phone sort control's four keys (`sortBy`, `sortDefault`, `sortAscending`,\n * `sortDescending`, 0.22.0) are {@link DataTableSortLabels}', declared beside the\n * control and part of this namespace like every other key. */\nexport interface DataTableLabels extends DataTableSortLabels {\n // Table chrome\n columns: string;\n selectAllRows: string;\n sortHint: string;\n filter: string;\n close: string;\n selectRow: string;\n /** The phone card list's way into selection mode for a keyboard and a screen reader\n * (0.40, #421) — a button, visually hidden until focused; a long press is the touch way in. */\n selectRows: string;\n /** Leaves the phone card list's selection mode, clearing the selection (0.40, #421). */\n cancelSelection: string;\n /**\n * The name of an opening row's disclosure button on desktop — visually hidden, in the\n * row's first cell, carrying `aria-expanded` and `aria-controls` (round 0.34, the\n * showcase's axe check: ARIA allows `aria-expanded` on a row only in a treegrid, and\n * the `<tr>` wore it). The same words whether the row is open or shut: the state is\n * the button's to say.\n */\n rowDetails: string;\n autoSize: string;\n /** Beside a column in the Columns panel that the table's width leaves out\n * (`fitPriority`, 0.48): \"hidden for width\". */\n hiddenForWidth: string;\n loading: string;\n filters: string;\n clearAll: string;\n done: string;\n // Pagination\n pageSize: string;\n pageSizeAll: string;\n prevPage: string;\n nextPage: string;\n // Filter popover\n clearFilter: string;\n filterPlaceholder: string;\n selectFilter: string;\n selectAll: string;\n selectNone: string;\n dateFrom: string;\n dateTo: string;\n numberMin: string;\n numberMax: string;\n numberAbs: string;\n // Date-range preset labels, keyed by preset key (see dateRangePresets()).\n presets: Record<string, string>;\n\n /**\n * The table's own accessible name. Deliberately a label rather than a second,\n * parallel `tableLabel` prop: the audit already records \"three different prop\n * names for the accessible name\" as a finding, and a table that is not \"Data\n * table\" says so with `labels={{ table: \"Transactions\" }}` at the call site.\n */\n table: string;\n\n // ---- Announcements ----\n // Spoken through the live region, never rendered. Each takes its numbers as\n // ARGUMENTS: a count glued into an English template literal is untranslatable,\n // and the grammar around it moves with the number in most languages.\n\n /** After a filter changes: \"12 of 137 rows\". */\n filterResults: (shown: number, total: number) => string;\n /** After a header is activated: \"Sorted by Name, ascending\". */\n sortedAscending: (column: string) => string;\n sortedDescending: (column: string) => string;\n /** The third click, which removes the column from the sort entirely. */\n sortCleared: (column: string) => string;\n /** After paging: \"Page 3 of 14\". `page` is 1-BASED, as spoken. */\n pageChanged: (page: number, totalPages: number) => string;\n\n // ---- Counted chrome ----\n // Visible text that interpolates numbers. These were template literals until the\n // audit flagged them: a consumer could translate every key in this file and still\n // be left with English (and with ASCII digit grouping) in the footer.\n\n /** The pagination range summary: \"1–25 / 137\". */\n pageRange: (from: number, to: number, total: number) => string;\n /** The same summary when the page size is \"all\" — just the row count. */\n rowCount: (total: number) => string;\n /** The column-settings rail and its heading: \"Columns (4/9)\". */\n columnsCount: (visible: number, total: number) => string;\n /**\n * A column's filter button and panel: \"Filter Amount\", and \"Filter Amount (active)\"\n * while the column is filtered (round 0.34 Wave 2, BUG-7c). Every column's used to be\n * \"Filter\" alike, and \"filtered\" was a colour. An app that translated only `filter`\n * gets `\"<filter>: <column>\"` from it: DataTable derives the one from the other.\n */\n filterColumn: (column: string, active: boolean) => string;\n\n // ---- Empty state, row actions, boolean cells (0.12.0) ----\n\n /** The body of a table with no rows and no `empty` prop of its own. A sentence, not\n * \"—\": a dash in the only row of a table reads as one row with a missing value. */\n empty: string;\n /** The accessible name of the `rowActions` column (its header is visually empty). */\n actions: string;\n /** The `kind: \"edit\"` row action's name — its tooltip and its swipe label. */\n edit: string;\n /** The `kind: \"delete\"` row action's name. */\n delete: string;\n /** What {@link BooleanMark} says to a screen reader for `true` / `false` / nothing. */\n booleanTrue: string;\n booleanFalse: string;\n booleanUnset: string;\n\n // ---- Load more (0.48, keksdose dev #605) ----\n\n /** The `loadMore` footer's button when the app names nothing (\"Load August 2026\"). */\n loadMore: string;\n /** The `loadMore` footer once nothing is left, unless the app says `endLabel`. */\n allLoaded: string;\n}\n\nexport const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {\n columns: \"Columns\",\n selectAllRows: \"Select all rows\",\n sortHint: \"Click to sort · Shift-click to add a sort\",\n filter: \"Filter\",\n close: \"Close\",\n selectRow: \"Select row\",\n selectRows: \"Select rows\",\n cancelSelection: \"Cancel\",\n rowDetails: \"Details\",\n autoSize: \"Auto-size columns\",\n hiddenForWidth: \"hidden for width\",\n loading: \"Loading…\",\n filters: \"Filters\",\n clearAll: \"Clear all\",\n done: \"Done\",\n pageSize: \"Rows per page\",\n pageSizeAll: \"All\",\n prevPage: \"Previous page\",\n nextPage: \"Next page\",\n clearFilter: \"Clear filter\",\n filterPlaceholder: \"Filter…\",\n selectFilter: \"Select\",\n selectAll: \"All\",\n selectNone: \"None\",\n dateFrom: \"From\",\n dateTo: \"To\",\n numberMin: \"Min\",\n numberMax: \"Max\",\n numberAbs: \"Absolute value\",\n presets: {\n today: \"Today\",\n yesterday: \"Yesterday\",\n this_week: \"This week\",\n last_week: \"Last week\",\n last_7_days: \"Last 7 days\",\n last_30_days: \"Last 30 days\",\n this_month: \"This month\",\n last_month: \"Last month\",\n last_3_months: \"Last 3 months\",\n ytd: \"Year to date\",\n last_year: \"Last year\",\n },\n table: \"Data table\",\n filterResults: (shown, total) => `${shown} of ${total} rows`,\n sortedAscending: (column) => `Sorted by ${column}, ascending`,\n sortedDescending: (column) => `Sorted by ${column}, descending`,\n sortCleared: (column) => `Sort cleared on ${column}`,\n pageChanged: (page, totalPages) => `Page ${page} of ${totalPages}`,\n // The separators are the ones that shipped, so adding the seam changes no screen.\n pageRange: (from, to, total) => `${from}–${to} / ${total}`,\n rowCount: (total) => `${total}`,\n columnsCount: (visible, total) => `Columns (${visible}/${total})`,\n filterColumn: (column, active) => (active ? `Filter ${column} (active)` : `Filter ${column}`),\n empty: \"No entries\",\n actions: \"Actions\",\n edit: \"Edit\",\n delete: \"Delete\",\n booleanTrue: \"Yes\",\n booleanFalse: \"No\",\n booleanUnset: \"Not set\",\n loadMore: \"Load more\",\n allLoaded: \"All rows loaded\",\n // The phone sort control (0.22.0). The same words as `DEFAULT_DATA_TABLE_SORT_LABELS`\n // in data-table.tsx, written out rather than spread in: importing that VALUE here\n // would make data-table.tsx and this module import each other (data-table.tsx reads\n // these defaults from here), and would pull the whole table into `i18n/defaults`.\n sortBy: \"Sort by\",\n sortDefault: \"Default order\",\n sortAscending: \"Ascending\",\n sortDescending: \"Descending\",\n};\n"],"mappings":";AA8HO,MAAM,4BAA6C;AAAA,EACxD,SAAS;AAAA,EACT,eAAe;AAAA,EACf,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,cAAc;AAAA,EACd,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AAAA,IACP,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,KAAK;AAAA,IACL,WAAW;AAAA,EACb;AAAA,EACA,OAAO;AAAA,EACP,eAAe,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,KAAK;AAAA,EACrD,iBAAiB,CAAC,WAAW,aAAa,MAAM;AAAA,EAChD,kBAAkB,CAAC,WAAW,aAAa,MAAM;AAAA,EACjD,aAAa,CAAC,WAAW,mBAAmB,MAAM;AAAA,EAClD,aAAa,CAAC,MAAM,eAAe,QAAQ,IAAI,OAAO,UAAU;AAAA;AAAA,EAEhE,WAAW,CAAC,MAAM,IAAI,UAAU,GAAG,IAAI,SAAI,EAAE,MAAM,KAAK;AAAA,EACxD,UAAU,CAAC,UAAU,GAAG,KAAK;AAAA,EAC7B,cAAc,CAAC,SAAS,UAAU,YAAY,OAAO,IAAI,KAAK;AAAA,EAC9D,cAAc,CAAC,QAAQ,WAAY,SAAS,UAAU,MAAM,cAAc,UAAU,MAAM;AAAA,EAC1F,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,cAAc;AAAA,EACd,cAAc;AAAA,EACd,UAAU;AAAA,EACV,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,EAKX,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,eAAe;AAAA,EACf,gBAAgB;AAClB;","names":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The desktop table's `leadingRow`: one full-width cell above the data rows. With
|
|
4
|
+
* `sticky` (0.48, keksdose dev #603) the cell is pinned under the sticky header while the
|
|
5
|
+
* rows scroll — `top` is the header's height, measured and followed (a header that wraps
|
|
6
|
+
* at Large text grows). On the cell, not the `<tr>`: position:sticky on a table row is not
|
|
7
|
+
* reliable across browsers, the reason the header's sticky sits on each `<th>`. Opaque
|
|
8
|
+
* (`bg-surface`), so the rows pass beneath it; `z-[5]`, under the header's `z-10` and over
|
|
9
|
+
* the rows' own sticky cells. The line over it is the cell's own `::before`, drawn over
|
|
10
|
+
* the content (a full-width disclosure header paints its own fill) and moving with the
|
|
11
|
+
* cell; a collapsed table's row border would stay behind at the row's place.
|
|
12
|
+
*/
|
|
13
|
+
export declare function DataTableLeadingRow({ colSpan, sticky, children, }: {
|
|
14
|
+
colSpan: number;
|
|
15
|
+
sticky?: boolean;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
function DataTableLeadingRow({
|
|
6
|
+
colSpan,
|
|
7
|
+
sticky = false,
|
|
8
|
+
children
|
|
9
|
+
}) {
|
|
10
|
+
const cellRef = useRef(null);
|
|
11
|
+
const [top, setTop] = useState(0);
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
if (!sticky) return;
|
|
14
|
+
const head = cellRef.current?.closest("table")?.tHead;
|
|
15
|
+
if (!head) return;
|
|
16
|
+
const measure = () => setTop(head.getBoundingClientRect().height);
|
|
17
|
+
measure();
|
|
18
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
19
|
+
const observer = new ResizeObserver(measure);
|
|
20
|
+
observer.observe(head);
|
|
21
|
+
return () => observer.disconnect();
|
|
22
|
+
}, [sticky]);
|
|
23
|
+
return /* @__PURE__ */ jsx("tr", { className: cn(!sticky && "border-t border-subtle"), children: /* @__PURE__ */ jsx(
|
|
24
|
+
"td",
|
|
25
|
+
{
|
|
26
|
+
ref: cellRef,
|
|
27
|
+
colSpan,
|
|
28
|
+
"data-leading-row": sticky ? "sticky" : "",
|
|
29
|
+
className: cn(
|
|
30
|
+
"p-0",
|
|
31
|
+
sticky && "sticky z-[5] bg-surface before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-[1] before:h-px before:bg-[var(--border)] before:content-['']"
|
|
32
|
+
),
|
|
33
|
+
style: sticky ? { top } : void 0,
|
|
34
|
+
children
|
|
35
|
+
}
|
|
36
|
+
) });
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
DataTableLeadingRow
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=data-table-leading-row.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/data-table-leading-row.tsx"],"sourcesContent":["import { useLayoutEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * The desktop table's `leadingRow`: one full-width cell above the data rows. With\n * `sticky` (0.48, keksdose dev #603) the cell is pinned under the sticky header while the\n * rows scroll — `top` is the header's height, measured and followed (a header that wraps\n * at Large text grows). On the cell, not the `<tr>`: position:sticky on a table row is not\n * reliable across browsers, the reason the header's sticky sits on each `<th>`. Opaque\n * (`bg-surface`), so the rows pass beneath it; `z-[5]`, under the header's `z-10` and over\n * the rows' own sticky cells. The line over it is the cell's own `::before`, drawn over\n * the content (a full-width disclosure header paints its own fill) and moving with the\n * cell; a collapsed table's row border would stay behind at the row's place.\n */\nexport function DataTableLeadingRow({\n colSpan,\n sticky = false,\n children,\n}: {\n colSpan: number;\n sticky?: boolean;\n children: ReactNode;\n}) {\n const cellRef = useRef<HTMLTableCellElement>(null);\n const [top, setTop] = useState(0);\n useLayoutEffect(() => {\n if (!sticky) return;\n const head = cellRef.current?.closest(\"table\")?.tHead;\n if (!head) return;\n const measure = () => setTop(head.getBoundingClientRect().height);\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n observer.observe(head);\n return () => observer.disconnect();\n }, [sticky]);\n return (\n <tr className={cn(!sticky && \"border-t border-subtle\")}>\n <td\n ref={cellRef}\n colSpan={colSpan}\n data-leading-row={sticky ? \"sticky\" : \"\"}\n className={cn(\n \"p-0\",\n sticky &&\n \"sticky z-[5] bg-surface before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-[1] before:h-px before:bg-[var(--border)] before:content-['']\",\n )}\n style={sticky ? { top } : undefined}\n >\n {children}\n </td>\n </tr>\n );\n}\n"],"mappings":";AAuCM;AAvCN,SAAS,iBAAiB,QAAQ,gBAAgB;AAElD,SAAS,UAAU;AAaZ,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA,SAAS;AAAA,EACT;AACF,GAIG;AACD,QAAM,UAAU,OAA6B,IAAI;AACjD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,CAAC;AAChC,kBAAgB,MAAM;AACpB,QAAI,CAAC,OAAQ;AACb,UAAM,OAAO,QAAQ,SAAS,QAAQ,OAAO,GAAG;AAChD,QAAI,CAAC,KAAM;AACX,UAAM,UAAU,MAAM,OAAO,KAAK,sBAAsB,EAAE,MAAM;AAChE,YAAQ;AACR,QAAI,OAAO,mBAAmB,YAAa;AAC3C,UAAM,WAAW,IAAI,eAAe,OAAO;AAC3C,aAAS,QAAQ,IAAI;AACrB,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,MAAM,CAAC;AACX,SACE,oBAAC,QAAG,WAAW,GAAG,CAAC,UAAU,wBAAwB,GACnD;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA,oBAAkB,SAAS,WAAW;AAAA,MACtC,WAAW;AAAA,QACT;AAAA,QACA,UACE;AAAA,MACJ;AAAA,MACA,OAAO,SAAS,EAAE,IAAI,IAAI;AAAA,MAEzB;AAAA;AAAA,EACH,GACF;AAEJ;","names":[]}
|
|
@@ -88,7 +88,7 @@ function useColumnResize({
|
|
|
88
88
|
const next = {};
|
|
89
89
|
const cols = columns.filter((c) => !hiddenCols.has(c.key));
|
|
90
90
|
cols.forEach((col, idx) => {
|
|
91
|
-
if (idx === cols.length - 1) return;
|
|
91
|
+
if (idx === cols.length - 1 || col.grow) return;
|
|
92
92
|
const th = headRefs.get(col.key);
|
|
93
93
|
if (th) next[col.key] = Math.ceil(th.getBoundingClientRect().width);
|
|
94
94
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/data-table-resize.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport type { Dispatch, SetStateAction } from \"react\";\nimport { isRtl } from \"../lib/direction\";\nimport type { DataTableColumn } from \"./data-table\";\n\n// DataTable's column resize and auto-size. Moved out of data-table.tsx whole (refactor\n// audit 2026-10, MOD-3 step 2): what it needs from the table is the header cells, the\n// widths and their setter, and which columns are shown — nothing else.\n\n/** A column resize in flight. Held in a ref rather than in state: the live width is\n * written straight onto the column's cells, so a drag costs zero renders and the\n * one `setWidths` happens when the pointer comes up. */\ninterface Resize {\n key: string;\n pointerId: number;\n startX: number;\n startWidth: number;\n /** +1 when a drag to the right widens the column, -1 in a right-to-left table,\n * where the handle sits on the column's LEFT edge and widening is a drag left. */\n sign: 1 | -1;\n /** What React last rendered this column at, so a CANCELLED drag can put the header\n * back — React cannot, it never saw the inline width the drag wrote. */\n hadWidth: number | undefined;\n /** The header and the body cells under it, collected ONCE when the drag starts.\n * A `<th>` alone cannot shrink a column whose `<td>`s already carry a pinned\n * width — and nothing re-renders during a drag, so the list cannot go stale. */\n cells: HTMLElement[];\n width: number;\n /** A press with no drag in it is not a resize — the handle is also the\n * double-click target that CLEARS a width, and committing the header's current\n * size on the way past would pin every column the user ever brushed. */\n moved: boolean;\n}\n\n/**\n * The resize handles' pointer handlers, and the column panel's \"Auto-size\".\n *\n * Pointer events with capture, rather than a `mousemove`/`mouseup` pair on `window`.\n * Three defects in one eleven-line handler (audit 2026-09-22):\n *\n * * a `setWidths` per `mousemove` re-rendered every row of the table on every pixel\n * of the drag — on a 500-row table that is the whole body, sixty times a second;\n * * the two `window` listeners were added from inside an event handler, so nothing\n * removed them if the table unmounted mid-drag (the row beneath opens a route),\n * and `document.body.style` stayed at `user-select: none; cursor: col-resize` for\n * the rest of the session — the whole page unselectable, with the wrong cursor;\n * * `mousedown` is a mouse. Under a finger the handle did nothing at all.\n *\n * `setPointerCapture` answers all three: the handle itself receives every move and\n * the terminating `pointerup`/`pointercancel` wherever the pointer travels, so there\n * is nothing global to leak, and a touch drag works because a finger is a pointer.\n * The live width is written straight onto the column's own cells — the same three\n * properties the render sets, so the drag and the commit cannot disagree — and\n * `setWidths` is called once, when the drag ends.\n */\nexport function useColumnResize<T>({\n headRefs,\n widths,\n setWidths,\n columns,\n hiddenCols,\n}: {\n /** The header cells by column key, as the `<th>`s' ref callbacks fill it. */\n headRefs: Map<string, HTMLTableCellElement>;\n /** The widths React renders the columns at. */\n widths: Record<string, number>;\n setWidths: Dispatch<SetStateAction<Record<string, number>>>;\n columns: DataTableColumn<T>[];\n hiddenCols: Set<string>;\n}) {\n const resizing = useRef<Resize | null>(null);\n\n /** Every cell in one column, header first, totals row included. `data-col` rather than a cell index:\n * an expanded row holds a single `colSpan` cell, so the nth `<td>` of a row is\n * not reliably the nth column. */\n const columnCells = (key: string): HTMLElement[] => {\n const th = headRefs.get(key);\n if (!th) return [];\n const table = th.closest(\"table\");\n if (!table) return [th];\n return [\n th,\n ...Array.from(table.querySelectorAll<HTMLElement>(\"tbody [data-col], tfoot [data-col]\")).filter(\n (el) => el.dataset.col === key,\n ),\n ];\n };\n\n const applyLiveWidth = (cells: HTMLElement[], width: number | undefined) => {\n const px = width == null ? \"\" : `${width}px`;\n for (const el of cells) {\n el.style.width = px;\n el.style.minWidth = px;\n el.style.maxWidth = px;\n }\n };\n\n const releaseResizeChrome = (handle: Element, pointerId: number) => {\n document.body.style.userSelect = \"\";\n document.body.style.cursor = \"\";\n // Optional calls: jsdom implements neither, and the handlers sit on the handle\n // itself, so a run without capture behaves the same for anything that stays\n // inside the element.\n if (handle.hasPointerCapture?.(pointerId)) handle.releasePointerCapture?.(pointerId);\n };\n\n const startResize = (e: React.PointerEvent<HTMLElement>, key: string) => {\n // Secondary buttons open menus; only a primary press starts a drag.\n if (e.button !== 0) return;\n e.preventDefault();\n e.stopPropagation();\n const th = headRefs.get(key);\n const startWidth = th?.getBoundingClientRect().width ?? 100;\n resizing.current = {\n key,\n pointerId: e.pointerId,\n startX: e.clientX,\n startWidth,\n sign: isRtl(e.currentTarget) ? -1 : 1,\n hadWidth: widths[key],\n cells: columnCells(key),\n width: startWidth,\n moved: false,\n };\n e.currentTarget.setPointerCapture?.(e.pointerId);\n document.body.style.userSelect = \"none\";\n document.body.style.cursor = \"col-resize\";\n };\n\n const moveResize = (e: React.PointerEvent<HTMLElement>) => {\n const drag = resizing.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n drag.width = Math.max(40, Math.round(drag.startWidth + drag.sign * (e.clientX - drag.startX)));\n drag.moved = true;\n applyLiveWidth(drag.cells, drag.width);\n };\n\n const finishResize = (e: React.PointerEvent<HTMLElement>, commit: boolean) => {\n const drag = resizing.current;\n // Before the early return: the page gets its selection and its cursor back even\n // for a pointer we were not tracking.\n releaseResizeChrome(e.currentTarget, e.pointerId);\n if (!drag || drag.pointerId !== e.pointerId) return;\n resizing.current = null;\n if (commit && drag.moved) {\n setWidths((w) => ({ ...w, [drag.key]: drag.width }));\n return;\n }\n // Cancelled — the browser took the pointer for a system gesture — or the press\n // never became a drag. Either way the column goes back to the width React last\n // rendered it at, which nothing else can do: the inline width the drag wrote is\n // invisible to React, so a re-render with an unchanged `widths` would keep it.\n applyLiveWidth(drag.cells, drag.hadWidth);\n };\n\n // The last exit path. A table that unmounts mid-drag takes the handle, and with it\n // every handler above, out of the document — this is the only place left to hand the\n // page back its text selection and its cursor.\n useEffect(\n () => () => {\n if (!resizing.current) return;\n resizing.current = null;\n document.body.style.userSelect = \"\";\n document.body.style.cursor = \"\";\n },\n [],\n );\n\n const autoSizeAll = () => {\n // Clear widths so the table reflows to natural sizes, then snapshot each\n // column's width and write it back — except for the last visible column,\n // which is left unconstrained so it absorbs any remaining horizontal space.\n setWidths({});\n requestAnimationFrame(() => {\n const next: Record<string, number> = {};\n const cols = columns.filter((c) => !hiddenCols.has(c.key));\n cols.forEach((col, idx) => {\n if (idx === cols.length - 1) return;\n const th = headRefs.get(col.key);\n if (th) next[col.key] = Math.ceil(th.getBoundingClientRect().width);\n });\n setWidths(next);\n });\n };\n\n return { startResize, moveResize, finishResize, autoSizeAll };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/data-table-resize.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport type { Dispatch, SetStateAction } from \"react\";\nimport { isRtl } from \"../lib/direction\";\nimport type { DataTableColumn } from \"./data-table\";\n\n// DataTable's column resize and auto-size. Moved out of data-table.tsx whole (refactor\n// audit 2026-10, MOD-3 step 2): what it needs from the table is the header cells, the\n// widths and their setter, and which columns are shown — nothing else.\n\n/** A column resize in flight. Held in a ref rather than in state: the live width is\n * written straight onto the column's cells, so a drag costs zero renders and the\n * one `setWidths` happens when the pointer comes up. */\ninterface Resize {\n key: string;\n pointerId: number;\n startX: number;\n startWidth: number;\n /** +1 when a drag to the right widens the column, -1 in a right-to-left table,\n * where the handle sits on the column's LEFT edge and widening is a drag left. */\n sign: 1 | -1;\n /** What React last rendered this column at, so a CANCELLED drag can put the header\n * back — React cannot, it never saw the inline width the drag wrote. */\n hadWidth: number | undefined;\n /** The header and the body cells under it, collected ONCE when the drag starts.\n * A `<th>` alone cannot shrink a column whose `<td>`s already carry a pinned\n * width — and nothing re-renders during a drag, so the list cannot go stale. */\n cells: HTMLElement[];\n width: number;\n /** A press with no drag in it is not a resize — the handle is also the\n * double-click target that CLEARS a width, and committing the header's current\n * size on the way past would pin every column the user ever brushed. */\n moved: boolean;\n}\n\n/**\n * The resize handles' pointer handlers, and the column panel's \"Auto-size\".\n *\n * Pointer events with capture, rather than a `mousemove`/`mouseup` pair on `window`.\n * Three defects in one eleven-line handler (audit 2026-09-22):\n *\n * * a `setWidths` per `mousemove` re-rendered every row of the table on every pixel\n * of the drag — on a 500-row table that is the whole body, sixty times a second;\n * * the two `window` listeners were added from inside an event handler, so nothing\n * removed them if the table unmounted mid-drag (the row beneath opens a route),\n * and `document.body.style` stayed at `user-select: none; cursor: col-resize` for\n * the rest of the session — the whole page unselectable, with the wrong cursor;\n * * `mousedown` is a mouse. Under a finger the handle did nothing at all.\n *\n * `setPointerCapture` answers all three: the handle itself receives every move and\n * the terminating `pointerup`/`pointercancel` wherever the pointer travels, so there\n * is nothing global to leak, and a touch drag works because a finger is a pointer.\n * The live width is written straight onto the column's own cells — the same three\n * properties the render sets, so the drag and the commit cannot disagree — and\n * `setWidths` is called once, when the drag ends.\n */\nexport function useColumnResize<T>({\n headRefs,\n widths,\n setWidths,\n columns,\n hiddenCols,\n}: {\n /** The header cells by column key, as the `<th>`s' ref callbacks fill it. */\n headRefs: Map<string, HTMLTableCellElement>;\n /** The widths React renders the columns at. */\n widths: Record<string, number>;\n setWidths: Dispatch<SetStateAction<Record<string, number>>>;\n columns: DataTableColumn<T>[];\n hiddenCols: Set<string>;\n}) {\n const resizing = useRef<Resize | null>(null);\n\n /** Every cell in one column, header first, totals row included. `data-col` rather than a cell index:\n * an expanded row holds a single `colSpan` cell, so the nth `<td>` of a row is\n * not reliably the nth column. */\n const columnCells = (key: string): HTMLElement[] => {\n const th = headRefs.get(key);\n if (!th) return [];\n const table = th.closest(\"table\");\n if (!table) return [th];\n return [\n th,\n ...Array.from(table.querySelectorAll<HTMLElement>(\"tbody [data-col], tfoot [data-col]\")).filter(\n (el) => el.dataset.col === key,\n ),\n ];\n };\n\n const applyLiveWidth = (cells: HTMLElement[], width: number | undefined) => {\n const px = width == null ? \"\" : `${width}px`;\n for (const el of cells) {\n el.style.width = px;\n el.style.minWidth = px;\n el.style.maxWidth = px;\n }\n };\n\n const releaseResizeChrome = (handle: Element, pointerId: number) => {\n document.body.style.userSelect = \"\";\n document.body.style.cursor = \"\";\n // Optional calls: jsdom implements neither, and the handlers sit on the handle\n // itself, so a run without capture behaves the same for anything that stays\n // inside the element.\n if (handle.hasPointerCapture?.(pointerId)) handle.releasePointerCapture?.(pointerId);\n };\n\n const startResize = (e: React.PointerEvent<HTMLElement>, key: string) => {\n // Secondary buttons open menus; only a primary press starts a drag.\n if (e.button !== 0) return;\n e.preventDefault();\n e.stopPropagation();\n const th = headRefs.get(key);\n const startWidth = th?.getBoundingClientRect().width ?? 100;\n resizing.current = {\n key,\n pointerId: e.pointerId,\n startX: e.clientX,\n startWidth,\n sign: isRtl(e.currentTarget) ? -1 : 1,\n hadWidth: widths[key],\n cells: columnCells(key),\n width: startWidth,\n moved: false,\n };\n e.currentTarget.setPointerCapture?.(e.pointerId);\n document.body.style.userSelect = \"none\";\n document.body.style.cursor = \"col-resize\";\n };\n\n const moveResize = (e: React.PointerEvent<HTMLElement>) => {\n const drag = resizing.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n drag.width = Math.max(40, Math.round(drag.startWidth + drag.sign * (e.clientX - drag.startX)));\n drag.moved = true;\n applyLiveWidth(drag.cells, drag.width);\n };\n\n const finishResize = (e: React.PointerEvent<HTMLElement>, commit: boolean) => {\n const drag = resizing.current;\n // Before the early return: the page gets its selection and its cursor back even\n // for a pointer we were not tracking.\n releaseResizeChrome(e.currentTarget, e.pointerId);\n if (!drag || drag.pointerId !== e.pointerId) return;\n resizing.current = null;\n if (commit && drag.moved) {\n setWidths((w) => ({ ...w, [drag.key]: drag.width }));\n return;\n }\n // Cancelled — the browser took the pointer for a system gesture — or the press\n // never became a drag. Either way the column goes back to the width React last\n // rendered it at, which nothing else can do: the inline width the drag wrote is\n // invisible to React, so a re-render with an unchanged `widths` would keep it.\n applyLiveWidth(drag.cells, drag.hadWidth);\n };\n\n // The last exit path. A table that unmounts mid-drag takes the handle, and with it\n // every handler above, out of the document — this is the only place left to hand the\n // page back its text selection and its cursor.\n useEffect(\n () => () => {\n if (!resizing.current) return;\n resizing.current = null;\n document.body.style.userSelect = \"\";\n document.body.style.cursor = \"\";\n },\n [],\n );\n\n const autoSizeAll = () => {\n // Clear widths so the table reflows to natural sizes, then snapshot each\n // column's width and write it back — except for the last visible column,\n // which is left unconstrained so it absorbs any remaining horizontal space.\n setWidths({});\n requestAnimationFrame(() => {\n const next: Record<string, number> = {};\n const cols = columns.filter((c) => !hiddenCols.has(c.key));\n cols.forEach((col, idx) => {\n // A `grow` column takes the rest by design: pinning it would undo that (0.48).\n if (idx === cols.length - 1 || col.grow) return;\n const th = headRefs.get(col.key);\n if (th) next[col.key] = Math.ceil(th.getBoundingClientRect().width);\n });\n setWidths(next);\n });\n };\n\n return { startResize, moveResize, finishResize, autoSizeAll };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from "react";
|
|
2
|
+
import { type DataTableGroupBy, type DataTableLoadMore } from "./data-table-groups.js";
|
|
3
|
+
export type { DataTableGroupBy, DataTableLoadMore } from "./data-table-groups.js";
|
|
2
4
|
import type { ColumnFilter, FilterValue } from "./data-table-filters.js";
|
|
3
5
|
import type { SortCycle, SortDir, SortState } from "./data-table-sort.js";
|
|
4
6
|
import type { SwipeAction } from "./swipeable-row.js";
|
|
@@ -89,6 +91,44 @@ export interface DataTableColumn<T> {
|
|
|
89
91
|
* end-aligned), its resized width, and drops out with the column when it is hidden.
|
|
90
92
|
*/
|
|
91
93
|
footer?: (rows: T[], page: T[]) => ReactNode;
|
|
94
|
+
/**
|
|
95
|
+
* This column takes the width the others leave, and its text ellipsises (0.48,
|
|
96
|
+
* keksdose live #451): `w-full max-w-0` on its cells, so in the table's auto layout it
|
|
97
|
+
* is "the rest of the row" and one long title can no longer widen the table. One per
|
|
98
|
+
* table. It has no resize handle, and a width stored for it (before it grew) is
|
|
99
|
+
* ignored: it always takes the rest. Resize the others to change it. Opt-in: without it nothing
|
|
100
|
+
* changes. It keeps at least 10rem: with {@link fitPriority} columns the table leaves
|
|
101
|
+
* those out first, and scrolls sideways once none is left. Every other cell keeps one
|
|
102
|
+
* line at Normal text size (`whitespace-nowrap`; a column's own
|
|
103
|
+
* `not-large:whitespace-normal` lets its prose wrap), so a figure or a name is never
|
|
104
|
+
* broken in two to make room for it.
|
|
105
|
+
*/
|
|
106
|
+
grow?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Leave this column out while the TABLE is too narrow for it (0.48, keksdose live
|
|
109
|
+
* #451) — measured on the table's own scroller, so a sidebar, the window and Large
|
|
110
|
+
* text count alike. The lowest number goes first (ties: the later column first), and
|
|
111
|
+
* comes back last, once the scroller is as wide as the table needed with it. Columns
|
|
112
|
+
* without it never go. Desktop only: the phone's card list is not narrowed.
|
|
113
|
+
*
|
|
114
|
+
* The Columns panel counts only what is drawn ("Columns (8/10)") and marks a column
|
|
115
|
+
* left out this way "hidden for width". Ticking it there keeps it in for as long as
|
|
116
|
+
* the table is mounted (not persisted): the table makes room by leaving out the next
|
|
117
|
+
* one instead, and scrolls sideways once none is left. Unticking hides it as any column.
|
|
118
|
+
*/
|
|
119
|
+
fitPriority?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Start this column HIDDEN (0.48, keksdose dev #617) — for as long as the view saved
|
|
122
|
+
* under `storageKey` holds no entry for it: on a first visit, on a device whose saved
|
|
123
|
+
* view predates the column (or the flag), and for a column the app adds later. The
|
|
124
|
+
* user's own choice in the Columns panel always wins: ticking it is remembered
|
|
125
|
+
* (`shown` in the saved view), unticking it again stores it as any hidden column. The
|
|
126
|
+
* default itself is never written into the saved view, so dropping the flag brings the
|
|
127
|
+
* column back on every device where nobody chose. In the panel it is simply unticked —
|
|
128
|
+
* not "hidden for width", which `fitPriority` alone says. Ignored, like every hidden
|
|
129
|
+
* column, with `columnSettings={false}`: nothing could bring it back.
|
|
130
|
+
*/
|
|
131
|
+
defaultHidden?: boolean;
|
|
92
132
|
}
|
|
93
133
|
/** A `data-*` attribute, typed so `{ "data-private": "" }` needs no cast. */
|
|
94
134
|
type DataAttributes = {
|
|
@@ -392,7 +432,9 @@ export interface DataTableProps<T> {
|
|
|
392
432
|
* contiguous (pair with a matching default sort). `mobileGroupLabel` formats
|
|
393
433
|
* the header. Desktop is unaffected. Feedback #317.
|
|
394
434
|
*/
|
|
395
|
-
mobileGroupBy?: (row: T) => string
|
|
435
|
+
mobileGroupBy?: ((row: T) => string) | DataTableGroupBy<T>;
|
|
436
|
+
/** The section header's words for `mobileGroupBy`'s key. Wins over a level's own
|
|
437
|
+
* `label` when `mobileGroupBy` is a {@link DataTableGroupBy}. */
|
|
396
438
|
mobileGroupLabel?: (key: string) => ReactNode;
|
|
397
439
|
/**
|
|
398
440
|
* Fully replace the default mobile card body (bold primary + labelled
|
|
@@ -464,6 +506,40 @@ export interface DataTableProps<T> {
|
|
|
464
506
|
* "hide upcoming/scheduled" toggle).
|
|
465
507
|
*/
|
|
466
508
|
leadingRow?: ReactNode;
|
|
509
|
+
/**
|
|
510
|
+
* 0.48 (keksdose dev #603: "it should rather be sticky, so the user can show hide them
|
|
511
|
+
* everywhere on click"): pin the {@link leadingRow} under the sticky header while the
|
|
512
|
+
* rows scroll beneath both — its `top` is the header's measured height. Desktop only,
|
|
513
|
+
* as `leadingRow` is; it sticks within the table's own scroller (a bounded body:
|
|
514
|
+
* `fillHeight` or `maxBodyHeight`). Default: it scrolls with the rows.
|
|
515
|
+
*/
|
|
516
|
+
leadingRowSticky?: boolean;
|
|
517
|
+
/**
|
|
518
|
+
* 0.48 (keksdose dev #605): group the desktop table's rows under full-width bands, one
|
|
519
|
+
* level or two (period → day). Each band is a row header spanning the visible columns
|
|
520
|
+
* (selection column included, hidden and fit-hidden columns not) with a label and an
|
|
521
|
+
* end slot (counts, totals), and opens its own `<tbody>`, so a screen reader announces
|
|
522
|
+
* it as the reader enters the group; the period band opens the first day's. Rows must
|
|
523
|
+
* already be ordered so equal keys are contiguous (as for `mobileGroupBy`); grouping
|
|
524
|
+
* runs over the rows on screen, page by page.
|
|
525
|
+
*
|
|
526
|
+
* The bands are sticky: the period under the header (and under a sticky
|
|
527
|
+
* {@link leadingRow}), the day under the period — measured offsets, inside the table's
|
|
528
|
+
* own scroller (`fillHeight` or `maxBodyHeight`). The `leadingRow` stays above the first
|
|
529
|
+
* group. On a phone pass the same level to `mobileGroupBy`:
|
|
530
|
+
* `groupBy={[period, day]} mobileGroupBy={period}`. See {@link DataTableGroupBy}.
|
|
531
|
+
*/
|
|
532
|
+
groupBy?: DataTableGroupBy<T> | readonly [DataTableGroupBy<T>, DataTableGroupBy<T>?];
|
|
533
|
+
/**
|
|
534
|
+
* 0.48 (keksdose dev #605): a "load more" footer in place of the pager. The table
|
|
535
|
+
* renders every row it is given (as `paginated={false}` does — pass that too, to say
|
|
536
|
+
* so), and the footer asks the owner for the next slice: as it scrolls into view (an
|
|
537
|
+
* IntersectionObserver, guarded against double calls while one is in flight) and with
|
|
538
|
+
* a button the keyboard reaches, named by the app ("Load August 2026"). With
|
|
539
|
+
* `serverPagination` it replaces that pager as well. Both layouts: on a phone it is the
|
|
540
|
+
* card list's last item. See {@link DataTableLoadMore}.
|
|
541
|
+
*/
|
|
542
|
+
loadMore?: DataTableLoadMore;
|
|
467
543
|
/**
|
|
468
544
|
* Row height. `"compact"` is lenkbank's in-card tables: five `text-xs` tables with
|
|
469
545
|
* `px-2 py-1` cells sitting inside cards, which at the default padding came out at
|
|
@@ -693,4 +769,4 @@ export interface DataTableSortLabels {
|
|
|
693
769
|
* (DataTableLabels extends DataTableSortLabels; one source for the English). */
|
|
694
770
|
export declare const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels;
|
|
695
771
|
import { type TableUrlSync } from "./use-table-state.js";
|
|
696
|
-
export declare function DataTable<T>({ rows, columns: columnsProp, rowKey, defaultPageSize, onRowClick, rowHref, expandedRow, isExpanded, rowClassName, rowAttributes, paginated, empty, storageKey, urlSync, fillHeight, maxBodyHeight, serverPagination, filters: filtersProp, onFiltersChange, sorts: sortsProp, onSortsChange, pageSize: pageSizeProp, onPageSizeChange, selection, labels: labelsProp, locale: localeProp, storageKeyPrefix: storageKeyPrefixProp, mobileExpandAsDialog, mobileDetailsInRow, rowName, mobileDialogBackCloses, mobileGroupBy, mobileGroupLabel, mobileCard, mobileSort, mobileSwipeActions, desktopSwipeActions, leadingRow, density, sortCycle, chrome, columnSettings: columnSettingsProp, resizable: resizableProp, multiSort: multiSortProp, frame, edgeFade, className, rowActions, rowActionsHeader, toolbar, footerLabel, footer: footerProp, }: DataTableProps<T>): import("react").JSX.Element;
|
|
772
|
+
export declare function DataTable<T>({ rows, columns: columnsProp, rowKey, defaultPageSize, onRowClick, rowHref, expandedRow, isExpanded, rowClassName, rowAttributes, paginated, empty, storageKey, urlSync, fillHeight, maxBodyHeight, serverPagination, filters: filtersProp, onFiltersChange, sorts: sortsProp, onSortsChange, pageSize: pageSizeProp, onPageSizeChange, selection, labels: labelsProp, locale: localeProp, storageKeyPrefix: storageKeyPrefixProp, mobileExpandAsDialog, mobileDetailsInRow, rowName, mobileDialogBackCloses, mobileGroupBy, mobileGroupLabel, mobileCard, mobileSort, mobileSwipeActions, desktopSwipeActions, leadingRow, leadingRowSticky, groupBy, loadMore, density, sortCycle, chrome, columnSettings: columnSettingsProp, resizable: resizableProp, multiSort: multiSortProp, frame, edgeFade, className, rowActions, rowActionsHeader, toolbar, footerLabel, footer: footerProp, }: DataTableProps<T>): import("react").JSX.Element;
|