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,496 @@
1
+ /**
2
+ * @file Modal.js
3
+ * @description Modal dialog instance with `open`/`close`/`updateContent`.
4
+ */
5
+ import { WidgetFactory } from "../widget-factory/index.js";
6
+ import { transition } from "../tools/index.js";
7
+ import { colors } from "../utils/themes.js";
8
+ import { injectKeyframes } from "../utils/styleInjector.js";
9
+ import { Container } from "./Container.js";
10
+ import { Column } from "./Column.js";
11
+ import { Row } from "./Row.js";
12
+ import { Text } from "./Text.js";
13
+ import { Icon } from "./Icon.js";
14
+
15
+ // ========== INTERNAL COMPONENTS ==========
16
+
17
+ const ModalOverlay = ({
18
+ closeOnOverlayClick,
19
+ onClose,
20
+ zIndex = 9998,
21
+ overlayColor,
22
+ }) => {
23
+ // main modal transparent
24
+ return WidgetFactory({
25
+ tag: "div",
26
+ position: "fixed",
27
+ top: "0",
28
+ left: "0",
29
+ width: "100%",
30
+ height: "100%",
31
+ backgroundColor: overlayColor || colors.overlay,
32
+ zIndex: zIndex,
33
+ // blur: 12,
34
+ display: "flex",
35
+ alignItems: "center",
36
+ justifyContent: "center",
37
+ opacity: "0",
38
+ visibility: "hidden",
39
+ transition: transition({
40
+ property: "opacity",
41
+ duration: 0.3,
42
+ timing: "ease",
43
+ delay: 0.1,
44
+ }),
45
+ // transition: "opacity 0.3s ease, visibility 0.3s ease",
46
+ // onclick: () => {
47
+ // if (closeOnOverlayClick) onClose();
48
+ // },
49
+ });
50
+ };
51
+
52
+ const ModalHeader = ({
53
+ title,
54
+ showCloseButton,
55
+ onClose,
56
+ headerBgColor,
57
+ headerTextColor,
58
+ headerBorder,
59
+ headerPadding,
60
+ headerElevation,
61
+ }) => {
62
+ const children = [];
63
+ if (title) {
64
+ children.push(
65
+ Text({
66
+ text: title,
67
+ size: 18,
68
+ weight: "80%",
69
+ color: headerTextColor || colors.text,
70
+ flex: 1,
71
+ }),
72
+ );
73
+ }
74
+ if (showCloseButton) {
75
+ const closeIcon = Icon({
76
+ name: "close",
77
+ size: 22,
78
+ color: headerTextColor || colors.textSecondary,
79
+ cursor: "pointer",
80
+ onclick: onClose,
81
+ transition: transition({
82
+ property: "opacity",
83
+ duration: 0.2,
84
+ timing: "ease",
85
+ delay: 0.1,
86
+ }),
87
+ // transition: "opacity 0.2s ease",
88
+ borderRadius: "50%",
89
+ padding: 4,
90
+ });
91
+ // Hover effect for close button
92
+ closeIcon.addEventListener("mouseenter", () => {
93
+ closeIcon.style.backgroundColor = colors.gray100;
94
+ });
95
+ closeIcon.addEventListener("mouseleave", () => {
96
+ closeIcon.style.backgroundColor = "transparent";
97
+ });
98
+ children.push(closeIcon);
99
+ }
100
+
101
+ // Default border: subtle divider
102
+ const defaultBorder = `1px solid ${colors.border}`;
103
+ const finalBorder =
104
+ headerBorder === false ? "none" : headerBorder || defaultBorder;
105
+
106
+ // Optional elevation shadow for header
107
+ const headerShadow = headerElevation
108
+ ? `0 ${headerElevation}px ${headerElevation * 2}px rgba(0,0,0,0.05)`
109
+ : "none";
110
+
111
+ return children.length
112
+ ? Row({
113
+ alignItems: "center",
114
+ justifyContent: "space-between",
115
+ padding: headerPadding || "16px 20px",
116
+ backgroundColor: headerBgColor || "transparent",
117
+ borderBottom: finalBorder,
118
+ flexShrink: 0,
119
+ boxShadow: headerShadow,
120
+ position: "relative",
121
+ zIndex: 1,
122
+ children,
123
+ })
124
+ : null;
125
+ };
126
+
127
+ const ModalContent = ({
128
+ content,
129
+ padding,
130
+ contentBgColor,
131
+ contentElevation,
132
+ }) => {
133
+ const wrapper = Container({
134
+ flex: 1,
135
+ overflow: "auto",
136
+ padding: padding,
137
+ backgroundColor: contentBgColor || "transparent",
138
+ boxShadow: contentElevation
139
+ ? `inset 0 ${contentElevation}px ${contentElevation * 2}px rgba(0,0,0,0.02)`
140
+ : "none",
141
+ });
142
+
143
+ if (content) {
144
+ if (content instanceof HTMLElement) {
145
+ wrapper.appendChild(content);
146
+ } else if (typeof content === "string") {
147
+ wrapper.appendChild(
148
+ Text({ text: content, size: 14, color: colors.text }),
149
+ );
150
+ } else if (Array.isArray(content)) {
151
+ content.forEach((item) => {
152
+ if (item instanceof HTMLElement) wrapper.appendChild(item);
153
+ else if (typeof item === "string")
154
+ wrapper.appendChild(
155
+ Text({ text: item, size: 14, color: colors.text }),
156
+ );
157
+ });
158
+ }
159
+ }
160
+ return wrapper;
161
+ };
162
+
163
+ const ModalFooter = ({
164
+ actions,
165
+ footerBgColor,
166
+ footerBorder,
167
+ footerPadding,
168
+ footerElevation,
169
+ }) => {
170
+ if (!actions || !actions.length) return null;
171
+
172
+ const defaultBorder = `1px solid ${colors.border}`;
173
+ const finalBorder =
174
+ footerBorder === false ? "none" : footerBorder || defaultBorder;
175
+ const footerShadow = footerElevation
176
+ ? `0 -${footerElevation}px ${footerElevation * 2}px rgba(0,0,0,0.05)`
177
+ : "none";
178
+
179
+ return Row({
180
+ alignItems: "center",
181
+ justifyContent: "flex-end",
182
+ gap: 8,
183
+ padding: footerPadding || "12px 20px",
184
+ backgroundColor: footerBgColor || "transparent",
185
+ borderTop: finalBorder,
186
+ flexShrink: 0,
187
+ boxShadow: footerShadow,
188
+ position: "relative",
189
+ zIndex: 1,
190
+ children: actions,
191
+ });
192
+ };
193
+
194
+ const ModalContainer = ({
195
+ width,
196
+ minWidth,
197
+ maxWidth,
198
+ maxHeight,
199
+ backgroundColor,
200
+ borderRadius,
201
+ border,
202
+ borderColor,
203
+ borderWidth,
204
+ shadow,
205
+ elevation,
206
+ children,
207
+ }) => {
208
+ // Elevation to shadow mapping
209
+ const elevationShadows = {
210
+ 0: "none",
211
+ 1: "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)",
212
+ 2: "0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.12)",
213
+ 3: "0 10px 20px rgba(0,0,0,0.15), 0 3px 6px rgba(0,0,0,0.10)",
214
+ 4: "0 15px 25px rgba(0,0,0,0.15), 0 5px 10px rgba(0,0,0,0.05)",
215
+ 5: "0 20px 40px rgba(0,0,0,0.2), 0 8px 16px rgba(0,0,0,0.1)",
216
+ };
217
+
218
+ const finalShadow =
219
+ shadow || (elevation ? elevationShadows[elevation] : elevationShadows[3]);
220
+
221
+ let containerStyle = {
222
+ display: "flex",
223
+ flexDirection: "column",
224
+ backgroundColor: backgroundColor || colors.surface,
225
+ borderRadius:
226
+ typeof borderRadius === "number"
227
+ ? `${borderRadius}px`
228
+ : borderRadius || "16px",
229
+ width: typeof width === "number" ? `${width}px` : width,
230
+ minWidth: typeof minWidth === "number" ? `${minWidth}px` : minWidth,
231
+ maxWidth: maxWidth || "90%",
232
+ maxHeight: maxHeight || "80vh",
233
+ overflow: "hidden",
234
+ boxShadow: finalShadow,
235
+ transform: "scale(0.9)",
236
+ opacity: 0,
237
+
238
+ transition:
239
+ "transform 0.3s cubic-bezier(0.2, 0.9, 0.4, 1.1), opacity 0.3s ease",
240
+ };
241
+
242
+ // Apply custom border
243
+ if (border) {
244
+ containerStyle.border = border;
245
+ } else if (borderWidth !== undefined && borderColor) {
246
+ containerStyle.border = `${borderWidth}px solid ${borderColor}`;
247
+ }
248
+
249
+ return WidgetFactory({
250
+ tag: "div",
251
+ children,
252
+ ...containerStyle,
253
+ });
254
+ };
255
+
256
+ // ========== MAIN MODAL ==========
257
+ export const Modal = (props = {}) => {
258
+ const {
259
+ // Content
260
+ title,
261
+ content,
262
+ actions = [],
263
+
264
+ // Behavior
265
+ closeOnOverlayClick = true,
266
+ closeOnEsc = true,
267
+
268
+ // Dimensions
269
+ width = 480,
270
+ minWidth = 320,
271
+ maxWidth = "90%",
272
+ maxHeight = "80vh",
273
+
274
+ // Main container styles
275
+ backgroundColor = colors.surface,
276
+ borderRadius = 20,
277
+ border = null,
278
+ borderColor = colors.border,
279
+ borderWidth = 1,
280
+ shadow = null,
281
+ elevation = 3, // ← New: shadow by elevation
282
+
283
+ // Content padding
284
+ padding = "20px",
285
+ contentBgColor = colors.surface,
286
+ contentElevation = 0, // ← Optional inner shadow
287
+
288
+ // Customizable header
289
+ showCloseButton = true,
290
+ headerBgColor = colors.surface,
291
+ headerTextColor = null,
292
+ headerBorder = null,
293
+ headerPadding = null,
294
+ headerElevation = 0,
295
+
296
+ // Customizable footer
297
+ footerBgColor = colors.surface,
298
+ footerBorder = null,
299
+ footerPadding = null,
300
+ footerElevation = 0,
301
+
302
+ // Overlay
303
+ overlayColor = colors.overlay,
304
+ zIndex = 9998,
305
+
306
+ // Callbacks
307
+ onOpen,
308
+ onClose,
309
+
310
+ ...rest
311
+ } = props;
312
+
313
+ let isOpen = false;
314
+ let overlay = null;
315
+ let modalBody = null;
316
+
317
+ // Handlers
318
+ const handleClose = () => {
319
+ if (!isOpen) return;
320
+ isOpen = false;
321
+ if (modalBody) {
322
+ modalBody.style.transform = "scale(0.9)";
323
+ modalBody.style.opacity = "0";
324
+ }
325
+ if (overlay) overlay.style.opacity = "0";
326
+ setTimeout(() => {
327
+ if (overlay) overlay.style.visibility = "hidden";
328
+ }, 300);
329
+ if (closeOnEsc) document.removeEventListener("keydown", handleKeyDown);
330
+ if (onClose) onClose();
331
+ };
332
+
333
+ const handleOpen = () => {
334
+ if (isOpen) return;
335
+ isOpen = true;
336
+ if (overlay) {
337
+ overlay.style.visibility = "visible";
338
+ overlay.style.opacity = "1";
339
+ }
340
+ if (modalBody) {
341
+ modalBody.style.transform = "scale(1)";
342
+ modalBody.style.opacity = "1";
343
+ }
344
+ if (onOpen) onOpen();
345
+ if (closeOnEsc) document.addEventListener("keydown", handleKeyDown);
346
+ };
347
+
348
+ const handleKeyDown = (e) => {
349
+ if (e.key === "Escape" && isOpen) handleClose();
350
+ };
351
+
352
+ // Build modal UI
353
+ const headerElement = ModalHeader({
354
+ title,
355
+ showCloseButton,
356
+ onClose: handleClose,
357
+ headerBgColor,
358
+ headerTextColor,
359
+ headerBorder,
360
+ headerPadding,
361
+ headerElevation,
362
+ });
363
+
364
+ const contentElement = ModalContent({
365
+ content,
366
+ padding: typeof padding === "number" ? `${padding}px` : padding,
367
+ contentBgColor,
368
+ contentElevation,
369
+ });
370
+
371
+ const footerElement = ModalFooter({
372
+ actions,
373
+ footerBgColor,
374
+ footerBorder,
375
+ footerPadding,
376
+ footerElevation,
377
+ });
378
+
379
+ // Filter out null elements
380
+ const modalChildren = [headerElement, contentElement, footerElement].filter(
381
+ Boolean,
382
+ );
383
+
384
+ modalBody = ModalContainer({
385
+ width,
386
+ minWidth,
387
+ maxWidth,
388
+ maxHeight,
389
+ backgroundColor,
390
+ borderRadius,
391
+ border,
392
+ borderColor,
393
+ borderWidth,
394
+ shadow,
395
+ elevation,
396
+ children: modalChildren,
397
+ });
398
+
399
+ overlay = ModalOverlay({
400
+ closeOnOverlayClick,
401
+ onClose: handleClose,
402
+ zIndex,
403
+ overlayColor,
404
+ });
405
+ overlay.appendChild(modalBody);
406
+ document.body.appendChild(overlay);
407
+
408
+ // Public API
409
+ const instance = {
410
+ open: handleOpen,
411
+ close: handleClose,
412
+ toggle: () => (isOpen ? handleClose() : handleOpen()),
413
+ destroy: () => {
414
+ if (overlay && overlay.parentNode)
415
+ overlay.parentNode.removeChild(overlay);
416
+ if (closeOnEsc) document.removeEventListener("keydown", handleKeyDown);
417
+ },
418
+ get isOpen() {
419
+ return isOpen;
420
+ },
421
+
422
+ // Methods for dynamic updates
423
+ updateContent: (newContent) => {
424
+ if (contentElement) {
425
+ while (contentElement.firstChild) {
426
+ contentElement.removeChild(contentElement.firstChild);
427
+ }
428
+ if (newContent instanceof HTMLElement) {
429
+ contentElement.appendChild(newContent);
430
+ } else if (typeof newContent === "string") {
431
+ contentElement.appendChild(
432
+ Text({ text: newContent, size: 14, color: colors.text }),
433
+ );
434
+ }
435
+ }
436
+ },
437
+
438
+ updateTitle: (newTitle) => {
439
+ const titleText =
440
+ modalBody?.querySelector(".modal-title") ||
441
+ headerElement?.querySelector("span:first-child");
442
+ if (titleText) titleText.textContent = newTitle;
443
+ },
444
+
445
+ updatePadding: (newPadding) => {
446
+ if (contentElement) {
447
+ contentElement.style.padding =
448
+ typeof newPadding === "number" ? `${newPadding}px` : newPadding;
449
+ }
450
+ },
451
+
452
+ setLoading: (loading = true, loadingText = "Loading...") => {
453
+ if (loading) {
454
+ const loadingEl = Container({
455
+ alignItems: "center",
456
+ justifyContent: "center",
457
+ gap: 12,
458
+ padding: 40,
459
+ child: Column({
460
+ alignItems: "center",
461
+ gap: 12,
462
+ children: [
463
+ WidgetFactory({
464
+ tag: "div",
465
+ width: 32,
466
+ height: 32,
467
+ border: `2px solid ${colors.border}`,
468
+ borderTop: `2px solid ${colors.primary}`,
469
+ borderRadius: 32,
470
+ animation: "modal-spin 0.8s linear infinite",
471
+ }),
472
+ Text({
473
+ text: loadingText,
474
+ size: 14,
475
+ color: colors.textSecondary,
476
+ }),
477
+ ],
478
+ }),
479
+ });
480
+
481
+ injectKeyframes(
482
+ "modal-spin",
483
+ "0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); }",
484
+ );
485
+
486
+ instance.updateContent(loadingEl);
487
+ } else {
488
+ instance.updateContent(content);
489
+ }
490
+ },
491
+ };
492
+
493
+ return instance;
494
+ };
495
+
496
+ export default Modal;
@@ -0,0 +1,195 @@
1
+ /**
2
+ * @file Pagination.js
3
+ * @description Page-number control built from Buttons inside a Row.
4
+ */
5
+ import { WidgetFactory } from "../widget-factory/index.js";
6
+ import { Row } from "./Row.js";
7
+ import { Button } from "./Button.js";
8
+ import { Text } from "./Text.js";
9
+ import { Icon } from "./Icon.js";
10
+ import { colors } from "../utils/themes.js";
11
+
12
+ export const Pagination = (props) => {
13
+ const {
14
+ totalItems = 0,
15
+ pageSize = 10,
16
+ currentPage = 1,
17
+ onPageChange,
18
+ showFirstLast = true,
19
+ showPrevNext = true,
20
+ maxButtons = 5,
21
+ variant = "outlined",
22
+ color = colors.primary,
23
+ size = "medium",
24
+ disabled = false,
25
+ showTotal = true,
26
+ label = "Page",
27
+ ...rest
28
+ } = props;
29
+
30
+ const totalPages = Math.ceil(totalItems / pageSize);
31
+ if (totalPages <= 1) return null;
32
+
33
+ let current = Math.min(Math.max(currentPage, 1), totalPages);
34
+
35
+ const handlePageChange = (page) => {
36
+ if (disabled) return;
37
+ if (page === current) return;
38
+ if (page < 1 || page > totalPages) return;
39
+ current = page;
40
+ if (onPageChange) onPageChange(current);
41
+ };
42
+
43
+ const getPageNumbers = () => {
44
+ const pages = [];
45
+ let start = Math.max(1, current - Math.floor(maxButtons / 2));
46
+ let end = Math.min(totalPages, start + maxButtons - 1);
47
+
48
+ if (end - start + 1 < maxButtons) {
49
+ start = Math.max(1, end - maxButtons + 1);
50
+ }
51
+
52
+ for (let i = start; i <= end; i++) {
53
+ pages.push(i);
54
+ }
55
+ return pages;
56
+ };
57
+
58
+ const pageNumbers = getPageNumbers();
59
+ const sizes = {
60
+ small: { buttonSize: 32, fontSize: 12, iconSize: 16, gap: 4 },
61
+ medium: { buttonSize: 36, fontSize: 14, iconSize: 20, gap: 6 },
62
+ large: { buttonSize: 42, fontSize: 16, iconSize: 24, gap: 8 },
63
+ };
64
+ const sz = sizes[size] || sizes.medium;
65
+
66
+ const buttonBase = {
67
+ minWidth: `${sz.buttonSize}px`,
68
+ height: `${sz.buttonSize}px`,
69
+ padding: "0",
70
+ fontSize: `${sz.fontSize}px`,
71
+ };
72
+
73
+ const createPageButton = (page, isActive = false) => {
74
+ const buttonVariant = isActive ? "filled" : variant;
75
+ const buttonColor = isActive ? color : colors.textSecondary;
76
+
77
+ return Button({
78
+ text: String(page),
79
+ variant: buttonVariant,
80
+ bgColor: isActive ? color : "transparent",
81
+ color: buttonColor,
82
+ size: size,
83
+ ...buttonBase,
84
+ onPress: () => handlePageChange(page),
85
+ disabled: disabled || isActive,
86
+ });
87
+ };
88
+
89
+ const createIconButton = (iconName, onClick, isDisabled = false) => {
90
+ return Button({
91
+ child: Icon({
92
+ name: iconName,
93
+ size: sz.iconSize,
94
+ color: colors.textSecondary,
95
+ }),
96
+ variant: variant,
97
+ size: size,
98
+ ...buttonBase,
99
+ onPress: onClick,
100
+ disabled: disabled || isDisabled,
101
+ disableNativeEffects: true,
102
+ });
103
+ };
104
+
105
+ // Main container using Row (already a widget)
106
+ const container = Row({
107
+ widgetName: "Pagination",
108
+ alignItems: "center",
109
+ gap: sz.gap,
110
+ justifyContent: "center",
111
+ flexWrap: "wrap",
112
+ ...rest,
113
+ });
114
+
115
+ // First button
116
+ if (showFirstLast && totalPages > 1) {
117
+ container.appendChild(
118
+ createIconButton("first_page", () => handlePageChange(1), current === 1),
119
+ );
120
+ }
121
+
122
+ // Previous button
123
+ if (showPrevNext) {
124
+ container.appendChild(
125
+ createIconButton(
126
+ "chevron_left",
127
+ () => handlePageChange(current - 1),
128
+ current === 1,
129
+ ),
130
+ );
131
+ }
132
+
133
+ // Page numbers
134
+ if (pageNumbers[0] > 1) {
135
+ container.appendChild(createPageButton(1));
136
+ if (pageNumbers[0] > 2) {
137
+ container.appendChild(
138
+ Text({ text: "...", size: sz.fontSize, color: colors.textSecondary }),
139
+ );
140
+ }
141
+ }
142
+
143
+ pageNumbers.forEach((page) => {
144
+ container.appendChild(createPageButton(page, page === current));
145
+ });
146
+
147
+ if (pageNumbers[pageNumbers.length - 1] < totalPages) {
148
+ if (pageNumbers[pageNumbers.length - 1] < totalPages - 1) {
149
+ container.appendChild(
150
+ Text({ text: "...", size: sz.fontSize, color: colors.textSecondary }),
151
+ );
152
+ }
153
+ container.appendChild(createPageButton(totalPages));
154
+ }
155
+
156
+ // Next button
157
+ if (showPrevNext) {
158
+ container.appendChild(
159
+ createIconButton(
160
+ "chevron_right",
161
+ () => handlePageChange(current + 1),
162
+ current === totalPages,
163
+ ),
164
+ );
165
+ }
166
+
167
+ // Last button
168
+ if (showFirstLast && totalPages > 1) {
169
+ container.appendChild(
170
+ createIconButton(
171
+ "last_page",
172
+ () => handlePageChange(totalPages),
173
+ current === totalPages,
174
+ ),
175
+ );
176
+ }
177
+
178
+ // Total info
179
+ if (showTotal && totalItems > 0) {
180
+ const start = (current - 1) * pageSize + 1;
181
+ const end = Math.min(current * pageSize, totalItems);
182
+ const infoText = `${label} ${current} of ${totalPages} (${start}-${end} of ${totalItems})`;
183
+ const info = Text({
184
+ text: infoText,
185
+ size: sz.fontSize - 2,
186
+ color: colors.textSecondary,
187
+ marginLeft: sz.gap * 2,
188
+ });
189
+ container.appendChild(info);
190
+ }
191
+
192
+ return container;
193
+ };
194
+
195
+ export default Pagination;