@monimesl/dot-renderer-react 0.1.0-alpha.21 → 0.1.0-alpha.24

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/README.md CHANGED
@@ -21,9 +21,11 @@ source at build time, from `@phosphor-icons/core`.
21
21
  ## The theme
22
22
 
23
23
  A dot sets `scheme`, `colors` and `shape` in `defineDot`. The colors are `primary`, `background`,
24
- `surface` and `text`, each for light and dark. The renderer fills in the rest of the palette (the
25
- neutrals and the status colors) from Dash's own look. `useTheme()` gives a component the resolved
26
- theme.
24
+ `surface`, `text`, `muted`, `border` and `subtle`, each for light and dark, and `custom` for the
25
+ dot's own named colors. A page can set its own colors over the theme's, with `configs.colors` or
26
+ `setColors`. The renderer fills in the rest of the palette from Dash's own look. On a background
27
+ that isn't Dash's, it works out surfaces and quieter text that sit well on it. `useTheme()` gives a
28
+ component the resolved theme.
27
29
 
28
30
  ## Only the components a dot uses
29
31
 
package/dist/color.js CHANGED
@@ -1,25 +1,12 @@
1
1
  //#region src/color.ts
2
- const READABLE_CONTRAST = 4.5;
3
- function contrast(first, second) {
4
- const firstLuminance = luminance(first);
5
- const secondLuminance = luminance(second);
6
- const lighter = Math.max(firstLuminance, secondLuminance);
7
- const darker = Math.min(firstLuminance, secondLuminance);
8
- return (lighter + .05) / (darker + .05);
2
+ function mix(from, to, amount) {
3
+ const end = channels(to);
4
+ return toHex(channels(from).map((start, index) => start + (end[index] - start) * amount));
9
5
  }
10
6
  function darken(color, points) {
11
7
  const [hue, saturation, lightness] = toHsl(channels(color));
12
8
  return toHex(fromHsl(hue, saturation, Math.max(0, lightness - points / 100)));
13
9
  }
14
- function luminance(color) {
15
- const [red, green, blue] = channels(color).map(linearLight);
16
- return .2126 * red + .7152 * green + .0722 * blue;
17
- }
18
- function linearLight(channel) {
19
- const share = channel / 255;
20
- if (share <= .03928) return share / 12.92;
21
- return ((share + .055) / 1.055) ** 2.4;
22
- }
23
10
  function channels(color) {
24
11
  let hex = color.slice(1);
25
12
  if (hex.length === 3 || hex.length === 4) hex = [...hex].map((digit) => digit + digit).join("");
@@ -27,7 +14,7 @@ function channels(color) {
27
14
  0,
28
15
  2,
29
16
  4
30
- ].map((start) => parseInt(hex.slice(start, start + 2), 16));
17
+ ].map((offset) => parseInt(hex.slice(offset, offset + 2), 16));
31
18
  return [
32
19
  red,
33
20
  green,
@@ -104,4 +91,4 @@ function fromHsl(hue, saturation, lightness) {
104
91
  ];
105
92
  }
106
93
  //#endregion
107
- export { READABLE_CONTRAST, contrast, darken };
94
+ export { darken, mix };
@@ -12,8 +12,8 @@ const AmountInput$1 = ({ value, currency = DEFAULT_CURRENCY, minorUnits, unit =
12
12
  const { colors } = useTheme();
13
13
  const decimalPlaces = minorUnits === void 0 ? currencyMinorUnits(currency) : Math.max(0, Math.round(minorUnits));
14
14
  const scale = unit === "major" ? 10 ** decimalPlaces : 1;
15
- const show = (minor) => formatMinorUnits(minor, decimalPlaces);
16
- const [text, typeText, reshowText] = useTypedText(amountText(value, scale), onChange !== void 0, (sent) => sent === "" ? "" : show(Number(sent)));
15
+ const formatAmount = (minor) => formatMinorUnits(minor, decimalPlaces);
16
+ const [text, typeText, reshowText] = useTypedText(amountText(value, scale), onChange !== void 0, (sent) => sent === "" ? "" : formatAmount(Number(sent)));
17
17
  const box = useFieldBox(error, disabled);
18
18
  const { noteId, controlAttributes } = useFieldNote(hint, error);
19
19
  return /* @__PURE__ */ jsx(Field, {
@@ -39,7 +39,7 @@ const AmountInput$1 = ({ value, currency = DEFAULT_CURRENCY, minorUnits, unit =
39
39
  children: currency
40
40
  }) : null, /* @__PURE__ */ jsx("input", {
41
41
  value: text,
42
- placeholder: placeholder ?? show(0),
42
+ placeholder: placeholder ?? formatAmount(0),
43
43
  inputMode: decimalPlaces > 0 ? "decimal" : "numeric",
44
44
  disabled,
45
45
  ...controlAttributes,
@@ -47,7 +47,7 @@ const AmountInput$1 = ({ value, currency = DEFAULT_CURRENCY, minorUnits, unit =
47
47
  onBlur: () => {
48
48
  box.onBlur();
49
49
  const amount = toMinorUnits(text, decimalPlaces);
50
- if (amount !== null) reshowText(show(amount));
50
+ if (amount !== null) reshowText(formatAmount(amount));
51
51
  },
52
52
  onChange: (event) => {
53
53
  const typed = cleanAmount(event.currentTarget.value, decimalPlaces);
@@ -81,10 +81,12 @@ function amountText(value, scale) {
81
81
  }
82
82
  function cleanAmount(typed, decimalPlaces) {
83
83
  const [whole = "", ...fractions] = typed.replace(/[^\d.]/g, "").split(".");
84
- const digits = whole.replace(/^0+/, "") || whole.slice(0, 1);
85
- if (decimalPlaces === 0 || fractions.length === 0) return groupThousands(digits);
84
+ let wholeDigits = whole.replace(/^0+/, "");
85
+ if (wholeDigits === "" && whole !== "") wholeDigits = "0";
86
+ if (decimalPlaces === 0 || fractions.length === 0) return groupThousands(wholeDigits);
87
+ if (wholeDigits === "") wholeDigits = "0";
86
88
  const fraction = fractions.join("").slice(0, decimalPlaces);
87
- return `${groupThousands(digits) || "0"}.${fraction}`;
89
+ return `${groupThousands(wholeDigits)}.${fraction}`;
88
90
  }
89
91
  function toMinorUnits(text, decimalPlaces) {
90
92
  const plain = text.replaceAll(",", "");
@@ -13,18 +13,23 @@ const AVATAR_SIZE = {
13
13
  function initials(name) {
14
14
  const words = name.trim().split(/\s+/).filter(Boolean);
15
15
  if (words.length === 0) return "";
16
- return (words.length === 1 ? [words[0]] : [words[0], words[words.length - 1]]).map((word) => word.charAt(0).toUpperCase()).join("");
16
+ const firstInitial = initialOf(words[0]);
17
+ if (words.length === 1) return firstInitial;
18
+ return firstInitial + initialOf(words[words.length - 1]);
19
+ }
20
+ function initialOf(word) {
21
+ return word.charAt(0).toUpperCase();
17
22
  }
18
23
  const Avatar = ({ name = "", image, size = "medium" }) => {
19
24
  const { colors } = useTheme();
20
- const [failed, setFailed] = useState();
25
+ const [failedImage, setFailedImage] = useState();
21
26
  const pixels = AVATAR_SIZE[size];
22
27
  const letters = initials(name);
23
28
  let content;
24
- if (image && failed !== image) content = /* @__PURE__ */ jsx("img", {
29
+ if (image && failedImage !== image) content = /* @__PURE__ */ jsx("img", {
25
30
  src: image,
26
31
  alt: "",
27
- onError: () => setFailed(image),
32
+ onError: () => setFailedImage(image),
28
33
  style: {
29
34
  width: "100%",
30
35
  height: "100%",
@@ -16,10 +16,8 @@ function boxStyle(props, theme, look = {}) {
16
16
  const radius = props.radius ?? look.radius;
17
17
  const style = {};
18
18
  if (background !== void 0) style.background = backgroundColor(background, theme);
19
- if (border !== void 0) {
20
- const color = border === "strong" ? theme.colors.subtle : theme.colors.border;
21
- style.border = border === "none" ? "none" : `1px solid ${color}`;
22
- }
19
+ if (border === "none") style.border = "none";
20
+ else if (border !== void 0) style.border = `1px solid ${border === "strong" ? theme.colors.subtle : theme.colors.border}`;
23
21
  if (radius !== void 0) {
24
22
  style.borderRadius = radius === "full" ? FULL_RADIUS : theme.radius * RADIUS_SCALE[radius];
25
23
  style.overflow = "hidden";
@@ -42,6 +40,7 @@ function backgroundColor(background, theme) {
42
40
  if (BACKGROUND_WORDS.has(background)) return theme.colors[background];
43
41
  return customColor(background, theme);
44
42
  }
43
+ const unknownColors = /* @__PURE__ */ new Set();
45
44
  function customColor(name, theme) {
46
45
  const color = theme.customColors[name];
47
46
  if (color === void 0 && !unknownColors.has(name)) {
@@ -50,7 +49,6 @@ function customColor(name, theme) {
50
49
  }
51
50
  return color;
52
51
  }
53
- const unknownColors = /* @__PURE__ */ new Set();
54
52
  function cssLength(value) {
55
53
  return value === "fill" ? "100%" : value;
56
54
  }
@@ -1,7 +1,7 @@
1
1
  import { useTheme } from "../theme.js";
2
2
  import { SPACE } from "./tokens.js";
3
+ import { clamp } from "./numbers.js";
3
4
  import { paddingStyle } from "./padding.js";
4
- import { clamp } from "./utils.js";
5
5
  import { createEvent } from "@monimesl/dot-core";
6
6
  import { Children, Fragment, isValidElement, useEffect, useEffectEvent, useRef, useState } from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -17,41 +17,41 @@ const Carousel$1 = ({ autoplay = false, interval = 4e3, loop = false, indicator
17
17
  const slides = slidesOf(children);
18
18
  const count = slides.length;
19
19
  const looping = loop && count > 1;
20
- const first = looping ? 1 : 0;
21
- const last = looping ? count + 1 : count - 1;
22
- const [position, setPosition] = useState(() => first + slideIndex(index ?? 0, count));
20
+ const firstPosition = looping ? 1 : 0;
21
+ const lastPosition = looping ? count + 1 : count - 1;
22
+ const [position, setPosition] = useState(() => firstPosition + slideIndex(index ?? 0, count));
23
23
  const [animated, setAnimated] = useState(true);
24
- const [drag, setDrag] = useState(0);
24
+ const [dragOffset, setDragOffset] = useState(0);
25
25
  const [dragging, setDragging] = useState(false);
26
- const [width, setWidth] = useState(0);
26
+ const [viewportWidth, setViewportWidth] = useState(0);
27
27
  const [previousIndex, setPreviousIndex] = useState(index);
28
- const viewport = useRef(null);
29
- const gesture = useRef(null);
30
- const swiped = useRef(false);
28
+ const viewportRef = useRef(null);
29
+ const gestureRef = useRef(null);
30
+ const ignoreNextClick = useRef(false);
31
31
  if (index !== previousIndex) {
32
32
  setPreviousIndex(index);
33
33
  if (index !== void 0) {
34
34
  setAnimated(true);
35
- setPosition(first + slideIndex(index, count));
35
+ setPosition(firstPosition + slideIndex(index, count));
36
36
  }
37
37
  }
38
- const shown = clamp(position, 0, last);
39
- const current = count === 0 ? 0 : wrap(shown - first, count);
40
- const moveTo = (target) => {
41
- const next = clamp(target, 0, last);
38
+ const shownPosition = clamp(position, 0, lastPosition);
39
+ const currentIndex = count === 0 ? 0 : wrapIndex(shownPosition - firstPosition, count);
40
+ const moveTo = (targetPosition) => {
41
+ const nextPosition = clamp(targetPosition, 0, lastPosition);
42
42
  setAnimated(true);
43
- setPosition(next);
44
- const nextIndex = wrap(next - first, count);
45
- if (nextIndex !== current) onChange?.(createEvent("change", { index: nextIndex }));
43
+ setPosition(nextPosition);
44
+ const nextIndex = wrapIndex(nextPosition - firstPosition, count);
45
+ if (nextIndex !== currentIndex) onChange?.(createEvent("change", { index: nextIndex }));
46
46
  };
47
- const step = (direction) => {
48
- const beyondEnd = current + direction < 0 || current + direction >= count;
49
- if (!looping && beyondEnd) moveTo(shown);
50
- else moveTo(shown + direction);
47
+ const stepBy = (direction) => {
48
+ const beyondEnd = currentIndex + direction < 0 || currentIndex + direction >= count;
49
+ if (!looping && beyondEnd) moveTo(shownPosition);
50
+ else moveTo(shownPosition + direction);
51
51
  };
52
52
  const advance = useEffectEvent(() => {
53
- if (!looping && current === count - 1) moveTo(first);
54
- else moveTo(shown + 1);
53
+ if (!looping && currentIndex === count - 1) moveTo(firstPosition);
54
+ else moveTo(shownPosition + 1);
55
55
  });
56
56
  useEffect(() => {
57
57
  if (!autoplay || count < 2 || dragging) return;
@@ -62,10 +62,11 @@ const Carousel$1 = ({ autoplay = false, interval = 4e3, loop = false, indicator
62
62
  count,
63
63
  dragging,
64
64
  interval,
65
- current
65
+ currentIndex
66
66
  ]);
67
67
  useEffect(() => {
68
- if (!looping || position !== 0 && position !== count + 1) return;
68
+ const onCopy = position === 0 || position === count + 1;
69
+ if (!looping || !onCopy) return;
69
70
  const timer = setTimeout(() => {
70
71
  setAnimated(false);
71
72
  setPosition(position === 0 ? count : 1);
@@ -77,27 +78,27 @@ const Carousel$1 = ({ autoplay = false, interval = 4e3, loop = false, indicator
77
78
  count
78
79
  ]);
79
80
  const gestureOf = (event) => {
80
- const inProgress = gesture.current;
81
+ const inProgress = gestureRef.current;
81
82
  return inProgress?.pointerId === event.pointerId ? inProgress : null;
82
83
  };
83
84
  const endGesture = () => {
84
- gesture.current = null;
85
+ gestureRef.current = null;
85
86
  setDragging(false);
86
- setDrag(0);
87
+ setDragOffset(0);
87
88
  setAnimated(true);
88
89
  };
89
90
  const onPointerDown = (event) => {
90
91
  if (count < 2 || !event.isPrimary) return;
91
- gesture.current = {
92
+ gestureRef.current = {
92
93
  pointerId: event.pointerId,
93
94
  startX: event.clientX,
94
95
  startY: event.clientY,
95
96
  startTime: event.timeStamp,
96
97
  swiping: false
97
98
  };
98
- swiped.current = false;
99
+ ignoreNextClick.current = false;
99
100
  setDragging(true);
100
- setWidth(viewport.current?.clientWidth ?? 0);
101
+ setViewportWidth(viewportRef.current?.clientWidth ?? 0);
101
102
  };
102
103
  const onPointerMove = (event) => {
103
104
  const swipe = gestureOf(event);
@@ -111,13 +112,14 @@ const Carousel$1 = ({ autoplay = false, interval = 4e3, loop = false, indicator
111
112
  return;
112
113
  }
113
114
  swipe.swiping = true;
114
- swiped.current = true;
115
+ ignoreNextClick.current = true;
115
116
  event.currentTarget.setPointerCapture?.(event.pointerId);
116
117
  setAnimated(false);
117
118
  }
118
- const beforeFirst = current === 0 && movedX > 0;
119
- const afterLast = current === count - 1 && movedX < 0;
120
- setDrag(!looping && (beforeFirst || afterLast) ? movedX * EDGE_RESISTANCE : movedX);
119
+ const beforeFirst = currentIndex === 0 && movedX > 0;
120
+ const afterLast = currentIndex === count - 1 && movedX < 0;
121
+ if (!looping && (beforeFirst || afterLast)) setDragOffset(movedX * EDGE_RESISTANCE);
122
+ else setDragOffset(movedX);
121
123
  };
122
124
  const onPointerUp = (event) => {
123
125
  const swipe = gestureOf(event);
@@ -126,13 +128,15 @@ const Carousel$1 = ({ autoplay = false, interval = 4e3, loop = false, indicator
126
128
  if (!swipe.swiping) return;
127
129
  const movedX = event.clientX - swipe.startX;
128
130
  const speed = Math.abs(movedX) / Math.max(1, event.timeStamp - swipe.startTime);
129
- if (Math.abs(movedX) > width * SWIPE_DISTANCE || speed > SWIPE_SPEED) step(movedX < 0 ? 1 : -1);
130
- else moveTo(shown);
131
+ if (Math.abs(movedX) > viewportWidth * SWIPE_DISTANCE || speed > SWIPE_SPEED) stepBy(movedX < 0 ? 1 : -1);
132
+ else moveTo(shownPosition);
131
133
  };
132
134
  const gapWidth = SPACE[gap];
133
135
  const transition = animated ? `transform ${SLIDE_DURATION}ms ${SLIDE_EASING}` : "none";
134
- const swipedSlides = width > 0 ? drag / (width + gapWidth) : 0;
135
- const track = looping ? [
136
+ let draggedSlides = 0;
137
+ if (viewportWidth > 0) draggedSlides = dragOffset / (viewportWidth + gapWidth);
138
+ let track = slides;
139
+ if (looping) track = [
136
140
  {
137
141
  ...slides[count - 1],
138
142
  key: "copy-of-last"
@@ -142,7 +146,7 @@ const Carousel$1 = ({ autoplay = false, interval = 4e3, loop = false, indicator
142
146
  ...slides[0],
143
147
  key: "copy-of-first"
144
148
  }
145
- ] : slides;
149
+ ];
146
150
  return /* @__PURE__ */ jsxs("div", {
147
151
  className: "dot-carousel",
148
152
  role: "region",
@@ -155,15 +159,15 @@ const Carousel$1 = ({ autoplay = false, interval = 4e3, loop = false, indicator
155
159
  ...paddingStyle(padding)
156
160
  },
157
161
  children: [/* @__PURE__ */ jsx("div", {
158
- ref: viewport,
162
+ ref: viewportRef,
159
163
  onPointerDown,
160
164
  onPointerMove,
161
165
  onPointerUp,
162
166
  onPointerCancel: endGesture,
163
167
  onDragStart: (event) => event.preventDefault(),
164
168
  onClickCapture: (event) => {
165
- if (swiped.current) {
166
- swiped.current = false;
169
+ if (ignoreNextClick.current) {
170
+ ignoreNextClick.current = false;
167
171
  event.stopPropagation();
168
172
  }
169
173
  },
@@ -176,17 +180,17 @@ const Carousel$1 = ({ autoplay = false, interval = 4e3, loop = false, indicator
176
180
  style: {
177
181
  display: "flex",
178
182
  gap: gapWidth,
179
- transform: `translateX(calc(${-shown} * (100% + ${gapWidth}px) + ${drag}px))`,
183
+ transform: `translateX(calc(${-shownPosition} * (100% + ${gapWidth}px) + ${dragOffset}px))`,
180
184
  transition
181
185
  },
182
186
  children: track.map((slide, trackPosition) => {
183
- const visible = trackPosition === shown;
184
- const distance = Math.abs(trackPosition - shown + swipedSlides);
187
+ const visible = trackPosition === shownPosition;
188
+ const distance = Math.abs(trackPosition - shownPosition + draggedSlides);
185
189
  const scale = 1 - Math.min(distance, 1) * .15000000000000002;
186
190
  return /* @__PURE__ */ jsx("div", {
187
191
  role: "group",
188
192
  "aria-roledescription": "slide",
189
- "aria-label": `${wrap(trackPosition - first, count) + 1} of ${count}`,
193
+ "aria-label": `${wrapIndex(trackPosition - firstPosition, count) + 1} of ${count}`,
190
194
  "aria-hidden": !visible,
191
195
  inert: !visible,
192
196
  style: {
@@ -205,13 +209,13 @@ const Carousel$1 = ({ autoplay = false, interval = 4e3, loop = false, indicator
205
209
  display: "flex",
206
210
  justifyContent: "center"
207
211
  },
208
- children: slides.map((slide, slideIndex) => {
209
- const active = slideIndex === current;
212
+ children: slides.map((slide, dotIndex) => {
213
+ const active = dotIndex === currentIndex;
210
214
  return /* @__PURE__ */ jsx("button", {
211
215
  type: "button",
212
- "aria-label": `Slide ${slideIndex + 1} of ${count}`,
216
+ "aria-label": `Slide ${dotIndex + 1} of ${count}`,
213
217
  "aria-current": active,
214
- onClick: () => moveTo(first + slideIndex),
218
+ onClick: () => moveTo(firstPosition + dotIndex),
215
219
  style: {
216
220
  padding: "6px 3px",
217
221
  border: "none",
@@ -247,7 +251,7 @@ function slidesOf(children, keyPrefix = "") {
247
251
  function slideIndex(index, count) {
248
252
  return clamp(Math.round(index), 0, count - 1);
249
253
  }
250
- function wrap(value, count) {
254
+ function wrapIndex(value, count) {
251
255
  return (value % count + count) % count;
252
256
  }
253
257
  //#endregion
@@ -1,8 +1,9 @@
1
1
  import { useTheme } from "../theme.js";
2
- import { DISABLED_OPACITY, SPACE, WEIGHTS } from "./tokens.js";
2
+ import { CENTERED, DISABLED_OPACITY, SPACE, WEIGHTS } from "./tokens.js";
3
3
  import { Field } from "./field.js";
4
4
  import { fieldBorderColor, useFieldNote } from "./field-box.js";
5
5
  import { useTypedText } from "./typed-text.js";
6
+ import { wholeCount } from "./numbers.js";
6
7
  import { createEvent } from "@monimesl/dot-core";
7
8
  import { useState } from "react";
8
9
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -12,17 +13,15 @@ const CodeInput$1 = ({ value, length = 6, label, hint, error, secure, disabled,
12
13
  const [text, typeText] = useTypedText(value, onChange !== void 0);
13
14
  const [focused, setFocused] = useState(false);
14
15
  const { noteId, controlAttributes } = useFieldNote(hint, error);
15
- const count = Math.max(1, Math.round(length));
16
+ const count = wholeCount(length);
16
17
  const digits = text.slice(0, count);
17
- const next = Math.min(digits.length, count - 1);
18
+ const nextBox = Math.min(digits.length, count - 1);
18
19
  const boxes = Array.from({ length: count }, (_, index) => {
19
20
  const digit = digits[index];
20
- const borderColor = fieldBorderColor(colors, error, focused && index === next);
21
+ const borderColor = fieldBorderColor(colors, error, focused && index === nextBox);
21
22
  return /* @__PURE__ */ jsx("span", {
22
23
  style: {
23
- display: "flex",
24
- alignItems: "center",
25
- justifyContent: "center",
24
+ ...CENTERED,
26
25
  flex: "1 1 0",
27
26
  maxWidth: 52,
28
27
  height: 52,
@@ -1,5 +1,5 @@
1
1
  import { useTheme } from "../theme.js";
2
- import { FONT_SIZE, ICON_SIZE, SPACE, WEIGHTS } from "./tokens.js";
2
+ import { CENTERED, FONT_SIZE, ICON_SIZE, SPACE, WEIGHTS } from "./tokens.js";
3
3
  import { insets, paddingStyle } from "./padding.js";
4
4
  import { IconGraphic } from "./icon.js";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -20,9 +20,7 @@ const EmptyState = ({ icon, title, text, children, ...padding }) => {
20
20
  children: [
21
21
  icon ? /* @__PURE__ */ jsx("span", {
22
22
  style: {
23
- display: "flex",
24
- alignItems: "center",
25
- justifyContent: "center",
23
+ ...CENTERED,
26
24
  width: 64,
27
25
  height: 64,
28
26
  borderRadius: "50%",
@@ -2,8 +2,8 @@ import { useTheme } from "../theme.js";
2
2
  import { FONT_SIZE, ICON_SIZE, SPACE, WEIGHTS } from "./tokens.js";
3
3
  import { CLOSE_ARTWORK } from "./icon-artwork.js";
4
4
  import { IconSvg } from "./icon-svg.js";
5
+ import { clamp } from "./numbers.js";
5
6
  import { insets, paddingStyle } from "./padding.js";
6
- import { clamp } from "./utils.js";
7
7
  import { STATUS_ARTWORK, toneBackground, toneColor } from "./tones.js";
8
8
  import { IconButton } from "./icon-button.js";
9
9
  import { createEvent } from "@monimesl/dot-core";
@@ -1,5 +1,6 @@
1
1
  import { useTheme } from "../theme.js";
2
- import { CARD_RADIUS_SCALE, FONT_SIZE, PILL_STYLE, SPACE } from "./tokens.js";
2
+ import { CARD_RADIUS_SCALE, CENTERED, FONT_SIZE, PILL_STYLE, SPACE } from "./tokens.js";
3
+ import { wholeCount } from "./numbers.js";
3
4
  import { paddingStyle } from "./padding.js";
4
5
  import { IconGraphic } from "./icon.js";
5
6
  import { pressable } from "./pressable.js";
@@ -11,7 +12,7 @@ const Grid$1 = ({ columns = 4, gap = "medium", children, ...padding }) => /* @__
11
12
  className: "dot-grid",
12
13
  style: {
13
14
  display: "grid",
14
- gridTemplateColumns: `repeat(${Math.max(1, Math.round(columns))}, minmax(0, 1fr))`,
15
+ gridTemplateColumns: `repeat(${wholeCount(columns)}, minmax(0, 1fr))`,
15
16
  gap: SPACE[gap],
16
17
  ...paddingStyle(padding)
17
18
  },
@@ -54,9 +55,7 @@ const GridItem$1 = ({ label, icon, image, badge, onTap }) => {
54
55
  children: [/* @__PURE__ */ jsxs("span", {
55
56
  style: {
56
57
  position: "relative",
57
- display: "flex",
58
- alignItems: "center",
59
- justifyContent: "center",
58
+ ...CENTERED,
60
59
  flexShrink: 0,
61
60
  width: TILE_SIZE,
62
61
  height: TILE_SIZE,
@@ -1,5 +1,5 @@
1
1
  import { useTheme } from "../theme.js";
2
- import { CARD_RADIUS_SCALE, FONT_SIZE, ICON_SIZE, WEIGHTS } from "./tokens.js";
2
+ import { CARD_RADIUS_SCALE, CENTERED, FONT_SIZE, ICON_SIZE, WEIGHTS } from "./tokens.js";
3
3
  import { CHEVRON_RIGHT_ARTWORK } from "./icon-artwork.js";
4
4
  import { IconSvg } from "./icon-svg.js";
5
5
  import { insets, paddingStyle } from "./padding.js";
@@ -11,22 +11,22 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
11
  const END_MARGIN = "0px 0px 200px 0px";
12
12
  const List$1 = ({ onEndReached, children, ...padding }) => {
13
13
  const { colors, radius } = useTheme();
14
- const list = useRef(null);
15
- const reachedAt = useRef(null);
14
+ const listRef = useRef(null);
15
+ const lastRowReached = useRef(null);
16
16
  useEffect(() => {
17
- const last = list.current?.lastElementChild;
18
- if (!onEndReached || !last || typeof IntersectionObserver === "undefined") return;
17
+ const lastRow = listRef.current?.lastElementChild;
18
+ if (!onEndReached || !lastRow || typeof IntersectionObserver === "undefined") return;
19
19
  const observer = new IntersectionObserver((entries) => {
20
- if (entries.some((entry) => entry.isIntersecting) && reachedAt.current !== last) {
21
- reachedAt.current = last;
20
+ if (entries.some((entry) => entry.isIntersecting) && lastRowReached.current !== lastRow) {
21
+ lastRowReached.current = lastRow;
22
22
  onEndReached(createEvent("endReached"));
23
23
  }
24
24
  }, { rootMargin: END_MARGIN });
25
- observer.observe(last);
25
+ observer.observe(lastRow);
26
26
  return () => observer.disconnect();
27
27
  });
28
28
  return /* @__PURE__ */ jsx("ul", {
29
- ref: list,
29
+ ref: listRef,
30
30
  className: "dot-list",
31
31
  style: {
32
32
  display: "flex",
@@ -59,9 +59,7 @@ const ListItem$1 = ({ title, subtitle, value, icon, image, onTap, children, ...p
59
59
  });
60
60
  else if (icon) leading = /* @__PURE__ */ jsx("span", {
61
61
  style: {
62
- display: "flex",
63
- alignItems: "center",
64
- justifyContent: "center",
62
+ ...CENTERED,
65
63
  width: 36,
66
64
  height: 36,
67
65
  borderRadius: "50%",
@@ -0,0 +1,9 @@
1
+ //#region src/components/numbers.ts
2
+ function clamp(value, min, max) {
3
+ return Math.max(min, Math.min(value, max));
4
+ }
5
+ function wholeCount(count) {
6
+ return Math.max(1, Math.round(count));
7
+ }
8
+ //#endregion
9
+ export { clamp, wholeCount };
@@ -14,14 +14,17 @@ function insets(block, inline = block) {
14
14
  bottom: block
15
15
  };
16
16
  }
17
- function paddingStyle(props, own = NO_INSETS) {
18
- const side = (space, pixels) => space === void 0 ? pixels : SPACE[space];
17
+ function paddingStyle(props, ownInsets = NO_INSETS) {
19
18
  return {
20
- paddingTop: side(props.paddingTop ?? props.padding, own.top),
21
- paddingInlineStart: side(props.paddingStart ?? props.padding, own.start),
22
- paddingInlineEnd: side(props.paddingEnd ?? props.padding, own.end),
23
- paddingBottom: side(props.paddingBottom ?? props.padding, own.bottom)
19
+ paddingTop: sidePadding(props.paddingTop ?? props.padding, ownInsets.top),
20
+ paddingInlineStart: sidePadding(props.paddingStart ?? props.padding, ownInsets.start),
21
+ paddingInlineEnd: sidePadding(props.paddingEnd ?? props.padding, ownInsets.end),
22
+ paddingBottom: sidePadding(props.paddingBottom ?? props.padding, ownInsets.bottom)
24
23
  };
25
24
  }
25
+ function sidePadding(pageSpace, ownPixels) {
26
+ if (pageSpace === void 0) return ownPixels;
27
+ return SPACE[pageSpace];
28
+ }
26
29
  //#endregion
27
30
  export { insets, paddingStyle };
@@ -6,13 +6,13 @@ import { jsx } from "react/jsx-runtime";
6
6
  //#region src/components/scroll.tsx
7
7
  const REST_DELAY_MS = 120;
8
8
  const Scroll$1 = ({ gap = "medium", snap = "none", index, onChange, children, ...padding }) => {
9
- const row = useRef(null);
9
+ const rowRef = useRef(null);
10
10
  const reportedIndex = useRef(void 0);
11
11
  const restTimer = useRef(void 0);
12
12
  const paddingCss = paddingStyle(padding);
13
13
  const startPadding = Number(paddingCss.paddingInlineStart);
14
14
  useEffect(() => {
15
- const element = row.current;
15
+ const element = rowRef.current;
16
16
  if (index === void 0 || index === reportedIndex.current || !element) return;
17
17
  const child = element.children[index];
18
18
  if (child) {
@@ -23,20 +23,22 @@ const Scroll$1 = ({ gap = "medium", snap = "none", index, onChange, children, ..
23
23
  });
24
24
  }
25
25
  }, [index, startPadding]);
26
- useEffect(() => () => clearTimeout(restTimer.current), []);
26
+ useEffect(() => {
27
+ return () => clearTimeout(restTimer.current);
28
+ }, []);
27
29
  return /* @__PURE__ */ jsx("div", {
28
- ref: row,
30
+ ref: rowRef,
29
31
  className: "dot-scroll",
30
32
  onScroll: onChange ? () => {
31
33
  clearTimeout(restTimer.current);
32
34
  restTimer.current = setTimeout(() => {
33
- const element = row.current;
35
+ const element = rowRef.current;
34
36
  if (!element) return;
35
37
  const visibleStart = element.scrollLeft + startPadding;
36
- const firstHalfInView = [...element.children].findIndex((item) => item.offsetLeft + item.offsetWidth / 2 >= visibleStart);
37
- if (firstHalfInView !== -1 && firstHalfInView !== reportedIndex.current) {
38
- reportedIndex.current = firstHalfInView;
39
- onChange(createEvent("change", { index: firstHalfInView }));
38
+ const firstShownIndex = [...element.children].findIndex((item) => item.offsetLeft + item.offsetWidth / 2 >= visibleStart);
39
+ if (firstShownIndex !== -1 && firstShownIndex !== reportedIndex.current) {
40
+ reportedIndex.current = firstShownIndex;
41
+ onChange(createEvent("change", { index: firstShownIndex }));
40
42
  }
41
43
  }, REST_DELAY_MS);
42
44
  } : void 0,
@@ -16,7 +16,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
16
16
  const SheetPanel = ({ open, title, showCloseIcon = false, onClose, children, ...padding }) => {
17
17
  const { colors, radius } = useTheme();
18
18
  const container = usePortalContainer();
19
- const corner = radius * CARD_RADIUS_SCALE;
19
+ const cornerRadius = radius * CARD_RADIUS_SCALE;
20
20
  return /* @__PURE__ */ jsx(DialogRoot, {
21
21
  open,
22
22
  onOpenChange: (next) => {
@@ -51,8 +51,8 @@ const SheetPanel = ({ open, title, showCloseIcon = false, onClose, children, ...
51
51
  end: 16,
52
52
  bottom: 24
53
53
  }),
54
- borderTopLeftRadius: corner,
55
- borderTopRightRadius: corner,
54
+ borderTopLeftRadius: cornerRadius,
55
+ borderTopRightRadius: cornerRadius,
56
56
  background: colors.surface,
57
57
  color: colors.text,
58
58
  fontFamily: FONT_FAMILY,