flet-box 1.1.0 → 1.1.2

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 (135) hide show
  1. package/README.md +16 -12
  2. package/bin/commands/checkUi.js +276 -45
  3. package/bin/commands/create.js +8 -1
  4. package/bin/commands/createBundle.js +18 -6
  5. package/bin/commands/createScreen.js +1 -1
  6. package/bin/commands/runBundle.js +1 -1
  7. package/bin/utils/bundleConfig.js +47 -3
  8. package/bin/utils/colors.js +0 -8
  9. package/bin/utils/helpers.js +0 -5
  10. package/bin/utils/images/jpeg.js +2 -3
  11. package/bin/utils/templates.js +101 -85
  12. package/docs/README.md +24 -1
  13. package/docs/cli/README.md +51 -10
  14. package/docs/guides/README.md +5 -2
  15. package/docs/guides/accessibility.md +282 -0
  16. package/docs/guides/clean-ui.md +53 -8
  17. package/docs/guides/first-app.md +6 -7
  18. package/docs/guides/minification-and-protection.md +1 -1
  19. package/docs/guides/mobile-and-platforms.md +51 -0
  20. package/docs/guides/online-updates.md +18 -6
  21. package/docs/guides/utilities.md +116 -4
  22. package/docs/guides/widget-structure.md +30 -0
  23. package/docs/tools/README.md +1 -1
  24. package/docs/tools/animation.md +134 -11
  25. package/docs/tools/device.md +75 -3
  26. package/docs/tools/text-and-time.md +116 -6
  27. package/docs/widget/Accordion.md +2 -0
  28. package/docs/widget/AnimatedBox.md +46 -20
  29. package/docs/widget/AnimatedText.md +13 -7
  30. package/docs/widget/Badge.md +1 -0
  31. package/docs/widget/COMMON_PROPS.md +26 -3
  32. package/docs/widget/Chart.md +1 -0
  33. package/docs/widget/Checkbox.md +7 -1
  34. package/docs/widget/CircularChart.md +2 -0
  35. package/docs/widget/Drawer.md +1 -0
  36. package/docs/widget/GridView.md +4 -1
  37. package/docs/widget/Input.md +2 -0
  38. package/docs/widget/InstallButton.md +1 -0
  39. package/docs/widget/Markdown.md +4 -0
  40. package/docs/widget/Rating.md +2 -0
  41. package/docs/widget/Slider.md +2 -0
  42. package/docs/widget/Stack.md +6 -5
  43. package/docs/widget/Stepper.md +2 -0
  44. package/docs/widget/Tooltip.md +3 -1
  45. package/package.json +3 -2
  46. package/skills/fletbox-design/SKILL.md +390 -14
  47. package/skills/fletbox-motion/SKILL.md +145 -47
  48. package/skills/fletbox-rules/SKILL.md +140 -1
  49. package/src/animations/AnimatedBox.js +321 -226
  50. package/src/animations/MatrixRain.js +7 -1
  51. package/src/animations/ParallaxBox.js +13 -10
  52. package/src/animations/animate.js +269 -106
  53. package/src/animations/color.js +91 -0
  54. package/src/animations/easings.js +275 -0
  55. package/src/animations/index.js +21 -0
  56. package/src/animations/motion.js +204 -0
  57. package/src/animations/transitions.js +327 -0
  58. package/src/core/runApp.js +0 -6
  59. package/src/helpers/boxEdges.js +69 -0
  60. package/src/helpers/html.js +39 -0
  61. package/src/index.d.ts +377 -25
  62. package/src/index.js +77 -0
  63. package/src/navigations/AdaptiveScaffold.js +6 -2
  64. package/src/navigations/AppBar.js +16 -7
  65. package/src/navigations/BottomNavigation.js +24 -1
  66. package/src/navigations/CollapsibleSideBar.js +0 -1
  67. package/src/navigations/Drawer.js +32 -1
  68. package/src/navigations/DrawerItem.js +1 -1
  69. package/src/navigations/Router.js +0 -1
  70. package/src/navigations/Scaffold.js +9 -3
  71. package/src/navigations/Tabs.js +104 -31
  72. package/src/services/RamStore.js +1 -2
  73. package/src/tools/breakpoints.js +113 -0
  74. package/src/tools/createList.js +0 -2
  75. package/src/tools/fonts.js +99 -0
  76. package/src/tools/index.js +74 -1
  77. package/src/tools/margin.js +5 -63
  78. package/src/tools/padding.js +5 -63
  79. package/src/tools/print.js +0 -8
  80. package/src/tools/scale.js +9 -2
  81. package/src/tools/time.js +56 -15
  82. package/src/tools/useState.js +1 -54
  83. package/src/utils/fieldFont.js +77 -0
  84. package/src/utils/focusRing.js +73 -0
  85. package/src/utils/focusScope.js +113 -0
  86. package/src/utils/markdownParser.js +7 -9
  87. package/src/utils/readableInk.js +7 -1
  88. package/src/utils/stackLayout.js +55 -0
  89. package/src/utils/styleInjector.js +25 -0
  90. package/src/utils/syntaxHighlight.js +1 -17
  91. package/src/utils/themes.js +15 -0
  92. package/src/utils/visualEffects.js +49 -47
  93. package/src/widget-factory/createWidget.js +16 -3
  94. package/src/widget-factory/effects.js +76 -3
  95. package/src/widget-factory/reactiveValue.js +0 -7
  96. package/src/widget-factory/tools.js +26 -143
  97. package/src/widget-factory/translateProps.js +5 -0
  98. package/src/widget-factory/widgetFactory.js +1 -1
  99. package/src/widgets/Accordion.js +70 -12
  100. package/src/widgets/AdMob.js +1 -1
  101. package/src/widgets/AdSense.js +1 -1
  102. package/src/widgets/AlertDialog.js +11 -9
  103. package/src/widgets/Avatar.js +10 -1
  104. package/src/widgets/Badge.js +7 -1
  105. package/src/widgets/BottomSheet.js +39 -5
  106. package/src/widgets/Chart.js +41 -9
  107. package/src/widgets/Checkbox.js +59 -9
  108. package/src/widgets/Chip.js +13 -1
  109. package/src/widgets/CircularBar.js +17 -5
  110. package/src/widgets/CircularChart.js +24 -2
  111. package/src/widgets/CodeViewer.js +5 -3
  112. package/src/widgets/Dropdown.js +223 -20
  113. package/src/widgets/GridView.js +17 -1
  114. package/src/widgets/Icon.js +21 -0
  115. package/src/widgets/Input.js +75 -44
  116. package/src/widgets/InstallButton.js +8 -2
  117. package/src/widgets/ListTile.js +6 -5
  118. package/src/widgets/ListView.js +21 -7
  119. package/src/widgets/Markdown.js +25 -44
  120. package/src/widgets/Modal.js +43 -6
  121. package/src/widgets/Pagination.js +35 -3
  122. package/src/widgets/ProgressBar.js +18 -0
  123. package/src/widgets/Radio.js +7 -0
  124. package/src/widgets/Rating.js +104 -16
  125. package/src/widgets/Skeleton.js +9 -5
  126. package/src/widgets/Slider.js +87 -7
  127. package/src/widgets/SnackBar.js +30 -1
  128. package/src/widgets/Stack.js +25 -13
  129. package/src/widgets/Stepper.js +112 -64
  130. package/src/widgets/Switch.js +10 -1
  131. package/src/widgets/Tooltip.js +93 -10
  132. package/src/widgets/TreeView.js +5 -5
  133. package/src/create-index.sh +0 -237
  134. package/src/widget-factory/pixTool.js +0 -40
  135. package/src/widget-factory/stringTool.js +0 -26
