@milktop/react-ui 0.0.0-stage → 0.1.2
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/README.md +173 -2
- package/dist/accordion/Accordion.d.ts +65 -0
- package/dist/accordion/Accordion.js +61 -0
- package/dist/accordion/accordion.css +17 -0
- package/dist/action-bar/ActionBar.d.ts +33 -0
- package/dist/action-bar/ActionBar.js +48 -0
- package/dist/action-bar/action-bar.css +22 -0
- package/dist/alert/Alert.d.ts +28 -0
- package/dist/alert/Alert.js +12 -0
- package/dist/alert/alert.css +32 -0
- package/dist/app-shell/AppShell.d.ts +38 -0
- package/dist/app-shell/AppShell.js +92 -0
- package/dist/app-shell/Topbar.d.ts +20 -0
- package/dist/app-shell/Topbar.js +13 -0
- package/dist/app-shell/app-shell.css +41 -0
- package/dist/app-shell/shell.d.ts +9 -0
- package/dist/app-shell/shell.js +3 -0
- package/dist/attachments/Attachments.d.ts +84 -0
- package/dist/attachments/Attachments.js +235 -0
- package/dist/attachments/attachments.css +86 -0
- package/dist/avatar/Avatar.d.ts +31 -0
- package/dist/avatar/Avatar.js +41 -0
- package/dist/avatar/AvatarGroup.d.ts +41 -0
- package/dist/avatar/AvatarGroup.js +17 -0
- package/dist/avatar/avatar.css +73 -0
- package/dist/badge/Badge.d.ts +26 -0
- package/dist/badge/Badge.js +10 -0
- package/dist/badge/badge.css +24 -0
- package/dist/banner/Banner.d.ts +33 -0
- package/dist/banner/Banner.js +14 -0
- package/dist/banner/banner.css +25 -0
- package/dist/breadcrumbs/Breadcrumbs.d.ts +39 -0
- package/dist/breadcrumbs/Breadcrumbs.js +28 -0
- package/dist/breadcrumbs/breadcrumbs.css +18 -0
- package/dist/button/Button.d.ts +40 -0
- package/dist/button/Button.js +19 -0
- package/dist/button/button.css +63 -0
- package/dist/card/Card.d.ts +355 -0
- package/dist/card/Card.js +56 -0
- package/dist/card/card.css +43 -0
- package/dist/chart/AreaChart.d.ts +19 -0
- package/dist/chart/AreaChart.js +61 -0
- package/dist/chart/BarChart.d.ts +3 -0
- package/dist/chart/BarChart.js +60 -0
- package/dist/chart/ChartFrame.d.ts +25 -0
- package/dist/chart/ChartFrame.js +22 -0
- package/dist/chart/DonutChart.d.ts +20 -0
- package/dist/chart/DonutChart.js +41 -0
- package/dist/chart/Sparkline.d.ts +26 -0
- package/dist/chart/Sparkline.js +36 -0
- package/dist/chart/chart.css +51 -0
- package/dist/chart/chart.d.ts +23 -0
- package/dist/chart/chart.js +19 -0
- package/dist/checkbox/Checkbox.d.ts +50 -0
- package/dist/checkbox/Checkbox.js +19 -0
- package/dist/checkbox/CheckboxGroup.d.ts +44 -0
- package/dist/checkbox/CheckboxGroup.js +80 -0
- package/dist/checkbox/checkbox.css +49 -0
- package/dist/collapsible/Collapsible.d.ts +41 -0
- package/dist/collapsible/Collapsible.js +15 -0
- package/dist/collapsible/collapsible.css +20 -0
- package/dist/color-scheme.d.ts +18 -0
- package/dist/color-scheme.js +84 -0
- package/dist/command/Command.d.ts +64 -0
- package/dist/command/Command.js +162 -0
- package/dist/command/command.css +47 -0
- package/dist/confirm/confirm.css +5 -0
- package/dist/confirm/confirm.d.ts +42 -0
- package/dist/confirm/confirm.js +115 -0
- package/dist/copy-button/CopyButton.d.ts +30 -0
- package/dist/copy-button/CopyButton.js +23 -0
- package/dist/copy-button/copy-button.css +12 -0
- package/dist/css.d.ts +2 -0
- package/dist/data-list/DataList.d.ts +55 -0
- package/dist/data-list/DataList.js +43 -0
- package/dist/data-list/data-list.css +19 -0
- package/dist/date-picker/DatePicker.d.ts +95 -0
- package/dist/date-picker/DatePicker.js +237 -0
- package/dist/date-picker/date-picker.css +53 -0
- package/dist/dialog/Dialog.d.ts +98 -0
- package/dist/dialog/Dialog.js +92 -0
- package/dist/dialog/dialog.css +31 -0
- package/dist/divider/Divider.d.ts +27 -0
- package/dist/divider/Divider.js +8 -0
- package/dist/divider/divider.css +12 -0
- package/dist/editable/Editable.d.ts +47 -0
- package/dist/editable/Editable.js +51 -0
- package/dist/editable/editable.css +25 -0
- package/dist/editor/Editor.d.ts +59 -0
- package/dist/editor/Editor.js +220 -0
- package/dist/editor/editor.css +42 -0
- package/dist/editor/image.d.ts +1 -0
- package/dist/editor/image.js +23 -0
- package/dist/empty-state/EmptyState.d.ts +22 -0
- package/dist/empty-state/EmptyState.js +9 -0
- package/dist/empty-state/empty-state.css +14 -0
- package/dist/event-calendar/EventCalendar.d.ts +213 -0
- package/dist/event-calendar/EventCalendar.js +686 -0
- package/dist/event-calendar/dates.d.ts +8 -0
- package/dist/event-calendar/dates.js +21 -0
- package/dist/event-calendar/event-calendar.css +157 -0
- package/dist/field/Field.d.ts +66 -0
- package/dist/field/Field.js +23 -0
- package/dist/field/field.css +26 -0
- package/dist/file-upload/FileUpload.d.ts +60 -0
- package/dist/file-upload/FileUpload.js +102 -0
- package/dist/file-upload/file-upload.css +32 -0
- package/dist/format/FormatNumber.d.ts +42 -0
- package/dist/format/FormatNumber.js +59 -0
- package/dist/format/LocaleProvider.d.ts +33 -0
- package/dist/format/LocaleProvider.js +48 -0
- package/dist/format/format.d.ts +37 -0
- package/dist/format/format.js +74 -0
- package/dist/format/messages.d.ts +211 -0
- package/dist/format/messages.js +202 -0
- package/dist/highlight/Highlight.d.ts +11 -0
- package/dist/highlight/Highlight.js +14 -0
- package/dist/highlight/highlight.css +17 -0
- package/dist/hover-card/HoverCard.d.ts +47 -0
- package/dist/hover-card/HoverCard.js +18 -0
- package/dist/hover-card/hover-card.css +19 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +85 -0
- package/dist/input/Input.d.ts +41 -0
- package/dist/input/Input.js +48 -0
- package/dist/input/input.css +25 -0
- package/dist/kbd/Kbd.d.ts +19 -0
- package/dist/kbd/Kbd.js +20 -0
- package/dist/kbd/kbd.css +8 -0
- package/dist/menu/Menu.d.ts +223 -0
- package/dist/menu/Menu.js +275 -0
- package/dist/menu/menu.css +38 -0
- package/dist/native-select/NativeSelect.d.ts +36 -0
- package/dist/native-select/NativeSelect.js +51 -0
- package/dist/native-select/native-select.css +16 -0
- package/dist/number-input/NumberInput.d.ts +56 -0
- package/dist/number-input/NumberInput.js +67 -0
- package/dist/number-input/number-input.css +21 -0
- package/dist/page/Page.d.ts +30 -0
- package/dist/page/Page.js +9 -0
- package/dist/page/page.css +17 -0
- package/dist/pagination/Pagination.d.ts +31 -0
- package/dist/pagination/Pagination.js +58 -0
- package/dist/pagination/pagination.css +15 -0
- package/dist/password-input/PasswordInput.d.ts +47 -0
- package/dist/password-input/PasswordInput.js +35 -0
- package/dist/password-input/password-input.css +18 -0
- package/dist/pin-input/PinInput.d.ts +45 -0
- package/dist/pin-input/PinInput.js +22 -0
- package/dist/pin-input/pin-input.css +13 -0
- package/dist/popover/Popover.d.ts +65 -0
- package/dist/popover/Popover.js +26 -0
- package/dist/popover/popover.css +16 -0
- package/dist/progress/Progress.d.ts +56 -0
- package/dist/progress/Progress.js +28 -0
- package/dist/progress/progress.css +24 -0
- package/dist/prose/Prose.d.ts +23 -0
- package/dist/prose/Prose.js +10 -0
- package/dist/prose/prose.css +29 -0
- package/dist/radio-group/RadioGroup.d.ts +59 -0
- package/dist/radio-group/RadioGroup.js +37 -0
- package/dist/radio-group/radio-group.css +30 -0
- package/dist/rolling-number/RollingNumber.d.ts +27 -0
- package/dist/rolling-number/RollingNumber.js +18 -0
- package/dist/rolling-number/rolling-number.css +10 -0
- package/dist/segment-group/Segmented.d.ts +68 -0
- package/dist/segment-group/Segmented.js +79 -0
- package/dist/segment-group/segment-group.css +25 -0
- package/dist/select/ListSelect.d.ts +5 -0
- package/dist/select/ListSelect.js +82 -0
- package/dist/select/SearchSelect.d.ts +43 -0
- package/dist/select/SearchSelect.js +219 -0
- package/dist/select/Select.d.ts +147 -0
- package/dist/select/Select.js +32 -0
- package/dist/select/items.d.ts +17 -0
- package/dist/select/items.js +14 -0
- package/dist/select/search-select.css +45 -0
- package/dist/select/select.css +52 -0
- package/dist/sheet/Sheet.d.ts +88 -0
- package/dist/sheet/Sheet.js +66 -0
- package/dist/sheet/sheet.css +44 -0
- package/dist/sidebar/NavGroup.d.ts +29 -0
- package/dist/sidebar/NavGroup.js +78 -0
- package/dist/sidebar/NavItem.d.ts +33 -0
- package/dist/sidebar/NavItem.js +50 -0
- package/dist/sidebar/NavSection.d.ts +20 -0
- package/dist/sidebar/NavSection.js +44 -0
- package/dist/sidebar/Sidebar.d.ts +65 -0
- package/dist/sidebar/Sidebar.js +59 -0
- package/dist/sidebar/SidebarUser.d.ts +31 -0
- package/dist/sidebar/SidebarUser.js +14 -0
- package/dist/sidebar/context.d.ts +13 -0
- package/dist/sidebar/context.js +6 -0
- package/dist/sidebar/sidebar.css +109 -0
- package/dist/skeleton/Skeleton.d.ts +25 -0
- package/dist/skeleton/Skeleton.js +8 -0
- package/dist/skeleton/skeleton.css +13 -0
- package/dist/slider/Slider.d.ts +70 -0
- package/dist/slider/Slider.js +65 -0
- package/dist/slider/slider.css +17 -0
- package/dist/sortable-list/SortableList.d.ts +46 -0
- package/dist/sortable-list/SortableList.js +85 -0
- package/dist/sortable-list/sortable-list.css +25 -0
- package/dist/spinner/Spinner.d.ts +18 -0
- package/dist/spinner/Spinner.js +11 -0
- package/dist/spinner/spinner.css +9 -0
- package/dist/stat/Stat.d.ts +42 -0
- package/dist/stat/Stat.js +21 -0
- package/dist/stat/Stats.d.ts +23 -0
- package/dist/stat/Stats.js +8 -0
- package/dist/stat/stat.css +34 -0
- package/dist/swatch-picker/SwatchPicker.d.ts +45 -0
- package/dist/swatch-picker/SwatchPicker.js +54 -0
- package/dist/swatch-picker/swatch-picker.css +38 -0
- package/dist/switch/Switch.d.ts +39 -0
- package/dist/switch/Switch.js +17 -0
- package/dist/switch/switch.css +13 -0
- package/dist/table/Table.d.ts +90 -0
- package/dist/table/Table.js +145 -0
- package/dist/table/table.css +44 -0
- package/dist/tabs/Tabs.d.ts +87 -0
- package/dist/tabs/Tabs.js +90 -0
- package/dist/tabs/tabs.css +53 -0
- package/dist/tags-input/TagsInput.d.ts +69 -0
- package/dist/tags-input/TagsInput.js +95 -0
- package/dist/tags-input/tags-input.css +37 -0
- package/dist/textarea/Textarea.d.ts +24 -0
- package/dist/textarea/Textarea.js +51 -0
- package/dist/textarea/textarea.css +14 -0
- package/dist/theme-toggle/ThemeToggle.d.ts +49 -0
- package/dist/theme-toggle/ThemeToggle.js +71 -0
- package/dist/theme-toggle/theme-toggle.css +23 -0
- package/dist/theme.css +121 -0
- package/dist/time-input/TimeInput.d.ts +25 -0
- package/dist/time-input/TimeInput.js +20 -0
- package/dist/timeline/Timeline.d.ts +50 -0
- package/dist/timeline/Timeline.js +15 -0
- package/dist/timeline/timeline.css +29 -0
- package/dist/toast/Toaster.d.ts +36 -0
- package/dist/toast/Toaster.js +25 -0
- package/dist/toast/toast.css +33 -0
- package/dist/tooltip/Tooltip.d.ts +55 -0
- package/dist/tooltip/Tooltip.js +48 -0
- package/dist/tooltip/tooltip.css +18 -0
- package/dist/tree-view/TreeView.d.ts +86 -0
- package/dist/tree-view/TreeView.js +74 -0
- package/dist/tree-view/tree-view.css +29 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +59 -0
- package/package.json +99 -4
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabs: the Tab children are the panels, and the tab list is built from
|
|
3
|
+
* their labels.
|
|
4
|
+
*
|
|
5
|
+
* <Tabs value={tab} onValueChange={setTab}>
|
|
6
|
+
* <Tab value="lessons" label="Lessons" icon="lucide:calendar">…</Tab>
|
|
7
|
+
* <Tab value="invoices" label="Invoices" count={2} countColor="danger">…</Tab>
|
|
8
|
+
* </Tabs>
|
|
9
|
+
*
|
|
10
|
+
* Arrow keys move between tabs (selecting as they go), Home/End jump to the
|
|
11
|
+
* ends. With no value, the first enabled tab is selected.
|
|
12
|
+
*
|
|
13
|
+
* - `value`, `defaultValue`: the selected Tab's value; `onValueChange`: called with it
|
|
14
|
+
* - `variant`: 'line' (default, an underline) or 'pills'
|
|
15
|
+
* - `counts`: where the tabs' counts sit: 'inline' (default, after the label)
|
|
16
|
+
* or 'corner' (top right, like a button's)
|
|
17
|
+
* - `renderTrigger`: a tab's content, given the tab's props; its count still follows
|
|
18
|
+
*
|
|
19
|
+
* By hand, with Ark's parts (data-variant on the root picks the look):
|
|
20
|
+
*
|
|
21
|
+
* <Tabs.Root defaultValue="a" data-variant="pills">
|
|
22
|
+
* <Tabs.List>
|
|
23
|
+
* <Tabs.Trigger value="a">First <Tabs.Count>3</Tabs.Count></Tabs.Trigger>
|
|
24
|
+
* <Tabs.Trigger value="b">Second</Tabs.Trigger>
|
|
25
|
+
* <Tabs.Indicator />
|
|
26
|
+
* </Tabs.List>
|
|
27
|
+
* <Tabs.Content value="a">…</Tabs.Content>
|
|
28
|
+
* <Tabs.Content value="b">…</Tabs.Content>
|
|
29
|
+
* </Tabs.Root>
|
|
30
|
+
*/
|
|
31
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
32
|
+
import { Tabs as Ark, type TabsValueChangeDetails } from '@ark-ui/react/tabs';
|
|
33
|
+
import 'iconify-icon';
|
|
34
|
+
import { type Classes } from '../utils.js';
|
|
35
|
+
import '../theme.css';
|
|
36
|
+
import './tabs.css';
|
|
37
|
+
type Value = string | number;
|
|
38
|
+
export type TabProps = Omit<Ark.ContentProps, 'value'> & {
|
|
39
|
+
value: Value;
|
|
40
|
+
label?: ReactNode;
|
|
41
|
+
icon?: string | null;
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
count?: number | null;
|
|
44
|
+
max?: number;
|
|
45
|
+
dot?: boolean;
|
|
46
|
+
pulse?: boolean;
|
|
47
|
+
countColor?: 'neutral' | 'accent' | 'danger' | 'success' | 'warning';
|
|
48
|
+
countLabel?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* One panel of a Tabs; `label` (and an optional `icon`) make its tab.
|
|
52
|
+
*
|
|
53
|
+
* - `value`: identifies it (the Tabs' value when selected)
|
|
54
|
+
* - `label`: its text; `icon`: an Iconify name before it; `disabled`
|
|
55
|
+
* - `count`: a small count after the label (hidden at 0 or null); past `max`
|
|
56
|
+
* it shows e.g. "99+"
|
|
57
|
+
* - `dot`: a dot there instead, for "something to look at"
|
|
58
|
+
* - `pulse`: a ring ripples out from the count or dot (not when the system
|
|
59
|
+
* asks for less motion)
|
|
60
|
+
* - `countColor`: 'neutral' (default, quiet), 'accent', 'danger', 'success'
|
|
61
|
+
* or 'warning', for the dot too
|
|
62
|
+
* - `countLabel`: what the count is, for assistive tech ("Invoices, 2 overdue")
|
|
63
|
+
*/
|
|
64
|
+
export declare function Tab({ value, label: _label, icon: _icon, disabled: _disabled, count: _count, max: _max, dot: _dot, pulse: _pulse, countColor: _countColor, countLabel: _countLabel, className, ...rest }: TabProps): import("react").JSX.Element;
|
|
65
|
+
export type TabsProps = Omit<Ark.RootProps, 'value' | 'defaultValue' | 'onValueChange'> & {
|
|
66
|
+
value?: Value | null;
|
|
67
|
+
defaultValue?: Value | null;
|
|
68
|
+
onValueChange?: (value: Value | null, details: TabsValueChangeDetails) => void;
|
|
69
|
+
variant?: 'line' | 'pills';
|
|
70
|
+
counts?: 'inline' | 'corner';
|
|
71
|
+
renderTrigger?: (tab: TabProps) => ReactNode;
|
|
72
|
+
classes?: Classes<'root' | 'list' | 'trigger' | 'icon' | 'count' | 'indicator' | 'panels' | 'content'>;
|
|
73
|
+
};
|
|
74
|
+
declare function TabsShortcut({ value, defaultValue, onValueChange, variant, counts, renderTrigger, className, classes, children, ...rest }: TabsProps): import("react").JSX.Element;
|
|
75
|
+
export declare const Tabs: typeof TabsShortcut & {
|
|
76
|
+
Root: import("react").ForwardRefExoticComponent<Ark.RootProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
77
|
+
List: import("react").ForwardRefExoticComponent<Ark.ListProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
78
|
+
Trigger: import("react").ForwardRefExoticComponent<Ark.TriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
79
|
+
Indicator: import("react").ForwardRefExoticComponent<Ark.IndicatorProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
80
|
+
Content: import("react").ForwardRefExoticComponent<Ark.ContentProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
81
|
+
Context: (props: Ark.ContextProps) => ReactNode;
|
|
82
|
+
Count: {
|
|
83
|
+
(props: import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>): ReactElement<any, string | import("react").JSXElementConstructor<any>>;
|
|
84
|
+
displayName: string;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Tabs: the Tab children are the panels, and the tab list is built from
|
|
4
|
+
* their labels.
|
|
5
|
+
*
|
|
6
|
+
* <Tabs value={tab} onValueChange={setTab}>
|
|
7
|
+
* <Tab value="lessons" label="Lessons" icon="lucide:calendar">…</Tab>
|
|
8
|
+
* <Tab value="invoices" label="Invoices" count={2} countColor="danger">…</Tab>
|
|
9
|
+
* </Tabs>
|
|
10
|
+
*
|
|
11
|
+
* Arrow keys move between tabs (selecting as they go), Home/End jump to the
|
|
12
|
+
* ends. With no value, the first enabled tab is selected.
|
|
13
|
+
*
|
|
14
|
+
* - `value`, `defaultValue`: the selected Tab's value; `onValueChange`: called with it
|
|
15
|
+
* - `variant`: 'line' (default, an underline) or 'pills'
|
|
16
|
+
* - `counts`: where the tabs' counts sit: 'inline' (default, after the label)
|
|
17
|
+
* or 'corner' (top right, like a button's)
|
|
18
|
+
* - `renderTrigger`: a tab's content, given the tab's props; its count still follows
|
|
19
|
+
*
|
|
20
|
+
* By hand, with Ark's parts (data-variant on the root picks the look):
|
|
21
|
+
*
|
|
22
|
+
* <Tabs.Root defaultValue="a" data-variant="pills">
|
|
23
|
+
* <Tabs.List>
|
|
24
|
+
* <Tabs.Trigger value="a">First <Tabs.Count>3</Tabs.Count></Tabs.Trigger>
|
|
25
|
+
* <Tabs.Trigger value="b">Second</Tabs.Trigger>
|
|
26
|
+
* <Tabs.Indicator />
|
|
27
|
+
* </Tabs.List>
|
|
28
|
+
* <Tabs.Content value="a">…</Tabs.Content>
|
|
29
|
+
* <Tabs.Content value="b">…</Tabs.Content>
|
|
30
|
+
* </Tabs.Root>
|
|
31
|
+
*/
|
|
32
|
+
import { Children, isValidElement, useEffect } from 'react';
|
|
33
|
+
import { Tabs as Ark } from '@ark-ui/react/tabs';
|
|
34
|
+
import 'iconify-icon';
|
|
35
|
+
import { cx, partOf, partsOf, useControllable, withParts } from '../utils.js';
|
|
36
|
+
import '../theme.css';
|
|
37
|
+
import './tabs.css';
|
|
38
|
+
/**
|
|
39
|
+
* One panel of a Tabs; `label` (and an optional `icon`) make its tab.
|
|
40
|
+
*
|
|
41
|
+
* - `value`: identifies it (the Tabs' value when selected)
|
|
42
|
+
* - `label`: its text; `icon`: an Iconify name before it; `disabled`
|
|
43
|
+
* - `count`: a small count after the label (hidden at 0 or null); past `max`
|
|
44
|
+
* it shows e.g. "99+"
|
|
45
|
+
* - `dot`: a dot there instead, for "something to look at"
|
|
46
|
+
* - `pulse`: a ring ripples out from the count or dot (not when the system
|
|
47
|
+
* asks for less motion)
|
|
48
|
+
* - `countColor`: 'neutral' (default, quiet), 'accent', 'danger', 'success'
|
|
49
|
+
* or 'warning', for the dot too
|
|
50
|
+
* - `countLabel`: what the count is, for assistive tech ("Invoices, 2 overdue")
|
|
51
|
+
*/
|
|
52
|
+
export function Tab({ value, label: _label, icon: _icon, disabled: _disabled, count: _count, max: _max, dot: _dot, pulse: _pulse, countColor: _countColor, countLabel: _countLabel, className, ...rest }) {
|
|
53
|
+
return _jsx(Ark.Content, { ...rest, value: String(value), "data-scope": "tabs", "data-part": "content", className: className });
|
|
54
|
+
}
|
|
55
|
+
function TabsShortcut({ value, defaultValue, onValueChange, variant = 'line', counts = 'inline', renderTrigger, className, classes, children, ...rest }) {
|
|
56
|
+
const part = partsOf('tabs', classes);
|
|
57
|
+
const tabs = Children.toArray(children).filter((child) => isValidElement(child) && child.type === Tab);
|
|
58
|
+
// With no value, the first enabled tab.
|
|
59
|
+
const first = tabs.find((tab) => !tab.props.disabled)?.props.value ?? null;
|
|
60
|
+
const [current, setCurrent] = useControllable(value, defaultValue ?? first, undefined);
|
|
61
|
+
const shown = current ?? first;
|
|
62
|
+
// A null value (controlled or not) selects the first enabled tab, and says so.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (current != null || first == null)
|
|
65
|
+
return;
|
|
66
|
+
setCurrent(first);
|
|
67
|
+
onValueChange?.(first, { value: String(first) });
|
|
68
|
+
}, [current, first]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
69
|
+
// Zag's values are strings; map them back to the tabs' own.
|
|
70
|
+
function change(details) {
|
|
71
|
+
const next = tabs.find((tab) => String(tab.props.value) === details.value)?.props.value ?? null;
|
|
72
|
+
setCurrent(next);
|
|
73
|
+
onValueChange?.(next, details);
|
|
74
|
+
}
|
|
75
|
+
return (_jsxs(Ark.Root, { ...rest, ...part('root'), className: cx(className, classes?.root), "data-variant": variant, "data-counts": counts, value: shown == null ? null : String(shown), onValueChange: change, children: [_jsxs(Ark.List, { ...part('list'), children: [tabs.map(({ props: tab }) => {
|
|
76
|
+
const { count, max = 99, dot, pulse, countColor = 'neutral', countLabel = '' } = tab;
|
|
77
|
+
const badge = count && count > 0 ? (count > max ? `${max}+` : String(count)) : dot ? '' : null;
|
|
78
|
+
const spoken = count && count > 0 ? [count, countLabel].filter(Boolean).join(' ') : dot ? countLabel : '';
|
|
79
|
+
return (_jsxs(Ark.Trigger, { value: String(tab.value), disabled: !!tab.disabled, ...part('trigger'), children: [renderTrigger ? renderTrigger(tab) : _jsxs(_Fragment, { children: [tab.icon && _jsx("iconify-icon", { ...part('icon'), icon: tab.icon, "aria-hidden": "true" }), _jsx("span", { children: tab.label })] }), badge != null && _jsxs(_Fragment, { children: [_jsx("span", { ...part('count'), "aria-hidden": "true", "data-dot": badge === '' || undefined, "data-pulse": pulse || undefined, "data-color": countColor, children: badge }), spoken && _jsxs("span", { ...part('sr-only'), children: [", ", spoken] })] })] }, String(tab.value)));
|
|
80
|
+
}), _jsx(Ark.Indicator, { ...part('indicator') })] }), _jsx("div", { ...part('panels'), children: children })] }));
|
|
81
|
+
}
|
|
82
|
+
export const Tabs = withParts(TabsShortcut, {
|
|
83
|
+
Root: Ark.Root,
|
|
84
|
+
List: Ark.List,
|
|
85
|
+
Trigger: Ark.Trigger,
|
|
86
|
+
Indicator: Ark.Indicator,
|
|
87
|
+
Content: Ark.Content,
|
|
88
|
+
Context: Ark.Context,
|
|
89
|
+
Count: partOf('tabs', 'count', 'span'),
|
|
90
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
@layer ui {
|
|
2
|
+
[data-scope=tabs][data-part=root] { display: block; width: 100%; color: var(--ui-text); font-family: var(--ui-font); }
|
|
3
|
+
[data-scope=tabs][data-part=list] { position: relative; display: flex; gap: 0.25rem; overflow-x: auto;
|
|
4
|
+
[data-variant=line] > & { border-bottom: 1px solid var(--ui-border); }
|
|
5
|
+
/* Pills: a raised pill behind the selected tab, like Segmented. */
|
|
6
|
+
[data-variant=pills] > & { display: inline-flex; padding: 0.25rem; background: var(--ui-hover); border-radius: var(--ui-radius); } }
|
|
7
|
+
[data-scope=tabs][data-part=trigger] {
|
|
8
|
+
position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 0.5rem; height: var(--ui-control-height); padding: 0 0.75rem;
|
|
9
|
+
border: none; background: transparent; color: var(--ui-muted); font: inherit; font-size: 14px; font-weight: 500; white-space: nowrap;
|
|
10
|
+
cursor: pointer; border-radius: var(--ui-radius);
|
|
11
|
+
&:hover, &[data-selected] { color: var(--ui-text); }
|
|
12
|
+
&:focus-visible { outline: 2px solid var(--ui-ring-soft); }
|
|
13
|
+
&[data-disabled] { opacity: 0.4; cursor: not-allowed; }
|
|
14
|
+
/* Taller than a control, so the underlined tabs have room to breathe. */
|
|
15
|
+
[data-variant=line] & { border-radius: 0; height: calc(var(--ui-control-height) + 0.5rem); padding: 0 0.875rem; }
|
|
16
|
+
[data-variant=pills] & { height: calc(var(--ui-control-height) - 8px); border-radius: calc(var(--ui-radius) - 2px); } }
|
|
17
|
+
[data-scope=tabs][data-part=icon] { display: block; font-size: 16px; }
|
|
18
|
+
/* The count after a label: quiet by default, coloured for status. */
|
|
19
|
+
[data-scope=tabs][data-part=count] { position: relative; display: inline-grid; place-items: center; min-width: 18px; height: 18px;
|
|
20
|
+
padding: 0 5px; box-sizing: border-box; border-radius: 9999px; isolation: isolate;
|
|
21
|
+
font-size: 11px; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums;
|
|
22
|
+
--count-bg: var(--ui-hover); --count-text: var(--ui-muted); background: var(--count-bg); color: var(--count-text);
|
|
23
|
+
&[data-color=accent] { --count-bg: var(--ui-accent); --count-text: var(--ui-accent-text); }
|
|
24
|
+
&[data-color=danger] { --count-bg: var(--ui-danger); --count-text: white; }
|
|
25
|
+
&[data-color=success] { --count-bg: var(--ui-success); --count-text: white; }
|
|
26
|
+
&[data-color=warning] { --count-bg: #f59e0b; --count-text: #451a03; }
|
|
27
|
+
&[data-dot] { min-width: 0; width: 8px; height: 8px; padding: 0; }
|
|
28
|
+
&[data-dot][data-color=neutral] { --count-bg: var(--ui-muted); }
|
|
29
|
+
[data-selected] > &[data-color=neutral]:not([data-dot]) { --count-bg: var(--ui-accent-soft); --count-text: var(--ui-accent-soft-text); }
|
|
30
|
+
/* A copy of its colour that grows and fades behind it. */
|
|
31
|
+
&[data-pulse]::after { content: ''; position: absolute; inset: 0; z-index: -1; border-radius: 9999px; background: inherit;
|
|
32
|
+
animation: ui-ping 1.6s cubic-bezier(0, 0, 0.2, 1) infinite; }
|
|
33
|
+
/* Corner counts: small, raised into the tab's top right (in room the
|
|
34
|
+
tab makes for them, below), ringed in the page colour like a button's. */
|
|
35
|
+
[data-counts=corner] & { position: absolute; top: 0.375rem; right: 0.25rem; min-width: 15px; height: 15px;
|
|
36
|
+
padding: 0 4px; font-size: 10px; box-shadow: 0 0 0 2px var(--ui-surface);
|
|
37
|
+
[data-variant=pills] & { top: 0.125rem; right: 0.125rem; }
|
|
38
|
+
&[data-dot] { min-width: 0; width: 7px; height: 7px; padding: 0; top: 0.625rem; right: 0.5rem;
|
|
39
|
+
[data-variant=pills] & { top: 0.375rem; right: 0.375rem; } } } }
|
|
40
|
+
/* A tab with a corner count makes room for it on the right. */
|
|
41
|
+
[data-counts=corner] [data-scope=tabs][data-part=trigger]:has(> [data-part=count]) { padding-right: 1.375rem;
|
|
42
|
+
&:has(> [data-dot]) { padding-right: 1.125rem; } }
|
|
43
|
+
[data-scope=tabs][data-part=sr-only] { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
44
|
+
@keyframes ui-ping { from { scale: 1; opacity: 0.7; } 75%, to { scale: 2.2; opacity: 0; } }
|
|
45
|
+
@media (prefers-reduced-motion: reduce) { [data-scope=tabs][data-part=count]::after { animation: none; } }
|
|
46
|
+
[data-scope=tabs][data-part=indicator] { transition-property: var(--transition-property); transition-duration: 200ms;
|
|
47
|
+
[data-variant=line] & { position: absolute; bottom: -1px; left: var(--left); width: var(--width); height: 2px; background: var(--ui-accent); }
|
|
48
|
+
[data-variant=pills] & { position: absolute; z-index: 0; top: var(--top); left: var(--left); width: var(--width); height: var(--height);
|
|
49
|
+
background: var(--ui-surface); border-radius: calc(var(--ui-radius) - 2px); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); } }
|
|
50
|
+
[data-scope=tabs][data-part=panels] { padding-top: 1rem; }
|
|
51
|
+
[data-scope=tabs][data-part=content] { display: block; outline: none;
|
|
52
|
+
&:focus-visible { outline: 2px solid var(--ui-ring-soft); outline-offset: 4px; border-radius: 4px; } }
|
|
53
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tags input: type and press Enter (or a comma) to add a tag; Backspace
|
|
3
|
+
* removes the last; double-click a tag to edit it.
|
|
4
|
+
*
|
|
5
|
+
* <TagsInput label="Topics" value={topics} onValueChange={setTopics} placeholder="Add a topic…" />
|
|
6
|
+
*
|
|
7
|
+
* - `value`, `defaultValue`: arrays of strings; `onValueChange`: called with the new array
|
|
8
|
+
* - `max`: the most tags allowed
|
|
9
|
+
* - `variant`: the tags' look: 'subtle', 'accent' or 'outline'
|
|
10
|
+
* - `editable`: false to stop editing tags in place
|
|
11
|
+
* - `delimiter`: typing it adds a tag too (',')
|
|
12
|
+
* - `validate`: function({ inputValue, value }) returning whether to add it; a
|
|
13
|
+
* rejected tag stays in the box to fix, and the field flashes red
|
|
14
|
+
* - `allowDuplicates`: let the same tag in twice
|
|
15
|
+
* - `addOnPaste`: split pasted text by the delimiter into tags (on by default)
|
|
16
|
+
* - `name`: for plain form posts (comma-separated)
|
|
17
|
+
*
|
|
18
|
+
* By hand, with Ark's parts:
|
|
19
|
+
*
|
|
20
|
+
* <TagsInput.Root>
|
|
21
|
+
* <TagsInput.Label>Topics</TagsInput.Label>
|
|
22
|
+
* <TagsInput.Control>
|
|
23
|
+
* <TagsInput.Context>{(api) => api.value.map((tag, index) => (
|
|
24
|
+
* <TagsInput.Item key={index} index={index} value={tag}>
|
|
25
|
+
* <TagsInput.ItemPreview><TagsInput.ItemText>{tag}</TagsInput.ItemText><TagsInput.ItemDeleteTrigger>×</TagsInput.ItemDeleteTrigger></TagsInput.ItemPreview>
|
|
26
|
+
* <TagsInput.ItemInput />
|
|
27
|
+
* </TagsInput.Item>
|
|
28
|
+
* ))}</TagsInput.Context>
|
|
29
|
+
* <TagsInput.Input />
|
|
30
|
+
* </TagsInput.Control>
|
|
31
|
+
* <TagsInput.HiddenInput />
|
|
32
|
+
* </TagsInput.Root>
|
|
33
|
+
*/
|
|
34
|
+
import { type ReactNode } from 'react';
|
|
35
|
+
import { TagsInput as Ark, type TagsInputValueChangeDetails } from '@ark-ui/react/tags-input';
|
|
36
|
+
import 'iconify-icon';
|
|
37
|
+
import { type Classes } from '../utils.js';
|
|
38
|
+
import '../theme.css';
|
|
39
|
+
import './tags-input.css';
|
|
40
|
+
export type TagsInputProps = Omit<Ark.RootProps, 'value' | 'defaultValue' | 'onValueChange' | 'label'> & {
|
|
41
|
+
label?: ReactNode;
|
|
42
|
+
value?: string[];
|
|
43
|
+
defaultValue?: string[];
|
|
44
|
+
onValueChange?: (value: string[], details: TagsInputValueChangeDetails) => void;
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
variant?: 'subtle' | 'accent' | 'outline';
|
|
47
|
+
classes?: Classes<'root' | 'label' | 'control' | 'tags' | 'item' | 'itemPreview' | 'itemText' | 'itemDeleteTrigger' | 'itemInput' | 'input' | 'clearTrigger'>;
|
|
48
|
+
};
|
|
49
|
+
declare function TagsInputShortcut({ label, value, defaultValue, onValueChange, placeholder: placeholderProp, variant, max, delimiter, validate, allowDuplicates, addOnPaste, editable, name, disabled, className, classes, ...rest }: TagsInputProps): import("react").JSX.Element;
|
|
50
|
+
export declare const TagsInput: typeof TagsInputShortcut & {
|
|
51
|
+
Root: import("react").ForwardRefExoticComponent<Ark.RootProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
52
|
+
RootProvider: import("react").ForwardRefExoticComponent<Ark.RootProviderProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
53
|
+
Label: import("react").ForwardRefExoticComponent<Ark.LabelProps & import("react").RefAttributes<HTMLLabelElement>>;
|
|
54
|
+
Control: import("react").ForwardRefExoticComponent<Ark.ControlProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
55
|
+
Input: import("react").ForwardRefExoticComponent<Ark.InputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
56
|
+
Item: import("react").ForwardRefExoticComponent<Ark.ItemProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
57
|
+
ItemPreview: import("react").ForwardRefExoticComponent<Ark.ItemPreviewProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
58
|
+
ItemText: import("react").ForwardRefExoticComponent<Ark.ItemTextProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
59
|
+
ItemDeleteTrigger: import("react").ForwardRefExoticComponent<Ark.ItemDeleteTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
60
|
+
ItemInput: import("react").ForwardRefExoticComponent<Ark.ItemInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
61
|
+
ClearTrigger: import("react").ForwardRefExoticComponent<Ark.ClearTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
62
|
+
HiddenInput: import("react").ForwardRefExoticComponent<Ark.HiddenInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
63
|
+
Context: (props: Ark.ContextProps) => ReactNode;
|
|
64
|
+
Tags: {
|
|
65
|
+
(props: import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>): import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
|
|
66
|
+
displayName: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A tags input: type and press Enter (or a comma) to add a tag; Backspace
|
|
4
|
+
* removes the last; double-click a tag to edit it.
|
|
5
|
+
*
|
|
6
|
+
* <TagsInput label="Topics" value={topics} onValueChange={setTopics} placeholder="Add a topic…" />
|
|
7
|
+
*
|
|
8
|
+
* - `value`, `defaultValue`: arrays of strings; `onValueChange`: called with the new array
|
|
9
|
+
* - `max`: the most tags allowed
|
|
10
|
+
* - `variant`: the tags' look: 'subtle', 'accent' or 'outline'
|
|
11
|
+
* - `editable`: false to stop editing tags in place
|
|
12
|
+
* - `delimiter`: typing it adds a tag too (',')
|
|
13
|
+
* - `validate`: function({ inputValue, value }) returning whether to add it; a
|
|
14
|
+
* rejected tag stays in the box to fix, and the field flashes red
|
|
15
|
+
* - `allowDuplicates`: let the same tag in twice
|
|
16
|
+
* - `addOnPaste`: split pasted text by the delimiter into tags (on by default)
|
|
17
|
+
* - `name`: for plain form posts (comma-separated)
|
|
18
|
+
*
|
|
19
|
+
* By hand, with Ark's parts:
|
|
20
|
+
*
|
|
21
|
+
* <TagsInput.Root>
|
|
22
|
+
* <TagsInput.Label>Topics</TagsInput.Label>
|
|
23
|
+
* <TagsInput.Control>
|
|
24
|
+
* <TagsInput.Context>{(api) => api.value.map((tag, index) => (
|
|
25
|
+
* <TagsInput.Item key={index} index={index} value={tag}>
|
|
26
|
+
* <TagsInput.ItemPreview><TagsInput.ItemText>{tag}</TagsInput.ItemText><TagsInput.ItemDeleteTrigger>×</TagsInput.ItemDeleteTrigger></TagsInput.ItemPreview>
|
|
27
|
+
* <TagsInput.ItemInput />
|
|
28
|
+
* </TagsInput.Item>
|
|
29
|
+
* ))}</TagsInput.Context>
|
|
30
|
+
* <TagsInput.Input />
|
|
31
|
+
* </TagsInput.Control>
|
|
32
|
+
* <TagsInput.HiddenInput />
|
|
33
|
+
* </TagsInput.Root>
|
|
34
|
+
*/
|
|
35
|
+
import { useRef, useState } from 'react';
|
|
36
|
+
import { TagsInput as Ark, useTagsInput } from '@ark-ui/react/tags-input';
|
|
37
|
+
import { useFieldContext } from '@ark-ui/react/field';
|
|
38
|
+
import 'iconify-icon';
|
|
39
|
+
import { cx, partOf, partsOf, useControllable, withParts } from '../utils.js';
|
|
40
|
+
import { useMessages } from '../format/LocaleProvider.js';
|
|
41
|
+
import '../theme.css';
|
|
42
|
+
import './tags-input.css';
|
|
43
|
+
function TagsInputShortcut({ label = null, value, defaultValue, onValueChange, placeholder: placeholderProp, variant = 'subtle', max = Infinity, delimiter = ',', validate, allowDuplicates = false, addOnPaste = true, editable = true, name, disabled, className, classes, ...rest }) {
|
|
44
|
+
const t = useMessages();
|
|
45
|
+
const placeholder = placeholderProp ?? t.tagsInput.placeholder;
|
|
46
|
+
const field = useFieldContext();
|
|
47
|
+
const part = partsOf('tags-input', classes);
|
|
48
|
+
const [tags, setTags] = useControllable(value, defaultValue ?? [], undefined);
|
|
49
|
+
const input = useRef(null);
|
|
50
|
+
// What was typed, to put back when `validate` turns it down (Zag clears it).
|
|
51
|
+
const typed = useRef('');
|
|
52
|
+
const [rejected, setRejected] = useState(false);
|
|
53
|
+
const props = {
|
|
54
|
+
value: tags, max, delimiter, allowDuplicates, addOnPaste, editable, blurBehavior: 'add',
|
|
55
|
+
...(validate && { validate }), ...(name && { name }), ...(disabled !== undefined && { disabled }),
|
|
56
|
+
...(t.zag.tagsInput && { translations: t.zag.tagsInput }),
|
|
57
|
+
onInputValueChange: (details) => { if (details.inputValue)
|
|
58
|
+
typed.current = details.inputValue; },
|
|
59
|
+
onValueInvalid: () => {
|
|
60
|
+
const text = typed.current;
|
|
61
|
+
// Zag clears the box in the next frame; put the text back after that.
|
|
62
|
+
requestAnimationFrame(() => requestAnimationFrame(() => api.current.setInputValue(text)));
|
|
63
|
+
setRejected(true);
|
|
64
|
+
setTimeout(() => setRejected(false), 700);
|
|
65
|
+
},
|
|
66
|
+
onValueChange: (details) => {
|
|
67
|
+
// Zag clears its own copy of the typed text after adding a tag, but not
|
|
68
|
+
// always the box itself (after a delimiter), so clear that too.
|
|
69
|
+
if (details.value.length > tags.length && input.current)
|
|
70
|
+
input.current.value = '';
|
|
71
|
+
setTags(details.value);
|
|
72
|
+
onValueChange?.(details.value, details);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const tagsInput = useTagsInput(props);
|
|
76
|
+
const api = useRef(tagsInput);
|
|
77
|
+
api.current = tagsInput;
|
|
78
|
+
return (_jsxs(Ark.RootProvider, { value: tagsInput, ...rest, ...part('root'), className: cx(className, classes?.root), "data-variant": variant, children: [label && !field && _jsx(Ark.Label, { ...part('label'), children: label }), _jsxs(Ark.Control, { ...part('control'), "data-rejected": rejected || undefined, children: [_jsxs("div", { ...part('tags'), children: [tagsInput.value.map((tag, index) => (_jsxs(Ark.Item, { index: index, value: tag, ...part('item'), children: [_jsxs(Ark.ItemPreview, { ...part('item-preview'), children: [_jsx(Ark.ItemText, { ...part('item-text'), children: tag }), _jsx(Ark.ItemDeleteTrigger, { ...part('item-delete-trigger'), children: _jsx("iconify-icon", { icon: "lucide:x" }) })] }), _jsx(Ark.ItemInput, { ...part('item-input') })] }, `${index}:${tag}`))), _jsx(Ark.Input, { ref: input, ...part('input'), placeholder: tagsInput.value.length ? '' : placeholder })] }), _jsx(Ark.ClearTrigger, { ...part('clear-trigger'), children: _jsx("iconify-icon", { icon: "lucide:x" }) })] }), _jsx(Ark.HiddenInput, {})] }));
|
|
79
|
+
}
|
|
80
|
+
export const TagsInput = withParts(TagsInputShortcut, {
|
|
81
|
+
Root: Ark.Root,
|
|
82
|
+
RootProvider: Ark.RootProvider,
|
|
83
|
+
Label: Ark.Label,
|
|
84
|
+
Control: Ark.Control,
|
|
85
|
+
Input: Ark.Input,
|
|
86
|
+
Item: Ark.Item,
|
|
87
|
+
ItemPreview: Ark.ItemPreview,
|
|
88
|
+
ItemText: Ark.ItemText,
|
|
89
|
+
ItemDeleteTrigger: Ark.ItemDeleteTrigger,
|
|
90
|
+
ItemInput: Ark.ItemInput,
|
|
91
|
+
ClearTrigger: Ark.ClearTrigger,
|
|
92
|
+
HiddenInput: Ark.HiddenInput,
|
|
93
|
+
Context: Ark.Context,
|
|
94
|
+
Tags: partOf('tags-input', 'tags'),
|
|
95
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* Tag parts are matched by Zag's names (item-preview, item-delete-trigger…);
|
|
2
|
+
`classes` uses the short ones (tag, tagRemove…). */
|
|
3
|
+
@layer ui {
|
|
4
|
+
[data-scope=tags-input][data-part=root] { display: block; min-width: 0; color: var(--ui-text); font-family: var(--ui-font);
|
|
5
|
+
--tag-bg: var(--ui-hover); --tag-text: var(--ui-text); --tag-border: transparent;
|
|
6
|
+
&[data-variant=accent] { --tag-bg: var(--ui-accent-soft); --tag-text: var(--ui-accent-soft-text); }
|
|
7
|
+
&[data-variant=outline] { --tag-bg: var(--ui-surface); --tag-border: var(--ui-border); } }
|
|
8
|
+
[data-scope=tags-input][data-part=label] { display: block; font-size: 14px; font-weight: 500; margin-bottom: 0.375rem; }
|
|
9
|
+
[data-scope=tags-input][data-part=control] { display: flex; align-items: center; gap: 0.25rem; min-height: var(--ui-control-height);
|
|
10
|
+
padding: 0.25rem 0.25rem 0.25rem 0.375rem; box-sizing: border-box; background: var(--ui-surface);
|
|
11
|
+
border: 1px solid var(--ui-border); border-radius: var(--ui-radius); font-size: 14px; cursor: text;
|
|
12
|
+
&:focus-within { border-color: var(--ui-ring); outline: 2px solid var(--ui-ring-soft); }
|
|
13
|
+
&[data-invalid], &:has([aria-invalid=true]), &[data-rejected] { border-color: var(--ui-danger); }
|
|
14
|
+
&[data-rejected] { animation: ui-shake 300ms; }
|
|
15
|
+
&[data-disabled] { opacity: 0.5; } }
|
|
16
|
+
[data-scope=tags-input][data-part=tags] { display: flex; flex-wrap: wrap; align-items: center; flex: 1; min-width: 0; gap: 0.375rem; }
|
|
17
|
+
[data-scope=tags-input][data-part=item-preview] { display: flex; align-items: center; gap: 0.25rem; height: 1.625rem;
|
|
18
|
+
padding: 0 0.25rem 0 0.625rem; box-sizing: border-box; border-radius: calc(var(--ui-radius) - 2px);
|
|
19
|
+
background: var(--tag-bg); color: var(--tag-text); border: 1px solid var(--tag-border);
|
|
20
|
+
font-size: 13px; font-weight: 500; line-height: 1;
|
|
21
|
+
&[data-highlighted] { outline: 2px solid var(--ui-ring); } }
|
|
22
|
+
[data-scope=tags-input][data-part=item-text] { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 12rem; }
|
|
23
|
+
[data-scope=tags-input][data-part=item-delete-trigger] { display: grid; place-items: center; width: 1.125rem; height: 1.125rem; padding: 0;
|
|
24
|
+
border: none; background: transparent; border-radius: 4px; color: inherit; opacity: 0.6; cursor: pointer;
|
|
25
|
+
&:hover { opacity: 1; background: rgba(0, 0, 0, 0.08); } }
|
|
26
|
+
[data-scope=tags-input][data-part=item-input] { height: 1.625rem; width: 7rem; padding: 0 0.5rem; box-sizing: border-box;
|
|
27
|
+
border: 1px solid var(--ui-ring); border-radius: calc(var(--ui-radius) - 2px); background: var(--ui-surface);
|
|
28
|
+
color: inherit; font-family: inherit; font-size: 13px; outline: none; }
|
|
29
|
+
[data-scope=tags-input][data-part=input] { flex: 1; min-width: 4rem; height: 1.625rem; padding: 0 0.25rem; border: none;
|
|
30
|
+
background: transparent; outline: none; color: inherit; font: inherit;
|
|
31
|
+
&::placeholder { color: var(--ui-muted); } }
|
|
32
|
+
[data-scope=tags-input][data-part=clear-trigger] { display: grid; place-items: center; flex-shrink: 0; align-self: center;
|
|
33
|
+
width: 1.75rem; height: 1.75rem; border: none; background: transparent; border-radius: 4px; color: var(--ui-muted); cursor: pointer;
|
|
34
|
+
&:hover { background: var(--ui-hover); color: var(--ui-text); } }
|
|
35
|
+
@keyframes ui-shake { 25% { translate: -3px 0; } 75% { translate: 3px 0; } }
|
|
36
|
+
@media (prefers-reduced-motion: reduce) { [data-scope=tags-input][data-part=control][data-rejected] { animation: none; } }
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A textarea that grows with its content, from `rows` lines up to `maxRows`
|
|
3
|
+
* (then it scrolls). Other attributes go on the <textarea>.
|
|
4
|
+
*
|
|
5
|
+
* <Textarea value={notes} onValueChange={setNotes} rows={3} maxRows={8} placeholder="Notes" />
|
|
6
|
+
*
|
|
7
|
+
* - `label`: a label above it (skipped inside a Field)
|
|
8
|
+
* - `value`, `defaultValue`; `onValueChange`: called with the new text
|
|
9
|
+
* - `rows`: the starting height in lines
|
|
10
|
+
* - `maxRows`: grow up to this many lines, then scroll
|
|
11
|
+
* - `autogrow`: false for a fixed height the user can resize
|
|
12
|
+
*/
|
|
13
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
14
|
+
import { type Classes } from '../utils.js';
|
|
15
|
+
import '../theme.css';
|
|
16
|
+
import './textarea.css';
|
|
17
|
+
export type TextareaProps = ComponentProps<'textarea'> & {
|
|
18
|
+
label?: ReactNode;
|
|
19
|
+
onValueChange?: (value: string) => void;
|
|
20
|
+
maxRows?: number | null;
|
|
21
|
+
autogrow?: boolean;
|
|
22
|
+
classes?: Classes<'root' | 'label' | 'control' | 'textarea'>;
|
|
23
|
+
};
|
|
24
|
+
export declare function Textarea({ label, rows, maxRows, autogrow, onValueChange, onChange, value, className, classes, ref, ...rest }: TextareaProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A textarea that grows with its content, from `rows` lines up to `maxRows`
|
|
4
|
+
* (then it scrolls). Other attributes go on the <textarea>.
|
|
5
|
+
*
|
|
6
|
+
* <Textarea value={notes} onValueChange={setNotes} rows={3} maxRows={8} placeholder="Notes" />
|
|
7
|
+
*
|
|
8
|
+
* - `label`: a label above it (skipped inside a Field)
|
|
9
|
+
* - `value`, `defaultValue`; `onValueChange`: called with the new text
|
|
10
|
+
* - `rows`: the starting height in lines
|
|
11
|
+
* - `maxRows`: grow up to this many lines, then scroll
|
|
12
|
+
* - `autogrow`: false for a fixed height the user can resize
|
|
13
|
+
*/
|
|
14
|
+
import { useCallback, useId, useLayoutEffect, useRef } from 'react';
|
|
15
|
+
import { Field, useFieldContext } from '@ark-ui/react/field';
|
|
16
|
+
import { cx, partsOf } from '../utils.js';
|
|
17
|
+
import '../theme.css';
|
|
18
|
+
import './textarea.css';
|
|
19
|
+
export function Textarea({ label = null, rows = 3, maxRows = null, autogrow = true, onValueChange, onChange, value, className, classes, ref, ...rest }) {
|
|
20
|
+
const field = useFieldContext();
|
|
21
|
+
const part = partsOf('textarea', classes);
|
|
22
|
+
const ownId = useId();
|
|
23
|
+
const id = rest.id ?? `${ownId}-textarea`;
|
|
24
|
+
const el = useRef(null);
|
|
25
|
+
// Fits the height to the content, capped at maxRows.
|
|
26
|
+
const fit = useCallback(() => {
|
|
27
|
+
const area = el.current;
|
|
28
|
+
if (!autogrow || !area)
|
|
29
|
+
return;
|
|
30
|
+
const line = parseFloat(getComputedStyle(area).lineHeight);
|
|
31
|
+
const max = maxRows ? line * maxRows : Infinity;
|
|
32
|
+
area.style.height = 'auto';
|
|
33
|
+
area.style.height = `${Math.min(area.scrollHeight, max)}px`;
|
|
34
|
+
area.style.overflowY = area.scrollHeight > max ? 'auto' : 'hidden';
|
|
35
|
+
}, [autogrow, maxRows]);
|
|
36
|
+
useLayoutEffect(fit, [fit, value]);
|
|
37
|
+
function change(e) {
|
|
38
|
+
onChange?.(e);
|
|
39
|
+
onValueChange?.(e.target.value);
|
|
40
|
+
fit();
|
|
41
|
+
}
|
|
42
|
+
// Both refs: ours, for the height, and the caller's.
|
|
43
|
+
const refs = (node) => {
|
|
44
|
+
el.current = node;
|
|
45
|
+
if (typeof ref === 'function')
|
|
46
|
+
ref(node);
|
|
47
|
+
else if (ref)
|
|
48
|
+
ref.current = node;
|
|
49
|
+
};
|
|
50
|
+
return (_jsxs("div", { ...part('root'), className: cx(className, classes?.root), children: [label && !field && _jsx("label", { ...part('label'), htmlFor: id, children: label }), _jsx("div", { ...part('control'), children: _jsx(Field.Textarea, { ref: refs, rows: rows, id: field ? undefined : id, value: value, ...rest, ...part('textarea'), "data-autogrow": autogrow || undefined, onChange: change }) })] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@layer ui {
|
|
2
|
+
[data-scope=textarea][data-part=root] { display: block; min-width: 0; color: var(--ui-text); font-family: var(--ui-font); }
|
|
3
|
+
[data-scope=textarea][data-part=label] { display: block; font-size: 14px; font-weight: 500; margin-bottom: 0.375rem; }
|
|
4
|
+
[data-scope=textarea][data-part=control] { display: block; min-width: 0; padding: 0.5rem 0.75rem; box-sizing: border-box;
|
|
5
|
+
background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius);
|
|
6
|
+
color: var(--ui-text); font-family: var(--ui-font); font-size: 14px;
|
|
7
|
+
&:focus-within { border-color: var(--ui-ring); outline: 2px solid var(--ui-ring-soft); }
|
|
8
|
+
&:has([aria-invalid=true]) { border-color: var(--ui-danger); }
|
|
9
|
+
&:has(:disabled) { opacity: 0.5; } }
|
|
10
|
+
[data-scope=textarea][data-part=textarea] { display: block; width: 100%; margin: 0; padding: 0; border: none; background: transparent;
|
|
11
|
+
outline: none; color: inherit; font: inherit; line-height: 1.5; resize: vertical;
|
|
12
|
+
&::placeholder { color: var(--ui-muted); }
|
|
13
|
+
&[data-autogrow] { resize: none; overflow: hidden; } }
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A light, dark and system switch, driving `colorScheme`, so every toggle on
|
|
3
|
+
* the page stays in step and the choice is saved.
|
|
4
|
+
*
|
|
5
|
+
* <ThemeToggle />
|
|
6
|
+
* <ThemeToggle variant="menu" />
|
|
7
|
+
* <ThemeToggle mobile="menu" />
|
|
8
|
+
* <ThemeToggle variant="toggle" />
|
|
9
|
+
*
|
|
10
|
+
* - `variant`: 'segmented' (default: icon-only buttons with tooltips), 'menu'
|
|
11
|
+
* (one button showing the current choice, opening a menu of all of them) or
|
|
12
|
+
* 'toggle' (one button flipping between light and dark)
|
|
13
|
+
* - `mobile`: the variant below `breakpoint` (e.g. 'menu' where space is tight)
|
|
14
|
+
* - `breakpoint`: the width in px `mobile` applies under (768)
|
|
15
|
+
* - `options`: which schemes to offer, in order (system, light, dark)
|
|
16
|
+
* - `system`: false leaves out 'system' whatever `options` says
|
|
17
|
+
* - `labels`: the tooltips and accessible names, `{ light, dark, system }`
|
|
18
|
+
* - `label`: names the group for assistive tech
|
|
19
|
+
* - `size`: 'sm', 'md' (default) or 'lg', for every variant
|
|
20
|
+
* - `placement`: where the menu variant opens ('bottom-end', or 'bottom' with `iconOnly`)
|
|
21
|
+
* - `keepOpen`: the menu variant stays open after a choice
|
|
22
|
+
* - `iconOnly`: the menu variant lists just the icons, the current one highlighted
|
|
23
|
+
* - `onChange`: called with 'light', 'dark' or 'system'
|
|
24
|
+
*/
|
|
25
|
+
import { type ComponentProps } from 'react';
|
|
26
|
+
import { Menu } from '../menu/Menu.js';
|
|
27
|
+
import 'iconify-icon';
|
|
28
|
+
import { type Scheme } from '../color-scheme.js';
|
|
29
|
+
import { type Classes } from '../utils.js';
|
|
30
|
+
import '../theme.css';
|
|
31
|
+
import './theme-toggle.css';
|
|
32
|
+
type Variant = 'segmented' | 'menu' | 'toggle';
|
|
33
|
+
export type ThemeToggleProps = Omit<ComponentProps<'div'>, 'onChange'> & {
|
|
34
|
+
variant?: Variant;
|
|
35
|
+
mobile?: Variant | null;
|
|
36
|
+
breakpoint?: number;
|
|
37
|
+
options?: Scheme[];
|
|
38
|
+
system?: boolean;
|
|
39
|
+
labels?: Record<Scheme, string>;
|
|
40
|
+
label?: string;
|
|
41
|
+
size?: 'sm' | 'md' | 'lg';
|
|
42
|
+
placement?: NonNullable<Parameters<typeof Menu>[0]['placement']> | null;
|
|
43
|
+
keepOpen?: boolean;
|
|
44
|
+
iconOnly?: boolean;
|
|
45
|
+
onChange?: (scheme: Scheme) => void;
|
|
46
|
+
classes?: Classes<'root' | 'trigger' | 'triggerIcon'>;
|
|
47
|
+
};
|
|
48
|
+
export declare function ThemeToggle({ variant, mobile, breakpoint, options, system, labels: labelsProp, label: labelProp, placement, size, keepOpen, iconOnly, onChange, className, classes, ...rest }: ThemeToggleProps): import("react").JSX.Element;
|
|
49
|
+
export {};
|