@structuralists/scaffolding 0.0.1
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/.storybook/main.ts +9 -0
- package/.storybook/manager.ts +13 -0
- package/.storybook/preview.tsx +18 -0
- package/CLAUDE.md +30 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/bun.lock +947 -0
- package/bunfig.toml +2 -0
- package/eslint.config.mjs +106 -0
- package/index.ts +1 -0
- package/package.json +50 -0
- package/src/components/Chat/ChatComposer/ChatComposer.stories.tsx +68 -0
- package/src/components/Chat/ChatComposer/index.tsx +74 -0
- package/src/components/Chat/ChatComposer/styles.module.css +88 -0
- package/src/components/Chat/ChatComposer/types.ts +11 -0
- package/src/components/Chat/ChatMessage/ChatMessage.stories.tsx +111 -0
- package/src/components/Chat/ChatMessage/index.tsx +42 -0
- package/src/components/Chat/ChatMessage/styles.module.css +58 -0
- package/src/components/Chat/ChatMessage/types.ts +14 -0
- package/src/components/Chat/ChatRecipientsHeader/ChatRecipientsHeader.stories.tsx +145 -0
- package/src/components/Chat/ChatRecipientsHeader/index.tsx +29 -0
- package/src/components/Chat/ChatRecipientsHeader/styles.module.css +48 -0
- package/src/components/Chat/ChatRecipientsHeader/types.ts +26 -0
- package/src/components/Chat/ChatShell/ChatShell.stories.tsx +203 -0
- package/src/components/Chat/ChatShell/index.tsx +16 -0
- package/src/components/Chat/ChatShell/styles.module.css +27 -0
- package/src/components/Chat/ChatShell/types.ts +7 -0
- package/src/components/Chat/PillCombobox/PillCombobox.stories.tsx +59 -0
- package/src/components/Chat/PillCombobox/index.tsx +17 -0
- package/src/components/Chat/PillCombobox/styles.module.css +29 -0
- package/src/components/Chat/PillCombobox/types.ts +28 -0
- package/src/components/Chat/PillComboboxCore/Core.tsx +235 -0
- package/src/components/Chat/PillComboboxCore/styles.module.css +79 -0
- package/src/components/Chat/index.ts +12 -0
- package/src/components/Content/Badge/Badge.stories.tsx +31 -0
- package/src/components/Content/Badge/index.tsx +22 -0
- package/src/components/Content/Badge/styles.module.css +25 -0
- package/src/components/Content/Badge/types.ts +7 -0
- package/src/components/Content/Card/Card.stories.tsx +24 -0
- package/src/components/Content/Card/index.tsx +21 -0
- package/src/components/Content/Card/styles.module.css +13 -0
- package/src/components/Content/Card/types.ts +8 -0
- package/src/components/Content/EditableMarkdown/EditableMarkdown.stories.tsx +58 -0
- package/src/components/Content/EditableMarkdown/index.tsx +140 -0
- package/src/components/Content/EditableMarkdown/styles.module.css +221 -0
- package/src/components/Content/EditableMarkdown/types.ts +11 -0
- package/src/components/Content/Heading/Heading.stories.tsx +26 -0
- package/src/components/Content/Heading/index.tsx +20 -0
- package/src/components/Content/Heading/styles.module.css +19 -0
- package/src/components/Content/Heading/types.ts +8 -0
- package/src/components/Content/Link/Link.stories.tsx +21 -0
- package/src/components/Content/Link/index.tsx +19 -0
- package/src/components/Content/Link/styles.module.css +11 -0
- package/src/components/Content/Link/types.ts +8 -0
- package/src/components/Content/List/List.stories.tsx +62 -0
- package/src/components/Content/List/index.tsx +26 -0
- package/src/components/Content/List/styles.module.css +41 -0
- package/src/components/Content/List/types.ts +33 -0
- package/src/components/Content/LoadingContainer/LoadingContainer.stories.tsx +105 -0
- package/src/components/Content/LoadingContainer/index.tsx +36 -0
- package/src/components/Content/LoadingContainer/styles.module.css +54 -0
- package/src/components/Content/LoadingContainer/types.ts +8 -0
- package/src/components/Content/Markdown/Markdown.stories.tsx +39 -0
- package/src/components/Content/Markdown/index.tsx +28 -0
- package/src/components/Content/Markdown/styles.module.css +79 -0
- package/src/components/Content/Markdown/types.ts +8 -0
- package/src/components/Content/Menu/Menu.stories.tsx +186 -0
- package/src/components/Content/Menu/index.tsx +259 -0
- package/src/components/Content/Menu/styles.module.css +103 -0
- package/src/components/Content/Menu/types.ts +25 -0
- package/src/components/Content/Text/Text.stories.tsx +36 -0
- package/src/components/Content/Text/index.tsx +35 -0
- package/src/components/Content/Text/styles.module.css +30 -0
- package/src/components/Content/Text/types.ts +11 -0
- package/src/components/Forms/Button/Button.stories.tsx +40 -0
- package/src/components/Forms/Button/index.tsx +43 -0
- package/src/components/Forms/Button/styles.module.css +67 -0
- package/src/components/Forms/Button/types.ts +16 -0
- package/src/components/Forms/ColorInput/index.tsx +22 -0
- package/src/components/Forms/ColorInput/styles.module.css +19 -0
- package/src/components/Forms/ColorInput/types.ts +12 -0
- package/src/components/Forms/Field/Field.stories.tsx +35 -0
- package/src/components/Forms/Field/index.tsx +17 -0
- package/src/components/Forms/Field/styles.module.css +21 -0
- package/src/components/Forms/Field/types.ts +9 -0
- package/src/components/Forms/IconButton/IconButton.stories.tsx +91 -0
- package/src/components/Forms/IconButton/index.tsx +55 -0
- package/src/components/Forms/IconButton/styles.module.css +61 -0
- package/src/components/Forms/IconButton/types.ts +23 -0
- package/src/components/Forms/Input/Input.stories.tsx +22 -0
- package/src/components/Forms/Input/index.tsx +42 -0
- package/src/components/Forms/Input/styles.module.css +30 -0
- package/src/components/Forms/Input/types.ts +18 -0
- package/src/components/Forms/SearchInput/index.tsx +41 -0
- package/src/components/Forms/SearchInput/styles.module.css +30 -0
- package/src/components/Forms/SearchInput/types.ts +17 -0
- package/src/components/Forms/Select/MultiSelect/MultiSelect.stories.tsx +116 -0
- package/src/components/Forms/Select/MultiSelect/index.tsx +74 -0
- package/src/components/Forms/Select/MultiSelect/types.ts +15 -0
- package/src/components/Forms/Select/SingleSelect/SingleSelect.stories.tsx +174 -0
- package/src/components/Forms/Select/SingleSelect/index.tsx +62 -0
- package/src/components/Forms/Select/SingleSelect/types.ts +12 -0
- package/src/components/Forms/Select/index.ts +4 -0
- package/src/components/Forms/Select/internal/OptionList.tsx +124 -0
- package/src/components/Forms/Select/internal/SelectTrigger.tsx +60 -0
- package/src/components/Forms/Select/internal/styles.module.css +122 -0
- package/src/components/Forms/Textarea/Textarea.stories.tsx +25 -0
- package/src/components/Forms/Textarea/index.tsx +48 -0
- package/src/components/Forms/Textarea/styles.module.css +34 -0
- package/src/components/Forms/Textarea/types.ts +24 -0
- package/src/components/Json/Json/Json.stories.tsx +33 -0
- package/src/components/Json/Json/index.tsx +38 -0
- package/src/components/Json/Json/types.ts +21 -0
- package/src/components/Json/JsonTable/JsonLeafNode.tsx +31 -0
- package/src/components/Json/JsonTable/JsonTable.stories.tsx +52 -0
- package/src/components/Json/JsonTable/index.tsx +33 -0
- package/src/components/Json/JsonTable/types.ts +13 -0
- package/src/components/Json/JsonTable/utils.ts +6 -0
- package/src/components/Layout/Bar/Bar.stories.tsx +100 -0
- package/src/components/Layout/Bar/index.tsx +17 -0
- package/src/components/Layout/Bar/styles.module.css +34 -0
- package/src/components/Layout/Bar/types.ts +10 -0
- package/src/components/Layout/Debug/Debug.stories.tsx +86 -0
- package/src/components/Layout/Debug/index.tsx +41 -0
- package/src/components/Layout/Debug/styles.module.css +13 -0
- package/src/components/Layout/Debug/types.ts +12 -0
- package/src/components/Layout/Divider/Divider.stories.tsx +22 -0
- package/src/components/Layout/Divider/index.tsx +3 -0
- package/src/components/Layout/Divider/styles.module.css +6 -0
- package/src/components/Layout/Grid/Grid.stories.tsx +28 -0
- package/src/components/Layout/Grid/index.tsx +29 -0
- package/src/components/Layout/Grid/styles.module.css +12 -0
- package/src/components/Layout/Grid/types.ts +9 -0
- package/src/components/Layout/Panels/Panels.stories.tsx +287 -0
- package/src/components/Layout/Panels/index.tsx +119 -0
- package/src/components/Layout/Panels/styles.module.css +103 -0
- package/src/components/Layout/Panels/types.ts +36 -0
- package/src/components/Layout/Stack/Stack.stories.tsx +45 -0
- package/src/components/Layout/Stack/index.tsx +73 -0
- package/src/components/Layout/Stack/styles.module.css +41 -0
- package/src/components/Layout/Stack/types.ts +17 -0
- package/src/components/Modals/ConfirmModal/ConfirmModal.stories.tsx +73 -0
- package/src/components/Modals/ConfirmModal/index.tsx +72 -0
- package/src/components/Modals/ConfirmModal/styles.module.css +62 -0
- package/src/components/Modals/ConfirmModal/types.ts +14 -0
- package/src/components/Modals/LargeModal/LargeModal.stories.tsx +75 -0
- package/src/components/Modals/LargeModal/index.tsx +9 -0
- package/src/components/Modals/LargeModal/styles.module.css +6 -0
- package/src/components/Modals/LargeModal/types.ts +18 -0
- package/src/components/Modals/MediumModal/MediumModal.stories.tsx +121 -0
- package/src/components/Modals/MediumModal/MediumModal.test.tsx +48 -0
- package/src/components/Modals/MediumModal/index.tsx +9 -0
- package/src/components/Modals/MediumModal/styles.module.css +5 -0
- package/src/components/Modals/MediumModal/types.ts +18 -0
- package/src/components/Modals/index.ts +3 -0
- package/src/components/Modals/internal/ModalBody.tsx +21 -0
- package/src/components/Modals/internal/ModalFooter.tsx +12 -0
- package/src/components/Modals/internal/ModalHeader.tsx +27 -0
- package/src/components/Modals/internal/ModalShell.tsx +112 -0
- package/src/components/Modals/internal/styles.module.css +141 -0
- package/src/components/Navigation/TabBar/TabBar.stories.tsx +59 -0
- package/src/components/Navigation/TabBar/index.tsx +25 -0
- package/src/components/Navigation/TabBar/styles.module.css +32 -0
- package/src/components/Navigation/TabBar/types.ts +22 -0
- package/src/components/Navigation/VerticalNav/VerticalNav.stories.tsx +41 -0
- package/src/components/Navigation/VerticalNav/index.tsx +25 -0
- package/src/components/Navigation/VerticalNav/styles.module.css +28 -0
- package/src/components/Navigation/VerticalNav/types.ts +19 -0
- package/src/components/Overlays/Popover/Popover.stories.tsx +154 -0
- package/src/components/Overlays/Popover/index.tsx +175 -0
- package/src/components/Overlays/Popover/styles.module.css +59 -0
- package/src/components/Overlays/Popover/types.ts +34 -0
- package/src/components/Overlays/Tooltip/Tooltip.stories.tsx +41 -0
- package/src/components/Overlays/Tooltip/index.tsx +115 -0
- package/src/components/Overlays/Tooltip/styles.module.css +25 -0
- package/src/components/Overlays/Tooltip/types.ts +15 -0
- package/src/components/Primitives/EmptyValue/EmptyValue.stories.tsx +18 -0
- package/src/components/Primitives/EmptyValue/index.tsx +3 -0
- package/src/components/Primitives/EmptyValue/styles.module.css +3 -0
- package/src/components/Primitives/LinedStack/LinedStack.stories.tsx +101 -0
- package/src/components/Primitives/LinedStack/index.tsx +41 -0
- package/src/components/Primitives/LinedStack/styles.module.css +27 -0
- package/src/components/Primitives/LinedStack/types.ts +49 -0
- package/src/components/Primitives/LongText/LongText.stories.tsx +72 -0
- package/src/components/Primitives/LongText/index.tsx +67 -0
- package/src/components/Primitives/LongText/styles.module.css +30 -0
- package/src/components/Primitives/LongText/types.ts +4 -0
- package/src/components/Primitives/Num/Num.stories.tsx +51 -0
- package/src/components/Primitives/Num/index.tsx +37 -0
- package/src/components/Primitives/Num/types.ts +19 -0
- package/src/components/Primitives/Percent/Percent.stories.tsx +48 -0
- package/src/components/Primitives/Percent/index.tsx +15 -0
- package/src/components/Primitives/Percent/types.ts +10 -0
- package/src/components/Primitives/RelativeTime/RelativeTime.stories.tsx +57 -0
- package/src/components/Primitives/RelativeTime/index.tsx +31 -0
- package/src/components/Primitives/RelativeTime/types.ts +3 -0
- package/src/components/Tables/BigTable/BigTable.stories.tsx +367 -0
- package/src/components/Tables/BigTable/CLAUDE.md +118 -0
- package/src/components/Tables/BigTable/columnDefs.tsx +208 -0
- package/src/components/Tables/BigTable/index.tsx +104 -0
- package/src/components/Tables/BigTable/styles.module.css +83 -0
- package/src/components/Tables/BigTable/types.ts +20 -0
- package/src/components/Tables/QuickTable/CLAUDE.md +118 -0
- package/src/components/Tables/QuickTable/QuickTable.stories.tsx +121 -0
- package/src/components/Tables/QuickTable/index.tsx +86 -0
- package/src/components/Tables/QuickTable/internal.tsx +48 -0
- package/src/components/Tables/QuickTable/styles.module.css +65 -0
- package/src/components/Tables/QuickTable/types.ts +40 -0
- package/src/env.d.ts +4 -0
- package/src/index.ts +87 -0
- package/src/storybook/CLAUDE.md +35 -0
- package/src/storybook/Composition.stories.tsx +269 -0
- package/src/storybook/Lorem/index.tsx +54 -0
- package/src/storybook/Placeholder/index.tsx +27 -0
- package/src/storybook/Placeholder/styles.module.css +20 -0
- package/src/storybook/Repeat/index.tsx +23 -0
- package/src/storybook/Toggle/index.tsx +29 -0
- package/src/storybook/_StoryUtils.stories.tsx +58 -0
- package/src/storybook/index.ts +4 -0
- package/src/tokens.ts +31 -0
- package/src/utils.test.ts +24 -0
- package/src/utils.ts +2 -0
- package/test-setup.ts +3 -0
- package/tokens.css +323 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import {
|
|
4
|
+
BigTable,
|
|
5
|
+
badgeColumn,
|
|
6
|
+
booleanColumn,
|
|
7
|
+
currencyColumn,
|
|
8
|
+
dateColumn,
|
|
9
|
+
idColumn,
|
|
10
|
+
numberColumn,
|
|
11
|
+
relativeDateColumn,
|
|
12
|
+
textColumn,
|
|
13
|
+
} from './index';
|
|
14
|
+
import type { ColumnDef } from './types';
|
|
15
|
+
import { Text } from '../../Content/Text';
|
|
16
|
+
import { LinedStack } from '../../Primitives/LinedStack';
|
|
17
|
+
import { LongText } from '../../Primitives/LongText';
|
|
18
|
+
import { Panels } from '../../Layout/Panels';
|
|
19
|
+
import { Bar } from '../../Layout/Bar';
|
|
20
|
+
import { Button } from '../../Forms/Button';
|
|
21
|
+
import { Heading } from '../../Content/Heading';
|
|
22
|
+
import { Stack } from '../../Layout/Stack';
|
|
23
|
+
|
|
24
|
+
const meta: Meta<typeof BigTable> = {
|
|
25
|
+
title: 'Tables/BigTable',
|
|
26
|
+
component: BigTable,
|
|
27
|
+
parameters: { layout: 'fullscreen' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default meta;
|
|
31
|
+
type Story = StoryObj<typeof BigTable>;
|
|
32
|
+
|
|
33
|
+
type User = {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
email: string;
|
|
37
|
+
role: 'engineer' | 'designer' | 'pm' | 'sales';
|
|
38
|
+
team: string;
|
|
39
|
+
amount: number;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
lastSeenAt: string;
|
|
42
|
+
isActive: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ROLES: User['role'][] = ['engineer', 'designer', 'pm', 'sales'];
|
|
46
|
+
const TEAMS = ['Platform', 'Growth', 'Infra', 'Brand', 'Revenue', 'Search'];
|
|
47
|
+
const FIRST = ['Alice', 'Bob', 'Charlie', 'Dana', 'Evan', 'Faye', 'Gus', 'Hana'];
|
|
48
|
+
const LAST = ['Smith', 'Jones', 'Park', 'Khan', 'Reyes', 'Fox', 'Wells'];
|
|
49
|
+
|
|
50
|
+
const makeUsers = (n: number): User[] => {
|
|
51
|
+
const users: User[] = [];
|
|
52
|
+
for (let i = 0; i < n; i++) {
|
|
53
|
+
const first = FIRST[i % FIRST.length];
|
|
54
|
+
const last = LAST[(i * 3) % LAST.length];
|
|
55
|
+
const created = Date.now() - (i + 1) * 86_400_000 * 7;
|
|
56
|
+
const seen = Date.now() - (i % 9) * 3_600_000;
|
|
57
|
+
users.push({
|
|
58
|
+
id: `usr_${1000 + i}`,
|
|
59
|
+
name: `${first} ${last}`,
|
|
60
|
+
email: `${first.toLowerCase()}.${last.toLowerCase()}@example.com`,
|
|
61
|
+
role: ROLES[i % ROLES.length],
|
|
62
|
+
team: TEAMS[i % TEAMS.length],
|
|
63
|
+
amount: (i + 1) * 1234,
|
|
64
|
+
createdAt: new Date(created).toISOString(),
|
|
65
|
+
lastSeenAt: new Date(seen).toISOString(),
|
|
66
|
+
isActive: i % 5 !== 0,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return users;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const fillStyle = { height: '100vh' };
|
|
73
|
+
|
|
74
|
+
export const PrefabColumns: Story = {
|
|
75
|
+
name: 'Prefab columns — typical case',
|
|
76
|
+
render: () => {
|
|
77
|
+
const data = makeUsers(40);
|
|
78
|
+
const columnDefs: ColumnDef<User>[] = [
|
|
79
|
+
idColumn({ id: 'id', header: 'ID', value: (u) => u.id }),
|
|
80
|
+
textColumn({ id: 'name', header: 'Name', value: (u) => u.name }),
|
|
81
|
+
textColumn({ id: 'email', header: 'Email', value: (u) => u.email, minWidth: 220 }),
|
|
82
|
+
badgeColumn({ id: 'role', header: 'Role', value: (u) => u.role }),
|
|
83
|
+
textColumn({ id: 'team', header: 'Team', value: (u) => u.team }),
|
|
84
|
+
currencyColumn({ id: 'amount', header: 'Amount', value: (u) => u.amount }),
|
|
85
|
+
booleanColumn({ id: 'active', header: 'Active', value: (u) => u.isActive }),
|
|
86
|
+
relativeDateColumn({ id: 'seen', header: 'Last Seen', value: (u) => u.lastSeenAt }),
|
|
87
|
+
dateColumn({ id: 'created', header: 'Created', value: (u) => u.createdAt }),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div style={fillStyle}>
|
|
92
|
+
<BigTable data={data} columnDefs={columnDefs} getRowKey={(u) => u.id} />
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const StickyFirstColumn: Story = {
|
|
99
|
+
name: 'Sticky first column + horizontal scroll',
|
|
100
|
+
render: () => {
|
|
101
|
+
const data = makeUsers(40);
|
|
102
|
+
const columnDefs: ColumnDef<User>[] = [
|
|
103
|
+
textColumn({ id: 'name', header: 'Name', value: (u) => u.name, width: 180 }),
|
|
104
|
+
idColumn({ id: 'id', header: 'ID', value: (u) => u.id }),
|
|
105
|
+
textColumn({ id: 'email', header: 'Email', value: (u) => u.email, minWidth: 240 }),
|
|
106
|
+
badgeColumn({ id: 'role', header: 'Role', value: (u) => u.role }),
|
|
107
|
+
textColumn({ id: 'team', header: 'Team', value: (u) => u.team, minWidth: 140 }),
|
|
108
|
+
currencyColumn({ id: 'q1', header: 'Q1', value: (u) => u.amount }),
|
|
109
|
+
currencyColumn({ id: 'q2', header: 'Q2', value: (u) => u.amount * 1.1 }),
|
|
110
|
+
currencyColumn({ id: 'q3', header: 'Q3', value: (u) => u.amount * 0.95 }),
|
|
111
|
+
currencyColumn({ id: 'q4', header: 'Q4', value: (u) => u.amount * 1.2 }),
|
|
112
|
+
numberColumn({ id: 'visits', header: 'Visits', value: (u) => u.amount % 97 }),
|
|
113
|
+
booleanColumn({ id: 'active', header: 'Active', value: (u) => u.isActive }),
|
|
114
|
+
relativeDateColumn({ id: 'seen', header: 'Last Seen', value: (u) => u.lastSeenAt }),
|
|
115
|
+
dateColumn({ id: 'created', header: 'Created', value: (u) => u.createdAt }),
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div style={fillStyle}>
|
|
120
|
+
<BigTable
|
|
121
|
+
data={data}
|
|
122
|
+
columnDefs={columnDefs}
|
|
123
|
+
getRowKey={(u) => u.id}
|
|
124
|
+
hasStickyFirstColumn
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const HandRolledColumn: Story = {
|
|
132
|
+
name: 'Hand-rolled ColumnDef (no prefab)',
|
|
133
|
+
render: () => {
|
|
134
|
+
const data = makeUsers(20);
|
|
135
|
+
const columnDefs: ColumnDef<User>[] = [
|
|
136
|
+
textColumn({ id: 'name', header: 'Name', value: (u) => u.name }),
|
|
137
|
+
{
|
|
138
|
+
id: 'profile',
|
|
139
|
+
header: 'Profile',
|
|
140
|
+
cell: (u) => (
|
|
141
|
+
<span>
|
|
142
|
+
<strong>{u.name}</strong>
|
|
143
|
+
{' — '}
|
|
144
|
+
<em style={{ color: 'var(--ui-muted)' }}>{u.email}</em>
|
|
145
|
+
</span>
|
|
146
|
+
),
|
|
147
|
+
minWidth: 280,
|
|
148
|
+
},
|
|
149
|
+
currencyColumn({ id: 'amount', header: 'Amount', value: (u) => u.amount }),
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div style={fillStyle}>
|
|
154
|
+
<BigTable data={data} columnDefs={columnDefs} getRowKey={(u) => u.id} />
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const CompositeCell: Story = {
|
|
161
|
+
name: 'Composite cell — LinedStack with link',
|
|
162
|
+
render: () => {
|
|
163
|
+
const data = makeUsers(40);
|
|
164
|
+
const columnDefs: ColumnDef<User>[] = [
|
|
165
|
+
{
|
|
166
|
+
id: 'user',
|
|
167
|
+
header: 'User',
|
|
168
|
+
cell: (u) => (
|
|
169
|
+
<LinedStack to={`/users/${u.id}`}>
|
|
170
|
+
<strong>{u.name}</strong>
|
|
171
|
+
<Text isMuted size="xsmall">
|
|
172
|
+
{u.email}
|
|
173
|
+
</Text>
|
|
174
|
+
</LinedStack>
|
|
175
|
+
),
|
|
176
|
+
minWidth: 240,
|
|
177
|
+
},
|
|
178
|
+
badgeColumn({ id: 'role', header: 'Role', value: (u) => u.role }),
|
|
179
|
+
textColumn({ id: 'team', header: 'Team', value: (u) => u.team }),
|
|
180
|
+
currencyColumn({ id: 'amount', header: 'Amount', value: (u) => u.amount }),
|
|
181
|
+
relativeDateColumn({ id: 'seen', header: 'Last Seen', value: (u) => u.lastSeenAt }),
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div style={fillStyle}>
|
|
186
|
+
<BigTable data={data} columnDefs={columnDefs} getRowKey={(u) => u.id} />
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const LongTextCells: Story = {
|
|
193
|
+
name: 'LongText cells — truncate + click to reveal',
|
|
194
|
+
render: () => {
|
|
195
|
+
const notes = [
|
|
196
|
+
'Short note.',
|
|
197
|
+
'Wants quarterly review pushed back, mentioned travel through end of month.',
|
|
198
|
+
'Renewal pending — legal review on the master agreement, contract expected to land week of the 15th. Needs procurement sign-off after. Has flagged the data-residency clause as potentially blocking; CISO is aware.',
|
|
199
|
+
null,
|
|
200
|
+
'Followed up via Slack, no response yet.',
|
|
201
|
+
'Account paused last week pending billing reconciliation. Disputed line items: 3. Resolution target this Friday.',
|
|
202
|
+
];
|
|
203
|
+
const data = makeUsers(40).map((u, i) => ({ ...u, note: notes[i % notes.length] }));
|
|
204
|
+
|
|
205
|
+
const columnDefs: ColumnDef<typeof data[number]>[] = [
|
|
206
|
+
textColumn({ id: 'name', header: 'Name', value: (u) => u.name, width: 160 }),
|
|
207
|
+
badgeColumn({ id: 'role', header: 'Role', value: (u) => u.role }),
|
|
208
|
+
{
|
|
209
|
+
id: 'note',
|
|
210
|
+
header: 'Note',
|
|
211
|
+
cell: (u) => <LongText>{u.note}</LongText>,
|
|
212
|
+
width: 280,
|
|
213
|
+
},
|
|
214
|
+
currencyColumn({ id: 'amount', header: 'Amount', value: (u) => u.amount }),
|
|
215
|
+
relativeDateColumn({ id: 'seen', header: 'Last Seen', value: (u) => u.lastSeenAt }),
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div style={fillStyle}>
|
|
220
|
+
<BigTable data={data} columnDefs={columnDefs} getRowKey={(u) => u.id} />
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const ClickableRows: Story = {
|
|
227
|
+
name: 'Clickable rows — onRowClick',
|
|
228
|
+
render: () => {
|
|
229
|
+
const data = makeUsers(40);
|
|
230
|
+
const columnDefs: ColumnDef<User>[] = [
|
|
231
|
+
idColumn({ id: 'id', header: 'ID', value: (u) => u.id }),
|
|
232
|
+
textColumn({ id: 'name', header: 'Name', value: (u) => u.name }),
|
|
233
|
+
textColumn({ id: 'email', header: 'Email', value: (u) => u.email, minWidth: 220 }),
|
|
234
|
+
badgeColumn({ id: 'role', header: 'Role', value: (u) => u.role }),
|
|
235
|
+
currencyColumn({ id: 'amount', header: 'Amount', value: (u) => u.amount }),
|
|
236
|
+
{
|
|
237
|
+
id: 'profile',
|
|
238
|
+
header: 'Profile',
|
|
239
|
+
cell: (u) => (
|
|
240
|
+
<LinedStack to={`/users/${u.id}`}>
|
|
241
|
+
<strong>view</strong>
|
|
242
|
+
</LinedStack>
|
|
243
|
+
),
|
|
244
|
+
minWidth: 80,
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div style={fillStyle}>
|
|
250
|
+
<BigTable
|
|
251
|
+
data={data}
|
|
252
|
+
columnDefs={columnDefs}
|
|
253
|
+
getRowKey={(u) => u.id}
|
|
254
|
+
onRowClick={(u) => {
|
|
255
|
+
alert(`Row clicked: ${u.name}`);
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export const MasterDetailInPanels: Story = {
|
|
264
|
+
name: 'Master-detail — BigTable inside Panels with row → overlay',
|
|
265
|
+
render: () => {
|
|
266
|
+
const Demo = () => {
|
|
267
|
+
const [selected, setSelected] = useState<User | null>(null);
|
|
268
|
+
const data = makeUsers(40);
|
|
269
|
+
|
|
270
|
+
const columnDefs: ColumnDef<User>[] = [
|
|
271
|
+
idColumn({ id: 'id', header: 'ID', value: (u) => u.id }),
|
|
272
|
+
textColumn({ id: 'name', header: 'Name', value: (u) => u.name }),
|
|
273
|
+
textColumn({ id: 'email', header: 'Email', value: (u) => u.email, minWidth: 220 }),
|
|
274
|
+
badgeColumn({ id: 'role', header: 'Role', value: (u) => u.role }),
|
|
275
|
+
textColumn({ id: 'team', header: 'Team', value: (u) => u.team }),
|
|
276
|
+
currencyColumn({ id: 'amount', header: 'Amount', value: (u) => u.amount }),
|
|
277
|
+
relativeDateColumn({ id: 'seen', header: 'Last Seen', value: (u) => u.lastSeenAt }),
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const detailRows: { label: string; value: string }[] = selected
|
|
281
|
+
? [
|
|
282
|
+
{ label: 'ID', value: selected.id },
|
|
283
|
+
{ label: 'Email', value: selected.email },
|
|
284
|
+
{ label: 'Role', value: selected.role },
|
|
285
|
+
{ label: 'Team', value: selected.team },
|
|
286
|
+
{ label: 'Amount', value: `$${selected.amount.toLocaleString()}` },
|
|
287
|
+
{ label: 'Active', value: selected.isActive ? 'Yes' : 'No' },
|
|
288
|
+
]
|
|
289
|
+
: [];
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div style={fillStyle}>
|
|
293
|
+
<Panels
|
|
294
|
+
header={<Bar title="Users" />}
|
|
295
|
+
rightOverlayWidth={400}
|
|
296
|
+
rightOverlay={
|
|
297
|
+
selected ? (
|
|
298
|
+
<Panels
|
|
299
|
+
header={
|
|
300
|
+
<Bar
|
|
301
|
+
title={selected.name}
|
|
302
|
+
right={
|
|
303
|
+
<Button size="small" onClick={() => setSelected(null)}>
|
|
304
|
+
Close
|
|
305
|
+
</Button>
|
|
306
|
+
}
|
|
307
|
+
/>
|
|
308
|
+
}
|
|
309
|
+
>
|
|
310
|
+
<div style={{ padding: 16 }}>
|
|
311
|
+
<Stack gap={3}>
|
|
312
|
+
<Heading level={3}>Profile</Heading>
|
|
313
|
+
<Stack gap={2}>
|
|
314
|
+
{detailRows.map((row) => {
|
|
315
|
+
const { label, value } = row;
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<Stack key={label} gap={0}>
|
|
319
|
+
<Text isMuted size="xsmall">
|
|
320
|
+
{label}
|
|
321
|
+
</Text>
|
|
322
|
+
<Text>{value}</Text>
|
|
323
|
+
</Stack>
|
|
324
|
+
);
|
|
325
|
+
})}
|
|
326
|
+
</Stack>
|
|
327
|
+
</Stack>
|
|
328
|
+
</div>
|
|
329
|
+
</Panels>
|
|
330
|
+
) : null
|
|
331
|
+
}
|
|
332
|
+
>
|
|
333
|
+
<BigTable
|
|
334
|
+
data={data}
|
|
335
|
+
columnDefs={columnDefs}
|
|
336
|
+
getRowKey={(u) => u.id}
|
|
337
|
+
onRowClick={(u) => setSelected((prev) => (prev?.id === u.id ? null : u))}
|
|
338
|
+
/>
|
|
339
|
+
</Panels>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
return <Demo />;
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export const ManyRows: Story = {
|
|
349
|
+
name: 'Many rows — vertical scroll, sticky header',
|
|
350
|
+
render: () => {
|
|
351
|
+
const data = makeUsers(500);
|
|
352
|
+
const columnDefs: ColumnDef<User>[] = [
|
|
353
|
+
idColumn({ id: 'id', header: 'ID', value: (u) => u.id }),
|
|
354
|
+
textColumn({ id: 'name', header: 'Name', value: (u) => u.name }),
|
|
355
|
+
badgeColumn({ id: 'role', header: 'Role', value: (u) => u.role }),
|
|
356
|
+
textColumn({ id: 'team', header: 'Team', value: (u) => u.team }),
|
|
357
|
+
currencyColumn({ id: 'amount', header: 'Amount', value: (u) => u.amount }),
|
|
358
|
+
relativeDateColumn({ id: 'seen', header: 'Last Seen', value: (u) => u.lastSeenAt }),
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<div style={fillStyle}>
|
|
363
|
+
<BigTable data={data} columnDefs={columnDefs} getRowKey={(u) => u.id} />
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
},
|
|
367
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# BigTable — read this before editing
|
|
2
|
+
|
|
3
|
+
`BigTable` is the data-driven sibling of `QuickTable`. It accepts a typed
|
|
4
|
+
`data: T[]` plus `columnDefs: ColumnDef<T>[]` and renders a sticky-header
|
|
5
|
+
table that fills its container.
|
|
6
|
+
|
|
7
|
+
## What it is
|
|
8
|
+
|
|
9
|
+
- **Data-driven.** Caller passes a row array and a column-def array. Each
|
|
10
|
+
column has a `cell: (row: T) => ReactNode` renderer. No JSX-children API.
|
|
11
|
+
- **Container-filling.** The shell is `height: 100%; overflow: auto`. The
|
|
12
|
+
parent must give it a bounded height (the way `Panels` works), or it
|
|
13
|
+
collapses.
|
|
14
|
+
- **Sticky header.** `<thead>` is `position: sticky; top: 0`.
|
|
15
|
+
- **Optional sticky first column.** `hasStickyFirstColumn` pins the leftmost
|
|
16
|
+
column horizontally. Only one column, only the leftmost — by design.
|
|
17
|
+
- **Horizontal scroll.** The table has `min-width: max-content`, so any column
|
|
18
|
+
that needs more space pushes total table width past the shell, triggering
|
|
19
|
+
horizontal scroll without truncating cells.
|
|
20
|
+
- **Optional row click.** `onRowClick(row)` is a pure passthrough — no
|
|
21
|
+
selection state, no highlight, no keyboard handling beyond what an
|
|
22
|
+
interactive cell child already provides. When set, rows get
|
|
23
|
+
`cursor: pointer` and a hover background. Clicks whose target is inside
|
|
24
|
+
an interactive element (`a, button, input, select, textarea, label,
|
|
25
|
+
[role="button"]`) are skipped, so a `linkColumn` or button-cell inside a
|
|
26
|
+
clickable row keeps its own behavior without firing the row handler too.
|
|
27
|
+
|
|
28
|
+
## What it is NOT
|
|
29
|
+
|
|
30
|
+
- **Not a feature-grown data grid.** No internal sorting, filtering, selection,
|
|
31
|
+
multi-column pinning, column resizing, virtualization, expand/collapse rows,
|
|
32
|
+
inline editing, footers, grouping, or pagination. The controller is responsible
|
|
33
|
+
for sort/filter/page on its side; BigTable just renders what it's handed.
|
|
34
|
+
- **Not a replacement for QuickTable.** QuickTable is for ad-hoc admin/debug
|
|
35
|
+
surfaces; BigTable is for application UI where a controller has a typed
|
|
36
|
+
array and wants efficient, consistent rendering.
|
|
37
|
+
- **Not virtualized in v0.** "Many rows" works at 500–2k rows; beyond that,
|
|
38
|
+
add row virtualization (TanStack Virtual is the lightest path) without
|
|
39
|
+
changing the public API.
|
|
40
|
+
|
|
41
|
+
If you feel the pull to add `sortable`, `onSort`, `selectable`, `onRowSelect`,
|
|
42
|
+
`columnReorderable`, `expandable`, `pageSize`, `footer`, `groupBy` —
|
|
43
|
+
**stop**. That's the signal that we either need a new primitive
|
|
44
|
+
(e.g. `DataGrid`) or that the controller should own the behavior. Adding
|
|
45
|
+
those here defeats the "limited scope" pact and slowly rebuilds Excel.
|
|
46
|
+
|
|
47
|
+
`onRowClick` is the one row-interaction prop that lives here. The line is
|
|
48
|
+
**state vs. event**: `onRowClick` is a pure event passthrough that owns no
|
|
49
|
+
state — no selected-row tracking, no highlight, no multi-select. Anything
|
|
50
|
+
that asks BigTable to *remember* which row(s) the user touched
|
|
51
|
+
(`selectedRowIds`, `onRowSelect`, `defaultSelected`, `selectionMode`)
|
|
52
|
+
crosses the line and belongs in a `DataGrid` or in the controller.
|
|
53
|
+
|
|
54
|
+
## ColumnDef anatomy
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
type ColumnDef<T> = {
|
|
58
|
+
id: string; // stable identity (React key, future pinning)
|
|
59
|
+
header: ReactNode; // header content — usually a string
|
|
60
|
+
cell: (row: T) => ReactNode; // generic renderer — the base case
|
|
61
|
+
align?: 'left' | 'right' | 'center';
|
|
62
|
+
width?: number | string; // px number or CSS unit; omitted = flexible
|
|
63
|
+
minWidth?: number; // floor when flexible
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can write a `ColumnDef<T>` by hand for any one-off rendering. For the
|
|
68
|
+
common cases, prefer a prefab — they set sensible `align` / `minWidth`
|
|
69
|
+
defaults and keep call sites consistent across the app.
|
|
70
|
+
|
|
71
|
+
## Prefabs
|
|
72
|
+
|
|
73
|
+
All prefabs take `{ id, header, value, ... }` and return a full
|
|
74
|
+
`ColumnDef<T>`. Override layout via the `width` / `minWidth` args; for any
|
|
75
|
+
deeper override, spread the prefab result and patch:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
{ ...numberColumn({ id, header, value: (r) => r.x }), align: 'left' }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Prefab | Value | Defaults |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `textColumn` | `string` | left |
|
|
84
|
+
| `numberColumn` | `number` | right, minWidth 80 |
|
|
85
|
+
| `currencyColumn` | `number` | right, minWidth 100, USD |
|
|
86
|
+
| `dateColumn` | `Date \| string \| number` | left, minWidth 110 |
|
|
87
|
+
| `relativeDateColumn` | `Date \| string \| number` | left, minWidth 120 |
|
|
88
|
+
| `badgeColumn` | `string` (rendered in `<Badge>`) | left |
|
|
89
|
+
| `booleanColumn` | `boolean` (Yes/No, configurable) | center, minWidth 60 |
|
|
90
|
+
| `linkColumn` | `string` (rendered in `<Link>`) | left |
|
|
91
|
+
| `idColumn` | `string` (mono, muted) | left, minWidth 100 |
|
|
92
|
+
|
|
93
|
+
## Container-fill contract
|
|
94
|
+
|
|
95
|
+
`<BigTable />` fills `height: 100%; width: 100%`. The parent has to give it
|
|
96
|
+
a bounded box — same contract as `Panels`. If the parent is `height: auto`,
|
|
97
|
+
the table grows with content and the sticky header has nothing to stick to.
|
|
98
|
+
|
|
99
|
+
## Empty / loading state
|
|
100
|
+
|
|
101
|
+
Not handled in v0. Caller checks `data.length === 0` (or `isLoading`) and
|
|
102
|
+
renders something else (a `LoadingContainer`, an empty-state card) before
|
|
103
|
+
mounting `BigTable`. Adding a built-in slot is cheap if it earns its keep,
|
|
104
|
+
but most callers already have an empty-state convention.
|
|
105
|
+
|
|
106
|
+
## Row identity
|
|
107
|
+
|
|
108
|
+
`getRowKey` is required, not optional. Forcing the caller to declare
|
|
109
|
+
identity prevents the index-key trap (rows get reordered, React reuses the
|
|
110
|
+
wrong DOM, focus/selection state leaks). One extra line per call site.
|
|
111
|
+
|
|
112
|
+
## File layout
|
|
113
|
+
|
|
114
|
+
- `index.tsx` — `BigTable` component + barrel re-exports.
|
|
115
|
+
- `types.ts` — `ColumnDef`, `BigTableProps`, `ColumnAlign`.
|
|
116
|
+
- `columnDefs.tsx` — prefab factories.
|
|
117
|
+
- `styles.module.css` — shell, sticky header, sticky column, alignment, mono.
|
|
118
|
+
- `BigTable.stories.tsx` — stories.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Badge } from '../../Content/Badge';
|
|
3
|
+
import { Link } from '../../Content/Link';
|
|
4
|
+
import { Num } from '../../Primitives/Num';
|
|
5
|
+
import type { NumVariant } from '../../Primitives/Num';
|
|
6
|
+
import { RelativeTime } from '../../Primitives/RelativeTime';
|
|
7
|
+
import { EmptyValue } from '../../Primitives/EmptyValue';
|
|
8
|
+
import type { ColumnDef } from './types';
|
|
9
|
+
import styles from './styles.module.css';
|
|
10
|
+
|
|
11
|
+
type Layout = {
|
|
12
|
+
width?: number | string;
|
|
13
|
+
minWidth?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Base<T, V> = Layout & {
|
|
17
|
+
id: string;
|
|
18
|
+
header: ReactNode;
|
|
19
|
+
value: (row: T) => V;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const textColumn = <T,>({
|
|
23
|
+
id,
|
|
24
|
+
header,
|
|
25
|
+
value,
|
|
26
|
+
width,
|
|
27
|
+
minWidth,
|
|
28
|
+
}: Base<T, string | null | undefined>): ColumnDef<T> => ({
|
|
29
|
+
id,
|
|
30
|
+
header,
|
|
31
|
+
cell: (row) => {
|
|
32
|
+
const v = value(row);
|
|
33
|
+
return v == null || v === '' ? <EmptyValue /> : v;
|
|
34
|
+
},
|
|
35
|
+
align: 'left',
|
|
36
|
+
width,
|
|
37
|
+
minWidth,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const numberColumn = <T,>({
|
|
41
|
+
id,
|
|
42
|
+
header,
|
|
43
|
+
value,
|
|
44
|
+
as,
|
|
45
|
+
precision,
|
|
46
|
+
width,
|
|
47
|
+
minWidth,
|
|
48
|
+
}: Base<T, number | null | undefined> & {
|
|
49
|
+
as?: NumVariant;
|
|
50
|
+
precision?: number;
|
|
51
|
+
}): ColumnDef<T> => ({
|
|
52
|
+
id,
|
|
53
|
+
header,
|
|
54
|
+
cell: (row) => <Num value={value(row)} as={as} precision={precision} />,
|
|
55
|
+
align: 'right',
|
|
56
|
+
width,
|
|
57
|
+
minWidth: minWidth ?? 80,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const currencyColumn = <T,>({
|
|
61
|
+
id,
|
|
62
|
+
header,
|
|
63
|
+
value,
|
|
64
|
+
currency,
|
|
65
|
+
precision,
|
|
66
|
+
width,
|
|
67
|
+
minWidth,
|
|
68
|
+
}: Base<T, number | null | undefined> & {
|
|
69
|
+
currency?: string;
|
|
70
|
+
precision?: number;
|
|
71
|
+
}): ColumnDef<T> => ({
|
|
72
|
+
id,
|
|
73
|
+
header,
|
|
74
|
+
cell: (row) => (
|
|
75
|
+
<Num value={value(row)} as="currency" currency={currency} precision={precision} />
|
|
76
|
+
),
|
|
77
|
+
align: 'right',
|
|
78
|
+
width,
|
|
79
|
+
minWidth: minWidth ?? 100,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const toDate = (v: Date | string | number): Date => (v instanceof Date ? v : new Date(v));
|
|
83
|
+
|
|
84
|
+
export const dateColumn = <T,>({
|
|
85
|
+
id,
|
|
86
|
+
header,
|
|
87
|
+
value,
|
|
88
|
+
width,
|
|
89
|
+
minWidth,
|
|
90
|
+
}: Base<T, Date | string | number | null | undefined>): ColumnDef<T> => ({
|
|
91
|
+
id,
|
|
92
|
+
header,
|
|
93
|
+
cell: (row) => {
|
|
94
|
+
const v = value(row);
|
|
95
|
+
if (v == null) return <EmptyValue />;
|
|
96
|
+
const d = toDate(v);
|
|
97
|
+
return Number.isNaN(d.getTime()) ? <EmptyValue /> : d.toLocaleDateString();
|
|
98
|
+
},
|
|
99
|
+
align: 'left',
|
|
100
|
+
width,
|
|
101
|
+
minWidth: minWidth ?? 110,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const relativeDateColumn = <T,>({
|
|
105
|
+
id,
|
|
106
|
+
header,
|
|
107
|
+
value,
|
|
108
|
+
width,
|
|
109
|
+
minWidth,
|
|
110
|
+
}: Base<T, Date | string | number | null | undefined>): ColumnDef<T> => ({
|
|
111
|
+
id,
|
|
112
|
+
header,
|
|
113
|
+
cell: (row) => <RelativeTime value={value(row)} />,
|
|
114
|
+
align: 'left',
|
|
115
|
+
width,
|
|
116
|
+
minWidth: minWidth ?? 120,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const badgeColumn = <T,>({
|
|
120
|
+
id,
|
|
121
|
+
header,
|
|
122
|
+
value,
|
|
123
|
+
isMono,
|
|
124
|
+
width,
|
|
125
|
+
minWidth,
|
|
126
|
+
}: Base<T, string | null | undefined> & { isMono?: boolean }): ColumnDef<T> => ({
|
|
127
|
+
id,
|
|
128
|
+
header,
|
|
129
|
+
cell: (row) => {
|
|
130
|
+
const v = value(row);
|
|
131
|
+
return v == null || v === '' ? <EmptyValue /> : <Badge isMono={isMono}>{v}</Badge>;
|
|
132
|
+
},
|
|
133
|
+
align: 'left',
|
|
134
|
+
width,
|
|
135
|
+
minWidth,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const booleanColumn = <T,>({
|
|
139
|
+
id,
|
|
140
|
+
header,
|
|
141
|
+
value,
|
|
142
|
+
labels = { true: 'Yes', false: 'No' },
|
|
143
|
+
width,
|
|
144
|
+
minWidth,
|
|
145
|
+
}: Base<T, boolean | null | undefined> & {
|
|
146
|
+
labels?: { true: string; false: string };
|
|
147
|
+
}): ColumnDef<T> => ({
|
|
148
|
+
id,
|
|
149
|
+
header,
|
|
150
|
+
cell: (row) => {
|
|
151
|
+
const v = value(row);
|
|
152
|
+
if (v == null) return <EmptyValue />;
|
|
153
|
+
return v ? labels.true : labels.false;
|
|
154
|
+
},
|
|
155
|
+
align: 'center',
|
|
156
|
+
width,
|
|
157
|
+
minWidth: minWidth ?? 60,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export const linkColumn = <T,>({
|
|
161
|
+
id,
|
|
162
|
+
header,
|
|
163
|
+
value,
|
|
164
|
+
href,
|
|
165
|
+
isExternal,
|
|
166
|
+
width,
|
|
167
|
+
minWidth,
|
|
168
|
+
}: Base<T, string | null | undefined> & {
|
|
169
|
+
href: (row: T) => string;
|
|
170
|
+
isExternal?: boolean;
|
|
171
|
+
}): ColumnDef<T> => ({
|
|
172
|
+
id,
|
|
173
|
+
header,
|
|
174
|
+
cell: (row) => {
|
|
175
|
+
const v = value(row);
|
|
176
|
+
if (v == null || v === '') return <EmptyValue />;
|
|
177
|
+
return (
|
|
178
|
+
<Link href={href(row)} isExternal={isExternal}>
|
|
179
|
+
{v}
|
|
180
|
+
</Link>
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
align: 'left',
|
|
184
|
+
width,
|
|
185
|
+
minWidth,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
export const idColumn = <T,>({
|
|
189
|
+
id,
|
|
190
|
+
header,
|
|
191
|
+
value,
|
|
192
|
+
width,
|
|
193
|
+
minWidth,
|
|
194
|
+
}: Base<T, string | null | undefined>): ColumnDef<T> => ({
|
|
195
|
+
id,
|
|
196
|
+
header,
|
|
197
|
+
cell: (row) => {
|
|
198
|
+
const v = value(row);
|
|
199
|
+
return v == null || v === '' ? (
|
|
200
|
+
<EmptyValue />
|
|
201
|
+
) : (
|
|
202
|
+
<span className={styles.cellMono}>{v}</span>
|
|
203
|
+
);
|
|
204
|
+
},
|
|
205
|
+
align: 'left',
|
|
206
|
+
width,
|
|
207
|
+
minWidth: minWidth ?? 100,
|
|
208
|
+
});
|