flet-box 1.0.3 → 1.0.5
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 +15 -0
- 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 +4 -2
- package/src/animations/AnimatedBox.js +32 -1
- package/src/animations/AnimatedText.js +7 -3
- package/src/animations/MatrixRain.js +37 -12
- package/src/animations/ParallaxBox.js +16 -3
- package/src/animations/animate.js +187 -58
- package/src/animations/index.js +1 -0
- package/src/core/runApp.js +13 -1
- package/src/create-index.sh +1 -1
- package/src/index.d.ts +227 -91
- package/src/index.js +1 -0
- package/src/navigations/AdaptiveScaffold.js +11 -2
- package/src/navigations/AppBar.js +8 -2
- package/src/navigations/BottomNavigation.js +24 -5
- package/src/navigations/DrawerItem.js +16 -3
- package/src/navigations/Scaffold.js +8 -1
- package/src/navigations/Tabs.js +40 -8
- package/src/tools/dimensions.js +11 -3
- package/src/tools/useState.js +139 -101
- package/src/utils/getWidgetProps.js +5 -3
- package/src/widget-factory/assignProps.js +30 -11
- package/src/widget-factory/processProps.js +17 -5
- package/src/widget-factory/reactiveValue.js +172 -0
- package/src/widget-factory/translateProps.js +777 -901
- package/src/widget-factory/warnings.js +15 -0
- package/src/widget-factory/widgetFactory.js +27 -0
- package/src/widgets/Accordion.js +6 -5
- package/src/widgets/AlertDialog.js +7 -5
- package/src/widgets/Audio.js +30 -8
- package/src/widgets/BottomSheet.js +24 -1
- package/src/widgets/Chart.js +4 -1
- package/src/widgets/Checkbox.js +16 -8
- package/src/widgets/CircularBar.js +12 -1
- package/src/widgets/CircularChart.js +4 -1
- package/src/widgets/CodeViewer.js +26 -8
- package/src/widgets/DataTable.js +11 -2
- package/src/widgets/Dropdown.js +5 -1
- package/src/widgets/FloatingActionButton.js +4 -7
- package/src/widgets/Image.js +7 -2
- package/src/widgets/Input.js +119 -6
- package/src/widgets/Inspector.js +16 -10
- package/src/widgets/ListView.js +80 -30
- package/src/widgets/Markdown.js +53 -8
- package/src/widgets/Modal.js +24 -1
- package/src/widgets/Pagination.js +6 -3
- package/src/widgets/ProgressBar.js +16 -13
- package/src/widgets/QRCode.js +9 -2
- package/src/widgets/Radio.js +11 -8
- package/src/widgets/Slider.js +26 -4
- package/src/widgets/SnackBar.js +20 -2
- package/src/widgets/Stack.js +36 -3
- package/src/widgets/Stepper.js +4 -3
- package/src/widgets/Switch.js +8 -5
- package/src/widgets/TreeView.js +3 -2
|
@@ -7,6 +7,14 @@ const IS_DEV = typeof FLETBOX_DEV !== 'undefined' ? FLETBOX_DEV : true;
|
|
|
7
7
|
const suggestionCache = new Map();
|
|
8
8
|
let propNames = [];
|
|
9
9
|
|
|
10
|
+
// Props already reported. Without this, every prop *application* re-printed the
|
|
11
|
+
// whole block — a captured stack trace plus ~8 styled console calls — so a widget
|
|
12
|
+
// carrying a single unknown prop paid ~137 µs on every `update()`, roughly 400×
|
|
13
|
+
// the cost of a known prop. Because `update()` re-applies every original prop, one
|
|
14
|
+
// unknown prop slowed that widget for its whole lifetime.
|
|
15
|
+
const warnedProps = new Set();
|
|
16
|
+
const warnedDeprecated = new Set();
|
|
17
|
+
|
|
10
18
|
export function setPropNames(names) {
|
|
11
19
|
propNames = names;
|
|
12
20
|
try { if (typeof window !== 'undefined') window.__FLETBOX_PROP_NAMES = names; } catch(e){}
|
|
@@ -216,6 +224,9 @@ function printDocs(prop) {
|
|
|
216
224
|
|
|
217
225
|
export function warnUnknownProp(propName, context = {}) {
|
|
218
226
|
if (!IS_DEV) return;
|
|
227
|
+
// Report each unknown prop once, not once per application.
|
|
228
|
+
if (warnedProps.has(propName)) return;
|
|
229
|
+
warnedProps.add(propName);
|
|
219
230
|
|
|
220
231
|
const suggestions = getSuggestions(propName);
|
|
221
232
|
const location = getCallLocation();
|
|
@@ -231,6 +242,10 @@ export function warnUnknownProp(propName, context = {}) {
|
|
|
231
242
|
|
|
232
243
|
export function warnDeprecatedProp(propName, replacement = null, context = {}) {
|
|
233
244
|
if (!IS_DEV) return;
|
|
245
|
+
// Same reasoning as `warnUnknownProp`: one report per prop/replacement pair.
|
|
246
|
+
const deprecatedKey = `${propName}\u0000${replacement || ""}`;
|
|
247
|
+
if (warnedDeprecated.has(deprecatedKey)) return;
|
|
248
|
+
warnedDeprecated.add(deprecatedKey);
|
|
234
249
|
|
|
235
250
|
const location = getCallLocation();
|
|
236
251
|
|
|
@@ -8,6 +8,29 @@ import { processProps } from "./processProps.js";
|
|
|
8
8
|
import { assignProps } from "./assignProps.js";
|
|
9
9
|
import { stackPosition } from "./stackPosition.js";
|
|
10
10
|
import { addChildren, removePropChildren } from "./addChildren.js";
|
|
11
|
+
import { reactiveKeyOf } from "./reactiveValue.js";
|
|
12
|
+
import { registerWidget } from "../tools/useState.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Auto-binds reactive props to their state key.
|
|
16
|
+
*
|
|
17
|
+
* A prop whose value is a live reactive (what `useState` returns) carries the key
|
|
18
|
+
* it came from, so the factory can subscribe this widget to that key. A later
|
|
19
|
+
* change then updates **only this widget** instead of rebuilding the whole app —
|
|
20
|
+
* which is what makes the plain
|
|
21
|
+
* `const [color, setColor] = useState("color", "red"); Widget({ bgColor: color })`
|
|
22
|
+
* syntax surgical.
|
|
23
|
+
*
|
|
24
|
+
* @param {HTMLElement} widget - The widget being built or updated.
|
|
25
|
+
* @param {Object} props - The props to scan.
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
const bindReactiveProps = (widget, props) => {
|
|
29
|
+
for (const [prop, value] of Object.entries(props || {})) {
|
|
30
|
+
const key = reactiveKeyOf(value);
|
|
31
|
+
if (key) registerWidget(key, widget, prop);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
11
34
|
|
|
12
35
|
/**
|
|
13
36
|
* Main widget factory. Creates a fully functional DOM widget with reactivity,
|
|
@@ -96,6 +119,7 @@ export function WidgetFactory(tag, customProps = {}) {
|
|
|
96
119
|
// ============================================================
|
|
97
120
|
const initialProps = processProps(finalProps, finalTag, widgetName);
|
|
98
121
|
assignProps(widget, initialProps);
|
|
122
|
+
bindReactiveProps(widget, finalProps);
|
|
99
123
|
stackPosition(widget, finalProps);
|
|
100
124
|
|
|
101
125
|
if (finalProps.child !== undefined) {
|
|
@@ -160,6 +184,9 @@ export function WidgetFactory(tag, customProps = {}) {
|
|
|
160
184
|
|
|
161
185
|
const processed = processProps(merged, finalTag, widgetName);
|
|
162
186
|
assignProps(widget, processed);
|
|
187
|
+
// Only the incoming keys can introduce a new binding; the ones present at
|
|
188
|
+
// construction were registered then.
|
|
189
|
+
bindReactiveProps(widget, normalized);
|
|
163
190
|
stackPosition(widget, merged);
|
|
164
191
|
|
|
165
192
|
if (hasChild || hasChildren) {
|
package/src/widgets/Accordion.js
CHANGED
|
@@ -156,11 +156,12 @@ export const Accordion = (props) => {
|
|
|
156
156
|
titleElement.style.fontSize = `${currentTitleSize}px`;
|
|
157
157
|
titleElement.style.fontWeight = currentTitleWeight;
|
|
158
158
|
|
|
159
|
-
// Swap icon based on current expanded state
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
// Swap icon based on current expanded state. `Icon` renders its glyph as
|
|
160
|
+
// TEXT (a material-icons ligature), so setting a `name` attribute changed
|
|
161
|
+
// nothing and the chevron stayed on `chevron_right` forever.
|
|
162
|
+
iconElement.textContent = isExpanded
|
|
163
|
+
? currentIconExpanded
|
|
164
|
+
: currentIconCollapsed;
|
|
164
165
|
iconElement.style.color = currentIconColor;
|
|
165
166
|
iconElement.style.fontSize = `${currentIconSize}px`;
|
|
166
167
|
|
|
@@ -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/Audio.js
CHANGED
|
@@ -74,6 +74,9 @@ export const Audio = (props) => {
|
|
|
74
74
|
let duration = 0;
|
|
75
75
|
let durationPoll = null;
|
|
76
76
|
let audioContextFallbackAttempted = false;
|
|
77
|
+
// Set on unmount so deferred work (the duration fallback, in-flight fetches)
|
|
78
|
+
// never touches a discarded widget.
|
|
79
|
+
let disposed = false;
|
|
77
80
|
|
|
78
81
|
/**
|
|
79
82
|
* Recalculates the current playback position and fires the progress callbacks.
|
|
@@ -139,19 +142,25 @@ export const Audio = (props) => {
|
|
|
139
142
|
* cannot determine duration without fully downloading the file.
|
|
140
143
|
*/
|
|
141
144
|
const fallbackGetDuration = async () => {
|
|
142
|
-
if (audioContextFallbackAttempted) return;
|
|
145
|
+
if (audioContextFallbackAttempted || disposed) return;
|
|
143
146
|
audioContextFallbackAttempted = true;
|
|
144
147
|
|
|
145
148
|
try {
|
|
146
149
|
const response = await fetch(src);
|
|
147
150
|
const arrayBuffer = await response.arrayBuffer();
|
|
151
|
+
if (disposed) return;
|
|
148
152
|
const audioContext = new (
|
|
149
153
|
window.AudioContext || window.webkitAudioContext
|
|
150
154
|
)();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
// `close()` must run even when decoding fails: AudioContexts are a capped
|
|
156
|
+
// resource (~6 per page in Chromium) and leaking them makes every later
|
|
157
|
+
// context fail to start, which breaks the fallback for good.
|
|
158
|
+
try {
|
|
159
|
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
160
|
+
setDuration(audioBuffer.duration);
|
|
161
|
+
} finally {
|
|
162
|
+
audioContext.close();
|
|
163
|
+
}
|
|
155
164
|
} catch (err) {
|
|
156
165
|
console.warn("[Audio] Fallback duration fetch failed:", err);
|
|
157
166
|
}
|
|
@@ -178,16 +187,29 @@ export const Audio = (props) => {
|
|
|
178
187
|
audio.addEventListener("error", (e) => console.warn("[Audio] Error:", e));
|
|
179
188
|
|
|
180
189
|
// Kick off duration detection immediately; if unavailable after 2 s, use fetch fallback
|
|
190
|
+
let fallbackTimer = null;
|
|
181
191
|
if (!tryGetDurationFromAudio()) {
|
|
182
192
|
pollDuration();
|
|
183
|
-
setTimeout(() => {
|
|
193
|
+
fallbackTimer = setTimeout(() => {
|
|
194
|
+
fallbackTimer = null;
|
|
184
195
|
if (!duration) fallbackGetDuration();
|
|
185
196
|
}, 2000);
|
|
186
197
|
}
|
|
187
198
|
|
|
188
|
-
//
|
|
199
|
+
// Release everything the widget owns on unmount. The 2 s fallback timer was
|
|
200
|
+
// never stored, so after teardown it still fetched the whole file and spun up
|
|
201
|
+
// an AudioContext (and called onLoad) for a widget that no longer exists — and
|
|
202
|
+
// a tree rebuild does that on every state change.
|
|
189
203
|
audio.onUnmount(() => {
|
|
190
|
-
|
|
204
|
+
disposed = true;
|
|
205
|
+
if (durationPoll) {
|
|
206
|
+
clearInterval(durationPoll);
|
|
207
|
+
durationPoll = null;
|
|
208
|
+
}
|
|
209
|
+
if (fallbackTimer) {
|
|
210
|
+
clearTimeout(fallbackTimer);
|
|
211
|
+
fallbackTimer = null;
|
|
212
|
+
}
|
|
191
213
|
});
|
|
192
214
|
|
|
193
215
|
// ========== PUBLIC PLAYBACK API ==========
|
|
@@ -58,6 +58,8 @@ export const BottomSheet = (props) => {
|
|
|
58
58
|
|
|
59
59
|
let isOpen = false;
|
|
60
60
|
let overlay = null;
|
|
61
|
+
// Pending "hide after the close animation" timer, so it can be cancelled.
|
|
62
|
+
let closeTimer = null;
|
|
61
63
|
let sheetContainer = null;
|
|
62
64
|
let startY = 0;
|
|
63
65
|
let currentY = 0;
|
|
@@ -190,6 +192,18 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
|
|
|
190
192
|
overlay.appendChild(sheetContainer);
|
|
191
193
|
document.body.appendChild(overlay);
|
|
192
194
|
|
|
195
|
+
// The overlay lives in `document.body` and can be removed without `close()`
|
|
196
|
+
// ever running (state rebuild, route change, plain removeChild of the host).
|
|
197
|
+
// `_cleanup` is not called on that path, so release the document listener and
|
|
198
|
+
// any pending animation timer through the lifecycle hook too.
|
|
199
|
+
overlay.onUnmount(() => {
|
|
200
|
+
if (closeTimer) {
|
|
201
|
+
clearTimeout(closeTimer);
|
|
202
|
+
closeTimer = null;
|
|
203
|
+
}
|
|
204
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
205
|
+
});
|
|
206
|
+
|
|
193
207
|
// Drag handlers
|
|
194
208
|
const onTouchStart = (e) => {
|
|
195
209
|
// ✅ Prevent the drag event from propagating to the overlay
|
|
@@ -229,6 +243,11 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
|
|
|
229
243
|
const open = () => {
|
|
230
244
|
if (isOpen) return;
|
|
231
245
|
isOpen = true;
|
|
246
|
+
// Cancel any pending hide from a close that is still animating out.
|
|
247
|
+
if (closeTimer) {
|
|
248
|
+
clearTimeout(closeTimer);
|
|
249
|
+
closeTimer = null;
|
|
250
|
+
}
|
|
232
251
|
overlay.style.visibility = "visible";
|
|
233
252
|
overlay.style.opacity = "1";
|
|
234
253
|
sheetContainer.style.transform = "translateY(0)";
|
|
@@ -250,7 +269,11 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
|
|
|
250
269
|
isOpen = false;
|
|
251
270
|
sheetContainer.style.transform = "translateY(100%)";
|
|
252
271
|
overlay.style.opacity = "0";
|
|
253
|
-
|
|
272
|
+
// Keep the id: `open()` clears it, otherwise reopening within the animation
|
|
273
|
+
// window lets a stale timer hide an OPEN sheet (invisible but isOpen true).
|
|
274
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
275
|
+
closeTimer = setTimeout(() => {
|
|
276
|
+
closeTimer = null;
|
|
254
277
|
overlay.style.visibility = "hidden";
|
|
255
278
|
}, animationDuration);
|
|
256
279
|
if (onClose) onClose();
|
package/src/widgets/Chart.js
CHANGED
|
@@ -134,7 +134,10 @@ export const Chart = (props) => {
|
|
|
134
134
|
const paddingRange = range * 0.1;
|
|
135
135
|
return {
|
|
136
136
|
maxValue: maxVal + paddingRange,
|
|
137
|
-
|
|
137
|
+
// Do NOT clamp to 0. With all-negative data that inverts the range
|
|
138
|
+
// (min > max) and every point is drawn far outside the canvas, so the
|
|
139
|
+
// chart looks empty and the axis labels run from 0 to a negative max.
|
|
140
|
+
minValue: minVal - paddingRange,
|
|
138
141
|
};
|
|
139
142
|
};
|
|
140
143
|
|
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",
|
|
@@ -67,18 +66,27 @@ export const Checkbox = (props) => {
|
|
|
67
66
|
|
|
68
67
|
// Handle click
|
|
69
68
|
if (!disabled) {
|
|
70
|
-
|
|
69
|
+
const handleClick = (e) => {
|
|
71
70
|
e.stopPropagation();
|
|
72
71
|
const newChecked = !internalChecked;
|
|
73
72
|
internalChecked = newChecked;
|
|
74
73
|
updateUI(newChecked);
|
|
75
74
|
if (onPress) onPress(newChecked);
|
|
76
75
|
};
|
|
76
|
+
// Route the handler through the factory's props instead of assigning
|
|
77
|
+
// `onclick` directly. `assignProps` clears `onclick` on every update and only
|
|
78
|
+
// re-applies handlers it can see in the merged props, so a direct assignment
|
|
79
|
+
// died on the first `update()` — and every reactive setter (opacity, bgColor,
|
|
80
|
+
// padding…) calls update — leaving the control unable to toggle ever again.
|
|
81
|
+
checkbox.update({ onPress: handleClick });
|
|
77
82
|
|
|
78
83
|
// Hover effect
|
|
79
84
|
checkbox.addEventListener("mouseenter", () => {
|
|
80
85
|
if (!disabled && !internalChecked) {
|
|
81
|
-
|
|
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)`;
|
|
82
90
|
}
|
|
83
91
|
});
|
|
84
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
|
});
|
|
@@ -335,7 +335,10 @@ export const CircularChart = (props) => {
|
|
|
335
335
|
if (!ctx || !canvas) return;
|
|
336
336
|
if (!startTime) {
|
|
337
337
|
startTime = timestamp;
|
|
338
|
-
|
|
338
|
+
// Keep the id. The previous frame's id is already spent, so cleanup would
|
|
339
|
+
// cancel nothing and the pending frame kept re-arming forever on a detached
|
|
340
|
+
// canvas — and a re-`startAnimation()` could run two loops at once.
|
|
341
|
+
animationId = requestAnimationFrame(updateAnimation);
|
|
339
342
|
return;
|
|
340
343
|
}
|
|
341
344
|
const elapsed = timestamp - startTime;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
6
|
import { colors } from "../utils/themes.js";
|
|
7
|
+
import { composeUpdate } from "../utils/composeUpdate.js";
|
|
7
8
|
import { generateHighlightedHtml } from "../utils/syntaxHighlight.js";
|
|
8
9
|
|
|
9
10
|
export const CodeViewer = (props) => {
|
|
@@ -81,8 +82,14 @@ code = "",
|
|
|
81
82
|
...rest,
|
|
82
83
|
});
|
|
83
84
|
|
|
84
|
-
// Header
|
|
85
|
-
|
|
85
|
+
// Header. Built through a helper so a later `title` update can create one on a
|
|
86
|
+
// widget that was constructed without it.
|
|
87
|
+
const ensureHeader = (text) => {
|
|
88
|
+
if (!showHeader || !text) return null;
|
|
89
|
+
if (headerRef) {
|
|
90
|
+
headerRef.textContent = text;
|
|
91
|
+
return headerRef;
|
|
92
|
+
}
|
|
86
93
|
headerRef = WidgetFactory({
|
|
87
94
|
tag: "div",
|
|
88
95
|
padding: "8px 12px",
|
|
@@ -91,10 +98,12 @@ code = "",
|
|
|
91
98
|
color: "#fff",
|
|
92
99
|
fontSize: "14px",
|
|
93
100
|
fontWeight: "bold",
|
|
94
|
-
textContent:
|
|
101
|
+
textContent: text,
|
|
95
102
|
});
|
|
96
|
-
container.
|
|
97
|
-
|
|
103
|
+
container.insertBefore(headerRef, container.firstChild);
|
|
104
|
+
return headerRef;
|
|
105
|
+
};
|
|
106
|
+
ensureHeader(currentTitle);
|
|
98
107
|
|
|
99
108
|
// Scroll wrapper
|
|
100
109
|
scrollWrapperRef = WidgetFactory({
|
|
@@ -209,11 +218,20 @@ code = "",
|
|
|
209
218
|
|
|
210
219
|
container.updateTitle = (newTitle) => {
|
|
211
220
|
currentTitle = newTitle;
|
|
212
|
-
|
|
213
|
-
headerRef.textContent = currentTitle;
|
|
214
|
-
}
|
|
221
|
+
ensureHeader(currentTitle);
|
|
215
222
|
};
|
|
216
223
|
|
|
224
|
+
// `title` is a reactive prop mapped to `textContent` in the prop database, so
|
|
225
|
+
// letting the factory see it would replace the header and the whole code
|
|
226
|
+
// wrapper with a bare text node. Own it and update the header instead.
|
|
227
|
+
composeUpdate(container, ["title"], (props) => {
|
|
228
|
+
if (props.title === undefined) return;
|
|
229
|
+
container.updateTitle(props.title);
|
|
230
|
+
// `makeReactive` rewrites the value into `_originalProps` on every setter
|
|
231
|
+
// call; leaving it there means the next factory update wipes the widget.
|
|
232
|
+
delete container._originalProps.title;
|
|
233
|
+
});
|
|
234
|
+
|
|
217
235
|
container.scrollTo = (x, y) => {
|
|
218
236
|
if (scrollWrapperRef) {
|
|
219
237
|
scrollWrapperRef.scrollLeft = x;
|
package/src/widgets/DataTable.js
CHANGED
|
@@ -83,7 +83,9 @@ export const DataTable = (props) => {
|
|
|
83
83
|
table.appendChild(thead);
|
|
84
84
|
|
|
85
85
|
// ========== BODY ==========
|
|
86
|
-
const
|
|
86
|
+
// `let`, not `const`: `updateData` replaces the body and must re-point here,
|
|
87
|
+
// otherwise the second call tries to replace an already-detached node.
|
|
88
|
+
let tbody = document.createElement("tbody");
|
|
87
89
|
|
|
88
90
|
rows.forEach((row, index) => {
|
|
89
91
|
const tr = document.createElement("tr");
|
|
@@ -152,8 +154,12 @@ export const DataTable = (props) => {
|
|
|
152
154
|
|
|
153
155
|
// Update table data
|
|
154
156
|
container.updateData = (newRows) => {
|
|
157
|
+
// Snapshot BEFORE clearing. `updateColumns` calls this with `rows` itself as
|
|
158
|
+
// the argument, so clearing first and then spreading produced an empty body
|
|
159
|
+
// and every row silently disappeared.
|
|
160
|
+
const nextRows = [...newRows];
|
|
155
161
|
rows.length = 0;
|
|
156
|
-
rows.push(...
|
|
162
|
+
rows.push(...nextRows);
|
|
157
163
|
|
|
158
164
|
// Rebuild body
|
|
159
165
|
const newTbody = document.createElement("tbody");
|
|
@@ -213,6 +219,9 @@ export const DataTable = (props) => {
|
|
|
213
219
|
});
|
|
214
220
|
|
|
215
221
|
table.replaceChild(newTbody, tbody);
|
|
222
|
+
// Re-point at the live node, otherwise the next call replaces an already
|
|
223
|
+
// detached node and throws NotFoundError.
|
|
224
|
+
tbody = newTbody;
|
|
216
225
|
};
|
|
217
226
|
|
|
218
227
|
// Update table columns
|
package/src/widgets/Dropdown.js
CHANGED
|
@@ -243,7 +243,11 @@ export const Dropdown = (props) => {
|
|
|
243
243
|
if (outsideClickListener)
|
|
244
244
|
document.removeEventListener("click", outsideClickListener);
|
|
245
245
|
if (resizeListener) window.removeEventListener("resize", resizeListener);
|
|
246
|
-
|
|
246
|
+
// The capture flag MUST match the one used when adding (see the `scroll`
|
|
247
|
+
// registration below), otherwise the listener is never removed: every
|
|
248
|
+
// open/close cycle leaked one window listener holding the whole closure.
|
|
249
|
+
if (scrollListener)
|
|
250
|
+
window.removeEventListener("scroll", scrollListener, true);
|
|
247
251
|
outsideClickListener = resizeListener = scrollListener = null;
|
|
248
252
|
};
|
|
249
253
|
|
|
@@ -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,11 +5,16 @@
|
|
|
5
5
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
6
|
|
|
7
7
|
export const Image = (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 || {};
|
|
8
12
|
return WidgetFactory({
|
|
9
13
|
tag: "img",
|
|
10
14
|
widgetName: "Image",
|
|
11
|
-
...
|
|
15
|
+
...(circular ? { borderRadius: "50%" } : null),
|
|
16
|
+
...rest,
|
|
12
17
|
});
|
|
13
18
|
};
|
|
14
19
|
|
|
15
|
-
export default Image;
|
|
20
|
+
export default Image;
|