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,254 @@
1
+ // utils/Inspector.js
2
+ import { getWidgetProps } from "../utils/getWidgetProps.js";
3
+
4
+ /**
5
+ * Converts a JavaScript value to its source-code string representation.
6
+ * Handles primitives, arrays, plain objects, and nested widget elements
7
+ * by recursively calling `Inspector` for any DOM node that carries `_widgetName`.
8
+ *
9
+ * @param {*} value - The value to stringify.
10
+ * @param {number} [indent=0] - Current indentation depth (used for pretty-printing nested structures).
11
+ * @returns {string} A human-readable code representation of the value.
12
+ */
13
+ const stringifyValue = (value, indent = 0) => {
14
+ const spaces = " ".repeat(indent);
15
+
16
+ if (value === null) return "null";
17
+ if (value === undefined) return "undefined";
18
+
19
+ if (typeof value === "function") {
20
+ return "() => {}";
21
+ }
22
+
23
+ if (typeof value === "string") {
24
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
25
+ return `'${escaped}'`;
26
+ }
27
+
28
+ if (typeof value === "number") return String(value);
29
+ if (typeof value === "boolean") return value ? "true" : "false";
30
+
31
+ if (Array.isArray(value)) {
32
+ if (value.length === 0) return "[]";
33
+ const items = value
34
+ .map((item) => stringifyValue(item, indent + 1))
35
+ .join(",\n" + spaces + " ");
36
+ return `[\n${spaces} ${items}\n${spaces}]`;
37
+ }
38
+
39
+ if (typeof value === "object" && value !== null) {
40
+ // If the object is a widget DOM node, delegate to Inspector for full reconstruction
41
+ if (value.nodeType === 1 && value._widgetName) {
42
+ return Inspector(value, indent);
43
+ }
44
+
45
+ const entries = Object.entries(value);
46
+ if (entries.length === 0) return "{}";
47
+
48
+ const props = entries
49
+ .map(([k, v]) => {
50
+ if (typeof v === "number" || typeof v === "boolean") {
51
+ return `${k}: ${v}`;
52
+ }
53
+ if (typeof v === "string") {
54
+ return `${k}: '${v.replace(/'/g, "\\'")}'`;
55
+ }
56
+ if (v && v.nodeType === 1) {
57
+ return `${k}: ${Inspector(v, indent + 1)}`;
58
+ }
59
+ return `${k}: ${stringifyValue(v, indent + 1)}`;
60
+ })
61
+ .join(",\n" + spaces + " ");
62
+
63
+ return `{\n${spaces} ${props}\n${spaces}}`;
64
+ }
65
+
66
+ return String(value);
67
+ };
68
+
69
+ /**
70
+ * Resolves the display name of a widget element.
71
+ *
72
+ * Names come from the underlying HTML tag (documented behavior): `span` →
73
+ * `Span()`, `div` → `Div()`, `button` → `Button()`, … `_widgetName` is used
74
+ * only as a fallback for tags without a mapping so the output reads like real
75
+ * DOM.
76
+ *
77
+ * @param {HTMLElement} widget - The DOM node whose name to resolve.
78
+ * @returns {string} The widget name (e.g. "Button", "Span", "Div").
79
+ */
80
+ const TAG_NAMES = {
81
+ SPAN: "Span",
82
+ DIV: "Div",
83
+ BUTTON: "Button",
84
+ INPUT: "Input",
85
+ TEXTAREA: "Textarea",
86
+ IMG: "Image",
87
+ CANVAS: "Canvas",
88
+ VIDEO: "Video",
89
+ AUDIO: "Audio",
90
+ SELECT: "Select",
91
+ TABLE: "Table",
92
+ TH: "Th",
93
+ TD: "Td",
94
+ TR: "Tr",
95
+ UL: "Ul",
96
+ OL: "Ol",
97
+ LI: "Li",
98
+ A: "A",
99
+ CODE: "Code",
100
+ PRE: "Pre",
101
+ HEADER: "Header",
102
+ FOOTER: "Footer",
103
+ };
104
+
105
+ const getWidgetName = (widget) => {
106
+ const tag = (widget.tagName || "").toUpperCase();
107
+ if (TAG_NAMES[tag]) return TAG_NAMES[tag];
108
+ if (widget._widgetName) return widget._widgetName;
109
+ return widget.tagName?.toLowerCase() || "unknown";
110
+ };
111
+
112
+ /**
113
+ * Collects all direct child elements of a widget, merging both the real DOM
114
+ * children and any virtual children stored in `_children` (set by WidgetFactory).
115
+ * Deduplication prevents the same node from appearing twice.
116
+ *
117
+ * @param {HTMLElement} widget - The widget whose children to collect.
118
+ * @returns {HTMLElement[]} Deduplicated array of child element nodes.
119
+ */
120
+ const collectChildren = (widget) => {
121
+ const children = [];
122
+
123
+ for (const child of widget.children) {
124
+ if (child.nodeType === 1) {
125
+ children.push(child);
126
+ }
127
+ }
128
+
129
+ // _children may hold widget references that aren't yet in the DOM
130
+ if (widget._children && Array.isArray(widget._children)) {
131
+ for (const child of widget._children) {
132
+ if (child && child.nodeType === 1 && !children.includes(child)) {
133
+ children.push(child);
134
+ }
135
+ }
136
+ }
137
+
138
+ return children;
139
+ };
140
+
141
+ /**
142
+ * Converts a widget element back into its equivalent source-code representation,
143
+ * recursively reconstructing its full prop tree and children.
144
+ *
145
+ * This is the core utility for the widget inspector — it lets you paste the output
146
+ * directly into your code to reproduce the exact widget configuration.
147
+ *
148
+ * @param {HTMLElement} widget - The root widget to inspect.
149
+ * @param {number} [indent=0] - Starting indentation depth for pretty-printing.
150
+ * @returns {string} Source code string such as `Row({ gap: 8, children: [...] })`.
151
+ */
152
+ export const Inspector = (widget, indent = 0) => {
153
+ if (!widget || widget.nodeType !== 1) {
154
+ return String(widget);
155
+ }
156
+
157
+ const name = getWidgetName(widget);
158
+ const props = getWidgetProps(widget);
159
+ const spaces = " ".repeat(indent);
160
+ const nextSpaces = " ".repeat(indent + 1);
161
+
162
+ // Separate structural props from the rest so we can render them last
163
+ const { child, children, ...otherProps } = props;
164
+
165
+ const propsList = Object.entries(otherProps)
166
+ .map(([key, value]) => {
167
+ const formattedValue = stringifyValue(value, indent + 1);
168
+ return `${key}: ${formattedValue}`;
169
+ })
170
+ .join(",\n" + nextSpaces);
171
+
172
+ // Reconstruct a single `child` prop
173
+ let childStr = "";
174
+ if (child && child.nodeType === 1) {
175
+ childStr = `\n${nextSpaces}child: ${Inspector(child, indent + 1)}`;
176
+ }
177
+
178
+ // Reconstruct a `children` array prop
179
+ let childrenStr = "";
180
+ if (children && Array.isArray(children) && children.length > 0) {
181
+ const childrenItems = children
182
+ .filter((c) => c && c.nodeType === 1)
183
+ .map((c) => Inspector(c, indent + 1))
184
+ .join(",\n" + nextSpaces + " ");
185
+
186
+ if (childrenItems) {
187
+ childrenStr = `\n${nextSpaces}children: [\n${nextSpaces} ${childrenItems}\n${nextSpaces}]`;
188
+ }
189
+ }
190
+
191
+ // Fall back to actual DOM children when no explicit child/children props exist
192
+ const domChildren = collectChildren(widget);
193
+ let domChildrenStr = "";
194
+ if (
195
+ domChildren.length > 0 &&
196
+ !child &&
197
+ (!children || children.length === 0)
198
+ ) {
199
+ const domChildrenItems = domChildren
200
+ .map((c) => Inspector(c, indent + 1))
201
+ .join(",\n" + nextSpaces + " ");
202
+
203
+ if (domChildrenItems) {
204
+ domChildrenStr = `\n${nextSpaces}children: [\n${nextSpaces} ${domChildrenItems}\n${nextSpaces}]`;
205
+ }
206
+ }
207
+
208
+ const allContent = [
209
+ propsList && `${nextSpaces}${propsList}`,
210
+ childStr,
211
+ childrenStr,
212
+ domChildrenStr,
213
+ ]
214
+ .filter(Boolean)
215
+ .join(",\n");
216
+
217
+ if (allContent) {
218
+ return `${name}({\n${allContent}\n${spaces}})`;
219
+ }
220
+ return `${name}()`;
221
+ };
222
+
223
+ /**
224
+ * Logs the widget's source-code representation to the console with green
225
+ * monospace styling — handy for quick visual inspection during development.
226
+ *
227
+ * @param {HTMLElement} widget - The widget to print.
228
+ */
229
+ export const printWidgetCode = (widget) => {
230
+ console.log(
231
+ "%c" + Inspector(widget),
232
+ "color: #4ade80; font-family: monospace;",
233
+ );
234
+ };
235
+
236
+ /**
237
+ * Performs a full diagnostic inspection of a widget, logging its name,
238
+ * resolved props, reconstructed source code, and child count as a grouped
239
+ * console entry. Returns the widget unchanged so it can be used inline
240
+ * (e.g. `inspectWidget(myWidget).style.color = "red"`).
241
+ *
242
+ * @param {HTMLElement} widget - The widget to inspect.
243
+ * @returns {HTMLElement} The same widget, unmodified.
244
+ */
245
+ export const inspectWidget = (widget) => {
246
+ console.group(`🔍 ${getWidgetName(widget)}`);
247
+ console.log("📦 Props:", getWidgetProps(widget));
248
+ console.log("📝 Code:\n", Inspector(widget));
249
+ console.log("👪 Children:", collectChildren(widget).length);
250
+ console.groupEnd();
251
+ return widget;
252
+ };
253
+
254
+ export default { Inspector, printWidgetCode, inspectWidget };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @file InstallButton.js
3
+ * @description PWA install prompt button; hidden unless install is available.
4
+ */
5
+ import { Button } from "./Button.js";
6
+ import { colors } from "../utils/themes.js";
7
+
8
+ export const InstallButton = (props) => {
9
+ const {
10
+ text = "📲 Install",
11
+ variant = "filled",
12
+ size = "medium",
13
+ borderRadius = 28,
14
+ padding = "12px 24px",
15
+ bottom = 20,
16
+ backgroundColor,
17
+ color,
18
+ onInstalled,
19
+ onClick,
20
+ ...rest
21
+ } = props;
22
+
23
+ let deferredPrompt = null;
24
+ let button = null;
25
+ let isInstalled = false;
26
+
27
+ const handleClick = async () => {
28
+ if (deferredPrompt) {
29
+ deferredPrompt.prompt();
30
+ const { outcome } = await deferredPrompt.userChoice;
31
+ if (outcome === "accepted") {
32
+ if (onInstalled) onInstalled();
33
+ hideButton();
34
+ }
35
+ deferredPrompt = null;
36
+ }
37
+ if (onClick) onClick();
38
+ };
39
+
40
+ const showButton = () => {
41
+ if (button && !isInstalled) {
42
+ button.style.opacity = "1";
43
+ button.style.visibility = "visible";
44
+ button.style.display = "flex";
45
+ }
46
+ };
47
+
48
+ const hideButton = () => {
49
+ if (button) {
50
+ button.style.opacity = "0";
51
+ button.style.visibility = "hidden";
52
+ button.style.display = "none";
53
+ }
54
+ };
55
+
56
+ // Createste button using the flet-box Button component
57
+ button = Button({
58
+ widgetName: "InstallButton",
59
+ text: text,
60
+ variant: variant,
61
+ size: size,
62
+ bgColor: backgroundColor,
63
+ color: color,
64
+ borderRadius: borderRadius,
65
+ padding: padding,
66
+ position: "fixed",
67
+ bottom: `${bottom}px`,
68
+ left: "50%",
69
+ transform: "translateX(-50%)",
70
+ zIndex: 10000,
71
+ opacity: "0",
72
+ visibility: "hidden",
73
+ display: "none",
74
+ boxShadow: "0 4px 12px rgba(0,0,0,0.2)",
75
+ onPress: handleClick,
76
+ ...rest,
77
+ });
78
+
79
+ // Listen to events
80
+ const handleBeforeInstallPrompt = (e) => {
81
+ e.preventDefault();
82
+ deferredPrompt = e;
83
+ showButton();
84
+ };
85
+
86
+ const handleAppInstalled = () => {
87
+ isInstalled = true;
88
+ hideButton();
89
+ if (onInstalled) onInstalled();
90
+ };
91
+
92
+ window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
93
+ window.addEventListener("appinstalled", handleAppInstalled);
94
+
95
+ // Cleanup
96
+ const cleanup = () => {
97
+ window.removeEventListener(
98
+ "beforeinstallprompt",
99
+ handleBeforeInstallPrompt,
100
+ );
101
+ window.removeEventListener("appinstalled", handleAppInstalled);
102
+ if (button && button.remove) button.remove();
103
+ };
104
+
105
+ button.onUnmount(cleanup);
106
+
107
+ return button;
108
+ };
109
+
110
+ export default InstallButton;
@@ -0,0 +1,224 @@
1
+ /**
2
+ * @file ListTile.js
3
+ * @description List row with leading/trailing content and optional divider.
4
+ */
5
+ import { WidgetFactory } from "../widget-factory/index.js";
6
+ import { Row } from "./Row.js";
7
+ import { Column } from "./Column.js";
8
+ import { Text } from "./Text.js";
9
+ import { colors, subscribeTheme } from "../utils/themes.js";
10
+
11
+ /**
12
+ * @typedef {Object} ListTileProps
13
+ * @property {HTMLElement} [leftItem] - Widget rendered on the leading edge (e.g. an avatar or icon).
14
+ * @property {HTMLElement} [rightItem] - Widget rendered on the trailing edge (e.g. a chevron or switch).
15
+ * @property {string} [title] - Primary text line.
16
+ * @property {string} [subtitle] - Secondary text line rendered below the title at reduced size.
17
+ * @property {string} [description] - Tertiary text line rendered below the subtitle, typically muted.
18
+ * @property {Function} [onPress] - Click handler. When provided the tile becomes interactive.
19
+ * @property {boolean} [selected=false] - Highlights the tile with `selectedBgColor` and bolds the title.
20
+ * @property {boolean} [disabled=false] - Reduces opacity and removes interactivity.
21
+ * @property {boolean} [divider=false] - When true, appends a thin horizontal rule below the tile.
22
+ * @property {number} [paddingHorizontal=16] - Horizontal padding in pixels.
23
+ * @property {number} [paddingVertical=12] - Vertical padding in pixels.
24
+ * @property {number} [gap=12] - Gap between the left item, text column, and right item.
25
+ * @property {number} [elevation=0] - Drop-shadow depth. 0 means no shadow.
26
+ * @property {number} [borderRadius=0] - Corner radius of the tile in pixels.
27
+ * @property {string} [bgColor] - Default background color.
28
+ * @property {string} [selectedBgColor] - Background color when `selected` is true.
29
+ * @property {string} [hoverColor] - Background color on mouse hover (only when interactive).
30
+ * @property {Object} [titleProps={}] - Extra props forwarded to the `Text` widget for the title.
31
+ * @property {Object} [subtitleProps={}] - Extra props forwarded to the `Text` widget for the subtitle.
32
+ * @property {Object} [descriptionProps={}] - Extra props forwarded to the `Text` widget for the description.
33
+ */
34
+
35
+ /**
36
+ * A single-row list item that follows the Material Design ListTile pattern.
37
+ *
38
+ * Composes a leading widget, a text column (title / subtitle / description),
39
+ * and a trailing widget into a horizontally-aligned row. Supports selection
40
+ * highlighting, hover effects, elevation shadows, and an optional divider.
41
+ * Automatically unsubscribes from the theme system when the tile is unmounted.
42
+ *
43
+ * @param {ListTileProps} props
44
+ * @returns {HTMLElement} The tile element (or a wrapper `<div>` when `divider` is true).
45
+ */
46
+ export const ListTile = (props) => {
47
+ const {
48
+ leftItem,
49
+ rightItem,
50
+ title,
51
+ subtitle,
52
+ description,
53
+ onPress,
54
+ selected = false,
55
+ disabled = false,
56
+ divider = false,
57
+ paddingHorizontal = 16,
58
+ paddingVertical = 12,
59
+ gap = 12,
60
+ elevation = 0,
61
+ borderRadius = 0,
62
+ bgColor = colors.surface,
63
+ selectedBgColor = `${colors.primary}20`,
64
+ hoverColor = colors.border,
65
+ titleProps = {},
66
+ subtitleProps = {},
67
+ descriptionProps = {},
68
+ ...rest
69
+ } = props;
70
+
71
+ // Only add pointer cursor and hover/click behavior when an onPress handler exists
72
+ // and the tile is not disabled.
73
+ const isInteractive = onPress && !disabled;
74
+
75
+ // Build text column — only include lines that were actually provided
76
+ const textChildren = [];
77
+
78
+ if (title) {
79
+ textChildren.push(
80
+ Text({
81
+ text: title,
82
+ size: 16,
83
+ weight: selected ? "600" : "500",
84
+ color: selected ? colors.primary : colors.text,
85
+ ...titleProps,
86
+ }),
87
+ );
88
+ }
89
+
90
+ if (subtitle) {
91
+ textChildren.push(
92
+ Text({
93
+ text: subtitle,
94
+ size: 13,
95
+ color: colors.textSecondary,
96
+ marginTop: 2,
97
+ ...subtitleProps,
98
+ }),
99
+ );
100
+ }
101
+
102
+ if (description) {
103
+ textChildren.push(
104
+ Text({
105
+ text: description,
106
+ size: 12,
107
+ color: colors.textDisabled,
108
+ marginTop: 2,
109
+ ...descriptionProps,
110
+ }),
111
+ );
112
+ }
113
+
114
+ // Wrap text lines in a Column only when at least one line was provided
115
+ const centerColumn =
116
+ textChildren.length > 0
117
+ ? Column({
118
+ gap: 2,
119
+ alignItems: "flex-start",
120
+ flex: 1,
121
+ minWidth: 0, // allows the column to shrink below its content size in a flex row
122
+ children: textChildren,
123
+ })
124
+ : null;
125
+
126
+ // Assemble the main content row: [leftItem] [textColumn] [rightItem]
127
+ const rowChildren = [];
128
+ if (leftItem) rowChildren.push(leftItem);
129
+ if (centerColumn) rowChildren.push(centerColumn);
130
+ if (rightItem) rowChildren.push(rightItem);
131
+
132
+ const contentRow = Row({
133
+ alignItems: "center",
134
+ gap: gap,
135
+ padding: `${paddingVertical}px ${paddingHorizontal}px`,
136
+ width: "100%",
137
+ boxSizing: "border-box",
138
+ children: rowChildren,
139
+ });
140
+
141
+ // Main tile element
142
+ const tile = WidgetFactory({
143
+ tag: "div",
144
+ widgetName: "ListTile",
145
+ backgroundColor: selected ? selectedBgColor : bgColor,
146
+ borderRadius: borderRadius,
147
+ cursor: isInteractive ? "pointer" : "default",
148
+ opacity: disabled ? 0.6 : 1,
149
+ boxShadow:
150
+ elevation > 0
151
+ ? `0 ${elevation}px ${elevation * 2}px rgba(0,0,0,0.1)`
152
+ : "none",
153
+ transition: "all 0.2s ease",
154
+ width: "100%",
155
+ child: contentRow,
156
+ onclick: isInteractive ? onPress : null,
157
+ ...rest,
158
+ });
159
+
160
+ // Hover effects — only when the tile can be interacted with
161
+ if (isInteractive) {
162
+ tile.addEventListener("mouseenter", () => {
163
+ if (!selected) tile.style.backgroundColor = hoverColor;
164
+ if (elevation > 0) {
165
+ tile.style.transform = "translateY(-2px)";
166
+ tile.style.boxShadow = `0 ${elevation + 2}px ${(elevation + 2) * 2}px rgba(0,0,0,0.15)`;
167
+ }
168
+ });
169
+ tile.addEventListener("mouseleave", () => {
170
+ if (!selected) tile.style.backgroundColor = bgColor;
171
+ if (elevation > 0) {
172
+ tile.style.transform = "translateY(0)";
173
+ tile.style.boxShadow = `0 ${elevation}px ${elevation * 2}px rgba(0,0,0,0.1)`;
174
+ }
175
+ });
176
+ }
177
+
178
+ // Theme subscription — keep the background in sync with the active theme.
179
+ // Only subscribe when the caller has not overridden bgColor/selectedBgColor,
180
+ // to avoid fighting with explicit colors.
181
+ let unsubscribeTheme = null;
182
+ if (!props.bgColor && !props.selectedBgColor) {
183
+ unsubscribeTheme = subscribeTheme(() => {
184
+ if (!selected) {
185
+ tile.style.backgroundColor = colors.surface;
186
+ } else {
187
+ tile.style.backgroundColor = `${colors.primary}20`;
188
+ }
189
+ });
190
+ }
191
+
192
+ // Cleanup — unsubscribe from the theme system when the tile is unmounted
193
+ tile.onUnmount(() => {
194
+ if (unsubscribeTheme) unsubscribeTheme();
195
+ });
196
+
197
+ // When a divider is requested, wrap the tile in a column and append a separator line
198
+ if (divider) {
199
+ const dividerLine = WidgetFactory({
200
+ height: 1,
201
+ backgroundColor: colors.border,
202
+ marginLeft: `${paddingHorizontal}px`,
203
+ width: `calc(100% - ${paddingHorizontal}px)`,
204
+ });
205
+
206
+ const container = WidgetFactory({
207
+ width: "100%",
208
+ display: "flex",
209
+ flexDirection: "column",
210
+ children: [tile, dividerLine],
211
+ });
212
+
213
+ // Propagate cleanup to the outer container as well
214
+ container.onUnmount(() => {
215
+ if (unsubscribeTheme) unsubscribeTheme();
216
+ });
217
+
218
+ return container;
219
+ }
220
+
221
+ return tile;
222
+ };
223
+
224
+ export default ListTile;