@monimesl/dot-renderer-react 0.1.0-alpha.9 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +24 -20
  2. package/dist/color.js +25 -36
  3. package/dist/components/amount-input.js +26 -21
  4. package/dist/components/avatar.d.ts +6 -0
  5. package/dist/components/avatar.js +68 -0
  6. package/dist/components/box.js +56 -0
  7. package/dist/components/carousel.js +70 -66
  8. package/dist/components/code-input.js +6 -7
  9. package/dist/components/disclosure.d.ts +6 -0
  10. package/dist/components/disclosure.js +76 -0
  11. package/dist/components/empty-state.js +2 -4
  12. package/dist/components/feedback.js +39 -23
  13. package/dist/components/footer.js +3 -2
  14. package/dist/components/grid.js +4 -5
  15. package/dist/components/https.js +11 -0
  16. package/dist/components/icon-artwork.js +139 -70
  17. package/dist/components/icon-button.js +28 -0
  18. package/dist/components/icon-svg.js +27 -0
  19. package/dist/components/icon.js +5 -20
  20. package/dist/components/index.d.ts +9 -1
  21. package/dist/components/index.js +22 -2
  22. package/dist/components/input.js +24 -2
  23. package/dist/components/link.js +1 -8
  24. package/dist/components/list.js +17 -17
  25. package/dist/components/numbers.js +9 -0
  26. package/dist/components/option.js +4 -3
  27. package/dist/components/padding.js +18 -7
  28. package/dist/components/prose-context.js +5 -0
  29. package/dist/components/prose.d.ts +9 -0
  30. package/dist/components/prose.js +67 -0
  31. package/dist/components/rich-text.d.ts +6 -0
  32. package/dist/components/rich-text.js +73 -0
  33. package/dist/components/scroll.js +27 -25
  34. package/dist/components/select.js +4 -3
  35. package/dist/components/sheet.js +9 -8
  36. package/dist/components/slider.d.ts +6 -0
  37. package/dist/components/slider.js +57 -0
  38. package/dist/components/stack.d.ts +6 -0
  39. package/dist/components/stack.js +25 -0
  40. package/dist/components/stepper.d.ts +6 -0
  41. package/dist/components/stepper.js +92 -0
  42. package/dist/components/steps.d.ts +7 -0
  43. package/dist/components/steps.js +110 -0
  44. package/dist/components/tabs.js +23 -19
  45. package/dist/components/text.js +74 -13
  46. package/dist/components/toast.js +4 -4
  47. package/dist/components/toggles.js +6 -7
  48. package/dist/components/tokens.js +14 -2
  49. package/dist/components/tones.js +13 -7
  50. package/dist/components/typed-text.js +2 -2
  51. package/dist/components/view.js +19 -9
  52. package/dist/index.d.ts +9 -1
  53. package/dist/index.js +10 -2
  54. package/dist/markdown.js +255 -0
  55. package/dist/page-root.js +1 -1
  56. package/dist/pull-to-refresh.js +120 -0
  57. package/dist/root.js +31 -11
  58. package/dist/styles.css +1 -95
  59. package/dist/theme.d.ts +3 -2
  60. package/dist/theme.js +60 -15
  61. package/package.json +8 -7
package/README.md CHANGED
@@ -1,41 +1,45 @@
1
1
  # @monimesl/dot-renderer-react
2
2
 
3
- Renders Dot pages with React. It is the renderer every dot runs with: dots depend on
4
- `@monimesl/dot-runtime`, which brings it in, and never import it themselves.
3
+ Renders Dot pages with React. Every dot runs with this renderer. Dots get it through
4
+ `@monimesl/dot-runtime` and never import it themselves.
5
5
 
6
6
  ## How it draws
7
7
 
8
- Each built-in component is a React component that draws plain elements with Dot's own inline
9
- styles, in the colors and shape of the dot's theme. A small stylesheet, `dist/styles.css`, holds
10
- what inline styles can't say, such as keyframes and scroll bars; the compiler copies it into each
11
- dot as `dot.css`.
8
+ Each built-in component is a React component that draws plain elements with inline styles, in
9
+ the colors and shape of the dot's theme. A small stylesheet, `dist/styles.css`, covers what inline
10
+ styles can't do, such as keyframes and scroll bars. The compiler copies it into each dot as
11
+ `dot.css`.
12
12
 
