@mshuja288/aksuicore 0.2.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 +46 -0
- package/PLAN.md +139 -0
- package/README.md +192 -0
- package/VERSIONING.md +33 -0
- package/dist/aksui.css +1633 -0
- package/dist/index.cjs +1145 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +182 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.js +1127 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
- package/src/index.ts +5 -0
- package/src/patterns/AppShell.tsx +69 -0
- package/src/patterns/RecordWorkspace.tsx +567 -0
- package/src/patterns/index.ts +2 -0
- package/src/primitives/Button.tsx +29 -0
- package/src/primitives/DateField.tsx +255 -0
- package/src/primitives/Dialog.tsx +45 -0
- package/src/primitives/FormActions.tsx +36 -0
- package/src/primitives/Pagination.tsx +75 -0
- package/src/primitives/Select.tsx +195 -0
- package/src/primitives/SideNav.tsx +35 -0
- package/src/primitives/Snackbar.tsx +87 -0
- package/src/primitives/Topbar.tsx +89 -0
- package/src/primitives/index.ts +9 -0
- package/src/theme/aksui.css +70 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/patterns.css +733 -0
- package/src/theme/primitives.css +732 -0
- package/src/theme/shell.css +92 -0
- package/src/theme/styles.css +4 -0
- package/src/theme/tokens.ts +22 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState, useSyncExternalStore, type MouseEvent, type ReactNode } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { Pagination } from "../primitives/Pagination";
|
|
6
|
+
import { Select, type SelectOption } from "../primitives/Select";
|
|
7
|
+
|
|
8
|
+
function useIsClient() {
|
|
9
|
+
return useSyncExternalStore(
|
|
10
|
+
() => () => {},
|
|
11
|
+
() => true,
|
|
12
|
+
() => false,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TableColumn<T> = {
|
|
17
|
+
key: string;
|
|
18
|
+
header: string;
|
|
19
|
+
value: (item: T) => string | number | null | undefined;
|
|
20
|
+
display?: (item: T) => string | number | null | undefined;
|
|
21
|
+
filterable?: boolean;
|
|
22
|
+
sortable?: boolean;
|
|
23
|
+
/** When set, filter uses an exact-match Select instead of text contains. */
|
|
24
|
+
filterOptions?: SelectOption[];
|
|
25
|
+
filterPlaceholder?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type SortDirection = "asc" | "desc";
|
|
29
|
+
type SortState = { key: string; direction: SortDirection };
|
|
30
|
+
type DetailLine = { label: string; value: string };
|
|
31
|
+
type TooltipState = { x: number; y: number; lines: DetailLine[] };
|
|
32
|
+
|
|
33
|
+
function compareColumnValues<T>(column: TableColumn<T>, left: T, right: T) {
|
|
34
|
+
const leftRaw = column.display ? column.display(left) : column.value(left);
|
|
35
|
+
const rightRaw = column.display ? column.display(right) : column.value(right);
|
|
36
|
+
|
|
37
|
+
if (leftRaw === null || leftRaw === undefined || leftRaw === "") {
|
|
38
|
+
return rightRaw === null || rightRaw === undefined || rightRaw === "" ? 0 : 1;
|
|
39
|
+
}
|
|
40
|
+
if (rightRaw === null || rightRaw === undefined || rightRaw === "") {
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof leftRaw === "number" && typeof rightRaw === "number") {
|
|
45
|
+
return leftRaw - rightRaw;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const leftText = String(leftRaw);
|
|
49
|
+
const rightText = String(rightRaw);
|
|
50
|
+
const leftNumber = Number(leftText);
|
|
51
|
+
const rightNumber = Number(rightText);
|
|
52
|
+
if (
|
|
53
|
+
leftText.trim() !== "" &&
|
|
54
|
+
rightText.trim() !== "" &&
|
|
55
|
+
Number.isFinite(leftNumber) &&
|
|
56
|
+
Number.isFinite(rightNumber) &&
|
|
57
|
+
/^-?\d+(\.\d+)?$/.test(leftText.trim()) &&
|
|
58
|
+
/^-?\d+(\.\d+)?$/.test(rightText.trim())
|
|
59
|
+
) {
|
|
60
|
+
return leftNumber - rightNumber;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return leftText.localeCompare(rightText, undefined, { sensitivity: "base", numeric: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function PlusIcon() {
|
|
67
|
+
return (
|
|
68
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
69
|
+
<path d="M10 4v12M4 10h12" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
|
70
|
+
</svg>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function FilterIcon() {
|
|
75
|
+
return (
|
|
76
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
77
|
+
<path d="M3 5h14M5.5 10h9M8 15h4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
|
78
|
+
</svg>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ResetIcon() {
|
|
83
|
+
return (
|
|
84
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
85
|
+
<path d="M4.5 8.2A6 6 0 1 1 4.8 13" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
|
86
|
+
<path d="M4.5 4.2v4h4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
|
87
|
+
</svg>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ChevronIcon({ open }: { open: boolean }) {
|
|
92
|
+
return (
|
|
93
|
+
<svg className={open ? "side-rail-chevron open" : "side-rail-chevron"} viewBox="0 0 20 20" aria-hidden="true">
|
|
94
|
+
<path d="M8 4.5 13.5 10 8 15.5" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
|
95
|
+
</svg>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function PreviewIcon() {
|
|
100
|
+
return (
|
|
101
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
102
|
+
<path d="M2.8 10S5.5 4.8 10 4.8 17.2 10 17.2 10 14.5 15.2 10 15.2 2.8 10 2.8 10Z" fill="none" stroke="currentColor" strokeWidth="1.6" />
|
|
103
|
+
<circle cx="10" cy="10" r="2.4" fill="none" stroke="currentColor" strokeWidth="1.6" />
|
|
104
|
+
</svg>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function DetailList({ lines }: { lines: DetailLine[] }) {
|
|
109
|
+
return (
|
|
110
|
+
<dl className="record-detail-list">
|
|
111
|
+
{lines.map((line) => (
|
|
112
|
+
<div key={line.label} className="record-detail-row">
|
|
113
|
+
<dt>{line.label}</dt>
|
|
114
|
+
<dd>{line.value}</dd>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
</dl>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function RecordWorkspace<T>({
|
|
122
|
+
title,
|
|
123
|
+
columns,
|
|
124
|
+
items,
|
|
125
|
+
rowKey,
|
|
126
|
+
empty,
|
|
127
|
+
onAdd,
|
|
128
|
+
addLabel = "Add",
|
|
129
|
+
canAdd = true,
|
|
130
|
+
onView,
|
|
131
|
+
onUpdate,
|
|
132
|
+
onDelete,
|
|
133
|
+
canUpdate,
|
|
134
|
+
canDelete,
|
|
135
|
+
toolbar,
|
|
136
|
+
form,
|
|
137
|
+
}: {
|
|
138
|
+
title: string;
|
|
139
|
+
columns: TableColumn<T>[];
|
|
140
|
+
items: T[];
|
|
141
|
+
rowKey: (item: T) => string | number;
|
|
142
|
+
empty: string;
|
|
143
|
+
onAdd?: () => void;
|
|
144
|
+
addLabel?: string;
|
|
145
|
+
canAdd?: boolean;
|
|
146
|
+
onView?: (item: T) => void;
|
|
147
|
+
onUpdate?: (item: T) => void;
|
|
148
|
+
onDelete?: (item: T) => void;
|
|
149
|
+
canUpdate?: (item: T) => boolean;
|
|
150
|
+
canDelete?: (item: T) => boolean;
|
|
151
|
+
toolbar?: ReactNode;
|
|
152
|
+
/** Slot for Add/Update/View dialogs owned by the host app. */
|
|
153
|
+
form?: ReactNode;
|
|
154
|
+
}) {
|
|
155
|
+
const filterableColumns = columns.filter((column) => column.filterable !== false);
|
|
156
|
+
const [filters, setFilters] = useState<Record<string, string>>({});
|
|
157
|
+
const [sort, setSort] = useState<SortState | null>(null);
|
|
158
|
+
const [sideOpen, setSideOpen] = useState(false);
|
|
159
|
+
const [previewEnabled, setPreviewEnabled] = useState(false);
|
|
160
|
+
const [previewKey, setPreviewKey] = useState<string | number | null>(null);
|
|
161
|
+
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
|
162
|
+
const [pageSize, setPageSize] = useState("15");
|
|
163
|
+
const [page, setPage] = useState(1);
|
|
164
|
+
const isClient = useIsClient();
|
|
165
|
+
|
|
166
|
+
const filtered = useMemo(() => {
|
|
167
|
+
const activeFilters = columns.filter((column) => column.filterable !== false);
|
|
168
|
+
return items.filter((item) =>
|
|
169
|
+
activeFilters.every((column) => {
|
|
170
|
+
const query = (filters[column.key] ?? "").trim();
|
|
171
|
+
if (!query) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
const raw = column.value(item);
|
|
175
|
+
const text = raw === null || raw === undefined ? "" : String(raw);
|
|
176
|
+
if (column.filterOptions) {
|
|
177
|
+
return text === query;
|
|
178
|
+
}
|
|
179
|
+
return text.toLowerCase().includes(query.toLowerCase());
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
}, [items, filters, columns]);
|
|
183
|
+
|
|
184
|
+
const sorted = useMemo(() => {
|
|
185
|
+
if (!sort) {
|
|
186
|
+
return filtered;
|
|
187
|
+
}
|
|
188
|
+
const column = columns.find((entry) => entry.key === sort.key);
|
|
189
|
+
if (!column || column.sortable === false) {
|
|
190
|
+
return filtered;
|
|
191
|
+
}
|
|
192
|
+
const direction = sort.direction === "asc" ? 1 : -1;
|
|
193
|
+
return [...filtered].sort((left, right) => compareColumnValues(column, left, right) * direction);
|
|
194
|
+
}, [filtered, sort, columns]);
|
|
195
|
+
|
|
196
|
+
const activeFilterCount = useMemo(
|
|
197
|
+
() => filterableColumns.reduce((count, column) => count + ((filters[column.key] ?? "").trim() ? 1 : 0), 0),
|
|
198
|
+
[filterableColumns, filters],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const showAll = pageSize === "all";
|
|
202
|
+
const numericPageSize = showAll ? Math.max(sorted.length, 1) : Number(pageSize);
|
|
203
|
+
const pageCount = showAll ? 1 : Math.max(1, Math.ceil(sorted.length / numericPageSize));
|
|
204
|
+
const currentPage = Math.min(page, pageCount);
|
|
205
|
+
const start = showAll ? 0 : (currentPage - 1) * numericPageSize;
|
|
206
|
+
const pageItems = showAll ? sorted : sorted.slice(start, start + numericPageSize);
|
|
207
|
+
const showAdd = canAdd && Boolean(onAdd);
|
|
208
|
+
const showFilters = filterableColumns.length > 0;
|
|
209
|
+
const previewItem = previewKey === null ? null : sorted.find((item) => rowKey(item) === previewKey) ?? null;
|
|
210
|
+
|
|
211
|
+
function cellText(column: TableColumn<T>, item: T) {
|
|
212
|
+
const raw = column.display ? column.display(item) : column.value(item);
|
|
213
|
+
return raw === null || raw === undefined || raw === "" ? "—" : String(raw);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function detailLines(item: T): DetailLine[] {
|
|
217
|
+
return columns.map((column) => ({
|
|
218
|
+
label: column.header,
|
|
219
|
+
value: cellText(column, item),
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const previewTitle = previewItem && columns[0] ? cellText(columns[0], previewItem) : null;
|
|
224
|
+
|
|
225
|
+
function changeFilter(key: string, value: string) {
|
|
226
|
+
setFilters((current) => ({ ...current, [key]: value }));
|
|
227
|
+
setPage(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function clearFilters() {
|
|
231
|
+
setFilters({});
|
|
232
|
+
setPage(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function toggleSort(column: TableColumn<T>) {
|
|
236
|
+
if (column.sortable === false) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
setSort((current) => {
|
|
240
|
+
if (!current || current.key !== column.key) {
|
|
241
|
+
return { key: column.key, direction: "asc" };
|
|
242
|
+
}
|
|
243
|
+
if (current.direction === "asc") {
|
|
244
|
+
return { key: column.key, direction: "desc" };
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
});
|
|
248
|
+
setPage(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function setPreviewEnabledValue(enabled: boolean) {
|
|
252
|
+
setPreviewEnabled(enabled);
|
|
253
|
+
setTooltip(null);
|
|
254
|
+
if (!enabled) {
|
|
255
|
+
setPreviewKey(null);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function focusRecord(item: T) {
|
|
260
|
+
if (previewEnabled) {
|
|
261
|
+
setPreviewKey(rowKey(item));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function showTooltip(item: T, event: MouseEvent) {
|
|
266
|
+
const padding = 16;
|
|
267
|
+
const width = 280;
|
|
268
|
+
const x = Math.min(event.clientX + 14, window.innerWidth - width - padding);
|
|
269
|
+
const y = Math.min(event.clientY + 14, window.innerHeight - padding - 24);
|
|
270
|
+
setTooltip({ x, y, lines: detailLines(item) });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const summary =
|
|
274
|
+
sorted.length === 0
|
|
275
|
+
? "0 records"
|
|
276
|
+
: showAll
|
|
277
|
+
? `1–${sorted.length} of ${sorted.length}`
|
|
278
|
+
: `${start + 1}–${Math.min(start + numericPageSize, sorted.length)} of ${sorted.length}`;
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div className="list-workspace" aria-label={title}>
|
|
282
|
+
<div
|
|
283
|
+
className={["list-body", sideOpen ? "side-open" : "", previewEnabled ? "preview-open" : ""]
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.join(" ")}
|
|
286
|
+
>
|
|
287
|
+
<div className="list-main">
|
|
288
|
+
<section className="table-card">
|
|
289
|
+
{toolbar ? (
|
|
290
|
+
<div className="table-card-bar">
|
|
291
|
+
<div className="table-card-context">{toolbar}</div>
|
|
292
|
+
</div>
|
|
293
|
+
) : null}
|
|
294
|
+
|
|
295
|
+
<div className="table-wrap">
|
|
296
|
+
<table className="data-table">
|
|
297
|
+
<thead>
|
|
298
|
+
<tr>
|
|
299
|
+
{columns.map((column) => {
|
|
300
|
+
const sortable = column.sortable !== false;
|
|
301
|
+
const active = sort?.key === column.key ? sort.direction : null;
|
|
302
|
+
return (
|
|
303
|
+
<th
|
|
304
|
+
key={column.key}
|
|
305
|
+
aria-sort={active === "asc" ? "ascending" : active === "desc" ? "descending" : "none"}
|
|
306
|
+
>
|
|
307
|
+
{sortable ? (
|
|
308
|
+
<button
|
|
309
|
+
type="button"
|
|
310
|
+
className={active ? `sort-header active ${active}` : "sort-header"}
|
|
311
|
+
onClick={() => toggleSort(column)}
|
|
312
|
+
>
|
|
313
|
+
<span>{column.header}</span>
|
|
314
|
+
<span className="sort-indicator" aria-hidden="true">
|
|
315
|
+
{active === "asc" ? "▲" : active === "desc" ? "▼" : "↕"}
|
|
316
|
+
</span>
|
|
317
|
+
</button>
|
|
318
|
+
) : (
|
|
319
|
+
column.header
|
|
320
|
+
)}
|
|
321
|
+
</th>
|
|
322
|
+
);
|
|
323
|
+
})}
|
|
324
|
+
<th className="actions-col">Actions</th>
|
|
325
|
+
</tr>
|
|
326
|
+
</thead>
|
|
327
|
+
<tbody>
|
|
328
|
+
{pageItems.length === 0 ? (
|
|
329
|
+
<tr>
|
|
330
|
+
<td colSpan={columns.length + 1}>
|
|
331
|
+
<p className="hint table-empty">
|
|
332
|
+
{items.length === 0 ? empty : "No records match the current filters."}
|
|
333
|
+
</p>
|
|
334
|
+
</td>
|
|
335
|
+
</tr>
|
|
336
|
+
) : (
|
|
337
|
+
pageItems.map((item) => {
|
|
338
|
+
const key = rowKey(item);
|
|
339
|
+
const updateAllowed = canUpdate ? canUpdate(item) : Boolean(onUpdate);
|
|
340
|
+
const deleteAllowed = canDelete ? canDelete(item) : Boolean(onDelete);
|
|
341
|
+
const selected = previewEnabled && previewKey === key;
|
|
342
|
+
return (
|
|
343
|
+
<tr
|
|
344
|
+
key={key}
|
|
345
|
+
className={selected ? "data-row selected" : "data-row"}
|
|
346
|
+
onMouseEnter={(event) => {
|
|
347
|
+
if (!previewEnabled) {
|
|
348
|
+
showTooltip(item, event);
|
|
349
|
+
}
|
|
350
|
+
focusRecord(item);
|
|
351
|
+
}}
|
|
352
|
+
onMouseMove={(event) => {
|
|
353
|
+
if (!previewEnabled) {
|
|
354
|
+
showTooltip(item, event);
|
|
355
|
+
}
|
|
356
|
+
}}
|
|
357
|
+
onMouseLeave={() => setTooltip(null)}
|
|
358
|
+
onClick={() => focusRecord(item)}
|
|
359
|
+
>
|
|
360
|
+
{columns.map((column) => (
|
|
361
|
+
<td key={column.key}>{cellText(column, item)}</td>
|
|
362
|
+
))}
|
|
363
|
+
<td className="actions-col" onClick={(event) => event.stopPropagation()}>
|
|
364
|
+
<div className="row-actions">
|
|
365
|
+
{onView ? (
|
|
366
|
+
<button type="button" className="ghost row-action" onClick={() => onView(item)}>
|
|
367
|
+
View
|
|
368
|
+
</button>
|
|
369
|
+
) : null}
|
|
370
|
+
{onUpdate ? (
|
|
371
|
+
<button
|
|
372
|
+
type="button"
|
|
373
|
+
className="ghost row-action"
|
|
374
|
+
disabled={!updateAllowed}
|
|
375
|
+
title={updateAllowed ? "Update" : "Update is not available"}
|
|
376
|
+
onClick={() => onUpdate(item)}
|
|
377
|
+
>
|
|
378
|
+
Update
|
|
379
|
+
</button>
|
|
380
|
+
) : null}
|
|
381
|
+
{onDelete ? (
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
className="ghost row-action danger"
|
|
385
|
+
disabled={!deleteAllowed}
|
|
386
|
+
title={deleteAllowed ? "Delete" : "Delete is not available"}
|
|
387
|
+
onClick={() => onDelete(item)}
|
|
388
|
+
>
|
|
389
|
+
Delete
|
|
390
|
+
</button>
|
|
391
|
+
) : null}
|
|
392
|
+
</div>
|
|
393
|
+
</td>
|
|
394
|
+
</tr>
|
|
395
|
+
);
|
|
396
|
+
})
|
|
397
|
+
)}
|
|
398
|
+
</tbody>
|
|
399
|
+
</table>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<Pagination
|
|
403
|
+
page={currentPage}
|
|
404
|
+
pageCount={pageCount}
|
|
405
|
+
pageSize={pageSize}
|
|
406
|
+
onPageChange={setPage}
|
|
407
|
+
onPageSizeChange={(value) => {
|
|
408
|
+
setPageSize(value);
|
|
409
|
+
setPage(1);
|
|
410
|
+
}}
|
|
411
|
+
summary={summary}
|
|
412
|
+
/>
|
|
413
|
+
</section>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
{previewEnabled ? (
|
|
417
|
+
<aside className="preview-panel" aria-label="Record preview">
|
|
418
|
+
<div className="preview-panel-head">
|
|
419
|
+
<div>
|
|
420
|
+
<p className="preview-kicker">Preview</p>
|
|
421
|
+
<h3>{previewTitle ?? "No record selected"}</h3>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
<div className="preview-panel-body">
|
|
425
|
+
{previewItem ? (
|
|
426
|
+
<DetailList lines={detailLines(previewItem)} />
|
|
427
|
+
) : (
|
|
428
|
+
<p className="preview-empty">Hover or click a row to preview its details here.</p>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
</aside>
|
|
432
|
+
) : null}
|
|
433
|
+
|
|
434
|
+
<aside className={sideOpen ? "side-rail open" : "side-rail"} aria-label="Actions and filters">
|
|
435
|
+
<button
|
|
436
|
+
type="button"
|
|
437
|
+
className="ghost side-rail-toggle"
|
|
438
|
+
aria-expanded={sideOpen}
|
|
439
|
+
title={sideOpen ? "Collapse" : "Expand"}
|
|
440
|
+
onClick={() => setSideOpen((open) => !open)}
|
|
441
|
+
>
|
|
442
|
+
<ChevronIcon open={sideOpen} />
|
|
443
|
+
{sideOpen ? <span>Collapse</span> : null}
|
|
444
|
+
</button>
|
|
445
|
+
|
|
446
|
+
{sideOpen ? (
|
|
447
|
+
<div className="side-rail-body">
|
|
448
|
+
<div className="preview-check">
|
|
449
|
+
<input
|
|
450
|
+
type="checkbox"
|
|
451
|
+
checked={previewEnabled}
|
|
452
|
+
onChange={(event) => setPreviewEnabledValue(event.target.checked)}
|
|
453
|
+
aria-label="Preview"
|
|
454
|
+
/>
|
|
455
|
+
<PreviewIcon />
|
|
456
|
+
<span aria-hidden="true">Preview</span>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
{showAdd ? (
|
|
460
|
+
<button type="button" className="side-rail-add" onClick={onAdd}>
|
|
461
|
+
<PlusIcon />
|
|
462
|
+
<span>{addLabel}</span>
|
|
463
|
+
</button>
|
|
464
|
+
) : null}
|
|
465
|
+
|
|
466
|
+
{showFilters ? (
|
|
467
|
+
<div className="side-rail-filters">
|
|
468
|
+
<div className="filter-panel-head">
|
|
469
|
+
<h3>Filters</h3>
|
|
470
|
+
<button
|
|
471
|
+
type="button"
|
|
472
|
+
className="ghost filter-clear"
|
|
473
|
+
title="Reset filters"
|
|
474
|
+
aria-label="Reset filters"
|
|
475
|
+
disabled={activeFilterCount === 0}
|
|
476
|
+
onClick={clearFilters}
|
|
477
|
+
>
|
|
478
|
+
<ResetIcon />
|
|
479
|
+
<span>Reset</span>
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
482
|
+
<div className="filter-panel-stack">
|
|
483
|
+
{filterableColumns.map((column) => (
|
|
484
|
+
<label key={column.key}>
|
|
485
|
+
{column.header}
|
|
486
|
+
{column.filterOptions ? (
|
|
487
|
+
<Select
|
|
488
|
+
value={filters[column.key] ?? ""}
|
|
489
|
+
options={[{ value: "", label: column.filterPlaceholder ?? "All" }, ...column.filterOptions]}
|
|
490
|
+
onChange={(value) => changeFilter(column.key, value)}
|
|
491
|
+
placeholder={column.filterPlaceholder ?? "All"}
|
|
492
|
+
/>
|
|
493
|
+
) : (
|
|
494
|
+
<input
|
|
495
|
+
value={filters[column.key] ?? ""}
|
|
496
|
+
onChange={(event) => changeFilter(column.key, event.target.value)}
|
|
497
|
+
placeholder={`Filter ${column.header.toLowerCase()}`}
|
|
498
|
+
/>
|
|
499
|
+
)}
|
|
500
|
+
</label>
|
|
501
|
+
))}
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
) : null}
|
|
505
|
+
</div>
|
|
506
|
+
) : (
|
|
507
|
+
<div className="side-rail-icons">
|
|
508
|
+
<label className="side-rail-icon preview-check-icon" title="Preview">
|
|
509
|
+
<input
|
|
510
|
+
type="checkbox"
|
|
511
|
+
checked={previewEnabled}
|
|
512
|
+
onChange={(event) => setPreviewEnabledValue(event.target.checked)}
|
|
513
|
+
aria-label="Preview"
|
|
514
|
+
/>
|
|
515
|
+
<PreviewIcon />
|
|
516
|
+
</label>
|
|
517
|
+
{showAdd ? (
|
|
518
|
+
<button type="button" className="ghost side-rail-icon" title={addLabel} aria-label={addLabel} onClick={onAdd}>
|
|
519
|
+
<PlusIcon />
|
|
520
|
+
</button>
|
|
521
|
+
) : null}
|
|
522
|
+
{showFilters ? (
|
|
523
|
+
<button
|
|
524
|
+
type="button"
|
|
525
|
+
className="ghost side-rail-icon"
|
|
526
|
+
title="Show filters"
|
|
527
|
+
aria-label="Show filters"
|
|
528
|
+
onClick={() => setSideOpen(true)}
|
|
529
|
+
>
|
|
530
|
+
<FilterIcon />
|
|
531
|
+
{activeFilterCount > 0 ? <span className="filter-badge">{activeFilterCount}</span> : null}
|
|
532
|
+
</button>
|
|
533
|
+
) : null}
|
|
534
|
+
{showFilters ? (
|
|
535
|
+
<button
|
|
536
|
+
type="button"
|
|
537
|
+
className="ghost side-rail-icon"
|
|
538
|
+
title="Reset filters"
|
|
539
|
+
aria-label="Reset filters"
|
|
540
|
+
disabled={activeFilterCount === 0}
|
|
541
|
+
onClick={clearFilters}
|
|
542
|
+
>
|
|
543
|
+
<ResetIcon />
|
|
544
|
+
</button>
|
|
545
|
+
) : null}
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
</aside>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
{form}
|
|
552
|
+
|
|
553
|
+
{isClient && tooltip && !previewEnabled
|
|
554
|
+
? createPortal(
|
|
555
|
+
<div className="record-tooltip" style={{ left: tooltip.x, top: tooltip.y }} role="tooltip">
|
|
556
|
+
<p className="record-tooltip-title">{title}</p>
|
|
557
|
+
<DetailList lines={tooltip.lines} />
|
|
558
|
+
</div>,
|
|
559
|
+
document.body,
|
|
560
|
+
)
|
|
561
|
+
: null}
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** @deprecated Prefer RecordWorkspace — alias kept for configurator migration */
|
|
567
|
+
export const RecordTable = RecordWorkspace;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
|
6
|
+
|
|
7
|
+
const variantClass: Record<ButtonVariant, string> = {
|
|
8
|
+
primary: "",
|
|
9
|
+
secondary: "secondary",
|
|
10
|
+
ghost: "ghost",
|
|
11
|
+
danger: "danger",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function Button({
|
|
15
|
+
variant = "primary",
|
|
16
|
+
className = "",
|
|
17
|
+
children,
|
|
18
|
+
...props
|
|
19
|
+
}: ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
20
|
+
variant?: ButtonVariant;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}) {
|
|
23
|
+
const classes = ["aks-button", variantClass[variant], className].filter(Boolean).join(" ");
|
|
24
|
+
return (
|
|
25
|
+
<button type="button" className={classes} {...props}>
|
|
26
|
+
{children}
|
|
27
|
+
</button>
|
|
28
|
+
);
|
|
29
|
+
}
|