@eifi1/ui-kit 0.4.0

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 (229) hide show
  1. package/README.md +219 -0
  2. package/dist/components/account-settings.d.ts +71 -0
  3. package/dist/components/account-settings.js +159 -0
  4. package/dist/components/account-settings.js.map +1 -0
  5. package/dist/components/alert-banner.d.ts +20 -0
  6. package/dist/components/alert-banner.js +39 -0
  7. package/dist/components/alert-banner.js.map +1 -0
  8. package/dist/components/amount-input.d.ts +117 -0
  9. package/dist/components/amount-input.js +243 -0
  10. package/dist/components/amount-input.js.map +1 -0
  11. package/dist/components/calculator.d.ts +42 -0
  12. package/dist/components/calculator.js +161 -0
  13. package/dist/components/calculator.js.map +1 -0
  14. package/dist/components/chart.d.ts +58 -0
  15. package/dist/components/chart.js +158 -0
  16. package/dist/components/chart.js.map +1 -0
  17. package/dist/components/combobox-core.d.ts +77 -0
  18. package/dist/components/combobox-core.js +263 -0
  19. package/dist/components/combobox-core.js.map +1 -0
  20. package/dist/components/combobox.d.ts +141 -0
  21. package/dist/components/combobox.js +550 -0
  22. package/dist/components/combobox.js.map +1 -0
  23. package/dist/components/currency-select.d.ts +37 -0
  24. package/dist/components/currency-select.js +131 -0
  25. package/dist/components/currency-select.js.map +1 -0
  26. package/dist/components/data-table-filter-popover.d.ts +28 -0
  27. package/dist/components/data-table-filter-popover.js +201 -0
  28. package/dist/components/data-table-filter-popover.js.map +1 -0
  29. package/dist/components/data-table-filters.d.ts +5 -0
  30. package/dist/components/data-table-filters.js +118 -0
  31. package/dist/components/data-table-filters.js.map +1 -0
  32. package/dist/components/data-table-labels.d.ts +33 -0
  33. package/dist/components/data-table-labels.js +53 -0
  34. package/dist/components/data-table-labels.js.map +1 -0
  35. package/dist/components/data-table-pagination.d.ts +18 -0
  36. package/dist/components/data-table-pagination.js +92 -0
  37. package/dist/components/data-table-pagination.js.map +1 -0
  38. package/dist/components/data-table-sort.d.ts +23 -0
  39. package/dist/components/data-table-sort.js +55 -0
  40. package/dist/components/data-table-sort.js.map +1 -0
  41. package/dist/components/data-table.d.ts +5 -0
  42. package/dist/components/data-table.js +926 -0
  43. package/dist/components/data-table.js.map +1 -0
  44. package/dist/components/date-picker.d.ts +85 -0
  45. package/dist/components/date-picker.js +253 -0
  46. package/dist/components/date-picker.js.map +1 -0
  47. package/dist/components/dropdown.d.ts +98 -0
  48. package/dist/components/dropdown.js +122 -0
  49. package/dist/components/dropdown.js.map +1 -0
  50. package/dist/components/entity-combobox.d.ts +49 -0
  51. package/dist/components/entity-combobox.js +115 -0
  52. package/dist/components/entity-combobox.js.map +1 -0
  53. package/dist/components/file-dropzone.d.ts +18 -0
  54. package/dist/components/file-dropzone.js +101 -0
  55. package/dist/components/file-dropzone.js.map +1 -0
  56. package/dist/components/full-bleed-dialog.d.ts +57 -0
  57. package/dist/components/full-bleed-dialog.js +68 -0
  58. package/dist/components/full-bleed-dialog.js.map +1 -0
  59. package/dist/components/grouped-picker.d.ts +37 -0
  60. package/dist/components/grouped-picker.js +87 -0
  61. package/dist/components/grouped-picker.js.map +1 -0
  62. package/dist/components/hover-menu.d.ts +16 -0
  63. package/dist/components/hover-menu.js +113 -0
  64. package/dist/components/hover-menu.js.map +1 -0
  65. package/dist/components/mini-calendar.d.ts +33 -0
  66. package/dist/components/mini-calendar.js +133 -0
  67. package/dist/components/mini-calendar.js.map +1 -0
  68. package/dist/components/modal.d.ts +76 -0
  69. package/dist/components/modal.js +156 -0
  70. package/dist/components/modal.js.map +1 -0
  71. package/dist/components/multi-entity-combobox.d.ts +47 -0
  72. package/dist/components/multi-entity-combobox.js +116 -0
  73. package/dist/components/multi-entity-combobox.js.map +1 -0
  74. package/dist/components/multi-select.d.ts +39 -0
  75. package/dist/components/multi-select.js +132 -0
  76. package/dist/components/multi-select.js.map +1 -0
  77. package/dist/components/number-input.d.ts +74 -0
  78. package/dist/components/number-input.js +114 -0
  79. package/dist/components/number-input.js.map +1 -0
  80. package/dist/components/numpad-sheet.d.ts +45 -0
  81. package/dist/components/numpad-sheet.js +102 -0
  82. package/dist/components/numpad-sheet.js.map +1 -0
  83. package/dist/components/picker-sheet.d.ts +44 -0
  84. package/dist/components/picker-sheet.js +78 -0
  85. package/dist/components/picker-sheet.js.map +1 -0
  86. package/dist/components/popover.d.ts +20 -0
  87. package/dist/components/popover.js +46 -0
  88. package/dist/components/popover.js.map +1 -0
  89. package/dist/components/search-field.d.ts +52 -0
  90. package/dist/components/search-field.js +64 -0
  91. package/dist/components/search-field.js.map +1 -0
  92. package/dist/components/settings-fields.d.ts +39 -0
  93. package/dist/components/settings-fields.js +17 -0
  94. package/dist/components/settings-fields.js.map +1 -0
  95. package/dist/components/swipeable-row.d.ts +60 -0
  96. package/dist/components/swipeable-row.js +115 -0
  97. package/dist/components/swipeable-row.js.map +1 -0
  98. package/dist/components/toggle-group.d.ts +34 -0
  99. package/dist/components/toggle-group.js +90 -0
  100. package/dist/components/toggle-group.js.map +1 -0
  101. package/dist/components/tooltip.d.ts +86 -0
  102. package/dist/components/tooltip.js +171 -0
  103. package/dist/components/tooltip.js.map +1 -0
  104. package/dist/components/ui.d.ts +239 -0
  105. package/dist/components/ui.js +512 -0
  106. package/dist/components/ui.js.map +1 -0
  107. package/dist/components/use-mobile-reveal.d.ts +31 -0
  108. package/dist/components/use-mobile-reveal.js +33 -0
  109. package/dist/components/use-mobile-reveal.js.map +1 -0
  110. package/dist/components/use-table-state.d.ts +54 -0
  111. package/dist/components/use-table-state.js +153 -0
  112. package/dist/components/use-table-state.js.map +1 -0
  113. package/dist/components/user-avatar.d.ts +23 -0
  114. package/dist/components/user-avatar.js +33 -0
  115. package/dist/components/user-avatar.js.map +1 -0
  116. package/dist/components/wizard-stepper.d.ts +14 -0
  117. package/dist/components/wizard-stepper.js +27 -0
  118. package/dist/components/wizard-stepper.js.map +1 -0
  119. package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
  120. package/dist/feedback/feedback-attachment.d.ts +63 -0
  121. package/dist/feedback/feedback-attachment.js +160 -0
  122. package/dist/feedback/feedback-attachment.js.map +1 -0
  123. package/dist/feedback/feedback-dialog.d.ts +72 -0
  124. package/dist/feedback/feedback-dialog.js +109 -0
  125. package/dist/feedback/feedback-dialog.js.map +1 -0
  126. package/dist/feedback/feedback-inbox.d.ts +226 -0
  127. package/dist/feedback/feedback-inbox.js +315 -0
  128. package/dist/feedback/feedback-inbox.js.map +1 -0
  129. package/dist/hooks/use-anchored-panel.d.ts +80 -0
  130. package/dist/hooks/use-anchored-panel.js +71 -0
  131. package/dist/hooks/use-anchored-panel.js.map +1 -0
  132. package/dist/hooks/use-anchored-rect.d.ts +24 -0
  133. package/dist/hooks/use-anchored-rect.js +40 -0
  134. package/dist/hooks/use-anchored-rect.js.map +1 -0
  135. package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
  136. package/dist/hooks/use-body-scroll-lock.js +41 -0
  137. package/dist/hooks/use-body-scroll-lock.js.map +1 -0
  138. package/dist/hooks/use-close-transition.d.ts +41 -0
  139. package/dist/hooks/use-close-transition.js +36 -0
  140. package/dist/hooks/use-close-transition.js.map +1 -0
  141. package/dist/hooks/use-dismiss.d.ts +34 -0
  142. package/dist/hooks/use-dismiss.js +41 -0
  143. package/dist/hooks/use-dismiss.js.map +1 -0
  144. package/dist/hooks/use-media-query.d.ts +8 -0
  145. package/dist/hooks/use-media-query.js +18 -0
  146. package/dist/hooks/use-media-query.js.map +1 -0
  147. package/dist/hooks/use-overlay-history.d.ts +10 -0
  148. package/dist/hooks/use-overlay-history.js +99 -0
  149. package/dist/hooks/use-overlay-history.js.map +1 -0
  150. package/dist/hooks/use-row-swipe.d.ts +30 -0
  151. package/dist/hooks/use-row-swipe.js +128 -0
  152. package/dist/hooks/use-row-swipe.js.map +1 -0
  153. package/dist/index.d.ts +75 -0
  154. package/dist/index.js +90 -0
  155. package/dist/index.js.map +1 -0
  156. package/dist/lib/calc.d.ts +89 -0
  157. package/dist/lib/calc.js +180 -0
  158. package/dist/lib/calc.js.map +1 -0
  159. package/dist/lib/cn.d.ts +6 -0
  160. package/dist/lib/cn.js +7 -0
  161. package/dist/lib/cn.js.map +1 -0
  162. package/dist/lib/dates.d.ts +84 -0
  163. package/dist/lib/dates.js +108 -0
  164. package/dist/lib/dates.js.map +1 -0
  165. package/dist/lib/logger.d.ts +17 -0
  166. package/dist/lib/logger.js +30 -0
  167. package/dist/lib/logger.js.map +1 -0
  168. package/dist/lib/safe-storage.d.ts +23 -0
  169. package/dist/lib/safe-storage.js +20 -0
  170. package/dist/lib/safe-storage.js.map +1 -0
  171. package/dist/search/command-palette.d.ts +71 -0
  172. package/dist/search/command-palette.js +198 -0
  173. package/dist/search/command-palette.js.map +1 -0
  174. package/dist/shell/app-shell.d.ts +64 -0
  175. package/dist/shell/app-shell.js +237 -0
  176. package/dist/shell/app-shell.js.map +1 -0
  177. package/dist/shell/option-switcher-menu.d.ts +24 -0
  178. package/dist/shell/option-switcher-menu.js +44 -0
  179. package/dist/shell/option-switcher-menu.js.map +1 -0
  180. package/dist/shell/role-switcher.d.ts +19 -0
  181. package/dist/shell/role-switcher.js +28 -0
  182. package/dist/shell/role-switcher.js.map +1 -0
  183. package/dist/shell/top-bar.d.ts +15 -0
  184. package/dist/shell/top-bar.js +29 -0
  185. package/dist/shell/top-bar.js.map +1 -0
  186. package/dist/shell/topbar-action-menu.d.ts +44 -0
  187. package/dist/shell/topbar-action-menu.js +49 -0
  188. package/dist/shell/topbar-action-menu.js.map +1 -0
  189. package/dist/shell/topbar-controls.d.ts +54 -0
  190. package/dist/shell/topbar-controls.js +152 -0
  191. package/dist/shell/topbar-controls.js.map +1 -0
  192. package/dist/theme/chart-palette.d.ts +60 -0
  193. package/dist/theme/chart-palette.js +79 -0
  194. package/dist/theme/chart-palette.js.map +1 -0
  195. package/dist/theme/palette-presets.d.ts +39 -0
  196. package/dist/theme/palette-presets.js +440 -0
  197. package/dist/theme/palette-presets.js.map +1 -0
  198. package/dist/theme/palette-store.d.ts +45 -0
  199. package/dist/theme/palette-store.js +52 -0
  200. package/dist/theme/palette-store.js.map +1 -0
  201. package/dist/theme/theme-store.d.ts +57 -0
  202. package/dist/theme/theme-store.js +96 -0
  203. package/dist/theme/theme-store.js.map +1 -0
  204. package/dist/tour/tour.d.ts +104 -0
  205. package/dist/tour/tour.js +351 -0
  206. package/dist/tour/tour.js.map +1 -0
  207. package/dist/wizard/stepper-nav.d.ts +30 -0
  208. package/dist/wizard/stepper-nav.js +146 -0
  209. package/dist/wizard/stepper-nav.js.map +1 -0
  210. package/dist/wizard/types.d.ts +124 -0
  211. package/dist/wizard/types.js +24 -0
  212. package/dist/wizard/types.js.map +1 -0
  213. package/dist/wizard/use-wizard.d.ts +6 -0
  214. package/dist/wizard/use-wizard.js +222 -0
  215. package/dist/wizard/use-wizard.js.map +1 -0
  216. package/dist/wizard/validation.d.ts +23 -0
  217. package/dist/wizard/validation.js +13 -0
  218. package/dist/wizard/validation.js.map +1 -0
  219. package/dist/wizard/wizard-context.d.ts +19 -0
  220. package/dist/wizard/wizard-context.js +15 -0
  221. package/dist/wizard/wizard-context.js.map +1 -0
  222. package/dist/wizard/wizard-step.d.ts +11 -0
  223. package/dist/wizard/wizard-step.js +12 -0
  224. package/dist/wizard/wizard-step.js.map +1 -0
  225. package/dist/wizard/wizard-summary.d.ts +18 -0
  226. package/dist/wizard/wizard-summary.js +41 -0
  227. package/dist/wizard/wizard-summary.js.map +1 -0
  228. package/package.json +87 -0
  229. package/tokens.css +254 -0
