flet-box 1.1.1 → 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 +17 -13
- 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 +18 -1
- package/bin/commands/createBundle.js +20 -8
- 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/bundleConfig.js +47 -3
- 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 +248 -30
- package/bin/utils/toolkit.js +191 -0
- package/docs/README.md +26 -1
- package/docs/arquitectura.md +1 -1
- package/docs/cli/README.md +201 -11
- 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/first-app.md +6 -7
- 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 +20 -6
- 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
|
@@ -7,9 +7,16 @@ import { makeReactive } from "./reactivity.js";
|
|
|
7
7
|
import { processProps } from "./processProps.js";
|
|
8
8
|
import { assignProps } from "./assignProps.js";
|
|
9
9
|
import { stackPosition } from "./stackPosition.js";
|
|
10
|
-
import { addChildren,
|
|
10
|
+
import { addChildren, takePropChildren } from "./addChildren.js";
|
|
11
11
|
import { reactiveKeyOf } from "./reactiveValue.js";
|
|
12
12
|
import { registerWidget } from "../tools/useState.js";
|
|
13
|
+
import {
|
|
14
|
+
FAILURE_ATTR,
|
|
15
|
+
clearFailure,
|
|
16
|
+
paintFailure,
|
|
17
|
+
renderFailure,
|
|
18
|
+
reportFailure,
|
|
19
|
+
} from "../utils/errorBoundary.js";
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
22
|
* Auto-binds reactive props to their state key.
|
|
@@ -117,22 +124,29 @@ export function WidgetFactory(tag, customProps = {}) {
|
|
|
117
124
|
// ============================================================
|
|
118
125
|
// 4. APPLY INITIAL PROPS
|
|
119
126
|
// ============================================================
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
// A throw inside a widget must not take the screen with it. The widget
|
|
128
|
+
// paints a placeholder in its own place — the rest of the tree keeps
|
|
129
|
+
// working — and the error is reported with this widget's name.
|
|
130
|
+
try {
|
|
131
|
+
const initialProps = processProps(finalProps, finalTag, widgetName);
|
|
132
|
+
assignProps(widget, initialProps);
|
|
133
|
+
bindReactiveProps(widget, finalProps);
|
|
134
|
+
stackPosition(widget, finalProps);
|
|
124
135
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
if (finalProps.child !== undefined) {
|
|
137
|
+
addChildren(widget, finalProps.child);
|
|
138
|
+
}
|
|
139
|
+
if (finalProps.children !== undefined) {
|
|
140
|
+
addChildren(widget, finalProps.children);
|
|
141
|
+
}
|
|
131
142
|
|
|
132
|
-
|
|
143
|
+
applyEffects(widget);
|
|
133
144
|
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
if (finalProps.ref && typeof finalProps.ref === "function") {
|
|
146
|
+
finalProps.ref(widget);
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
renderFailure(widget, error, { widgetName, phase: "render" });
|
|
136
150
|
}
|
|
137
151
|
|
|
138
152
|
// ============================================================
|
|
@@ -160,6 +174,12 @@ export function WidgetFactory(tag, customProps = {}) {
|
|
|
160
174
|
|
|
161
175
|
updateDepth++;
|
|
162
176
|
try {
|
|
177
|
+
// A previous failure painted a placeholder. This update may be the retry
|
|
178
|
+
// that fixes it, so the placeholder goes before the real content comes
|
|
179
|
+
// back.
|
|
180
|
+
const hadFailure = Boolean(widget._failureBox);
|
|
181
|
+
clearFailure(widget);
|
|
182
|
+
|
|
163
183
|
// Normalize: never allow child AND children simultaneously
|
|
164
184
|
const normalized = { ...newProps };
|
|
165
185
|
// Reserved: `widgetName` labels the widget for Inspector and must never
|
|
@@ -190,9 +210,22 @@ export function WidgetFactory(tag, customProps = {}) {
|
|
|
190
210
|
stackPosition(widget, merged);
|
|
191
211
|
|
|
192
212
|
if (hasChild || hasChildren) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
213
|
+
// The old content comes out first, but it is kept: if attaching the new
|
|
214
|
+
// content throws, the widget goes back to what it had instead of being
|
|
215
|
+
// left empty.
|
|
216
|
+
const previous = takePropChildren(widget);
|
|
217
|
+
try {
|
|
218
|
+
if (hasChild) addChildren(widget, newProps.child);
|
|
219
|
+
if (hasChildren) addChildren(widget, newProps.children);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
addChildren(widget, previous);
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
} else if (hadFailure) {
|
|
225
|
+
// The build failed before the children were attached, so this update is
|
|
226
|
+
// the retry: they go in now.
|
|
227
|
+
if (merged.child !== undefined) addChildren(widget, merged.child);
|
|
228
|
+
if (merged.children !== undefined) addChildren(widget, merged.children);
|
|
196
229
|
}
|
|
197
230
|
|
|
198
231
|
applyEffects(widget);
|
|
@@ -200,6 +233,15 @@ export function WidgetFactory(tag, customProps = {}) {
|
|
|
200
233
|
if (newProps.ref && typeof newProps.ref === "function") {
|
|
201
234
|
newProps.ref(widget);
|
|
202
235
|
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// The widget keeps the content it had: a failed refresh is stale, not
|
|
238
|
+
// empty, and the screen must not blank over it.
|
|
239
|
+
const failure = reportFailure(error, { widgetName, phase: "update" });
|
|
240
|
+
widget.setAttribute(FAILURE_ATTR, "update");
|
|
241
|
+
// A widget with nothing in it is a hole; one that says it failed is not.
|
|
242
|
+
if (!widget.firstChild) {
|
|
243
|
+
paintFailure(widget, failure, { widgetName, phase: "update" });
|
|
244
|
+
}
|
|
203
245
|
} finally {
|
|
204
246
|
updateDepth--;
|
|
205
247
|
}
|
package/src/widgets/Accordion.js
CHANGED
|
@@ -38,7 +38,7 @@ const ACCORDION_PROP_KEYS = [
|
|
|
38
38
|
* @param {HTMLElement|HTMLElement[]} props.children - Content rendered inside the expanded panel.
|
|
39
39
|
* @param {boolean} [props.expanded=false] - Whether the accordion starts in the expanded state.
|
|
40
40
|
* @param {Function} [props.onPress] - Callback fired when the expanded state changes. Receives `(isExpanded: boolean)`.
|
|
41
|
-
* @param {Function} [props.onToggle] - @deprecated Use `onPress` instead.
|
|
41
|
+
* @param {Function} [props.onToggle] - @deprecated Use `onPress` instead.
|
|
42
42
|
* @param {'contained'|'outlined'|'ghost'} [props.variant='contained'] - Visual style variant.
|
|
43
43
|
* @param {number} [props.borderRadius=8] - Corner radius in pixels.
|
|
44
44
|
* @param {string} [props.bgColor=colors.primaryBg] - Background color of the container (used in 'contained' variant).
|
|
@@ -70,6 +70,7 @@ export const Accordion = (props) => {
|
|
|
70
70
|
title,
|
|
71
71
|
children,
|
|
72
72
|
expanded = false,
|
|
73
|
+
/** @deprecated Use `onPress` instead — it receives the same boolean. */
|
|
73
74
|
onToggle,
|
|
74
75
|
onPress,
|
|
75
76
|
variant = "contained", // 'contained', 'outlined', 'ghost'
|
|
@@ -154,6 +155,10 @@ export const Accordion = (props) => {
|
|
|
154
155
|
let iconElement = null;
|
|
155
156
|
let resizeObserver = null;
|
|
156
157
|
let isAnimating = false;
|
|
158
|
+
// Pending animation timer ids — cancelled in onUnmount so the callbacks never
|
|
159
|
+
// fire on a widget that has already left the DOM.
|
|
160
|
+
let animTimer1 = null;
|
|
161
|
+
let animTimer2 = null;
|
|
157
162
|
|
|
158
163
|
// ========== UI UPDATE FUNCTIONS ==========
|
|
159
164
|
|
|
@@ -317,13 +322,15 @@ export const Accordion = (props) => {
|
|
|
317
322
|
|
|
318
323
|
// Animate from 0 → measured height → "auto" (to stay responsive to content changes)
|
|
319
324
|
setHeight("0px");
|
|
320
|
-
setTimeout(() => {
|
|
325
|
+
animTimer1 = setTimeout(() => {
|
|
326
|
+
animTimer1 = null;
|
|
321
327
|
setHeight(targetHeight);
|
|
322
|
-
setTimeout(() => {
|
|
328
|
+
animTimer2 = setTimeout(() => {
|
|
329
|
+
animTimer2 = null;
|
|
323
330
|
setHeight("auto", true);
|
|
324
331
|
contentWrapper.style.overflow = "visible";
|
|
325
332
|
isAnimating = false;
|
|
326
|
-
const expandCb =
|
|
333
|
+
const expandCb = onPress || onToggle;
|
|
327
334
|
if (triggerCallback && expandCb) expandCb(true);
|
|
328
335
|
updateUI();
|
|
329
336
|
}, currentAnimationDuration);
|
|
@@ -351,15 +358,17 @@ export const Accordion = (props) => {
|
|
|
351
358
|
contentWrapper.style.overflow = "hidden";
|
|
352
359
|
setHeight(currentHeight);
|
|
353
360
|
|
|
354
|
-
setTimeout(() => {
|
|
361
|
+
animTimer1 = setTimeout(() => {
|
|
362
|
+
animTimer1 = null;
|
|
355
363
|
setHeight("0px");
|
|
356
364
|
isExpanded = false;
|
|
357
|
-
setTimeout(() => {
|
|
365
|
+
animTimer2 = setTimeout(() => {
|
|
366
|
+
animTimer2 = null;
|
|
358
367
|
contentWrapper.style.display = "none";
|
|
359
368
|
contentWrapper.style.overflow = "";
|
|
360
369
|
contentInner.style.display = "";
|
|
361
370
|
isAnimating = false;
|
|
362
|
-
const collapseCb =
|
|
371
|
+
const collapseCb = onPress || onToggle;
|
|
363
372
|
if (triggerCallback && collapseCb) collapseCb(false);
|
|
364
373
|
updateUI();
|
|
365
374
|
}, currentAnimationDuration);
|
|
@@ -608,6 +617,11 @@ export const Accordion = (props) => {
|
|
|
608
617
|
* Registered through `onUnmount` so it runs on every teardown path.
|
|
609
618
|
*/
|
|
610
619
|
container.onUnmount(() => {
|
|
620
|
+
// Cancel any in-flight animation timers so the callbacks never fire on a
|
|
621
|
+
// widget that has already left the DOM.
|
|
622
|
+
if (animTimer1 !== null) { clearTimeout(animTimer1); animTimer1 = null; }
|
|
623
|
+
if (animTimer2 !== null) { clearTimeout(animTimer2); animTimer2 = null; }
|
|
624
|
+
isAnimating = false;
|
|
611
625
|
if (resizeObserver) resizeObserver.disconnect();
|
|
612
626
|
if (contentWrapper && contentWrapper._cleanup) contentWrapper._cleanup();
|
|
613
627
|
});
|
package/src/widgets/Badge.js
CHANGED
|
@@ -37,6 +37,12 @@ import { toREM } from "../utils/units.js";
|
|
|
37
37
|
* @returns {HTMLElement} The wrapped Stack element, or the child element directly if the badge is hidden.
|
|
38
38
|
*/
|
|
39
39
|
export const Badge = (props) => {
|
|
40
|
+
// Track whether the caller explicitly passed these color props, BEFORE
|
|
41
|
+
// defaults are applied — the theme subscription should only fire when the
|
|
42
|
+
// caller left the colors to the theme.
|
|
43
|
+
const callerPassedBgColor = props.bgColor !== undefined;
|
|
44
|
+
const callerPassedColor = props.color !== undefined;
|
|
45
|
+
|
|
40
46
|
const {
|
|
41
47
|
value,
|
|
42
48
|
child,
|
|
@@ -134,8 +140,11 @@ export const Badge = (props) => {
|
|
|
134
140
|
});
|
|
135
141
|
|
|
136
142
|
// Subscribe to theme changes to keep the badge color in sync when no explicit
|
|
137
|
-
// bgColor/color was provided by the caller
|
|
138
|
-
|
|
143
|
+
// bgColor/color was provided by the caller.
|
|
144
|
+
// FIX [BODY-1]: compare against the caller's original intent, not the
|
|
145
|
+
// post-default values — `bgColor` and `color` both have defaults, so
|
|
146
|
+
// `!bgColor && !color` was always false and the subscription never ran.
|
|
147
|
+
if (!callerPassedBgColor && !callerPassedColor) {
|
|
139
148
|
unsubscribeTheme = subscribeTheme(() => {
|
|
140
149
|
badgeElement.style.backgroundColor = colors.danger;
|
|
141
150
|
});
|
package/src/widgets/Button.js
CHANGED
|
@@ -109,9 +109,17 @@ function buildButtonChildren(text, textColor, fontSize, gap, iconElements) {
|
|
|
109
109
|
const middleRow = [];
|
|
110
110
|
if (iconElements.left) middleRow.push(iconElements.left);
|
|
111
111
|
if (text) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const label = Text({ text, color: textColor, size: fontSize, weight: "bold" });
|
|
113
|
+
// A button label is a label, not a paragraph. Left loose, a long one wraps
|
|
114
|
+
// to three lines inside a fixed-height pill: the text outgrows the padding
|
|
115
|
+
// box and the capsule's rounded ends cut into the words. One line with an
|
|
116
|
+
// ellipsis keeps the shape; the full string stays the accessible name. A
|
|
117
|
+
// block of copy that needs to wrap is a card or a list tile, not a button.
|
|
118
|
+
label.style.whiteSpace = "nowrap";
|
|
119
|
+
label.style.overflow = "hidden";
|
|
120
|
+
label.style.textOverflow = "ellipsis";
|
|
121
|
+
label.style.minWidth = "0";
|
|
122
|
+
middleRow.push(label);
|
|
115
123
|
}
|
|
116
124
|
if (iconElements.right) middleRow.push(iconElements.right);
|
|
117
125
|
|
|
@@ -124,6 +132,10 @@ function buildButtonChildren(text, textColor, fontSize, gap, iconElements) {
|
|
|
124
132
|
alignItems: "center",
|
|
125
133
|
justifyContent: "center",
|
|
126
134
|
gap,
|
|
135
|
+
// Without this the row refuses to shrink below the label's intrinsic
|
|
136
|
+
// width and the ellipsis never engages inside a constrained button.
|
|
137
|
+
minWidth: 0,
|
|
138
|
+
maxWidth: "100%",
|
|
127
139
|
child: middleRow,
|
|
128
140
|
}),
|
|
129
141
|
);
|
package/src/widgets/Carousel.js
CHANGED
|
@@ -279,11 +279,16 @@ export const Carousel = (props) => {
|
|
|
279
279
|
updatePosition(false);
|
|
280
280
|
updateDots();
|
|
281
281
|
|
|
282
|
-
// Autoplay setup
|
|
282
|
+
// Autoplay setup — start from onMount, not from construction.
|
|
283
|
+
// If the widget is built but never inserted into the DOM, setInterval would
|
|
284
|
+
// run forever because lifecycle's orphan-prune never fires unmount on a node
|
|
285
|
+
// that was never mounted. Hover listeners must follow the same path.
|
|
283
286
|
if (autoPlay) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
+
container.onMount(() => {
|
|
288
|
+
startAutoPlay();
|
|
289
|
+
container.addEventListener("mouseenter", stopAutoPlay);
|
|
290
|
+
container.addEventListener("mouseleave", startAutoPlay);
|
|
291
|
+
});
|
|
287
292
|
}
|
|
288
293
|
|
|
289
294
|
// Public methods
|
|
@@ -295,6 +300,9 @@ export const Carousel = (props) => {
|
|
|
295
300
|
// Cleanup
|
|
296
301
|
container.onUnmount(() => {
|
|
297
302
|
stopAutoPlay();
|
|
303
|
+
// Remove hover listeners regardless of whether onMount ran
|
|
304
|
+
container.removeEventListener("mouseenter", stopAutoPlay);
|
|
305
|
+
container.removeEventListener("mouseleave", startAutoPlay);
|
|
298
306
|
});
|
|
299
307
|
|
|
300
308
|
return container;
|
package/src/widgets/Chart.js
CHANGED
|
@@ -104,7 +104,9 @@ export const Chart = (props) => {
|
|
|
104
104
|
...rest
|
|
105
105
|
} = props;
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
// FIX [CHART-2]: returning null for empty data prevents updateData() from
|
|
108
|
+
// ever working — the container doesn't exist. Render an empty canvas instead.
|
|
109
|
+
// if (!data.length) return null;
|
|
108
110
|
|
|
109
111
|
let canvasRef = null;
|
|
110
112
|
let context = null;
|
|
@@ -230,7 +232,10 @@ export const Chart = (props) => {
|
|
|
230
232
|
ctx.fillStyle = barColor;
|
|
231
233
|
ctx.fillRect(x, Math.min(y, baselineY), barWidth, barHeight);
|
|
232
234
|
|
|
233
|
-
|
|
235
|
+
// FIX [CHART-2]: `showValues` was gated on `labels[index]` — so values
|
|
236
|
+
// were never drawn when no labels array was provided. Show values
|
|
237
|
+
// independently of labels.
|
|
238
|
+
if (showValues) {
|
|
234
239
|
ctx.fillStyle = textColor;
|
|
235
240
|
ctx.font = "8px sans-serif";
|
|
236
241
|
ctx.fillText(value, x + barWidth / 2 - 5, y - 2);
|
package/src/widgets/Checkbox.js
CHANGED
|
@@ -11,6 +11,7 @@ import { TAP_MINIMUM, LABEL_TEXT } from "../tools/index.js";
|
|
|
11
11
|
export const Checkbox = (props) => {
|
|
12
12
|
const {
|
|
13
13
|
checked = false,
|
|
14
|
+
/** @deprecated Use `onPress` instead — it receives the same boolean. */
|
|
14
15
|
onCheck,
|
|
15
16
|
onPress: onPressProp,
|
|
16
17
|
disabled = false,
|
|
@@ -22,8 +23,6 @@ export const Checkbox = (props) => {
|
|
|
22
23
|
...rest
|
|
23
24
|
} = props;
|
|
24
25
|
|
|
25
|
-
/** @deprecated Use onPress instead */
|
|
26
|
-
// onCheck kept as alias for backward compatibility
|
|
27
26
|
const onPress = onPressProp || onCheck;
|
|
28
27
|
|
|
29
28
|
let internalChecked = checked;
|
package/src/widgets/Chip.js
CHANGED
|
@@ -10,6 +10,7 @@ import { Row } from "./Row.js";
|
|
|
10
10
|
import { Text } from "./Text.js";
|
|
11
11
|
import { Icon } from "./Icon.js";
|
|
12
12
|
import { colors } from "../utils/themes.js";
|
|
13
|
+
import { readableInk } from "../utils/readableInk.js";
|
|
13
14
|
import { PILL, TAP_MINIMUM } from "../tools/index.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -40,6 +41,12 @@ export const Chip = (props) => {
|
|
|
40
41
|
onDelete,
|
|
41
42
|
variant = "filled",
|
|
42
43
|
color = colors.primary,
|
|
44
|
+
// The chip's own plate. It has to be taken out of `props` here: left in, it
|
|
45
|
+
// travels through `...rest` onto the outer wrapper — and the wrapper is the
|
|
46
|
+
// 40-unit tap target with no radius, so a chip that named its background
|
|
47
|
+
// painted a rectangle of it behind the pill.
|
|
48
|
+
bgColor,
|
|
49
|
+
backgroundColor,
|
|
43
50
|
textColor,
|
|
44
51
|
borderColor,
|
|
45
52
|
borderRadius = PILL,
|
|
@@ -67,19 +74,25 @@ export const Chip = (props) => {
|
|
|
67
74
|
? { paddingVertical: padding, paddingHorizontal: padding * 2 }
|
|
68
75
|
: { padding };
|
|
69
76
|
|
|
70
|
-
// Derive background, text, and border colors from the variant
|
|
71
|
-
|
|
77
|
+
// Derive background, text, and border colors from the variant. A filled chip
|
|
78
|
+
// takes the plate its caller named; an outlined one keeps the variant's
|
|
79
|
+
// contract and stays transparent, the same answer `Button` gives when a
|
|
80
|
+
// `bgColor` is handed to `variant: "outlined"`. Either way the name is consumed
|
|
81
|
+
// here, so it never reaches the wrapper and paints a square plate.
|
|
82
|
+
const plate = bgColor || backgroundColor;
|
|
83
|
+
let pillColor, txtColor, brdColor;
|
|
72
84
|
|
|
73
85
|
if (variant === "outlined") {
|
|
74
|
-
|
|
86
|
+
pillColor = "transparent";
|
|
75
87
|
txtColor = textColor || color;
|
|
76
88
|
brdColor = borderColor || color;
|
|
77
89
|
} else {
|
|
78
|
-
|
|
79
|
-
// The label sits on the
|
|
80
|
-
//
|
|
81
|
-
// would vanish
|
|
82
|
-
|
|
90
|
+
pillColor = plate || color;
|
|
91
|
+
// The label sits on the plate, so its ink follows the plate. On the default
|
|
92
|
+
// primary that is the theme's ink — in dark mode a primary chip is near-white
|
|
93
|
+
// and white words would vanish — and once the caller names a pale plate,
|
|
94
|
+
// picking the readable ink is what stops the label disappearing again.
|
|
95
|
+
txtColor = textColor || (plate ? readableInk(plate, colors.text) : colors.onPrimary);
|
|
83
96
|
brdColor = "transparent";
|
|
84
97
|
}
|
|
85
98
|
|
|
@@ -131,7 +144,7 @@ export const Chip = (props) => {
|
|
|
131
144
|
const chipContent = Row({
|
|
132
145
|
alignItems: "center",
|
|
133
146
|
gap: gap,
|
|
134
|
-
backgroundColor:
|
|
147
|
+
backgroundColor: pillColor,
|
|
135
148
|
border: variant === "outlined" ? `1px solid ${brdColor}` : "none",
|
|
136
149
|
borderRadius: borderRadius,
|
|
137
150
|
...chipPadding,
|
|
@@ -175,7 +188,8 @@ export const Chip = (props) => {
|
|
|
175
188
|
if (variant === "filled") {
|
|
176
189
|
chip.style.opacity = "1";
|
|
177
190
|
} else if (chipContent) {
|
|
178
|
-
|
|
191
|
+
// Leaving restores the chip's own plate, which is not always transparent.
|
|
192
|
+
chipContent.style.backgroundColor = pillColor;
|
|
179
193
|
}
|
|
180
194
|
});
|
|
181
195
|
}
|
|
@@ -125,9 +125,11 @@ export const CircularBar = (props) => {
|
|
|
125
125
|
|
|
126
126
|
const getFormattedValue = () => {
|
|
127
127
|
if (customValueFormatter) return customValueFormatter(currentValue, max);
|
|
128
|
+
// Guard: max === 0 would produce Infinity or NaN in every branch.
|
|
129
|
+
const safeMax = max > 0 ? max : 1;
|
|
128
130
|
if (valueFormat === "percent") {
|
|
129
131
|
const percent =
|
|
130
|
-
Math.round((currentValue /
|
|
132
|
+
Math.round((currentValue / safeMax) * 100 * Math.pow(10, valueDecimals)) /
|
|
131
133
|
Math.pow(10, valueDecimals);
|
|
132
134
|
return `${valuePrefix}${percent}${valueSuffix}`;
|
|
133
135
|
}
|
|
@@ -243,7 +245,9 @@ export const CircularBar = (props) => {
|
|
|
243
245
|
const drawStatic = () => {
|
|
244
246
|
if (!ctx || !canvas) return;
|
|
245
247
|
currentValue = value;
|
|
246
|
-
|
|
248
|
+
// Guard: max === 0 would produce Infinity radians → canvas draws nothing.
|
|
249
|
+
const safeMax = max > 0 ? max : 1;
|
|
250
|
+
const angle = (value / safeMax) * Math.PI * 2;
|
|
247
251
|
ctx.clearRect(0, 0, currentWidth, currentWidth);
|
|
248
252
|
drawBackground();
|
|
249
253
|
drawProgress(angle);
|
|
@@ -261,7 +265,9 @@ export const CircularBar = (props) => {
|
|
|
261
265
|
const elapsed = timestamp - startTime;
|
|
262
266
|
const progress = Math.min(1, elapsed / animationDuration);
|
|
263
267
|
currentValue = progress * value;
|
|
264
|
-
|
|
268
|
+
// Guard: max === 0 → Infinity radians.
|
|
269
|
+
const safeMax = max > 0 ? max : 1;
|
|
270
|
+
const angle = (currentValue / safeMax) * Math.PI * 2;
|
|
265
271
|
ctx.clearRect(0, 0, currentWidth, currentWidth);
|
|
266
272
|
drawBackground();
|
|
267
273
|
drawProgress(angle);
|
|
@@ -289,9 +289,11 @@ export const CircularChart = (props) => {
|
|
|
289
289
|
ctx.font = `${labelSize}px system-ui`;
|
|
290
290
|
ctx.fillStyle = labelInk(color);
|
|
291
291
|
ctx.shadowBlur = 0;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
// FIX [CC-2]: percent was computed as value / (end-start) * 100, which
|
|
293
|
+
// divides by the slice's radian angle (~0.002 for small slices) and gives
|
|
294
|
+
// nonsense like 4775%. Divide by the total instead.
|
|
295
|
+
const total = data.reduce((s, d) => s + (d?.value || 0), 0);
|
|
296
|
+
const percent = total > 0 ? Math.round((slice.value / total) * 100) : 0;
|
|
295
297
|
ctx.fillText(slice.original?.label || `${percent}%`, x, y);
|
|
296
298
|
}
|
|
297
299
|
};
|
package/src/widgets/Container.js
CHANGED
|
@@ -13,6 +13,7 @@ export const Container = (props = {}) => {
|
|
|
13
13
|
overflow = "auto",
|
|
14
14
|
boxSizing = "border-box",
|
|
15
15
|
bgColor = colors.surface,
|
|
16
|
+
expand,
|
|
16
17
|
flex,
|
|
17
18
|
flexGrow,
|
|
18
19
|
flexShrink,
|
|
@@ -28,6 +29,10 @@ export const Container = (props = {}) => {
|
|
|
28
29
|
? { flexShrink: 0 }
|
|
29
30
|
: { flex, flexGrow, flexShrink };
|
|
30
31
|
|
|
32
|
+
// FIX [CONTAINER-1]: `expand` was documented but never applied.
|
|
33
|
+
// It maps to `flex: 1` so the container fills available space in its parent.
|
|
34
|
+
const expandProps = expand ? { flex: 1, flexShrink: 1 } : {};
|
|
35
|
+
|
|
31
36
|
return WidgetFactory({
|
|
32
37
|
tag: "div",
|
|
33
38
|
widgetName: "Container",
|
|
@@ -37,6 +42,7 @@ export const Container = (props = {}) => {
|
|
|
37
42
|
boxSizing: boxSizing,
|
|
38
43
|
backgroundColor: bgColor,
|
|
39
44
|
...flexDefaults,
|
|
45
|
+
...expandProps,
|
|
40
46
|
...rest,
|
|
41
47
|
});
|
|
42
48
|
};
|
package/src/widgets/Divider.js
CHANGED
|
@@ -7,7 +7,9 @@ import { colors } from "../utils/themes.js";
|
|
|
7
7
|
|
|
8
8
|
export const Divider = (props = {}) => {
|
|
9
9
|
const {
|
|
10
|
-
|
|
10
|
+
// FIX [DIVIDER-1]: default was `colors.surface` — same color as the
|
|
11
|
+
// background — making every divider invisible unless explicitly colored.
|
|
12
|
+
color = colors.border,
|
|
11
13
|
thickness = 1,
|
|
12
14
|
margin = 16,
|
|
13
15
|
orientation = "horizontal",
|
package/src/widgets/DraggBox.js
CHANGED
|
@@ -110,6 +110,15 @@ export const DraggBox = (props) => {
|
|
|
110
110
|
if (onDragEnd) onDragEnd(e, data);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
// FIX [DRAGBOX-1]: if the widget is unmounted while a drag is in progress,
|
|
114
|
+
// `dragend` never fires and the clone stays in document.body forever.
|
|
115
|
+
wrapper.onUnmount(() => {
|
|
116
|
+
if (dragClone && dragClone.parentNode) {
|
|
117
|
+
dragClone.parentNode.removeChild(dragClone);
|
|
118
|
+
dragClone = null;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
113
122
|
return wrapper;
|
|
114
123
|
};
|
|
115
124
|
|
package/src/widgets/Dropdown.js
CHANGED
|
@@ -60,8 +60,8 @@ export const Dropdown = (inputProps = {}) => {
|
|
|
60
60
|
const {
|
|
61
61
|
options = [],
|
|
62
62
|
value = null,
|
|
63
|
+
/** @deprecated Use `onPress` instead — it receives the same value. */
|
|
63
64
|
onChange,
|
|
64
|
-
/** @deprecated Use onPress instead */
|
|
65
65
|
onPress: onPressProp,
|
|
66
66
|
placeholder = "Select...",
|
|
67
67
|
disabled = false,
|
|
@@ -276,7 +276,7 @@ export const Dropdown = (inputProps = {}) => {
|
|
|
276
276
|
alignItems: "center",
|
|
277
277
|
gap: 8,
|
|
278
278
|
// minWidth: 0 para que un texto largo ENCOJA y no empuje al icono: sin
|
|
279
|
-
//
|
|
279
|
+
// el span cannot shrink below its content width and the
|
|
280
280
|
// expand_more sale del selector.
|
|
281
281
|
flex: 1,
|
|
282
282
|
minWidth: 0,
|
|
@@ -286,7 +286,7 @@ export const Dropdown = (inputProps = {}) => {
|
|
|
286
286
|
// width "100%" por defecto, y con eso este bloque se apropiaba de casi
|
|
287
287
|
// todo el ancho del selector: al no llevar justifyContent, el glifo
|
|
288
288
|
// quedaba anclado a su borde izquierdo, pegado al texto, en vez de
|
|
289
|
-
//
|
|
289
|
+
// the right side. flexShrink 0 additionally prevents squeezing when
|
|
290
290
|
// el texto es largo.
|
|
291
291
|
Row({
|
|
292
292
|
alignItems: "center",
|
package/src/widgets/DroppBox.js
CHANGED
|
@@ -149,6 +149,11 @@ export const DroppBox = (props) => {
|
|
|
149
149
|
if (disabled) return;
|
|
150
150
|
e.preventDefault();
|
|
151
151
|
|
|
152
|
+
// FIX [DROPBOX-1]: `dragleave` fires when the pointer moves onto a child
|
|
153
|
+
// element inside the drop zone. `relatedTarget` is the element being entered
|
|
154
|
+
// next; if it is still inside this wrapper the drag hasn't really left.
|
|
155
|
+
if (wrapper.contains(e.relatedTarget)) return;
|
|
156
|
+
|
|
152
157
|
if (isActive) {
|
|
153
158
|
isActive = false;
|
|
154
159
|
removeActiveStyles();
|
|
@@ -47,7 +47,7 @@ export const FloatingActionButton = (props) => {
|
|
|
47
47
|
children.push(
|
|
48
48
|
Text({
|
|
49
49
|
// FIX: `value` es special y `applySpecial` solo lo aplica en inputs
|
|
50
|
-
//
|
|
50
|
+
// a span did nothing and the label stayed empty.
|
|
51
51
|
text: label,
|
|
52
52
|
size: 14,
|
|
53
53
|
fontWeight: "500",
|
package/src/widgets/Icon.js
CHANGED
|
@@ -13,6 +13,10 @@ export const Icon = (props) => {
|
|
|
13
13
|
name,
|
|
14
14
|
size = 24,
|
|
15
15
|
color = colors.textSecondary,
|
|
16
|
+
// FIX [ICON-1]: extract className from rest so it can be merged with
|
|
17
|
+
// `material-icons` instead of replacing it. An app that passes className
|
|
18
|
+
// to add a custom style would otherwise break the ligature entirely.
|
|
19
|
+
className: extraClassName,
|
|
16
20
|
...rest
|
|
17
21
|
} = props;
|
|
18
22
|
const iconName = icon || name || "";
|
|
@@ -35,7 +39,7 @@ export const Icon = (props) => {
|
|
|
35
39
|
return WidgetFactory({
|
|
36
40
|
tag: "span",
|
|
37
41
|
widgetName: "Icon",
|
|
38
|
-
className: "material-icons",
|
|
42
|
+
className: extraClassName ? `material-icons ${extraClassName}` : "material-icons",
|
|
39
43
|
text: iconName, // ✅ CHANGED: text instead of textContent
|
|
40
44
|
fontSize: toREM(size),
|
|
41
45
|
color: color,
|
|
@@ -35,11 +35,13 @@ export const InstallButton = (props) => {
|
|
|
35
35
|
if (deferredPrompt) {
|
|
36
36
|
deferredPrompt.prompt();
|
|
37
37
|
const { outcome } = await deferredPrompt.userChoice;
|
|
38
|
+
deferredPrompt = null;
|
|
38
39
|
if (outcome === "accepted") {
|
|
39
|
-
|
|
40
|
+
// FIX [INSTALL-1]: do NOT call onInstalled here — the browser fires
|
|
41
|
+
// `appinstalled` right after acceptance, and handleAppInstalled calls it
|
|
42
|
+
// there. Calling it in both places caused the double-fire.
|
|
40
43
|
hideButton();
|
|
41
44
|
}
|
|
42
|
-
deferredPrompt = null;
|
|
43
45
|
}
|
|
44
46
|
if (onClick) onClick();
|
|
45
47
|
};
|