flet-box 1.1.0 → 1.1.2
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 +16 -12
- package/bin/commands/checkUi.js +276 -45
- package/bin/commands/create.js +8 -1
- package/bin/commands/createBundle.js +18 -6
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- package/bin/utils/bundleConfig.js +47 -3
- package/bin/utils/colors.js +0 -8
- package/bin/utils/helpers.js +0 -5
- package/bin/utils/images/jpeg.js +2 -3
- package/bin/utils/templates.js +101 -85
- package/docs/README.md +24 -1
- package/docs/cli/README.md +51 -10
- package/docs/guides/README.md +5 -2
- package/docs/guides/accessibility.md +282 -0
- package/docs/guides/clean-ui.md +53 -8
- package/docs/guides/first-app.md +6 -7
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- package/docs/guides/online-updates.md +18 -6
- package/docs/guides/utilities.md +116 -4
- package/docs/guides/widget-structure.md +30 -0
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +134 -11
- package/docs/tools/device.md +75 -3
- package/docs/tools/text-and-time.md +116 -6
- package/docs/widget/Accordion.md +2 -0
- package/docs/widget/AnimatedBox.md +46 -20
- package/docs/widget/AnimatedText.md +13 -7
- package/docs/widget/Badge.md +1 -0
- package/docs/widget/COMMON_PROPS.md +26 -3
- package/docs/widget/Chart.md +1 -0
- package/docs/widget/Checkbox.md +7 -1
- package/docs/widget/CircularChart.md +2 -0
- package/docs/widget/Drawer.md +1 -0
- package/docs/widget/GridView.md +4 -1
- package/docs/widget/Input.md +2 -0
- package/docs/widget/InstallButton.md +1 -0
- package/docs/widget/Markdown.md +4 -0
- package/docs/widget/Rating.md +2 -0
- package/docs/widget/Slider.md +2 -0
- package/docs/widget/Stack.md +6 -5
- package/docs/widget/Stepper.md +2 -0
- package/docs/widget/Tooltip.md +3 -1
- package/package.json +3 -2
- package/skills/fletbox-design/SKILL.md +390 -14
- package/skills/fletbox-motion/SKILL.md +145 -47
- package/skills/fletbox-rules/SKILL.md +140 -1
- package/src/animations/AnimatedBox.js +321 -226
- package/src/animations/MatrixRain.js +7 -1
- package/src/animations/ParallaxBox.js +13 -10
- package/src/animations/animate.js +269 -106
- package/src/animations/color.js +91 -0
- package/src/animations/easings.js +275 -0
- package/src/animations/index.js +21 -0
- package/src/animations/motion.js +204 -0
- package/src/animations/transitions.js +327 -0
- package/src/core/runApp.js +0 -6
- package/src/helpers/boxEdges.js +69 -0
- package/src/helpers/html.js +39 -0
- package/src/index.d.ts +377 -25
- package/src/index.js +77 -0
- package/src/navigations/AdaptiveScaffold.js +6 -2
- package/src/navigations/AppBar.js +16 -7
- package/src/navigations/BottomNavigation.js +24 -1
- package/src/navigations/CollapsibleSideBar.js +0 -1
- package/src/navigations/Drawer.js +32 -1
- package/src/navigations/DrawerItem.js +1 -1
- package/src/navigations/Router.js +0 -1
- package/src/navigations/Scaffold.js +9 -3
- package/src/navigations/Tabs.js +104 -31
- package/src/services/RamStore.js +1 -2
- package/src/tools/breakpoints.js +113 -0
- package/src/tools/createList.js +0 -2
- package/src/tools/fonts.js +99 -0
- package/src/tools/index.js +74 -1
- package/src/tools/margin.js +5 -63
- package/src/tools/padding.js +5 -63
- package/src/tools/print.js +0 -8
- package/src/tools/scale.js +9 -2
- package/src/tools/time.js +56 -15
- package/src/tools/useState.js +1 -54
- package/src/utils/fieldFont.js +77 -0
- package/src/utils/focusRing.js +73 -0
- package/src/utils/focusScope.js +113 -0
- package/src/utils/markdownParser.js +7 -9
- package/src/utils/readableInk.js +7 -1
- package/src/utils/stackLayout.js +55 -0
- package/src/utils/styleInjector.js +25 -0
- package/src/utils/syntaxHighlight.js +1 -17
- package/src/utils/themes.js +15 -0
- package/src/utils/visualEffects.js +49 -47
- package/src/widget-factory/createWidget.js +16 -3
- package/src/widget-factory/effects.js +76 -3
- package/src/widget-factory/reactiveValue.js +0 -7
- package/src/widget-factory/tools.js +26 -143
- package/src/widget-factory/translateProps.js +5 -0
- package/src/widget-factory/widgetFactory.js +1 -1
- package/src/widgets/Accordion.js +70 -12
- package/src/widgets/AdMob.js +1 -1
- package/src/widgets/AdSense.js +1 -1
- package/src/widgets/AlertDialog.js +11 -9
- package/src/widgets/Avatar.js +10 -1
- package/src/widgets/Badge.js +7 -1
- package/src/widgets/BottomSheet.js +39 -5
- package/src/widgets/Chart.js +41 -9
- package/src/widgets/Checkbox.js +59 -9
- package/src/widgets/Chip.js +13 -1
- package/src/widgets/CircularBar.js +17 -5
- package/src/widgets/CircularChart.js +24 -2
- package/src/widgets/CodeViewer.js +5 -3
- package/src/widgets/Dropdown.js +223 -20
- package/src/widgets/GridView.js +17 -1
- package/src/widgets/Icon.js +21 -0
- package/src/widgets/Input.js +75 -44
- package/src/widgets/InstallButton.js +8 -2
- package/src/widgets/ListTile.js +6 -5
- package/src/widgets/ListView.js +21 -7
- package/src/widgets/Markdown.js +25 -44
- package/src/widgets/Modal.js +43 -6
- package/src/widgets/Pagination.js +35 -3
- package/src/widgets/ProgressBar.js +18 -0
- package/src/widgets/Radio.js +7 -0
- package/src/widgets/Rating.js +104 -16
- package/src/widgets/Skeleton.js +9 -5
- package/src/widgets/Slider.js +87 -7
- package/src/widgets/SnackBar.js +30 -1
- package/src/widgets/Stack.js +25 -13
- package/src/widgets/Stepper.js +112 -64
- package/src/widgets/Switch.js +10 -1
- package/src/widgets/Tooltip.js +93 -10
- package/src/widgets/TreeView.js +5 -5
- package/src/create-index.sh +0 -237
- package/src/widget-factory/pixTool.js +0 -40
- package/src/widget-factory/stringTool.js +0 -26
|
@@ -37,6 +37,72 @@ const nextElevation = (value) => {
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The tags HTML already makes reachable: they are on the Tab path, they answer to
|
|
42
|
+
* Enter or Space, and a screen reader already knows what they are. Nothing here
|
|
43
|
+
* has to be done for them, and nothing here may be done *to* them — a native
|
|
44
|
+
* `<button>` given `role="button"` and a second key handler fires its action
|
|
45
|
+
* twice.
|
|
46
|
+
*
|
|
47
|
+
* @type {Set<string>}
|
|
48
|
+
*/
|
|
49
|
+
const NATIVELY_REACHABLE = new Set([
|
|
50
|
+
"A",
|
|
51
|
+
"BUTTON",
|
|
52
|
+
"INPUT",
|
|
53
|
+
"SELECT",
|
|
54
|
+
"TEXTAREA",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Makes an element that only answers to a mouse answer to a keyboard too.
|
|
59
|
+
*
|
|
60
|
+
* `onPress` is routed to `click`, and a `click` on a `div` is not something a
|
|
61
|
+
* keyboard can produce: the widget was invisible to Tab, had no name for an
|
|
62
|
+
* assistive technology, and did nothing on Enter. This is the whole of the
|
|
63
|
+
* contract the framework owes — the same three things HTML would have given the
|
|
64
|
+
* widget for free had it been built from a `button`.
|
|
65
|
+
*
|
|
66
|
+
* Everything is conditional on the app not having said so already: an explicit
|
|
67
|
+
* `role` or `tabIndex` is a decision, and `-1` ("keep this off the Tab path") is
|
|
68
|
+
* a decision the engine must not overwrite.
|
|
69
|
+
*
|
|
70
|
+
* @param {HTMLElement} widget - The pressable element.
|
|
71
|
+
* @param {Array<{event: string, handler: Function}>} listeners - The effect
|
|
72
|
+
* listener list, so the key handler is released by the next update like every
|
|
73
|
+
* other effect here.
|
|
74
|
+
* @returns {void}
|
|
75
|
+
*/
|
|
76
|
+
const makeKeyboardPressable = (widget, listeners) => {
|
|
77
|
+
if (NATIVELY_REACHABLE.has(widget.tagName)) return;
|
|
78
|
+
if (widget.disabled === true || widget.getAttribute("aria-disabled") === "true") {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!widget.hasAttribute("role")) widget.setAttribute("role", "button");
|
|
83
|
+
if (!widget.hasAttribute("tabindex")) widget.tabIndex = 0;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Enter and Space are what a press is for someone with no pointer.
|
|
87
|
+
*/
|
|
88
|
+
const onKeyDown = (event) => {
|
|
89
|
+
// A nested control owns its own keys: pressing Enter on a Button inside a
|
|
90
|
+
// clickable Card must open the card as well as fire the button.
|
|
91
|
+
if (event.target !== widget) return;
|
|
92
|
+
if (event.key !== "Enter" && event.key !== " " && event.key !== "Spacebar") {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Held down, Space is one press, not sixty.
|
|
96
|
+
if (event.repeat) return;
|
|
97
|
+
// Space would otherwise scroll the page instead of pressing the control.
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
if (typeof widget.onclick === "function") widget.onclick(event);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
widget.addEventListener("keydown", onKeyDown);
|
|
103
|
+
listeners.push({ event: "keydown", handler: onKeyDown });
|
|
104
|
+
};
|
|
105
|
+
|
|
40
106
|
/**
|
|
41
107
|
* Applies interactive visual effects to a widget that has a click handler.
|
|
42
108
|
*
|
|
@@ -47,7 +113,11 @@ const nextElevation = (value) => {
|
|
|
47
113
|
* **Base effects (always applied):**
|
|
48
114
|
* - `-webkit-tap-highlight-color` set to `transparent` to suppress the default
|
|
49
115
|
* mobile tap flash.
|
|
50
|
-
*
|
|
116
|
+
*
|
|
117
|
+
* The focus ring is **not** touched here. It used to be: this function wrote
|
|
118
|
+
* `outline: none` onto every widget, which read as a cleaner screenshot and cost
|
|
119
|
+
* every keyboard user the answer to "where am I". The ring is the document's
|
|
120
|
+
* own rule (`src/utils/focusRing.js`) and an inline `outline` would outrank it.
|
|
51
121
|
*
|
|
52
122
|
* **Press / hover effects (only when a click handler is present and
|
|
53
123
|
* `disableTransform !== true`):**
|
|
@@ -79,11 +149,15 @@ export const applyEffects = (widget) => {
|
|
|
79
149
|
}
|
|
80
150
|
|
|
81
151
|
widget.style.webkitTapHighlightColor = "transparent";
|
|
82
|
-
widget.style.outline = "none";
|
|
83
152
|
|
|
84
153
|
const hasClick = widget.onclick && typeof widget.onclick === "function";
|
|
85
154
|
if (!hasClick) return widget;
|
|
86
155
|
|
|
156
|
+
// Declared here because reachability is not a transform: a widget with
|
|
157
|
+
// `disableTransform` still has to be operable without a mouse.
|
|
158
|
+
const listeners = [];
|
|
159
|
+
makeKeyboardPressable(widget, listeners);
|
|
160
|
+
|
|
87
161
|
// Store the current shadow so it can be restored after press / hover effects.
|
|
88
162
|
//
|
|
89
163
|
// Skipped while the pointer is over the widget: `update()` re-runs
|
|
@@ -93,7 +167,6 @@ export const applyEffects = (widget) => {
|
|
|
93
167
|
const hoverShadow = nextElevation(widget._originalShadow);
|
|
94
168
|
|
|
95
169
|
const disableTransform = widget._originalProps?.disableTransform === true;
|
|
96
|
-
const listeners = [];
|
|
97
170
|
|
|
98
171
|
if (!disableTransform) {
|
|
99
172
|
let isPressed = false;
|
|
@@ -76,12 +76,6 @@ export const endRenderPass = () => {
|
|
|
76
76
|
*/
|
|
77
77
|
export const isStructuralRead = (key) => structuralKeys.has(key);
|
|
78
78
|
|
|
79
|
-
/** Forgets every observed key. Used by hot-reload teardown. */
|
|
80
|
-
export const resetStructuralReads = () => {
|
|
81
|
-
structuralKeys = new Set();
|
|
82
|
-
passKeys = renderPassDepth > 0 ? new Set() : null;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
79
|
/**
|
|
86
80
|
* True when `val` is a reactive value (live or snapshot).
|
|
87
81
|
*
|
|
@@ -166,7 +160,6 @@ export default {
|
|
|
166
160
|
resolveReactive,
|
|
167
161
|
createReactiveValue,
|
|
168
162
|
isStructuralRead,
|
|
169
|
-
resetStructuralReads,
|
|
170
163
|
beginRenderPass,
|
|
171
164
|
endRenderPass,
|
|
172
165
|
};
|
|
@@ -1,29 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Reads one or all inline styles from a DOM element.
|
|
7
|
-
*
|
|
8
|
-
* @param {HTMLElement|null} el - The target element.
|
|
9
|
-
* @param {string} [prop] - CSS property name. When provided, returns only that
|
|
10
|
-
* property's value (or `null` when absent). When omitted, returns an object
|
|
11
|
-
* containing every non-empty inline style.
|
|
12
|
-
* @returns {string|Object|null} The property value, a styles object, or `null`
|
|
13
|
-
* when `el` is falsy or the property is not set.
|
|
14
|
-
*/
|
|
15
|
-
export function getStyles(el, prop) {
|
|
16
|
-
if (!el) return null;
|
|
17
|
-
if (prop) return el.style[prop] || null;
|
|
18
|
-
|
|
19
|
-
const styles = {};
|
|
20
|
-
for (let i = 0; i < el.style.length; i++) {
|
|
21
|
-
const p = el.style[i];
|
|
22
|
-
const v = el.style[p];
|
|
23
|
-
if (v !== "") styles[p] = v;
|
|
24
|
-
}
|
|
25
|
-
return styles;
|
|
26
|
-
}
|
|
1
|
+
// The engine owns the reduced-motion contract: a widget that writes an
|
|
2
|
+
// `infinite` CSS animation through these helpers is marked so the injected
|
|
3
|
+
// `@media (prefers-reduced-motion: reduce)` rule can reach it, even when the
|
|
4
|
+
// reader flips the OS setting long after the element was built.
|
|
5
|
+
import { markMotionLoop } from "../animations/motion.js";
|
|
27
6
|
|
|
28
7
|
// ============================================================
|
|
29
8
|
// ➕ CREATE / UPDATE: Assign styles
|
|
@@ -81,6 +60,23 @@ const toCSSValue = (key, value) => {
|
|
|
81
60
|
return value;
|
|
82
61
|
};
|
|
83
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Whether a style assignment puts an endless animation on the element.
|
|
65
|
+
*
|
|
66
|
+
* Only a loop is marked: a one-shot entrance finishes on its own, and the
|
|
67
|
+
* reduced-motion rule would then only be noise.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} prop - The CSS property being written.
|
|
70
|
+
* @param {*} value - The value being written.
|
|
71
|
+
* @returns {boolean} True when this write starts an `infinite` animation.
|
|
72
|
+
*/
|
|
73
|
+
const isLoopingAnimation = (prop, value) => {
|
|
74
|
+
if (typeof value !== "string") return false;
|
|
75
|
+
if (prop === "animation") return value.includes("infinite");
|
|
76
|
+
if (prop === "animationIterationCount") return value.trim() === "infinite";
|
|
77
|
+
return false;
|
|
78
|
+
};
|
|
79
|
+
|
|
84
80
|
/**
|
|
85
81
|
* Applies one or more styles to a DOM element.
|
|
86
82
|
*
|
|
@@ -110,12 +106,14 @@ export function setStyles(el, prop, value) {
|
|
|
110
106
|
} catch (e) {
|
|
111
107
|
// Silently ignore errors
|
|
112
108
|
}
|
|
109
|
+
if (isLoopingAnimation(prop, cssValue)) markMotionLoop(el);
|
|
113
110
|
}
|
|
114
111
|
return el;
|
|
115
112
|
}
|
|
116
113
|
|
|
117
114
|
// Case 2: styles object
|
|
118
115
|
if (typeof prop === "object" && prop !== null) {
|
|
116
|
+
let loops = false;
|
|
119
117
|
for (const [key, val] of Object.entries(prop)) {
|
|
120
118
|
const cssValue = toCSSValue(key, val);
|
|
121
119
|
if (cssValue !== null) {
|
|
@@ -124,126 +122,11 @@ export function setStyles(el, prop, value) {
|
|
|
124
122
|
} catch (e) {
|
|
125
123
|
// Silently ignore errors
|
|
126
124
|
}
|
|
125
|
+
if (isLoopingAnimation(key, cssValue)) loops = true;
|
|
127
126
|
}
|
|
128
127
|
}
|
|
128
|
+
if (loops) markMotionLoop(el);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
return el;
|
|
132
132
|
}
|
|
133
|
-
|
|
134
|
-
// ============================================================
|
|
135
|
-
// 🔄 TOGGLE: Toggle styles
|
|
136
|
-
// ============================================================
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Toggles a CSS property between two values.
|
|
140
|
-
*
|
|
141
|
-
* If the property currently equals `val1` it is set to `val2`, and vice versa.
|
|
142
|
-
* Useful for toggling visibility, themes, or active states without external
|
|
143
|
-
* boolean tracking.
|
|
144
|
-
*
|
|
145
|
-
* @param {HTMLElement} el - The target element.
|
|
146
|
-
* @param {string} prop - CSS property name (camelCase).
|
|
147
|
-
* @param {*} val1 - First value (the one to switch away from when current).
|
|
148
|
-
* @param {*} val2 - Second value (the one to switch away from when current).
|
|
149
|
-
* @returns {HTMLElement} The element (for chaining).
|
|
150
|
-
*/
|
|
151
|
-
export function updateStyle(el, prop, val1, val2) {
|
|
152
|
-
if (!el || !prop) return el;
|
|
153
|
-
|
|
154
|
-
const current = el.style[prop] || "";
|
|
155
|
-
const newValue = current === val1 ? val2 : val1;
|
|
156
|
-
const cssValue = toCSSValue(prop, newValue);
|
|
157
|
-
|
|
158
|
-
if (cssValue !== null) {
|
|
159
|
-
try {
|
|
160
|
-
el.style[prop] = cssValue;
|
|
161
|
-
} catch (e) {
|
|
162
|
-
// Silently ignore errors
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return el;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ============================================================
|
|
170
|
-
// 🗑️ DELETE: Remove styles
|
|
171
|
-
// ============================================================
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Removes one or more inline styles from a DOM element by setting them to an
|
|
175
|
-
* empty string (which causes the browser to fall back to inherited / default
|
|
176
|
-
* values).
|
|
177
|
-
*
|
|
178
|
-
* - `string` → removes that single property.
|
|
179
|
-
* - `Array<string>` → removes every listed property.
|
|
180
|
-
* - `undefined` → removes ALL currently set inline styles.
|
|
181
|
-
*
|
|
182
|
-
* @param {HTMLElement} el - The target element.
|
|
183
|
-
* @param {string|string[]|undefined} [prop] - Property or properties to remove.
|
|
184
|
-
* Omit to remove all inline styles.
|
|
185
|
-
* @returns {HTMLElement} The element (for chaining).
|
|
186
|
-
*/
|
|
187
|
-
export function removeStyles(el, prop) {
|
|
188
|
-
if (!el) return el;
|
|
189
|
-
|
|
190
|
-
if (typeof prop === "string") {
|
|
191
|
-
try {
|
|
192
|
-
el.style[prop] = "";
|
|
193
|
-
} catch (e) {}
|
|
194
|
-
} else if (Array.isArray(prop)) {
|
|
195
|
-
prop.forEach((p) => {
|
|
196
|
-
try {
|
|
197
|
-
el.style[p] = "";
|
|
198
|
-
} catch (e) {}
|
|
199
|
-
});
|
|
200
|
-
} else if (prop === undefined) {
|
|
201
|
-
const styles = getStyles(el);
|
|
202
|
-
Object.keys(styles).forEach((p) => {
|
|
203
|
-
try {
|
|
204
|
-
el.style[p] = "";
|
|
205
|
-
} catch (e) {}
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
return el;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ============================================================
|
|
212
|
-
// 🔍 UTILITY: Check styles
|
|
213
|
-
// ============================================================
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Checks whether a specific inline style is set on an element.
|
|
217
|
-
*
|
|
218
|
-
* @param {HTMLElement|null} el - The target element.
|
|
219
|
-
* @param {string} prop - CSS property name (camelCase).
|
|
220
|
-
* @returns {boolean} `true` if the property has a non-empty inline value.
|
|
221
|
-
*/
|
|
222
|
-
export function hasStyle(el, prop) {
|
|
223
|
-
if (!el || !prop) return false;
|
|
224
|
-
return el.style[prop] !== "";
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Logs all inline styles of an element to the console in a readable format.
|
|
229
|
-
* Useful for debugging style issues during development.
|
|
230
|
-
*
|
|
231
|
-
* @param {HTMLElement|null} el - The target element.
|
|
232
|
-
* @param {string} [label="Styles"] - Label prefix shown in the console output.
|
|
233
|
-
* @returns {Object|null} The styles object (same as `getStyles(el)`), or `null`
|
|
234
|
-
* when `el` is falsy.
|
|
235
|
-
*/
|
|
236
|
-
export function logStyles(el, label = "Styles") {
|
|
237
|
-
if (!el) {
|
|
238
|
-
console.warn("⚠️ Invalid element");
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
const styles = getStyles(el);
|
|
242
|
-
console.log(`📋 ${label}:`);
|
|
243
|
-
if (Object.keys(styles).length === 0) {
|
|
244
|
-
console.log(" (No inline styles)");
|
|
245
|
-
} else {
|
|
246
|
-
Object.entries(styles).forEach(([p, v]) => console.log(` ${p}: ${v}`));
|
|
247
|
-
}
|
|
248
|
-
return styles;
|
|
249
|
-
}
|
|
@@ -137,6 +137,11 @@ export const ALL_PROPS = [
|
|
|
137
137
|
{ prop: "justify", domProp: "justifyContent", type: "style", unit: "none", reactive: true },
|
|
138
138
|
{ prop: "justifyContent", domProp: "justifyContent", type: "style", unit: "none", reactive: true },
|
|
139
139
|
{ prop: "alignItems", domProp: "alignItems", type: "style", unit: "none", reactive: true },
|
|
140
|
+
// The child's own answer to the parent's `alignItems`: a control that stays where
|
|
141
|
+
// it wants inside a row that stretches everything else. Routed by the engine, so
|
|
142
|
+
// it belongs to every widget that can be a flex child, not only to the layouts.
|
|
143
|
+
{ prop: "alignSelf", domProp: "alignSelf", type: "style", unit: "none", reactive: true },
|
|
144
|
+
{ prop: "selfAlign", domProp: "alignSelf", type: "style", unit: "none", reactive: true },
|
|
140
145
|
{ prop: "wrap", domProp: "flexWrap", type: "style", unit: "none", reactive: true, boolValues: { true: "wrap", false: "nowrap" } },
|
|
141
146
|
{ prop: "flexWrap", domProp: "flexWrap", type: "style", unit: "none", reactive: true, boolValues: { true: "wrap", false: "nowrap" } },
|
|
142
147
|
{ prop: "flex", domProp: "flex", type: "style", unit: "none", reactive: true },
|
|
@@ -104,7 +104,7 @@ export function WidgetFactory(tag, customProps = {}) {
|
|
|
104
104
|
// ============================================================
|
|
105
105
|
// 3. CREATE BASE WIDGET
|
|
106
106
|
// ============================================================
|
|
107
|
-
const widget = createWidget(finalTag
|
|
107
|
+
const widget = createWidget(finalTag);
|
|
108
108
|
widget._widgetName = widgetName;
|
|
109
109
|
widget._updating = false;
|
|
110
110
|
widget._events = [];
|
package/src/widgets/Accordion.js
CHANGED
|
@@ -41,7 +41,7 @@ const ACCORDION_PROP_KEYS = [
|
|
|
41
41
|
* @param {Function} [props.onToggle] - @deprecated Use `onPress` instead. Kept as alias for backward compatibility.
|
|
42
42
|
* @param {'contained'|'outlined'|'ghost'} [props.variant='contained'] - Visual style variant.
|
|
43
43
|
* @param {number} [props.borderRadius=8] - Corner radius in pixels.
|
|
44
|
-
* @param {string} [props.bgColor=colors.
|
|
44
|
+
* @param {string} [props.bgColor=colors.primaryBg] - Background color of the container (used in 'contained' variant).
|
|
45
45
|
* @param {string} [props.titleColor=colors.text] - Text color of the title when collapsed.
|
|
46
46
|
* @param {string} [props.expandedColor=colors.primary] - Accent color applied to the title-bar background when expanded.
|
|
47
47
|
* @param {string|null} [props.border=null] - Explicit CSS border shorthand. Overrides variant-based border logic.
|
|
@@ -63,6 +63,8 @@ const ACCORDION_PROP_KEYS = [
|
|
|
63
63
|
* @param {number} [props.elevation=0] - Box-shadow depth level (0–4).
|
|
64
64
|
* @returns {HTMLElement} The accordion container element, augmented with public methods.
|
|
65
65
|
*/
|
|
66
|
+
let accordionIdSeed = 0;
|
|
67
|
+
|
|
66
68
|
export const Accordion = (props) => {
|
|
67
69
|
const {
|
|
68
70
|
title,
|
|
@@ -73,7 +75,13 @@ export const Accordion = (props) => {
|
|
|
73
75
|
variant = "contained", // 'contained', 'outlined', 'ghost'
|
|
74
76
|
borderRadius = 8,
|
|
75
77
|
// Colors
|
|
76
|
-
|
|
78
|
+
//
|
|
79
|
+
// A 'contained' panel is a plate, so it has to read as one. `colors.surface`
|
|
80
|
+
// is the card an accordion already sits on — 1.00:1 in dark, 1.04:1 in light —
|
|
81
|
+
// which made the variant indistinguishable from 'ghost'. `primaryBg` is the
|
|
82
|
+
// palette's raised plate: 1.19:1 over the dark card (the overlay Material
|
|
83
|
+
// uses for elevation) and the same near-white step in light.
|
|
84
|
+
bgColor = colors.primaryBg,
|
|
77
85
|
titleColor = colors.text,
|
|
78
86
|
expandedColor = colors.primary,
|
|
79
87
|
// Borders
|
|
@@ -128,6 +136,16 @@ export const Accordion = (props) => {
|
|
|
128
136
|
let currentElevation = elevation;
|
|
129
137
|
let currentAnimationDuration = animationDuration;
|
|
130
138
|
|
|
139
|
+
// The header and its panel reference each other (`aria-controls` one way,
|
|
140
|
+
// `aria-labelledby` back), so both need ids. Derived from the caller's `id`
|
|
141
|
+
// when there is one, the way `Input` does it, so a re-render keeps the pairing.
|
|
142
|
+
const baseId =
|
|
143
|
+
typeof rest.id === "string" && rest.id
|
|
144
|
+
? rest.id
|
|
145
|
+
: `fletbox-accordion-${++accordionIdSeed}`;
|
|
146
|
+
const headerId = `${baseId}-header`;
|
|
147
|
+
const panelId = `${baseId}-panel`;
|
|
148
|
+
|
|
131
149
|
// DOM references populated during widget construction
|
|
132
150
|
let contentWrapper = null;
|
|
133
151
|
let contentInner = null;
|
|
@@ -172,6 +190,13 @@ export const Accordion = (props) => {
|
|
|
172
190
|
titleBar.style.opacity = currentDisabled ? "0.5" : "1";
|
|
173
191
|
titleBar.style.padding = toREM(currentTitlePadding);
|
|
174
192
|
|
|
193
|
+
// `update()` cannot re-run the factory for the header, so the state the
|
|
194
|
+
// header announces lives here: a `disabled: true` update has to take the
|
|
195
|
+
// hinge off the Tab path, and an `expanded` change has to be answered.
|
|
196
|
+
titleBar.setAttribute("aria-expanded", String(isExpanded));
|
|
197
|
+
titleBar.setAttribute("aria-disabled", String(Boolean(currentDisabled)));
|
|
198
|
+
titleBar.tabIndex = currentDisabled ? -1 : 0;
|
|
199
|
+
|
|
175
200
|
// Tint the title bar background slightly when expanded (contained only)
|
|
176
201
|
if (currentVariant === "contained") {
|
|
177
202
|
titleBar.style.backgroundColor = isExpanded
|
|
@@ -181,12 +206,17 @@ export const Accordion = (props) => {
|
|
|
181
206
|
titleBar.style.backgroundColor = "transparent";
|
|
182
207
|
}
|
|
183
208
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
// The header's hairline is the only thing that says "this row is a panel".
|
|
210
|
+
// The default 'contained' variant paints `colors.surface`, which measures
|
|
211
|
+
// 1.00:1 against a dark card and 1.04:1 against a light one, `elevation`
|
|
212
|
+
// starts at 0 and the border belongs to 'outlined' — so a shut accordion used
|
|
213
|
+
// to be a sentence with a chevron on it, and nothing marked it as pressable.
|
|
214
|
+
// The line belongs to the header, not to the gap below it: it is there open
|
|
215
|
+
// or shut, so it never doubles when the content appears.
|
|
216
|
+
titleBar.style.borderBottom = currentDivider
|
|
217
|
+
? `1px solid ${currentDividerColor}`
|
|
218
|
+
: "none";
|
|
219
|
+
contentInner.style.borderTop = "none";
|
|
190
220
|
|
|
191
221
|
// Apply border according to priority: explicit > outlined variant > none
|
|
192
222
|
if (currentBorder) {
|
|
@@ -271,6 +301,10 @@ export const Accordion = (props) => {
|
|
|
271
301
|
|
|
272
302
|
isAnimating = true;
|
|
273
303
|
isExpanded = true;
|
|
304
|
+
// The disclosure state is answered at the press, not when the height
|
|
305
|
+
// animation lands: `updateUI` runs at the end of the transition, and a reader
|
|
306
|
+
// told "collapsed" while the panel is already opening is told a lie.
|
|
307
|
+
titleBar.setAttribute("aria-expanded", "true");
|
|
274
308
|
|
|
275
309
|
// Make the content visible before measuring it
|
|
276
310
|
contentWrapper.style.display = "block";
|
|
@@ -307,6 +341,10 @@ export const Accordion = (props) => {
|
|
|
307
341
|
if (!isExpanded) return;
|
|
308
342
|
|
|
309
343
|
isAnimating = true;
|
|
344
|
+
isExpanded = false;
|
|
345
|
+
// Same as `expand`: the disclosure answers for the state the press caused,
|
|
346
|
+
// not for the height the transition has not finished animating.
|
|
347
|
+
titleBar.setAttribute("aria-expanded", "false");
|
|
310
348
|
|
|
311
349
|
// Pin the height to a pixel value first — "auto" cannot be animated
|
|
312
350
|
const currentHeight = contentInner.scrollHeight + "px";
|
|
@@ -395,6 +433,21 @@ export const Accordion = (props) => {
|
|
|
395
433
|
|
|
396
434
|
// Clickable title bar that contains the text and icon
|
|
397
435
|
titleBar = Row({
|
|
436
|
+
// The press is a prop, not an assignment made after `Row()` has returned:
|
|
437
|
+
// the engine reads `widget.onclick` while the widget is built, and a header
|
|
438
|
+
// wired afterwards is a hinge only a mouse can operate.
|
|
439
|
+
onPress: () => toggle(),
|
|
440
|
+
role: "button",
|
|
441
|
+
id: headerId,
|
|
442
|
+
// A disclosure has to say which way it opens. Without `aria-expanded` a reader
|
|
443
|
+
// hears the same words for a closed section and an open one.
|
|
444
|
+
"aria-expanded": String(isExpanded),
|
|
445
|
+
"aria-controls": panelId,
|
|
446
|
+
"aria-disabled": String(Boolean(currentDisabled)),
|
|
447
|
+
tabIndex: currentDisabled ? -1 : 0,
|
|
448
|
+
// A header spans the whole card; the 2% press scale would move the title and
|
|
449
|
+
// the chevron with it.
|
|
450
|
+
disableTransform: true,
|
|
398
451
|
alignItems: "center",
|
|
399
452
|
justifyContent: "space-between",
|
|
400
453
|
gap: 8,
|
|
@@ -411,6 +464,11 @@ export const Accordion = (props) => {
|
|
|
411
464
|
// Outer wrapper whose height is animated — starts hidden when collapsed
|
|
412
465
|
contentWrapper = WidgetFactory({
|
|
413
466
|
tag: "div",
|
|
467
|
+
id: panelId,
|
|
468
|
+
// The panel is a landmark a reader can jump to from the header; `aria-labelledby`
|
|
469
|
+
// is what names it, since the title lives in the button rather than in a heading.
|
|
470
|
+
role: "region",
|
|
471
|
+
"aria-labelledby": headerId,
|
|
414
472
|
overflow: "hidden",
|
|
415
473
|
transition: animate
|
|
416
474
|
? `height ${currentAnimationDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`
|
|
@@ -436,9 +494,6 @@ export const Accordion = (props) => {
|
|
|
436
494
|
// Sync all styles with current state
|
|
437
495
|
updateUI();
|
|
438
496
|
|
|
439
|
-
// Wire up the click handler on the title bar
|
|
440
|
-
titleBar.onclick = () => toggle();
|
|
441
|
-
|
|
442
497
|
// Start watching for content size changes
|
|
443
498
|
setupResizeObserver();
|
|
444
499
|
|
|
@@ -491,7 +546,10 @@ export const Accordion = (props) => {
|
|
|
491
546
|
}
|
|
492
547
|
}
|
|
493
548
|
}
|
|
494
|
-
if (newProps.disabled !== undefined
|
|
549
|
+
if (newProps.disabled !== undefined && newProps.disabled !== currentDisabled) {
|
|
550
|
+
currentDisabled = newProps.disabled;
|
|
551
|
+
needsUIUpdate = true;
|
|
552
|
+
}
|
|
495
553
|
if (newProps.variant !== undefined) currentVariant = newProps.variant;
|
|
496
554
|
if (newProps.bgColor !== undefined) currentBgColor = newProps.bgColor;
|
|
497
555
|
if (newProps.expandedColor !== undefined)
|
package/src/widgets/AdMob.js
CHANGED
package/src/widgets/AdSense.js
CHANGED
|
@@ -47,21 +47,24 @@ export const AlertDialog = (props) => {
|
|
|
47
47
|
...rest
|
|
48
48
|
} = props;
|
|
49
49
|
|
|
50
|
-
// Map each variant to its icon name, icon
|
|
50
|
+
// Map each variant to its icon name, icon colour, and confirm-button plate.
|
|
51
|
+
// The signal tones (`colors.danger` and friends) are words-on-a-page inks; a
|
|
52
|
+
// filled confirm carries a label, so it takes the plate that label can sit on
|
|
53
|
+
// and lets Button pick the ink from the fill (`readableInk`).
|
|
51
54
|
const variantColors = {
|
|
52
55
|
normal: { confirm: colors.primary, icon: "info", iconColor: colors.info },
|
|
53
56
|
danger: {
|
|
54
|
-
confirm: colors.
|
|
57
|
+
confirm: colors.dangerPlate,
|
|
55
58
|
icon: "warning",
|
|
56
59
|
iconColor: colors.danger,
|
|
57
60
|
},
|
|
58
61
|
warning: {
|
|
59
|
-
confirm: colors.
|
|
62
|
+
confirm: colors.warningPlate,
|
|
60
63
|
icon: "error",
|
|
61
64
|
iconColor: colors.warning,
|
|
62
65
|
},
|
|
63
66
|
success: {
|
|
64
|
-
confirm: colors.
|
|
67
|
+
confirm: colors.successPlate,
|
|
65
68
|
icon: "check_circle",
|
|
66
69
|
iconColor: colors.success,
|
|
67
70
|
},
|
|
@@ -109,9 +112,10 @@ export const AlertDialog = (props) => {
|
|
|
109
112
|
actions.push(
|
|
110
113
|
Button({
|
|
111
114
|
text: cancelText,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
// Cancel is the way out, not the decision: a second filled plate made
|
|
116
|
+
// both answers shout, and the red label measured 3.61:1 on the surface.
|
|
117
|
+
variant: "text",
|
|
118
|
+
color: colors.dangerStrong,
|
|
115
119
|
onPress: () => {
|
|
116
120
|
if (onCancel) onCancel();
|
|
117
121
|
closeModal();
|
|
@@ -125,8 +129,6 @@ export const AlertDialog = (props) => {
|
|
|
125
129
|
text: confirmText,
|
|
126
130
|
variant: "filled",
|
|
127
131
|
bgColor: variantStyle.confirm,
|
|
128
|
-
color: colors.text,
|
|
129
|
-
borderColor: colors.surface,
|
|
130
132
|
onPress: () => {
|
|
131
133
|
if (onConfirm) onConfirm();
|
|
132
134
|
closeModal();
|
package/src/widgets/Avatar.js
CHANGED
|
@@ -142,7 +142,16 @@ export const Avatar = (inputProps = {}) => {
|
|
|
142
142
|
/** Chooses the child element for the props currently held. */
|
|
143
143
|
const buildContent = () => {
|
|
144
144
|
if (src) {
|
|
145
|
-
return Image({
|
|
145
|
+
return Image({
|
|
146
|
+
src,
|
|
147
|
+
width: "100%",
|
|
148
|
+
height: "100%",
|
|
149
|
+
fit: "cover",
|
|
150
|
+
// The avatar is decoration around the person's name, which the screen
|
|
151
|
+
// reads somewhere else. An `alt` here repeats a name the reader already
|
|
152
|
+
// hears; the empty string is the decision, not the omission.
|
|
153
|
+
alt: "",
|
|
154
|
+
});
|
|
146
155
|
}
|
|
147
156
|
if (icon) return Icon({ name: icon, size: size * 0.5, color: textColor });
|
|
148
157
|
if (name) {
|
package/src/widgets/Badge.js
CHANGED
|
@@ -149,10 +149,16 @@ export const Badge = (props) => {
|
|
|
149
149
|
return badgeElement;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
// Wrap the child and the badge in a Stack so the badge is positioned relative to
|
|
152
|
+
// Wrap the child and the badge in a Stack so the badge is positioned relative to
|
|
153
|
+
// the child. The wrapper has to BE the child's box: `display: inline-block` only
|
|
154
|
+
// shrinks to fit inside a block parent, and the common parent here is a flex
|
|
155
|
+
// Column, whose default `align-items: stretch` pulled the anchor to the full row
|
|
156
|
+
// width — so a badge on a 28px icon landed 250px away from it, on the far edge of
|
|
157
|
+
// the card. `alignSelf` is what resists that stretch.
|
|
153
158
|
const badgeStack = Stack({
|
|
154
159
|
position: "relative",
|
|
155
160
|
display: "inline-block",
|
|
161
|
+
alignSelf: "flex-start",
|
|
156
162
|
children: [validChild, badgeElement],
|
|
157
163
|
...rest,
|
|
158
164
|
});
|