@tempoxyz/ds 0.3.10 → 0.3.13

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.
Files changed (210) hide show
  1. package/README.md +7 -6
  2. package/dist/docs/AGENTS.md +12 -0
  3. package/dist/docs/index.md +145 -0
  4. package/dist/docs/marketing/introduction/getting-started.md +241 -0
  5. package/dist/docs/product/components/accordion.md +489 -0
  6. package/dist/docs/product/components/address-card.md +147 -0
  7. package/dist/docs/product/components/agent-logo.md +194 -0
  8. package/dist/docs/product/components/alert-dialog.md +631 -0
  9. package/dist/docs/product/components/alert.md +448 -0
  10. package/dist/docs/product/components/amount-input.md +305 -0
  11. package/dist/docs/product/components/amount.md +794 -0
  12. package/dist/docs/product/components/app-shell.md +559 -0
  13. package/dist/docs/product/components/asset-chip.md +294 -0
  14. package/dist/docs/product/components/autocomplete.md +620 -0
  15. package/dist/docs/product/components/avatar.md +321 -0
  16. package/dist/docs/product/components/badge.md +339 -0
  17. package/dist/docs/product/components/badged-logo.md +116 -0
  18. package/dist/docs/product/components/banner.md +379 -0
  19. package/dist/docs/product/components/bar-chart.md +208 -0
  20. package/dist/docs/product/components/breadcrumbs.md +380 -0
  21. package/dist/docs/product/components/button-group.md +286 -0
  22. package/dist/docs/product/components/button.md +650 -0
  23. package/dist/docs/product/components/calendar.md +375 -0
  24. package/dist/docs/product/components/callout.md +96 -0
  25. package/dist/docs/product/components/checkbox-group.md +521 -0
  26. package/dist/docs/product/components/checkbox.md +467 -0
  27. package/dist/docs/product/components/choice-group.md +516 -0
  28. package/dist/docs/product/components/choicebox.md +641 -0
  29. package/dist/docs/product/components/code.md +100 -0
  30. package/dist/docs/product/components/combobox.md +939 -0
  31. package/dist/docs/product/components/container.md +257 -0
  32. package/dist/docs/product/components/copy-button.md +642 -0
  33. package/dist/docs/product/components/date-picker.md +514 -0
  34. package/dist/docs/product/components/dialog.md +1332 -0
  35. package/dist/docs/product/components/drawer.md +966 -0
  36. package/dist/docs/product/components/empty.md +340 -0
  37. package/dist/docs/product/components/field.md +570 -0
  38. package/dist/docs/product/components/fieldset.md +398 -0
  39. package/dist/docs/product/components/frame.md +434 -0
  40. package/dist/docs/product/components/gauge.md +299 -0
  41. package/dist/docs/product/components/get-started-visual.md +117 -0
  42. package/dist/docs/product/components/grid.md +288 -0
  43. package/dist/docs/product/components/hero-visual.md +155 -0
  44. package/dist/docs/product/components/hex.md +487 -0
  45. package/dist/docs/product/components/icon-tile.md +292 -0
  46. package/dist/docs/product/components/identicon.md +371 -0
  47. package/dist/docs/product/components/inline.md +233 -0
  48. package/dist/docs/product/components/inset.md +225 -0
  49. package/dist/docs/product/components/kbd.md +423 -0
  50. package/dist/docs/product/components/list-group.md +981 -0
  51. package/dist/docs/product/components/loading-dots.md +237 -0
  52. package/dist/docs/product/components/menu.md +934 -0
  53. package/dist/docs/product/components/native-select.md +204 -0
  54. package/dist/docs/product/components/network-logo.md +248 -0
  55. package/dist/docs/product/components/number-field.md +543 -0
  56. package/dist/docs/product/components/otp-field.md +616 -0
  57. package/dist/docs/product/components/pagination.md +516 -0
  58. package/dist/docs/product/components/popover.md +544 -0
  59. package/dist/docs/product/components/progress.md +388 -0
  60. package/dist/docs/product/components/qr.md +314 -0
  61. package/dist/docs/product/components/radio-group.md +528 -0
  62. package/dist/docs/product/components/secret-value.md +406 -0
  63. package/dist/docs/product/components/select.md +751 -0
  64. package/dist/docs/product/components/separator.md +379 -0
  65. package/dist/docs/product/components/sidebar-menu.md +922 -0
  66. package/dist/docs/product/components/skeleton.md +517 -0
  67. package/dist/docs/product/components/slider.md +539 -0
  68. package/dist/docs/product/components/snippet.md +424 -0
  69. package/dist/docs/product/components/spinner.md +216 -0
  70. package/dist/docs/product/components/split-bar.md +139 -0
  71. package/dist/docs/product/components/split-button.md +464 -0
  72. package/dist/docs/product/components/stack.md +299 -0
  73. package/dist/docs/product/components/status-chip.md +226 -0
  74. package/dist/docs/product/components/status-indicator.md +119 -0
  75. package/dist/docs/product/components/status-row.md +174 -0
  76. package/dist/docs/product/components/stepper.md +451 -0
  77. package/dist/docs/product/components/success-hero.md +157 -0
  78. package/dist/docs/product/components/surface.md +639 -0
  79. package/dist/docs/product/components/switch.md +472 -0
  80. package/dist/docs/product/components/table.md +967 -0
  81. package/dist/docs/product/components/tabs.md +627 -0
  82. package/dist/docs/product/components/tempo-logo.md +185 -0
  83. package/dist/docs/product/components/text-input.md +624 -0
  84. package/dist/docs/product/components/textarea.md +352 -0
  85. package/dist/docs/product/components/toast.md +535 -0
  86. package/dist/docs/product/components/token-logo.md +371 -0
  87. package/dist/docs/product/components/token.md +247 -0
  88. package/dist/docs/product/components/tooltip.md +490 -0
  89. package/dist/docs/product/components/verification-code.md +116 -0
  90. package/dist/docs/product/components/visual.md +249 -0
  91. package/dist/docs/product/components.md +203 -0
  92. package/dist/docs/product/foundations/accessibility.md +377 -0
  93. package/dist/docs/product/foundations/agents.md +150 -0
  94. package/dist/docs/product/foundations/color.md +522 -0
  95. package/dist/docs/product/foundations/content/tone.md +194 -0
  96. package/dist/docs/product/foundations/content/voice.md +194 -0
  97. package/dist/docs/product/foundations/content/writing-style.md +406 -0
  98. package/dist/docs/product/foundations/iconography.md +282 -0
  99. package/dist/docs/product/foundations/intent.md +360 -0
  100. package/dist/docs/product/foundations/layout.md +508 -0
  101. package/dist/docs/product/foundations/linter.md +496 -0
  102. package/dist/docs/product/foundations/motion.md +151 -0
  103. package/dist/docs/product/foundations/radius-and-padding.md +86 -0
  104. package/dist/docs/product/foundations/typography.md +266 -0
  105. package/dist/docs/product/introduction/faq.md +133 -0
  106. package/dist/docs/product/introduction/getting-started.md +377 -0
  107. package/dist/docs/product/patterns/blockchain-values.md +709 -0
  108. package/dist/docs/product/patterns/dashboards.md +638 -0
  109. package/dist/docs/product/patterns/destructive-actions.md +626 -0
  110. package/dist/docs/product/patterns/empty-states.md +612 -0
  111. package/dist/docs/product/patterns/errors-and-validation.md +808 -0
  112. package/dist/docs/product/patterns/feedback-and-status.md +518 -0
  113. package/dist/docs/product/patterns/forms.md +679 -0
  114. package/dist/docs/product/patterns/loading.md +630 -0
  115. package/dist/docs/product/patterns/navigation.md +397 -0
  116. package/dist/docs/product/patterns/payments.md +765 -0
  117. package/dist/docs/product/patterns/responsive-layout.md +997 -0
  118. package/dist/docs/product/patterns/tables-and-lists.md +964 -0
  119. package/dist/docs/product/products/tempo-console.md +671 -0
  120. package/dist/docs/product/products/tempo-performance.md +437 -0
  121. package/dist/docs/product/products/tempo-wallet.md +438 -0
  122. package/dist/marketing/index.d.ts +4 -0
  123. package/dist/marketing/index.d.ts.map +1 -0
  124. package/dist/marketing/index.js +4 -0
  125. package/dist/marketing/index.js.map +1 -0
  126. package/dist/marketing/index.js.zyzz.json +1 -0
  127. package/dist/marketing/vars.d.ts +907 -0
  128. package/dist/marketing/vars.d.ts.map +1 -0
  129. package/dist/marketing/vars.js +15 -0
  130. package/dist/marketing/vars.js.map +1 -0
  131. package/dist/marketing/vars.js.zyzz.json +1 -0
  132. package/dist/marketing/zyzz.config.d.ts +4512 -0
  133. package/dist/marketing/zyzz.config.d.ts.map +1 -0
  134. package/dist/marketing/zyzz.config.js +63 -0
  135. package/dist/marketing/zyzz.config.js.map +1 -0
  136. package/dist/marketing/zyzz.config.js.zyzz.json +1 -0
  137. package/dist/oxlint/index.d.ts +12 -1
  138. package/dist/oxlint/index.d.ts.map +1 -1
  139. package/dist/oxlint/index.js +39 -1
  140. package/dist/oxlint/index.js.map +1 -1
  141. package/dist/oxlint/internal/Docs.d.ts +9 -0
  142. package/dist/oxlint/internal/Docs.d.ts.map +1 -0
  143. package/dist/oxlint/internal/Docs.js +15 -0
  144. package/dist/oxlint/internal/Docs.js.map +1 -0
  145. package/dist/oxlint/internal/Imports.d.ts +1 -1
  146. package/dist/oxlint/internal/Imports.d.ts.map +1 -1
  147. package/dist/oxlint/internal/Imports.js +2 -0
  148. package/dist/oxlint/internal/Imports.js.map +1 -1
  149. package/dist/oxlint/internal/Styles.d.ts +40 -0
  150. package/dist/oxlint/internal/Styles.d.ts.map +1 -0
  151. package/dist/oxlint/internal/Styles.js +194 -0
  152. package/dist/oxlint/internal/Styles.js.map +1 -0
  153. package/dist/oxlint/internal/Tokens.d.ts +16 -0
  154. package/dist/oxlint/internal/Tokens.d.ts.map +1 -0
  155. package/dist/oxlint/internal/Tokens.js +39 -0
  156. package/dist/oxlint/internal/Tokens.js.map +1 -0
  157. package/dist/oxlint/rules/NoCrossSurface.d.ts +2 -2
  158. package/dist/oxlint/rules/NoCrossSurface.js +3 -3
  159. package/dist/oxlint/rules/NoCrossSurface.js.map +1 -1
  160. package/dist/oxlint/rules/NoCustomValues.d.ts +11 -0
  161. package/dist/oxlint/rules/NoCustomValues.d.ts.map +1 -0
  162. package/dist/oxlint/rules/NoCustomValues.js +150 -0
  163. package/dist/oxlint/rules/NoCustomValues.js.map +1 -0
  164. package/dist/oxlint/rules/NoSingleKeyShortcuts.d.ts +11 -0
  165. package/dist/oxlint/rules/NoSingleKeyShortcuts.d.ts.map +1 -0
  166. package/dist/oxlint/rules/NoSingleKeyShortcuts.js +158 -0
  167. package/dist/oxlint/rules/NoSingleKeyShortcuts.js.map +1 -0
  168. package/dist/oxlint/rules/PreferComponent.d.ts +3 -2
  169. package/dist/oxlint/rules/PreferComponent.d.ts.map +1 -1
  170. package/dist/oxlint/rules/PreferComponent.js +11 -4
  171. package/dist/oxlint/rules/PreferComponent.js.map +1 -1
  172. package/dist/oxlint/rules/PreferLayout.d.ts +9 -0
  173. package/dist/oxlint/rules/PreferLayout.d.ts.map +1 -0
  174. package/dist/oxlint/rules/PreferLayout.js +90 -0
  175. package/dist/oxlint/rules/PreferLayout.js.map +1 -0
  176. package/dist/oxlint/rules/PreferProgress.d.ts +9 -0
  177. package/dist/oxlint/rules/PreferProgress.d.ts.map +1 -0
  178. package/dist/oxlint/rules/PreferProgress.js +43 -0
  179. package/dist/oxlint/rules/PreferProgress.js.map +1 -0
  180. package/dist/oxlint/rules/PreferSurface.d.ts +9 -0
  181. package/dist/oxlint/rules/PreferSurface.d.ts.map +1 -0
  182. package/dist/oxlint/rules/PreferSurface.js +55 -0
  183. package/dist/oxlint/rules/PreferSurface.js.map +1 -0
  184. package/dist/oxlint/rules/RequireAccessibleName.d.ts.map +1 -1
  185. package/dist/oxlint/rules/RequireAccessibleName.js +3 -1
  186. package/dist/oxlint/rules/RequireAccessibleName.js.map +1 -1
  187. package/dist/oxlint/rules/RequireScrollRegion.d.ts +11 -0
  188. package/dist/oxlint/rules/RequireScrollRegion.d.ts.map +1 -0
  189. package/dist/oxlint/rules/RequireScrollRegion.js +71 -0
  190. package/dist/oxlint/rules/RequireScrollRegion.js.map +1 -0
  191. package/dist/web/icons/index.js.zyzz.json +1 -0
  192. package/package.json +6 -1
  193. package/src/marketing/README.md +21 -0
  194. package/src/marketing/index.ts +3 -0
  195. package/src/marketing/vars.ts +751 -0
  196. package/src/marketing/zyzz.config.ts +376 -0
  197. package/src/oxlint/index.ts +40 -1
  198. package/src/oxlint/internal/Docs.ts +18 -0
  199. package/src/oxlint/internal/Imports.ts +2 -1
  200. package/src/oxlint/internal/Styles.ts +240 -0
  201. package/src/oxlint/internal/Tokens.ts +43 -0
  202. package/src/oxlint/rules/NoCrossSurface.ts +3 -3
  203. package/src/oxlint/rules/NoCustomValues.ts +170 -0
  204. package/src/oxlint/rules/NoSingleKeyShortcuts.ts +190 -0
  205. package/src/oxlint/rules/PreferComponent.ts +11 -4
  206. package/src/oxlint/rules/PreferLayout.ts +96 -0
  207. package/src/oxlint/rules/PreferProgress.ts +49 -0
  208. package/src/oxlint/rules/PreferSurface.ts +70 -0
  209. package/src/oxlint/rules/RequireAccessibleName.ts +3 -1
  210. package/src/oxlint/rules/RequireScrollRegion.ts +92 -0