@@ -0,0 +1,99 @@
1
+ // src/tools/fonts.js
2
+ // Webfont loading, so a `fontFamily` prop has something to name.
3
+
4
+ /**
5
+ * `fontFamily` is a CSS family name; if the bytes of that face are not on the
6
+ * machine, the browser silently paints its fallback and the app ships a typeface
7
+ * nobody chose. This module is the missing half: ask for the family, get a
8
+ * promise that settles when the request is done, and set the prop from it.
9
+ *
10
+ * One thing this cannot promise: a webfont is a network request. A self-hosted
11
+ * same-origin stylesheet survives an offline launch; a provider URL does not.
12
+ * `loadFont` therefore never fails a screen — it resolves `false` when the face
13
+ * could not be had, and the app keeps its fallback.
14
+ */
15
+
16
+ /** @type {Map<string, Promise<boolean>>} One request per family, however many callers ask. */
17
+ const loading = new Map();
18
+
19
+ const toCssTime = (weights) => weights.map((weight) => Number(weight) || 0).join(";");
20
+
21
+ const googleUrl = (family, weights, display) =>
22
+ `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}${
23
+ weights.length ? `:wght@${toCssTime(weights)}` : ""
24
+ }&display=${display}`;
25
+
26
+ const appendLink = (rel, href, attributes = {}) => {
27
+ const link = document.createElement("link");
28
+ link.rel = rel;
29
+ if (href) link.href = href;
30
+ for (const [name, value] of Object.entries(attributes)) link.setAttribute(name, value);
31
+ document.head.appendChild(link);
32
+ return link;
33
+ };
34
+
35
+ // A cross-origin stylesheet wants its origin warm before the request, or the face
36
+ // arrives a round trip late. Once per origin: every family on the same host would
37
+ // otherwise add its own pair.
38
+ const preconnect = (href) => {
39
+ if (document.head.querySelector(`link[rel="preconnect"][href="${href}"]`)) return;
40
+ appendLink("preconnect", href, { crossorigin: "anonymous" });
41
+ };
42
+
43
+ /**
44
+ * Load a webfont family.
45
+ *
46
+ * @param {string} family - The CSS family name, e.g. `"Inter"`.
47
+ * @param {object} [options]
48
+ * @param {number[]} [options.weights] - Weights to fetch (`[400, 600]`). A Google
49
+ * family with no weight list arrives at 400 only, which is a face that cannot
50
+ * carry a heading.
51
+ * @param {string} [options.url] - A stylesheet to load instead of Google's: a
52
+ * self-hosted `@font-face` file, or any other provider.
53
+ * @param {"auto" | "block" | "swap" | "fallback" | "optional"} [options.display="swap"]
54
+ * CSS `font-display`, Google URLs only. `swap` paints the fallback at once and
55
+ * replaces it when the face lands; `optional` may skip it on a slow connection.
56
+ * @returns {Promise<boolean>} `true` when the stylesheet landed, `false` when it
57
+ * did not — and an immediate `false` where there is no document to write to, so
58
+ * importing this module in Node never throws.
59
+ */
60
+ export const loadFont = (family, options = {}) => {
61
+ if (typeof document === "undefined" || !family) return Promise.resolve(false);
62
+ const key = String(family);
63
+ if (loading.has(key)) return loading.get(key);
64
+
65
+ const run = new Promise((resolve) => {
66
+ const { weights = [], url, display = "swap" } = options;
67
+ const href = url || googleUrl(key, weights, display);
68
+ if (!url) {
69
+ preconnect("https://fonts.googleapis.com");
70
+ preconnect("https://fonts.gstatic.com");
71
+ }
72
+
73
+ const link = appendLink("stylesheet", href, { "data-fb-font": key });
74
+ // The face itself still has to paint; `document.fonts.ready` is the point
75
+ // where the engine has finished everything it started, loaded or not.
76
+ const settle = () => {
77
+ if (typeof document.fonts?.ready?.then !== "function") return resolve(true);
78
+ document.fonts.ready.then(() => resolve(true), () => resolve(false));
79
+ };
80
+ link.addEventListener("load", settle);
81
+ link.addEventListener("error", () => resolve(false));
82
+ });
83
+
84
+ loading.set(key, run);
85
+ return run;
86
+ };
87
+
88
+ /**
89
+ * Wait for every face the document has already started, without asking for one.
90
+ * Use it before a canvas paint or an export, where text that reflows after the
91
+ * drawing is a visible bug.
92
+ *
93
+ * @returns {Promise<boolean>} `true` when the font engine settled, `false` where
94
+ * there is none.
95
+ */
96
+ export const fontsReady = () => {
97
+ if (typeof document === "undefined" || !document.fonts) return Promise.resolve(false);
98
+ return document.fonts.ready.then(() => true, () => false);
99
+ };
@@ -4,6 +4,16 @@ export { shuffle, reverse, sort, unique, chunk } from "./array.js";
4
4
  export { border } from "./border.js";
