@granularjs/ui 0.2.0 → 0.3.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/granular-ui.min.js +208 -64
- package/dist/granular-ui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/components/Autocomplete.js +179 -0
- package/src/components/DateInput.js +1 -3
- package/src/components/Pagination.js +2 -1
- package/src/components/ProgressRing.js +41 -7
- package/src/components/Radio.js +32 -3
- package/src/components/RadioGroup.js +24 -4
- package/src/components/RangePicker.js +66 -26
- package/src/components/Select.js +3 -1
- package/src/components/SelectSearch.js +2 -34
- package/src/components/Slider.js +15 -4
- package/src/components/Stepper.js +4 -3
- package/src/components/Switch.js +32 -4
- package/src/components/SwitchGroup.js +20 -4
- package/src/components/Table.js +39 -13
- package/src/components/Timeline.js +247 -10
- package/src/components/Toast.js +18 -6
- package/src/components/ToastStack.js +9 -15
- package/src/index.js +1 -0
- package/src/theme/icons.js +2 -1
- package/src/theme/styles.js +194 -50
- package/types/components/Autocomplete.d.ts +1 -0
- package/types/components/RadioGroup.d.ts +1 -0
- package/types/components/SwitchGroup.d.ts +1 -0
- package/types/index.d.ts +1 -0
- package/types/theme/icons.d.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@granularjs/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "90+ production-ready UI components for Granular. Dark/light themes, CSS variables, accessible, zero dependencies beyond @granularjs/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/granular-ui.min.js",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Div, state, after, list, when } from '@granularjs/core';
|
|
2
|
+
import { cx, splitPropsChildren, resolveValue, classFlag } from '../utils.js';
|
|
3
|
+
import { TextInput } from './TextInput.js';
|
|
4
|
+
import { ScrollArea } from './ScrollArea.js';
|
|
5
|
+
|
|
6
|
+
function getByPath(obj, path) {
|
|
7
|
+
if (path == null || path === '') return obj;
|
|
8
|
+
const keys = String(path).trim().split('.');
|
|
9
|
+
let v = obj;
|
|
10
|
+
for (const k of keys) v = v?.[k];
|
|
11
|
+
return v;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function defaultFilter(query, item, getLabel) {
|
|
15
|
+
const label = getLabel(item);
|
|
16
|
+
return String(label ?? '').toLowerCase().includes(String(query ?? '').toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Autocomplete(...args) {
|
|
20
|
+
const { props, rawProps } = splitPropsChildren(args, {
|
|
21
|
+
data: [],
|
|
22
|
+
size: 'md',
|
|
23
|
+
valuePath: 'value',
|
|
24
|
+
labelPath: 'label',
|
|
25
|
+
});
|
|
26
|
+
const {
|
|
27
|
+
data,
|
|
28
|
+
value,
|
|
29
|
+
size,
|
|
30
|
+
valuePath,
|
|
31
|
+
labelPath,
|
|
32
|
+
filter,
|
|
33
|
+
placeholder,
|
|
34
|
+
label,
|
|
35
|
+
description,
|
|
36
|
+
error,
|
|
37
|
+
leftSection,
|
|
38
|
+
rightSection,
|
|
39
|
+
className,
|
|
40
|
+
inputClassName,
|
|
41
|
+
disabled,
|
|
42
|
+
...rest
|
|
43
|
+
} = props;
|
|
44
|
+
const { onChange, renderItem } = rawProps;
|
|
45
|
+
|
|
46
|
+
const opened = state(false);
|
|
47
|
+
const query = state('');
|
|
48
|
+
const currentValue = state(resolveValue(value));
|
|
49
|
+
|
|
50
|
+
after(value).change((next) => {
|
|
51
|
+
const resolved = resolveValue(next);
|
|
52
|
+
if (resolved === currentValue.get()) return;
|
|
53
|
+
currentValue.set(resolved);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const dataResolved = after(data).compute((d) => (Array.isArray(d) ? d : []));
|
|
57
|
+
|
|
58
|
+
const selectedItem = after(dataResolved, currentValue, valuePath).compute(([items, val, vPath]) => {
|
|
59
|
+
if (val === undefined || val === null) return null;
|
|
60
|
+
const getVal = (it) => (vPath == null || vPath === '' ? it : getByPath(it, vPath));
|
|
61
|
+
return items.find((it) => getVal(it) === val) ?? null;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const displayText = after(selectedItem, opened, query, labelPath).compute(([sel, isOpen, q, lPath]) => {
|
|
65
|
+
const getLabelCur = (item) =>
|
|
66
|
+
lPath == null || lPath === '' ? String(item ?? '') : String(getByPath(item, lPath) ?? '');
|
|
67
|
+
if (isOpen) return q ?? '';
|
|
68
|
+
return sel ? getLabelCur(sel) : '';
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const filteredItems = after(dataResolved, query, filter, labelPath).compute(([items, q, filterVal, lPath]) => {
|
|
72
|
+
const getLabelCur = (item) =>
|
|
73
|
+
lPath == null || lPath === '' ? String(item ?? '') : String(getByPath(item, lPath) ?? '');
|
|
74
|
+
const fn = resolveValue(filterVal) ?? defaultFilter;
|
|
75
|
+
return items.filter((item) => fn(q, item, getLabelCur));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const selectItem = (item) => {
|
|
79
|
+
const vPath = resolveValue(valuePath);
|
|
80
|
+
const val = vPath == null || vPath === '' ? item : getByPath(item, vPath);
|
|
81
|
+
currentValue.set(val);
|
|
82
|
+
query.set('');
|
|
83
|
+
opened.set(false);
|
|
84
|
+
onChange?.(val);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const open = () => {
|
|
88
|
+
if (resolveValue(disabled)) return;
|
|
89
|
+
opened.set(true);
|
|
90
|
+
const sel = selectedItem.get();
|
|
91
|
+
const lPath = resolveValue(labelPath);
|
|
92
|
+
const getLabelCur = (item) =>
|
|
93
|
+
lPath == null || lPath === '' ? String(item ?? '') : String(getByPath(item, lPath) ?? '');
|
|
94
|
+
query.set(sel ? getLabelCur(sel) : '');
|
|
95
|
+
};
|
|
96
|
+
const close = () => {
|
|
97
|
+
opened.set(false);
|
|
98
|
+
const sel = selectedItem.get();
|
|
99
|
+
const lPath = resolveValue(labelPath);
|
|
100
|
+
const getLabelCur = (item) =>
|
|
101
|
+
lPath == null || lPath === '' ? String(item ?? '') : String(getByPath(item, lPath) ?? '');
|
|
102
|
+
query.set(sel ? getLabelCur(sel) : '');
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const { onChange: _omitOnChange, ...restForInput } = rest;
|
|
106
|
+
const inputProps = {
|
|
107
|
+
...restForInput,
|
|
108
|
+
size,
|
|
109
|
+
label,
|
|
110
|
+
description,
|
|
111
|
+
error,
|
|
112
|
+
leftSection,
|
|
113
|
+
rightSection,
|
|
114
|
+
placeholder: resolveValue(placeholder) ?? undefined,
|
|
115
|
+
disabled,
|
|
116
|
+
value: displayText,
|
|
117
|
+
onInput: (ev) => query.set(ev?.target?.value ?? ''),
|
|
118
|
+
onFocus: open,
|
|
119
|
+
onClick: open,
|
|
120
|
+
onBlur: () => setTimeout(() => close(), 150),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const getValueForItem = (item) => {
|
|
124
|
+
const vPath = resolveValue(valuePath);
|
|
125
|
+
return vPath == null || vPath === '' ? item : getByPath(item, vPath);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const itemActiveClass = (item) =>
|
|
129
|
+
after(currentValue, valuePath).compute(([v, vPath]) => {
|
|
130
|
+
const getVal = (it) => (vPath == null || vPath === '' ? it : getByPath(it, vPath));
|
|
131
|
+
return getVal(item) === v ? 'g-ui-autocomplete-item-active' : '';
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const renderOption = (item) => {
|
|
135
|
+
if (renderItem && typeof renderItem === 'function') {
|
|
136
|
+
const node = renderItem(item);
|
|
137
|
+
if (node != null)
|
|
138
|
+
return Div(
|
|
139
|
+
{
|
|
140
|
+
className: cx('g-ui-autocomplete-item', itemActiveClass(item)),
|
|
141
|
+
onClick: () => selectItem(item),
|
|
142
|
+
role: 'option',
|
|
143
|
+
},
|
|
144
|
+
node
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return Div(
|
|
148
|
+
{
|
|
149
|
+
className: cx('g-ui-autocomplete-item', itemActiveClass(item)),
|
|
150
|
+
onClick: () => selectItem(item),
|
|
151
|
+
role: 'option',
|
|
152
|
+
},
|
|
153
|
+
after(labelPath).compute((lPath) =>
|
|
154
|
+
lPath == null || lPath === '' ? String(item ?? '') : String(getByPath(item, lPath) ?? '')
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return Div(
|
|
160
|
+
{ className: cx('g-ui-autocomplete', className, classFlag('g-ui-autocomplete-disabled', disabled)) },
|
|
161
|
+
TextInput({
|
|
162
|
+
...inputProps,
|
|
163
|
+
className: cx('g-ui-autocomplete-input-wrapper', inputProps.className),
|
|
164
|
+
inputClassName: cx('g-ui-autocomplete-input', inputClassName),
|
|
165
|
+
}),
|
|
166
|
+
when(opened, () =>
|
|
167
|
+
Div(
|
|
168
|
+
{
|
|
169
|
+
className: 'g-ui-autocomplete-dropdown',
|
|
170
|
+
role: 'listbox',
|
|
171
|
+
},
|
|
172
|
+
ScrollArea(
|
|
173
|
+
{ className: 'g-ui-autocomplete-list', style: { maxHeight: '240px' } },
|
|
174
|
+
list(filteredItems, (item) => renderOption(item))
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -70,8 +70,8 @@ export function DateInput(...args) {
|
|
|
70
70
|
const opened = state(false);
|
|
71
71
|
|
|
72
72
|
after(value).change((next) => {
|
|
73
|
-
if (value.get() == next) return;
|
|
74
73
|
const resolved = resolveDate(next);
|
|
74
|
+
if (isSameDay(resolved, currentDate.get())) return;
|
|
75
75
|
if (resolved == null) {
|
|
76
76
|
currentDate.set(null);
|
|
77
77
|
textValue.set('');
|
|
@@ -83,8 +83,6 @@ export function DateInput(...args) {
|
|
|
83
83
|
|
|
84
84
|
after(textValue).change((next) => {
|
|
85
85
|
const parsed = parseDate(next);
|
|
86
|
-
console.log('parsed', parsed);
|
|
87
|
-
console.log('currentDate', currentDate.get());
|
|
88
86
|
if (!parsed) return;
|
|
89
87
|
if (isSameDay(parsed, currentDate.get())) return;
|
|
90
88
|
currentDate.set(parsed);
|
|
@@ -5,7 +5,8 @@ export function Pagination(...args) {
|
|
|
5
5
|
const { props, rawProps } = splitPropsChildren(args, { total: 1, size: 'md' });
|
|
6
6
|
const { page, total, size, className, ...rest } = props;
|
|
7
7
|
const { onChange } = rawProps;
|
|
8
|
-
const currentState = state(resolveValue(page ?? 1)
|
|
8
|
+
const currentState = state(resolveValue(page) ?? 1);
|
|
9
|
+
|
|
9
10
|
after(page).change((next) => {
|
|
10
11
|
const resolved = resolveValue(next);
|
|
11
12
|
if (resolved == null) return;
|
|
@@ -1,11 +1,45 @@
|
|
|
1
|
-
import { Div } from '@granularjs/core';
|
|
2
|
-
import { cx, splitPropsChildren } from '../utils.js';
|
|
1
|
+
import { Div, after, resolve } from '@granularjs/core';
|
|
2
|
+
import { cx, splitPropsChildren, classVar } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
function clampValue(v) {
|
|
5
|
+
const n = Number(v);
|
|
6
|
+
if (Number.isNaN(n)) return 0;
|
|
7
|
+
return Math.max(0, Math.min(100, n));
|
|
8
|
+
}
|
|
3
9
|
|
|
4
10
|
export function ProgressRing(...args) {
|
|
5
|
-
const { props } = splitPropsChildren(args, { size: 'md' });
|
|
6
|
-
const { size, className, ...rest } = props;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const { props } = splitPropsChildren(args, { size: 'md', value: null, color: 'primary' });
|
|
12
|
+
const { size, value, color, className, ...rest } = props;
|
|
13
|
+
|
|
14
|
+
const hasValue = after(value).compute((v) => v != null && v !== '');
|
|
15
|
+
const ringValue = after(value).compute((v) => clampValue(v));
|
|
16
|
+
const style = after(ringValue, hasValue).compute(([val, has]) => {
|
|
17
|
+
if (!has) return {};
|
|
18
|
+
const num = Number(val);
|
|
19
|
+
const deg = Number.isNaN(num) ? 0 : Math.max(0, Math.min(360, num * 3.6));
|
|
20
|
+
return {
|
|
21
|
+
background: `conic-gradient(var(--g-ui-progress-ring-fill, var(--g-ui-primary)) 0deg ${deg}deg, var(--g-ui-border-muted) ${deg}deg 360deg)`,
|
|
22
|
+
};
|
|
10
23
|
});
|
|
24
|
+
const indeterminateClass = after(hasValue).compute((has) => (has ? '' : 'g-ui-progress-ring-indeterminate'));
|
|
25
|
+
const ariaValueNow = after(hasValue, value).compute(([has, v]) => (has ? clampValue(v) : undefined));
|
|
26
|
+
|
|
27
|
+
return Div(
|
|
28
|
+
{
|
|
29
|
+
...rest,
|
|
30
|
+
className: cx(
|
|
31
|
+
'g-ui-progress-ring',
|
|
32
|
+
classVar('g-ui-progress-ring-', color, 'primary'),
|
|
33
|
+
[size, (s) => `g-ui-progress-ring-size-${resolve(s) ?? 'md'}`],
|
|
34
|
+
indeterminateClass,
|
|
35
|
+
className
|
|
36
|
+
),
|
|
37
|
+
style,
|
|
38
|
+
role: 'progressbar',
|
|
39
|
+
'aria-valuemin': 0,
|
|
40
|
+
'aria-valuemax': 100,
|
|
41
|
+
'aria-valuenow': ariaValueNow,
|
|
42
|
+
},
|
|
43
|
+
Div({ className: 'g-ui-progress-ring-hole' })
|
|
44
|
+
);
|
|
11
45
|
}
|
package/src/components/Radio.js
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
|
-
import { Div, Input, Label, Span, when } from '@granularjs/core';
|
|
1
|
+
import { Div, Input, Label, Span, when, after, state } from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren, classVar } from '../utils.js';
|
|
3
|
+
import { radioGroupContext } from './RadioGroup.js';
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
export function Radio(...args) {
|
|
5
|
-
const { props } = splitPropsChildren(args, { size: 'md' });
|
|
6
|
-
const { label, description, size, className, inputProps, ...rest } = props;
|
|
7
|
+
const { props, rawProps } = splitPropsChildren(args, { size: 'md' });
|
|
8
|
+
const { label, name, value, checked, description, size, className, inputProps, ...rest } = props;
|
|
9
|
+
const { onChange } = rawProps;
|
|
10
|
+
|
|
11
|
+
const checkedState = state(checked?.get() ?? false);
|
|
12
|
+
|
|
13
|
+
const radioGroupState = radioGroupContext.state();
|
|
14
|
+
const inputName = after(radioGroupState.name, name).compute(([radioGroupName, inputGroupName]) => radioGroupName || inputGroupName);
|
|
15
|
+
|
|
16
|
+
after(checked, radioGroupState.selected).change((values) => {
|
|
17
|
+
const [checkedValue, selectedValue] = values;
|
|
18
|
+
if (radioGroupState.get().name) {
|
|
19
|
+
checkedState.set(selectedValue === value.get());
|
|
20
|
+
} else {
|
|
21
|
+
checkedState.set(checkedValue);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
after(checkedState).change((next) => {
|
|
26
|
+
onChange?.(next);
|
|
27
|
+
if (!next) return;
|
|
28
|
+
if (radioGroupState.get().name) {
|
|
29
|
+
radioGroupState.set().selected = value.get();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
7
33
|
const control = Label(
|
|
8
34
|
{ className: 'g-ui-radio-control' },
|
|
9
35
|
Input({
|
|
10
36
|
type: 'radio',
|
|
37
|
+
name: inputName,
|
|
38
|
+
value: value,
|
|
39
|
+
checked: checkedState,
|
|
11
40
|
className: cx('g-ui-radio-input', classVar('g-ui-radio-size-', size, 'md'), inputProps?.className),
|
|
12
41
|
...rest,
|
|
13
42
|
}),
|
|
@@ -1,8 +1,28 @@
|
|
|
1
|
-
import { Div } from '@granularjs/core';
|
|
1
|
+
import { Div, context, after } from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren } from '../utils.js';
|
|
3
3
|
|
|
4
|
+
export const radioGroupContext = context({ name: '', selected: null });
|
|
5
|
+
|
|
4
6
|
export function RadioGroup(...args) {
|
|
5
|
-
const { props, children } = splitPropsChildren(args);
|
|
6
|
-
const { className, ...rest } = props;
|
|
7
|
-
|
|
7
|
+
const { props, children, rawProps } = splitPropsChildren(args);
|
|
8
|
+
const { className, name, selected, onChange: _onChange, ...rest } = props;
|
|
9
|
+
const { onChange } = rawProps;
|
|
10
|
+
|
|
11
|
+
const scope = radioGroupContext.scope({ name: name?.get(), selected: selected?.get() });
|
|
12
|
+
console.log('selected?.get()', selected?.get())
|
|
13
|
+
|
|
14
|
+
after(name).change((next) => {
|
|
15
|
+
scope.set().name = next;
|
|
16
|
+
});
|
|
17
|
+
after(selected).change((next) => {
|
|
18
|
+
if (next === scope.selected.get()) return;
|
|
19
|
+
scope.set().selected = next;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
after(scope.selected).change((next) => {
|
|
23
|
+
onChange?.(next);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
return scope.serve(Div({ ...rest, className: cx('g-ui-stack g-ui-gap-sm', className) }, children));
|
|
8
28
|
}
|
|
@@ -1,45 +1,85 @@
|
|
|
1
|
-
import { Div, state, after } from '@granularjs/core';
|
|
1
|
+
import { Div, Span, state, after } from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren, resolveValue } from '../utils.js';
|
|
3
|
-
import {
|
|
3
|
+
import { DateInput } from './DateInput.js';
|
|
4
|
+
|
|
5
|
+
function resolveDate(v) {
|
|
6
|
+
const resolved = resolveValue(v);
|
|
7
|
+
if (resolved instanceof Date) return resolved;
|
|
8
|
+
if (resolved == null || resolved === '') return null;
|
|
9
|
+
const d = new Date(resolved);
|
|
10
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toDatePair(value) {
|
|
14
|
+
const raw = resolveValue(value);
|
|
15
|
+
if (raw == null) return [null, null];
|
|
16
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
17
|
+
return [resolveDate(arr[0]), resolveDate(arr[1])];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isSameDay(a, b) {
|
|
21
|
+
if (!a || !b) return a === b;
|
|
22
|
+
return (
|
|
23
|
+
a.getFullYear() === b.getFullYear() &&
|
|
24
|
+
a.getMonth() === b.getMonth() &&
|
|
25
|
+
a.getDate() === b.getDate()
|
|
26
|
+
);
|
|
27
|
+
}
|
|
4
28
|
|
|
5
29
|
export function RangePicker(...args) {
|
|
6
30
|
const { props, rawProps } = splitPropsChildren(args, { size: 'md' });
|
|
7
|
-
const { value, size, className, ...rest } = props;
|
|
31
|
+
const { value, size, minDate, maxDate, className, placeholderMin, placeholderMax, ...rest } = props;
|
|
8
32
|
const { onChange } = rawProps;
|
|
9
|
-
|
|
33
|
+
|
|
34
|
+
const [initialStart, initialEnd] = toDatePair(value);
|
|
35
|
+
const startState = state(initialStart);
|
|
36
|
+
const endState = state(initialEnd);
|
|
10
37
|
|
|
11
38
|
after(value).change((next) => {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
14
|
-
|
|
39
|
+
const [s, e] = toDatePair(next);
|
|
40
|
+
if (isSameDay(s, startState.get()) && isSameDay(e, endState.get())) return;
|
|
41
|
+
startState.set(s);
|
|
42
|
+
endState.set(e);
|
|
15
43
|
});
|
|
16
44
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
45
|
+
const notify = () => {
|
|
46
|
+
const s = startState.get();
|
|
47
|
+
const e = endState.get();
|
|
48
|
+
onChange?.([s ?? null, e ?? null]);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const setStart = (date) => {
|
|
52
|
+
const end = endState.get();
|
|
53
|
+
if (date && end && date > end) endState.set(date);
|
|
54
|
+
startState.set(date);
|
|
55
|
+
notify();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const setEnd = (date) => {
|
|
59
|
+
const start = startState.get();
|
|
60
|
+
if (date && start && date < start) startState.set(date);
|
|
61
|
+
endState.set(date);
|
|
62
|
+
notify();
|
|
20
63
|
};
|
|
21
64
|
|
|
22
65
|
return Div(
|
|
23
66
|
{ ...rest, className: cx('g-ui-range-picker', props.className ?? className) },
|
|
24
|
-
|
|
67
|
+
DateInput({
|
|
68
|
+
...rest,
|
|
25
69
|
size,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const current = currentState.get() ?? ['', ''];
|
|
31
|
-
setValue([ev.target.value, current[1]]);
|
|
32
|
-
},
|
|
70
|
+
value: startState,
|
|
71
|
+
onChange: setStart,
|
|
72
|
+
maxDate: endState,
|
|
73
|
+
placeholder: resolveValue(placeholderMin) ?? undefined,
|
|
33
74
|
}),
|
|
34
|
-
|
|
75
|
+
Span({ className: 'g-ui-range-picker-separator' }, '–'),
|
|
76
|
+
DateInput({
|
|
77
|
+
...rest,
|
|
35
78
|
size,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const current = currentState.get() ?? ['', ''];
|
|
41
|
-
setValue([current[0], ev.target.value]);
|
|
42
|
-
},
|
|
79
|
+
value: endState,
|
|
80
|
+
onChange: setEnd,
|
|
81
|
+
minDate: startState,
|
|
82
|
+
placeholder: resolveValue(placeholderMax) ?? undefined,
|
|
43
83
|
})
|
|
44
84
|
);
|
|
45
85
|
}
|
package/src/components/Select.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Div, Span, when, state, after } from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren, classVar, resolveValue } from '../utils.js';
|
|
3
|
+
import { keyboardArrowDownSvg } from '../theme/icons.js';
|
|
4
|
+
import { Icon } from './Icon.js';
|
|
3
5
|
|
|
4
6
|
export function Select(...args) {
|
|
5
7
|
const { props, rawProps } = splitPropsChildren(args, { data: [], size: 'md' });
|
|
@@ -44,7 +46,7 @@ export function Select(...args) {
|
|
|
44
46
|
Span({ className: 'g-ui-select-value' }, displayLabel)
|
|
45
47
|
),
|
|
46
48
|
when(rightSection, () => Div({ className: 'g-ui-input-section' }, rightSection), () =>
|
|
47
|
-
Span({ className: 'g-ui-select-caret' },
|
|
49
|
+
Span({ className: 'g-ui-select-caret' }, Icon({ innerHTML: keyboardArrowDownSvg }))
|
|
48
50
|
)
|
|
49
51
|
),
|
|
50
52
|
when(open, () =>
|
|
@@ -1,37 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cx, splitPropsChildren } from '../utils.js';
|
|
3
|
-
import { state } from '@granularjs/core';
|
|
4
|
-
import { TextInput } from './TextInput.js';
|
|
1
|
+
import { Autocomplete } from './Autocomplete.js';
|
|
5
2
|
|
|
6
3
|
export function SelectSearch(...args) {
|
|
7
|
-
|
|
8
|
-
const { data, className, ...rest } = props;
|
|
9
|
-
const { onChange } = rawProps;
|
|
10
|
-
const query = state('');
|
|
11
|
-
|
|
12
|
-
const items = data.filter((item) =>
|
|
13
|
-
String(item.label || '').toLowerCase().includes(String(query.get()).toLowerCase())
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
return Div(
|
|
17
|
-
{ ...rest, className: cx('g-ui-select-search', className) },
|
|
18
|
-
TextInput({
|
|
19
|
-
inputClassName: 'g-ui-select-search-input',
|
|
20
|
-
placeholder: 'Search...',
|
|
21
|
-
value: query,
|
|
22
|
-
onInput: (ev) => query.set(ev?.target?.value ?? ''),
|
|
23
|
-
}),
|
|
24
|
-
Div(
|
|
25
|
-
{ className: 'g-ui-select-search-list' },
|
|
26
|
-
items.map((item) =>
|
|
27
|
-
Div(
|
|
28
|
-
{
|
|
29
|
-
className: 'g-ui-select-search-item',
|
|
30
|
-
onClick: () => onChange?.(item.value),
|
|
31
|
-
},
|
|
32
|
-
item.label
|
|
33
|
-
)
|
|
34
|
-
)
|
|
35
|
-
)
|
|
36
|
-
);
|
|
4
|
+
return Autocomplete(...args);
|
|
37
5
|
}
|
package/src/components/Slider.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Div, after, state, when } from '@granularjs/core';
|
|
1
|
+
import { Div, after, state, when, list } from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren, classVar, classFlag, resolveBool, resolveValue } from '../utils.js';
|
|
3
3
|
|
|
4
4
|
export function Slider(...args) {
|
|
@@ -21,10 +21,14 @@ export function Slider(...args) {
|
|
|
21
21
|
...rest
|
|
22
22
|
} = props;
|
|
23
23
|
const currentState = state(resolveValue(value ?? min));
|
|
24
|
+
|
|
25
|
+
const hasMarks = after(marks).compute((m) => m && m.length > 0);
|
|
26
|
+
|
|
24
27
|
after(value).change((next) => {
|
|
25
28
|
if (next == null) return;
|
|
26
29
|
currentState.set(resolveValue(next));
|
|
27
30
|
});
|
|
31
|
+
|
|
28
32
|
const getBounds = () => {
|
|
29
33
|
const minValue = Number(resolveValue(min));
|
|
30
34
|
const maxValue = Number(resolveValue(max));
|
|
@@ -33,11 +37,13 @@ export function Slider(...args) {
|
|
|
33
37
|
}
|
|
34
38
|
return { minValue: Math.min(minValue, maxValue), maxValue: Math.max(minValue, maxValue) };
|
|
35
39
|
};
|
|
40
|
+
|
|
36
41
|
const getStep = () => {
|
|
37
42
|
const stepValue = Number(resolveValue(step));
|
|
38
43
|
if (Number.isFinite(stepValue) && stepValue > 0) return stepValue;
|
|
39
44
|
return 1;
|
|
40
45
|
};
|
|
46
|
+
|
|
41
47
|
const setValue = (next) => {
|
|
42
48
|
const { minValue, maxValue } = getBounds();
|
|
43
49
|
const stepValue = getStep();
|
|
@@ -47,6 +53,7 @@ export function Slider(...args) {
|
|
|
47
53
|
currentState.set(stepped);
|
|
48
54
|
onChange?.(stepped);
|
|
49
55
|
};
|
|
56
|
+
|
|
50
57
|
const percent = after(currentState).compute((v) => {
|
|
51
58
|
const { minValue, maxValue } = getBounds();
|
|
52
59
|
const range = maxValue - minValue;
|
|
@@ -54,6 +61,7 @@ export function Slider(...args) {
|
|
|
54
61
|
const pct = ((Number(v ?? minValue) - minValue) / range) * 100;
|
|
55
62
|
return Math.max(0, Math.min(100, pct));
|
|
56
63
|
});
|
|
64
|
+
|
|
57
65
|
const updateFromEvent = (ev, getRect) => {
|
|
58
66
|
const rect = getRect?.();
|
|
59
67
|
if (!rect || rect.width === 0) return;
|
|
@@ -62,6 +70,7 @@ export function Slider(...args) {
|
|
|
62
70
|
const { minValue, maxValue } = getBounds();
|
|
63
71
|
setValue(minValue + ratio * (maxValue - minValue));
|
|
64
72
|
};
|
|
73
|
+
|
|
65
74
|
const startDrag = (ev) => {
|
|
66
75
|
if (resolveBool(disabled)) return;
|
|
67
76
|
ev.preventDefault?.();
|
|
@@ -78,6 +87,7 @@ export function Slider(...args) {
|
|
|
78
87
|
window.addEventListener('pointermove', handleMove);
|
|
79
88
|
window.addEventListener('pointerup', handleUp);
|
|
80
89
|
};
|
|
90
|
+
|
|
81
91
|
return Div(
|
|
82
92
|
{
|
|
83
93
|
...rest,
|
|
@@ -99,9 +109,10 @@ export function Slider(...args) {
|
|
|
99
109
|
style: after(percent).compute((p) => ({ left: `${p}%` })),
|
|
100
110
|
})
|
|
101
111
|
),
|
|
112
|
+
when(hasMarks, () => Div({ className: 'g-ui-slider-marks-placeholder' })),
|
|
102
113
|
when(marks, () => Div(
|
|
103
114
|
{ className: 'g-ui-slider-marks' },
|
|
104
|
-
marks
|
|
115
|
+
list(marks, (mark) => SliderMark({ mark, getBounds }))
|
|
105
116
|
))
|
|
106
117
|
);
|
|
107
118
|
}
|
|
@@ -110,8 +121,8 @@ export const SliderMark = ({ mark, getBounds }) => {
|
|
|
110
121
|
const { minValue, maxValue } = getBounds();
|
|
111
122
|
const range = maxValue - minValue;
|
|
112
123
|
|
|
113
|
-
const value = mark.value ??
|
|
114
|
-
const label = mark.label ?? String(
|
|
124
|
+
const value = after(mark).compute((m) => m.value ?? m);
|
|
125
|
+
const label = after(mark).compute((m) => m.label ?? String(m.value) ?? value);
|
|
115
126
|
|
|
116
127
|
const markValue = Number(value);
|
|
117
128
|
let pct = 0;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Div } from '@granularjs/core';
|
|
1
|
+
import { Div, list } from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren } from '../utils.js';
|
|
3
3
|
|
|
4
4
|
export function Stepper(...args) {
|
|
@@ -6,10 +6,11 @@ export function Stepper(...args) {
|
|
|
6
6
|
const { active, items, className, ...rest } = props;
|
|
7
7
|
return Div(
|
|
8
8
|
{ ...rest, className: cx('g-ui-stepper', className) },
|
|
9
|
-
items
|
|
9
|
+
list(items, (item, idx) =>
|
|
10
10
|
Div(
|
|
11
11
|
{ className: cx('g-ui-stepper-item', [active, (value) => {
|
|
12
|
-
|
|
12
|
+
console.log('value', value, 'idx', idx);
|
|
13
|
+
if (idx.get() === value) return 'g-ui-stepper-active';
|
|
13
14
|
return '';
|
|
14
15
|
}]) },
|
|
15
16
|
Div({ className: 'g-ui-stepper-index' }, String(idx + 1)),
|
package/src/components/Switch.js
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
|
-
import { Input, Label, Span, when } from '@granularjs/core';
|
|
1
|
+
import { Input, Label, Span, when, after, state } from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren, classVar } from '../utils.js';
|
|
3
|
+
import { switchGroupContext } from './SwitchGroup.js';
|
|
3
4
|
|
|
4
5
|
export function Switch(...args) {
|
|
5
|
-
const { props } = splitPropsChildren(args, { size: 'md' });
|
|
6
|
-
const { label, size, className, style, inputProps, ...rest } = props;
|
|
6
|
+
const { props, rawProps } = splitPropsChildren(args, { size: 'md' });
|
|
7
|
+
const { label, size, className, style, inputProps, checked, value, ...rest } = props;
|
|
8
|
+
const { onChange } = rawProps;
|
|
9
|
+
const checkedState = state(checked);
|
|
10
|
+
const switchGroupState = switchGroupContext.state();
|
|
11
|
+
|
|
12
|
+
const switchGroupInfo = after(switchGroupState).compute((value) => {
|
|
13
|
+
return {
|
|
14
|
+
name: value.name,
|
|
15
|
+
type: value.name ? 'radio' : 'checkbox'
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
after(switchGroupState.selected).change((selected) => {
|
|
20
|
+
checkedState.set(selected === value.get());
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
after(checkedState).change((next) => {
|
|
24
|
+
onChange?.(next);
|
|
25
|
+
if (!next) return;
|
|
26
|
+
const selectedState = switchGroupState.get().selected
|
|
27
|
+
switchGroupState.set().selected = value.get();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
7
32
|
return Label(
|
|
8
33
|
{ className: cx('g-ui-switch', classVar('g-ui-switch-size-', size, 'md'), className) },
|
|
9
34
|
Input({
|
|
10
|
-
type:
|
|
35
|
+
type: switchGroupInfo.type,
|
|
36
|
+
name: switchGroupInfo.name,
|
|
37
|
+
value: value,
|
|
38
|
+
checked: checkedState,
|
|
11
39
|
className: cx('g-ui-switch-input', classVar('g-ui-switch-size-', size, 'md'), inputProps?.className),
|
|
12
40
|
...rest,
|
|
13
41
|
}),
|