@idyeah/diya 0.0.0 → 1.0.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/CHANGELOG.md +332 -0
- package/NOTICE +29 -0
- package/README.md +596 -5
- package/dist/DiyaProvider.d.ts +79 -0
- package/dist/DiyaProvider.js +77 -0
- package/dist/Icon.d.ts +10 -0
- package/dist/Icon.js +27 -0
- package/dist/accordion/Accordion.d.ts +34 -0
- package/dist/accordion/Accordion.js +15 -0
- package/dist/alert/Alert.d.ts +49 -0
- package/dist/alert/Alert.js +11 -0
- package/dist/alert/messages.d.ts +9 -0
- package/dist/alert/messages.js +2 -0
- package/dist/api.json +7277 -0
- package/dist/api.schema.json +214 -0
- package/dist/application-patterns/Chatbot.d.ts +114 -0
- package/dist/application-patterns/Chatbot.js +110 -0
- package/dist/application-patterns/SideNavigation.d.ts +74 -0
- package/dist/application-patterns/SideNavigation.js +31 -0
- package/dist/application-patterns/chatbot-messages.d.ts +20 -0
- package/dist/application-patterns/chatbot-messages.js +2 -0
- package/dist/application-patterns/side-navigation-messages.d.ts +5 -0
- package/dist/application-patterns/side-navigation-messages.js +2 -0
- package/dist/avatar/Avatar.d.ts +32 -0
- package/dist/avatar/Avatar.js +19 -0
- package/dist/avatar/messages.d.ts +7 -0
- package/dist/avatar/messages.js +2 -0
- package/dist/badge/Badge.d.ts +42 -0
- package/dist/badge/Badge.js +16 -0
- package/dist/badge/badge-utils.d.ts +6 -0
- package/dist/badge/badge-utils.js +24 -0
- package/dist/badge/messages.d.ts +4 -0
- package/dist/badge/messages.js +2 -0
- package/dist/breadcrumbs/Breadcrumbs.d.ts +34 -0
- package/dist/breadcrumbs/Breadcrumbs.js +19 -0
- package/dist/breadcrumbs/messages.d.ts +5 -0
- package/dist/breadcrumbs/messages.js +2 -0
- package/dist/button/Button.d.ts +46 -0
- package/dist/button/Button.js +44 -0
- package/dist/calendar/Calendar.d.ts +55 -0
- package/dist/calendar/Calendar.js +61 -0
- package/dist/calendar/messages.d.ts +8 -0
- package/dist/calendar/messages.js +2 -0
- package/dist/calendar/month.d.ts +61 -0
- package/dist/calendar/month.js +134 -0
- package/dist/card/Card.d.ts +46 -0
- package/dist/card/Card.js +8 -0
- package/dist/choice/Choice.d.ts +72 -0
- package/dist/choice/Choice.js +68 -0
- package/dist/choice/messages.d.ts +4 -0
- package/dist/choice/messages.js +2 -0
- package/dist/code-block/CodeBlock.d.ts +20 -0
- package/dist/code-block/CodeBlock.js +49 -0
- package/dist/code-block/messages.d.ts +8 -0
- package/dist/code-block/messages.js +2 -0
- package/dist/combobox/Combobox.d.ts +60 -0
- package/dist/combobox/Combobox.js +160 -0
- package/dist/combobox/messages.d.ts +9 -0
- package/dist/combobox/messages.js +2 -0
- package/dist/combobox/useAnchoredPopover.d.ts +5 -0
- package/dist/combobox/useAnchoredPopover.js +100 -0
- package/dist/data-viz/DataViz.d.ts +140 -0
- package/dist/data-viz/DataViz.js +469 -0
- package/dist/data-viz/index.d.ts +2 -0
- package/dist/data-viz/index.js +1 -0
- package/dist/data-viz/messages.d.ts +11 -0
- package/dist/data-viz/messages.js +2 -0
- package/dist/date-picker/DatePicker.d.ts +57 -0
- package/dist/date-picker/DatePicker.js +146 -0
- package/dist/date-picker/messages.d.ts +13 -0
- package/dist/date-picker/messages.js +2 -0
- package/dist/dialog/Dialog.d.ts +84 -0
- package/dist/dialog/Dialog.js +107 -0
- package/dist/dialog/messages.d.ts +9 -0
- package/dist/dialog/messages.js +2 -0
- package/dist/fonts/DIYASans-Italic.woff2 +0 -0
- package/dist/fonts/DIYASans.woff2 +0 -0
- package/dist/fonts/OFL.txt +94 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +46 -0
- package/dist/internal/brand.d.ts +33 -0
- package/dist/internal/brand.js +57 -0
- package/dist/internal/cx.d.ts +2 -0
- package/dist/internal/cx.js +4 -0
- package/dist/internal/link.d.ts +14 -0
- package/dist/internal/link.js +7 -0
- package/dist/internal/slot.d.ts +15 -0
- package/dist/internal/slot.js +67 -0
- package/dist/layout/Inline.d.ts +4 -0
- package/dist/layout/Inline.js +7 -0
- package/dist/layout/Stack.d.ts +4 -0
- package/dist/layout/Stack.js +7 -0
- package/dist/layout/layout.d.ts +32 -0
- package/dist/layout/layout.js +12 -0
- package/dist/link/TextLink.d.ts +23 -0
- package/dist/link/TextLink.js +17 -0
- package/dist/list/List.d.ts +48 -0
- package/dist/list/List.js +17 -0
- package/dist/list/messages.d.ts +4 -0
- package/dist/list/messages.js +2 -0
- package/dist/menu/Menu.d.ts +49 -0
- package/dist/menu/Menu.js +54 -0
- package/dist/messages-defaults.d.ts +201 -0
- package/dist/messages-defaults.js +61 -0
- package/dist/messages.d.ts +12 -0
- package/dist/messages.js +1 -0
- package/dist/metric/Metric.d.ts +42 -0
- package/dist/metric/Metric.js +11 -0
- package/dist/metric/messages.d.ts +9 -0
- package/dist/metric/messages.js +2 -0
- package/dist/overlay/FloatingSurface.d.ts +29 -0
- package/dist/overlay/FloatingSurface.js +254 -0
- package/dist/page-header/PageHeader.d.ts +38 -0
- package/dist/page-header/PageHeader.js +10 -0
- package/dist/pagination/Pagination.d.ts +44 -0
- package/dist/pagination/Pagination.js +26 -0
- package/dist/pagination/messages.d.ts +11 -0
- package/dist/pagination/messages.js +2 -0
- package/dist/popover/Popover.d.ts +23 -0
- package/dist/popover/Popover.js +8 -0
- package/dist/progress/Progress.d.ts +33 -0
- package/dist/progress/Progress.js +14 -0
- package/dist/progress/messages.d.ts +4 -0
- package/dist/progress/messages.js +2 -0
- package/dist/prose/Prose.d.ts +14 -0
- package/dist/prose/Prose.js +7 -0
- package/dist/rating/Rating.d.ts +32 -0
- package/dist/rating/Rating.js +27 -0
- package/dist/rating/messages.d.ts +6 -0
- package/dist/rating/messages.js +2 -0
- package/dist/search/Search.d.ts +11 -0
- package/dist/search/Search.js +9 -0
- package/dist/select/Select.d.ts +65 -0
- package/dist/select/Select.js +19 -0
- package/dist/select/messages.d.ts +4 -0
- package/dist/select/messages.js +2 -0
- package/dist/site-header/SiteHeader.d.ts +88 -0
- package/dist/site-header/SiteHeader.js +211 -0
- package/dist/site-header/messages.d.ts +8 -0
- package/dist/site-header/messages.js +2 -0
- package/dist/slider/Slider.d.ts +54 -0
- package/dist/slider/Slider.js +89 -0
- package/dist/slider/messages.d.ts +5 -0
- package/dist/slider/messages.js +2 -0
- package/dist/styles.css +3386 -0
- package/dist/switch/Switch.d.ts +47 -0
- package/dist/switch/Switch.js +18 -0
- package/dist/table/Table.d.ts +99 -0
- package/dist/table/Table.js +158 -0
- package/dist/table/messages.d.ts +23 -0
- package/dist/table/messages.js +2 -0
- package/dist/tabs/Tabs.d.ts +49 -0
- package/dist/tabs/Tabs.js +26 -0
- package/dist/tag/Tag.d.ts +37 -0
- package/dist/tag/Tag.js +20 -0
- package/dist/tag/messages.d.ts +4 -0
- package/dist/tag/messages.js +2 -0
- package/dist/text-area/TextArea.d.ts +47 -0
- package/dist/text-area/TextArea.js +19 -0
- package/dist/text-input/TextInput.d.ts +53 -0
- package/dist/text-input/TextInput.js +35 -0
- package/dist/text-input/messages.d.ts +4 -0
- package/dist/text-input/messages.js +3 -0
- package/dist/text-input/native-field.d.ts +15 -0
- package/dist/text-input/native-field.js +56 -0
- package/dist/toast/Toast.d.ts +24 -0
- package/dist/toast/Toast.js +79 -0
- package/dist/toast/messages.d.ts +4 -0
- package/dist/toast/messages.js +2 -0
- package/dist/tooltip/Tooltip.d.ts +22 -0
- package/dist/tooltip/Tooltip.js +92 -0
- package/dist/workflow/ContentState.d.ts +44 -0
- package/dist/workflow/ContentState.js +11 -0
- package/dist/workflow/Drawer.d.ts +48 -0
- package/dist/workflow/Drawer.js +51 -0
- package/dist/workflow/FileUpload.d.ts +67 -0
- package/dist/workflow/FileUpload.js +47 -0
- package/dist/workflow/Form.d.ts +52 -0
- package/dist/workflow/Form.js +16 -0
- package/dist/workflow/Wizard.d.ts +76 -0
- package/dist/workflow/Wizard.js +31 -0
- package/dist/workflow/drawer-messages.d.ts +4 -0
- package/dist/workflow/drawer-messages.js +2 -0
- package/dist/workflow/file-upload-messages.d.ts +11 -0
- package/dist/workflow/file-upload-messages.js +2 -0
- package/dist/workflow/form-messages.d.ts +5 -0
- package/dist/workflow/form-messages.js +2 -0
- package/dist/workflow/wizard-messages.d.ts +9 -0
- package/dist/workflow/wizard-messages.js +2 -0
- package/package.json +82 -7
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type InputHTMLAttributes } from 'react';
|
|
2
|
+
/** Native checkbox attributes, including `name`, `onChange`, `onBlur` and the ref, go on the switch input; `className` and `style` go on the outer field wrapper. */
|
|
3
|
+
export type SwitchProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'role' | 'size' | 'value' | 'checked' | 'defaultChecked' | 'children'> & {
|
|
4
|
+
/** Whether the switch is on, when controlled; pair it with `onCheckedChange`. */
|
|
5
|
+
checked?: boolean;
|
|
6
|
+
/** Whether the switch starts on when uncontrolled; a form reset returns to it. */
|
|
7
|
+
defaultChecked?: boolean;
|
|
8
|
+
/** Called with the new on/off state when the switch is toggled. */
|
|
9
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
10
|
+
/** The value sent with the form under `name` while the switch is on; defaults to "on", like a checkbox. */
|
|
11
|
+
value?: string;
|
|
12
|
+
/** Visible label beside the switch; also its accessible name. Hide it with hideLabel. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Visible hint below the label, announced with the switch; `error` replaces it while set. */
|
|
15
|
+
helperText?: string;
|
|
16
|
+
/** Visible error message below the label; marks the switch invalid while set. */
|
|
17
|
+
error?: string;
|
|
18
|
+
/** Hides the label visually but keeps it as the switch's accessible name. */
|
|
19
|
+
hideLabel?: boolean;
|
|
20
|
+
/** Prevents toggling and dims the switch. */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Track and thumb size; defaults to 'medium'. */
|
|
23
|
+
size?: 'small' | 'medium' | 'large';
|
|
24
|
+
};
|
|
25
|
+
/** An on/off toggle (a native checkbox with role="switch") with a label and optional helper text or error. */
|
|
26
|
+
export declare const Switch: import("react").ForwardRefExoticComponent<Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "children" | "size" | "type" | "defaultChecked" | "role" | "checked"> & {
|
|
27
|
+
/** Whether the switch is on, when controlled; pair it with `onCheckedChange`. */
|
|
28
|
+
checked?: boolean;
|
|
29
|
+
/** Whether the switch starts on when uncontrolled; a form reset returns to it. */
|
|
30
|
+
defaultChecked?: boolean;
|
|
31
|
+
/** Called with the new on/off state when the switch is toggled. */
|
|
32
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
33
|
+
/** The value sent with the form under `name` while the switch is on; defaults to "on", like a checkbox. */
|
|
34
|
+
value?: string;
|
|
35
|
+
/** Visible label beside the switch; also its accessible name. Hide it with hideLabel. */
|
|
36
|
+
label: string;
|
|
37
|
+
/** Visible hint below the label, announced with the switch; `error` replaces it while set. */
|
|
38
|
+
helperText?: string;
|
|
39
|
+
/** Visible error message below the label; marks the switch invalid while set. */
|
|
40
|
+
error?: string;
|
|
41
|
+
/** Hides the label visually but keeps it as the switch's accessible name. */
|
|
42
|
+
hideLabel?: boolean;
|
|
43
|
+
/** Prevents toggling and dims the switch. */
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** Track and thumb size; defaults to 'medium'. */
|
|
46
|
+
size?: "small" | "medium" | "large";
|
|
47
|
+
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useId } from 'react';
|
|
4
|
+
import { cx } from '../internal/cx.js';
|
|
5
|
+
/** An on/off toggle (a native checkbox with role="switch") with a label and optional helper text or error. */
|
|
6
|
+
export const Switch = forwardRef(function Switch({ checked, defaultChecked, onCheckedChange, onChange, onKeyDown, label, helperText, error, hideLabel = false, disabled = false, size = 'medium', id, className, style, 'aria-describedby': describedBy, ...props }, ref) {
|
|
7
|
+
const description = error || helperText;
|
|
8
|
+
const generatedId = useId();
|
|
9
|
+
const inputId = id ?? generatedId;
|
|
10
|
+
return _jsxs("div", { className: cx('diya-switch-field', className), style: style, "data-size": size, "data-disabled": disabled, children: [_jsx("input", { ...props, ref: ref, id: inputId, type: "checkbox", role: "switch", className: "diya-switch", ...(checked !== undefined ? { checked } : { defaultChecked }), "aria-describedby": [describedBy, description ? `${inputId}-description` : undefined].filter(Boolean).join(' ') || undefined, "aria-invalid": error ? true : props['aria-invalid'], disabled: disabled, onChange: (event) => { onChange?.(event); onCheckedChange?.(event.target.checked); }, onKeyDown: (event) => {
|
|
11
|
+
onKeyDown?.(event);
|
|
12
|
+
// A switch toggles with Enter as well as Space.
|
|
13
|
+
if (event.key === 'Enter' && !event.defaultPrevented && !disabled) {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
event.currentTarget.click();
|
|
16
|
+
}
|
|
17
|
+
} }), _jsxs("div", { children: [_jsx("label", { htmlFor: inputId, className: hideLabel ? 'diya-sr-only' : 'diya-switch-label', children: label }), description && _jsxs("p", { id: `${inputId}-description`, className: "diya-switch-description", "data-invalid": error ? true : undefined, children: [error && _jsx("span", { "aria-hidden": "true", children: "!" }), description] })] })] });
|
|
18
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode, type RefAttributes } from 'react';
|
|
2
|
+
import { type MenuItem } from '../menu/Menu.js';
|
|
3
|
+
import { tableMessages } from './messages.js';
|
|
4
|
+
declare module '../messages.js' {
|
|
5
|
+
interface DiyaMessages {
|
|
6
|
+
table: typeof tableMessages;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export type TableColumn<T> = {
|
|
10
|
+
/** Stable column identity, also used as the sort key. */
|
|
11
|
+
key: string;
|
|
12
|
+
/** Visible column header text. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Renders the cell for a row. */
|
|
15
|
+
render: (row: T) => ReactNode;
|
|
16
|
+
/** Value the column sorts by; giving it makes the header a sort button. */
|
|
17
|
+
sortValue?: (row: T) => string | number;
|
|
18
|
+
/** Right-aligns the column for numbers; defaults to false. */
|
|
19
|
+
numeric?: boolean;
|
|
20
|
+
/** Column width in pixels; tables with more than four columns scroll sideways, using 160 for columns without one. */
|
|
21
|
+
width?: number;
|
|
22
|
+
};
|
|
23
|
+
export type TableFilter<T> = {
|
|
24
|
+
/** Stable identity for this filter's state. */
|
|
25
|
+
key: string;
|
|
26
|
+
/** Visible filter button text; the chosen option is appended, e.g. "Status: Active". */
|
|
27
|
+
label: string;
|
|
28
|
+
/** Choices in the filter menu, as plain strings or value/label pairs; an "All" choice is added first. */
|
|
29
|
+
options: (string | {
|
|
30
|
+
value: string;
|
|
31
|
+
label: string;
|
|
32
|
+
})[];
|
|
33
|
+
/** Returns the row's value to compare with the chosen option's value. */
|
|
34
|
+
getValue: (row: T) => string;
|
|
35
|
+
};
|
|
36
|
+
export type TableBulkAction = {
|
|
37
|
+
/** Visible button and menu item text. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Called with the selected row ids (see `getRowId`) from the toolbar, or with one row's id from its menu; the selection then clears. */
|
|
40
|
+
onAction: (ids: string[]) => void;
|
|
41
|
+
/** Marks the menu item as destructive; defaults to false. */
|
|
42
|
+
destructive?: boolean;
|
|
43
|
+
};
|
|
44
|
+
export type TableProps<T> = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
45
|
+
/** The table's title: shown as its heading unless `showTitle` is false, and always its caption and accessible name. */
|
|
46
|
+
label: string;
|
|
47
|
+
/** Shows `label` as a visible heading above the table; defaults to true. */
|
|
48
|
+
showTitle?: boolean;
|
|
49
|
+
/** Heading level of the visible title, h1 to h6. Defaults to 3. */
|
|
50
|
+
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
51
|
+
/** The data. Search, filters, sorting and pagination run on these rows in the browser. */
|
|
52
|
+
rows: T[];
|
|
53
|
+
/** Returns a row's unique id, used for keys and selection; defaults to the row's `id` field (a string or number, as a string). */
|
|
54
|
+
getRowId?: (row: T) => string;
|
|
55
|
+
/** Column definitions, in display order. */
|
|
56
|
+
columns: TableColumn<T>[];
|
|
57
|
+
/** Short name for a row, used in its checkbox and menu accessible names, e.g. "Select Ada Lovelace". */
|
|
58
|
+
rowLabel: (row: T) => string;
|
|
59
|
+
/** Text a row matches against; giving it shows the search field. */
|
|
60
|
+
searchText?: (row: T) => string;
|
|
61
|
+
/** Filter menus in the toolbar; a non-empty list shows the filter UI. */
|
|
62
|
+
filterOptions?: TableFilter<T>[];
|
|
63
|
+
/** Row height; defaults to 'comfortable'. */
|
|
64
|
+
density?: 'comfortable' | 'compact';
|
|
65
|
+
/** Shows the search field; defaults to on when `searchText` is given. */
|
|
66
|
+
search?: boolean;
|
|
67
|
+
/** Row checkboxes; defaults to on when there are bulk actions, a selection handler or `selectedIds`. */
|
|
68
|
+
selectable?: boolean;
|
|
69
|
+
/** Shows pagination under the table; defaults to true. */
|
|
70
|
+
pagination?: boolean;
|
|
71
|
+
/** Rows per page at first; defaults to 10. */
|
|
72
|
+
defaultPageSize?: number;
|
|
73
|
+
/** Choices in the rows-per-page menu; defaults to [10, 25, 50]. */
|
|
74
|
+
pageSizeOptions?: number[];
|
|
75
|
+
/** Wide-table mode: pins the checkbox and first column left and the row menu right while scrolling sideways; defaults to false. */
|
|
76
|
+
sticky?: boolean;
|
|
77
|
+
/** Shows skeleton rows and a loading status, and disables the tools; defaults to false. */
|
|
78
|
+
loading?: boolean;
|
|
79
|
+
/** Actions for the selected rows; each also appears in a row's own menu. */
|
|
80
|
+
bulkActions?: TableBulkAction[];
|
|
81
|
+
/** Adds an "open" item (openLabel) to each row's menu, called with that row. */
|
|
82
|
+
onRowOpen?: (row: T) => void;
|
|
83
|
+
/** Builds a row's menu items, replacing the open and bulk items; giving it shows the row menu. */
|
|
84
|
+
rowActions?: (row: T) => MenuItem[];
|
|
85
|
+
/** Selected row ids (see `getRowId`), for a controlled selection; leave it out to let the table keep its own. */
|
|
86
|
+
selectedIds?: string[];
|
|
87
|
+
/** Called with the full list of selected row ids whenever the selection changes. */
|
|
88
|
+
onSelectionChange?: (ids: string[]) => void;
|
|
89
|
+
/** Empty-state heading when there are no rows; defaults to "No data yet". */
|
|
90
|
+
emptyTitle?: string;
|
|
91
|
+
/** Empty-state text when there are no rows; defaults to "Items will appear here once they are added." */
|
|
92
|
+
emptyDescription?: string;
|
|
93
|
+
/** Status text announced while loading; defaults to "Loading…". */
|
|
94
|
+
loadingLabel?: string;
|
|
95
|
+
/** Text of the row menu item added by `onRowOpen`; defaults to "View details". */
|
|
96
|
+
openLabel?: string;
|
|
97
|
+
};
|
|
98
|
+
/** A data table with optional search, filters, sorting, row selection, bulk and row actions, and pagination, all run in the browser. */
|
|
99
|
+
export declare const Table: <T>(props: TableProps<T> & RefAttributes<HTMLDivElement>) => ReactNode;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, Fragment, useState } from 'react';
|
|
4
|
+
import { Search } from '../search/Search.js';
|
|
5
|
+
import { Menu } from '../menu/Menu.js';
|
|
6
|
+
import { Pagination } from '../pagination/Pagination.js';
|
|
7
|
+
import { Button } from '../button/Button.js';
|
|
8
|
+
import { useDiyaMessages } from '../DiyaProvider.js';
|
|
9
|
+
import { cx } from '../internal/cx.js';
|
|
10
|
+
import { tableMessages } from './messages.js';
|
|
11
|
+
let warnedMissingId = false;
|
|
12
|
+
function defaultRowId(row, index) {
|
|
13
|
+
const id = row?.id;
|
|
14
|
+
if (typeof id === 'string' || typeof id === 'number')
|
|
15
|
+
return String(id);
|
|
16
|
+
if (!warnedMissingId) {
|
|
17
|
+
warnedMissingId = true;
|
|
18
|
+
console.error('DIYA Table: a row has no string or number `id`. Pass `getRowId` to say which field identifies a row.');
|
|
19
|
+
}
|
|
20
|
+
return `diya-row-${index}`;
|
|
21
|
+
}
|
|
22
|
+
/** A data table with optional search, filters, sorting, row selection, bulk and row actions, and pagination, all run in the browser. */
|
|
23
|
+
export const Table = forwardRef(function Table({ label, showTitle = true, headingLevel = 3, rows, getRowId, columns, rowLabel, searchText, filterOptions, density = 'comfortable', search, selectable, pagination = true, defaultPageSize = 10, pageSizeOptions = [10, 25, 50], sticky = false, loading = false, bulkActions = [], onRowOpen, rowActions, selectedIds, onSelectionChange, emptyTitle, emptyDescription, loadingLabel, openLabel, className, ...props }, ref) {
|
|
24
|
+
const t = useDiyaMessages('table', tableMessages);
|
|
25
|
+
const Heading = `h${headingLevel}`;
|
|
26
|
+
const ids = new Map();
|
|
27
|
+
rows.forEach((row, index) => ids.set(row, getRowId ? getRowId(row) : defaultRowId(row, index)));
|
|
28
|
+
const idOf = (row) => ids.get(row);
|
|
29
|
+
const [query, setQuery] = useState('');
|
|
30
|
+
const [filterValue, setFilterValue] = useState({});
|
|
31
|
+
const activeFilter = (key) => typeof filterValue[key] === 'string';
|
|
32
|
+
const showSearch = search ?? !!searchText;
|
|
33
|
+
const selection = selectable ?? (bulkActions.length > 0 || !!onSelectionChange || selectedIds !== undefined);
|
|
34
|
+
const filters = !!filterOptions?.length;
|
|
35
|
+
const optionsOf = (filter) => filter.options.map((option) => typeof option === 'string' ? { value: option, label: option } : option);
|
|
36
|
+
const [sort, setSort] = useState({ key: '', direction: 1 });
|
|
37
|
+
const [localSelected, setLocalSelected] = useState([]);
|
|
38
|
+
const selected = selectedIds ?? localSelected;
|
|
39
|
+
const showActions = !!(rowActions || bulkActions.length || onRowOpen);
|
|
40
|
+
function setSelection(ids) {
|
|
41
|
+
if (selectedIds === undefined)
|
|
42
|
+
setLocalSelected(ids);
|
|
43
|
+
onSelectionChange?.(ids);
|
|
44
|
+
}
|
|
45
|
+
function actionsFor(row) {
|
|
46
|
+
return (rowActions?.(row) ?? [
|
|
47
|
+
...(onRowOpen ? [{ id: 'open', label: openLabel ?? t.open, onSelect: () => onRowOpen(row) }] : []),
|
|
48
|
+
...bulkActions.map((action, index) => ({
|
|
49
|
+
id: `bulk-${index}`,
|
|
50
|
+
label: action.label,
|
|
51
|
+
destructive: action.destructive,
|
|
52
|
+
onSelect: () => action.onAction([idOf(row)]),
|
|
53
|
+
})),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
const [page, setPage] = useState(1);
|
|
57
|
+
const [pageSize, setPageSize] = useState(defaultPageSize);
|
|
58
|
+
const matches = rows.filter((row) => (!showSearch || !searchText || searchText(row).toLowerCase().includes(query.trim().toLowerCase())) &&
|
|
59
|
+
(!filters ||
|
|
60
|
+
!filterOptions ||
|
|
61
|
+
filterOptions.every((item) => !activeFilter(item.key) || item.getValue(row) === filterValue[item.key])));
|
|
62
|
+
const sortColumn = columns.find((column) => column.key === sort.key);
|
|
63
|
+
if (sortColumn?.sortValue)
|
|
64
|
+
matches.sort((a, b) => {
|
|
65
|
+
const x = sortColumn.sortValue(a), y = sortColumn.sortValue(b);
|
|
66
|
+
return ((typeof x === 'number' && typeof y === 'number'
|
|
67
|
+
? x - y
|
|
68
|
+
: String(x).localeCompare(String(y))) * sort.direction);
|
|
69
|
+
});
|
|
70
|
+
const current = Math.min(page, Math.max(1, Math.ceil(matches.length / pageSize)));
|
|
71
|
+
const visible = pagination ? matches.slice((current - 1) * pageSize, current * pageSize) : matches;
|
|
72
|
+
const activeSelection = selected.filter((id) => rows.some((row) => idOf(row) === id));
|
|
73
|
+
const allSelected = visible.length > 0 && visible.every((row) => selected.includes(idOf(row)));
|
|
74
|
+
const hasFilters = (showSearch && !!query.trim()) || (filters && filterOptions?.some(item => activeFilter(item.key)));
|
|
75
|
+
const someSelected = visible.some((row) => selected.includes(idOf(row)));
|
|
76
|
+
return (_jsxs("div", { ...props, ref: ref, className: cx('diya-table', className), "data-density": density, "data-sticky": sticky || undefined, "data-selection": selection || undefined, "data-rich": columns.length > 4 || undefined, children: [(showTitle || filters || showSearch) && _jsxs("div", { className: "diya-table-toolbar", children: [showTitle && _jsx(Heading, { className: "diya-table-title", children: label }), _jsxs("div", { className: "diya-table-tools", children: [filters && filterOptions && (_jsxs("div", { className: "diya-table-filters", children: [filterOptions.map((item) => (_jsx("div", { className: "diya-table-filter", "data-active": activeFilter(item.key) || undefined, children: _jsx(Menu, { disabled: loading, label: activeFilter(item.key) ? t.filterValue(item.label, optionsOf(item).find((option) => option.value === filterValue[item.key])?.label || t.noValue) : item.label, items: [
|
|
77
|
+
{
|
|
78
|
+
id: 'all-values',
|
|
79
|
+
label: t.all,
|
|
80
|
+
type: 'radio',
|
|
81
|
+
checked: !activeFilter(item.key),
|
|
82
|
+
onSelect: () => {
|
|
83
|
+
setFilterValue({ ...filterValue, [item.key]: null });
|
|
84
|
+
setPage(1);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
...optionsOf(item).map((option, index) => ({
|
|
88
|
+
id: 'value-' + index,
|
|
89
|
+
label: option.label || t.noValue,
|
|
90
|
+
type: 'radio',
|
|
91
|
+
checked: filterValue[item.key] === option.value,
|
|
92
|
+
onSelect: () => {
|
|
93
|
+
setFilterValue({ ...filterValue, [item.key]: option.value });
|
|
94
|
+
setPage(1);
|
|
95
|
+
},
|
|
96
|
+
})),
|
|
97
|
+
] }) }, item.key))), filterOptions.some(item => activeFilter(item.key)) && (_jsx("button", { type: "button", className: "diya-table-clear", onClick: () => {
|
|
98
|
+
setFilterValue({});
|
|
99
|
+
setPage(1);
|
|
100
|
+
}, children: t.clearFilters }))] })), showSearch && (_jsx(Search, { disabled: loading, size: "small", label: t.searchLabel(label), hideLabel: true, placeholder: t.search, clearable: true, value: query, onValueChange: (value) => {
|
|
101
|
+
setQuery(value);
|
|
102
|
+
setPage(1);
|
|
103
|
+
} }))] })] }), selection && activeSelection.length > 0 && (_jsxs("div", { className: "diya-table-bulk", children: [_jsx("span", { role: "status", children: t.selected(activeSelection.length) }), bulkActions.map((action, index) => (_jsx(Button, { size: "small", variant: "outline", disabled: loading, onClick: () => {
|
|
104
|
+
action.onAction(activeSelection);
|
|
105
|
+
setSelection([]);
|
|
106
|
+
}, children: action.label }, `${index}-${action.label}`))), _jsx(Button, { size: "small", variant: "quiet", onClick: () => setSelection([]), children: t.clearSelection })] })), _jsx("div", { className: "diya-table-scroll", tabIndex: 0, role: "region", "aria-label": t.scrollHint(label), onFocus: (event) => {
|
|
107
|
+
// Browsers leave a partly visible control where it is; bring it, and its focus ring, fully into view.
|
|
108
|
+
const target = event.target;
|
|
109
|
+
let keyboard = false;
|
|
110
|
+
try {
|
|
111
|
+
keyboard = target !== event.currentTarget && target.matches(':focus-visible');
|
|
112
|
+
}
|
|
113
|
+
catch { /* Older engines lack :focus-visible. */ }
|
|
114
|
+
if (keyboard)
|
|
115
|
+
target.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
|
|
116
|
+
}, children: _jsxs("table", { "aria-busy": loading, style: columns.length > 4
|
|
117
|
+
? {
|
|
118
|
+
width: columns.reduce((sum, column) => sum + (column.width ?? 160), 0) +
|
|
119
|
+
(selection ? 48 : 0) +
|
|
120
|
+
(showActions ? 56 : 0),
|
|
121
|
+
minWidth: '100%',
|
|
122
|
+
}
|
|
123
|
+
: undefined, children: [_jsxs("colgroup", { children: [selection && _jsx("col", { style: { width: 48 } }), columns.map((column) => (_jsx("col", { style: column.width ? { width: column.width } : undefined }, column.key))), showActions && _jsx("col", { style: { width: 56 } })] }), _jsx("caption", { className: "diya-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [selection && (_jsx("th", { className: "diya-table-check", "data-pinned": "selection", scope: "col", children: _jsx("input", { type: "checkbox", "aria-label": t.selectPage, checked: allSelected, disabled: loading || !visible.length, ref: (node) => {
|
|
124
|
+
if (node)
|
|
125
|
+
node.indeterminate = someSelected && !allSelected;
|
|
126
|
+
}, onChange: () => setSelection(allSelected
|
|
127
|
+
? selected.filter((id) => !visible.some((row) => idOf(row) === id))
|
|
128
|
+
: [...new Set([...selected, ...visible.map(idOf)])]) }) })), columns.map((column, columnIndex) => (_jsx("th", { scope: "col", "data-pinned": columnIndex === 0 ? 'primary' : undefined, className: column.numeric ? 'diya-table-numeric' : undefined, "aria-sort": column.sortValue
|
|
129
|
+
? sort.key === column.key
|
|
130
|
+
? sort.direction === 1
|
|
131
|
+
? 'ascending'
|
|
132
|
+
: 'descending'
|
|
133
|
+
: 'none'
|
|
134
|
+
: undefined, children: column.sortValue ? (_jsxs("button", { type: "button", disabled: loading, className: "diya-table-sort", onClick: () => {
|
|
135
|
+
setSort({
|
|
136
|
+
key: column.key,
|
|
137
|
+
direction: sort.key === column.key && sort.direction === 1 ? -1 : 1,
|
|
138
|
+
});
|
|
139
|
+
setPage(1);
|
|
140
|
+
}, children: [column.label, _jsxs("svg", { width: "12", height: "14", viewBox: "0 0 12 14", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { opacity: sort.key === column.key && sort.direction === -1 ? 0.25 : 1, d: "m3 5 3-3 3 3" }), _jsx("path", { opacity: sort.key === column.key && sort.direction === 1 ? 0.25 : 1, d: "m3 9 3 3 3-3" })] })] })) : (column.label) }, column.key))), showActions && (_jsx("th", { scope: "col", className: "diya-table-actions", children: _jsx("span", { className: "diya-sr-only", children: t.actions }) }))] }) }), _jsxs("tbody", { children: [loading
|
|
141
|
+
? Array.from({ length: 5 }, (_, i) => (_jsx("tr", { children: Array.from({
|
|
142
|
+
length: columns.length +
|
|
143
|
+
Number(selection) +
|
|
144
|
+
Number(showActions),
|
|
145
|
+
}, (_, j) => (_jsx("td", { children: _jsx("span", { className: "diya-table-skeleton", "aria-hidden": "true" }) }, j))) }, i)))
|
|
146
|
+
: visible.map((row) => (_jsx(Fragment, { children: _jsxs("tr", { "data-selected": (selection && selected.includes(idOf(row))) || undefined, children: [selection && (_jsx("td", { className: "diya-table-check", "data-pinned": "selection", children: _jsx("input", { type: "checkbox", "aria-label": t.selectRow(rowLabel(row)), checked: selected.includes(idOf(row)), onChange: () => setSelection(selected.includes(idOf(row))
|
|
147
|
+
? selected.filter((id) => id !== idOf(row))
|
|
148
|
+
: [...selected, idOf(row)]) }) })), columns.map((column, columnIndex) => (_jsx("td", { "data-pinned": columnIndex === 0 ? 'primary' : undefined, className: column.numeric ? 'diya-table-numeric' : undefined, children: column.render(row) }, column.key))), showActions && (_jsx("td", { className: "diya-table-actions", children: _jsx(Menu, { label: t.rowActions(rowLabel(row)), triggerIcon: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: [_jsx("circle", { cx: "5", cy: "12", r: "1.5" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5" }), _jsx("circle", { cx: "19", cy: "12", r: "1.5" })] }), items: actionsFor(row) }) }))] }) }, idOf(row)))), !loading && !visible.length && (_jsx("tr", { children: _jsx("td", { colSpan: columns.length +
|
|
149
|
+
Number(selection) +
|
|
150
|
+
Number(showActions), children: _jsxs("div", { className: "diya-table-empty", children: [_jsx("strong", { children: hasFilters ? t.noMatches : emptyTitle ?? t.emptyTitle }), _jsx("p", { children: hasFilters ? t.noMatchesDescription : emptyDescription ?? t.emptyDescription }), hasFilters && (_jsx(Button, { variant: "outline", size: "small", onClick: () => {
|
|
151
|
+
setQuery('');
|
|
152
|
+
setFilterValue({});
|
|
153
|
+
setPage(1);
|
|
154
|
+
}, children: t.clearAll }))] }) }) }))] })] }) }), loading && (_jsx("p", { role: "status", className: "diya-table-loading", children: loadingLabel ?? t.loading })), pagination && !loading && (_jsx(Pagination, { page: current, total: matches.length, pageSize: pageSize, pageSizeOptions: pageSizeOptions, onPageChange: setPage, onPageSizeChange: (size) => {
|
|
155
|
+
setPageSize(size);
|
|
156
|
+
setPage(1);
|
|
157
|
+
} }))] }));
|
|
158
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** table: English defaults, overridable through DiyaProvider `messages`. */
|
|
2
|
+
export declare const tableMessages: {
|
|
3
|
+
loading: string;
|
|
4
|
+
emptyTitle: string;
|
|
5
|
+
emptyDescription: string;
|
|
6
|
+
noMatches: string;
|
|
7
|
+
noMatchesDescription: string;
|
|
8
|
+
clearAll: string;
|
|
9
|
+
clearFilters: string;
|
|
10
|
+
clearSelection: string;
|
|
11
|
+
selected: (count: number) => string;
|
|
12
|
+
search: string;
|
|
13
|
+
searchLabel: (label: string) => string;
|
|
14
|
+
filterValue: (filter: string, value: string) => string;
|
|
15
|
+
noValue: string;
|
|
16
|
+
all: string;
|
|
17
|
+
open: string;
|
|
18
|
+
actions: string;
|
|
19
|
+
rowActions: (row: string) => string;
|
|
20
|
+
selectPage: string;
|
|
21
|
+
selectRow: (row: string) => string;
|
|
22
|
+
scrollHint: (label: string) => string;
|
|
23
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** table: English defaults, overridable through DiyaProvider `messages`. */
|
|
2
|
+
export const tableMessages = { loading: 'Loading…', emptyTitle: 'No data yet', emptyDescription: 'Items will appear here once they are added.', noMatches: 'No matching results', noMatchesDescription: 'Try another search or clear your filters.', clearAll: 'Clear search and filters', clearFilters: 'Clear filters', clearSelection: 'Clear selection', selected: (count) => `${count} selected`, search: 'Search', searchLabel: (label) => `Search ${label.toLowerCase()}`, filterValue: (filter, value) => `${filter}: ${value}`, noValue: 'No value', all: 'All', open: 'View details', actions: 'Actions', rowActions: (row) => `Actions for ${row}`, selectPage: 'Select this page', selectRow: (row) => `Select ${row}`, scrollHint: (label) => `${label}, scroll to see all columns` };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import { type BadgeProps } from '../badge/Badge.js';
|
|
3
|
+
export type TabItem = {
|
|
4
|
+
/** Unique key for the tab; `value` and `onValueChange` use it. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Visible tab text. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Decorative icon before the label; hidden from screen readers. */
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
/** Small Badge after the label, such as an unread count; its `label` is the badge's accessible name. */
|
|
11
|
+
badge?: Omit<BadgeProps, 'size'>;
|
|
12
|
+
/** Skips this tab for clicks and arrow keys. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Panel shown while this tab is selected. */
|
|
15
|
+
content: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
export type TabsProps = HTMLAttributes<HTMLDivElement> & {
|
|
18
|
+
/** The tabs, in order. */
|
|
19
|
+
items: TabItem[];
|
|
20
|
+
/** `value` of the selected tab; falls back to the first enabled tab. */
|
|
21
|
+
value: string;
|
|
22
|
+
/** Called with a tab's `value` when it is clicked or reached with the arrow, Home or End keys. */
|
|
23
|
+
onValueChange: (value: string) => void;
|
|
24
|
+
/** Accessible name for the tab list; not shown. */
|
|
25
|
+
label: string;
|
|
26
|
+
/** 'underline' marks the selected tab with a line; 'pill' fills it. Defaults to 'underline'. */
|
|
27
|
+
variant?: 'underline' | 'pill';
|
|
28
|
+
/** Tab height and text size. Defaults to 'medium'. */
|
|
29
|
+
size?: 'small' | 'medium' | 'large';
|
|
30
|
+
/** Disables every tab. */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
};
|
|
33
|
+
/** Tabs that switch between panels in place, with arrow-key navigation. The ref, `className`, `style` and other attributes go to the outer element. */
|
|
34
|
+
export declare const Tabs: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & {
|
|
35
|
+
/** The tabs, in order. */
|
|
36
|
+
items: TabItem[];
|
|
37
|
+
/** `value` of the selected tab; falls back to the first enabled tab. */
|
|
38
|
+
value: string;
|
|
39
|
+
/** Called with a tab's `value` when it is clicked or reached with the arrow, Home or End keys. */
|
|
40
|
+
onValueChange: (value: string) => void;
|
|
41
|
+
/** Accessible name for the tab list; not shown. */
|
|
42
|
+
label: string;
|
|
43
|
+
/** 'underline' marks the selected tab with a line; 'pill' fills it. Defaults to 'underline'. */
|
|
44
|
+
variant?: "underline" | "pill";
|
|
45
|
+
/** Tab height and text size. Defaults to 'medium'. */
|
|
46
|
+
size?: "small" | "medium" | "large";
|
|
47
|
+
/** Disables every tab. */
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useId, useRef } from 'react';
|
|
4
|
+
import { Badge } from '../badge/Badge.js';
|
|
5
|
+
import { cx } from '../internal/cx.js';
|
|
6
|
+
/** Tabs that switch between panels in place, with arrow-key navigation. The ref, `className`, `style` and other attributes go to the outer element. */
|
|
7
|
+
export const Tabs = forwardRef(function Tabs({ items, value, onValueChange, label, variant = 'underline', size = 'medium', disabled = false, className, ...props }, ref) {
|
|
8
|
+
const id = useId();
|
|
9
|
+
const buttons = useRef([]);
|
|
10
|
+
const selected = items.findIndex((item) => item.value === value && !item.disabled);
|
|
11
|
+
const active = selected >= 0 ? selected : items.findIndex((item) => !item.disabled);
|
|
12
|
+
function navigate(event, index) {
|
|
13
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
|
|
14
|
+
return;
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
const available = items.map((item, i) => !item.disabled ? i : -1).filter((i) => i >= 0);
|
|
17
|
+
if (!available.length)
|
|
18
|
+
return;
|
|
19
|
+
const position = available.indexOf(index);
|
|
20
|
+
const next = event.key === 'Home' ? available[0] : event.key === 'End' ? available[available.length - 1]
|
|
21
|
+
: available[(position + (event.key === 'ArrowRight' ? 1 : -1) + available.length) % available.length];
|
|
22
|
+
buttons.current[next]?.focus();
|
|
23
|
+
onValueChange(items[next].value);
|
|
24
|
+
}
|
|
25
|
+
return (_jsxs("div", { ...props, ref: ref, className: cx('diya-tabs', className), "data-variant": variant, "data-size": size, children: [_jsx("div", { className: "diya-tab-list", role: "tablist", "aria-label": label, "aria-orientation": "horizontal", children: items.map((item, index) => (_jsxs("button", { ref: (node) => { buttons.current[index] = node; }, className: "diya-tab", type: "button", role: "tab", id: `${id}-tab-${index}`, "aria-controls": `${id}-panel-${index}`, "aria-selected": index === active, tabIndex: index === active && !disabled ? 0 : -1, disabled: disabled || item.disabled, onClick: () => onValueChange(item.value), onKeyDown: (event) => navigate(event, index), children: [item.icon && _jsx("span", { className: "diya-tab-icon", "aria-hidden": "true", children: item.icon }), _jsx("span", { children: item.label }), item.badge && _jsx(Badge, { ...item.badge, size: "small" })] }, item.value))) }), items.map((item, index) => (_jsx("div", { className: "diya-tab-panel", role: "tabpanel", id: `${id}-panel-${index}`, "aria-labelledby": `${id}-tab-${index}`, hidden: index !== active, tabIndex: 0, children: item.content }, item.value)))] }));
|
|
26
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import { type Tone } from '../badge/Badge.js';
|
|
3
|
+
import { tagMessages } from './messages.js';
|
|
4
|
+
declare module '../messages.js' {
|
|
5
|
+
interface DiyaMessages {
|
|
6
|
+
tag: typeof tagMessages;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export type TagProps = HTMLAttributes<HTMLSpanElement> & {
|
|
10
|
+
/** Visible tag text. */
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
/** 'dot' shows a small dot in the tag's tone; any other node is a decorative icon. Both are hidden from screen readers. */
|
|
13
|
+
leading?: 'dot' | ReactNode;
|
|
14
|
+
/** Color meaning: 'neutral' plain, 'brand' a soft tint of your brand color, 'success' done, 'warning' needs care, 'error' failed, 'info' for information. Defaults to 'neutral'. */
|
|
15
|
+
tone?: Tone;
|
|
16
|
+
/** Tag height and text size. Defaults to 'medium'. */
|
|
17
|
+
size?: 'small' | 'medium' | 'large';
|
|
18
|
+
/** When given, shows a remove button that calls it. */
|
|
19
|
+
onRemove?: () => void;
|
|
20
|
+
/** Plain-text name for the tag, not shown; the remove button is announced as "Remove {label}". Defaults to the text of `children`. */
|
|
21
|
+
label?: string;
|
|
22
|
+
};
|
|
23
|
+
/** A compact label for a category, status or filter, optionally removable. */
|
|
24
|
+
export declare const Tag: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLSpanElement> & {
|
|
25
|
+
/** Visible tag text. */
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
/** 'dot' shows a small dot in the tag's tone; any other node is a decorative icon. Both are hidden from screen readers. */
|
|
28
|
+
leading?: "dot" | ReactNode;
|
|
29
|
+
/** Color meaning: 'neutral' plain, 'brand' a soft tint of your brand color, 'success' done, 'warning' needs care, 'error' failed, 'info' for information. Defaults to 'neutral'. */
|
|
30
|
+
tone?: Tone;
|
|
31
|
+
/** Tag height and text size. Defaults to 'medium'. */
|
|
32
|
+
size?: "small" | "medium" | "large";
|
|
33
|
+
/** When given, shows a remove button that calls it. */
|
|
34
|
+
onRemove?: () => void;
|
|
35
|
+
/** Plain-text name for the tag, not shown; the remove button is announced as "Remove {label}". Defaults to the text of `children`. */
|
|
36
|
+
label?: string;
|
|
37
|
+
} & import("react").RefAttributes<HTMLSpanElement>>;
|
package/dist/tag/Tag.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Children, forwardRef, isValidElement } from 'react';
|
|
4
|
+
import { useDiyaMessages } from '../DiyaProvider.js';
|
|
5
|
+
import { Badge } from '../badge/Badge.js';
|
|
6
|
+
import { cx } from '../internal/cx.js';
|
|
7
|
+
import { tagMessages } from './messages.js';
|
|
8
|
+
/** The plain text inside a node tree, such as "Design" from <strong>Design</strong>. */
|
|
9
|
+
function textOf(node) {
|
|
10
|
+
return Children.toArray(node)
|
|
11
|
+
.map((child) => (typeof child === 'string' || typeof child === 'number' ? String(child) : isValidElement(child) ? textOf(child.props.children) : ''))
|
|
12
|
+
.join('')
|
|
13
|
+
.trim();
|
|
14
|
+
}
|
|
15
|
+
/** A compact label for a category, status or filter, optionally removable. */
|
|
16
|
+
export const Tag = forwardRef(function Tag({ children, leading, tone = 'neutral', size = 'medium', onRemove, label, className, ...props }, ref) {
|
|
17
|
+
const t = useDiyaMessages('tag', tagMessages);
|
|
18
|
+
const name = label ?? textOf(children);
|
|
19
|
+
return _jsxs("span", { ...props, ref: ref, className: cx('diya-tag', className), "data-size": size, "data-color": tone, children: [leading === 'dot' ? _jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { variant: "dot", tone: tone, label: name }) }) : leading && _jsx("span", { className: "diya-tag-icon", "aria-hidden": "true", children: leading }), _jsx("span", { children: children }), onRemove && _jsx("button", { type: "button", "aria-label": t.remove(name), onClick: onRemove, children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true", children: _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) }) })] });
|
|
20
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type TextareaHTMLAttributes } from 'react';
|
|
2
|
+
/** Native textarea attributes, including `name`, `onChange`, `onBlur` and the ref, go on the textarea; `className` and `style` go on the outer field wrapper. */
|
|
3
|
+
export type TextAreaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'defaultValue'> & {
|
|
4
|
+
/** Visible label above the textarea; also its accessible name. Hide it with hideLabel. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Hides the label visually but keeps it as the textarea's accessible name. */
|
|
7
|
+
hideLabel?: boolean;
|
|
8
|
+
/** Visible hint below the textarea, announced with it; `error` replaces it while set. */
|
|
9
|
+
helperText?: string;
|
|
10
|
+
/** Visible error message below the textarea; marks the field invalid while set. */
|
|
11
|
+
error?: string;
|
|
12
|
+
/** Type size and padding; defaults to 'medium'. */
|
|
13
|
+
size?: 'small' | 'medium' | 'large';
|
|
14
|
+
/** The current text when controlled; pair it with `onValueChange`. */
|
|
15
|
+
value?: string;
|
|
16
|
+
/** The starting text when uncontrolled; a form reset returns to it. */
|
|
17
|
+
defaultValue?: string;
|
|
18
|
+
/** Called with the new text on every edit. */
|
|
19
|
+
onValueChange?: (value: string) => void;
|
|
20
|
+
/** Shows a visible character count below the textarea, as "n / maxLength" when maxLength is set. */
|
|
21
|
+
showCount?: boolean;
|
|
22
|
+
/** Lets people drag the textarea taller; defaults to true. */
|
|
23
|
+
resizable?: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** A multi-line labeled text field with optional helper text, error and character count. */
|
|
26
|
+
export declare const TextArea: import("react").ForwardRefExoticComponent<Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "value" | "defaultValue"> & {
|
|
27
|
+
/** Visible label above the textarea; also its accessible name. Hide it with hideLabel. */
|
|
28
|
+
label: string;
|
|
29
|
+
/** Hides the label visually but keeps it as the textarea's accessible name. */
|
|
30
|
+
hideLabel?: boolean;
|
|
31
|
+
/** Visible hint below the textarea, announced with it; `error` replaces it while set. */
|
|
32
|
+
helperText?: string;
|
|
33
|
+
/** Visible error message below the textarea; marks the field invalid while set. */
|
|
34
|
+
error?: string;
|
|
35
|
+
/** Type size and padding; defaults to 'medium'. */
|
|
36
|
+
size?: "small" | "medium" | "large";
|
|
37
|
+
/** The current text when controlled; pair it with `onValueChange`. */
|
|
38
|
+
value?: string;
|
|
39
|
+
/** The starting text when uncontrolled; a form reset returns to it. */
|
|
40
|
+
defaultValue?: string;
|
|
41
|
+
/** Called with the new text on every edit. */
|
|
42
|
+
onValueChange?: (value: string) => void;
|
|
43
|
+
/** Shows a visible character count below the textarea, as "n / maxLength" when maxLength is set. */
|
|
44
|
+
showCount?: boolean;
|
|
45
|
+
/** Lets people drag the textarea taller; defaults to true. */
|
|
46
|
+
resizable?: boolean;
|
|
47
|
+
} & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useId, useRef } from 'react';
|
|
4
|
+
import { cx } from '../internal/cx.js';
|
|
5
|
+
import { useFormReset, useMergedRef, useNativeMirror } from '../text-input/native-field.js';
|
|
6
|
+
/** A multi-line labeled text field with optional helper text, error and character count. */
|
|
7
|
+
export const TextArea = forwardRef(function TextArea({ label, hideLabel = false, helperText, error, size = 'medium', value, defaultValue, onValueChange, onChange, showCount, resizable = true, id, maxLength, className, style, 'aria-invalid': ariaInvalid, 'aria-describedby': describedBy, ...props }, ref) {
|
|
8
|
+
const generatedId = useId();
|
|
9
|
+
const inputId = id ?? generatedId;
|
|
10
|
+
const description = error || helperText;
|
|
11
|
+
const textarea = useRef(null);
|
|
12
|
+
const textareaRef = useMergedRef(ref, textarea);
|
|
13
|
+
const controlled = value !== undefined;
|
|
14
|
+
const [current, sync] = useNativeMirror(() => textarea.current?.value, value ?? defaultValue ?? '');
|
|
15
|
+
useFormReset(textarea, sync);
|
|
16
|
+
const text = controlled ? value : current;
|
|
17
|
+
return _jsxs("div", { className: cx('diya-text-field diya-textarea', className), style: style, "data-size": size, "data-invalid": !!error, "data-disabled": props.disabled, children: [_jsx("label", { htmlFor: inputId, className: hideLabel ? 'diya-sr-only' : undefined, children: label }), _jsx("textarea", { ...props, ref: textareaRef, id: inputId, ...(controlled ? { value } : { defaultValue }), maxLength: maxLength, onChange: (event) => { onChange?.(event); if (!controlled)
|
|
18
|
+
sync(); onValueChange?.(event.target.value); }, "aria-invalid": error ? true : ariaInvalid, "aria-describedby": [describedBy, description ? `${inputId}-description` : undefined, showCount ? `${inputId}-count` : undefined].filter(Boolean).join(' ') || undefined, style: { resize: resizable ? 'vertical' : 'none' } }), _jsxs("div", { className: "diya-textarea-footer", children: [description && _jsxs("p", { className: "diya-text-field-description", id: `${inputId}-description`, children: [error && _jsx("span", { "aria-hidden": "true", children: "!" }), description] }), showCount && _jsxs("span", { className: "diya-textarea-count", id: `${inputId}-count`, children: [text.length, maxLength ? ` / ${maxLength}` : ''] })] })] });
|
|
19
|
+
});
|