5
5
  export { clipboard } from "./clipboard.js";
6
6
  export { color } from "./color.js";
7
+ export {
8
+ BREAKPOINTS,
9
+ BREAKPOINT_NAMES,
10
+ breakpointForWidth,
11
+ currentBreakpoint,
12
+ atLeast,
13
+ pick,
14
+ onBreakpointChange,
15
+ } from "./breakpoints.js";
16
+ export { loadFont, fontsReady } from "./fonts.js";
7
17
  export { createList } from "./createList.js";
8
18
  export { delay, withMinDelay, retry } from "./delay.js";
9
19
  export { device } from "./device.js";
@@ -36,8 +46,71 @@ export {
36
46
  upperCase,
37
47
  reverseString,
38
48
  truncate,
49
+ startsWith,
50
+ endsWith,
51
+ includes,
52
+ split,
53
+ join,
54
+ trim,
39
55
  } from "./string.js";
40
- export { formatDate, relativeTime, sleep, now } from "./time.js";
56
+ export {
57
+ // units
58
+ seconds,
59
+ minutes,
60
+ hours,
61
+ days,
62
+ weeks,
63
+ months,
64
+ years,
65
+ // current
66
+ now,
67
+ today,
68
+ yesterday,
69
+ tomorrow,
70
+ // info
71
+ dayOfWeek,
72
+ monthName,
73
+ isToday,
74
+ isYesterday,
75
+ isTomorrow,
76
+ isWeekend,
77
+ isLeapYear,
78
+ // format
79
+ formatDate,
80
+ formatDateLong,
81
+ formatDateShort,
82
+ formatTime,
83
+ formatTimeAmPm,
84
+ // parse & diff
85
+ parseDate,
86
+ dateDiff,
87
+ // manipulate
88
+ addDays,
89
+ addHours,
90
+ addMinutes,
91
+ addSeconds,
92
+ subtractDays,
93
+ subtractHours,
94
+ subtractMinutes,
95
+ subtractSeconds,
96
+ // relative
97
+ timeAgo,
98
+ relativeTime,
99
+ timeAgoLocale,
100
+ // wait
101
+ waitFor,
102
+ wait,
103
+ sleep,
104
+ waitUntil,
105
+ waitForElement,
106
+ // timer & interval
107
+ timer,
108
+ every,
109
+ everySecond,
110
+ everyMinute,
111
+ everyHour,
112
+ everyDay,
113
+ } from "./time.js";
41
114
  export { transform } from "./transform.js";
