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

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.
@@ -8,9 +8,9 @@ import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/components/slider.tsx
9
9
  const Slider$1 = ({ value, min = 0, max = 100, step = 1, showValue, label, hint, disabled, onChange }) => {
10
10
  const { colors } = useTheme();
11
- const [position, move] = useTypedText(value === void 0 ? void 0 : String(value), onChange !== void 0);
11
+ const [positionText, showPosition] = useTypedText(value === void 0 ? void 0 : String(value), onChange !== void 0);
12
12
  const { noteId, controlAttributes } = useFieldNote(hint, void 0);
13
- const shown = position === "" ? String(min) : position;
13
+ const shownValue = positionText === "" ? String(min) : positionText;
14
14
  return /* @__PURE__ */ jsxs(Field, {
15
15
  label: void 0,
16
16
  hint,
@@ -27,7 +27,7 @@ const Slider$1 = ({ value, min = 0, max = 100, step = 1, showValue, label, hint,
27
27
  color: colors.subtle,
28
28
  fontVariantNumeric: "tabular-nums"
29
29
  },
30
- children: shown
30
+ children: shownValue
31
31
  }) : null]
32
32
  }) : null, /* @__PURE__ */ jsx("input", {
33
33
  className: "dot-slider",
@@ -35,13 +35,13 @@ const Slider$1 = ({ value, min = 0, max = 100, step = 1, showValue, label, hint,
35
35
  min,
36
36
  max,
37
37
  step,
38
- value: shown,
38
+ value: shownValue,
39
39
  disabled,
40
40
  "aria-label": label,
41
41
  ...controlAttributes,
42
42
  onChange: (event) => {
43
43
  const next = event.currentTarget.value;
44
- move(next);
44
+ showPosition(next);
45
45
  onChange?.(createEvent("change", { value: Number(next) }));
46
46
  },
47
47
  style: {
@@ -1,5 +1,5 @@
1
1
  import { useTheme } from "../theme.js";
2
- import { DISABLED_OPACITY, FONT_SIZE, ICON_SIZE, WEIGHTS } from "./tokens.js";
2
+ import { CENTERED, DISABLED_OPACITY, FONT_SIZE, ICON_SIZE, WEIGHTS } from "./tokens.js";
3
3
  import { Field } from "./field.js";
4
4
  import { useFieldNote } from "./field-box.js";
5
5
  import { useTypedText } from "./typed-text.js";
@@ -9,11 +9,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/components/stepper.tsx
10
10
  const Stepper$1 = ({ value, min, max, step = 1, label, hint, disabled, onChange }) => {
11
11
  const { colors } = useTheme();
12
- const [shown, show] = useTypedText(value === void 0 ? void 0 : String(value), onChange !== void 0);
12
+ const [shownText, showText] = useTypedText(value === void 0 ? void 0 : String(value), onChange !== void 0);
13
13
  const { noteId, controlAttributes } = useFieldNote(hint, void 0);
14
- const current = shown === "" ? min ?? 0 : Number(shown);
14
+ const current = shownText === "" ? min ?? 0 : Number(shownText);
15
15
  const moveTo = (next) => {
16
- show(String(next));
16
+ showText(String(next));
17
17
  onChange?.(createEvent("change", { value: next }));
18
18
  };
19
19
  const atMin = min !== void 0 && current - step < min;
@@ -71,9 +71,7 @@ function StepButton({ icon, label, disabled, onPress }) {
71
71
  disabled,
72
72
  onClick: onPress,
73
73
  style: {
74
- display: "flex",
75
- alignItems: "center",
76
- justifyContent: "center",
74
+ ...CENTERED,
77
75
  width: 40,
78
76
  height: 40,
79
77
  border: "none",
@@ -1,5 +1,5 @@
1
1
  import { useTheme } from "../theme.js";
2
- import { FONT_SIZE, WEIGHTS } from "./tokens.js";
2
+ import { CENTERED, FONT_SIZE, WEIGHTS } from "./tokens.js";
3
3
  import { paddingStyle } from "./padding.js";
4
4
  import { IconGraphic } from "./icon.js";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -15,7 +15,7 @@ const Steps = ({ children, ...padding }) => /* @__PURE__ */ jsx("ol", {
15
15
  },
16
16
  children
17
17
  });
18
- function mark(colors, status) {
18
+ function stepMark(colors, status) {
19
19
  switch (status) {
20
20
  case "done": return {
21
21
  background: colors.success,
@@ -41,7 +41,7 @@ function mark(colors, status) {
41
41
  }
42
42
  const Step = ({ title, text, status = "upcoming" }) => {
43
43
  const { colors } = useTheme();
44
- const { background, border, icon } = mark(colors, status);
44
+ const { background, border, icon } = stepMark(colors, status);
45
45
  return /* @__PURE__ */ jsxs("li", {
46
46
  className: "dot-step",
47
47
  "aria-current": status === "current" ? "step" : void 0,
@@ -58,9 +58,7 @@ const Step = ({ title, text, status = "upcoming" }) => {
58
58
  },
59
59
  children: [/* @__PURE__ */ jsx("span", {
60
60
  style: {
61
- display: "flex",
62
- alignItems: "center",
63
- justifyContent: "center",
61
+ ...CENTERED,
64
62
  flexShrink: 0,
65
63
  width: MARK_SIZE,
66
64
  height: MARK_SIZE,
@@ -1,5 +1,5 @@
1
1
  import { useTheme } from "../theme.js";
2
- import { FONT_SIZE, PILL_STYLE, WEIGHTS } from "./tokens.js";
2
+ import { CENTERED, FONT_SIZE, PILL_STYLE, WEIGHTS } from "./tokens.js";
3
3
  import { paddingStyle } from "./padding.js";
4
4
  import { TabsRoot } from "../vendor/@base-ui/react/tabs/root/TabsRoot.js";
5
5
  import { TabsTab } from "../vendor/@base-ui/react/tabs/tab/TabsTab.js";
@@ -15,6 +15,14 @@ const TabsContext = createContext({
15
15
  const Tabs$1 = ({ value, variant = "underline", onChange, children, ...padding }) => {
16
16
  const { colors, radius } = useTheme();
17
17
  const segmented = variant === "segmented";
18
+ let variantStyle;
19
+ if (segmented) variantStyle = {
20
+ gap: 2,
21
+ padding: 3,
22
+ borderRadius: radius,
23
+ background: colors.muted
24
+ };
25
+ else variantStyle = { borderBottom: `1px solid ${colors.border}` };
18
26
  return /* @__PURE__ */ jsx(TabsRoot, {
19
27
  value: value ?? null,
20
28
  onValueChange: (next) => {
@@ -26,12 +34,7 @@ const Tabs$1 = ({ value, variant = "underline", onChange, children, ...padding }
26
34
  style: {
27
35
  display: "flex",
28
36
  overflowX: "auto",
29
- ...segmented ? {
30
- gap: 2,
31
- padding: 3,
32
- borderRadius: radius,
33
- background: colors.muted
34
- } : { borderBottom: `1px solid ${colors.border}` }
37
+ ...variantStyle
35
38
  },
36
39
  children: /* @__PURE__ */ jsx(TabsContext.Provider, {
37
40
  value: {
@@ -50,13 +53,22 @@ const Tab$1 = ({ value, label, badge }) => {
50
53
  const selected = value === chosen;
51
54
  let color = colors.subtle;
52
55
  if (selected) color = segmented ? colors.text : colors.primary;
56
+ let variantStyle;
57
+ if (segmented) variantStyle = {
58
+ borderRadius: Math.max(radius - 2, 0),
59
+ background: selected ? colors.surface : "transparent",
60
+ boxShadow: selected ? "0 1px 3px rgba(0, 0, 0, 0.15)" : "none"
61
+ };
62
+ else variantStyle = {
63
+ marginBottom: -1,
64
+ background: "transparent",
65
+ borderBottom: `2px solid ${selected ? colors.primary : "transparent"}`
66
+ };
53
67
  return /* @__PURE__ */ jsxs(TabsTab, {
54
68
  className: "dot-tab",
55
69
  value,
56
70
  style: {
57
- display: "flex",
58
- alignItems: "center",
59
- justifyContent: "center",
71
+ ...CENTERED,
60
72
  gap: 6,
61
73
  flex: "1 0 auto",
62
74
  minHeight: segmented ? 36 : 44,
@@ -67,15 +79,7 @@ const Tab$1 = ({ value, label, badge }) => {
67
79
  whiteSpace: "nowrap",
68
80
  color,
69
81
  cursor: "pointer",
70
- ...segmented ? {
71
- borderRadius: Math.max(radius - 2, 0),
72
- background: selected ? colors.surface : "transparent",
73
- boxShadow: selected ? "0 1px 3px rgba(0, 0, 0, 0.15)" : "none"
74
- } : {
75
- marginBottom: -1,
76
- background: "transparent",
77
- borderBottom: `2px solid ${selected ? colors.primary : "transparent"}`
78
- }
82
+ ...variantStyle
79
83
  },
80
84
  children: [label, badge ? /* @__PURE__ */ jsx("span", {
81
85
  style: {
@@ -52,12 +52,12 @@ const DECORATION = {
52
52
  };
53
53
  const Text = ({ variant = "body", tone, weight, align, maxLines, decoration, numeric, leading, italic, selectable, children, ...padding }) => {
54
54
  const theme = useTheme();
55
- const nested = useContext(InsideText);
55
+ const insideText = useContext(InsideText);
56
56
  const inProse = useContext(InsideProse);
57
57
  const { heading, lineHeight, weight: defaultWeight } = VARIANTS[variant];
58
58
  const defaultLeading = inProse && !heading ? "relaxed" : "normal";
59
59
  const style = {
60
- display: nested ? "inline" : "block",
60
+ display: insideText ? "inline" : "block",
61
61
  marginInline: 0,
62
62
  marginBlockEnd: 0,
63
63
  marginBlockStart: inProse && heading ? void 0 : 0,
@@ -72,7 +72,7 @@ const Text = ({ variant = "body", tone, weight, align, maxLines, decoration, num
72
72
  ...selectable ? SELECTABLE : void 0,
73
73
  ...paddingStyle(padding)
74
74
  };
75
- if (!nested) {
75
+ if (!insideText) {
76
76
  style.lineHeight = lineHeight + LEADING_CHANGE[leading ?? defaultLeading];
77
77
  style.textWrap = heading ? "balance" : "pretty";
78
78
  style.overflowWrap = "anywhere";
@@ -87,7 +87,7 @@ const Text = ({ variant = "body", tone, weight, align, maxLines, decoration, num
87
87
  value: true,
88
88
  children
89
89
  });
90
- if ((variant === "title" || variant === "subtitle") && !nested) return /* @__PURE__ */ jsx(variant === "title" ? "h2" : "h3", {
90
+ if ((variant === "title" || variant === "subtitle") && !insideText) return /* @__PURE__ */ jsx(variant === "title" ? "h2" : "h3", {
91
91
  className: "dot-text dot-heading",
92
92
  style,
93
93
  children: content
@@ -100,7 +100,8 @@ const Text = ({ variant = "body", tone, weight, align, maxLines, decoration, num
100
100
  };
101
101
  function textColor(tone, theme) {
102
102
  if (isTone(tone)) return toneColor(theme.colors, tone);
103
- return tone === "onPrimary" ? theme.colors.onPrimary : customColor(tone, theme);
103
+ if (tone === "onPrimary") return theme.colors.onPrimary;
104
+ return customColor(tone, theme);
104
105
  }
105
106
  //#endregion
106
107
  export { Text };
@@ -1,5 +1,5 @@
1
1
  import { useTheme } from "../theme.js";
2
- import { DISABLED_OPACITY, FONT_SIZE, ICON_SIZE, SPACE } from "./tokens.js";
2
+ import { CENTERED, DISABLED_OPACITY, FONT_SIZE, ICON_SIZE, SPACE } from "./tokens.js";
3
3
  import { FieldLabel } from "./field.js";
4
4
  import { CHECK_ARTWORK } from "./icon-artwork.js";
5
5
  import { IconSvg } from "./icon-svg.js";
@@ -35,9 +35,7 @@ function ChoiceMark({ round, checked, children }) {
35
35
  const filled = checked && !round;
36
36
  return /* @__PURE__ */ jsx("span", {
37
37
  style: {
38
- display: "flex",
39
- alignItems: "center",
40
- justifyContent: "center",
38
+ ...CENTERED,
41
39
  flexShrink: 0,
42
40
  width: 22,
43
41
  height: 22,
@@ -20,6 +20,11 @@ const WEIGHTS = {
20
20
  };
21
21
  const CARD_RADIUS_SCALE = 1.5;
22
22
  const DISABLED_OPACITY = .6;
23
+ const CENTERED = {
24
+ display: "flex",
25
+ alignItems: "center",
26
+ justifyContent: "center"
27
+ };
23
28
  const ICON_SIZE = {
24
29
  small: 16,
25
30
  medium: 20,
@@ -41,4 +46,4 @@ const SELECTABLE = {
41
46
  WebkitUserSelect: "text"
42
47
  };
43
48
  //#endregion
44
- export { CARD_RADIUS_SCALE, DISABLED_OPACITY, FONT_SIZE, ICON_SIZE, PILL_STYLE, SELECTABLE, SPACE, WEIGHTS };
49
+ export { CARD_RADIUS_SCALE, CENTERED, DISABLED_OPACITY, FONT_SIZE, ICON_SIZE, PILL_STYLE, SELECTABLE, SPACE, WEIGHTS };
package/dist/markdown.js CHANGED
@@ -148,13 +148,15 @@ function parseInline(text) {
148
148
  take({ kind: "break" }, 1);
149
149
  continue;
150
150
  }
151
- const codeEnd = char === "`" ? text.indexOf("`", index + 1) : -1;
152
- if (codeEnd > index + 1) {
153
- take({
154
- kind: "code",
155
- text: text.slice(index + 1, codeEnd)
156
- }, codeEnd + 1 - index);
157
- continue;
151
+ if (char === "`") {
152
+ const codeEnd = text.indexOf("`", index + 1);
153
+ if (codeEnd > index + 1) {
154
+ take({
155
+ kind: "code",
156
+ text: text.slice(index + 1, codeEnd)
157
+ }, codeEnd + 1 - index);
158
+ continue;
159
+ }
158
160
  }
159
161
  const link = LINK.exec(rest);
160
162
  if (link) {
@@ -182,21 +184,23 @@ function parseInline(text) {
182
184
  }, whole.length);
183
185
  continue;
184
186
  }
185
- const strong = char === "*" || char === "_" ? between(text, index, char + char) : null;
186
- if (strong) {
187
- take({
188
- kind: "strong",
189
- children: parseInline(strong.inner)
190
- }, strong.length);
191
- continue;
192
- }
193
- const emphasis = char === "*" || char === "_" ? between(text, index, char) : null;
194
- if (emphasis) {
195
- take({
196
- kind: "emphasis",
197
- children: parseInline(emphasis.inner)
198
- }, emphasis.length);
199
- continue;
187
+ if (char === "*" || char === "_") {
188
+ const strong = between(text, index, char + char);
189
+ if (strong) {
190
+ take({
191
+ kind: "strong",
192
+ children: parseInline(strong.inner)
193
+ }, strong.length);
194
+ continue;
195
+ }
196
+ const emphasis = between(text, index, char);
197
+ if (emphasis) {
198
+ take({
199
+ kind: "emphasis",
200
+ children: parseInline(emphasis.inner)
201
+ }, emphasis.length);
202
+ continue;
203
+ }
200
204
  }
201
205
  plain += char;
202
206
  index += 1;
@@ -1,20 +1,22 @@
1
1
  import { useTheme } from "./theme.js";
2
+ import { CENTERED } from "./components/tokens.js";
2
3
  import { LoadingIcon } from "./components/spinner.js";
3
4
  import { useEffect, useRef, useState, useSyncExternalStore } from "react";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
5
6
  //#region src/pull-to-refresh.tsx
6
7
  const PULL_DISTANCE = 64;
8
+ const LONGEST_PULL = PULL_DISTANCE * 1.5;
7
9
  const RESISTANCE = .5;
8
10
  const INDICATOR_SIZE = 36;
9
11
  const DRAG_DISTANCE = 6;
10
12
  function PullToRefresh({ page, children }) {
11
13
  const { colors } = useTheme();
12
14
  const refreshing = useSyncExternalStore(page.subscribeToRefreshing, page.isRefreshing);
13
- const [pull, setPull] = useState(0);
14
- const area = useRef(null);
15
+ const [pullDistance, setPullDistance] = useState(0);
16
+ const areaRef = useRef(null);
15
17
  useEffect(() => {
16
- const element = area.current;
17
- const view = element.ownerDocument.defaultView;
18
+ const area = areaRef.current;
19
+ const view = area.ownerDocument.defaultView;
18
20
  let startY;
19
21
  let pulled = 0;
20
22
  let dragged = false;
@@ -28,19 +30,19 @@ function PullToRefresh({ page, children }) {
28
30
  const distance = (y - startY) * RESISTANCE;
29
31
  if (distance <= 0) {
30
32
  pulled = 0;
31
- setPull(0);
33
+ setPullDistance(0);
32
34
  return;
33
35
  }
34
36
  if (event.cancelable) event.preventDefault();
35
37
  if (distance > DRAG_DISTANCE) dragged = true;
36
- pulled = Math.min(distance, PULL_DISTANCE * 1.5);
37
- setPull(pulled);
38
+ pulled = Math.min(distance, LONGEST_PULL);
39
+ setPullDistance(pulled);
38
40
  };
39
41
  const end = () => {
40
42
  if (startY !== void 0 && pulled >= PULL_DISTANCE) page.refresh();
41
43
  startY = void 0;
42
44
  pulled = 0;
43
- setPull(0);
45
+ setPullDistance(0);
44
46
  };
45
47
  const ignoreDragClick = (event) => {
46
48
  if (dragged) {
@@ -55,25 +57,27 @@ function PullToRefresh({ page, children }) {
55
57
  if (event.button === 0) begin(event.clientY);
56
58
  };
57
59
  const mouseMove = (event) => move(event.clientY, event);
58
- const stops = [
59
- listen(element, "touchstart", touchStart, { passive: true }),
60
- listen(element, "touchmove", touchMove, { passive: false }),
61
- listen(element, "touchend", end),
62
- listen(element, "touchcancel", end),
63
- listen(element, "mousedown", mouseDown),
64
- listen(element, "click", ignoreDragClick, { capture: true }),
60
+ const removeListeners = [
61
+ listen(area, "touchstart", touchStart, { passive: true }),
62
+ listen(area, "touchmove", touchMove, { passive: false }),
63
+ listen(area, "touchend", end),
64
+ listen(area, "touchcancel", end),
65
+ listen(area, "mousedown", mouseDown),
66
+ listen(area, "click", ignoreDragClick, { capture: true }),
65
67
  listen(view, "mousemove", mouseMove),
66
68
  listen(view, "mouseup", end)
67
69
  ];
68
- return () => stops.forEach((stop) => stop());
70
+ return () => {
71
+ for (const removeListener of removeListeners) removeListener();
72
+ };
69
73
  }, [page]);
70
- const shown = refreshing ? PULL_DISTANCE : pull;
71
- const ready = pull >= PULL_DISTANCE;
74
+ const indicatorDistance = refreshing ? PULL_DISTANCE : pullDistance;
75
+ const arrowTurn = Math.min(pullDistance, PULL_DISTANCE) / PULL_DISTANCE * 180;
72
76
  return /* @__PURE__ */ jsxs("div", {
73
- ref: area,
77
+ ref: areaRef,
74
78
  className: "dot-pull-to-refresh",
75
79
  style: { display: "contents" },
76
- children: [children, shown > 0 ? /* @__PURE__ */ jsx("span", {
80
+ children: [children, indicatorDistance > 0 ? /* @__PURE__ */ jsx("span", {
77
81
  role: refreshing ? "status" : void 0,
78
82
  "aria-label": refreshing ? "Refreshing" : void 0,
79
83
  style: {
@@ -81,9 +85,7 @@ function PullToRefresh({ page, children }) {
81
85
  top: 0,
82
86
  left: "50%",
83
87
  zIndex: 2,
84
- display: "flex",
85
- alignItems: "center",
86
- justifyContent: "center",
88
+ ...CENTERED,
87
89
  width: INDICATOR_SIZE,
88
90
  height: INDICATOR_SIZE,
89
91
  marginLeft: -18,
@@ -91,8 +93,8 @@ function PullToRefresh({ page, children }) {
91
93
  background: colors.surface,
92
94
  color: colors.primary,
93
95
  boxShadow: "0 2px 8px rgba(0, 0, 0, 0.2)",
94
- transform: `translateY(${shown - INDICATOR_SIZE / 2}px)`,
95
- transition: pull > 0 ? "none" : "transform 0.2s",
96
+ transform: `translateY(${indicatorDistance - INDICATOR_SIZE / 2}px)`,
97
+ transition: pullDistance > 0 ? "none" : "transform 0.2s",
96
98
  pointerEvents: "none"
97
99
  },
98
100
  children: refreshing ? /* @__PURE__ */ jsx(LoadingIcon, { size: 20 }) : /* @__PURE__ */ jsx("svg", {
@@ -104,7 +106,7 @@ function PullToRefresh({ page, children }) {
104
106
  strokeWidth: 2.5,
105
107
  strokeLinecap: "round",
106
108
  strokeLinejoin: "round",
107
- style: { transform: `rotate(${ready ? 180 : pull / PULL_DISTANCE * 180}deg)` },
109
+ style: { transform: `rotate(${arrowTurn}deg)` },
108
110
  children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M6 13l6 6 6-6" })
109
111
  })
110
112
  }) : null]
package/dist/root.js CHANGED
@@ -2,40 +2,53 @@ import { FONT_FAMILY, ThemeContext, resolveTheme, useTheme } from "./theme.js";
2
2
  import { PortalContainerContext } from "./portal.js";
3
3
  import { PageRoot } from "./page-root.js";
4
4
  import { PullToRefresh } from "./pull-to-refresh.js";
5
+ import { mergeColors } from "@monimesl/dot-core";
5
6
  import { useMemo, useState, useSyncExternalStore } from "react";
6
7
  import { jsx } from "react/jsx-runtime";
7
8
  //#region src/root.tsx
8
9
  function Root({ page, dotContext, components, wrapper: Wrapper }) {
9
10
  const theme = useSyncExternalStore(dotContext.theme.subscribe, dotContext.theme.get);
10
11
  const systemContext = useSyncExternalStore(dotContext.systemContext.subscribe, dotContext.systemContext.get);
11
- const resolved = useMemo(() => resolveTheme(theme, systemContext.scheme), [theme, systemContext.scheme]);
12
+ const pageColors = useSyncExternalStore(page.subscribeToColors, page.getColors);
13
+ const resolvedTheme = useMemo(() => resolveTheme(theme, systemContext.scheme, pageColors), [
14
+ theme,
15
+ systemContext.scheme,
16
+ pageColors
17
+ ]);
12
18
  const [rootElement, setRootElement] = useState(null);
13
- const content = /* @__PURE__ */ jsx(PageRoot, {
19
+ const pageContent = /* @__PURE__ */ jsx(PageRoot, {
14
20
  page,
15
21
  components
16
22
  });
17
23
  return /* @__PURE__ */ jsx(ThemeContext.Provider, {
18
- value: resolved,
24
+ value: resolvedTheme,
19
25
  children: /* @__PURE__ */ jsx("div", {
20
26
  ref: setRootElement,
21
27
  className: "dot-theme-root",
22
- "data-dot-scheme": resolved.scheme,
28
+ "data-dot-scheme": resolvedTheme.scheme,
23
29
  children: /* @__PURE__ */ jsx(PortalContainerContext.Provider, {
24
30
  value: rootElement,
25
31
  children: /* @__PURE__ */ jsx(PullToRefresh, {
26
32
  page,
27
33
  children: Wrapper ? /* @__PURE__ */ jsx(Wrapper, {
28
- theme: {
29
- ...theme,
30
- scheme: resolved.scheme
31
- },
32
- children: content
33
- }) : /* @__PURE__ */ jsx(DefaultSurface, { children: content })
34
+ theme: withPageColors(theme, resolvedTheme.scheme, pageColors),
35
+ children: pageContent
36
+ }) : /* @__PURE__ */ jsx(DefaultSurface, { children: pageContent })
34
37
  })
35
38
  })
36
39
  })
37
40
  });
38
41
  }
42
+ function withPageColors(theme, scheme, pageColors) {
43
+ return {
44
+ ...theme,
45
+ scheme,
46
+ colors: {
47
+ ...theme.colors,
48
+ ...mergeColors(theme.colors ?? {}, pageColors)
49
+ }
50
+ };
51
+ }
39
52
  function DefaultSurface({ children }) {
40
53
  const { colors } = useTheme();
41
54
  return /* @__PURE__ */ jsx("div", {
package/dist/theme.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ColorScheme, Theme, ThemeShape } from "@monimesl/dot-core";
1
+ import { ColorScheme, PageColors, Theme, ThemeShape } from "@monimesl/dot-core";
2
2
  //#region src/theme.d.ts
3
3
  export interface ResolvedColors {
4
4
  primary: string;
@@ -21,6 +21,6 @@ export interface ResolvedTheme {
21
21
  shape: ThemeShape;
22
22
  radius: number;
23
23
  }
24
- export declare function resolveTheme(theme: Theme, systemScheme: ColorScheme): ResolvedTheme;
24
+ export declare function resolveTheme(theme: Theme, systemScheme: ColorScheme, pageColors?: PageColors): ResolvedTheme;
25
25
  export declare function useTheme(): ResolvedTheme;
26
26
  //#endregion
package/dist/theme.js CHANGED
@@ -1,4 +1,5 @@
1
- import { contrast, darken } from "./color.js";
1
+ import { darken, mix } from "./color.js";
2
+ import { DEFAULT_BACKGROUND, READABLE_CONTRAST, contrast, isDark, pageBackground, textOn, themeScheme } from "@monimesl/dot-core";
2
3
  import { createContext, useContext } from "react";
3
4
  //#region src/theme.ts
4
5
  const FONT_FAMILY = "system-ui, sans-serif";
@@ -11,7 +12,7 @@ const SHAPE_RADIUS = {
11
12
  const DEFAULT_COLORS = {
12
13
  light: {
13
14
  primary: "#1c1c1e",
14
- background: "#f2f2f2",
15
+ background: DEFAULT_BACKGROUND.light,
15
16
  surface: "#ffffff",
16
17
  text: "#111111",
17
18
  subtle: "#8e8e93",
@@ -24,7 +25,7 @@ const DEFAULT_COLORS = {
24
25
  },
25
26
  dark: {
26
27
  primary: "#f5f5f5",
27
- background: "#121212",
28
+ background: DEFAULT_BACKGROUND.dark,
28
29
  surface: "#1e1e1e",
29
30
  text: "#f5f5f5",
30
31
  subtle: "#8e8e93",
@@ -36,30 +37,64 @@ const DEFAULT_COLORS = {
36
37
  danger: "#ff6961"
37
38
  }
38
39
  };
39
- const MOST_DARKENING = 15;
40
- function resolveTheme(theme, systemScheme) {
41
- const scheme = !theme.scheme || theme.scheme === "system" ? systemScheme : theme.scheme;
40
+ function resolveTheme(theme, systemScheme, pageColors = {}) {
41
+ const scheme = themeScheme(theme, systemScheme);
42
42
  const defaults = DEFAULT_COLORS[scheme];
43
- const colors = theme.colors ?? {};
44
- const pick = (color, fallback) => color?.[scheme] ?? fallback;
43
+ const themeColors = theme.colors ?? {};
44
+ const ownColor = (name) => pageColors[name]?.[scheme] ?? themeColors[name]?.[scheme];
45
45
  const shape = theme.shape ?? "soft";
46
- const surface = pick(colors.surface, defaults.surface);
47
- const text = pick(colors.text, defaults.text);
48
- const primary = pick(colors.primary, defaults.primary);
46
+ const background = pageBackground(pageColors.background, theme, systemScheme);
47
+ const onDashBackground = background === defaults.background;
48
+ let surfaces = defaults;
49
+ if (!onDashBackground) surfaces = surfacesOn(background);
50
+ const surface = ownColor("surface") ?? surfaces.surface;
51
+ const text = ownColor("text") ?? readableText(defaults.text, background);
52
+ let subtle = defaults.subtle;
53
+ if (!onDashBackground) subtle = quieterText(text, background, surface);
54
+ const primary = ownColor("primary") ?? defaults.primary;
49
55
  return {
50
56
  scheme,
51
57
  colors: {
52
58
  ...defaults,
53
59
  ...primaryAndOnPrimary(primary, surface, text, scheme),
54
- background: pick(colors.background, defaults.background),
60
+ background,
55
61
  surface,
56
- text
62
+ text,
63
+ muted: ownColor("muted") ?? surfaces.muted,
64
+ border: ownColor("border") ?? surfaces.border,
65
+ subtle: ownColor("subtle") ?? subtle
57
66
  },
58
- customColors: resolveCustomColors(colors.custom ?? {}, scheme),
67
+ customColors: resolveCustomColors(themeColors.custom ?? {}, scheme),
59
68
  shape,
60
69
  radius: SHAPE_RADIUS[shape]
61
70
  };
62
71
  }
72
+ function surfacesOn(background) {
73
+ if (isDark(background)) return {
74
+ surface: mix(background, "#ffffff", .08),
75
+ muted: mix(background, "#ffffff", .14),
76
+ border: mix(background, "#ffffff", .16)
77
+ };
78
+ return {
79
+ surface: mix(background, "#ffffff", .75),
80
+ muted: mix(background, "#000000", .06),
81
+ border: mix(background, "#000000", .1)
82
+ };
83
+ }
84
+ const QUIET_CONTRAST = 3;
85
+ function quieterText(text, background, surface) {
86
+ for (let shareOfBackground = .5; shareOfBackground > 0; shareOfBackground -= .05) {
87
+ const quieter = mix(text, background, shareOfBackground);
88
+ const readsOnBackground = contrast(quieter, background) >= QUIET_CONTRAST;
89
+ const readsOnSurface = contrast(quieter, surface) >= QUIET_CONTRAST;
90
+ if (readsOnBackground && readsOnSurface) return quieter;
91
+ }
92
+ return text;
93
+ }
94
+ function readableText(text, background) {
95
+ if (contrast(text, background) >= READABLE_CONTRAST) return text;
96
+ return textOn(background);
97
+ }
63
98
  function resolveCustomColors(custom, scheme) {
64
99
  const resolved = {};
65
100
  for (const [name, color] of Object.entries(custom)) {
@@ -68,14 +103,15 @@ function resolveCustomColors(custom, scheme) {
68
103
  }
69
104
  return resolved;
70
105
  }
106
+ const MOST_DARKENING = 15;
71
107
  function primaryAndOnPrimary(primary, surface, text, scheme) {
72
- if (contrast(primary, surface) >= 4.5) return {
108
+ if (contrast(primary, surface) >= READABLE_CONTRAST) return {
73
109
  primary,
74
110
  onPrimary: surface
75
111
  };
76
112
  if (scheme === "light") for (let points = .5; points <= MOST_DARKENING; points += .5) {
77
113
  const darker = darken(primary, points);
78
- if (contrast(darker, surface) >= 4.5) return {
114
+ if (contrast(darker, surface) >= READABLE_CONTRAST) return {
79
115
  primary: darker,
80
116
  onPrimary: surface
81
117
  };