@omega.js/web 0.1.0

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 (1162) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +640 -0
  3. package/bin/omega +2 -0
  4. package/bin/omega-web +2 -0
  5. package/bin/omg +2 -0
  6. package/core/_includes/admin/sections/sidebar.json +100 -0
  7. package/core/_includes/admin/sections/topbar.json +11 -0
  8. package/core/_includes/backend/sections/sidebar.json +39 -0
  9. package/core/_includes/backend/sections/topbar.json +16 -0
  10. package/core/_includes/core/body.html +102 -0
  11. package/core/_includes/core/foot.html +536 -0
  12. package/core/_includes/core/head.html +423 -0
  13. package/core/_includes/core/pricing/price-per-unit.html +33 -0
  14. package/core/_includes/core/showcase/entry-body.html +94 -0
  15. package/core/_includes/core/showcase/index-body.html +43 -0
  16. package/core/_includes/core/showcase/nav.html +60 -0
  17. package/core/_includes/frontend/sections/footer.json +74 -0
  18. package/core/_includes/frontend/sections/nav.json +70 -0
  19. package/core/_includes/global/sections/account.json +31 -0
  20. package/core/_includes/modules/engagement/giscus.html +26 -0
  21. package/core/_layouts/blueprint/404.html +10 -0
  22. package/core/_layouts/blueprint/about.html +10 -0
  23. package/core/_layouts/blueprint/account/index.html +11 -0
  24. package/core/_layouts/blueprint/admin/calendar/index.html +590 -0
  25. package/core/_layouts/blueprint/admin/dashboard/index.html +301 -0
  26. package/core/_layouts/blueprint/admin/firebase/index.html +214 -0
  27. package/core/_layouts/blueprint/admin/posts/editor.html +157 -0
  28. package/core/_layouts/blueprint/admin/posts/index.html +126 -0
  29. package/core/_layouts/blueprint/admin/stackblitz/index.html +30 -0
  30. package/core/_layouts/blueprint/admin/studio/index.html +85 -0
  31. package/core/_layouts/blueprint/admin/users/index.html +272 -0
  32. package/core/_layouts/blueprint/admin/users/new.html +82 -0
  33. package/core/_layouts/blueprint/admin/verts/index.html +204 -0
  34. package/core/_layouts/blueprint/alternatives/alternative.html +15 -0
  35. package/core/_layouts/blueprint/alternatives/index.html +10 -0
  36. package/core/_layouts/blueprint/app.html +10 -0
  37. package/core/_layouts/blueprint/auth/reset.html +19 -0
  38. package/core/_layouts/blueprint/auth/signin.html +19 -0
  39. package/core/_layouts/blueprint/auth/signup.html +19 -0
  40. package/core/_layouts/blueprint/auth/token.html +19 -0
  41. package/core/_layouts/blueprint/blog/categories/category.html +8 -0
  42. package/core/_layouts/blueprint/blog/categories/index.html +11 -0
  43. package/core/_layouts/blueprint/blog/index.html +10 -0
  44. package/core/_layouts/blueprint/blog/post.html +5 -0
  45. package/core/_layouts/blueprint/blog/tags/index.html +11 -0
  46. package/core/_layouts/blueprint/blog/tags/tag.html +8 -0
  47. package/core/_layouts/blueprint/careers.md +13 -0
  48. package/core/_layouts/blueprint/collection/category.html +9 -0
  49. package/core/_layouts/blueprint/collection/document.html +9 -0
  50. package/core/_layouts/blueprint/collection/index.html +9 -0
  51. package/core/_layouts/blueprint/connections/callback.html +11 -0
  52. package/core/_layouts/blueprint/contact.html +14 -0
  53. package/core/_layouts/blueprint/dashboard/index.html +10 -0
  54. package/core/_layouts/blueprint/download.html +20 -0
  55. package/core/_layouts/blueprint/extension/index.html +14 -0
  56. package/core/_layouts/blueprint/extension/installed.html +11 -0
  57. package/core/_layouts/blueprint/feedback.html +10 -0
  58. package/core/_layouts/blueprint/index.html +16 -0
  59. package/core/_layouts/blueprint/legal/cookies.md +57 -0
  60. package/core/_layouts/blueprint/legal/privacy.md +109 -0
  61. package/core/_layouts/blueprint/legal/terms.md +155 -0
  62. package/core/_layouts/blueprint/payment/checkout.html +15 -0
  63. package/core/_layouts/blueprint/payment/confirmation.html +14 -0
  64. package/core/_layouts/blueprint/portal/email-preferences.html +15 -0
  65. package/core/_layouts/blueprint/pricing.html +20 -0
  66. package/core/_layouts/blueprint/status.html +10 -0
  67. package/core/_layouts/blueprint/team/index.html +10 -0
  68. package/core/_layouts/blueprint/team/member.html +5 -0
  69. package/core/_layouts/blueprint/updates/index.html +10 -0
  70. package/core/_layouts/blueprint/updates/update.html +10 -0
  71. package/core/_layouts/core/root.html +45 -0
  72. package/core/_layouts/modules/utilities/redirect.html +129 -0
  73. package/core/css/_studio.scss +237 -0
  74. package/core/css/components/_index.scss +146 -0
  75. package/core/css/core/_.scss +0 -0
  76. package/core/css/core/_alert.scss +87 -0
  77. package/core/css/core/_animations.scss +343 -0
  78. package/core/css/core/_bindings.scss +116 -0
  79. package/core/css/core/_blog-search.scss +57 -0
  80. package/core/css/core/_chatsy.scss +61 -0
  81. package/core/css/core/_consent.scss +424 -0
  82. package/core/css/core/_custom-font-awesome.scss +146 -0
  83. package/core/css/core/_exit-popup.scss +42 -0
  84. package/core/css/core/_generated-badge.scss +33 -0
  85. package/core/css/core/_initialize.scss +29 -0
  86. package/core/css/core/_lazy-loading.scss +104 -0
  87. package/core/css/core/_social-sharing.scss +41 -0
  88. package/core/css/core/_tones.scss +32 -0
  89. package/core/css/core/_utilities.scss +157 -0
  90. package/core/css/main.scss +54 -0
  91. package/core/css/motion/_index.scss +368 -0
  92. package/core/css/pages/404/index.scss +0 -0
  93. package/core/css/pages/admin/calendar/index.scss +692 -0
  94. package/core/css/pages/admin/firebase/index.scss +33 -0
  95. package/core/css/pages/admin/index.scss +9 -0
  96. package/core/css/pages/admin/users/index.scss +9 -0
  97. package/core/css/pages/admin/verts/index.scss +9 -0
  98. package/core/css/pages/alternatives/[alternative]/index.scss +0 -0
  99. package/core/css/pages/blog/[slug].scss +55 -0
  100. package/core/css/pages/dashboard/account/index.scss +242 -0
  101. package/core/css/pages/download/index.scss +480 -0
  102. package/core/css/pages/extension/installed/index.scss +317 -0
  103. package/core/css/pages/feedback/index.scss +31 -0
  104. package/core/css/pages/payment/confirmation/index.scss +14 -0
  105. package/core/css/pages/pricing/index.scss +0 -0
  106. package/core/css/pages/status/index.scss +162 -0
  107. package/core/css/pages/test/libraries/layers/index.scss +28 -0
  108. package/core/css/pages/updates/[update].scss +46 -0
  109. package/core/css/pages/updates/index.scss +13 -0
  110. package/core/css/shell/_index.scss +207 -0
  111. package/core/css/tokens/_index.scss +166 -0
  112. package/core/icons/flags/ar.svg +1 -0
  113. package/core/icons/flags/bd.svg +1 -0
  114. package/core/icons/flags/ca.svg +1 -0
  115. package/core/icons/flags/ch.svg +1 -0
  116. package/core/icons/flags/cn.svg +1 -0
  117. package/core/icons/flags/co.svg +1 -0
  118. package/core/icons/flags/cw.svg +1 -0
  119. package/core/icons/flags/cz.svg +1 -0
  120. package/core/icons/flags/de.svg +1 -0
  121. package/core/icons/flags/dk.svg +1 -0
  122. package/core/icons/flags/eg.svg +1 -0
  123. package/core/icons/flags/es.svg +1 -0
  124. package/core/icons/flags/fi.svg +1 -0
  125. package/core/icons/flags/fr.svg +1 -0
  126. package/core/icons/flags/gb.svg +1 -0
  127. package/core/icons/flags/gq.svg +1 -0
  128. package/core/icons/flags/gr.svg +1 -0
  129. package/core/icons/flags/gt.svg +1 -0
  130. package/core/icons/flags/hn.svg +1 -0
  131. package/core/icons/flags/id.svg +1 -0
  132. package/core/icons/flags/ie.svg +1 -0
  133. package/core/icons/flags/in.svg +1 -0
  134. package/core/icons/flags/ir.svg +1 -0
  135. package/core/icons/flags/it.svg +1 -0
  136. package/core/icons/flags/jp.svg +1 -0
  137. package/core/icons/flags/kp.svg +1 -0
  138. package/core/icons/flags/kr.svg +1 -0
  139. package/core/icons/flags/ly.svg +1 -0
  140. package/core/icons/flags/ma.svg +1 -0
  141. package/core/icons/flags/mu.svg +1 -0
  142. package/core/icons/flags/mx.svg +1 -0
  143. package/core/icons/flags/ng.svg +1 -0
  144. package/core/icons/flags/pa.svg +1 -0
  145. package/core/icons/flags/ph.svg +1 -0
  146. package/core/icons/flags/pk.svg +1 -0
  147. package/core/icons/flags/pt.svg +1 -0
  148. package/core/icons/flags/ru.svg +1 -0
  149. package/core/icons/flags/sa.svg +1 -0
  150. package/core/icons/flags/sd.svg +1 -0
  151. package/core/icons/flags/se.svg +1 -0
  152. package/core/icons/flags/th.svg +1 -0
  153. package/core/icons/flags/tm.svg +1 -0
  154. package/core/icons/flags/tr.svg +1 -0
  155. package/core/icons/flags/us.svg +1 -0
  156. package/core/icons/flags/uy.svg +1 -0
  157. package/core/icons/flags/vn.svg +1 -0
  158. package/core/icons/flags/za.svg +1 -0
  159. package/core/images/exit-popup/subscriber-1.jpg +0 -0
  160. package/core/images/exit-popup/subscriber-2.jpg +0 -0
  161. package/core/images/exit-popup/subscriber-3.jpg +0 -0
  162. package/core/images/exit-popup/subscriber-4.jpg +0 -0
  163. package/core/images/placeholder/photo-1.jpg +0 -0
  164. package/core/images/placeholder/photo-2.jpg +0 -0
  165. package/core/images/placeholder/photo-3.jpg +0 -0
  166. package/core/js/core/analytics-loader.js +253 -0
  167. package/core/js/core/app-shell.js +136 -0
  168. package/core/js/core/appearance.js +195 -0
  169. package/core/js/core/auth.js +457 -0
  170. package/core/js/core/complete.js +72 -0
  171. package/core/js/core/consent.js +384 -0
  172. package/core/js/core/dev-icon-audit.js +63 -0
  173. package/core/js/core/dev-palette.js +670 -0
  174. package/core/js/core/dev-sections.js +53 -0
  175. package/core/js/core/exit-popup.js +276 -0
  176. package/core/js/core/initialize.js +8 -0
  177. package/core/js/core/language-switcher.js +167 -0
  178. package/core/js/core/lazy-loading.js +479 -0
  179. package/core/js/core/motion.js +28 -0
  180. package/core/js/core/query-strings.js +146 -0
  181. package/core/js/core/service-worker.js +28 -0
  182. package/core/js/core/social-sharing.js +293 -0
  183. package/core/js/first-paint.js +35 -0
  184. package/core/js/layouts/modules/utilities/redirect.js +122 -0
  185. package/core/js/libs/admin-helpers.js +62 -0
  186. package/core/js/libs/alert-dismiss.js +35 -0
  187. package/core/js/libs/analytics.js +368 -0
  188. package/core/js/libs/auth/email.js +144 -0
  189. package/core/js/libs/auth/errors.js +119 -0
  190. package/core/js/libs/auth/forms.js +206 -0
  191. package/core/js/libs/auth/index.js +181 -0
  192. package/core/js/libs/auth/oauth.js +394 -0
  193. package/core/js/libs/auth/orphan.js +110 -0
  194. package/core/js/libs/auth/password-toggle.js +44 -0
  195. package/core/js/libs/auth/session-params.js +266 -0
  196. package/core/js/libs/auth/tracking.js +41 -0
  197. package/core/js/libs/charts.js +516 -0
  198. package/core/js/libs/consent-region.js +127 -0
  199. package/core/js/libs/dev.js +150 -0
  200. package/core/js/libs/graph.js +201 -0
  201. package/core/js/libs/initialize-tooltips.js +31 -0
  202. package/core/js/libs/logger.js +38 -0
  203. package/core/js/libs/omega-copy.js +89 -0
  204. package/core/js/libs/path-prefix.js +44 -0
  205. package/core/js/libs/payment-config.js +76 -0
  206. package/core/js/libs/recaptcha.js +94 -0
  207. package/core/js/libs/sale-name.js +295 -0
  208. package/core/js/libs/showcase-frames.js +37 -0
  209. package/core/js/libs/srcset.js +69 -0
  210. package/core/js/libs/tracking-consent.js +161 -0
  211. package/core/js/main.js +144 -0
  212. package/core/js/pages/404/index.js +57 -0
  213. package/core/js/pages/admin/calendar/calendar-core.js +701 -0
  214. package/core/js/pages/admin/calendar/calendar-events.js +853 -0
  215. package/core/js/pages/admin/calendar/calendar-renderer.js +807 -0
  216. package/core/js/pages/admin/calendar/campaign-preview.js +105 -0
  217. package/core/js/pages/admin/calendar/index.js +40 -0
  218. package/core/js/pages/admin/firebase/index.js +727 -0
  219. package/core/js/pages/admin/index.js +531 -0
  220. package/core/js/pages/admin/posts/editor/index.js +230 -0
  221. package/core/js/pages/admin/posts/index.js +237 -0
  222. package/core/js/pages/admin/studio/index.js +538 -0
  223. package/core/js/pages/admin/users/index.js +669 -0
  224. package/core/js/pages/admin/users/new/index.js +65 -0
  225. package/core/js/pages/admin/verts/index.js +407 -0
  226. package/core/js/pages/alternatives/[alternative]/index.js +17 -0
  227. package/core/js/pages/app/index.js +135 -0
  228. package/core/js/pages/blog/[slug].js +132 -0
  229. package/core/js/pages/blog/_search.mjs +108 -0
  230. package/core/js/pages/blog/index.js +193 -0
  231. package/core/js/pages/blog/page/[num]/index.js +7 -0
  232. package/core/js/pages/connections/callback/index.js +148 -0
  233. package/core/js/pages/contact/index.js +170 -0
  234. package/core/js/pages/cookies.js +2 -0
  235. package/core/js/pages/dashboard/account/index.js +366 -0
  236. package/core/js/pages/dashboard/account/modules/orders.js +82 -0
  237. package/core/js/pages/dashboard/account/sections/api-keys.js +149 -0
  238. package/core/js/pages/dashboard/account/sections/billing.js +1667 -0
  239. package/core/js/pages/dashboard/account/sections/connections.js +333 -0
  240. package/core/js/pages/dashboard/account/sections/data-request.js +225 -0
  241. package/core/js/pages/dashboard/account/sections/delete.js +80 -0
  242. package/core/js/pages/dashboard/account/sections/notifications.js +167 -0
  243. package/core/js/pages/dashboard/account/sections/orders.js +151 -0
  244. package/core/js/pages/dashboard/account/sections/profile.js +200 -0
  245. package/core/js/pages/dashboard/account/sections/referrals.js +217 -0
  246. package/core/js/pages/dashboard/account/sections/refund.js +253 -0
  247. package/core/js/pages/dashboard/account/sections/security.js +726 -0
  248. package/core/js/pages/dashboard/account/sections/team.js +247 -0
  249. package/core/js/pages/download/index.js +418 -0
  250. package/core/js/pages/extension/index.js +164 -0
  251. package/core/js/pages/extension/installed/index.js +135 -0
  252. package/core/js/pages/feedback/index.js +193 -0
  253. package/core/js/pages/legal/_document.js +91 -0
  254. package/core/js/pages/payment/checkout/index.js +420 -0
  255. package/core/js/pages/payment/checkout/modules/api.js +139 -0
  256. package/core/js/pages/payment/checkout/modules/dev-section.js +175 -0
  257. package/core/js/pages/payment/checkout/modules/discount.js +69 -0
  258. package/core/js/pages/payment/checkout/modules/pricing.js +73 -0
  259. package/core/js/pages/payment/checkout/modules/state.js +339 -0
  260. package/core/js/pages/payment/checkout/modules/tracking.js +54 -0
  261. package/core/js/pages/payment/confirmation/index.js +91 -0
  262. package/core/js/pages/payment/confirmation/modules/celebration.js +45 -0
  263. package/core/js/pages/payment/confirmation/modules/state.js +112 -0
  264. package/core/js/pages/payment/confirmation/modules/tracking.js +133 -0
  265. package/core/js/pages/payment/confirmation/modules/verify.js +142 -0
  266. package/core/js/pages/portal/email-preferences/index.js +183 -0
  267. package/core/js/pages/pricing/index.js +425 -0
  268. package/core/js/pages/privacy.js +2 -0
  269. package/core/js/pages/reset/index.js +12 -0
  270. package/core/js/pages/signin/index.js +22 -0
  271. package/core/js/pages/signup/index.js +23 -0
  272. package/core/js/pages/status/index.js +687 -0
  273. package/core/js/pages/terms.js +2 -0
  274. package/core/js/pages/test/components/[category]/[name]/index.js +26 -0
  275. package/core/js/pages/test/libraries/appearance/index.js +220 -0
  276. package/core/js/pages/test/libraries/form-manager/index.js +328 -0
  277. package/core/js/pages/test/libraries/layers/index.js +16 -0
  278. package/core/js/pages/test/libraries/verts/index.js +68 -0
  279. package/core/js/pages/test/sections/[category]/[name]/index.js +28 -0
  280. package/core/js/pages/test/styleguide/index.js +35 -0
  281. package/core/js/pages/token/index.js +203 -0
  282. package/core/logos/brandmarks/original/apple-app-store.svg +1 -0
  283. package/core/logos/brandmarks/original/apple.svg +6 -0
  284. package/core/logos/brandmarks/original/bitbucket.svg +14 -0
  285. package/core/logos/brandmarks/original/bitcoin.svg +1 -0
  286. package/core/logos/brandmarks/original/bluesky.svg +7 -0
  287. package/core/logos/brandmarks/original/bootstrap.svg +43 -0
  288. package/core/logos/brandmarks/original/brave.svg +1 -0
  289. package/core/logos/brandmarks/original/chrome.svg +1 -0
  290. package/core/logos/brandmarks/original/discord.svg +1 -0
  291. package/core/logos/brandmarks/original/dropbox.svg +10 -0
  292. package/core/logos/brandmarks/original/edge.svg +1 -0
  293. package/core/logos/brandmarks/original/facebook-f.svg +1 -0
  294. package/core/logos/brandmarks/original/facebook.svg +8 -0
  295. package/core/logos/brandmarks/original/figma.svg +10 -0
  296. package/core/logos/brandmarks/original/firefox.svg +1 -0
  297. package/core/logos/brandmarks/original/github.svg +6 -0
  298. package/core/logos/brandmarks/original/gitlab.svg +17 -0
  299. package/core/logos/brandmarks/original/google-a-s.svg +1 -0
  300. package/core/logos/brandmarks/original/google-chrome-webstore.svg +49 -0
  301. package/core/logos/brandmarks/original/google-play-store.svg +1 -0
  302. package/core/logos/brandmarks/original/google-web-search.svg +1 -0
  303. package/core/logos/brandmarks/original/google.svg +1 -0
  304. package/core/logos/brandmarks/original/gulp.svg +4 -0
  305. package/core/logos/brandmarks/original/html-5.svg +1 -0
  306. package/core/logos/brandmarks/original/instagram.svg +1 -0
  307. package/core/logos/brandmarks/original/javascript.svg +1 -0
  308. package/core/logos/brandmarks/original/jira.svg +18 -0
  309. package/core/logos/brandmarks/original/kick.svg +7 -0
  310. package/core/logos/brandmarks/original/linkedin.svg +1 -0
  311. package/core/logos/brandmarks/original/linux.svg +28 -0
  312. package/core/logos/brandmarks/original/messenger.svg +16 -0
  313. package/core/logos/brandmarks/original/microsoft.svg +1 -0
  314. package/core/logos/brandmarks/original/npm.svg +8 -0
  315. package/core/logos/brandmarks/original/opera.svg +1 -0
  316. package/core/logos/brandmarks/original/patreon.svg +6 -0
  317. package/core/logos/brandmarks/original/pinterest.svg +6 -0
  318. package/core/logos/brandmarks/original/reddit.svg +1 -0
  319. package/core/logos/brandmarks/original/safari.svg +31 -0
  320. package/core/logos/brandmarks/original/sass.svg +3 -0
  321. package/core/logos/brandmarks/original/signal.svg +6 -0
  322. package/core/logos/brandmarks/original/skype.svg +47 -0
  323. package/core/logos/brandmarks/original/slack.svg +33 -0
  324. package/core/logos/brandmarks/original/soundcloud.svg +1 -0
  325. package/core/logos/brandmarks/original/spotify.svg +5 -0
  326. package/core/logos/brandmarks/original/steam.svg +6 -0
  327. package/core/logos/brandmarks/original/telegram.svg +14 -0
  328. package/core/logos/brandmarks/original/tiktok.svg +1 -0
  329. package/core/logos/brandmarks/original/trello.svg +16 -0
  330. package/core/logos/brandmarks/original/tumblr.svg +1 -0
  331. package/core/logos/brandmarks/original/twitch.svg +6 -0
  332. package/core/logos/brandmarks/original/twitter.svg +6 -0
  333. package/core/logos/brandmarks/original/unity.svg +6 -0
  334. package/core/logos/brandmarks/original/x.svg +7 -0
  335. package/core/logos/brandmarks/original/yahoo-y.svg +1 -0
  336. package/core/logos/brandmarks/original/youtube-square.svg +1 -0
  337. package/core/logos/brandmarks/original/youtube.svg +1 -0
  338. package/core/logos/combomarks/dark/airbnb.svg +30 -0
  339. package/core/logos/combomarks/dark/slack.svg +38 -0
  340. package/core/logos/combomarks/dark/spotify.svg +35 -0
  341. package/core/logos/combomarks/dark/twilio.svg +26 -0
  342. package/core/logos/combomarks/dark/weebly.svg +35 -0
  343. package/core/logos/combomarks/gray/airbnb.svg +33 -0
  344. package/core/logos/combomarks/gray/alphabet.svg +23 -0
  345. package/core/logos/combomarks/gray/amazon.svg +38 -0
  346. package/core/logos/combomarks/gray/boring-company.svg +91 -0
  347. package/core/logos/combomarks/gray/burberry.svg +239 -0
  348. package/core/logos/combomarks/gray/embark.svg +21 -0
  349. package/core/logos/combomarks/gray/fitbit.svg +61 -0
  350. package/core/logos/combomarks/gray/fred-perry.svg +94 -0
  351. package/core/logos/combomarks/gray/google.svg +27 -0
  352. package/core/logos/combomarks/gray/hollister.svg +34 -0
  353. package/core/logos/combomarks/gray/hubspot.svg +27 -0
  354. package/core/logos/combomarks/gray/kaplan.svg +33 -0
  355. package/core/logos/combomarks/gray/lenovo.svg +27 -0
  356. package/core/logos/combomarks/gray/levis.svg +25 -0
  357. package/core/logos/combomarks/gray/mapbox.svg +37 -0
  358. package/core/logos/combomarks/gray/netflix.svg +15 -0
  359. package/core/logos/combomarks/gray/new-balance.svg +32 -0
  360. package/core/logos/combomarks/gray/nike.svg +17 -0
  361. package/core/logos/combomarks/gray/paypal.svg +41 -0
  362. package/core/logos/combomarks/gray/puma.svg +82 -0
  363. package/core/logos/combomarks/gray/samsung.svg +26 -0
  364. package/core/logos/combomarks/gray/shopify.svg +36 -0
  365. package/core/logos/combomarks/gray/slack.svg +42 -0
  366. package/core/logos/combomarks/gray/spotify.svg +38 -0
  367. package/core/logos/combomarks/gray/stripe.svg +24 -0
  368. package/core/logos/combomarks/gray/tnf.svg +30 -0
  369. package/core/logos/combomarks/gray/twilio.svg +29 -0
  370. package/core/logos/combomarks/gray/uber.svg +24 -0
  371. package/core/logos/combomarks/gray/weebly.svg +38 -0
  372. package/core/logos/combomarks/original/airbnb.svg +33 -0
  373. package/core/logos/combomarks/original/alphabet.svg +23 -0
  374. package/core/logos/combomarks/original/amazon.svg +38 -0
  375. package/core/logos/combomarks/original/asana.svg +8 -0
  376. package/core/logos/combomarks/original/boring-company.svg +91 -0
  377. package/core/logos/combomarks/original/discord.svg +6 -0
  378. package/core/logos/combomarks/original/embark.svg +24 -0
  379. package/core/logos/combomarks/original/fitbit.svg +60 -0
  380. package/core/logos/combomarks/original/github.svg +6 -0
  381. package/core/logos/combomarks/original/gitlab.svg +20 -0
  382. package/core/logos/combomarks/original/google.svg +28 -0
  383. package/core/logos/combomarks/original/hubspot.svg +29 -0
  384. package/core/logos/combomarks/original/instagram.svg +7 -0
  385. package/core/logos/combomarks/original/kickstarter.svg +6 -0
  386. package/core/logos/combomarks/original/lenovo.svg +24 -0
  387. package/core/logos/combomarks/original/linkedin.svg +6 -0
  388. package/core/logos/combomarks/original/mapbox.svg +39 -0
  389. package/core/logos/combomarks/original/mastodon.svg +7 -0
  390. package/core/logos/combomarks/original/medium.svg +6 -0
  391. package/core/logos/combomarks/original/meta.svg +22 -0
  392. package/core/logos/combomarks/original/microsoft.svg +13 -0
  393. package/core/logos/combomarks/original/monday.svg +12 -0
  394. package/core/logos/combomarks/original/netflix.svg +15 -0
  395. package/core/logos/combomarks/original/notion.svg +9 -0
  396. package/core/logos/combomarks/original/paypal.svg +41 -0
  397. package/core/logos/combomarks/original/reddit.svg +13 -0
  398. package/core/logos/combomarks/original/samsung.svg +25 -0
  399. package/core/logos/combomarks/original/shopify.svg +35 -0
  400. package/core/logos/combomarks/original/slack.svg +47 -0
  401. package/core/logos/combomarks/original/spotify.svg +37 -0
  402. package/core/logos/combomarks/original/stackoverflow.svg +28 -0
  403. package/core/logos/combomarks/original/stripe.svg +24 -0
  404. package/core/logos/combomarks/original/tiktok.svg +12 -0
  405. package/core/logos/combomarks/original/tumblr.svg +6 -0
  406. package/core/logos/combomarks/original/uber.svg +24 -0
  407. package/core/logos/combomarks/original/weebly.svg +34 -0
  408. package/core/logos/combomarks/original/whatsapp.svg +20 -0
  409. package/core/logos/combomarks/original/youtube.svg +10 -0
  410. package/core/logos/combomarks/original/zoom.svg +7 -0
  411. package/core/logos/combomarks/white/airbnb.svg +33 -0
  412. package/core/logos/combomarks/white/alphabet.svg +23 -0
  413. package/core/logos/combomarks/white/amazon.svg +38 -0
  414. package/core/logos/combomarks/white/boring-company.svg +91 -0
  415. package/core/logos/combomarks/white/embark.svg +21 -0
  416. package/core/logos/combomarks/white/fitbit.svg +61 -0
  417. package/core/logos/combomarks/white/google.svg +27 -0
  418. package/core/logos/combomarks/white/hubspot.svg +27 -0
  419. package/core/logos/combomarks/white/kaplan.svg +33 -0
  420. package/core/logos/combomarks/white/lenovo.svg +27 -0
  421. package/core/logos/combomarks/white/mapbox.svg +37 -0
  422. package/core/logos/combomarks/white/netflix.svg +15 -0
  423. package/core/logos/combomarks/white/paypal.svg +40 -0
  424. package/core/logos/combomarks/white/samsung.svg +26 -0
  425. package/core/logos/combomarks/white/shopify.svg +36 -0
  426. package/core/logos/combomarks/white/slack.svg +42 -0
  427. package/core/logos/combomarks/white/spotify.svg +38 -0
  428. package/core/logos/combomarks/white/stripe.svg +24 -0
  429. package/core/logos/combomarks/white/twilio.svg +29 -0
  430. package/core/logos/combomarks/white/uber.svg +24 -0
  431. package/core/logos/combomarks/white/weebly.svg +38 -0
  432. package/defaults/pages/404.md +4 -0
  433. package/defaults/pages/about.md +4 -0
  434. package/defaults/pages/admin/calendar/index.html +4 -0
  435. package/defaults/pages/admin/firebase/index.html +4 -0
  436. package/defaults/pages/admin/index.md +4 -0
  437. package/defaults/pages/admin/posts/editor.html +4 -0
  438. package/defaults/pages/admin/posts/index.html +4 -0
  439. package/defaults/pages/admin/stackblitz/index.html +4 -0
  440. package/defaults/pages/admin/studio/index.html +4 -0
  441. package/defaults/pages/admin/users/index.html +4 -0
  442. package/defaults/pages/admin/users/new.html +4 -0
  443. package/defaults/pages/admin/verts/index.html +4 -0
  444. package/defaults/pages/ads-txt.html +11 -0
  445. package/defaults/pages/alternatives/index.md +4 -0
  446. package/defaults/pages/app.md +4 -0
  447. package/defaults/pages/auth/reset.md +4 -0
  448. package/defaults/pages/auth/signin.md +4 -0
  449. package/defaults/pages/auth/signup.md +4 -0
  450. package/defaults/pages/auth/token.md +4 -0
  451. package/defaults/pages/blog/categories/category.html +21 -0
  452. package/defaults/pages/blog/categories.md +4 -0
  453. package/defaults/pages/blog/index-json.html +27 -0
  454. package/defaults/pages/blog/tags/tag.html +21 -0
  455. package/defaults/pages/blog/tags.md +4 -0
  456. package/defaults/pages/blog.md +22 -0
  457. package/defaults/pages/careers.md +4 -0
  458. package/defaults/pages/connections/callback.md +4 -0
  459. package/defaults/pages/contact.md +4 -0
  460. package/defaults/pages/dashboard/account.md +4 -0
  461. package/defaults/pages/dashboard/index.md +12 -0
  462. package/defaults/pages/download.md +4 -0
  463. package/defaults/pages/extension/index.html +4 -0
  464. package/defaults/pages/extension/installed.html +4 -0
  465. package/defaults/pages/feedback.md +4 -0
  466. package/defaults/pages/feeds/posts-json.html +40 -0
  467. package/defaults/pages/feeds/posts-xml.html +38 -0
  468. package/defaults/pages/humans.html +25 -0
  469. package/defaults/pages/index.md +4 -0
  470. package/defaults/pages/legal/cookies.md +4 -0
  471. package/defaults/pages/legal/privacy.md +4 -0
  472. package/defaults/pages/legal/terms.md +4 -0
  473. package/defaults/pages/llms-txt.html +46 -0
  474. package/defaults/pages/opensearch.html +16 -0
  475. package/defaults/pages/pages-json.html +42 -0
  476. package/defaults/pages/payment/checkout.md +4 -0
  477. package/defaults/pages/payment/confirmation.md +4 -0
  478. package/defaults/pages/portal/email-preferences.md +4 -0
  479. package/defaults/pages/pricing.md +4 -0
  480. package/defaults/pages/redirects/account.md +12 -0
  481. package/defaults/pages/redirects/admin/dashboard.md +11 -0
  482. package/defaults/pages/redirects/auth/change-password.md +9 -0
  483. package/defaults/pages/redirects/auth/forgot.md +9 -0
  484. package/defaults/pages/redirects/auth/join.md +9 -0
  485. package/defaults/pages/redirects/auth/login.md +9 -0
  486. package/defaults/pages/redirects/auth/recover.md +9 -0
  487. package/defaults/pages/redirects/auth/register.md +9 -0
  488. package/defaults/pages/redirects/auth/reset-password.md +9 -0
  489. package/defaults/pages/redirects/billing/cancel.md +9 -0
  490. package/defaults/pages/redirects/billing/refund.md +9 -0
  491. package/defaults/pages/robots.html +36 -0
  492. package/defaults/pages/security-txt.html +11 -0
  493. package/defaults/pages/sitemap.html +37 -0
  494. package/defaults/pages/status.md +4 -0
  495. package/defaults/pages/team/index.md +4 -0
  496. package/defaults/pages/test/account/dashboard.html +16 -0
  497. package/defaults/pages/test/index.md +12 -0
  498. package/defaults/pages/test/libraries/appearance.html +268 -0
  499. package/defaults/pages/test/libraries/cover.html +50 -0
  500. package/defaults/pages/test/libraries/error.html +30 -0
  501. package/defaults/pages/test/libraries/firestore.html +202 -0
  502. package/defaults/pages/test/libraries/form-manager.html +374 -0
  503. package/defaults/pages/test/libraries/layers.html +52 -0
  504. package/defaults/pages/test/libraries/lazy-loading.html +450 -0
  505. package/defaults/pages/test/libraries/verts.html +149 -0
  506. package/defaults/pages/test/redirect/external.md +13 -0
  507. package/defaults/pages/test/redirect/internal.md +13 -0
  508. package/defaults/pages/test/styleguide/index.html +588 -0
  509. package/defaults/pages/test/translation/index.md +50 -0
  510. package/defaults/pages/updates/index.md +4 -0
  511. package/defaults/sample-posts/2026-01-12-welcome-to-the-blog.md +26 -0
  512. package/defaults/sample-posts/2026-02-03-a-field-guide-to-good-defaults.md +50 -0
  513. package/defaults/sample-posts/2026-02-24-shipping-week-notes.md +37 -0
  514. package/defaults/sample-posts/2026-03-17-the-case-for-one-config.md +36 -0
  515. package/defaults/sample-posts/2026-04-09-write-release-notes-people-read.md +34 -0
  516. package/defaults/sample-posts/2026-05-21-dark-mode-done-properly.md +38 -0
  517. package/defaults/sample-posts/2026-06-02-the-boring-deploy-is-the-good-deploy.md +30 -0
  518. package/defaults/sample-posts/2026-06-11-naming-things-a-survival-guide.md +30 -0
  519. package/defaults/sample-posts/2026-06-19-your-pricing-page-is-a-product.md +28 -0
  520. package/defaults/sample-posts/2026-06-30-email-is-an-api-with-feelings.md +28 -0
  521. package/defaults/sample-posts/2026-07-08-ship-the-docs-with-the-diff.md +30 -0
  522. package/defaults/sample-team/avery-quinn.md +22 -0
  523. package/defaults/sample-team/jordan-reyes.md +25 -0
  524. package/defaults/sample-team/riley-nakamura.md +22 -0
  525. package/defaults/sample-team/sam-okafor.md +22 -0
  526. package/defaults/sample-updates/v1.0.0.md +28 -0
  527. package/defaults/sample-updates/v1.1.0.md +27 -0
  528. package/defaults/sample-updates/v1.2.0.md +27 -0
  529. package/defaults/sample-updates/v1.3.0.md +28 -0
  530. package/defaults/showcase/components.html +62 -0
  531. package/defaults/showcase/entry.html +76 -0
  532. package/defaults/showcase/frame.html +62 -0
  533. package/defaults/showcase/index.html +59 -0
  534. package/dist/assets.js +1349 -0
  535. package/dist/brand-tokens.js +195 -0
  536. package/dist/build.js +192 -0
  537. package/dist/cachebreak-html.js +71 -0
  538. package/dist/cli-run.js +25 -0
  539. package/dist/cli.js +47 -0
  540. package/dist/collections.js +183 -0
  541. package/dist/commands/audit.js +470 -0
  542. package/dist/commands/build.js +197 -0
  543. package/dist/commands/clean.js +18 -0
  544. package/dist/commands/customize.js +125 -0
  545. package/dist/commands/deploy.js +441 -0
  546. package/dist/commands/dev.js +1067 -0
  547. package/dist/commands/install.js +31 -0
  548. package/dist/commands/lib/deploy-precheck.js +43 -0
  549. package/dist/commands/lib/ensure-target.js +115 -0
  550. package/dist/commands/migrate.js +86 -0
  551. package/dist/commands/purge.js +31 -0
  552. package/dist/commands/test.js +237 -0
  553. package/dist/commands/translate.js +57 -0
  554. package/dist/commands/update.js +22 -0
  555. package/dist/commands/version.js +11 -0
  556. package/dist/config-sections.js +245 -0
  557. package/dist/consumer-scan.js +118 -0
  558. package/dist/consumer.js +49 -0
  559. package/dist/customize.js +328 -0
  560. package/dist/decisions.js +153 -0
  561. package/dist/dist-audit.js +417 -0
  562. package/dist/dynamic-pages.js +357 -0
  563. package/dist/engine.js +1402 -0
  564. package/dist/firebase-auth-helpers.js +198 -0
  565. package/dist/font-metrics.js +420 -0
  566. package/dist/frontmatter-liquid.js +253 -0
  567. package/dist/github-secrets.js +80 -0
  568. package/dist/hero-animations.js +170 -0
  569. package/dist/imagemin.js +261 -0
  570. package/dist/index.js +41 -0
  571. package/dist/inline-icons.js +65 -0
  572. package/dist/language-flags.js +55 -0
  573. package/dist/layers.js +80 -0
  574. package/dist/layouts.js +44 -0
  575. package/dist/limit-collections.js +199 -0
  576. package/dist/link-resolver.js +303 -0
  577. package/dist/markdown-images.js +85 -0
  578. package/dist/merge.js +20 -0
  579. package/dist/migrate/codemod.js +172 -0
  580. package/dist/migrate/config-convert.js +448 -0
  581. package/dist/migrate/consumer-assets.js +130 -0
  582. package/dist/migrate/index.js +223 -0
  583. package/dist/migrate/lint.js +108 -0
  584. package/dist/migrate/rules.js +1929 -0
  585. package/dist/minify-html.js +148 -0
  586. package/dist/mode-helpers.js +96 -0
  587. package/dist/omega-bin.js +7 -0
  588. package/dist/overrides.js +190 -0
  589. package/dist/path-prefix.js +167 -0
  590. package/dist/paths.js +37 -0
  591. package/dist/pricing.js +301 -0
  592. package/dist/purge.js +95 -0
  593. package/dist/redirect-page.js +45 -0
  594. package/dist/sample-content.js +210 -0
  595. package/dist/scaffold.js +123 -0
  596. package/dist/sections.js +810 -0
  597. package/dist/service-worker.js +200 -0
  598. package/dist/social-pages.js +142 -0
  599. package/dist/srcset.js +67 -0
  600. package/dist/static-assets.js +154 -0
  601. package/dist/target-shortlinks.js +123 -0
  602. package/dist/theme-vocabulary.js +76 -0
  603. package/dist/translate/collect-text-nodes.js +93 -0
  604. package/dist/translate/default-routes.js +152 -0
  605. package/dist/translate/generate-defaults.js +297 -0
  606. package/dist/translate/index.js +568 -0
  607. package/dist/translate/packaged-defaults.js +127 -0
  608. package/dist/translate/sitemap.js +108 -0
  609. package/dist/vendor/account/engine.js +182 -0
  610. package/dist/vendor/account/features.js +220 -0
  611. package/dist/vendor/account/index.js +53 -0
  612. package/dist/vendor/account/schema.js +272 -0
  613. package/dist/vendor/account/subscription.js +38 -0
  614. package/dist/vendor/config/company.js +31 -0
  615. package/dist/vendor/config/defaults.js +173 -0
  616. package/dist/vendor/config/demo.js +18 -0
  617. package/dist/vendor/config/desktop-artifacts.js +110 -0
  618. package/dist/vendor/config/edit.js +769 -0
  619. package/dist/vendor/config/env-delivery.js +145 -0
  620. package/dist/vendor/config/env-rules.js +93 -0
  621. package/dist/vendor/config/env-schema.js +1078 -0
  622. package/dist/vendor/config/env.js +445 -0
  623. package/dist/vendor/config/hooks.js +97 -0
  624. package/dist/vendor/config/index.js +237 -0
  625. package/dist/vendor/config/instances.js +208 -0
  626. package/dist/vendor/config/load.js +490 -0
  627. package/dist/vendor/config/merge.js +68 -0
  628. package/dist/vendor/config/order.js +139 -0
  629. package/dist/vendor/config/ports.js +374 -0
  630. package/dist/vendor/config/providers.js +32 -0
  631. package/dist/vendor/config/repo.js +142 -0
  632. package/dist/vendor/config/retired-keys.js +430 -0
  633. package/dist/vendor/config/schema.js +1610 -0
  634. package/dist/vendor/config/secrets.js +50 -0
  635. package/dist/vendor/config/seed.js +34 -0
  636. package/dist/vendor/config/site-global.js +205 -0
  637. package/dist/vendor/config/validate.js +554 -0
  638. package/dist/vendor/config/winback.js +61 -0
  639. package/dist/vendor/devkit/actions-secrets.js +145 -0
  640. package/dist/vendor/devkit/attach-log-file.js +262 -0
  641. package/dist/vendor/devkit/bare-requires.js +153 -0
  642. package/dist/vendor/devkit/bundle.js +292 -0
  643. package/dist/vendor/devkit/ci-workflows.js +520 -0
  644. package/dist/vendor/devkit/cli-router.js +156 -0
  645. package/dist/vendor/devkit/command-path.js +46 -0
  646. package/dist/vendor/devkit/defaults-engine.js +372 -0
  647. package/dist/vendor/devkit/deploy-precheck.js +54 -0
  648. package/dist/vendor/devkit/deploy-record.js +180 -0
  649. package/dist/vendor/devkit/deploy.js +286 -0
  650. package/dist/vendor/devkit/env-watch.js +132 -0
  651. package/dist/vendor/devkit/framework-deps.js +46 -0
  652. package/dist/vendor/devkit/icons.js +178 -0
  653. package/dist/vendor/devkit/license.js +155 -0
  654. package/dist/vendor/devkit/local-https.js +360 -0
  655. package/dist/vendor/devkit/local.js +1905 -0
  656. package/dist/vendor/devkit/logger.js +128 -0
  657. package/dist/vendor/devkit/merge-line-files.js +296 -0
  658. package/dist/vendor/devkit/omega-bin.js +345 -0
  659. package/dist/vendor/devkit/reads.js +274 -0
  660. package/dist/vendor/devkit/safe-install.js +18 -0
  661. package/dist/vendor/devkit/scaffold-guard.js +96 -0
  662. package/dist/vendor/devkit/stop-signals.js +28 -0
  663. package/dist/vendor/devkit/strip-dev-blocks-plugin.js +29 -0
  664. package/dist/vendor/devkit/strip-dev-blocks.js +32 -0
  665. package/dist/vendor/devkit/target-secrets.js +165 -0
  666. package/dist/vendor/devkit/test/scope.js +162 -0
  667. package/dist/vendor/devkit/translate/cache.js +84 -0
  668. package/dist/vendor/devkit/translate/engine.js +243 -0
  669. package/dist/vendor/devkit/translate/index.js +49 -0
  670. package/dist/vendor/devkit/translate/languages.js +134 -0
  671. package/dist/vendor/devkit/translate/providers.js +188 -0
  672. package/dist/vendor/devkit/update.js +569 -0
  673. package/dist/vendor/template-kit/data/language-flags.js +54 -0
  674. package/dist/vendor/template-kit/data/languages.js +195 -0
  675. package/dist/vendor/template-kit/data/social-urls.js +26 -0
  676. package/dist/vendor/template-kit/filters.js +227 -0
  677. package/dist/vendor/template-kit/index.js +28 -0
  678. package/dist/vendor/template-kit/jekyll-compat.js +268 -0
  679. package/dist/vendor/template-kit/register-liquid.js +0 -0
  680. package/dist/vendor/template-kit/tags/collections.js +191 -0
  681. package/dist/vendor/template-kit/tags/conditionals.js +101 -0
  682. package/dist/vendor/template-kit/tags/content.js +175 -0
  683. package/dist/vendor/template-kit/tags/index.js +33 -0
  684. package/dist/vendor/template-kit/tags/media.js +376 -0
  685. package/dist/vendor/template-kit/variable-resolver.js +163 -0
  686. package/docs/ads-system.md +109 -0
  687. package/docs/classy-v2/DIRECTION.md +92 -0
  688. package/docs/classy-v2/draft-1.html +1932 -0
  689. package/docs/classy-v2/draft-2.html +1956 -0
  690. package/docs/frontmatter.md +153 -0
  691. package/docs/index.md +254 -0
  692. package/docs/libs.md +298 -0
  693. package/docs/omega-sections-spec.md +159 -0
  694. package/docs/page-contract.md +112 -0
  695. package/docs/sections.md +845 -0
  696. package/docs/shared/agent-docs.md +89 -0
  697. package/docs/shared/analytics.md +612 -0
  698. package/docs/shared/brands.md +51 -0
  699. package/docs/shared/breaking-changes.md +497 -0
  700. package/docs/shared/config.md +1387 -0
  701. package/docs/shared/deploys.md +215 -0
  702. package/docs/shared/icons.md +201 -0
  703. package/docs/shared/local-dev.md +147 -0
  704. package/docs/shared/logging.md +202 -0
  705. package/docs/shared/monitoring.md +153 -0
  706. package/docs/shared/publishing.md +183 -0
  707. package/docs/shared/rulings.md +34 -0
  708. package/docs/shared/testing.md +147 -0
  709. package/docs/shared/theming.md +604 -0
  710. package/docs/shared/translation.md +291 -0
  711. package/docs/shared/updates.md +61 -0
  712. package/docs/template-kit.md +128 -0
  713. package/docs/test-framework.md +163 -0
  714. package/package.json +122 -0
  715. package/runtime/boot.js +149 -0
  716. package/runtime/icons.js +77 -0
  717. package/runtime/manager.js +36 -0
  718. package/scaffold/.github/workflows/build.yml +139 -0
  719. package/scaffold/.nvmrc +1 -0
  720. package/scaffold/AGENTS.md +55 -0
  721. package/scaffold/CLAUDE.md +1 -0
  722. package/scaffold/_.gitattributes +4 -0
  723. package/scaffold/_.gitignore +27 -0
  724. package/scaffold/config/omega.json5 +20 -0
  725. package/scaffold/src/assets/css/pages/.gitkeep +0 -0
  726. package/scaffold/src/assets/images/.gitkeep +0 -0
  727. package/scaffold/src/assets/js/pages/.gitkeep +0 -0
  728. package/scaffold/src/pages/.gitkeep +0 -0
  729. package/scaffold/src/pages/example.md.txt +94 -0
  730. package/scaffold/src/service-worker.js +16 -0
  731. package/sw/entry.js +19 -0
  732. package/sw/manager.js +281 -0
  733. package/themes/_template/README.md +115 -0
  734. package/themes/_template/_config.scss +60 -0
  735. package/themes/_template/_theme.js +14 -0
  736. package/themes/_template/_theme.scss +28 -0
  737. package/themes/_template/css/base/_root.scss +19 -0
  738. package/themes/_template/css/components/_components.scss +23 -0
  739. package/themes/base/README.md +5 -0
  740. package/themes/base/_components/data/org-chart/component.html +33 -0
  741. package/themes/base/_components/data/org-chart/component.json5 +33 -0
  742. package/themes/base/_components/data/org-chart/component.scss +183 -0
  743. package/themes/base/_components/heading/masthead/component.html +22 -0
  744. package/themes/base/_components/heading/masthead/component.json5 +30 -0
  745. package/themes/base/_components/heading/section-head/component.html +18 -0
  746. package/themes/base/_components/heading/section-head/component.json5 +27 -0
  747. package/themes/base/_components/pricing/features/component.html +29 -0
  748. package/themes/base/_components/pricing/features/component.json5 +30 -0
  749. package/themes/base/_hero/orbit/index.html +30 -0
  750. package/themes/base/_hero/orbit/script.js +33 -0
  751. package/themes/base/_hero/orbit/style.scss +133 -0
  752. package/themes/base/_includes/admin/sections/sidebar.html +3 -0
  753. package/themes/base/_includes/admin/sections/topbar.html +3 -0
  754. package/themes/base/_includes/backend/sections/sidebar.html +3 -0
  755. package/themes/base/_includes/backend/sections/topbar.html +3 -0
  756. package/themes/base/_includes/frontend/components/connection-card.html +49 -0
  757. package/themes/base/_includes/frontend/components/post-card.html +54 -0
  758. package/themes/base/_includes/frontend/sections/account-connections.html +42 -0
  759. package/themes/base/_includes/frontend/sections/account-section-header.html +11 -0
  760. package/themes/base/_includes/frontend/sections/footer.html +154 -0
  761. package/themes/base/_includes/frontend/sections/nav.html +276 -0
  762. package/themes/base/_includes/global/sections/account.html +72 -0
  763. package/themes/base/_includes/global/sections/app-sidebar.html +248 -0
  764. package/themes/base/_includes/global/sections/app-topbar.html +228 -0
  765. package/themes/base/_includes/global/sections/page-header.html +99 -0
  766. package/themes/base/_layouts/admin/core/minimal-viewport-locked.html +22 -0
  767. package/themes/base/_layouts/admin/core/minimal.html +23 -0
  768. package/themes/base/_layouts/backend/core/base.html +57 -0
  769. package/themes/base/_layouts/backend/core/minimal-viewport-locked.html +32 -0
  770. package/themes/base/_layouts/backend/core/minimal.html +32 -0
  771. package/themes/base/_layouts/backend/pages/dashboard/index.html +230 -0
  772. package/themes/base/_layouts/frontend/core/base.html +31 -0
  773. package/themes/base/_layouts/frontend/core/cover.html +40 -0
  774. package/themes/base/_layouts/frontend/core/minimal.html +47 -0
  775. package/themes/base/_layouts/frontend/pages/404.html +54 -0
  776. package/themes/base/_layouts/frontend/pages/about.html +144 -0
  777. package/themes/base/_layouts/frontend/pages/account/index.html +2104 -0
  778. package/themes/base/_layouts/frontend/pages/alternatives/alternative.html +319 -0
  779. package/themes/base/_layouts/frontend/pages/alternatives/index.html +186 -0
  780. package/themes/base/_layouts/frontend/pages/app.html +142 -0
  781. package/themes/base/_layouts/frontend/pages/auth/reset.html +53 -0
  782. package/themes/base/_layouts/frontend/pages/auth/signin.html +90 -0
  783. package/themes/base/_layouts/frontend/pages/auth/signup.html +141 -0
  784. package/themes/base/_layouts/frontend/pages/auth/token.html +44 -0
  785. package/themes/base/_layouts/frontend/pages/blog/categories/category.html +88 -0
  786. package/themes/base/_layouts/frontend/pages/blog/categories/index.html +88 -0
  787. package/themes/base/_layouts/frontend/pages/blog/index.html +255 -0
  788. package/themes/base/_layouts/frontend/pages/blog/post.html +261 -0
  789. package/themes/base/_layouts/frontend/pages/blog/tags/index.html +84 -0
  790. package/themes/base/_layouts/frontend/pages/blog/tags/tag.html +88 -0
  791. package/themes/base/_layouts/frontend/pages/collection/category.html +72 -0
  792. package/themes/base/_layouts/frontend/pages/collection/document.html +54 -0
  793. package/themes/base/_layouts/frontend/pages/collection/index.html +84 -0
  794. package/themes/base/_layouts/frontend/pages/connections/callback.html +55 -0
  795. package/themes/base/_layouts/frontend/pages/contact.html +214 -0
  796. package/themes/base/_layouts/frontend/pages/download.html +793 -0
  797. package/themes/base/_layouts/frontend/pages/extension/index.html +253 -0
  798. package/themes/base/_layouts/frontend/pages/extension/installed.html +198 -0
  799. package/themes/base/_layouts/frontend/pages/feedback.html +171 -0
  800. package/themes/base/_layouts/frontend/pages/index.html +66 -0
  801. package/themes/base/_layouts/frontend/pages/legal/document.html +78 -0
  802. package/themes/base/_layouts/frontend/pages/payment/checkout.html +459 -0
  803. package/themes/base/_layouts/frontend/pages/payment/confirmation.html +131 -0
  804. package/themes/base/_layouts/frontend/pages/portal/email-preferences.html +104 -0
  805. package/themes/base/_layouts/frontend/pages/pricing.html +596 -0
  806. package/themes/base/_layouts/frontend/pages/status.html +361 -0
  807. package/themes/base/_layouts/frontend/pages/team/index.html +210 -0
  808. package/themes/base/_layouts/frontend/pages/team/member.html +125 -0
  809. package/themes/base/_layouts/frontend/pages/updates/index.html +119 -0
  810. package/themes/base/_layouts/frontend/pages/updates/update.html +118 -0
  811. package/themes/base/_sections/about/hero/section.html +68 -0
  812. package/themes/base/_sections/about/hero/section.json5 +58 -0
  813. package/themes/base/_sections/about/letter/section.html +60 -0
  814. package/themes/base/_sections/about/letter/section.json5 +56 -0
  815. package/themes/base/_sections/about/photo-band/section.html +24 -0
  816. package/themes/base/_sections/about/photo-band/section.json5 +23 -0
  817. package/themes/base/_sections/about/principles/section.html +17 -0
  818. package/themes/base/_sections/about/principles/section.json5 +34 -0
  819. package/themes/base/_sections/about/timeline/section.html +20 -0
  820. package/themes/base/_sections/about/timeline/section.json5 +34 -0
  821. package/themes/base/_sections/marketing/bento/section.html +91 -0
  822. package/themes/base/_sections/marketing/bento/section.json5 +152 -0
  823. package/themes/base/_sections/marketing/cta/section.html +44 -0
  824. package/themes/base/_sections/marketing/cta/section.json5 +47 -0
  825. package/themes/base/_sections/marketing/faq/section.html +52 -0
  826. package/themes/base/_sections/marketing/faq/section.json5 +68 -0
  827. package/themes/base/_sections/marketing/hero/section.html +349 -0
  828. package/themes/base/_sections/marketing/hero/section.json5 +335 -0
  829. package/themes/base/_sections/marketing/newsletter-cta/section.html +38 -0
  830. package/themes/base/_sections/marketing/newsletter-cta/section.js +86 -0
  831. package/themes/base/_sections/marketing/newsletter-cta/section.json5 +29 -0
  832. package/themes/base/_sections/marketing/pricing-cards/section.html +115 -0
  833. package/themes/base/_sections/marketing/pricing-cards/section.json5 +72 -0
  834. package/themes/base/_sections/marketing/product-demo/section.html +62 -0
  835. package/themes/base/_sections/marketing/product-demo/section.js +44 -0
  836. package/themes/base/_sections/marketing/product-demo/section.json5 +52 -0
  837. package/themes/base/_sections/marketing/prose/section.html +22 -0
  838. package/themes/base/_sections/marketing/prose/section.json5 +41 -0
  839. package/themes/base/_sections/marketing/showcase/section.html +35 -0
  840. package/themes/base/_sections/marketing/showcase/section.json5 +52 -0
  841. package/themes/base/_sections/marketing/stats/section.html +32 -0
  842. package/themes/base/_sections/marketing/stats/section.json5 +44 -0
  843. package/themes/base/_sections/marketing/testimonials/section.html +54 -0
  844. package/themes/base/_sections/marketing/testimonials/section.json5 +70 -0
  845. package/themes/base/_sections/marketing/trusted-by/section.html +30 -0
  846. package/themes/base/_sections/marketing/trusted-by/section.json5 +38 -0
  847. package/themes/base/_sections/verts/unit/section.html +7 -0
  848. package/themes/base/_sections/verts/unit/section.js +22 -0
  849. package/themes/base/_sections/verts/unit/section.json5 +24 -0
  850. package/themes/base/_sections/verts/unit/section.scss +24 -0
  851. package/themes/base/_theme.js +13 -0
  852. package/themes/base/_theme.scss +4 -0
  853. package/themes/bootstrap/_theme.js +16 -0
  854. package/themes/bootstrap/_theme.scss +11 -0
  855. package/themes/bootstrap/js/index.esm.js +19 -0
  856. package/themes/bootstrap/js/index.umd.js +34 -0
  857. package/themes/bootstrap/js/src/alert.js +87 -0
  858. package/themes/bootstrap/js/src/base-component.js +86 -0
  859. package/themes/bootstrap/js/src/button.js +72 -0
  860. package/themes/bootstrap/js/src/carousel.js +474 -0
  861. package/themes/bootstrap/js/src/collapse.js +297 -0
  862. package/themes/bootstrap/js/src/dom/data.js +55 -0
  863. package/themes/bootstrap/js/src/dom/event-handler.js +317 -0
  864. package/themes/bootstrap/js/src/dom/manipulator.js +71 -0
  865. package/themes/bootstrap/js/src/dom/selector-engine.js +126 -0
  866. package/themes/bootstrap/js/src/dropdown.js +458 -0
  867. package/themes/bootstrap/js/src/modal.js +378 -0
  868. package/themes/bootstrap/js/src/offcanvas.js +282 -0
  869. package/themes/bootstrap/js/src/popover.js +97 -0
  870. package/themes/bootstrap/js/src/scrollspy.js +296 -0
  871. package/themes/bootstrap/js/src/tab.js +315 -0
  872. package/themes/bootstrap/js/src/toast.js +224 -0
  873. package/themes/bootstrap/js/src/tooltip.js +632 -0
  874. package/themes/bootstrap/js/src/util/backdrop.js +151 -0
  875. package/themes/bootstrap/js/src/util/component-functions.js +35 -0
  876. package/themes/bootstrap/js/src/util/config.js +65 -0
  877. package/themes/bootstrap/js/src/util/focustrap.js +115 -0
  878. package/themes/bootstrap/js/src/util/index.js +306 -0
  879. package/themes/bootstrap/js/src/util/sanitizer.js +117 -0
  880. package/themes/bootstrap/js/src/util/scrollbar.js +114 -0
  881. package/themes/bootstrap/js/src/util/swipe.js +146 -0
  882. package/themes/bootstrap/js/src/util/template-factory.js +160 -0
  883. package/themes/bootstrap/overrides/_accordion.scss +21 -0
  884. package/themes/bootstrap/overrides/_avatars.scss +51 -0
  885. package/themes/bootstrap/overrides/_blog-images.scss +45 -0
  886. package/themes/bootstrap/overrides/_buttons-adaptive.scss +141 -0
  887. package/themes/bootstrap/overrides/_color-shades.scss +25 -0
  888. package/themes/bootstrap/overrides/_cursor-utilities.scss +13 -0
  889. package/themes/bootstrap/overrides/_index.scss +30 -0
  890. package/themes/bootstrap/overrides/_links.scss +26 -0
  891. package/themes/bootstrap/overrides/_soft-colors.scss +99 -0
  892. package/themes/bootstrap/overrides/_spacing-extended.scss +74 -0
  893. package/themes/bootstrap/overrides/_spinners.scss +22 -0
  894. package/themes/bootstrap/overrides/_typography-utilities.scss +40 -0
  895. package/themes/bootstrap/scss/_accordion.scss +153 -0
  896. package/themes/bootstrap/scss/_alert.scss +68 -0
  897. package/themes/bootstrap/scss/_badge.scss +38 -0
  898. package/themes/bootstrap/scss/_breadcrumb.scss +40 -0
  899. package/themes/bootstrap/scss/_button-group.scss +147 -0
  900. package/themes/bootstrap/scss/_buttons.scss +216 -0
  901. package/themes/bootstrap/scss/_card.scss +238 -0
  902. package/themes/bootstrap/scss/_carousel.scss +226 -0
  903. package/themes/bootstrap/scss/_close.scss +66 -0
  904. package/themes/bootstrap/scss/_containers.scss +41 -0
  905. package/themes/bootstrap/scss/_dropdown.scss +250 -0
  906. package/themes/bootstrap/scss/_forms.scss +9 -0
  907. package/themes/bootstrap/scss/_functions.scss +302 -0
  908. package/themes/bootstrap/scss/_grid.scss +39 -0
  909. package/themes/bootstrap/scss/_helpers.scss +12 -0
  910. package/themes/bootstrap/scss/_images.scss +42 -0
  911. package/themes/bootstrap/scss/_list-group.scss +199 -0
  912. package/themes/bootstrap/scss/_maps.scss +174 -0
  913. package/themes/bootstrap/scss/_mixins.scss +42 -0
  914. package/themes/bootstrap/scss/_modal.scss +240 -0
  915. package/themes/bootstrap/scss/_nav.scss +197 -0
  916. package/themes/bootstrap/scss/_navbar.scss +289 -0
  917. package/themes/bootstrap/scss/_offcanvas.scss +147 -0
  918. package/themes/bootstrap/scss/_pagination.scss +109 -0
  919. package/themes/bootstrap/scss/_placeholders.scss +51 -0
  920. package/themes/bootstrap/scss/_popover.scss +196 -0
  921. package/themes/bootstrap/scss/_progress.scss +68 -0
  922. package/themes/bootstrap/scss/_reboot.scss +611 -0
  923. package/themes/bootstrap/scss/_root.scss +187 -0
  924. package/themes/bootstrap/scss/_spinners.scss +85 -0
  925. package/themes/bootstrap/scss/_tables.scss +171 -0
  926. package/themes/bootstrap/scss/_toasts.scss +73 -0
  927. package/themes/bootstrap/scss/_tooltip.scss +119 -0
  928. package/themes/bootstrap/scss/_transitions.scss +27 -0
  929. package/themes/bootstrap/scss/_type.scss +106 -0
  930. package/themes/bootstrap/scss/_utilities.scss +806 -0
  931. package/themes/bootstrap/scss/_variables-dark.scss +102 -0
  932. package/themes/bootstrap/scss/_variables.scss +1753 -0
  933. package/themes/bootstrap/scss/bootstrap-grid.scss +62 -0
  934. package/themes/bootstrap/scss/bootstrap-reboot.scss +10 -0
  935. package/themes/bootstrap/scss/bootstrap-utilities.scss +19 -0
  936. package/themes/bootstrap/scss/bootstrap.scss +52 -0
  937. package/themes/bootstrap/scss/forms/_floating-labels.scss +97 -0
  938. package/themes/bootstrap/scss/forms/_form-check.scss +189 -0
  939. package/themes/bootstrap/scss/forms/_form-control.scss +214 -0
  940. package/themes/bootstrap/scss/forms/_form-range.scss +91 -0
  941. package/themes/bootstrap/scss/forms/_form-select.scss +80 -0
  942. package/themes/bootstrap/scss/forms/_form-text.scss +11 -0
  943. package/themes/bootstrap/scss/forms/_input-group.scss +132 -0
  944. package/themes/bootstrap/scss/forms/_labels.scss +36 -0
  945. package/themes/bootstrap/scss/forms/_validation.scss +12 -0
  946. package/themes/bootstrap/scss/helpers/_clearfix.scss +3 -0
  947. package/themes/bootstrap/scss/helpers/_color-bg.scss +7 -0
  948. package/themes/bootstrap/scss/helpers/_colored-links.scss +30 -0
  949. package/themes/bootstrap/scss/helpers/_focus-ring.scss +5 -0
  950. package/themes/bootstrap/scss/helpers/_icon-link.scss +25 -0
  951. package/themes/bootstrap/scss/helpers/_position.scss +36 -0
  952. package/themes/bootstrap/scss/helpers/_ratio.scss +26 -0
  953. package/themes/bootstrap/scss/helpers/_stacks.scss +15 -0
  954. package/themes/bootstrap/scss/helpers/_stretched-link.scss +15 -0
  955. package/themes/bootstrap/scss/helpers/_text-truncation.scss +7 -0
  956. package/themes/bootstrap/scss/helpers/_visually-hidden.scss +8 -0
  957. package/themes/bootstrap/scss/helpers/_vr.scss +8 -0
  958. package/themes/bootstrap/scss/mixins/_alert.scss +18 -0
  959. package/themes/bootstrap/scss/mixins/_backdrop.scss +14 -0
  960. package/themes/bootstrap/scss/mixins/_banner.scss +7 -0
  961. package/themes/bootstrap/scss/mixins/_border-radius.scss +78 -0
  962. package/themes/bootstrap/scss/mixins/_box-shadow.scss +18 -0
  963. package/themes/bootstrap/scss/mixins/_breakpoints.scss +127 -0
  964. package/themes/bootstrap/scss/mixins/_buttons.scss +70 -0
  965. package/themes/bootstrap/scss/mixins/_caret.scss +69 -0
  966. package/themes/bootstrap/scss/mixins/_clearfix.scss +9 -0
  967. package/themes/bootstrap/scss/mixins/_color-mode.scss +21 -0
  968. package/themes/bootstrap/scss/mixins/_color-scheme.scss +7 -0
  969. package/themes/bootstrap/scss/mixins/_container.scss +11 -0
  970. package/themes/bootstrap/scss/mixins/_deprecate.scss +10 -0
  971. package/themes/bootstrap/scss/mixins/_forms.scss +163 -0
  972. package/themes/bootstrap/scss/mixins/_gradients.scss +47 -0
  973. package/themes/bootstrap/scss/mixins/_grid.scss +151 -0
  974. package/themes/bootstrap/scss/mixins/_image.scss +16 -0
  975. package/themes/bootstrap/scss/mixins/_list-group.scss +26 -0
  976. package/themes/bootstrap/scss/mixins/_lists.scss +7 -0
  977. package/themes/bootstrap/scss/mixins/_pagination.scss +10 -0
  978. package/themes/bootstrap/scss/mixins/_reset-text.scss +17 -0
  979. package/themes/bootstrap/scss/mixins/_resize.scss +6 -0
  980. package/themes/bootstrap/scss/mixins/_table-variants.scss +24 -0
  981. package/themes/bootstrap/scss/mixins/_text-truncate.scss +8 -0
  982. package/themes/bootstrap/scss/mixins/_transition.scss +26 -0
  983. package/themes/bootstrap/scss/mixins/_utilities.scss +97 -0
  984. package/themes/bootstrap/scss/mixins/_visually-hidden.scss +38 -0
  985. package/themes/bootstrap/scss/tests/jasmine.js +16 -0
  986. package/themes/bootstrap/scss/tests/mixins/_auto-import-of-variables-dark.test.scss +7 -0
  987. package/themes/bootstrap/scss/tests/mixins/_color-modes.test.scss +69 -0
  988. package/themes/bootstrap/scss/tests/mixins/_media-query-color-mode-full.test.scss +8 -0
  989. package/themes/bootstrap/scss/tests/mixins/_utilities.test.scss +393 -0
  990. package/themes/bootstrap/scss/tests/sass-true/register.js +14 -0
  991. package/themes/bootstrap/scss/tests/sass-true/runner.js +17 -0
  992. package/themes/bootstrap/scss/tests/utilities/_api.test.scss +75 -0
  993. package/themes/bootstrap/scss/utilities/_api.scss +47 -0
  994. package/themes/bootstrap/scss/vendor/_rfs.scss +348 -0
  995. package/themes/classy/README.md +89 -0
  996. package/themes/classy/_config.scss +245 -0
  997. package/themes/classy/_theme.js +31 -0
  998. package/themes/classy/_theme.scss +57 -0
  999. package/themes/classy/css/app/_panels.scss +183 -0
  1000. package/themes/classy/css/base/_fonts.scss +79 -0
  1001. package/themes/classy/css/base/_no-raise.scss +49 -0
  1002. package/themes/classy/css/base/_root.scss +113 -0
  1003. package/themes/classy/css/base/_typography.scss +154 -0
  1004. package/themes/classy/css/base/_utilities.scss +284 -0
  1005. package/themes/classy/css/components/_alerts.scss +51 -0
  1006. package/themes/classy/css/components/_badges.scss +46 -0
  1007. package/themes/classy/css/components/_buttons.scss +175 -0
  1008. package/themes/classy/css/components/_cards.scss +41 -0
  1009. package/themes/classy/css/components/_dropdowns.scss +48 -0
  1010. package/themes/classy/css/components/_forms.scss +171 -0
  1011. package/themes/classy/css/components/_receipt.scss +45 -0
  1012. package/themes/classy/css/layout/_footer.scss +298 -0
  1013. package/themes/classy/css/layout/_general.scss +209 -0
  1014. package/themes/classy/css/layout/_nav.scss +154 -0
  1015. package/themes/classy/css/layout/_shell.scss +458 -0
  1016. package/themes/classy/css/marketing/_bento.scss +238 -0
  1017. package/themes/classy/css/marketing/_content.scss +1328 -0
  1018. package/themes/classy/css/marketing/_downloads.scss +246 -0
  1019. package/themes/classy/css/marketing/_hero.scss +311 -0
  1020. package/themes/classy/css/marketing/_sections.scss +649 -0
  1021. package/themes/classy/css/pages/404/index.scss +67 -0
  1022. package/themes/classy/css/pages/_auth.scss +235 -0
  1023. package/themes/classy/css/pages/admin/firebase/index.scss +22 -0
  1024. package/themes/classy/css/pages/blog/[slug].scss +56 -0
  1025. package/themes/classy/css/pages/cookies.scss +2 -0
  1026. package/themes/classy/css/pages/dashboard/account/index.scss +31 -0
  1027. package/themes/classy/css/pages/feedback/index.scss +39 -0
  1028. package/themes/classy/css/pages/legal/_document.scss +192 -0
  1029. package/themes/classy/css/pages/payment/checkout/index.scss +585 -0
  1030. package/themes/classy/css/pages/payment/confirmation/index.scss +9 -0
  1031. package/themes/classy/css/pages/privacy.scss +2 -0
  1032. package/themes/classy/css/pages/status/index.scss +164 -0
  1033. package/themes/classy/css/pages/terms.scss +2 -0
  1034. package/themes/classy/css/pages/updates/index.scss +128 -0
  1035. package/themes/classy/fonts/inter-italic-latin-ext.woff2 +0 -0
  1036. package/themes/classy/fonts/inter-italic-latin.woff2 +0 -0
  1037. package/themes/classy/fonts/inter-normal-latin-ext.woff2 +0 -0
  1038. package/themes/classy/fonts/inter-normal-latin.woff2 +0 -0
  1039. package/themes/classy/fonts/newsreader-italic-latin-ext.woff2 +0 -0
  1040. package/themes/classy/fonts/newsreader-italic-latin.woff2 +0 -0
  1041. package/themes/classy/fonts/newsreader-normal-latin-ext.woff2 +0 -0
  1042. package/themes/classy/fonts/newsreader-normal-latin.woff2 +0 -0
  1043. package/themes/classy/js/hero-demo-form.js +56 -0
  1044. package/themes/neobrutalism/README.md +118 -0
  1045. package/themes/neobrutalism/_config.scss +139 -0
  1046. package/themes/neobrutalism/_layouts/frontend/core/base.html +30 -0
  1047. package/themes/neobrutalism/_layouts/frontend/pages/index.html +275 -0
  1048. package/themes/neobrutalism/_theme.js +24 -0
  1049. package/themes/neobrutalism/_theme.scss +61 -0
  1050. package/themes/neobrutalism/css/base/_mixins.scss +46 -0
  1051. package/themes/neobrutalism/css/base/_root.scss +169 -0
  1052. package/themes/neobrutalism/css/base/_typography.scss +61 -0
  1053. package/themes/neobrutalism/css/base/_utilities.scss +89 -0
  1054. package/themes/neobrutalism/css/components/_billing-toggle.scss +74 -0
  1055. package/themes/neobrutalism/css/components/_buttons.scss +148 -0
  1056. package/themes/neobrutalism/css/components/_cards.scss +47 -0
  1057. package/themes/neobrutalism/css/components/_forms.scss +88 -0
  1058. package/themes/neobrutalism/css/components/_marketing.scss +244 -0
  1059. package/themes/neobrutalism/css/layout/_general.scss +213 -0
  1060. package/themes/neobrutalism/css/layout/_navigation.scss +171 -0
  1061. package/themes/neobrutalism/css/pages/index.scss +203 -0
  1062. package/themes/neobrutalism/css/pages/pricing/index.scss +58 -0
  1063. package/themes/neobrutalism/css/pages/test/libraries/layers/index.scss +7 -0
  1064. package/themes/neobrutalism/js/pages/test/libraries/layers/index.js +9 -0
  1065. package/themes/newsflash/README.md +117 -0
  1066. package/themes/newsflash/_components/heading/lede/component.html +14 -0
  1067. package/themes/newsflash/_components/heading/lede/component.json5 +23 -0
  1068. package/themes/newsflash/_components/heading/rule-head/component.html +17 -0
  1069. package/themes/newsflash/_components/heading/rule-head/component.json5 +23 -0
  1070. package/themes/newsflash/_components/news/byline/component.html +12 -0
  1071. package/themes/newsflash/_components/news/byline/component.json5 +18 -0
  1072. package/themes/newsflash/_components/news/story-card/component.html +25 -0
  1073. package/themes/newsflash/_components/news/story-card/component.json5 +21 -0
  1074. package/themes/newsflash/_config.scss +147 -0
  1075. package/themes/newsflash/_layouts/frontend/core/base.html +51 -0
  1076. package/themes/newsflash/_layouts/frontend/pages/blog/index.html +299 -0
  1077. package/themes/newsflash/_layouts/frontend/pages/blog/post.html +235 -0
  1078. package/themes/newsflash/_layouts/frontend/pages/index.html +328 -0
  1079. package/themes/newsflash/_sections/marketing/desks/section.html +39 -0
  1080. package/themes/newsflash/_sections/marketing/desks/section.json5 +49 -0
  1081. package/themes/newsflash/_sections/marketing/newsletter-cta/section.html +103 -0
  1082. package/themes/newsflash/_sections/marketing/newsletter-cta/section.json5 +50 -0
  1083. package/themes/newsflash/_sections/marketing/rundown/section.html +28 -0
  1084. package/themes/newsflash/_sections/marketing/rundown/section.json5 +40 -0
  1085. package/themes/newsflash/_theme.js +24 -0
  1086. package/themes/newsflash/_theme.scss +70 -0
  1087. package/themes/newsflash/css/base/_fonts.scss +80 -0
  1088. package/themes/newsflash/css/base/_mixins.scss +51 -0
  1089. package/themes/newsflash/css/base/_root.scss +257 -0
  1090. package/themes/newsflash/css/base/_typography.scss +49 -0
  1091. package/themes/newsflash/css/base/_utilities.scss +122 -0
  1092. package/themes/newsflash/css/components/_badges.scss +65 -0
  1093. package/themes/newsflash/css/components/_billing-toggle.scss +61 -0
  1094. package/themes/newsflash/css/components/_buttons.scss +139 -0
  1095. package/themes/newsflash/css/components/_cards.scss +47 -0
  1096. package/themes/newsflash/css/components/_editorial.scss +173 -0
  1097. package/themes/newsflash/css/components/_forms.scss +75 -0
  1098. package/themes/newsflash/css/components/_infinite-scroll.scss +102 -0
  1099. package/themes/newsflash/css/components/_panels.scss +156 -0
  1100. package/themes/newsflash/css/components/_ticker.scss +67 -0
  1101. package/themes/newsflash/css/layout/_general.scss +330 -0
  1102. package/themes/newsflash/css/layout/_navigation.scss +168 -0
  1103. package/themes/newsflash/css/pages/404/index.scss +79 -0
  1104. package/themes/newsflash/css/pages/blog/[slug].scss +186 -0
  1105. package/themes/newsflash/css/pages/blog/index.scss +17 -0
  1106. package/themes/newsflash/css/pages/index.scss +157 -0
  1107. package/themes/newsflash/css/pages/pricing/index.scss +55 -0
  1108. package/themes/newsflash/css/pages/test/libraries/layers/index.scss +7 -0
  1109. package/themes/newsflash/fonts/fraunces-italic-latin-ext.woff2 +0 -0
  1110. package/themes/newsflash/fonts/fraunces-italic-latin.woff2 +0 -0
  1111. package/themes/newsflash/fonts/fraunces-normal-latin-ext.woff2 +0 -0
  1112. package/themes/newsflash/fonts/fraunces-normal-latin.woff2 +0 -0
  1113. package/themes/newsflash/fonts/schibsted-grotesk-italic-latin-ext.woff2 +0 -0
  1114. package/themes/newsflash/fonts/schibsted-grotesk-italic-latin.woff2 +0 -0
  1115. package/themes/newsflash/fonts/schibsted-grotesk-normal-latin-ext.woff2 +0 -0
  1116. package/themes/newsflash/fonts/schibsted-grotesk-normal-latin.woff2 +0 -0
  1117. package/themes/newsflash/js/pages/blog/[slug].js +29 -0
  1118. package/themes/newsflash/js/pages/test/libraries/layers/index.js +9 -0
  1119. package/translations/es/pages/404.json +86 -0
  1120. package/translations/es/pages/account.json +36 -0
  1121. package/translations/es/pages/app.json +44 -0
  1122. package/translations/es/pages/cancel.json +36 -0
  1123. package/translations/es/pages/change-password.json +36 -0
  1124. package/translations/es/pages/connections/callback.json +41 -0
  1125. package/translations/es/pages/dashboard/account.json +475 -0
  1126. package/translations/es/pages/dashboard.json +36 -0
  1127. package/translations/es/pages/forgot.json +36 -0
  1128. package/translations/es/pages/join.json +36 -0
  1129. package/translations/es/pages/login.json +36 -0
  1130. package/translations/es/pages/payment/checkout.json +83 -0
  1131. package/translations/es/pages/payment/confirmation.json +62 -0
  1132. package/translations/es/pages/portal/email-preferences.json +46 -0
  1133. package/translations/es/pages/recover.json +36 -0
  1134. package/translations/es/pages/refund.json +36 -0
  1135. package/translations/es/pages/register.json +36 -0
  1136. package/translations/es/pages/reset-password.json +36 -0
  1137. package/translations/es/pages/reset.json +42 -0
  1138. package/translations/es/pages/signin.json +45 -0
  1139. package/translations/es/pages/signup.json +60 -0
  1140. package/translations/es/pages/token.json +39 -0
  1141. package/translations/fa/pages/404.json +86 -0
  1142. package/translations/fa/pages/account.json +36 -0
  1143. package/translations/fa/pages/app.json +44 -0
  1144. package/translations/fa/pages/cancel.json +36 -0
  1145. package/translations/fa/pages/change-password.json +36 -0
  1146. package/translations/fa/pages/connections/callback.json +41 -0
  1147. package/translations/fa/pages/dashboard/account.json +475 -0
  1148. package/translations/fa/pages/dashboard.json +36 -0
  1149. package/translations/fa/pages/forgot.json +36 -0
  1150. package/translations/fa/pages/join.json +36 -0
  1151. package/translations/fa/pages/login.json +36 -0
  1152. package/translations/fa/pages/payment/checkout.json +83 -0
  1153. package/translations/fa/pages/payment/confirmation.json +62 -0
  1154. package/translations/fa/pages/portal/email-preferences.json +46 -0
  1155. package/translations/fa/pages/recover.json +36 -0
  1156. package/translations/fa/pages/refund.json +36 -0
  1157. package/translations/fa/pages/register.json +36 -0
  1158. package/translations/fa/pages/reset-password.json +36 -0
  1159. package/translations/fa/pages/reset.json +42 -0
  1160. package/translations/fa/pages/signin.json +45 -0
  1161. package/translations/fa/pages/signup.json +60 -0
  1162. package/translations/fa/pages/token.json +39 -0