42
115
  export { transition } from "./transition.js";
43
116
  export { useState, useWatchState } from "./useState.js";
@@ -1,4 +1,5 @@
1
1
  // tools/margin.js
2
+ import { boxEdges } from "../helpers/boxEdges.js";
2
3
 
3
4
  /**
4
5
  * Generates a CSS `margin` shorthand string from a number, a plain CSS string,
@@ -14,6 +15,9 @@
14
15
  * - `vertical` — applies to `top` and `bottom`.
15
16
  * - `top`, `right`, `bottom`, `left` — individual side overrides.
16
17
  *
18
+ * The grammar is shared with `padding()` and lives in `helpers/boxEdges.js`, so
19
+ * the two cannot disagree about how a side is resolved.
20
+ *
17
21
  * @param {number|string|Object} value
18
22
  * A number (converted to `px`), a raw CSS string, or an options object.
19
23
  * @returns {string} CSS margin value.
@@ -26,68 +30,6 @@
26
30
  * margin({ left: 20, right: 20 }) // "0px 20px"
27
31
  * margin({ top: 10, bottom: 10 }) // "10px 0px"
28
32
  */
29
- export const margin = (value) => {
30
- // A number or raw CSS string applies uniformly to all sides
31
- if (typeof value === "number" || typeof value === "string") {
32
- const px = typeof value === "number" ? `${value}px` : value;
33
- return px;
34
- }
35
-
36
- // Object form — resolve each side independently
37
- if (typeof value === "object") {
38
- const top =
39
- value.top !== undefined
40
- ? value.top
41
- : value.vertical !== undefined
42
- ? value.vertical
43
- : value.all !== undefined
44
- ? value.all
45
- : 0;
46
- const right =
47
- value.right !== undefined
48
- ? value.right
49
- : value.horizontal !== undefined
50
- ? value.horizontal
51
- : value.all !== undefined
52
- ? value.all
53
- : 0;
54
- const bottom =
55
- value.bottom !== undefined
56
- ? value.bottom
57
- : value.vertical !== undefined
58
- ? value.vertical
59
- : value.all !== undefined
60
- ? value.all
61
- : 0;
62
- const left =
63
- value.left !== undefined
64
- ? value.left
65
- : value.horizontal !== undefined
66
- ? value.horizontal
67
- : value.all !== undefined
68
- ? value.all
69
- : 0;
70
-
71
- const topPx = typeof top === "number" ? `${top}px` : top;
72
- const rightPx = typeof right === "number" ? `${right}px` : right;
73
- const bottomPx = typeof bottom === "number" ? `${bottom}px` : bottom;
74
- const leftPx = typeof left === "number" ? `${left}px` : left;
75
-
76
- // All sides equal — return a single value
77
- if (topPx === rightPx && rightPx === bottomPx && bottomPx === leftPx) {
78
- return topPx;
79
- }
80
-
81
- // Only horizontal and vertical differ — use two-value shorthand
82
- if (topPx === bottomPx && rightPx === leftPx) {
83
- return `${topPx} ${rightPx}`;
84
- }
85
-
86
- // Full four-value form
87
- return `${topPx} ${rightPx} ${bottomPx} ${leftPx}`;
88
- }
89
-
90
- return "0px";
91
- };
33
+ export const margin = boxEdges;
92
34
 