13
- [Base UI](https://base-ui.com)'s headless parts give five components the focus, keyboard and
13
+ Five components use [Base UI](https://base-ui.com)'s headless parts for the focus, keyboard and
14
14
  screen-reader behaviour that is hard to get right: `Sheet` (and the sheet a `Select` opens), `Tabs`,
15
15
  `Switch`, `Checkbox` and `RadioGroup`. A sheet keeps focus inside it, closes on Escape and stops the
16
16
  page behind from scrolling. Fields link their hint or error to the control they describe.
17
17
 
18
- Icons are [Phosphor](https://phosphoricons.com)'s bold artwork, generated into the source at build
19
- time from `@phosphor-icons/core`.
18
+ Icons are [Phosphor](https://phosphoricons.com)'s bold artwork. A script generates them into the
19
+ source at build time, from `@phosphor-icons/core`.
20
20
 
21
21
  ## The theme
22
22
 
23
- A dot sets `scheme`, its `colors` (`primary`, `background`, `surface` and `text`, each for light
24
- and dark) and `shape` in `defineDot`. The renderer fills in the rest of its palette, the neutrals
25
- and the status colors, from Dash's own look. `useTheme()` gives a component the resolved theme.
23
+ A dot sets `scheme`, `colors` and `shape` in `defineDot`. The colors are `primary`, `background`,
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.
26
29
 
27
30
  ## Only the components a dot uses
28
31
 
29
32
  A dot's production build carries only the components its pages use. The compiler lists them from
30
- the pages and makes the webview with `createReactRenderer({ components })`, importing each one by
31
- name. The renderer is built as a file per module, with the libraries it bundles under
32
- `dist/vendor/`, so a dot's bundle leaves out a component, and what only it imports, entirely. A
33
- development build has every component, since a hot reload keeps the renderer a page runs with.
33
+ the pages, imports each one by name and makes the webview with
34
+ `createReactRenderer({ components })`. The renderer is built as one file per module, with the
35
+ libraries it bundles under `dist/vendor/`. So a dot's bundle can leave out a component, and
36
+ anything only that component imports. A development build has every component, because a hot reload
37
+ keeps the renderer the page already runs with.
34
38
 
35
- `reactRenderer()`, the default export, makes a renderer with every built-in component, and takes
36
- `components` to add or replace some, and a `wrapper` around each page.
39
+ `reactRenderer()`, the default export, makes a renderer with every built-in component. Pass
40
+ `components` to add or replace some, and `wrapper` to wrap each page.
37
41
 
38
42
  ## React
39
43
 
40
- Hosts serve React, so dots don't bundle it. The build makes the React that hosts ship, in
41
- `dist/react-host/<version>/`, and a dot's page imports it through an import map.
44
+ Hosts serve React, so dots don't bundle it. This package's build makes the React that hosts ship,
45
+ in `dist/react-host/<version>/`. A dot's page imports it through an import map.
package/dist/color.js CHANGED
@@ -1,20 +1,12 @@
1
1
  //#region src/color.ts
2
- const READABLE_CONTRAST = 4.5;
3
- function contrast(first, second) {
4
- const [lighter, darker] = [luminance(first), luminance(second)].sort((a, b) => b - a);
5
- 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));
6
5
  }
7
6
  function darken(color, points) {
8
7
  const [hue, saturation, lightness] = toHsl(channels(color));
9
8
  return toHex(fromHsl(hue, saturation, Math.max(0, lightness - points / 100)));
10
9
  }
11
- function luminance(color) {
12
- const [red, green, blue] = channels(color).map((channel) => {
13
- const share = channel / 255;
14
- return share <= .03928 ? share / 12.92 : ((share + .055) / 1.055) ** 2.4;
15
- });
16
- return .2126 * red + .7152 * green + .0722 * blue;
17
- }
18
10
  function channels(color) {
19
11
  let hex = color.slice(1);
20
12
  if (hex.length === 3 || hex.length === 4) hex = [...hex].map((digit) => digit + digit).join("");
@@ -22,7 +14,7 @@ function channels(color) {
22
14
  0,
23
15
  2,
24
16
  4
25
- ].map((start) => parseInt(hex.slice(start, start + 2), 16));
17
+ ].map((offset) => parseInt(hex.slice(offset, offset + 2), 16));
26
18
  return [
27
19
  red,
28
20
  green,
@@ -33,13 +25,11 @@ function toHex(rgb) {
33
25
  return `#${rgb.map((channel) => Math.round(channel).toString(16).padStart(2, "0")).join("")}`;
34
26
  }
35
27
  function toHsl([red, green, blue]) {
36
- const [r, g, b] = [
37
- red / 255,
38
- green / 255,
39
- blue / 255
40
- ];
41
- const max = Math.max(r, g, b);
42
- const min = Math.min(r, g, b);
28
+ const redShare = red / 255;
29
+ const greenShare = green / 255;
30
+ const blueShare = blue / 255;
31
+ const max = Math.max(redShare, greenShare, blueShare);
32
+ const min = Math.min(redShare, greenShare, blueShare);
43
33
  const lightness = (max + min) / 2;
44
34
  const chroma = max - min;
45
35
  if (chroma === 0) return [
@@ -49,9 +39,9 @@ function toHsl([red, green, blue]) {
49
39
  ];
50
40
  const saturation = chroma / (1 - Math.abs(2 * lightness - 1));
51
41
  let hue;
52
- if (max === r) hue = (g - b) / chroma % 6;
53
- else if (max === g) hue = (b - r) / chroma + 2;
54
- else hue = (r - g) / chroma + 4;
42
+ if (max === redShare) hue = (greenShare - blueShare) / chroma % 6;
43
+ else if (max === greenShare) hue = (blueShare - redShare) / chroma + 2;
44
+ else hue = (redShare - greenShare) / chroma + 4;
55
45
  return [
56
46
  (hue * 60 + 360) % 360,
57
47
  saturation,
@@ -60,46 +50,45 @@ function toHsl([red, green, blue]) {
60
50
  }
61
51
  function fromHsl(hue, saturation, lightness) {
62
52
  const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
63
- const second = chroma * (1 - Math.abs(hue / 60 % 2 - 1));
64
- const sector = Math.floor(hue / 60) % 6;
65
- const [r, g, b] = [
53
+ const middle = chroma * (1 - Math.abs(hue / 60 % 2 - 1));
54
+ const [red, green, blue] = [
66
55
  [
67
56
  chroma,
68
- second,
57
+ middle,
69
58
  0
70
59
  ],
71
60
  [
72
- second,
61
+ middle,
73
62
  chroma,
74
63
  0
75
64
  ],
76
65
  [
77
66
  0,
78
67
  chroma,
79
- second
68
+ middle
80
69
  ],
81
70
  [
82
71
  0,
83
- second,
72
+ middle,
84
73
  chroma
85
74
  ],
86
75
  [
87
- second,
76
+ middle,
88
77
  0,
89
78
  chroma
90
79
  ],
91
80
  [
92
81
  chroma,
93
82
  0,
94
- second
83
+ middle
95
84
  ]
96
- ][sector];
85
+ ][Math.floor(hue / 60) % 6];
97
86
  const lift = lightness - chroma / 2;
98
87
  return [
99
- (r + lift) * 255,
100
- (g + lift) * 255,
101
- (b + lift) * 255
88
+ (red + lift) * 255,
89
+ (green + lift) * 255,
90
+ (blue + lift) * 255
102
91
  ];
103
92
  }
104
93
  //#endregion
105
- export { READABLE_CONTRAST, contrast, darken };
94
+ export { darken, mix };
@@ -10,10 +10,10 @@ const AMOUNT_FONT_SIZE = 28;
10
10
  const DEFAULT_CURRENCY = "SLE";
11
11
  const AmountInput$1 = ({ value, currency = DEFAULT_CURRENCY, minorUnits, unit = "minor", label, placeholder, hint, error, disabled, onChange }) => {
12
12
  const { colors } = useTheme();
13
- const places = minorUnits === void 0 ? currencyMinorUnits(currency) : Math.max(0, Math.round(minorUnits));
14
- const scale = unit === "major" ? 10 ** places : 1;
15
- const show = (minor) => formatMinorUnits(minor, places);
16
- const [text, typeText, reshowText] = useTypedText(amountText(value, scale), onChange !== void 0, (sent) => sent === "" ? "" : show(Number(sent)));
13
+ const decimalPlaces = minorUnits === void 0 ? currencyMinorUnits(currency) : Math.max(0, Math.round(minorUnits));
14
+ const scale = unit === "major" ? 10 ** decimalPlaces : 1;
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,19 +39,19 @@ 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),
43
- inputMode: places > 0 ? "decimal" : "numeric",
42
+ placeholder: placeholder ?? formatAmount(0),
43
+ inputMode: decimalPlaces > 0 ? "decimal" : "numeric",
44
44
  disabled,
45
45
  ...controlAttributes,
46
46
  onFocus: box.onFocus,
47
47
  onBlur: () => {
48
48
  box.onBlur();
49
- const amount = toMinorUnits(text, places);
50
- if (amount !== null) reshowText(show(amount));
49
+ const amount = toMinorUnits(text, decimalPlaces);
50
+ if (amount !== null) reshowText(formatAmount(amount));
51
51
  },
52
52
  onChange: (event) => {
53
- const typed = cleanAmount(event.currentTarget.value, places);
54
- const amount = toMinorUnits(typed, places);
53
+ const typed = cleanAmount(event.currentTarget.value, decimalPlaces);
54
+ const amount = toMinorUnits(typed, decimalPlaces);
55
55
  typeText(typed, amount === null ? "" : String(amount));
56
56
  onChange?.(createEvent("change", { value: amount === null ? null : amount / scale }));
57
57
  },
@@ -79,23 +79,28 @@ function amountText(value, scale) {
79
79
  if (value === void 0) return;
80
80
  return Number.isFinite(value) ? String(Math.round(value * scale)) : "";
81
81
  }
82
- function cleanAmount(typed, places) {
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 (places === 0 || fractions.length === 0) return groupThousands(digits);
86
- const fraction = fractions.join("").slice(0, places);
87
- return `${groupThousands(digits) || "0"}.${fraction}`;
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";
88
+ const fraction = fractions.join("").slice(0, decimalPlaces);
89
+ return `${groupThousands(wholeDigits)}.${fraction}`;
88
90
  }
89
- function toMinorUnits(text, places) {
91
+ function toMinorUnits(text, decimalPlaces) {
90
92
  const plain = text.replaceAll(",", "");
91
93
  if (plain === "") return null;
92
94
  const [whole = "", fraction = ""] = plain.split(".");
93
- return Number(`${whole}${fraction.padEnd(places, "0").slice(0, places)}`);
95
+ const fractionDigits = fraction.padEnd(decimalPlaces, "0").slice(0, decimalPlaces);
96
+ return Number(`${whole}${fractionDigits}`);
94
97
  }
95
- function formatMinorUnits(minorUnits, places) {
96
- const digits = String(Math.abs(minorUnits)).padStart(places + 1, "0");
97
- const grouped = groupThousands(digits.slice(0, digits.length - places));
98
- return places === 0 ? grouped : `${grouped}.${digits.slice(digits.length - places)}`;
98
+ function formatMinorUnits(minorUnits, decimalPlaces) {
99
+ const digits = String(Math.abs(minorUnits)).padStart(decimalPlaces + 1, "0");
100
+ const wholeEnd = digits.length - decimalPlaces;
101
+ const grouped = groupThousands(digits.slice(0, wholeEnd));
102
+ if (decimalPlaces === 0) return grouped;
103
+ return `${grouped}.${digits.slice(wholeEnd)}`;
99
104
  }
100
105
  function groupThousands(digits) {
101
106
  const groups = [];
@@ -0,0 +1,6 @@
1
+ import { ReactComponent } from "../types.js";
2
+ import { Avatar } from "@monimesl/dot-core";
3
+ //#region src/components/avatar.d.ts
4
+ declare const Avatar$1: ReactComponent<typeof Avatar>;
5
+ //#endregion
6
+ export { Avatar$1 as Avatar };
@@ -0,0 +1,68 @@
1
+ import { useTheme } from "../theme.js";
2
+ import { WEIGHTS } from "./tokens.js";
3
+ import { USER_ARTWORK } from "./icon-artwork.js";
4
+ import { IconSvg } from "./icon-svg.js";
5
+ import { useState } from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
+ //#region src/components/avatar.tsx
8
+ const AVATAR_SIZE = {
9
+ small: 32,
10
+ medium: 40,
11
+ large: 56
12
+ };
13
+ function initials(name) {
14
+ const words = name.trim().split(/\s+/).filter(Boolean);
15
+ if (words.length === 0) return "";
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();
22
+ }
23
+ const Avatar = ({ name = "", image, size = "medium" }) => {
24
+ const { colors } = useTheme();
25
+ const [failedImage, setFailedImage] = useState();
26
+ const pixels = AVATAR_SIZE[size];
27
+ const letters = initials(name);
28
+ let content;
29
+ if (image && failedImage !== image) content = /* @__PURE__ */ jsx("img", {
30
+ src: image,
31
+ alt: "",
32
+ onError: () => setFailedImage(image),
33
+ style: {
34
+ width: "100%",
35
+ height: "100%",
36
+ objectFit: "cover"
37
+ }
38
+ });
39
+ else if (letters) content = letters;
40
+ else content = /* @__PURE__ */ jsx(IconSvg, {
41
+ artwork: USER_ARTWORK,
42
+ size: Math.round(pixels * .55),
43
+ color: "currentColor"
44
+ });
45
+ return /* @__PURE__ */ jsx("span", {
46
+ className: "dot-avatar",
47
+ role: "img",
48
+ "aria-label": name || void 0,
49
+ style: {
50
+ display: "inline-flex",
51
+ flexShrink: 0,
52
+ alignItems: "center",
53
+ justifyContent: "center",
54
+ width: pixels,
55
+ height: pixels,
56
+ borderRadius: "50%",
57
+ overflow: "hidden",
58
+ background: colors.muted,
59
+ color: colors.subtle,
60
+ fontSize: Math.round(pixels * .4),
61
+ fontWeight: WEIGHTS.bold,
62
+ lineHeight: 1
63
+ },
64
+ children: content
65
+ });
66
+ };
67
+ //#endregion
68
+ export { Avatar };
@@ -0,0 +1,56 @@
1
+ import { CARD_RADIUS_SCALE } from "./tokens.js";
2
+ import { isTone, toneBackground } from "./tones.js";
3
+ import { BACKGROUNDS } from "@monimesl/dot-core";
4
+ //#region src/components/box.ts
5
+ const RADIUS_SCALE = {
6
+ none: 0,
7
+ small: .5,
8
+ medium: 1,
9
+ large: CARD_RADIUS_SCALE
10
+ };
11
+ const FULL_RADIUS = 9999;
12
+ const BACKGROUND_WORDS = new Set(BACKGROUNDS.words);
13
+ function boxStyle(props, theme, look = {}) {
14
+ const background = props.background ?? look.background;
15
+ const border = props.border ?? look.border;
16
+ const radius = props.radius ?? look.radius;
17
+ const style = {};
18
+ if (background !== void 0) style.background = backgroundColor(background, theme);
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}`;
21
+ if (radius !== void 0) {
22
+ style.borderRadius = radius === "full" ? FULL_RADIUS : theme.radius * RADIUS_SCALE[radius];
23
+ style.overflow = "hidden";
24
+ }
25
+ if (props.width !== void 0) {
26
+ style.width = cssLength(props.width);
27
+ style.flexShrink = 0;
28
+ }
29
+ if (props.aspect !== void 0) style.aspectRatio = props.aspect.replace(":", " / ");
30
+ else if (props.height !== void 0) {
31
+ style.height = cssLength(props.height);
32
+ style.flexShrink = 0;
33
+ }
34
+ if (props.minHeight !== void 0) style.minHeight = props.minHeight;
35
+ if (props.maxWidth !== void 0) style.maxWidth = props.maxWidth;
36
+ return style;
37
+ }
38
+ function backgroundColor(background, theme) {
39
+ if (isTone(background)) return toneBackground(theme.colors, background);
40
+ if (BACKGROUND_WORDS.has(background)) return theme.colors[background];
41
+ return customColor(background, theme);
42
+ }
43
+ const unknownColors = /* @__PURE__ */ new Set();
44
+ function customColor(name, theme) {
45
+ const color = theme.customColors[name];
46
+ if (color === void 0 && !unknownColors.has(name)) {
47
+ unknownColors.add(name);
48
+ console.warn(`The theme has no colour named "${name}". Add it to colors.custom in dot.ts.`);
49
+ }
50
+ return color;
51
+ }
52
+ function cssLength(value) {
53
+ return value === "fill" ? "100%" : value;
54
+ }
55
+ //#endregion
56
+ export { boxStyle, customColor };