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.
- package/AGENTS_FOR_CODE_GEN.md +284 -0
- package/CONTRIBUTING.md +238 -0
- package/README.md +2 -2
- package/bin/cli.js +88 -1
- package/bin/commands/auditFix.js +181 -27
- package/bin/commands/checkApi.js +14 -3
- package/bin/commands/checkStructure.js +368 -0
- package/bin/commands/checkSyntax.js +293 -0
- package/bin/commands/checkUi.js +666 -14
- package/bin/commands/create.js +14 -0
- package/bin/commands/createBundle.js +2 -2
- package/bin/commands/metrics.js +448 -0
- package/bin/commands/release.js +18 -0
- package/bin/commands/report.js +849 -0
- package/bin/commands/tools.js +76 -0
- package/bin/commands/unpack.js +39 -11
- package/bin/commands/verify.js +879 -0
- package/bin/mcp.js +280 -0
- package/bin/utils/contract.js +27 -0
- package/bin/utils/dtsProps.js +5 -1
- package/bin/utils/migrationGuide.js +146 -0
- package/bin/utils/moduleExports.js +9 -5
- package/bin/utils/templates.js +225 -18
- package/bin/utils/toolkit.js +191 -0
- package/docs/README.md +5 -1
- package/docs/arquitectura.md +1 -1
- package/docs/cli/README.md +168 -2
- package/docs/guides/README.md +6 -0
- package/docs/guides/accessibility.md +2 -1
- package/docs/guides/ai-toolkit.md +166 -0
- package/docs/guides/compatibility.md +1 -1
- package/docs/guides/migration.md +92 -0
- package/docs/guides/minification-and-protection.md +2 -1
- package/docs/guides/mobile-and-platforms.md +1 -1
- package/docs/guides/online-updates.md +2 -0
- package/docs/server/deployment.md +1 -1
- package/docs/tools/styling.md +2 -2
- package/docs/tools/theme.md +139 -4
- package/docs/widget/Accordion.md +74 -8
- package/docs/widget/AdMob.md +68 -3
- package/docs/widget/AdSense.md +67 -2
- package/docs/widget/AlertDialog.md +1 -1
- package/docs/widget/AnimatedBox.md +80 -39
- package/docs/widget/AnimatedText.md +74 -18
- package/docs/widget/Audio.md +73 -14
- package/docs/widget/Avatar.md +69 -4
- package/docs/widget/Badge.md +68 -3
- package/docs/widget/BottomSheet.md +2 -2
- package/docs/widget/Button.md +69 -3
- package/docs/widget/Card.md +69 -4
- package/docs/widget/Carousel.md +67 -2
- package/docs/widget/Chart.md +67 -2
- package/docs/widget/Checkbox.md +74 -8
- package/docs/widget/Chip.md +68 -3
- package/docs/widget/CircularBar.md +74 -15
- package/docs/widget/CircularChart.md +71 -10
- package/docs/widget/CodeViewer.md +67 -2
- package/docs/widget/Column.md +68 -3
- package/docs/widget/Container.md +68 -3
- package/docs/widget/DataTable.md +67 -2
- package/docs/widget/Divider.md +67 -2
- package/docs/widget/DraggBox.md +67 -2
- package/docs/widget/Dropdown.md +76 -10
- package/docs/widget/DroppBox.md +67 -2
- package/docs/widget/FloatingActionButton.md +68 -3
- package/docs/widget/GridView.md +67 -2
- package/docs/widget/Icon.md +71 -5
- package/docs/widget/Image.md +68 -3
- package/docs/widget/Input.md +68 -3
- package/docs/widget/Inspector.md +2 -2
- package/docs/widget/InstallButton.md +73 -13
- package/docs/widget/ListTile.md +69 -3
- package/docs/widget/ListView.md +67 -2
- package/docs/widget/Maps.md +68 -3
- package/docs/widget/Markdown.md +74 -15
- package/docs/widget/MatrixRain.md +72 -11
- package/docs/widget/Menu.md +209 -0
- package/docs/widget/Modal.md +3 -3
- package/docs/widget/Pagination.md +68 -3
- package/docs/widget/ParallaxBox.md +72 -12
- package/docs/widget/ProgressBar.md +69 -4
- package/docs/widget/QRCode.md +67 -2
- package/docs/widget/README.md +54 -50
- package/docs/widget/Radio.md +76 -10
- package/docs/widget/Rating.md +70 -6
- package/docs/widget/Row.md +68 -3
- package/docs/widget/Skeleton.md +68 -3
- package/docs/widget/Slider.md +75 -9
- package/docs/widget/SnackBar.md +2 -2
- package/docs/widget/Stack.md +68 -3
- package/docs/widget/Stepper.md +68 -3
- package/docs/widget/Switch.md +74 -8
- package/docs/widget/Text.md +71 -4
- package/docs/widget/ThemePicker.md +197 -0
- package/docs/widget/Tooltip.md +72 -7
- package/docs/widget/TreeView.md +70 -10
- package/docs/widget/Video.md +71 -10
- package/package.json +10 -3
- package/skills/fletbox-design/SKILL.md +89 -5
- package/skills/fletbox-motion/SKILL.md +1 -1
- package/skills/fletbox-rules/SKILL.md +56 -4
- package/src/core/runApp.js +12 -0
- package/src/index.d.ts +374 -101
- package/src/index.js +9 -0
- package/src/navigations/AdaptiveScaffold.js +2 -2
- package/src/navigations/BottomNavigation.js +1 -1
- package/src/navigations/Scaffold.js +18 -6
- package/src/tools/useState.js +11 -4
- package/src/utils/colors.js +275 -0
- package/src/utils/errorBoundary.js +288 -0
- package/src/utils/index.js +10 -2
- package/src/utils/stackPosition.js +39 -24
- package/src/utils/syntaxHighlight.js +4 -1
- package/src/utils/themes.js +279 -3
- package/src/widget-factory/addChildren.js +17 -10
- package/src/widget-factory/assignProps.js +18 -6
- package/src/widget-factory/createWidget.js +7 -0
- package/src/widget-factory/deprecations.js +77 -0
- package/src/widget-factory/translateProps.js +11 -1
- package/src/widget-factory/warnings.js +1 -27
- package/src/widget-factory/widgetFactory.js +59 -17
- package/src/widgets/Accordion.js +21 -7
- package/src/widgets/Badge.js +11 -2
- package/src/widgets/Button.js +15 -3
- package/src/widgets/Carousel.js +12 -4
- package/src/widgets/Chart.js +7 -2
- package/src/widgets/Checkbox.js +1 -2
- package/src/widgets/Chip.js +24 -10
- package/src/widgets/CircularBar.js +9 -3
- package/src/widgets/CircularChart.js +5 -3
- package/src/widgets/Container.js +6 -0
- package/src/widgets/Divider.js +3 -1
- package/src/widgets/DraggBox.js +9 -0
- package/src/widgets/Dropdown.js +3 -3
- package/src/widgets/DroppBox.js +5 -0
- package/src/widgets/FloatingActionButton.js +1 -1
- package/src/widgets/Icon.js +5 -1
- package/src/widgets/InstallButton.js +4 -2
- package/src/widgets/Markdown.js +171 -103
- package/src/widgets/Menu.js +340 -0
- package/src/widgets/Modal.js +7 -1
- package/src/widgets/Pagination.js +10 -5
- package/src/widgets/ProgressBar.js +5 -4
- package/src/widgets/QRCode.js +4 -0
- package/src/widgets/Radio.js +1 -2
- package/src/widgets/Rating.js +9 -0
- package/src/widgets/Slider.js +4 -1
- package/src/widgets/SnackBar.js +2 -2
- package/src/widgets/Switch.js +1 -2
- package/src/widgets/Text.js +9 -5
- package/src/widgets/ThemePicker.js +234 -0
- package/src/widgets/TreeView.js +2 -1
- package/src/widgets/index.js +2 -0
- 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;
|
package/src/widgets/Modal.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
47
|
-
let end = Math.min(totalPages, start +
|
|
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 <
|
|
50
|
-
start = Math.max(1, end -
|
|
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
|
-
|
|
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
|
-
//
|
|
126
|
-
//
|
|
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.
|
package/src/widgets/QRCode.js
CHANGED
|
@@ -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
|
package/src/widgets/Radio.js
CHANGED
|
@@ -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;
|
package/src/widgets/Rating.js
CHANGED
|
@@ -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),
|
package/src/widgets/Slider.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/widgets/SnackBar.js
CHANGED
|
@@ -10,7 +10,7 @@ import { readableInk } from "../utils/readableInk.js";
|
|
|
10
10
|
import { toREM } from "../utils/units.js";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* SnackBar -
|
|
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
|
-
//
|
|
87
|
+
// Creates the main container
|
|
88
88
|
const snackbar = WidgetFactory({
|
|
89
89
|
tag: "div",
|
|
90
90
|
position: "fixed",
|
package/src/widgets/Switch.js
CHANGED
|
@@ -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
|
package/src/widgets/Text.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
66
|
+
safeStyles.includes("underline") &&
|
|
67
|
+
safeStyles.includes("strikethrough")
|
|
64
68
|
) {
|
|
65
69
|
cssStyles.textDecoration = "underline line-through";
|
|
66
|
-
} else if (
|
|
70
|
+
} else if (safeStyles.includes("underline")) {
|
|
67
71
|
cssStyles.textDecoration = "underline";
|
|
68
|
-
} else if (
|
|
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 = [...
|
|
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") {
|