93
35
  export default margin;
@@ -1,4 +1,5 @@
1
1
  // tools/padding.js
2
+ import { boxEdges } from "../helpers/boxEdges.js";
2
3
 
3
4
  /**
4
5
  * Generates a CSS `padding` shorthand string from a number, a plain CSS string,
@@ -14,6 +15,9 @@
14
15
  * - `vertical` — applies to `top` and `bottom`.
15
16
  * - `top`, `right`, `bottom`, `left` — individual side overrides.
16
17
  *
18
+ * The grammar is shared with `margin()` and lives in `helpers/boxEdges.js`, so
19
+ * the two cannot disagree about how a side is resolved.
20
+ *
17
21
  * @param {number|string|Object} value
18
22
  * A number (converted to `px`), a raw CSS string, or an options object.
19
23
  * @returns {string} CSS padding value.
@@ -26,68 +30,6 @@
26
30
  * padding({ left: 20, right: 20 }) // "0px 20px"
27
31
  * padding({ top: 10, bottom: 10 }) // "10px 0px"
28
32
  */
29
- export const padding = (value) => {
30
- // A number or raw CSS string applies uniformly to all sides
31
- if (typeof value === "number" || typeof value === "string") {
32
- const px = typeof value === "number" ? `${value}px` : value;
33
- return px;
34
- }
35
-
36
- // Object form — resolve each side independently
37
- if (typeof value === "object") {
38
- const top =
39
- value.top !== undefined
40
- ? value.top
41
- : value.vertical !== undefined
42
- ? value.vertical
43
- : value.all !== undefined
44
- ? value.all
45
- : 0;
46
- const right =
47
- value.right !== undefined
48
- ? value.right
49
- : value.horizontal !== undefined
50
- ? value.horizontal
51
- : value.all !== undefined
52
- ? value.all
53
- : 0;
54
- const bottom =
55
- value.bottom !== undefined
56
- ? value.bottom
57
- : value.vertical !== undefined
58
- ? value.vertical
59
- : value.all !== undefined
60
- ? value.all
61
- : 0;
62
- const left =
63
- value.left !== undefined
64
- ? value.left
65
- : value.horizontal !== undefined
66
- ? value.horizontal
67
- : value.all !== undefined
68
- ? value.all
69
- : 0;
70
-
71
- const topPx = typeof top === "number" ? `${top}px` : top;
72
- const rightPx = typeof right === "number" ? `${right}px` : right;
73
- const bottomPx = typeof bottom === "number" ? `${bottom}px` : bottom;
74
- const leftPx = typeof left === "number" ? `${left}px` : left;
75
-
76
- // All sides equal — return a single value
77
- if (topPx === rightPx && rightPx === bottomPx && bottomPx === leftPx) {
78
- return topPx;
79
- }
80
-
81
- // Only horizontal and vertical differ — use two-value shorthand
82
- if (topPx === bottomPx && rightPx === leftPx) {
83
- return `${topPx} ${rightPx}`;
84
- }
85
-
86
- // Full four-value form
87
- return `${topPx} ${rightPx} ${bottomPx} ${leftPx}`;
88
- }
89
-
90
- return "0px";
91
- };
33
+ export const padding = boxEdges;
92
34
 
93
35
  export default padding;
@@ -58,12 +58,4 @@ export const print = (message, type = "log", options = {}) => {
58
58
  console[config.method](finalMessage);
59
59
  };
60
60
 
61
- // Shorthand aliases (uncomment to enable individual exports):
62
- // export const log = (msg) => print(msg, "log");
63
- // export const warn = (msg) => print(msg, "warn");
64
- // export const error = (msg) => print(msg, "error");
65
- // export const info = (msg) => print(msg, "info");
66
- // export const success = (msg) => print(msg, "success");
67
- // export const debug = (msg) => print(msg, "debug");
68
-
69
61
  export default print;