@@ -0,0 +1,1667 @@
1
+ /**
2
+ * Billing Section JavaScript
3
+ */
4
+
5
+ // Libraries
6
+ import { FormManager } from '@omega.js/client/modules/form-manager.js';
7
+ import omega from '@omega.js/client';
8
+ import { createLogger } from '__main_assets__/js/libs/logger.js';
9
+ import initializeTooltips from '__main_assets__/js/libs/initialize-tooltips.js';
10
+ import { event } from '__main_assets__/js/libs/analytics.js';
11
+ import { FREQUENCIES, getAvailableFrequencies } from '../../../payment/checkout/modules/state.js';
12
+ import { resolveFeatures } from '@omega.js/client/modules/features.js';
13
+
14
+ const logger = createLogger('account:billing');
15
+
16
+ let paymentConfig = null;
17
+ let cancelFormManager = null;
18
+ let currentAccount = null;
19
+
20
+ // Whether the account's provider can do these at all is the BACKEND's answer
21
+ // (the capability gate): we attempt the route and branch on the code that comes
22
+ // back, never on a provider map here. A refusal retires the button for the
23
+ // session — the billing portal is the path that works for that provider.
24
+ let uncancelSupported = true;
25
+ let planSwitchSupported = true;
26
+ let winbackSupported = true;
27
+
28
+ // The save offer (#268) is pitched per CANCEL ATTEMPT, never once per session
29
+ // ([#324]): every fresh click on "Cancel subscription" re-evaluates
30
+ // eligibility, so a customer who declined, shut the questionnaire and came back
31
+ // to cancel is pitched again — the subscription is still live and the offer is
32
+ // still unclaimed, which is exactly the moment the pitch is for. A one-shot
33
+ // flag made the SECOND attempt the silent one, which is the state Ian's QA
34
+ // found.
35
+ //
36
+ // Only two things end the pitch, and both are facts about the account rather
37
+ // than about the click: the offer has been CLAIMED (the discount is on the
38
+ // subscription, and the backend refuses a second claim), or the backend has
39
+ // refused it in a way no retry fixes (`winbackSupported`).
40
+ let winbackClaimed = false;
41
+
42
+ // Cancellation reasons (will be shuffled on each render)
43
+ const CANCEL_REASONS = [
44
+ 'Too expensive',
45
+ 'Not using it enough',
46
+ 'Missing features I need',
47
+ 'Found a better alternative',
48
+ 'Technical issues or bugs',
49
+ 'Just testing or temporary need',
50
+ 'Other',
51
+ ];
52
+
53
+ // Status display configuration (classy status pill: dot + label, never color alone)
54
+ const STATUS_CONFIG = {
55
+ free: { label: 'Free', badgeClass: 'omega-status', dotClass: 'omega-dot omega-dot--muted' },
56
+ active: { label: 'Active', badgeClass: 'omega-status omega-status--ok', dotClass: 'omega-dot omega-dot--ok' },
57
+ trialing: { label: 'Active', badgeClass: 'omega-status omega-status--ok', dotClass: 'omega-dot omega-dot--ok' },
58
+ cancelling: { label: 'Active', badgeClass: 'omega-status omega-status--ok', dotClass: 'omega-dot omega-dot--ok' },
59
+ suspended: { label: 'Suspended', badgeClass: 'omega-status omega-status--danger', dotClass: 'omega-dot omega-dot--danger' },
60
+ cancelled: { label: 'Cancelled', badgeClass: 'omega-status', dotClass: 'omega-dot omega-dot--muted' },
61
+ };
62
+
63
+ const FREQUENCY_LABELS = { daily: 'day', weekly: 'week', monthly: 'month', annually: 'year' };
64
+
65
+ // The cadence toggle's segment copy — the same wording the pricing page's
66
+ // billing toggle uses, for the same catalog frequencies.
67
+ const CADENCE_LABELS = { daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly', annually: 'Annually' };
68
+
69
+ // The details row's slots always render, so a value the subscription never
70
+ // recorded needs something to SAY: one placeholder word for every slot, drawn
71
+ // in the muted ink so it reads as "not known" rather than as a fact ([#236] QA
72
+ // round 3). Never an empty gap, and never `undefined`/`NaN`.
73
+ //
74
+ // Both class names are spelled out in FULL, never composed: the purge pass
75
+ // reads the built JS as content, and a class it only ever sees assembled at
76
+ // runtime is a class it deletes.
77
+ const UNKNOWN_DETAIL = 'Unknown';
78
+ const DETAIL_CLASS = 'omega-billing-detail';
79
+ const UNKNOWN_DETAIL_CLASS = 'omega-billing-detail omega-billing-detail--unknown';
80
+
81
+ // How many bullets a plan option shows in the switcher. The modal is a billing
82
+ // ACTION, not the pricing page — "Compare plans" carries the full story.
83
+ const HEADLINE_FEATURE_LIMIT = 4;
84
+
85
+ // The cadence the switcher is currently showing prices at. Set from the
86
+ // account's own cadence every time the modal opens, then by the toggle.
87
+ let switcherFrequency = null;
88
+
89
+ // The plan the PRICING page sent us here to switch to (`?product=&frequency=`),
90
+ // held from the moment the URL is read until the billing tab is on screen —
91
+ // a modal cannot open inside a section that is still `d-none`.
92
+ let pendingSwitch = null;
93
+
94
+ // Initialize billing section
95
+ export async function init() {
96
+ setupActionButtons();
97
+ setupTrialCancelWarning();
98
+ setupWinbackOffer();
99
+ setupRetentionWarning();
100
+ setupCancellationForm();
101
+ setupUncancelConfirm();
102
+ setupPlanSwitcher();
103
+ }
104
+
105
+ // Load billing data
106
+ export async function loadData(account, sharedPaymentConfig) {
107
+ if (!account) {
108
+ return;
109
+ }
110
+
111
+ paymentConfig = sharedPaymentConfig;
112
+ currentAccount = account;
113
+
114
+ updateUI(account);
115
+ readPlanSwitchRequest();
116
+ }
117
+
118
+ // Called when section is shown
119
+ export function onShow() {
120
+ openRequestedPlanSwitch();
121
+ }
122
+
123
+ // ─── UI Update ──────────────────────────────────────────────
124
+
125
+ // Previewing a billing state is the dev palette's job and only its job (Ian's
126
+ // ruling, [#329]): a seeded persona shows this card a real account, so there is
127
+ // no synthetic preview path beside it to keep honest.
128
+
129
+ function updateUI(account) {
130
+ const state = buildBillingState(account);
131
+
132
+ omega.bindings().update(state);
133
+ syncCancelTriggerToggle(dialogOwed(state));
134
+ updateUsageInfo(account);
135
+ }
136
+
137
+ // Does a dialog stand in front of the questionnaire right now? ALL THREE
138
+ // pre-questionnaire steps ride the same switch: the trial warning (#267), the
139
+ // save offer (#268) and the retention warning ([#341]) each need the
140
+ // declarative collapse toggle off the trigger, and they are mutually exclusive
141
+ // by state.
142
+ //
143
+ // A step only counts if the DIALOG IT OPENS is on the page. The gate works by
144
+ // taking the trigger's toggle away, so a layout that dropped a dialog (a
145
+ // consumer override, a trimmed template) would otherwise be left with a cancel
146
+ // button that opens nothing at all — the one path in this flow that cannot be
147
+ // clicked out of ([#324]).
148
+ function dialogOwed(state) {
149
+ return (state.billing.cancelWarning.show && !!document.getElementById('cancel-trial-warning-modal'))
150
+ || (state.billing.winbackOffer.show && !!document.getElementById('cancel-winback-modal'))
151
+ || (state.billing.cancelRetention.show && !!document.getElementById('cancel-retention-modal'));
152
+ }
153
+
154
+ function buildBillingState(account) {
155
+ const subscription = account?.subscription || {};
156
+ const resolved = omega.auth().resolveSubscription(account);
157
+ const rawStatus = subscription.status;
158
+ const isPaid = subscription.product?.id !== 'basic' && !!subscription.product?.id;
159
+ const displayName = getDisplayName(subscription);
160
+
161
+ // Map raw + resolved state to UI config
162
+ let configKey = 'free';
163
+ if (isPaid) {
164
+ if (rawStatus === 'suspended') configKey = 'suspended';
165
+ else if (rawStatus === 'cancelled') configKey = 'cancelled';
166
+ else if (resolved.active) configKey = 'active';
167
+ }
168
+ const config = STATUS_CONFIG[configKey] || STATUS_CONFIG.free;
169
+
170
+ // Pre-format alert dates
171
+ const cancelTimestamp = subscription.cancellation?.date?.timestampUNIX;
172
+ const cancelDate = formatDate(cancelTimestamp) || 'the end of your billing period';
173
+ const trialEndDate = formatDate(subscription.trial?.expires?.timestampUNIX);
174
+
175
+ // Pre-format billing details. EVERY paid state owes the user its price, its
176
+ // cadence and a date line — the row used to gate on `resolved.active &&
177
+ // hasValidBilling`, so a cancelled or cancelling account, the two states
178
+ // where "when does this end?" IS the question, showed nothing at all
179
+ // ([#236]). Every slot now RENDERS in every paid state: a piece the
180
+ // subscription does not record shows the placeholder (the QA persona whose
181
+ // doc carries neither price nor frequency dropped two of the three slots and
182
+ // left the row looking half-built).
183
+ const nextBillingUnix = subscription.expires?.timestampUNIX;
184
+ const amount = subscription.payment?.price;
185
+ const currency = paymentConfig?.currency || 'USD';
186
+ const frequency = subscription.payment?.frequency;
187
+ const amountText = (typeof amount === 'number' && amount > 0) ? formatCurrency(amount, currency) : '';
188
+ const cadenceText = CADENCE_LABELS[frequency] || frequency || '';
189
+
190
+ // The date line's LABEL is the claim it makes, so only a renewing
191
+ // subscription may say "renews": a scheduled cancellation says how long
192
+ // access lasts, an ended one says when it ended, and a suspended one — where
193
+ // nothing is scheduled while a payment is failing — names the slot without
194
+ // claiming a date for it.
195
+ let dateLabel = 'Next billing';
196
+ let dateValue = '';
197
+ if (configKey === 'cancelled') {
198
+ dateLabel = 'Ended';
199
+ dateValue = formatDate(cancelTimestamp) || formatDate(nextBillingUnix);
200
+ } else if (subscription.cancellation?.pending === true) {
201
+ dateLabel = 'Access until';
202
+ dateValue = formatDate(cancelTimestamp) || formatDate(nextBillingUnix);
203
+ } else if (configKey === 'active') {
204
+ dateLabel = 'Renews';
205
+ dateValue = formatDate(nextBillingUnix);
206
+ }
207
+
208
+ // One home for "is cancelling on the table at all" — the button and the trial
209
+ // warning that gates it both read it (#267)
210
+ const canCancel = isPaid && rawStatus !== 'cancelled' && !resolved.cancelling;
211
+
212
+ // And one home for "this cancel would end a TRIAL", read by the warning dialog
213
+ // AND by the questionnaire copy behind it (#267). Two rules would eventually
214
+ // disagree, and a form contradicting the dialog that opened it is the bug the
215
+ // dialog exists to fix.
216
+ const trialCancel = canCancel && resolved.trialing;
217
+
218
+ // The save offer (#268): pitched to a PAID cancel, before the questionnaire.
219
+ // A trial is never offered a discount on a cycle it has not paid for — its
220
+ // access ends today (#267), so "off your next month" is the wrong sentence —
221
+ // which is what keeps the two gates from ever wanting the same click.
222
+ //
223
+ // The offer itself is the brand's, resolved at BUILD time from omega.json5
224
+ // (`payment.winback`, @omega.js/config's resolveWinbackOffer): the 50%-off
225
+ // default has one home and the browser never applies one of its own.
226
+ //
227
+ // The offer is only pitchable to a subscription the apply route can REACH:
228
+ // it discounts the live subscription at the provider, so it needs the
229
+ // provider and the resource it holds it under. A paid, active subscription
230
+ // can carry neither — granted by an admin, imported, a backfill that never
231
+ // landed — and pitching that account an offer whose only button can 400 is a
232
+ // dead end in front of the cancel it came to make ([#311]).
233
+ const reachable = !!subscription.payment?.provider && !!subscription.payment?.resourceId;
234
+
235
+ const offer = paymentConfig?.winback;
236
+ const offerable = canCancel
237
+ && !trialCancel
238
+ && reachable
239
+ && offer?.enabled === true
240
+ && winbackSupported
241
+ && !winbackClaimed
242
+ // A claim persisted on the account ([#325]): the backend stamps the discount
243
+ // it applied with source 'winback', so a past claimant is not re-pitched in
244
+ // a fresh session. Only that source suppresses the pitch — a checkout code's
245
+ // discount rides the same node with its own source and keeps the offer open.
246
+ && subscription.discount?.source !== 'winback';
247
+
248
+ // The fallback dialog ([#341]): a cancel the other two steps have nothing to
249
+ // say to still gets a word before the questionnaire. Ian's QA found the hole
250
+ // in three states, and every one of them is a real cancel walking into
251
+ // silence: a brand that turned the offer off, a subscription the apply route
252
+ // cannot reach, a customer who already claimed the discount.
253
+ //
254
+ // It is the LAST gate by construction: it reads the other two rather than
255
+ // restating their rules, so exactly one dialog is ever owed and adding a
256
+ // condition to either of them can never leave the click with two.
257
+ const retentionCancel = canCancel && !offerable && !trialCancel;
258
+
259
+ // The discount riding the subscription right now ([#325]). Accepting the save
260
+ // offer applies a real discount at the provider and the card said nothing
261
+ // about it at all, so the saving a customer had just been given was invisible
262
+ // on the one page that exists to explain their billing.
263
+ //
264
+ // The shape is the one the whole payment stack already speaks — a
265
+ // discount-codes validate result (`{ valid, percent | amount, duration }`),
266
+ // the same object the apply route records and hands back — so nothing here
267
+ // learns a second shape, and an invalid one is no discount at all.
268
+ const discount = subscription.discount?.valid === true ? subscription.discount : null;
269
+
270
+ return {
271
+ billing: {
272
+ plan: {
273
+ name: displayName,
274
+ },
275
+ status: {
276
+ label: config.label,
277
+ badgeClass: config.badgeClass,
278
+ dotClass: config.dotClass,
279
+ },
280
+ description: {
281
+ free: !isPaid,
282
+ active: resolved.active,
283
+ suspended: rawStatus === 'suspended',
284
+ cancelled: rawStatus === 'cancelled',
285
+ },
286
+ alerts: {
287
+ suspended: rawStatus === 'suspended',
288
+ cancelling: resolved.cancelling,
289
+ trialing: resolved.trialing,
290
+ cancelDate: cancelDate,
291
+ trialEndDate: trialEndDate,
292
+ trialHasEndDate: !!trialEndDate,
293
+ },
294
+ details: {
295
+ visible: isPaid,
296
+ dateLabel: dateLabel,
297
+ date: dateValue || UNKNOWN_DETAIL,
298
+ dateClass: detailClass(dateValue),
299
+ amount: amountText || UNKNOWN_DETAIL,
300
+ amountClass: detailClass(amountText),
301
+ cadence: cadenceText || UNKNOWN_DETAIL,
302
+ cadenceClass: detailClass(cadenceText),
303
+ },
304
+ // Cancelling a TRIAL ends access immediately (#267), which is the
305
+ // opposite of what the questionnaire's own copy promises — so a trial is
306
+ // told before it answers questions, never after. The gate reads the
307
+ // cancel button's OWN flag rather than restating its rule: a warning that
308
+ // could disagree with the button it guards is the bug twice.
309
+ cancelWarning: {
310
+ show: trialCancel,
311
+ trialEndDate: trialEndDate,
312
+ hasTrialEndDate: !!trialEndDate,
313
+ },
314
+ // What leaving MIGHT cost, for the cancel neither the offer nor the trial
315
+ // warning speaks to ([#341]). The dialog's copy is a retention-policy
316
+ // statement, not a threat, so all this carries is whether it is owed.
317
+ cancelRetention: {
318
+ show: retentionCancel,
319
+ },
320
+ // The questionnaire the warning hands the customer to: its explanation and
321
+ // the checkbox they must tick both state what THIS cancel does, so a trial
322
+ // is never asked to attest to access it will not get.
323
+ cancelForm: {
324
+ trial: trialCancel,
325
+ },
326
+ // The saving already on the subscription (#325): how much comes off, and
327
+ // which bills it comes off. Both are TEXT — a discount announced by a
328
+ // colored pill alone is not announced at all.
329
+ discount: {
330
+ show: !!discount,
331
+ label: discount ? `${discountOffText(discount)} off` : '',
332
+ when: discount ? discountWhenText(discount, subscription.payment?.frequency) : '',
333
+ },
334
+ // The save offer the customer reads before the questionnaire (#268). Its
335
+ // wording is built here, from the brand's own numbers and the cadence the
336
+ // subscription is billed at, so the markup carries no discount at all.
337
+ winbackOffer: {
338
+ show: offerable,
339
+ headline: offerable ? winbackHeadline(offer, subscription.payment?.frequency) : '',
340
+ },
341
+ buttons: {
342
+ upgrade: !isPaid || rawStatus === 'cancelled',
343
+ change: canChangePlan(account),
344
+ manage: isPaid && rawStatus !== 'cancelled',
345
+ cancel: canCancel,
346
+ // Undo reads the SAME raw flag Change does, not `resolved.cancelling`
347
+ // (which is `pending && !trialing`): a TRIALING subscription with a
348
+ // scheduled cancellation is reachable — the provider's own billing
349
+ // portal schedules one — and reading the derived flag left that account
350
+ // with neither button, a dead end on its own billing page.
351
+ uncancel: uncancelSupported && isPaid && rawStatus === 'active' && subscription.cancellation?.pending === true,
352
+ },
353
+ },
354
+ };
355
+ }
356
+
357
+ // Is a plan switch on the table at all? ONE home for the rule, read by the
358
+ // Change button and by the pricing page's preselect alike — a modal that opens
359
+ // on a plan the button would not have offered is the same bug twice.
360
+ //
361
+ // Change is hidden while a cancellation is scheduled: the providers swap the
362
+ // price, never the schedule, so the backend refuses the switch outright
363
+ // (`cancellation-pending`, [#237]). Undo cancellation is the honest button in
364
+ // that state, and this reads the SAME raw flag the backend guard does so the
365
+ // two can never disagree.
366
+ function canChangePlan(account) {
367
+ return planSwitchSupported
368
+ && omega.auth().resolveSubscription(account).active
369
+ && account?.subscription?.cancellation?.pending !== true;
370
+ }
371
+
372
+ // ─── Action Buttons ──────────────────────────────────────────
373
+
374
+ function setupActionButtons() {
375
+ const $upgradeBtn = document.getElementById('upgrade-plan-btn');
376
+ const $changeBtn = document.getElementById('change-plan-btn');
377
+ const $manageBtn = document.getElementById('manage-billing-btn');
378
+
379
+ if ($upgradeBtn) {
380
+ $upgradeBtn.addEventListener('click', () => {
381
+ trackBilling('upgrade_click');
382
+ window.location.href = '/pricing';
383
+ });
384
+ }
385
+
386
+ // The plan switcher modal itself opens declaratively (data-bs-toggle), the
387
+ // same way the signin link modal does — this only records the intent
388
+ if ($changeBtn) {
389
+ $changeBtn.addEventListener('click', () => {
390
+ trackBilling('change_plan_click');
391
+ });
392
+ }
393
+
394
+ if ($manageBtn) {
395
+ $manageBtn.addEventListener('click', () => {
396
+ trackBilling('manage_billing_click');
397
+ openBillingPortal();
398
+ });
399
+ }
400
+ }
401
+
402
+ // ─── Billing Portal ─────────────────────────────────────────
403
+
404
+ async function openBillingPortal() {
405
+ const $manageBtn = document.getElementById('manage-billing-btn');
406
+ const $btnText = $manageBtn?.querySelector('.button-text');
407
+ const originalText = $btnText?.textContent;
408
+
409
+ try {
410
+ // Show loading state
411
+ if ($manageBtn) $manageBtn.disabled = true;
412
+ if ($btnText) $btnText.textContent = 'Opening...';
413
+
414
+ const response = await omega.request(`/omega/payments/portal`, {
415
+ method: 'POST',
416
+ timeout: 15000,
417
+ body: {
418
+ returnUrl: window.location.href,
419
+ },
420
+ });
421
+
422
+ if (response.url) {
423
+ window.open(response.url, '_blank');
424
+ } else {
425
+ throw new Error('No portal URL returned');
426
+ }
427
+ } catch (error) {
428
+ console.error('Failed to open billing portal:', error);
429
+ omega.utilities().showNotification(error.message || 'Failed to open billing portal. Please try again later.', 'danger');
430
+ } finally {
431
+ if ($manageBtn) $manageBtn.disabled = false;
432
+ if ($btnText) $btnText.textContent = originalText;
433
+ }
434
+ }
435
+
436
+ // ─── Undo Cancellation ──────────────────────────────────────
437
+
438
+ function setupUncancelConfirm() {
439
+ const $confirmBtn = document.getElementById('uncancel-confirm-btn');
440
+ if (!$confirmBtn) {
441
+ return;
442
+ }
443
+
444
+ $confirmBtn.addEventListener('click', () => uncancelSubscription($confirmBtn));
445
+ }
446
+
447
+ // Withdraw the scheduled cancellation so the subscription just keeps renewing.
448
+ // Nothing is charged today — the provider resumes the existing term.
449
+ async function uncancelSubscription($confirmBtn) {
450
+ const $btnText = $confirmBtn.querySelector('.button-text');
451
+ const originalText = $btnText?.textContent;
452
+
453
+ trackBilling('uncancel_submit');
454
+
455
+ try {
456
+ // Show loading state
457
+ $confirmBtn.disabled = true;
458
+ if ($btnText) $btnText.textContent = 'Resuming...';
459
+
460
+ await omega.request(`/omega/payments/uncancel`, {
461
+ method: 'POST',
462
+ timeout: 30000,
463
+ body: {
464
+ confirmed: true,
465
+ },
466
+ });
467
+
468
+ logger.log('Cancellation withdrawn:', { productId: currentAccount?.subscription?.product?.id });
469
+
470
+ // The webhook pipeline writes the real state — patch locally so the card
471
+ // stops saying "cancellation scheduled" the moment the route succeeds
472
+ const currentSub = currentAccount?.subscription;
473
+ if (currentSub) {
474
+ currentSub.cancellation = { pending: false };
475
+ updateUI(currentAccount);
476
+ }
477
+
478
+ collapseUncancelConfirm();
479
+
480
+ omega.utilities().showNotification('Your subscription will continue as normal. You were not charged today.', 'success');
481
+ } catch (error) {
482
+ logger.error('Failed to withdraw cancellation:', error);
483
+
484
+ // The provider cannot resume a subscription AT ALL (the backend's
485
+ // capability gate). The button is a dead end for this account, so retire it
486
+ // for the session — the message already points at the billing portal, which
487
+ // is the "Manage billing" button right beside it.
488
+ if (error.properties?.additional?.code === 'not-supported-by-provider') {
489
+ uncancelSupported = false;
490
+ updateUI(currentAccount);
491
+ collapseUncancelConfirm();
492
+ omega.utilities().showNotification(error.message, { type: 'warning', timeout: 8000 });
493
+ } else {
494
+ omega.utilities().showNotification(error.message || 'Failed to resume your subscription. Please try again later.', 'danger');
495
+ }
496
+ } finally {
497
+ $confirmBtn.disabled = false;
498
+ if ($btnText) $btnText.textContent = originalText;
499
+ }
500
+ }
501
+
502
+ function collapseUncancelConfirm() {
503
+ const $confirm = document.getElementById('uncancel-subscription-confirm');
504
+ if (!$confirm) {
505
+ return;
506
+ }
507
+
508
+ const bsCollapse = bootstrap.Collapse.getInstance($confirm);
509
+ if (bsCollapse) bsCollapse.hide();
510
+ }
511
+
512
+ // ─── Plan Switcher ──────────────────────────────────────────
513
+
514
+ function setupPlanSwitcher() {
515
+ const $modal = document.getElementById('change-plan-modal');
516
+ if (!$modal) {
517
+ return;
518
+ }
519
+
520
+ const $cadence = document.getElementById('change-plan-cadence');
521
+ const $options = document.getElementById('change-plan-options');
522
+ const $confirmBtn = document.getElementById('change-plan-confirm-btn');
523
+
524
+ // Rebuild the picker every time it opens — the plan it must exclude, and the
525
+ // cadence it opens on, are whatever the account is on right now
526
+ $modal.addEventListener('show.bs.modal', () => {
527
+ switcherFrequency = defaultCadence();
528
+ renderCadenceToggle($cadence);
529
+ populatePlanOptions($options);
530
+ $confirmBtn.disabled = true;
531
+ });
532
+
533
+ // Flipping the cadence re-prices every card, which rebuilds the radios: the
534
+ // pick does not survive it, so neither does the enabled Confirm button
535
+ $cadence.addEventListener('change', (event) => {
536
+ switcherFrequency = event.target.value;
537
+ populatePlanOptions($options);
538
+ $confirmBtn.disabled = true;
539
+ });
540
+
541
+ // Nothing to confirm until a plan is picked
542
+ $options.addEventListener('change', () => {
543
+ $confirmBtn.disabled = !getSelectedPlan();
544
+ });
545
+
546
+ // The WHOLE card is the target, not just the head: the feature list and the
547
+ // card's padding are most of its area, and they were dead space — worst on
548
+ // touch, where the head is a thin strip. Two things keep their own clicks: a
549
+ // label (the browser already checks the radio for it, and doing it again
550
+ // would fire a second change event) and a feature's hover explanation, whose
551
+ // tooltip a stretched overlay would have swallowed entirely.
552
+ $options.addEventListener('click', (event) => {
553
+ if (event.target.closest('[data-bs-toggle="tooltip"]') || event.target.closest('label')) {
554
+ return;
555
+ }
556
+
557
+ const $card = event.target.closest('.omega-plan-card');
558
+ const $radio = $card?.previousElementSibling;
559
+
560
+ if (!$radio || $radio.disabled || $radio.checked) {
561
+ return;
562
+ }
563
+
564
+ $radio.checked = true;
565
+ $radio.dispatchEvent(new Event('change', { bubbles: true }));
566
+ });
567
+
568
+ $confirmBtn.addEventListener('click', () => changePlan($confirmBtn, $modal));
569
+ }
570
+
571
+ // ─── Arriving from the pricing page ─────────────────────────
572
+
573
+ // The pricing page's "Switch to this plan" button lands here naming the plan it
574
+ // meant: `/dashboard/account?product=<id>&frequency=<cadence>#billing`
575
+ // ([#236]). Reading it is one thing and acting on it is another — the modal
576
+ // cannot open while the billing section is still `d-none` — so the request is
577
+ // read the moment the account loads and honoured when the tab is on screen.
578
+ //
579
+ // The params are consumed ONCE and struck from the URL right here: a refresh
580
+ // (or a back to this page) must not reopen a modal the user already closed.
581
+ function readPlanSwitchRequest() {
582
+ const params = new URLSearchParams(window.location.search || '');
583
+ const productId = params.get('product');
584
+
585
+ if (!productId) {
586
+ return;
587
+ }
588
+
589
+ pendingSwitch = { productId: productId, frequency: params.get('frequency') };
590
+
591
+ params.delete('product');
592
+ params.delete('frequency');
593
+
594
+ const query = params.toString();
595
+ window.history.replaceState({}, '', `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash || ''}`);
596
+ }
597
+
598
+ // Open the switcher on the plan the pricing page named. A state that cannot
599
+ // switch at all lands on the billing tab plainly: while a cancellation is
600
+ // scheduled the Change button is hidden (the backend refuses the switch), and
601
+ // opening the modal anyway would offer a move that cannot happen.
602
+ function openRequestedPlanSwitch() {
603
+ const request = pendingSwitch;
604
+ pendingSwitch = null;
605
+
606
+ const $modal = document.getElementById('change-plan-modal');
607
+ if (!request || !$modal || !canChangePlan(currentAccount)) {
608
+ return;
609
+ }
610
+
611
+ // Bootstrap's show() fires `show.bs.modal`, which is what renders the toggle
612
+ // and the cards — so the picking below happens over the real thing.
613
+ bootstrap.Modal.getOrCreateInstance($modal).show();
614
+
615
+ // The cadence goes FIRST: flipping it re-prices and rebuilds every card, so
616
+ // a plan picked before it would not survive the rebuild.
617
+ if (request.frequency) {
618
+ pickRadio(document.getElementById('change-plan-cadence'), (radio) => radio.value === request.frequency);
619
+ }
620
+
621
+ // Exactly the mechanism a card click uses — check the radio, announce it —
622
+ // so Confirm arms the way it would have by hand.
623
+ pickRadio(document.getElementById('change-plan-options'), (radio) => radio.value === request.productId && !radio.disabled);
624
+ }
625
+
626
+ // Check one radio inside a container and announce it the way a click does. The
627
+ // match is a PREDICATE, never a selector built from the URL.
628
+ function pickRadio($container, matches) {
629
+ const $radio = [...($container?.querySelectorAll('input[type="radio"]') || [])].find(matches);
630
+
631
+ if (!$radio || $radio.checked) {
632
+ return;
633
+ }
634
+
635
+ $radio.checked = true;
636
+ $radio.dispatchEvent(new Event('change', { bubbles: true }));
637
+ }
638
+
639
+ // The plans a live subscription can move to: the brand's subscription
640
+ // products, minus the free tier (leaving a paid plan is CANCELLING, not
641
+ // switching) and minus one-time purchases. The enterprise tier excludes
642
+ // itself — it carries no prices, so it is on sale at no cadence.
643
+ function switchableProducts() {
644
+ return (paymentConfig?.products || [])
645
+ .filter(product => (product.type || 'subscription') === 'subscription' && product.id !== 'basic');
646
+ }
647
+
648
+ // Every cadence the brand actually sells a switchable plan at, shortest first.
649
+ function availableCadences() {
650
+ const sold = new Set(switchableProducts().flatMap(product => getAvailableFrequencies(product)));
651
+
652
+ return FREQUENCIES.filter(frequency => sold.has(frequency));
653
+ }
654
+
655
+ // The cadence the modal opens on: the one the account is billed at, whenever
656
+ // the brand still sells it. A subscription with no recorded frequency opens on
657
+ // monthly, and a brand that doesn't sell monthly opens on its shortest cadence.
658
+ function defaultCadence() {
659
+ const cadences = availableCadences();
660
+ const current = currentAccount?.subscription?.payment?.frequency;
661
+
662
+ if (current && cadences.includes(current)) {
663
+ return current;
664
+ }
665
+
666
+ return cadences.includes('monthly') ? 'monthly' : (cadences[0] || 'monthly');
667
+ }
668
+
669
+ // The one cadence control, in the pricing page's own vocabulary
670
+ // (`omega-billing-toggle`): a segmented group of real radios that re-prices
671
+ // every card. A brand selling a single cadence gets no toggle — there is
672
+ // nothing to toggle between.
673
+ function renderCadenceToggle($container) {
674
+ const cadences = availableCadences();
675
+
676
+ // Nothing rendered means nothing reserved either — an empty container still
677
+ // held its bottom margin, so a single-cadence brand opened on a gap.
678
+ if (cadences.length < 2) {
679
+ $container.innerHTML = '';
680
+ $container.classList.add('d-none');
681
+ return;
682
+ }
683
+
684
+ $container.classList.remove('d-none');
685
+
686
+ const savings = annualSavingsPercent();
687
+
688
+ const segments = cadences.map((frequency) => {
689
+ const id = `change-plan-cadence-${frequency}`;
690
+ const save = (frequency === 'annually' && savings > 0)
691
+ ? ` <span class="omega-chip omega-chip--accent omega-billing-toggle__save ms-1">Save ${savings}%</span>`
692
+ : '';
693
+
694
+ return `
695
+ <input type="radio" class="btn-check" name="change_plan_cadence" id="${id}" value="${omega.utilities().escapeHTML(frequency)}" autocomplete="off"${frequency === switcherFrequency ? ' checked' : ''}>
696
+ <label class="btn" for="${id}">${CADENCE_LABELS[frequency] || frequency}${save}</label>
697
+ `;
698
+ }).join('');
699
+
700
+ // `data-omega-segmented` is the pricing page's own attribute: the motion
701
+ // engine injects the thumb and glides it under the checked segment. The
702
+ // engine's MutationObserver would find this node eventually, but the modal
703
+ // renders and opens in the same tick — scanning the container here is the
704
+ // deterministic path, and adoption is idempotent (`omegaSegmentedReady`
705
+ // makes a second pass a no-op). The thumb measures 0 while the modal is
706
+ // still hidden and the engine's own ResizeObserver re-places it the moment
707
+ // Bootstrap shows the dialog.
708
+ $container.innerHTML = `<div class="omega-billing-toggle" role="group" aria-label="Billing cadence" data-omega-segmented>${segments}</div>`;
709
+
710
+ omega.library?.().motion?.scan($container);
711
+ }
712
+
713
+ // The honest annual discount: the best saving the catalog really offers across
714
+ // the plans on sale, the same arithmetic the pricing page's badge runs
715
+ // (`packages/web/src/pricing.js`). Annual pricing that doesn't beat twelve
716
+ // months of monthly earns no tag.
717
+ function annualSavingsPercent() {
718
+ let best = 0;
719
+
720
+ for (const product of switchableProducts()) {
721
+ const monthly = resolvePlanPrice(product, 'monthly');
722
+ const annually = resolvePlanPrice(product, 'annually');
723
+
724
+ if (monthly <= 0 || annually <= 0) {
725
+ continue;
726
+ }
727
+
728
+ const percent = Math.round((1 - (annually / (monthly * 12))) * 100);
729
+ if (percent > best) {
730
+ best = percent;
731
+ }
732
+ }
733
+
734
+ return best;
735
+ }
736
+
737
+ // ONE card per plan the brand sells at the cadence the toggle is on — the
738
+ // price on the card follows the toggle, so the same plan never occupies two
739
+ // rows ([#236] round 2). The plan the account is already on RENDERS —
740
+ // greyed out, badged "Current plan", not selectable — rather than
741
+ // disappearing: a plan list missing the plan you are on reads as a bug, and
742
+ // dropping it was what let a no-op switch through when the recorded frequency
743
+ // was absent.
744
+ function populatePlanOptions($container) {
745
+ const subscription = currentAccount?.subscription || {};
746
+ const currentProductId = subscription.product?.id;
747
+ const currentFrequency = subscription.payment?.frequency;
748
+ const currency = paymentConfig?.currency || 'USD';
749
+ const frequency = switcherFrequency;
750
+
751
+ const options = switchableProducts()
752
+ .filter(product => getAvailableFrequencies(product).includes(frequency))
753
+ .map(product => ({
754
+ product: product,
755
+ current: isCurrentPlan(product.id, frequency, currentProductId, currentFrequency),
756
+ }));
757
+
758
+ // Bootstrap keeps ONE instance per element and never learns the element is
759
+ // gone: the bullets below are about to be replaced (every open, every
760
+ // cadence flip), so retire their tooltips first or each pass leaks a set.
761
+ disposeTooltips($container);
762
+
763
+ if (options.length === 0) {
764
+ $container.innerHTML = '<div class="text-muted small">There are no other plans to switch to right now.</div>';
765
+ return;
766
+ }
767
+
768
+ const list = options.map(({ product, current }, i) => {
769
+ // The bullets sit OUTSIDE the label (a list is not phrasing content), so the
770
+ // radio points at them with aria-describedby — otherwise a screen reader
771
+ // hears the plan's name and price and never its features.
772
+ const featuresId = `change-plan-features-${i}`;
773
+ const features = renderPlanFeatures(product, featuresId);
774
+ const id = `change-plan-option-${i}`;
775
+
776
+ return `
777
+ <div class="omega-plan-option">
778
+ <input class="btn-check" type="radio" name="change_plan_option" id="${id}" value="${omega.utilities().escapeHTML(product.id)}" data-frequency="${omega.utilities().escapeHTML(frequency)}" autocomplete="off"${current ? ' disabled' : ''}${features ? ` aria-describedby="${featuresId}"` : ''}>
779
+ <div class="omega-plan-card${current ? ' omega-plan-card--current' : ''}">
780
+ <label class="omega-plan-card__head" for="${id}">
781
+ <span class="omega-plan-card__name">${omega.utilities().escapeHTML(product.name || product.id)}</span>
782
+ ${current ? '<span class="omega-chip omega-plan-card__badge">Current plan</span>' : ''}
783
+ <span class="omega-plan-card__price">${omega.utilities().escapeHTML(formatCurrency(resolvePlanPrice(product, frequency), currency))}<span class="omega-plan-card__per"> / ${FREQUENCY_LABELS[frequency] || frequency}</span></span>
784
+ </label>
785
+ ${features}
786
+ </div>
787
+ </div>
788
+ `;
789
+ }).join('');
790
+
791
+ // Every card IS the current plan — a brand that sells a single plan. The
792
+ // card still renders (the plan you are on is a fact worth seeing), with the
793
+ // sentence that explains why nothing here is pickable.
794
+ const nothingToPick = options.every(option => option.current)
795
+ ? '<div class="text-muted small mt-3">There are no other plans to switch to right now.</div>'
796
+ : '';
797
+
798
+ $container.innerHTML = `${list}${nothingToPick}`;
799
+
800
+ // The bullets are freshly injected, so the page-load pass never saw them
801
+ initializeTooltips($container);
802
+ }
803
+
804
+ // Retire the tooltips of a container whose contents are about to be replaced.
805
+ function disposeTooltips($container) {
806
+ $container.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(($el) => {
807
+ bootstrap.Tooltip.getInstance($el)?.dispose();
808
+ });
809
+ }
810
+
811
+ // Is this option the plan the account is on right now? When the subscription
812
+ // carries no recorded frequency, the product id alone decides — deliberately
813
+ // conservative, so BOTH cadences disable instead of offering a switch the
814
+ // backend's same-plan guard would refuse anyway ([#237]).
815
+ function isCurrentPlan(productId, frequency, currentProductId, currentFrequency) {
816
+ if (productId !== currentProductId) {
817
+ return false;
818
+ }
819
+
820
+ return !currentFrequency || frequency === currentFrequency;
821
+ }
822
+
823
+ // A plan's headline features, from the SAME two halves the pricing page composes
824
+ // its cards from — the FEATURES CATALOG (name, icon, definition, row order) and
825
+ // the product's own values map — and presented the way the pricing card presents
826
+ // them: a green check, the value, and the feature's definition on hover behind
827
+ // the same dotted underline. -1 is the catalog's "unlimited" sentinel, and a
828
+ // value of `false` (or absent) means the tier does not include the feature at
829
+ // all ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)).
830
+ //
831
+ // The definition is ALSO spelled out for assistive tech: a tooltip is
832
+ // hover/focus text on an element that is neither, and these bullets are a
833
+ // radio's description rather than a stop of their own.
834
+ function renderPlanFeatures(product, id) {
835
+ const catalog = featureCatalog();
836
+ const values = product.features || {};
837
+ const features = Object.keys(catalog)
838
+ .filter((featureId) => values[featureId] !== undefined && values[featureId] !== false)
839
+ .slice(0, HEADLINE_FEATURE_LIMIT)
840
+ .map((featureId) => ({ id: featureId, ...catalog[featureId] }));
841
+
842
+ if (features.length === 0) {
843
+ return '';
844
+ }
845
+
846
+ const items = features.map((feature) => {
847
+ const value = values[feature.id];
848
+ const definition = feature.definition || null;
849
+ let prefix = '';
850
+
851
+ // A value only prints when it SAYS something. `true` means "included" —
852
+ // which the check already says — and so do `false`, `''` and absent, which
853
+ // used to print themselves literally ("false Priority support"). A number
854
+ // always prints, zero included: "0 Seats" beside a check is the honest
855
+ // reading. Same visible output as the pricing composer's rule.
856
+ if (value === -1) {
857
+ prefix = 'Unlimited ';
858
+ } else if (typeof value === 'number') {
859
+ prefix = `${value.toLocaleString()} `;
860
+ } else if (typeof value === 'string' && value !== '') {
861
+ prefix = `${value} `;
862
+ }
863
+
864
+ const name = omega.utilities().escapeHTML(feature.name);
865
+ const explained = definition
866
+ ? `<span class="text-decoration-underline text-decoration-dotted cursor-help" data-bs-toggle="tooltip" data-bs-title="${omega.utilities().escapeHTML(definition)}">${name}</span><span class="visually-hidden"> &mdash; ${omega.utilities().escapeHTML(definition)}</span>`
867
+ : name;
868
+
869
+ return `<li><span class="omega-feature-check text-success"><i class="fa-solid fa-check fa-sm"></i></span><span>${omega.utilities().escapeHTML(prefix)}${explained}</span></li>`;
870
+ });
871
+
872
+ return `<ul class="omega-plan-card__features list-unstyled mb-0" id="${id}">${items.join('')}</ul>`;
873
+ }
874
+
875
+ // The FEATURES CATALOG — the ONE home of what a feature is called, what icon it
876
+ // wears and what its definition says. A product names only its value, so no copy
877
+ // here can disagree with the pricing page's
878
+ // ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)).
879
+ function featureCatalog() {
880
+ return omega.config?.features || {};
881
+ }
882
+
883
+ function getSelectedPlan() {
884
+ const $selected = document.querySelector('input[name="change_plan_option"]:checked');
885
+ if (!$selected) {
886
+ return null;
887
+ }
888
+
889
+ return { productId: $selected.value, frequency: $selected.dataset.frequency };
890
+ }
891
+
892
+ // Move the live subscription onto another plan. The provider prorates the
893
+ // difference — no cancel-and-resubscribe, no second checkout.
894
+ async function changePlan($confirmBtn, $modal) {
895
+ const selection = getSelectedPlan();
896
+ if (!selection) {
897
+ return;
898
+ }
899
+
900
+ const $btnText = $confirmBtn.querySelector('.button-text');
901
+ const originalText = $btnText?.textContent;
902
+
903
+ trackBilling('change_plan_submit');
904
+
905
+ try {
906
+ // Show loading state
907
+ $confirmBtn.disabled = true;
908
+ if ($btnText) $btnText.textContent = 'Changing...';
909
+
910
+ await omega.request(`/omega/payments/plan`, {
911
+ method: 'POST',
912
+ timeout: 30000,
913
+ body: {
914
+ productId: selection.productId,
915
+ frequency: selection.frequency,
916
+ confirmed: true,
917
+ },
918
+ });
919
+
920
+ logger.log('Plan change requested:', selection);
921
+
922
+ // The webhook pipeline writes the real state — patch locally so the card
923
+ // shows the plan they just picked
924
+ const currentSub = currentAccount?.subscription;
925
+ const product = paymentConfig?.products?.find(candidate => candidate.id === selection.productId);
926
+ if (currentSub && product) {
927
+ currentSub.product = { id: product.id, name: product.name };
928
+ currentSub.payment = {
929
+ ...currentSub.payment,
930
+ frequency: selection.frequency,
931
+ price: resolvePlanPrice(product, selection.frequency),
932
+ };
933
+
934
+ updateUI(currentAccount);
935
+ }
936
+
937
+ hidePlanSwitcher($modal);
938
+
939
+ omega.utilities().showNotification(`You're now on the ${product?.name || selection.productId} plan. Your next invoice reflects the change.`, 'success');
940
+ } catch (error) {
941
+ logger.error('Failed to change plan:', error);
942
+
943
+ // Same capability gate as uncancel: the provider cannot move a live
944
+ // subscription at all, so retire the button for the session and let the
945
+ // message send them to the billing portal
946
+ if (error.properties?.additional?.code === 'not-supported-by-provider') {
947
+ planSwitchSupported = false;
948
+ updateUI(currentAccount);
949
+ hidePlanSwitcher($modal);
950
+ omega.utilities().showNotification(error.message, { type: 'warning', timeout: 8000 });
951
+ } else {
952
+ omega.utilities().showNotification(error.message || 'Failed to change your plan. Please try again later.', 'danger');
953
+ }
954
+ } finally {
955
+ $confirmBtn.disabled = false;
956
+ if ($btnText) $btnText.textContent = originalText;
957
+ }
958
+ }
959
+
960
+ function hidePlanSwitcher($modal) {
961
+ const bsModal = bootstrap.Modal.getInstance($modal);
962
+ if (bsModal) bsModal.hide();
963
+ }
964
+
965
+ // ─── Trial Cancel Warning ───────────────────────────────────
966
+
967
+ // Cancelling a free trial ends access IMMEDIATELY (#267) — the opposite of what
968
+ // the questionnaire's own copy promises — so a trial is told before it answers
969
+ // questions. The "Cancel subscription" link opens the questionnaire
970
+ // declaratively, and that toggle CANNOT be intercepted from the button:
971
+ // Bootstrap's collapse data-api is a delegate on `document`, and its
972
+ // EventHandler passes `isDelegated` straight through as `useCapture`, so it
973
+ // runs on the way DOWN (`document` → button) and has already opened the
974
+ // accordion before any listener on the button — capture or bubble — is
975
+ // reached. Taking `data-bs-toggle` off the button is the only thing that stops
976
+ // it, which is what syncCancelTriggerToggle() does for a cancel that owes a
977
+ // warning; the listener below then owns that button's clicks outright. A paid
978
+ // cancel keeps the attribute, is never intercepted, and behaves exactly as it
979
+ // did.
980
+ function setupTrialCancelWarning() {
981
+ const $trigger = document.getElementById('cancel-subscription-trigger-btn');
982
+ const $modal = document.getElementById('cancel-trial-warning-modal');
983
+
984
+ if (!$trigger || !$modal) {
985
+ return;
986
+ }
987
+
988
+ const $accordion = document.getElementById('cancel-subscription-accordion');
989
+
990
+ $trigger.addEventListener('click', (event) => {
991
+ if (!needsTrialCancelWarning()) {
992
+ return;
993
+ }
994
+
995
+ event.preventDefault();
996
+ event.stopPropagation();
997
+
998
+ // The button is a TOGGLE, and half of a toggle is closing. With the
999
+ // questionnaire already open the warning has been shown and answered, so
1000
+ // this click only shuts it — warning again would leave the accordion with
1001
+ // no way closed at all.
1002
+ if ($accordion?.classList.contains('show')) {
1003
+ bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).hide();
1004
+ return;
1005
+ }
1006
+
1007
+ // Open FIRST, count second: counting is the analytics providers' business
1008
+ // and this dialog is the customer's, so a blocked snippet must never be
1009
+ // able to leave this button doing nothing at all.
1010
+ bootstrap.Modal.getOrCreateInstance($modal).show();
1011
+ trackBilling('cancel_trial_warning_shown');
1012
+ }, true);
1013
+
1014
+ // While the gate holds the trigger, Bootstrap does not count it as one of the
1015
+ // collapse's triggers and stops keeping its expanded state honest. Mirror it
1016
+ // off the collapse itself, which covers every way the questionnaire opens or
1017
+ // shuts — this button, "Keep my plan" inside it, and the auto-collapse after
1018
+ // a cancel goes through.
1019
+ $accordion?.addEventListener('shown.bs.collapse', () => setCancelTriggerExpanded($trigger, true));
1020
+ $accordion?.addEventListener('hidden.bs.collapse', () => setCancelTriggerExpanded($trigger, false));
1021
+
1022
+ document.getElementById('cancel-trial-continue-btn')?.addEventListener('click', () => {
1023
+ bootstrap.Modal.getInstance($modal)?.hide();
1024
+
1025
+ if ($accordion) {
1026
+ bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).show();
1027
+ }
1028
+
1029
+ trackBilling('cancel_trial_warning_continue');
1030
+ });
1031
+
1032
+ document.getElementById('cancel-trial-keep-btn')?.addEventListener('click', () => {
1033
+ trackBilling('cancel_trial_warning_keep');
1034
+ });
1035
+ }
1036
+
1037
+ // The declarative collapse toggle on the "Cancel subscription" button: ON for
1038
+ // every cancel that goes straight to the questionnaire, OFF for one that owes a
1039
+ // warning first (#267). Bootstrap re-reads `[data-bs-toggle="collapse"]` from
1040
+ // the document on every click, so the attribute IS the switch — and it is
1041
+ // driven from the SAME state that decides whether the warning is owed at all,
1042
+ // so the markup and the gate can never disagree.
1043
+ function syncCancelTriggerToggle(warningOwed) {
1044
+ const $trigger = document.getElementById('cancel-subscription-trigger-btn');
1045
+
1046
+ if (!$trigger) {
1047
+ return;
1048
+ }
1049
+
1050
+ if (warningOwed) {
1051
+ $trigger.removeAttribute('data-bs-toggle');
1052
+ } else {
1053
+ $trigger.setAttribute('data-bs-toggle', 'collapse');
1054
+ }
1055
+ }
1056
+
1057
+ // What Bootstrap's own `_addAriaAndCollapsedClass` would do for this trigger,
1058
+ // for the states where it no longer tracks it.
1059
+ function setCancelTriggerExpanded($trigger, isOpen) {
1060
+ $trigger.classList.toggle('collapsed', !isOpen);
1061
+ $trigger.setAttribute('aria-expanded', isOpen);
1062
+ }
1063
+
1064
+ // Does the account on screen owe a warning before the questionnaire? Read off
1065
+ // the SAME state the bindings render, so the gate and the button it guards can
1066
+ // never disagree.
1067
+ function needsTrialCancelWarning() {
1068
+ return buildBillingState(currentAccount).billing.cancelWarning.show === true;
1069
+ }
1070
+
1071
+ // ─── Winback Offer ──────────────────────────────────────────
1072
+
1073
+ // A customer who starts cancelling a PAID subscription is pitched a discount on
1074
+ // the next cycle before they are asked why they are leaving (#268). The system
1075
+ // already read a returning subscriber as a purchase (the `subscription-winback`
1076
+ // transition); this is the offer made while they are still here to keep.
1077
+ //
1078
+ // The gate is #267's, for the same reason: the "Cancel subscription" link opens
1079
+ // the questionnaire through Bootstrap's collapse data-api, which is a delegate
1080
+ // on `document` running in the CAPTURE phase, so the only thing that stops it is
1081
+ // taking `data-bs-toggle` off the button (syncCancelTriggerToggle) — and this
1082
+ // listener then owns that button's clicks. The two gates never contend: a trial
1083
+ // cancel is warned, a paid cancel is offered, and `offersWinback()` reads the
1084
+ // same state the button does.
1085
+ function setupWinbackOffer() {
1086
+ const $trigger = document.getElementById('cancel-subscription-trigger-btn');
1087
+ const $modal = document.getElementById('cancel-winback-modal');
1088
+
1089
+ if (!$trigger || !$modal) {
1090
+ return;
1091
+ }
1092
+
1093
+ const $accordion = document.getElementById('cancel-subscription-accordion');
1094
+
1095
+ $trigger.addEventListener('click', (event) => {
1096
+ if (!offersWinback()) {
1097
+ return;
1098
+ }
1099
+
1100
+ event.preventDefault();
1101
+ event.stopPropagation();
1102
+
1103
+ // Half of a toggle is closing: with the questionnaire already open this
1104
+ // click only shuts it, exactly as the trial gate handles its own.
1105
+ if ($accordion?.classList.contains('show')) {
1106
+ bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).hide();
1107
+ return;
1108
+ }
1109
+
1110
+ // Open FIRST, count second — a blocked analytics snippet must never be able
1111
+ // to leave this button doing nothing ([#283], [#306]).
1112
+ bootstrap.Modal.getOrCreateInstance($modal).show();
1113
+ trackBilling('winback_offer_shown');
1114
+ }, true);
1115
+
1116
+ // Declining answers THIS attempt: the questionnaire opens behind the dialog
1117
+ // and nothing is retired ([#324]). The trigger keeps its gate, so shutting the
1118
+ // questionnaire and coming back to cancel meets the offer again.
1119
+ document.getElementById('cancel-winback-decline-btn')?.addEventListener('click', () => {
1120
+ bootstrap.Modal.getInstance($modal)?.hide();
1121
+
1122
+ if ($accordion) {
1123
+ bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).show();
1124
+ }
1125
+
1126
+ trackBilling('winback_offer_declined');
1127
+ });
1128
+
1129
+ const $acceptBtn = document.getElementById('cancel-winback-accept-btn');
1130
+
1131
+ $acceptBtn?.addEventListener('click', () => acceptWinbackOffer($acceptBtn, $modal, $accordion));
1132
+ }
1133
+
1134
+ // Apply the discount to the live subscription and call the cancel off. The
1135
+ // route talks to the provider and the webhook pipeline writes whatever state
1136
+ // changes — the subscription the customer keeps is the one they already had, so
1137
+ // the only thing patched locally is the discount the route hands back, which is
1138
+ // what the billing card's indicator reads ([#325]).
1139
+ async function acceptWinbackOffer($acceptBtn, $modal, $accordion) {
1140
+ const $btnText = $acceptBtn.querySelector('.button-text');
1141
+ const originalText = $btnText?.textContent;
1142
+
1143
+ try {
1144
+ // Show loading state
1145
+ $acceptBtn.disabled = true;
1146
+ if ($btnText) $btnText.textContent = 'Applying...';
1147
+
1148
+ const response = await omega.request(`/omega/payments/winback`, {
1149
+ method: 'POST',
1150
+ timeout: 30000,
1151
+ body: {
1152
+ confirmed: true,
1153
+ },
1154
+ });
1155
+
1156
+ logger.log('Winback offer accepted:', { productId: currentAccount?.subscription?.product?.id });
1157
+
1158
+ claimWinbackOffer(response?.discount);
1159
+
1160
+ bootstrap.Modal.getInstance($modal)?.hide();
1161
+
1162
+ trackBilling('winback_offer_accepted');
1163
+
1164
+ omega.utilities().showNotification('Your discount is applied to your next bill. Nothing else changes, and you can still cancel any time.', 'success');
1165
+ } catch (error) {
1166
+ logger.error('Failed to apply the winback offer:', error);
1167
+
1168
+ // Same capability gate uncancel and plan-switch ride: the provider cannot
1169
+ // discount a live subscription at all, the claim has nowhere to be
1170
+ // recorded, or it was already claimed once (the offer's memory lives on the
1171
+ // order doc, which the account this card reads never carries, so a past
1172
+ // claimant IS pitched again) — either way the offer is a dead end for this
1173
+ // account, so retire it and open the questionnaire — a customer who came
1174
+ // here to cancel must never be left in a dialog that cannot answer them.
1175
+ //
1176
+ // The route names every refusal ([#311]), and the rest of them are dead
1177
+ // ends for the same reason: the brand turned the offer off since this page
1178
+ // loaded, the subscription is not the state the offer is for (ended,
1179
+ // suspended, trialing, already scheduled to cancel), or it carries no
1180
+ // provider to discount through. Pressing the button again changes none of
1181
+ // them. The one refusal LEFT OFF is `confirmation-required`: that is a
1182
+ // request that went out without its confirmation, which the same button
1183
+ // sending it again is the fix for.
1184
+ const deadEndCodes = [
1185
+ 'not-supported-by-provider',
1186
+ 'offer-not-claimable',
1187
+ 'offer-already-claimed',
1188
+ 'offer-disabled',
1189
+ 'no-active-subscription',
1190
+ 'trial-not-eligible',
1191
+ 'cancellation-pending',
1192
+ 'missing-payment-details',
1193
+ 'unknown-provider',
1194
+ ];
1195
+ if (deadEndCodes.includes(error.properties?.additional?.code)) {
1196
+ winbackSupported = false;
1197
+ updateUI(currentAccount);
1198
+
1199
+ bootstrap.Modal.getInstance($modal)?.hide();
1200
+
1201
+ if ($accordion) {
1202
+ bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).show();
1203
+ }
1204
+
1205
+ omega.utilities().showNotification(error.message, { type: 'warning', timeout: 8000 });
1206
+ } else {
1207
+ // A failure is not a decline: the dialog stays up so the same button can
1208
+ // be pressed again.
1209
+ omega.utilities().showNotification(error.message || 'We could not apply your discount right now. Please try again later.', 'danger');
1210
+ }
1211
+ } finally {
1212
+ $acceptBtn.disabled = false;
1213
+ if ($btnText) $btnText.textContent = originalText;
1214
+ }
1215
+ }
1216
+
1217
+ // The offer has been CLAIMED. The discount is on the live subscription now and
1218
+ // the backend refuses a second claim, so no later cancel attempt is pitched it
1219
+ // again ([#324]). With nothing left to pitch, the trigger keeps its gate: the
1220
+ // next click meets the retention warning ([#341]) instead of the offer.
1221
+ //
1222
+ // The claim's durable home is the backend's (the order doc, and whatever the
1223
+ // webhook pipeline writes onto the account). The discount the route hands back
1224
+ // is patched onto the account this page holds so the card's indicator ([#325])
1225
+ // shows the saving in the same breath the customer earned it, exactly as the
1226
+ // cancel and plan-switch paths patch what they just changed.
1227
+ function claimWinbackOffer(discount) {
1228
+ winbackClaimed = true;
1229
+
1230
+ const currentSub = currentAccount?.subscription;
1231
+ if (currentSub && discount?.valid === true) {
1232
+ // The route's response is the discount shape without its source; the
1233
+ // persisted doc carries source 'winback' (payments/winback/post.js), so the
1234
+ // in-session patch stamps the same ([#325]). The persisted node also
1235
+ // carries a resourceId stamp ([#333]) this patch skips: nothing on the
1236
+ // client reads it, and the next account read carries it.
1237
+ currentSub.discount = { ...discount, source: 'winback' };
1238
+ }
1239
+
1240
+ updateUI(currentAccount);
1241
+ }
1242
+
1243
+ // Is a save offer owed before the questionnaire? Read off the SAME state the
1244
+ // bindings render, so the gate and the dialog can never disagree.
1245
+ function offersWinback() {
1246
+ return buildBillingState(currentAccount).billing.winbackOffer.show === true;
1247
+ }
1248
+
1249
+ // ─── Retention Warning ──────────────────────────────────────
1250
+
1251
+ // The cancel flow's LAST word ([#341]): a cancel that gets neither the save
1252
+ // offer (#268) nor the trial warning (#267) used to reach the questionnaire
1253
+ // with nothing said at all. This dialog is what it owes them: what leaving
1254
+ // might cost, stated as the retention policy states it.
1255
+ //
1256
+ // The gate is #267's and #268's, mechanism for mechanism: Bootstrap's collapse
1257
+ // data-api is a CAPTURE-phase delegate on `document`, so the only thing that
1258
+ // stops it is taking `data-bs-toggle` off the trigger (syncCancelTriggerToggle),
1259
+ // and this listener then owns that button's clicks. The three never contend:
1260
+ // `needsRetentionWarning()` reads the same state the other two do, and that
1261
+ // state only owes this dialog when neither of theirs is owed.
1262
+ function setupRetentionWarning() {
1263
+ const $trigger = document.getElementById('cancel-subscription-trigger-btn');
1264
+ const $modal = document.getElementById('cancel-retention-modal');
1265
+
1266
+ if (!$trigger || !$modal) {
1267
+ return;
1268
+ }
1269
+
1270
+ const $accordion = document.getElementById('cancel-subscription-accordion');
1271
+
1272
+ $trigger.addEventListener('click', (event) => {
1273
+ if (!needsRetentionWarning()) {
1274
+ return;
1275
+ }
1276
+
1277
+ event.preventDefault();
1278
+ event.stopPropagation();
1279
+
1280
+ // Half of a toggle is closing: with the questionnaire already open this
1281
+ // click only shuts it, exactly as the other two gates handle their own.
1282
+ if ($accordion?.classList.contains('show')) {
1283
+ bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).hide();
1284
+ return;
1285
+ }
1286
+
1287
+ // Open FIRST, count second: a blocked analytics snippet must never be able
1288
+ // to leave this button doing nothing ([#283], [#306]).
1289
+ bootstrap.Modal.getOrCreateInstance($modal).show();
1290
+ trackBilling('cancel_retention_warning_shown');
1291
+ }, true);
1292
+
1293
+ document.getElementById('cancel-retention-continue-btn')?.addEventListener('click', () => {
1294
+ bootstrap.Modal.getInstance($modal)?.hide();
1295
+
1296
+ if ($accordion) {
1297
+ bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).show();
1298
+ }
1299
+
1300
+ trackBilling('cancel_retention_warning_continue');
1301
+ });
1302
+
1303
+ document.getElementById('cancel-retention-keep-btn')?.addEventListener('click', () => {
1304
+ trackBilling('cancel_retention_warning_keep');
1305
+ });
1306
+ }
1307
+
1308
+ // Is the retention warning owed before the questionnaire? Read off the SAME
1309
+ // state the bindings render, so the gate and the dialog can never disagree.
1310
+ function needsRetentionWarning() {
1311
+ return buildBillingState(currentAccount).billing.cancelRetention.show === true;
1312
+ }
1313
+
1314
+ // What a discount is WORTH, in the brand's own numbers: a flat amount off in
1315
+ // the brand's currency, or a percentage. ONE home, read by the offer being
1316
+ // pitched (#268) and by the discount already applied (#325) — two spellings of
1317
+ // the same number read as two different discounts.
1318
+ function discountOffText(discount) {
1319
+ return discount.amount > 0
1320
+ ? formatCurrency(discount.amount, paymentConfig?.currency || 'USD')
1321
+ : `${discount.percent}%`;
1322
+ }
1323
+
1324
+ // Which bills an applied discount comes off, in the cadence the subscription is
1325
+ // actually billed at. A permanent cut does not promise a single cycle.
1326
+ function discountWhenText(discount, frequency) {
1327
+ if (discount.duration === 'forever') {
1328
+ return 'Applied to every bill for as long as you stay';
1329
+ }
1330
+
1331
+ return `Applied to your next ${FREQUENCY_LABELS[frequency] || 'bill'}`;
1332
+ }
1333
+
1334
+ // What the offer SAYS: the brand's own number and the cadence the subscription
1335
+ // is billed at, in the dialog's promotional voice (#323) — short, forward, and
1336
+ // the whole pitch in one line.
1337
+ function winbackHeadline(offer, frequency) {
1338
+ const off = discountOffText(offer);
1339
+
1340
+ if (offer.duration === 'forever') {
1341
+ return `Take ${off} off for as long as you stay`;
1342
+ }
1343
+
1344
+ return `Take ${off} off your next ${FREQUENCY_LABELS[frequency] || 'cycle'}`;
1345
+ }
1346
+
1347
+ // ─── Cancellation Form ──────────────────────────────────────
1348
+
1349
+ function setupCancellationForm() {
1350
+ const $form = document.getElementById('cancel-subscription-form');
1351
+ if (!$form) {
1352
+ return;
1353
+ }
1354
+
1355
+ // Populate randomized reasons
1356
+ populateCancelReasons();
1357
+
1358
+ cancelFormManager = new FormManager('#cancel-subscription-form', {
1359
+ allowResubmit: false,
1360
+ warnOnUnsavedChanges: false,
1361
+ submittingText: 'Cancelling...',
1362
+ submittedText: 'Subscription Cancelled',
1363
+ });
1364
+
1365
+ cancelFormManager.on('submit', async ({ data }) => {
1366
+ // Get selected reason
1367
+ const $selectedReason = document.querySelector('input[name="cancel_reason"]:checked');
1368
+ const reason = $selectedReason?.value || '';
1369
+
1370
+ // Capture state BEFORE the API call — the auth listener may update currentAccount
1371
+ // with Firestore data (cancellation.pending=true) before we reach the post-cancel code
1372
+ const resolvedBeforeCancel = omega.auth().resolveSubscription(currentAccount);
1373
+ const isTrialCancel = resolvedBeforeCancel.trialing;
1374
+
1375
+ logger.log('Cancelling:', { plan: resolvedBeforeCancel.plan, isTrialCancel });
1376
+
1377
+ trackBilling('cancel_submit');
1378
+
1379
+ const response = await omega.request(`/omega/payments/cancel`, {
1380
+ method: 'POST',
1381
+ timeout: 30000,
1382
+ body: {
1383
+ reason: reason,
1384
+ feedback: data.feedback || '',
1385
+ confirmed: true,
1386
+ },
1387
+ });
1388
+
1389
+ if (response.error) {
1390
+ throw new Error(response.message || 'Failed to cancel subscription. Please try again.');
1391
+ }
1392
+
1393
+ logger.log('Cancel complete:', { isTrialCancel, productId: currentAccount?.subscription?.product?.id });
1394
+
1395
+ if (isTrialCancel) {
1396
+ cancelFormManager.showSuccess('Your trial has been cancelled. You\'ve been moved to the free plan. You can subscribe again anytime.');
1397
+ } else {
1398
+ cancelFormManager.showSuccess('Your subscription has been cancelled. You\'ll continue to have access until the end of your current billing period.');
1399
+ }
1400
+
1401
+ // Collapse the cancel form after a short delay
1402
+ setTimeout(() => {
1403
+ const $accordion = document.getElementById('cancel-subscription-accordion');
1404
+ if ($accordion) {
1405
+ const bsCollapse = bootstrap.Collapse.getInstance($accordion);
1406
+ if (bsCollapse) bsCollapse.hide();
1407
+ }
1408
+ }, 1000);
1409
+
1410
+ // Update the UI to reflect cancellation
1411
+ // Re-read currentAccount.subscription since the auth listener may have replaced it
1412
+ const currentSub = currentAccount?.subscription;
1413
+ if (currentSub) {
1414
+ if (isTrialCancel) {
1415
+ // Trial cancellations are immediate — downgrade to basic
1416
+ currentSub.status = 'cancelled';
1417
+ currentSub.product = { id: 'basic', name: 'Basic' };
1418
+ currentSub.cancellation = {
1419
+ pending: false,
1420
+ date: {
1421
+ timestamp: new Date().toISOString(),
1422
+ timestampUNIX: Math.floor(Date.now() / 1000),
1423
+ },
1424
+ };
1425
+ } else {
1426
+ // Non-trial cancellations are pending until end of billing period
1427
+ const expiresUnix = currentSub.expires?.timestampUNIX || 0;
1428
+ currentSub.cancellation = {
1429
+ pending: true,
1430
+ date: {
1431
+ timestamp: new Date(expiresUnix * 1000).toISOString(),
1432
+ timestampUNIX: expiresUnix,
1433
+ },
1434
+ };
1435
+ }
1436
+
1437
+ logger.log('UI update after cancel:', { status: currentSub.status, productId: currentSub.product?.id, cancellationPending: currentSub.cancellation?.pending });
1438
+
1439
+ updateUI(currentAccount);
1440
+ }
1441
+ });
1442
+ }
1443
+
1444
+ function populateCancelReasons() {
1445
+ const $container = document.getElementById('cancel-reasons-container');
1446
+ if (!$container) {
1447
+ return;
1448
+ }
1449
+
1450
+ const reasons = [...CANCEL_REASONS];
1451
+ const other = reasons.pop(); // Remove 'Other' from the end
1452
+ const shuffled = [...shuffleArray(reasons), other]; // Shuffle rest, append 'Other' last
1453
+
1454
+ $container.innerHTML = shuffled.map((reason, i) => `
1455
+ <div class="form-check mb-2">
1456
+ <input class="form-check-input" type="radio" name="cancel_reason" id="cancel-reason-${i}" value="${omega.utilities().escapeHTML(reason)}">
1457
+ <label class="form-check-label" for="cancel-reason-${i}">${omega.utilities().escapeHTML(reason)}</label>
1458
+ </div>
1459
+ `).join('');
1460
+ }
1461
+
1462
+ // ─── Usage Metrics ───────────────────────────────────────────
1463
+
1464
+ // A bar per COUNTED feature the catalog defines: what is left today, what is
1465
+ // left this month, and the override credits included in the month's number when
1466
+ // an admin granted any ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)).
1467
+ //
1468
+ // Both numbers are shown because either one can be the thing standing in the
1469
+ // user's way: a paced feature refuses on the day's share long before the month
1470
+ // runs out, and a bar that only drew the month would leave "why did that fail?"
1471
+ // unanswered. The arithmetic is @omega.js/account's, the same module the
1472
+ // backend's gate reads, so a bar can never draw a limit the gate would not
1473
+ // enforce. Names, icons and definitions come from the catalog.
1474
+ function updateUsageInfo(account) {
1475
+ const $container = document.getElementById('usage-metrics-container');
1476
+
1477
+ if (!$container) {
1478
+ return;
1479
+ }
1480
+
1481
+ // The EFFECTIVE plan (basic if cancelled/suspended) is the one whose numbers apply
1482
+ const resolved = omega.auth().resolveSubscription(account);
1483
+ const product = paymentConfig?.products?.find(p => p.id === resolved.plan);
1484
+ const catalog = featureCatalog();
1485
+
1486
+ const features = resolveFeatures({ catalog, product: product || {}, account })
1487
+ .filter((feature) => feature.counted && feature.limit !== 0);
1488
+
1489
+ // Clear container
1490
+ $container.innerHTML = '';
1491
+
1492
+ if (features.length === 0) {
1493
+ $container.innerHTML = '<div class="text-muted">Usage tracking not available for this plan.</div>';
1494
+ return;
1495
+ }
1496
+
1497
+ $container.innerHTML = features.map(renderUsageFeature).join('');
1498
+
1499
+ initializeTooltips($container);
1500
+ }
1501
+
1502
+ // One feature's block: its name (with the catalog's definition on hover), the
1503
+ // day bar when the feature is paced, and the month bar always.
1504
+ function renderUsageFeature(feature) {
1505
+ const escape = omega.utilities().escapeHTML;
1506
+ const icon = feature.icon
1507
+ ? `<i class="fa-solid fa-${escape(feature.icon)} fa-sm me-1 text-muted"></i>`
1508
+ : '';
1509
+ const name = feature.definition
1510
+ ? `<span class="text-decoration-underline text-decoration-dotted cursor-help" data-bs-toggle="tooltip" data-bs-title="${escape(feature.definition)}">${escape(feature.name)}</span>`
1511
+ + `<span class="visually-hidden"> &mdash; ${escape(feature.definition)}</span>`
1512
+ : escape(feature.name);
1513
+
1514
+ // The override is EXTRA credits an admin granted ON TOP of the plan — say so,
1515
+ // because the number beside it is not the number the pricing page quotes.
1516
+ // Only when it is genuinely extra: an override can also dial a plan DOWN for
1517
+ // one account, and subtracting unconditionally announced that as "+-50
1518
+ // granted". A lower override is still the limit the bar draws; it just has no
1519
+ // gift to claim.
1520
+ const extra = feature.override !== null ? feature.override - feature.planLimit : 0;
1521
+ const granted = extra > 0
1522
+ ? `<span class="badge text-bg-success-subtle ms-2">+${escape(formatMetricValue(feature.id, extra))} granted</span>`
1523
+ : '';
1524
+
1525
+ const bars = [
1526
+ feature.day.limit >= 0 ? renderUsageBar(feature, feature.day.limit, feature.day.used, feature.day.left, 'today') : '',
1527
+ renderUsageBar(feature, feature.limit, feature.used, feature.left, 'this month'),
1528
+ ].join('');
1529
+
1530
+ return `
1531
+ <div class="mb-3">
1532
+ <div class="d-flex justify-content-between align-items-center mb-1">
1533
+ <small class="text-muted fw-semibold">${icon}${name}${granted}</small>
1534
+ </div>
1535
+ ${bars}
1536
+ </div>
1537
+ `;
1538
+ }
1539
+
1540
+ // One bar. `limit` of -1 is unlimited: no percentage can be honest about a
1541
+ // number with no ceiling, so the bar stays empty and the label says so.
1542
+ function renderUsageBar(feature, limit, used, left, period) {
1543
+ const escape = omega.utilities().escapeHTML;
1544
+ const isUnlimited = limit < 0;
1545
+ const usagePercent = isUnlimited ? 0 : Math.min(100, Math.round((used / limit) * 100));
1546
+
1547
+ let progressClass = 'bg-success';
1548
+ if (!isUnlimited && usagePercent >= 80) {
1549
+ progressClass = 'bg-danger';
1550
+ } else if (!isUnlimited && usagePercent >= 50) {
1551
+ progressClass = 'bg-warning';
1552
+ }
1553
+
1554
+ const label = isUnlimited
1555
+ ? `Unlimited ${period}`
1556
+ : `${formatMetricValue(feature.id, left)} of ${formatMetricValue(feature.id, limit)} left ${period}`;
1557
+
1558
+ return `
1559
+ <div class="d-flex justify-content-between align-items-center mb-1">
1560
+ <small class="text-muted">${escape(period)}</small>
1561
+ <small class="text-muted">${escape(label)}</small>
1562
+ </div>
1563
+ <div class="progress mb-2" style="height: 20px;">
1564
+ <div class="progress-bar ${progressClass}" role="progressbar"
1565
+ style="width: ${usagePercent}%"
1566
+ aria-valuenow="${usagePercent}"
1567
+ aria-valuemin="0"
1568
+ aria-valuemax="100"
1569
+ aria-label="${escape(`${feature.name} used ${period}`)}">
1570
+ ${usagePercent}%
1571
+ </div>
1572
+ </div>
1573
+ `;
1574
+ }
1575
+
1576
+ // ─── Helpers ─────────────────────────────────────────────────
1577
+
1578
+ function getDisplayName(subscription) {
1579
+ // Use backend-provided product name first
1580
+ if (subscription.product?.name && subscription.product.name !== 'Basic') {
1581
+ return subscription.product.name;
1582
+ }
1583
+
1584
+ // Fall back to config product name
1585
+ const productId = subscription.product?.id || 'basic';
1586
+ const product = paymentConfig?.products?.find(p => p.id === productId);
1587
+ return product?.name || 'Free';
1588
+ }
1589
+
1590
+ // A plan's price at one frequency (a price entry is either `{ amount: N }` or
1591
+ // a plain `N`, the same two shapes checkout resolves)
1592
+ function resolvePlanPrice(product, frequency) {
1593
+ const entry = product.prices?.[frequency];
1594
+ if (entry == null) {
1595
+ return 0;
1596
+ }
1597
+
1598
+ return typeof entry === 'object' ? (entry.amount || 0) : Number(entry) || 0;
1599
+ }
1600
+
1601
+ // How one details slot is drawn: a known value at full ink, an unknown one in
1602
+ // the muted placeholder's own class.
1603
+ function detailClass(value) {
1604
+ return value ? DETAIL_CLASS : UNKNOWN_DETAIL_CLASS;
1605
+ }
1606
+
1607
+ // A UNIX timestamp as the local short date, or '' when there is no date to
1608
+ // show — the caller decides what an absent date means for its own line.
1609
+ function formatDate(timestampUNIX) {
1610
+ return (timestampUNIX && timestampUNIX > 0)
1611
+ ? new Date(timestampUNIX * 1000).toLocaleDateString()
1612
+ : '';
1613
+ }
1614
+
1615
+ function formatCurrency(amount, currency) {
1616
+ return new Intl.NumberFormat('en-US', {
1617
+ style: 'currency',
1618
+ currency: (currency || 'USD').toUpperCase(),
1619
+ }).format(amount); // amount is already in display dollars
1620
+ }
1621
+
1622
+ // A feature's value in the units it is counted in. The NAME is never derived
1623
+ // here any more — the catalog holds it (#647), so a brand that renames a
1624
+ // feature renames it in one place.
1625
+ function formatMetricValue(metricId, value) {
1626
+ if (metricId === 'storage' || metricId === 'bandwidth') {
1627
+ return formatBytes(value);
1628
+ }
1629
+ return value.toLocaleString();
1630
+ }
1631
+
1632
+ function formatBytes(bytes, decimals = 2) {
1633
+ if (bytes === 0) {
1634
+ return '0 Bytes';
1635
+ }
1636
+
1637
+ const k = 1024;
1638
+ const dm = decimals < 0 ? 0 : decimals;
1639
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
1640
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1641
+
1642
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
1643
+ }
1644
+
1645
+ function shuffleArray(arr) {
1646
+ for (let i = arr.length - 1; i > 0; i--) {
1647
+ const j = Math.floor(Math.random() * (i + 1));
1648
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1649
+ }
1650
+ return arr;
1651
+ }
1652
+
1653
+ // ─── Tracking ────────────────────────────────────────────────
1654
+
1655
+ // Counting a billing action may never COST the customer that action ([#283]).
1656
+ // An ad blocker does not stub these snippets, it stops them loading, so the
1657
+ // names are simply never defined — and every caller here counts before it acts,
1658
+ // so one ReferenceError turned "Undo cancellation" into a dead button. The
1659
+ // guard this card was given first is now the ONE canonical call ([#328]): the
1660
+ // catalog decides who hears `user_billing_action`, and the transport under it asks
1661
+ // for each provider on its own — blockers work per list, so a page with Google
1662
+ // allowed and Meta blocked still counts what it can.
1663
+ function trackBilling(action) {
1664
+ event('user_billing_action', {
1665
+ action: action,
1666
+ });
1667
+ }