@mshuja288/aksuicore 0.2.0 → 0.3.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.
@@ -25,6 +25,15 @@ export type TableColumn<T> = {
25
25
  filterPlaceholder?: string;
26
26
  };
27
27
 
28
+ /** Formats supported by the shared table download control (matches akscore TableExport). */
29
+ export type ExportDownloadFormat = "csv" | "xlsx" | "json";
30
+
31
+ const EXPORT_FORMAT_OPTIONS: { format: ExportDownloadFormat; label: string }[] = [
32
+ { format: "csv", label: "CSV" },
33
+ { format: "xlsx", label: "Excel" },
34
+ { format: "json", label: "JSON" },
35
+ ];
36
+
28
37
  type SortDirection = "asc" | "desc";
29
38
  type SortState = { key: string; direction: SortDirection };
30
39
  type DetailLine = { label: string; value: string };
@@ -105,6 +114,16 @@ function PreviewIcon() {
105
114
  );
106
115
  }
107
116
 
117
+ function DownloadIcon() {
118
+ return (
119
+ <svg viewBox="0 0 20 20" aria-hidden="true">
120
+ <path d="M10 3.5v8.2" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
121
+ <path d="M6.5 9.2 10 12.8l3.5-3.6" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
122
+ <path d="M4 15.5h12" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
123
+ </svg>
124
+ );
125
+ }
126
+
108
127
  function DetailList({ lines }: { lines: DetailLine[] }) {
109
128
  return (
110
129
  <dl className="record-detail-list">
@@ -132,6 +151,9 @@ export function RecordWorkspace<T>({
132
151
  onDelete,
133
152
  canUpdate,
134
153
  canDelete,
154
+ onDownload,
155
+ canDownload = true,
156
+ downloadBusy = false,
135
157
  toolbar,
136
158
  form,
137
159
  }: {
@@ -148,6 +170,10 @@ export function RecordWorkspace<T>({
148
170
  onDelete?: (item: T) => void;
149
171
  canUpdate?: (item: T) => boolean;
150
172
  canDelete?: (item: T) => boolean;
173
+ /** When set, shows CSV / Excel / JSON download controls in the side rail. */
174
+ onDownload?: (format: ExportDownloadFormat) => void;
175
+ canDownload?: boolean;
176
+ downloadBusy?: boolean;
151
177
  toolbar?: ReactNode;
152
178
  /** Slot for Add/Update/View dialogs owned by the host app. */
153
179
  form?: ReactNode;
@@ -156,6 +182,7 @@ export function RecordWorkspace<T>({
156
182
  const [filters, setFilters] = useState<Record<string, string>>({});
157
183
  const [sort, setSort] = useState<SortState | null>(null);
158
184
  const [sideOpen, setSideOpen] = useState(false);
185
+ const [downloadOpen, setDownloadOpen] = useState(false);
159
186
  const [previewEnabled, setPreviewEnabled] = useState(false);
160
187
  const [previewKey, setPreviewKey] = useState<string | number | null>(null);
161
188
  const [tooltip, setTooltip] = useState<TooltipState | null>(null);
@@ -205,9 +232,18 @@ export function RecordWorkspace<T>({
205
232
  const start = showAll ? 0 : (currentPage - 1) * numericPageSize;
206
233
  const pageItems = showAll ? sorted : sorted.slice(start, start + numericPageSize);
207
234
  const showAdd = canAdd && Boolean(onAdd);
235
+ const showDownload = Boolean(onDownload) && canDownload;
208
236
  const showFilters = filterableColumns.length > 0;
209
237
  const previewItem = previewKey === null ? null : sorted.find((item) => rowKey(item) === previewKey) ?? null;
210
238
 
239
+ function requestDownload(format: ExportDownloadFormat) {
240
+ if (!onDownload || downloadBusy) {
241
+ return;
242
+ }
243
+ onDownload(format);
244
+ setDownloadOpen(false);
245
+ }
246
+
211
247
  function cellText(column: TableColumn<T>, item: T) {
212
248
  const raw = column.display ? column.display(item) : column.value(item);
213
249
  return raw === null || raw === undefined || raw === "" ? "—" : String(raw);
@@ -463,6 +499,36 @@ export function RecordWorkspace<T>({
463
499
  </button>
464
500
  ) : null}
465
501
 
502
+ {showDownload ? (
503
+ <div className="side-rail-download">
504
+ <button
505
+ type="button"
506
+ className="ghost side-rail-download-toggle"
507
+ aria-expanded={downloadOpen}
508
+ disabled={downloadBusy}
509
+ onClick={() => setDownloadOpen((open) => !open)}
510
+ >
511
+ <DownloadIcon />
512
+ <span>{downloadBusy ? "Downloading…" : "Download"}</span>
513
+ </button>
514
+ {downloadOpen ? (
515
+ <div className="side-rail-download-formats" role="group" aria-label="Download format">
516
+ {EXPORT_FORMAT_OPTIONS.map((option) => (
517
+ <button
518
+ key={option.format}
519
+ type="button"
520
+ className="ghost side-rail-download-format"
521
+ disabled={downloadBusy}
522
+ onClick={() => requestDownload(option.format)}
523
+ >
524
+ {option.label}
525
+ </button>
526
+ ))}
527
+ </div>
528
+ ) : null}
529
+ </div>
530
+ ) : null}
531
+
466
532
  {showFilters ? (
467
533
  <div className="side-rail-filters">
468
534
  <div className="filter-panel-head">
@@ -519,6 +585,22 @@ export function RecordWorkspace<T>({
519
585
  <PlusIcon />
520
586
  </button>
521
587
  ) : null}
588
+ {showDownload ? (
589
+ <button
590
+ type="button"
591
+ className="ghost side-rail-icon"
592
+ title="Download"
593
+ aria-label="Download"
594
+ aria-expanded={downloadOpen}
595
+ disabled={downloadBusy}
596
+ onClick={() => {
597
+ setSideOpen(true);
598
+ setDownloadOpen(true);
599
+ }}
600
+ >
601
+ <DownloadIcon />
602
+ </button>
603
+ ) : null}
522
604
  {showFilters ? (
523
605
  <button
524
606
  type="button"
@@ -1,2 +1,7 @@
1
1
  export { AppShell, type AppShellProps } from "./AppShell";
2
- export { RecordWorkspace, RecordTable, type TableColumn } from "./RecordWorkspace";
2
+ export {
3
+ RecordWorkspace,
4
+ RecordTable,
5
+ type TableColumn,
6
+ type ExportDownloadFormat,
7
+ } from "./RecordWorkspace";
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { Button } from "./Button";
4
+ import { Dialog } from "./Dialog";
5
+
6
+ export function ConfirmDeleteDialog({
7
+ open,
8
+ message,
9
+ pending = false,
10
+ onCancel,
11
+ onConfirm,
12
+ }: {
13
+ open: boolean;
14
+ message: string;
15
+ pending?: boolean;
16
+ onCancel: () => void;
17
+ onConfirm: () => void;
18
+ }) {
19
+ return (
20
+ <Dialog title="Confirm delete" open={open} onClose={pending ? () => undefined : onCancel} wide={false}>
21
+ <div className="card-form">
22
+ <p className="hint">{message}</p>
23
+ <div className="actions">
24
+ <Button variant="ghost" onClick={onCancel} disabled={pending}>
25
+ Cancel
26
+ </Button>
27
+ <Button variant="danger" onClick={onConfirm} disabled={pending}>
28
+ {pending ? "Deleting…" : "Delete"}
29
+ </Button>
30
+ </div>
31
+ </div>
32
+ </Dialog>
33
+ );
34
+ }
@@ -2,6 +2,7 @@ export { Button, type ButtonVariant } from "./Button";
2
2
  export { Select, AppSelect, type SelectOption, type AppSelectOption } from "./Select";
3
3
  export { DateField } from "./DateField";
4
4
  export { Dialog, FormDialog } from "./Dialog";
5
+ export { ConfirmDeleteDialog } from "./ConfirmDeleteDialog";
5
6
  export { Snackbar, StatusMessage } from "./Snackbar";
6
7
  export { Topbar } from "./Topbar";
7
8
  export { SideNav, type SideNavItem } from "./SideNav";
@@ -404,6 +404,65 @@ button.side-rail-add:hover {
404
404
  background: #2a3546;
405
405
  }
406
406
 
407
+ .side-rail-download {
408
+ display: grid;
409
+ gap: 8px;
410
+ }
411
+
412
+ button.side-rail-download-toggle {
413
+ display: inline-flex;
414
+ align-items: center;
415
+ justify-content: center;
416
+ gap: 8px;
417
+ width: 100%;
418
+ padding: 10px 12px;
419
+ border: 1px solid var(--aks-border, #d7dde5);
420
+ border-radius: var(--aks-radius);
421
+ background: #fff;
422
+ color: var(--aks-brand);
423
+ box-shadow: none;
424
+ }
425
+
426
+ button.side-rail-download-toggle svg {
427
+ width: 18px;
428
+ height: 18px;
429
+ }
430
+
431
+ button.side-rail-download-toggle:hover:not(:disabled) {
432
+ background: #f5f7fa;
433
+ }
434
+
435
+ button.side-rail-download-toggle:disabled {
436
+ opacity: 0.55;
437
+ cursor: default;
438
+ }
439
+
440
+ .side-rail-download-formats {
441
+ display: grid;
442
+ grid-template-columns: repeat(3, minmax(0, 1fr));
443
+ gap: 6px;
444
+ }
445
+
446
+ button.side-rail-download-format {
447
+ padding: 8px 6px;
448
+ border: 1px solid var(--aks-border, #d7dde5);
449
+ border-radius: var(--aks-radius);
450
+ background: #fff;
451
+ color: var(--aks-brand);
452
+ font-size: 12px;
453
+ font-weight: 600;
454
+ box-shadow: none;
455
+ }
456
+
457
+ button.side-rail-download-format:hover:not(:disabled) {
458
+ background: #f5f7fa;
459
+ }
460
+
461
+ button.side-rail-download-format:disabled {
462
+ opacity: 0.45;
463
+ cursor: default;
464
+ }
465
+
407
466
  .side-rail-filters {
408
467
  display: grid;
409
468
  gap: 12px;