@granularjs/ui 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@granularjs/ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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);
@@ -17,11 +17,6 @@ export function List(...args) {
17
17
  value && typeof value === 'object' && typeof value.tagName === 'string' &&
18
18
  value.tagName.toLowerCase() === 'li';
19
19
  const wrapChild = (child) => {
20
- console.log('INFO ABOUT ITEM', child,
21
- typeof child,
22
- typeof child?.tagName,
23
- child?.tagName?.toLowerCase()
24
- );
25
20
  const wrapValue = (value) => {
26
21
  if (value?.nodeType === 'granular-list-node') return value;
27
22
  if (value == null || value === false) return null;
@@ -55,12 +50,17 @@ export function List(...args) {
55
50
 
56
51
  export function ListItem(...args) {
57
52
  const { props, children } = splitPropsChildren(args, { withBorder: false });
58
- const { leftSection, rightSection, title, body, withBorder, className, ...rest } = props;
53
+ const { leftSection, rightSection, title, body, withBorder, className, verticalPadding, horizontalPadding, ...rest } = props;
59
54
  const hasStructured = after(title, body).compute(([nextTitle, nextBody]) => !!nextTitle || !!nextBody);
60
55
  return Li(
61
56
  {
62
57
  ...rest,
63
- className: cx('g-ui-list-item', classFlag('g-ui-list-item-border', withBorder), className),
58
+ className: cx('g-ui-list-item',
59
+ classFlag('g-ui-list-item-border', withBorder),
60
+ classVar('g-ui-list-item-vertical-padding-', verticalPadding, 'md'),
61
+ classVar('g-ui-list-item-horizontal-padding-', horizontalPadding, 'md'),
62
+ className
63
+ ),
64
64
  },
65
65
  Div(
66
66
  { className: 'g-ui-list-item-shell' },
@@ -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
- return Div({
8
- ...rest,
9
- className: cx('g-ui-progress-ring', [size, (value) => `g-ui-progress-ring-size-${value}`], className),
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
  }
@@ -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
- return Div({ ...rest, className: cx('g-ui-stack g-ui-gap-sm', className) }, children);
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 { TextInput } from './TextInput.js';
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
- const currentState = state(resolveValue(value) ?? ['', '']);
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 resolved = resolveValue(next);
13
- if (resolved == null) return;
14
- currentState.set(resolved);
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 setValue = (next) => {
18
- currentState.set(next);
19
- onChange?.(next);
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
- TextInput({
67
+ DateInput({
68
+ ...rest,
25
69
  size,
26
- type: 'text',
27
- inputMode: 'numeric',
28
- value: after(currentState).compute((current) => current?.[0] ?? ''),
29
- onInput: (ev) => {
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
- TextInput({
75
+ Span({ className: 'g-ui-range-picker-separator' }, '–'),
76
+ DateInput({
77
+ ...rest,
35
78
  size,
36
- type: 'text',
37
- inputMode: 'numeric',
38
- value: after(currentState).compute((current) => current?.[1] ?? ''),
39
- onInput: (ev) => {
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
  }
@@ -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 { Div, Span } from '@granularjs/core';
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
- const { props, rawProps } = splitPropsChildren(args, { data: [] });
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
  }
@@ -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.map((mark) => SliderMark({ mark, getBounds }))
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 ?? mark;
114
- const label = mark.label ?? String(mark.value) ?? value;
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.map((item, idx) =>
9
+ list(items, (item, idx) =>
10
10
  Div(
11
11
  { className: cx('g-ui-stepper-item', [active, (value) => {
12
- if (idx === value) return 'g-ui-stepper-active';
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)),