@surflane/ui 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-shell.d.ts +10 -0
- package/dist/app-shell.js +7 -1
- package/dist/bar-list.d.ts +31 -0
- package/dist/bar-list.js +25 -0
- package/dist/bottom-navigation.d.ts +6 -2
- package/dist/bottom-navigation.js +2 -2
- package/dist/chart.d.ts +56 -3
- package/dist/chart.js +266 -48
- package/dist/css/bar-list.css +9 -0
- package/dist/css/chart.css +2 -0
- package/dist/css/dashboard.css +20 -0
- package/dist/css/rules/alert-dialog.css +4 -0
- package/dist/css/rules/app-shell.css +10 -0
- package/dist/css/rules/bar-list.css +101 -0
- package/dist/css/rules/bottom-navigation.css +16 -0
- package/dist/css/rules/card.css +5 -0
- package/dist/css/rules/chart.css +74 -3
- package/dist/css/rules/dashboard.css +66 -0
- package/dist/css/rules/date-picker.css +10 -3
- package/dist/css/rules/dialog.css +11 -0
- package/dist/css/rules/field-control.css +7 -0
- package/dist/css/rules/filter-bar.css +8 -1
- package/dist/css/rules/input-group.css +9 -0
- package/dist/css/rules/otp-field.css +7 -4
- package/dist/css/rules/panel.css +9 -1
- package/dist/css/rules/select.css +25 -1
- package/dist/css/rules/sizing.css +10 -0
- package/dist/css/rules/slider.css +4 -2
- package/dist/css/rules/stat.css +57 -0
- package/dist/css/rules/time-range-picker.css +14 -0
- package/dist/css/styles.css +3 -0
- package/dist/css/time-range-picker.css +14 -0
- package/dist/dashboard.d.ts +46 -0
- package/dist/dashboard.js +35 -0
- package/dist/icon.d.ts +10 -0
- package/dist/icon.js +11 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/select.d.ts +33 -6
- package/dist/select.js +94 -21
- package/dist/stat.d.ts +20 -0
- package/dist/stat.js +15 -1
- package/dist/time-range-picker.d.ts +42 -0
- package/dist/time-range-picker.js +69 -0
- package/package.json +18 -3
package/dist/icon.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ declare const shapes: {
|
|
|
23
23
|
'arrow-back': React.JSX.Element;
|
|
24
24
|
'arrow-up': React.JSX.Element;
|
|
25
25
|
'arrow-down': React.JSX.Element;
|
|
26
|
+
refresh: React.JSX.Element;
|
|
26
27
|
sort: React.JSX.Element;
|
|
27
28
|
'external-link': React.JSX.Element;
|
|
28
29
|
copy: React.JSX.Element;
|
|
@@ -31,6 +32,15 @@ declare const shapes: {
|
|
|
31
32
|
calendar: React.JSX.Element;
|
|
32
33
|
user: React.JSX.Element;
|
|
33
34
|
settings: React.JSX.Element;
|
|
35
|
+
server: React.JSX.Element;
|
|
36
|
+
database: React.JSX.Element;
|
|
37
|
+
disk: React.JSX.Element;
|
|
38
|
+
chip: React.JSX.Element;
|
|
39
|
+
memory: React.JSX.Element;
|
|
40
|
+
network: React.JSX.Element;
|
|
41
|
+
bolt: React.JSX.Element;
|
|
42
|
+
token: React.JSX.Element;
|
|
43
|
+
thermometer: React.JSX.Element;
|
|
34
44
|
};
|
|
35
45
|
export type IconName = keyof typeof shapes;
|
|
36
46
|
export declare const iconNames: readonly IconName[];
|
package/dist/icon.js
CHANGED
|
@@ -23,6 +23,7 @@ const shapes = {
|
|
|
23
23
|
'arrow-back': _jsx("path", { d: 'M20 12H4m7-7-7 7 7 7' }),
|
|
24
24
|
'arrow-up': _jsx("path", { d: 'M12 19V5m-6 6 6-6 6 6' }),
|
|
25
25
|
'arrow-down': _jsx("path", { d: 'M12 5v14m-6-6 6 6 6-6' }),
|
|
26
|
+
refresh: _jsx("path", { d: 'M20 12a8 8 0 1 1-2.34-5.66M20 4v4h-4' }),
|
|
26
27
|
sort: _jsx("path", { d: 'm8 9 4-4 4 4m-8 6 4 4 4-4' }),
|
|
27
28
|
'external-link': (_jsx(_Fragment, { children: _jsx("path", { d: 'M14 3h7v7m0-7L10 14M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5' }) })),
|
|
28
29
|
copy: (_jsxs(_Fragment, { children: [_jsx("rect", { x: '8', y: '8', width: '12', height: '13', rx: '2' }), _jsx("path", { d: 'M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3' })] })),
|
|
@@ -31,6 +32,16 @@ const shapes = {
|
|
|
31
32
|
calendar: (_jsxs(_Fragment, { children: [_jsx("rect", { x: '3', y: '5', width: '18', height: '16', rx: '3' }), _jsx("path", { d: 'M7 3v4m10-4v4M3 11h18m-12 4h2' })] })),
|
|
32
33
|
user: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: '12', cy: '8', r: '4' }), _jsx("path", { d: 'M4 21v-2a8 8 0 0 1 16 0v2' })] })),
|
|
33
34
|
settings: (_jsx(_Fragment, { children: _jsx("path", { d: 'M4 6h16M4 12h16M4 18h16M9 3v6m6 0v6m-6 0v6' }) })),
|
|
35
|
+
// What a product runs on: machines, storage, links, power and model tokens.
|
|
36
|
+
server: (_jsxs(_Fragment, { children: [_jsx("rect", { x: '3', y: '4', width: '18', height: '7', rx: '2' }), _jsx("rect", { x: '3', y: '13', width: '18', height: '7', rx: '2' }), _jsx("path", { d: 'M7 7.5h3m-3 9h3' })] })),
|
|
37
|
+
database: (_jsxs(_Fragment, { children: [_jsx("ellipse", { cx: '12', cy: '6', rx: '8', ry: '3' }), _jsx("path", { d: 'M4 6v12c0 1.66 3.58 3 8 3s8-1.34 8-3V6M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3' })] })),
|
|
38
|
+
disk: (_jsxs(_Fragment, { children: [_jsx("path", { d: 'M3 14 5.6 5.4A2 2 0 0 1 7.5 4h9a2 2 0 0 1 1.9 1.4L21 14' }), _jsx("rect", { x: '3', y: '14', width: '18', height: '6', rx: '2' }), _jsx("path", { d: 'M7 17h2' })] })),
|
|
39
|
+
chip: (_jsxs(_Fragment, { children: [_jsx("rect", { x: '6', y: '6', width: '12', height: '12', rx: '2' }), _jsx("rect", { x: '9.5', y: '9.5', width: '5', height: '5', rx: '1' }), _jsx("path", { d: 'M9.5 2.5V6m5-3.5V6m-5 12v3.5m5-3.5v3.5M2.5 9.5H6m-3.5 5H6m12-5h3.5M18 14.5h3.5' })] })),
|
|
40
|
+
memory: (_jsxs(_Fragment, { children: [_jsx("rect", { x: '2', y: '7', width: '20', height: '10', rx: '2' }), _jsx("path", { d: 'M6 17v3m4-3v3m4-3v3m4-3v3M7 10.5v3m5-3v3m5-3v3' })] })),
|
|
41
|
+
network: (_jsxs(_Fragment, { children: [_jsx("rect", { x: '9', y: '3', width: '6', height: '5', rx: '1' }), _jsx("rect", { x: '3', y: '16', width: '6', height: '5', rx: '1' }), _jsx("rect", { x: '15', y: '16', width: '6', height: '5', rx: '1' }), _jsx("path", { d: 'M12 8v4M6 16v-4h12v4' })] })),
|
|
42
|
+
bolt: _jsx("path", { d: 'M13 2.5 4.5 13.5H12l-1 8 8.5-11H12l1-8Z' }),
|
|
43
|
+
token: (_jsxs(_Fragment, { children: [_jsx("path", { d: 'm12 3 7.8 4.5v9L12 21l-7.8-4.5v-9L12 3Z' }), _jsx("circle", { cx: '12', cy: '12', r: '3' })] })),
|
|
44
|
+
thermometer: (_jsxs(_Fragment, { children: [_jsx("path", { d: 'M14 14.5V5a2 2 0 0 0-4 0v9.5a4 4 0 1 0 4 0Z' }), _jsx("path", { d: 'M12 9v7.5' })] })),
|
|
34
45
|
};
|
|
35
46
|
export const iconNames = Object.keys(shapes);
|
|
36
47
|
// These point along the reading direction, so they mirror in right-to-left text.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { Button, type ButtonVariant, type ButtonSize, type ButtonProps, LinkButton, type LinkButtonProps, } from './button.js';
|
|
2
2
|
export { Toolbar, ToolbarButton, ToolbarGroup, ToolbarInput, ToolbarLink, ToolbarRoot, ToolbarSeparator, } from './toolbar.js';
|
|
3
3
|
export { Input } from './input.js';
|
|
4
|
-
export { Select, type SelectOption, type SelectGroup, type SelectProps, } from './select.js';
|
|
4
|
+
export { Select, type SelectOption, type SelectGroup, type SelectProps, type SelectMultipleProps, type SelectMultipleLabels, } from './select.js';
|
|
5
5
|
export { NativeSelect, NativeSelectOption, NativeSelectOptGroup, } from './native-select.js';
|
|
6
6
|
export { Field, FieldControl, FieldDescription, FieldError, FieldItem, FieldLabel, FieldRoot, FieldValidity, } from './field.js';
|
|
7
7
|
export { Fieldset, FieldsetLegend, FieldsetRoot } from './fieldset.js';
|
|
@@ -45,8 +45,11 @@ export { Table, TableContainer, TableRoot, TableCaption, TableHead, TableBody, T
|
|
|
45
45
|
export { CodeBlock, type CodeBlockProps, type CodeBlockLabels } from './code.js';
|
|
46
46
|
export { SyntaxHighlighter, syntaxLanguages, type SyntaxHighlighterProps, } from './syntax-highlighter.js';
|
|
47
47
|
export { List, ListRoot, ListItem, ListContent, ListTitle, ListDescription, ListActions, } from './list.js';
|
|
48
|
-
export { Stat, StatRoot, StatLabel, StatValue, StatChange, StatCaption, type StatChangeProps, type StatDirection, } from './stat.js';
|
|
48
|
+
export { Stat, StatRoot, StatLabel, StatValue, StatChange, StatCaption, StatStatus, StatGroup, type StatChangeProps, type StatStatusProps, type StatGroupProps, type StatDirection, } from './stat.js';
|
|
49
49
|
export { Sparkline, type SparklineProps, type SparklineTrend, } from './sparkline.js';
|
|
50
|
+
export { BarList, type BarListProps, type BarListItem } from './bar-list.js';
|
|
51
|
+
export { Dashboard, DashboardRoot, DashboardPanel, type DashboardRootProps, type DashboardPanelProps, type DashboardPanelLabels, } from './dashboard.js';
|
|
52
|
+
export { TimeRangePicker, timeRangeOptions, refreshIntervalOptions, type TimeRangePickerProps, type TimeRangePickerLabels, type TimeRangeOption, } from './time-range-picker.js';
|
|
50
53
|
export { ScrollArea } from './scroll-area.js';
|
|
51
54
|
export { Separator } from './separator.js';
|
|
52
55
|
export { Progress, ProgressIndicator, ProgressLabel, ProgressRoot, ProgressTrack, ProgressValue, } from './progress.js';
|
|
@@ -63,6 +66,6 @@ export { Icon, iconNames, type IconName, type IconProps } from './icon.js';
|
|
|
63
66
|
export { SizeProvider, type ComponentSize, type ComponentSizeAlias, } from './sizing.js';
|
|
64
67
|
export { DirectionProvider } from '@base-ui/react/direction-provider';
|
|
65
68
|
export { Slot } from './slot.js';
|
|
66
|
-
export { AppShell, AppShellRoot, AppShellHeader, AppShellSidebar, AppShellSidebarToggle, type AppShellRootProps, type AppShellSidebarToggleProps, type AppShellSidebarToggleLabels, AppShellBrand, type AppShellBrandProps, AppShellNav, type AppShellNavProps, AppShellContent, AppShellPage, AppShellFooter, AppShellLabel, AppShellLink, type AppShellLinkProps, } from './app-shell.js';
|
|
69
|
+
export { AppShell, AppShellRoot, AppShellHeader, AppShellSidebar, AppShellSidebarToggle, AppShellMenuTrigger, type AppShellRootProps, type AppShellSidebarToggleProps, type AppShellSidebarToggleLabels, type AppShellMenuTriggerProps, AppShellBrand, type AppShellBrandProps, AppShellNav, type AppShellNavProps, AppShellContent, AppShellPage, AppShellFooter, AppShellLabel, AppShellLink, type AppShellLinkProps, } from './app-shell.js';
|
|
67
70
|
export { PageHeader } from './page-header.js';
|
|
68
71
|
export { FilterBar, FilterBarRoot, FilterBarControls, FilterBarField, FilterBarActions, FilterBarSummary, } from './filter-bar.js';
|
package/dist/index.js
CHANGED
|
@@ -55,8 +55,11 @@ export { Table, TableContainer, TableRoot, TableCaption, TableHead, TableBody, T
|
|
|
55
55
|
export { CodeBlock } from './code.js';
|
|
56
56
|
export { SyntaxHighlighter, syntaxLanguages, } from './syntax-highlighter.js';
|
|
57
57
|
export { List, ListRoot, ListItem, ListContent, ListTitle, ListDescription, ListActions, } from './list.js';
|
|
58
|
-
export { Stat, StatRoot, StatLabel, StatValue, StatChange, StatCaption, } from './stat.js';
|
|
58
|
+
export { Stat, StatRoot, StatLabel, StatValue, StatChange, StatCaption, StatStatus, StatGroup, } from './stat.js';
|
|
59
59
|
export { Sparkline, } from './sparkline.js';
|
|
60
|
+
export { BarList } from './bar-list.js';
|
|
61
|
+
export { Dashboard, DashboardRoot, DashboardPanel, } from './dashboard.js';
|
|
62
|
+
export { TimeRangePicker, timeRangeOptions, refreshIntervalOptions, } from './time-range-picker.js';
|
|
60
63
|
// Structure
|
|
61
64
|
export { ScrollArea } from './scroll-area.js';
|
|
62
65
|
export { Separator } from './separator.js';
|
|
@@ -77,6 +80,6 @@ export { SizeProvider, } from './sizing.js';
|
|
|
77
80
|
export { DirectionProvider } from '@base-ui/react/direction-provider';
|
|
78
81
|
export { Slot } from './slot.js';
|
|
79
82
|
// Patterns
|
|
80
|
-
export { AppShell, AppShellRoot, AppShellHeader, AppShellSidebar, AppShellSidebarToggle, AppShellBrand, AppShellNav, AppShellContent, AppShellPage, AppShellFooter, AppShellLabel, AppShellLink, } from './app-shell.js';
|
|
83
|
+
export { AppShell, AppShellRoot, AppShellHeader, AppShellSidebar, AppShellSidebarToggle, AppShellMenuTrigger, AppShellBrand, AppShellNav, AppShellContent, AppShellPage, AppShellFooter, AppShellLabel, AppShellLink, } from './app-shell.js';
|
|
81
84
|
export { PageHeader } from './page-header.js';
|
|
82
85
|
export { FilterBar, FilterBarRoot, FilterBarControls, FilterBarField, FilterBarActions, FilterBarSummary, } from './filter-bar.js';
|
package/dist/select.d.ts
CHANGED
|
@@ -10,11 +10,8 @@ export type SelectGroup = {
|
|
|
10
10
|
label: React.ReactNode;
|
|
11
11
|
options: SelectOption[];
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
type SelectCommonProps = {
|
|
14
14
|
options: (SelectOption | SelectGroup)[];
|
|
15
|
-
value?: string;
|
|
16
|
-
defaultValue?: string;
|
|
17
|
-
onValueChange?: (value: string) => void;
|
|
18
15
|
name?: string;
|
|
19
16
|
form?: string;
|
|
20
17
|
required?: boolean;
|
|
@@ -24,12 +21,42 @@ export type SelectProps = {
|
|
|
24
21
|
inputRef?: React.Ref<HTMLInputElement>;
|
|
25
22
|
triggerRef?: React.Ref<HTMLButtonElement>;
|
|
26
23
|
size?: ComponentSizeAlias;
|
|
24
|
+
/** fill takes its container's width, as a form field does. content fits the
|
|
25
|
+
* longest option, as a native select does, so it sits beside other controls in
|
|
26
|
+
* a header or toolbar and keeps its width when the value changes. */
|
|
27
|
+
width?: 'fill' | 'content';
|
|
27
28
|
'aria-label'?: string;
|
|
28
29
|
'aria-labelledby'?: string;
|
|
29
30
|
'aria-describedby'?: string;
|
|
30
31
|
'aria-invalid'?: React.AriaAttributes['aria-invalid'];
|
|
31
32
|
};
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
export type SelectProps = SelectCommonProps & {
|
|
34
|
+
multiple?: false;
|
|
35
|
+
value?: string;
|
|
36
|
+
defaultValue?: string;
|
|
37
|
+
onValueChange?: (value: string) => void;
|
|
38
|
+
};
|
|
39
|
+
/** What a multiple Select's trigger says, and its first option. */
|
|
40
|
+
export type SelectMultipleLabels = {
|
|
41
|
+
/** Every option chosen, such as "All projects"; also the option that chooses them. */
|
|
42
|
+
all: string;
|
|
43
|
+
/** Nothing chosen, such as "No projects". */
|
|
44
|
+
none: string;
|
|
45
|
+
/** Several chosen, such as "3 projects". */
|
|
46
|
+
count: (count: number) => string;
|
|
47
|
+
};
|
|
48
|
+
/** Several choices from a list, summed up on one line: a filter of projects, models
|
|
49
|
+
* or nodes. Its first option, labels.all, chooses every option or none. */
|
|
50
|
+
export type SelectMultipleProps = SelectCommonProps & {
|
|
51
|
+
multiple: true;
|
|
52
|
+
value?: readonly string[];
|
|
53
|
+
defaultValue?: readonly string[];
|
|
54
|
+
onValueChange?: (value: string[]) => void;
|
|
55
|
+
labels?: Partial<SelectMultipleLabels>;
|
|
56
|
+
};
|
|
57
|
+
/** A choice from a list, or several with multiple. The ref reaches the trigger
|
|
58
|
+
* button, as triggerRef does. */
|
|
59
|
+
export declare const Select: (allProps: (SelectProps | SelectMultipleProps) & {
|
|
34
60
|
ref?: React.Ref<HTMLButtonElement>;
|
|
35
61
|
}) => React.JSX.Element;
|
|
62
|
+
export {};
|
package/dist/select.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { Icon } from './icon.js';
|
|
5
5
|
import { useComponentSize } from './sizing.js';
|
|
@@ -7,8 +7,17 @@ import { Select as BaseSelect } from '@base-ui/react/select';
|
|
|
7
7
|
import { sizeName, cx } from './parts.js';
|
|
8
8
|
const isGroup = (item) => 'options' in item;
|
|
9
9
|
const flatten = (items) => items.flatMap((item) => (isGroup(item) ? item.options : [item]));
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const multipleLabels = {
|
|
11
|
+
all: 'All',
|
|
12
|
+
none: 'None',
|
|
13
|
+
count: (count) => `${count} selected`,
|
|
14
|
+
};
|
|
15
|
+
// The value the "all" option carries; it never reaches a form or a callback.
|
|
16
|
+
const ALL = '\u0000all';
|
|
17
|
+
/** A choice from a list, or several with multiple. The ref reaches the trigger
|
|
18
|
+
* button, as triggerRef does. */
|
|
19
|
+
export const Select = function Select(allProps) {
|
|
20
|
+
const { options: items, className, inputRef, triggerRef, size, width = 'fill', ref, multiple, value: _value, defaultValue: _defaultValue, onValueChange: _onValueChange, labels: _labels, ...props } = allProps;
|
|
12
21
|
const scheme = useComponentSize(size);
|
|
13
22
|
const options = flatten(items);
|
|
14
23
|
// One stable callback, so the trigger ref is not detached on every render.
|
|
@@ -19,11 +28,16 @@ export const Select = function Select({ options: items, value, defaultValue, onV
|
|
|
19
28
|
else if (target)
|
|
20
29
|
target.current = node;
|
|
21
30
|
}, [ref, triggerRef]);
|
|
22
|
-
const
|
|
31
|
+
const many = allProps.multiple === true ? allProps : undefined;
|
|
32
|
+
const one = allProps.multiple === true ? undefined : allProps;
|
|
33
|
+
const initialValue = many
|
|
34
|
+
? (many.defaultValue ?? [])
|
|
35
|
+
: (one.defaultValue ?? options[0]?.value ?? '');
|
|
36
|
+
const [localValue, setLocalValue] = React.useState(initialValue);
|
|
23
37
|
const nativeInput = React.useRef(null);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
const resetCallback = React.useRef(undefined);
|
|
39
|
+
resetCallback.current = allProps.onValueChange;
|
|
40
|
+
const resetKey = many ? initialValue.toString() : initialValue;
|
|
27
41
|
React.useEffect(() => {
|
|
28
42
|
const owner = nativeInput.current?.form;
|
|
29
43
|
if (!owner)
|
|
@@ -35,7 +49,9 @@ export const Select = function Select({ options: items, value, defaultValue, onV
|
|
|
35
49
|
if (event.defaultPrevented)
|
|
36
50
|
return;
|
|
37
51
|
setLocalValue(initialValue);
|
|
38
|
-
resetCallback.current?.(initialValue)
|
|
52
|
+
resetCallback.current?.((Array.isArray(initialValue)
|
|
53
|
+
? [...initialValue]
|
|
54
|
+
: initialValue));
|
|
39
55
|
}, 0);
|
|
40
56
|
pending.add(timer);
|
|
41
57
|
};
|
|
@@ -44,20 +60,77 @@ export const Select = function Select({ options: items, value, defaultValue, onV
|
|
|
44
60
|
owner.removeEventListener('reset', reset);
|
|
45
61
|
pending.forEach(clearTimeout);
|
|
46
62
|
};
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
// initialValue is read through resetKey, so a new array of the same values
|
|
64
|
+
// does not re-register the listener.
|
|
65
|
+
}, [resetKey, props.form]);
|
|
49
66
|
const { name, form, required, disabled, id, ...aria } = props;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
const setInput = (node) => {
|
|
68
|
+
nativeInput.current = node;
|
|
69
|
+
if (typeof inputRef === 'function')
|
|
70
|
+
inputRef(node);
|
|
71
|
+
else if (inputRef)
|
|
72
|
+
inputRef.current = node;
|
|
73
|
+
};
|
|
74
|
+
// Several values: the "all" option leads the list, and the trigger sums them up.
|
|
75
|
+
const choosable = options.filter((option) => !option.disabled);
|
|
76
|
+
const text = { ...multipleLabels, ...(many?.labels ?? {}) };
|
|
77
|
+
const chosen = many
|
|
78
|
+
? (many.value ?? localValue)
|
|
79
|
+
: [];
|
|
80
|
+
const everything = choosable.length > 0 &&
|
|
81
|
+
choosable.every((option) => chosen.includes(option.value));
|
|
82
|
+
const summary = (values) => values.length === 0
|
|
83
|
+
? text.none
|
|
84
|
+
: choosable.every((option) => values.includes(option.value))
|
|
85
|
+
? text.all
|
|
86
|
+
: values.length === 1
|
|
87
|
+
? options.find((option) => option.value === values[0])?.label
|
|
88
|
+
: text.count(values.length);
|
|
89
|
+
const selected = many
|
|
90
|
+
? everything
|
|
91
|
+
? [ALL, ...chosen]
|
|
92
|
+
: chosen
|
|
93
|
+
: (one.value ?? localValue);
|
|
94
|
+
const value = many ? summary(chosen) : _jsx(BaseSelect.Value, {});
|
|
95
|
+
// Content width: every label the trigger can show, stacked in one cell.
|
|
96
|
+
const sizes = many
|
|
97
|
+
? [
|
|
98
|
+
text.all,
|
|
99
|
+
text.none,
|
|
100
|
+
text.count(options.length),
|
|
101
|
+
...options.map((option) => option.label),
|
|
102
|
+
]
|
|
103
|
+
: options.map((option) => option.label);
|
|
104
|
+
return (_jsx("span", { className: cx('surf-select', className), "data-size": sizeName(size), "data-ui-size": scheme, "data-width": width, "data-disabled": disabled || undefined, children: _jsxs(BaseSelect.Root, { items: options, multiple: multiple, value: selected, onValueChange: (next) => {
|
|
105
|
+
if (many) {
|
|
106
|
+
const list = next ?? [];
|
|
107
|
+
const toggledAll = list.includes(ALL) !== everything;
|
|
108
|
+
const values = toggledAll
|
|
109
|
+
? everything
|
|
110
|
+
? []
|
|
111
|
+
: choosable.map((option) => option.value)
|
|
112
|
+
: // Unchecking one option of "all" leaves the others.
|
|
113
|
+
list.filter((item) => item !== ALL);
|
|
114
|
+
setLocalValue(values);
|
|
115
|
+
many.onValueChange?.(values);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
setLocalValue(next ?? '');
|
|
119
|
+
one.onValueChange?.(next ?? '');
|
|
120
|
+
}
|
|
121
|
+
}, inputRef: setInput,
|
|
122
|
+
// A multiple Select submits its own inputs, without the "all" value.
|
|
123
|
+
name: many ? undefined : name, form: form, required: required, disabled: disabled, id: id, children: [_jsxs(BaseSelect.Trigger, { ...aria, ref: setTrigger, className: 'surf-select-trigger', "data-value": many ? chosen.join(' ') : selected, children: [width === 'content' ? (
|
|
124
|
+
// Every label sits in one cell, hidden but the chosen one, so the
|
|
125
|
+
// trigger is as wide as the longest.
|
|
126
|
+
_jsxs("span", { className: 'surf-select-sizer', children: [_jsx("span", { className: 'surf-select-value', children: value }), sizes.map((label, index) => (_jsx("span", { "aria-hidden": 'true', children: label }, index)))] })) : many ? (_jsx("span", { className: 'surf-select-value', children: value })) : (value), _jsx(BaseSelect.Icon, { className: 'surf-select-icon', children: _jsx(Icon, { name: 'chevron-down' }) })] }), many &&
|
|
127
|
+
name &&
|
|
128
|
+
chosen.map((item) => (_jsx("input", { type: 'hidden', name: name, value: item, form: form }, item))), _jsx(BaseSelect.Portal, { children: _jsx(BaseSelect.Positioner, { className: 'surf-positioner', sideOffset: 6, alignItemWithTrigger: false, children: _jsx(BaseSelect.Popup, { className: 'surf-select-popup', "data-ui-size": scheme, children: _jsxs(BaseSelect.List, { children: [many && (_jsxs(_Fragment, { children: [renderOption({
|
|
129
|
+
value: ALL,
|
|
130
|
+
label: text.all,
|
|
131
|
+
disabled: choosable.length === 0,
|
|
132
|
+
}), _jsx(BaseSelect.Separator, { className: 'surf-select-separator' })] })), items.map((item, index) => isGroup(item) ? (_jsxs(BaseSelect.Group, { className: 'surf-select-group', children: [_jsx(BaseSelect.GroupLabel, { className: 'surf-select-group-label', children: item.label }), item.options.map(renderOption)] }, index)) : (renderOption(item)))] }) }) }) })] }) }));
|
|
60
133
|
};
|
|
61
134
|
function renderOption(option) {
|
|
62
|
-
return (_jsxs(BaseSelect.Item, { className: 'surf-select-item', value: option.value, "data-option-value": option.value, disabled: option.disabled, children: [_jsx(BaseSelect.ItemIndicator, { className: 'surf-select-check', children: _jsx(Icon, { name: 'check' }) }), _jsx(BaseSelect.ItemText, { children: option.label })] }, option.value));
|
|
135
|
+
return (_jsxs(BaseSelect.Item, { className: 'surf-select-item', value: option.value, "data-option-value": option.value === ALL ? undefined : option.value, disabled: option.disabled, children: [_jsx(BaseSelect.ItemIndicator, { className: 'surf-select-check', children: _jsx(Icon, { name: 'check' }) }), _jsx(BaseSelect.ItemText, { children: option.label })] }, option.value));
|
|
63
136
|
}
|
package/dist/stat.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { type StatusTone } from './status.js';
|
|
2
3
|
export type StatDirection = 'up' | 'down' | 'flat';
|
|
3
4
|
export type StatChangeProps = Omit<React.ComponentPropsWithoutRef<'dd'>, 'children'> & {
|
|
4
5
|
/** The change, such as 0.023 for 2.3%. Its sign sets the direction. */
|
|
@@ -11,6 +12,15 @@ export type StatChangeProps = Omit<React.ComponentPropsWithoutRef<'dd'>, 'childr
|
|
|
11
12
|
* still points the way the value moved. */
|
|
12
13
|
lowerIsBetter?: boolean;
|
|
13
14
|
};
|
|
15
|
+
export type StatStatusProps = React.ComponentPropsWithoutRef<'dd'> & {
|
|
16
|
+
/** The value's state: caution for one to watch, danger for one past its limit. */
|
|
17
|
+
tone: StatusTone;
|
|
18
|
+
};
|
|
19
|
+
export type StatGroupProps = React.ComponentPropsWithoutRef<'div'> & {
|
|
20
|
+
/** What every value in the group measures, such as "GPU temperature". It shows
|
|
21
|
+
* above them and names the group. */
|
|
22
|
+
label: string;
|
|
23
|
+
};
|
|
14
24
|
/** A key figure: what it measures, its value, how it moved and what it covers. Renders
|
|
15
25
|
* a description list, so the label names the value for assistive technology. */
|
|
16
26
|
export declare const Stat: {
|
|
@@ -29,10 +39,16 @@ export declare const Stat: {
|
|
|
29
39
|
Change: ({ value, format, direction, lowerIsBetter, className, ref, ...props }: StatChangeProps & {
|
|
30
40
|
ref?: React.Ref<HTMLElement>;
|
|
31
41
|
}) => React.JSX.Element;
|
|
42
|
+
Status: ({ tone, className, children, ref, ...props }: StatStatusProps & {
|
|
43
|
+
ref?: React.Ref<HTMLElement>;
|
|
44
|
+
}) => React.JSX.Element;
|
|
32
45
|
Caption: ({ className, size, ref, ...rest }: Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "ref">, "size"> & {
|
|
33
46
|
size?: import("./size.js").ComponentSize;
|
|
34
47
|
ref?: React.Ref<HTMLElement> | undefined;
|
|
35
48
|
}) => React.JSX.Element;
|
|
49
|
+
Group: ({ label, className, children, ref, ...props }: StatGroupProps & {
|
|
50
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
51
|
+
}) => React.JSX.Element;
|
|
36
52
|
};
|
|
37
53
|
export declare const StatRoot: ({ className, size, ref, ...rest }: Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDListElement>, HTMLDListElement>, "ref">, "size"> & {
|
|
38
54
|
size?: import("./size.js").ComponentSize;
|
|
@@ -45,7 +61,11 @@ export declare const StatRoot: ({ className, size, ref, ...rest }: Omit<Omit<Rea
|
|
|
45
61
|
ref?: React.Ref<HTMLElement> | undefined;
|
|
46
62
|
}) => React.JSX.Element, StatChange: ({ value, format, direction, lowerIsBetter, className, ref, ...props }: StatChangeProps & {
|
|
47
63
|
ref?: React.Ref<HTMLElement>;
|
|
64
|
+
}) => React.JSX.Element, StatStatus: ({ tone, className, children, ref, ...props }: StatStatusProps & {
|
|
65
|
+
ref?: React.Ref<HTMLElement>;
|
|
48
66
|
}) => React.JSX.Element, StatCaption: ({ className, size, ref, ...rest }: Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "ref">, "size"> & {
|
|
49
67
|
size?: import("./size.js").ComponentSize;
|
|
50
68
|
ref?: React.Ref<HTMLElement> | undefined;
|
|
69
|
+
}) => React.JSX.Element, StatGroup: ({ label, className, children, ref, ...props }: StatGroupProps & {
|
|
70
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
51
71
|
}) => React.JSX.Element;
|
package/dist/stat.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Icon } from './icon.js';
|
|
|
3
3
|
import { cx } from './class-names.js';
|
|
4
4
|
import { elementPart } from './element-part.js';
|
|
5
5
|
import { formatPercent } from './format.js';
|
|
6
|
+
import { StatusIcon } from './status.js';
|
|
6
7
|
const signed = (value) => formatPercent(value, { signed: true });
|
|
7
8
|
const arrows = { up: 'arrow-up', down: 'arrow-down', flat: 'minus' };
|
|
8
9
|
/** A change against an earlier value: an arrow, a signed figure and its direction's
|
|
@@ -12,6 +13,17 @@ const ChangePart = function StatChange({ value, format = signed, direction, lowe
|
|
|
12
13
|
const tone = way === 'flat' || !lowerIsBetter ? way : way === 'up' ? 'down' : 'up';
|
|
13
14
|
return (_jsxs("dd", { ...props, ref: ref, "data-direction": way, "data-tone": tone, className: cx('surf-stat-change', className), children: [_jsx(Icon, { name: arrows[way] }), format(value)] }));
|
|
14
15
|
};
|
|
16
|
+
/** The value's state against a threshold, such as "Below 30 GiB": the tone's icon and
|
|
17
|
+
* text in its color. A caution or danger status colors the value too, and the text
|
|
18
|
+
* says why, so color is never alone. */
|
|
19
|
+
const StatusPart = function StatStatus({ tone, className, children, ref, ...props }) {
|
|
20
|
+
return (_jsxs("dd", { ...props, ref: ref, "data-tone": tone, className: cx('surf-stat-status', className), children: [_jsx(StatusIcon, { tone: tone }), children] }));
|
|
21
|
+
};
|
|
22
|
+
/** Several values of one measure side by side, such as one per node: a shared label
|
|
23
|
+
* over Stat.Roots whose own labels name each. */
|
|
24
|
+
const GroupPart = function StatGroup({ label, className, children, ref, ...props }) {
|
|
25
|
+
return (_jsxs("div", { role: 'group', "aria-label": label, ...props, ref: ref, className: cx('surf-stat-group', className), children: [_jsx("span", { className: 'surf-stat-group-label', "aria-hidden": 'true', children: label }), _jsx("div", { className: 'surf-stat-group-items', children: children })] }));
|
|
26
|
+
};
|
|
15
27
|
/** A key figure: what it measures, its value, how it moved and what it covers. Renders
|
|
16
28
|
* a description list, so the label names the value for assistive technology. */
|
|
17
29
|
export const Stat = {
|
|
@@ -19,7 +31,9 @@ export const Stat = {
|
|
|
19
31
|
Label: /* @__PURE__ */ elementPart('dt', 'surf-stat-label'),
|
|
20
32
|
Value: /* @__PURE__ */ elementPart('dd', 'surf-stat-value'),
|
|
21
33
|
Change: ChangePart,
|
|
34
|
+
Status: StatusPart,
|
|
22
35
|
Caption: /* @__PURE__ */ elementPart('dd', 'surf-stat-caption'),
|
|
36
|
+
Group: GroupPart,
|
|
23
37
|
};
|
|
24
38
|
// Flat names for React Server Components, which cannot dot into client exports.
|
|
25
|
-
export const { Root: StatRoot, Label: StatLabel, Value: StatValue, Change: StatChange, Caption: StatCaption, } = Stat;
|
|
39
|
+
export const { Root: StatRoot, Label: StatLabel, Value: StatValue, Change: StatChange, Status: StatStatus, Caption: StatCaption, Group: StatGroup, } = Stat;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { ComponentSizeAlias } from './size.js';
|
|
3
|
+
/** A span of time back from now, or a refresh interval, in milliseconds. */
|
|
4
|
+
export type TimeRangeOption = {
|
|
5
|
+
value: number;
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const timeRangeOptions: readonly TimeRangeOption[];
|
|
9
|
+
/** 0 is off. */
|
|
10
|
+
export declare const refreshIntervalOptions: readonly TimeRangeOption[];
|
|
11
|
+
export type TimeRangePickerLabels = {
|
|
12
|
+
/** The range select's name. */
|
|
13
|
+
range: string;
|
|
14
|
+
/** The refresh interval select's name. */
|
|
15
|
+
refresh: string;
|
|
16
|
+
/** The button that refreshes now. */
|
|
17
|
+
refreshNow: string;
|
|
18
|
+
};
|
|
19
|
+
export type TimeRangePickerProps = Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> & {
|
|
20
|
+
/** Names the group, such as "Dashboard time". */
|
|
21
|
+
label: string;
|
|
22
|
+
/** The span back from now, in ms; one of ranges. */
|
|
23
|
+
range: number;
|
|
24
|
+
onRangeChange: (range: number) => void;
|
|
25
|
+
/** The span options, such as timeRangeOptions with translated labels. */
|
|
26
|
+
ranges?: readonly TimeRangeOption[];
|
|
27
|
+
/** How often onRefresh runs, in ms; 0 is off. Leave it out for no interval select. */
|
|
28
|
+
refreshInterval?: number;
|
|
29
|
+
onRefreshIntervalChange?: (interval: number) => void;
|
|
30
|
+
refreshIntervals?: readonly TimeRangeOption[];
|
|
31
|
+
/** Runs when the button is pressed and on every interval while the page is shown.
|
|
32
|
+
* Leave it out for no refresh button. */
|
|
33
|
+
onRefresh?: () => void;
|
|
34
|
+
labels?: Partial<TimeRangePickerLabels>;
|
|
35
|
+
size?: ComponentSizeAlias;
|
|
36
|
+
};
|
|
37
|
+
/** The span a monitoring screen shows, back from now, with how often it refreshes and
|
|
38
|
+
* a button to refresh now. For a fixed span between two dates, put a DateRangePicker
|
|
39
|
+
* beside it. */
|
|
40
|
+
export declare const TimeRangePicker: ({ label, range, onRangeChange, ranges, refreshInterval, onRefreshIntervalChange, refreshIntervals: intervals, onRefresh, labels, size, className, ref, ...props }: TimeRangePickerProps & {
|
|
41
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
42
|
+
}) => React.JSX.Element;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Button } from './button.js';
|
|
5
|
+
import { Icon } from './icon.js';
|
|
6
|
+
import { Select } from './select.js';
|
|
7
|
+
import { cx } from './class-names.js';
|
|
8
|
+
const MINUTE = 60_000, HOUR = 60 * MINUTE, DAY = 24 * HOUR;
|
|
9
|
+
export const timeRangeOptions = [
|
|
10
|
+
{ value: 15 * MINUTE, label: 'Last 15 minutes' },
|
|
11
|
+
{ value: HOUR, label: 'Last hour' },
|
|
12
|
+
{ value: 6 * HOUR, label: 'Last 6 hours' },
|
|
13
|
+
{ value: DAY, label: 'Last 24 hours' },
|
|
14
|
+
{ value: 7 * DAY, label: 'Last 7 days' },
|
|
15
|
+
{ value: 30 * DAY, label: 'Last 30 days' },
|
|
16
|
+
];
|
|
17
|
+
/** 0 is off. */
|
|
18
|
+
export const refreshIntervalOptions = [
|
|
19
|
+
{ value: 0, label: 'Off' },
|
|
20
|
+
{ value: 10_000, label: 'Every 10 seconds' },
|
|
21
|
+
{ value: 30_000, label: 'Every 30 seconds' },
|
|
22
|
+
{ value: MINUTE, label: 'Every minute' },
|
|
23
|
+
{ value: 5 * MINUTE, label: 'Every 5 minutes' },
|
|
24
|
+
];
|
|
25
|
+
const pickerLabels = {
|
|
26
|
+
range: 'Time range',
|
|
27
|
+
refresh: 'Refresh interval',
|
|
28
|
+
refreshNow: 'Refresh now',
|
|
29
|
+
};
|
|
30
|
+
/** The span a monitoring screen shows, back from now, with how often it refreshes and
|
|
31
|
+
* a button to refresh now. For a fixed span between two dates, put a DateRangePicker
|
|
32
|
+
* beside it. */
|
|
33
|
+
export const TimeRangePicker = function TimeRangePicker({ label, range, onRangeChange, ranges = timeRangeOptions, refreshInterval, onRefreshIntervalChange, refreshIntervals: intervals = refreshIntervalOptions, onRefresh, labels, size, className, ref, ...props }) {
|
|
34
|
+
const text = { ...pickerLabels, ...labels };
|
|
35
|
+
const refresh = React.useRef(onRefresh);
|
|
36
|
+
refresh.current = onRefresh;
|
|
37
|
+
// The interval pauses while the page is hidden, and refreshes once it shows again.
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
if (!refreshInterval || !refresh.current)
|
|
40
|
+
return;
|
|
41
|
+
let due = false;
|
|
42
|
+
const timer = setInterval(() => {
|
|
43
|
+
if (document.hidden)
|
|
44
|
+
due = true;
|
|
45
|
+
else
|
|
46
|
+
refresh.current?.();
|
|
47
|
+
}, refreshInterval);
|
|
48
|
+
const shown = () => {
|
|
49
|
+
if (!document.hidden && due) {
|
|
50
|
+
due = false;
|
|
51
|
+
refresh.current?.();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
document.addEventListener('visibilitychange', shown);
|
|
55
|
+
return () => {
|
|
56
|
+
clearInterval(timer);
|
|
57
|
+
document.removeEventListener('visibilitychange', shown);
|
|
58
|
+
};
|
|
59
|
+
}, [refreshInterval]);
|
|
60
|
+
const options = (list) => list.map((option) => ({
|
|
61
|
+
value: String(option.value),
|
|
62
|
+
label: option.label,
|
|
63
|
+
}));
|
|
64
|
+
return (_jsxs("div", { role: 'group', "aria-label": label, ...props, ref: ref, className: cx('surf-time-range', className), children: [_jsx(Select, { width: 'content', size: size, options: options(ranges), value: String(range), onValueChange: (value) => onRangeChange(Number(value)), "aria-label": text.range }), refreshInterval !== undefined && (_jsx(Select, { width: 'content', size: size, options: options(intervals), value: String(refreshInterval), onValueChange: (value) => onRefreshIntervalChange?.(Number(value)), "aria-label": text.refresh })), onRefresh && (_jsx(Button, { variant: 'outline', size: size === 'small' || size === 'sm'
|
|
65
|
+
? 'icon-small'
|
|
66
|
+
: size === 'large' || size === 'lg'
|
|
67
|
+
? 'icon-large'
|
|
68
|
+
: 'icon', "aria-label": text.refreshNow, onClick: () => onRefresh(), children: _jsx(Icon, { name: 'refresh' }) }))] }));
|
|
69
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surflane/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Surflane React components built on Base UI, with data, chart, code and Markdown components.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design-system",
|
|
@@ -90,6 +90,9 @@
|
|
|
90
90
|
"./list.css": "./dist/css/list.css",
|
|
91
91
|
"./stat.css": "./dist/css/stat.css",
|
|
92
92
|
"./sparkline.css": "./dist/css/sparkline.css",
|
|
93
|
+
"./bar-list.css": "./dist/css/bar-list.css",
|
|
94
|
+
"./dashboard.css": "./dist/css/dashboard.css",
|
|
95
|
+
"./time-range-picker.css": "./dist/css/time-range-picker.css",
|
|
93
96
|
"./tree.css": "./dist/css/tree.css",
|
|
94
97
|
"./chart.css": "./dist/css/chart.css",
|
|
95
98
|
"./scroll-area.css": "./dist/css/scroll-area.css",
|
|
@@ -316,6 +319,18 @@
|
|
|
316
319
|
"types": "./dist/sparkline.d.ts",
|
|
317
320
|
"import": "./dist/sparkline.js"
|
|
318
321
|
},
|
|
322
|
+
"./bar-list": {
|
|
323
|
+
"types": "./dist/bar-list.d.ts",
|
|
324
|
+
"import": "./dist/bar-list.js"
|
|
325
|
+
},
|
|
326
|
+
"./dashboard": {
|
|
327
|
+
"types": "./dist/dashboard.d.ts",
|
|
328
|
+
"import": "./dist/dashboard.js"
|
|
329
|
+
},
|
|
330
|
+
"./time-range-picker": {
|
|
331
|
+
"types": "./dist/time-range-picker.d.ts",
|
|
332
|
+
"import": "./dist/time-range-picker.js"
|
|
333
|
+
},
|
|
319
334
|
"./tree": {
|
|
320
335
|
"types": "./dist/tree.d.ts",
|
|
321
336
|
"import": "./dist/tree.js"
|
|
@@ -406,8 +421,8 @@
|
|
|
406
421
|
"@base-ui/react": "^1.9.0",
|
|
407
422
|
"@headless-tree/core": "^1.8.0",
|
|
408
423
|
"@headless-tree/react": "^1.8.0",
|
|
409
|
-
"@surflane/css": "^0.
|
|
410
|
-
"@surflane/design-tokens": "^0.
|
|
424
|
+
"@surflane/css": "^0.12.0",
|
|
425
|
+
"@surflane/design-tokens": "^0.12.0",
|
|
411
426
|
"@tanstack/react-virtual": "^3.14.21",
|
|
412
427
|
"react": "^19.0.0",
|
|
413
428
|
"react-aria-components": "^1.22.1",
|