affora 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/CHECKS.md +40 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/agent.md +70 -0
- package/checks/cli.mjs +44 -0
- package/checks/substrate.mjs +269 -0
- package/cli/affora.mjs +131 -0
- package/design.md +113 -0
- package/package.json +71 -0
- package/registry.json +83 -0
- package/rules/antd.js +173 -0
- package/rules/generic.js +299 -0
- package/rules/hidden-radio.js +94 -0
- package/rules/magento.js +325 -0
- package/rules/mui.js +230 -0
- package/rules/shadcn-baseui.js +169 -0
- package/rules/shadcn-radix.js +289 -0
- package/src/components/combobox.tsx +279 -0
- package/src/components/datatable.tsx +454 -0
- package/src/components/dialog.tsx +548 -0
- package/src/components/productcard.tsx +551 -0
- package/src/components/settingsform.tsx +583 -0
- package/src/patterns/confirmundo.tsx +134 -0
- package/src/patterns/errremedy.tsx +155 -0
- package/src/patterns/flowform.tsx +179 -0
- package/src/patterns/gatedaction.tsx +157 -0
- package/src/patterns/persistentfeedback.tsx +106 -0
- package/src/primitives/actionbutton.tsx +64 -0
- package/src/primitives/textfield.tsx +57 -0
- package/src/tokens/themes.css +1068 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import type React from "react"
|
|
2
|
+
import { useId, useMemo, useState } from "react"
|
|
3
|
+
|
|
4
|
+
/* DataTable — a dense orders data table.
|
|
5
|
+
*
|
|
6
|
+
* Agent-legible substrate (never compromised):
|
|
7
|
+
* - real <table> with <caption>, <thead>/<th scope="col">, <tbody>/<td>
|
|
8
|
+
* - sortable columns are real <button>s inside the <th>, toggling aria-sort
|
|
9
|
+
* (none -> ascending -> descending) with a visible caret
|
|
10
|
+
* - every task-relevant value lives as text in the DOM (Name, Role, Status,
|
|
11
|
+
* Last active, Commits); the status pill's meaning is real text, not colour-only
|
|
12
|
+
* - each row's "view" control is a real <button>; clicking it calls onCommit(name)
|
|
13
|
+
*
|
|
14
|
+
* Style-neutral: every visual property is a var(--token). Authored once, rendered
|
|
15
|
+
* under all five themes; the look is entirely carried by the theme wrapper.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type SortKey = "name" | "role" | "status" | "active" | "commits"
|
|
19
|
+
type Dir = "asc" | "desc"
|
|
20
|
+
|
|
21
|
+
type Member = {
|
|
22
|
+
name: string
|
|
23
|
+
role: string
|
|
24
|
+
status: "active" | "away" | "offline"
|
|
25
|
+
/** minutes since last active — sortable key; `activeLabel` is what's displayed */
|
|
26
|
+
activeMins: number
|
|
27
|
+
activeLabel: string
|
|
28
|
+
commits: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ROWS: Member[] = [
|
|
32
|
+
{ name: "Order #1001", role: "TRK-12", status: "active", activeMins: 2, activeLabel: "2 min ago", commits: 1284 },
|
|
33
|
+
{ name: "Order #1002", role: "TRK-88", status: "active", activeMins: 11, activeLabel: "11 min ago", commits: 642 },
|
|
34
|
+
{ name: "Order #1003", role: "TRK-31", status: "away", activeMins: 47, activeLabel: "47 min ago", commits: 98 },
|
|
35
|
+
{ name: "Order #1004", role: "TRK-57", status: "active", activeMins: 5, activeLabel: "5 min ago", commits: 917 },
|
|
36
|
+
{ name: "Order #1005", role: "TRK-09", status: "away", activeMins: 133, activeLabel: "2 hr ago", commits: 405 },
|
|
37
|
+
{ name: "Order #1006", role: "TRK-44", status: "offline", activeMins: 1440, activeLabel: "Yesterday", commits: 1553 },
|
|
38
|
+
{ name: "Order #1007", role: "TRK-73", status: "offline", activeMins: 4320, activeLabel: "3 days ago", commits: 271 },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const STATUS_LABEL: Record<Member["status"], string> = {
|
|
42
|
+
active: "Active",
|
|
43
|
+
away: "Away",
|
|
44
|
+
offline: "Offline",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Rank used only for sorting the Status column (active first).
|
|
48
|
+
const STATUS_RANK: Record<Member["status"], number> = { active: 0, away: 1, offline: 2 }
|
|
49
|
+
|
|
50
|
+
const COLS: { key: SortKey; label: string; sortable: boolean; numeric?: boolean }[] = [
|
|
51
|
+
{ key: "name", label: "Order", sortable: true },
|
|
52
|
+
{ key: "role", label: "Tracking", sortable: true },
|
|
53
|
+
{ key: "status", label: "Status", sortable: true },
|
|
54
|
+
{ key: "active", label: "Last active", sortable: true },
|
|
55
|
+
{ key: "commits", label: "Total", sortable: true, numeric: true },
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const Caret: React.FC<{ dir: Dir | null }> = ({ dir }) => (
|
|
59
|
+
<svg
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
viewBox="0 0 16 16"
|
|
62
|
+
width="1em"
|
|
63
|
+
height="1em"
|
|
64
|
+
style={{
|
|
65
|
+
flex: "0 0 auto",
|
|
66
|
+
opacity: dir ? 1 : 0.28,
|
|
67
|
+
transform: dir === "desc" ? "rotate(180deg)" : "rotate(0deg)",
|
|
68
|
+
transition: "transform var(--dur) var(--ease), opacity var(--dur-fast) var(--ease)",
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{/* An up-chevron; rotates 180° for descending. Muted when column is unsorted. */}
|
|
72
|
+
<path
|
|
73
|
+
d="M4 10.5 L8 6 L12 10.5"
|
|
74
|
+
fill="none"
|
|
75
|
+
stroke="currentColor"
|
|
76
|
+
strokeWidth="1.75"
|
|
77
|
+
strokeLinecap="round"
|
|
78
|
+
strokeLinejoin="round"
|
|
79
|
+
/>
|
|
80
|
+
</svg>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const StatusPill: React.FC<{ status: Member["status"] }> = ({ status }) => {
|
|
84
|
+
const tone =
|
|
85
|
+
status === "active" ? "var(--success)" : status === "away" ? "var(--warning)" : "var(--fg-subtle)"
|
|
86
|
+
return (
|
|
87
|
+
<span
|
|
88
|
+
className="dtf-pill"
|
|
89
|
+
style={{
|
|
90
|
+
// colour-mix keeps the pill fill tied to the semantic token in every theme
|
|
91
|
+
// while staying legible; the text label carries the meaning for agents.
|
|
92
|
+
color: tone,
|
|
93
|
+
borderColor: `color-mix(in srgb, ${tone} 34%, transparent)`,
|
|
94
|
+
background: `color-mix(in srgb, ${tone} 12%, transparent)`,
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<span
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
style={{
|
|
100
|
+
width: "0.5em",
|
|
101
|
+
height: "0.5em",
|
|
102
|
+
borderRadius: "var(--radius-full)",
|
|
103
|
+
background: tone,
|
|
104
|
+
flex: "0 0 auto",
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
{STATUS_LABEL[status]}
|
|
108
|
+
</span>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const DataTable: React.FC<{ onCommit?: (v: string) => void }> = ({ onCommit }) => {
|
|
113
|
+
const confirmId = useId()
|
|
114
|
+
const [entry, setEntry] = useState("")
|
|
115
|
+
const [sortKey, setSortKey] = useState<SortKey>("commits")
|
|
116
|
+
const [dir, setDir] = useState<Dir>("desc")
|
|
117
|
+
|
|
118
|
+
const rows = useMemo(() => {
|
|
119
|
+
const factor = dir === "asc" ? 1 : -1
|
|
120
|
+
const val = (m: Member): number | string => {
|
|
121
|
+
switch (sortKey) {
|
|
122
|
+
case "name": return m.name.toLowerCase()
|
|
123
|
+
case "role": return m.role.toLowerCase()
|
|
124
|
+
case "status": return STATUS_RANK[m.status]
|
|
125
|
+
case "active": return m.activeMins
|
|
126
|
+
case "commits": return m.commits
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...ROWS].sort((a, b) => {
|
|
130
|
+
const va = val(a), vb = val(b)
|
|
131
|
+
if (va < vb) return -1 * factor
|
|
132
|
+
if (va > vb) return 1 * factor
|
|
133
|
+
return 0
|
|
134
|
+
})
|
|
135
|
+
}, [sortKey, dir])
|
|
136
|
+
|
|
137
|
+
const onSort = (key: SortKey) => {
|
|
138
|
+
if (key === sortKey) {
|
|
139
|
+
setDir((d) => (d === "asc" ? "desc" : "asc"))
|
|
140
|
+
} else {
|
|
141
|
+
setSortKey(key)
|
|
142
|
+
// numeric columns default to descending (biggest first); text to ascending
|
|
143
|
+
setDir(key === "commits" || key === "active" ? "desc" : "asc")
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const ariaSort = (key: SortKey): "none" | "ascending" | "descending" =>
|
|
148
|
+
key !== sortKey ? "none" : dir === "asc" ? "ascending" : "descending"
|
|
149
|
+
|
|
150
|
+
const px = (mult: number) => `calc(var(--text-base) * ${mult})`
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
className="dtf-root"
|
|
155
|
+
style={{
|
|
156
|
+
fontFamily: "var(--font)",
|
|
157
|
+
fontSize: "var(--text-base)",
|
|
158
|
+
lineHeight: "var(--leading)",
|
|
159
|
+
color: "var(--fg)",
|
|
160
|
+
background: "var(--bg)",
|
|
161
|
+
letterSpacing: "var(--tracking-tight)",
|
|
162
|
+
padding: "var(--pad-loose)",
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<style>{`
|
|
166
|
+
.dtf-root { -webkit-font-smoothing: antialiased; }
|
|
167
|
+
.dtf-confirm {
|
|
168
|
+
display: flex; align-items: center; gap: var(--pad-tight);
|
|
169
|
+
padding: var(--pad) var(--pad-loose);
|
|
170
|
+
border-top: 1px solid var(--border);
|
|
171
|
+
}
|
|
172
|
+
.dtf-confirm-label { color: var(--fg-muted); }
|
|
173
|
+
.dtf-confirm-input {
|
|
174
|
+
flex: 1; min-width: 0;
|
|
175
|
+
padding: var(--pad-tight) var(--pad);
|
|
176
|
+
font: inherit; color: var(--fg);
|
|
177
|
+
background: var(--surface-2);
|
|
178
|
+
border: 1px solid var(--border-strong);
|
|
179
|
+
border-radius: var(--radius);
|
|
180
|
+
}
|
|
181
|
+
.dtf-confirm-input:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
|
|
182
|
+
.dtf-confirm-btn {
|
|
183
|
+
padding: var(--pad-tight) var(--pad);
|
|
184
|
+
font: inherit; font-weight: var(--weight-medium);
|
|
185
|
+
color: var(--accent-fg); background: var(--accent);
|
|
186
|
+
border: 1px solid var(--accent); border-radius: var(--radius);
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
}
|
|
189
|
+
[data-layout] .dtf-shell { max-width: var(--lay-width, none); }
|
|
190
|
+
[data-layout] .dtf-cell, [data-layout] .dtf-th {
|
|
191
|
+
padding-block: calc(var(--pad-tight) * var(--lay-density, 1));
|
|
192
|
+
}
|
|
193
|
+
.dtf-shell {
|
|
194
|
+
background: var(--glass-bg, var(--surface));
|
|
195
|
+
backdrop-filter: var(--glass-blur, none);
|
|
196
|
+
-webkit-backdrop-filter: var(--glass-blur, none);
|
|
197
|
+
border: 1px solid var(--glass-border, var(--border));
|
|
198
|
+
border-radius: var(--radius-lg);
|
|
199
|
+
box-shadow: var(--shadow-sm);
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
}
|
|
202
|
+
.dtf-head {
|
|
203
|
+
display: flex; align-items: baseline; justify-content: space-between;
|
|
204
|
+
gap: var(--gap);
|
|
205
|
+
padding: var(--pad) var(--pad-loose);
|
|
206
|
+
border-bottom: 1px solid var(--border);
|
|
207
|
+
background: var(--surface);
|
|
208
|
+
}
|
|
209
|
+
.dtf-scroll { overflow-x: auto; }
|
|
210
|
+
.dtf-table {
|
|
211
|
+
width: 100%;
|
|
212
|
+
border-collapse: collapse;
|
|
213
|
+
text-align: left;
|
|
214
|
+
font-variant-numeric: tabular-nums;
|
|
215
|
+
}
|
|
216
|
+
.dtf-table caption {
|
|
217
|
+
text-align: left;
|
|
218
|
+
padding: 0;
|
|
219
|
+
margin: 0;
|
|
220
|
+
caption-side: top;
|
|
221
|
+
}
|
|
222
|
+
.dtf-th {
|
|
223
|
+
position: sticky; top: 0;
|
|
224
|
+
background: var(--surface-2);
|
|
225
|
+
color: var(--fg-muted);
|
|
226
|
+
font-family: var(--font-display);
|
|
227
|
+
font-weight: var(--weight-medium);
|
|
228
|
+
text-transform: uppercase;
|
|
229
|
+
letter-spacing: 0.06em;
|
|
230
|
+
white-space: nowrap;
|
|
231
|
+
border-bottom: 1px solid var(--border-strong);
|
|
232
|
+
padding: 0;
|
|
233
|
+
}
|
|
234
|
+
.dtf-th-inner {
|
|
235
|
+
display: block;
|
|
236
|
+
padding: var(--pad-tight) var(--pad);
|
|
237
|
+
}
|
|
238
|
+
.dtf-sortbtn {
|
|
239
|
+
appearance: none;
|
|
240
|
+
-webkit-appearance: none;
|
|
241
|
+
font: inherit;
|
|
242
|
+
text-transform: inherit;
|
|
243
|
+
letter-spacing: inherit;
|
|
244
|
+
color: inherit;
|
|
245
|
+
background: transparent;
|
|
246
|
+
border: 0;
|
|
247
|
+
margin: 0;
|
|
248
|
+
padding: var(--pad-tight) var(--pad);
|
|
249
|
+
width: 100%;
|
|
250
|
+
display: inline-flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
gap: calc(var(--gap) * 0.5);
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
border-radius: var(--radius-sm);
|
|
255
|
+
transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
|
|
256
|
+
}
|
|
257
|
+
.dtf-sortbtn.num { justify-content: flex-end; }
|
|
258
|
+
.dtf-sortbtn:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 5%, transparent); }
|
|
259
|
+
.dtf-th[aria-sort="ascending"] .dtf-sortbtn,
|
|
260
|
+
.dtf-th[aria-sort="descending"] .dtf-sortbtn { color: var(--fg); }
|
|
261
|
+
.dtf-sortbtn:focus-visible {
|
|
262
|
+
outline: none;
|
|
263
|
+
box-shadow: 0 0 0 2px var(--surface-2), 0 0 0 4px var(--ring);
|
|
264
|
+
}
|
|
265
|
+
.dtf-td {
|
|
266
|
+
padding: var(--pad) var(--pad);
|
|
267
|
+
border-bottom: 1px solid var(--border);
|
|
268
|
+
color: var(--fg);
|
|
269
|
+
vertical-align: middle;
|
|
270
|
+
white-space: nowrap;
|
|
271
|
+
}
|
|
272
|
+
.dtf-row:last-child .dtf-td { border-bottom: 0; }
|
|
273
|
+
.dtf-row:nth-child(even) .dtf-td { background: color-mix(in srgb, var(--fg) 3%, transparent); }
|
|
274
|
+
.dtf-row:hover .dtf-td { background: var(--accent-weak); }
|
|
275
|
+
.dtf-num {
|
|
276
|
+
text-align: right;
|
|
277
|
+
font-family: var(--font-mono);
|
|
278
|
+
font-variant-numeric: tabular-nums;
|
|
279
|
+
font-weight: var(--weight-medium);
|
|
280
|
+
}
|
|
281
|
+
.dtf-pill {
|
|
282
|
+
display: inline-flex; align-items: center; gap: 0.5em;
|
|
283
|
+
padding: 0.2em 0.66em;
|
|
284
|
+
border: 1px solid;
|
|
285
|
+
border-radius: var(--radius-full);
|
|
286
|
+
font-size: 0.86em;
|
|
287
|
+
font-weight: var(--weight-medium);
|
|
288
|
+
line-height: 1.2;
|
|
289
|
+
white-space: nowrap;
|
|
290
|
+
}
|
|
291
|
+
.dtf-view {
|
|
292
|
+
appearance: none; -webkit-appearance: none;
|
|
293
|
+
font: inherit;
|
|
294
|
+
font-family: var(--font);
|
|
295
|
+
font-weight: var(--weight-medium);
|
|
296
|
+
font-size: 0.9em;
|
|
297
|
+
cursor: pointer;
|
|
298
|
+
color: var(--accent);
|
|
299
|
+
background: transparent;
|
|
300
|
+
border: 1px solid var(--border-strong);
|
|
301
|
+
border-radius: var(--radius-sm);
|
|
302
|
+
padding: calc(var(--pad-tight) * 0.75) var(--pad);
|
|
303
|
+
display: inline-flex; align-items: center; gap: 0.4em;
|
|
304
|
+
transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
|
|
305
|
+
}
|
|
306
|
+
.dtf-view:hover { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
|
|
307
|
+
.dtf-view:active { transform: translateY(0.5px); }
|
|
308
|
+
.dtf-view:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--ring); }
|
|
309
|
+
`}</style>
|
|
310
|
+
|
|
311
|
+
<div className="dtf-shell">
|
|
312
|
+
<div className="dtf-head">
|
|
313
|
+
<div>
|
|
314
|
+
<div
|
|
315
|
+
style={{
|
|
316
|
+
fontFamily: "var(--font-display)",
|
|
317
|
+
fontWeight: "var(--weight-display)",
|
|
318
|
+
fontSize: px(1.35),
|
|
319
|
+
lineHeight: 1.2,
|
|
320
|
+
color: "var(--fg)",
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
Orders
|
|
324
|
+
</div>
|
|
325
|
+
<div style={{ color: "var(--fg-muted)", fontSize: px(0.9), marginTop: "calc(var(--gap) * 0.25)" }}>
|
|
326
|
+
{ROWS.length} people · sorted by {COLS.find((c) => c.key === sortKey)?.label.toLowerCase()}
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<span
|
|
330
|
+
style={{
|
|
331
|
+
fontFamily: "var(--font-mono)",
|
|
332
|
+
fontSize: px(0.82),
|
|
333
|
+
color: "var(--fg-subtle)",
|
|
334
|
+
fontVariantNumeric: "tabular-nums",
|
|
335
|
+
whiteSpace: "nowrap",
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
{ROWS.filter((r) => r.status === "active").length} active now
|
|
339
|
+
</span>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div className="dtf-scroll">
|
|
343
|
+
<table className="dtf-table">
|
|
344
|
+
<caption
|
|
345
|
+
style={{
|
|
346
|
+
padding: "var(--pad-tight) var(--pad-loose)",
|
|
347
|
+
color: "var(--fg-subtle)",
|
|
348
|
+
fontSize: px(0.82),
|
|
349
|
+
borderBottom: "1px solid var(--border)",
|
|
350
|
+
}}
|
|
351
|
+
>
|
|
352
|
+
Orders with role, presence, last-active time and lifetime commit count. Column headers sort the table.
|
|
353
|
+
</caption>
|
|
354
|
+
<thead>
|
|
355
|
+
<tr>
|
|
356
|
+
{COLS.map((c) => (
|
|
357
|
+
<th
|
|
358
|
+
key={c.key}
|
|
359
|
+
scope="col"
|
|
360
|
+
className="dtf-th"
|
|
361
|
+
aria-sort={c.sortable ? ariaSort(c.key) : undefined}
|
|
362
|
+
style={c.numeric ? { textAlign: "right" } : undefined}
|
|
363
|
+
>
|
|
364
|
+
{c.sortable ? (
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
className={"dtf-sortbtn" + (c.numeric ? " num" : "")}
|
|
368
|
+
onClick={() => onSort(c.key)}
|
|
369
|
+
aria-label={`Sort by ${c.label}${
|
|
370
|
+
ariaSort(c.key) === "none"
|
|
371
|
+
? ""
|
|
372
|
+
: ariaSort(c.key) === "ascending"
|
|
373
|
+
? ", currently ascending"
|
|
374
|
+
: ", currently descending"
|
|
375
|
+
}`}
|
|
376
|
+
>
|
|
377
|
+
<span>{c.label}</span>
|
|
378
|
+
<Caret dir={sortKey === c.key ? dir : null} />
|
|
379
|
+
</button>
|
|
380
|
+
) : (
|
|
381
|
+
<span className="dtf-th-inner">{c.label}</span>
|
|
382
|
+
)}
|
|
383
|
+
</th>
|
|
384
|
+
))}
|
|
385
|
+
<th scope="col" className="dtf-th" style={{ textAlign: "right" }}>
|
|
386
|
+
<span className="dtf-th-inner" style={{ textAlign: "right" }}>
|
|
387
|
+
<span style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }}>
|
|
388
|
+
Actions
|
|
389
|
+
</span>
|
|
390
|
+
<span aria-hidden="true"> </span>
|
|
391
|
+
</span>
|
|
392
|
+
</th>
|
|
393
|
+
</tr>
|
|
394
|
+
</thead>
|
|
395
|
+
<tbody>
|
|
396
|
+
{rows.map((m) => (
|
|
397
|
+
<tr key={m.name} className="dtf-row">
|
|
398
|
+
<td className="dtf-td">
|
|
399
|
+
<span style={{ fontWeight: "var(--weight-bold)", color: "var(--fg)" }}>{m.name}</span>
|
|
400
|
+
</td>
|
|
401
|
+
<td className="dtf-td" style={{ color: "var(--fg-muted)" }}>
|
|
402
|
+
{m.role}
|
|
403
|
+
</td>
|
|
404
|
+
<td className="dtf-td">
|
|
405
|
+
<StatusPill status={m.status} />
|
|
406
|
+
</td>
|
|
407
|
+
<td className="dtf-td" style={{ color: "var(--fg-muted)" }}>
|
|
408
|
+
{m.activeLabel}
|
|
409
|
+
</td>
|
|
410
|
+
<td className="dtf-td dtf-num">{m.commits.toLocaleString("en-US")}</td>
|
|
411
|
+
<td className="dtf-td" style={{ textAlign: "right" }}>
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
className="dtf-view"
|
|
415
|
+
onClick={() => setEntry(m.role)}
|
|
416
|
+
aria-label={`Use ${m.role} from ${m.name} — fills the tracking field`}
|
|
417
|
+
>
|
|
418
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="1em" height="1em" style={{ flex: "0 0 auto" }}>
|
|
419
|
+
<path
|
|
420
|
+
d="M1.5 8s2.4-4.5 6.5-4.5S14.5 8 14.5 8s-2.4 4.5-6.5 4.5S1.5 8 1.5 8Z"
|
|
421
|
+
fill="none"
|
|
422
|
+
stroke="currentColor"
|
|
423
|
+
strokeWidth="1.25"
|
|
424
|
+
strokeLinejoin="round"
|
|
425
|
+
/>
|
|
426
|
+
<circle cx="8" cy="8" r="1.9" fill="none" stroke="currentColor" strokeWidth="1.25" />
|
|
427
|
+
</svg>
|
|
428
|
+
Use
|
|
429
|
+
</button>
|
|
430
|
+
</td>
|
|
431
|
+
</tr>
|
|
432
|
+
))}
|
|
433
|
+
</tbody>
|
|
434
|
+
</table>
|
|
435
|
+
<div className="dtf-confirm">
|
|
436
|
+
<label className="dtf-confirm-label" htmlFor={confirmId}>Tracking number</label>
|
|
437
|
+
<input
|
|
438
|
+
id={confirmId}
|
|
439
|
+
className="dtf-confirm-input"
|
|
440
|
+
value={entry}
|
|
441
|
+
placeholder="e.g. TRK-00"
|
|
442
|
+
onChange={(e) => setEntry(e.target.value)}
|
|
443
|
+
/>
|
|
444
|
+
<button type="button" className="dtf-confirm-btn" onClick={() => onCommit?.(entry.trim())}>
|
|
445
|
+
Submit
|
|
446
|
+
</button>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export default DataTable
|