flet-box 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +190 -2
- package/bin/cli.js +394 -0
- package/bin/commands/buildAndroid.js +200 -0
- package/bin/commands/buildIOS.js +237 -0
- package/bin/commands/create.js +406 -0
- package/bin/commands/createBundle.js +375 -0
- package/bin/commands/createComponent.js +115 -0
- package/bin/commands/createScreen.js +161 -0
- package/bin/commands/killServer.js +107 -0
- package/bin/commands/package.js +131 -0
- package/bin/commands/release.js +189 -0
- package/bin/commands/runBundle.js +62 -0
- package/bin/commands/runCloudflare.js +89 -0
- package/bin/commands/runNative.js +109 -0
- package/bin/commands/runServer.js +390 -0
- package/bin/ota/loader.js +361 -0
- package/bin/utils/bundleConfig.js +178 -0
- package/bin/utils/capacitor.js +137 -0
- package/bin/utils/colors.js +215 -0
- package/bin/utils/helpers.js +80 -0
- package/bin/utils/spinner.js +96 -0
- package/bin/utils/templates.js +1824 -0
- package/bin/utils/version.js +108 -0
- package/package.json +58 -4
- package/src/animations/AnimatedBox.js +607 -0
- package/src/animations/AnimatedText.js +105 -0
- package/src/animations/MatrixRain.js +220 -0
- package/src/animations/ParallaxBox.js +369 -0
- package/src/animations/animate.js +332 -0
- package/src/animations/index.js +25 -0
- package/src/app.js +41 -0
- package/src/assets/icon-1024-maskable.png +0 -0
- package/src/assets/icon-192-maskable.png +0 -0
- package/src/assets/icon-192.png +0 -0
- package/src/assets/icon-512-maskable.png +0 -0
- package/src/assets/icon-512.png +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/App.js +44 -0
- package/src/core/hmr-client.js +142 -0
- package/src/core/index.js +34 -0
- package/src/core/pwa.js +100 -0
- package/src/core/runApp.js +430 -0
- package/src/create-index.sh +237 -0
- package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
- package/src/fonts/icons.css +23 -0
- package/src/index.d.ts +1930 -0
- package/src/index.js +288 -0
- package/src/navigations/AdaptiveScaffold.js +149 -0
- package/src/navigations/AppBar.js +331 -0
- package/src/navigations/BottomNavigation.js +298 -0
- package/src/navigations/CollapsibleSideBar.js +139 -0
- package/src/navigations/Drawer.js +333 -0
- package/src/navigations/DrawerItem.js +260 -0
- package/src/navigations/Router.js +475 -0
- package/src/navigations/Scaffold.js +537 -0
- package/src/navigations/Tabs.js +384 -0
- package/src/navigations/index.js +57 -0
- package/src/services/RamStore.js +213 -0
- package/src/services/Session.js +310 -0
- package/src/services/Storage.js +309 -0
- package/src/services/http.js +350 -0
- package/src/services/index.js +57 -0
- package/src/tools/animation.js +42 -0
- package/src/tools/array.js +95 -0
- package/src/tools/border.js +28 -0
- package/src/tools/clipboard.js +104 -0
- package/src/tools/color.js +329 -0
- package/src/tools/createList.js +215 -0
- package/src/tools/delay.js +55 -0
- package/src/tools/device.js +138 -0
- package/src/tools/dict.js +297 -0
- package/src/tools/dimensions.js +118 -0
- package/src/tools/filter.js +41 -0
- package/src/tools/flex.js +26 -0
- package/src/tools/gradient.js +34 -0
- package/src/tools/grid.js +57 -0
- package/src/tools/index.js +38 -0
- package/src/tools/mapList.js +107 -0
- package/src/tools/margin.js +93 -0
- package/src/tools/memo.js +60 -0
- package/src/tools/os.js +104 -0
- package/src/tools/padding.js +93 -0
- package/src/tools/print.js +69 -0
- package/src/tools/random.js +299 -0
- package/src/tools/ref.js +70 -0
- package/src/tools/rgba.js +4 -0
- package/src/tools/shadow.js +23 -0
- package/src/tools/string.js +85 -0
- package/src/tools/time.js +674 -0
- package/src/tools/transform.js +68 -0
- package/src/tools/transition.js +48 -0
- package/src/tools/useState.js +268 -0
- package/src/tools/uuid.js +42 -0
- package/src/utils/TextInputValidator.js +256 -0
- package/src/utils/composeUpdate.js +65 -0
- package/src/utils/getWidgetProps.js +35 -0
- package/src/utils/index.js +76 -0
- package/src/utils/markdownParser.js +368 -0
- package/src/utils/mediaTime.js +79 -0
- package/src/utils/navigation.js +263 -0
- package/src/utils/scriptInjector.js +73 -0
- package/src/utils/stackPosition.js +31 -0
- package/src/utils/stopWebRefresh.js +77 -0
- package/src/utils/styleInjector.js +38 -0
- package/src/utils/syntaxHighlight.js +339 -0
- package/src/utils/themes.js +347 -0
- package/src/utils/units.js +87 -0
- package/src/utils/visualEffects.js +169 -0
- package/src/widget-factory/addChildren.js +114 -0
- package/src/widget-factory/assignProps.js +256 -0
- package/src/widget-factory/createWidget.js +28 -0
- package/src/widget-factory/effects.js +132 -0
- package/src/widget-factory/index.js +7 -0
- package/src/widget-factory/lifecycle.js +210 -0
- package/src/widget-factory/makeParentable.js +89 -0
- package/src/widget-factory/pixTool.js +40 -0
- package/src/widget-factory/processProps.js +122 -0
- package/src/widget-factory/reactivity.js +98 -0
- package/src/widget-factory/remTool.js +34 -0
- package/src/widget-factory/stackPosition.js +87 -0
- package/src/widget-factory/stringTool.js +26 -0
- package/src/widget-factory/tools.js +249 -0
- package/src/widget-factory/translateProps.js +1049 -0
- package/src/widget-factory/warnings.js +284 -0
- package/src/widget-factory/widgetFactory.js +200 -0
- package/src/widgets/Accordion.js +562 -0
- package/src/widgets/AdMob.js +315 -0
- package/src/widgets/AdSense.js +268 -0
- package/src/widgets/AlertDialog.js +162 -0
- package/src/widgets/Audio.js +311 -0
- package/src/widgets/Avatar.js +210 -0
- package/src/widgets/Badge.js +189 -0
- package/src/widgets/BottomSheet.js +287 -0
- package/src/widgets/Button.js +247 -0
- package/src/widgets/Card.js +33 -0
- package/src/widgets/Carousel.js +301 -0
- package/src/widgets/Chart.js +632 -0
- package/src/widgets/Checkbox.js +106 -0
- package/src/widgets/Chip.js +153 -0
- package/src/widgets/CircularBar.js +352 -0
- package/src/widgets/CircularChart.js +549 -0
- package/src/widgets/CodeViewer.js +236 -0
- package/src/widgets/Column.js +29 -0
- package/src/widgets/Container.js +31 -0
- package/src/widgets/DataTable.js +257 -0
- package/src/widgets/Divider.js +93 -0
- package/src/widgets/DraggBox.js +115 -0
- package/src/widgets/Dropdown.js +505 -0
- package/src/widgets/DroppBox.js +180 -0
- package/src/widgets/FloatingActionButton.js +91 -0
- package/src/widgets/GridView.js +20 -0
- package/src/widgets/Icon.js +33 -0
- package/src/widgets/Image.js +15 -0
- package/src/widgets/Input.js +565 -0
- package/src/widgets/Inspector.js +254 -0
- package/src/widgets/InstallButton.js +110 -0
- package/src/widgets/ListTile.js +224 -0
- package/src/widgets/ListView.js +503 -0
- package/src/widgets/Markdown.js +1236 -0
- package/src/widgets/Modal.js +496 -0
- package/src/widgets/Pagination.js +195 -0
- package/src/widgets/ProgressBar.js +178 -0
- package/src/widgets/QRCode.js +467 -0
- package/src/widgets/Radio.js +132 -0
- package/src/widgets/Rating.js +261 -0
- package/src/widgets/Row.js +29 -0
- package/src/widgets/Skeleton.js +243 -0
- package/src/widgets/Slider.js +527 -0
- package/src/widgets/SnackBar.js +227 -0
- package/src/widgets/Stack.js +29 -0
- package/src/widgets/Stepper.js +296 -0
- package/src/widgets/Switch.js +102 -0
- package/src/widgets/Text.js +98 -0
- package/src/widgets/Tooltip.js +267 -0
- package/src/widgets/TreeView.js +370 -0
- package/src/widgets/Video.js +92 -0
- package/src/widgets/index.js +52 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Accordion.js
|
|
3
|
+
* @description A collapsible panel widget that reveals or hides its content
|
|
4
|
+
* when the title bar is clicked. Supports smooth height animations, multiple
|
|
5
|
+
* visual variants, elevation shadows, and a ResizeObserver that keeps the
|
|
6
|
+
* expanded height correct when the content changes dynamically.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
10
|
+
import { colors } from "../utils/themes.js";
|
|
11
|
+
import { composeUpdate } from "../utils/composeUpdate.js";
|
|
12
|
+
import { Container } from "./Container.js";
|
|
13
|
+
import { Row } from "./Row.js";
|
|
14
|
+
import { Text } from "./Text.js";
|
|
15
|
+
import { Icon } from "./Icon.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Props owned by the Accordion's own closure state. They are routed to the
|
|
19
|
+
* widget's `update` and never to the factory's (see `composeUpdate`).
|
|
20
|
+
*/
|
|
21
|
+
const ACCORDION_PROP_KEYS = [
|
|
22
|
+
"title", "children", "expanded", "onToggle", "onPress", "variant",
|
|
23
|
+
"borderRadius", "bgColor", "titleColor", "expandedColor",
|
|
24
|
+
"border", "borderColor", "borderWidth",
|
|
25
|
+
"titleSize", "titleWeight", "titlePadding", "contentPadding",
|
|
26
|
+
"iconCollapsed", "iconExpanded", "iconColor", "iconSize",
|
|
27
|
+
"divider", "dividerColor", "disabled", "animate", "animationDuration",
|
|
28
|
+
"elevation",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates an Accordion widget — a collapsible section with an animated title bar.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} props - Configuration options for the accordion.
|
|
35
|
+
* @param {string} props.title - The text displayed in the title bar.
|
|
36
|
+
* @param {HTMLElement|HTMLElement[]} props.children - Content rendered inside the expanded panel.
|
|
37
|
+
* @param {boolean} [props.expanded=false] - Whether the accordion starts in the expanded state.
|
|
38
|
+
* @param {Function} [props.onPress] - Callback fired when the expanded state changes. Receives `(isExpanded: boolean)`.
|
|
39
|
+
* @param {Function} [props.onToggle] - @deprecated Use `onPress` instead. Kept as alias for backward compatibility.
|
|
40
|
+
* @param {'contained'|'outlined'|'ghost'} [props.variant='contained'] - Visual style variant.
|
|
41
|
+
* @param {number} [props.borderRadius=8] - Corner radius in pixels.
|
|
42
|
+
* @param {string} [props.bgColor=colors.surface] - Background color of the container (used in 'contained' variant).
|
|
43
|
+
* @param {string} [props.titleColor=colors.text] - Text color of the title when collapsed.
|
|
44
|
+
* @param {string} [props.expandedColor=colors.primary] - Accent color applied to the title and background when expanded.
|
|
45
|
+
* @param {string|null} [props.border=null] - Explicit CSS border shorthand. Overrides variant-based border logic.
|
|
46
|
+
* @param {string} [props.borderColor=colors.border] - Border color used in 'outlined' variant.
|
|
47
|
+
* @param {number} [props.borderWidth=1] - Border width in pixels (outlined variant).
|
|
48
|
+
* @param {number} [props.titleSize=14] - Font size of the title text in pixels.
|
|
49
|
+
* @param {string} [props.titleWeight='500'] - Font weight of the title text.
|
|
50
|
+
* @param {string} [props.titlePadding='12px 16px'] - CSS padding for the title bar.
|
|
51
|
+
* @param {string} [props.contentPadding='16px'] - CSS padding applied to the inner content area.
|
|
52
|
+
* @param {string} [props.iconCollapsed='chevron_right'] - Material icon name shown when collapsed.
|
|
53
|
+
* @param {string} [props.iconExpanded='expand_more'] - Material icon name shown when expanded.
|
|
54
|
+
* @param {string} [props.iconColor=colors.textSecondary] - Color of the expand/collapse icon.
|
|
55
|
+
* @param {number} [props.iconSize=20] - Size of the icon in pixels.
|
|
56
|
+
* @param {boolean} [props.divider=true] - Whether to show a dividing line between the title and the content.
|
|
57
|
+
* @param {string} [props.dividerColor=colors.border] - Color of the dividing line.
|
|
58
|
+
* @param {boolean} [props.disabled=false] - When true, interactions are blocked and the component is visually dimmed.
|
|
59
|
+
* @param {boolean} [props.animate=true] - Whether to animate the expand/collapse transition.
|
|
60
|
+
* @param {number} [props.animationDuration=300] - Duration of the animation in milliseconds.
|
|
61
|
+
* @param {number} [props.elevation=0] - Box-shadow depth level (0–4).
|
|
62
|
+
* @returns {HTMLElement} The accordion container element, augmented with public methods.
|
|
63
|
+
*/
|
|
64
|
+
export const Accordion = (props) => {
|
|
65
|
+
const {
|
|
66
|
+
title,
|
|
67
|
+
children,
|
|
68
|
+
expanded = false,
|
|
69
|
+
onToggle,
|
|
70
|
+
onPress,
|
|
71
|
+
variant = "contained", // 'contained', 'outlined', 'ghost'
|
|
72
|
+
borderRadius = 8,
|
|
73
|
+
// Colors
|
|
74
|
+
bgColor = colors.surface,
|
|
75
|
+
titleColor = colors.text,
|
|
76
|
+
expandedColor = colors.primary,
|
|
77
|
+
// Borders
|
|
78
|
+
border = null,
|
|
79
|
+
borderColor = colors.border,
|
|
80
|
+
borderWidth = 1,
|
|
81
|
+
// Title
|
|
82
|
+
titleSize = 14,
|
|
83
|
+
titleWeight = "500",
|
|
84
|
+
titlePadding = "12px 16px",
|
|
85
|
+
// Content
|
|
86
|
+
contentPadding = "16px",
|
|
87
|
+
// Icon
|
|
88
|
+
iconCollapsed = "chevron_right",
|
|
89
|
+
iconExpanded = "expand_more",
|
|
90
|
+
iconColor = colors.textSecondary,
|
|
91
|
+
iconSize = 20,
|
|
92
|
+
// Divider
|
|
93
|
+
divider = true,
|
|
94
|
+
dividerColor = colors.border,
|
|
95
|
+
// State
|
|
96
|
+
disabled = false,
|
|
97
|
+
animate = true,
|
|
98
|
+
animationDuration = 300,
|
|
99
|
+
elevation = 0,
|
|
100
|
+
...rest
|
|
101
|
+
} = props;
|
|
102
|
+
|
|
103
|
+
// Internal mutable state — mirrors the props so updates don't mutate the originals
|
|
104
|
+
let isExpanded = expanded;
|
|
105
|
+
let currentTitle = title;
|
|
106
|
+
let currentChildren = children;
|
|
107
|
+
let currentDisabled = disabled;
|
|
108
|
+
let currentVariant = variant;
|
|
109
|
+
let currentBgColor = bgColor;
|
|
110
|
+
let currentExpandedColor = expandedColor;
|
|
111
|
+
let currentTitleColor = titleColor;
|
|
112
|
+
let currentTitleSize = titleSize;
|
|
113
|
+
let currentTitleWeight = titleWeight;
|
|
114
|
+
let currentTitlePadding = titlePadding;
|
|
115
|
+
let currentContentPadding = contentPadding;
|
|
116
|
+
let currentIconCollapsed = iconCollapsed;
|
|
117
|
+
let currentIconExpanded = iconExpanded;
|
|
118
|
+
let currentIconColor = iconColor;
|
|
119
|
+
let currentIconSize = iconSize;
|
|
120
|
+
let currentDivider = divider;
|
|
121
|
+
let currentDividerColor = dividerColor;
|
|
122
|
+
let currentBorderRadius = borderRadius;
|
|
123
|
+
let currentBorder = border;
|
|
124
|
+
let currentBorderColor = borderColor;
|
|
125
|
+
let currentBorderWidth = borderWidth;
|
|
126
|
+
let currentElevation = elevation;
|
|
127
|
+
let currentAnimationDuration = animationDuration;
|
|
128
|
+
|
|
129
|
+
// DOM references populated during widget construction
|
|
130
|
+
let contentWrapper = null;
|
|
131
|
+
let contentInner = null;
|
|
132
|
+
let titleBar = null;
|
|
133
|
+
let titleElement = null;
|
|
134
|
+
let iconElement = null;
|
|
135
|
+
let resizeObserver = null;
|
|
136
|
+
let isAnimating = false;
|
|
137
|
+
|
|
138
|
+
// ========== UI UPDATE FUNCTIONS ==========
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Synchronises all visual properties (colors, borders, icon, etc.) to the
|
|
142
|
+
* current state without rebuilding the DOM. Called after any prop change.
|
|
143
|
+
*/
|
|
144
|
+
const updateUI = () => {
|
|
145
|
+
if (!titleElement || !iconElement || !titleBar || !contentInner) return;
|
|
146
|
+
|
|
147
|
+
// Update title text only when it actually changed, to avoid unnecessary repaints
|
|
148
|
+
if (titleElement.textContent !== currentTitle) {
|
|
149
|
+
titleElement.textContent = currentTitle;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Title color shifts to the accent color when expanded
|
|
153
|
+
titleElement.style.color = isExpanded
|
|
154
|
+
? currentExpandedColor
|
|
155
|
+
: currentTitleColor;
|
|
156
|
+
titleElement.style.fontSize = `${currentTitleSize}px`;
|
|
157
|
+
titleElement.style.fontWeight = currentTitleWeight;
|
|
158
|
+
|
|
159
|
+
// Swap icon based on current expanded state
|
|
160
|
+
iconElement.setAttribute(
|
|
161
|
+
"name",
|
|
162
|
+
isExpanded ? currentIconExpanded : currentIconCollapsed,
|
|
163
|
+
);
|
|
164
|
+
iconElement.style.color = currentIconColor;
|
|
165
|
+
iconElement.style.fontSize = `${currentIconSize}px`;
|
|
166
|
+
|
|
167
|
+
// Title bar cursor and opacity reflect the disabled state
|
|
168
|
+
titleBar.style.cursor = currentDisabled ? "not-allowed" : "pointer";
|
|
169
|
+
titleBar.style.opacity = currentDisabled ? "0.5" : "1";
|
|
170
|
+
titleBar.style.padding = currentTitlePadding;
|
|
171
|
+
|
|
172
|
+
// Tint the title bar background slightly when expanded (contained only)
|
|
173
|
+
if (currentVariant === "contained") {
|
|
174
|
+
titleBar.style.backgroundColor = isExpanded
|
|
175
|
+
? `${currentExpandedColor}10`
|
|
176
|
+
: "transparent";
|
|
177
|
+
} else {
|
|
178
|
+
titleBar.style.backgroundColor = "transparent";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Show/hide the horizontal divider between title and content
|
|
182
|
+
if (currentDivider && isExpanded) {
|
|
183
|
+
contentInner.style.borderTop = `1px solid ${currentDividerColor}`;
|
|
184
|
+
} else {
|
|
185
|
+
contentInner.style.borderTop = "none";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Apply border according to priority: explicit > outlined variant > none
|
|
189
|
+
if (currentBorder) {
|
|
190
|
+
container.style.border = currentBorder;
|
|
191
|
+
} else if (currentVariant === "outlined") {
|
|
192
|
+
container.style.border = `${currentBorderWidth}px solid ${currentBorderColor}`;
|
|
193
|
+
} else {
|
|
194
|
+
container.style.border = "none";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Background is only painted in 'contained' mode
|
|
198
|
+
if (currentVariant === "contained") {
|
|
199
|
+
container.style.backgroundColor = currentBgColor;
|
|
200
|
+
} else {
|
|
201
|
+
container.style.backgroundColor = "transparent";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
container.style.borderRadius = `${currentBorderRadius}px`;
|
|
205
|
+
container.style.overflow = "hidden";
|
|
206
|
+
|
|
207
|
+
// Predefined shadow presets keyed by elevation level
|
|
208
|
+
if (currentElevation > 0) {
|
|
209
|
+
const shadows = {
|
|
210
|
+
1: "0 1px 3px rgba(0,0,0,0.12)",
|
|
211
|
+
2: "0 3px 6px rgba(0,0,0,0.16)",
|
|
212
|
+
3: "0 6px 12px rgba(0,0,0,0.2)",
|
|
213
|
+
4: "0 10px 20px rgba(0,0,0,0.25)",
|
|
214
|
+
};
|
|
215
|
+
container.style.boxShadow = shadows[currentElevation] || shadows[2];
|
|
216
|
+
} else {
|
|
217
|
+
container.style.boxShadow = "none";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
contentInner.style.padding = currentContentPadding;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// ========== HEIGHT ANIMATION ==========
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Accurately measures the natural height of the content by cloning it into
|
|
227
|
+
* the DOM off-screen. This avoids the 0-height problem when the element is
|
|
228
|
+
* hidden (display: none) at measurement time.
|
|
229
|
+
*
|
|
230
|
+
* @returns {number} The pixel height the content would occupy when visible.
|
|
231
|
+
*/
|
|
232
|
+
const getContentHeight = () => {
|
|
233
|
+
if (!contentInner) return 0;
|
|
234
|
+
// Clone off-screen to measure true height regardless of current visibility
|
|
235
|
+
const clone = contentInner.cloneNode(true);
|
|
236
|
+
clone.style.position = "absolute";
|
|
237
|
+
clone.style.visibility = "hidden";
|
|
238
|
+
clone.style.display = "block";
|
|
239
|
+
clone.style.height = "auto";
|
|
240
|
+
clone.style.padding = currentContentPadding;
|
|
241
|
+
document.body.appendChild(clone);
|
|
242
|
+
const height = clone.offsetHeight;
|
|
243
|
+
document.body.removeChild(clone);
|
|
244
|
+
return height;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Sets the wrapper height, optionally skipping the CSS transition.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} height - CSS height value (e.g. "250px" or "auto").
|
|
251
|
+
* @param {boolean} [instant=false] - When true, skips the transition entirely.
|
|
252
|
+
*/
|
|
253
|
+
const setHeight = (height, instant = false) => {
|
|
254
|
+
if (!contentWrapper) return;
|
|
255
|
+
if (animate && !instant) {
|
|
256
|
+
contentWrapper.style.transition = `height ${currentAnimationDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`;
|
|
257
|
+
contentWrapper.style.height = height;
|
|
258
|
+
} else {
|
|
259
|
+
contentWrapper.style.transition = "none";
|
|
260
|
+
contentWrapper.style.height = height;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Expands the accordion with an animated height transition.
|
|
266
|
+
* No-ops if already expanded or currently animating.
|
|
267
|
+
*
|
|
268
|
+
* @param {boolean} [triggerCallback=true] - Whether to fire the `onToggle` callback.
|
|
269
|
+
*/
|
|
270
|
+
const expand = (triggerCallback = true) => {
|
|
271
|
+
if (currentDisabled || isAnimating) return;
|
|
272
|
+
if (isExpanded) return;
|
|
273
|
+
|
|
274
|
+
isAnimating = true;
|
|
275
|
+
isExpanded = true;
|
|
276
|
+
|
|
277
|
+
// Make the content visible before measuring it
|
|
278
|
+
contentWrapper.style.display = "block";
|
|
279
|
+
contentInner.style.display = "block";
|
|
280
|
+
|
|
281
|
+
// Force a reflow so the browser registers the block display before we animate
|
|
282
|
+
void contentWrapper.offsetHeight;
|
|
283
|
+
|
|
284
|
+
const targetHeight = getContentHeight() + "px";
|
|
285
|
+
|
|
286
|
+
// Animate from 0 → measured height → "auto" (to stay responsive to content changes)
|
|
287
|
+
setHeight("0px");
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
setHeight(targetHeight);
|
|
290
|
+
setTimeout(() => {
|
|
291
|
+
setHeight("auto", true);
|
|
292
|
+
contentWrapper.style.overflow = "visible";
|
|
293
|
+
isAnimating = false;
|
|
294
|
+
const expandCb = onToggle || onPress;
|
|
295
|
+
if (triggerCallback && expandCb) expandCb(true);
|
|
296
|
+
updateUI();
|
|
297
|
+
}, currentAnimationDuration);
|
|
298
|
+
}, 10);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Collapses the accordion with an animated height transition.
|
|
303
|
+
* No-ops if already collapsed or currently animating.
|
|
304
|
+
*
|
|
305
|
+
* @param {boolean} [triggerCallback=true] - Whether to fire the `onToggle` callback.
|
|
306
|
+
*/
|
|
307
|
+
const collapse = (triggerCallback = true) => {
|
|
308
|
+
if (currentDisabled || isAnimating) return;
|
|
309
|
+
if (!isExpanded) return;
|
|
310
|
+
|
|
311
|
+
isAnimating = true;
|
|
312
|
+
|
|
313
|
+
// Pin the height to a pixel value first — "auto" cannot be animated
|
|
314
|
+
const currentHeight = contentInner.scrollHeight + "px";
|
|
315
|
+
contentWrapper.style.overflow = "hidden";
|
|
316
|
+
setHeight(currentHeight);
|
|
317
|
+
|
|
318
|
+
setTimeout(() => {
|
|
319
|
+
setHeight("0px");
|
|
320
|
+
isExpanded = false;
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
contentWrapper.style.display = "none";
|
|
323
|
+
contentWrapper.style.overflow = "";
|
|
324
|
+
contentInner.style.display = "";
|
|
325
|
+
isAnimating = false;
|
|
326
|
+
const collapseCb = onToggle || onPress;
|
|
327
|
+
if (triggerCallback && collapseCb) collapseCb(false);
|
|
328
|
+
updateUI();
|
|
329
|
+
}, currentAnimationDuration);
|
|
330
|
+
}, 10);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Toggles between expanded and collapsed states.
|
|
335
|
+
* No-ops when disabled or mid-animation.
|
|
336
|
+
*/
|
|
337
|
+
const toggle = () => {
|
|
338
|
+
if (currentDisabled || isAnimating) return;
|
|
339
|
+
if (isExpanded) collapse();
|
|
340
|
+
else expand();
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// ========== RESIZE OBSERVER ==========
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Attaches a ResizeObserver to the inner content element so that if its
|
|
347
|
+
* height changes (e.g. dynamic children added), the wrapper height is updated
|
|
348
|
+
* to match without requiring a manual re-render.
|
|
349
|
+
*/
|
|
350
|
+
const setupResizeObserver = () => {
|
|
351
|
+
if (!contentInner || !animate) return;
|
|
352
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
353
|
+
resizeObserver = new ResizeObserver(() => {
|
|
354
|
+
if (isExpanded && contentWrapper && !isAnimating) {
|
|
355
|
+
const newHeight = getContentHeight() + "px";
|
|
356
|
+
// Only adjust when the height is a fixed pixel value (not "auto")
|
|
357
|
+
if (
|
|
358
|
+
contentWrapper.style.height !== "auto" &&
|
|
359
|
+
contentWrapper.style.height !== newHeight
|
|
360
|
+
) {
|
|
361
|
+
setHeight(newHeight);
|
|
362
|
+
setTimeout(() => setHeight("auto", true), currentAnimationDuration);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
resizeObserver.observe(contentInner);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// ========== WIDGET CONSTRUCTION ==========
|
|
371
|
+
|
|
372
|
+
const container = Container({
|
|
373
|
+
widgetName: "Accordion",
|
|
374
|
+
width: "100%",
|
|
375
|
+
overflow: "hidden",
|
|
376
|
+
...rest,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Title text element
|
|
380
|
+
titleElement = Text({
|
|
381
|
+
text: currentTitle,
|
|
382
|
+
size: currentTitleSize,
|
|
383
|
+
weight: currentTitleWeight,
|
|
384
|
+
color: isExpanded ? currentExpandedColor : currentTitleColor,
|
|
385
|
+
flex: 1,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Chevron/expand icon
|
|
389
|
+
iconElement = Icon({
|
|
390
|
+
name: isExpanded ? currentIconExpanded : currentIconCollapsed,
|
|
391
|
+
size: currentIconSize,
|
|
392
|
+
color: currentIconColor,
|
|
393
|
+
transition: animate
|
|
394
|
+
? `transform ${currentAnimationDuration}ms ease`
|
|
395
|
+
: "none",
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Clickable title bar that contains the text and icon
|
|
399
|
+
titleBar = Row({
|
|
400
|
+
alignItems: "center",
|
|
401
|
+
justifyContent: "space-between",
|
|
402
|
+
gap: 8,
|
|
403
|
+
padding: currentTitlePadding,
|
|
404
|
+
cursor: currentDisabled ? "not-allowed" : "pointer",
|
|
405
|
+
opacity: currentDisabled ? 0.5 : 1,
|
|
406
|
+
backgroundColor:
|
|
407
|
+
isExpanded && currentVariant === "contained"
|
|
408
|
+
? `${currentExpandedColor}10`
|
|
409
|
+
: "transparent",
|
|
410
|
+
children: [titleElement, iconElement],
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Outer wrapper whose height is animated — starts hidden when collapsed
|
|
414
|
+
contentWrapper = WidgetFactory({
|
|
415
|
+
tag: "div",
|
|
416
|
+
overflow: "hidden",
|
|
417
|
+
transition: animate
|
|
418
|
+
? `height ${currentAnimationDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`
|
|
419
|
+
: "none",
|
|
420
|
+
height: isExpanded ? "auto" : "0",
|
|
421
|
+
display: isExpanded ? "block" : "none",
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Inner container holds the actual slot content; not animated directly
|
|
425
|
+
contentInner = Container({
|
|
426
|
+
padding: currentContentPadding,
|
|
427
|
+
borderTop:
|
|
428
|
+
currentDivider && isExpanded
|
|
429
|
+
? `1px solid ${currentDividerColor}`
|
|
430
|
+
: "none",
|
|
431
|
+
children: currentChildren,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
contentWrapper.appendChild(contentInner);
|
|
435
|
+
container.appendChild(titleBar);
|
|
436
|
+
container.appendChild(contentWrapper);
|
|
437
|
+
|
|
438
|
+
// Sync all styles with current state
|
|
439
|
+
updateUI();
|
|
440
|
+
|
|
441
|
+
// Wire up the click handler on the title bar
|
|
442
|
+
titleBar.onclick = () => toggle();
|
|
443
|
+
|
|
444
|
+
// Start watching for content size changes
|
|
445
|
+
setupResizeObserver();
|
|
446
|
+
|
|
447
|
+
// ========== PUBLIC API ==========
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Programmatically sets the expanded state.
|
|
451
|
+
*
|
|
452
|
+
* @param {boolean} exp - `true` to expand, `false` to collapse.
|
|
453
|
+
* @param {boolean} [triggerCallback=true] - Whether to fire the `onToggle` callback.
|
|
454
|
+
*/
|
|
455
|
+
const setExpanded = (exp, triggerCallback = true) => {
|
|
456
|
+
if (exp === isExpanded) return;
|
|
457
|
+
if (exp) expand(triggerCallback);
|
|
458
|
+
else collapse(triggerCallback);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Updates one or more props without rebuilding the component.
|
|
463
|
+
* Only the fields present in `newProps` are applied; others are unchanged.
|
|
464
|
+
*
|
|
465
|
+
* @param {Partial<Object>} newProps - The subset of props to update.
|
|
466
|
+
*/
|
|
467
|
+
const update = (newProps) => {
|
|
468
|
+
let needsUIUpdate = false;
|
|
469
|
+
|
|
470
|
+
if (newProps.title !== undefined && newProps.title !== currentTitle) {
|
|
471
|
+
currentTitle = newProps.title;
|
|
472
|
+
needsUIUpdate = true;
|
|
473
|
+
}
|
|
474
|
+
if (newProps.children !== undefined) {
|
|
475
|
+
currentChildren = newProps.children;
|
|
476
|
+
// Replace the slot content in place
|
|
477
|
+
while (contentInner.firstChild)
|
|
478
|
+
contentInner.removeChild(contentInner.firstChild);
|
|
479
|
+
const childrenArray = Array.isArray(currentChildren)
|
|
480
|
+
? currentChildren
|
|
481
|
+
: [currentChildren];
|
|
482
|
+
childrenArray.forEach((child) => {
|
|
483
|
+
if (child instanceof HTMLElement) contentInner.appendChild(child);
|
|
484
|
+
else if (typeof child === "string")
|
|
485
|
+
contentInner.appendChild(document.createTextNode(child));
|
|
486
|
+
});
|
|
487
|
+
// If currently expanded, recalculate the wrapper height to fit new content
|
|
488
|
+
if (isExpanded && animate && !isAnimating) {
|
|
489
|
+
const newHeight = getContentHeight() + "px";
|
|
490
|
+
if (contentWrapper.style.height !== "auto") {
|
|
491
|
+
setHeight(newHeight);
|
|
492
|
+
setTimeout(() => setHeight("auto", true), currentAnimationDuration);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (newProps.disabled !== undefined) currentDisabled = newProps.disabled;
|
|
497
|
+
if (newProps.variant !== undefined) currentVariant = newProps.variant;
|
|
498
|
+
if (newProps.bgColor !== undefined) currentBgColor = newProps.bgColor;
|
|
499
|
+
if (newProps.expandedColor !== undefined)
|
|
500
|
+
currentExpandedColor = newProps.expandedColor;
|
|
501
|
+
if (newProps.titleColor !== undefined)
|
|
502
|
+
currentTitleColor = newProps.titleColor;
|
|
503
|
+
if (newProps.titleSize !== undefined) currentTitleSize = newProps.titleSize;
|
|
504
|
+
if (newProps.titleWeight !== undefined)
|
|
505
|
+
currentTitleWeight = newProps.titleWeight;
|
|
506
|
+
if (newProps.titlePadding !== undefined)
|
|
507
|
+
currentTitlePadding = newProps.titlePadding;
|
|
508
|
+
if (newProps.contentPadding !== undefined) {
|
|
509
|
+
currentContentPadding = newProps.contentPadding;
|
|
510
|
+
contentInner.style.padding = currentContentPadding;
|
|
511
|
+
}
|
|
512
|
+
if (newProps.iconCollapsed !== undefined)
|
|
513
|
+
currentIconCollapsed = newProps.iconCollapsed;
|
|
514
|
+
if (newProps.iconExpanded !== undefined)
|
|
515
|
+
currentIconExpanded = newProps.iconExpanded;
|
|
516
|
+
if (newProps.iconColor !== undefined) currentIconColor = newProps.iconColor;
|
|
517
|
+
if (newProps.iconSize !== undefined) currentIconSize = newProps.iconSize;
|
|
518
|
+
if (newProps.divider !== undefined) currentDivider = newProps.divider;
|
|
519
|
+
if (newProps.dividerColor !== undefined)
|
|
520
|
+
currentDividerColor = newProps.dividerColor;
|
|
521
|
+
if (newProps.borderRadius !== undefined)
|
|
522
|
+
currentBorderRadius = newProps.borderRadius;
|
|
523
|
+
if (newProps.border !== undefined) currentBorder = newProps.border;
|
|
524
|
+
if (newProps.borderColor !== undefined)
|
|
525
|
+
currentBorderColor = newProps.borderColor;
|
|
526
|
+
if (newProps.borderWidth !== undefined)
|
|
527
|
+
currentBorderWidth = newProps.borderWidth;
|
|
528
|
+
if (newProps.elevation !== undefined) currentElevation = newProps.elevation;
|
|
529
|
+
if (newProps.animationDuration !== undefined)
|
|
530
|
+
currentAnimationDuration = newProps.animationDuration;
|
|
531
|
+
|
|
532
|
+
if (needsUIUpdate) updateUI();
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Reactive property accessors so callers can use `accordion.expanded = true`
|
|
536
|
+
Object.defineProperty(container, "expanded", {
|
|
537
|
+
get: () => isExpanded,
|
|
538
|
+
set: (val) => setExpanded(val, true),
|
|
539
|
+
});
|
|
540
|
+
Object.defineProperty(container, "disabled", {
|
|
541
|
+
get: () => currentDisabled,
|
|
542
|
+
set: (val) => update({ disabled: val }),
|
|
543
|
+
});
|
|
544
|
+
container.setExpanded = setExpanded;
|
|
545
|
+
container.toggle = toggle;
|
|
546
|
+
composeUpdate(container, ACCORDION_PROP_KEYS, update);
|
|
547
|
+
|
|
548
|
+
// ========== CLEANUP ==========
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Disconnects the ResizeObserver and propagates cleanup to child widgets.
|
|
552
|
+
* Registered through `onUnmount` so it runs on every teardown path.
|
|
553
|
+
*/
|
|
554
|
+
container.onUnmount(() => {
|
|
555
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
556
|
+
if (contentWrapper && contentWrapper._cleanup) contentWrapper._cleanup();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return container;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
export default Accordion;
|