@monimesl/dot-renderer-react 0.1.0-alpha.3 → 0.1.0-alpha.4

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/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createEvent, toList, toText } from "@monimesl/dot-core";
2
- import { Fragment, createContext, createElement, useContext, useEffect, useEffectEvent, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { Children, Fragment, createContext, createElement, isValidElement, useContext, useEffect, useEffectEvent, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
3
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
4
  import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Banknote, Bell, Calendar, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, CircleAlert, CircleCheck, CircleHelp, Clock, Copy, CreditCard, Download, Droplet, Ellipsis, Eye, EyeOff, File, Gift, Heart, History, House, Image, Info, Landmark, LayoutGrid, List, Lock, LogOut, Mail, MapPin, Minus, Pencil, Phone, Plus, QrCode, Receipt, RefreshCw, Search, Send, Settings, Share2, ShoppingCart, Smartphone, Star, Trash2, TriangleAlert, Upload, User, Users, Wallet, Wifi, X, Zap } from "lucide";
5
+ import { encode } from "uqr";
5
6
  import { createRoot } from "react-dom/client";
6
7
  //#region src/theme.ts
7
8
  const FONT_FAMILY = "system-ui, sans-serif";
@@ -82,6 +83,165 @@ const ICON_SIZE = {
82
83
  large: 28
83
84
  };
84
85
  //#endregion
86
+ //#region src/components/field.tsx
87
+ function Field({ label, hint, error, children }) {
88
+ const { colors } = useTheme();
89
+ const note = error || hint;
90
+ return /* @__PURE__ */ jsxs("label", {
91
+ className: "dot-field",
92
+ style: {
93
+ display: "flex",
94
+ flexDirection: "column",
95
+ gap: SPACE.small
96
+ },
97
+ children: [
98
+ label ? /* @__PURE__ */ jsx(FieldLabel, { children: label }) : null,
99
+ children,
100
+ note ? /* @__PURE__ */ jsx("span", {
101
+ style: {
102
+ fontSize: FONT_SIZE.caption,
103
+ color: error ? colors.danger : colors.subtle
104
+ },
105
+ children: note
106
+ }) : null
107
+ ]
108
+ });
109
+ }
110
+ function FieldLabel({ children }) {
111
+ return /* @__PURE__ */ jsx("span", {
112
+ style: {
113
+ fontSize: FONT_SIZE.caption,
114
+ fontWeight: 500
115
+ },
116
+ children
117
+ });
118
+ }
119
+ //#endregion
120
+ //#region src/components/field-box.ts
121
+ function useFieldBox(error, disabled) {
122
+ const { colors, radius } = useTheme();
123
+ const [focused, setFocused] = useState(false);
124
+ let borderColor = colors.border;
125
+ if (error) borderColor = colors.danger;
126
+ else if (focused) borderColor = colors.primary;
127
+ return {
128
+ style: {
129
+ border: `1px solid ${borderColor}`,
130
+ borderRadius: radius,
131
+ background: colors.surface,
132
+ color: colors.text,
133
+ fontSize: 16,
134
+ opacity: disabled ? DISABLED_OPACITY : 1
135
+ },
136
+ onFocus: () => setFocused(true),
137
+ onBlur: () => setFocused(false)
138
+ };
139
+ }
140
+ //#endregion
141
+ //#region src/components/typed-text.ts
142
+ function useTypedText(value, sendsChanges, show = (text) => text) {
143
+ const [text, setText] = useState(() => show(value ?? ""));
144
+ const [awaitingEcho, setAwaitingEcho] = useState([]);
145
+ const [previousValue, setPreviousValue] = useState(value);
146
+ if (value !== previousValue) {
147
+ setPreviousValue(value);
148
+ const echo = awaitingEcho.indexOf(value ?? "");
149
+ if (echo >= 0) setAwaitingEcho(awaitingEcho.slice(echo + 1));
150
+ else {
151
+ setAwaitingEcho([]);
152
+ setText(show(value ?? ""));
153
+ }
154
+ }
155
+ const type = (next, sent = next) => {
156
+ setText(next);
157
+ if (sendsChanges && value !== void 0) setAwaitingEcho([...awaitingEcho, sent]);
158
+ };
159
+ return [text, type];
160
+ }
161
+ //#endregion
162
+ //#region src/components/amount-input.tsx
163
+ const AMOUNT_FONT_SIZE = 28;
164
+ const AmountInput = ({ value, currency, decimals = 0, label, placeholder = "0", hint, error, disabled, onChange }) => {
165
+ const { colors } = useTheme();
166
+ const places = Math.max(0, Math.round(decimals));
167
+ const [text, typeText] = useTypedText(amountText(value), onChange !== void 0, (sent) => sent === "" ? "" : formatAmount(Number(sent), places));
168
+ const box = useFieldBox(error, disabled);
169
+ return /* @__PURE__ */ jsx(Field, {
170
+ label,
171
+ hint,
172
+ error,
173
+ children: /* @__PURE__ */ jsxs("span", {
174
+ className: "dot-amount-input",
175
+ style: {
176
+ ...box.style,
177
+ display: "flex",
178
+ alignItems: "baseline",
179
+ gap: SPACE.medium,
180
+ padding: "10px 14px"
181
+ },
182
+ children: [currency ? /* @__PURE__ */ jsx("span", {
183
+ style: {
184
+ color: colors.subtle,
185
+ fontSize: 20,
186
+ fontWeight: 500
187
+ },
188
+ children: currency
189
+ }) : null, /* @__PURE__ */ jsx("input", {
190
+ value: text,
191
+ placeholder,
192
+ inputMode: places > 0 ? "decimal" : "numeric",
193
+ disabled,
194
+ "aria-invalid": error ? true : void 0,
195
+ onFocus: box.onFocus,
196
+ onBlur: box.onBlur,
197
+ onChange: (event) => {
198
+ const typed = cleanAmount(event.currentTarget.value, places);
199
+ const amount = parseAmount(typed);
200
+ typeText(typed, amount === null ? "" : String(amount));
201
+ onChange?.(createEvent("change", { value: amount }));
202
+ },
203
+ style: {
204
+ flexGrow: 1,
205
+ minWidth: 0,
206
+ padding: 0,
207
+ border: "none",
208
+ outline: "none",
209
+ background: "transparent",
210
+ color: "inherit",
211
+ font: "inherit",
212
+ fontSize: AMOUNT_FONT_SIZE,
213
+ fontWeight: 600
214
+ }
215
+ })]
216
+ })
217
+ });
218
+ };
219
+ function amountText(value) {
220
+ if (value === void 0) return;
221
+ return Number.isFinite(value) ? String(value) : "";
222
+ }
223
+ function cleanAmount(typed, places) {
224
+ const [whole = "", ...fractions] = typed.replace(/[^\d.]/g, "").split(".");
225
+ const digits = whole.replace(/^0+/, "") || whole.slice(0, 1);
226
+ if (places === 0 || fractions.length === 0) return groupThousands(digits);
227
+ const fraction = fractions.join("").slice(0, places);
228
+ return `${groupThousands(digits) || "0"}.${fraction}`;
229
+ }
230
+ function parseAmount(text) {
231
+ const plain = text.replaceAll(",", "");
232
+ return plain === "" ? null : Number(plain);
233
+ }
234
+ function formatAmount(amount, places) {
235
+ const [whole = "", fraction] = Math.abs(amount).toFixed(places).split(".");
236
+ const grouped = groupThousands(whole);
237
+ return fraction === void 0 ? grouped : `${grouped}.${fraction}`;
238
+ }
239
+ function groupThousands(digits) {
240
+ const groups = [];
241
+ for (let end = digits.length; end > 0; end -= 3) groups.unshift(digits.slice(Math.max(0, end - 3), end));
242
+ return groups.join(",");
243
+ }
244
+ //#endregion
85
245
  //#region src/components/spinner.tsx
86
246
  function LoadingIcon({ size }) {
87
247
  return /* @__PURE__ */ jsxs("svg", {
@@ -161,20 +321,251 @@ const Button = ({ variant = "primary", disabled, loading, onClick, children }) =
161
321
  });
162
322
  };
163
323
  //#endregion
164
- //#region src/components/divider.tsx
165
- const Divider = () => {
324
+ //#region src/components/carousel.tsx
325
+ const SLIDE_DURATION = 350;
326
+ const SLIDE_EASING = "cubic-bezier(0.22, 0.61, 0.36, 1)";
327
+ const SWIPE_DISTANCE = .2;
328
+ const SWIPE_SPEED = .4;
329
+ const EDGE_RESISTANCE = .3;
330
+ const TAP_SLOP = 6;
331
+ const Carousel = ({ autoplay = false, interval = 4e3, loop = false, indicator = "dots", gap = "none", effect = "slide", index, onChange, children }) => {
166
332
  const { colors } = useTheme();
167
- return /* @__PURE__ */ jsx("div", {
168
- className: "dot-divider",
169
- role: "separator",
170
- style: {
171
- height: 1,
172
- alignSelf: "stretch",
173
- flexShrink: 0,
174
- background: colors.border
333
+ const slides = slidesOf(children);
334
+ const count = slides.length;
335
+ const looping = loop && count > 1;
336
+ const first = looping ? 1 : 0;
337
+ const last = looping ? count + 1 : count - 1;
338
+ const [position, setPosition] = useState(() => first + slideIndex(index ?? 0, count));
339
+ const [animated, setAnimated] = useState(true);
340
+ const [drag, setDrag] = useState(0);
341
+ const [dragging, setDragging] = useState(false);
342
+ const [width, setWidth] = useState(0);
343
+ const [previousIndex, setPreviousIndex] = useState(index);
344
+ const viewport = useRef(null);
345
+ const gesture = useRef(null);
346
+ const swiped = useRef(false);
347
+ if (index !== previousIndex) {
348
+ setPreviousIndex(index);
349
+ if (index !== void 0) {
350
+ setAnimated(true);
351
+ setPosition(first + slideIndex(index, count));
175
352
  }
353
+ }
354
+ const shown = clamp(position, 0, last);
355
+ const current = count === 0 ? 0 : wrap(shown - first, count);
356
+ const moveTo = (target) => {
357
+ const next = clamp(target, 0, last);
358
+ setAnimated(true);
359
+ setPosition(next);
360
+ const nextIndex = wrap(next - first, count);
361
+ if (nextIndex !== current) onChange?.(createEvent("change", { index: nextIndex }));
362
+ };
363
+ const step = (direction) => {
364
+ const beyondEnd = current + direction < 0 || current + direction >= count;
365
+ moveTo(!looping && beyondEnd ? shown : shown + direction);
366
+ };
367
+ const advance = useEffectEvent(() => {
368
+ if (!looping && current === count - 1) moveTo(first);
369
+ else moveTo(shown + 1);
370
+ });
371
+ useEffect(() => {
372
+ if (!autoplay || count < 2 || dragging) return;
373
+ const timer = setTimeout(advance, interval);
374
+ return () => clearTimeout(timer);
375
+ }, [
376
+ autoplay,
377
+ count,
378
+ dragging,
379
+ interval,
380
+ current
381
+ ]);
382
+ useEffect(() => {
383
+ if (!looping || position !== 0 && position !== count + 1) return;
384
+ const timer = setTimeout(() => {
385
+ setAnimated(false);
386
+ setPosition(position === 0 ? count : 1);
387
+ }, SLIDE_DURATION);
388
+ return () => clearTimeout(timer);
389
+ }, [
390
+ looping,
391
+ position,
392
+ count
393
+ ]);
394
+ const gestureOf = (event) => {
395
+ const touch = gesture.current;
396
+ return touch?.pointerId === event.pointerId ? touch : null;
397
+ };
398
+ const endGesture = () => {
399
+ gesture.current = null;
400
+ setDragging(false);
401
+ setDrag(0);
402
+ setAnimated(true);
403
+ };
404
+ const onPointerDown = (event) => {
405
+ if (count < 2 || !event.isPrimary) return;
406
+ gesture.current = {
407
+ pointerId: event.pointerId,
408
+ startX: event.clientX,
409
+ startY: event.clientY,
410
+ startTime: event.timeStamp,
411
+ swiping: false
412
+ };
413
+ swiped.current = false;
414
+ setDragging(true);
415
+ setWidth(viewport.current?.clientWidth ?? 0);
416
+ };
417
+ const onPointerMove = (event) => {
418
+ const touch = gestureOf(event);
419
+ if (!touch) return;
420
+ const dx = event.clientX - touch.startX;
421
+ const dy = event.clientY - touch.startY;
422
+ if (!touch.swiping) {
423
+ if (Math.abs(dx) < TAP_SLOP && Math.abs(dy) < TAP_SLOP) return;
424
+ if (Math.abs(dy) > Math.abs(dx)) {
425
+ endGesture();
426
+ return;
427
+ }
428
+ touch.swiping = true;
429
+ swiped.current = true;
430
+ event.currentTarget.setPointerCapture?.(event.pointerId);
431
+ setAnimated(false);
432
+ }
433
+ const pastEnd = !looping && (current === 0 && dx > 0 || current === count - 1 && dx < 0);
434
+ setDrag(pastEnd ? dx * EDGE_RESISTANCE : dx);
435
+ };
436
+ const onPointerUp = (event) => {
437
+ const touch = gestureOf(event);
438
+ if (!touch) return;
439
+ endGesture();
440
+ if (!touch.swiping) return;
441
+ const dx = event.clientX - touch.startX;
442
+ const speed = Math.abs(dx) / Math.max(1, event.timeStamp - touch.startTime);
443
+ if (Math.abs(dx) > width * SWIPE_DISTANCE || speed > SWIPE_SPEED) step(dx < 0 ? 1 : -1);
444
+ else moveTo(shown);
445
+ };
446
+ const gapWidth = SPACE[gap];
447
+ const transition = animated ? `transform ${SLIDE_DURATION}ms ${SLIDE_EASING}` : "none";
448
+ const swipedSlides = width > 0 ? drag / (width + gapWidth) : 0;
449
+ const track = looping ? [
450
+ {
451
+ ...slides[count - 1],
452
+ key: "copy-of-last"
453
+ },
454
+ ...slides,
455
+ {
456
+ ...slides[0],
457
+ key: "copy-of-first"
458
+ }
459
+ ] : slides;
460
+ return /* @__PURE__ */ jsxs("div", {
461
+ className: "dot-carousel",
462
+ role: "region",
463
+ "aria-roledescription": "carousel",
464
+ style: {
465
+ display: "flex",
466
+ flexDirection: "column",
467
+ gap: SPACE.medium,
468
+ minWidth: 0
469
+ },
470
+ children: [/* @__PURE__ */ jsx("div", {
471
+ ref: viewport,
472
+ onPointerDown,
473
+ onPointerMove,
474
+ onPointerUp,
475
+ onPointerCancel: endGesture,
476
+ onDragStart: (event) => event.preventDefault(),
477
+ onClickCapture: (event) => {
478
+ if (swiped.current) {
479
+ swiped.current = false;
480
+ event.stopPropagation();
481
+ }
482
+ },
483
+ style: {
484
+ overflow: "hidden",
485
+ touchAction: "pan-y",
486
+ userSelect: dragging ? "none" : void 0
487
+ },
488
+ children: /* @__PURE__ */ jsx("div", {
489
+ style: {
490
+ display: "flex",
491
+ gap: gapWidth,
492
+ transform: `translateX(calc(${-shown} * (100% + ${gapWidth}px) + ${drag}px))`,
493
+ transition
494
+ },
495
+ children: track.map((slide, trackPosition) => {
496
+ const visible = trackPosition === shown;
497
+ const distance = Math.abs(trackPosition - shown + swipedSlides);
498
+ const scale = 1 - Math.min(distance, 1) * .15000000000000002;
499
+ return /* @__PURE__ */ jsx("div", {
500
+ role: "group",
501
+ "aria-roledescription": "slide",
502
+ "aria-label": `${wrap(trackPosition - first, count) + 1} of ${count}`,
503
+ "aria-hidden": !visible,
504
+ inert: !visible,
505
+ style: {
506
+ display: "grid",
507
+ flex: "0 0 100%",
508
+ minWidth: 0,
509
+ transform: effect === "scale" ? `scale(${scale})` : void 0,
510
+ transition: effect === "scale" ? transition : void 0
511
+ },
512
+ children: slide.node
513
+ }, slide.key);
514
+ })
515
+ })
516
+ }), indicator === "dots" && count > 1 ? /* @__PURE__ */ jsx("div", {
517
+ style: {
518
+ display: "flex",
519
+ justifyContent: "center"
520
+ },
521
+ children: slides.map((slide, slideIndex) => {
522
+ const active = slideIndex === current;
523
+ return /* @__PURE__ */ jsx("button", {
524
+ type: "button",
525
+ "aria-label": `Slide ${slideIndex + 1} of ${count}`,
526
+ "aria-current": active,
527
+ onClick: () => moveTo(first + slideIndex),
528
+ style: {
529
+ padding: "6px 3px",
530
+ border: "none",
531
+ background: "none",
532
+ cursor: "pointer"
533
+ },
534
+ children: /* @__PURE__ */ jsx("span", { style: {
535
+ display: "block",
536
+ width: active ? 18 : 6,
537
+ height: 6,
538
+ borderRadius: 3,
539
+ background: active ? colors.primary : colors.subtle,
540
+ transition: "width 0.3s, background 0.3s"
541
+ } })
542
+ }, slide.key);
543
+ })
544
+ }) : null]
176
545
  });
177
546
  };
547
+ function slidesOf(children, keyPrefix = "") {
548
+ const slides = [];
549
+ for (const child of Children.toArray(children)) {
550
+ if (!isValidElement(child)) continue;
551
+ const key = `${keyPrefix}${String(child.key)}`;
552
+ if (child.type === Fragment) slides.push(...slidesOf(child.props.children, `${key}/`));
553
+ else slides.push({
554
+ key,
555
+ node: child
556
+ });
557
+ }
558
+ return slides;
559
+ }
560
+ function clamp(value, min, max) {
561
+ return Math.max(min, Math.min(value, max));
562
+ }
563
+ function slideIndex(index, count) {
564
+ return clamp(Math.round(index), 0, count - 1);
565
+ }
566
+ function wrap(value, count) {
567
+ return (value % count + count) % count;
568
+ }
178
569
  //#endregion
179
570
  //#region src/components/tones.ts
180
571
  const STATUS_ICONS = {
@@ -295,6 +686,203 @@ const Icon = ({ name, size = "medium", tone = "default" }) => {
295
686
  });
296
687
  };
297
688
  //#endregion
689
+ //#region src/components/chip.tsx
690
+ const Chip = ({ label, icon, selected = false, disabled, onTap }) => {
691
+ const { colors } = useTheme();
692
+ const color = selected ? colors.surface : colors.text;
693
+ return /* @__PURE__ */ jsxs("button", {
694
+ type: "button",
695
+ className: "dot-chip",
696
+ "aria-pressed": selected,
697
+ disabled,
698
+ onClick: onTap ? () => onTap(createEvent("tap")) : void 0,
699
+ style: {
700
+ display: "inline-flex",
701
+ alignItems: "center",
702
+ gap: 6,
703
+ flexShrink: 0,
704
+ minHeight: 34,
705
+ padding: "0 14px",
706
+ borderRadius: 17,
707
+ border: `1px solid ${selected ? colors.primary : colors.border}`,
708
+ background: selected ? colors.primary : colors.surface,
709
+ color,
710
+ font: "inherit",
711
+ fontSize: FONT_SIZE.caption,
712
+ fontWeight: 500,
713
+ whiteSpace: "nowrap",
714
+ opacity: disabled ? DISABLED_OPACITY : 1,
715
+ cursor: disabled ? "default" : "pointer"
716
+ },
717
+ children: [icon ? /* @__PURE__ */ jsx(IconGraphic, {
718
+ name: icon,
719
+ size: ICON_SIZE.small,
720
+ color
721
+ }) : null, label]
722
+ });
723
+ };
724
+ //#endregion
725
+ //#region src/components/code-input.tsx
726
+ const CodeInput = ({ value, length = 6, label, hint, error, secure, disabled, onChange, onComplete }) => {
727
+ const { colors, radius } = useTheme();
728
+ const [text, typeText] = useTypedText(value, onChange !== void 0);
729
+ const [focused, setFocused] = useState(false);
730
+ const count = Math.max(1, Math.round(length));
731
+ const digits = text.slice(0, count);
732
+ const next = Math.min(digits.length, count - 1);
733
+ const boxes = Array.from({ length: count }, (_, index) => {
734
+ const digit = digits[index];
735
+ let borderColor = colors.border;
736
+ if (error) borderColor = colors.danger;
737
+ else if (focused && index === next) borderColor = colors.primary;
738
+ return /* @__PURE__ */ jsx("span", {
739
+ style: {
740
+ display: "flex",
741
+ alignItems: "center",
742
+ justifyContent: "center",
743
+ flex: "1 1 0",
744
+ maxWidth: 52,
745
+ height: 52,
746
+ border: `1.5px solid ${borderColor}`,
747
+ borderRadius: radius,
748
+ background: colors.surface,
749
+ fontSize: 22,
750
+ fontWeight: 600
751
+ },
752
+ children: digit !== void 0 && secure ? /* @__PURE__ */ jsx("span", {
753
+ "aria-hidden": "true",
754
+ style: {
755
+ width: 12,
756
+ height: 12,
757
+ borderRadius: "50%",
758
+ background: colors.text
759
+ }
760
+ }) : digit
761
+ }, index);
762
+ });
763
+ return /* @__PURE__ */ jsx(Field, {
764
+ label,
765
+ hint,
766
+ error,
767
+ children: /* @__PURE__ */ jsxs("span", {
768
+ className: "dot-code-input",
769
+ style: {
770
+ position: "relative",
771
+ display: "flex",
772
+ gap: SPACE.medium,
773
+ color: colors.text,
774
+ opacity: disabled ? DISABLED_OPACITY : 1
775
+ },
776
+ children: [boxes, /* @__PURE__ */ jsx("input", {
777
+ value: digits,
778
+ inputMode: "numeric",
779
+ autoComplete: "one-time-code",
780
+ maxLength: count,
781
+ disabled,
782
+ "aria-invalid": error ? true : void 0,
783
+ onFocus: () => setFocused(true),
784
+ onBlur: () => setFocused(false),
785
+ onSelect: (event) => {
786
+ const input = event.currentTarget;
787
+ const end = input.value.length;
788
+ if (input.selectionStart !== end || input.selectionEnd !== end) input.setSelectionRange(end, end);
789
+ },
790
+ onChange: (event) => {
791
+ const typed = event.currentTarget.value.replace(/\D/g, "").slice(0, count);
792
+ if (typed === digits) return;
793
+ typeText(typed);
794
+ onChange?.(createEvent("change", { value: typed }));
795
+ if (typed.length === count) onComplete?.(createEvent("complete", { value: typed }));
796
+ },
797
+ style: {
798
+ position: "absolute",
799
+ inset: 0,
800
+ width: "100%",
801
+ height: "100%",
802
+ padding: 0,
803
+ border: "none",
804
+ opacity: 0,
805
+ fontSize: 16,
806
+ cursor: disabled ? "default" : "text"
807
+ }
808
+ })]
809
+ })
810
+ });
811
+ };
812
+ //#endregion
813
+ //#region src/components/divider.tsx
814
+ const Divider = () => {
815
+ const { colors } = useTheme();
816
+ return /* @__PURE__ */ jsx("div", {
817
+ className: "dot-divider",
818
+ role: "separator",
819
+ style: {
820
+ height: 1,
821
+ alignSelf: "stretch",
822
+ flexShrink: 0,
823
+ background: colors.border
824
+ }
825
+ });
826
+ };
827
+ //#endregion
828
+ //#region src/components/empty-state.tsx
829
+ const EmptyState = ({ icon, title, text, children }) => {
830
+ const { colors } = useTheme();
831
+ return /* @__PURE__ */ jsxs("div", {
832
+ className: "dot-empty-state",
833
+ style: {
834
+ display: "flex",
835
+ flexDirection: "column",
836
+ alignItems: "center",
837
+ gap: SPACE.medium,
838
+ padding: "32px 16px",
839
+ textAlign: "center",
840
+ color: colors.text
841
+ },
842
+ children: [
843
+ icon ? /* @__PURE__ */ jsx("span", {
844
+ style: {
845
+ display: "flex",
846
+ alignItems: "center",
847
+ justifyContent: "center",
848
+ width: 64,
849
+ height: 64,
850
+ borderRadius: "50%",
851
+ background: colors.muted
852
+ },
853
+ children: /* @__PURE__ */ jsx(IconGraphic, {
854
+ name: icon,
855
+ size: ICON_SIZE.large,
856
+ color: colors.subtle
857
+ })
858
+ }) : null,
859
+ title ? /* @__PURE__ */ jsx("span", {
860
+ style: {
861
+ fontSize: 17,
862
+ fontWeight: 600
863
+ },
864
+ children: title
865
+ }) : null,
866
+ text ? /* @__PURE__ */ jsx("span", {
867
+ style: {
868
+ maxWidth: 300,
869
+ fontSize: FONT_SIZE.body,
870
+ color: colors.subtle
871
+ },
872
+ children: text
873
+ }) : null,
874
+ children ? /* @__PURE__ */ jsx("div", {
875
+ style: {
876
+ display: "flex",
877
+ gap: SPACE.medium,
878
+ paddingTop: SPACE.small
879
+ },
880
+ children
881
+ }) : null
882
+ ]
883
+ });
884
+ };
885
+ //#endregion
298
886
  //#region src/components/feedback.tsx
299
887
  const ProgressBar = ({ value = 0 }) => {
300
888
  const { colors } = useTheme();
@@ -390,6 +978,107 @@ const Footer = ({ children }) => {
390
978
  });
391
979
  };
392
980
  //#endregion
981
+ //#region src/components/pill.tsx
982
+ function Pill({ background, color, children }) {
983
+ return /* @__PURE__ */ jsx("span", {
984
+ style: {
985
+ minWidth: 18,
986
+ padding: "1px 6px",
987
+ borderRadius: 9,
988
+ background,
989
+ color,
990
+ fontSize: 11,
991
+ fontWeight: 600,
992
+ lineHeight: "16px",
993
+ textAlign: "center",
994
+ whiteSpace: "nowrap"
995
+ },
996
+ children
997
+ });
998
+ }
999
+ //#endregion
1000
+ //#region src/components/grid.tsx
1001
+ const TILE_SIZE = 52;
1002
+ const Grid = ({ columns = 4, gap = "medium", children }) => /* @__PURE__ */ jsx("div", {
1003
+ className: "dot-grid",
1004
+ style: {
1005
+ display: "grid",
1006
+ gridTemplateColumns: `repeat(${Math.max(1, Math.round(columns))}, minmax(0, 1fr))`,
1007
+ gap: SPACE[gap]
1008
+ },
1009
+ children
1010
+ });
1011
+ const GridItem = ({ label, icon, image, badge, onTap }) => {
1012
+ const { colors, radius } = useTheme();
1013
+ const tileRadius = radius * CARD_RADIUS_SCALE;
1014
+ let picture = null;
1015
+ if (image) picture = /* @__PURE__ */ jsx("img", {
1016
+ src: image,
1017
+ alt: "",
1018
+ width: TILE_SIZE,
1019
+ height: TILE_SIZE,
1020
+ style: {
1021
+ display: "block",
1022
+ borderRadius: tileRadius,
1023
+ objectFit: "cover"
1024
+ }
1025
+ });
1026
+ else if (icon) picture = /* @__PURE__ */ jsx(IconGraphic, {
1027
+ name: icon,
1028
+ size: 24,
1029
+ color: colors.primary
1030
+ });
1031
+ return /* @__PURE__ */ jsxs("div", {
1032
+ className: "dot-grid-item",
1033
+ role: onTap ? "button" : void 0,
1034
+ onClick: onTap ? () => onTap(createEvent("tap")) : void 0,
1035
+ style: {
1036
+ display: "flex",
1037
+ flexDirection: "column",
1038
+ alignItems: "center",
1039
+ gap: 6,
1040
+ padding: `${SPACE.small}px 0`,
1041
+ cursor: onTap ? "pointer" : void 0
1042
+ },
1043
+ children: [/* @__PURE__ */ jsxs("span", {
1044
+ style: {
1045
+ position: "relative",
1046
+ display: "flex",
1047
+ alignItems: "center",
1048
+ justifyContent: "center",
1049
+ width: TILE_SIZE,
1050
+ height: TILE_SIZE,
1051
+ borderRadius: tileRadius,
1052
+ background: colors.muted
1053
+ },
1054
+ children: [picture, badge ? /* @__PURE__ */ jsx("span", {
1055
+ style: {
1056
+ position: "absolute",
1057
+ top: -6,
1058
+ right: -10
1059
+ },
1060
+ children: /* @__PURE__ */ jsx(Pill, {
1061
+ background: colors.danger,
1062
+ color: "#ffffff",
1063
+ children: badge
1064
+ })
1065
+ }) : null]
1066
+ }), label ? /* @__PURE__ */ jsx("span", {
1067
+ style: {
1068
+ display: "-webkit-box",
1069
+ WebkitLineClamp: 2,
1070
+ WebkitBoxOrient: "vertical",
1071
+ overflow: "hidden",
1072
+ width: "100%",
1073
+ textAlign: "center",
1074
+ fontSize: FONT_SIZE.caption,
1075
+ color: colors.text
1076
+ },
1077
+ children: label
1078
+ }) : null]
1079
+ });
1080
+ };
1081
+ //#endregion
393
1082
  //#region src/components/image.tsx
394
1083
  const Image$1 = ({ src, alt, width, height, fit = "cover", shape = "rounded", onLoad, onError }) => {
395
1084
  const { radius } = useTheme();
@@ -419,82 +1108,6 @@ const Image$1 = ({ src, alt, width, height, fit = "cover", shape = "rounded", on
419
1108
  });
420
1109
  };
421
1110
  //#endregion
422
- //#region src/components/field.tsx
423
- function Field({ label, hint, error, children }) {
424
- const { colors } = useTheme();
425
- const note = error || hint;
426
- return /* @__PURE__ */ jsxs("label", {
427
- className: "dot-field",
428
- style: {
429
- display: "flex",
430
- flexDirection: "column",
431
- gap: SPACE.small
432
- },
433
- children: [
434
- label ? /* @__PURE__ */ jsx(FieldLabel, { children: label }) : null,
435
- children,
436
- note ? /* @__PURE__ */ jsx("span", {
437
- style: {
438
- fontSize: FONT_SIZE.caption,
439
- color: error ? colors.danger : colors.subtle
440
- },
441
- children: note
442
- }) : null
443
- ]
444
- });
445
- }
446
- function FieldLabel({ children }) {
447
- return /* @__PURE__ */ jsx("span", {
448
- style: {
449
- fontSize: FONT_SIZE.caption,
450
- fontWeight: 500
451
- },
452
- children
453
- });
454
- }
455
- //#endregion
456
- //#region src/components/field-box.ts
457
- function useFieldBox(error, disabled) {
458
- const { colors, radius } = useTheme();
459
- const [focused, setFocused] = useState(false);
460
- let borderColor = colors.border;
461
- if (error) borderColor = colors.danger;
462
- else if (focused) borderColor = colors.primary;
463
- return {
464
- style: {
465
- border: `1px solid ${borderColor}`,
466
- borderRadius: radius,
467
- background: colors.surface,
468
- color: colors.text,
469
- fontSize: 16,
470
- opacity: disabled ? DISABLED_OPACITY : 1
471
- },
472
- onFocus: () => setFocused(true),
473
- onBlur: () => setFocused(false)
474
- };
475
- }
476
- //#endregion
477
- //#region src/components/typed-text.ts
478
- function useTypedText(value, sendsChanges) {
479
- const [text, setText] = useState(value ?? "");
480
- const [awaitingEcho, setAwaitingEcho] = useState([]);
481
- const [previousValue, setPreviousValue] = useState(value);
482
- if (value !== previousValue) {
483
- setPreviousValue(value);
484
- const echo = awaitingEcho.indexOf(value ?? "");
485
- if (echo >= 0) setAwaitingEcho(awaitingEcho.slice(echo + 1));
486
- else {
487
- setAwaitingEcho([]);
488
- setText(value ?? "");
489
- }
490
- }
491
- const type = (next) => {
492
- setText(next);
493
- if (sendsChanges && value !== void 0) setAwaitingEcho([...awaitingEcho, next]);
494
- };
495
- return [text, type];
496
- }
497
- //#endregion
498
1111
  //#region src/components/input.tsx
499
1112
  const HTML_TYPES = {
500
1113
  text: {
@@ -681,6 +1294,55 @@ const ListItem = ({ title, subtitle, value, icon, image, onTap, children }) => {
681
1294
  });
682
1295
  };
683
1296
  //#endregion
1297
+ //#region src/components/qr-code.tsx
1298
+ const DARK = "#000000";
1299
+ const LIGHT = "#ffffff";
1300
+ const QUIET_ZONE = 2;
1301
+ const QRCode = ({ value, size = 200, label }) => {
1302
+ const { radius } = useTheme();
1303
+ const drawing = useMemo(() => value ? drawCode(value) : null, [value]);
1304
+ return /* @__PURE__ */ jsx("svg", {
1305
+ className: "dot-qr-code",
1306
+ role: "img",
1307
+ "aria-label": label ?? value,
1308
+ width: size,
1309
+ height: size,
1310
+ viewBox: drawing ? `0 0 ${drawing.size} ${drawing.size}` : "0 0 1 1",
1311
+ shapeRendering: "crispEdges",
1312
+ style: {
1313
+ display: "block",
1314
+ flexShrink: 0,
1315
+ borderRadius: radius,
1316
+ background: LIGHT
1317
+ },
1318
+ children: drawing ? /* @__PURE__ */ jsx("path", {
1319
+ d: drawing.path,
1320
+ fill: DARK
1321
+ }) : null
1322
+ });
1323
+ };
1324
+ function drawCode(value) {
1325
+ let modules;
1326
+ try {
1327
+ modules = encode(value, {
1328
+ ecc: "M",
1329
+ border: QUIET_ZONE
1330
+ }).data;
1331
+ } catch {
1332
+ return null;
1333
+ }
1334
+ const squares = [];
1335
+ modules.forEach((row, y) => {
1336
+ row.forEach((dark, x) => {
1337
+ if (dark) squares.push(`M${x} ${y}h1v1h-1z`);
1338
+ });
1339
+ });
1340
+ return {
1341
+ size: modules.length,
1342
+ path: squares.join("")
1343
+ };
1344
+ }
1345
+ //#endregion
684
1346
  //#region src/components/animate.ts
685
1347
  function animate(element, keyframes, options) {
686
1348
  if (!element || typeof element.animate !== "function") return;
@@ -917,6 +1579,85 @@ const Skeleton = ({ shape = "text", lines = 1, height }) => {
917
1579
  });
918
1580
  };
919
1581
  //#endregion
1582
+ //#region src/components/tabs.tsx
1583
+ const TabsContext = createContext({
1584
+ value: void 0,
1585
+ segmented: false,
1586
+ choose: void 0
1587
+ });
1588
+ const Tabs = ({ value, variant = "underline", onChange, children }) => {
1589
+ const { colors, radius } = useTheme();
1590
+ const segmented = variant === "segmented";
1591
+ const choose = onChange ? (next) => {
1592
+ if (next !== value) onChange(createEvent("change", { value: next }));
1593
+ } : void 0;
1594
+ return /* @__PURE__ */ jsx("div", {
1595
+ className: "dot-tabs",
1596
+ role: "tablist",
1597
+ style: {
1598
+ display: "flex",
1599
+ overflowX: "auto",
1600
+ ...segmented ? {
1601
+ gap: 2,
1602
+ padding: 3,
1603
+ borderRadius: radius,
1604
+ background: colors.muted
1605
+ } : { borderBottom: `1px solid ${colors.border}` }
1606
+ },
1607
+ children: /* @__PURE__ */ jsx(TabsContext.Provider, {
1608
+ value: {
1609
+ value,
1610
+ segmented,
1611
+ choose
1612
+ },
1613
+ children
1614
+ })
1615
+ });
1616
+ };
1617
+ const Tab = ({ value, label, badge }) => {
1618
+ const { colors, radius } = useTheme();
1619
+ const { value: chosen, segmented, choose } = useContext(TabsContext);
1620
+ const selected = value !== void 0 && value === chosen;
1621
+ let color = colors.subtle;
1622
+ if (selected) color = segmented ? colors.text : colors.primary;
1623
+ return /* @__PURE__ */ jsxs("button", {
1624
+ type: "button",
1625
+ role: "tab",
1626
+ "aria-selected": selected,
1627
+ onClick: choose && value !== void 0 ? () => choose(value) : void 0,
1628
+ style: {
1629
+ display: "flex",
1630
+ alignItems: "center",
1631
+ justifyContent: "center",
1632
+ gap: 6,
1633
+ flex: "1 0 auto",
1634
+ minHeight: segmented ? 36 : 44,
1635
+ padding: "0 12px",
1636
+ border: "none",
1637
+ font: "inherit",
1638
+ fontSize: FONT_SIZE.body,
1639
+ fontWeight: selected ? 600 : 500,
1640
+ whiteSpace: "nowrap",
1641
+ color,
1642
+ cursor: "pointer",
1643
+ ...segmented ? {
1644
+ borderRadius: radius - 2,
1645
+ background: selected ? colors.surface : "transparent",
1646
+ boxShadow: selected ? "0 1px 3px rgba(0, 0, 0, 0.15)" : "none"
1647
+ } : {
1648
+ marginBottom: -1,
1649
+ background: "transparent",
1650
+ borderBottom: `2px solid ${selected ? colors.primary : "transparent"}`
1651
+ }
1652
+ },
1653
+ children: [label, badge ? /* @__PURE__ */ jsx(Pill, {
1654
+ background: colors.primary,
1655
+ color: colors.surface,
1656
+ children: badge
1657
+ }) : null]
1658
+ });
1659
+ };
1660
+ //#endregion
920
1661
  //#region src/components/text.tsx
921
1662
  const VARIANTS = {
922
1663
  body: { fontSize: FONT_SIZE.body },
@@ -1230,11 +1971,18 @@ const View = ({ direction = "column", gap = "medium", align = "stretch", justify
1230
1971
  //#endregion
1231
1972
  //#region src/components/index.ts
1232
1973
  const builtins = {
1974
+ AmountInput,
1233
1975
  Badge,
1234
1976
  Button,
1977
+ Carousel,
1235
1978
  Checkbox,
1979
+ Chip,
1980
+ CodeInput,
1236
1981
  Divider,
1982
+ EmptyState,
1237
1983
  Footer,
1984
+ Grid,
1985
+ GridItem,
1238
1986
  Icon,
1239
1987
  Image: Image$1,
1240
1988
  Input,
@@ -1243,6 +1991,7 @@ const builtins = {
1243
1991
  Notice,
1244
1992
  Option,
1245
1993
  ProgressBar,
1994
+ QRCode,
1246
1995
  Radio,
1247
1996
  RadioGroup,
1248
1997
  Select,
@@ -1250,6 +1999,8 @@ const builtins = {
1250
1999
  Skeleton,
1251
2000
  Spinner,
1252
2001
  Switch,
2002
+ Tab,
2003
+ Tabs,
1253
2004
  Text,
1254
2005
  TextArea,
1255
2006
  Toast,