@tempoxyz/ds 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/AGENTS.md +9 -0
- package/dist/docs/components/accordion.md +489 -0
- package/dist/docs/components/address-card.md +147 -0
- package/dist/docs/components/agent-logo.md +194 -0
- package/dist/docs/components/alert-dialog.md +631 -0
- package/dist/docs/components/alert.md +448 -0
- package/dist/docs/components/amount-input.md +305 -0
- package/dist/docs/components/amount.md +794 -0
- package/dist/docs/components/app-shell.md +559 -0
- package/dist/docs/components/asset-chip.md +294 -0
- package/dist/docs/components/autocomplete.md +619 -0
- package/dist/docs/components/avatar.md +321 -0
- package/dist/docs/components/badge.md +339 -0
- package/dist/docs/components/badged-logo.md +116 -0
- package/dist/docs/components/banner.md +379 -0
- package/dist/docs/components/bar-chart.md +208 -0
- package/dist/docs/components/breadcrumbs.md +380 -0
- package/dist/docs/components/button-group.md +286 -0
- package/dist/docs/components/button.md +650 -0
- package/dist/docs/components/calendar.md +375 -0
- package/dist/docs/components/callout.md +96 -0
- package/dist/docs/components/checkbox-group.md +521 -0
- package/dist/docs/components/checkbox.md +467 -0
- package/dist/docs/components/choice-group.md +516 -0
- package/dist/docs/components/choicebox.md +641 -0
- package/dist/docs/components/code.md +100 -0
- package/dist/docs/components/combobox.md +939 -0
- package/dist/docs/components/container.md +257 -0
- package/dist/docs/components/copy-button.md +642 -0
- package/dist/docs/components/date-picker.md +514 -0
- package/dist/docs/components/dialog.md +1332 -0
- package/dist/docs/components/drawer.md +966 -0
- package/dist/docs/components/empty.md +340 -0
- package/dist/docs/components/field.md +570 -0
- package/dist/docs/components/fieldset.md +398 -0
- package/dist/docs/components/frame.md +434 -0
- package/dist/docs/components/gauge.md +299 -0
- package/dist/docs/components/get-started-visual.md +117 -0
- package/dist/docs/components/grid.md +288 -0
- package/dist/docs/components/hero-visual.md +155 -0
- package/dist/docs/components/hex.md +487 -0
- package/dist/docs/components/icon-tile.md +292 -0
- package/dist/docs/components/identicon.md +371 -0
- package/dist/docs/components/inline.md +233 -0
- package/dist/docs/components/inset.md +225 -0
- package/dist/docs/components/kbd.md +423 -0
- package/dist/docs/components/list-group.md +981 -0
- package/dist/docs/components/loading-dots.md +237 -0
- package/dist/docs/components/menu.md +934 -0
- package/dist/docs/components/native-select.md +204 -0
- package/dist/docs/components/network-logo.md +248 -0
- package/dist/docs/components/number-field.md +543 -0
- package/dist/docs/components/otp-field.md +616 -0
- package/dist/docs/components/pagination.md +516 -0
- package/dist/docs/components/popover.md +544 -0
- package/dist/docs/components/progress.md +388 -0
- package/dist/docs/components/qr.md +314 -0
- package/dist/docs/components/radio-group.md +528 -0
- package/dist/docs/components/secret-value.md +406 -0
- package/dist/docs/components/select.md +751 -0
- package/dist/docs/components/separator.md +379 -0
- package/dist/docs/components/sidebar-menu.md +922 -0
- package/dist/docs/components/skeleton.md +517 -0
- package/dist/docs/components/slider.md +539 -0
- package/dist/docs/components/snippet.md +424 -0
- package/dist/docs/components/spinner.md +216 -0
- package/dist/docs/components/split-bar.md +139 -0
- package/dist/docs/components/split-button.md +464 -0
- package/dist/docs/components/stack.md +299 -0
- package/dist/docs/components/status-chip.md +226 -0
- package/dist/docs/components/status-indicator.md +119 -0
- package/dist/docs/components/status-row.md +174 -0
- package/dist/docs/components/stepper.md +451 -0
- package/dist/docs/components/success-hero.md +157 -0
- package/dist/docs/components/surface.md +639 -0
- package/dist/docs/components/switch.md +472 -0
- package/dist/docs/components/table.md +967 -0
- package/dist/docs/components/tabs.md +627 -0
- package/dist/docs/components/tempo-logo.md +185 -0
- package/dist/docs/components/text-input.md +624 -0
- package/dist/docs/components/textarea.md +352 -0
- package/dist/docs/components/toast.md +535 -0
- package/dist/docs/components/token-logo.md +371 -0
- package/dist/docs/components/token.md +247 -0
- package/dist/docs/components/tooltip.md +490 -0
- package/dist/docs/components/verification-code.md +116 -0
- package/dist/docs/components/visual.md +249 -0
- package/dist/docs/components.md +203 -0
- package/dist/docs/foundations/accessibility.md +377 -0
- package/dist/docs/foundations/agents.md +150 -0
- package/dist/docs/foundations/color.md +522 -0
- package/dist/docs/foundations/content/tone.md +194 -0
- package/dist/docs/foundations/content/voice.md +194 -0
- package/dist/docs/foundations/content/writing-style.md +406 -0
- package/dist/docs/foundations/iconography.md +282 -0
- package/dist/docs/foundations/intent.md +360 -0
- package/dist/docs/foundations/layout.md +508 -0
- package/dist/docs/foundations/linter.md +296 -0
- package/dist/docs/foundations/motion.md +151 -0
- package/dist/docs/foundations/radius-and-padding.md +86 -0
- package/dist/docs/foundations/typography.md +266 -0
- package/dist/docs/index.md +137 -0
- package/dist/docs/introduction/faq.md +132 -0
- package/dist/docs/introduction/getting-started.md +377 -0
- package/dist/docs/patterns/blockchain-values.md +709 -0
- package/dist/docs/patterns/dashboards.md +638 -0
- package/dist/docs/patterns/destructive-actions.md +626 -0
- package/dist/docs/patterns/empty-states.md +612 -0
- package/dist/docs/patterns/errors-and-validation.md +808 -0
- package/dist/docs/patterns/feedback-and-status.md +518 -0
- package/dist/docs/patterns/forms.md +679 -0
- package/dist/docs/patterns/loading.md +630 -0
- package/dist/docs/patterns/navigation.md +397 -0
- package/dist/docs/patterns/payments.md +765 -0
- package/dist/docs/patterns/responsive-layout.md +997 -0
- package/dist/docs/patterns/tables-and-lists.md +964 -0
- package/dist/docs/products/tempo-console.md +671 -0
- package/dist/docs/products/tempo-performance.md +437 -0
- package/dist/docs/products/tempo-wallet.md +438 -0
- package/dist/web/components/OtpField.css +1 -1
- package/dist/web/components/OtpField.d.ts +1 -1
- package/dist/web/components/OtpField.d.ts.map +1 -1
- package/dist/web/components/OtpField.js +3 -4
- package/dist/web/components/OtpField.js.map +1 -1
- package/dist/web/components/OtpField.js.zyzz.json +1 -1
- package/dist/web/index.js.zyzz.json +1 -1
- package/package.json +1 -1
- package/src/web/components/OtpField.tsx +2 -24
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
# Tables and Lists
|
|
2
|
+
|
|
3
|
+
Present records, such as payments and API keys, to scan, compare, and act on.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Tempo collections include wallet activity, scheduled payouts, API keys, webhooks, and blocks. Each needs a structure that fits its reading pattern, consistent alignment, and clear loading, empty, and narrow-screen states.
|
|
8
|
+
|
|
9
|
+
> [!NOTE]
|
|
10
|
+
> The live examples are reference layouts built from library components and `style`, not shipped components. No data grid component exists, so sorting, selection, and paging state live in your app.
|
|
11
|
+
|
|
12
|
+
## Choose a Structure
|
|
13
|
+
|
|
14
|
+
* **Table (Table):** Records compared across the same columns, such as payments, payouts, API keys, or blocks.
|
|
15
|
+
* **ListGroup (ListGroup):** The key-value details of one record, settings, and navigation lists.
|
|
16
|
+
* **Stacked List Items:** Items with a main line and a few attributes, such as activity. Also narrow-screen tables.
|
|
17
|
+
|
|
18
|
+
**Do:** Use [ListGroup](/docs/components/list-group) with `ListGroup.Label` for the fields of one
|
|
19
|
+
record.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
/**
|
|
23
|
+
* Do example for "Tables and Lists" on the Tables and Lists page.
|
|
24
|
+
* Use ListGroup with `ListGroup.Label` for the fields of one record.
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
import { ListGroup, style, Surface } from '@tempoxyz/ds'
|
|
28
|
+
|
|
29
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
30
|
+
export function Demo() {
|
|
31
|
+
return (
|
|
32
|
+
<Surface size="small" {...styles.card()}>
|
|
33
|
+
<ListGroup.Root>
|
|
34
|
+
<ListGroup.Row>
|
|
35
|
+
<ListGroup.Label>Status</ListGroup.Label>
|
|
36
|
+
Confirmed
|
|
37
|
+
</ListGroup.Row>
|
|
38
|
+
<ListGroup.Row>
|
|
39
|
+
<ListGroup.Label>Amount</ListGroup.Label>
|
|
40
|
+
$1,200.00
|
|
41
|
+
</ListGroup.Row>
|
|
42
|
+
<ListGroup.Row>
|
|
43
|
+
<ListGroup.Label>Network fee</ListGroup.Label>
|
|
44
|
+
$0.00003
|
|
45
|
+
</ListGroup.Row>
|
|
46
|
+
</ListGroup.Root>
|
|
47
|
+
</Surface>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
namespace styles {
|
|
52
|
+
export const card = style({
|
|
53
|
+
boxShadow: '0 0 0 1px light-dark(#0000001a, #ffffff1f) !custom',
|
|
54
|
+
width: '16rem !custom',
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Don't:** Build a two-column Field and Value table for a single record.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
/**
|
|
63
|
+
* Don't example for "Tables and Lists" on the Tables and Lists page.
|
|
64
|
+
* Don't build a two-column Field and Value table for a single record.
|
|
65
|
+
* @module
|
|
66
|
+
*/
|
|
67
|
+
import { style, Surface, Table } from '@tempoxyz/ds'
|
|
68
|
+
|
|
69
|
+
const fields = [
|
|
70
|
+
['Status', 'Confirmed'],
|
|
71
|
+
['Amount', '$1,200.00'],
|
|
72
|
+
['Network fee', '$0.00003'],
|
|
73
|
+
] as const
|
|
74
|
+
|
|
75
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
76
|
+
export function Demo() {
|
|
77
|
+
return (
|
|
78
|
+
<Surface size="small" {...styles.card()}>
|
|
79
|
+
<Table.Root size="small">
|
|
80
|
+
<Table.Head>
|
|
81
|
+
<Table.Row>
|
|
82
|
+
<Table.HeaderCell>Field</Table.HeaderCell>
|
|
83
|
+
<Table.HeaderCell>Value</Table.HeaderCell>
|
|
84
|
+
</Table.Row>
|
|
85
|
+
</Table.Head>
|
|
86
|
+
<Table.Body>
|
|
87
|
+
{fields.map(([field, value]) => (
|
|
88
|
+
<Table.Row key={field}>
|
|
89
|
+
<Table.Cell>{field}</Table.Cell>
|
|
90
|
+
<Table.Cell>{value}</Table.Cell>
|
|
91
|
+
</Table.Row>
|
|
92
|
+
))}
|
|
93
|
+
</Table.Body>
|
|
94
|
+
</Table.Root>
|
|
95
|
+
</Surface>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
namespace styles {
|
|
100
|
+
export const card = style({
|
|
101
|
+
boxShadow: '0 0 0 1px light-dark(#0000001a, #ffffff1f) !custom',
|
|
102
|
+
width: '16rem !custom',
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Recommended Structure
|
|
108
|
+
|
|
109
|
+
Place a table in a [Surface](/docs/components/surface), with a `Surface.Header` above it for the title and actions. A direct child `table` bleeds into the padding of the surface, so row fills and dividers reach its edges.
|
|
110
|
+
|
|
111
|
+
* **Header (1):** The title of the collection, filters, and actions such as Export CSV.
|
|
112
|
+
* **Table (2):** The table itself, named by a caption or by `aria-labelledby`.
|
|
113
|
+
* **Footer (3):** The visible range, such as 1–20 of 248, and pagination or a Load more button.
|
|
114
|
+
|
|
115
|
+
## Align Columns by Content
|
|
116
|
+
|
|
117
|
+
* **Start-Align Text:** Start-align text, such as names, descriptions, and dates.
|
|
118
|
+
* **Monospace Identifiers (mono):** Use it for hashes and other identifiers. Shorten them in the middle with `formatHex`.
|
|
119
|
+
* **Amount Last:** Put the amount in the last column where possible. The last column aligns to the end automatically.
|
|
120
|
+
|
|
121
|
+
**Do:** End-align amounts and their headers with `justify="end"` and `numeric` cells, so decimals line
|
|
122
|
+
up.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
/**
|
|
126
|
+
* Do example for "Tables and Lists" on the Tables and Lists page.
|
|
127
|
+
* End-align amounts and their headers with `justify="end"` and `numeric` cells, so decimals line up.
|
|
128
|
+
* @module
|
|
129
|
+
*/
|
|
130
|
+
import { style, Table } from '@tempoxyz/ds'
|
|
131
|
+
|
|
132
|
+
const rows = [
|
|
133
|
+
['Payroll', '$1,200.00'],
|
|
134
|
+
['Hosting', '$48.50'],
|
|
135
|
+
['Refund', '$9.99'],
|
|
136
|
+
] as const
|
|
137
|
+
|
|
138
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
139
|
+
export function Demo() {
|
|
140
|
+
return (
|
|
141
|
+
<Table.Root size="small" {...styles.table()}>
|
|
142
|
+
<Table.Head>
|
|
143
|
+
<Table.Row>
|
|
144
|
+
<Table.HeaderCell>Payment</Table.HeaderCell>
|
|
145
|
+
<Table.HeaderCell justify="end">Amount</Table.HeaderCell>
|
|
146
|
+
</Table.Row>
|
|
147
|
+
</Table.Head>
|
|
148
|
+
<Table.Body>
|
|
149
|
+
{rows.map(([payment, amount]) => (
|
|
150
|
+
<Table.Row key={payment}>
|
|
151
|
+
<Table.Cell>{payment}</Table.Cell>
|
|
152
|
+
<Table.Cell justify="end" numeric>
|
|
153
|
+
{amount}
|
|
154
|
+
</Table.Cell>
|
|
155
|
+
</Table.Row>
|
|
156
|
+
))}
|
|
157
|
+
</Table.Body>
|
|
158
|
+
</Table.Root>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
namespace styles {
|
|
163
|
+
export const table = style({ maxWidth: '16rem !custom' })
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Don't:** Start-align amounts. Digits of different lengths do not line up.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
/**
|
|
171
|
+
* Don't example for "Tables and Lists" on the Tables and Lists page.
|
|
172
|
+
* Don't start-align amounts. Digits of different lengths do not line up.
|
|
173
|
+
* @module
|
|
174
|
+
*/
|
|
175
|
+
import { style, Table } from '@tempoxyz/ds'
|
|
176
|
+
|
|
177
|
+
const rows = [
|
|
178
|
+
['$1,200.00', 'Payroll'],
|
|
179
|
+
['$48.50', 'Hosting'],
|
|
180
|
+
['$9.99', 'Refund'],
|
|
181
|
+
] as const
|
|
182
|
+
|
|
183
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
184
|
+
export function Demo() {
|
|
185
|
+
return (
|
|
186
|
+
<Table.Root size="small" {...styles.table()}>
|
|
187
|
+
<Table.Head>
|
|
188
|
+
<Table.Row>
|
|
189
|
+
<Table.HeaderCell>Amount</Table.HeaderCell>
|
|
190
|
+
<Table.HeaderCell>Payment</Table.HeaderCell>
|
|
191
|
+
</Table.Row>
|
|
192
|
+
</Table.Head>
|
|
193
|
+
<Table.Body>
|
|
194
|
+
{rows.map(([amount, payment]) => (
|
|
195
|
+
<Table.Row key={payment}>
|
|
196
|
+
<Table.Cell>{amount}</Table.Cell>
|
|
197
|
+
<Table.Cell>{payment}</Table.Cell>
|
|
198
|
+
</Table.Row>
|
|
199
|
+
))}
|
|
200
|
+
</Table.Body>
|
|
201
|
+
</Table.Root>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
namespace styles {
|
|
206
|
+
export const table = style({ maxWidth: '16rem !custom' })
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Keep Multi-Line Rows Readable
|
|
211
|
+
|
|
212
|
+
A row with a wrapped description or a second line of detail grows taller than its neighbours. Keep each value whole and each row anchored to its first line.
|
|
213
|
+
|
|
214
|
+
* **Top-Align Tall Rows (verticalAlign):** If any cell in a table wraps or adds a subtitle, top-align every cell, so each row reads from its first line.
|
|
215
|
+
* **Keep Dates Whole (nowrap):** Keep a date and its time on one line. If the column is narrow, drop the time from secondary dates.
|
|
216
|
+
* **Shorten Every Identifier (formatHex):** Shorten each address inside a compound value, such as a route. A symbol, such as USDC, stays whole.
|
|
217
|
+
* **Wrap Prose Only (Table.Subtitle):** Let only sentences wrap. Put a secondary value in a `Table.Subtitle` below the cell's first line.
|
|
218
|
+
|
|
219
|
+
**Do:** Top-align a tall row, keep the date on one line, and shorten each address in the route.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
/**
|
|
223
|
+
* Do example for "Keep Multi-Line Rows Readable" on the Tables and Lists page.
|
|
224
|
+
* Top-align a row whose cells wrap, keep dates whole, and shorten each address in a compound value.
|
|
225
|
+
* @module
|
|
226
|
+
*/
|
|
227
|
+
import { formatHex, style, Table } from '@tempoxyz/ds'
|
|
228
|
+
|
|
229
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
230
|
+
export function Demo() {
|
|
231
|
+
return (
|
|
232
|
+
<Table.Root size="small" {...styles.table()}>
|
|
233
|
+
<Table.Head>
|
|
234
|
+
<Table.Row>
|
|
235
|
+
<Table.HeaderCell>Route</Table.HeaderCell>
|
|
236
|
+
<Table.HeaderCell>Updated</Table.HeaderCell>
|
|
237
|
+
</Table.Row>
|
|
238
|
+
</Table.Head>
|
|
239
|
+
<Table.Body>
|
|
240
|
+
<Table.Row>
|
|
241
|
+
<Table.Cell mono {...styles.top()}>
|
|
242
|
+
{formatHex('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', { evm: false })} →{' '}
|
|
243
|
+
{formatHex('0x20c0000000000000000000000000000000000000')}
|
|
244
|
+
<Table.Subtitle>Chain 792703809 → 4217</Table.Subtitle>
|
|
245
|
+
</Table.Cell>
|
|
246
|
+
<Table.Cell {...styles.date()}>Oct 9, 2026, 12:04 AM</Table.Cell>
|
|
247
|
+
</Table.Row>
|
|
248
|
+
</Table.Body>
|
|
249
|
+
</Table.Root>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
namespace styles {
|
|
254
|
+
export const date = style({ verticalAlign: 'top', whiteSpace: 'nowrap' })
|
|
255
|
+
|
|
256
|
+
export const table = style({ maxWidth: '18rem !custom' })
|
|
257
|
+
|
|
258
|
+
export const top = style({ verticalAlign: 'top' })
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Don't:** Let a full address or a date wrap across lines in a middle-aligned row.
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
/**
|
|
266
|
+
* Don't example for "Keep Multi-Line Rows Readable" on the Tables and Lists page.
|
|
267
|
+
* A full address wraps across lines, the date breaks mid-value, and middle alignment staggers the row.
|
|
268
|
+
* @module
|
|
269
|
+
*/
|
|
270
|
+
import { style, Table } from '@tempoxyz/ds'
|
|
271
|
+
|
|
272
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
273
|
+
export function Demo() {
|
|
274
|
+
return (
|
|
275
|
+
<Table.Root size="small" {...styles.table()}>
|
|
276
|
+
<Table.Head>
|
|
277
|
+
<Table.Row>
|
|
278
|
+
<Table.HeaderCell>Route</Table.HeaderCell>
|
|
279
|
+
<Table.HeaderCell>Updated</Table.HeaderCell>
|
|
280
|
+
</Table.Row>
|
|
281
|
+
</Table.Head>
|
|
282
|
+
<Table.Body>
|
|
283
|
+
<Table.Row>
|
|
284
|
+
<Table.Cell {...styles.wrap()}>
|
|
285
|
+
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v →
|
|
286
|
+
0x20c0000000000000000000000000000000000000
|
|
287
|
+
<Table.Subtitle>Chain 792703809 → 4217</Table.Subtitle>
|
|
288
|
+
</Table.Cell>
|
|
289
|
+
<Table.Cell {...styles.narrow()}>Oct 9, 2026, 12:04 AM</Table.Cell>
|
|
290
|
+
</Table.Row>
|
|
291
|
+
</Table.Body>
|
|
292
|
+
</Table.Root>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
namespace styles {
|
|
297
|
+
export const narrow = style({ width: '5rem !custom' })
|
|
298
|
+
|
|
299
|
+
export const table = style({ maxWidth: '18rem !custom', tableLayout: 'fixed !custom' })
|
|
300
|
+
|
|
301
|
+
export const wrap = style({ overflowWrap: 'anywhere' })
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Select Rows and Act in Bulk
|
|
306
|
+
|
|
307
|
+
If an action applies to several records at once, such as pausing payouts or revoking keys, add a leading column of small [Checkbox](/docs/components/checkbox) controls. Select rows in the example to show the bulk actions.
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
import { Amount, Badge, Button, Checkbox, Inset, style, Surface, Table } from '@tempoxyz/ds'
|
|
311
|
+
import { ArrowDown, ArrowUp, Sort } from '@tempoxyz/ds/icons'
|
|
312
|
+
import { useState } from 'react'
|
|
313
|
+
|
|
314
|
+
export const fill = true
|
|
315
|
+
const payouts = [
|
|
316
|
+
{
|
|
317
|
+
amount: '1200.00',
|
|
318
|
+
date: '2026-10-20',
|
|
319
|
+
id: 'p1',
|
|
320
|
+
recipient: 'Ada Lovelace',
|
|
321
|
+
status: 'Scheduled',
|
|
322
|
+
},
|
|
323
|
+
{ amount: '48.50', date: '2026-10-18', id: 'p2', recipient: 'Grace Hopper', status: 'Scheduled' },
|
|
324
|
+
{ amount: '310.00', date: '2026-10-21', id: 'p3', recipient: 'Alan Turing', status: 'Paused' },
|
|
325
|
+
{
|
|
326
|
+
amount: '9.99',
|
|
327
|
+
date: '2026-10-19',
|
|
328
|
+
id: 'p4',
|
|
329
|
+
recipient: 'Katherine Johnson',
|
|
330
|
+
status: 'Scheduled',
|
|
331
|
+
},
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
type Column = 'amount' | 'date'
|
|
335
|
+
|
|
336
|
+
export function Demo() {
|
|
337
|
+
const [selected, setSelected] = useState<readonly string[]>([])
|
|
338
|
+
const [sort, setSort] = useState<{ column: Column; direction: 'ascending' | 'descending' }>({
|
|
339
|
+
column: 'date',
|
|
340
|
+
direction: 'descending',
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const rows = [...payouts].sort((a, b) => {
|
|
344
|
+
const order =
|
|
345
|
+
sort.column === 'amount' ? Number(a.amount) - Number(b.amount) : a.date.localeCompare(b.date)
|
|
346
|
+
return sort.direction === 'ascending' ? order : -order
|
|
347
|
+
})
|
|
348
|
+
const all = selected.length === payouts.length
|
|
349
|
+
const some = selected.length > 0 && !all
|
|
350
|
+
|
|
351
|
+
function toggle(column: Column) {
|
|
352
|
+
setSort((current) => ({
|
|
353
|
+
column,
|
|
354
|
+
direction:
|
|
355
|
+
current.column === column && current.direction === 'descending'
|
|
356
|
+
? 'ascending'
|
|
357
|
+
: 'descending',
|
|
358
|
+
}))
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function header(column: Column, label: string) {
|
|
362
|
+
const active = sort.column === column
|
|
363
|
+
const Icon = (() => {
|
|
364
|
+
if (!active) return Sort
|
|
365
|
+
return sort.direction === 'ascending' ? ArrowUp : ArrowDown
|
|
366
|
+
})()
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<Table.HeaderCell
|
|
370
|
+
aria-sort={active ? sort.direction : undefined}
|
|
371
|
+
justify={column === 'amount' ? 'end' : 'start'}
|
|
372
|
+
>
|
|
373
|
+
<button onClick={() => toggle(column)} type="button" {...styles.sort()}>
|
|
374
|
+
{label}
|
|
375
|
+
<Icon aria-hidden="true" />
|
|
376
|
+
</button>
|
|
377
|
+
</Table.HeaderCell>
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<Inset
|
|
383
|
+
aria-label="Scheduled payouts"
|
|
384
|
+
role="region"
|
|
385
|
+
space="16"
|
|
386
|
+
tabIndex={0}
|
|
387
|
+
{...styles.region()}
|
|
388
|
+
>
|
|
389
|
+
<Surface {...styles.surface()}>
|
|
390
|
+
<Surface.Header>
|
|
391
|
+
<Surface.Title>Scheduled payouts</Surface.Title>
|
|
392
|
+
<div {...styles.actions()}>
|
|
393
|
+
<span role="status" {...styles.count()}>
|
|
394
|
+
{selected.length > 0 ? `${selected.length} selected` : ''}
|
|
395
|
+
</span>
|
|
396
|
+
{selected.length > 0 ? (
|
|
397
|
+
<>
|
|
398
|
+
<Button onClick={() => setSelected([])} size="small" variant="tertiary">
|
|
399
|
+
Clear
|
|
400
|
+
</Button>
|
|
401
|
+
<Button size="small" variant="secondary">
|
|
402
|
+
Pause
|
|
403
|
+
</Button>
|
|
404
|
+
<Button size="small">Cancel payouts</Button>
|
|
405
|
+
</>
|
|
406
|
+
) : (
|
|
407
|
+
<Button size="small" variant="secondary">
|
|
408
|
+
Export CSV
|
|
409
|
+
</Button>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
</Surface.Header>
|
|
413
|
+
<Table.Root>
|
|
414
|
+
<Table.Head>
|
|
415
|
+
<Table.Row>
|
|
416
|
+
<Table.HeaderCell {...styles.check()}>
|
|
417
|
+
<Checkbox
|
|
418
|
+
aria-label="Select all payouts"
|
|
419
|
+
checked={all}
|
|
420
|
+
indeterminate={some}
|
|
421
|
+
onCheckedChange={() => setSelected(all ? [] : payouts.map((payout) => payout.id))}
|
|
422
|
+
size="small"
|
|
423
|
+
/>
|
|
424
|
+
</Table.HeaderCell>
|
|
425
|
+
<Table.HeaderCell>Recipient</Table.HeaderCell>
|
|
426
|
+
{header('date', 'Date')}
|
|
427
|
+
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
428
|
+
{header('amount', 'Amount')}
|
|
429
|
+
</Table.Row>
|
|
430
|
+
</Table.Head>
|
|
431
|
+
<Table.Body>
|
|
432
|
+
{rows.map((payout) => {
|
|
433
|
+
const checked = selected.includes(payout.id)
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<Table.Row data-selected={checked || undefined} key={payout.id} {...styles.row()}>
|
|
437
|
+
<Table.Cell {...styles.check()}>
|
|
438
|
+
<Checkbox
|
|
439
|
+
aria-label={`Select payout to ${payout.recipient}`}
|
|
440
|
+
checked={checked}
|
|
441
|
+
onCheckedChange={() =>
|
|
442
|
+
setSelected(
|
|
443
|
+
checked
|
|
444
|
+
? selected.filter((id) => id !== payout.id)
|
|
445
|
+
: [...selected, payout.id],
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
size="small"
|
|
449
|
+
/>
|
|
450
|
+
</Table.Cell>
|
|
451
|
+
<Table.Cell>{payout.recipient}</Table.Cell>
|
|
452
|
+
<Table.Cell>
|
|
453
|
+
<time dateTime={payout.date}>
|
|
454
|
+
{new Date(`${payout.date}T12:00:00`).toLocaleDateString(undefined, {
|
|
455
|
+
day: 'numeric',
|
|
456
|
+
month: 'short',
|
|
457
|
+
})}
|
|
458
|
+
</time>
|
|
459
|
+
</Table.Cell>
|
|
460
|
+
<Table.Cell>
|
|
461
|
+
<Badge size="small">{payout.status}</Badge>
|
|
462
|
+
</Table.Cell>
|
|
463
|
+
<Table.Cell justify="end" numeric>
|
|
464
|
+
<Amount align="right" currency="USD" value={payout.amount} />
|
|
465
|
+
</Table.Cell>
|
|
466
|
+
</Table.Row>
|
|
467
|
+
)
|
|
468
|
+
})}
|
|
469
|
+
</Table.Body>
|
|
470
|
+
</Table.Root>
|
|
471
|
+
</Surface>
|
|
472
|
+
</Inset>
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
namespace styles {
|
|
477
|
+
export const actions = style({ alignItems: 'center', columnGap: '8', display: 'flex' })
|
|
478
|
+
|
|
479
|
+
export const check = style({ width: '1px !custom' })
|
|
480
|
+
|
|
481
|
+
export const count = style({ color: 'content.secondary', typography: 'body.b3' })
|
|
482
|
+
|
|
483
|
+
export const region = style({ backgroundColor: 'background.primary', overflowX: 'auto' })
|
|
484
|
+
|
|
485
|
+
export const row = style({
|
|
486
|
+
selectors: { '&[data-selected]': { backgroundColor: 'container.regular' } },
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
export const sort = style({
|
|
490
|
+
alignItems: 'center',
|
|
491
|
+
appearance: 'none !custom',
|
|
492
|
+
backgroundColor: 'transparent !custom',
|
|
493
|
+
border: 'none !custom',
|
|
494
|
+
borderRadius: '3xs',
|
|
495
|
+
color: 'inherit !custom',
|
|
496
|
+
columnGap: '4',
|
|
497
|
+
cursor: 'pointer',
|
|
498
|
+
display: 'inline-flex',
|
|
499
|
+
font: 'inherit !custom',
|
|
500
|
+
outlineColor: 'transparent !custom',
|
|
501
|
+
outlineOffset: '2',
|
|
502
|
+
outlineStyle: 'solid !custom',
|
|
503
|
+
outlineWidth: '2',
|
|
504
|
+
padding: '0',
|
|
505
|
+
transitionDuration: '160',
|
|
506
|
+
transitionProperty: 'color, outline-color',
|
|
507
|
+
transitionTimingFunction: 'snappy',
|
|
508
|
+
selectors: {
|
|
509
|
+
'&:focus-visible': { outlineColor: 'border.focus' },
|
|
510
|
+
'&:hover': { color: 'content.primary' },
|
|
511
|
+
},
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
export const surface = style({ minWidth: '36rem !custom' })
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/TablesSelection.tsx)
|
|
519
|
+
|
|
520
|
+
* **Name Each Checkbox:** Name each checkbox by its row, such as Select payout to Ada Lovelace.
|
|
521
|
+
* **Header Checkbox (indeterminate):** The header checkbox selects every row on the page. It shows this state while only some rows are selected.
|
|
522
|
+
* **Selection Count:** Show the count, such as 2 selected, beside the bulk actions and a Clear action.
|
|
523
|
+
* **Select All Matching:** If more pages exist, state that the header selects this page only. Offer to select all matches.
|
|
524
|
+
|
|
525
|
+
Confirm a destructive bulk action in an [AlertDialog](/docs/components/alert-dialog) that names the count, such as Cancel 2 payouts, as [Destructive Actions](/docs/patterns/destructive-actions) describes.
|
|
526
|
+
|
|
527
|
+
## Sort Columns
|
|
528
|
+
|
|
529
|
+
[Table](/docs/components/table) has no built-in sorting. Put a `button` inside each sortable `Table.HeaderCell`. Set `aria-sort` to `ascending` or `descending` on the sorted column.
|
|
530
|
+
|
|
531
|
+
* **Direction:** Show the sort direction with an arrow, and mark other sortable columns with the sort icon.
|
|
532
|
+
* **Default order:** Sort activity newest first. A first click on a numeric column sorts largest first.
|
|
533
|
+
* **Whole data set:** Sort every record, not only the rows on the current page.
|
|
534
|
+
|
|
535
|
+
## Paginate or Load More
|
|
536
|
+
|
|
537
|
+
* **Pagination (Pagination):** For lists revisited by position or shared, such as invoices, audit logs, or blocks. Give each page a URL.
|
|
538
|
+
* **Load More:** For feeds read from the top, such as wallet activity. Append the next batch below the current rows.
|
|
539
|
+
* **Infinite Scroll (Avoid):** On the web, it hides the footer and leaves no position to return to.
|
|
540
|
+
|
|
541
|
+
Show the range beside either control, such as Showing 20 of 248. After Load more, keep focus on the button.
|
|
542
|
+
|
|
543
|
+
Below 640px, show Previous and Next with a Page 1 of 3 label between them, as in the stacked example. Use `medium` pagination for touch targets.
|
|
544
|
+
|
|
545
|
+
## Choose a Density
|
|
546
|
+
|
|
547
|
+
* **Small (32px):** For dense consoles and dashboards with many rows. Pair with small buttons.
|
|
548
|
+
* **Medium (40px):** The default size, for most tables in consoles and settings.
|
|
549
|
+
* **Large (56px):** For touch-first screens and rows with two lines or an [Avatar](/docs/components/avatar).
|
|
550
|
+
|
|
551
|
+
Use one density per table. [ListGroup](/docs/components/list-group) keep one 36px height for key-value facts.
|
|
552
|
+
|
|
553
|
+
## Keep Headers in View
|
|
554
|
+
|
|
555
|
+
If a long table scrolls, set `sticky` on `Table.Head`. This works inside a container with a fixed height or with the page. Header cells take the surface background, so rows pass beneath them. An ancestor with `overflow: hidden` stops the header from sticking.
|
|
556
|
+
|
|
557
|
+
## States
|
|
558
|
+
|
|
559
|
+
Keep the headers and filters in place in every state, so the table does not jump.
|
|
560
|
+
|
|
561
|
+
```tsx
|
|
562
|
+
import { Amount, Button, ChoiceGroup, Empty, Hex, Skeleton, Stack, style, Table } from '@tempoxyz/ds'
|
|
563
|
+
import { History } from '@tempoxyz/ds/icons'
|
|
564
|
+
import { useState } from 'react'
|
|
565
|
+
|
|
566
|
+
export const fill = true
|
|
567
|
+
const activity = [
|
|
568
|
+
['0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060', 'Sent', '-1200.00'],
|
|
569
|
+
['0x91bd07e1c3e05a2277a1c0d94f2a91c3b6a900fa2b585dd299e03d12fa4293bc', 'Received', '4005.00'],
|
|
570
|
+
['0xc3e05a2291bd07e14f2a91c3b6a900fa2b585dd299e03d12fa4293bc77a1c0d9', 'Sent', '-48.50'],
|
|
571
|
+
['0x77a1c0d94f2a91c3b6a900fa2b585dd299e03d12fa4293bcc3e05a2291bd07e1', 'Received', '250.00'],
|
|
572
|
+
['0x2b585dd299e03d12fa4293bc77a1c0d94f2a91c3b6a900fac3e05a2291bd07e1', 'Sent', '-9.99'],
|
|
573
|
+
['0xd299e03d12fa4293bc77a1c0d94f2a91c3b6a900fa2b585dc3e05a2291bd07e1', 'Received', '1250.50'],
|
|
574
|
+
['0x4f2a91c3b6a900fa2b585dd299e03d12fa4293bc77a1c0d9c3e05a2291bd07e1', 'Sent', '-310.00'],
|
|
575
|
+
['0xb6a900fa2b585dd299e03d12fa4293bc77a1c0d94f2a91c3c3e05a2291bd07e1', 'Received', '75.25'],
|
|
576
|
+
] as const
|
|
577
|
+
|
|
578
|
+
type State = 'empty' | 'loaded' | 'loading'
|
|
579
|
+
|
|
580
|
+
export function Demo() {
|
|
581
|
+
const [state, setState] = useState<State>('loaded')
|
|
582
|
+
|
|
583
|
+
return (
|
|
584
|
+
<Stack gap="16">
|
|
585
|
+
<ChoiceGroup.Root
|
|
586
|
+
aria-label="Table state"
|
|
587
|
+
onValueChange={(value) => setState(value as State)}
|
|
588
|
+
size="small"
|
|
589
|
+
value={state}
|
|
590
|
+
>
|
|
591
|
+
<ChoiceGroup.Item value="loaded">Loaded</ChoiceGroup.Item>
|
|
592
|
+
<ChoiceGroup.Item value="loading">Loading</ChoiceGroup.Item>
|
|
593
|
+
<ChoiceGroup.Item value="empty">Empty</ChoiceGroup.Item>
|
|
594
|
+
</ChoiceGroup.Root>
|
|
595
|
+
<div aria-labelledby="activity-title" role="region" tabIndex={0} {...styles.scroll()}>
|
|
596
|
+
<Table.Root aria-busy={state === 'loading' || undefined} size="small">
|
|
597
|
+
<Table.Caption id="activity-title">Recent activity</Table.Caption>
|
|
598
|
+
<Table.Head sticky>
|
|
599
|
+
<Table.Row>
|
|
600
|
+
<Table.HeaderCell>Transaction</Table.HeaderCell>
|
|
601
|
+
<Table.HeaderCell>Type</Table.HeaderCell>
|
|
602
|
+
<Table.HeaderCell justify="end">Amount</Table.HeaderCell>
|
|
603
|
+
</Table.Row>
|
|
604
|
+
</Table.Head>
|
|
605
|
+
<Table.Body>
|
|
606
|
+
{state === 'loaded' &&
|
|
607
|
+
activity.map(([hash, type, amount]) => (
|
|
608
|
+
<Table.Row key={hash}>
|
|
609
|
+
<Table.Cell>
|
|
610
|
+
<div {...styles.hash()}>
|
|
611
|
+
<Hex value={hash} />
|
|
612
|
+
</div>
|
|
613
|
+
</Table.Cell>
|
|
614
|
+
<Table.Cell>{type}</Table.Cell>
|
|
615
|
+
<Table.Cell numeric>
|
|
616
|
+
<Amount
|
|
617
|
+
align="right"
|
|
618
|
+
sign={amount.startsWith('-') ? '−' : '+'}
|
|
619
|
+
symbol="USDC"
|
|
620
|
+
value={amount.replace('-', '')}
|
|
621
|
+
/>
|
|
622
|
+
</Table.Cell>
|
|
623
|
+
</Table.Row>
|
|
624
|
+
))}
|
|
625
|
+
{state === 'loading' &&
|
|
626
|
+
['60%', '45%', '70%', '50%'].map((width) => (
|
|
627
|
+
<Table.Row key={width}>
|
|
628
|
+
<Table.Cell>
|
|
629
|
+
<Skeleton shape="pill" style={{ height: 12, width }} />
|
|
630
|
+
</Table.Cell>
|
|
631
|
+
<Table.Cell>
|
|
632
|
+
<Skeleton shape="pill" style={{ height: 12, width: '4rem' }} />
|
|
633
|
+
</Table.Cell>
|
|
634
|
+
<Table.Cell>
|
|
635
|
+
<Skeleton
|
|
636
|
+
shape="pill"
|
|
637
|
+
style={{ height: 12, marginInlineStart: 'auto', width: '5rem' }}
|
|
638
|
+
/>
|
|
639
|
+
</Table.Cell>
|
|
640
|
+
</Table.Row>
|
|
641
|
+
))}
|
|
642
|
+
{state === 'empty' && (
|
|
643
|
+
<Table.Row>
|
|
644
|
+
<Table.Cell colSpan={3}>
|
|
645
|
+
<Empty.Root>
|
|
646
|
+
<Empty.Icon>
|
|
647
|
+
<History />
|
|
648
|
+
</Empty.Icon>
|
|
649
|
+
<Empty.Title>No activity yet</Empty.Title>
|
|
650
|
+
<Empty.Description>Payments you send or receive appear here.</Empty.Description>
|
|
651
|
+
<Empty.Actions>
|
|
652
|
+
<Button size="small">Add funds</Button>
|
|
653
|
+
</Empty.Actions>
|
|
654
|
+
</Empty.Root>
|
|
655
|
+
</Table.Cell>
|
|
656
|
+
</Table.Row>
|
|
657
|
+
)}
|
|
658
|
+
</Table.Body>
|
|
659
|
+
</Table.Root>
|
|
660
|
+
</div>
|
|
661
|
+
{state === 'loaded' && (
|
|
662
|
+
<div {...styles.footer()}>
|
|
663
|
+
<span {...styles.count()}>Showing 8 of 24</span>
|
|
664
|
+
<Button size="small" variant="secondary">
|
|
665
|
+
Load more
|
|
666
|
+
</Button>
|
|
667
|
+
</div>
|
|
668
|
+
)}
|
|
669
|
+
</Stack>
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
namespace styles {
|
|
674
|
+
export const count = style({ color: 'content.secondary', typography: 'body.b3' })
|
|
675
|
+
|
|
676
|
+
export const footer = style({
|
|
677
|
+
alignItems: 'center',
|
|
678
|
+
display: 'flex',
|
|
679
|
+
flexWrap: 'wrap',
|
|
680
|
+
gap: '12',
|
|
681
|
+
justifyContent: 'space-between',
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
export const hash = style({ maxWidth: '12rem !custom' })
|
|
685
|
+
|
|
686
|
+
export const scroll = style({
|
|
687
|
+
backgroundColor: 'background.elevated',
|
|
688
|
+
borderColor: 'line.secondary',
|
|
689
|
+
borderRadius: 'sm',
|
|
690
|
+
borderStyle: 'solid',
|
|
691
|
+
borderWidth: 'regular',
|
|
692
|
+
maxHeight: '18rem !custom',
|
|
693
|
+
overflowY: 'auto',
|
|
694
|
+
})
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/TablesStates.tsx)
|
|
699
|
+
|
|
700
|
+
* **Loading (aria-busy):** Keep the headers and show three to five placeholder rows. Mark the table busy.
|
|
701
|
+
* **Empty (Empty):** If no records exist yet, offer the action that creates the first one. [Empty States](/docs/patterns/empty-states) covers the variants.
|
|
702
|
+
* **Error (negative):** Show a negative [Alert](/docs/components/alert) above the table with Retry. Keep rows that already loaded.
|
|
703
|
+
* **Updating:** Keep the current rows while a sort or filter loads. Replace them when the new rows arrive.
|
|
704
|
+
|
|
705
|
+
**Do:** Explain an empty result in a row that spans every column, with a way out.
|
|
706
|
+
|
|
707
|
+
```tsx
|
|
708
|
+
/**
|
|
709
|
+
* Do example for "Tables and Lists" on the Tables and Lists page.
|
|
710
|
+
* Explain an empty result in a row that spans every column, with a way out.
|
|
711
|
+
* @module
|
|
712
|
+
*/
|
|
713
|
+
import { Button, style, Table } from '@tempoxyz/ds'
|
|
714
|
+
|
|
715
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
716
|
+
export function Demo() {
|
|
717
|
+
return (
|
|
718
|
+
<Table.Root size="small" {...styles.table()}>
|
|
719
|
+
<Table.Head>
|
|
720
|
+
<Table.Row>
|
|
721
|
+
<Table.HeaderCell>Key</Table.HeaderCell>
|
|
722
|
+
<Table.HeaderCell>Last used</Table.HeaderCell>
|
|
723
|
+
</Table.Row>
|
|
724
|
+
</Table.Head>
|
|
725
|
+
<Table.Body>
|
|
726
|
+
<Table.Row>
|
|
727
|
+
<Table.Cell colSpan={2}>
|
|
728
|
+
<div {...styles.empty()}>
|
|
729
|
+
<span {...styles.text()}>No keys match Staging.</span>
|
|
730
|
+
<Button size="small" variant="secondary">
|
|
731
|
+
Clear filter
|
|
732
|
+
</Button>
|
|
733
|
+
</div>
|
|
734
|
+
</Table.Cell>
|
|
735
|
+
</Table.Row>
|
|
736
|
+
</Table.Body>
|
|
737
|
+
</Table.Root>
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
namespace styles {
|
|
742
|
+
export const empty = style({
|
|
743
|
+
alignItems: 'center',
|
|
744
|
+
display: 'flex',
|
|
745
|
+
flexDirection: 'column',
|
|
746
|
+
paddingBlock: '16',
|
|
747
|
+
rowGap: '12',
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
export const table = style({ maxWidth: '17rem !custom' })
|
|
751
|
+
|
|
752
|
+
export const text = style({ color: 'content.secondary', typography: 'body.b2' })
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
**Don't:** Leave headers above an empty body.
|
|
757
|
+
|
|
758
|
+
```tsx
|
|
759
|
+
/**
|
|
760
|
+
* Don't example for "Tables and Lists" on the Tables and Lists page.
|
|
761
|
+
* Don't leave headers above an empty body.
|
|
762
|
+
* @module
|
|
763
|
+
*/
|
|
764
|
+
import { style, Table } from '@tempoxyz/ds'
|
|
765
|
+
|
|
766
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
767
|
+
export function Demo() {
|
|
768
|
+
return (
|
|
769
|
+
<Table.Root size="small" {...styles.table()}>
|
|
770
|
+
<Table.Head>
|
|
771
|
+
<Table.Row>
|
|
772
|
+
<Table.HeaderCell>Key</Table.HeaderCell>
|
|
773
|
+
<Table.HeaderCell>Last used</Table.HeaderCell>
|
|
774
|
+
</Table.Row>
|
|
775
|
+
</Table.Head>
|
|
776
|
+
<Table.Body />
|
|
777
|
+
</Table.Root>
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
namespace styles {
|
|
782
|
+
export const table = style({ maxWidth: '17rem !custom' })
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
## Below 640px
|
|
787
|
+
|
|
788
|
+
Tables with more than two or three columns do not fit a phone. Turn each row into a stacked list item: the main text and amount on the first line, the date and status beneath. Below 640px, the preview switches layouts.
|
|
789
|
+
|
|
790
|
+
```tsx
|
|
791
|
+
import { Amount, Badge, ListGroup, Pagination, Stack, style, Surface, Table } from '@tempoxyz/ds'
|
|
792
|
+
|
|
793
|
+
export const fill = true
|
|
794
|
+
const payments = [
|
|
795
|
+
{ amount: '1200.00', date: 'Oct 20', recipient: 'Ada Lovelace', status: 'Confirmed' },
|
|
796
|
+
{ amount: '48.50', date: 'Oct 19', recipient: 'Grace Hopper', status: 'Confirmed' },
|
|
797
|
+
{ amount: '310.00', date: 'Oct 18', recipient: 'Alan Turing', status: 'Pending' },
|
|
798
|
+
{ amount: '9.99', date: 'Oct 18', recipient: 'Katherine Johnson', status: 'Failed' },
|
|
799
|
+
] as const
|
|
800
|
+
|
|
801
|
+
const intents = { Confirmed: 'positive', Failed: 'negative', Pending: 'warning' } as const
|
|
802
|
+
|
|
803
|
+
export function Demo() {
|
|
804
|
+
return (
|
|
805
|
+
<Stack gap="16" {...styles.demo()}>
|
|
806
|
+
<h2 id="payments-title" {...styles.title()}>
|
|
807
|
+
Payments
|
|
808
|
+
</h2>
|
|
809
|
+
{/* Both layouts render, and `display: none` keeps only the visible one in the accessibility tree. */}
|
|
810
|
+
<div {...styles.wide()}>
|
|
811
|
+
<Surface>
|
|
812
|
+
<Table.Root aria-labelledby="payments-title">
|
|
813
|
+
<Table.Head>
|
|
814
|
+
<Table.Row>
|
|
815
|
+
<Table.HeaderCell>Recipient</Table.HeaderCell>
|
|
816
|
+
<Table.HeaderCell>Date</Table.HeaderCell>
|
|
817
|
+
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
818
|
+
<Table.HeaderCell justify="end">Amount</Table.HeaderCell>
|
|
819
|
+
</Table.Row>
|
|
820
|
+
</Table.Head>
|
|
821
|
+
<Table.Body>
|
|
822
|
+
{payments.map((payment) => (
|
|
823
|
+
<Table.Row key={payment.recipient}>
|
|
824
|
+
<Table.Cell>{payment.recipient}</Table.Cell>
|
|
825
|
+
<Table.Cell>{payment.date}</Table.Cell>
|
|
826
|
+
<Table.Cell status={intents[payment.status]}>{payment.status}</Table.Cell>
|
|
827
|
+
<Table.Cell numeric>
|
|
828
|
+
<Amount align="right" currency="USD" value={payment.amount} />
|
|
829
|
+
</Table.Cell>
|
|
830
|
+
</Table.Row>
|
|
831
|
+
))}
|
|
832
|
+
</Table.Body>
|
|
833
|
+
</Table.Root>
|
|
834
|
+
</Surface>
|
|
835
|
+
</div>
|
|
836
|
+
<div {...styles.narrow()}>
|
|
837
|
+
<Surface size="small">
|
|
838
|
+
<ListGroup.Root aria-labelledby="payments-title" role="list">
|
|
839
|
+
{payments.map((payment) => (
|
|
840
|
+
<ListGroup.Row key={payment.recipient} role="listitem">
|
|
841
|
+
<ListGroup.Info>
|
|
842
|
+
<ListGroup.Title>{payment.recipient}</ListGroup.Title>
|
|
843
|
+
<ListGroup.Description>{payment.date}</ListGroup.Description>
|
|
844
|
+
</ListGroup.Info>
|
|
845
|
+
<span {...styles.end()}>
|
|
846
|
+
<Amount currency="USD" value={payment.amount} />
|
|
847
|
+
<Badge size="small">{payment.status}</Badge>
|
|
848
|
+
</span>
|
|
849
|
+
</ListGroup.Row>
|
|
850
|
+
))}
|
|
851
|
+
</ListGroup.Root>
|
|
852
|
+
</Surface>
|
|
853
|
+
</div>
|
|
854
|
+
<div {...styles.footer()}>
|
|
855
|
+
<span {...styles.count()}>1–4 of 12</span>
|
|
856
|
+
<div {...styles.wide()}>
|
|
857
|
+
<Pagination>
|
|
858
|
+
<Pagination.Previous disabled />
|
|
859
|
+
<Pagination.Page current href="#">
|
|
860
|
+
1
|
|
861
|
+
</Pagination.Page>
|
|
862
|
+
<Pagination.Page href="#">2</Pagination.Page>
|
|
863
|
+
<Pagination.Page href="#">3</Pagination.Page>
|
|
864
|
+
<Pagination.Next href="#" />
|
|
865
|
+
</Pagination>
|
|
866
|
+
</div>
|
|
867
|
+
<div {...styles.narrow()}>
|
|
868
|
+
<Pagination>
|
|
869
|
+
<Pagination.Previous disabled />
|
|
870
|
+
<Pagination.Ellipsis>Page 1 of 3</Pagination.Ellipsis>
|
|
871
|
+
<Pagination.Next href="#" />
|
|
872
|
+
</Pagination>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
</Stack>
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
namespace styles {
|
|
880
|
+
export const count = style({
|
|
881
|
+
color: 'content.secondary',
|
|
882
|
+
display: 'none',
|
|
883
|
+
typography: 'body.b3',
|
|
884
|
+
'@media 640': { display: 'inline' },
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
export const demo = style({ backgroundColor: 'background.primary', padding: '16' })
|
|
888
|
+
|
|
889
|
+
export const end = style({
|
|
890
|
+
alignItems: 'flex-end',
|
|
891
|
+
display: 'flex',
|
|
892
|
+
flexDirection: 'column',
|
|
893
|
+
flexShrink: '0 !custom',
|
|
894
|
+
rowGap: '4',
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
export const footer = style({
|
|
898
|
+
alignItems: 'center',
|
|
899
|
+
display: 'flex',
|
|
900
|
+
flexWrap: 'wrap',
|
|
901
|
+
gap: '12',
|
|
902
|
+
justifyContent: 'space-between',
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
export const narrow = style({ '@media 640': { display: 'none' } })
|
|
906
|
+
|
|
907
|
+
export const title = style({ margin: '0', typography: 'heading.h3' })
|
|
908
|
+
|
|
909
|
+
export const wide = style({ display: 'none', '@media 640': { display: 'block' } })
|
|
910
|
+
}
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/TablesResponsive.tsx)
|
|
914
|
+
|
|
915
|
+
* **Switch Layouts (sm):** Render a table from this breakpoint and a list below it. Only the visible layout stays in the accessibility tree.
|
|
916
|
+
* **Same Records and Actions:** Keep records, order, and actions the same in both layouts. Put columns that do not fit in the detail view.
|
|
917
|
+
* **Scroll to Compare (tabIndex=0):** Keep a table only to compare values across columns, and [scroll it inside its own region](/docs/patterns/responsive-layout#scroll-wide-content-inside-its-own-region).
|
|
918
|
+
|
|
919
|
+
## Content
|
|
920
|
+
|
|
921
|
+
* **Headers:** Put the unit of a metric in its header, such as p99 (ms). Keep the token on each amount, because one table can mix assets.
|
|
922
|
+
* **Missing values:** Show a missing value as a word, such as None or Never, instead of an empty cell.
|
|
923
|
+
* **Dates:** Format dates the same way in every row, as [Blockchain Values](/docs/patterns/blockchain-values#timestamps) describes, and keep each date on one line.
|
|
924
|
+
|
|
925
|
+
## Accessibility
|
|
926
|
+
|
|
927
|
+
* **Native Table (scope=col):** A native table with column headers. Pass `scope="row"` for a row header. Do not use it for layout.
|
|
928
|
+
* **Clickable Rows (interactive):** Adds hover and focus styles only. Put a link or button in each clickable row.
|
|
929
|
+
* **Live Counts (status):** Announce the selection count and loaded batches through an always-rendered status region.
|
|
930
|
+
* **Status Text (status):** Colors text only. The text must state the status, such as Failed.
|
|
931
|
+
|
|
932
|
+
## Related
|
|
933
|
+
|
|
934
|
+
[Table](/docs/components/table)
|
|
935
|
+
|
|
936
|
+
Use for tabular data.
|
|
937
|
+
|
|
938
|
+
[ListGroup](/docs/components/list-group)
|
|
939
|
+
|
|
940
|
+
Use for details, settings, and stacked items.
|
|
941
|
+
|
|
942
|
+
[Pagination](/docs/components/pagination)
|
|
943
|
+
|
|
944
|
+
Use for paged lists.
|
|
945
|
+
|
|
946
|
+
[Checkbox](/docs/components/checkbox)
|
|
947
|
+
|
|
948
|
+
Use for row selection.
|
|
949
|
+
|
|
950
|
+
[Responsive Layout](/docs/patterns/responsive-layout)
|
|
951
|
+
|
|
952
|
+
Covers the shared breakpoints.
|
|
953
|
+
|
|
954
|
+
[Loading](/docs/patterns/loading)
|
|
955
|
+
|
|
956
|
+
Covers skeletons and spinners.
|
|
957
|
+
|
|
958
|
+
[Empty States](/docs/patterns/empty-states)
|
|
959
|
+
|
|
960
|
+
Covers first use and no results.
|
|
961
|
+
|
|
962
|
+
[Destructive Actions](/docs/patterns/destructive-actions)
|
|
963
|
+
|
|
964
|
+
Covers confirming bulk actions.
|