@@ -28,8 +28,15 @@ export const PILL = 999;
28
28
  /** The 8-based rhythm, with the two steps a real layout needs (18 for field text, 96 for hero gutters). */
29
29
  export const SPACING_SCALE = new Set([0, 4, 8, 12, 16, 18, 24, 32, 40, 48, 64, 96]);
30
30
 
31
- /** Corner radii. `999` is a pill, `0` a flush edge — both are decisions, not omissions. */
32
- export const RADIUS_SCALE = new Set([0, 4, 8, 12, 16, PILL]);
31
+ /**
32
+ * Corner radii. `999` is a pill, `0` a flush edge — both are decisions, not omissions.
33
+ *
34
+ * The scale reaches 24 on purpose. The nesting rule (`outer = inner + padding`) is
35
+ * arithmetic, not taste: a 16-radius tile inside 8 of padding *needs* a 24 card, so a
36
+ * scale that stops at 16 makes the correct radius illegal and pushes the screen to a
37
+ * pill it did not ask for.
38
+ */
39
+ export const RADIUS_SCALE = new Set([0, 4, 8, 12, 16, 20, 24, PILL]);
33
40
 
34
41
  /** Type sizes. Two steps per jump: a 15 next to a 16 reads as a mistake, not as hierarchy. */
35
42
  export const TYPE_SCALE = new Set([12, 14, 16, 18, 24, 32, 40, 48]);
package/src/tools/time.js CHANGED
@@ -6,6 +6,35 @@
6
6
  * ============================================================
7
7
  */
8
8
 
9
+ // ========== INPUT ==========
10
+
11
+ /**
12
+ * Turns any accepted date input into a `Date`, reading a bare calendar date as a
13
+ * calendar date.
14
+ *
15
+ * `new Date("2024-01-15")` is not January the fifteenth: the spec parses a
16
+ * date-only string as UTC midnight, and every getter then reads it back in the
17
+ * local zone. West of Greenwich that is the fourteenth, which is how
18
+ * `isToday(today())` came back `false` and `dayOfWeek("2024-01-15")` said
19
+ * "Sunday" — the module disagreeing with itself, on a date the module produced.
20
+ *
21
+ * A value that carries a time, an offset or a `Z` is a timestamp, not a day, and
22
+ * `new Date` already places it; only `YYYY-MM-DD` needs the local reading.
23
+ *
24
+ * @param {Date|number|string} date - Any value a time helper accepts.
25
+ * @returns {Date}
26
+ */
27
+ const toDate = (date) => {
28
+ if (typeof date === "string") {
29
+ const calendarDay = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
30
+ if (calendarDay) {
31
+ const [, year, month, day] = calendarDay;
32
+ return new Date(Number(year), Number(month) - 1, Number(day));
33
+ }
34
+ }
35
+ return new Date(date);
36
+ };
37
+
9
38
  // ========== TIME UNITS ==========
10
39
 
11
40
  /**
@@ -100,7 +129,7 @@ export const dayOfWeek = (date) => {
100
129
  "Friday",
101
130
  "Saturday",
102
131
  ];
103
- return days[new Date(date).getDay()];
132
+ return days[toDate(date).getDay()];
104
133
  };
105
134
 
106
135
  /**
@@ -123,7 +152,7 @@ export const monthName = (date) => {
123
152
  "November",
124
153
  "December",
125
154
  ];
126
- return months[new Date(date).getMonth()];
155
+ return months[toDate(date).getMonth()];
127
156
  };
128
157
 
129
158
  /**
@@ -152,7 +181,7 @@ export const isTomorrow = (date) => formatDate(date) === tomorrow();
152
181
  * @param {Date|number|string} date - Date to check.
153
182
  * @returns {boolean}
154
183
  */
155
- export const isWeekend = (date) => [0, 6].includes(new Date(date).getDay());
184
+ export const isWeekend = (date) => [0, 6].includes(toDate(date).getDay());
156
185
 
157
186
  /**
158
187
  * Returns `true` if the given year is a leap year.
@@ -179,7 +208,7 @@ export const isLeapYear = (year) => {
179
208
  * formatDate(new Date(), "YYYY-MM-DD HH:mm:ss") // "2024-01-15 09:30:00"
180
209
  */
