@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/dist/granular-ui.js +9462 -0
- package/dist/granular-ui.js.map +7 -0
- package/dist/granular-ui.min.js +308 -67
- 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/List.js +7 -7
- 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 +38 -13
- package/src/components/Timeline.js +373 -17
- 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 +294 -53
- 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/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
|
}),
|
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import { Div } from '@granularjs/core';
|
|
1
|
+
import { Div, context, state, after } from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren } from '../utils.js';
|
|
3
3
|
|
|
4
|
+
|
|
5
|
+
export const switchGroupContext = context({ name: null, selected: null });
|
|
6
|
+
|
|
4
7
|
export function SwitchGroup(...args) {
|
|
5
|
-
const { props, children } = splitPropsChildren(args);
|
|
6
|
-
const { className, ...rest } = props;
|
|
7
|
-
|
|
8
|
+
const { props, rawProps, children } = splitPropsChildren(args);
|
|
9
|
+
const { className, name, selected, onChange: _onChange, ...rest } = props;
|
|
10
|
+
const { onChange } = rawProps;
|
|
11
|
+
|
|
12
|
+
const scope = switchGroupContext.scope({ name: name?.get(), selected: selected?.get() });
|
|
13
|
+
|
|
14
|
+
after(scope.selected).change((next) => {
|
|
15
|
+
onChange?.(next);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
after(selected).change((next) => {
|
|
19
|
+
if(next === scope.selected.get()) return;
|
|
20
|
+
scope.set().selected = next;
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return scope.serve(Div({ ...rest, className: cx('g-ui-switch-group', className) }, children));
|
|
8
24
|
}
|
package/src/components/Table.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Table as HtmlTable, Thead, Tbody, Tr, Th, Td } from '@granularjs/core';
|
|
1
|
+
import { Table as HtmlTable, Thead, Tbody, Tr, Th, Td, list, when, after } from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren, classFlag } from '../utils.js';
|
|
3
3
|
|
|
4
4
|
export function Table(...args) {
|
|
@@ -10,10 +10,13 @@ export function Table(...args) {
|
|
|
10
10
|
highlightOnHover,
|
|
11
11
|
withBorder,
|
|
12
12
|
withColumnBorders,
|
|
13
|
+
withRowBorders,
|
|
13
14
|
className,
|
|
14
15
|
style,
|
|
15
16
|
...rest
|
|
16
17
|
} = props;
|
|
18
|
+
|
|
19
|
+
const hasHeaders = after(headers).compute((next) => next.length > 0);
|
|
17
20
|
return HtmlTable(
|
|
18
21
|
{
|
|
19
22
|
...rest,
|
|
@@ -23,20 +26,42 @@ export function Table(...args) {
|
|
|
23
26
|
classFlag('g-ui-table-hover', highlightOnHover),
|
|
24
27
|
classFlag('g-ui-table-with-border', withBorder),
|
|
25
28
|
classFlag('g-ui-table-column-borders', withColumnBorders),
|
|
29
|
+
classFlag('g-ui-table-row-borders', withRowBorders),
|
|
26
30
|
className
|
|
27
31
|
),
|
|
28
32
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Tbody(
|
|
33
|
-
rows.map((row) =>
|
|
34
|
-
Tr(
|
|
35
|
-
Array.isArray(row)
|
|
36
|
-
? row.map((cell) => Td(cell))
|
|
37
|
-
: Object.values(row).map((cell) => Td(cell))
|
|
38
|
-
)
|
|
39
|
-
)
|
|
40
|
-
)
|
|
33
|
+
when(hasHeaders, () => Thead(
|
|
34
|
+
TableRow(headers, true)
|
|
35
|
+
)),
|
|
36
|
+
Tbody(list(rows, (row) => TableRow(row, false)))
|
|
41
37
|
);
|
|
42
38
|
}
|
|
39
|
+
const TableRow = (row, header) => {
|
|
40
|
+
const isArray = after(row).compute((next) => Array.isArray(next));
|
|
41
|
+
|
|
42
|
+
const ObjectRow = (row) => {
|
|
43
|
+
const cells = after(row).compute((next) => Object.values(next));
|
|
44
|
+
return ArrayRow(cells)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ArrayRow = (row) => {
|
|
48
|
+
return list(row, (next) => {
|
|
49
|
+
return header ? TableHeaderCell(next) : TableCell(next)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Tr(
|
|
54
|
+
when(isArray,
|
|
55
|
+
() => ArrayRow(row),
|
|
56
|
+
() => ObjectRow(row)
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const TableCell = (content) => {
|
|
62
|
+
return Td(content)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const TableHeaderCell = (content) => {
|
|
66
|
+
return Th(content)
|
|
67
|
+
}
|
|
@@ -1,22 +1,378 @@
|
|
|
1
|
-
import { Div, when } from '@granularjs/core';
|
|
2
|
-
import { cx, splitPropsChildren } from '../utils.js';
|
|
1
|
+
import { Div, when, list, after, resolve, state, Img, Span } from '@granularjs/core';
|
|
2
|
+
import { cx, splitPropsChildren, classVar } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
const PIN_CENTER_OFFSET = { xs: 6, sm: 8, md: 10, lg: 12, xl: 14 };
|
|
5
|
+
const LINE_WIDTH_CSS = { xs: '2px', sm: '3px', md: '4px', lg: '6px', xl: '8px' };
|
|
6
|
+
const PIN_HALF_CSS = { xs: '6px', sm: '8px', md: '10px', lg: '12px', xl: '14px' };
|
|
7
|
+
const PIN_COLUMN_CENTER_PX = 14;
|
|
8
|
+
|
|
9
|
+
function resolveActiveColor(color) {
|
|
10
|
+
if (color == null || color === '') return 'var(--g-ui-primary)';
|
|
11
|
+
const s = String(color).trim();
|
|
12
|
+
if (s.startsWith('#')) return s;
|
|
13
|
+
return `var(--g-ui-${s})`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getWeights(items) {
|
|
17
|
+
const list = items ?? [];
|
|
18
|
+
return list.map((item) => {
|
|
19
|
+
if (item == null) return 1;
|
|
20
|
+
const w = Number(item.weight);
|
|
21
|
+
return Number.isFinite(w) && w >= 0 ? w : 1;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getWeightContext(items) {
|
|
26
|
+
const n = (items ?? []).length;
|
|
27
|
+
if (n < 2) return { totalWeight: 0, cumulativeWeights: [0] };
|
|
28
|
+
const weights = getWeights(items);
|
|
29
|
+
const segmentWeights = weights.slice(0, n - 1);
|
|
30
|
+
const totalWeight = segmentWeights.reduce((s, w) => s + w, 0);
|
|
31
|
+
const cumulativeWeights = [0];
|
|
32
|
+
for (let i = 0; i < segmentWeights.length; i++) {
|
|
33
|
+
cumulativeWeights.push(cumulativeWeights[i] + segmentWeights[i]);
|
|
34
|
+
}
|
|
35
|
+
return { totalWeight, cumulativeWeights };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function progressToActiveStepWeighted(progressPct, cumulativeWeights, totalWeight, n) {
|
|
39
|
+
if (n < 1 || totalWeight <= 0) return 0;
|
|
40
|
+
const pct = Math.max(0, Math.min(100, progressPct));
|
|
41
|
+
for (let k = n - 1; k >= 0; k--) {
|
|
42
|
+
const threshold = (cumulativeWeights[k] / totalWeight) * 100;
|
|
43
|
+
if (pct >= threshold) return k;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function computeActiveStepAndFill(mode, active, progress, elapsedMs, stepDurationsMs, totalDurationMs, items) {
|
|
49
|
+
const m = mode;
|
|
50
|
+
const n = (items ?? []).length;
|
|
51
|
+
if (n === 0) return { activeStep: 0, progressPct: 0 };
|
|
52
|
+
|
|
53
|
+
if (m === 'step') {
|
|
54
|
+
const step = Math.max(0, Math.min(n - 1, Math.floor(active ?? 0)));
|
|
55
|
+
return { activeStep: step, progressPct: 0 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (m === 'percent') {
|
|
59
|
+
const pct = Math.max(0, Math.min(100, Number(progress) || 0));
|
|
60
|
+
const ctx = getWeightContext(items);
|
|
61
|
+
const step = progressToActiveStepWeighted(pct, ctx.cumulativeWeights, ctx.totalWeight, n);
|
|
62
|
+
return { activeStep: step, progressPct: pct };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (m === 'time') {
|
|
66
|
+
const elapsed = Number(elapsedMs) || 0;
|
|
67
|
+
const durations = stepDurationsMs;
|
|
68
|
+
const total = totalDurationMs;
|
|
69
|
+
let totalDuration = 0;
|
|
70
|
+
if (Array.isArray(durations) && durations.length >= n) {
|
|
71
|
+
totalDuration = durations.slice(0, n).reduce((s, d) => s + (Number(d) || 0), 0);
|
|
72
|
+
} else if (typeof total === 'number' && total > 0) {
|
|
73
|
+
totalDuration = total;
|
|
74
|
+
}
|
|
75
|
+
if (totalDuration <= 0) return { activeStep: 0, progressPct: 0 };
|
|
76
|
+
const segment = totalDuration / n;
|
|
77
|
+
let step = 0;
|
|
78
|
+
if (Array.isArray(durations) && durations.length >= n) {
|
|
79
|
+
let cumulative = 0;
|
|
80
|
+
for (let i = 0; i < n; i++) {
|
|
81
|
+
cumulative += Number(durations[i]) || 0;
|
|
82
|
+
if (elapsed < cumulative) {
|
|
83
|
+
step = i;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
step = i;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
step = Math.min(n - 1, Math.floor(elapsed / segment));
|
|
90
|
+
}
|
|
91
|
+
const progressPct = Math.min(100, (elapsed / totalDuration) * 100);
|
|
92
|
+
const ctx = getWeightContext(items);
|
|
93
|
+
const stepWeighted = progressToActiveStepWeighted(progressPct, ctx.cumulativeWeights, ctx.totalWeight, n);
|
|
94
|
+
return { activeStep: stepWeighted, progressPct };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { activeStep: 0, progressPct: 0 };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function computeSegmentFillPercent(mode, activeStep, progressPct, segmentIndex, n, weightContext) {
|
|
101
|
+
if (n < 2 || segmentIndex < 0 || segmentIndex >= n - 1) return 0;
|
|
102
|
+
if (mode === 'step') {
|
|
103
|
+
return activeStep > segmentIndex ? 100 : 0;
|
|
104
|
+
}
|
|
105
|
+
const { totalWeight, cumulativeWeights } = weightContext ?? getWeightContext([]);
|
|
106
|
+
if (totalWeight <= 0) return 0;
|
|
107
|
+
const segmentStart = (cumulativeWeights[segmentIndex] / totalWeight) * 100;
|
|
108
|
+
const segmentEnd = (cumulativeWeights[segmentIndex + 1] / totalWeight) * 100;
|
|
109
|
+
if (progressPct <= segmentStart) return 0;
|
|
110
|
+
if (progressPct >= segmentEnd) return 100;
|
|
111
|
+
const range = segmentEnd - segmentStart;
|
|
112
|
+
return range <= 0 ? 0 : ((progressPct - segmentStart) / range) * 100;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let timelineIdCounter = 0;
|
|
116
|
+
|
|
117
|
+
function measureSegmentLayout(timelineId) {
|
|
118
|
+
const el = document.getElementById(timelineId);
|
|
119
|
+
if (!el) return [];
|
|
120
|
+
const itemEls = el.querySelectorAll('.g-ui-timeline-item');
|
|
121
|
+
if (itemEls.length < 2) return [];
|
|
122
|
+
const pinSize = el.dataset.pinSize || 'md';
|
|
123
|
+
const offset = PIN_CENTER_OFFSET[pinSize] ?? PIN_CENTER_OFFSET.md;
|
|
124
|
+
const segments = [];
|
|
125
|
+
for (let i = 0; i < itemEls.length - 1; i++) {
|
|
126
|
+
const top = itemEls[i].offsetTop + offset;
|
|
127
|
+
const height = itemEls[i + 1].offsetTop - itemEls[i].offsetTop;
|
|
128
|
+
segments.push({ top, height });
|
|
129
|
+
}
|
|
130
|
+
return segments;
|
|
131
|
+
}
|
|
3
132
|
|
|
4
133
|
export function Timeline(...args) {
|
|
5
|
-
const { props } = splitPropsChildren(args, {
|
|
6
|
-
|
|
134
|
+
const { props, rawProps } = splitPropsChildren(args, {
|
|
135
|
+
items: [],
|
|
136
|
+
mode: 'step',
|
|
137
|
+
active: 0,
|
|
138
|
+
progress: 0,
|
|
139
|
+
elapsedMs: 0,
|
|
140
|
+
stepDurationsMs: null,
|
|
141
|
+
totalDurationMs: null,
|
|
142
|
+
clickable: false,
|
|
143
|
+
pinRadius: 'md',
|
|
144
|
+
reverseActive: false,
|
|
145
|
+
lineWidth: 'md',
|
|
146
|
+
pinSize: 'md',
|
|
147
|
+
activeColor: 'primary',
|
|
148
|
+
align: 'left',
|
|
149
|
+
pinMode: 'default',
|
|
150
|
+
});
|
|
151
|
+
const {
|
|
152
|
+
items,
|
|
153
|
+
mode,
|
|
154
|
+
active,
|
|
155
|
+
progress,
|
|
156
|
+
elapsedMs,
|
|
157
|
+
stepDurationsMs,
|
|
158
|
+
totalDurationMs,
|
|
159
|
+
clickable,
|
|
160
|
+
pinRadius,
|
|
161
|
+
reverseActive,
|
|
162
|
+
lineWidth,
|
|
163
|
+
pinSize,
|
|
164
|
+
activeColor,
|
|
165
|
+
align,
|
|
166
|
+
pinMode,
|
|
167
|
+
className,
|
|
168
|
+
...rest
|
|
169
|
+
} = props;
|
|
170
|
+
const { onChange } = rawProps;
|
|
171
|
+
|
|
172
|
+
const activeColorResolved = after(activeColor).compute((c) => resolveActiveColor(resolve(c)));
|
|
173
|
+
|
|
174
|
+
const timelineId = `g-ui-timeline-${++timelineIdCounter}`;
|
|
175
|
+
const segmentLayout = state([]);
|
|
176
|
+
const state_ = after(mode, active, progress, elapsedMs, stepDurationsMs, totalDurationMs, items).compute(
|
|
177
|
+
(values) => {
|
|
178
|
+
const [mode, active, progress, elapsedMs, stepDurationsMs, totalDurationMs, items] = values;
|
|
179
|
+
return computeActiveStepAndFill(
|
|
180
|
+
mode,
|
|
181
|
+
active,
|
|
182
|
+
progress,
|
|
183
|
+
elapsedMs,
|
|
184
|
+
stepDurationsMs,
|
|
185
|
+
totalDurationMs,
|
|
186
|
+
items
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
const reverseTrackLayout = after(segmentLayout, reverseActive).compute(([segs, rev]) => {
|
|
191
|
+
if (!resolve(rev) || !segs?.length) return null;
|
|
192
|
+
const first = segs[0];
|
|
193
|
+
let totalHeight = 0;
|
|
194
|
+
for (const s of segs) totalHeight += s.height;
|
|
195
|
+
return { top: first.top, height: totalHeight };
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const reverseFillHeight = after(state_).compute((s) =>
|
|
199
|
+
s?.progressPct != null ? `${Math.max(0, Math.min(100, s.progressPct))}%` : '0%'
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
const showTrack = after(mode).compute((m) => {
|
|
205
|
+
const v = resolve(m);
|
|
206
|
+
return v === 'time' || v === 'percent' || v === 'step';
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const resolvedMode = after(mode).compute((m) => resolve(m));
|
|
210
|
+
|
|
211
|
+
function scheduleMeasure() {
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
const segments = measureSegmentLayout(timelineId);
|
|
214
|
+
if (segments.length) segmentLayout.set(segments);
|
|
215
|
+
}, 0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
after(items).change(() => scheduleMeasure());
|
|
219
|
+
after(pinSize).change(() => scheduleMeasure());
|
|
220
|
+
scheduleMeasure();
|
|
221
|
+
|
|
7
222
|
return Div(
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
)
|
|
20
|
-
|
|
223
|
+
{
|
|
224
|
+
...rest,
|
|
225
|
+
id: timelineId,
|
|
226
|
+
'data-pin-size': after(pinSize).compute((s) => resolve(s) ?? 'md'),
|
|
227
|
+
'data-active-color': after(activeColor).compute((a) => {
|
|
228
|
+
const v = resolve(a);
|
|
229
|
+
if (v == null || typeof v !== 'string') return 'primary';
|
|
230
|
+
const s = String(v).trim();
|
|
231
|
+
if (s.startsWith('#')) return 'custom';
|
|
232
|
+
return s || 'primary';
|
|
233
|
+
}),
|
|
234
|
+
style: after(activeColor, lineWidth, pinSize).compute(([a, lw, ps]) => {
|
|
235
|
+
const res = {
|
|
236
|
+
'--g-ui-timeline-line-width': LINE_WIDTH_CSS[resolve(lw)] ?? '4px',
|
|
237
|
+
'--g-ui-timeline-track-offset': `calc(${PIN_COLUMN_CENTER_PX}px - var(--g-ui-timeline-line-width) / 2)`,
|
|
238
|
+
'--g-ui-timeline-pin-half': PIN_HALF_CSS[resolve(ps)] ?? '10px',
|
|
239
|
+
};
|
|
240
|
+
const colorVal = resolve(a);
|
|
241
|
+
if (colorVal && String(colorVal).trim().startsWith('#'))
|
|
242
|
+
res['--g-ui-timeline-active-color'] = String(colorVal).trim();
|
|
243
|
+
return res;
|
|
244
|
+
}),
|
|
245
|
+
className: cx(
|
|
246
|
+
'g-ui-timeline',
|
|
247
|
+
after(mode).compute((m) => (m ? `g-ui-timeline-mode-${resolve(m)}` : '')),
|
|
248
|
+
after(showTrack).compute((show) => (show ? 'g-ui-timeline-has-track' : '')),
|
|
249
|
+
after(clickable).compute((c) => (resolve(c) ? 'g-ui-timeline-clickable' : '')),
|
|
250
|
+
after(reverseActive).compute((r) => (resolve(r) ? 'g-ui-timeline-reverse' : '')),
|
|
251
|
+
after(align).compute((a) => (resolve(a) === 'right' ? 'g-ui-timeline-align-right' : '')),
|
|
252
|
+
classVar('g-ui-timeline-pin-radius-', pinRadius, 'md'),
|
|
253
|
+
classVar('g-ui-timeline-line-width-', lineWidth, 'md'),
|
|
254
|
+
classVar('g-ui-timeline-pin-size-', pinSize, 'md'),
|
|
255
|
+
after(pinMode).compute((p) => (p ? `g-ui-timeline-pin-mode-${resolve(p)}` : '')),
|
|
256
|
+
className
|
|
257
|
+
),
|
|
258
|
+
},
|
|
259
|
+
when(showTrack, () =>
|
|
260
|
+
after(reverseActive).compute((rev) => {
|
|
261
|
+
if (resolve(rev)) {
|
|
262
|
+
return Div(
|
|
263
|
+
{
|
|
264
|
+
className: 'g-ui-timeline-track-segment g-ui-timeline-track-reverse',
|
|
265
|
+
style: after(reverseTrackLayout).compute((l) =>
|
|
266
|
+
l ? { top: `${l.top}px`, height: `${l.height}px` } : {}
|
|
267
|
+
),
|
|
268
|
+
},
|
|
269
|
+
Div({
|
|
270
|
+
className: 'g-ui-timeline-track-fill',
|
|
271
|
+
style: after(reverseFillHeight).compute((h) => (h ? { height: h } : { height: '0%' })),
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return list(segmentLayout, (seg, idx) => {
|
|
276
|
+
const segStyle = after(seg).compute((s) =>
|
|
277
|
+
s ? { top: `${s.top}px`, height: `${s.height}px` } : {}
|
|
278
|
+
);
|
|
279
|
+
const fillPct = after(state_, idx, items, resolvedMode).compute(([s, i, its, m]) => {
|
|
280
|
+
const itsList = its ?? [];
|
|
281
|
+
const n = itsList.length;
|
|
282
|
+
const weightContext = getWeightContext(itsList);
|
|
283
|
+
return computeSegmentFillPercent(
|
|
284
|
+
m,
|
|
285
|
+
s?.activeStep ?? 0,
|
|
286
|
+
s?.progressPct ?? 0,
|
|
287
|
+
resolve(i) ?? 0,
|
|
288
|
+
n,
|
|
289
|
+
weightContext
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
const fillHeight = after(fillPct).compute((p) => `${Math.max(0, Math.min(100, p))}%`);
|
|
293
|
+
return Div(
|
|
294
|
+
{
|
|
295
|
+
className: 'g-ui-timeline-track-segment',
|
|
296
|
+
style: after(segStyle).compute((x) => x),
|
|
297
|
+
},
|
|
298
|
+
Div({
|
|
299
|
+
className: 'g-ui-timeline-track-fill',
|
|
300
|
+
style: after(fillHeight).compute((h) => (h ? { height: h } : { height: '0%' })),
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
})
|
|
305
|
+
),
|
|
306
|
+
list(items, (item, idx) => {
|
|
307
|
+
const itemState = after(state_, idx, items, reverseActive).compute(([s, i, its, rev]) => {
|
|
308
|
+
const step = s?.activeStep ?? 0;
|
|
309
|
+
const index = resolve(i) ?? 0;
|
|
310
|
+
const n = (its ?? []).length;
|
|
311
|
+
const logicalIndex = resolve(rev) ? n - 1 - index : index;
|
|
312
|
+
if (logicalIndex < step) return 'completed';
|
|
313
|
+
if (logicalIndex === step) return 'active';
|
|
314
|
+
return 'future';
|
|
315
|
+
});
|
|
316
|
+
const itemClass = after(itemState).compute((st) => (st ? `g-ui-timeline-item-${st}` : ''));
|
|
317
|
+
const handleClick =
|
|
318
|
+
resolve(clickable) && typeof onChange === 'function'
|
|
319
|
+
? () => {
|
|
320
|
+
const i = resolve(idx);
|
|
321
|
+
if (typeof i === 'number') onChange(i);
|
|
322
|
+
}
|
|
323
|
+
: undefined;
|
|
324
|
+
const pinModeVal = after(pinMode).compute((p) => resolve(p) ?? 'default');
|
|
325
|
+
const pinExtra = after(pinModeVal, item).compute(([mode, it]) => {
|
|
326
|
+
const m = mode ?? 'default';
|
|
327
|
+
if (m === 'icon' && (it?.icon != null || it?.pinIcon != null))
|
|
328
|
+
return Span(
|
|
329
|
+
{ className: 'g-ui-timeline-pin-icon material-symbols-outlined' },
|
|
330
|
+
it.icon ?? it.pinIcon ?? ''
|
|
331
|
+
);
|
|
332
|
+
if (m === 'image' && (it?.image != null || it?.pinImage != null || it?.src != null))
|
|
333
|
+
return Img({
|
|
334
|
+
className: 'g-ui-timeline-pin-image',
|
|
335
|
+
src: it.image ?? it.pinImage ?? it.src,
|
|
336
|
+
alt: it.pinImageAlt ?? '',
|
|
337
|
+
});
|
|
338
|
+
if (m === 'custom' && it?.pinContent != null) return it.pinContent;
|
|
339
|
+
return null;
|
|
340
|
+
});
|
|
341
|
+
const hasPinExtra = after(pinModeVal, item).compute(([mode, it]) => {
|
|
342
|
+
const m = mode ?? 'default';
|
|
343
|
+
if (m === 'icon') return it?.icon != null || it?.pinIcon != null;
|
|
344
|
+
if (m === 'image') return it?.image != null || it?.pinImage != null || it?.src != null;
|
|
345
|
+
if (m === 'custom') return it?.pinContent != null;
|
|
346
|
+
return false;
|
|
347
|
+
});
|
|
348
|
+
const dotBlock = Div(
|
|
349
|
+
{ className: 'g-ui-timeline-dot' },
|
|
350
|
+
Div({ className: 'g-ui-timeline-dot-inner' }),
|
|
351
|
+
when(hasPinExtra, () => pinExtra)
|
|
352
|
+
);
|
|
353
|
+
const contentBlock = Div(
|
|
354
|
+
{ className: 'g-ui-timeline-content' },
|
|
355
|
+
when(item.title, () => Div({ className: 'g-ui-timeline-title' }, item.title)),
|
|
356
|
+
when(item.description, () =>
|
|
357
|
+
Div({ className: 'g-ui-timeline-desc' }, item.description)
|
|
358
|
+
),
|
|
359
|
+
item.content
|
|
360
|
+
);
|
|
361
|
+
return Div(
|
|
362
|
+
{
|
|
363
|
+
className: cx('g-ui-timeline-item', itemClass),
|
|
364
|
+
style: after(activeColorResolved).compute((c) =>
|
|
365
|
+
c ? { '--g-ui-timeline-active-color': c } : undefined
|
|
366
|
+
),
|
|
367
|
+
onClick: handleClick,
|
|
368
|
+
role: handleClick ? 'button' : undefined,
|
|
369
|
+
tabIndex: handleClick ? 0 : undefined,
|
|
370
|
+
},
|
|
371
|
+
Div({ className: 'g-ui-dot-wrapper' },
|
|
372
|
+
dotBlock,
|
|
373
|
+
),
|
|
374
|
+
contentBlock
|
|
375
|
+
);
|
|
376
|
+
})
|
|
21
377
|
);
|
|
22
378
|
}
|
package/src/components/Toast.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
import { Div, Button, when } from '@granularjs/core';
|
|
1
|
+
import { Div, Button, when, state} from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren } from '../utils.js';
|
|
3
|
+
import { closeSvg } from '../theme/icons.js';
|
|
4
|
+
import { Icon } from './Icon.js';
|
|
3
5
|
|
|
4
6
|
export function Toast(...args) {
|
|
5
|
-
const { props, children } = splitPropsChildren(args);
|
|
6
|
-
const { title,
|
|
7
|
-
|
|
7
|
+
const { props, rawProps, children } = splitPropsChildren(args);
|
|
8
|
+
const { title, className, ...rest } = props;
|
|
9
|
+
const { onClose } = rawProps;
|
|
10
|
+
const visible = state(true);
|
|
11
|
+
const close = () => {
|
|
12
|
+
visible.set(false);
|
|
13
|
+
onClose?.();
|
|
14
|
+
console.log('close');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return when(visible, () => Div(
|
|
8
18
|
{ ...rest, className: cx('g-ui-toast', className) },
|
|
9
19
|
Div(
|
|
10
20
|
{ className: 'g-ui-toast-row' },
|
|
11
21
|
when(title, () => Div({ className: 'g-ui-toast-title' }, title)),
|
|
12
|
-
|
|
22
|
+
Button({ className: 'g-ui-toast-close', onClick: close },
|
|
23
|
+
Icon({ innerHTML: closeSvg })
|
|
24
|
+
)
|
|
13
25
|
),
|
|
14
26
|
children
|
|
15
|
-
)
|
|
27
|
+
))
|
|
16
28
|
}
|
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import { Div, when } from '@granularjs/core';
|
|
1
|
+
import { Div, when, list, portal } from '@granularjs/core';
|
|
2
2
|
import { cx, splitPropsChildren } from '../utils.js';
|
|
3
|
+
import { Toast } from './Toast.js';
|
|
3
4
|
|
|
4
5
|
export function ToastStack(...args) {
|
|
5
|
-
const { props } = splitPropsChildren(args, { items: [] });
|
|
6
|
-
const { items, className,
|
|
7
|
-
|
|
6
|
+
const { props, rawProps } = splitPropsChildren(args, { items: [] });
|
|
7
|
+
const { items, className, timeout, ...rest } = props;
|
|
8
|
+
const { onClose } = rawProps;
|
|
9
|
+
return portal(Div(
|
|
8
10
|
{ ...rest, className: cx('g-ui-toast-stack', className) },
|
|
9
|
-
items
|
|
10
|
-
|
|
11
|
-
{ className: cx('g-ui-toast', [timeout, 'g-ui-toast-auto']) },
|
|
12
|
-
Div(
|
|
13
|
-
{ className: 'g-ui-toast-row' },
|
|
14
|
-
when(item.title, () => Div({ className: 'g-ui-toast-title' }, item.title)),
|
|
15
|
-
when(onClose, () => Div({ className: 'g-ui-toast-close', onClick: () => onClose(item) }, '×'))
|
|
16
|
-
),
|
|
17
|
-
item.message
|
|
18
|
-
)
|
|
11
|
+
list(items, (item) =>
|
|
12
|
+
Toast({ title: item.title, onClose: () => onClose?.(item) }, item.message)
|
|
19
13
|
)
|
|
20
|
-
);
|
|
14
|
+
));
|
|
21
15
|
}
|
package/src/index.js
CHANGED
|
@@ -76,6 +76,7 @@ export { SearchInput } from './components/SearchInput.js';
|
|
|
76
76
|
export { CopyButton } from './components/CopyButton.js';
|
|
77
77
|
export { ProgressRing } from './components/ProgressRing.js';
|
|
78
78
|
export { Toast } from './components/Toast.js';
|
|
79
|
+
export { Autocomplete } from './components/Autocomplete.js';
|
|
79
80
|
export { SelectSearch } from './components/SelectSearch.js';
|
|
80
81
|
export { SwitchGroup } from './components/SwitchGroup.js';
|
|
81
82
|
export { RangePicker } from './components/RangePicker.js';
|
package/src/theme/icons.js
CHANGED
|
@@ -7,4 +7,5 @@ export const searchSvg = '<svg xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
|
7
7
|
export const plusSvg = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z"/></svg>';
|
|
8
8
|
export const editSvg = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h357l-80 80H200v560h560v-278l80-80v358q0 33-23.5 56.5T760-120H200Zm280-360v-80h240v80H480Zm0 160v-80h320v80H480Zm0 160v-80h320v80H480ZM360-360v-80h80v80h-80Zm0 160v-80h80v80h-80Zm0 160v-80h80v80h-80Zm160-320h280l-36-37 37-37v74H520Zm-160 0h80v-80h-80v80ZM120-600v-160l160-160h160l-80 80H200v240h-80Zm80-240v-80 80Z"/></svg>';
|
|
9
9
|
export const deleteSvg = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>';
|
|
10
|
-
export const calendarTodaySvg = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Z"/></svg>';
|
|
10
|
+
export const calendarTodaySvg = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Z"/></svg>';
|
|
11
|
+
export const keyboardArrowDownSvg = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z"/></svg>';
|