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.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -2
  3. package/bin/cli.js +394 -0
  4. package/bin/commands/buildAndroid.js +200 -0
  5. package/bin/commands/buildIOS.js +237 -0
  6. package/bin/commands/create.js +406 -0
  7. package/bin/commands/createBundle.js +375 -0
  8. package/bin/commands/createComponent.js +115 -0
  9. package/bin/commands/createScreen.js +161 -0
  10. package/bin/commands/killServer.js +107 -0
  11. package/bin/commands/package.js +131 -0
  12. package/bin/commands/release.js +189 -0
  13. package/bin/commands/runBundle.js +62 -0
  14. package/bin/commands/runCloudflare.js +89 -0
  15. package/bin/commands/runNative.js +109 -0
  16. package/bin/commands/runServer.js +390 -0
  17. package/bin/ota/loader.js +361 -0
  18. package/bin/utils/bundleConfig.js +178 -0
  19. package/bin/utils/capacitor.js +137 -0
  20. package/bin/utils/colors.js +215 -0
  21. package/bin/utils/helpers.js +80 -0
  22. package/bin/utils/spinner.js +96 -0
  23. package/bin/utils/templates.js +1824 -0
  24. package/bin/utils/version.js +108 -0
  25. package/package.json +58 -4
  26. package/src/animations/AnimatedBox.js +607 -0
  27. package/src/animations/AnimatedText.js +105 -0
  28. package/src/animations/MatrixRain.js +220 -0
  29. package/src/animations/ParallaxBox.js +369 -0
  30. package/src/animations/animate.js +332 -0
  31. package/src/animations/index.js +25 -0
  32. package/src/app.js +41 -0
  33. package/src/assets/icon-1024-maskable.png +0 -0
  34. package/src/assets/icon-192-maskable.png +0 -0
  35. package/src/assets/icon-192.png +0 -0
  36. package/src/assets/icon-512-maskable.png +0 -0
  37. package/src/assets/icon-512.png +0 -0
  38. package/src/assets/logo.png +0 -0
  39. package/src/core/App.js +44 -0
  40. package/src/core/hmr-client.js +142 -0
  41. package/src/core/index.js +34 -0
  42. package/src/core/pwa.js +100 -0
  43. package/src/core/runApp.js +430 -0
  44. package/src/create-index.sh +237 -0
  45. package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
  46. package/src/fonts/icons.css +23 -0
  47. package/src/index.d.ts +1930 -0
  48. package/src/index.js +288 -0
  49. package/src/navigations/AdaptiveScaffold.js +149 -0
  50. package/src/navigations/AppBar.js +331 -0
  51. package/src/navigations/BottomNavigation.js +298 -0
  52. package/src/navigations/CollapsibleSideBar.js +139 -0
  53. package/src/navigations/Drawer.js +333 -0
  54. package/src/navigations/DrawerItem.js +260 -0
  55. package/src/navigations/Router.js +475 -0
  56. package/src/navigations/Scaffold.js +537 -0
  57. package/src/navigations/Tabs.js +384 -0
  58. package/src/navigations/index.js +57 -0
  59. package/src/services/RamStore.js +213 -0
  60. package/src/services/Session.js +310 -0
  61. package/src/services/Storage.js +309 -0
  62. package/src/services/http.js +350 -0
  63. package/src/services/index.js +57 -0
  64. package/src/tools/animation.js +42 -0
  65. package/src/tools/array.js +95 -0
  66. package/src/tools/border.js +28 -0
  67. package/src/tools/clipboard.js +104 -0
  68. package/src/tools/color.js +329 -0
  69. package/src/tools/createList.js +215 -0
  70. package/src/tools/delay.js +55 -0
  71. package/src/tools/device.js +138 -0
  72. package/src/tools/dict.js +297 -0
  73. package/src/tools/dimensions.js +118 -0
  74. package/src/tools/filter.js +41 -0
  75. package/src/tools/flex.js +26 -0
  76. package/src/tools/gradient.js +34 -0
  77. package/src/tools/grid.js +57 -0
  78. package/src/tools/index.js +38 -0
  79. package/src/tools/mapList.js +107 -0
  80. package/src/tools/margin.js +93 -0
  81. package/src/tools/memo.js +60 -0
  82. package/src/tools/os.js +104 -0
  83. package/src/tools/padding.js +93 -0
  84. package/src/tools/print.js +69 -0
  85. package/src/tools/random.js +299 -0
  86. package/src/tools/ref.js +70 -0
  87. package/src/tools/rgba.js +4 -0
  88. package/src/tools/shadow.js +23 -0
  89. package/src/tools/string.js +85 -0
  90. package/src/tools/time.js +674 -0
  91. package/src/tools/transform.js +68 -0
  92. package/src/tools/transition.js +48 -0
  93. package/src/tools/useState.js +268 -0
  94. package/src/tools/uuid.js +42 -0
  95. package/src/utils/TextInputValidator.js +256 -0
  96. package/src/utils/composeUpdate.js +65 -0
  97. package/src/utils/getWidgetProps.js +35 -0
  98. package/src/utils/index.js +76 -0
  99. package/src/utils/markdownParser.js +368 -0
  100. package/src/utils/mediaTime.js +79 -0
  101. package/src/utils/navigation.js +263 -0
  102. package/src/utils/scriptInjector.js +73 -0
  103. package/src/utils/stackPosition.js +31 -0
  104. package/src/utils/stopWebRefresh.js +77 -0
  105. package/src/utils/styleInjector.js +38 -0
  106. package/src/utils/syntaxHighlight.js +339 -0
  107. package/src/utils/themes.js +347 -0
  108. package/src/utils/units.js +87 -0
  109. package/src/utils/visualEffects.js +169 -0
  110. package/src/widget-factory/addChildren.js +114 -0
  111. package/src/widget-factory/assignProps.js +256 -0
  112. package/src/widget-factory/createWidget.js +28 -0
  113. package/src/widget-factory/effects.js +132 -0
  114. package/src/widget-factory/index.js +7 -0
  115. package/src/widget-factory/lifecycle.js +210 -0
  116. package/src/widget-factory/makeParentable.js +89 -0
  117. package/src/widget-factory/pixTool.js +40 -0
  118. package/src/widget-factory/processProps.js +122 -0
  119. package/src/widget-factory/reactivity.js +98 -0
  120. package/src/widget-factory/remTool.js +34 -0
  121. package/src/widget-factory/stackPosition.js +87 -0
  122. package/src/widget-factory/stringTool.js +26 -0
  123. package/src/widget-factory/tools.js +249 -0
  124. package/src/widget-factory/translateProps.js +1049 -0
  125. package/src/widget-factory/warnings.js +284 -0
  126. package/src/widget-factory/widgetFactory.js +200 -0
  127. package/src/widgets/Accordion.js +562 -0
  128. package/src/widgets/AdMob.js +315 -0
  129. package/src/widgets/AdSense.js +268 -0
  130. package/src/widgets/AlertDialog.js +162 -0
  131. package/src/widgets/Audio.js +311 -0
  132. package/src/widgets/Avatar.js +210 -0
  133. package/src/widgets/Badge.js +189 -0
  134. package/src/widgets/BottomSheet.js +287 -0
  135. package/src/widgets/Button.js +247 -0
  136. package/src/widgets/Card.js +33 -0
  137. package/src/widgets/Carousel.js +301 -0
  138. package/src/widgets/Chart.js +632 -0
  139. package/src/widgets/Checkbox.js +106 -0
  140. package/src/widgets/Chip.js +153 -0
  141. package/src/widgets/CircularBar.js +352 -0
  142. package/src/widgets/CircularChart.js +549 -0
  143. package/src/widgets/CodeViewer.js +236 -0
  144. package/src/widgets/Column.js +29 -0
  145. package/src/widgets/Container.js +31 -0
  146. package/src/widgets/DataTable.js +257 -0
  147. package/src/widgets/Divider.js +93 -0
  148. package/src/widgets/DraggBox.js +115 -0
  149. package/src/widgets/Dropdown.js +505 -0
  150. package/src/widgets/DroppBox.js +180 -0
  151. package/src/widgets/FloatingActionButton.js +91 -0
  152. package/src/widgets/GridView.js +20 -0
  153. package/src/widgets/Icon.js +33 -0
  154. package/src/widgets/Image.js +15 -0
  155. package/src/widgets/Input.js +565 -0
  156. package/src/widgets/Inspector.js +254 -0
  157. package/src/widgets/InstallButton.js +110 -0
  158. package/src/widgets/ListTile.js +224 -0
  159. package/src/widgets/ListView.js +503 -0
  160. package/src/widgets/Markdown.js +1236 -0
  161. package/src/widgets/Modal.js +496 -0
  162. package/src/widgets/Pagination.js +195 -0
  163. package/src/widgets/ProgressBar.js +178 -0
  164. package/src/widgets/QRCode.js +467 -0
  165. package/src/widgets/Radio.js +132 -0
  166. package/src/widgets/Rating.js +261 -0
  167. package/src/widgets/Row.js +29 -0
  168. package/src/widgets/Skeleton.js +243 -0
  169. package/src/widgets/Slider.js +527 -0
  170. package/src/widgets/SnackBar.js +227 -0
  171. package/src/widgets/Stack.js +29 -0
  172. package/src/widgets/Stepper.js +296 -0
  173. package/src/widgets/Switch.js +102 -0
  174. package/src/widgets/Text.js +98 -0
  175. package/src/widgets/Tooltip.js +267 -0
  176. package/src/widgets/TreeView.js +370 -0
  177. package/src/widgets/Video.js +92 -0
  178. 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;