@surflane/ui 0.11.0 → 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/dialog.css +11 -0
- package/dist/css/rules/filter-bar.css +8 -1
- 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/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/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",
|