mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 11:26:31 +00:00
Add option to sticky header row in tables (#4134)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add option to sticky header row in tables
|
||||
@@ -349,7 +349,7 @@
|
||||
"react-dom": "catalog:",
|
||||
},
|
||||
"catalog": {
|
||||
"@gitbook/api": "0.172.0",
|
||||
"@gitbook/api": "0.173.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
@@ -746,7 +746,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.172.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-EoOhOt4cZpwZKPaQ0E5jb3Ea4Fvmi11at5Y/62cFy5ahyB3355jfKp0HPxe1bGsKfm421RkaM4Ico0vtVZwQ6Q=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.173.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-AUuRgSi9gCZDZ7GcDQ1o+6CGF70H1RizqydAsW/btKc2FTCYJK5ifbH6VTvsdmgHV38WIk3kTqRujczQ9uEegQ=="],
|
||||
|
||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@
|
||||
"catalog": {
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@gitbook/api": "0.172.0",
|
||||
"@gitbook/api": "0.173.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
||||
@@ -233,7 +233,7 @@ const testCases: TestsCase[] = [
|
||||
name: 'run-ai-docs.nvidia.com',
|
||||
contentBaseURL: 'https://run-ai-docs.nvidia.com',
|
||||
tests: [
|
||||
{ name: 'Home', url: '/' },
|
||||
// { name: 'Home', url: '/' } Temporarily skipped: this page is unstable in CI during Argos screenshots.
|
||||
{ name: 'OG Image', url: '/~gitbook/ogimage/h17zQIFwy3MaafVNmItO', mode: 'image' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -4,8 +4,7 @@ import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { RecordColumnValue } from './RecordColumnValue';
|
||||
import type { TableRecordKV, TableViewProps } from './Table';
|
||||
import { getColumnWidth } from './ViewGrid';
|
||||
import styles from './table.module.css';
|
||||
import { getColumnWidth } from './layout';
|
||||
import { getColumnVerticalAlignment } from './utils';
|
||||
|
||||
export function RecordRow(
|
||||
@@ -18,7 +17,15 @@ export function RecordRow(
|
||||
const { view, autoSizedColumns, fixedColumns, block, context } = props;
|
||||
|
||||
return (
|
||||
<div className={styles.row} role="row">
|
||||
<div
|
||||
className={tcls(
|
||||
'flex',
|
||||
'border-tint-subtle',
|
||||
'transition-colors',
|
||||
'hover:bg-tint-hover'
|
||||
)}
|
||||
role="row"
|
||||
>
|
||||
{view.columns.map((column) => {
|
||||
const columnWidth = getColumnWidth({
|
||||
column,
|
||||
@@ -33,7 +40,10 @@ export function RecordRow(
|
||||
<div
|
||||
key={column}
|
||||
role="cell"
|
||||
className={tcls(styles.cell)}
|
||||
className={tcls(
|
||||
'relative flex flex-1 border-r px-3 py-2 align-middle text-sm last:border-r-0',
|
||||
'border-tint-subtle'
|
||||
)}
|
||||
style={{
|
||||
width: columnWidth,
|
||||
minWidth: columnWidth || '100px',
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
'use client';
|
||||
import { useScrollListener } from '@/components/hooks/useScrollListener';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
interface StickyViewGridProps {
|
||||
className?: string;
|
||||
header: ReactNode;
|
||||
tableClassName?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function StickyViewGrid({
|
||||
className,
|
||||
header,
|
||||
tableClassName,
|
||||
children,
|
||||
}: StickyViewGridProps) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const stickyHeaderRef = useRef<HTMLDivElement>(null);
|
||||
const bodyScrollRef = useRef<HTMLDivElement>(null);
|
||||
const bodyTableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onStickyHeaderWheel = useCallback((event: WheelEvent) => {
|
||||
const bodyScrollElement = bodyScrollRef.current;
|
||||
if (!bodyScrollElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const horizontalDelta = event.deltaX || (event.shiftKey && event.deltaY ? event.deltaY : 0);
|
||||
if (horizontalDelta === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
bodyScrollElement.scrollLeft += horizontalDelta;
|
||||
event.preventDefault();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const stickyHeaderElement = stickyHeaderRef.current;
|
||||
if (!stickyHeaderElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
stickyHeaderElement.addEventListener('wheel', onStickyHeaderWheel, { passive: false });
|
||||
|
||||
return () => {
|
||||
stickyHeaderElement.removeEventListener('wheel', onStickyHeaderWheel);
|
||||
};
|
||||
}, [onStickyHeaderWheel]);
|
||||
|
||||
const syncStickyLayout = useCallback(() => {
|
||||
const rootElement = rootRef.current;
|
||||
const bodyScrollElement = bodyScrollRef.current;
|
||||
const bodyTableElement = bodyTableRef.current;
|
||||
if (!rootElement || !bodyScrollElement || !bodyTableElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
rootElement.style.setProperty(
|
||||
'--table-sticky-scroll-left',
|
||||
`${-bodyScrollElement.scrollLeft}px`
|
||||
);
|
||||
rootElement.style.setProperty(
|
||||
'--table-sticky-table-width',
|
||||
`${bodyTableElement.scrollWidth}px`
|
||||
);
|
||||
rootElement.dataset.scrollable = `${
|
||||
bodyScrollElement.scrollWidth > bodyScrollElement.clientWidth + 1
|
||||
}`;
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
syncStickyLayout();
|
||||
}, [syncStickyLayout]);
|
||||
|
||||
useScrollListener(syncStickyLayout, bodyScrollRef);
|
||||
|
||||
useEffect(() => {
|
||||
const bodyScrollElement = bodyScrollRef.current;
|
||||
const bodyTableElement = bodyTableRef.current;
|
||||
|
||||
if (!bodyScrollElement && !bodyTableElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(syncStickyLayout);
|
||||
|
||||
if (bodyScrollElement) {
|
||||
resizeObserver.observe(bodyScrollElement);
|
||||
}
|
||||
|
||||
if (bodyTableElement) {
|
||||
resizeObserver.observe(bodyTableElement);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [syncStickyLayout]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={tcls(
|
||||
'group/table relative flex w-full min-w-0 max-w-full flex-col rounded-lg border-tint-subtle',
|
||||
'data-[scrollable=true]:border'
|
||||
)}
|
||||
data-scrollable="false"
|
||||
>
|
||||
<div
|
||||
ref={stickyHeaderRef}
|
||||
className={tcls(
|
||||
'-mx-px sticky z-10 w-full min-w-0 max-w-full overflow-hidden rounded-t-[inherit] px-px',
|
||||
'[top:var(--toc-top-offset,var(--outline-top-offset,0px))]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-col',
|
||||
tableClassName ?? 'w-fit',
|
||||
'[transform:translateX(var(--table-sticky-scroll-left,0px))]'
|
||||
)}
|
||||
style={{ width: 'var(--table-sticky-table-width)' }}
|
||||
>
|
||||
{header}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={bodyScrollRef}
|
||||
className={tcls(
|
||||
'w-full min-w-0 overflow-x-auto overflow-y-hidden overscroll-x-none border-tint-subtle',
|
||||
'group-data-[scrollable=true]/table:mx-px',
|
||||
'group-data-[scrollable=true]/table:border-0',
|
||||
'group-data-[scrollable=true]/table:rounded-none'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={bodyTableRef}
|
||||
className={tcls('flex', 'flex-col', tableClassName ?? 'w-fit')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { DocumentBlockTable, DocumentTableRecord } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { isBlockOffscreen } from '../utils';
|
||||
import { StickyViewGrid } from './StickyViewGrid';
|
||||
import { ViewCards } from './ViewCards';
|
||||
import { ViewGrid } from './ViewGrid';
|
||||
import { ViewGrid, ViewGridHeader } from './ViewGrid';
|
||||
import { getViewGridLayout, hasVisibleHeader } from './layout';
|
||||
|
||||
export type TableRecordKV = [string, DocumentTableRecord];
|
||||
|
||||
@@ -15,7 +19,7 @@ export interface TableViewProps<View> extends BlockProps<DocumentBlockTable> {
|
||||
}
|
||||
|
||||
export function Table(props: BlockProps<DocumentBlockTable>) {
|
||||
const { block, ancestorBlocks, document } = props;
|
||||
const { block, ancestorBlocks, document, context, style } = props;
|
||||
const isOffscreen = isBlockOffscreen({ block, ancestorBlocks, document });
|
||||
|
||||
const records: TableRecordKV[] = Object.entries(block.data.records).sort((a, b) => {
|
||||
@@ -32,15 +36,68 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
case 'grid':
|
||||
case 'grid': {
|
||||
const gridProps = {
|
||||
...props,
|
||||
view: block.data.view,
|
||||
isOffscreen,
|
||||
records,
|
||||
};
|
||||
const { tableWidth } = getViewGridLayout({
|
||||
block,
|
||||
view: block.data.view,
|
||||
mode: context.mode,
|
||||
});
|
||||
const tableContainerClassName =
|
||||
tableWidth === 'w-full' ? 'min-w-full w-fit' : tableWidth;
|
||||
const withHeader = hasVisibleHeader(block, block.data.view);
|
||||
const withStickyHeader =
|
||||
withHeader && context.mode !== 'print' && block.data.view.stickyHeader === true;
|
||||
|
||||
if (withStickyHeader) {
|
||||
return (
|
||||
<StickyViewGrid
|
||||
className={tcls(style, 'relative mx-auto grid w-full min-w-0')}
|
||||
tableClassName={tableContainerClassName}
|
||||
header={
|
||||
<ViewGridHeader
|
||||
{...gridProps}
|
||||
className={tcls(
|
||||
'mb-0 rounded-b-none border-t border-r border-l',
|
||||
'group-data-[scrollable=false]/table:mb-1',
|
||||
'group-data-[scrollable=false]/table:rounded-b-lg',
|
||||
'group-data-[scrollable=true]/table:border-t-0',
|
||||
'group-data-[scrollable=true]/table:border-r-0',
|
||||
'group-data-[scrollable=true]/table:border-l-0'
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ViewGrid {...gridProps} />
|
||||
</StickyViewGrid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewGrid
|
||||
view={block.data.view}
|
||||
isOffscreen={isOffscreen}
|
||||
records={records}
|
||||
{...props}
|
||||
/>
|
||||
<div className={tcls(style, 'relative mx-auto grid w-full min-w-0')}>
|
||||
<div
|
||||
className={tcls(
|
||||
'w-full min-w-0 overflow-x-auto overflow-y-hidden overscroll-x-none border-tint-subtle '
|
||||
)}
|
||||
>
|
||||
<div className={tcls('flex', 'flex-col', tableContainerClassName)}>
|
||||
{withHeader ? (
|
||||
<ViewGridHeader
|
||||
{...gridProps}
|
||||
className={tableContainerClassName}
|
||||
/>
|
||||
) : null}
|
||||
<ViewGrid {...gridProps} tableClassName={tableContainerClassName} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default:
|
||||
assertNever(block.data.view);
|
||||
}
|
||||
|
||||
@@ -4,118 +4,94 @@ import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { RecordRow } from './RecordRow';
|
||||
import type { TableViewProps } from './Table';
|
||||
import styles from './table.module.css';
|
||||
import { getColumnWidth, getViewGridLayout } from './layout';
|
||||
import { getColumnAlignment } from './utils';
|
||||
|
||||
/* Columns are sized in 3 ways:
|
||||
1. Set to auto-size by default, these columns share the available width
|
||||
2. Explicitly set by the user by dragging column separator (we then turn off auto-size)
|
||||
3. Auto-size is turned off without setting a width, we then default to a fixed width of 100px
|
||||
*/
|
||||
export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
|
||||
const { block, view, records, style, context } = props;
|
||||
interface ViewGridHeaderProps extends TableViewProps<DocumentTableViewGrid> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/* Calculate how many columns are auto-sized vs fixed width */
|
||||
const columnWidths = context.mode === 'print' ? undefined : view.columnWidths;
|
||||
const autoSizedColumns = view.columns.filter((column) => !columnWidths?.[column]);
|
||||
const fixedColumns = view.columns.filter((column) => columnWidths?.[column]);
|
||||
interface ViewGridProps extends TableViewProps<DocumentTableViewGrid> {
|
||||
tableClassName?: string;
|
||||
}
|
||||
|
||||
const tableWidth = autoSizedColumns.length > 0 ? 'w-full' : 'w-fit';
|
||||
|
||||
/* Only show the header when configured and not empty */
|
||||
const withHeader =
|
||||
!view.hideHeader &&
|
||||
view.columns.some(
|
||||
(columnId) => (block.data.definition[columnId]?.title.trim().length ?? 0) > 0
|
||||
);
|
||||
export function ViewGridHeader(props: ViewGridHeaderProps) {
|
||||
const { block, view, context, className } = props;
|
||||
const { tableWidth, columnWidths, autoSizedColumns, fixedColumns } = getViewGridLayout({
|
||||
block,
|
||||
view,
|
||||
mode: context.mode,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={tcls(style, styles.tableWrapper)}>
|
||||
{/* Table */}
|
||||
<div role="table" className={tcls('flex', 'flex-col')}>
|
||||
{/* Header */}
|
||||
{withHeader && (
|
||||
<div
|
||||
role="rowgroup"
|
||||
className={tcls(
|
||||
tableWidth,
|
||||
styles.rowGroup,
|
||||
'straight-corners:rounded-none',
|
||||
'circular-corners:rounded-xl'
|
||||
)}
|
||||
>
|
||||
<div role="row" className={tcls('flex', 'w-full')}>
|
||||
{view.columns.map((column) => {
|
||||
const definition = block.data.definition[column]!;
|
||||
return (
|
||||
<div
|
||||
key={column}
|
||||
role="columnheader"
|
||||
className={tcls(
|
||||
styles.columnHeader,
|
||||
getColumnAlignment(definition)
|
||||
)}
|
||||
style={{
|
||||
width: getColumnWidth({
|
||||
column,
|
||||
columnWidths,
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
}),
|
||||
minWidth: columnWidths?.[column] || '100px',
|
||||
}}
|
||||
title={definition.title}
|
||||
>
|
||||
{definition.title}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
role="rowgroup"
|
||||
className={tcls(
|
||||
tableWidth,
|
||||
'mb-1 flex flex-col rounded-lg border border-tint-subtle bg-tint',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div role="row" className={tcls('flex', 'w-full')}>
|
||||
{view.columns.map((column) => {
|
||||
const definition = block.data.definition[column];
|
||||
if (!definition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={column}
|
||||
role="columnheader"
|
||||
className={tcls(
|
||||
'px-3 py-2 font-medium text-sm text-tint-strong',
|
||||
getColumnAlignment(definition)
|
||||
)}
|
||||
style={{
|
||||
width: getColumnWidth({
|
||||
column,
|
||||
columnWidths,
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
}),
|
||||
minWidth: columnWidths?.[column] || '100px',
|
||||
}}
|
||||
title={definition.title}
|
||||
>
|
||||
{definition.title}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
role="rowgroup"
|
||||
className={tcls('flex', 'flex-col', tableWidth, '[&>*+*]:border-t')}
|
||||
>
|
||||
{records.map((record) => (
|
||||
<RecordRow
|
||||
key={record[0]}
|
||||
record={record}
|
||||
autoSizedColumns={autoSizedColumns}
|
||||
fixedColumns={fixedColumns}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const getColumnWidth = ({
|
||||
column,
|
||||
columnWidths,
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
}: {
|
||||
column: string;
|
||||
columnWidths: Record<string, number> | undefined;
|
||||
autoSizedColumns: string[];
|
||||
fixedColumns: string[];
|
||||
}) => {
|
||||
const columnWidth = columnWidths?.[column];
|
||||
export function ViewGrid(props: ViewGridProps) {
|
||||
const { block, view, records, context, tableClassName } = props;
|
||||
const { tableWidth, autoSizedColumns, fixedColumns } = getViewGridLayout({
|
||||
block,
|
||||
view,
|
||||
mode: context.mode,
|
||||
});
|
||||
|
||||
/* Column was explicitly set by user or user turned off auto-sizing (in that case, columnWidth should've also been set to 100px) */
|
||||
if (columnWidth) return `${columnWidth}px`;
|
||||
const body = (
|
||||
<div role="rowgroup" className={tcls('flex', 'flex-col', tableWidth, '[&>*+*]:border-t')}>
|
||||
{records.map((record) => (
|
||||
<RecordRow
|
||||
key={record[0]}
|
||||
record={record}
|
||||
autoSizedColumns={autoSizedColumns}
|
||||
fixedColumns={fixedColumns}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
/* Fallback minimum width for columns, so the columns don't become unreadable from being too narrow and instead table will become scrollable. */
|
||||
const minAutoColumnWidth = '100px';
|
||||
|
||||
const totalFixedWidth = fixedColumns.reduce((sum, col) => {
|
||||
return sum + (columnWidths?.[col] || 0);
|
||||
}, 0);
|
||||
|
||||
/* Column should use auto-sizing, which means it grows to fill available space */
|
||||
const availableWidth = `calc((100% - ${totalFixedWidth}px) / ${autoSizedColumns.length})`;
|
||||
return `clamp(${minAutoColumnWidth}, ${availableWidth}, 100%)`;
|
||||
};
|
||||
return (
|
||||
<div role="table" className={tcls('flex', 'flex-col', tableClassName ?? 'w-fit')}>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { DocumentBlockTable, DocumentTableViewGrid } from '@gitbook/api';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
|
||||
export function hasVisibleHeader(block: DocumentBlockTable, view: DocumentTableViewGrid): boolean {
|
||||
return (
|
||||
!view.hideHeader &&
|
||||
view.columns.some(
|
||||
(columnId) => (block.data.definition[columnId]?.title.trim().length ?? 0) > 0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/* Columns are sized in 3 ways:
|
||||
1. Set to auto-size by default, these columns share the available width
|
||||
2. Explicitly set by the user by dragging column separator (we then turn off auto-size)
|
||||
3. Auto-size is turned off without setting a width, we then default to a fixed width of 100px
|
||||
*/
|
||||
export function getViewGridLayout({
|
||||
block,
|
||||
view,
|
||||
mode,
|
||||
}: {
|
||||
block: DocumentBlockTable;
|
||||
view: DocumentTableViewGrid;
|
||||
mode: BlockProps<DocumentBlockTable>['context']['mode'];
|
||||
}) {
|
||||
const columnWidths = mode === 'print' ? undefined : view.columnWidths;
|
||||
const autoSizedColumns = view.columns.filter((column) => !columnWidths?.[column]);
|
||||
const fixedColumns = view.columns.filter((column) => columnWidths?.[column]);
|
||||
const tableWidth = autoSizedColumns.length > 0 ? 'w-full' : 'w-fit';
|
||||
|
||||
return {
|
||||
columnWidths,
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
tableWidth,
|
||||
withHeader: hasVisibleHeader(block, view),
|
||||
};
|
||||
}
|
||||
|
||||
export const getColumnWidth = ({
|
||||
column,
|
||||
columnWidths,
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
}: {
|
||||
column: string;
|
||||
columnWidths: Record<string, number> | undefined;
|
||||
autoSizedColumns: string[];
|
||||
fixedColumns: string[];
|
||||
}) => {
|
||||
const columnWidth = columnWidths?.[column];
|
||||
|
||||
/* Column was explicitly set by user or user turned off auto-sizing (in that case, columnWidth should've also been set to 100px) */
|
||||
if (columnWidth) return `${columnWidth}px`;
|
||||
|
||||
/* Fallback minimum width for columns, so the columns don't become unreadable from being too narrow and instead table will become scrollable. */
|
||||
const minAutoColumnWidth = '100px';
|
||||
|
||||
const totalFixedWidth = fixedColumns.reduce((sum, col) => {
|
||||
return sum + (columnWidths?.[col] || 0);
|
||||
}, 0);
|
||||
|
||||
/* Column should use auto-sizing, which means it grows to fill available space */
|
||||
const availableWidth = `calc((100% - ${totalFixedWidth}px) / ${autoSizedColumns.length})`;
|
||||
return `clamp(${minAutoColumnWidth}, ${availableWidth}, 100%)`;
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
@reference "../../RootLayout/globals.css";
|
||||
|
||||
/* Detect whether a scrollbar exists on the table */
|
||||
@keyframes detect-scroll {
|
||||
from,
|
||||
to {
|
||||
--can-scroll: ;
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply styles to the Table if scrollbar exists */
|
||||
.tableWrapper {
|
||||
animation: detect-scroll linear;
|
||||
animation-timeline: scroll(self x);
|
||||
|
||||
--border-radius-if-can-scroll: var(--can-scroll) 0.375rem;
|
||||
--border-radius-if-cant-scroll: 0;
|
||||
border-radius: var(--border-radius-if-can-scroll, var(--border-radius-if-cant-scroll));
|
||||
|
||||
--border-width-if-can-scroll: var(--can-scroll) 1px;
|
||||
--border-width-if-cant-scroll: 0;
|
||||
border-width: var(--border-width-if-can-scroll, var(--border-width-if-cant-scroll));
|
||||
|
||||
@apply relative grid w-full overflow-x-auto overflow-y-hidden mx-auto border-tint-subtle;
|
||||
}
|
||||
|
||||
.columnHeader {
|
||||
@apply text-sm font-medium py-2 px-3 text-tint-strong;
|
||||
}
|
||||
|
||||
.row {
|
||||
@apply flex border-tint-subtle hover:bg-tint-hover transition-colors;
|
||||
}
|
||||
|
||||
.rowGroup {
|
||||
@apply flex flex-col border rounded-lg bg-tint border-tint-subtle mb-1;
|
||||
}
|
||||
|
||||
.cell {
|
||||
@apply flex-1 flex align-middle border-tint-subtle py-2 px-3 text-sm relative;
|
||||
}
|
||||
|
||||
.cell:not(:last-child) {
|
||||
@apply border-r;
|
||||
}
|
||||
Reference in New Issue
Block a user