@@ -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](../components/list-group.md) 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](../components/surface.md), 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](../components/checkbox.md) 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](../components/alert-dialog.md) that names the count, such as Cancel 2 payouts, as [Destructive Actions](destructive-actions.md) describes.
526
+
527
+ ## Sort Columns
528
+
529
+ [Table](../components/table.md) 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](../components/avatar.md).
550
+
551
+ Use one density per table. [ListGroup](../components/list-group.md) 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](empty-states.md) covers the variants.
702
+ * **Error (negative):** Show a negative [Alert](../components/alert.md) 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](responsive-layout.md#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](blockchain-values.md#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](../components/table.md)
935
+
936
+ Use for tabular data.
937
+
938
+ [ListGroup](../components/list-group.md)
939
+
940
+ Use for details, settings, and stacked items.
941
+
942
+ [Pagination](../components/pagination.md)
943
+
944
+ Use for paged lists.
945
+
946
+ [Checkbox](../components/checkbox.md)
947
+
948
+ Use for row selection.
949
+
950
+ [Responsive Layout](responsive-layout.md)
951
+
952
+ Covers the shared breakpoints.
953
+
954
+ [Loading](loading.md)
955
+
956
+ Covers skeletons and spinners.
957
+
958
+ [Empty States](empty-states.md)
959
+
960
+ Covers first use and no results.
961
+
962
+ [Destructive Actions](destructive-actions.md)
963
+
964
+ Covers confirming bulk actions.