flet-box 1.1.2 → 1.1.3

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 (154) hide show
  1. package/AGENTS_FOR_CODE_GEN.md +284 -0
  2. package/CONTRIBUTING.md +238 -0
  3. package/README.md +2 -2
  4. package/bin/cli.js +88 -1
  5. package/bin/commands/auditFix.js +181 -27
  6. package/bin/commands/checkApi.js +14 -3
  7. package/bin/commands/checkStructure.js +368 -0
  8. package/bin/commands/checkSyntax.js +293 -0
  9. package/bin/commands/checkUi.js +666 -14
  10. package/bin/commands/create.js +14 -0
  11. package/bin/commands/createBundle.js +2 -2
  12. package/bin/commands/metrics.js +448 -0
  13. package/bin/commands/release.js +18 -0
  14. package/bin/commands/report.js +849 -0
  15. package/bin/commands/tools.js +76 -0
  16. package/bin/commands/unpack.js +39 -11
  17. package/bin/commands/verify.js +879 -0
  18. package/bin/mcp.js +280 -0
  19. package/bin/utils/contract.js +27 -0
  20. package/bin/utils/dtsProps.js +5 -1
  21. package/bin/utils/migrationGuide.js +146 -0
  22. package/bin/utils/moduleExports.js +9 -5
  23. package/bin/utils/templates.js +225 -18
  24. package/bin/utils/toolkit.js +191 -0
  25. package/docs/README.md +5 -1
  26. package/docs/arquitectura.md +1 -1
  27. package/docs/cli/README.md +168 -2
  28. package/docs/guides/README.md +6 -0
  29. package/docs/guides/accessibility.md +2 -1
  30. package/docs/guides/ai-toolkit.md +166 -0
  31. package/docs/guides/compatibility.md +1 -1
  32. package/docs/guides/migration.md +92 -0
  33. package/docs/guides/minification-and-protection.md +2 -1
  34. package/docs/guides/mobile-and-platforms.md +1 -1
  35. package/docs/guides/online-updates.md +2 -0
  36. package/docs/server/deployment.md +1 -1
  37. package/docs/tools/styling.md +2 -2
  38. package/docs/tools/theme.md +139 -4
  39. package/docs/widget/Accordion.md +74 -8
  40. package/docs/widget/AdMob.md +68 -3
  41. package/docs/widget/AdSense.md +67 -2
  42. package/docs/widget/AlertDialog.md +1 -1
  43. package/docs/widget/AnimatedBox.md +80 -39
  44. package/docs/widget/AnimatedText.md +74 -18
  45. package/docs/widget/Audio.md +73 -14
  46. package/docs/widget/Avatar.md +69 -4
  47. package/docs/widget/Badge.md +68 -3
  48. package/docs/widget/BottomSheet.md +2 -2
  49. package/docs/widget/Button.md +69 -3
  50. package/docs/widget/Card.md +69 -4
  51. package/docs/widget/Carousel.md +67 -2
  52. package/docs/widget/Chart.md +67 -2
  53. package/docs/widget/Checkbox.md +74 -8
  54. package/docs/widget/Chip.md +68 -3
  55. package/docs/widget/CircularBar.md +74 -15
  56. package/docs/widget/CircularChart.md +71 -10
  57. package/docs/widget/CodeViewer.md +67 -2
  58. package/docs/widget/Column.md +68 -3
  59. package/docs/widget/Container.md +68 -3
  60. package/docs/widget/DataTable.md +67 -2
  61. package/docs/widget/Divider.md +67 -2
  62. package/docs/widget/DraggBox.md +67 -2
  63. package/docs/widget/Dropdown.md +76 -10
  64. package/docs/widget/DroppBox.md +67 -2
  65. package/docs/widget/FloatingActionButton.md +68 -3
  66. package/docs/widget/GridView.md +67 -2
  67. package/docs/widget/Icon.md +71 -5
  68. package/docs/widget/Image.md +68 -3
  69. package/docs/widget/Input.md +68 -3
  70. package/docs/widget/Inspector.md +2 -2
  71. package/docs/widget/InstallButton.md +73 -13
  72. package/docs/widget/ListTile.md +69 -3
  73. package/docs/widget/ListView.md +67 -2
  74. package/docs/widget/Maps.md +68 -3
  75. package/docs/widget/Markdown.md +74 -15
  76. package/docs/widget/MatrixRain.md +72 -11
  77. package/docs/widget/Menu.md +209 -0
  78. package/docs/widget/Modal.md +3 -3
  79. package/docs/widget/Pagination.md +68 -3
  80. package/docs/widget/ParallaxBox.md +72 -12
  81. package/docs/widget/ProgressBar.md +69 -4
  82. package/docs/widget/QRCode.md +67 -2
  83. package/docs/widget/README.md +54 -50
  84. package/docs/widget/Radio.md +76 -10
  85. package/docs/widget/Rating.md +70 -6
  86. package/docs/widget/Row.md +68 -3
  87. package/docs/widget/Skeleton.md +68 -3
  88. package/docs/widget/Slider.md +75 -9
  89. package/docs/widget/SnackBar.md +2 -2
  90. package/docs/widget/Stack.md +68 -3
  91. package/docs/widget/Stepper.md +68 -3
  92. package/docs/widget/Switch.md +74 -8
  93. package/docs/widget/Text.md +71 -4
  94. package/docs/widget/ThemePicker.md +197 -0
  95. package/docs/widget/Tooltip.md +72 -7
  96. package/docs/widget/TreeView.md +70 -10
  97. package/docs/widget/Video.md +71 -10
  98. package/package.json +10 -3
  99. package/skills/fletbox-design/SKILL.md +89 -5
  100. package/skills/fletbox-motion/SKILL.md +1 -1
  101. package/skills/fletbox-rules/SKILL.md +56 -4
  102. package/src/core/runApp.js +12 -0
  103. package/src/index.d.ts +374 -101
  104. package/src/index.js +9 -0
  105. package/src/navigations/AdaptiveScaffold.js +2 -2
  106. package/src/navigations/BottomNavigation.js +1 -1
  107. package/src/navigations/Scaffold.js +18 -6
  108. package/src/tools/useState.js +11 -4
  109. package/src/utils/colors.js +275 -0
  110. package/src/utils/errorBoundary.js +288 -0
  111. package/src/utils/index.js +10 -2
  112. package/src/utils/stackPosition.js +39 -24
  113. package/src/utils/syntaxHighlight.js +4 -1
  114. package/src/utils/themes.js +279 -3
  115. package/src/widget-factory/addChildren.js +17 -10
  116. package/src/widget-factory/assignProps.js +18 -6
  117. package/src/widget-factory/createWidget.js +7 -0
  118. package/src/widget-factory/deprecations.js +77 -0
  119. package/src/widget-factory/translateProps.js +11 -1
  120. package/src/widget-factory/warnings.js +1 -27
  121. package/src/widget-factory/widgetFactory.js +59 -17
  122. package/src/widgets/Accordion.js +21 -7
  123. package/src/widgets/Badge.js +11 -2
  124. package/src/widgets/Button.js +15 -3
  125. package/src/widgets/Carousel.js +12 -4
  126. package/src/widgets/Chart.js +7 -2
  127. package/src/widgets/Checkbox.js +1 -2
  128. package/src/widgets/Chip.js +24 -10
  129. package/src/widgets/CircularBar.js +9 -3
  130. package/src/widgets/CircularChart.js +5 -3
  131. package/src/widgets/Container.js +6 -0
  132. package/src/widgets/Divider.js +3 -1
  133. package/src/widgets/DraggBox.js +9 -0
  134. package/src/widgets/Dropdown.js +3 -3
  135. package/src/widgets/DroppBox.js +5 -0
  136. package/src/widgets/FloatingActionButton.js +1 -1
  137. package/src/widgets/Icon.js +5 -1
  138. package/src/widgets/InstallButton.js +4 -2
  139. package/src/widgets/Markdown.js +171 -103
  140. package/src/widgets/Menu.js +340 -0
  141. package/src/widgets/Modal.js +7 -1
  142. package/src/widgets/Pagination.js +10 -5
  143. package/src/widgets/ProgressBar.js +5 -4
  144. package/src/widgets/QRCode.js +4 -0
  145. package/src/widgets/Radio.js +1 -2
  146. package/src/widgets/Rating.js +9 -0
  147. package/src/widgets/Slider.js +4 -1
  148. package/src/widgets/SnackBar.js +2 -2
  149. package/src/widgets/Switch.js +1 -2
  150. package/src/widgets/Text.js +9 -5
  151. package/src/widgets/ThemePicker.js +234 -0
  152. package/src/widgets/TreeView.js +2 -1
  153. package/src/widgets/index.js +2 -0
  154. package/docs/CONTRIBUTING.md +0 -156
