flet-box 1.0.4 → 1.0.6
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/README.md +10 -0
- package/bin/cli.js +23 -7
- package/bin/commands/checkApi.js +761 -0
- package/bin/commands/create.js +77 -5
- package/bin/utils/dtsProps.js +191 -0
- package/bin/utils/moduleExports.js +165 -0
- package/bin/utils/neutralise.js +64 -0
- package/bin/utils/templates.js +366 -27
- package/docs/CONTRIBUTING.md +156 -0
- package/docs/README.md +68 -0
- package/docs/arquitectura.md +164 -0
- package/docs/cli/README.md +542 -0
- package/docs/guides/README.md +33 -0
- package/docs/guides/app-templates.md +197 -0
- package/docs/guides/compatibility.md +95 -0
- package/docs/guides/embedding.md +284 -0
- package/docs/guides/first-app.md +208 -0
- package/docs/guides/frontend-services.md +171 -0
- package/docs/guides/minification-and-protection.md +260 -0
- package/docs/guides/mobile-and-platforms.md +439 -0
- package/docs/guides/online-updates.md +196 -0
- package/docs/guides/router.md +282 -0
- package/docs/guides/state.md +283 -0
- package/docs/guides/syntax-philosophy.md +150 -0
- package/docs/guides/utilities.md +241 -0
- package/docs/guides/widget-structure.md +163 -0
- package/docs/server/README.md +68 -0
- package/docs/server/START_HERE.md +121 -0
- package/docs/server/api.md +225 -0
- package/docs/server/data-services.md +300 -0
- package/docs/server/deployment.md +113 -0
- package/docs/server/security.md +196 -0
- package/docs/services/README.md +79 -0
- package/docs/services/RamStore.md +84 -0
- package/docs/services/Session.md +84 -0
- package/docs/services/Storage.md +88 -0
- package/docs/services/http.md +86 -0
- package/docs/tools/README.md +138 -0
- package/docs/tools/animation.md +87 -0
- package/docs/tools/async-ids-log.md +80 -0
- package/docs/tools/device.md +132 -0
- package/docs/tools/introspection.md +67 -0
- package/docs/tools/lists.md +96 -0
- package/docs/tools/markdown.md +79 -0
- package/docs/tools/reactivity.md +288 -0
- package/docs/tools/state.md +103 -0
- package/docs/tools/styling.md +92 -0
- package/docs/tools/text-and-time.md +89 -0
- package/docs/tools/theme.md +102 -0
- package/docs/tools/validation.md +75 -0
- package/docs/tools/visual-effects.md +84 -0
- package/docs/widget/Accordion.md +144 -0
- package/docs/widget/AdMob.md +144 -0
- package/docs/widget/AdSense.md +133 -0
- package/docs/widget/AdaptiveScaffold.md +105 -0
- package/docs/widget/AlertDialog.md +103 -0
- package/docs/widget/AnimatedBox.md +130 -0
- package/docs/widget/AnimatedText.md +123 -0
- package/docs/widget/AppBar.md +118 -0
- package/docs/widget/Audio.md +135 -0
- package/docs/widget/Avatar.md +119 -0
- package/docs/widget/Badge.md +128 -0
- package/docs/widget/BottomNavigation.md +116 -0
- package/docs/widget/BottomSheet.md +125 -0
- package/docs/widget/Button.md +137 -0
- package/docs/widget/COMMON_PROPS.md +150 -0
- package/docs/widget/Card.md +125 -0
- package/docs/widget/Carousel.md +151 -0
- package/docs/widget/Chart.md +151 -0
- package/docs/widget/Checkbox.md +118 -0
- package/docs/widget/Chip.md +126 -0
- package/docs/widget/CircularBar.md +168 -0
- package/docs/widget/CircularChart.md +164 -0
- package/docs/widget/CodeViewer.md +130 -0
- package/docs/widget/CollapsibleSideBar.md +112 -0
- package/docs/widget/Column.md +129 -0
- package/docs/widget/Container.md +136 -0
- package/docs/widget/DataTable.md +150 -0
- package/docs/widget/Divider.md +109 -0
- package/docs/widget/DraggBox.md +129 -0
- package/docs/widget/Drawer.md +135 -0
- package/docs/widget/DrawerItem.md +98 -0
- package/docs/widget/Dropdown.md +139 -0
- package/docs/widget/DroppBox.md +152 -0
- package/docs/widget/FloatingActionButton.md +120 -0
- package/docs/widget/GridView.md +140 -0
- package/docs/widget/Icon.md +117 -0
- package/docs/widget/Image.md +125 -0
- package/docs/widget/Input.md +151 -0
- package/docs/widget/Inspector.md +92 -0
- package/docs/widget/InstallButton.md +128 -0
- package/docs/widget/ListTile.md +132 -0
- package/docs/widget/ListView.md +138 -0
- package/docs/widget/Markdown.md +175 -0
- package/docs/widget/MatrixRain.md +122 -0
- package/docs/widget/Modal.md +129 -0
- package/docs/widget/Pagination.md +135 -0
- package/docs/widget/ParallaxBox.md +128 -0
- package/docs/widget/ProgressBar.md +130 -0
- package/docs/widget/QRCode.md +117 -0
- package/docs/widget/README.md +188 -0
- package/docs/widget/Radio.md +123 -0
- package/docs/widget/Rating.md +131 -0
- package/docs/widget/Row.md +131 -0
- package/docs/widget/START_HERE.md +146 -0
- package/docs/widget/Scaffold.md +152 -0
- package/docs/widget/Skeleton.md +130 -0
- package/docs/widget/Slider.md +146 -0
- package/docs/widget/SnackBar.md +102 -0
- package/docs/widget/Stack.md +128 -0
- package/docs/widget/Stepper.md +145 -0
- package/docs/widget/Switch.md +118 -0
- package/docs/widget/Tabs.md +124 -0
- package/docs/widget/Text.md +133 -0
- package/docs/widget/Tooltip.md +145 -0
- package/docs/widget/TreeView.md +176 -0
- package/docs/widget/Video.md +127 -0
- package/docs/widget/_template.md +61 -0
- package/package.json +3 -2
- package/src/animations/animate.js +180 -70
- package/src/animations/index.js +1 -0
- package/src/create-index.sh +1 -1
- package/src/index.d.ts +45 -22
- package/src/index.js +1 -0
- package/src/navigations/AdaptiveScaffold.js +11 -2
- package/src/navigations/BottomNavigation.js +10 -3
- package/src/navigations/Tabs.js +6 -2
- package/src/tools/useState.js +54 -63
- package/src/widget-factory/assignProps.js +23 -1
- package/src/widget-factory/processProps.js +10 -3
- package/src/widget-factory/reactiveValue.js +43 -18
- package/src/widget-factory/translateProps.js +777 -901
- package/src/widgets/AlertDialog.js +7 -5
- package/src/widgets/Checkbox.js +9 -7
- package/src/widgets/CircularBar.js +12 -1
- package/src/widgets/FloatingActionButton.js +4 -7
- package/src/widgets/Image.js +5 -7
- package/src/widgets/Input.js +92 -0
- package/src/widgets/ListView.js +73 -29
- package/src/widgets/Pagination.js +6 -3
- package/src/widgets/ProgressBar.js +16 -13
- package/src/widgets/Radio.js +6 -7
- package/src/widgets/Slider.js +23 -4
- package/src/widgets/Stack.js +36 -3
- package/src/widgets/Stepper.js +4 -3
- package/src/widgets/Switch.js +3 -4
- package/src/widgets/TreeView.js +0 -1
|
@@ -40,7 +40,7 @@ export const AlertDialog = (props) => {
|
|
|
40
40
|
onConfirm,
|
|
41
41
|
onCancel,
|
|
42
42
|
onClose,
|
|
43
|
-
variant = "normal",
|
|
43
|
+
variant = "normal",
|
|
44
44
|
showCancel = true,
|
|
45
45
|
confirmText = "Accept",
|
|
46
46
|
cancelText = "Cancel",
|
|
@@ -83,14 +83,16 @@ export const AlertDialog = (props) => {
|
|
|
83
83
|
children: [
|
|
84
84
|
icon,
|
|
85
85
|
Text({
|
|
86
|
-
|
|
86
|
+
// FIX: `value` no aplica a <span> → título quedaba vacío. Usar `text`.
|
|
87
|
+
text: title,
|
|
87
88
|
size: 24,
|
|
88
89
|
fontWeight: "bold",
|
|
89
90
|
color: colors.text,
|
|
90
91
|
textAlign: "center",
|
|
91
92
|
}),
|
|
92
93
|
Text({
|
|
93
|
-
|
|
94
|
+
// FIX: idem para el mensaje.
|
|
95
|
+
text: message,
|
|
94
96
|
size: 18,
|
|
95
97
|
fontWeight: "bold",
|
|
96
98
|
color: colors.textSecondary,
|
|
@@ -134,7 +136,7 @@ export const AlertDialog = (props) => {
|
|
|
134
136
|
|
|
135
137
|
let modal = null;
|
|
136
138
|
// Placeholder until the modal is created below — avoids a temporal dead-zone
|
|
137
|
-
let closeModal = () => {};
|
|
139
|
+
let closeModal = () => { };
|
|
138
140
|
|
|
139
141
|
// Delegate to the base Modal widget, passing our content and actions
|
|
140
142
|
modal = Modal({
|
|
@@ -159,4 +161,4 @@ export const AlertDialog = (props) => {
|
|
|
159
161
|
return { open, close, modal };
|
|
160
162
|
};
|
|
161
163
|
|
|
162
|
-
export default AlertDialog;
|
|
164
|
+
export default AlertDialog;
|
package/src/widgets/Checkbox.js
CHANGED
|
@@ -12,6 +12,7 @@ export const Checkbox = (props) => {
|
|
|
12
12
|
onPress: onPressProp,
|
|
13
13
|
disabled = false,
|
|
14
14
|
size = 20,
|
|
15
|
+
activeColor = colors.primary,
|
|
15
16
|
...rest
|
|
16
17
|
} = props;
|
|
17
18
|
|
|
@@ -25,10 +26,8 @@ export const Checkbox = (props) => {
|
|
|
25
26
|
|
|
26
27
|
const updateUI = (newChecked) => {
|
|
27
28
|
if (checkboxRef) {
|
|
28
|
-
checkboxRef.style.backgroundColor = newChecked
|
|
29
|
-
|
|
30
|
-
: "transparent";
|
|
31
|
-
checkboxRef.style.border = `2px solid ${newChecked ? colors.primary : colors.border}`;
|
|
29
|
+
checkboxRef.style.backgroundColor = newChecked ? activeColor : "transparent";
|
|
30
|
+
checkboxRef.style.border = `2px solid ${newChecked ? activeColor : colors.border}`;
|
|
32
31
|
}
|
|
33
32
|
if (checkMarkRef) {
|
|
34
33
|
checkMarkRef.style.display = newChecked ? "inline-block" : "none";
|
|
@@ -51,9 +50,9 @@ export const Checkbox = (props) => {
|
|
|
51
50
|
widgetName: "Checkbox",
|
|
52
51
|
width: size,
|
|
53
52
|
height: size,
|
|
54
|
-
border: `2px solid ${disabled ? colors.border : internalChecked ?
|
|
53
|
+
border: `2px solid ${disabled ? colors.border : internalChecked ? activeColor : colors.border}`,
|
|
55
54
|
borderRadius: 6,
|
|
56
|
-
backgroundColor: internalChecked ?
|
|
55
|
+
backgroundColor: internalChecked ? activeColor : "transparent",
|
|
57
56
|
display: "flex",
|
|
58
57
|
alignItems: "center",
|
|
59
58
|
justifyContent: "center",
|
|
@@ -84,7 +83,10 @@ export const Checkbox = (props) => {
|
|
|
84
83
|
// Hover effect
|
|
85
84
|
checkbox.addEventListener("mouseenter", () => {
|
|
86
85
|
if (!disabled && !internalChecked) {
|
|
87
|
-
|
|
86
|
+
// `color-mix` rather than `activeColor + "20"`: that string concat only
|
|
87
|
+
// produced a valid color for `#rrggbb` hexes and silently broke on
|
|
88
|
+
// `rgba()`, named colors and 3-digit hex `activeColor` values.
|
|
89
|
+
checkbox.style.backgroundColor = `color-mix(in srgb, ${activeColor} 12%, transparent)`;
|
|
88
90
|
}
|
|
89
91
|
});
|
|
90
92
|
|
|
@@ -104,7 +104,18 @@ export const CircularBar = (props) => {
|
|
|
104
104
|
if (!gradient) return color;
|
|
105
105
|
if (typeof gradient === "string") return gradient;
|
|
106
106
|
if (Array.isArray(gradient)) {
|
|
107
|
-
|
|
107
|
+
// CSS angle convention: 0deg points up and angles grow clockwise, so the
|
|
108
|
+
// default 135deg keeps the historical top-left to bottom-right sweep.
|
|
109
|
+
const rad = (gradientAngle * Math.PI) / 180;
|
|
110
|
+
const centre = currentWidth / 2;
|
|
111
|
+
const dx = Math.sin(rad) * centre;
|
|
112
|
+
const dy = -Math.cos(rad) * centre;
|
|
113
|
+
const grad = ctx.createLinearGradient(
|
|
114
|
+
centre - dx,
|
|
115
|
+
centre - dy,
|
|
116
|
+
centre + dx,
|
|
117
|
+
centre + dy,
|
|
118
|
+
);
|
|
108
119
|
gradient.forEach((c, i) => {
|
|
109
120
|
grad.addColorStop(i / (gradient.length - 1), c);
|
|
110
121
|
});
|
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
* @file FloatingActionButton.js
|
|
3
3
|
* @description Circular floating action button anchored to a corner.
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
6
|
-
* FloatingActionButton Component
|
|
7
|
-
* @module widgets/FloatingActionButton
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
5
|
import { Container } from "./Container.js";
|
|
11
6
|
import { Row } from "./Row.js";
|
|
12
7
|
import { Text } from "./Text.js";
|
|
@@ -45,7 +40,9 @@ export const FloatingActionButton = (props) => {
|
|
|
45
40
|
if (extended && label) {
|
|
46
41
|
children.push(
|
|
47
42
|
Text({
|
|
48
|
-
|
|
43
|
+
// FIX: `value` es special y `applySpecial` solo lo aplica en inputs
|
|
44
|
+
// → en un <span> no hacía nada y el label quedaba vacío.
|
|
45
|
+
text: label,
|
|
49
46
|
size: 14,
|
|
50
47
|
fontWeight: "500",
|
|
51
48
|
color: foregroundColor,
|
|
@@ -88,4 +85,4 @@ export const FloatingActionButton = (props) => {
|
|
|
88
85
|
return button;
|
|
89
86
|
};
|
|
90
87
|
|
|
91
|
-
export default FloatingActionButton;
|
|
88
|
+
export default FloatingActionButton;
|
package/src/widgets/Image.js
CHANGED
|
@@ -5,18 +5,16 @@
|
|
|
5
5
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
6
|
|
|
7
7
|
export const Image = (props) => {
|
|
8
|
-
// `fit`
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
const { fit, circular, ...rest } = props || {};
|
|
8
|
+
// `fit` is now a proper style prop in translateProps (→ object-fit), so it
|
|
9
|
+
// flows through the factory like any other CSS property. Only `circular`
|
|
10
|
+
// needs special handling — it maps to borderRadius 50%.
|
|
11
|
+
const { circular, ...rest } = props || {};
|
|
13
12
|
return WidgetFactory({
|
|
14
13
|
tag: "img",
|
|
15
14
|
widgetName: "Image",
|
|
16
|
-
...(fit !== undefined ? { objectFit: fit } : null),
|
|
17
15
|
...(circular ? { borderRadius: "50%" } : null),
|
|
18
16
|
...rest,
|
|
19
17
|
});
|
|
20
18
|
};
|
|
21
19
|
|
|
22
|
-
export default Image;
|
|
20
|
+
export default Image;
|
package/src/widgets/Input.js
CHANGED
|
@@ -26,6 +26,9 @@ import { composeUpdate } from "../utils/composeUpdate.js";
|
|
|
26
26
|
* @property {string} [iconColor] - Color of the icons.
|
|
27
27
|
* @property {number} [iconSize=20] - Size of the icons in pixels.
|
|
28
28
|
* @property {Function} [onIconPress] - Called when either icon is pressed.
|
|
29
|
+
* @property {boolean} [clearable=false] - Shows a trailing button that empties the field.
|
|
30
|
+
* @property {boolean} [passwordToggle=false] - Shows a trailing button that reveals or hides the value.
|
|
31
|
+
* @property {Function} [onEnter] - Called with (value, event) when the user presses Enter in the field.
|
|
29
32
|
* @property {"none"|"letters"|"numbers"|"email"|"alphanumeric"|"safe"|"custom"} [validation="none"] - Validation mode applied on each keystroke and on blur.
|
|
30
33
|
* @property {number} [maxLength] - Maximum number of characters allowed.
|
|
31
34
|
* @property {boolean} [required=false] - Marks the field as required; validation fails on empty values.
|
|
@@ -39,6 +42,9 @@ import { composeUpdate } from "../utils/composeUpdate.js";
|
|
|
39
42
|
* @property {Function} [onBlur] - Called when the input loses focus and final validation runs.
|
|
40
43
|
*/
|
|
41
44
|
|
|
45
|
+
/** Suffix for generated field ids, so two Inputs never share one. */
|
|
46
|
+
let inputIdSeed = 0;
|
|
47
|
+
|
|
42
48
|
/**
|
|
43
49
|
* A fully-featured text input widget with built-in validation, icons, and theme support.
|
|
44
50
|
*
|
|
@@ -78,6 +84,9 @@ export const Input = (props) => {
|
|
|
78
84
|
iconSize = 20,
|
|
79
85
|
onIconPress,
|
|
80
86
|
|
|
87
|
+
clearable = false,
|
|
88
|
+
passwordToggle = false,
|
|
89
|
+
|
|
81
90
|
validation = "none",
|
|
82
91
|
maxLength,
|
|
83
92
|
required = false,
|
|
@@ -90,6 +99,7 @@ export const Input = (props) => {
|
|
|
90
99
|
onChange,
|
|
91
100
|
onFocus,
|
|
92
101
|
onBlur,
|
|
102
|
+
onEnter,
|
|
93
103
|
...rest
|
|
94
104
|
} = props;
|
|
95
105
|
|
|
@@ -98,12 +108,22 @@ export const Input = (props) => {
|
|
|
98
108
|
let isValid = true;
|
|
99
109
|
let validationMessage = "";
|
|
100
110
|
|
|
111
|
+
// The `<label>` needs a `for` that matches the native input's `id`, otherwise
|
|
112
|
+
// assistive tech never hears the field's name — the click-to-focus handler is
|
|
113
|
+
// not an association. Deriving it from the caller's `id` keeps the generated
|
|
114
|
+
// DOM predictable when the field is inside a form.
|
|
115
|
+
const fieldId =
|
|
116
|
+
typeof rest.id === "string" && rest.id
|
|
117
|
+
? `${rest.id}-input`
|
|
118
|
+
: `fletbox-input-${++inputIdSeed}`;
|
|
119
|
+
|
|
101
120
|
// DOM element references — populated during the build phase below
|
|
102
121
|
let inputElement = null;
|
|
103
122
|
let validationIcon = null;
|
|
104
123
|
let errorMessageElement = null;
|
|
105
124
|
let inputWrapper = null;
|
|
106
125
|
let container = null;
|
|
126
|
+
let clearButton = null;
|
|
107
127
|
|
|
108
128
|
// Whether the native input currently holds focus. Used to keep the focus
|
|
109
129
|
// ring (a box-shadow) in sync with validation colors without ever changing
|
|
@@ -286,6 +306,7 @@ export const Input = (props) => {
|
|
|
286
306
|
if (inputElement) {
|
|
287
307
|
inputElement.value = filtered;
|
|
288
308
|
}
|
|
309
|
+
if (clearButton) clearButton.style.display = filtered ? "flex" : "none";
|
|
289
310
|
|
|
290
311
|
const result = validateValue(filtered);
|
|
291
312
|
isValid = result.valid;
|
|
@@ -324,6 +345,7 @@ export const Input = (props) => {
|
|
|
324
345
|
if (!text) return;
|
|
325
346
|
labelEl = WidgetFactory({
|
|
326
347
|
tag: "label",
|
|
348
|
+
for: fieldId,
|
|
327
349
|
text,
|
|
328
350
|
fontSize: 12,
|
|
329
351
|
fontWeight: 500,
|
|
@@ -406,6 +428,7 @@ export const Input = (props) => {
|
|
|
406
428
|
// - Accessibility (label association, screen readers)
|
|
407
429
|
// - Browser autofill support
|
|
408
430
|
inputElement = document.createElement("input");
|
|
431
|
+
inputElement.id = fieldId;
|
|
409
432
|
inputElement.type = type;
|
|
410
433
|
inputElement.placeholder = placeholder || "";
|
|
411
434
|
inputElement.disabled = disabled || false;
|
|
@@ -468,6 +491,75 @@ export const Input = (props) => {
|
|
|
468
491
|
|
|
469
492
|
inputWrapper.appendChild(inputElement);
|
|
470
493
|
|
|
494
|
+
inputElement.addEventListener("keydown", (e) => {
|
|
495
|
+
// Enter is not a DOM event of its own: the callback receives the value the
|
|
496
|
+
// user just confirmed, after the same filtering `onChange` applies.
|
|
497
|
+
if (e.key === "Enter" && onEnter) onEnter(currentValue, e);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// --- Trailing controls (clear / reveal) ---
|
|
501
|
+
// Real `<button type="button">` elements: they are keyboard-reachable, and the
|
|
502
|
+
// `type` keeps them from submitting a form the input happens to live in.
|
|
503
|
+
const makeTrailingButton = (glyph, label, onPress) => {
|
|
504
|
+
const btn = document.createElement("button");
|
|
505
|
+
btn.type = "button";
|
|
506
|
+
btn.textContent = glyph;
|
|
507
|
+
btn.setAttribute("aria-label", label);
|
|
508
|
+
btn.style.width = "28px";
|
|
509
|
+
btn.style.height = "28px";
|
|
510
|
+
btn.style.flexShrink = "0";
|
|
511
|
+
btn.style.marginRight = "6px";
|
|
512
|
+
btn.style.padding = "0";
|
|
513
|
+
btn.style.border = "none";
|
|
514
|
+
btn.style.background = "transparent";
|
|
515
|
+
btn.style.color = colors.textSecondary;
|
|
516
|
+
btn.style.fontSize = "15px";
|
|
517
|
+
btn.style.lineHeight = "1";
|
|
518
|
+
btn.style.cursor = disabled ? "not-allowed" : "pointer";
|
|
519
|
+
btn.style.display = "flex";
|
|
520
|
+
btn.style.alignItems = "center";
|
|
521
|
+
btn.style.justifyContent = "center";
|
|
522
|
+
btn.addEventListener("click", (e) => {
|
|
523
|
+
// The wrapper focuses the input on any click; a trailing control must not
|
|
524
|
+
// steal that, and it must stay out of the form's submit path.
|
|
525
|
+
e.preventDefault();
|
|
526
|
+
e.stopPropagation();
|
|
527
|
+
if (disabled || readonly) return;
|
|
528
|
+
onPress(e);
|
|
529
|
+
});
|
|
530
|
+
inputWrapper.appendChild(btn);
|
|
531
|
+
return btn;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
if (clearable) {
|
|
535
|
+
clearButton = makeTrailingButton("✕", "Clear", () => {
|
|
536
|
+
handleInput("", null);
|
|
537
|
+
inputElement.focus();
|
|
538
|
+
});
|
|
539
|
+
clearButton.style.display = currentValue ? "flex" : "none";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (passwordToggle) {
|
|
543
|
+
let revealed = type !== "password";
|
|
544
|
+
const paint = (btn) => {
|
|
545
|
+
btn.textContent = revealed ? "🙈" : "👁";
|
|
546
|
+
btn.setAttribute("aria-label", revealed ? "Hide value" : "Show value");
|
|
547
|
+
};
|
|
548
|
+
const btn = makeTrailingButton("👁", "Show value", () => {
|
|
549
|
+
revealed = !revealed;
|
|
550
|
+
inputElement.type = revealed ? "text" : "password";
|
|
551
|
+
paint(btn);
|
|
552
|
+
// Keep the caret where the user left it: swapping the type recreates the
|
|
553
|
+
// native field's rendering, so refocusing without restoring the selection
|
|
554
|
+
// would jump the cursor to the end.
|
|
555
|
+
const end = inputElement.value.length;
|
|
556
|
+
inputElement.setSelectionRange?.(end, end);
|
|
557
|
+
inputElement.focus();
|
|
558
|
+
});
|
|
559
|
+
paint(btn);
|
|
560
|
+
// `inputElement.type` was already set from `type`; the button only mirrors it.
|
|
561
|
+
}
|
|
562
|
+
|
|
471
563
|
// --- Inline validation icon (✓ / ✗) shown inside the input on the right ---
|
|
472
564
|
if (showValidationIcon && validation !== "none") {
|
|
473
565
|
validationIcon = document.createElement("span");
|
package/src/widgets/ListView.js
CHANGED
|
@@ -128,6 +128,9 @@ export const ListView = (props) => {
|
|
|
128
128
|
let visibleStart = 0,
|
|
129
129
|
visibleEnd = 0; // current render window (avoids redundant re-renders when nothing changed)
|
|
130
130
|
let ticking = false; // requestAnimationFrame guard for the scroll handler
|
|
131
|
+
let scrollRaf = 0; // id of the frame the scroll handler queued, so unmount can cancel it
|
|
132
|
+
let mountRaf = 0; // id of the first-render frame
|
|
133
|
+
let disposed = false; // after unmount no queued frame may touch the DOM or call back
|
|
131
134
|
let headerElement = null,
|
|
132
135
|
footerElement = null,
|
|
133
136
|
emptyElement = null;
|
|
@@ -140,27 +143,35 @@ export const ListView = (props) => {
|
|
|
140
143
|
/** @returns {number} Total number of rows (accounting for grid columns). */
|
|
141
144
|
const getTotalRows = () => Math.ceil(_data.length / cols);
|
|
142
145
|
|
|
146
|
+
/** @returns {number} Height the header occupies inside the scroll container. */
|
|
147
|
+
const getHeaderHeight = () => headerElement?.offsetHeight || 0;
|
|
148
|
+
|
|
143
149
|
/**
|
|
144
|
-
* Calculates the absolute top offset of an item
|
|
150
|
+
* Calculates the absolute top offset of an item inside `innerContainer`.
|
|
151
|
+
*
|
|
152
|
+
* The header is a SIBLING placed above `innerContainer`, so the browser already
|
|
153
|
+
* pushes every row down by the header's height. Adding it here counted it twice:
|
|
154
|
+
* rows sat one header below where the window math expected them, and
|
|
155
|
+
* `scrollToIndex` overshot by the same amount.
|
|
156
|
+
*
|
|
145
157
|
* @param {number} index - Zero-based item index.
|
|
146
158
|
* @returns {number} Top position in pixels.
|
|
147
159
|
*/
|
|
148
160
|
const getItemTop = (index) => {
|
|
149
161
|
const row = Math.floor(index / cols);
|
|
150
|
-
|
|
151
|
-
return headerHeight + row * (itemSize + gap);
|
|
162
|
+
return row * (itemSize + gap);
|
|
152
163
|
};
|
|
153
164
|
|
|
154
165
|
/**
|
|
155
|
-
* Calculates the
|
|
156
|
-
*
|
|
166
|
+
* Calculates the logical height of the virtual item area. Header and footer are
|
|
167
|
+
* siblings of `innerContainer` and contribute to the scrollbar through their own
|
|
168
|
+
* layout boxes, so they must not be added here.
|
|
157
169
|
* @returns {number} Total height in pixels.
|
|
158
170
|
*/
|
|
159
171
|
const getTotalHeight = () => {
|
|
160
172
|
const rows = getTotalRows();
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return headerH + (rows * (itemSize + gap) - gap) + footerH;
|
|
173
|
+
if (rows === 0) return 0;
|
|
174
|
+
return Math.max(0, rows * (itemSize + gap) - gap);
|
|
164
175
|
};
|
|
165
176
|
|
|
166
177
|
/** Applies the correct logical height to the inner container. */
|
|
@@ -178,15 +189,17 @@ export const ListView = (props) => {
|
|
|
178
189
|
const renderVisibleItems = () => {
|
|
179
190
|
if (!scrollContainer) return;
|
|
180
191
|
|
|
181
|
-
const scrollTop = scrollContainer.scrollTop;
|
|
182
192
|
const viewportH = scrollContainer.clientHeight;
|
|
183
193
|
const itemTotalH = itemSize + gap;
|
|
194
|
+
// Convert the scroll position into ITEM space: rows start below the header, so
|
|
195
|
+
// the first rows are still on screen while `scrollTop` is inside the header.
|
|
196
|
+
const itemsTop = Math.max(0, scrollContainer.scrollTop - getHeaderHeight());
|
|
184
197
|
|
|
185
198
|
// Clamp start/end rows to valid range, including the buffer zone
|
|
186
|
-
let startRow = Math.max(0, Math.floor(
|
|
199
|
+
let startRow = Math.max(0, Math.floor(itemsTop / itemTotalH) - bufferSize);
|
|
187
200
|
let endRow = Math.min(
|
|
188
201
|
getTotalRows(),
|
|
189
|
-
Math.ceil((
|
|
202
|
+
Math.ceil((itemsTop + viewportH) / itemTotalH) + bufferSize,
|
|
190
203
|
);
|
|
191
204
|
|
|
192
205
|
let start = startRow * cols;
|
|
@@ -247,24 +260,39 @@ export const ListView = (props) => {
|
|
|
247
260
|
* Scroll event handler. Uses `requestAnimationFrame` throttling so the handler
|
|
248
261
|
* runs at most once per frame even during rapid scroll events. Also checks
|
|
249
262
|
* whether the user has scrolled close to the bottom and triggers `onEndReached`.
|
|
263
|
+
*
|
|
264
|
+
* The queued frame is tracked so unmounting can cancel it: an untracked frame
|
|
265
|
+
* rendered into a detached DOM and called `onEndReached` on a widget nobody owns
|
|
266
|
+
* anymore — which, for an infinite-scroll handler that appends data, is an
|
|
267
|
+
* unbounded fetch loop.
|
|
250
268
|
*/
|
|
251
269
|
const handleScroll = () => {
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
270
|
+
if (ticking || disposed) return;
|
|
271
|
+
ticking = true;
|
|
272
|
+
scrollRaf = requestAnimationFrame(() => {
|
|
273
|
+
scrollRaf = 0;
|
|
274
|
+
ticking = false;
|
|
275
|
+
if (disposed) return;
|
|
276
|
+
renderVisibleItems();
|
|
277
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
|
278
|
+
if (
|
|
279
|
+
onEndReached &&
|
|
280
|
+
_data.length &&
|
|
281
|
+
scrollHeight - (scrollTop + clientHeight) <=
|
|
282
|
+
clientHeight * onEndReachedThreshold
|
|
283
|
+
) {
|
|
284
|
+
onEndReached();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Recomputes the render window when the viewport itself changes size (window
|
|
291
|
+
* resize, a collapsing panel above the list). Without this, more rows becoming
|
|
292
|
+
* visible never triggered a render because no scroll event fired.
|
|
293
|
+
*/
|
|
294
|
+
const handleViewportResize = () => {
|
|
295
|
+
if (!disposed) renderVisibleItems();
|
|
268
296
|
};
|
|
269
297
|
|
|
270
298
|
// ---- Header / footer / empty-state ----
|
|
@@ -424,7 +452,8 @@ export const ListView = (props) => {
|
|
|
424
452
|
*/
|
|
425
453
|
const scrollToIndex = (index, animated = true) => {
|
|
426
454
|
if (index < 0 || index >= _data.length) return;
|
|
427
|
-
|
|
455
|
+
// Back to SCROLL space: the header's height sits in front of the item area.
|
|
456
|
+
const top = getItemTop(index) + getHeaderHeight();
|
|
428
457
|
scrollContainer.scrollTo({ top, behavior: animated ? "smooth" : "auto" });
|
|
429
458
|
};
|
|
430
459
|
|
|
@@ -475,6 +504,8 @@ export const ListView = (props) => {
|
|
|
475
504
|
});
|
|
476
505
|
|
|
477
506
|
// ---- Lifecycle hooks ----
|
|
507
|
+
let resizeObserver = null;
|
|
508
|
+
|
|
478
509
|
element.onMount(() => {
|
|
479
510
|
renderHeader();
|
|
480
511
|
renderFooter();
|
|
@@ -482,13 +513,21 @@ export const ListView = (props) => {
|
|
|
482
513
|
|
|
483
514
|
// Defer the first actual render to the next animation frame so the header/footer
|
|
484
515
|
// have already been measured and their heights are available.
|
|
485
|
-
requestAnimationFrame(() => {
|
|
516
|
+
mountRaf = requestAnimationFrame(() => {
|
|
517
|
+
mountRaf = 0;
|
|
518
|
+
if (disposed) return;
|
|
486
519
|
updateInnerHeight(); // Recalculate total height now that header/footer are in the DOM
|
|
487
520
|
renderVisibleItems(); // Render the initially visible rows
|
|
488
521
|
});
|
|
489
522
|
|
|
490
523
|
renderEmpty();
|
|
491
524
|
scrollContainer.addEventListener("scroll", handleScroll);
|
|
525
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
526
|
+
resizeObserver = new ResizeObserver(handleViewportResize);
|
|
527
|
+
resizeObserver.observe(scrollContainer);
|
|
528
|
+
} else {
|
|
529
|
+
window.addEventListener("resize", handleViewportResize);
|
|
530
|
+
}
|
|
492
531
|
if (onRefresh) setupPullToRefresh();
|
|
493
532
|
|
|
494
533
|
// Inject the spinner keyframes once, globally, so pull-to-refresh has its animation
|
|
@@ -499,6 +538,11 @@ export const ListView = (props) => {
|
|
|
499
538
|
});
|
|
500
539
|
|
|
501
540
|
element.onUnmount(() => {
|
|
541
|
+
disposed = true;
|
|
542
|
+
if (scrollRaf) cancelAnimationFrame(scrollRaf);
|
|
543
|
+
if (mountRaf) cancelAnimationFrame(mountRaf);
|
|
544
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
545
|
+
else window.removeEventListener("resize", handleViewportResize);
|
|
502
546
|
scrollContainer.removeEventListener("scroll", handleScroll);
|
|
503
547
|
itemCache.clear();
|
|
504
548
|
});
|
|
@@ -72,7 +72,10 @@ export const Pagination = (props) => {
|
|
|
72
72
|
|
|
73
73
|
const createPageButton = (page, isActive = false) => {
|
|
74
74
|
const buttonVariant = isActive ? "filled" : variant;
|
|
75
|
-
|
|
75
|
+
// FIX: antes `isActive ? color : colors.textSecondary` — pero bgColor también
|
|
76
|
+
// es `color`, así que texto y fondo eran el mismo tono → número invisible.
|
|
77
|
+
// Uso blanco para el activo (texto sobre fondo de color).
|
|
78
|
+
const buttonColor = isActive ? "#ffffff" : colors.textSecondary;
|
|
76
79
|
|
|
77
80
|
return Button({
|
|
78
81
|
text: String(page),
|
|
@@ -98,7 +101,7 @@ export const Pagination = (props) => {
|
|
|
98
101
|
...buttonBase,
|
|
99
102
|
onPress: onClick,
|
|
100
103
|
disabled: disabled || isDisabled,
|
|
101
|
-
|
|
104
|
+
disableTransform: true,
|
|
102
105
|
});
|
|
103
106
|
};
|
|
104
107
|
|
|
@@ -192,4 +195,4 @@ export const Pagination = (props) => {
|
|
|
192
195
|
return container;
|
|
193
196
|
};
|
|
194
197
|
|
|
195
|
-
export default Pagination;
|
|
198
|
+
export default Pagination;
|
|
@@ -43,9 +43,9 @@ export const ProgressBar = (props) => {
|
|
|
43
43
|
position: "relative",
|
|
44
44
|
...(striped || indeterminate
|
|
45
45
|
? {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
backgroundImage: `repeating-linear-gradient(45deg, rgba(255,255,255,0.15) 0px, rgba(255,255,255,0.15) 10px, transparent 10px, transparent 20px)`,
|
|
47
|
+
backgroundSize: "28px 28px",
|
|
48
|
+
}
|
|
49
49
|
: {}),
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -115,9 +115,15 @@ export const ProgressBar = (props) => {
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
// Value percentage
|
|
118
|
+
// Value percentage.
|
|
119
|
+
// FIX: guardo referencia directa al span del valor (`valueElRef`) en vez de
|
|
120
|
+
// buscarlo con `querySelector("span:last-child")` — la versión anterior
|
|
121
|
+
// podía escribir el porcentaje dentro del label (si el label estaba a la
|
|
122
|
+
// derecha, era el último span) y el check `valueEl !== label` nunca fallaba
|
|
123
|
+
// porque `label` era un string.
|
|
124
|
+
let valueElRef = null;
|
|
119
125
|
if (showValue && !indeterminate) {
|
|
120
|
-
|
|
126
|
+
valueElRef = WidgetFactory({
|
|
121
127
|
tag: "span",
|
|
122
128
|
textContent: `${Math.round(percentage)}%`,
|
|
123
129
|
fontSize: "12px",
|
|
@@ -129,9 +135,9 @@ export const ProgressBar = (props) => {
|
|
|
129
135
|
});
|
|
130
136
|
|
|
131
137
|
if (valuePosition === "left" || valuePosition === "top") {
|
|
132
|
-
children.unshift(
|
|
138
|
+
children.unshift(valueElRef);
|
|
133
139
|
} else {
|
|
134
|
-
children.push(
|
|
140
|
+
children.push(valueElRef);
|
|
135
141
|
}
|
|
136
142
|
}
|
|
137
143
|
|
|
@@ -154,11 +160,8 @@ export const ProgressBar = (props) => {
|
|
|
154
160
|
percentage = (progressValue / max) * 100;
|
|
155
161
|
fill.style.width = `${percentage}%`;
|
|
156
162
|
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
if (valueEl && valueEl !== label) {
|
|
160
|
-
valueEl.textContent = `${Math.round(percentage)}%`;
|
|
161
|
-
}
|
|
163
|
+
if (valueElRef) {
|
|
164
|
+
valueElRef.textContent = `${Math.round(percentage)}%`;
|
|
162
165
|
}
|
|
163
166
|
};
|
|
164
167
|
|
|
@@ -175,4 +178,4 @@ export const ProgressBar = (props) => {
|
|
|
175
178
|
return container;
|
|
176
179
|
};
|
|
177
180
|
|
|
178
|
-
export default ProgressBar;
|
|
181
|
+
export default ProgressBar;
|
package/src/widgets/Radio.js
CHANGED
|
@@ -13,6 +13,7 @@ export const Radio = (props) => {
|
|
|
13
13
|
disabled = false,
|
|
14
14
|
size = 20,
|
|
15
15
|
name,
|
|
16
|
+
activeColor = colors.primary,
|
|
16
17
|
...rest
|
|
17
18
|
} = props;
|
|
18
19
|
|
|
@@ -26,10 +27,8 @@ export const Radio = (props) => {
|
|
|
26
27
|
|
|
27
28
|
const updateUI = (newSelected) => {
|
|
28
29
|
if (radioRef) {
|
|
29
|
-
radioRef.style.backgroundColor = newSelected
|
|
30
|
-
|
|
31
|
-
: "transparent";
|
|
32
|
-
radioRef.style.border = `2px solid ${newSelected ? colors.primary : colors.border}`;
|
|
30
|
+
radioRef.style.backgroundColor = newSelected ? activeColor : "transparent";
|
|
31
|
+
radioRef.style.border = `2px solid ${newSelected ? activeColor : colors.border}`;
|
|
33
32
|
}
|
|
34
33
|
if (dotRef) {
|
|
35
34
|
dotRef.style.display = newSelected ? "block" : "none";
|
|
@@ -51,9 +50,9 @@ export const Radio = (props) => {
|
|
|
51
50
|
widgetName: "Radio",
|
|
52
51
|
width: size,
|
|
53
52
|
height: size,
|
|
54
|
-
border: `2px solid ${disabled ? colors.border : internalSelected ?
|
|
53
|
+
border: `2px solid ${disabled ? colors.border : internalSelected ? activeColor : colors.border}`,
|
|
55
54
|
borderRadius: "50%",
|
|
56
|
-
backgroundColor: internalSelected ?
|
|
55
|
+
backgroundColor: internalSelected ? activeColor : "transparent",
|
|
57
56
|
display: "flex",
|
|
58
57
|
alignItems: "center",
|
|
59
58
|
justifyContent: "center",
|
|
@@ -96,7 +95,7 @@ export const Radio = (props) => {
|
|
|
96
95
|
// Hover effect
|
|
97
96
|
radio.addEventListener("mouseenter", () => {
|
|
98
97
|
if (!disabled && !internalSelected) {
|
|
99
|
-
radio.style.backgroundColor =
|
|
98
|
+
radio.style.backgroundColor = `color-mix(in srgb, ${activeColor} 12%, transparent)`;
|
|
100
99
|
}
|
|
101
100
|
});
|
|
102
101
|
|