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,65 @@
1
+ /**
2
+ * @file composeUpdate.js
3
+ * @description
4
+ * Composes a widget's own prop-application logic with the factory's reactive
5
+ * `update` instead of overwriting it.
6
+ *
7
+ * Widgets such as Accordion, Slider and Dropdown keep widget state (currentValue,
8
+ * currentOptions, …) in a closure and need a bespoke `update` to apply it. The
9
+ * naive implementation `container.update = update` destroys the factory's update,
10
+ * which re-applies styles, events, effects and children — so generic calls like
11
+ * `slider.update({ opacity: .5 })` silently stopped working.
12
+ *
13
+ * `composeUpdate` splits every `update(newProps)` call in two:
14
+ * - keys owned by the widget (the `widgetPropKeys` set) → `applyWidgetProps`
15
+ * - everything else → the factory's original `update`
16
+ *
17
+ * Widget-owned keys are also stripped from `_originalProps`, because the factory
18
+ * merges them back on every update: a stale construction-time value (say
19
+ * `border: "1px solid red"`) would be re-applied as a style and silently clobber
20
+ * a later widget-level change of the same prop.
21
+ *
22
+ * @module composeUpdate
23
+ */
24
+
25
+ /**
26
+ * Wires `container.update` so widget props and factory props coexist.
27
+ *
28
+ * @param {HTMLElement} container - The widget root returned by `WidgetFactory`.
29
+ * @param {Iterable<string>} widgetPropKeys - Prop names owned by the widget.
30
+ * These are routed to `applyWidgetProps` and never reach the factory.
31
+ * @param {function(Object): void} applyWidgetProps - The widget's own prop
32
+ * application logic. It receives only widget-owned keys and may ignore keys it
33
+ * does not handle (preserving each widget's existing behavior).
34
+ * @returns {HTMLElement} The same container, for chaining.
35
+ */
36
+ export function composeUpdate(container, widgetPropKeys, applyWidgetProps) {
37
+ const widgetKeys = new Set(widgetPropKeys);
38
+ const factoryUpdate = container.update;
39
+
40
+ // The factory re-processes `_originalProps` on every update; widget-owned keys
41
+ // must not live there or they would be re-applied as styles/attributes from
42
+ // their construction-time values.
43
+ if (container._originalProps) {
44
+ for (const key of widgetKeys) delete container._originalProps[key];
45
+ }
46
+
47
+ container.update = (newProps = {}) => {
48
+ const widgetProps = {};
49
+ const factoryProps = {};
50
+ for (const key of Object.keys(newProps)) {
51
+ if (widgetKeys.has(key)) widgetProps[key] = newProps[key];
52
+ else factoryProps[key] = newProps[key];
53
+ }
54
+
55
+ applyWidgetProps(widgetProps);
56
+
57
+ if (Object.keys(factoryProps).length > 0) factoryUpdate(factoryProps);
58
+
59
+ return container;
60
+ };
61
+
62
+ return container;
63
+ }
64
+
65
+ export default composeUpdate;
@@ -0,0 +1,35 @@
1
+ // utils/getWidgetProps.js
2
+
3
+ /**
4
+ * Gets the current props of a widget
5
+ * @param {HTMLElement} widget - The widget to inspect
6
+ * @returns {Object} Dictionary with the widget's props
7
+ *
8
+ * @example
9
+ * const props = getWidgetProps(myButton);
10
+ * // { text: 'Click', variant: 'filled', color: '#007aff' }
11
+ */
12
+ export const getWidgetProps = (widget) => {
13
+ return { ...(widget._props || {}) };
14
+ };
15
+
16
+ /**
17
+ * Gets a specific prop from a widget
18
+ * @param {HTMLElement} widget - The widget to inspect
19
+ * @param {string} propName - Name of the prop
20
+ * @returns {any} Prop value or undefined
21
+ */
22
+ export const getWidgetProp = (widget, propName) => {
23
+ return widget._props ? widget._props[propName] : undefined;
24
+ };
25
+
26
+ /**
27
+ * Gets all props as a formatted string
28
+ * @param {HTMLElement} widget - The widget to inspect
29
+ * @returns {string} Formatted props string
30
+ */
31
+ export const stringifyWidgetProps = (widget) => {
32
+ return JSON.stringify(widget._props || {}, null, 2);
33
+ };
34
+
35
+ export default { getWidgetProps, getWidgetProp, stringifyWidgetProps };
@@ -0,0 +1,76 @@
1
+ // src/utils/index.js
2
+ /**
3
+ * @module utils
4
+ * @description
5
+ * Central barrel file — re-exports every public utility from the `utils/`
6
+ * directory so consumers can import from a single entry point:
7
+ *
8
+ * ```js
9
+ * import { colors, toREM, addNavigation, TextInputValidator } from './utils';
10
+ * ```
11
+ *
12
+ * Included utilities:
13
+ * - **themes** — color palette, theme switching, CSS variable sync
14
+ * - **getWidgetProps** — read widget `_props` safely
15
+ * - **syntaxHighlight** — JavaScript/FletBox tokenizer and HTML highlighter
16
+ * - **markdownParser** — markdown → FletBox widget tree converter
17
+ * - **visualEffects** — CSS animations (stripes, shimmer, glow, pulse)
18
+ * - **stackPosition** — dataset-based absolute positioning for Stack children
19
+ * - **units** — px ↔ rem conversion helpers
20
+ * - **navigation** — tree-navigation mixin for widget objects
21
+ * - **stopWebRefresh** — disables browser refresh gestures
22
+ * - **TextInputValidator** — input filtering and XSS sanitization
23
+ * - **mediaTime** — time formatting for media players
24
+ */
25
+ export {
26
+ colors,
27
+ setTheme,
28
+ getTheme,
29
+ toggleTheme,
30
+ subscribeTheme,
31
+ applySystemTheme,
32
+ watchSystemTheme,
33
+ getColor,
34
+ palettes,
35
+ ThemeProvider,
36
+ useTheme,
37
+ } from "./themes.js";
38
+ export {
39
+ getWidgetProps,
40
+ getWidgetProp,
41
+ stringifyWidgetProps,
42
+ } from "./getWidgetProps.js";
43
+ export {
44
+ tokenize,
45
+ generateHighlightedHtml,
46
+ highlightColors,
47
+ } from "./syntaxHighlight.js";
48
+ export {
49
+ markdownToWidgets,
50
+ parseMarkdownToWidgets,
51
+ parseInlineToWidgets,
52
+ parseMarkdown,
53
+ parseInlineMarkdown,
54
+ escapeHtml,
55
+ } from "./markdownParser.js";
56
+ export {
57
+ applyStripes,
58
+ removeStripes,
59
+ applyShimmer,
60
+ applyGlow,
61
+ applyIndeterminate,
62
+ applyPulse,
63
+ injectKeyframes,
64
+ } from "./visualEffects.js";
65
+ export { default as stackPosition } from "./stackPosition.js";
66
+ export { toREM, setBaseFontSize, toPX, getBaseFontSize } from "./units.js";
67
+ export { addNavigation } from "./navigation.js";
68
+ export { stopWebRefresh } from "./stopWebRefresh.js";
69
+ export { TextInputValidator } from "./TextInputValidator.js";
70
+ export {
71
+ formatMediaTime,
72
+ formatMediaTimeLong,
73
+ getProgressPercent,
74
+ percentToSeconds,
75
+ formatMediaProgress,
76
+ } from "./mediaTime.js";
@@ -0,0 +1,368 @@
1
+ // utils/markdownParser.js
2
+ import { DataTable } from "../widgets/DataTable.js";
3
+ import { Text } from "../widgets/Text.js";
4
+ import { Container } from "../widgets/Container.js";
5
+ import { Row } from "../widgets/Row.js";
6
+ import { Icon } from "../widgets/Icon.js";
7
+ import { CodeViewer } from "../widgets/CodeViewer.js";
8
+ import { colors } from "./themes.js";
9
+
10
+ // ========== TABLES ==========
11
+ const isTableLine = (line) => {
12
+ return (
13
+ line.includes("|") &&
14
+ line.trim().startsWith("|") &&
15
+ line.trim().endsWith("|")
16
+ );
17
+ };
18
+
19
+ const isTableSeparatorLine = (line) => {
20
+ return (
21
+ line.includes("|") &&
22
+ line.includes("-") &&
23
+ /[\s\|]*:?-+:?[\s\|]*/.test(line)
24
+ );
25
+ };
26
+
27
+ const parseTable = (lines, startIndex) => {
28
+ let i = startIndex;
29
+ const headerLine = lines[i].trim();
30
+ const separatorLine = lines[i + 1]?.trim();
31
+
32
+ if (!isTableLine(headerLine)) return null;
33
+ if (!isTableSeparatorLine(separatorLine)) return null;
34
+
35
+ const headers = headerLine
36
+ .split("|")
37
+ .filter((cell) => cell.trim() !== "")
38
+ .map((cell) => cell.trim());
39
+
40
+ if (headers.length === 0) return null;
41
+
42
+ i += 2;
43
+
44
+ const rows = [];
45
+ while (i < lines.length) {
46
+ const line = lines[i].trim();
47
+ if (!isTableLine(line)) break;
48
+
49
+ const cells = line
50
+ .split("|")
51
+ .filter((cell) => cell.trim() !== "")
52
+ .map((cell) => cell.trim());
53
+
54
+ if (cells.length === headers.length) {
55
+ const rowData = {};
56
+ headers.forEach((header, idx) => {
57
+ rowData[header] = cells[idx] || "";
58
+ });
59
+ rows.push(rowData);
60
+ }
61
+ i++;
62
+ }
63
+
64
+ const columns = headers.map((h) => ({ key: h, label: h }));
65
+ const tableWidget = DataTable({
66
+ columns: columns,
67
+ rows: rows,
68
+ striped: true,
69
+ hoverable: true,
70
+ bordered: true,
71
+ compact: false,
72
+ });
73
+
74
+ return { widget: tableWidget, nextIndex: i };
75
+ };
76
+
77
+ // ========== INLINE TEXT ==========
78
+ /**
79
+ * Parses an inline markdown string and converts it to an array of widgets.
80
+ * Currently handles `**bold**` syntax; all other text is rendered as plain
81
+ * `Text` widgets. Multiple parts are wrapped in a `Row` with word-wrapping.
82
+ * @param {string} text - Inline markdown text to parse.
83
+ * @returns {Array} Array of widget instances (Text or Row).
84
+ */
85
+ export const parseInlineToWidgets = (text) => {
86
+ if (!text) return [Text({ text: "" })];
87
+
88
+ const parts = [];
89
+ let remaining = text;
90
+
91
+ const boldRegex = /\*\*(.*?)\*\*/;
92
+ let match;
93
+ while ((match = boldRegex.exec(remaining)) !== null) {
94
+ const before = remaining.substring(0, match.index);
95
+ if (before) parts.push(Text({ text: before, size: 14 }));
96
+ parts.push(Text({ text: match[1], size: 14, weight: "bold" }));
97
+ remaining = remaining.substring(match.index + match[0].length);
98
+ }
99
+ if (remaining) parts.push(Text({ text: remaining, size: 14 }));
100
+
101
+ if (parts.length === 0) return [Text({ text: text, size: 14 })];
102
+ if (parts.length === 1) return parts;
103
+
104
+ return [
105
+ Row({ alignItems: "center", gap: 4, children: parts, flexWrap: "wrap" }),
106
+ ];
107
+ };
108
+
109
+ // ========== MAIN PARSER ==========
110
+ /**
111
+ * Converts a markdown string to an array of FletBox widgets.
112
+ *
113
+ * Supported syntax:
114
+ * - Headings: `#`, `##`, `###`
115
+ * - Bold: `**text**`
116
+ * - Horizontal rules: `---` / `***`
117
+ * - Blockquotes: `> text`
118
+ * - Unordered lists: `- item`, `* item`, `+ item`
119
+ * - Ordered lists: `1. item`
120
+ * - Fenced code blocks: `` ```lang ``
121
+ * - Tables (GFM pipe tables)
122
+ * - Empty lines (spacers)
123
+ * - Plain paragraphs
124
+ *
125
+ * @param {string} text - Markdown source string.
126
+ * @param {Object} [options={}] - Rendering options.
127
+ * @param {number} [options.codeMaxHeight=300] - Maximum height in pixels for code blocks.
128
+ * @param {number} [options.codeFontSize=12] - Font size in pixels for code blocks.
129
+ * @returns {Array} Flat array of widget instances ready to be rendered.
130
+ */
131
+ export const markdownToWidgets = (text, options = {}) => {
132
+ if (!text) return [];
133
+
134
+ const widgets = [];
135
+ const lines = text.split("\n");
136
+ let i = 0;
137
+ let inCodeBlock = false;
138
+ let codeContent = [];
139
+ let codeLang = "";
140
+
141
+ while (i < lines.length) {
142
+ const line = lines[i];
143
+
144
+ // CODE BLOCKS
145
+ if (!inCodeBlock && line.trim().startsWith("```")) {
146
+ inCodeBlock = true;
147
+ codeLang = line.trim().replace(/```/g, "").trim();
148
+ i++;
149
+ continue;
150
+ }
151
+ if (inCodeBlock && line.trim().startsWith("```")) {
152
+ inCodeBlock = false;
153
+ const codeWidget = CodeViewer({
154
+ code: codeContent.join("\n"),
155
+ language: codeLang || "javascript",
156
+ maxHeight: options.codeMaxHeight || 300,
157
+ fontSize: options.codeFontSize || 12,
158
+ showHeader: false,
159
+ showLineNumbers: true,
160
+ });
161
+ widgets.push(codeWidget);
162
+ codeContent = [];
163
+ codeLang = "";
164
+ i++;
165
+ continue;
166
+ }
167
+ if (inCodeBlock) {
168
+ codeContent.push(line);
169
+ i++;
170
+ continue;
171
+ }
172
+
173
+ // TABLES
174
+ if (
175
+ isTableLine(line) &&
176
+ i + 1 < lines.length &&
177
+ isTableSeparatorLine(lines[i + 1])
178
+ ) {
179
+ const result = parseTable(lines, i);
180
+ if (result) {
181
+ widgets.push(result.widget);
182
+ i = result.nextIndex;
183
+ continue;
184
+ }
185
+ }
186
+
187
+ // HEADERS
188
+ if (line.match(/^### /)) {
189
+ widgets.push(
190
+ Text({
191
+ text: line.substring(4),
192
+ size: 18,
193
+ weight: "bold",
194
+ marginTop: 16,
195
+ marginBottom: 8,
196
+ }),
197
+ );
198
+ i++;
199
+ continue;
200
+ }
201
+ if (line.match(/^## /)) {
202
+ widgets.push(
203
+ Text({
204
+ text: line.substring(3),
205
+ size: 22,
206
+ weight: "bold",
207
+ marginTop: 20,
208
+ marginBottom: 10,
209
+ }),
210
+ );
211
+ i++;
212
+ continue;
213
+ }
214
+ if (line.match(/^# /)) {
215
+ widgets.push(
216
+ Text({
217
+ text: line.substring(2),
218
+ size: 28,
219
+ weight: "bold",
220
+ marginTop: 24,
221
+ marginBottom: 12,
222
+ }),
223
+ );
224
+ i++;
225
+ continue;
226
+ }
227
+
228
+ // HORIZONTAL RULE
229
+ if (line.match(/^---$/) || line.match(/^\*\*\*$/)) {
230
+ widgets.push(
231
+ Container({ height: 1, bgColor: colors.border, marginVertical: 16 }),
232
+ );
233
+ i++;
234
+ continue;
235
+ }
236
+
237
+ // BLOCKQUOTE
238
+ if (line.match(/^> /)) {
239
+ const content = line.substring(2);
240
+ const innerWidgets = parseInlineToWidgets(content);
241
+ widgets.push(
242
+ Container({
243
+ padding: 12,
244
+ bgColor: `${colors.primary}10`,
245
+ borderRadius: 8,
246
+ borderLeft: `4px solid ${colors.primary}`,
247
+ marginVertical: 8,
248
+ child: Row({ alignItems: "center", gap: 8, children: innerWidgets }),
249
+ }),
250
+ );
251
+ i++;
252
+ continue;
253
+ }
254
+
255
+ // UNORDERED LIST
256
+ if (line.match(/^\s*[-*+]\s/)) {
257
+ const content = line.replace(/^\s*[-*+]\s/, "");
258
+ const innerWidgets = parseInlineToWidgets(content);
259
+ widgets.push(
260
+ Row({
261
+ alignItems: "center",
262
+ gap: 8,
263
+ marginVertical: 4,
264
+ children: [
265
+ Icon({ name: "circle", size: 8, color: colors.primary }),
266
+ ...innerWidgets,
267
+ ],
268
+ }),
269
+ );
270
+ i++;
271
+ continue;
272
+ }
273
+
274
+ // ORDERED LIST
275
+ if (line.match(/^\s*\d+\.\s/)) {
276
+ const number = line.match(/^\s*(\d+)\.\s/)[1];
277
+ const content = line.replace(/^\s*\d+\.\s/, "");
278
+ const innerWidgets = parseInlineToWidgets(content);
279
+ widgets.push(
280
+ Row({
281
+ alignItems: "center",
282
+ gap: 8,
283
+ marginVertical: 4,
284
+ children: [
285
+ Text({
286
+ text: `${number}.`,
287
+ size: 14,
288
+ weight: "bold",
289
+ color: colors.primary,
290
+ }),
291
+ ...innerWidgets,
292
+ ],
293
+ }),
294
+ );
295
+ i++;
296
+ continue;
297
+ }
298
+
299
+ // EMPTY LINE
300
+ if (line.trim() === "") {
301
+ widgets.push(Container({ height: 8 }));
302
+ i++;
303
+ continue;
304
+ }
305
+
306
+ // PARAGRAPH
307
+ const paragraphWidgets = parseInlineToWidgets(line);
308
+ if (paragraphWidgets.length === 1) {
309
+ widgets.push(paragraphWidgets[0]);
310
+ } else {
311
+ widgets.push(
312
+ Row({
313
+ alignItems: "center",
314
+ gap: 4,
315
+ children: paragraphWidgets,
316
+ flexWrap: "wrap",
317
+ marginVertical: 4,
318
+ }),
319
+ );
320
+ }
321
+ i++;
322
+ }
323
+
324
+ return widgets;
325
+ };
326
+
327
+ // ========== MAIN EXPORTS ==========
328
+ /** Alias for {@link markdownToWidgets}. */
329
+ export const parseMarkdownToWidgets = markdownToWidgets;
330
+
331
+ /**
332
+ * Identity function — returns the raw markdown string unchanged.
333
+ * Provided for API symmetry where a parse step is expected but plain text is acceptable.
334
+ * @param {string} text - Markdown source.
335
+ * @returns {string} The same string, unmodified.
336
+ */
337
+ export const parseMarkdown = (text) => text;
338
+
339
+ /**
340
+ * Identity function — returns the raw inline markdown string unchanged.
341
+ * @param {string} text - Inline markdown source.
342
+ * @returns {string} The same string, unmodified.
343
+ */
344
+ export const parseInlineMarkdown = (text) => text;
345
+
346
+ /**
347
+ * Escapes HTML special characters to prevent injection when inserting into innerHTML.
348
+ * Converts `& < > " '` to their corresponding HTML entities.
349
+ * @param {string} text - Raw text to escape.
350
+ * @returns {string} HTML-safe string.
351
+ */
352
+ export const escapeHtml = (text) =>
353
+ String(text)
354
+ .replace(/&/g, "&amp;")
355
+ .replace(/</g, "&lt;")
356
+ .replace(/>/g, "&gt;")
357
+ .replace(/"/g, "&quot;")
358
+ .replace(/'/g, "&#39;");
359
+
360
+ // Default export
361
+ export default {
362
+ markdownToWidgets,
363
+ parseMarkdownToWidgets,
364
+ parseInlineToWidgets,
365
+ parseMarkdown,
366
+ parseInlineMarkdown,
367
+ escapeHtml,
368
+ };
@@ -0,0 +1,79 @@
1
+ // src/utils/mediaTime.js
2
+ // Time formatting utilities for media players (audio/video)
3
+
4
+ /**
5
+ * Formats a duration in seconds as a `MM:SS` string.
6
+ * Returns `"00:00"` for falsy or `NaN` values.
7
+ * @param {number} seconds - Duration in seconds.
8
+ * @returns {string} Zero-padded `"MM:SS"` string (e.g. `"03:07"`).
9
+ * @example
10
+ * formatMediaTime(187) // "03:07"
11
+ * formatMediaTime(0) // "00:00"
12
+ */
13
+ export const formatMediaTime = (seconds) => {
14
+ if (!seconds || isNaN(seconds)) return "00:00";
15
+ const mins = Math.floor(seconds / 60);
16
+ const secs = Math.floor(seconds % 60);
17
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
18
+ };
19
+
20
+ /**
21
+ * Formats a duration in seconds as a `HH:MM:SS` or `MM:SS` string,
22
+ * including hours only when the value is 3600 seconds or more.
23
+ * Returns `"00:00"` for falsy or `NaN` values.
24
+ * @param {number} seconds - Duration in seconds.
25
+ * @returns {string} Zero-padded `"HH:MM:SS"` or `"MM:SS"` string.
26
+ * @example
27
+ * formatMediaTimeLong(3723) // "01:02:03"
28
+ * formatMediaTimeLong(187) // "03:07"
29
+ */
30
+ export const formatMediaTimeLong = (seconds) => {
31
+ if (!seconds || isNaN(seconds)) return "00:00";
32
+ const hrs = Math.floor(seconds / 3600);
33
+ const mins = Math.floor((seconds % 3600) / 60);
34
+ const secs = Math.floor(seconds % 60);
35
+ if (hrs > 0) {
36
+ return `${hrs.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
37
+ }
38
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
39
+ };
40
+
41
+ /**
42
+ * Calculates the playback progress as a percentage between 0 and 100.
43
+ * Returns `0` when `total` is falsy or zero to avoid division by zero.
44
+ * @param {number} current - Current playback position in seconds.
45
+ * @param {number} total - Total media duration in seconds.
46
+ * @returns {number} Progress percentage clamped to `[0, 100]`.
47
+ * @example
48
+ * getProgressPercent(30, 120) // 25
49
+ * getProgressPercent(0, 0) // 0
50
+ */
51
+ export const getProgressPercent = (current, total) => {
52
+ if (!total || total <= 0) return 0;
53
+ return Math.min(100, Math.max(0, (current / total) * 100));
54
+ };
55
+
56
+ /**
57
+ * Converts a progress percentage back to an absolute position in seconds.
58
+ * Returns `0` when `total` is falsy or zero.
59
+ * @param {number} percent - Progress value between 0 and 100.
60
+ * @param {number} total - Total media duration in seconds.
61
+ * @returns {number} Corresponding position in seconds.
62
+ * @example
63
+ * percentToSeconds(25, 120) // 30
64
+ */
65
+ export const percentToSeconds = (percent, total) => {
66
+ if (!total || total <= 0) return 0;
67
+ return (percent / 100) * total;
68
+ };
69
+
70
+ /**
71
+ * Formats the combined current/total progress as a `"MM:SS / MM:SS"` string.
72
+ * Delegates to {@link formatMediaTime} for each value.
73
+ * @param {number} current - Current playback position in seconds.
74
+ * @param {number} total - Total media duration in seconds.
75
+ * @returns {string} Progress string (e.g. `"01:23 / 04:56"`).
76
+ */
77
+ export const formatMediaProgress = (current, total) => {
78
+ return `${formatMediaTime(current)} / ${formatMediaTime(total)}`;
79
+ };