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

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,6 +1,7 @@
1
1
  import { createEvent, toList, toText } from "@monimesl/dot-core";
2
- import { Fragment, createContext, createElement, useContext, useMemo, useState, useSyncExternalStore } from "react";
3
- import { jsx } from "react/jsx-runtime";
2
+ import { Fragment, createContext, createElement, useContext, useEffect, useEffectEvent, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
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";
4
5
  import { createRoot } from "react-dom/client";
5
6
  //#region src/theme.ts
6
7
  const FONT_FAMILY = "system-ui, sans-serif";
@@ -13,7 +14,11 @@ const DEFAULT_COLORS = {
13
14
  text: "#111111",
14
15
  subtle: "#8e8e93",
15
16
  muted: "#e9e9eb",
16
- border: "#e9e9eb"
17
+ border: "#e9e9eb",
18
+ info: "#0a64d6",
19
+ success: "#1a8a4a",
20
+ warning: "#b25e00",
21
+ danger: "#d0312d"
17
22
  },
18
23
  dark: {
19
24
  primary: "#f5f5f5",
@@ -22,7 +27,11 @@ const DEFAULT_COLORS = {
22
27
  text: "#f5f5f5",
23
28
  subtle: "#8e8e93",
24
29
  muted: "#2c2c2e",
25
- border: "#2a2a2c"
30
+ border: "#2a2a2c",
31
+ info: "#4c9dff",
32
+ success: "#3ecf72",
33
+ warning: "#ffb340",
34
+ danger: "#ff6961"
26
35
  }
27
36
  };
28
37
  function resolveTheme(theme, systemScheme) {
@@ -39,7 +48,11 @@ function resolveTheme(theme, systemScheme) {
39
48
  text: pick(colors.text, defaults.text),
40
49
  subtle: defaults.subtle,
41
50
  muted: defaults.muted,
42
- border: defaults.border
51
+ border: defaults.border,
52
+ info: defaults.info,
53
+ success: defaults.success,
54
+ warning: defaults.warning,
55
+ danger: defaults.danger
43
56
  },
44
57
  radius: theme.radius ?? DEFAULT_RADIUS
45
58
  };
@@ -50,7 +63,7 @@ function useTheme() {
50
63
  }
51
64
  //#endregion
52
65
  //#region src/components/tokens.ts
53
- const GAP = {
66
+ const SPACE = {
54
67
  none: 0,
55
68
  small: 4,
56
69
  medium: 8,
@@ -63,22 +76,77 @@ const FONT_SIZE = {
63
76
  };
64
77
  const CARD_RADIUS_SCALE = 1.5;
65
78
  const DISABLED_OPACITY = .6;
79
+ const ICON_SIZE = {
80
+ small: 16,
81
+ medium: 20,
82
+ large: 28
83
+ };
84
+ //#endregion
85
+ //#region src/components/spinner.tsx
86
+ function LoadingIcon({ size }) {
87
+ return /* @__PURE__ */ jsxs("svg", {
88
+ className: "dot-spinner",
89
+ width: size,
90
+ height: size,
91
+ viewBox: "0 0 24 24",
92
+ fill: "none",
93
+ stroke: "currentColor",
94
+ strokeWidth: 3,
95
+ role: "progressbar",
96
+ "aria-label": "Loading",
97
+ style: { flexShrink: 0 },
98
+ children: [/* @__PURE__ */ jsx("circle", {
99
+ cx: "12",
100
+ cy: "12",
101
+ r: "9",
102
+ strokeOpacity: .25
103
+ }), /* @__PURE__ */ jsx("path", {
104
+ d: "M21 12a9 9 0 0 0-9-9",
105
+ strokeLinecap: "round",
106
+ children: /* @__PURE__ */ jsx("animateTransform", {
107
+ attributeName: "transform",
108
+ type: "rotate",
109
+ from: "0 12 12",
110
+ to: "360 12 12",
111
+ dur: "0.8s",
112
+ repeatCount: "indefinite"
113
+ })
114
+ })]
115
+ });
116
+ }
117
+ const Spinner = ({ size = "medium" }) => {
118
+ const { colors } = useTheme();
119
+ return /* @__PURE__ */ jsx("span", {
120
+ style: {
121
+ display: "inline-flex",
122
+ alignSelf: "center",
123
+ color: colors.subtle
124
+ },
125
+ children: /* @__PURE__ */ jsx(LoadingIcon, { size: ICON_SIZE[size] })
126
+ });
127
+ };
66
128
  //#endregion
67
129
  //#region src/components/button.tsx
68
- const Button = ({ variant = "primary", disabled, onClick, children }) => {
130
+ const Button = ({ variant = "primary", disabled, loading, onClick, children }) => {
69
131
  const { colors, radius } = useTheme();
70
132
  const filled = variant === "primary" && !disabled;
71
133
  let color = colors.text;
72
134
  if (filled) color = colors.surface;
73
135
  else if (disabled) color = colors.subtle;
74
- return /* @__PURE__ */ jsx("button", {
136
+ return /* @__PURE__ */ jsxs("button", {
75
137
  type: "button",
76
138
  className: "dot-button",
77
139
  "data-variant": variant,
78
140
  disabled,
79
- onClick: onClick ? () => onClick(createEvent("click")) : void 0,
141
+ "aria-busy": loading,
142
+ onClick: onClick && !loading ? () => onClick(createEvent("click")) : void 0,
80
143
  style: {
81
- padding: "10px 16px",
144
+ display: "inline-flex",
145
+ alignItems: "center",
146
+ justifyContent: "center",
147
+ gap: SPACE.medium,
148
+ minHeight: 48,
149
+ padding: "12px 20px",
82
150
  border: "none",
83
151
  borderRadius: radius,
84
152
  background: filled ? colors.primary : colors.muted,
@@ -87,22 +155,259 @@ const Button = ({ variant = "primary", disabled, onClick, children }) => {
87
155
  fontSize: FONT_SIZE.body,
88
156
  fontWeight: 600,
89
157
  opacity: disabled ? DISABLED_OPACITY : 1,
90
- cursor: disabled ? "default" : "pointer"
158
+ cursor: disabled || loading ? "default" : "pointer"
159
+ },
160
+ children: [loading ? /* @__PURE__ */ jsx(LoadingIcon, { size: ICON_SIZE.small }) : null, children]
161
+ });
162
+ };
163
+ //#endregion
164
+ //#region src/components/divider.tsx
165
+ const Divider = () => {
166
+ 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
175
+ }
176
+ });
177
+ };
178
+ //#endregion
179
+ //#region src/components/tones.ts
180
+ const STATUS_ICONS = {
181
+ default: "info",
182
+ info: "info",
183
+ success: "success",
184
+ warning: "warning",
185
+ danger: "error"
186
+ };
187
+ function toneColor(colors, tone) {
188
+ switch (tone) {
189
+ case "default": return colors.text;
190
+ case "muted": return colors.subtle;
191
+ default: return colors[tone];
192
+ }
193
+ }
194
+ function toneBackground(colors, tone) {
195
+ if (tone === "default" || tone === "muted") return colors.muted;
196
+ return withOpacity(colors[tone], .14);
197
+ }
198
+ function withOpacity(color, opacity) {
199
+ return `${color}${Math.round(opacity * 255).toString(16).padStart(2, "0")}`;
200
+ }
201
+ //#endregion
202
+ //#region src/components/icon.tsx
203
+ const ICONS = {
204
+ "arrow-down": ArrowDown,
205
+ "arrow-left": ArrowLeft,
206
+ "arrow-right": ArrowRight,
207
+ "arrow-up": ArrowUp,
208
+ bank: Landmark,
209
+ bell: Bell,
210
+ bolt: Zap,
211
+ calendar: Calendar,
212
+ card: CreditCard,
213
+ cart: ShoppingCart,
214
+ cash: Banknote,
215
+ check: Check,
216
+ "chevron-down": ChevronDown,
217
+ "chevron-left": ChevronLeft,
218
+ "chevron-right": ChevronRight,
219
+ "chevron-up": ChevronUp,
220
+ clock: Clock,
221
+ close: X,
222
+ copy: Copy,
223
+ download: Download,
224
+ edit: Pencil,
225
+ error: CircleAlert,
226
+ eye: Eye,
227
+ "eye-off": EyeOff,
228
+ file: File,
229
+ gift: Gift,
230
+ grid: LayoutGrid,
231
+ heart: Heart,
232
+ help: CircleHelp,
233
+ history: History,
234
+ home: House,
235
+ image: Image,
236
+ info: Info,
237
+ list: List,
238
+ location: MapPin,
239
+ lock: Lock,
240
+ logout: LogOut,
241
+ mail: Mail,
242
+ minus: Minus,
243
+ mobile: Smartphone,
244
+ more: Ellipsis,
245
+ phone: Phone,
246
+ plus: Plus,
247
+ "qr-code": QrCode,
248
+ receipt: Receipt,
249
+ refresh: RefreshCw,
250
+ search: Search,
251
+ send: Send,
252
+ settings: Settings,
253
+ share: Share2,
254
+ star: Star,
255
+ success: CircleCheck,
256
+ trash: Trash2,
257
+ upload: Upload,
258
+ user: User,
259
+ users: Users,
260
+ wallet: Wallet,
261
+ warning: TriangleAlert,
262
+ water: Droplet,
263
+ wifi: Wifi
264
+ };
265
+ function IconGraphic({ name, size, color }) {
266
+ return /* @__PURE__ */ jsx("svg", {
267
+ className: "dot-icon",
268
+ width: size,
269
+ height: size,
270
+ viewBox: "0 0 24 24",
271
+ fill: "none",
272
+ stroke: color,
273
+ strokeWidth: 2,
274
+ strokeLinecap: "round",
275
+ strokeLinejoin: "round",
276
+ "aria-hidden": "true",
277
+ style: {
278
+ display: "inline-block",
279
+ verticalAlign: "middle",
280
+ flexShrink: 0
281
+ },
282
+ children: ICONS[name].map(([tag, attributes], index) => createElement(tag, {
283
+ key: index,
284
+ ...attributes
285
+ }))
286
+ });
287
+ }
288
+ const Icon = ({ name, size = "medium", tone = "default" }) => {
289
+ const { colors } = useTheme();
290
+ if (!name) return null;
291
+ return /* @__PURE__ */ jsx(IconGraphic, {
292
+ name,
293
+ size: ICON_SIZE[size],
294
+ color: toneColor(colors, tone)
295
+ });
296
+ };
297
+ //#endregion
298
+ //#region src/components/feedback.tsx
299
+ const ProgressBar = ({ value = 0 }) => {
300
+ const { colors } = useTheme();
301
+ const percent = Math.min(100, Math.max(0, value));
302
+ return /* @__PURE__ */ jsx("div", {
303
+ className: "dot-progress-bar",
304
+ role: "progressbar",
305
+ "aria-valuemin": 0,
306
+ "aria-valuemax": 100,
307
+ "aria-valuenow": percent,
308
+ style: {
309
+ height: 6,
310
+ borderRadius: 3,
311
+ background: colors.muted,
312
+ overflow: "hidden"
313
+ },
314
+ children: /* @__PURE__ */ jsx("div", { style: {
315
+ width: `${percent}%`,
316
+ height: "100%",
317
+ background: colors.primary,
318
+ transition: "width 0.2s"
319
+ } })
320
+ });
321
+ };
322
+ const Notice = ({ tone = "default", title, children }) => {
323
+ const { colors, radius } = useTheme();
324
+ return /* @__PURE__ */ jsxs("div", {
325
+ className: "dot-notice",
326
+ role: tone === "danger" ? "alert" : "status",
327
+ style: {
328
+ display: "flex",
329
+ gap: SPACE.medium,
330
+ padding: "12px 14px",
331
+ borderRadius: radius,
332
+ background: toneBackground(colors, tone),
333
+ color: colors.text,
334
+ fontSize: FONT_SIZE.body
335
+ },
336
+ children: [/* @__PURE__ */ jsx(IconGraphic, {
337
+ name: STATUS_ICONS[tone],
338
+ size: ICON_SIZE.medium,
339
+ color: toneColor(colors, tone)
340
+ }), /* @__PURE__ */ jsxs("div", {
341
+ style: {
342
+ display: "flex",
343
+ flexDirection: "column",
344
+ gap: 2
345
+ },
346
+ children: [title ? /* @__PURE__ */ jsx("span", {
347
+ style: { fontWeight: 600 },
348
+ children: title
349
+ }) : null, children ? /* @__PURE__ */ jsx("span", { children }) : null]
350
+ })]
351
+ });
352
+ };
353
+ const Badge = ({ tone = "default", children }) => {
354
+ const { colors } = useTheme();
355
+ return /* @__PURE__ */ jsx("span", {
356
+ className: "dot-badge",
357
+ style: {
358
+ display: "inline-flex",
359
+ alignItems: "center",
360
+ width: "fit-content",
361
+ padding: "2px 8px",
362
+ borderRadius: 999,
363
+ background: toneBackground(colors, tone),
364
+ color: toneColor(colors, tone),
365
+ fontSize: FONT_SIZE.caption,
366
+ fontWeight: 600,
367
+ whiteSpace: "nowrap"
368
+ },
369
+ children
370
+ });
371
+ };
372
+ //#endregion
373
+ //#region src/components/footer.tsx
374
+ const Footer = ({ children }) => {
375
+ const { colors } = useTheme();
376
+ return /* @__PURE__ */ jsx("div", {
377
+ className: "dot-footer",
378
+ style: {
379
+ position: "sticky",
380
+ bottom: 0,
381
+ marginTop: "auto",
382
+ display: "flex",
383
+ flexDirection: "column",
384
+ gap: SPACE.medium,
385
+ paddingTop: SPACE.medium,
386
+ paddingBottom: SPACE.medium,
387
+ background: colors.background
91
388
  },
92
389
  children
93
390
  });
94
391
  };
95
392
  //#endregion
96
393
  //#region src/components/image.tsx
97
- const Image = ({ src, alt, width, height, onLoad, onError }) => {
394
+ const Image$1 = ({ src, alt, width, height, fit = "cover", shape = "rounded", onLoad, onError }) => {
98
395
  const { radius } = useTheme();
396
+ const corners = {
397
+ rounded: radius,
398
+ square: 0,
399
+ circle: "50%"
400
+ }[shape];
99
401
  return /* @__PURE__ */ jsx("img", {
100
402
  className: "dot-image",
101
403
  src,
102
404
  alt: alt ?? "",
103
405
  width,
104
406
  height,
105
- style: { borderRadius: radius },
407
+ style: {
408
+ borderRadius: corners,
409
+ objectFit: fit
410
+ },
106
411
  onLoad: onLoad ? (event) => {
107
412
  const { naturalWidth, naturalHeight } = event.currentTarget;
108
413
  onLoad(createEvent("load", {
@@ -114,6 +419,82 @@ const Image = ({ src, alt, width, height, onLoad, onError }) => {
114
419
  });
115
420
  };
116
421
  //#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
117
498
  //#region src/components/input.tsx
118
499
  const HTML_TYPES = {
119
500
  text: {
@@ -137,50 +518,402 @@ const HTML_TYPES = {
137
518
  inputMode: "text"
138
519
  }
139
520
  };
140
- const Input = ({ value, placeholder, label, type = "text", maxLength, disabled, onChange, onSubmit }) => {
521
+ const Input = ({ value, placeholder, label, hint, error, prefix, suffix, type = "text", maxLength, disabled, onChange, onSubmit }) => {
522
+ const { colors } = useTheme();
523
+ const [text, typeText] = useTypedText(value, onChange !== void 0);
524
+ const box = useFieldBox(error, disabled);
525
+ return /* @__PURE__ */ jsx(Field, {
526
+ label,
527
+ hint,
528
+ error,
529
+ children: /* @__PURE__ */ jsxs("span", {
530
+ className: "dot-input",
531
+ style: {
532
+ ...box.style,
533
+ display: "flex",
534
+ alignItems: "center",
535
+ gap: SPACE.medium,
536
+ padding: "0 14px"
537
+ },
538
+ children: [
539
+ prefix ? /* @__PURE__ */ jsx("span", {
540
+ style: { color: colors.subtle },
541
+ children: prefix
542
+ }) : null,
543
+ /* @__PURE__ */ jsx("input", {
544
+ ...HTML_TYPES[type],
545
+ value: text,
546
+ placeholder,
547
+ maxLength,
548
+ disabled,
549
+ "aria-invalid": error ? true : void 0,
550
+ onFocus: box.onFocus,
551
+ onBlur: box.onBlur,
552
+ onChange: (event) => {
553
+ const next = event.currentTarget.value;
554
+ typeText(next);
555
+ onChange?.(createEvent("change", { value: next }));
556
+ },
557
+ onKeyDown: (event) => {
558
+ if (event.key === "Enter" && onSubmit) onSubmit(createEvent("submit", { value: text }));
559
+ },
560
+ style: {
561
+ flexGrow: 1,
562
+ minWidth: 0,
563
+ padding: "12px 0",
564
+ border: "none",
565
+ outline: "none",
566
+ background: "transparent",
567
+ color: "inherit",
568
+ font: "inherit"
569
+ }
570
+ }),
571
+ suffix ? /* @__PURE__ */ jsx("span", {
572
+ style: { color: colors.subtle },
573
+ children: suffix
574
+ }) : null
575
+ ]
576
+ })
577
+ });
578
+ };
579
+ //#endregion
580
+ //#region src/components/list.tsx
581
+ const List$1 = ({ children }) => {
141
582
  const { colors, radius } = useTheme();
142
- const [text, setText] = useState(value ?? "");
143
- const [awaitingEcho, setAwaitingEcho] = useState([]);
144
- const [previousValue, setPreviousValue] = useState(value);
145
- if (value !== previousValue) {
146
- setPreviousValue(value);
147
- const echo = awaitingEcho.indexOf(value ?? "");
148
- if (echo >= 0) setAwaitingEcho(awaitingEcho.slice(echo + 1));
149
- else {
150
- setAwaitingEcho([]);
151
- setText(value ?? "");
152
- }
153
- }
154
- return /* @__PURE__ */ jsx("input", {
155
- className: "dot-input",
156
- ...HTML_TYPES[type],
157
- value: text,
158
- placeholder,
159
- "aria-label": label,
160
- maxLength,
161
- disabled,
162
- onChange: (event) => {
163
- const next = event.currentTarget.value;
164
- setText(next);
165
- if (onChange && value !== void 0) setAwaitingEcho([...awaitingEcho, next]);
166
- onChange?.(createEvent("change", { value: next }));
167
- },
168
- onKeyDown: (event) => {
169
- if (event.key === "Enter" && onSubmit) onSubmit(createEvent("submit", { value: text }));
170
- },
583
+ return /* @__PURE__ */ jsx("div", {
584
+ className: "dot-list",
171
585
  style: {
172
- width: "100%",
173
- boxSizing: "border-box",
174
- padding: "12px 14px",
586
+ display: "flex",
587
+ flexDirection: "column",
588
+ gap: 1,
589
+ overflow: "hidden",
590
+ borderRadius: radius * CARD_RADIUS_SCALE,
175
591
  border: `1px solid ${colors.border}`,
592
+ background: colors.border
593
+ },
594
+ children
595
+ });
596
+ };
597
+ const ListItem = ({ title, subtitle, value, icon, image, onTap, children }) => {
598
+ const { colors, radius } = useTheme();
599
+ let leading = null;
600
+ if (image) leading = /* @__PURE__ */ jsx("img", {
601
+ src: image,
602
+ alt: "",
603
+ width: 40,
604
+ height: 40,
605
+ style: {
176
606
  borderRadius: radius,
607
+ objectFit: "cover",
608
+ flexShrink: 0
609
+ }
610
+ });
611
+ else if (icon) leading = /* @__PURE__ */ jsx("span", {
612
+ style: {
613
+ display: "flex",
614
+ alignItems: "center",
615
+ justifyContent: "center",
616
+ width: 36,
617
+ height: 36,
618
+ borderRadius: "50%",
619
+ background: colors.muted,
620
+ flexShrink: 0
621
+ },
622
+ children: /* @__PURE__ */ jsx(IconGraphic, {
623
+ name: icon,
624
+ size: ICON_SIZE.medium,
625
+ color: colors.text
626
+ })
627
+ });
628
+ let trailing = children;
629
+ if (!trailing && value) trailing = /* @__PURE__ */ jsx("span", {
630
+ style: { color: colors.subtle },
631
+ children: value
632
+ });
633
+ return /* @__PURE__ */ jsxs("div", {
634
+ className: "dot-list-item",
635
+ role: onTap ? "button" : void 0,
636
+ onClick: onTap ? () => onTap(createEvent("tap")) : void 0,
637
+ style: {
638
+ display: "flex",
639
+ alignItems: "center",
640
+ gap: 12,
641
+ padding: "12px 16px",
177
642
  background: colors.surface,
643
+ fontSize: FONT_SIZE.body,
644
+ cursor: onTap ? "pointer" : void 0
645
+ },
646
+ children: [
647
+ leading,
648
+ /* @__PURE__ */ jsxs("span", {
649
+ style: {
650
+ display: "flex",
651
+ flexDirection: "column",
652
+ flexGrow: 1,
653
+ minWidth: 0
654
+ },
655
+ children: [title ? /* @__PURE__ */ jsx("span", {
656
+ style: { fontWeight: 500 },
657
+ children: title
658
+ }) : null, subtitle ? /* @__PURE__ */ jsx("span", {
659
+ style: {
660
+ fontSize: FONT_SIZE.caption,
661
+ color: colors.subtle
662
+ },
663
+ children: subtitle
664
+ }) : null]
665
+ }),
666
+ trailing ? /* @__PURE__ */ jsx("span", {
667
+ style: {
668
+ display: "flex",
669
+ alignItems: "center",
670
+ gap: SPACE.small,
671
+ flexShrink: 0
672
+ },
673
+ children: trailing
674
+ }) : null,
675
+ onTap ? /* @__PURE__ */ jsx(IconGraphic, {
676
+ name: "chevron-right",
677
+ size: ICON_SIZE.small,
678
+ color: colors.subtle
679
+ }) : null
680
+ ]
681
+ });
682
+ };
683
+ //#endregion
684
+ //#region src/components/animate.ts
685
+ function animate(element, keyframes, options) {
686
+ if (!element || typeof element.animate !== "function") return;
687
+ return element.animate(keyframes, options);
688
+ }
689
+ //#endregion
690
+ //#region src/components/sheet.tsx
691
+ const SLIDE_UP = [{ transform: "translateY(100%)" }, { transform: "translateY(0)" }];
692
+ function SheetPanel({ open, title, onClose, children }) {
693
+ const { colors, radius } = useTheme();
694
+ const panel = useRef(null);
695
+ useEffect(() => {
696
+ if (open) animate(panel.current, SLIDE_UP, {
697
+ duration: 200,
698
+ easing: "ease-out"
699
+ });
700
+ }, [open]);
701
+ if (!open) return null;
702
+ const corner = radius * CARD_RADIUS_SCALE;
703
+ return /* @__PURE__ */ jsx("div", {
704
+ className: "dot-sheet",
705
+ onClick: onClose,
706
+ style: {
707
+ position: "fixed",
708
+ inset: 0,
709
+ zIndex: 100,
710
+ display: "flex",
711
+ flexDirection: "column",
712
+ justifyContent: "flex-end",
713
+ background: "rgba(0, 0, 0, 0.4)"
714
+ },
715
+ children: /* @__PURE__ */ jsxs("div", {
716
+ ref: panel,
717
+ role: "dialog",
718
+ "aria-modal": "true",
719
+ "aria-label": title,
720
+ onClick: (event) => event.stopPropagation(),
721
+ style: {
722
+ display: "flex",
723
+ flexDirection: "column",
724
+ gap: SPACE.medium,
725
+ maxHeight: "85vh",
726
+ overflowY: "auto",
727
+ padding: `${SPACE.large}px ${SPACE.large}px 24px`,
728
+ borderTopLeftRadius: corner,
729
+ borderTopRightRadius: corner,
730
+ background: colors.surface,
731
+ color: colors.text
732
+ },
733
+ children: [title ? /* @__PURE__ */ jsx("span", {
734
+ style: {
735
+ fontSize: FONT_SIZE.body,
736
+ fontWeight: 600,
737
+ textAlign: "center"
738
+ },
739
+ children: title
740
+ }) : null, children]
741
+ })
742
+ });
743
+ }
744
+ const Sheet = ({ open = false, title, onClose, children }) => /* @__PURE__ */ jsx(SheetPanel, {
745
+ open,
746
+ title,
747
+ onClose: () => onClose?.(createEvent("close")),
748
+ children
749
+ });
750
+ //#endregion
751
+ //#region src/components/select.tsx
752
+ const OptionPlaceContext = createContext({
753
+ inField: false,
754
+ value: void 0,
755
+ choose: void 0
756
+ });
757
+ const Select = ({ value, label, placeholder, hint, error, disabled, onChange, children }) => {
758
+ const { colors } = useTheme();
759
+ const [open, setOpen] = useState(false);
760
+ const box = useFieldBox(error, disabled);
761
+ const choose = (next) => {
762
+ setOpen(false);
763
+ onChange?.(createEvent("change", { value: next }));
764
+ };
765
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Field, {
766
+ label,
767
+ hint,
768
+ error,
769
+ children: /* @__PURE__ */ jsxs("button", {
770
+ type: "button",
771
+ className: "dot-select",
772
+ disabled,
773
+ onClick: () => setOpen(true),
774
+ onFocus: box.onFocus,
775
+ onBlur: box.onBlur,
776
+ style: {
777
+ font: "inherit",
778
+ ...box.style,
779
+ display: "flex",
780
+ alignItems: "center",
781
+ gap: SPACE.medium,
782
+ width: "100%",
783
+ padding: "12px 14px",
784
+ textAlign: "start",
785
+ cursor: disabled ? "default" : "pointer"
786
+ },
787
+ children: [/* @__PURE__ */ jsx("span", {
788
+ style: { flexGrow: 1 },
789
+ children: value ? /* @__PURE__ */ jsx(OptionPlaceContext.Provider, {
790
+ value: {
791
+ inField: true,
792
+ value,
793
+ choose: void 0
794
+ },
795
+ children
796
+ }) : /* @__PURE__ */ jsx("span", {
797
+ style: { color: colors.subtle },
798
+ children: placeholder
799
+ })
800
+ }), /* @__PURE__ */ jsx(IconGraphic, {
801
+ name: "chevron-down",
802
+ size: ICON_SIZE.small,
803
+ color: colors.subtle
804
+ })]
805
+ })
806
+ }), /* @__PURE__ */ jsx(SheetPanel, {
807
+ open,
808
+ title: label,
809
+ onClose: () => setOpen(false),
810
+ children: /* @__PURE__ */ jsx("div", {
811
+ role: "listbox",
812
+ style: {
813
+ display: "flex",
814
+ flexDirection: "column"
815
+ },
816
+ children: /* @__PURE__ */ jsx(OptionPlaceContext.Provider, {
817
+ value: {
818
+ inField: false,
819
+ value,
820
+ choose
821
+ },
822
+ children
823
+ })
824
+ })
825
+ })] });
826
+ };
827
+ const Option = ({ value, label }) => {
828
+ const { colors } = useTheme();
829
+ const place = useContext(OptionPlaceContext);
830
+ const chosen = value !== void 0 && value === place.value;
831
+ if (place.inField) return chosen ? /* @__PURE__ */ jsx(Fragment$1, { children: label }) : null;
832
+ const { choose } = place;
833
+ return /* @__PURE__ */ jsxs("button", {
834
+ type: "button",
835
+ className: "dot-option",
836
+ role: "option",
837
+ "aria-selected": chosen,
838
+ onClick: choose && value !== void 0 ? () => choose(value) : void 0,
839
+ style: {
840
+ display: "flex",
841
+ alignItems: "center",
842
+ justifyContent: "space-between",
843
+ padding: "14px 4px",
844
+ border: "none",
845
+ borderBottom: `1px solid ${colors.border}`,
846
+ background: "transparent",
178
847
  color: colors.text,
179
- outlineColor: colors.primary,
180
848
  font: "inherit",
181
- fontSize: 16,
182
- opacity: disabled ? DISABLED_OPACITY : 1
183
- }
849
+ fontSize: FONT_SIZE.body,
850
+ fontWeight: chosen ? 600 : 400,
851
+ textAlign: "start",
852
+ cursor: "pointer"
853
+ },
854
+ children: [/* @__PURE__ */ jsx("span", { children: label }), chosen ? /* @__PURE__ */ jsx(IconGraphic, {
855
+ name: "check",
856
+ size: ICON_SIZE.medium,
857
+ color: colors.primary
858
+ }) : null]
859
+ });
860
+ };
861
+ //#endregion
862
+ //#region src/components/skeleton.tsx
863
+ const PULSE = [
864
+ { opacity: 1 },
865
+ { opacity: .4 },
866
+ { opacity: 1 }
867
+ ];
868
+ const LINE_HEIGHT = 12;
869
+ const Skeleton = ({ shape = "text", lines = 1, height }) => {
870
+ const { colors, radius } = useTheme();
871
+ const placeholder = useRef(null);
872
+ useEffect(() => {
873
+ const pulse = animate(placeholder.current, PULSE, {
874
+ duration: 1200,
875
+ iterations: Infinity
876
+ });
877
+ return () => pulse?.cancel();
878
+ }, []);
879
+ let content;
880
+ if (shape === "circle") {
881
+ const size = height ?? 40;
882
+ content = /* @__PURE__ */ jsx("div", { style: {
883
+ width: size,
884
+ height: size,
885
+ borderRadius: "50%",
886
+ background: colors.muted
887
+ } });
888
+ } else if (shape === "box") content = /* @__PURE__ */ jsx("div", { style: {
889
+ height: height ?? 80,
890
+ borderRadius: radius,
891
+ background: colors.muted
892
+ } });
893
+ else {
894
+ const count = Math.max(1, lines);
895
+ content = Array.from({ length: count }, (_, index) => {
896
+ const last = count > 1 && index === count - 1;
897
+ return /* @__PURE__ */ jsx("div", { style: {
898
+ width: last ? "60%" : "100%",
899
+ height: LINE_HEIGHT,
900
+ borderRadius: LINE_HEIGHT / 2,
901
+ background: colors.muted
902
+ } }, index);
903
+ });
904
+ }
905
+ return /* @__PURE__ */ jsx("div", {
906
+ ref: placeholder,
907
+ className: "dot-skeleton",
908
+ "aria-busy": "true",
909
+ style: {
910
+ display: "flex",
911
+ flexDirection: "column",
912
+ gap: SPACE.medium,
913
+ width: shape === "circle" ? void 0 : "100%",
914
+ flexShrink: shape === "circle" ? 0 : 1
915
+ },
916
+ children: content
184
917
  });
185
918
  };
186
919
  //#endregion
@@ -193,11 +926,265 @@ const VARIANTS = {
193
926
  fontWeight: 600
194
927
  }
195
928
  };
196
- const Text = ({ variant = "body", children }) => /* @__PURE__ */ jsx("span", {
197
- className: "dot-text",
198
- style: VARIANTS[variant],
199
- children
929
+ const WEIGHTS = {
930
+ regular: 400,
931
+ medium: 500,
932
+ bold: 600
933
+ };
934
+ const Text = ({ variant = "body", tone, weight, align, maxLines, children }) => {
935
+ const { colors } = useTheme();
936
+ const style = {
937
+ ...VARIANTS[variant],
938
+ textAlign: align
939
+ };
940
+ if (tone) style.color = toneColor(colors, tone);
941
+ if (weight) style.fontWeight = WEIGHTS[weight];
942
+ if (maxLines) Object.assign(style, {
943
+ display: "-webkit-box",
944
+ WebkitLineClamp: maxLines,
945
+ WebkitBoxOrient: "vertical",
946
+ overflow: "hidden"
947
+ });
948
+ return /* @__PURE__ */ jsx("span", {
949
+ className: "dot-text",
950
+ style,
951
+ children
952
+ });
953
+ };
954
+ //#endregion
955
+ //#region src/components/text-area.tsx
956
+ const TextArea = ({ value, placeholder, label, hint, error, rows = 3, maxLength, disabled, onChange }) => {
957
+ const [text, typeText] = useTypedText(value, onChange !== void 0);
958
+ const box = useFieldBox(error, disabled);
959
+ return /* @__PURE__ */ jsx(Field, {
960
+ label,
961
+ hint,
962
+ error,
963
+ children: /* @__PURE__ */ jsx("textarea", {
964
+ className: "dot-text-area",
965
+ value: text,
966
+ placeholder,
967
+ rows,
968
+ maxLength,
969
+ disabled,
970
+ "aria-invalid": error ? true : void 0,
971
+ onFocus: box.onFocus,
972
+ onBlur: box.onBlur,
973
+ onChange: (event) => {
974
+ const next = event.currentTarget.value;
975
+ typeText(next);
976
+ onChange?.(createEvent("change", { value: next }));
977
+ },
978
+ style: {
979
+ font: "inherit",
980
+ ...box.style,
981
+ boxSizing: "border-box",
982
+ width: "100%",
983
+ padding: "12px 14px",
984
+ outline: "none",
985
+ resize: "none"
986
+ }
987
+ })
988
+ });
989
+ };
990
+ //#endregion
991
+ //#region src/components/toast.tsx
992
+ const Toast = ({ message, tone = "default", duration = 2500, onHide }) => {
993
+ const { colors, radius } = useTheme();
994
+ const [hidden, setHidden] = useState(false);
995
+ const [previousMessage, setPreviousMessage] = useState(message);
996
+ if (message !== previousMessage) {
997
+ setPreviousMessage(message);
998
+ setHidden(false);
999
+ }
1000
+ const hide = useEffectEvent(() => {
1001
+ setHidden(true);
1002
+ onHide?.(createEvent("hide"));
1003
+ });
1004
+ useEffect(() => {
1005
+ if (!message) return;
1006
+ const timer = setTimeout(hide, duration);
1007
+ return () => clearTimeout(timer);
1008
+ }, [message, duration]);
1009
+ if (!message || hidden) return null;
1010
+ return /* @__PURE__ */ jsxs("div", {
1011
+ className: "dot-toast",
1012
+ role: "status",
1013
+ style: {
1014
+ position: "fixed",
1015
+ top: SPACE.large,
1016
+ left: "50%",
1017
+ transform: "translateX(-50%)",
1018
+ zIndex: 110,
1019
+ display: "flex",
1020
+ alignItems: "center",
1021
+ gap: SPACE.medium,
1022
+ maxWidth: "calc(100% - 32px)",
1023
+ width: "max-content",
1024
+ padding: "10px 16px",
1025
+ borderRadius: radius,
1026
+ background: colors.primary,
1027
+ color: colors.surface,
1028
+ fontSize: FONT_SIZE.body,
1029
+ boxShadow: "0 4px 16px rgba(0, 0, 0, 0.2)"
1030
+ },
1031
+ children: [tone === "default" ? null : /* @__PURE__ */ jsx(IconGraphic, {
1032
+ name: STATUS_ICONS[tone],
1033
+ size: ICON_SIZE.medium,
1034
+ color: toneColor(colors, tone)
1035
+ }), /* @__PURE__ */ jsx("span", { children: message })]
1036
+ });
1037
+ };
1038
+ //#endregion
1039
+ //#region src/components/toggles.tsx
1040
+ function rowStyle(colors, disabled) {
1041
+ return {
1042
+ display: "flex",
1043
+ alignItems: "center",
1044
+ gap: 12,
1045
+ width: "100%",
1046
+ padding: "6px 0",
1047
+ border: "none",
1048
+ background: "transparent",
1049
+ color: colors.text,
1050
+ font: "inherit",
1051
+ fontSize: FONT_SIZE.body,
1052
+ textAlign: "start",
1053
+ opacity: disabled ? DISABLED_OPACITY : 1,
1054
+ cursor: disabled ? "default" : "pointer"
1055
+ };
1056
+ }
1057
+ function changeTo(checked, onChange) {
1058
+ return onChange ? () => onChange(createEvent("change", { checked })) : void 0;
1059
+ }
1060
+ function ChoiceMark({ round, checked, children }) {
1061
+ const { colors } = useTheme();
1062
+ const filled = checked && !round;
1063
+ return /* @__PURE__ */ jsx("span", {
1064
+ style: {
1065
+ display: "flex",
1066
+ alignItems: "center",
1067
+ justifyContent: "center",
1068
+ flexShrink: 0,
1069
+ width: 22,
1070
+ height: 22,
1071
+ boxSizing: "border-box",
1072
+ borderRadius: round ? "50%" : 6,
1073
+ border: `2px solid ${checked ? colors.primary : colors.subtle}`,
1074
+ background: filled ? colors.primary : "transparent"
1075
+ },
1076
+ children
1077
+ });
1078
+ }
1079
+ const Switch = ({ checked = false, label, disabled, onChange }) => {
1080
+ const { colors } = useTheme();
1081
+ return /* @__PURE__ */ jsxs("button", {
1082
+ type: "button",
1083
+ className: "dot-switch",
1084
+ role: "switch",
1085
+ "aria-checked": checked,
1086
+ disabled,
1087
+ onClick: changeTo(!checked, onChange),
1088
+ style: {
1089
+ ...rowStyle(colors, disabled),
1090
+ justifyContent: "space-between"
1091
+ },
1092
+ children: [label ? /* @__PURE__ */ jsx("span", { children: label }) : null, /* @__PURE__ */ jsx("span", {
1093
+ style: {
1094
+ position: "relative",
1095
+ flexShrink: 0,
1096
+ width: 44,
1097
+ height: 26,
1098
+ borderRadius: 13,
1099
+ background: checked ? colors.primary : colors.subtle,
1100
+ transition: "background 0.2s"
1101
+ },
1102
+ children: /* @__PURE__ */ jsx("span", { style: {
1103
+ position: "absolute",
1104
+ top: 2,
1105
+ left: checked ? 20 : 2,
1106
+ width: 22,
1107
+ height: 22,
1108
+ borderRadius: "50%",
1109
+ background: colors.surface,
1110
+ transition: "left 0.2s"
1111
+ } })
1112
+ })]
1113
+ });
1114
+ };
1115
+ const Checkbox = ({ checked = false, label, disabled, onChange }) => {
1116
+ const { colors } = useTheme();
1117
+ return /* @__PURE__ */ jsxs("button", {
1118
+ type: "button",
1119
+ className: "dot-checkbox",
1120
+ role: "checkbox",
1121
+ "aria-checked": checked,
1122
+ disabled,
1123
+ onClick: changeTo(!checked, onChange),
1124
+ style: rowStyle(colors, disabled),
1125
+ children: [/* @__PURE__ */ jsx(ChoiceMark, {
1126
+ round: false,
1127
+ checked,
1128
+ children: checked ? /* @__PURE__ */ jsx(IconGraphic, {
1129
+ name: "check",
1130
+ size: ICON_SIZE.small,
1131
+ color: colors.surface
1132
+ }) : null
1133
+ }), label ? /* @__PURE__ */ jsx("span", { children: label }) : null]
1134
+ });
1135
+ };
1136
+ const RadioGroupContext = createContext({
1137
+ value: void 0,
1138
+ disabled: true,
1139
+ choose: void 0
200
1140
  });
1141
+ const RadioGroup = ({ value, label, disabled = false, onChange, children }) => {
1142
+ const choose = onChange ? (next) => onChange(createEvent("change", { value: next })) : void 0;
1143
+ return /* @__PURE__ */ jsxs("div", {
1144
+ className: "dot-radio-group",
1145
+ role: "radiogroup",
1146
+ "aria-label": label,
1147
+ style: {
1148
+ display: "flex",
1149
+ flexDirection: "column",
1150
+ gap: SPACE.small
1151
+ },
1152
+ children: [label ? /* @__PURE__ */ jsx(FieldLabel, { children: label }) : null, /* @__PURE__ */ jsx(RadioGroupContext.Provider, {
1153
+ value: {
1154
+ value,
1155
+ disabled,
1156
+ choose
1157
+ },
1158
+ children
1159
+ })]
1160
+ });
1161
+ };
1162
+ const Radio = ({ value, label, disabled }) => {
1163
+ const { colors } = useTheme();
1164
+ const group = useContext(RadioGroupContext);
1165
+ const selected = value !== void 0 && value === group.value;
1166
+ const inactive = disabled || group.disabled;
1167
+ const { choose } = group;
1168
+ return /* @__PURE__ */ jsxs("button", {
1169
+ type: "button",
1170
+ className: "dot-radio",
1171
+ role: "radio",
1172
+ "aria-checked": selected,
1173
+ disabled: inactive,
1174
+ onClick: choose && value !== void 0 ? () => choose(value) : void 0,
1175
+ style: rowStyle(colors, inactive),
1176
+ children: [/* @__PURE__ */ jsx(ChoiceMark, {
1177
+ round: true,
1178
+ checked: selected,
1179
+ children: selected ? /* @__PURE__ */ jsx("span", { style: {
1180
+ width: 10,
1181
+ height: 10,
1182
+ borderRadius: "50%",
1183
+ background: colors.primary
1184
+ } }) : null
1185
+ }), label ? /* @__PURE__ */ jsx("span", { children: label }) : null]
1186
+ });
1187
+ };
201
1188
  //#endregion
202
1189
  //#region src/components/view.tsx
203
1190
  const ALIGN = {
@@ -206,18 +1193,32 @@ const ALIGN = {
206
1193
  center: "center",
207
1194
  end: "flex-end"
208
1195
  };
209
- const View = ({ direction = "column", gap = "medium", align = "stretch", variant = "plain", onTap, children }) => {
1196
+ const JUSTIFY = {
1197
+ start: "flex-start",
1198
+ center: "center",
1199
+ end: "flex-end",
1200
+ between: "space-between"
1201
+ };
1202
+ const View = ({ direction = "column", gap = "medium", align = "stretch", justify = "start", wrap = false, grow = false, padding, paddingTop, paddingStart, paddingEnd, paddingBottom, variant = "plain", onTap, children }) => {
210
1203
  const { colors, radius } = useTheme();
211
1204
  const card = variant === "card";
1205
+ const allSides = padding ?? (card ? "large" : "none");
212
1206
  return /* @__PURE__ */ jsx("div", {
213
1207
  className: "dot-view",
214
1208
  onClick: onTap ? () => onTap(createEvent("tap")) : void 0,
215
1209
  style: {
216
1210
  display: "flex",
217
1211
  flexDirection: direction,
218
- gap: GAP[gap],
1212
+ gap: SPACE[gap],
219
1213
  alignItems: ALIGN[align],
220
- padding: card ? 16 : void 0,
1214
+ justifyContent: JUSTIFY[justify],
1215
+ flexWrap: wrap ? "wrap" : void 0,
1216
+ flexGrow: grow ? 1 : void 0,
1217
+ minWidth: grow ? 0 : void 0,
1218
+ paddingTop: SPACE[paddingTop ?? allSides],
1219
+ paddingInlineStart: SPACE[paddingStart ?? allSides],
1220
+ paddingInlineEnd: SPACE[paddingEnd ?? allSides],
1221
+ paddingBottom: SPACE[paddingBottom ?? allSides],
221
1222
  borderRadius: card ? radius * CARD_RADIUS_SCALE : void 0,
222
1223
  background: card ? colors.surface : void 0,
223
1224
  border: card ? `1px solid ${colors.border}` : void 0,
@@ -229,10 +1230,29 @@ const View = ({ direction = "column", gap = "medium", align = "stretch", variant
229
1230
  //#endregion
230
1231
  //#region src/components/index.ts
231
1232
  const builtins = {
1233
+ Badge,
232
1234
  Button,
233
- Image,
1235
+ Checkbox,
1236
+ Divider,
1237
+ Footer,
1238
+ Icon,
1239
+ Image: Image$1,
234
1240
  Input,
1241
+ List: List$1,
1242
+ ListItem,
1243
+ Notice,
1244
+ Option,
1245
+ ProgressBar,
1246
+ Radio,
1247
+ RadioGroup,
1248
+ Select,
1249
+ Sheet,
1250
+ Skeleton,
1251
+ Spinner,
1252
+ Switch,
235
1253
  Text,
1254
+ TextArea,
1255
+ Toast,
236
1256
  View
237
1257
  };
238
1258
  //#endregion
@@ -246,11 +1266,11 @@ function PageRoot({ page, components }) {
246
1266
  return createElement(component, props, ...children);
247
1267
  },
248
1268
  text: toText,
249
- on: (handler) => page.on(handler),
1269
+ on: (handler, data) => page.on(handler, data),
250
1270
  each: (items, render, key) => createElement(Fragment, null, toList(items).map((item, index) => createElement(Fragment, { key: key ? String(key(item, index)) : index }, ...render(item, index))))
251
1271
  }), [page, components]);
252
1272
  if (!state) return null;
253
- return createElement(Fragment, null, ...page.viewRenderer(state, context));
1273
+ return page.viewRenderer(state, context);
254
1274
  }
255
1275
  //#endregion
256
1276
  //#region src/root.tsx
@@ -279,11 +1299,8 @@ function DefaultSurface({ children }) {
279
1299
  className: "dot-surface",
280
1300
  style: {
281
1301
  minHeight: "100vh",
282
- boxSizing: "border-box",
283
- padding: 16,
284
1302
  display: "flex",
285
1303
  flexDirection: "column",
286
- gap: 12,
287
1304
  background: colors.background,
288
1305
  color: colors.text,
289
1306
  fontFamily: FONT_FAMILY