@@ -0,0 +1,340 @@
1
+ /**
2
+ * @file Menu.js
3
+ * @description A trigger that opens an anchored menu of actions (kebab menu).
4
+ */
5
+ import { colors } from "../utils/themes.js";
6
+ import { composeUpdate } from "../utils/composeUpdate.js";
7
+ import { Container } from "./Container.js";
8
+ import { Row } from "./Row.js";
9
+ import { Column } from "./Column.js";
10
+ import { Text } from "./Text.js";
11
+ import { Icon } from "./Icon.js";
12
+ import { Button } from "./Button.js";
13
+ import { TAP_MINIMUM } from "../tools/index.js";
14
+ import { trapFocus } from "../utils/focusScope.js";
15
+
16
+ /**
17
+ * Props owned by the closure. They are routed to `update()` and never to the
18
+ * factory: `items` is an array of records, not something the DOM can receive.
19
+ */
20
+ const MENU_PROP_KEYS = ["items", "disabled", "label"];
21
+
22
+ /** The panel is a portal on `document.body`, so the trigger points at it by id. */
23
+ let menuIdSeed = 0;
24
+
25
+ /**
26
+ * A kebab (or any icon) trigger with a menu of actions. It is the widget a row
27
+ * uses for "rename / delete" without spending two permanent buttons on it.
28
+ *
29
+ * The panel is a real menu, not a list of links: the trigger keeps the Tab stop
30
+ * and the arrow keys move focus through `role="menuitem"` rows. `Escape` closes
31
+ * and returns focus; `Tab` closes and lets the page continue. Every listener it
32
+ * adds is released when the widget unmounts.
33
+ */
34
+ export const Menu = (inputProps = {}) => {
35
+ const {
36
+ items = [],
37
+ label = "Open menu",
38
+ icon = "more_vert",
39
+ width = 200,
40
+ align = "end",
41
+ disabled = false,
42
+ portal = true,
43
+ bgColor = colors.surface,
44
+ borderRadius = 8,
45
+ size = TAP_MINIMUM,
46
+ ...rest
47
+ } = inputProps;
48
+
49
+ let currentItems = Array.isArray(items) ? [...items] : [];
50
+ let currentLabel = String(label);
51
+ let currentDisabled = Boolean(disabled);
52
+ let isOpen = false;
53
+ let panel = null;
54
+ let itemNodes = [];
55
+ let outsideClickListener = null;
56
+ let positionListener = null;
57
+ let releaseFocus = null;
58
+
59
+ const baseId =
60
+ typeof rest.id === "string" && rest.id ? rest.id : `fletbox-menu-${++menuIdSeed}`;
61
+ const panelId = `${baseId}-panel`;
62
+
63
+ const trigger = Button({
64
+ icon,
65
+ variant: "text",
66
+ width: size,
67
+ height: size,
68
+ padding: 8,
69
+ borderRadius,
70
+ color: colors.textSecondary,
71
+ bgColor: "transparent",
72
+ "aria-label": currentLabel,
73
+ "aria-haspopup": "menu",
74
+ "aria-expanded": "false",
75
+ onPress: () => {
76
+ if (isOpen) closeMenu(true);
77
+ else openMenu();
78
+ },
79
+ });
80
+
81
+ const root = Container({
82
+ widgetName: "Menu",
83
+ position: "relative",
84
+ width: "auto",
85
+ display: "inline-flex",
86
+ bgColor: "transparent",
87
+ ...rest,
88
+ children: [trigger],
89
+ });
90
+
91
+ const enabledNodes = () =>
92
+ itemNodes.filter((node) => node.getAttribute("aria-disabled") !== "true");
93
+
94
+ const focusNode = (node) => {
95
+ if (node) node.focus();
96
+ };
97
+
98
+ /** Close first, then run the action: a modal opened by the action takes the
99
+ * focus after the trigger has already got it back. */
100
+ const activate = (index) => {
101
+ const item = currentItems[index];
102
+ if (!item || item.disabled || currentDisabled) return;
103
+ closeMenu(true);
104
+ item.onPress?.();
105
+ };
106
+
107
+ const buildPanel = () => {
108
+ const node = Column({
109
+ id: panelId,
110
+ role: "menu",
111
+ "aria-label": currentLabel,
112
+ zIndex: 10000,
113
+ width,
114
+ maxHeight: 320,
115
+ padding: 4,
116
+ gap: 4,
117
+ overflow: "auto",
118
+ bgColor,
119
+ border: `1px solid ${colors.border}`,
120
+ borderRadius,
121
+ boxShadow: "0 12px 32px rgba(15,23,42,0.16)",
122
+ boxSizing: "border-box",
123
+ });
124
+
125
+ currentItems.forEach((item, index) => {
126
+ const danger = Boolean(item.danger);
127
+ const itemDisabled = Boolean(item.disabled) || currentDisabled;
128
+ const ink = danger ? colors.dangerStrong : colors.text;
129
+ const row = Row({
130
+ role: "menuitem",
131
+ tabIndex: -1,
132
+ // A menu item is reached with the arrows, never with Tab: the trigger is
133
+ // the control, and the panel is a list of actions it owns.
134
+ "aria-disabled": itemDisabled ? "true" : undefined,
135
+ minHeight: TAP_MINIMUM,
136
+ paddingVertical: 8,
137
+ paddingHorizontal: 12,
138
+ gap: 8,
139
+ borderRadius: 4,
140
+ alignItems: "center",
141
+ cursor: itemDisabled ? "not-allowed" : "pointer",
142
+ opacity: itemDisabled ? 0.5 : 1,
143
+ disableTransform: true,
144
+ bgColor: "transparent",
145
+ onclick: itemDisabled ? undefined : () => activate(index),
146
+ children: [
147
+ item.icon
148
+ ? Icon({
149
+ name: item.icon,
150
+ size: 18,
151
+ color: danger ? colors.dangerStrong : colors.textSecondary,
152
+ })
153
+ : null,
154
+ Text({
155
+ text: item.text || "",
156
+ size: 14,
157
+ color: ink,
158
+ flex: 1,
159
+ minWidth: 0,
160
+ overflow: "hidden",
161
+ textOverflow: "ellipsis",
162
+ whiteSpace: "nowrap",
163
+ }),
164
+ ].filter(Boolean),
165
+ });
166
+ row.onmouseenter = () => {
167
+ if (!itemDisabled) row.style.backgroundColor = colors.border;
168
+ };
169
+ row.onmouseleave = () => {
170
+ row.style.backgroundColor = "transparent";
171
+ };
172
+ node.appendChild(row);
173
+ itemNodes.push(row);
174
+ });
175
+
176
+ return node;
177
+ };
178
+
179
+ /** The panel is measured in px because it is already laid out: this is the
180
+ * one place a pixel is the truth (viewport geometry), not a frozen size. */
181
+ const positionPanel = () => {
182
+ if (!panel) return;
183
+ const rect = trigger.getBoundingClientRect();
184
+ const panelRect = panel.getBoundingClientRect();
185
+ const left =
186
+ align === "end" ? rect.right - panelRect.width : rect.left;
187
+ const topCandidate = rect.bottom + 4;
188
+ const fitsBelow = topCandidate + panelRect.height <= window.innerHeight - 8;
189
+ const top = fitsBelow
190
+ ? topCandidate
191
+ : Math.max(8, rect.top - panelRect.height - 4);
192
+ panel.style.position = "fixed";
193
+ panel.style.left = `${Math.max(8, Math.min(left, window.innerWidth - panelRect.width - 8))}px`;
194
+ panel.style.top = `${Math.max(8, Math.min(top, window.innerHeight - 8))}px`;
195
+ };
196
+
197
+ const onPanelKeyDown = (event) => {
198
+ const nodes = enabledNodes();
199
+ const index = nodes.indexOf(document.activeElement);
200
+
201
+ if (event.key === "Escape") {
202
+ event.preventDefault();
203
+ event.stopPropagation();
204
+ closeMenu(true);
205
+ return;
206
+ }
207
+ if (event.key === "Tab") {
208
+ // Release before the trap's own listener reads the key: a menu closes on
209
+ // Tab, it does not keep the reader inside a panel they asked to leave.
210
+ if (releaseFocus) {
211
+ releaseFocus();
212
+ releaseFocus = null;
213
+ }
214
+ closeMenu(false);
215
+ return;
216
+ }
217
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
218
+ if (!nodes.length) return;
219
+ event.preventDefault();
220
+ const step = event.key === "ArrowDown" ? 1 : -1;
221
+ const next = index < 0
222
+ ? (step > 0 ? 0 : nodes.length - 1)
223
+ : (index + step + nodes.length) % nodes.length;
224
+ focusNode(nodes[next]);
225
+ return;
226
+ }
227
+ if (event.key === "Home") {
228
+ event.preventDefault();
229
+ focusNode(nodes[0]);
230
+ return;
231
+ }
232
+ if (event.key === "End") {
233
+ event.preventDefault();
234
+ focusNode(nodes[nodes.length - 1]);
235
+ }
236
+ };
237
+
238
+ const openMenu = () => {
239
+ if (currentDisabled || isOpen) return;
240
+ isOpen = true;
241
+ panel = buildPanel();
242
+ if (portal) document.body.appendChild(panel);
243
+ else root.appendChild(panel);
244
+ positionPanel();
245
+ // The panel holds Tab while it is open and gives focus back on close; this
246
+ // is the same scope the three overlays use, not a second listener.
247
+ releaseFocus = trapFocus(panel);
248
+ // Capture: the Tab branch above has to release the trap before the trap's
249
+ // own listener runs, or the browser swallows the key and the panel closes
250
+ // without the focus moving on.
251
+ panel.addEventListener("keydown", onPanelKeyDown, true);
252
+
253
+ trigger.setAttribute("aria-expanded", "true");
254
+ trigger.setAttribute("aria-controls", panelId);
255
+ focusNode(enabledNodes()[0]);
256
+
257
+ outsideClickListener = (event) => {
258
+ if (root.contains(event.target)) return;
259
+ if (panel && panel.contains(event.target)) return;
260
+ closeMenu(false);
261
+ };
262
+ positionListener = () => positionPanel();
263
+ document.addEventListener("click", outsideClickListener);
264
+ window.addEventListener("resize", positionListener);
265
+ window.addEventListener("scroll", positionListener, true);
266
+ };
267
+
268
+ const closeMenu = (returnFocus) => {
269
+ if (!isOpen) return;
270
+ isOpen = false;
271
+ if (panel) {
272
+ panel.removeEventListener("keydown", onPanelKeyDown, true);
273
+ panel.remove();
274
+ }
275
+ panel = null;
276
+ itemNodes = [];
277
+ if (releaseFocus) {
278
+ releaseFocus();
279
+ releaseFocus = null;
280
+ }
281
+ trigger.setAttribute("aria-expanded", "false");
282
+ trigger.removeAttribute("aria-controls");
283
+ if (outsideClickListener) {
284
+ document.removeEventListener("click", outsideClickListener);
285
+ outsideClickListener = null;
286
+ }
287
+ if (positionListener) {
288
+ window.removeEventListener("resize", positionListener);
289
+ window.removeEventListener("scroll", positionListener, true);
290
+ positionListener = null;
291
+ }
292
+ if (returnFocus) trigger.focus();
293
+ };
294
+
295
+ // ArrowDown/ArrowUp open the menu from the trigger, the way a native menu
296
+ // button does. Enter and Space are the engine's own press path.
297
+ const onTriggerKeyDown = (event) => {
298
+ if (event.target !== trigger || currentDisabled || isOpen) return;
299
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
300
+ event.preventDefault();
301
+ openMenu();
302
+ const nodes = enabledNodes();
303
+ focusNode(event.key === "ArrowDown" ? nodes[0] : nodes[nodes.length - 1]);
304
+ }
305
+ };
306
+ trigger.addEventListener("keydown", onTriggerKeyDown);
307
+
308
+ const update = (newProps) => {
309
+ if (newProps.items !== undefined) {
310
+ currentItems = Array.isArray(newProps.items) ? [...newProps.items] : [];
311
+ if (isOpen) {
312
+ closeMenu(false);
313
+ openMenu();
314
+ }
315
+ }
316
+ if (newProps.label !== undefined) {
317
+ currentLabel = String(newProps.label);
318
+ trigger.setAttribute("aria-label", currentLabel);
319
+ }
320
+ if (newProps.disabled !== undefined) {
321
+ currentDisabled = Boolean(newProps.disabled);
322
+ trigger.update({ disabled: currentDisabled });
323
+ if (currentDisabled) closeMenu(false);
324
+ }
325
+ };
326
+
327
+ composeUpdate(root, MENU_PROP_KEYS, update);
328
+
329
+ root.open = openMenu;
330
+ root.close = () => closeMenu(false);
331
+
332
+ root.onUnmount(() => {
333
+ closeMenu(false);
334
+ trigger.removeEventListener("keydown", onTriggerKeyDown);
335
+ });
336
+
337
+ return root;
338
+ };
339
+
340
+ export default Menu;
@@ -468,6 +468,10 @@ export const Modal = (props = {}) => {
468
468
  });
