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,315 @@
1
+ /**
2
+ * @file AdMob.js
3
+ * @description Google AdMob for native Capacitor targets (Android/iOS). This is
4
+ * the mobile counterpart of `AdSense`: instead of a web SDK it drives the native
5
+ * `@capacitor-community/admob` plugin. The widget renders a DOM placeholder that
6
+ * reserves space and, when `autoInitialize: true`, shows the banner / prepares
7
+ * full-screen ads over the native layer. On the web (no Capacitor) it degrades
8
+ * to the placeholder and logs a single warning.
9
+ *
10
+ * Native ads are an explicit opt-in (`autoInitialize: true`) because the Google
11
+ * SDK hard-aborts the app when its AdMob App ID is not configured. Mounting an
12
+ * AdMob widget must never crash a misconfigured app, so the placeholder renders
13
+ * and the plugin is left untouched until the developer opts in and sets the App
14
+ * ID in the native config (`GADApplicationIdentifier` in Info.plist, `APPLICATION_ID`
15
+ * meta-data in AndroidManifest.xml).
16
+ */
17
+
18
+ import { WidgetFactory } from "../widget-factory/index.js";
19
+ import { composeUpdate } from "../utils/composeUpdate.js";
20
+
21
+ // Google's public sample unit ids — safe to use while developing. Replace them
22
+ // with your own real ids before shipping (keep isTesting once you go live only
23
+ // if you actually want test ads).
24
+ const TEST_BANNER_ID = "ca-app-pub-3940256099942544/6300978111";
25
+ const TEST_INTERSTITIAL_ID = "ca-app-pub-3940256099942544/1033173712";
26
+ const TEST_REWARDED_ID = "ca-app-pub-3940256099942544/5224354917";
27
+
28
+ const DEFAULT_COLOR = "#9ca3af";
29
+ const DEFAULT_BORDER_COLOR = "#d1d5db";
30
+
31
+ const WIDGET_PROP_KEYS = [
32
+ "adId",
33
+ "isTesting",
34
+ "position",
35
+ "size",
36
+ "margin",
37
+ "label",
38
+ "placeholder",
39
+ "interstitialId",
40
+ "rewardedId",
41
+ "autoInitialize",
42
+ ];
43
+
44
+ // Initialisation is app-wide, so it only needs to happen once per plugin.
45
+ let initPromise = null;
46
+ let warnedMissingPlugin = false;
47
+ let warnedNotInitialized = false;
48
+
49
+ /**
50
+ * Resolves the native AdMob plugin from an explicit prop or the Capacitor
51
+ * bridge. `@capacitor-community/admob` registers itself as
52
+ * `Capacitor.Plugins.AdMob`, which keeps FletBox dependency-free.
53
+ *
54
+ * @param {Object|null} [explicit] - Plugin instance passed by the caller.
55
+ * @returns {Object|null}
56
+ */
57
+ const resolvePlugin = (explicit) => {
58
+ if (explicit) return explicit;
59
+ const capacitor = globalThis.Capacitor;
60
+ return (capacitor && capacitor.Plugins && capacitor.Plugins.AdMob) || null;
61
+ };
62
+
63
+ const ensureInitialized = (plugin) => {
64
+ if (!plugin || typeof plugin.initialize !== "function") return Promise.resolve();
65
+ if (!initPromise) {
66
+ initPromise = Promise.resolve(plugin.initialize({})).catch((error) => {
67
+ console.warn("[FletBox AdMob] initialize failed:", error);
68
+ });
69
+ }
70
+ return initPromise;
71
+ };
72
+
73
+ /**
74
+ * Creates a Google AdMob widget for native Capacitor builds.
75
+ *
76
+ * On a real device it shows a banner (top/bottom) and exposes methods for
77
+ * interstitial and rewarded ads. In the browser it returns the same element but
78
+ * does nothing beyond a one-time warning, so the app keeps working during web
79
+ * development.
80
+ *
81
+ * @param {Object} props
82
+ * @param {string} [props.adId=""] - Banner ad unit id. Defaults to Google's test unit.
83
+ * @param {boolean} [props.isTesting=false] - Force Google test ads (recommended before release).
84
+ * @param {"top"|"bottom"} [props.position="bottom"] - Native banner position.
85
+ * @param {string} [props.size="ADAPTIVE_BANNER"] - Banner size keyword ("ADAPTIVE_BANNER", "BANNER", "LARGE_BANNER", "MEDIUM_RECTANGLE", "FULL_BANNER", "LEADERBOARD", "SMART_BANNER").
86
+ * @param {number} [props.margin=0] - Distance in pixels from the chosen edge.
87
+ * @param {string} [props.interstitialId=""] - Ad unit id for `showInterstitial()`.
88
+ * @param {string} [props.rewardedId=""] - Ad unit id for `showRewarded()`.
89
+ * @param {boolean} [props.autoShow=true] - Show the banner automatically on mount.
90
+ * @param {boolean} [props.autoInitialize=false] - Opt in to native ads. Requires a
91
+ * configured AdMob App ID (`GADApplicationIdentifier` in Info.plist, `APPLICATION_ID`
92
+ * in AndroidManifest.xml); until set, mounting the widget is placeholder-only so a
93
+ * misconfigured app never crashes (the Google SDK aborts otherwise).
94
+ * @param {number|string} [props.width="100%"] - Placeholder width.
95
+ * @param {number|string} [props.height=50] - Placeholder height (reserve banner space).
96
+ * @param {boolean} [props.placeholder=true] - Show the dashed placeholder.
97
+ * @param {string} [props.label="AdMob banner"] - Placeholder text.
98
+ * @param {string} [props.bgColor] - Placeholder background color.
99
+ * @param {string} [props.color=#9ca3af] - Placeholder label color.
100
+ * @param {string} [props.borderColor=#d1d5db] - Placeholder dashed border color.
101
+ * @param {Object} [props.plugin] - Explicit plugin instance (defaults to `Capacitor.Plugins.AdMob`).
102
+ * @param {(widget: HTMLElement) => void} [props.onLoaded] - Called after the banner is shown.
103
+ * @param {(error: Error, widget: HTMLElement) => void} [props.onFailed] - Called when a native call fails.
104
+ * @param {() => void} [props.onDismissed] - Called after a full-screen ad is dismissed.
105
+ * @returns {HTMLElement} The placeholder container, stamped `widgetName: "AdMob"`.
106
+ */
107
+ export const AdMob = (props) => {
108
+ const {
109
+ adId = "",
110
+ isTesting = false,
111
+ position = "bottom",
112
+ size = "ADAPTIVE_BANNER",
113
+ margin = 0,
114
+ interstitialId = "",
115
+ rewardedId = "",
116
+ autoShow = true,
117
+ autoInitialize = false,
118
+ width = "100%",
119
+ height = 50,
120
+ placeholder = true,
121
+ label = "AdMob banner",
122
+ bgColor,
123
+ color = DEFAULT_COLOR,
124
+ borderColor = DEFAULT_BORDER_COLOR,
125
+ plugin: pluginProp = null,
126
+ onLoaded,
127
+ onFailed,
128
+ onDismissed,
129
+ ...rest
130
+ } = props;
131
+
132
+ let currentAdId = adId;
133
+ let currentIsTesting = isTesting;
134
+ let currentPosition = position;
135
+ let currentSize = size;
136
+ let currentMargin = margin;
137
+ let currentLabel = label;
138
+ let currentPlaceholder = placeholder;
139
+ let currentAutoInitialize = autoInitialize;
140
+ let bannerVisible = false;
141
+ let disposed = false;
142
+ let labelElement = null;
143
+
144
+ const bannerId = () => currentAdId || TEST_BANNER_ID;
145
+
146
+ const withPlugin = (run) => {
147
+ if (!currentAutoInitialize) {
148
+ if (!warnedNotInitialized) {
149
+ warnedNotInitialized = true;
150
+ console.warn(
151
+ "[FletBox AdMob] native ads are disabled (missing `autoInitialize: true`). Mounting an AdMob widget is placeholder-only by default because the Google SDK aborts the app when its AdMob App ID is not configured. To enable ads, set `autoInitialize: true` AND add the App ID to the native config: `GADApplicationIdentifier` in ios/App/App/Info.plist or the `APPLICATION_ID` meta-data in AndroidManifest.xml, then run `npm run ios:sync` / `android:sync`.",
152
+ );
153
+ }
154
+ return Promise.resolve(null);
155
+ }
156
+ const plugin = resolvePlugin(pluginProp);
157
+ if (!plugin) {
158
+ if (!warnedMissingPlugin) {
159
+ warnedMissingPlugin = true;
160
+ console.warn(
161
+ "[FletBox AdMob] native plugin not found. Install @capacitor-community/admob and run `npx cap sync` in your Capacitor project.",
162
+ );
163
+ }
164
+ return Promise.resolve(null);
165
+ }
166
+ return ensureInitialized(plugin).then(() => run(plugin));
167
+ };
168
+
169
+ const container = WidgetFactory({
170
+ widgetName: "AdMob",
171
+ boxSizing: "border-box",
172
+ display: "flex",
173
+ alignItems: "center",
174
+ justifyContent: "center",
175
+ overflow: "hidden",
176
+ width,
177
+ height,
178
+ backgroundColor: bgColor,
179
+ borderStyle: "dashed",
180
+ borderWidth: currentPlaceholder ? 1 : 0,
181
+ borderColor,
182
+ ...rest,
183
+ });
184
+
185
+ const refreshPlaceholder = () => {
186
+ container.style.borderWidth = currentPlaceholder ? "1px" : "0px";
187
+ if (!labelElement) return;
188
+ labelElement.style.display = currentPlaceholder ? "" : "none";
189
+ if (currentPlaceholder) labelElement.textContent = currentLabel.toUpperCase();
190
+ };
191
+
192
+ if (currentPlaceholder) {
193
+ labelElement = WidgetFactory({
194
+ widgetName: "Text",
195
+ tag: "span",
196
+ text: currentLabel.toUpperCase(),
197
+ color,
198
+ fontSize: 11,
199
+ fontWeight: 600,
200
+ letterSpacing: 2,
201
+ userSelect: "none",
202
+ pointerEvents: "none",
203
+ });
204
+ container.appendChild(labelElement);
205
+ }
206
+
207
+ const showBanner = () => {
208
+ if (disposed || !autoShow || bannerVisible) return Promise.resolve(null);
209
+ return withPlugin((plugin) =>
210
+ Promise.resolve(
211
+ plugin.showBanner({
212
+ adId: bannerId(),
213
+ adSize: currentSize,
214
+ position: currentPosition === "top" ? "TOP_CENTER" : "BOTTOM_CENTER",
215
+ margin: currentMargin,
216
+ isTesting: currentIsTesting,
217
+ }),
218
+ ).then(() => {
219
+ bannerVisible = true;
220
+ if (typeof onLoaded === "function") onLoaded(container);
221
+ }),
222
+ ).catch((error) => {
223
+ if (disposed) return null;
224
+ console.warn("[FletBox AdMob] showBanner failed:", error);
225
+ if (typeof onFailed === "function") onFailed(error, container);
226
+ return null;
227
+ });
228
+ };
229
+
230
+ const hideBanner = () => {
231
+ bannerVisible = false;
232
+ return withPlugin((plugin) => {
233
+ const remove = plugin.removeBanner || plugin.hideBanner;
234
+ return typeof remove === "function" ? Promise.resolve(remove.call(plugin)) : null;
235
+ }).catch(() => null);
236
+ };
237
+
238
+ const showFullScreen = (prepareProps, prepareName, showName) =>
239
+ withPlugin((plugin) => {
240
+ const prepare = plugin[prepareName];
241
+ if (typeof prepare !== "function") {
242
+ console.warn(`[FletBox AdMob] plugin has no ${prepareName}()`);
243
+ return null;
244
+ }
245
+ return Promise.resolve(prepare.call(plugin, prepareProps))
246
+ .then(() => plugin[showName]())
247
+ .then((result) => {
248
+ if (typeof onDismissed === "function") onDismissed();
249
+ return result;
250
+ });
251
+ }).catch((error) => {
252
+ console.warn(`[FletBox AdMob] ${showName} failed:`, error);
253
+ if (typeof onFailed === "function") onFailed(error, container);
254
+ return null;
255
+ });
256
+
257
+ /** Shows or re-shows the banner. */
258
+ container.show = showBanner;
259
+
260
+ /** Hides the banner (keeps it prepared). */
261
+ container.hide = hideBanner;
262
+
263
+ /** Prepares and shows an interstitial ad. */
264
+ container.showInterstitial = () =>
265
+ showFullScreen(
266
+ {
267
+ adId: interstitialId || TEST_INTERSTITIAL_ID,
268
+ isTesting: currentIsTesting,
269
+ },
270
+ "prepareInterstitial",
271
+ "showInterstitial",
272
+ );
273
+
274
+ /** Prepares and shows a rewarded video ad. */
275
+ container.showRewarded = () =>
276
+ showFullScreen(
277
+ { adId: rewardedId || TEST_REWARDED_ID, isTesting: currentIsTesting },
278
+ "prepareRewardVideoAd",
279
+ "showRewardVideoAd",
280
+ );
281
+
282
+ /** @returns {Object|null} The resolved native plugin, if any. */
283
+ container.getPlugin = () => resolvePlugin(pluginProp);
284
+
285
+ composeUpdate(container, WIDGET_PROP_KEYS, (widgetProps) => {
286
+ if ("adId" in widgetProps) currentAdId = widgetProps.adId;
287
+ if ("isTesting" in widgetProps) currentIsTesting = widgetProps.isTesting;
288
+ if ("position" in widgetProps) currentPosition = widgetProps.position;
289
+ if ("size" in widgetProps) currentSize = widgetProps.size;
290
+ if ("margin" in widgetProps) currentMargin = widgetProps.margin;
291
+ if ("label" in widgetProps) currentLabel = widgetProps.label;
292
+ if ("placeholder" in widgetProps) currentPlaceholder = widgetProps.placeholder;
293
+ if ("autoInitialize" in widgetProps) currentAutoInitialize = widgetProps.autoInitialize;
294
+ refreshPlaceholder();
295
+ });
296
+
297
+ const dispose = () => {
298
+ if (disposed) return;
299
+ disposed = true;
300
+ if (!currentAutoInitialize) return;
301
+ const plugin = resolvePlugin(pluginProp);
302
+ if (plugin && typeof plugin.removeBanner === "function") {
303
+ Promise.resolve(plugin.removeBanner()).catch(() => {});
304
+ }
305
+ };
306
+ container.onUnmount(dispose);
307
+
308
+ container.onMount(showBanner);
309
+
310
+ refreshPlaceholder();
311
+
312
+ return container;
313
+ };
314
+
315
+ export default AdMob;
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @file AdSense.js
3
+ * @description Google AdSense display unit for the web. Loads the official
4
+ * `adsbygoogle.js` SDK exactly once per publisher client and asks it to fill a
5
+ * `<ins class="adsbygoogle">` slot. Use this widget in browser / PWA / webview
6
+ * targets; for native Android/iOS banners use the `AdMob` widget instead.
7
+ */
8
+
9
+ import { WidgetFactory } from "../widget-factory/index.js";
10
+ import { composeUpdate } from "../utils/composeUpdate.js";
11
+ import { injectScript } from "../utils/scriptInjector.js";
12
+ import { toREM } from "../utils/units.js";
13
+
14
+ const ADSENSE_SRC =
15
+ "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js";
16
+
17
+ // Widget-owned props: routed to the widget's own updater, never to the factory.
18
+ const WIDGET_PROP_KEYS = ["client", "slot", "format", "responsive", "label", "placeholder", "test", "height"];
19
+
20
+ // Formats that size themselves from content (Google grows the slot after fill).
21
+ const SELF_SIZING_FORMATS = new Set(["autorelaxed", "fluid"]);
22
+
23
+ const DEFAULT_COLOR = "#9ca3af";
24
+ const DEFAULT_BORDER_COLOR = "#d1d5db";
25
+
26
+ /** Builds the per-publisher SDK URL so each client loads the script once. */
27
+ const adsenseScriptUrl = (client) =>
28
+ `${ADSENSE_SRC}?client=${encodeURIComponent(client)}`;
29
+
30
+ /** Hands one unit to the shared `adsbygoogle` command queue. */
31
+ const pushAd = () => {
32
+ if (typeof window === "undefined") return;
33
+ try {
34
+ (window.adsbygoogle = window.adsbygoogle || []).push({});
35
+ } catch (error) {
36
+ console.warn("[FletBox AdSense] could not request an ad:", error);
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Creates a Google AdSense display unit.
42
+ *
43
+ * The widget is inert until both `client` (publisher id) and `slot` (ad unit id)
44
+ * are set. Once configured and mounted it injects the AdSense SDK (shared with
45
+ * every other `AdSense` on the page) and pushes the unit to be filled. A dashed
46
+ * placeholder is shown while the widget is unconfigured unless
47
+ * `placeholder={false}`.
48
+ *
49
+ * @param {Object} props
50
+ * @param {string} [props.client=""] - Publisher id, e.g. `"ca-pub-1234567890123456"`.
51
+ * @param {string} [props.slot=""] - Ad unit id (numeric string).
52
+ * @param {string} [props.format="auto"] - `data-ad-format` keyword ("auto", "horizontal", "rectangle", "vertical", "fluid", "autorelaxed").
53
+ * @param {boolean} [props.responsive=true] - Emit `data-full-width-responsive="true"`.
54
+ * @param {boolean} [props.test=false] - Emit `data-adtest="on"` so Google serves
55
+ * sample test ads from its test pool (ignores domain approval). Never ship
56
+ * with `test` enabled.
57
+ * @param {number|string} [props.width="100%"] - Slot width.
58
+ * @param {number|string} [props.height=280] - Reserved slot height.
59
+ * @param {boolean} [props.placeholder=true] - Show the dashed placeholder while unconfigured.
60
+ * @param {string} [props.label="Advertisement"] - Placeholder text.
61
+ * @param {string} [props.bgColor] - Slot background color.
62
+ * @param {string} [props.color=#9ca3af] - Placeholder label color.
63
+ * @param {string} [props.borderColor=#d1d5db] - Placeholder dashed border color.
64
+ * @param {(widget: HTMLElement) => void} [props.onLoad] - Called once the SDK is ready and the unit pushed.
65
+ * @param {(error: Error, widget: HTMLElement) => void} [props.onError] - Called when the SDK fails to load.
66
+ * @param {Widget[]} [props.children] - Extra host markup placed inside the slot.
67
+ * @returns {HTMLElement} The slot container, stamped `widgetName: "AdSense"`.
68
+ */
69
+ export const AdSense = (props) => {
70
+ let {
71
+ client = "",
72
+ slot = "",
73
+ format = "auto",
74
+ responsive = true,
75
+ test = false,
76
+ width = "100%",
77
+ height = 280,
78
+ placeholder = true,
79
+ label = "Advertisement",
80
+ bgColor,
81
+ color = DEFAULT_COLOR,
82
+ borderColor = DEFAULT_BORDER_COLOR,
83
+ onLoad,
84
+ onError,
85
+ ...rest
86
+ } = props;
87
+
88
+ let currentClient = client;
89
+ let currentSlot = slot;
90
+ let currentFormat = format;
91
+ let currentResponsive = responsive;
92
+ let currentTest = test;
93
+ let insElement = null;
94
+ let labelElement = null;
95
+ let disposed = false;
96
+ let mounted = false;
97
+ let configured = !!client && !!slot;
98
+
99
+ const isConfigured = () => !!currentClient && !!currentSlot;
100
+
101
+ const isSelfSizing = () => SELF_SIZING_FORMATS.has(currentFormat);
102
+
103
+ const container = WidgetFactory({
104
+ widgetName: "AdSense",
105
+ boxSizing: "border-box",
106
+ position: "relative",
107
+ display: "flex",
108
+ alignItems: "center",
109
+ justifyContent: "center",
110
+ overflow: "hidden",
111
+ width,
112
+ height,
113
+ backgroundColor: bgColor,
114
+ borderStyle: "dashed",
115
+ borderWidth: isConfigured() ? 0 : 1,
116
+ borderColor,
117
+ ...rest,
118
+ });
119
+
120
+ // Display/auto units reserve a fixed slot height; self-sizing formats
121
+ // (autorelaxed, fluid) must grow with the content Google injects.
122
+ const syncSizing = () => {
123
+ if (isSelfSizing()) {
124
+ container.style.height = "auto";
125
+ container.style.overflow = "visible";
126
+ } else {
127
+ container.style.height = toREM(height);
128
+ container.style.overflow = "hidden";
129
+ }
130
+ if (insElement) insElement.style.height = isSelfSizing() ? "auto" : "100%";
131
+ };
132
+
133
+ // If the initial format is self-sizing, drop the reserved height immediately.
134
+ syncSizing();
135
+
136
+ const applyAttrs = () => {
137
+ if (!insElement) return;
138
+ insElement.setAttribute("data-ad-client", currentClient);
139
+ insElement.setAttribute("data-ad-slot", currentSlot);
140
+ if (currentFormat) insElement.setAttribute("data-ad-format", currentFormat);
141
+ else insElement.removeAttribute("data-ad-format");
142
+ if (currentResponsive) {
143
+ insElement.setAttribute("data-full-width-responsive", "true");
144
+ } else {
145
+ insElement.removeAttribute("data-full-width-responsive");
146
+ }
147
+ if (currentTest) {
148
+ insElement.setAttribute("data-adtest", "on");
149
+ } else {
150
+ insElement.removeAttribute("data-adtest");
151
+ }
152
+ };
153
+
154
+ const ensureIns = () => {
155
+ if (insElement) return insElement;
156
+ insElement = document.createElement("ins");
157
+ insElement.className = "adsbygoogle";
158
+ insElement.style.display = "block";
159
+ insElement.style.width = "100%";
160
+ container.appendChild(insElement);
161
+ applyAttrs();
162
+ syncSizing();
163
+ return insElement;
164
+ };
165
+
166
+ const syncPlaceholder = () => {
167
+ const show = placeholder && !isConfigured();
168
+ container.style.borderWidth = show ? "1px" : "0px";
169
+ if (!show) {
170
+ if (labelElement) labelElement.style.display = "none";
171
+ return;
172
+ }
173
+ if (!labelElement) {
174
+ labelElement = WidgetFactory({
175
+ widgetName: "Text",
176
+ tag: "span",
177
+ text: label.toUpperCase(),
178
+ color,
179
+ fontSize: 11,
180
+ fontWeight: 600,
181
+ letterSpacing: 2,
182
+ userSelect: "none",
183
+ pointerEvents: "none",
184
+ position: "absolute",
185
+ });
186
+ container.appendChild(labelElement);
187
+ }
188
+ labelElement.style.display = "";
189
+ labelElement.textContent = label.toUpperCase();
190
+ };
191
+
192
+ const load = () => {
193
+ if (disposed) return;
194
+ if (!isConfigured()) {
195
+ syncPlaceholder();
196
+ return;
197
+ }
198
+ syncPlaceholder();
199
+ ensureIns();
200
+ injectScript(adsenseScriptUrl(currentClient), {
201
+ async: true,
202
+ crossOrigin: "anonymous",
203
+ })
204
+ .then(() => {
205
+ if (disposed) return;
206
+ pushAd();
207
+ if (typeof onLoad === "function") onLoad(container);
208
+ })
209
+ .catch((error) => {
210
+ if (disposed) return;
211
+ console.warn("[FletBox AdSense] SDK failed to load:", error);
212
+ if (typeof onError === "function") onError(error, container);
213
+ });
214
+ };
215
+
216
+ /** Requests another ad for the same unit (e.g. after a route change). */
217
+ container.refresh = () => {
218
+ if (isConfigured()) {
219
+ ensureIns();
220
+ pushAd();
221
+ }
222
+ return container;
223
+ };
224
+
225
+ /** Starts (or restarts) the SDK load + fill cycle. */
226
+ container.load = load;
227
+
228
+ /** @returns {HTMLElement} The `<ins>` unit when live, otherwise the container. */
229
+ container.getElement = () => insElement || container;
230
+
231
+ composeUpdate(container, WIDGET_PROP_KEYS, (widgetProps) => {
232
+ if ("client" in widgetProps) currentClient = widgetProps.client;
233
+ if ("slot" in widgetProps) currentSlot = widgetProps.slot;
234
+ if ("format" in widgetProps) currentFormat = widgetProps.format;
235
+ if ("responsive" in widgetProps) currentResponsive = widgetProps.responsive;
236
+ if ("test" in widgetProps) currentTest = widgetProps.test;
237
+ if ("height" in widgetProps) height = widgetProps.height;
238
+ if ("label" in widgetProps) label = widgetProps.label;
239
+ if ("placeholder" in widgetProps) placeholder = widgetProps.placeholder;
240
+ applyAttrs();
241
+ syncSizing();
242
+ syncPlaceholder();
243
+
244
+ // Going from unconfigured to configured after mount should actually start
245
+ // serving; before mount the `onMount` hook handles it.
246
+ const nowConfigured = isConfigured();
247
+ if (nowConfigured && !configured && mounted) load();
248
+ configured = nowConfigured;
249
+ });
250
+
251
+ const dispose = () => {
252
+ if (disposed) return;
253
+ disposed = true;
254
+ mounted = false;
255
+ };
256
+ container.onUnmount(dispose);
257
+
258
+ container.onMount(() => {
259
+ mounted = true;
260
+ load();
261
+ });
262
+
263
+ syncPlaceholder();
264
+
265
+ return container;
266
+ };
267
+
268
+ export default AdSense;