181
210
  export const formatDate = (date, format = "YYYY-MM-DD") => {
182
- const d = new Date(date);
211
+ const d = toDate(date);
183
212
  const year = d.getFullYear();
184
213
  const month = String(d.getMonth() + 1).padStart(2, "0");
185
214
  const day = String(d.getDate()).padStart(2, "0");
@@ -204,7 +233,7 @@ export const formatDate = (date, format = "YYYY-MM-DD") => {
204
233
  * @returns {string} Long date string, e.g. `"Monday, January 15, 2024"`.
205
234
  */
206
235
  export const formatDateLong = (date, locale = "en-US") => {
207
- return new Date(date).toLocaleDateString(locale, {
236
+ return toDate(date).toLocaleDateString(locale, {
208
237
  weekday: "long",
209
238
  year: "numeric",
210
239
  month: "long",
@@ -220,7 +249,7 @@ export const formatDateLong = (date, locale = "en-US") => {
220
249
  * @returns {string} Short date string, e.g. `"Jan 15, 2024"`.
221
250
  */
222
251
  export const formatDateShort = (date, locale = "en-US") => {
223
- return new Date(date).toLocaleDateString(locale, {
252
+ return toDate(date).toLocaleDateString(locale, {
224
253
  year: "numeric",
225
254
  month: "short",
226
255
  day: "numeric",
@@ -240,7 +269,7 @@ export const formatTime = (date) => formatDate(date, "HH:mm:ss");
240
269
  * @returns {string} Time string, e.g. `"02:05:30 PM"`.
241
270
  */
242
271
  export const formatTimeAmPm = (date) => {
243
- const d = new Date(date);
272
+ const d = toDate(date);
244
273
  let hours = d.getHours();
245
274
  const ampm = hours >= 12 ? "PM" : "AM";
246
275
  hours = hours % 12 || 12;
@@ -304,7 +333,7 @@ export const parseDate = (str, format = "YYYY-MM-DD") => {
304
333
  * dateDiff("2024-01-01", "2024-01-15", "days") // 14
305
334
  */
306
335
  export const dateDiff = (date1, date2, unit = "days") => {
307
- const diff = Math.abs(new Date(date1) - new Date(date2));
336
+ const diff = Math.abs(toDate(date1) - toDate(date2));
308
337
  const units = {
309
338
  milliseconds: 1,
310
339
  seconds: 1000,
@@ -322,12 +351,24 @@ export const dateDiff = (date1, date2, unit = "days") => {
322
351
 
323
352
  /**
324
353
  * Adds `n` days to a date and returns a new `Date`.
354
+ *
355
+ * Whole days are counted on the calendar, not in milliseconds: a 24h step lands
356
+ * an hour off the wall clock either side of a DST change, so
357
+ * `addDays("2024-10-26", 7)` in Madrid would print November 1. A fractional `n`
358
+ * is elapsed time and keeps the millisecond walk.
359
+ *
325
360
  * @param {Date|number|string} date - Base date.
326
361
  * @param {number} n - Number of days to add (negative to subtract).
327
362
  * @returns {Date}
328
363
  */
329
- export const addDays = (date, n) =>
330
- new Date(new Date(date).getTime() + days(n));
364
+ export const addDays = (date, n) => {
365
+ const d = toDate(date);
366
+ if (Number.isInteger(n)) {
367
+ d.setDate(d.getDate() + n);
368
+ return d;
369
+ }
370
+ return new Date(d.getTime() + days(n));
371
+ };
331
372
 
332
373
  /**
333
374
  * Adds `n` hours to a date and returns a new `Date`.
@@ -336,7 +377,7 @@ export const addDays = (date, n) =>
336
377
  * @returns {Date}
337
378
  */
338
379
  export const addHours = (date, n) =>
339
- new Date(new Date(date).getTime() + hours(n));
380
+ new Date(toDate(date).getTime() + hours(n));
340
381
 
341
382
  /**
342
383
  * Adds `n` minutes to a date and returns a new `Date`.
@@ -345,7 +386,7 @@ export const addHours = (date, n) =>
345
386
  * @returns {Date}
346
387
  */
347
388
  export const addMinutes = (date, n) =>
348
- new Date(new Date(date).getTime() + minutes(n));
389
+ new Date(toDate(date).getTime() + minutes(n));
349
390
 
350
391
  /**
351
392
  * Adds `n` seconds to a date and returns a new `Date`.
@@ -354,7 +395,7 @@ export const addMinutes = (date, n) =>
354
395
  * @returns {Date}
355
396
  */
356
397
  export const addSeconds = (date, n) =>
357
- new Date(new Date(date).getTime() + seconds(n));
398
+ new Date(toDate(date).getTime() + seconds(n));
358
399
 
359
400
  /**
360
401
  * Subtracts `n` days from a date and returns a new `Date`.
@@ -402,7 +443,7 @@ export const subtractSeconds = (date, n) => addSeconds(date, -n);
402
443
  */
403
444
  export const timeAgo = (date) => {
404
445
  const now = new Date();
405
- const d = new Date(date);
446
+ const d = toDate(date);
406
447
  const diff = Math.floor((now - d) / 1000);
407
448
 
408
449
  const intervals = [
@@ -439,7 +480,7 @@ export const relativeTime = timeAgo;
439
480
  * timeAgoLocale(Date.now() - 3600000, "es") // "hace 1 hora"
440
481
  */
441
482
  export const timeAgoLocale = (date, locale = "en") => {
442
- const d = new Date(date);
483
+ const d = toDate(date);
443
484
  const now = new Date();
444
485
  const diff = Math.floor((now - d) / 1000);
445
486
 
@@ -3,7 +3,6 @@ import { saveRam, getRam, subscribeRam } from "../services/RamStore.js";
3
3
  import {
4
4
  createReactiveValue,
5
5
  isStructuralRead,
6
- resetStructuralReads,
7
6
  } from "../widget-factory/reactiveValue.js";
8
7
 
9
8
  // Store callbacks for each state key
@@ -57,7 +56,7 @@ const ensureRamDispatcher = () => {
57
56
  if (ramDispatcherReady) return;
58
57
  ramDispatcherReady = true;
59
58
 
60
- const unsubscribe = subscribeRam((changedKey, newValue, oldValue) => {
59
+ subscribeRam((changedKey, newValue, oldValue) => {
61
60
  const callbacks = subscribers.get(changedKey);
62
61
  // A key nobody ever passed to `useState` has no listeners: ignore it.
63
62
  if (!callbacks) return;
@@ -83,11 +82,6 @@ const ensureRamDispatcher = () => {
83
82
  // list): the surgical patch above cannot change what the component computes.
84
83
  if (handled === 0 || isStructuralRead(changedKey)) triggerRender();
85
84
  });
86
-
87
- if (typeof window !== "undefined") {
88
- if (!window._useStateCleanups) window._useStateCleanups = [];
89
- window._useStateCleanups.push(unsubscribe);
90
- }
91
85
  };
92
86
 
93
87
  /**
@@ -251,53 +245,6 @@ export const useWatchState = (key, callback) => {
251
245
  };
252
246
  };
253
247
 
254
- /**
255
- * Cleans up all state subscriptions, widget registrations, and pending render
256
- * timers. Should be called during hot-reload to reset the module to a clean state.
257
- *
258
- * @returns {void}
259
- */
260
- export const cleanupUseState = () => {
261
- if (window._useStateCleanups) {
262
- window._useStateCleanups.forEach((cleanup) => {
263
- if (typeof cleanup === "function") cleanup();
264
- });
265
- window._useStateCleanups = [];
266
- }
267
- subscribers.clear();
268
- widgetsByKey.clear();
269
- resetStructuralReads();
270
- // The dispatcher was unsubscribed above, so a later call must reinstall it.
271
- ramDispatcherReady = false;
272
- globalRender = null;
273
- if (renderTimeout) clearTimeout(renderTimeout);
274
- };
275
-
276
- /**
277
- * Returns diagnostic statistics about the current state of the useState module.
278
- * Useful for debugging subscription leaks or widget registration issues.
279
- *
280
- * @returns {{ stateKeys: number, boundWidgets: number, watchers: number, globalRender: boolean }}
281
- */
282
- export const getUseStateStats = () => {
283
- // `widgetsByKey` maps key -> widget -> prop names, so its size is the number of
284
- // watched *keys*, not of widgets. Count one per widget.
285
- let boundWidgets = 0;
286
- let watchers = 0;
287
- widgetsByKey.forEach((perWidget) => {
288
- boundWidgets += perWidget.size;
289
- });
290
- subscribers.forEach((callbacks) => {
291
- watchers += callbacks.size;
292
- });
293
- return {
294
- stateKeys: subscribers.size,
295
- boundWidgets,
296
- watchers,
297
- globalRender: !!globalRender,
298
- };
299
- };
300
-
301
248
  // Re-export RamStore functions under convenience aliases
302
249
  export {
303
250
  saveRam as saveState,