469
469
 
470
470
  // Public API
471
+ // FIX [MODAL-3]: track the "live" content so setLoading(false) restores
472
+ // whatever updateContent last set, not the original construction value.
473
+ let currentContent = content;
474
+
471
475
  const instance = {
472
476
  open: handleOpen,
473
477
  close: handleClose,
@@ -484,6 +488,8 @@ export const Modal = (props = {}) => {
484
488
 
485
489
  // Methods for dynamic updates
486
490
  updateContent: (newContent) => {
491
+ // Track the most recent content so setLoading(false) can restore it
492
+ currentContent = newContent;
487
493
  if (contentElement) {
488
494
  while (contentElement.firstChild) {
489
495
  contentElement.removeChild(contentElement.firstChild);
@@ -548,7 +554,7 @@ export const Modal = (props = {}) => {
548
554
 
549
555
  instance.updateContent(loadingEl);
550
556
  } else {
551
- instance.updateContent(content);
557
+ instance.updateContent(currentContent);
552
558
  }
553
559
  },
554
560
  };
@@ -28,7 +28,12 @@ export const Pagination = (props) => {
28
28
  ...rest
29
29
  } = props;
30
30
 
31
- const totalPages = Math.ceil(totalItems / pageSize);
31
+ // FIX [PAGINATION-1]: pageSize:0 → division by zero → totalPages=Infinity.
32
+ // maxButtons:0 → getPageNumbers returns [] but the ellipsis logic still adds
33
+ // boundary buttons. Guard both.
34
+ const safePageSize = pageSize > 0 ? pageSize : 1;
35
+ const safeMaxButtons = maxButtons > 0 ? maxButtons : 1;
36
+ const totalPages = Math.ceil(totalItems / safePageSize);
32
37
  if (totalPages <= 1) return null;
33
38
 
34
39
  let current = Math.min(Math.max(currentPage, 1), totalPages);
@@ -43,11 +48,11 @@ export const Pagination = (props) => {
43
48
 
44
49
  const getPageNumbers = () => {
45
50
  const pages = [];
46
- let start = Math.max(1, current - Math.floor(maxButtons / 2));
47
- let end = Math.min(totalPages, start + maxButtons - 1);
51
+ let start = Math.max(1, current - Math.floor(safeMaxButtons / 2));
52
+ let end = Math.min(totalPages, start + safeMaxButtons - 1);
48
53
 
49
- if (end - start + 1 < maxButtons) {
50
- start = Math.max(1, end - maxButtons + 1);
54
+ if (end - start + 1 < safeMaxButtons) {
55
+ start = Math.max(1, end - safeMaxButtons + 1);
51
56
  }
52
57
 
53
58
  for (let i = start; i <= end; i++) {
@@ -28,7 +28,8 @@ export const ProgressBar = (props) => {
28
28
  } = props;
29
29
 
30
30
  let progressValue = Math.min(Math.max(value, 0), max);
31
- let percentage = (progressValue / max) * 100;
31
+ // Guard against max === 0 to avoid NaN% in the fill and the aria attribute.
32
+ let percentage = max > 0 ? (progressValue / max) * 100 : 0;
32
33
  const finalBorderRadius =
33
34
  borderRadius !== undefined ? borderRadius : height / 2;
34
35
  const isColumn = valuePosition === "top" || valuePosition === "bottom";
@@ -122,8 +123,8 @@ export const ProgressBar = (props) => {
122
123
 
123
124
  // Value percentage.
124
125
  // FIX: guardo referencia directa al span del valor (`valueElRef`) en vez de
125
- // buscarlo con `querySelector("span:last-child")` — la versión anterior
126
- // podía escribir el porcentaje dentro del label (si el label estaba a la
126
+ // querySelector("span:last-child") - the old version could
127
+ // write the percentage inside the label (if the label was at the
127
128
  // derecha, era el último span) y el check `valueEl !== label` nunca fallaba
128
129
  // porque `label` era un string.
129
130
  let valueElRef = null;
@@ -174,7 +175,7 @@ export const ProgressBar = (props) => {
174
175
  const updateProgress = (newValue) => {
175
176
  if (indeterminate) return;
176
177
  progressValue = Math.min(Math.max(newValue, 0), max);
177
- percentage = (progressValue / max) * 100;
178
+ percentage = max > 0 ? (progressValue / max) * 100 : 0;
178
179
  fill.style.width = `${percentage}%`;
179
180
  // The announced number has to be the drawn number: a reader kept on the build
180
181
  // value would hear "0%" while the bar filled.
@@ -216,6 +216,10 @@ function buildQRMatrix(text, ecLevel = "M") {
216
216
  for (let aj = 0; aj < ap.length; aj++) {
217
217
  const r = ap[ai], c = ap[aj];
218
218
  if (reserved[r][c]) continue; // skip if overlaps finder
219
+ // FIX [QR-2]: also skip patterns centred on timing rows/cols (row 6 or
220
+ // col 6). An alignment pattern centred at (6,x) or (x,6) would corrupt
221
+ // the timing pattern. ISO 18004 §7.3.6 excludes those positions.
222
+ if (r === 6 || c === 6) continue;
219
223
  for (let dr = -2; dr <= 2; dr++) {
220
224
  for (let dc = -2; dc <= 2; dc++) {
221
225
  const v = (Math.abs(dr) === 2 || Math.abs(dc) === 2) ? 1
@@ -10,6 +10,7 @@ import { TAP_MINIMUM, LABEL_TEXT } from "../tools/index.js";
10
10
  export const Radio = (props) => {
11
11
  const {
12
12
  selected = false,
13
+ /** @deprecated Use `onPress` instead — it receives the same boolean. */
13
14
  onSelect,
14
15
  onPress: onPressProp,
15
16
  disabled = false,
@@ -20,8 +21,6 @@ export const Radio = (props) => {
20
21
  ...rest
21
22
  } = props;
22
23
 
23
- /** @deprecated Use onPress instead */
24
- // onSelect kept as alias for backward compatibility
25
24
  const onPress = onPressProp || onSelect;
26
25
 
27
26
  let internalSelected = selected;
@@ -8,6 +8,7 @@ import { Text } from "./Text.js";
8
8
  import { Icon } from "./Icon.js";
9
9
  import { colors, subscribeTheme } from "../utils/themes.js";
10
10
  import { TAP_MINIMUM } from "../tools/index.js";
11
+ import { composeUpdate } from "../utils/composeUpdate.js";
11
12
 
12
13
  export const Rating = (props) => {
13
14
  const {
@@ -355,6 +356,14 @@ export const Rating = (props) => {
355
356
  ratingContainer.getValue = () => currentValue;
356
357
  ratingContainer.updateStars = updateStars;
357
358
 
359
+ // FIX [RATING-1]: expose `update({value})` via composeUpdate so the checker's
360
+ // no-update-overwrite rule is satisfied and `rating.update({value: 3})` works.
361
+ const RATING_PROP_KEYS = ["value"];
362
+ const ratingUpdate = (newProps) => {
363
+ if (newProps.value !== undefined) setValue(newProps.value);
364
+ };
365
+ composeUpdate(ratingContainer, RATING_PROP_KEYS, ratingUpdate);
366
+
358
367
  Object.defineProperty(ratingContainer, "value", {
359
368
  get: () => currentValue,
360
369
  set: (newValue) => setValue(newValue),
@@ -415,7 +415,10 @@ export const Slider = (props) => {
415
415
  }
416
416
  if (inverted) percent = 1 - percent;
417
417
  let raw = currentMin + percent * (currentMax - currentMin);
418
- let stepped = Math.round(raw / currentStep) * currentStep;
418
+ // FIX [SLIDER-3]: snap relative to `min`, not to zero.
419
+ // `Math.round(raw / step) * step` snapped to 0, step, 2*step, …
420
+ // For min=1, step=2 that gave 2, 4, 6 instead of 1, 3, 5.
421
+ let stepped = Math.round((raw - currentMin) / currentStep) * currentStep + currentMin;
419
422
  return Math.min(Math.max(stepped, currentMin), currentMax);
420
423
  }
421
424
 
@@ -10,7 +10,7 @@ import { readableInk } from "../utils/readableInk.js";
10
10
  import { toREM } from "../utils/units.js";
11
11
 
12
12
  /**
13
- * SnackBar - Notificación temporal
13
+ * SnackBar - Temporary notification
14
14
  * @param {Object} options
15
15
  * @returns {Object} { close, show, getElement }
16
16
  */
@@ -84,7 +84,7 @@ export function SnackBar(options) {
84
84
  const finalAction =
85
85
  customAction || (customBg ? finalText : preset.action);
86
86
 
87
- // Createste main container
87
+ // Creates the main container
88
88
  const snackbar = WidgetFactory({
89
89
  tag: "div",
90
90
  position: "fixed",
@@ -11,6 +11,7 @@ import { PILL, TAP_MINIMUM, LABEL_TEXT } from "../tools/index.js";
11
11
  export const Switch = (props) => {
12
12
  const {
13
13
  value = false,
14
+ /** @deprecated Use `onPress` instead — it receives the same boolean. */
14
15
  onToggle,
15
16
  onPress: onPressProp,
16
17
  disabled = false,
@@ -20,8 +21,6 @@ export const Switch = (props) => {
20
21
  ...rest
21
22
  } = props;
22
23
 
23
- /** @deprecated Use onPress instead */
24
- // onToggle kept as alias for backward compatibility
25
24
  const onPress = onPressProp || onToggle;
26
25
 
27
26
  // Sizes. Track height is knob + 2 x pad, so the knob can never be asked to sit
@@ -45,6 +45,10 @@ export const Text = (props) => {
45
45
  };
46
46
  if (type && headingTags[type]) baseTag = headingTags[type];
47
47
 
48
+ // FIX [TEXT-1]: guard against `styles: null` — Array.isArray(null) is false
49
+ // but `[...null]` throws; an explicit null means "no styles", same as [].
50
+ const safeStyles = Array.isArray(textStyles) ? textStyles : [];
51
+
48
52
  const cssStyles = {
49
53
  fontSize: size,
50
54
  fontWeight: weight,
@@ -59,13 +63,13 @@ export const Text = (props) => {
59
63
  if (type === "p") cssStyles.marginBottom = "1em";
60
64
 
61
65
  if (
62
- textStyles.includes("underline") &&
63
- textStyles.includes("strikethrough")
66
+ safeStyles.includes("underline") &&
67
+ safeStyles.includes("strikethrough")
64
68
  ) {
65
69
  cssStyles.textDecoration = "underline line-through";
66
- } else if (textStyles.includes("underline")) {
70
+ } else if (safeStyles.includes("underline")) {
67
71
  cssStyles.textDecoration = "underline";
68
- } else if (textStyles.includes("strikethrough")) {
72
+ } else if (safeStyles.includes("strikethrough")) {
69
73
  cssStyles.textDecoration = "line-through";
70
74
  }
71
75
 
@@ -78,7 +82,7 @@ export const Text = (props) => {
78
82
  ...rest,
79
83
  });
80
84
 
81
- const orderedStyles = [...textStyles].reverse();
85
+ const orderedStyles = [...safeStyles].reverse();
82
86
  for (const style of orderedStyles) {
83
87
  const tag = styleMap[style];
84
88
  if (tag && style !== "underline" && style !== "strikethrough") {