@birdycom/omniaui-react 0.12.5 → 0.12.6
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/dist/index.d.ts +323 -35
- package/dist/index.js +2642 -1701
- package/manifest.json +90 -6
- package/package.json +1 -1
- package/src/blocks/BlockAppShell.tsx +1 -1
- package/src/blocks/BlockProfileHeader.tsx +20 -3
- package/src/components/Attachment.tsx +1 -1
- package/src/components/AvatarPicker.tsx +68 -0
- package/src/components/Barcode.tsx +42 -0
- package/src/components/Breadcrumb.tsx +8 -4
- package/src/components/Button.tsx +22 -7
- package/src/components/ChatComposer.tsx +1 -1
- package/src/components/DataTable.tsx +182 -33
- package/src/components/DateField.tsx +41 -0
- package/src/components/EmojiPicker.tsx +2 -2
- package/src/components/FormSection.tsx +32 -0
- package/src/components/Gallery.tsx +56 -0
- package/src/components/Glass.tsx +5 -3
- package/src/components/GlassBar.tsx +42 -0
- package/src/components/LazyImage.tsx +45 -0
- package/src/components/Message.tsx +7 -7
- package/src/components/MessageThread.tsx +10 -10
- package/src/components/MeterRow.tsx +38 -0
- package/src/components/NameCard.tsx +59 -0
- package/src/components/Price.tsx +45 -0
- package/src/components/Prose.tsx +3 -3
- package/src/components/Quantity.tsx +55 -0
- package/src/components/Rating.tsx +69 -0
- package/src/components/Ribbon.tsx +30 -0
- package/src/components/SpecList.tsx +36 -0
- package/src/components/Stock.tsx +28 -0
- package/src/index.ts +15 -1
- package/src/utils.ts +1 -1
- package/src/components/ColumnMenu.tsx +0 -66
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
// File: packages/react/src/components/DataTable.tsx
|
|
2
|
-
// Map: column and
|
|
2
|
+
// Map: icons -> column, row, tab and filter types -> name cell helper -> shell markup -> bind the list-table script on mount
|
|
3
3
|
"use client";
|
|
4
4
|
|
|
5
5
|
import { useEffect, useId, useRef } from "react";
|
|
6
6
|
import type { ReactNode } from "react";
|
|
7
7
|
import { bindDataTable } from "../../../ui/src/data-table.js";
|
|
8
|
+
import { Icon } from "./Icon";
|
|
9
|
+
|
|
10
|
+
/** Webicon paths (MDI) for the toolbar, drawn inline so the table needs no icon loader. */
|
|
11
|
+
const MDI = {
|
|
12
|
+
cog_outline: "M12 8a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 2a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2m-2 12c-.25 0-.46-.18-.5-.42l-.37-2.65c-.63-.25-1.17-.59-1.69-.99l-2.49 1.01c-.22.08-.49 0-.61-.22l-2-3.46a.493.493 0 0 1 .12-.64l2.11-1.66L4.5 12l.07-1l-2.11-1.63a.493.493 0 0 1-.12-.64l2-3.46c.12-.22.39-.31.61-.22l2.49 1c.52-.39 1.06-.73 1.69-.98l.37-2.65c.04-.24.25-.42.5-.42h4c.25 0 .46.18.5.42l.37 2.65c.63.25 1.17.59 1.69.98l2.49-1c.22-.09.49 0 .61.22l2 3.46c.13.22.07.49-.12.64L19.43 11l.07 1l-.07 1l2.11 1.63c.19.15.25.42.12.64l-2 3.46c-.12.22-.39.31-.61.22l-2.49-1c-.52.39-1.06.73-1.69.98l-.37 2.65c-.04.24-.25.42-.5.42zm1.25-18l-.37 2.61c-1.2.25-2.26.89-3.03 1.78L5.44 7.35l-.75 1.3L6.8 10.2a5.55 5.55 0 0 0 0 3.6l-2.12 1.56l.75 1.3l2.43-1.04c.77.88 1.82 1.52 3.01 1.76l.37 2.62h1.52l.37-2.61c1.19-.25 2.24-.89 3.01-1.77l2.43 1.04l.75-1.3l-2.12-1.55c.4-1.17.4-2.44 0-3.61l2.11-1.55l-.75-1.3l-2.41 1.04a5.42 5.42 0 0 0-3.03-1.77L12.75 4z",
|
|
13
|
+
format_list_bulleted: "M7 5h14v2H7zm0 8v-2h14v2zM4 4.5A1.5 1.5 0 0 1 5.5 6A1.5 1.5 0 0 1 4 7.5A1.5 1.5 0 0 1 2.5 6A1.5 1.5 0 0 1 4 4.5m0 6A1.5 1.5 0 0 1 5.5 12A1.5 1.5 0 0 1 4 13.5A1.5 1.5 0 0 1 2.5 12A1.5 1.5 0 0 1 4 10.5M7 19v-2h14v2zm-3-2.5A1.5 1.5 0 0 1 5.5 18A1.5 1.5 0 0 1 4 19.5A1.5 1.5 0 0 1 2.5 18A1.5 1.5 0 0 1 4 16.5",
|
|
14
|
+
view_grid_outline: "M3 11h8V3H3m2 2h4v4H5m8 12h8v-8h-8m2 2h4v4h-4M3 21h8v-8H3m2 2h4v4H5m8-16v8h8V3m-2 6h-4V5h4Z",
|
|
15
|
+
tune_variant: "M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2s2 .9 2 2s-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2s2 .9 2 2s-.9 2-2 2",
|
|
16
|
+
view_column_outline: "M4 5v13h17V5zm10 2v9h-3V7zM6 7h3v9H6zm13 9h-3V7h3z",
|
|
17
|
+
table_cog: "M3 3h14a2 2 0 0 1 2 2v7.08a6 6 0 0 0-4.32.92H11v4h1.08c-.11.68-.11 1.35 0 2H3a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2m0 4v4h6V7zm8 0v4h6V7zm-8 6v4h6v-4zm19.78 6.32l-1.07-.82c.02-.17.04-.33.04-.5s-.01-.33-.04-.5l1.06-.82a.26.26 0 0 0 .06-.32l-1-1.73c-.06-.13-.19-.13-.33-.13l-1.22.5c-.28-.18-.54-.35-.85-.47l-.19-1.32c-.01-.12-.12-.21-.24-.21h-2c-.12 0-.23.09-.25.21l-.19 1.32c-.3.13-.59.29-.85.47l-1.24-.5c-.11 0-.24 0-.31.13l-1 1.73c-.06.11-.04.24.06.32l1.06.82a4.2 4.2 0 0 0 0 1l-1.06.82a.26.26 0 0 0-.06.32l1 1.73c.06.13.19.13.31.13l1.24-.5c.26.18.54.35.85.47l.19 1.32c.02.12.12.21.25.21h2c.12 0 .23-.09.25-.21l.19-1.32c.3-.13.56-.29.84-.47l1.22.5c.14 0 .27 0 .34-.13l1-1.73a.26.26 0 0 0-.06-.32M18 19.5c-.83 0-1.5-.67-1.5-1.5s.68-1.5 1.5-1.5s1.5.67 1.5 1.5s-.66 1.5-1.5 1.5",
|
|
18
|
+
pencil_outline: "m14.06 9l.94.94L5.92 19H5v-.92zm3.6-6c-.25 0-.51.1-.7.29l-1.83 1.83l3.75 3.75l1.83-1.83c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29m-3.6 3.19L3 17.25V21h3.75L17.81 9.94z",
|
|
19
|
+
filter_outline: "M15 19.88c.04.3-.06.62-.29.83a.996.996 0 0 1-1.41 0L9.29 16.7a.99.99 0 0 1-.29-.83v-5.12L4.21 4.62a1 1 0 0 1 .17-1.4c.19-.14.4-.22.62-.22h14c.22 0 .43.08.62.22a1 1 0 0 1 .17 1.4L15 10.75zM7.04 5L11 10.06v5.52l2 2v-7.53L16.96 5z",
|
|
20
|
+
};
|
|
8
21
|
|
|
9
22
|
export interface DataTableColumn {
|
|
10
23
|
key: string;
|
|
@@ -17,17 +30,38 @@ export interface DataTableColumn {
|
|
|
17
30
|
hideable?: boolean;
|
|
18
31
|
/** Start hidden until the user checks it in the Columns menu. */
|
|
19
32
|
defaultHidden?: boolean;
|
|
33
|
+
/** Cut long text with an ellipsis at this width (e.g. "12rem"); the full text goes in the title. */
|
|
34
|
+
truncate?: string;
|
|
35
|
+
/** Sort the table on this column when it starts. */
|
|
36
|
+
defaultSort?: "ascending" | "descending";
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
export interface DataTableRow {
|
|
23
40
|
key: string;
|
|
24
41
|
status?: "active" | "deleted" | "archived";
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
/** Text or any node per column: a <DataTableName>, a badge, a count button … */
|
|
43
|
+
cells: Record<string, ReactNode>;
|
|
44
|
+
/** Sort, filter, search and export value when it differs from the visible content (needed for nodes). */
|
|
27
45
|
values?: Record<string, string>;
|
|
28
46
|
href?: string;
|
|
29
47
|
}
|
|
30
48
|
|
|
49
|
+
/** Tabs above the toolbar that filter one column (All / Customers / Companies …). */
|
|
50
|
+
export interface DataTableTabs {
|
|
51
|
+
column: string;
|
|
52
|
+
label?: string;
|
|
53
|
+
/** tone colors the count like the type badge in the table. */
|
|
54
|
+
items: { value: string; label: string; icon?: ReactNode; count?: number; tone?: "success" | "info" | "warning" }[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A select in the filter row that filters one column; "" is the "all" option. */
|
|
58
|
+
export interface DataTableColumnFilter {
|
|
59
|
+
column: string;
|
|
60
|
+
label: string;
|
|
61
|
+
icon?: ReactNode;
|
|
62
|
+
options: { value: string; label: string }[];
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
export interface DataTableProps {
|
|
32
66
|
id?: string;
|
|
33
67
|
title?: string;
|
|
@@ -36,9 +70,28 @@ export interface DataTableProps {
|
|
|
36
70
|
pageSize?: number;
|
|
37
71
|
searchPlaceholder?: string;
|
|
38
72
|
selectable?: boolean;
|
|
73
|
+
/** Grip column to drag rows. The drag itself is OmniaUI Pro (data-table-pro.js). Default false. */
|
|
39
74
|
draggable?: boolean;
|
|
40
|
-
/**
|
|
75
|
+
/** Handles to drag a column width. The resize itself is OmniaUI Pro (data-table-pro.js). Default false. */
|
|
76
|
+
resizable?: boolean;
|
|
77
|
+
/** Column chooser in the actions header (show or hide columns). Needs the OmniaUI Pro CSS. Default false. */
|
|
78
|
+
columnMenu?: boolean;
|
|
79
|
+
/** Trash pills (active / including trash / trash only). Default true. */
|
|
41
80
|
filters?: boolean;
|
|
81
|
+
/** Also an "Archive only" pill. Default true. */
|
|
82
|
+
archive?: boolean;
|
|
83
|
+
tabs?: DataTableTabs;
|
|
84
|
+
columnFilters?: DataTableColumnFilter[];
|
|
85
|
+
/** The buttons in the actions column of a row. Default: one soft edit button. */
|
|
86
|
+
rowActions?: (row: DataTableRow) => ReactNode;
|
|
87
|
+
/** Footer count, e.g. "Showing {from}–{to} of {total} customers". */
|
|
88
|
+
summary?: string;
|
|
89
|
+
/** Offer "Import CSV" in the settings (fires oc-datatable-import with the file). Default false. */
|
|
90
|
+
importable?: boolean;
|
|
91
|
+
/** "bordered" = version 2: tinted header, lines between the columns, sort chevrons always visible. */
|
|
92
|
+
variant?: "default" | "bordered";
|
|
93
|
+
/** Striped rows. Default true. */
|
|
94
|
+
striped?: boolean;
|
|
42
95
|
/** Demo mode: trash, restore, duplicate and delete change the rows in the page. */
|
|
43
96
|
live?: boolean;
|
|
44
97
|
/** Page actions on the right of the toolbar (New article, …). */
|
|
@@ -48,7 +101,39 @@ export interface DataTableProps {
|
|
|
48
101
|
|
|
49
102
|
const PAGE_SIZES = [10, 15, 20, 50, 100];
|
|
50
103
|
|
|
51
|
-
|
|
104
|
+
export interface DataTableNameProps {
|
|
105
|
+
name: string;
|
|
106
|
+
/** Initials in the avatar; default: the first letters of the name. */
|
|
107
|
+
initials?: string;
|
|
108
|
+
/** Image in the avatar instead of initials. */
|
|
109
|
+
avatar?: string;
|
|
110
|
+
/** A badge after the name (type, status …). */
|
|
111
|
+
badge?: ReactNode;
|
|
112
|
+
/** Muted second line (VAT number, e-mail …). */
|
|
113
|
+
sub?: ReactNode;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Name cell: avatar, name with a badge and a muted second line. */
|
|
117
|
+
export function DataTableName({ name, initials, avatar, badge, sub }: DataTableNameProps) {
|
|
118
|
+
const letters = initials ?? name.split(/[\s&-]+/).filter(Boolean).slice(0, 2).map((w) => w[0]).join("").toUpperCase();
|
|
119
|
+
return (
|
|
120
|
+
<span className="oc-data-table-name">
|
|
121
|
+
<span className="oc-avatar" aria-hidden="true">{avatar ? <img src={avatar} alt="" /> : letters}</span>
|
|
122
|
+
<span className="oc-data-table-name-text">
|
|
123
|
+
<span className="oc-data-table-name-line">{name} {badge}</span>
|
|
124
|
+
{sub ? <span className="oc-data-table-name-sub">{sub}</span> : null}
|
|
125
|
+
</span>
|
|
126
|
+
</span>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const defaultActions = (row: DataTableRow) => (
|
|
131
|
+
<button className="oc-btn oc-btn--soft-primary oc-btn--icon oc-btn--sm" type="button" aria-label={`Edit ${row.values?.name ?? row.key}`} title="Edit">
|
|
132
|
+
<Icon d={MDI.pencil_outline} />
|
|
133
|
+
</button>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
/** List table for business apps. The script in @birdycom/omniaui/data-table.js does the work. */
|
|
52
137
|
export function DataTable({
|
|
53
138
|
id,
|
|
54
139
|
title = "List",
|
|
@@ -57,8 +142,18 @@ export function DataTable({
|
|
|
57
142
|
pageSize = 15,
|
|
58
143
|
searchPlaceholder = "Search...",
|
|
59
144
|
selectable = true,
|
|
60
|
-
draggable =
|
|
145
|
+
draggable = false,
|
|
146
|
+
resizable = false,
|
|
147
|
+
columnMenu = false,
|
|
61
148
|
filters = true,
|
|
149
|
+
archive = true,
|
|
150
|
+
tabs,
|
|
151
|
+
columnFilters,
|
|
152
|
+
rowActions = defaultActions,
|
|
153
|
+
summary,
|
|
154
|
+
importable = false,
|
|
155
|
+
variant = "default",
|
|
156
|
+
striped = true,
|
|
62
157
|
live = false,
|
|
63
158
|
actions,
|
|
64
159
|
onBulk,
|
|
@@ -83,27 +178,53 @@ export function DataTable({
|
|
|
83
178
|
|
|
84
179
|
const lead = (selectable ? 1 : 0) + (draggable ? 1 : 0);
|
|
85
180
|
const sizes = [...new Set([...PAGE_SIZES, pageSize])].sort((a, b) => a - b);
|
|
181
|
+
const label = (row: DataTableRow) => row.values?.name ?? (typeof row.cells.name === "string" ? row.cells.name : row.key);
|
|
86
182
|
|
|
87
183
|
return (
|
|
88
184
|
<div
|
|
89
185
|
ref={rootRef}
|
|
90
|
-
className="oc-data-table oc-column-menu-scope"
|
|
186
|
+
className={["oc-data-table", variant === "bordered" && "oc-data-table--bordered", !striped && "oc-data-table--plain", columnMenu && "oc-column-menu-scope"].filter(Boolean).join(" ")}
|
|
91
187
|
id={tableId}
|
|
92
188
|
data-page-size={pageSize}
|
|
93
189
|
data-oc-live={live ? "true" : undefined}
|
|
94
190
|
>
|
|
191
|
+
{tabs && (
|
|
192
|
+
<div className="oc-tab-list oc-data-table-tabs" role="tablist" aria-label={tabs.label ?? "Filter"}>
|
|
193
|
+
{tabs.items.map((tab, i) => (
|
|
194
|
+
<button
|
|
195
|
+
key={tab.value}
|
|
196
|
+
className="oc-tab-trigger"
|
|
197
|
+
role="tab"
|
|
198
|
+
type="button"
|
|
199
|
+
data-oc-col-filter={tabs.column}
|
|
200
|
+
data-oc-value={tab.value}
|
|
201
|
+
aria-selected={i === 0 ? "true" : "false"}
|
|
202
|
+
>
|
|
203
|
+
{tab.icon}{tab.label}
|
|
204
|
+
{tab.count != null && <span className={`oc-data-table-tab-count${tab.tone ? ` oc-data-table-tab-count--${tab.tone}` : ""}`}>{tab.count}</span>}
|
|
205
|
+
</button>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
95
209
|
<div className="oc-data-table-toolbar">
|
|
96
210
|
<div className="oc-data-table-heading">
|
|
97
211
|
<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="
|
|
212
|
+
<button className="oc-btn oc-btn--outline oc-btn--icon oc-btn--sm" type="button" popoverTarget={`${tableId}-menu`} aria-label="List actions" title="List actions">
|
|
213
|
+
<Icon d={MDI.tune_variant} />
|
|
214
|
+
</button>
|
|
99
215
|
<div className="oc-dropdown-menu" id={`${tableId}-menu`} popover="auto">
|
|
100
216
|
<button className="oc-dropdown-item" type="button" data-oc-export="">Export CSV</button>
|
|
101
|
-
|
|
102
|
-
|
|
217
|
+
{importable && (
|
|
218
|
+
<>
|
|
219
|
+
<button className="oc-dropdown-item" type="button" data-oc-import="">Import CSV</button>
|
|
220
|
+
<input className="oc-data-table-file" type="file" accept=".csv,text/csv" data-oc-file="" />
|
|
221
|
+
</>
|
|
222
|
+
)}
|
|
223
|
+
<button className="oc-dropdown-item" type="button" data-oc-bulk="duplicate">Duplicate selection</button>
|
|
224
|
+
<button className="oc-dropdown-item" type="button" data-oc-bulk="archive">Move selection to trash</button>
|
|
103
225
|
<div className="oc-dropdown-separator" />
|
|
104
226
|
<button className="oc-dropdown-item" type="button" data-oc-density="comfortable">Comfortable rows</button>
|
|
105
227
|
<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
228
|
</div>
|
|
108
229
|
</div>
|
|
109
230
|
<input className="oc-input oc-data-table-search" type="search" placeholder={searchPlaceholder} aria-label={`Search ${title}`} />
|
|
@@ -112,22 +233,36 @@ export function DataTable({
|
|
|
112
233
|
<button className="oc-filter-pill" type="button" data-oc-filter="active" aria-current="true">Active only</button>
|
|
113
234
|
<button className="oc-filter-pill" type="button" data-oc-filter="all">Including trash</button>
|
|
114
235
|
<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>
|
|
236
|
+
{archive && <button className="oc-filter-pill" type="button" data-oc-filter="archived">Archive only</button>}
|
|
116
237
|
</div>
|
|
117
238
|
)}
|
|
118
239
|
<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"
|
|
120
|
-
|
|
240
|
+
<button className="oc-btn oc-btn--outline oc-btn--icon oc-btn--sm" type="button" data-oc-view="table" aria-pressed="true" aria-label="Table">
|
|
241
|
+
<Icon d={MDI.format_list_bulleted} />
|
|
242
|
+
</button>
|
|
243
|
+
<button className="oc-btn oc-btn--outline oc-btn--icon oc-btn--sm" type="button" data-oc-view="grid" aria-label="Cards">
|
|
244
|
+
<Icon d={MDI.view_grid_outline} />
|
|
245
|
+
</button>
|
|
121
246
|
</div>
|
|
122
247
|
{actions && <div className="oc-data-table-actions">{actions}</div>}
|
|
123
248
|
<p className="oc-data-table-sort-summary" data-oc-sort-summary="" role="status" />
|
|
124
249
|
</div>
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
250
|
+
{columnFilters?.length ? (
|
|
251
|
+
<div className="oc-data-table-filters" id={`${tableId}-filters`}>
|
|
252
|
+
{columnFilters.map((f) => (
|
|
253
|
+
<label className="oc-data-table-filter" key={f.column}>
|
|
254
|
+
{f.icon ?? <Icon d={MDI.filter_outline} />}
|
|
255
|
+
<select className="oc-select" data-oc-col-filter={f.column} aria-label={f.label} defaultValue="">
|
|
256
|
+
{f.options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
|
257
|
+
</select>
|
|
258
|
+
</label>
|
|
259
|
+
))}
|
|
260
|
+
</div>
|
|
261
|
+
) : null}
|
|
262
|
+
{columnMenu && (<div className="oc-column-menu" id={`${tableId}-columns`} popover="auto">
|
|
263
|
+
<p className="oc-column-menu-title">Columns</p>
|
|
264
|
+
{resizable && <button className="oc-dropdown-item" type="button" data-oc-reset-widths="">Reset column widths</button>}
|
|
265
|
+
{resizable && <div className="oc-dropdown-separator" />}
|
|
131
266
|
<form className="oc-column-menu-list">
|
|
132
267
|
{columns.map((col, index) => col.hideable === false ? null : (
|
|
133
268
|
<label className="oc-column-menu-item" key={col.key}>
|
|
@@ -135,7 +270,7 @@ export function DataTable({
|
|
|
135
270
|
</label>
|
|
136
271
|
))}
|
|
137
272
|
</form>
|
|
138
|
-
</div>
|
|
273
|
+
</div>)}
|
|
139
274
|
<div className="oc-data-table-bulk" hidden>
|
|
140
275
|
<span data-oc-selected-count="">0 selected</span>
|
|
141
276
|
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" data-oc-bulk="export">Export selection</button>
|
|
@@ -153,33 +288,47 @@ export function DataTable({
|
|
|
153
288
|
{selectable && <th data-oc-col="select"><input className="oc-checkbox" type="checkbox" data-oc-select-all="" aria-label="Select all" /></th>}
|
|
154
289
|
{draggable && <th data-oc-col="order"><span className="oc-visually-hidden">Order</span></th>}
|
|
155
290
|
{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}>
|
|
291
|
+
<th key={col.key} data-oc-col={col.key} data-oc-sort={col.numeric ? "numeric" : "text"} data-numeric={col.numeric ? "" : undefined} aria-sort={col.defaultSort}>
|
|
157
292
|
<span className="oc-data-table-th">
|
|
158
293
|
{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}`} />
|
|
294
|
+
{resizable && <span className="oc-data-table-resize" role="separator" aria-orientation="vertical" aria-label={`Resize ${col.header}`} />}
|
|
160
295
|
</span>
|
|
161
296
|
</th>
|
|
162
297
|
))}
|
|
163
|
-
<th data-oc-col="actions"
|
|
298
|
+
<th data-oc-col="actions">
|
|
299
|
+
<span className="oc-data-table-th">
|
|
300
|
+
Actions
|
|
301
|
+
{columnMenu && (
|
|
302
|
+
<button className="oc-btn oc-btn--plain oc-data-table-columns-btn" type="button" popoverTarget={`${tableId}-columns`} aria-label="Columns" title="Columns">
|
|
303
|
+
<Icon d={MDI.view_column_outline} />
|
|
304
|
+
</button>
|
|
305
|
+
)}
|
|
306
|
+
</span>
|
|
307
|
+
</th>
|
|
164
308
|
</tr>
|
|
165
309
|
</thead>
|
|
166
310
|
<tbody>
|
|
167
311
|
{rows.map((row) => (
|
|
168
312
|
<tr key={row.key} data-oc-row="" data-oc-key={row.key} data-oc-status={row.status || "active"} data-oc-href={row.href}>
|
|
169
313
|
{selectable && (
|
|
170
|
-
<td data-oc-col="select"><input className="oc-checkbox" type="checkbox" data-oc-select="" aria-label={`Select ${row
|
|
314
|
+
<td data-oc-col="select"><input className="oc-checkbox" type="checkbox" data-oc-select="" aria-label={`Select ${label(row)}`} /></td>
|
|
171
315
|
)}
|
|
172
316
|
{draggable && (
|
|
173
317
|
<td data-oc-col="order" className="oc-sortable-row-grip-cell">
|
|
174
|
-
<button className="oc-sortable-row-grip" type="button" aria-label={`Move ${row
|
|
318
|
+
<button className="oc-sortable-row-grip" type="button" aria-label={`Move ${label(row)}`} />
|
|
175
319
|
</td>
|
|
176
320
|
)}
|
|
177
|
-
{columns.map((col) =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
321
|
+
{columns.map((col) => {
|
|
322
|
+
const cell = row.cells[col.key] ?? "";
|
|
323
|
+
return (
|
|
324
|
+
<td key={col.key} data-oc-col={col.key} data-numeric={col.numeric ? "" : undefined} data-oc-value={row.values?.[col.key]}>
|
|
325
|
+
{col.truncate && typeof cell === "string"
|
|
326
|
+
? <span className="oc-data-table-truncate" style={{ ["--oc-data-table-truncate" as string]: col.truncate }} title={cell}>{cell}</span>
|
|
327
|
+
: cell}
|
|
328
|
+
</td>
|
|
329
|
+
);
|
|
330
|
+
})}
|
|
331
|
+
<td data-oc-col="actions"><div style={{ display: "flex", gap: "0.375rem", justifyContent: "flex-end" }}>{rowActions(row)}</div></td>
|
|
183
332
|
</tr>
|
|
184
333
|
))}
|
|
185
334
|
</tbody>
|
|
@@ -188,7 +337,7 @@ export function DataTable({
|
|
|
188
337
|
<p className="oc-data-table-empty" hidden>No results</p>
|
|
189
338
|
</div>
|
|
190
339
|
<div className="oc-data-table-footer">
|
|
191
|
-
<p className="oc-data-table-summary" data-oc-summary="" />
|
|
340
|
+
<p className="oc-data-table-summary" data-oc-summary={summary ?? ""} />
|
|
192
341
|
<label className="oc-data-table-per-page">Rows per page
|
|
193
342
|
<select className="oc-select" data-oc-page-size="" aria-label="Rows per page" defaultValue={String(pageSize)}>
|
|
194
343
|
{sizes.map((n) => <option key={n} value={n}>{n}</option>)}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useEffect, useState } from "react";
|
|
4
|
+
import type { InputHTMLAttributes } from "react";
|
|
5
|
+
import { isoWeek } from "../../../ui/src/form-extras.js";
|
|
6
|
+
import { cx } from "../utils";
|
|
7
|
+
import { Icon } from "./Icon";
|
|
8
|
+
|
|
9
|
+
export interface DateFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "size"> {
|
|
10
|
+
/** Show the ISO week at the end. Default true. */
|
|
11
|
+
week?: boolean;
|
|
12
|
+
/** Week label template: {week} and {year}. Default "Week {week}". */
|
|
13
|
+
weekLabel?: string;
|
|
14
|
+
size?: "md" | "sm";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Native date input with a calendar icon zone and the ISO week (`.oc-date-field`). */
|
|
18
|
+
export const DateField = forwardRef<HTMLInputElement, DateFieldProps>(function DateField(
|
|
19
|
+
{ week = true, weekLabel = "Week {week}", size = "md", className, value, defaultValue, onChange, ...rest },
|
|
20
|
+
ref,
|
|
21
|
+
) {
|
|
22
|
+
const [current, setCurrent] = useState(String(value ?? defaultValue ?? ""));
|
|
23
|
+
useEffect(() => { if (value !== undefined) setCurrent(String(value)); }, [value]);
|
|
24
|
+
const [y, m, d] = current.split("-").map(Number);
|
|
25
|
+
const label = week && y ? (() => { const w = isoWeek(new Date(y, m - 1, d)); return weekLabel.replace("{week}", String(w.week)).replace("{year}", String(w.year)); })() : "";
|
|
26
|
+
return (
|
|
27
|
+
<div className={cx("oc-date-field", size === "sm" && "oc-date-field--sm", className)}>
|
|
28
|
+
<span className="oc-date-field__icon"><Icon d="M19 3h-1V1h-2v2H8V1H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.11 0 2-.89 2-2V5a2 2 0 0 0-2-2m0 16H5V9h14zm0-12H5V5h14z" /></span>
|
|
29
|
+
<input
|
|
30
|
+
ref={ref}
|
|
31
|
+
type="date"
|
|
32
|
+
className="oc-date-field__input"
|
|
33
|
+
value={value}
|
|
34
|
+
defaultValue={defaultValue}
|
|
35
|
+
onChange={(event) => { setCurrent(event.target.value); onChange?.(event); }}
|
|
36
|
+
{...rest}
|
|
37
|
+
/>
|
|
38
|
+
{week && <output className="oc-date-field__week">{label}</output>}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
});
|
|
@@ -24,14 +24,14 @@ export interface EmojiPickerProps extends Omit<HTMLAttributes<HTMLDivElement>, "
|
|
|
24
24
|
onSelect?: (emoji: string) => void;
|
|
25
25
|
/** Always-visible panel without trigger / popover. */
|
|
26
26
|
inline?: boolean;
|
|
27
|
-
/** Trigger content (default
|
|
27
|
+
/** Trigger content (default: a smiley webicon) and its accessible name. */
|
|
28
28
|
trigger?: ReactNode;
|
|
29
29
|
triggerLabel?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** `.oc-emoji-picker`: popover grid of emoji buttons, anchored to its trigger. */
|
|
33
33
|
export const EmojiPicker = forwardRef<HTMLDivElement, EmojiPickerProps>(function EmojiPicker(
|
|
34
|
-
{ groups = defaultGroups, onSelect, inline, trigger = "
|
|
34
|
+
{ groups = defaultGroups, onSelect, inline, trigger = <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20 12a8 8 0 0 0-8-8a8 8 0 0 0-8 8a8 8 0 0 0 8 8a8 8 0 0 0 8-8m2 0a10 10 0 0 1-10 10A10 10 0 0 1 2 12A10 10 0 0 1 12 2a10 10 0 0 1 10 10m-12-3.5c0 .8-.7 1.5-1.5 1.5S7 9.3 7 8.5S7.7 7 8.5 7s1.5.7 1.5 1.5m7 0c0 .8-.7 1.5-1.5 1.5S14 9.3 14 8.5S14.7 7 15.5 7s1.5.7 1.5 1.5m-5 9.73c-1.75 0-3.29-.73-4.19-1.81L9.23 14c.45.72 1.52 1.23 2.77 1.23s2.32-.51 2.77-1.23l1.42 1.42c-.9 1.08-2.44 1.81-4.19 1.81"/></svg>, triggerLabel = "Insert emoji", className, id, "aria-label": label = "Emoji", ...rest },
|
|
35
35
|
ref,
|
|
36
36
|
) {
|
|
37
37
|
const auto = useId();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { DetailsHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface FormSectionProps extends Omit<DetailsHTMLAttributes<HTMLDetailsElement>, "title"> {
|
|
6
|
+
title: ReactNode;
|
|
7
|
+
hint?: ReactNode;
|
|
8
|
+
/** Webicon or SVG in the tinted square. */
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
/** Right side of the header: a badge (Complete, 1 error) … */
|
|
11
|
+
status?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** `<details class="oc-form-section">`: icon, title, hint, status; children are the fields (responsive grid). */
|
|
15
|
+
export const FormSection = forwardRef<HTMLDetailsElement, FormSectionProps>(function FormSection(
|
|
16
|
+
{ title, hint, icon, status, className, children, ...rest },
|
|
17
|
+
ref,
|
|
18
|
+
) {
|
|
19
|
+
return (
|
|
20
|
+
<details ref={ref} className={cx("oc-form-section", className)} {...rest}>
|
|
21
|
+
<summary className="oc-form-section__summary">
|
|
22
|
+
<span className="oc-form-section__icon">{icon}</span>
|
|
23
|
+
<span className="oc-form-section__heading">
|
|
24
|
+
<span className="oc-form-section__title">{title}</span>
|
|
25
|
+
{hint && <span className="oc-form-section__hint">{hint}</span>}
|
|
26
|
+
</span>
|
|
27
|
+
<span className="oc-form-section__status">{status}</span>
|
|
28
|
+
</summary>
|
|
29
|
+
<div className="oc-form-section__body">{children}</div>
|
|
30
|
+
</details>
|
|
31
|
+
);
|
|
32
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Same markup as packages/ui/snippets/gallery.html.
|
|
4
|
+
import { forwardRef, useId } from "react";
|
|
5
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
6
|
+
import { cx } from "../utils";
|
|
7
|
+
|
|
8
|
+
export interface GalleryImage {
|
|
9
|
+
src: string;
|
|
10
|
+
alt: string;
|
|
11
|
+
/** Thumbnail image; default the same src. */
|
|
12
|
+
thumb?: string;
|
|
13
|
+
/** Short name of the thumbnail (its radio label); default the alt. */
|
|
14
|
+
thumbAlt?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GalleryProps extends HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
images: GalleryImage[];
|
|
19
|
+
/** Name of the photos (thumbnail group legend). */
|
|
20
|
+
label?: string;
|
|
21
|
+
badge?: ReactNode;
|
|
22
|
+
side?: boolean;
|
|
23
|
+
/** Radio name; default generated. */
|
|
24
|
+
name?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Product photos with thumbnails (`.oc-gallery`), CSS only (radios + :has, up to 8). */
|
|
28
|
+
export const Gallery = forwardRef<HTMLDivElement, GalleryProps>(function Gallery(
|
|
29
|
+
{ images, label = "Photos", badge, side, name, className, ...rest },
|
|
30
|
+
ref,
|
|
31
|
+
) {
|
|
32
|
+
const auto = useId();
|
|
33
|
+
const group = name ?? `oc-gallery-${auto.replace(/:/g, "")}`;
|
|
34
|
+
const list = images.slice(0, 8);
|
|
35
|
+
return (
|
|
36
|
+
<div ref={ref} className={cx("oc-gallery", side && "oc-gallery--side", className)} {...rest}>
|
|
37
|
+
<div className="oc-gallery__stage">
|
|
38
|
+
{badge != null && <span className="oc-badge oc-badge--destructive oc-gallery__badge">{badge}</span>}
|
|
39
|
+
{list.map((img, i) => (
|
|
40
|
+
<figure key={i} className="oc-gallery__slide"><img src={img.src} alt={img.alt} loading={i ? "lazy" : undefined} /></figure>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
{list.length > 1 && (
|
|
44
|
+
<fieldset className="oc-gallery__thumbs">
|
|
45
|
+
<legend>{label}</legend>
|
|
46
|
+
{list.map((img, i) => (
|
|
47
|
+
<label key={i} className="oc-gallery__thumb">
|
|
48
|
+
<input type="radio" name={group} defaultChecked={i === 0} />
|
|
49
|
+
<img src={img.thumb ?? img.src} alt={img.thumbAlt ?? img.alt} />
|
|
50
|
+
</label>
|
|
51
|
+
))}
|
|
52
|
+
</fieldset>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
});
|
package/src/components/Glass.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// File: packages/react/src/components/Glass.tsx | Map: types -> Glass (sheet: --strong, --flat) -> GlassPanel (layer: --strong)
|
|
1
|
+
// File: packages/react/src/components/Glass.tsx | Map: types -> Glass (sheet: --strong, --flat, --liquid) -> GlassPanel (layer: --strong)
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
import type { HTMLAttributes } from "react";
|
|
4
4
|
import { cx } from "../utils";
|
|
@@ -8,15 +8,17 @@ export interface GlassProps extends HTMLAttributes<HTMLElement> {
|
|
|
8
8
|
strong?: boolean;
|
|
9
9
|
/** Desktop window where the OS draws the blur: no own blur, no drop shadow. */
|
|
10
10
|
flat?: boolean;
|
|
11
|
+
/** Liquid glass: clear tint, strong blur and saturation, specular rim. */
|
|
12
|
+
liquid?: boolean;
|
|
11
13
|
as?: "div" | "section" | "aside" | "article" | "main";
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/** `.oc-glass`: see-through sheet with a light top edge; .oc-card inside becomes a glass panel. */
|
|
15
17
|
export const Glass = forwardRef<HTMLElement, GlassProps>(function Glass(
|
|
16
|
-
{ strong, flat, as: Tag = "div", className, ...rest },
|
|
18
|
+
{ strong, flat, liquid, as: Tag = "div", className, ...rest },
|
|
17
19
|
ref,
|
|
18
20
|
) {
|
|
19
|
-
return <Tag ref={ref as React.Ref<HTMLDivElement>} className={cx("oc-glass", strong && "oc-glass--strong", flat && "oc-glass--flat", className)} {...rest} />;
|
|
21
|
+
return <Tag ref={ref as React.Ref<HTMLDivElement>} className={cx("oc-glass", strong && "oc-glass--strong", flat && "oc-glass--flat", liquid && "oc-glass--liquid", className)} {...rest} />;
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
export interface GlassPanelProps extends HTMLAttributes<HTMLDivElement> {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// File: packages/react/src/components/GlassBar.tsx | Map: types -> GlassBar (liquid glass menu bar: items with icon and label, the current one raised)
|
|
2
|
+
// Same markup as the "Liquid glass menu bar" demo of packages/ui (parts/media-glass-liquid.css).
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
5
|
+
import { cx } from "../utils";
|
|
6
|
+
|
|
7
|
+
export interface GlassBarItem {
|
|
8
|
+
label: string;
|
|
9
|
+
/** An <Icon>, an <iconify-icon> or an inline SVG. */
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
href?: string;
|
|
12
|
+
current?: boolean;
|
|
13
|
+
onClick?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GlassBarProps extends HTMLAttributes<HTMLElement> {
|
|
17
|
+
items: GlassBarItem[];
|
|
18
|
+
/** Icons only (the label becomes the accessible name). */
|
|
19
|
+
icons?: boolean;
|
|
20
|
+
/** Accessible name of the bar. */
|
|
21
|
+
label?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `.oc-glass-bar`: a floating liquid glass menu bar; the current item sits in a raised bubble. */
|
|
25
|
+
export const GlassBar = forwardRef<HTMLElement, GlassBarProps>(function GlassBar(
|
|
26
|
+
{ items, icons, label = "Main", className, ...rest },
|
|
27
|
+
ref,
|
|
28
|
+
) {
|
|
29
|
+
return (
|
|
30
|
+
<nav ref={ref} className={cx("oc-glass-bar", icons && "oc-glass-bar--icons", className)} aria-label={label} {...rest}>
|
|
31
|
+
{items.map((item, i) => {
|
|
32
|
+
const content = <>{item.icon}{!icons && <span>{item.label}</span>}</>;
|
|
33
|
+
const common = { className: "oc-glass-bar__item", "aria-label": icons ? item.label : undefined };
|
|
34
|
+
return item.href != null ? (
|
|
35
|
+
<a key={i} {...common} href={item.href} aria-current={item.current ? "page" : undefined} onClick={item.onClick}>{content}</a>
|
|
36
|
+
) : (
|
|
37
|
+
<button key={i} {...common} type="button" aria-pressed={item.current ?? false} onClick={item.onClick}>{content}</button>
|
|
38
|
+
);
|
|
39
|
+
})}
|
|
40
|
+
</nav>
|
|
41
|
+
);
|
|
42
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// File: packages/react/src/components/LazyImage.tsx | Map: props -> figure.oc-lazy (ratio, preview) -> img loading=lazy -> loaded / error state
|
|
2
|
+
// Same markup as packages/ui/snippets/lazy.html.
|
|
3
|
+
import { forwardRef, useState } from "react";
|
|
4
|
+
import type { CSSProperties, HTMLAttributes, ImgHTMLAttributes, ReactNode } from "react";
|
|
5
|
+
import { cx } from "../utils";
|
|
6
|
+
|
|
7
|
+
export interface LazyImageProps extends HTMLAttributes<HTMLElement> {
|
|
8
|
+
src: string;
|
|
9
|
+
alt: string;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
/** Aspect ratio of the box, e.g. "16 / 10" (default) or "1". */
|
|
13
|
+
ratio?: string;
|
|
14
|
+
/** URL of a tiny copy, shown blurred until the photo is there (instead of the shimmer). */
|
|
15
|
+
preview?: string;
|
|
16
|
+
caption?: ReactNode;
|
|
17
|
+
/** "lazy" (default) or "eager" for images in the first screen. */
|
|
18
|
+
loading?: "lazy" | "eager";
|
|
19
|
+
imgProps?: ImgHTMLAttributes<HTMLImageElement>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `.oc-lazy`: an image that keeps its place, shows a placeholder and fades in once loaded. */
|
|
23
|
+
export const LazyImage = forwardRef<HTMLElement, LazyImageProps>(function LazyImage(
|
|
24
|
+
{ src, alt, width, height, ratio, preview, caption, loading = "lazy", imgProps, className, style, ...rest },
|
|
25
|
+
ref,
|
|
26
|
+
) {
|
|
27
|
+
const [state, setState] = useState<"" | "is-loaded" | "is-error">("");
|
|
28
|
+
const vars = { ...(ratio ? { "--oc-lazy-ratio": ratio } : {}), ...(preview ? { "--oc-lazy-preview": `url(${preview})` } : {}), ...style } as CSSProperties;
|
|
29
|
+
return (
|
|
30
|
+
<figure ref={ref} className={cx("oc-lazy", state, className)} style={vars} {...rest}>
|
|
31
|
+
<img
|
|
32
|
+
src={src}
|
|
33
|
+
alt={alt}
|
|
34
|
+
width={width}
|
|
35
|
+
height={height}
|
|
36
|
+
loading={loading}
|
|
37
|
+
decoding="async"
|
|
38
|
+
onLoad={() => setState("is-loaded")}
|
|
39
|
+
onError={() => setState("is-error")}
|
|
40
|
+
{...imgProps}
|
|
41
|
+
/>
|
|
42
|
+
{caption != null && <figcaption>{caption}</figcaption>}
|
|
43
|
+
</figure>
|
|
44
|
+
);
|
|
45
|
+
});
|