@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.
@@ -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: 'checkbox',
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
- return Div({ ...rest, className: cx('g-ui-switch-group', className) }, children);
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
  }
@@ -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
- headers.length
30
- ? Thead(Tr(headers.map((header) => Th(header))))
31
- : null,
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, { items: [] });
6
- const { items, className, ...rest } = props;
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
- { ...rest, className: cx('g-ui-timeline', className) },
9
- items.map((item) =>
10
- Div(
11
- { className: 'g-ui-timeline-item' },
12
- Div({ className: 'g-ui-timeline-dot' }),
13
- Div(
14
- { className: 'g-ui-timeline-content' },
15
- when(item.title, () => Div({ className: 'g-ui-timeline-title' }, item.title)),
16
- when(item.description, () => Div({ className: 'g-ui-timeline-desc' }, item.description)),
17
- item.content
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
  }
@@ -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, onClose, className, ...rest } = props;
7
- return Div(
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
- when(onClose, () => Button({ className: 'g-ui-toast-close', onClick: onClose }, '×'))
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, onClose, timeout, ...rest } = props;
7
- return Div(
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.map((item) =>
10
- Div(
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';
@@ -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>';