@birdycom/omniaui-react 0.11.0 → 0.11.1
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/LICENSE +21 -21
- package/README.md +21 -21
- package/package.json +71 -71
- package/src/blocks/BlockAboutTimeline.tsx +46 -46
- package/src/blocks/BlockAppHeader.tsx +52 -52
- package/src/blocks/BlockAppShell.tsx +56 -56
- package/src/blocks/BlockAuth.tsx +53 -53
- package/src/blocks/BlockAuthSplit.tsx +84 -84
- package/src/blocks/BlockContactGrid.tsx +71 -71
- package/src/blocks/BlockCta.tsx +23 -23
- package/src/blocks/BlockDataTable.tsx +57 -57
- package/src/blocks/BlockFaq.tsx +55 -55
- package/src/blocks/BlockFeatures.tsx +38 -38
- package/src/blocks/BlockFooter.tsx +50 -50
- package/src/blocks/BlockFormPage.tsx +26 -26
- package/src/blocks/BlockHero.tsx +27 -27
- package/src/blocks/BlockHeroCode.tsx +63 -63
- package/src/blocks/BlockHeroOrbit.tsx +73 -73
- package/src/blocks/BlockHeroSplit.tsx +28 -28
- package/src/blocks/BlockModuleHeader.tsx +34 -34
- package/src/blocks/BlockPageHero.tsx +25 -25
- package/src/blocks/BlockPortfolioGrid.tsx +50 -50
- package/src/blocks/BlockPricing.tsx +62 -62
- package/src/blocks/BlockProcess.tsx +50 -50
- package/src/blocks/BlockProductGrid.tsx +54 -54
- package/src/blocks/BlockProfileHeader.tsx +57 -57
- package/src/blocks/BlockShowcase.tsx +58 -58
- package/src/blocks/BlockSiteFooter.tsx +60 -60
- package/src/blocks/BlockSiteHeader.tsx +42 -42
- package/src/blocks/BlockSpotlight.tsx +58 -58
- package/src/blocks/BlockStats.tsx +26 -26
- package/src/blocks/BlockStatsCounter.tsx +31 -31
- package/src/blocks/BlockTeamList.tsx +48 -48
- package/src/blocks/BlockTwoFactor.tsx +57 -57
- package/src/blocks/BlockWizardDialog.tsx +101 -101
- package/src/components/Accordion.tsx +31 -31
- package/src/components/AccordionAccent.tsx +39 -39
- package/src/components/AffixInput.tsx +47 -47
- package/src/components/Alert.tsx +24 -24
- package/src/components/AlertDialog.tsx +60 -60
- package/src/components/AlphaTabs.tsx +63 -63
- package/src/components/Attachment.tsx +140 -140
- package/src/components/Avatar.tsx +32 -32
- package/src/components/BackLink.tsx +11 -11
- package/src/components/Badge.tsx +14 -14
- package/src/components/BarcodeInput.tsx +64 -64
- package/src/components/Breadcrumb.tsx +32 -32
- package/src/components/BrowserFrame.tsx +48 -48
- package/src/components/BusyOverlay.tsx +52 -52
- package/src/components/Button.tsx +49 -49
- package/src/components/Card.tsx +27 -27
- package/src/components/ChartLine.tsx +119 -119
- package/src/components/ChatComposer.tsx +73 -73
- package/src/components/Checkbox.tsx +14 -14
- package/src/components/Checklist.tsx +45 -45
- package/src/components/ChipFilter.tsx +60 -60
- package/src/components/Collapsible.tsx +43 -43
- package/src/components/ColorField.tsx +48 -48
- package/src/components/ColorSelect.tsx +49 -49
- package/src/components/ColumnMenu.tsx +66 -66
- package/src/components/Command.tsx +113 -113
- package/src/components/ConfirmDialog.tsx +95 -95
- package/src/components/CookieConsent.tsx +137 -137
- package/src/components/CopyField.tsx +44 -44
- package/src/components/Counter.tsx +21 -21
- package/src/components/DataTable.tsx +201 -201
- package/src/components/DecimalInput.tsx +87 -87
- package/src/components/DefinitionList.tsx +38 -38
- package/src/components/DocViewer.tsx +110 -110
- package/src/components/Dropdown.tsx +52 -52
- package/src/components/Dropzone.tsx +46 -46
- package/src/components/EmojiPicker.tsx +72 -72
- package/src/components/EmptyState.tsx +27 -27
- package/src/components/EntityCard.tsx +56 -56
- package/src/components/FactList.tsx +43 -43
- package/src/components/FilterCounts.tsx +50 -50
- package/src/components/FlipCard.tsx +55 -55
- package/src/components/FormField.tsx +54 -54
- package/src/components/Greeting.tsx +63 -63
- package/src/components/Heading.tsx +27 -27
- package/src/components/Icon.tsx +25 -25
- package/src/components/ImageZoom.tsx +51 -51
- package/src/components/Input.tsx +21 -21
- package/src/components/InputGroup.tsx +24 -24
- package/src/components/LanguageSelect.tsx +64 -64
- package/src/components/LauncherCard.tsx +50 -50
- package/src/components/Lightbox.tsx +83 -83
- package/src/components/LinkArrow.tsx +27 -27
- package/src/components/ListToolbar.tsx +67 -67
- package/src/components/Marquee.tsx +38 -38
- package/src/components/MediaCard.tsx +47 -47
- package/src/components/MediaRow.tsx +53 -53
- package/src/components/MediaThumb.tsx +49 -49
- package/src/components/Message.tsx +171 -171
- package/src/components/MessageThread.tsx +177 -177
- package/src/components/Modal.tsx +81 -81
- package/src/components/Navmenu.tsx +62 -62
- package/src/components/NextTeaser.tsx +29 -29
- package/src/components/NumberInput.tsx +64 -64
- package/src/components/OtpInput.tsx +48 -48
- package/src/components/PageHeader.tsx +43 -43
- package/src/components/Pagination.tsx +42 -42
- package/src/components/PasswordInput.tsx +36 -36
- package/src/components/PasswordRequirements.tsx +46 -46
- package/src/components/PhoneInput.tsx +93 -93
- package/src/components/Popover.tsx +34 -34
- package/src/components/PriceCard.tsx +83 -83
- package/src/components/ProductMark.tsx +35 -35
- package/src/components/Progress.tsx +32 -32
- package/src/components/ProjectCard.tsx +46 -46
- package/src/components/Prose.tsx +79 -79
- package/src/components/RecoveryCodes.tsx +65 -65
- package/src/components/Segmented.tsx +47 -47
- package/src/components/Select.tsx +27 -27
- package/src/components/SeverityCard.tsx +53 -53
- package/src/components/Sheet.tsx +62 -62
- package/src/components/Skeleton.tsx +12 -12
- package/src/components/SortableRow.tsx +46 -46
- package/src/components/StackCard.tsx +40 -40
- package/src/components/StackList.tsx +42 -42
- package/src/components/StatCard.tsx +49 -49
- package/src/components/Stepper.tsx +54 -54
- package/src/components/Switch.tsx +18 -18
- package/src/components/Table.tsx +75 -75
- package/src/components/TableActions.tsx +54 -54
- package/src/components/TableCompact.tsx +61 -61
- package/src/components/Tabs.tsx +45 -45
- package/src/components/TagInput.tsx +73 -73
- package/src/components/TerminalLog.tsx +63 -63
- package/src/components/Textarea.tsx +19 -19
- package/src/components/ThemeSwitch.tsx +45 -45
- package/src/components/TimeInput.tsx +76 -76
- package/src/components/Timeline.tsx +45 -45
- package/src/components/Toast.tsx +138 -138
- package/src/components/ToggleField.tsx +39 -39
- package/src/components/Tooltip.tsx +14 -14
- package/src/components/TopProgress.tsx +54 -54
- package/src/components/Tree.tsx +97 -97
- package/src/components/UpgradeBanner.tsx +62 -62
- package/src/components/User.tsx +31 -31
- package/src/components/VersionBanner.tsx +41 -41
- package/src/hooks.tsx +141 -141
- package/src/utils.ts +25 -25
|
@@ -1,201 +1,201 @@
|
|
|
1
|
-
// File: packages/react/src/components/DataTable.tsx
|
|
2
|
-
// Map: column and row types -> shell markup -> bind the list-table script on mount
|
|
3
|
-
"use client";
|
|
4
|
-
|
|
5
|
-
import { useEffect, useId, useRef } from "react";
|
|
6
|
-
import type { ReactNode } from "react";
|
|
7
|
-
import { bindDataTable } from "../../../ui/src/data-table.js";
|
|
8
|
-
|
|
9
|
-
export interface DataTableColumn {
|
|
10
|
-
key: string;
|
|
11
|
-
header: string;
|
|
12
|
-
/** Right aligned, sorted as a number (5,15 and 5.15). */
|
|
13
|
-
numeric?: boolean;
|
|
14
|
-
/** Click the header to sort. Default true. */
|
|
15
|
-
sortable?: boolean;
|
|
16
|
-
/** Show the column in the Columns menu. Default true. */
|
|
17
|
-
hideable?: boolean;
|
|
18
|
-
/** Start hidden until the user checks it in the Columns menu. */
|
|
19
|
-
defaultHidden?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface DataTableRow {
|
|
23
|
-
key: string;
|
|
24
|
-
status?: "active" | "deleted" | "archived";
|
|
25
|
-
cells: Record<string, string>;
|
|
26
|
-
/** Sort and export value when it differs from the visible text. */
|
|
27
|
-
values?: Record<string, string>;
|
|
28
|
-
href?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface DataTableProps {
|
|
32
|
-
id?: string;
|
|
33
|
-
title?: string;
|
|
34
|
-
columns: DataTableColumn[];
|
|
35
|
-
rows: DataTableRow[];
|
|
36
|
-
pageSize?: number;
|
|
37
|
-
searchPlaceholder?: string;
|
|
38
|
-
selectable?: boolean;
|
|
39
|
-
draggable?: boolean;
|
|
40
|
-
/** Trash and archive pills. Default true. */
|
|
41
|
-
filters?: boolean;
|
|
42
|
-
/** Demo mode: trash, restore, duplicate and delete change the rows in the page. */
|
|
43
|
-
live?: boolean;
|
|
44
|
-
/** Page actions on the right of the toolbar (New article, …). */
|
|
45
|
-
actions?: ReactNode;
|
|
46
|
-
onBulk?: (action: string, keys: string[]) => void;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const PAGE_SIZES = [10, 15, 20, 50, 100];
|
|
50
|
-
|
|
51
|
-
/** Korrekt / OmniaDesk list table. The script in @birdycom/omniaui/data-table.js does the work. */
|
|
52
|
-
export function DataTable({
|
|
53
|
-
id,
|
|
54
|
-
title = "List",
|
|
55
|
-
columns,
|
|
56
|
-
rows,
|
|
57
|
-
pageSize = 15,
|
|
58
|
-
searchPlaceholder = "Search...",
|
|
59
|
-
selectable = true,
|
|
60
|
-
draggable = true,
|
|
61
|
-
filters = true,
|
|
62
|
-
live = false,
|
|
63
|
-
actions,
|
|
64
|
-
onBulk,
|
|
65
|
-
}: DataTableProps) {
|
|
66
|
-
const autoId = useId().replace(/:/g, "");
|
|
67
|
-
const tableId = id || `oc-dt-${autoId}`;
|
|
68
|
-
const rootRef = useRef<HTMLDivElement>(null);
|
|
69
|
-
const onBulkRef = useRef(onBulk);
|
|
70
|
-
onBulkRef.current = onBulk;
|
|
71
|
-
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
const root = rootRef.current;
|
|
74
|
-
if (!root) return;
|
|
75
|
-
bindDataTable(root);
|
|
76
|
-
const onBulkEvent = (event: Event) => {
|
|
77
|
-
const detail = (event as CustomEvent<{ action: string; keys: string[] }>).detail;
|
|
78
|
-
onBulkRef.current?.(detail.action, detail.keys);
|
|
79
|
-
};
|
|
80
|
-
root.addEventListener("oc-datatable-bulk", onBulkEvent);
|
|
81
|
-
return () => root.removeEventListener("oc-datatable-bulk", onBulkEvent);
|
|
82
|
-
}, [columns, rows]);
|
|
83
|
-
|
|
84
|
-
const lead = (selectable ? 1 : 0) + (draggable ? 1 : 0);
|
|
85
|
-
const sizes = [...new Set([...PAGE_SIZES, pageSize])].sort((a, b) => a - b);
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div
|
|
89
|
-
ref={rootRef}
|
|
90
|
-
className="oc-data-table oc-column-menu-scope"
|
|
91
|
-
id={tableId}
|
|
92
|
-
data-page-size={pageSize}
|
|
93
|
-
data-oc-live={live ? "true" : undefined}
|
|
94
|
-
>
|
|
95
|
-
<div className="oc-data-table-toolbar">
|
|
96
|
-
<div className="oc-data-table-heading">
|
|
97
|
-
<span className="oc-data-table-title">{title}</span>
|
|
98
|
-
<button className="oc-btn oc-btn--outline oc-btn--icon oc-btn--sm" type="button" popoverTarget={`${tableId}-menu`} aria-label="Table actions">⚙</button>
|
|
99
|
-
<div className="oc-dropdown-menu" id={`${tableId}-menu`} popover="auto">
|
|
100
|
-
<button className="oc-dropdown-item" type="button" data-oc-export="">Export CSV</button>
|
|
101
|
-
<button className="oc-dropdown-item" type="button" data-oc-import="">Import CSV</button>
|
|
102
|
-
<input className="oc-data-table-file" type="file" accept=".csv,text/csv" data-oc-file="" />
|
|
103
|
-
<div className="oc-dropdown-separator" />
|
|
104
|
-
<button className="oc-dropdown-item" type="button" data-oc-density="comfortable">Comfortable rows</button>
|
|
105
|
-
<button className="oc-dropdown-item" type="button" data-oc-density="compact">Compact rows</button>
|
|
106
|
-
<button className="oc-dropdown-item" type="button" data-oc-reset-widths="">Reset column widths</button>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
<input className="oc-input oc-data-table-search" type="search" placeholder={searchPlaceholder} aria-label={`Search ${title}`} />
|
|
110
|
-
{filters && (
|
|
111
|
-
<div className="oc-filter-pills" role="group" aria-label="Trash filter">
|
|
112
|
-
<button className="oc-filter-pill" type="button" data-oc-filter="active" aria-current="true">Active only</button>
|
|
113
|
-
<button className="oc-filter-pill" type="button" data-oc-filter="all">Including trash</button>
|
|
114
|
-
<button className="oc-filter-pill" type="button" data-oc-filter="deleted">Trash only</button>
|
|
115
|
-
<button className="oc-filter-pill" type="button" data-oc-filter="archived">Archive only</button>
|
|
116
|
-
</div>
|
|
117
|
-
)}
|
|
118
|
-
<div className="oc-data-table-view" role="group" aria-label="View">
|
|
119
|
-
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-view="table" aria-pressed="true">Table</button>
|
|
120
|
-
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-view="grid">Cards</button>
|
|
121
|
-
</div>
|
|
122
|
-
{actions && <div className="oc-data-table-actions">{actions}</div>}
|
|
123
|
-
<p className="oc-data-table-sort-summary" data-oc-sort-summary="" role="status" />
|
|
124
|
-
</div>
|
|
125
|
-
<div className="oc-table-toolbar">
|
|
126
|
-
<span className="oc-table-toolbar-spacer" />
|
|
127
|
-
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" popoverTarget={`${tableId}-columns`}>Columns</button>
|
|
128
|
-
</div>
|
|
129
|
-
<div className="oc-column-menu" id={`${tableId}-columns`} popover="auto">
|
|
130
|
-
<p className="oc-column-menu-title">Visible columns</p>
|
|
131
|
-
<form className="oc-column-menu-list">
|
|
132
|
-
{columns.map((col, index) => col.hideable === false ? null : (
|
|
133
|
-
<label className="oc-column-menu-item" key={col.key}>
|
|
134
|
-
<input type="checkbox" data-col={lead + index + 1} defaultChecked={!col.defaultHidden} /> {col.header}
|
|
135
|
-
</label>
|
|
136
|
-
))}
|
|
137
|
-
</form>
|
|
138
|
-
</div>
|
|
139
|
-
<div className="oc-data-table-bulk" hidden>
|
|
140
|
-
<span data-oc-selected-count="">0 selected</span>
|
|
141
|
-
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="export">Export selection</button>
|
|
142
|
-
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="duplicate">Duplicate</button>
|
|
143
|
-
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="archive">Move to trash</button>
|
|
144
|
-
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="restore">Restore</button>
|
|
145
|
-
<button className="oc-btn oc-btn--destructive oc-btn--sm" type="button" data-oc-bulk="delete">Delete permanently</button>
|
|
146
|
-
<button className="oc-btn oc-btn--ghost oc-btn--sm" type="button" data-oc-bulk="clear">Clear</button>
|
|
147
|
-
</div>
|
|
148
|
-
<div className="oc-data-table-frame">
|
|
149
|
-
<div className="oc-data-table-scroll">
|
|
150
|
-
<table className="oc-table oc-data-table-table">
|
|
151
|
-
<thead>
|
|
152
|
-
<tr>
|
|
153
|
-
{selectable && <th data-oc-col="select"><input className="oc-checkbox" type="checkbox" data-oc-select-all="" aria-label="Select all" /></th>}
|
|
154
|
-
{draggable && <th data-oc-col="order"><span className="oc-visually-hidden">Order</span></th>}
|
|
155
|
-
{columns.map((col) => (
|
|
156
|
-
<th key={col.key} data-oc-col={col.key} data-oc-sort={col.numeric ? "numeric" : "text"} data-numeric={col.numeric ? "" : undefined}>
|
|
157
|
-
<span className="oc-data-table-th">
|
|
158
|
-
{col.sortable === false ? col.header : <button className="oc-table-sort" type="button">{col.header}</button>}
|
|
159
|
-
<span className="oc-data-table-resize" role="separator" aria-orientation="vertical" aria-label={`Resize ${col.header}`} />
|
|
160
|
-
</span>
|
|
161
|
-
</th>
|
|
162
|
-
))}
|
|
163
|
-
<th data-oc-col="actions"><span className="oc-visually-hidden">Actions</span></th>
|
|
164
|
-
</tr>
|
|
165
|
-
</thead>
|
|
166
|
-
<tbody>
|
|
167
|
-
{rows.map((row) => (
|
|
168
|
-
<tr key={row.key} data-oc-row="" data-oc-key={row.key} data-oc-status={row.status || "active"} data-oc-href={row.href}>
|
|
169
|
-
{selectable && (
|
|
170
|
-
<td data-oc-col="select"><input className="oc-checkbox" type="checkbox" data-oc-select="" aria-label={`Select ${row.cells.name || row.key}`} /></td>
|
|
171
|
-
)}
|
|
172
|
-
{draggable && (
|
|
173
|
-
<td data-oc-col="order" className="oc-sortable-row-grip-cell">
|
|
174
|
-
<button className="oc-sortable-row-grip" type="button" aria-label={`Move ${row.cells.name || row.key}`} />
|
|
175
|
-
</td>
|
|
176
|
-
)}
|
|
177
|
-
{columns.map((col) => (
|
|
178
|
-
<td key={col.key} data-oc-col={col.key} data-numeric={col.numeric ? "" : undefined} data-oc-value={row.values?.[col.key]}>
|
|
179
|
-
{row.cells[col.key] ?? ""}
|
|
180
|
-
</td>
|
|
181
|
-
))}
|
|
182
|
-
<td data-oc-col="actions" className="oc-table-actions"><button className="oc-table-row-action" type="button">Edit</button></td>
|
|
183
|
-
</tr>
|
|
184
|
-
))}
|
|
185
|
-
</tbody>
|
|
186
|
-
</table>
|
|
187
|
-
</div>
|
|
188
|
-
<p className="oc-data-table-empty" hidden>No results</p>
|
|
189
|
-
</div>
|
|
190
|
-
<div className="oc-data-table-footer">
|
|
191
|
-
<p className="oc-data-table-summary" data-oc-summary="" />
|
|
192
|
-
<label className="oc-data-table-per-page">Rows per page
|
|
193
|
-
<select className="oc-select" data-oc-page-size="" aria-label="Rows per page" defaultValue={String(pageSize)}>
|
|
194
|
-
{sizes.map((n) => <option key={n} value={n}>{n}</option>)}
|
|
195
|
-
</select>
|
|
196
|
-
</label>
|
|
197
|
-
<nav className="oc-data-table-pager" data-oc-pager="" aria-label="Pagination" />
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
);
|
|
201
|
-
}
|
|
1
|
+
// File: packages/react/src/components/DataTable.tsx
|
|
2
|
+
// Map: column and row types -> shell markup -> bind the list-table script on mount
|
|
3
|
+
"use client";
|
|
4
|
+
|
|
5
|
+
import { useEffect, useId, useRef } from "react";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import { bindDataTable } from "../../../ui/src/data-table.js";
|
|
8
|
+
|
|
9
|
+
export interface DataTableColumn {
|
|
10
|
+
key: string;
|
|
11
|
+
header: string;
|
|
12
|
+
/** Right aligned, sorted as a number (5,15 and 5.15). */
|
|
13
|
+
numeric?: boolean;
|
|
14
|
+
/** Click the header to sort. Default true. */
|
|
15
|
+
sortable?: boolean;
|
|
16
|
+
/** Show the column in the Columns menu. Default true. */
|
|
17
|
+
hideable?: boolean;
|
|
18
|
+
/** Start hidden until the user checks it in the Columns menu. */
|
|
19
|
+
defaultHidden?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DataTableRow {
|
|
23
|
+
key: string;
|
|
24
|
+
status?: "active" | "deleted" | "archived";
|
|
25
|
+
cells: Record<string, string>;
|
|
26
|
+
/** Sort and export value when it differs from the visible text. */
|
|
27
|
+
values?: Record<string, string>;
|
|
28
|
+
href?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DataTableProps {
|
|
32
|
+
id?: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
columns: DataTableColumn[];
|
|
35
|
+
rows: DataTableRow[];
|
|
36
|
+
pageSize?: number;
|
|
37
|
+
searchPlaceholder?: string;
|
|
38
|
+
selectable?: boolean;
|
|
39
|
+
draggable?: boolean;
|
|
40
|
+
/** Trash and archive pills. Default true. */
|
|
41
|
+
filters?: boolean;
|
|
42
|
+
/** Demo mode: trash, restore, duplicate and delete change the rows in the page. */
|
|
43
|
+
live?: boolean;
|
|
44
|
+
/** Page actions on the right of the toolbar (New article, …). */
|
|
45
|
+
actions?: ReactNode;
|
|
46
|
+
onBulk?: (action: string, keys: string[]) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const PAGE_SIZES = [10, 15, 20, 50, 100];
|
|
50
|
+
|
|
51
|
+
/** Korrekt / OmniaDesk list table. The script in @birdycom/omniaui/data-table.js does the work. */
|
|
52
|
+
export function DataTable({
|
|
53
|
+
id,
|
|
54
|
+
title = "List",
|
|
55
|
+
columns,
|
|
56
|
+
rows,
|
|
57
|
+
pageSize = 15,
|
|
58
|
+
searchPlaceholder = "Search...",
|
|
59
|
+
selectable = true,
|
|
60
|
+
draggable = true,
|
|
61
|
+
filters = true,
|
|
62
|
+
live = false,
|
|
63
|
+
actions,
|
|
64
|
+
onBulk,
|
|
65
|
+
}: DataTableProps) {
|
|
66
|
+
const autoId = useId().replace(/:/g, "");
|
|
67
|
+
const tableId = id || `oc-dt-${autoId}`;
|
|
68
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
69
|
+
const onBulkRef = useRef(onBulk);
|
|
70
|
+
onBulkRef.current = onBulk;
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const root = rootRef.current;
|
|
74
|
+
if (!root) return;
|
|
75
|
+
bindDataTable(root);
|
|
76
|
+
const onBulkEvent = (event: Event) => {
|
|
77
|
+
const detail = (event as CustomEvent<{ action: string; keys: string[] }>).detail;
|
|
78
|
+
onBulkRef.current?.(detail.action, detail.keys);
|
|
79
|
+
};
|
|
80
|
+
root.addEventListener("oc-datatable-bulk", onBulkEvent);
|
|
81
|
+
return () => root.removeEventListener("oc-datatable-bulk", onBulkEvent);
|
|
82
|
+
}, [columns, rows]);
|
|
83
|
+
|
|
84
|
+
const lead = (selectable ? 1 : 0) + (draggable ? 1 : 0);
|
|
85
|
+
const sizes = [...new Set([...PAGE_SIZES, pageSize])].sort((a, b) => a - b);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
ref={rootRef}
|
|
90
|
+
className="oc-data-table oc-column-menu-scope"
|
|
91
|
+
id={tableId}
|
|
92
|
+
data-page-size={pageSize}
|
|
93
|
+
data-oc-live={live ? "true" : undefined}
|
|
94
|
+
>
|
|
95
|
+
<div className="oc-data-table-toolbar">
|
|
96
|
+
<div className="oc-data-table-heading">
|
|
97
|
+
<span className="oc-data-table-title">{title}</span>
|
|
98
|
+
<button className="oc-btn oc-btn--outline oc-btn--icon oc-btn--sm" type="button" popoverTarget={`${tableId}-menu`} aria-label="Table actions">⚙</button>
|
|
99
|
+
<div className="oc-dropdown-menu" id={`${tableId}-menu`} popover="auto">
|
|
100
|
+
<button className="oc-dropdown-item" type="button" data-oc-export="">Export CSV</button>
|
|
101
|
+
<button className="oc-dropdown-item" type="button" data-oc-import="">Import CSV</button>
|
|
102
|
+
<input className="oc-data-table-file" type="file" accept=".csv,text/csv" data-oc-file="" />
|
|
103
|
+
<div className="oc-dropdown-separator" />
|
|
104
|
+
<button className="oc-dropdown-item" type="button" data-oc-density="comfortable">Comfortable rows</button>
|
|
105
|
+
<button className="oc-dropdown-item" type="button" data-oc-density="compact">Compact rows</button>
|
|
106
|
+
<button className="oc-dropdown-item" type="button" data-oc-reset-widths="">Reset column widths</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<input className="oc-input oc-data-table-search" type="search" placeholder={searchPlaceholder} aria-label={`Search ${title}`} />
|
|
110
|
+
{filters && (
|
|
111
|
+
<div className="oc-filter-pills" role="group" aria-label="Trash filter">
|
|
112
|
+
<button className="oc-filter-pill" type="button" data-oc-filter="active" aria-current="true">Active only</button>
|
|
113
|
+
<button className="oc-filter-pill" type="button" data-oc-filter="all">Including trash</button>
|
|
114
|
+
<button className="oc-filter-pill" type="button" data-oc-filter="deleted">Trash only</button>
|
|
115
|
+
<button className="oc-filter-pill" type="button" data-oc-filter="archived">Archive only</button>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
<div className="oc-data-table-view" role="group" aria-label="View">
|
|
119
|
+
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-view="table" aria-pressed="true">Table</button>
|
|
120
|
+
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-view="grid">Cards</button>
|
|
121
|
+
</div>
|
|
122
|
+
{actions && <div className="oc-data-table-actions">{actions}</div>}
|
|
123
|
+
<p className="oc-data-table-sort-summary" data-oc-sort-summary="" role="status" />
|
|
124
|
+
</div>
|
|
125
|
+
<div className="oc-table-toolbar">
|
|
126
|
+
<span className="oc-table-toolbar-spacer" />
|
|
127
|
+
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" popoverTarget={`${tableId}-columns`}>Columns</button>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="oc-column-menu" id={`${tableId}-columns`} popover="auto">
|
|
130
|
+
<p className="oc-column-menu-title">Visible columns</p>
|
|
131
|
+
<form className="oc-column-menu-list">
|
|
132
|
+
{columns.map((col, index) => col.hideable === false ? null : (
|
|
133
|
+
<label className="oc-column-menu-item" key={col.key}>
|
|
134
|
+
<input type="checkbox" data-col={lead + index + 1} defaultChecked={!col.defaultHidden} /> {col.header}
|
|
135
|
+
</label>
|
|
136
|
+
))}
|
|
137
|
+
</form>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="oc-data-table-bulk" hidden>
|
|
140
|
+
<span data-oc-selected-count="">0 selected</span>
|
|
141
|
+
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="export">Export selection</button>
|
|
142
|
+
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="duplicate">Duplicate</button>
|
|
143
|
+
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="archive">Move to trash</button>
|
|
144
|
+
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="restore">Restore</button>
|
|
145
|
+
<button className="oc-btn oc-btn--destructive oc-btn--sm" type="button" data-oc-bulk="delete">Delete permanently</button>
|
|
146
|
+
<button className="oc-btn oc-btn--ghost oc-btn--sm" type="button" data-oc-bulk="clear">Clear</button>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="oc-data-table-frame">
|
|
149
|
+
<div className="oc-data-table-scroll">
|
|
150
|
+
<table className="oc-table oc-data-table-table">
|
|
151
|
+
<thead>
|
|
152
|
+
<tr>
|
|
153
|
+
{selectable && <th data-oc-col="select"><input className="oc-checkbox" type="checkbox" data-oc-select-all="" aria-label="Select all" /></th>}
|
|
154
|
+
{draggable && <th data-oc-col="order"><span className="oc-visually-hidden">Order</span></th>}
|
|
155
|
+
{columns.map((col) => (
|
|
156
|
+
<th key={col.key} data-oc-col={col.key} data-oc-sort={col.numeric ? "numeric" : "text"} data-numeric={col.numeric ? "" : undefined}>
|
|
157
|
+
<span className="oc-data-table-th">
|
|
158
|
+
{col.sortable === false ? col.header : <button className="oc-table-sort" type="button">{col.header}</button>}
|
|
159
|
+
<span className="oc-data-table-resize" role="separator" aria-orientation="vertical" aria-label={`Resize ${col.header}`} />
|
|
160
|
+
</span>
|
|
161
|
+
</th>
|
|
162
|
+
))}
|
|
163
|
+
<th data-oc-col="actions"><span className="oc-visually-hidden">Actions</span></th>
|
|
164
|
+
</tr>
|
|
165
|
+
</thead>
|
|
166
|
+
<tbody>
|
|
167
|
+
{rows.map((row) => (
|
|
168
|
+
<tr key={row.key} data-oc-row="" data-oc-key={row.key} data-oc-status={row.status || "active"} data-oc-href={row.href}>
|
|
169
|
+
{selectable && (
|
|
170
|
+
<td data-oc-col="select"><input className="oc-checkbox" type="checkbox" data-oc-select="" aria-label={`Select ${row.cells.name || row.key}`} /></td>
|
|
171
|
+
)}
|
|
172
|
+
{draggable && (
|
|
173
|
+
<td data-oc-col="order" className="oc-sortable-row-grip-cell">
|
|
174
|
+
<button className="oc-sortable-row-grip" type="button" aria-label={`Move ${row.cells.name || row.key}`} />
|
|
175
|
+
</td>
|
|
176
|
+
)}
|
|
177
|
+
{columns.map((col) => (
|
|
178
|
+
<td key={col.key} data-oc-col={col.key} data-numeric={col.numeric ? "" : undefined} data-oc-value={row.values?.[col.key]}>
|
|
179
|
+
{row.cells[col.key] ?? ""}
|
|
180
|
+
</td>
|
|
181
|
+
))}
|
|
182
|
+
<td data-oc-col="actions" className="oc-table-actions"><button className="oc-table-row-action" type="button">Edit</button></td>
|
|
183
|
+
</tr>
|
|
184
|
+
))}
|
|
185
|
+
</tbody>
|
|
186
|
+
</table>
|
|
187
|
+
</div>
|
|
188
|
+
<p className="oc-data-table-empty" hidden>No results</p>
|
|
189
|
+
</div>
|
|
190
|
+
<div className="oc-data-table-footer">
|
|
191
|
+
<p className="oc-data-table-summary" data-oc-summary="" />
|
|
192
|
+
<label className="oc-data-table-per-page">Rows per page
|
|
193
|
+
<select className="oc-select" data-oc-page-size="" aria-label="Rows per page" defaultValue={String(pageSize)}>
|
|
194
|
+
{sizes.map((n) => <option key={n} value={n}>{n}</option>)}
|
|
195
|
+
</select>
|
|
196
|
+
</label>
|
|
197
|
+
<nav className="oc-data-table-pager" data-oc-pager="" aria-label="Pagination" />
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
// File: packages/react/src/components/DecimalInput.tsx
|
|
2
|
-
// Map: helpers (locale separators, parse, format) -> component (text field, optional unit frame)
|
|
3
|
-
// Same markup as packages/ui/snippets/decimal-input.html. Formats on blur with Intl.NumberFormat.
|
|
4
|
-
"use client";
|
|
5
|
-
|
|
6
|
-
import { forwardRef, useEffect, useState } from "react";
|
|
7
|
-
import type { InputHTMLAttributes } from "react";
|
|
8
|
-
import { cx } from "../utils";
|
|
9
|
-
|
|
10
|
-
export interface DecimalInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "type"> {
|
|
11
|
-
/** The number; null = empty. */
|
|
12
|
-
value?: number | null;
|
|
13
|
-
defaultValue?: number | null;
|
|
14
|
-
onValueChange?: (value: number | null) => void;
|
|
15
|
-
/** BCP 47 locale for separators, e.g. "nl-BE" (1.234,56) or "en-US" (1,234.56). */
|
|
16
|
-
locale?: string;
|
|
17
|
-
/** Fixed number of decimals shown after blur. */
|
|
18
|
-
decimals?: number;
|
|
19
|
-
/** Unit text inside the frame (m², kg). */
|
|
20
|
-
unit?: string;
|
|
21
|
-
/** Left-aligned digits. */
|
|
22
|
-
alignStart?: boolean;
|
|
23
|
-
error?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Group and decimal separator of a locale, e.g. nl-BE -> { group: ".", decimal: "," }. */
|
|
27
|
-
function separators(locale: string) {
|
|
28
|
-
const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
|
|
29
|
-
return { group: parts.find((p) => p.type === "group")?.value ?? ",", decimal: parts.find((p) => p.type === "decimal")?.value ?? "." };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** "1.234,56" (nl-BE) -> 1234.56. Returns null for empty or unreadable text. */
|
|
33
|
-
export function parseDecimal(text: string, locale = "nl-BE"): number | null {
|
|
34
|
-
const { group, decimal } = separators(locale);
|
|
35
|
-
const clean = text.trim().split(group).join("").replace(decimal, ".").replace(/\s/g, "");
|
|
36
|
-
if (!clean) return null;
|
|
37
|
-
const n = Number(clean);
|
|
38
|
-
return Number.isFinite(n) ? n : null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** 1234.56 -> "1.234,56" (nl-BE, 2 decimals). */
|
|
42
|
-
export function formatDecimal(n: number | null | undefined, locale = "nl-BE", decimals = 2): string {
|
|
43
|
-
if (n == null) return "";
|
|
44
|
-
return new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(n);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** `.oc-decimal-input`: free typing, formatted on blur, value reported as a number. */
|
|
48
|
-
export const DecimalInput = forwardRef<HTMLInputElement, DecimalInputProps>(function DecimalInput(
|
|
49
|
-
{ value, defaultValue, onValueChange, locale = "nl-BE", decimals = 2, unit, alignStart, error, className, onBlur, onFocus, ...rest },
|
|
50
|
-
ref,
|
|
51
|
-
) {
|
|
52
|
-
const [text, setText] = useState(() => formatDecimal(value ?? defaultValue, locale, decimals));
|
|
53
|
-
const [focused, setFocused] = useState(false);
|
|
54
|
-
|
|
55
|
-
// Controlled: show a new outside value, but never while the user is typing.
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (value !== undefined && !focused) setText(formatDecimal(value, locale, decimals));
|
|
58
|
-
}, [value, locale, decimals, focused]);
|
|
59
|
-
|
|
60
|
-
const input = (
|
|
61
|
-
<input
|
|
62
|
-
ref={ref}
|
|
63
|
-
className={cx("oc-input", "oc-decimal-input", alignStart && "oc-decimal-input--start", className)}
|
|
64
|
-
type="text"
|
|
65
|
-
inputMode="decimal"
|
|
66
|
-
aria-invalid={error ? true : undefined}
|
|
67
|
-
value={text}
|
|
68
|
-
onFocus={(e) => { setFocused(true); onFocus?.(e); }}
|
|
69
|
-
onChange={(e) => { setText(e.target.value); onValueChange?.(parseDecimal(e.target.value, locale)); }}
|
|
70
|
-
onBlur={(e) => {
|
|
71
|
-
setFocused(false);
|
|
72
|
-
const n = parseDecimal(text, locale);
|
|
73
|
-
if (n != null || !text.trim()) setText(formatDecimal(n, locale, decimals));
|
|
74
|
-
onBlur?.(e);
|
|
75
|
-
}}
|
|
76
|
-
{...rest}
|
|
77
|
-
/>
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
if (!unit) return input;
|
|
81
|
-
return (
|
|
82
|
-
<div className="oc-decimal-field">
|
|
83
|
-
{input}
|
|
84
|
-
<span className="oc-decimal-field-unit">{unit}</span>
|
|
85
|
-
</div>
|
|
86
|
-
);
|
|
87
|
-
});
|
|
1
|
+
// File: packages/react/src/components/DecimalInput.tsx
|
|
2
|
+
// Map: helpers (locale separators, parse, format) -> component (text field, optional unit frame)
|
|
3
|
+
// Same markup as packages/ui/snippets/decimal-input.html. Formats on blur with Intl.NumberFormat.
|
|
4
|
+
"use client";
|
|
5
|
+
|
|
6
|
+
import { forwardRef, useEffect, useState } from "react";
|
|
7
|
+
import type { InputHTMLAttributes } from "react";
|
|
8
|
+
import { cx } from "../utils";
|
|
9
|
+
|
|
10
|
+
export interface DecimalInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "type"> {
|
|
11
|
+
/** The number; null = empty. */
|
|
12
|
+
value?: number | null;
|
|
13
|
+
defaultValue?: number | null;
|
|
14
|
+
onValueChange?: (value: number | null) => void;
|
|
15
|
+
/** BCP 47 locale for separators, e.g. "nl-BE" (1.234,56) or "en-US" (1,234.56). */
|
|
16
|
+
locale?: string;
|
|
17
|
+
/** Fixed number of decimals shown after blur. */
|
|
18
|
+
decimals?: number;
|
|
19
|
+
/** Unit text inside the frame (m², kg). */
|
|
20
|
+
unit?: string;
|
|
21
|
+
/** Left-aligned digits. */
|
|
22
|
+
alignStart?: boolean;
|
|
23
|
+
error?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Group and decimal separator of a locale, e.g. nl-BE -> { group: ".", decimal: "," }. */
|
|
27
|
+
function separators(locale: string) {
|
|
28
|
+
const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
|
|
29
|
+
return { group: parts.find((p) => p.type === "group")?.value ?? ",", decimal: parts.find((p) => p.type === "decimal")?.value ?? "." };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** "1.234,56" (nl-BE) -> 1234.56. Returns null for empty or unreadable text. */
|
|
33
|
+
export function parseDecimal(text: string, locale = "nl-BE"): number | null {
|
|
34
|
+
const { group, decimal } = separators(locale);
|
|
35
|
+
const clean = text.trim().split(group).join("").replace(decimal, ".").replace(/\s/g, "");
|
|
36
|
+
if (!clean) return null;
|
|
37
|
+
const n = Number(clean);
|
|
38
|
+
return Number.isFinite(n) ? n : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 1234.56 -> "1.234,56" (nl-BE, 2 decimals). */
|
|
42
|
+
export function formatDecimal(n: number | null | undefined, locale = "nl-BE", decimals = 2): string {
|
|
43
|
+
if (n == null) return "";
|
|
44
|
+
return new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(n);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `.oc-decimal-input`: free typing, formatted on blur, value reported as a number. */
|
|
48
|
+
export const DecimalInput = forwardRef<HTMLInputElement, DecimalInputProps>(function DecimalInput(
|
|
49
|
+
{ value, defaultValue, onValueChange, locale = "nl-BE", decimals = 2, unit, alignStart, error, className, onBlur, onFocus, ...rest },
|
|
50
|
+
ref,
|
|
51
|
+
) {
|
|
52
|
+
const [text, setText] = useState(() => formatDecimal(value ?? defaultValue, locale, decimals));
|
|
53
|
+
const [focused, setFocused] = useState(false);
|
|
54
|
+
|
|
55
|
+
// Controlled: show a new outside value, but never while the user is typing.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (value !== undefined && !focused) setText(formatDecimal(value, locale, decimals));
|
|
58
|
+
}, [value, locale, decimals, focused]);
|
|
59
|
+
|
|
60
|
+
const input = (
|
|
61
|
+
<input
|
|
62
|
+
ref={ref}
|
|
63
|
+
className={cx("oc-input", "oc-decimal-input", alignStart && "oc-decimal-input--start", className)}
|
|
64
|
+
type="text"
|
|
65
|
+
inputMode="decimal"
|
|
66
|
+
aria-invalid={error ? true : undefined}
|
|
67
|
+
value={text}
|
|
68
|
+
onFocus={(e) => { setFocused(true); onFocus?.(e); }}
|
|
69
|
+
onChange={(e) => { setText(e.target.value); onValueChange?.(parseDecimal(e.target.value, locale)); }}
|
|
70
|
+
onBlur={(e) => {
|
|
71
|
+
setFocused(false);
|
|
72
|
+
const n = parseDecimal(text, locale);
|
|
73
|
+
if (n != null || !text.trim()) setText(formatDecimal(n, locale, decimals));
|
|
74
|
+
onBlur?.(e);
|
|
75
|
+
}}
|
|
76
|
+
{...rest}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (!unit) return input;
|
|
81
|
+
return (
|
|
82
|
+
<div className="oc-decimal-field">
|
|
83
|
+
{input}
|
|
84
|
+
<span className="oc-decimal-field-unit">{unit}</span>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
});
|