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,162 @@
1
+ /**
2
+ * @file AlertDialog.js
3
+ * @description A modal alert dialog built on top of the Modal widget.
4
+ * Supports four semantic variants (normal, danger, warning, success), each
5
+ * with its own icon and accent color. Exposes open/close methods and fires
6
+ * optional callbacks on confirm, cancel, and close.
7
+ */
8
+
9
+ import { Modal } from "./Modal.js";
10
+ import { Button } from "./Button.js";
11
+ import { Text } from "./Text.js";
12
+ import { Column } from "./Column.js";
13
+ import { Icon } from "./Icon.js";
14
+ import { colors } from "../utils/themes.js";
15
+
16
+ /**
17
+ * Creates an AlertDialog — a pre-styled modal with a title, message, icon,
18
+ * and confirm/cancel buttons whose appearance adapts to the chosen variant.
19
+ *
20
+ * Returns a plain object (not an HTMLElement) with `open`, `close`, and
21
+ * `modal` so the caller can programmatically control visibility while keeping
22
+ * the underlying Modal instance accessible for advanced use.
23
+ *
24
+ * @param {Object} props - Configuration for the dialog.
25
+ * @param {string} props.title - Heading text shown at the top of the dialog.
26
+ * @param {string} props.message - Body text providing more detail to the user.
27
+ * @param {Function} [props.onConfirm] - Called when the user clicks the confirm button, before the dialog closes.
28
+ * @param {Function} [props.onCancel] - Called when the user clicks the cancel button, before the dialog closes.
29
+ * @param {Function} [props.onClose] - Called whenever the dialog closes, regardless of how it was dismissed.
30
+ * @param {'normal'|'danger'|'warning'|'success'} [props.variant='normal'] - Determines the icon and button accent color.
31
+ * @param {boolean} [props.showCancel=true] - When false, the cancel button is omitted.
32
+ * @param {string} [props.confirmText='Accept'] - Label for the confirm button.
33
+ * @param {string} [props.cancelText='Cancel'] - Label for the cancel button.
34
+ * @returns {{ open: Function, close: Function, modal: HTMLElement }} Controller object for the dialog.
35
+ */
36
+ export const AlertDialog = (props) => {
37
+ let {
38
+ title,
39
+ message,
40
+ onConfirm,
41
+ onCancel,
42
+ onClose,
43
+ variant = "normal", // 'normal', 'danger', 'warning', 'success'
44
+ showCancel = true,
45
+ confirmText = "Accept",
46
+ cancelText = "Cancel",
47
+ ...rest
48
+ } = props;
49
+
50
+ // Map each variant to its icon name, icon color, and confirm-button color
51
+ const variantColors = {
52
+ normal: { confirm: colors.primary, icon: "info", iconColor: colors.info },
53
+ danger: {
54
+ confirm: colors.danger,
55
+ icon: "warning",
56
+ iconColor: colors.danger,
57
+ },
58
+ warning: {
59
+ confirm: colors.warning,
60
+ icon: "error",
61
+ iconColor: colors.warning,
62
+ },
63
+ success: {
64
+ confirm: colors.success,
65
+ icon: "check_circle",
66
+ iconColor: colors.success,
67
+ },
68
+ };
69
+ const variantStyle = variantColors[variant] || variantColors.normal;
70
+
71
+ // Decorative icon at the top of the dialog body
72
+ const icon = Icon({
73
+ name: variantStyle.icon,
74
+ size: 40,
75
+ color: variantStyle.iconColor,
76
+ marginBottom: 8,
77
+ });
78
+
79
+ // Centered column: icon → title → message
80
+ const content = Column({
81
+ alignItems: "center",
82
+ textAlign: "center",
83
+ children: [
84
+ icon,
85
+ Text({
86
+ value: title,
87
+ size: 24,
88
+ fontWeight: "bold",
89
+ color: colors.text,
90
+ textAlign: "center",
91
+ }),
92
+ Text({
93
+ value: message,
94
+ size: 18,
95
+ fontWeight: "bold",
96
+ color: colors.textSecondary,
97
+ textAlign: "center",
98
+ marginTop: 8,
99
+ }),
100
+ ],
101
+ });
102
+
103
+ // Build the action-button row — cancel first (if shown), then confirm
104
+ const actions = [];
105
+
106
+ if (showCancel) {
107
+ actions.push(
108
+ Button({
109
+ text: cancelText,
110
+ borderColor: colors.danger,
111
+ variant: "filled",
112
+ color: colors.danger,
113
+ onPress: () => {
114
+ if (onCancel) onCancel();
115
+ closeModal();
116
+ },
117
+ }),
118
+ );
119
+ }
120
+
121
+ actions.push(
122
+ Button({
123
+ text: confirmText,
124
+ variant: "filled",
125
+ bgColor: variantStyle.confirm,
126
+ color: colors.text,
127
+ borderColor: colors.surface,
128
+ onPress: () => {
129
+ if (onConfirm) onConfirm();
130
+ closeModal();
131
+ },
132
+ }),
133
+ );
134
+
135
+ let modal = null;
136
+ // Placeholder until the modal is created below — avoids a temporal dead-zone
137
+ let closeModal = () => {};
138
+
139
+ // Delegate to the base Modal widget, passing our content and actions
140
+ modal = Modal({
141
+ content: content,
142
+ actions: actions,
143
+ showCloseButton: false,
144
+ width: 320,
145
+ borderRadius: 24,
146
+ onClose: () => {
147
+ if (onClose) onClose();
148
+ },
149
+ ...rest,
150
+ });
151
+
152
+ // Now that modal exists, wire up the real close function
153
+ closeModal = () => modal.close();
154
+
155
+ // Public controller API
156
+ const open = () => modal.open();
157
+ const close = () => modal.close();
158
+
159
+ return { open, close, modal };
160
+ };
161
+
162
+ export default AlertDialog;
@@ -0,0 +1,311 @@
1
+ /**
2
+ * @file Audio.js
3
+ * @description A thin wrapper around the native `<audio>` element that adds
4
+ * reliable duration detection (with an AudioContext fallback for formats where
5
+ * the browser cannot report duration from metadata alone), progress callbacks,
6
+ * and a clean imperative API for controlling playback.
7
+ */
8
+
9
+ import { WidgetFactory } from "../widget-factory/index.js";
10
+
11
+ /**
12
+ * Creates an Audio widget — an `<audio>` element enriched with helper methods
13
+ * and reliable duration detection.
14
+ *
15
+ * Duration detection works in three stages:
16
+ * 1. Read directly from `audio.duration` on `loadedmetadata` / `canplay`.
17
+ * 2. Poll every 200 ms for up to two seconds if the element still reports NaN or Infinity.
18
+ * 3. Fetch the file as an ArrayBuffer and decode it with AudioContext as a last resort.
19
+ *
20
+ * @param {Object} props - Configuration for the audio widget.
21
+ * @param {string} props.src - URL of the audio file to load.
22
+ * @param {boolean} [props.autoplay=false] - Whether playback starts automatically on load.
23
+ * @param {boolean} [props.controls=false] - Whether to render the browser's native playback controls.
24
+ * @param {boolean} [props.loop=false] - Whether playback restarts automatically when the track ends.
25
+ * @param {boolean} [props.muted=false] - Whether the audio starts muted.
26
+ * @param {number} [props.volume=1] - Initial volume level (0.0–1.0).
27
+ * @param {Function} [props.onPlay] - Fired when playback begins.
28
+ * @param {Function} [props.onPause] - Fired when playback is paused.
29
+ * @param {Function} [props.onEnd] - Fired when the track finishes.
30
+ * @param {Function} [props.onTimeUpdate] - Fired on each timeupdate event. Receives `(currentTime, duration, percentPlayed)`.
31
+ * @param {Function} [props.onProgress] - Fired on each timeupdate event with only the percent value.
32
+ * @param {Function} [props.onLoad] - Fired once when the duration is successfully resolved. Receives `(duration)`.
33
+ * @param {Function} [props.ref] - Callback that receives the `<audio>` element after creation.
34
+ * @returns {HTMLAudioElement} The audio element, extended with playback control methods.
35
+ */
36
+ export const Audio = (props) => {
37
+ const {
38
+ src,
39
+ autoplay = false,
40
+ controls = false,
41
+ loop = false,
42
+ muted = false,
43
+ volume = 1,
44
+ onPlay,
45
+ onPause,
46
+ onEnd,
47
+ onTimeUpdate,
48
+ onProgress,
49
+ onLoad,
50
+ ref,
51
+ ...rest
52
+ } = props;
53
+
54
+ const audio = WidgetFactory({
55
+ tag: "audio",
56
+ widgetName: "Audio",
57
+ src: src,
58
+ ...rest,
59
+ });
60
+
61
+ // Audio attributes are element properties, not CSS attributes — set them directly
62
+ audio.controls = Boolean(controls);
63
+ audio.loop = Boolean(loop);
64
+ audio.muted = Boolean(muted);
65
+ audio.autoplay = Boolean(autoplay);
66
+ audio.preload = "metadata";
67
+ audio.volume = typeof volume === "number" ? volume : 1;
68
+
69
+ // Keep native play/pause bound before we might override them
70
+ const nativePlay = audio.play.bind(audio);
71
+ const nativePause = audio.pause.bind(audio);
72
+
73
+ let currentTime = 0;
74
+ let duration = 0;
75
+ let durationPoll = null;
76
+ let audioContextFallbackAttempted = false;
77
+
78
+ /**
79
+ * Recalculates the current playback position and fires the progress callbacks.
80
+ * Called on every `timeupdate` event from the element.
81
+ */
82
+ const updateTime = () => {
83
+ currentTime = audio.currentTime || 0;
84
+ const percent = duration ? (currentTime / duration) * 100 : 0;
85
+ if (onTimeUpdate) onTimeUpdate(currentTime, duration, percent);
86
+ if (onProgress) onProgress(percent);
87
+ };
88
+
89
+ /**
90
+ * Stores the resolved duration and fires `onLoad` exactly once.
91
+ * Clears any active polling interval once the duration is known.
92
+ *
93
+ * @param {number} dur - The resolved audio duration in seconds.
94
+ */
95
+ const setDuration = (dur) => {
96
+ if (dur > 0 && duration === 0) {
97
+ duration = dur;
98
+ if (onLoad) onLoad(duration);
99
+ if (durationPoll) clearInterval(durationPoll);
100
+ durationPoll = null;
101
+ }
102
+ };
103
+
104
+ /**
105
+ * Attempts to read the duration directly from the audio element.
106
+ *
107
+ * @returns {boolean} `true` if a valid finite duration was obtained.
108
+ */
109
+ const tryGetDurationFromAudio = () => {
110
+ const d = audio.duration;
111
+ if (d && d !== Infinity && d > 0) {
112
+ setDuration(d);
113
+ return true;
114
+ }
115
+ return false;
116
+ };
117
+
118
+ /**
119
+ * Starts a polling interval that retries duration detection every 200 ms.
120
+ * Useful for formats (e.g. MP3 with VBR) where the browser reports duration
121
+ * later than `loadedmetadata`.
122
+ */
123
+ const pollDuration = () => {
124
+ if (durationPoll) return;
125
+ durationPoll = setInterval(() => {
126
+ if (tryGetDurationFromAudio()) {
127
+ clearInterval(durationPoll);
128
+ durationPoll = null;
129
+ }
130
+ }, 200);
131
+ };
132
+
133
+ /**
134
+ * Last-resort fallback: fetches the audio file as a binary buffer, decodes
135
+ * it with the Web Audio API, and extracts the duration from the resulting
136
+ * AudioBuffer. Only attempted once per instance.
137
+ *
138
+ * This is necessary for some file formats where the browser's media pipeline
139
+ * cannot determine duration without fully downloading the file.
140
+ */
141
+ const fallbackGetDuration = async () => {
142
+ if (audioContextFallbackAttempted) return;
143
+ audioContextFallbackAttempted = true;
144
+
145
+ try {
146
+ const response = await fetch(src);
147
+ const arrayBuffer = await response.arrayBuffer();
148
+ const audioContext = new (
149
+ window.AudioContext || window.webkitAudioContext
150
+ )();
151
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
152
+ const durationSeconds = audioBuffer.duration;
153
+ setDuration(durationSeconds);
154
+ audioContext.close();
155
+ } catch (err) {
156
+ console.warn("[Audio] Fallback duration fetch failed:", err);
157
+ }
158
+ };
159
+
160
+ // Wire up native audio element events
161
+ audio.addEventListener("loadedmetadata", () => {
162
+ if (!tryGetDurationFromAudio()) pollDuration();
163
+ });
164
+ audio.addEventListener("durationchange", () => tryGetDurationFromAudio());
165
+ audio.addEventListener("canplay", () => {
166
+ if (!tryGetDurationFromAudio()) pollDuration();
167
+ });
168
+ audio.addEventListener("timeupdate", updateTime);
169
+ audio.addEventListener("play", () => {
170
+ if (onPlay) onPlay();
171
+ });
172
+ audio.addEventListener("pause", () => {
173
+ if (onPause) onPause();
174
+ });
175
+ audio.addEventListener("ended", () => {
176
+ if (onEnd) onEnd();
177
+ });
178
+ audio.addEventListener("error", (e) => console.warn("[Audio] Error:", e));
179
+
180
+ // Kick off duration detection immediately; if unavailable after 2 s, use fetch fallback
181
+ if (!tryGetDurationFromAudio()) {
182
+ pollDuration();
183
+ setTimeout(() => {
184
+ if (!duration) fallbackGetDuration();
185
+ }, 2000);
186
+ }
187
+
188
+ // Cancel the duration polling interval when the widget unmounts
189
+ audio.onUnmount(() => {
190
+ if (durationPoll) clearInterval(durationPoll);
191
+ });
192
+
193
+ // ========== PUBLIC PLAYBACK API ==========
194
+
195
+ /**
196
+ * Starts playback. Silently swallows the NotAllowedError thrown by browsers
197
+ * that require a user gesture before audio can play.
198
+ * @returns {HTMLAudioElement} The audio element (for chaining).
199
+ */
200
+ audio.playAudio = () => {
201
+ nativePlay().catch((e) => console.warn(e));
202
+ return audio;
203
+ };
204
+
205
+ /**
206
+ * Pauses playback.
207
+ * @returns {HTMLAudioElement} The audio element (for chaining).
208
+ */
209
+ audio.pauseAudio = () => {
210
+ nativePause();
211
+ return audio;
212
+ };
213
+
214
+ /**
215
+ * Stops playback and resets the playhead to the beginning.
216
+ * @returns {HTMLAudioElement} The audio element (for chaining).
217
+ */
218
+ audio.stop = () => {
219
+ nativePause();
220
+ audio.currentTime = 0;
221
+ updateTime();
222
+ return audio;
223
+ };
224
+
225
+ /**
226
+ * Seeks to an absolute position in seconds.
227
+ * Silently ignores out-of-range values.
228
+ *
229
+ * @param {number} seconds - Target time in seconds.
230
+ * @returns {HTMLAudioElement} The audio element (for chaining).
231
+ */
232
+ audio.seekTo = (seconds) => {
233
+ if (seconds >= 0 && seconds <= (duration || audio.duration)) {
234
+ audio.currentTime = seconds;
235
+ updateTime();
236
+ }
237
+ return audio;
238
+ };
239
+
240
+ /**
241
+ * Seeks to a percentage of the total duration (0–100).
242
+ *
243
+ * @param {number} percent - Target position as a percentage.
244
+ * @returns {HTMLAudioElement} The audio element (for chaining).
245
+ */
246
+ audio.seekToPercent = (percent) => {
247
+ const total = duration || audio.duration;
248
+ if (total) audio.seekTo((percent / 100) * total);
249
+ return audio;
250
+ };
251
+
252
+ /** @returns {number} The current playback position in seconds. */
253
+ audio.getCurrentTime = () => currentTime;
254
+
255
+ /** @returns {number} The total track duration in seconds, or 0 if not yet known. */
256
+ audio.getDuration = () => duration || audio.duration || 0;
257
+
258
+ /** @returns {number} The current playback position as a percentage (0–100). */
259
+ audio.getProgressPercent = () => {
260
+ const total = duration || audio.duration;
261
+ return total ? (currentTime / total) * 100 : 0;
262
+ };
263
+
264
+ /**
265
+ * Increases volume by `step` (clamped to 1.0).
266
+ * @param {number} [step=0.1] - Volume increment.
267
+ * @returns {HTMLAudioElement} The audio element (for chaining).
268
+ */
269
+ audio.volumeUp = (step = 0.1) => {
270
+ audio.volume = Math.min(1, audio.volume + step);
271
+ return audio;
272
+ };
273
+
274
+ /**
275
+ * Decreases volume by `step` (clamped to 0.0).
276
+ * @param {number} [step=0.1] - Volume decrement.
277
+ * @returns {HTMLAudioElement} The audio element (for chaining).
278
+ */
279
+ audio.volumeDown = (step = 0.1) => {
280
+ audio.volume = Math.max(0, audio.volume - step);
281
+ return audio;
282
+ };
283
+
284
+ /**
285
+ * Mutes the audio element without changing the volume setting.
286
+ * @returns {HTMLAudioElement} The audio element (for chaining).
287
+ */
288
+ audio.muteAudio = () => {
289
+ audio.muted = true;
290
+ return audio;
291
+ };
292
+
293
+ /**
294
+ * Unmutes the audio element.
295
+ * @returns {HTMLAudioElement} The audio element (for chaining).
296
+ */
297
+ audio.unmuteAudio = () => {
298
+ audio.muted = false;
299
+ return audio;
300
+ };
301
+
302
+ /** @returns {boolean} `true` if the audio is currently playing. */
303
+ audio.isPlaying = () => !audio.paused;
304
+
305
+ // Expose the element via ref callback if provided
306
+ if (ref && typeof ref === "function") ref(audio);
307
+
308
+ return audio;
309
+ };
310
+
311
+ export default Audio;
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @file Avatar.js
3
+ * @description A user avatar widget that renders an image, an icon, or
4
+ * initials derived from a name, depending on what props are provided.
5
+ * Falls back gracefully: image → icon → initials → generic person icon.
6
+ * Supports circular, rounded-square, and square shapes, and exposes a method
7
+ * to update its content without rebuilding the DOM.
8
+ */
9
+
10
+ import { WidgetFactory } from "../widget-factory/index.js";
11
+ import { colors } from "../utils/themes.js";
12
+ import { Image } from "./Image.js";
13
+ import { Icon } from "./Icon.js";
14
+ import { Text } from "./Text.js";
15
+
16
+ /**
17
+ * Creates an Avatar widget.
18
+ *
19
+ * Content priority (first match wins):
20
+ * 1. `src` — renders an `<img>` filling the avatar circle.
21
+ * 2. `icon` — renders a Material icon centered in the avatar.
22
+ * 3. `name` — extracts up to two initials and renders them as text.
23
+ * 4. Fallback — renders a generic "person" icon.
24
+ *
25
+ * @param {Object} props - Configuration for the avatar.
26
+ * @param {string} [props.src] - URL of a profile image.
27
+ * @param {string} [props.name] - Full name from which initials are derived (e.g. "Jane Doe" → "JD").
28
+ * @param {number} [props.size=40] - Diameter of the avatar in pixels.
29
+ * @param {number} [props.fontSize=16] - Font size of initials text in pixels (overridden by the auto-computed size when `size` changes).
30
+ * @param {'circle'|'rounded'|'square'} [props.shape='circle'] - Shape of the avatar container.
31
+ * @param {string} [props.bgColor=colors.primary] - Background color shown behind icons or initials.
32
+ * @param {string} [props.textColor=colors.text] - Color of icon or initials.
33
+ * @param {string} [props.icon] - Material icon name to display instead of an image or initials.
34
+ * @param {Function} [props.onPress] - Click handler. When provided, a hover scale effect is also applied.
35
+ * @returns {HTMLElement} The avatar container element, augmented with an `updateContent` method.
36
+ */
37
+ export const Avatar = (props) => {
38
+ let {
39
+ src,
40
+ name,
41
+ size = 40,
42
+ fontSize = 16,
43
+ shape = "circle",
44
+ bgColor = colors.primary,
45
+ textColor = colors.text,
46
+ icon,
47
+ onPress,
48
+ ...rest
49
+ } = props;
50
+
51
+ // Map shape to a CSS border-radius value
52
+ let borderRadius = "50%";
53
+ if (shape === "rounded") borderRadius = `${size * 0.2}px`;
54
+ if (shape === "square") borderRadius = "0";
55
+
56
+ /**
57
+ * Extracts up to two initials from a full name.
58
+ * Single-word names return just the first letter; multi-word names use
59
+ * the first letter of the first and last words.
60
+ *
61
+ * @param {string} fullName - The user's full name.
62
+ * @returns {string} 1–2 uppercase initials, or "?" if the name is empty.
63
+ */
64
+ const getInitials = (fullName) => {
65
+ if (!fullName) return "?";
66
+ const parts = fullName.trim().split(/\s+/);
67
+ if (parts.length === 1) return parts[0].charAt(0).toUpperCase();
68
+ return (
69
+ parts[0].charAt(0) + parts[parts.length - 1].charAt(0)
70
+ ).toUpperCase();
71
+ };
72
+
73
+ // Choose the appropriate child element based on prop priority
74
+ let content = null;
75
+ if (src) {
76
+ content = Image({
77
+ src: src,
78
+ width: "100%",
79
+ height: "100%",
80
+ fit: "cover",
81
+ });
82
+ } else if (icon) {
83
+ content = Icon({
84
+ name: icon,
85
+ size: size * 0.5,
86
+ color: textColor,
87
+ });
88
+ } else if (name) {
89
+ const initials = getInitials(name);
90
+ content = Text({
91
+ text: initials,
92
+ color: textColor,
93
+ size: size * 0.4,
94
+ weight: "bold",
95
+ });
96
+ } else {
97
+ // Generic fallback when no content is supplied
98
+ content = Icon({
99
+ name: "person",
100
+ size: size * 0.5,
101
+ color: textColor,
102
+ });
103
+ }
104
+
105
+ const avatar = WidgetFactory({
106
+ widgetName: "Avatar",
107
+ display: "inline-flex",
108
+ alignItems: "center",
109
+ justifyContent: "center",
110
+ width: size,
111
+ height: size,
112
+ borderRadius: borderRadius,
113
+ backgroundColor: bgColor,
114
+ color: textColor,
115
+ overflow: "hidden",
116
+ fontSize: fontSize,
117
+ fontWeight: "500",
118
+ userSelect: "none",
119
+ cursor: onPress ? "pointer" : "default",
120
+ transition: "transform 0.2s ease",
121
+ child: content,
122
+ onclick: onPress,
123
+ ...rest,
124
+ });
125
+
126
+ // Add a subtle scale-up on hover when the avatar is interactive
127
+ if (onPress) {
128
+ avatar.addEventListener("mouseenter", () => {
129
+ avatar.style.transform = "scale(1.05)";
130
+ });
131
+ avatar.addEventListener("mouseleave", () => {
132
+ avatar.style.transform = "scale(1)";
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Updates the avatar's content and/or dimensions without recreating the element.
138
+ * Only the properties present in `newProps` are applied; others are left as-is.
139
+ *
140
+ * @param {Object} newProps - Partial props to apply.
141
+ * @param {string} [newProps.src] - New image URL.
142
+ * @param {string} [newProps.name] - New name (re-derives initials).
143
+ * @param {string} [newProps.icon] - New icon name.
144
+ * @param {string} [newProps.bgColor] - New background color.
145
+ * @param {number|string} [newProps.size] - New avatar diameter.
146
+ */
147
+ avatar.updateContent = (newProps) => {
148
+ // Sync the mutable local variables with any supplied overrides
149
+ if (newProps.src !== undefined) src = newProps.src;
150
+ if (newProps.name !== undefined) name = newProps.name;
151
+ if (newProps.icon !== undefined) icon = newProps.icon;
152
+ if (newProps.bgColor !== undefined)
153
+ avatar.style.backgroundColor = newProps.bgColor;
154
+ if (newProps.size !== undefined) {
155
+ const newSize = newProps.size;
156
+ avatar.style.width =
157
+ typeof newSize === "number" ? `${newSize}px` : newSize;
158
+ avatar.style.height =
159
+ typeof newSize === "number" ? `${newSize}px` : newSize;
160
+ avatar.style.fontSize =
161
+ typeof newSize === "number" ? `${newSize * 0.4}px` : "16px";
162
+ }
163
+
164
+ // Rebuild the inner content using the updated props
165
+ let newContent = null;
166
+ if (newProps.src !== undefined || src !== undefined) {
167
+ const finalSrc = newProps.src !== undefined ? newProps.src : src;
168
+ newContent = Image({
169
+ src: finalSrc,
170
+ width: "100%",
171
+ height: "100%",
172
+ fit: "cover",
173
+ });
174
+ } else if (newProps.icon !== undefined || icon !== undefined) {
175
+ const finalIcon = newProps.icon !== undefined ? newProps.icon : icon;
176
+ const finalSize = newProps.size || size;
177
+ newContent = Icon({
178
+ name: finalIcon,
179
+ size: finalSize * 0.5,
180
+ color: textColor,
181
+ });
182
+ } else if (newProps.name !== undefined || name !== undefined) {
183
+ const finalName = newProps.name !== undefined ? newProps.name : name;
184
+ const initials = getInitials(finalName);
185
+ const finalSize = newProps.size || size;
186
+ newContent = Text({
187
+ text: initials,
188
+ color: textColor,
189
+ size: finalSize * 0.4,
190
+ weight: "bold",
191
+ });
192
+ } else {
193
+ const finalSize = newProps.size || size;
194
+ newContent = Icon({
195
+ name: "person",
196
+ size: finalSize * 0.5,
197
+ color: textColor,
198
+ });
199
+ }
200
+
201
+ // Replace the existing child with the new content
202
+ while (avatar.firstChild) avatar.removeChild(avatar.firstChild);
203
+ avatar.appendChild(newContent);
204
+ avatar._child = newContent;
205
+ };
206
+
207
+ return avatar;
208
+ };
209
+
210
+ export default Avatar;