@@ -0,0 +1,180 @@
1
+ function tokenize(input) {
2
+ const tokens = [];
3
+ let i = 0;
4
+ while (i < input.length) {
5
+ const ch = input[i];
6
+ if (ch === " ") {
7
+ i++;
8
+ continue;
9
+ }
10
+ if (ch === "+" || ch === "-" || ch === "*" || ch === "/") {
11
+ tokens.push({ kind: "op", value: ch });
12
+ i++;
13
+ continue;
14
+ }
15
+ if (ch === "(") {
16
+ tokens.push({ kind: "lparen" });
17
+ i++;
18
+ continue;
19
+ }
20
+ if (ch === ")") {
21
+ tokens.push({ kind: "rparen" });
22
+ i++;
23
+ continue;
24
+ }
25
+ if (ch >= "0" && ch <= "9" || ch === ".") {
26
+ let j = i;
27
+ let dotSeen = false;
28
+ while (j < input.length && (input[j] >= "0" && input[j] <= "9" || input[j] === ".")) {
29
+ if (input[j] === ".") {
30
+ if (dotSeen) return null;
31
+ dotSeen = true;
32
+ }
33
+ j++;
34
+ }
35
+ const num = Number(input.slice(i, j));
36
+ if (!Number.isFinite(num)) return null;
37
+ tokens.push({ kind: "num", value: num });
38
+ i = j;
39
+ continue;
40
+ }
41
+ return null;
42
+ }
43
+ return tokens;
44
+ }
45
+ function parse(tokens) {
46
+ let pos = 0;
47
+ function parseExpr() {
48
+ let value = parseTerm();
49
+ if (value === null) return null;
50
+ while (pos < tokens.length) {
51
+ const tok = tokens[pos];
52
+ if (tok.kind === "op" && (tok.value === "+" || tok.value === "-")) {
53
+ pos++;
54
+ const rhs = parseTerm();
55
+ if (rhs === null) return null;
56
+ value = tok.value === "+" ? value + rhs : value - rhs;
57
+ } else break;
58
+ }
59
+ return value;
60
+ }
61
+ function parseTerm() {
62
+ let value = parseFactor();
63
+ if (value === null) return null;
64
+ while (pos < tokens.length) {
65
+ const tok = tokens[pos];
66
+ if (tok.kind === "op" && (tok.value === "*" || tok.value === "/")) {
67
+ pos++;
68
+ const rhs = parseFactor();
69
+ if (rhs === null) return null;
70
+ if (tok.value === "/") {
71
+ if (rhs === 0) return null;
72
+ value = value / rhs;
73
+ } else {
74
+ value = value * rhs;
75
+ }
76
+ } else break;
77
+ }
78
+ return value;
79
+ }
80
+ function parseFactor() {
81
+ const tok = tokens[pos];
82
+ if (!tok) return null;
83
+ if (tok.kind === "op" && (tok.value === "+" || tok.value === "-")) {
84
+ pos++;
85
+ const operand = parseFactor();
86
+ if (operand === null) return null;
87
+ return tok.value === "-" ? -operand : operand;
88
+ }
89
+ if (tok.kind === "lparen") {
90
+ pos++;
91
+ const inner = parseExpr();
92
+ if (inner === null) return null;
93
+ if (tokens[pos]?.kind !== "rparen") return null;
94
+ pos++;
95
+ return inner;
96
+ }
97
+ if (tok.kind === "num") {
98
+ pos++;
99
+ return tok.value;
100
+ }
101
+ return null;
102
+ }
103
+ const result = parseExpr();
104
+ if (result === null) return null;
105
+ if (pos !== tokens.length) return null;
106
+ return result;
107
+ }
108
+ function evaluateExpression(expr) {
109
+ const normalized = expr.replace(/×/g, "*").replace(/÷/g, "/").replace(/[−–—]/g, "-").replace(/,/g, ".").trim();
110
+ if (normalized === "") return null;
111
+ const tokens = tokenize(normalized);
112
+ if (tokens === null || tokens.length === 0) return null;
113
+ const result = parse(tokens);
114
+ if (result === null || !Number.isFinite(result)) return null;
115
+ return result;
116
+ }
117
+ function looksLikeExpression(s) {
118
+ const t = s.trim();
119
+ if (t === "") return false;
120
+ if (/^-?\d*[.,]?\d*$/.test(t)) return false;
121
+ return /[+\-*/×÷−–—()]/.test(t.replace(/^-/, ""));
122
+ }
123
+ function formatResult(n) {
124
+ if (!Number.isFinite(n)) return "";
125
+ if (Math.abs(n) >= 1e21) return BigInt(n).toString();
126
+ return trimTrailingZeros(n.toFixed(10));
127
+ }
128
+ function trimTrailingZeros(fixed) {
129
+ if (!fixed.includes(".")) return fixed;
130
+ const trimmed = fixed.replace(/0+$/, "").replace(/\.$/, "");
131
+ return trimmed === "-0" || trimmed === "" ? "0" : trimmed;
132
+ }
133
+ function commitExpression(raw) {
134
+ const s = raw.trim();
135
+ if (looksLikeExpression(s)) {
136
+ const n = evaluateExpression(s);
137
+ if (n !== null) return formatResult(n);
138
+ }
139
+ const cleaned = s.replace(/,/g, ".").replace(/[^0-9.-]/g, "").replace(/(?!^)-/g, "");
140
+ return cleaned.replace(/^(-?\d*\.?\d*).*$/, "$1");
141
+ }
142
+ function sanitizeLive(raw) {
143
+ return dropExtraDots(raw.replace(/,/g, ".").replace(/[^0-9.+\-*/×÷()]/g, ""));
144
+ }
145
+ function dropExtraDots(s) {
146
+ let out = "";
147
+ let dotInNumber = false;
148
+ for (const ch of s) {
149
+ if (ch === ".") {
150
+ if (dotInNumber) continue;
151
+ dotInNumber = true;
152
+ } else if (ch < "0" || ch > "9") {
153
+ dotInNumber = false;
154
+ }
155
+ out += ch;
156
+ }
157
+ return out;
158
+ }
159
+ function isBareAmount(s) {
160
+ const t = s.trim();
161
+ return t !== "" && /^\d*[.,]?\d*$/.test(t);
162
+ }
163
+ function splitLeadingSign(text) {
164
+ const trimmed = text.trimStart();
165
+ const sign = trimmed.charAt(0);
166
+ if (sign !== "-" && sign !== "+") return { sign: "", rest: text };
167
+ const rest = trimmed.slice(1);
168
+ if (rest !== "" && !isBareAmount(rest)) return { sign: "", rest: text };
169
+ return { sign, rest };
170
+ }
171
+ export {
172
+ commitExpression,
173
+ evaluateExpression,
174
+ formatResult,
175
+ isBareAmount,
176
+ looksLikeExpression,
177
+ sanitizeLive,
178
+ splitLeadingSign
179
+ };
180
+ //# sourceMappingURL=calc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/calc.ts"],"sourcesContent":["/**\n * A tiny, dependency-free arithmetic evaluator for the numeric input fields.\n * It lets users type a calculation (e.g. \"12.50+3.20\" or \"100/3\") straight into\n * an amount field, or build one in the calculator popover, and have it resolve\n * to a number. Deliberately NOT backed by `eval`/`Function` — it's a hand-rolled\n * tokenizer + recursive-descent parser over a closed grammar (digits, decimals,\n * `+ - * /`, unary minus, parentheses), so no arbitrary code can run.\n */\n\ntype Token =\n | { kind: \"num\"; value: number }\n | { kind: \"op\"; value: \"+\" | \"-\" | \"*\" | \"/\" }\n | { kind: \"lparen\" }\n | { kind: \"rparen\" };\n\nfunction tokenize(input: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \" \") {\n i++;\n continue;\n }\n if (ch === \"+\" || ch === \"-\" || ch === \"*\" || ch === \"/\") {\n tokens.push({ kind: \"op\", value: ch });\n i++;\n continue;\n }\n if (ch === \"(\") {\n tokens.push({ kind: \"lparen\" });\n i++;\n continue;\n }\n if (ch === \")\") {\n tokens.push({ kind: \"rparen\" });\n i++;\n continue;\n }\n if ((ch >= \"0\" && ch <= \"9\") || ch === \".\") {\n let j = i;\n let dotSeen = false;\n while (j < input.length && ((input[j] >= \"0\" && input[j] <= \"9\") || input[j] === \".\")) {\n if (input[j] === \".\") {\n if (dotSeen) return null; // two dots in one number → invalid\n dotSeen = true;\n }\n j++;\n }\n const num = Number(input.slice(i, j));\n if (!Number.isFinite(num)) return null;\n tokens.push({ kind: \"num\", value: num });\n i = j;\n continue;\n }\n return null; // unknown character\n }\n return tokens;\n}\n\n/**\n * Recursive-descent parse honouring `* /` over `+ -` and left-associativity.\n * Returns `null` for any malformed input (the `null` sentinel is safe because a\n * valid result of 0 is distinct from it). Division by zero is treated as invalid.\n *\n * expr := term (('+' | '-') term)*\n * term := factor (('*' | '/') factor)*\n * factor := ('+' | '-') factor | number | '(' expr ')'\n */\nfunction parse(tokens: Token[]): number | null {\n let pos = 0;\n\n function parseExpr(): number | null {\n let value = parseTerm();\n if (value === null) return null;\n while (pos < tokens.length) {\n const tok = tokens[pos];\n if (tok.kind === \"op\" && (tok.value === \"+\" || tok.value === \"-\")) {\n pos++;\n const rhs = parseTerm();\n if (rhs === null) return null;\n value = tok.value === \"+\" ? value + rhs : value - rhs;\n } else break;\n }\n return value;\n }\n\n function parseTerm(): number | null {\n let value = parseFactor();\n if (value === null) return null;\n while (pos < tokens.length) {\n const tok = tokens[pos];\n if (tok.kind === \"op\" && (tok.value === \"*\" || tok.value === \"/\")) {\n pos++;\n const rhs = parseFactor();\n if (rhs === null) return null;\n if (tok.value === \"/\") {\n if (rhs === 0) return null;\n value = value / rhs;\n } else {\n value = value * rhs;\n }\n } else break;\n }\n return value;\n }\n\n function parseFactor(): number | null {\n const tok = tokens[pos];\n if (!tok) return null;\n if (tok.kind === \"op\" && (tok.value === \"+\" || tok.value === \"-\")) {\n pos++;\n const operand = parseFactor();\n if (operand === null) return null;\n return tok.value === \"-\" ? -operand : operand;\n }\n if (tok.kind === \"lparen\") {\n pos++;\n const inner = parseExpr();\n if (inner === null) return null;\n if (tokens[pos]?.kind !== \"rparen\") return null;\n pos++;\n return inner;\n }\n if (tok.kind === \"num\") {\n pos++;\n return tok.value;\n }\n return null;\n }\n\n const result = parseExpr();\n if (result === null) return null;\n if (pos !== tokens.length) return null; // leftover tokens → invalid\n return result;\n}\n\n/** Evaluate an arithmetic string, returning `null` if it isn't a valid sum.\n * Accepts the on-screen operator glyphs (`× ÷ −`) and comma decimals too. */\nexport function evaluateExpression(expr: string): number | null {\n const normalized = expr\n .replace(/×/g, \"*\")\n .replace(/÷/g, \"/\")\n .replace(/[−–—]/g, \"-\") // unicode minus / en / em dash → ascii\n .replace(/,/g, \".\")\n .trim();\n if (normalized === \"\") return null;\n const tokens = tokenize(normalized);\n if (tokens === null || tokens.length === 0) return null;\n const result = parse(tokens);\n if (result === null || !Number.isFinite(result)) return null;\n return result;\n}\n\n/** True when `s` carries arithmetic beyond a single leading minus — i.e. a plain\n * number (or a lone negative like \"-5\") is NOT an expression, but \"12+5\" is.\n * Used to decide whether a field's text should be evaluated on commit.\n *\n * The operator set is the one {@link evaluateExpression} accepts, on-screen glyphs\n * included: the calculator keypad inserts `× ÷ −`, and a `−` that this said was not\n * arithmetic went on to be read as a plain number — where the digits-only fallback\n * in {@link commitExpression} silently ate it, turning \"-42−−50\" (= 8) into a bare\n * 8 with no record that a sum had happened at all. */\nexport function looksLikeExpression(s: string): boolean {\n const t = s.trim();\n if (t === \"\") return false;\n if (/^-?\\d*[.,]?\\d*$/.test(t)) return false; // plain (optionally negative) decimal\n return /[+\\-*/×÷−–—()]/.test(t.replace(/^-/, \"\"));\n}\n\n/** Render an evaluated number back to a field-friendly string, trimming binary\n * float noise (so 0.1 + 0.2 reads \"0.3\", not \"0.30000000000000004\").\n *\n * FIXED notation, always, and that is the whole point of the function rather than a\n * detail of it. This used to end in `parseFloat(n.toFixed(10)).toString()`, and\n * `Number.prototype.toString` switches to EXPONENT notation below 1e-6 and at or\n * above 1e21 — an alphabet containing `e`, `+` and `-`. Neither function that reads a\n * field's text back accepts it: {@link tokenize} rejects `e` as an unknown character\n * and {@link sanitizeLive} deletes it. So {@link commitExpression} fell through to\n * its digits-only cleanup, which stripped the `e` and CONCATENATED the mantissa to\n * the exponent. A field committed its own output as a different number:\n *\n * \"1/10000000\" -> \"1e-7\" -> \"17\" (AmountInput: \"6\")\n * \"999999999999*999999999999\" -> \"9.99999999998e+23\" -> \"9.9999999999823\"\n *\n * Both controls commit on blur AND on Enter, so Enter-then-click-away was enough on\n * its own, it was silent, and it was in a money field.\n *\n * The invariant, which the test asserts directly rather than by example:\n * **`commitExpression(formatResult(n))` equals `formatResult(n)` for every finite\n * `n`.** Anything rendered here has to survive being read back. */\nexport function formatResult(n: number): string {\n if (!Number.isFinite(n)) return \"\";\n // At or above 1e21 `toFixed` gives up and hands back `toString`'s exponent form —\n // but every double that large is an integer, so BigInt renders it exactly.\n if (Math.abs(n) >= 1e21) return BigInt(n).toString();\n return trimTrailingZeros(n.toFixed(10));\n}\n\n/** \"15.7000000000\" -> \"15.7\", \"0.0000000000\" -> \"0\". The ten decimals are where the\n * binary float noise gets rounded off; this is only about not showing the padding. */\nfunction trimTrailingZeros(fixed: string): string {\n if (!fixed.includes(\".\")) return fixed;\n const trimmed = fixed.replace(/0+$/, \"\").replace(/\\.$/, \"\");\n // `toFixed` keeps the sign on a magnitude that rounded away to nothing\n // (\"-0.0000000000\"); `toString` did not, and \"-0\" in an amount field is noise.\n return trimmed === \"-0\" || trimmed === \"\" ? \"0\" : trimmed;\n}\n\n/** The single \"on blur / Enter\" transform a numeric field applies: evaluate the\n * text if it's an expression, otherwise normalise it to a bare number string\n * (comma → dot, dropping stray characters, non-leading minuses and anything after\n * a second decimal point).\n *\n * The last clause is the backstop for Keksdose live #201. A field the user typed in\n * cannot reach here malformed any more — {@link sanitizeLive} runs on every change,\n * including a paste — but a value handed in from elsewhere (a recurring rule, an\n * import, a test) still can, and \"1.2.3\" is not an expression (no operator), so it\n * used to fall straight through unchanged and be read as NaN. Here the text is CUT\n * at the second dot (\"1.2\") rather than joined (\"1.23\"): with no keystroke to\n * ignore there is nothing to say which reading was meant, and the shorter one\n * cannot silently invent a bigger number. */\nexport function commitExpression(raw: string): string {\n const s = raw.trim();\n if (looksLikeExpression(s)) {\n const n = evaluateExpression(s);\n if (n !== null) return formatResult(n);\n // Unparseable (e.g. a half-typed \"12+\"): fall through to a digits-only clean.\n }\n // Global, like sanitizeLive's: these two are the two halves of one rule and must\n // not appear to state it differently. (The `[^0-9.-]` strip on the same line\n // already ate whatever commas a non-global replace missed, so this changes no\n // behaviour — it removes the need for the next reader to run it to find that out.)\n const cleaned = s.replace(/,/g, \".\").replace(/[^0-9.-]/g, \"\").replace(/(?!^)-/g, \"\");\n return cleaned.replace(/^(-?\\d*\\.?\\d*).*$/, \"$1\");\n}\n\n/** The live, per-keystroke transform a numeric field applies: comma → dot and\n * drop anything that isn't a digit, decimal point, arithmetic operator (incl.\n * the on-screen `× ÷` glyphs) or paren — so a typed calculation survives\n * keystroke-by-keystroke but stray characters can't be entered. A lone leading\n * `-` is kept — in a plain field it is simply a negative amount; in a field whose\n * caller owns the sign it is read off again by {@link splitLeadingSign}. The text\n * is resolved to a plain number on blur/Enter via {@link commitExpression}. */\nexport function sanitizeLive(raw: string): string {\n return dropExtraDots(raw.replace(/,/g, \".\").replace(/[^0-9.+\\-*/×÷()]/g, \"\"));\n}\n\n/**\n * At most ONE decimal point per number (Keksdose live #201).\n *\n * Per NUMBER, not per string: \"1.5+2.5\" is a legitimate calculation with two dots\n * in two operands, so the counter resets at every operator and parenthesis.\n *\n * A second dot inside one operand is a **no-op keystroke** — the digits after it\n * keep landing, so \"1.2\" + \".\" + \"3\" is \"1.23\". That is what every calculator does\n * with a repeated decimal point, and it is the only option that does not make the\n * field feel broken (\"my 3 disappeared\"). This runs on paste as well as on typing,\n * so the malformed value never reaches the rest of the app: everything downstream\n * reads the field with `Number()`, NaN became a silent 0, and on the create form\n * that 0 was painted with the income green — a figure that was not a number at all\n * asserting money was coming in.\n */\nfunction dropExtraDots(s: string): string {\n let out = \"\";\n let dotInNumber = false;\n for (const ch of s) {\n if (ch === \".\") {\n if (dotInNumber) continue;\n dotInNumber = true;\n } else if (ch < \"0\" || ch > \"9\") {\n // An operator or a paren ends the current operand.\n dotInNumber = false;\n }\n out += ch;\n }\n return out;\n}\n\n/** A bare, unsigned amount: digits with at most one decimal separator and nothing\n * else — \"42\", \"42.\", \".5\", \"0,5\". NOT \"\" (an empty field has no figure to put a\n * sign in front of), not \"-42\", not \"12+5\". It is the only shape a DISPLAYED sign\n * prefix makes sense in front of; an expression carries its own arithmetic and has\n * to survive keystroke-for-keystroke. */\nexport function isBareAmount(s: string): boolean {\n const t = s.trim();\n return t !== \"\" && /^\\d*[.,]?\\d*$/.test(t);\n}\n\n/**\n * Split a LEADING sign off a plain signed number (Keksdose live #201 rework):\n * `\"-42\"` → `{ sign: \"-\", rest: \"42\" }`, everything else → `{ sign: \"\", rest: text }`.\n *\n * Only in front of a bare amount, because a minus with arithmetic after it IS\n * arithmetic: \"12-30\" is a subtraction, and \"-12+5\" is -7, not -(12+5). Those come\n * back untouched and are resolved by {@link commitExpression} on blur — at which\n * point the RESULT is a plain signed number and this function does get to speak.\n *\n * A lone \"-\" splits (rest \"\"): the sign is a statement about the direction the\n * moment it is typed, before any digit exists.\n */\nexport function splitLeadingSign(text: string): { sign: \"\" | \"-\" | \"+\"; rest: string } {\n const trimmed = text.trimStart();\n const sign = trimmed.charAt(0);\n if (sign !== \"-\" && sign !== \"+\") return { sign: \"\", rest: text };\n const rest = trimmed.slice(1);\n if (rest !== \"\" && !isBareAmount(rest)) return { sign: \"\", rest: text };\n return { sign, rest };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,6 @@
1
+ import { clsx } from 'clsx';
2
+
3
+ /** Merge class lists with Tailwind conflict resolution (later wins). */
4
+ declare const cn: (...inputs: Parameters<typeof clsx>) => string;
5
+
6
+ export { cn };
package/dist/lib/cn.js ADDED
@@ -0,0 +1,7 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ const cn = (...inputs) => twMerge(clsx(inputs));
4
+ export {
5
+ cn
6
+ };
7
+ //# sourceMappingURL=cn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/cn.ts"],"sourcesContent":["import { clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/** Merge class lists with Tailwind conflict resolution (later wins). */\nexport const cn = (...inputs: Parameters<typeof clsx>) => twMerge(clsx(inputs));\n"],"mappings":"AAAA,SAAS,YAAY;AACrB,SAAS,eAAe;AAGjB,MAAM,KAAK,IAAI,WAAoC,QAAQ,KAAK,MAAM,CAAC;","names":[]}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ISO date helpers, collected from copies that were scattered across the pages.
3
+ *
4
+ * **Every "what day is it" helper here reads the LOCAL calendar** — one family, not
5
+ * two. It used to be two: these all built a local Date and then read it back through
6
+ * `toISOString()`, which is UTC, so east of Greenwich they all returned YESTERDAY
7
+ * between local midnight and the UTC rollover. The old module note called that
8
+ * deliberate and warned against changing it. It was not defensible, and dev#471 is
9
+ * what it cost:
10
+ *
11
+ * > *"Just entered a tx now from scheduled. But it still shows as upcoming. Is there
12
+ * > a problem with timezones? It is currently 1 o clock middle european summer time."*
13
+ *
14
+ * There was: at 01:00 CEST `todayIso()` answered with the 14th, the row the scheduler
15
+ * had just booked was dated the 15th, and `isUpcoming` compares the two strings — so a
16
+ * transaction entered a second ago read as one still to come. The same slip moved
17
+ * `startOfMonthIso()` onto the last day of the PREVIOUS month for an hour every first
18
+ * of the month, which is a report range nobody would have questioned.
19
+ *
20
+ * A calendar day only means anything to the human reading it, and every caller here —
21
+ * the register's today, the date-range presets, the budget cursor, a new
22
+ * transaction's default date — is asking that question. There is no caller for whom
23
+ * "the UTC day" is the right answer; the server compares plain dates with no zone at
24
+ * all, so sending it the user's local day is also what it wants.
25
+ */
26
+ /** Zero-pad a number to two digits ("3" -> "03"). */
27
+ declare function pad(n: number): string;
28
+ /** Today as "YYYY-MM-DD" on the LOCAL calendar (see the module note). */
29
+ declare function todayIso(): string;
30
+ /** A Date as a local-time "YYYY-MM-DD" string. */
31
+ declare function toLocalIso(d: Date): string;
32
+ /** A Date as a local-time "YYYY-MM" month key. */
33
+ declare function monthKey(d: Date): string;
34
+ /** Today shifted by `days`, as "YYYY-MM-DD". */
35
+ declare function shiftIso(days: number): string;
36
+ /** First day (Monday) of the current week, as "YYYY-MM-DD". */
37
+ declare function startOfWeekIso(): string;
38
+ /** First day of the month `offsetMonths` from now, as "YYYY-MM-DD". */
39
+ declare function startOfMonthIso(offsetMonths?: number): string;
40
+ /** Last day of the month `offsetMonths` from now, as "YYYY-MM-DD". */
41
+ declare function endOfMonthIso(offsetMonths?: number): string;
42
+ /** First day of the year `offsetYears` from now, as "YYYY-MM-DD". */
43
+ declare function startOfYearIso(offsetYears?: number): string;
44
+ /** Last day of the year `offsetYears` from now, as "YYYY-MM-DD". */
45
+ declare function endOfYearIso(offsetYears?: number): string;
46
+ /** Parse a "YYYY-MM-DD" string to a local-midnight Date, or null if malformed —
47
+ * where "malformed" includes a date that does not exist.
48
+ *
49
+ * The falsy check alone was not enough. `new Date(y, m - 1, d)` ROLLS OVER out-of-range
50
+ * components rather than refusing them, so "2026-13-45" came back as 2027-02-14 and
51
+ * "2026-02-30" as 2026-03-02: a valid-looking Date for a day that never happened. No
52
+ * caller could tell — {@link formatIsoDate} blanks only on `null`, so the invented
53
+ * date formatted and displayed exactly like a real one.
54
+ *
55
+ * The round-trip is the check: build the Date, then read the components back off it.
56
+ * A value that rolled over cannot answer with the numbers it was given. That is the
57
+ * same shape {@link sameYmd} already expresses, applied to a Date and its source. */
58
+ declare function parseIsoDate(s: string): Date | null;
59
+ /** Format a "YYYY-MM-DD" string for display in `locale` (empty string when
60
+ * unparseable). Defaults to the locale's short numeric date; pass `options` to
61
+ * override (e.g. `{ dateStyle: "medium" }`). */
62
+ declare function formatIsoDate(iso: string, locale: string, options?: Intl.DateTimeFormatOptions): string;
63
+ /** A "YYYY-MM-DD" string shifted by `days` (negative shifts back), or the input
64
+ * unchanged when it isn't a parseable date.
65
+ *
66
+ * Local-time throughout — {@link parseIsoDate} builds a local-midnight Date and
67
+ * {@link toLocalIso} reads local components back, so this never slides a day the way
68
+ * a `toISOString()` round-trip does west of UTC. */
69
+ declare function addDaysIso(iso: string, days: number): string;
70
+ /** Whether two Dates fall on the same local calendar day. */
71
+ declare function sameYmd(a: Date, b: Date): boolean;
72
+ interface DateRangePreset {
73
+ key: string;
74
+ from: string;
75
+ to: string;
76
+ }
77
+ /**
78
+ * The named ranges offered by the data-table date filter (local-calendar, like
79
+ * {@link todayIso} — see the module note). `key` maps to the consumer's
80
+ * `table.preset_*` labels.
81
+ */
82
+ declare function dateRangePresets(): DateRangePreset[];
83
+
84
+ export { type DateRangePreset, addDaysIso, dateRangePresets, endOfMonthIso, endOfYearIso, formatIsoDate, monthKey, pad, parseIsoDate, sameYmd, shiftIso, startOfMonthIso, startOfWeekIso, startOfYearIso, toLocalIso, todayIso };
@@ -0,0 +1,108 @@
1
+ function pad(n) {
2
+ return String(n).padStart(2, "0");
3
+ }
4
+ function todayIso() {
5
+ return toLocalIso(/* @__PURE__ */ new Date());
6
+ }
7
+ function toLocalIso(d) {
8
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
9
+ }
10
+ function monthKey(d) {
11
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;
12
+ }
13
+ function shiftIso(days) {
14
+ const d = /* @__PURE__ */ new Date();
15
+ d.setDate(d.getDate() + days);
16
+ return toLocalIso(d);
17
+ }
18
+ function startOfWeekIso() {
19
+ const d = /* @__PURE__ */ new Date();
20
+ const day = d.getDay();
21
+ const diff = (day + 6) % 7;
22
+ d.setDate(d.getDate() - diff);
23
+ return toLocalIso(d);
24
+ }
25
+ function startOfMonthIso(offsetMonths = 0) {
26
+ const d = /* @__PURE__ */ new Date();
27
+ d.setDate(1);
28
+ d.setMonth(d.getMonth() + offsetMonths);
29
+ return toLocalIso(d);
30
+ }
31
+ function endOfMonthIso(offsetMonths = 0) {
32
+ const d = /* @__PURE__ */ new Date();
33
+ d.setDate(1);
34
+ d.setMonth(d.getMonth() + offsetMonths + 1);
35
+ d.setDate(0);
36
+ return toLocalIso(d);
37
+ }
38
+ function startOfYearIso(offsetYears = 0) {
39
+ const d = /* @__PURE__ */ new Date();
40
+ d.setMonth(0);
41
+ d.setDate(1);
42
+ d.setFullYear(d.getFullYear() + offsetYears);
43
+ return toLocalIso(d);
44
+ }
45
+ function endOfYearIso(offsetYears = 0) {
46
+ const d = /* @__PURE__ */ new Date();
47
+ d.setMonth(11);
48
+ d.setDate(31);
49
+ d.setFullYear(d.getFullYear() + offsetYears);
50
+ return toLocalIso(d);
51
+ }
52
+ function parseIsoDate(s) {
53
+ if (!s) return null;
54
+ const [y, m, d] = s.split("-").map(Number);
55
+ if (!y || !m || !d) return null;
56
+ if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) return null;
57
+ const dt = new Date(y, m - 1, d);
58
+ if (Number.isNaN(dt.getTime())) return null;
59
+ if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;
60
+ return dt;
61
+ }
62
+ function formatIsoDate(iso, locale, options) {
63
+ const d = parseIsoDate(iso);
64
+ return d ? d.toLocaleDateString(locale, options) : "";
65
+ }
66
+ function addDaysIso(iso, days) {
67
+ const d = parseIsoDate(iso);
68
+ if (!d) return iso;
69
+ d.setDate(d.getDate() + days);
70
+ return toLocalIso(d);
71
+ }
72
+ function sameYmd(a, b) {
73
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
74
+ }
75
+ function dateRangePresets() {
76
+ const mondayOffset = ((/* @__PURE__ */ new Date()).getDay() + 6) % 7;
77
+ return [
78
+ { key: "today", from: todayIso(), to: todayIso() },
79
+ { key: "yesterday", from: shiftIso(-1), to: shiftIso(-1) },
80
+ { key: "this_week", from: startOfWeekIso(), to: todayIso() },
81
+ { key: "last_week", from: shiftIso(-7 - mondayOffset), to: shiftIso(-1 - mondayOffset) },
82
+ { key: "last_7_days", from: shiftIso(-6), to: todayIso() },
83
+ { key: "last_30_days", from: shiftIso(-29), to: todayIso() },
84
+ { key: "this_month", from: startOfMonthIso(0), to: todayIso() },
85
+ { key: "last_month", from: startOfMonthIso(-1), to: endOfMonthIso(-1) },
86
+ { key: "last_3_months", from: shiftIso(-90), to: todayIso() },
87
+ { key: "ytd", from: startOfYearIso(0), to: todayIso() },
88
+ { key: "last_year", from: startOfYearIso(-1), to: endOfYearIso(-1) }
89
+ ];
90
+ }
91
+ export {
92
+ addDaysIso,
93
+ dateRangePresets,
94
+ endOfMonthIso,
95
+ endOfYearIso,
96
+ formatIsoDate,
97
+ monthKey,
98
+ pad,
99
+ parseIsoDate,
100
+ sameYmd,
101
+ shiftIso,
102
+ startOfMonthIso,
103
+ startOfWeekIso,
104
+ startOfYearIso,
105
+ toLocalIso,
106
+ todayIso
107
+ };
108
+ //# sourceMappingURL=dates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/dates.ts"],"sourcesContent":["/**\n * ISO date helpers, collected from copies that were scattered across the pages.\n *\n * **Every \"what day is it\" helper here reads the LOCAL calendar** — one family, not\n * two. It used to be two: these all built a local Date and then read it back through\n * `toISOString()`, which is UTC, so east of Greenwich they all returned YESTERDAY\n * between local midnight and the UTC rollover. The old module note called that\n * deliberate and warned against changing it. It was not defensible, and dev#471 is\n * what it cost:\n *\n * > *\"Just entered a tx now from scheduled. But it still shows as upcoming. Is there\n * > a problem with timezones? It is currently 1 o clock middle european summer time.\"*\n *\n * There was: at 01:00 CEST `todayIso()` answered with the 14th, the row the scheduler\n * had just booked was dated the 15th, and `isUpcoming` compares the two strings — so a\n * transaction entered a second ago read as one still to come. The same slip moved\n * `startOfMonthIso()` onto the last day of the PREVIOUS month for an hour every first\n * of the month, which is a report range nobody would have questioned.\n *\n * A calendar day only means anything to the human reading it, and every caller here —\n * the register's today, the date-range presets, the budget cursor, a new\n * transaction's default date — is asking that question. There is no caller for whom\n * \"the UTC day\" is the right answer; the server compares plain dates with no zone at\n * all, so sending it the user's local day is also what it wants.\n */\n\n/** Zero-pad a number to two digits (\"3\" -> \"03\"). */\nexport function pad(n: number): string {\n return String(n).padStart(2, \"0\");\n}\n\n/** Today as \"YYYY-MM-DD\" on the LOCAL calendar (see the module note). */\nexport function todayIso(): string {\n return toLocalIso(new Date());\n}\n\n/** A Date as a local-time \"YYYY-MM-DD\" string. */\nexport function toLocalIso(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n}\n\n/** A Date as a local-time \"YYYY-MM\" month key. */\nexport function monthKey(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;\n}\n\n// ---------- Date-range presets + calendar helpers ----------\n// Local-calendar like todayIso (see the module note); used by the data-table date\n// filter and its MiniCalendar. Each one mutates a local Date and must therefore READ\n// it back locally — going out through toISOString() is what shifted every one of\n// these by a day near midnight (dev#471).\n\n/** Today shifted by `days`, as \"YYYY-MM-DD\". */\nexport function shiftIso(days: number): string {\n const d = new Date();\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** First day (Monday) of the current week, as \"YYYY-MM-DD\". */\nexport function startOfWeekIso(): string {\n const d = new Date();\n const day = d.getDay();\n const diff = (day + 6) % 7; // monday = 0\n d.setDate(d.getDate() - diff);\n return toLocalIso(d);\n}\n\n/** First day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function startOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths);\n return toLocalIso(d);\n}\n\n/** Last day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function endOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths + 1);\n d.setDate(0);\n return toLocalIso(d);\n}\n\n/** First day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function startOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(0);\n d.setDate(1);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Last day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function endOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(11);\n d.setDate(31);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Parse a \"YYYY-MM-DD\" string to a local-midnight Date, or null if malformed —\n * where \"malformed\" includes a date that does not exist.\n *\n * The falsy check alone was not enough. `new Date(y, m - 1, d)` ROLLS OVER out-of-range\n * components rather than refusing them, so \"2026-13-45\" came back as 2027-02-14 and\n * \"2026-02-30\" as 2026-03-02: a valid-looking Date for a day that never happened. No\n * caller could tell — {@link formatIsoDate} blanks only on `null`, so the invented\n * date formatted and displayed exactly like a real one.\n *\n * The round-trip is the check: build the Date, then read the components back off it.\n * A value that rolled over cannot answer with the numbers it was given. That is the\n * same shape {@link sameYmd} already expresses, applied to a Date and its source. */\nexport function parseIsoDate(s: string): Date | null {\n if (!s) return null;\n const [y, m, d] = s.split(\"-\").map(Number);\n if (!y || !m || !d) return null;\n if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) return null;\n const dt = new Date(y, m - 1, d);\n if (Number.isNaN(dt.getTime())) return null;\n if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;\n return dt;\n}\n\n/** Format a \"YYYY-MM-DD\" string for display in `locale` (empty string when\n * unparseable). Defaults to the locale's short numeric date; pass `options` to\n * override (e.g. `{ dateStyle: \"medium\" }`). */\nexport function formatIsoDate(\n iso: string,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const d = parseIsoDate(iso);\n return d ? d.toLocaleDateString(locale, options) : \"\";\n}\n\n/** A \"YYYY-MM-DD\" string shifted by `days` (negative shifts back), or the input\n * unchanged when it isn't a parseable date.\n *\n * Local-time throughout — {@link parseIsoDate} builds a local-midnight Date and\n * {@link toLocalIso} reads local components back, so this never slides a day the way\n * a `toISOString()` round-trip does west of UTC. */\nexport function addDaysIso(iso: string, days: number): string {\n const d = parseIsoDate(iso);\n if (!d) return iso;\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** Whether two Dates fall on the same local calendar day. */\nexport function sameYmd(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\nexport interface DateRangePreset {\n key: string;\n from: string;\n to: string;\n}\n\n/**\n * The named ranges offered by the data-table date filter (local-calendar, like\n * {@link todayIso} — see the module note). `key` maps to the consumer's\n * `table.preset_*` labels.\n */\nexport function dateRangePresets(): DateRangePreset[] {\n const mondayOffset = (new Date().getDay() + 6) % 7; // monday = 0\n return [\n { key: \"today\", from: todayIso(), to: todayIso() },\n { key: \"yesterday\", from: shiftIso(-1), to: shiftIso(-1) },\n { key: \"this_week\", from: startOfWeekIso(), to: todayIso() },\n { key: \"last_week\", from: shiftIso(-7 - mondayOffset), to: shiftIso(-1 - mondayOffset) },\n { key: \"last_7_days\", from: shiftIso(-6), to: todayIso() },\n { key: \"last_30_days\", from: shiftIso(-29), to: todayIso() },\n { key: \"this_month\", from: startOfMonthIso(0), to: todayIso() },\n { key: \"last_month\", from: startOfMonthIso(-1), to: endOfMonthIso(-1) },\n { key: \"last_3_months\", from: shiftIso(-90), to: todayIso() },\n { key: \"ytd\", from: startOfYearIso(0), to: todayIso() },\n { key: \"last_year\", from: startOfYearIso(-1), to: endOfYearIso(-1) },\n ];\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,17 @@
1
+ import { StoreMutatorIdentifier, StateCreator } from 'zustand';
2
+
3
+ /**
4
+ * zustand logging middleware.
5
+ *
6
+ * Logs every state transition (a store "action") to the DevTools console as a
7
+ * single grouped line showing the prev/next state. Enabled in dev (Vite
8
+ * `import.meta.env.DEV`) by default; override at runtime with
9
+ * `localStorage.setItem("store_log", "0")` to silence or `"1"` to force-on in a
10
+ * production build.
11
+ */
12
+ type Logger = <T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = []>(f: StateCreator<T, Mps, Mcs>, name?: string) => StateCreator<T, Mps, Mcs>;
13
+ /** Turn store logging on or off for the rest of the session (devtools escape hatch). */
14
+ declare function setStoreLog(enabled: boolean): void;
15
+ declare const logger: Logger;
16
+
17
+ export { logger, setStoreLog };
@@ -0,0 +1,30 @@
1
+ import { readStored } from "./safe-storage.js";
2
+ let storeLog = (() => {
3
+ const override = readStored("store_log");
4
+ if (override === "0") return false;
5
+ if (override === "1") return true;
6
+ return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
7
+ })();
8
+ function setStoreLog(enabled) {
9
+ storeLog = enabled;
10
+ }
11
+ const loggerImpl = (f, name) => (set, get, store) => {
12
+ const loggedSet = ((...args) => {
13
+ const prev = get();
14
+ set(...args);
15
+ if (storeLog) {
16
+ console.debug(
17
+ `%c[store${name ? `:${name}` : ""}]`,
18
+ "color:#0d9488",
19
+ { prev, next: get() }
20
+ );
21
+ }
22
+ });
23
+ return f(loggedSet, get, store);
24
+ };
25
+ const logger = loggerImpl;
26
+ export {
27
+ logger,
28
+ setStoreLog
29
+ };
30
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/logger.ts"],"sourcesContent":["import type { StateCreator, StoreMutatorIdentifier } from \"zustand\";\n\nimport { readStored } from \"./safe-storage\";\n\n/**\n * zustand logging middleware.\n *\n * Logs every state transition (a store \"action\") to the DevTools console as a\n * single grouped line showing the prev/next state. Enabled in dev (Vite\n * `import.meta.env.DEV`) by default; override at runtime with\n * `localStorage.setItem(\"store_log\", \"0\")` to silence or `\"1\"` to force-on in a\n * production build.\n */\ntype Logger = <\n T,\n Mps extends [StoreMutatorIdentifier, unknown][] = [],\n Mcs extends [StoreMutatorIdentifier, unknown][] = [],\n>(\n f: StateCreator<T, Mps, Mcs>,\n name?: string,\n) => StateCreator<T, Mps, Mcs>;\n\ntype LoggerImpl = <T>(f: StateCreator<T, [], []>, name?: string) => StateCreator<T, [], []>;\n\n/**\n * Read ONCE, at module scope, not on every `set`.\n *\n * This used to run inside the wrapped `set`, which means a synchronous\n * `localStorage.getItem` on every state transition of every logged store — in\n * production builds too, because the override has to stay readable there by design.\n * The stores this middleware wraps include ones that transition per pointer event.\n *\n * The override cannot change without someone opening devtools and reloading, so\n * once per session is the same behaviour at a fraction of the cost. {@link setStoreLog}\n * keeps the ability to flip it live from a console, which is the only thing the\n * per-call read was actually buying.\n */\nlet storeLog: boolean = (() => {\n const override = readStored(\"store_log\");\n if (override === \"0\") return false;\n if (override === \"1\") return true;\n // NOT under vitest. `DEV` distinguishes a dev build from a production one; a test\n // run is neither, and vitest sets `DEV=true` — so every transition of every logged\n // store printed a prev/next object into the suite output. Measured on the consumer's\n // suite: 822 blocks, several thousand lines, in a log whose whole job is to make one\n // real `console.error` findable. `store_log=1` still forces it on.\n // Optional-chained because `import.meta.env` is a Vite injection: it is\n // undefined under plain Node ESM, and this package is published publicly,\n // so a non-Vite consumer must be able to import it without throwing.\n return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;\n})();\n\n/** Turn store logging on or off for the rest of the session (devtools escape hatch). */\nexport function setStoreLog(enabled: boolean): void {\n storeLog = enabled;\n}\n\nconst loggerImpl: LoggerImpl = (f, name) => (set, get, store) => {\n const loggedSet = ((...args: unknown[]) => {\n const prev = get();\n (set as (...a: unknown[]) => void)(...args);\n if (storeLog) {\n console.debug(\n `%c[store${name ? `:${name}` : \"\"}]`,\n \"color:#0d9488\",\n { prev, next: get() },\n );\n }\n }) as typeof set;\n return f(loggedSet, get, store);\n};\n\nexport const logger = loggerImpl as unknown as Logger;\n"],"mappings":"AAEA,SAAS,kBAAkB;AAmC3B,IAAI,YAAqB,MAAM;AAC7B,QAAM,WAAW,WAAW,WAAW;AACvC,MAAI,aAAa,IAAK,QAAO;AAC7B,MAAI,aAAa,IAAK,QAAO;AAS7B,SAAO,QAAQ,YAAY,KAAK,GAAG,KAAK,CAAC,YAAY,KAAK;AAC5D,GAAG;AAGI,SAAS,YAAY,SAAwB;AAClD,aAAW;AACb;AAEA,MAAM,aAAyB,CAAC,GAAG,SAAS,CAAC,KAAK,KAAK,UAAU;AAC/D,QAAM,aAAa,IAAI,SAAoB;AACzC,UAAM,OAAO,IAAI;AACjB,IAAC,IAAkC,GAAG,IAAI;AAC1C,QAAI,UAAU;AACZ,cAAQ;AAAA,QACN,WAAW,OAAO,IAAI,IAAI,KAAK,EAAE;AAAA,QACjC;AAAA,QACA,EAAE,MAAM,MAAM,IAAI,EAAE;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,WAAW,KAAK,KAAK;AAChC;AAEO,MAAM,SAAS;","names":[]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `localStorage` that cannot take the page down with it.
3
+ *
4
+ * Accessing `window.localStorage` **throws** — rather than returning null — when a
5
+ * browser has site data blocked: Safari private browsing, Chrome with third-party
6
+ * site data disabled, a partitioned webview. Three places in this package already
7
+ * knew that and wrapped their calls in try/catch (`logger.ts`, and the DataTable's
8
+ * `loadPersisted`/`savePersisted`). One did not: `AppShell` read the sidebar's
9
+ * collapse flag straight out of a `useState` initialiser, so the throw happened
10
+ * during render of the application's top-level shell. That is not a lost preference
11
+ * — nothing mounts, in every consumer of this package at once.
12
+ *
13
+ * So the guard lives here, once, and the next storage-backed preference cannot
14
+ * forget it. A read that cannot happen is indistinguishable from a key that was
15
+ * never set, which is the right answer for every caller: the default.
16
+ */
17
+ /** The stored string for `key`, or null when absent, unreadable, or blocked. */
18
+ declare function readStored(key: string): string | null;
19
+ /** Store `value` under `key`, silently doing nothing when storage is unavailable
20
+ * or full. A preference that cannot be persisted is not worth an exception. */
21
+ declare function writeStored(key: string, value: string): void;
22
+
23
+ export { readStored, writeStored };
@@ -0,0 +1,20 @@
1
+ function readStored(key) {
2
+ if (typeof window === "undefined") return null;
3
+ try {
4
+ return window.localStorage.getItem(key);
5
+ } catch {
6
+ return null;
7
+ }
8
+ }
9
+ function writeStored(key, value) {
10
+ if (typeof window === "undefined") return;
11
+ try {
12
+ window.localStorage.setItem(key, value);
13
+ } catch {
14
+ }
15
+ }
16
+ export {
17
+ readStored,
18
+ writeStored
19
+ };
20
+ //# sourceMappingURL=safe-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/safe-storage.ts"],"sourcesContent":["/**\n * `localStorage` that cannot take the page down with it.\n *\n * Accessing `window.localStorage` **throws** — rather than returning null — when a\n * browser has site data blocked: Safari private browsing, Chrome with third-party\n * site data disabled, a partitioned webview. Three places in this package already\n * knew that and wrapped their calls in try/catch (`logger.ts`, and the DataTable's\n * `loadPersisted`/`savePersisted`). One did not: `AppShell` read the sidebar's\n * collapse flag straight out of a `useState` initialiser, so the throw happened\n * during render of the application's top-level shell. That is not a lost preference\n * — nothing mounts, in every consumer of this package at once.\n *\n * So the guard lives here, once, and the next storage-backed preference cannot\n * forget it. A read that cannot happen is indistinguishable from a key that was\n * never set, which is the right answer for every caller: the default.\n */\n\n/** The stored string for `key`, or null when absent, unreadable, or blocked. */\nexport function readStored(key: string): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\n/** Store `value` under `key`, silently doing nothing when storage is unavailable\n * or full. A preference that cannot be persisted is not worth an exception. */\nexport function writeStored(key: string, value: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // ignore (private mode, quota, partitioned storage)\n }\n}\n"],"mappings":"AAkBO,SAAS,WAAW,KAA4B;AACrD,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,aAAa,QAAQ,GAAG;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIO,SAAS,YAAY,KAAa,OAAqB;AAC5D,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,WAAO,aAAa,QAAQ,KAAK,KAAK;AAAA,EACxC,QAAQ;AAAA,EAER;AACF;","names":[]}
@@ -0,0 +1,71 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface CommandItem {
5
+ id: string;
6
+ /** Primary text. */
7
+ label: string;
8
+ /** Secondary text on the right (e.g. an amount, a date, a group hint). */
9
+ hint?: string;
10
+ /** Heading this item is grouped under. */
11
+ group: string;
12
+ /** Leading icon / element. */
13
+ icon?: ReactNode;
14
+ /** Invoked when the item is chosen (navigate, run an action, …). */
15
+ onSelect: () => void;
16
+ /** Where this item leads, when it is a navigation rather than an action. Renders
17
+ * the row as a real anchor so it can be middle-/⌘-clicked into a new tab
18
+ * (Keksdose feedback #451); `onSelect` still runs for a plain click and for ↵, so
19
+ * in-app navigation stays client-side. `href` only adds what the browser can do
20
+ * with a link and JavaScript cannot fake — the rejected alternative, an
21
+ * `onAuxClick` calling `window.open`, gets the middle click and nothing else. */
22
+ href?: string;
23
+ }
24
+ interface CommandPaletteLabels {
25
+ placeholder: string;
26
+ empty: string;
27
+ loading: string;
28
+ /** Hint shown in the footer, e.g. "↑↓ to navigate · ↵ to select · esc to close". */
29
+ hint?: string;
30
+ }
31
+ interface CommandPaletteProps {
32
+ open: boolean;
33
+ onClose: () => void;
34
+ /**
35
+ * Returns results for a query (called debounced as the user types; an empty
36
+ * query is allowed — return defaults like pages/recent). Sync or async; stale
37
+ * async responses are ignored.
38
+ *
39
+ * Its IDENTITY is not a re-run signal — it is read through a ref, so the palette
40
+ * never re-searches merely because the caller rebuilt the callback. That is
41
+ * deliberate: a provider closing over a list rendered from a query allocates a new
42
+ * function on most renders, and re-running the search on each would make typing
43
+ * stutter. Use {@link CommandPaletteProps.revision} when the DATA behind the
44
+ * provider changes.
45
+ */
46
+ search: (query: string) => CommandItem[] | Promise<CommandItem[]>;
47
+ /**
48
+ * Anything whose change means the current results are out of date — typically the
49
+ * arrays the provider searches over. Compared by identity (as an effect dependency
50
+ * would be), so pass stable references: a `data = []` default allocates a fresh
51
+ * array on every render while a query is pending and would re-search forever.
52
+ *
53
+ * Without it, a provider whose data arrives AFTER the palette opened never gets
54
+ * asked again: the user's last keystroke settles the debounce, the queries resolve a
55
+ * moment later, and the groups they would have contributed are simply absent until
56
+ * another keystroke. That is the ordinary first open on a cold cache.
57
+ */
58
+ revision?: unknown;
59
+ labels?: Partial<CommandPaletteLabels>;
60
+ }
61
+ /** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */
62
+ declare function useCommandKey(onOpen: () => void): void;
63
+ /**
64
+ * A ⌘K-style command palette: a top-centred modal with a search field and a
65
+ * grouped, keyboard-navigable result list. Domain-free — the app supplies
66
+ * results via the `search` provider (pages, actions, records, …); each result
67
+ * carries its own `onSelect`.
68
+ */
69
+ declare function CommandPalette({ open, onClose, search, revision, labels, }: CommandPaletteProps): react.ReactPortal | null;
70
+
71
+ export { type CommandItem, CommandPalette, type CommandPaletteLabels, useCommandKey };