@hot-updater/console 1.0.0-rc.4 → 1.0.0-rc.41

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 (330) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/ConsoleFeatureUnavailable-DRzeMGRM.js +1 -0
  3. package/.output/public/assets/EventDetails-SB5ygJzS.js +1 -0
  4. package/.output/public/assets/HashValueDisplay-BFX_kY3-.js +1 -0
  5. package/.output/public/assets/InsightsInfo-btYppp0B.js +1 -0
  6. package/.output/public/assets/InsightsPageHeader-Ba8psD8t.js +1 -0
  7. package/.output/public/assets/NotFoundPage-CyRG2eca.js +1 -0
  8. package/.output/public/assets/alert-dialog-Bbk_4-WJ.js +1 -0
  9. package/.output/public/assets/api-DUgI6yOI.js +1 -0
  10. package/.output/public/assets/api-keys-CIO9L_wu.js +1 -0
  11. package/.output/public/assets/api-keys-CmkNKOhS.js +1 -0
  12. package/.output/public/assets/arrow-left-CnMYpqB7.js +1 -0
  13. package/.output/public/assets/arrow-up-right-BtffZA6e.js +1 -0
  14. package/.output/public/assets/chevron-right-BqD5wtFb.js +1 -0
  15. package/.output/public/assets/clipboard-DS9ucUQL.js +1 -0
  16. package/.output/public/assets/console-features-api-BMsI3Bj8.js +1 -0
  17. package/.output/public/assets/dialog-Bqbb7B0N.js +1 -0
  18. package/.output/public/assets/dist-LxKJB-Vu.js +1 -0
  19. package/.output/public/assets/empty-CuT2UhFp.js +1 -0
  20. package/.output/public/assets/field-BBDpjIqu.js +1 -0
  21. package/.output/public/assets/index-BEfQ2_7f.js +11 -0
  22. package/.output/public/assets/input-group-ja_30qaF.js +1 -0
  23. package/.output/public/assets/insights-B0ib9qOE.js +2 -0
  24. package/.output/public/assets/insights-BIMJx-0p.js +1 -0
  25. package/.output/public/assets/insights-CndqcWEj.js +42 -0
  26. package/.output/public/assets/insights-api-B6Ewc-OG.js +1 -0
  27. package/.output/public/assets/insights-usage-rpc-DYEhsCKT.js +1 -0
  28. package/.output/public/assets/insights_.distribution-B26PMy4k.js +2 -0
  29. package/.output/public/assets/insights_.distribution-BBDtqqYS.js +1 -0
  30. package/.output/public/assets/insights_.distribution-DhHmYS8k.js +1 -0
  31. package/.output/public/assets/installations-C3cMuwRx.js +1 -0
  32. package/.output/public/assets/installations-C3yrjwDh.js +2 -0
  33. package/.output/public/assets/installations-D3edlN75.js +1 -0
  34. package/.output/public/assets/key-round-wS_SIZ6b.js +1 -0
  35. package/.output/public/assets/list-CgrYiadM.js +1 -0
  36. package/.output/public/assets/refresh-cw-BYgoNCco.js +1 -0
  37. package/.output/public/assets/remote-config-0i-gDkeU.js +1 -0
  38. package/.output/public/assets/remote-config-An1ROIOQ.js +6 -0
  39. package/.output/public/assets/remote-config-CoeWgA9i.js +1 -0
  40. package/.output/public/assets/routes-DNoaJIQV.js +2 -0
  41. package/.output/public/assets/routes-cWBDtghZ.js +1 -0
  42. package/.output/public/assets/search-DaTDPW25.js +1 -0
  43. package/.output/public/assets/select-CZMQKT7F.js +1 -0
  44. package/.output/public/assets/sidebar-DNVyJPfX.js +3 -0
  45. package/.output/public/assets/smartphone-DpTFgtKf.js +1 -0
  46. package/.output/public/assets/styles-D8mxMvCB.css +2 -0
  47. package/.output/public/assets/switch-CU4-fupc.js +1 -0
  48. package/.output/public/assets/table-Gm4qRWwE.js +1 -0
  49. package/.output/public/assets/tabs-R1I8CRjR.js +1 -0
  50. package/.output/public/assets/undo-2-CmrHjwFc.js +1 -0
  51. package/.output/public/assets/useCompositeListItem-BaPeC9ZO.js +1 -0
  52. package/.output/public/assets/valueToPercent-CH8GCHaL.js +1 -0
  53. package/.output/server/__23tanstack-start-server-fn-resolver-DTAqlLRd.mjs +178 -0
  54. package/.output/server/_libs/@base-ui/react+[...].mjs +6450 -2741
  55. package/.output/server/_libs/@tanstack/react-router+[...].mjs +3802 -4022
  56. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +140 -97
  57. package/.output/server/_libs/lucide-react.mjs +235 -31
  58. package/.output/server/_libs/next-themes.mjs +1 -1
  59. package/.output/server/_libs/react-redux+[...].mjs +1 -1
  60. package/.output/server/_libs/recharts+[...].mjs +483 -901
  61. package/.output/server/_libs/sonner.mjs +1 -1
  62. package/.output/server/_libs/tanstack__history.mjs +68 -41
  63. package/.output/server/_libs/tanstack__query-core.mjs +2010 -309
  64. package/.output/server/_libs/tanstack__react-query.mjs +448 -21
  65. package/.output/server/_libs/verkit.mjs +102 -1
  66. package/.output/server/_ssr/BundleIdDisplay-BNoVW3Dv.mjs +13 -0
  67. package/.output/server/_ssr/ConsoleFeatureUnavailable-BPFa-cmJ.mjs +58 -0
  68. package/.output/server/_ssr/EventDetails-9CeshfdR.mjs +344 -0
  69. package/.output/server/_ssr/HashValueDisplay-DXZlkcps.mjs +55 -0
  70. package/.output/server/_ssr/InsightsInfo-DLNdJQ0m.mjs +75 -0
  71. package/.output/server/_ssr/InsightsPageHeader-DZGVDgvv.mjs +145 -0
  72. package/.output/server/_ssr/NotFoundPage-mYHHg-2s.mjs +37 -0
  73. package/.output/server/_ssr/{dialog-DUaIgpgd.mjs → alert-dialog-Chkim7vZ.mjs} +6 -83
  74. package/.output/server/_ssr/api-C-Il5-sa.mjs +203 -0
  75. package/.output/server/_ssr/api-keys-01FGqj4d.mjs +5 -0
  76. package/.output/server/_ssr/{api-keys-DJml_XoK.mjs → api-keys-B_f9h7ln.mjs} +48 -10
  77. package/.output/server/_ssr/api-keys-rpc-Cf4YCyFc.mjs +52 -0
  78. package/.output/server/_ssr/api-rpc-DRqwxxT_.mjs +296 -0
  79. package/.output/server/_ssr/{auth-BF8mbAgF.mjs → auth-UP0wXs4i.mjs} +1 -1
  80. package/.output/server/_ssr/{auth-rpc-D_hv52-J.mjs → auth-rpc-D8nFpU9C.mjs} +6 -6
  81. package/.output/server/_ssr/auth.server-COZoXu4i.mjs +5425 -0
  82. package/.output/server/_ssr/bundleActivity-M5gzBFNg.mjs +18 -0
  83. package/.output/server/_ssr/config-WVX4IxSR.mjs +15 -0
  84. package/.output/server/_ssr/config.server-Dm2l7w0O.mjs +36 -0
  85. package/.output/server/_ssr/console-access--o9FXikU.mjs +17 -0
  86. package/.output/server/_ssr/console-features-BftSkf3f.mjs +60 -0
  87. package/.output/server/_ssr/console-features-api-CYclUQ4h.mjs +43 -0
  88. package/.output/server/_ssr/console-features-rpc-1WaAkPM_.mjs +19 -0
  89. package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-Cm6Tn0_Y.mjs} +67 -56
  90. package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-RLR4H-Fn.mjs} +2 -2
  91. package/.output/server/_ssr/databaseConfig-C5SkAyUy.mjs +5 -0
  92. package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Csbuzc9Q.mjs} +28 -5
  93. package/.output/server/_ssr/databaseRows-BUuNU-qt.mjs +74 -0
  94. package/.output/server/_ssr/dialog-mfSANqjC.mjs +84 -0
  95. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-wmnTk7w6.mjs} +140 -6
  96. package/.output/server/_ssr/{downloadBundle-B3ert-pY.mjs → downloadBundle-DViuIa9U.mjs} +10 -10
  97. package/.output/server/_ssr/empty-BPayFad3.mjs +60 -0
  98. package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CPtwX8a7.mjs} +1 -1
  99. package/.output/server/_ssr/failureReports-DKDhVsXA.mjs +33 -0
  100. package/.output/server/_ssr/field-DjZ5t7rH.mjs +189 -0
  101. package/.output/server/_ssr/getBundleChildren-CmjDg6Re.mjs +22 -0
  102. package/.output/server/_ssr/{input-group-Bb8ueylQ.mjs → input-group-Dx-MEkNM.mjs} +3 -52
  103. package/.output/server/_ssr/insights-BopxyJjk.mjs +14 -0
  104. package/.output/server/_ssr/insights-Bvm03d61.mjs +5 -0
  105. package/.output/server/_ssr/insights-CrLAHSDy.mjs +2315 -0
  106. package/.output/server/_ssr/insights-api-Bg8ZV_h_.mjs +122 -0
  107. package/.output/server/_ssr/insights-errors-BjnFUyZj.mjs +34 -0
  108. package/.output/server/_ssr/insights-failures-rpc-tyy5m9Cc.mjs +36 -0
  109. package/.output/server/_ssr/insights-failures-xaa41Buf.mjs +46 -0
  110. package/.output/server/_ssr/{insights-recovery-DSFM-YTL.mjs → insights-recovery-BWQhiZaF.mjs} +9 -2
  111. package/.output/server/_ssr/insights-recovery-rpc-CPJOiJAV.mjs +32 -0
  112. package/.output/server/_ssr/{insights-rpc-DrIQCjpD.mjs → insights-rpc-DoC3hla9.mjs} +22 -14
  113. package/.output/server/_ssr/insights-usage-BpBM0U0c.mjs +29 -0
  114. package/.output/server/_ssr/insights-usage-rpc-CFuTYp3T.mjs +29 -0
  115. package/.output/server/_ssr/insights-usage-rpc-O2ruP4iE.mjs +41 -0
  116. package/.output/server/_ssr/insightsUsage-CzhVjsc8.mjs +56 -0
  117. package/.output/server/_ssr/insights_.distribution-B3BVPD00.mjs +16 -0
  118. package/.output/server/_ssr/insights_.distribution-BMoisxRG.mjs +347 -0
  119. package/.output/server/_ssr/insights_.distribution-BiCcFyrG.mjs +7 -0
  120. package/.output/server/_ssr/{installations-YumqpNpa.mjs → installations-Cx3jIrwl.mjs} +280 -364
  121. package/.output/server/_ssr/installations-DTGPaKXx.mjs +69 -0
  122. package/.output/server/_ssr/installations-osiAuNKr.mjs +5 -0
  123. package/.output/server/_ssr/listReleases-BfUBlCHo.mjs +79 -0
  124. package/.output/server/_ssr/release-adoption-005tQRpo.mjs +55 -0
  125. package/.output/server/_ssr/release-adoption-rpc-BVySDR0L.mjs +41 -0
  126. package/.output/server/_ssr/releaseAdoption-2PJPA09N.mjs +114 -0
  127. package/.output/server/_ssr/releaseManagement-BTCWEY8-.mjs +11 -0
  128. package/.output/server/_ssr/remote-config-CUoAxUMy.mjs +430 -0
  129. package/.output/server/_ssr/remote-config-Cc5P4J-r.mjs +17 -0
  130. package/.output/server/_ssr/remote-config-CjdxcZ9B.mjs +5 -0
  131. package/.output/server/_ssr/remote-config-D2GmbGwI.mjs +2594 -0
  132. package/.output/server/_ssr/remote-config-rpc-kjoZ9PRS.mjs +115 -0
  133. package/.output/server/_ssr/rolldown-runtime-rSIU-vHC.mjs +13 -0
  134. package/.output/server/_ssr/{router-iP95Vqi2.mjs → router-DwGVewL-.mjs} +141 -139
  135. package/.output/server/_ssr/{routes-ivsV2TDu.mjs → routes-C4yxv3JU.mjs} +192 -685
  136. package/.output/server/_ssr/routes-D3_aegLY.mjs +409 -0
  137. package/.output/server/_ssr/runtime.server-BoWf4QhU.mjs +1716 -0
  138. package/.output/server/_ssr/schema-DrOtkRVD.mjs +361 -0
  139. package/.output/server/_ssr/select-DkzM2miy.mjs +107 -0
  140. package/.output/server/_ssr/server-BW0gzB0p.mjs +8325 -0
  141. package/.output/server/_ssr/{createSsrRpc-NHF2PxuK.mjs → sidebar-DPcOijvL.mjs} +107 -47
  142. package/.output/server/_ssr/ssr.mjs +1 -1
  143. package/.output/server/_ssr/start-BfFbHKMK.mjs +4 -0
  144. package/.output/server/_ssr/switch-BqJ2mAwC.mjs +17 -0
  145. package/.output/server/_ssr/{table-D1mL8fJu.mjs → table-R7RY92q9.mjs} +2 -2
  146. package/.output/server/_ssr/tabs-trDcbRkT.mjs +45 -0
  147. package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +40 -0
  148. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-ctA_XVZA.mjs} +2 -2
  149. package/.output/server/_tanstack-start-manifest_v-BwInaUTw.mjs +139 -0
  150. package/.output/server/index.mjs +315 -154
  151. package/LICENSE +13 -14
  152. package/README.md +82 -72
  153. package/lib/index.d.mts +23 -11
  154. package/lib/vite-internal.mjs +13 -3
  155. package/lib/vite.d.mts +2 -0
  156. package/package.json +14 -14
  157. package/src/components/AppSidebar.tsx +100 -71
  158. package/src/components/ConsoleAccessPage.tsx +5 -1
  159. package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
  160. package/src/components/NotFoundPage.tsx +7 -7
  161. package/src/components/features/FeatureSlots.tsx +103 -0
  162. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  163. package/src/components/features/bundles/BundleMetadata.tsx +8 -5
  164. package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
  165. package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
  166. package/src/components/features/insights/AppUsage.tsx +366 -0
  167. package/src/components/features/insights/BuiltinBundle.tsx +36 -0
  168. package/src/components/features/insights/ComparedBundlesChart.tsx +310 -0
  169. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  170. package/src/components/features/insights/EventDetails.tsx +400 -39
  171. package/src/components/features/insights/EventHistoryCard.tsx +112 -26
  172. package/src/components/features/insights/EventHistoryList.tsx +2 -2
  173. package/src/components/features/insights/FailureReports.tsx +407 -0
  174. package/src/components/features/insights/InsightsControls.tsx +265 -88
  175. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  176. package/src/components/features/insights/InsightsInfo.tsx +48 -0
  177. package/src/components/features/insights/InsightsOverview.tsx +604 -355
  178. package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
  179. package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
  180. package/src/components/features/insights/InstallationHistoryCard.tsx +66 -11
  181. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  182. package/src/components/features/insights/ReleaseActivity.tsx +308 -0
  183. package/src/components/features/insights/ReportingDevicesSummary.tsx +48 -0
  184. package/src/components/features/insights/RollbackButton.tsx +118 -0
  185. package/src/components/features/insights/UpdateFailures.tsx +382 -0
  186. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  187. package/src/components/features/releases/ReleaseEditorSheet.tsx +26 -12
  188. package/src/components/features/remote-config/ConditionDialog.tsx +581 -0
  189. package/src/components/features/remote-config/ConditionsCard.tsx +244 -0
  190. package/src/components/features/remote-config/ParameterDialog.tsx +407 -0
  191. package/src/components/features/remote-config/ParametersCard.tsx +244 -0
  192. package/src/components/features/remote-config/PreviewCard.tsx +284 -0
  193. package/src/components/features/remote-config/PublishDialog.tsx +319 -0
  194. package/src/components/features/remote-config/RemoteConfigPage.tsx +353 -0
  195. package/src/components/features/remote-config/RuleSummary.tsx +21 -0
  196. package/src/components/features/remote-config/ValueInput.tsx +148 -0
  197. package/src/components/features/remote-config/VersionsCard.tsx +361 -0
  198. package/src/components/features/remote-config/remoteConfigFeatureUi.tsx +15 -0
  199. package/src/components/features/remote-config/useDateTimeText.ts +16 -0
  200. package/src/components/ui/card.tsx +1 -1
  201. package/src/components/ui/scroll-area.tsx +52 -0
  202. package/src/components/ui/sidebar.tsx +22 -29
  203. package/src/components/ui/tabs.tsx +81 -0
  204. package/src/index.ts +26 -19
  205. package/src/lib/api-keys-api.ts +0 -28
  206. package/src/lib/api-keys-rpc.ts +27 -38
  207. package/src/lib/api-rpc.ts +189 -266
  208. package/src/lib/api.ts +19 -102
  209. package/src/lib/bundle-activity.ts +24 -4
  210. package/src/lib/console-access.ts +20 -0
  211. package/src/lib/console-features-api.ts +68 -0
  212. package/src/lib/console-features-rpc.ts +13 -0
  213. package/src/lib/console-features.ts +97 -0
  214. package/src/lib/insights-api.ts +56 -2
  215. package/src/lib/insights-errors.ts +64 -0
  216. package/src/lib/insights-failures-rpc.ts +36 -0
  217. package/src/lib/insights-failures.ts +130 -0
  218. package/src/lib/insights-recovery-rpc.ts +13 -17
  219. package/src/lib/insights-recovery.ts +10 -27
  220. package/src/lib/insights-retention.ts +21 -0
  221. package/src/lib/insights-rpc.ts +30 -12
  222. package/src/lib/insights-search.ts +93 -0
  223. package/src/lib/insights-usage-rpc.ts +29 -0
  224. package/src/lib/insights-usage.ts +76 -0
  225. package/src/lib/insights-view.ts +34 -5
  226. package/src/lib/release-adoption-api.ts +214 -0
  227. package/src/lib/release-adoption-rpc.ts +49 -0
  228. package/src/lib/release-adoption.ts +164 -0
  229. package/src/lib/remote-config-api.ts +119 -0
  230. package/src/lib/remote-config-draft.ts +652 -0
  231. package/src/lib/remote-config-rpc.ts +196 -0
  232. package/src/lib/scroll-restoration.ts +21 -0
  233. package/src/lib/server/adminInsights.ts +124 -0
  234. package/src/lib/server/auth.server.ts +13 -5
  235. package/src/lib/server/bundleActivity.ts +27 -28
  236. package/src/lib/server/config.server.ts +21 -74
  237. package/src/lib/server/console-feature-apis.ts +70 -0
  238. package/src/lib/server/console-runtime.server.ts +82 -38
  239. package/src/lib/server/downloadBundle.ts +16 -15
  240. package/src/lib/server/failureReports.ts +49 -0
  241. package/src/lib/server/getBundleChildren.ts +31 -122
  242. package/src/lib/server/insightsUsage.ts +63 -0
  243. package/src/lib/server/listReleases.ts +109 -44
  244. package/src/lib/server/releaseAdoption.ts +154 -0
  245. package/src/lib/server/releaseReachability.ts +8 -7
  246. package/src/lib/server/remoteConfig.ts +191 -0
  247. package/src/lib/server/runtime.server.ts +120 -31
  248. package/src/lib/server/updateFailures.ts +65 -0
  249. package/src/routeTree.gen.ts +65 -44
  250. package/src/router.tsx +3 -2
  251. package/src/routes/-installations-search.ts +32 -8
  252. package/src/routes/-releases-search.ts +76 -29
  253. package/src/routes/__root.tsx +10 -4
  254. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  255. package/src/routes/api-keys.tsx +5 -2
  256. package/src/routes/index.tsx +210 -152
  257. package/src/routes/insights.tsx +133 -18
  258. package/src/routes/insights_.distribution.tsx +422 -0
  259. package/src/routes/installations.tsx +98 -41
  260. package/src/routes/remote-config.tsx +42 -0
  261. package/src/styles.css +9 -0
  262. package/src/vite-internal.ts +15 -3
  263. package/src/vite.ts +2 -0
  264. package/.output/public/assets/InsightsPageHeader-C-Xc2MB9.js +0 -1
  265. package/.output/public/assets/api-keys-C1sgC6ko.js +0 -1
  266. package/.output/public/assets/api-keys-api-LchP65vQ.js +0 -1
  267. package/.output/public/assets/badge-BuMDU0Rf.js +0 -1
  268. package/.output/public/assets/card-BEvx_QKh.js +0 -1
  269. package/.output/public/assets/chart-no-axes-combined-C0G0fQ3J.js +0 -1
  270. package/.output/public/assets/composite-CHTH1Xhh.js +0 -1
  271. package/.output/public/assets/createServerFn-DHc2erub.js +0 -3
  272. package/.output/public/assets/dialog-dyZ1SphZ.js +0 -1
  273. package/.output/public/assets/dist-Bh1gaDfB.js +0 -1
  274. package/.output/public/assets/field-79SVH26s.js +0 -1
  275. package/.output/public/assets/index-BNkxg4jz.js +0 -10
  276. package/.output/public/assets/input-group-BbHfaBzQ.js +0 -1
  277. package/.output/public/assets/insights-D_7uSKfc.js +0 -1
  278. package/.output/public/assets/installations-BzoHWuXw.js +0 -1
  279. package/.output/public/assets/installations-DvjbUqd1.js +0 -2
  280. package/.output/public/assets/lazyRouteComponent-BN0MS05o.js +0 -1
  281. package/.output/public/assets/link-BG06whwB.js +0 -1
  282. package/.output/public/assets/loader-circle-Bbp8LGVC.js +0 -1
  283. package/.output/public/assets/preload-helper-uTix4PVD.js +0 -1
  284. package/.output/public/assets/refresh-cw-DZgiAOMk.js +0 -1
  285. package/.output/public/assets/routes-BFE-oRIT.js +0 -1
  286. package/.output/public/assets/routes-DaPMY31P.js +0 -2
  287. package/.output/public/assets/shield-check-DoIiaaXU.js +0 -1
  288. package/.output/public/assets/signing-BTRhaF5D.js +0 -1
  289. package/.output/public/assets/styles-C7U0oVke.css +0 -2
  290. package/.output/public/assets/table-BBEzJ7Xl.js +0 -1
  291. package/.output/public/assets/useCompositeListItem-BkPDaqyj.js +0 -42
  292. package/.output/server/__23tanstack-start-server-fn-resolver-Dr2ixDgQ.mjs +0 -146
  293. package/.output/server/_ssr/InsightsPageHeader-C5Has8Ry.mjs +0 -60
  294. package/.output/server/_ssr/api-keys-api-CrCZyvnl.mjs +0 -60
  295. package/.output/server/_ssr/api-keys-rpc-CFaNf-q0.mjs +0 -136
  296. package/.output/server/_ssr/api-rpc-B61KL3Eo.mjs +0 -1399
  297. package/.output/server/_ssr/auth.server-DF_shCeq.mjs +0 -47
  298. package/.output/server/_ssr/badge-BgsCP9Dp.mjs +0 -28
  299. package/.output/server/_ssr/bundle-signing-rpc-BBipZnFs.mjs +0 -16
  300. package/.output/server/_ssr/bundle-signing.server-Co_zHdJx.mjs +0 -12
  301. package/.output/server/_ssr/bundleActivity-D3_fIda2.mjs +0 -27
  302. package/.output/server/_ssr/card-DrbxCPtG.mjs +0 -86
  303. package/.output/server/_ssr/chart-qxa-_30a.mjs +0 -162
  304. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  305. package/.output/server/_ssr/config.server-CBr6PDj5.mjs +0 -368
  306. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  307. package/.output/server/_ssr/deleteBundle-BAq3JuGr.mjs +0 -66
  308. package/.output/server/_ssr/field-CqNUlqUq.mjs +0 -74
  309. package/.output/server/_ssr/getBundleChildren-COMt16m_.mjs +0 -50
  310. package/.output/server/_ssr/insights-B-DUbksX.mjs +0 -502
  311. package/.output/server/_ssr/insights-recovery-rpc-P71FH66n.mjs +0 -37
  312. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
  313. package/.output/server/_ssr/insightsRecovery-CqcosG7B.mjs +0 -149
  314. package/.output/server/_ssr/installations-BQrppti7.mjs +0 -29
  315. package/.output/server/_ssr/routes-BDabsmc7.mjs +0 -37
  316. package/.output/server/_ssr/runtime.server-jmZOKIQM.mjs +0 -438
  317. package/.output/server/_ssr/server-p7yrVBfD.mjs +0 -7468
  318. package/.output/server/_ssr/signing-CxCbnFCt.mjs +0 -134
  319. package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
  320. package/.output/server/_tanstack-start-manifest_v-Cwpu0C9Q.mjs +0 -100
  321. package/src/components/features/bundles/BundleActivityChart.tsx +0 -95
  322. package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -121
  323. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  324. package/src/lib/bundle-signing-api.ts +0 -14
  325. package/src/lib/bundle-signing-rpc.ts +0 -16
  326. package/src/lib/server/bundle-signing.server.ts +0 -23
  327. package/src/lib/server/deleteBundle.ts +0 -145
  328. package/src/lib/server/insightsHistory.ts +0 -52
  329. package/src/lib/server/insightsRecovery.ts +0 -186
  330. package/src/routes/signing.tsx +0 -37
@@ -0,0 +1,2594 @@
1
+ import { i as __toESM } from "../_runtime.mjs";
2
+ import { u as createServerFn } from "./createMiddleware-Cm6Tn0_Y.mjs";
3
+ import { _t as require_react, gt as require_jsx_runtime, n as Toggle, t as ToggleGroup } from "../_libs/@base-ui/react+[...].mjs";
4
+ import { A as GitBranch, J as Braces, L as Clipboard, N as Eye, T as LoaderCircle, Y as ArrowUp, Z as ArrowRight, _ as Plus, a as Trash2, d as SlidersHorizontal, et as ArrowDown, g as RefreshCw, h as RotateCcw, i as TriangleAlert, k as History, n as Upload, p as Search, r as Undo2, t as X, u as Smartphone, v as Pencil } from "../_libs/lucide-react.mjs";
5
+ import { t as cva } from "../_libs/class-variance-authority+clsx.mjs";
6
+ import { A as createSsrRpc, C as Skeleton, S as SidebarTrigger, k as cn, n as Input, t as Button$1 } from "./sidebar-DPcOijvL.mjs";
7
+ import { t as consoleAccess } from "./console-access--o9FXikU.mjs";
8
+ import { f as lazyRouteComponent, p as createFileRoute } from "../_libs/@tanstack/react-router+[...].mjs";
9
+ import { i as useQuery, n as useMutation, s as useQueryClient, t as useInfiniteQuery } from "../_libs/tanstack__react-query.mjs";
10
+ import { l as keepPreviousData } from "../_libs/tanstack__query-core.mjs";
11
+ import { r as requireConsoleFeature } from "./console-features-api-CYclUQ4h.mjs";
12
+ import { a as EmptyMedia, i as EmptyHeader, n as EmptyContent, o as EmptyTitle, r as EmptyDescription, t as Empty } from "./empty-BPayFad3.mjs";
13
+ import { _ as FieldLegend, a as Badge, c as CardDescription, d as CardTitle, f as Field, g as FieldLabel, h as FieldGroup, i as AlertTitle, m as FieldError, n as AlertAction, o as Card, p as FieldDescription, r as AlertDescription, s as CardContent, t as Alert, u as CardHeader, v as FieldSet } from "./field-DjZ5t7rH.mjs";
14
+ import { a as AlertDialogDescription, i as AlertDialogContent, l as AlertDialogTitle, n as AlertDialogAction, o as AlertDialogFooter, r as AlertDialogCancel, s as AlertDialogHeader, t as AlertDialog$1 } from "./alert-dialog-Chkim7vZ.mjs";
15
+ import { a as DialogHeader, i as DialogFooter, n as DialogContent, o as DialogTitle, r as DialogDescription, t as Dialog$1 } from "./dialog-mfSANqjC.mjs";
16
+ import { n as toast } from "../_libs/sonner.mjs";
17
+ import { i as TabsTrigger, n as TabsContent, r as TabsList, t as Tabs$1 } from "./tabs-trDcbRkT.mjs";
18
+ import { a as SelectItem, i as SelectGroup, n as Select$1, o as SelectTrigger, r as SelectContent, s as SelectValue, t as PlatformIcon } from "./select-DkzM2miy.mjs";
19
+ import { i as InputGroupInput, n as InputGroupAddon, o as Textarea, r as InputGroupButton, t as InputGroup } from "./input-group-Dx-MEkNM.mjs";
20
+ import { i as useChannelsQuery } from "./api-C-Il5-sa.mjs";
21
+ import { t as Switch$1 } from "./switch-BqJ2mAwC.mjs";
22
+ import { n as require_dayjs_min, t as require_relativeTime } from "../_libs/dayjs.mjs";
23
+ //#region node_modules/.nitro/vite/services/ssr/assets/remote-config-D2GmbGwI.js
24
+ var import_react = /* @__PURE__ */ __toESM(require_react());
25
+ var import_jsx_runtime = require_jsx_runtime();
26
+ var import_dayjs_min = /* @__PURE__ */ __toESM(require_dayjs_min());
27
+ var import_relativeTime = /* @__PURE__ */ __toESM(require_relativeTime());
28
+ /** Remote Config, once the console's access check and the feature guard pass. */
29
+ var field = (input, key) => typeof input === "object" && input !== null ? Reflect.get(input, key) : void 0;
30
+ var versionNumber = (value, name) => {
31
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer.`);
32
+ return value;
33
+ };
34
+ var parseCursor = (input) => {
35
+ const cursor = field(input, "cursor");
36
+ if (cursor !== void 0 && typeof cursor !== "string") throw new TypeError("cursor must be text.");
37
+ return cursor === void 0 ? {} : { cursor };
38
+ };
39
+ var parseVersion = (input) => ({ version: versionNumber(field(input, "version"), "version") });
40
+ var parsePublish = (input) => {
41
+ const description = field(input, "description");
42
+ if (description !== void 0 && typeof description !== "string") throw new TypeError("description must be text.");
43
+ return {
44
+ template: field(input, "template"),
45
+ baseVersion: versionNumber(field(input, "baseVersion"), "baseVersion"),
46
+ ...description === void 0 ? {} : { description }
47
+ };
48
+ };
49
+ var parseRollback = (input) => ({
50
+ version: versionNumber(field(input, "version"), "version"),
51
+ baseVersion: versionNumber(field(input, "baseVersion"), "baseVersion")
52
+ });
53
+ var CONTEXT_FIELDS = [
54
+ "platform",
55
+ "appVersion",
56
+ "channel",
57
+ "cohort",
58
+ "fingerprintHash"
59
+ ];
60
+ var parsePreview = (input) => {
61
+ const raw = field(input, "context");
62
+ const context = {};
63
+ for (const key of CONTEXT_FIELDS) {
64
+ const value = field(raw, key);
65
+ if (typeof value === "string" && value.trim().length > 0) context[key] = value.trim();
66
+ }
67
+ if (context.platform !== void 0 && context.platform !== "ios" && context.platform !== "android") throw new TypeError("platform must be \"ios\" or \"android\".");
68
+ const now = field(raw, "now");
69
+ if (now !== void 0 && (typeof now !== "number" || !Number.isFinite(now))) throw new TypeError("now must be a time in milliseconds since the epoch.");
70
+ return {
71
+ template: field(input, "template"),
72
+ context: {
73
+ ...context,
74
+ now: now ?? Date.now()
75
+ }
76
+ };
77
+ };
78
+ var getRemoteConfigRpc = createServerFn({ method: "GET" }).middleware([consoleAccess]).handler(createSsrRpc("0ccfae64b399b03d820328fa98a38e2caa0b622d3da5a7ca633c5a0e8101e3f6"));
79
+ var listRemoteConfigVersionsRpc = createServerFn({ method: "GET" }).middleware([consoleAccess]).validator(parseCursor).handler(createSsrRpc("3377db68b9d1d971971595469029c0466da0777a177b294d655cd0c039737f1a"));
80
+ var getRemoteConfigVersionRpc = createServerFn({ method: "GET" }).middleware([consoleAccess]).validator(parseVersion).handler(createSsrRpc("af71e3c55a659ce46093e33b9d70e729359610eec7d576502e8f0e26c785b317"));
81
+ var publishRemoteConfigRpc = createServerFn({ method: "POST" }).middleware([consoleAccess]).validator(parsePublish).handler(createSsrRpc("4182bf9ccbb675700638f30bc6122517ccd90b16a339a6823a0f5f6c808d70a6"));
82
+ var rollbackRemoteConfigRpc = createServerFn({ method: "POST" }).middleware([consoleAccess]).validator(parseRollback).handler(createSsrRpc("98c10bdc514eb4d3fe65af546be44452b492a81092cfe45625778a4c9f731727"));
83
+ /** What a device with `context` would receive from a template, published or not. */
84
+ var previewRemoteConfigRpc = createServerFn({ method: "POST" }).middleware([consoleAccess]).validator(parsePreview).handler(createSsrRpc("1371bff16085636e2c44b60a573ae434b556c63045c40a0a93a93ab69ec47999"));
85
+ var remoteConfigQueryKeys = {
86
+ all: ["remote-config"],
87
+ active: ["remote-config", "active"],
88
+ versions: ["remote-config", "versions"],
89
+ version: (version) => [
90
+ "remote-config",
91
+ "version",
92
+ version
93
+ ],
94
+ preview: (input) => [
95
+ "remote-config",
96
+ "preview",
97
+ input
98
+ ]
99
+ };
100
+ /** The published template, read once a visit unless a publish changes it. */
101
+ var useRemoteConfigQuery = () => useQuery({
102
+ queryKey: remoteConfigQueryKeys.active,
103
+ queryFn: () => getRemoteConfigRpc(),
104
+ staleTime: 6e4
105
+ });
106
+ /** Published versions, newest first, a page at a time. */
107
+ var useRemoteConfigVersionsQuery = ({ enabled }) => useInfiniteQuery({
108
+ queryKey: remoteConfigQueryKeys.versions,
109
+ queryFn: ({ pageParam }) => listRemoteConfigVersionsRpc({ data: pageParam === void 0 ? {} : { cursor: pageParam } }),
110
+ initialPageParam: void 0,
111
+ getNextPageParam: (page) => page.next,
112
+ staleTime: 6e4,
113
+ enabled
114
+ });
115
+ /** One version with its template; a version never changes once published. */
116
+ var useRemoteConfigVersionQuery = (version) => useQuery({
117
+ queryKey: remoteConfigQueryKeys.version(version ?? 0),
118
+ queryFn: () => getRemoteConfigVersionRpc({ data: { version } }),
119
+ enabled: version !== null,
120
+ staleTime: Infinity
121
+ });
122
+ var useRefreshAfterPublish = () => {
123
+ const queryClient = useQueryClient();
124
+ return async () => {
125
+ await Promise.all([queryClient.invalidateQueries({ queryKey: remoteConfigQueryKeys.active }), queryClient.invalidateQueries({ queryKey: remoteConfigQueryKeys.versions })]);
126
+ };
127
+ };
128
+ var usePublishRemoteConfigMutation = () => {
129
+ const refresh = useRefreshAfterPublish();
130
+ return useMutation({
131
+ mutationFn: (input) => publishRemoteConfigRpc({ data: input }),
132
+ onSuccess: async (result) => {
133
+ if (result.status === "published" || result.status === "conflict") await refresh();
134
+ }
135
+ });
136
+ };
137
+ var useRollbackRemoteConfigMutation = () => {
138
+ return useMutation({
139
+ mutationFn: (input) => rollbackRemoteConfigRpc({ data: input }),
140
+ onSuccess: useRefreshAfterPublish()
141
+ });
142
+ };
143
+ /** A device's view of the draft, kept on screen while the next one loads. */
144
+ var useRemoteConfigPreviewQuery = (input) => useQuery({
145
+ queryKey: remoteConfigQueryKeys.preview(input),
146
+ queryFn: () => previewRemoteConfigRpc({ data: input }),
147
+ placeholderData: keepPreviousData,
148
+ staleTime: Infinity
149
+ });
150
+ var VALUE_TYPE_LABELS = {
151
+ STRING: "String",
152
+ NUMBER: "Number",
153
+ BOOLEAN: "Boolean",
154
+ JSON: "JSON"
155
+ };
156
+ var RULE_TYPE_LABELS = {
157
+ platform: "Platform",
158
+ channel: "Channel",
159
+ appVersion: "App version",
160
+ percent: "Percentage of installs",
161
+ cohort: "Cohort",
162
+ fingerprint: "Fingerprint",
163
+ dateTime: "Date and time"
164
+ };
165
+ /**
166
+ * A parameter's value for a condition. Condition names come from users, so
167
+ * `constructor` must name theirs, never Object.prototype's.
168
+ */
169
+ var conditionalValueFor = (values, name) => values !== void 0 && Object.hasOwn(values, name) ? values[name] : void 0;
170
+ /** Whether two templates publish the same thing. */
171
+ var isSameTemplate = (left, right) => JSON.stringify(left) === JSON.stringify(right);
172
+ /** Adds a parameter, or replaces `previousKey`'s in its place. */
173
+ var upsertParameter = (template, previousKey, key, parameter) => {
174
+ const entries = Object.entries(template.parameters);
175
+ const index = previousKey === null ? -1 : entries.findIndex(([existing]) => existing === previousKey);
176
+ const next = index === -1 ? [...entries.filter(([existing]) => existing !== key), [key, parameter]] : entries.map((entry, position) => position === index ? [key, parameter] : entry);
177
+ return {
178
+ ...template,
179
+ parameters: Object.fromEntries(next)
180
+ };
181
+ };
182
+ var removeParameter = (template, key) => ({
183
+ ...template,
184
+ parameters: Object.fromEntries(Object.entries(template.parameters).filter(([existing]) => existing !== key))
185
+ });
186
+ /** Every parameter's conditional values, with `rename` applied to their condition names. */
187
+ var mapConditionalValues = (template, rename) => Object.fromEntries(Object.entries(template.parameters).map(([key, parameter]) => {
188
+ const { conditionalValues, ...rest } = parameter;
189
+ const kept = Object.entries(conditionalValues ?? {}).flatMap(([name, value]) => {
190
+ const next = rename(name);
191
+ return next === null ? [] : [[next, value]];
192
+ });
193
+ return [key, kept.length === 0 ? rest : {
194
+ ...rest,
195
+ conditionalValues: Object.fromEntries(kept)
196
+ }];
197
+ }));
198
+ /**
199
+ * Adds a condition last, or replaces `previousName`'s in its place; a
200
+ * rename carries the parameters' values for it to the new name.
201
+ */
202
+ var upsertCondition = (template, previousName, condition) => {
203
+ if (previousName === null) return {
204
+ ...template,
205
+ conditions: [...template.conditions, condition]
206
+ };
207
+ return {
208
+ conditions: template.conditions.map((existing) => existing.name === previousName ? condition : existing),
209
+ parameters: previousName === condition.name ? template.parameters : mapConditionalValues(template, (name) => name === previousName ? condition.name : name)
210
+ };
211
+ };
212
+ /** Removes a condition and every parameter's value for it. */
213
+ var removeCondition = (template, name) => ({
214
+ conditions: template.conditions.filter((condition) => condition.name !== name),
215
+ parameters: mapConditionalValues(template, (existing) => existing === name ? null : existing)
216
+ });
217
+ /** Moves a condition up (`-1`) or down (`1`) in priority. */
218
+ var moveCondition = (template, index, offset) => {
219
+ const target = index + offset;
220
+ if (target < 0 || target >= template.conditions.length) return template;
221
+ const conditions = [...template.conditions];
222
+ [conditions[index], conditions[target]] = [conditions[target], conditions[index]];
223
+ return {
224
+ ...template,
225
+ conditions
226
+ };
227
+ };
228
+ /** The parameters that give a condition a value. */
229
+ var parametersUsing = (template, name) => Object.entries(template.parameters).filter(([, parameter]) => conditionalValueFor(parameter.conditionalValues, name) !== void 0).map(([key]) => key);
230
+ var listText = (values, limit = 3) => values.length <= limit ? values.join(", ") : `${values.slice(0, limit).join(", ")} +${values.length - limit}`;
231
+ var percentText = (value) => Number.isInteger(value) ? String(value) : value.toFixed(1);
232
+ /** Dates in English with their offset, as Insights shows them. */
233
+ var createDateTimeText = (timeZone) => {
234
+ const format = new Intl.DateTimeFormat("en", {
235
+ timeZone,
236
+ timeZoneName: timeZone === "UTC" ? "short" : "shortOffset",
237
+ year: "numeric",
238
+ month: "short",
239
+ day: "numeric",
240
+ hour: "2-digit",
241
+ minute: "2-digit",
242
+ hourCycle: "h23"
243
+ });
244
+ return (iso) => format.format(new Date(iso));
245
+ };
246
+ /** The server renders in UTC; the page switches to the viewer's zone once mounted. */
247
+ var UTC_DATE_TIME_TEXT = createDateTimeText("UTC");
248
+ /** The quick choices for a rule's start, as UTC instants. */
249
+ var startPresets = (now = /* @__PURE__ */ new Date()) => {
250
+ const nextHour = new Date(now);
251
+ nextHour.setMinutes(60, 0, 0);
252
+ const tomorrow = new Date(now);
253
+ tomorrow.setDate(tomorrow.getDate() + 1);
254
+ tomorrow.setHours(9, 0, 0, 0);
255
+ const minute = new Date(now);
256
+ minute.setSeconds(0, 0);
257
+ return [
258
+ {
259
+ label: "Now",
260
+ iso: minute.toISOString()
261
+ },
262
+ {
263
+ label: "Next hour",
264
+ iso: nextHour.toISOString()
265
+ },
266
+ {
267
+ label: "Tomorrow 09:00",
268
+ iso: tomorrow.toISOString()
269
+ }
270
+ ];
271
+ };
272
+ /** The quick choices for a rule's end, counted from its start or from now. */
273
+ var endPresets = (from, now = /* @__PURE__ */ new Date()) => {
274
+ const start = from === void 0 ? now : new Date(from);
275
+ const after = (days) => {
276
+ const end = new Date(start);
277
+ end.setDate(end.getDate() + days);
278
+ end.setSeconds(0, 0);
279
+ return end.toISOString();
280
+ };
281
+ return [{
282
+ label: "+1 day",
283
+ iso: after(1)
284
+ }, {
285
+ label: "+1 week",
286
+ iso: after(7)
287
+ }];
288
+ };
289
+ /** An instant as a `datetime-local` input shows it, in the viewer's time zone. */
290
+ var toLocalDateTimeInput = (iso) => {
291
+ if (iso === void 0) return "";
292
+ const date = new Date(iso);
293
+ const pad = (value) => String(value).padStart(2, "0");
294
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
295
+ };
296
+ /** A `datetime-local` input's value, in the viewer's time zone, as a UTC instant. */
297
+ var fromLocalDateTimeInput = (value) => {
298
+ if (value.length === 0) return void 0;
299
+ const ms = new Date(value).getTime();
300
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : void 0;
301
+ };
302
+ /** A rule in a few words, such as `Channel is beta, qa`. */
303
+ var describeRule = (rule, dateTimeText = UTC_DATE_TIME_TEXT) => {
304
+ switch (rule.type) {
305
+ case "platform": return `Platform is ${listText(rule.platforms.map((platform) => platform === "ios" ? "iOS" : "Android"))}`;
306
+ case "channel": return `Channel is ${listText(rule.channels)}`;
307
+ case "appVersion": return `App version ${rule.range}`;
308
+ case "cohort": return `Cohort is ${listText(rule.cohorts)}`;
309
+ case "percent": return rule.from === 0 ? `${percentText(rule.to)}% of installs` : `${percentText(rule.from)}–${percentText(rule.to)}% of installs`;
310
+ case "fingerprint": return `Fingerprint is ${listText(rule.hashes.map((hash) => hash.slice(0, 8)))}`;
311
+ case "dateTime": return rule.from !== void 0 && rule.to !== void 0 ? `From ${dateTimeText(rule.from)} until ${dateTimeText(rule.to)}` : rule.from !== void 0 ? `From ${dateTimeText(rule.from)}` : `Until ${dateTimeText(rule.to ?? "")}`;
312
+ }
313
+ };
314
+ /** A value as the page shows it: its text, or the app's own default. */
315
+ var describeValue = (value) => "value" in value ? value.value.length === 0 ? "\"\"" : value.value : "In-app default";
316
+ /** A short random seed, so a new percentage rule picks its own installs. */
317
+ var createSeed = () => {
318
+ const bytes = crypto.getRandomValues(new Uint8Array(6));
319
+ return Array.from(bytes, (byte) => byte.toString(36).padStart(2, "0")).join("").slice(0, 8);
320
+ };
321
+ var diffKeys = (base, draft) => ({
322
+ added: Object.keys(draft).filter((key) => !Object.hasOwn(base, key)),
323
+ changed: Object.keys(draft).filter((key) => Object.hasOwn(base, key) && JSON.stringify(base[key]) !== JSON.stringify(draft[key])),
324
+ removed: Object.keys(base).filter((key) => !Object.hasOwn(draft, key))
325
+ });
326
+ /** What a publish changes, for the publish dialog. */
327
+ var summarizeChanges = (base, draft) => {
328
+ const byName = (template) => Object.fromEntries(template.conditions.map((condition) => [condition.name, condition]));
329
+ const kept = (template, other) => template.conditions.map(({ name }) => name).filter((name) => other.conditions.some((it) => it.name === name));
330
+ return {
331
+ parameters: diffKeys(base.parameters, draft.parameters),
332
+ conditions: diffKeys(byName(base), byName(draft)),
333
+ reordered: kept(base, draft).join("\n") !== kept(draft, base).join("\n")
334
+ };
335
+ };
336
+ var NOT_SET = "Not set";
337
+ /** What changes in a parameter the draft changes, field by field. */
338
+ var describeParameterChange = (before, after) => {
339
+ const changes = [];
340
+ const add = (field, from, to) => {
341
+ if (from !== to) changes.push({
342
+ field,
343
+ before: from,
344
+ after: to
345
+ });
346
+ };
347
+ add("Type", VALUE_TYPE_LABELS[before.valueType], VALUE_TYPE_LABELS[after.valueType]);
348
+ add("Default", describeValue(before.defaultValue), describeValue(after.defaultValue));
349
+ const conditionNames = new Set([...Object.keys(before.conditionalValues ?? {}), ...Object.keys(after.conditionalValues ?? {})]);
350
+ for (const name of conditionNames) {
351
+ const from = conditionalValueFor(before.conditionalValues, name);
352
+ const to = conditionalValueFor(after.conditionalValues, name);
353
+ add(`When ${name}`, from === void 0 ? NOT_SET : describeValue(from), to === void 0 ? NOT_SET : describeValue(to));
354
+ }
355
+ add("Description", before.description ?? NOT_SET, after.description ?? NOT_SET);
356
+ return changes;
357
+ };
358
+ /** What changes in a condition the draft changes: its rules. */
359
+ var describeConditionChange = (before, after, dateTimeText = UTC_DATE_TIME_TEXT) => {
360
+ const rules = (condition) => condition.rules.map((rule) => describeRule(rule, dateTimeText)).join(" · ");
361
+ const from = rules(before);
362
+ const to = rules(after);
363
+ return from === to ? [] : [{
364
+ field: "Rules",
365
+ before: from,
366
+ after: to
367
+ }];
368
+ };
369
+ var DRAFT_STORAGE_KEY = "hot-updater:remote-config-draft";
370
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
371
+ var isParameterValueShape = (value) => isRecord(value) && (typeof value.value === "string" || value.useInAppDefault === true);
372
+ /** Whether a stored value has a template's shape, so the page can render it. */
373
+ var isTemplateShape = (value) => isRecord(value) && isRecord(value.parameters) && Array.isArray(value.conditions) && Object.values(value.parameters).every((parameter) => isRecord(parameter) && Object.hasOwn(VALUE_TYPE_LABELS, String(parameter.valueType)) && isParameterValueShape(parameter.defaultValue) && (parameter.conditionalValues === void 0 || isRecord(parameter.conditionalValues) && Object.values(parameter.conditionalValues).every(isParameterValueShape))) && value.conditions.every((condition) => isRecord(condition) && typeof condition.name === "string" && Array.isArray(condition.rules) && condition.rules.every((rule) => isRecord(rule) && Object.hasOwn(RULE_TYPE_LABELS, String(rule.type))));
374
+ /**
375
+ * The draft this browser kept for a server whose active version is
376
+ * `published`, or null. Several projects' Consoles can share one origin, so
377
+ * a draft comes back only onto the version and template it started from.
378
+ */
379
+ var readStoredDraft = (published) => {
380
+ try {
381
+ const raw = window.localStorage.getItem(DRAFT_STORAGE_KEY);
382
+ if (raw === null) return null;
383
+ const stored = JSON.parse(raw);
384
+ if (!isRecord(stored) || stored.baseVersion !== published.version || !isTemplateShape(stored.base) || !isTemplateShape(stored.template) || !isSameTemplate(stored.base, published.template) || isSameTemplate(stored.template, published.template)) return null;
385
+ return {
386
+ baseVersion: published.version,
387
+ base: stored.base,
388
+ template: stored.template
389
+ };
390
+ } catch {
391
+ return null;
392
+ }
393
+ };
394
+ /** Keeps the draft in this browser, or forgets it; false when the browser refuses. */
395
+ var storeDraft = (draft) => {
396
+ try {
397
+ if (draft === null) window.localStorage.removeItem(DRAFT_STORAGE_KEY);
398
+ else window.localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draft));
399
+ return true;
400
+ } catch {
401
+ return false;
402
+ }
403
+ };
404
+ var PARAMETER_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/u;
405
+ /** Why a parameter key cannot be used, or null. */
406
+ var parameterKeyError = (key, taken) => {
407
+ if (key.length === 0) return "Enter a key.";
408
+ if (!PARAMETER_KEY_PATTERN.test(key)) return "Start with a letter or _, then use letters, digits, and _.";
409
+ return taken.includes(key) ? "Another parameter uses this key." : null;
410
+ };
411
+ /** Why a value's text does not suit its type, or null. */
412
+ var valueTextError = (valueType, text) => {
413
+ if (valueType === "NUMBER") return text.trim().length > 0 && Number.isFinite(Number(text)) ? null : "Enter a number.";
414
+ if (valueType === "JSON") try {
415
+ JSON.parse(text);
416
+ return null;
417
+ } catch {
418
+ return "Enter valid JSON.";
419
+ }
420
+ if (valueType === "BOOLEAN" && text !== "true" && text !== "false") return "Choose true or false.";
421
+ return null;
422
+ };
423
+ /**
424
+ * A name for a condition saved without one: its rules in a few words, kept
425
+ * within 100 characters and apart from the names in use.
426
+ */
427
+ var suggestConditionName = (rules, taken, dateTimeText) => {
428
+ const text = rules.map((rule) => describeRule(rule, dateTimeText)).join(" · ");
429
+ const base = text.length <= 100 ? text : `${text.slice(0, 99).trimEnd()}…`;
430
+ if (base.length === 0 || !taken.includes(base)) return base;
431
+ for (let copy = 2;; copy += 1) {
432
+ const suffix = ` (${copy})`;
433
+ const name = `${base.slice(0, 100 - suffix.length)}${suffix}`;
434
+ if (!taken.includes(name)) return name;
435
+ }
436
+ };
437
+ /** Why a condition name cannot be used, or null. */
438
+ var conditionNameError = (name, taken) => {
439
+ const trimmed = name.trim();
440
+ if (trimmed.length === 0) return "Enter a name.";
441
+ if (trimmed.length > 100) return "Use 100 characters or fewer.";
442
+ return taken.includes(trimmed) ? "Another condition uses this name." : null;
443
+ };
444
+ /** Splits a comma- or newline-separated list, without blanks or repeats. */
445
+ var splitList = (text) => [...new Set(text.split(/[\n,]/u).map((entry) => entry.trim()).filter((entry) => entry.length > 0))];
446
+ /** The problem with a rule, for its editor; null when the server would take it. */
447
+ var ruleError = (rule) => {
448
+ switch (rule.type) {
449
+ case "platform": return rule.platforms.length === 0 ? "Choose a platform." : null;
450
+ case "channel": return rule.channels.length === 0 ? "Enter a channel." : null;
451
+ case "appVersion": return rule.range.trim().length === 0 ? "Enter a version range, such as >=1.4.0." : null;
452
+ case "cohort": return rule.cohorts.length === 0 ? "Enter a cohort." : null;
453
+ case "percent": return rule.from >= rule.to || rule.from < 0 || rule.to > 100 || Math.abs(rule.to * 10 - Math.round(rule.to * 10)) > 1e-9 || Math.abs(rule.from * 10 - Math.round(rule.from * 10)) > 1e-9 ? "Use a range from 0 to 100 in steps of 0.1." : null;
454
+ case "fingerprint": return rule.hashes.length === 0 ? "Enter a fingerprint." : null;
455
+ case "dateTime":
456
+ if (rule.from === void 0 && rule.to === void 0) return "Choose a start, an end, or both.";
457
+ return rule.from !== void 0 && rule.to !== void 0 && rule.from >= rule.to ? "End after the start." : null;
458
+ }
459
+ };
460
+ /** A new rule of a type, with sensible starting values. */
461
+ var createRule = (type) => {
462
+ switch (type) {
463
+ case "platform": return {
464
+ type,
465
+ platforms: ["ios"]
466
+ };
467
+ case "channel": return {
468
+ type,
469
+ channels: []
470
+ };
471
+ case "appVersion": return {
472
+ type,
473
+ range: ""
474
+ };
475
+ case "cohort": return {
476
+ type,
477
+ cohorts: []
478
+ };
479
+ case "percent": return {
480
+ type,
481
+ seed: createSeed(),
482
+ from: 0,
483
+ to: 10
484
+ };
485
+ case "fingerprint": return {
486
+ type,
487
+ hashes: []
488
+ };
489
+ case "dateTime": return {
490
+ type,
491
+ from: startPresets()[1].iso
492
+ };
493
+ }
494
+ };
495
+ var toggleVariants = cva("group/toggle inline-flex items-center justify-center gap-1 rounded-md text-xs font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
496
+ variants: {
497
+ variant: {
498
+ default: "bg-transparent",
499
+ outline: "border border-input bg-transparent hover:bg-muted"
500
+ },
501
+ size: {
502
+ default: "h-7 min-w-7 px-2 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
503
+ sm: "h-6 min-w-6 rounded-[min(var(--radius-md),8px)] px-2 text-[0.625rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
504
+ lg: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2"
505
+ }
506
+ },
507
+ defaultVariants: {
508
+ variant: "default",
509
+ size: "default"
510
+ }
511
+ });
512
+ var ToggleGroupContext = import_react.createContext({
513
+ size: "default",
514
+ variant: "default",
515
+ spacing: 2,
516
+ orientation: "horizontal"
517
+ });
518
+ function ToggleGroup$1({ className, variant, size, spacing = 2, orientation = "horizontal", children, ...props }) {
519
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToggleGroup, {
520
+ "data-slot": "toggle-group",
521
+ "data-variant": variant,
522
+ "data-size": size,
523
+ "data-spacing": spacing,
524
+ "data-orientation": orientation,
525
+ style: { "--gap": spacing },
526
+ className: cn("group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[size=sm]:rounded-[min(var(--radius-md),8px)] data-vertical:flex-col data-vertical:items-stretch", className),
527
+ ...props,
528
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToggleGroupContext.Provider, {
529
+ value: {
530
+ variant,
531
+ size,
532
+ spacing,
533
+ orientation
534
+ },
535
+ children
536
+ })
537
+ });
538
+ }
539
+ function ToggleGroupItem({ className, children, variant = "default", size = "default", ...props }) {
540
+ const context = import_react.useContext(ToggleGroupContext);
541
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Toggle, {
542
+ "data-slot": "toggle-group-item",
543
+ "data-variant": context.variant || variant,
544
+ "data-size": context.size || size,
545
+ "data-spacing": context.spacing,
546
+ className: cn("shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t", toggleVariants({
547
+ variant: context.variant || variant,
548
+ size: context.size || size
549
+ }), className),
550
+ ...props,
551
+ children
552
+ });
553
+ }
554
+ /**
555
+ * Dates in the viewer's time zone, with its name. The server renders in UTC,
556
+ * and the page switches once mounted, so both render the same first.
557
+ */
558
+ function useDateTimeText() {
559
+ const [timeZone, setTimeZone] = (0, import_react.useState)("UTC");
560
+ (0, import_react.useEffect)(() => {
561
+ setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone);
562
+ }, []);
563
+ return {
564
+ timeZone,
565
+ dateTimeText: (0, import_react.useMemo)(() => createDateTimeText(timeZone), [timeZone])
566
+ };
567
+ }
568
+ var RULE_TYPES = Object.keys(RULE_TYPE_LABELS);
569
+ /** A list typed as text, kept as typed until it is saved. */
570
+ function ListInput({ id, values, onChange, placeholder, multiline = false }) {
571
+ const [text, setText] = (0, import_react.useState)(values.join(", "));
572
+ const [shown, setShown] = (0, import_react.useState)(values);
573
+ if (shown !== values) {
574
+ setShown(values);
575
+ if (splitList(text).join("\n") !== values.join("\n")) setText(values.join(", "));
576
+ }
577
+ const update = (next) => {
578
+ setText(next);
579
+ onChange(splitList(next));
580
+ };
581
+ return multiline ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
582
+ className: "min-h-20 font-mono text-xs",
583
+ id,
584
+ onChange: (event) => update(event.target.value),
585
+ placeholder,
586
+ spellCheck: false,
587
+ value: text
588
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
589
+ autoComplete: "off",
590
+ className: "min-h-11 sm:min-h-9",
591
+ id,
592
+ onChange: (event) => update(event.target.value),
593
+ placeholder,
594
+ value: text
595
+ });
596
+ }
597
+ /** One end of a date and time rule: a picker, a clear button, and quick choices. */
598
+ function DateTimeField({ id, label, value, presets, onChange }) {
599
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [
600
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
601
+ htmlFor: id,
602
+ children: label
603
+ }),
604
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(InputGroup, {
605
+ className: "h-11 sm:h-9",
606
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupInput, {
607
+ className: "h-11 tabular-nums sm:h-9",
608
+ id,
609
+ onChange: (event) => onChange(fromLocalDateTimeInput(event.target.value)),
610
+ type: "datetime-local",
611
+ value: toLocalDateTimeInput(value)
612
+ }), value === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupAddon, {
613
+ align: "inline-end",
614
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupButton, {
615
+ "aria-label": `Clear ${label.toLowerCase()}`,
616
+ className: "size-11 sm:size-5",
617
+ onClick: () => onChange(void 0),
618
+ size: "icon-xs",
619
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(X, {})
620
+ })
621
+ })]
622
+ }),
623
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
624
+ className: "flex flex-wrap gap-1.5",
625
+ children: presets.map((preset) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
626
+ "aria-pressed": preset.iso === value,
627
+ className: "min-h-11 sm:min-h-6",
628
+ onClick: () => onChange(preset.iso),
629
+ size: "xs",
630
+ type: "button",
631
+ variant: preset.iso === value ? "secondary" : "outline",
632
+ children: preset.label
633
+ }, preset.label))
634
+ })
635
+ ] });
636
+ }
637
+ function RuleFields({ rule, onChange }) {
638
+ const id = (0, import_react.useId)();
639
+ const channels = useChannelsQuery();
640
+ switch (rule.type) {
641
+ case "platform": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToggleGroup$1, {
642
+ "aria-label": "Platforms",
643
+ multiple: true,
644
+ onValueChange: (values) => onChange({
645
+ ...rule,
646
+ platforms: values.filter((value) => value === "ios" || value === "android")
647
+ }),
648
+ value: [...rule.platforms],
649
+ variant: "outline",
650
+ children: ["ios", "android"].map((platform) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ToggleGroupItem, {
651
+ className: "min-h-11 px-3 sm:min-h-8",
652
+ value: platform,
653
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlatformIcon, {
654
+ className: "size-3.5",
655
+ platform
656
+ }), platform === "ios" ? "iOS" : "Android"]
657
+ }, platform))
658
+ });
659
+ case "channel": {
660
+ const known = (channels.data ?? []).map(({ name }) => name).filter((channel) => !rule.channels.includes(channel));
661
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
662
+ className: "flex flex-col gap-2",
663
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ListInput, {
664
+ id: `${id}-channels`,
665
+ onChange: (next) => onChange({
666
+ ...rule,
667
+ channels: next
668
+ }),
669
+ placeholder: "production, beta",
670
+ values: rule.channels
671
+ }), known.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
672
+ className: "flex flex-wrap items-center gap-1.5",
673
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
674
+ className: "text-xs text-muted-foreground",
675
+ children: "Add:"
676
+ }), known.slice(0, 8).map((channel) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
677
+ className: "min-h-11 sm:min-h-6",
678
+ onClick: () => onChange({
679
+ ...rule,
680
+ channels: [...rule.channels, channel]
681
+ }),
682
+ size: "xs",
683
+ type: "button",
684
+ variant: "outline",
685
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { "data-icon": "inline-start" }), channel]
686
+ }, channel))]
687
+ }) : null]
688
+ });
689
+ }
690
+ case "appVersion": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
691
+ "aria-label": "App version range",
692
+ autoComplete: "off",
693
+ className: "min-h-11 font-mono sm:min-h-9",
694
+ onChange: (event) => onChange({
695
+ ...rule,
696
+ range: event.target.value
697
+ }),
698
+ placeholder: ">=1.4.0",
699
+ value: rule.range
700
+ });
701
+ case "cohort": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ListInput, {
702
+ id: `${id}-cohorts`,
703
+ onChange: (next) => onChange({
704
+ ...rule,
705
+ cohorts: next
706
+ }),
707
+ placeholder: "1, 2, beta-testers",
708
+ values: rule.cohorts
709
+ });
710
+ case "percent": return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
711
+ className: "grid gap-2 sm:grid-cols-[repeat(2,minmax(0,8rem))_minmax(0,1fr)]",
712
+ children: [
713
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
714
+ htmlFor: `${id}-from`,
715
+ children: "From %"
716
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
717
+ className: "min-h-11 tabular-nums sm:min-h-9",
718
+ id: `${id}-from`,
719
+ inputMode: "decimal",
720
+ max: 100,
721
+ min: 0,
722
+ onChange: (event) => onChange({
723
+ ...rule,
724
+ from: Number(event.target.value)
725
+ }),
726
+ step: .1,
727
+ type: "number",
728
+ value: rule.from
729
+ })] }),
730
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
731
+ htmlFor: `${id}-to`,
732
+ children: "To %"
733
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
734
+ className: "min-h-11 tabular-nums sm:min-h-9",
735
+ id: `${id}-to`,
736
+ inputMode: "decimal",
737
+ max: 100,
738
+ min: 0,
739
+ onChange: (event) => onChange({
740
+ ...rule,
741
+ to: Number(event.target.value)
742
+ }),
743
+ step: .1,
744
+ type: "number",
745
+ value: rule.to
746
+ })] }),
747
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
748
+ htmlFor: `${id}-seed`,
749
+ children: "Seed"
750
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
751
+ className: "flex gap-2",
752
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
753
+ autoComplete: "off",
754
+ className: "min-h-11 font-mono sm:min-h-9",
755
+ id: `${id}-seed`,
756
+ onChange: (event) => onChange({
757
+ ...rule,
758
+ seed: event.target.value
759
+ }),
760
+ value: rule.seed
761
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
762
+ "aria-label": "New seed",
763
+ className: "size-11 sm:size-9",
764
+ onClick: () => onChange({
765
+ ...rule,
766
+ seed: createSeed()
767
+ }),
768
+ size: "icon",
769
+ type: "button",
770
+ variant: "outline",
771
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCw, {})
772
+ })]
773
+ })] })
774
+ ]
775
+ });
776
+ case "fingerprint": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ListInput, {
777
+ id: `${id}-hashes`,
778
+ multiline: true,
779
+ onChange: (next) => onChange({
780
+ ...rule,
781
+ hashes: next
782
+ }),
783
+ placeholder: "One fingerprint hash per line",
784
+ values: rule.hashes
785
+ });
786
+ case "dateTime": {
787
+ const change = (field, instant) => {
788
+ const { [field]: _previous, ...rest } = rule;
789
+ onChange(instant === void 0 ? rest : {
790
+ ...rest,
791
+ [field]: instant
792
+ });
793
+ };
794
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
795
+ className: "grid gap-3 sm:grid-cols-2",
796
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DateTimeField, {
797
+ id: `${id}-from`,
798
+ label: "From",
799
+ onChange: (instant) => change("from", instant),
800
+ presets: startPresets(),
801
+ value: rule.from
802
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DateTimeField, {
803
+ id: `${id}-until`,
804
+ label: "Until",
805
+ onChange: (instant) => change("to", instant),
806
+ presets: endPresets(rule.from),
807
+ value: rule.to
808
+ })]
809
+ });
810
+ }
811
+ }
812
+ }
813
+ var RULE_HINTS = {
814
+ platform: "Devices on any of these platforms.",
815
+ channel: "Devices on any of these channels, separated by commas.",
816
+ appVersion: "A semver range of native app versions, as release targets use: 2.x, >=1.4.0 <2.0.0.",
817
+ percent: "Installs whose numeric cohort lands in this share. Rules with one seed split the same order, so 0–10 and 10–20 never overlap.",
818
+ cohort: "Numeric cohorts from 1 to 1000, or custom cohort names.",
819
+ fingerprint: "Native builds with any of these fingerprints.",
820
+ dateTime: "Leave either end empty to keep it open. Devices get the change at their next fetch."
821
+ };
822
+ /** Adds or edits a condition: a name and the rules a device must all match. */
823
+ function ConditionDialog({ open, onOpenChange, condition, takenNames, onSave }) {
824
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog$1, {
825
+ onOpenChange,
826
+ open,
827
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogContent, {
828
+ className: "max-h-[calc(100svh-2rem)] overflow-y-auto sm:max-w-xl",
829
+ children: open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConditionForm, {
830
+ condition,
831
+ onCancel: () => onOpenChange(false),
832
+ onSave: (next) => {
833
+ onSave(next);
834
+ onOpenChange(false);
835
+ },
836
+ takenNames
837
+ }) : null
838
+ })
839
+ });
840
+ }
841
+ function ConditionForm({ condition, takenNames, onSave, onCancel }) {
842
+ const id = (0, import_react.useId)();
843
+ const { dateTimeText, timeZone } = useDateTimeText();
844
+ const [name, setName] = (0, import_react.useState)(condition?.name ?? "");
845
+ const [rules, setRules] = (0, import_react.useState)(condition === null ? [createRule("platform")] : [...condition.rules]);
846
+ const [submitted, setSubmitted] = (0, import_react.useState)(false);
847
+ const suggestedName = suggestConditionName(rules, takenNames, dateTimeText);
848
+ const savedName = name.trim().length === 0 ? suggestedName : name;
849
+ const nameError = conditionNameError(savedName, takenNames);
850
+ const ruleErrors = rules.map(ruleError);
851
+ const unused = RULE_TYPES.filter((type) => !rules.some((rule) => rule.type === type));
852
+ const invalid = nameError !== null || rules.length === 0 || ruleErrors.some((error) => error !== null);
853
+ const submit = (event) => {
854
+ event.preventDefault();
855
+ event.stopPropagation();
856
+ setSubmitted(true);
857
+ if (invalid) return;
858
+ onSave({
859
+ name: savedName.trim(),
860
+ rules: rules.map((rule) => rule.type === "appVersion" ? {
861
+ ...rule,
862
+ range: rule.range.trim()
863
+ } : rule)
864
+ });
865
+ };
866
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", {
867
+ className: "flex flex-col gap-4",
868
+ noValidate: true,
869
+ onSubmit: submit,
870
+ children: [
871
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogTitle, { children: condition === null ? "Add condition" : "Edit condition" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogDescription, { children: "A device matches when it matches every rule." })] }),
872
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(FieldGroup, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, {
873
+ "data-invalid": submitted && nameError !== null || void 0,
874
+ children: [
875
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
876
+ htmlFor: `${id}-name`,
877
+ children: "Name"
878
+ }),
879
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
880
+ autoComplete: "off",
881
+ className: "min-h-11 sm:min-h-9",
882
+ id: `${id}-name`,
883
+ maxLength: 101,
884
+ onChange: (event) => setName(event.target.value),
885
+ placeholder: suggestedName || "Beta testers on iOS",
886
+ value: name
887
+ }),
888
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldDescription, { children: "Optional: without one, it is named after its rules." }),
889
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldError, { children: submitted ? nameError : null })
890
+ ]
891
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(FieldSet, { children: [
892
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLegend, {
893
+ variant: "label",
894
+ children: "Rules"
895
+ }),
896
+ rules.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldError, { children: submitted ? "Add at least one rule." : null }) : null,
897
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", {
898
+ className: "flex flex-col gap-3",
899
+ children: rules.map((rule, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", {
900
+ className: "rounded-lg border p-3",
901
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, {
902
+ "data-invalid": submitted && ruleErrors[index] !== null || void 0,
903
+ children: [
904
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
905
+ className: "flex items-center justify-between gap-2",
906
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
907
+ className: "text-sm font-medium",
908
+ children: RULE_TYPE_LABELS[rule.type]
909
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
910
+ "aria-label": `Remove the ${RULE_TYPE_LABELS[rule.type].toLowerCase()} rule`,
911
+ className: "size-11 sm:size-7",
912
+ onClick: () => setRules(rules.filter((_, at) => at !== index)),
913
+ size: "icon-sm",
914
+ type: "button",
915
+ variant: "ghost",
916
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2, {})
917
+ })]
918
+ }),
919
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RuleFields, {
920
+ onChange: (next) => setRules(rules.map((existing, at) => at === index ? next : existing)),
921
+ rule
922
+ }),
923
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldDescription, { children: rule.type === "dateTime" ? `In your time zone, ${timeZone}. ${RULE_HINTS.dateTime}` : RULE_HINTS[rule.type] }),
924
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldError, { children: submitted ? ruleErrors[index] : null })
925
+ ]
926
+ })
927
+ }, rule.type))
928
+ }),
929
+ unused.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
930
+ items: Object.fromEntries(unused.map((type) => [type, RULE_TYPE_LABELS[type]])),
931
+ onValueChange: (value) => {
932
+ if (value !== null) setRules([...rules, createRule(value)]);
933
+ },
934
+ value: null,
935
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectTrigger, {
936
+ "aria-label": "Add a rule",
937
+ className: "min-h-11 w-full sm:min-h-9 sm:w-56",
938
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, {
939
+ "aria-hidden": "true",
940
+ className: "size-3.5"
941
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, { placeholder: "Add a rule" })]
942
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectGroup, { children: unused.map((type) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
943
+ value: type,
944
+ children: RULE_TYPE_LABELS[type]
945
+ }, type)) }) })]
946
+ }) : null
947
+ ] })] }),
948
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
949
+ onClick: onCancel,
950
+ type: "button",
951
+ variant: "outline",
952
+ children: "Cancel"
953
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
954
+ type: "submit",
955
+ children: condition === null ? "Add condition" : "Save condition"
956
+ })] })
957
+ ]
958
+ });
959
+ }
960
+ /** A condition's rules in a few words, unless its name already says them. */
961
+ function RuleSummary({ condition, dateTimeText }) {
962
+ const text = condition.rules.map((rule) => describeRule(rule, dateTimeText)).join(" · ");
963
+ return text === condition.name ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
964
+ className: "text-xs text-muted-foreground",
965
+ children: text
966
+ });
967
+ }
968
+ /** The draft's conditions in priority order: the first match decides a value. */
969
+ function ConditionsCard({ template, onSaveCondition, onRemoveCondition, onMoveCondition }) {
970
+ const [editing, setEditing] = (0, import_react.useState)(null);
971
+ const [removing, setRemoving] = (0, import_react.useState)(null);
972
+ const { dateTimeText } = useDateTimeText();
973
+ const { conditions } = template;
974
+ const editingName = editing?.name ?? null;
975
+ const removingUsers = removing === null ? [] : parametersUsing(template, removing);
976
+ const remove = (name) => {
977
+ if (parametersUsing(template, name).length === 0) onRemoveCondition(name);
978
+ else setRemoving(name);
979
+ };
980
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, {
981
+ className: "overflow-hidden shadow-sm",
982
+ children: [
983
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardHeader, {
984
+ className: "flex-row flex-wrap items-center justify-between gap-3 space-y-0 border-b px-4 py-3 sm:px-6",
985
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
986
+ className: "min-w-0",
987
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
988
+ className: "text-sm",
989
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "Conditions" })
990
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardDescription, {
991
+ className: "text-xs",
992
+ children: "Listed by priority: when several match a device, the first wins."
993
+ })]
994
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
995
+ className: "min-h-11 sm:min-h-7",
996
+ onClick: () => setEditing({ name: null }),
997
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { "data-icon": "inline-start" }), "Add condition"]
998
+ })]
999
+ }),
1000
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, {
1001
+ className: "p-0",
1002
+ children: conditions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Empty, {
1003
+ className: "py-10",
1004
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(EmptyHeader, { children: [
1005
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyMedia, {
1006
+ variant: "icon",
1007
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GitBranch, {})
1008
+ }),
1009
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyTitle, { children: "No conditions" }),
1010
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyDescription, { children: "Add a condition to give some devices other values: a platform, channel, app version range, cohort, or a share of installs." })
1011
+ ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
1012
+ className: "min-h-11 sm:min-h-7",
1013
+ onClick: () => setEditing({ name: null }),
1014
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { "data-icon": "inline-start" }), "Add condition"]
1015
+ }) })]
1016
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", {
1017
+ "aria-label": "Conditions",
1018
+ className: "divide-y",
1019
+ children: conditions.map((condition, index) => {
1020
+ const users = parametersUsing(template, condition.name);
1021
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
1022
+ className: "grid gap-3 px-4 py-3 sm:px-6 md:grid-cols-[auto_minmax(0,1fr)_auto] md:items-center",
1023
+ children: [
1024
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1025
+ "aria-label": `Priority ${index + 1}`,
1026
+ className: "flex size-6 items-center justify-center rounded-md bg-muted text-xs font-medium tabular-nums",
1027
+ children: index + 1
1028
+ }),
1029
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1030
+ className: "min-w-0",
1031
+ children: [
1032
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1033
+ className: "truncate text-sm font-medium",
1034
+ children: condition.name
1035
+ }),
1036
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RuleSummary, {
1037
+ condition,
1038
+ dateTimeText
1039
+ }),
1040
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1041
+ className: "mt-1 text-xs text-muted-foreground",
1042
+ children: users.length === 0 ? "No parameter uses it yet" : `Sets ${users.length === 1 ? "1 parameter" : `${users.length} parameters`}: ${users.slice(0, 3).join(", ")}${users.length > 3 ? "…" : ""}`
1043
+ })
1044
+ ]
1045
+ }),
1046
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1047
+ className: "flex flex-wrap gap-1 md:justify-end",
1048
+ children: [
1049
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1050
+ "aria-label": `Move ${condition.name} up`,
1051
+ className: "size-11 md:size-7",
1052
+ disabled: index === 0,
1053
+ onClick: () => onMoveCondition(index, -1),
1054
+ size: "icon",
1055
+ variant: "ghost",
1056
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowUp, {})
1057
+ }),
1058
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1059
+ "aria-label": `Move ${condition.name} down`,
1060
+ className: "size-11 md:size-7",
1061
+ disabled: index === conditions.length - 1,
1062
+ onClick: () => onMoveCondition(index, 1),
1063
+ size: "icon",
1064
+ variant: "ghost",
1065
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowDown, {})
1066
+ }),
1067
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
1068
+ "aria-label": `Edit ${condition.name}`,
1069
+ className: "min-h-11 md:min-h-7",
1070
+ onClick: () => setEditing({ name: condition.name }),
1071
+ size: "sm",
1072
+ variant: "outline",
1073
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pencil, { "data-icon": "inline-start" }), "Edit"]
1074
+ }),
1075
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1076
+ "aria-label": `Delete ${condition.name}`,
1077
+ className: "size-11 md:size-7",
1078
+ onClick: () => remove(condition.name),
1079
+ size: "icon",
1080
+ variant: "ghost",
1081
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2, {})
1082
+ })
1083
+ ]
1084
+ })
1085
+ ]
1086
+ }, condition.name);
1087
+ })
1088
+ })
1089
+ }),
1090
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConditionDialog, {
1091
+ condition: editingName === null ? null : conditions.find(({ name }) => name === editingName) ?? null,
1092
+ onOpenChange: (open) => {
1093
+ if (!open) setEditing(null);
1094
+ },
1095
+ onSave: (condition) => onSaveCondition(editingName, condition),
1096
+ open: editing !== null,
1097
+ takenNames: conditions.map(({ name }) => name).filter((name) => name !== editingName)
1098
+ }),
1099
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialog$1, {
1100
+ onOpenChange: (open) => {
1101
+ if (!open) setRemoving(null);
1102
+ },
1103
+ open: removing !== null,
1104
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogTitle, { children: [
1105
+ "Delete ",
1106
+ removing,
1107
+ "?"
1108
+ ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogDescription, { children: [
1109
+ removingUsers.length === 1 ? "1 parameter has a value for it" : `${removingUsers.length} parameters have a value for it`,
1110
+ " ",
1111
+ "(",
1112
+ removingUsers.join(", "),
1113
+ "). Those values are removed too, so matching devices get the default value after you publish."
1114
+ ] })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogCancel, { children: "Cancel" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogAction, {
1115
+ onClick: (event) => {
1116
+ event.preventDefault();
1117
+ if (removing !== null) onRemoveCondition(removing);
1118
+ setRemoving(null);
1119
+ },
1120
+ variant: "destructive",
1121
+ children: "Delete condition"
1122
+ })] })] })
1123
+ })
1124
+ ]
1125
+ });
1126
+ }
1127
+ /** A value's starting text for a type: what a new value of that type holds. */
1128
+ var initialValueText = (valueType) => valueType === "BOOLEAN" ? "false" : valueType === "NUMBER" ? "0" : valueType === "JSON" ? "{}" : "";
1129
+ /** The problem with a value, or null; an in-app default has none. */
1130
+ var parameterValueError = (valueType, value) => "value" in value ? valueTextError(valueType, value.value) : null;
1131
+ /**
1132
+ * Edits one value of a parameter, by its type, with the option to leave the
1133
+ * app's in-app default in place.
1134
+ */
1135
+ function ValueInput({ id, label, valueType, value, onChange, showErrors }) {
1136
+ const inAppDefault = !("value" in value);
1137
+ const text = "value" in value ? value.value : "";
1138
+ const error = showErrors ? parameterValueError(valueType, value) : null;
1139
+ const setText = (next) => onChange({ value: next });
1140
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, {
1141
+ "data-invalid": error !== null || void 0,
1142
+ children: [
1143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1144
+ className: "flex flex-wrap items-center justify-between gap-2",
1145
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1146
+ htmlFor: id,
1147
+ id: `${id}-label`,
1148
+ children: label
1149
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1150
+ className: "flex min-h-11 items-center gap-2 text-xs text-muted-foreground sm:min-h-0",
1151
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch$1, {
1152
+ checked: inAppDefault,
1153
+ id: `${id}-in-app-default`,
1154
+ onCheckedChange: (checked) => onChange(checked ? { useInAppDefault: true } : { value: initialValueText(valueType) })
1155
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", {
1156
+ htmlFor: `${id}-in-app-default`,
1157
+ children: "Use in-app default"
1158
+ })]
1159
+ })]
1160
+ }),
1161
+ inAppDefault ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
1162
+ className: "rounded-md border border-dashed px-3 py-2 text-xs text-muted-foreground",
1163
+ id,
1164
+ children: [
1165
+ "Devices keep the default set in the app's",
1166
+ " ",
1167
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("code", {
1168
+ className: "font-mono",
1169
+ children: [
1170
+ "remoteConfig(",
1171
+ "{ defaults }",
1172
+ ")"
1173
+ ]
1174
+ }),
1175
+ "."
1176
+ ]
1177
+ }) : valueType === "BOOLEAN" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ToggleGroup$1, {
1178
+ "aria-labelledby": `${id}-label`,
1179
+ id,
1180
+ onValueChange: (values) => {
1181
+ const [next] = values;
1182
+ if (next === "true" || next === "false") setText(next);
1183
+ },
1184
+ value: [text],
1185
+ variant: "outline",
1186
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToggleGroupItem, {
1187
+ className: "min-h-11 px-4 sm:min-h-8",
1188
+ value: "true",
1189
+ children: "true"
1190
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToggleGroupItem, {
1191
+ className: "min-h-11 px-4 sm:min-h-8",
1192
+ value: "false",
1193
+ children: "false"
1194
+ })]
1195
+ }) : valueType === "NUMBER" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
1196
+ autoComplete: "off",
1197
+ className: "min-h-11 font-mono sm:min-h-9",
1198
+ id,
1199
+ inputMode: "decimal",
1200
+ onChange: (event) => setText(event.target.value),
1201
+ value: text
1202
+ }) : valueType === "JSON" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1203
+ className: "flex flex-col gap-2",
1204
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
1205
+ className: "min-h-24 font-mono text-xs",
1206
+ id,
1207
+ onChange: (event) => setText(event.target.value),
1208
+ spellCheck: false,
1209
+ value: text
1210
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
1211
+ className: "self-start",
1212
+ disabled: valueTextError("JSON", text) !== null,
1213
+ onClick: () => setText(JSON.stringify(JSON.parse(text), null, 2)),
1214
+ size: "sm",
1215
+ type: "button",
1216
+ variant: "outline",
1217
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Braces, { "data-icon": "inline-start" }), "Format JSON"]
1218
+ })]
1219
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
1220
+ className: "min-h-11 text-sm sm:min-h-9",
1221
+ id,
1222
+ onChange: (event) => setText(event.target.value),
1223
+ rows: 2,
1224
+ value: text
1225
+ }),
1226
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldError, { children: error })
1227
+ ]
1228
+ });
1229
+ }
1230
+ var VALUE_TYPES = Object.keys(VALUE_TYPE_LABELS);
1231
+ /** How the app reads a value of each type; JSON arrives as text to parse. */
1232
+ var READ_METHODS = {
1233
+ STRING: "getString",
1234
+ NUMBER: "getNumber",
1235
+ BOOLEAN: "getBoolean",
1236
+ JSON: "getString"
1237
+ };
1238
+ /** A value converted to another type: kept when it still fits, reset when not. */
1239
+ var retype = (value, valueType) => "value" in value && parameterValueError(valueType, value) !== null ? { value: initialValueText(valueType) } : value;
1240
+ /**
1241
+ * Adds or edits a parameter: its key, type, default value, and a value for
1242
+ * any of the template's conditions, which it can also add on the spot.
1243
+ */
1244
+ function ParameterDialog({ open, onOpenChange, parameterKey, parameter, takenKeys, conditions, onSave, onAddCondition }) {
1245
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog$1, {
1246
+ onOpenChange,
1247
+ open,
1248
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogContent, {
1249
+ className: "max-h-[calc(100svh-2rem)] overflow-y-auto data-nested-dialog-open:after:absolute data-nested-dialog-open:after:inset-0 data-nested-dialog-open:after:rounded-xl data-nested-dialog-open:after:bg-background/70 sm:max-w-2xl",
1250
+ children: open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ParameterForm, {
1251
+ conditions,
1252
+ onAddCondition,
1253
+ onCancel: () => onOpenChange(false),
1254
+ onSave: (key, next) => {
1255
+ onSave(key, next);
1256
+ onOpenChange(false);
1257
+ },
1258
+ parameter,
1259
+ parameterKey,
1260
+ takenKeys
1261
+ }) : null
1262
+ })
1263
+ });
1264
+ }
1265
+ function ParameterForm({ parameterKey, parameter, takenKeys, conditions, onSave, onCancel, onAddCondition }) {
1266
+ const id = (0, import_react.useId)();
1267
+ const { dateTimeText } = useDateTimeText();
1268
+ const [key, setKey] = (0, import_react.useState)(parameterKey ?? "");
1269
+ const [valueType, setValueType] = (0, import_react.useState)(parameter?.valueType ?? "STRING");
1270
+ const [description, setDescription] = (0, import_react.useState)(parameter?.description ?? "");
1271
+ const descriptionTooLong = description.trim().length > 256;
1272
+ const [defaultValue, setDefaultValue] = (0, import_react.useState)(parameter?.defaultValue ?? { value: "" });
1273
+ const [conditionalValues, setConditionalValues] = (0, import_react.useState)(parameter?.conditionalValues ?? {});
1274
+ const [submitted, setSubmitted] = (0, import_react.useState)(false);
1275
+ const [addingCondition, setAddingCondition] = (0, import_react.useState)(false);
1276
+ const keyError = parameterKeyError(key.trim(), takenKeys);
1277
+ const valued = conditions.filter(({ name }) => conditionalValueFor(conditionalValues, name) !== void 0);
1278
+ const unvalued = conditions.filter(({ name }) => conditionalValueFor(conditionalValues, name) === void 0);
1279
+ const invalid = keyError !== null || descriptionTooLong || [defaultValue, ...Object.values(conditionalValues)].some((value) => parameterValueError(valueType, value) !== null);
1280
+ const changeType = (next) => {
1281
+ setValueType(next);
1282
+ setDefaultValue(retype(defaultValue, next));
1283
+ setConditionalValues(Object.fromEntries(Object.entries(conditionalValues).map(([name, value]) => [name, retype(value, next)])));
1284
+ };
1285
+ const addValueFor = (name) => setConditionalValues({
1286
+ ...conditionalValues,
1287
+ [name]: { value: initialValueText(valueType) }
1288
+ });
1289
+ const submit = (event) => {
1290
+ event.preventDefault();
1291
+ setSubmitted(true);
1292
+ if (invalid) return;
1293
+ const kept = Object.fromEntries(valued.map(({ name }) => [name, conditionalValueFor(conditionalValues, name)]));
1294
+ onSave(key.trim(), {
1295
+ valueType,
1296
+ defaultValue,
1297
+ ...Object.keys(kept).length === 0 ? {} : { conditionalValues: kept },
1298
+ ...description.trim().length === 0 ? {} : { description: description.trim() }
1299
+ });
1300
+ };
1301
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", {
1302
+ className: "flex flex-col gap-4",
1303
+ noValidate: true,
1304
+ onSubmit: submit,
1305
+ children: [
1306
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogTitle, { children: parameterKey === null ? "Add parameter" : "Edit parameter" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogDescription, { children: [
1307
+ "The app reads it with",
1308
+ " ",
1309
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("code", {
1310
+ className: "font-mono",
1311
+ children: [
1312
+ "hotUpdater.remoteConfig.",
1313
+ READ_METHODS[valueType],
1314
+ "(\"",
1315
+ key.trim() || "key",
1316
+ "\")"
1317
+ ]
1318
+ }),
1319
+ ", which is null when neither the server nor the app's defaults have a value."
1320
+ ] })] }),
1321
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(FieldGroup, { children: [
1322
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1323
+ className: "grid gap-4 sm:grid-cols-[minmax(0,1fr)_11rem]",
1324
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, {
1325
+ "data-invalid": submitted && keyError !== null || void 0,
1326
+ children: [
1327
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1328
+ htmlFor: `${id}-key`,
1329
+ children: "Key"
1330
+ }),
1331
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
1332
+ autoComplete: "off",
1333
+ className: "min-h-11 font-mono sm:min-h-9",
1334
+ id: `${id}-key`,
1335
+ maxLength: 257,
1336
+ onChange: (event) => setKey(event.target.value),
1337
+ placeholder: "welcome_message",
1338
+ spellCheck: false,
1339
+ value: key
1340
+ }),
1341
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldError, { children: submitted ? keyError : null })
1342
+ ]
1343
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1344
+ htmlFor: `${id}-type`,
1345
+ children: "Type"
1346
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
1347
+ items: VALUE_TYPE_LABELS,
1348
+ onValueChange: (value) => {
1349
+ if (value !== null) changeType(value);
1350
+ },
1351
+ value: valueType,
1352
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, {
1353
+ className: "min-h-11 w-full sm:min-h-9",
1354
+ id: `${id}-type`,
1355
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, {})
1356
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectGroup, { children: VALUE_TYPES.map((type) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
1357
+ value: type,
1358
+ children: VALUE_TYPE_LABELS[type]
1359
+ }, type)) }) })]
1360
+ })] })]
1361
+ }),
1362
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, {
1363
+ "data-invalid": descriptionTooLong || void 0,
1364
+ children: [
1365
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1366
+ htmlFor: `${id}-description`,
1367
+ children: "Description"
1368
+ }),
1369
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
1370
+ autoComplete: "off",
1371
+ className: "min-h-11 sm:min-h-9",
1372
+ id: `${id}-description`,
1373
+ onChange: (event) => setDescription(event.target.value),
1374
+ placeholder: "Optional: what the app does with it",
1375
+ value: description
1376
+ }),
1377
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldError, { children: descriptionTooLong ? "Use 256 characters or fewer." : null })
1378
+ ]
1379
+ }),
1380
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ValueInput, {
1381
+ id: `${id}-default`,
1382
+ label: "Default value",
1383
+ onChange: setDefaultValue,
1384
+ showErrors: submitted,
1385
+ value: defaultValue,
1386
+ valueType
1387
+ }),
1388
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(FieldSet, { children: [
1389
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLegend, {
1390
+ variant: "label",
1391
+ children: "Conditional values"
1392
+ }),
1393
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldDescription, { children: "Devices that match a condition get its value. When several match, the first in the Conditions list wins." }),
1394
+ valued.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
1395
+ className: "flex flex-col gap-3",
1396
+ children: valued.map((condition) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
1397
+ className: "rounded-lg border p-3",
1398
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1399
+ className: "mb-2 flex items-start justify-between gap-2",
1400
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1401
+ className: "min-w-0",
1402
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1403
+ className: "truncate text-sm font-medium",
1404
+ children: condition.name
1405
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RuleSummary, {
1406
+ condition,
1407
+ dateTimeText
1408
+ })]
1409
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1410
+ "aria-label": `Remove the value for ${condition.name}`,
1411
+ className: "size-11 sm:size-7",
1412
+ onClick: () => {
1413
+ const { [condition.name]: _removed, ...rest } = conditionalValues;
1414
+ setConditionalValues(rest);
1415
+ },
1416
+ size: "icon-sm",
1417
+ type: "button",
1418
+ variant: "ghost",
1419
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2, {})
1420
+ })]
1421
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ValueInput, {
1422
+ id: `${id}-${condition.name}`,
1423
+ label: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: ["Value", /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
1424
+ className: "sr-only",
1425
+ children: [" for ", condition.name]
1426
+ })] }),
1427
+ onChange: (value) => setConditionalValues({
1428
+ ...conditionalValues,
1429
+ [condition.name]: value
1430
+ }),
1431
+ showErrors: submitted,
1432
+ value: conditionalValueFor(conditionalValues, condition.name),
1433
+ valueType
1434
+ })]
1435
+ }, condition.name))
1436
+ }) : null,
1437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1438
+ className: "flex flex-wrap gap-2",
1439
+ children: [unvalued.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
1440
+ items: Object.fromEntries(unvalued.map(({ name }) => [name, name])),
1441
+ onValueChange: (value) => {
1442
+ if (value !== null) addValueFor(value);
1443
+ },
1444
+ value: null,
1445
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectTrigger, {
1446
+ "aria-label": "Add a value for a condition",
1447
+ className: "min-h-11 w-full sm:min-h-9 sm:w-64",
1448
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, {
1449
+ "aria-hidden": "true",
1450
+ className: "size-3.5"
1451
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, { placeholder: "Add a value for a condition" })]
1452
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectGroup, { children: unvalued.map(({ name }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
1453
+ value: name,
1454
+ children: name
1455
+ }, name)) }) })]
1456
+ }) : null, /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
1457
+ className: "min-h-11 sm:min-h-9",
1458
+ onClick: () => setAddingCondition(true),
1459
+ type: "button",
1460
+ variant: "outline",
1461
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { "data-icon": "inline-start" }), "New condition"]
1462
+ })]
1463
+ })
1464
+ ] })
1465
+ ] }),
1466
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1467
+ onClick: onCancel,
1468
+ type: "button",
1469
+ variant: "outline",
1470
+ children: "Cancel"
1471
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1472
+ type: "submit",
1473
+ children: parameterKey === null ? "Add parameter" : "Save parameter"
1474
+ })] })
1475
+ ]
1476
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConditionDialog, {
1477
+ condition: null,
1478
+ onOpenChange: setAddingCondition,
1479
+ onSave: (condition) => {
1480
+ onAddCondition(condition);
1481
+ addValueFor(condition.name);
1482
+ },
1483
+ open: addingCondition,
1484
+ takenNames: conditions.map(({ name }) => name)
1485
+ })] });
1486
+ }
1487
+ /** A value's text in one line, as the list shows it. */
1488
+ function ValueText({ text }) {
1489
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1490
+ className: "block min-w-0 truncate font-mono text-xs text-foreground",
1491
+ title: text,
1492
+ children: text
1493
+ });
1494
+ }
1495
+ function ParameterRow({ parameterKey, parameter, conditions, onEdit, onRemove }) {
1496
+ const conditional = conditions.filter(({ name }) => conditionalValueFor(parameter.conditionalValues, name) !== void 0);
1497
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
1498
+ className: "grid gap-3 px-4 py-3 sm:px-6 md:grid-cols-[minmax(0,15rem)_minmax(0,1fr)_auto] md:items-start",
1499
+ children: [
1500
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1501
+ className: "min-w-0",
1502
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1503
+ className: "flex min-w-0 items-center gap-2",
1504
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1505
+ className: "truncate font-mono text-sm font-medium",
1506
+ children: parameterKey
1507
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
1508
+ className: "shrink-0",
1509
+ variant: "outline",
1510
+ children: VALUE_TYPE_LABELS[parameter.valueType]
1511
+ })]
1512
+ }), parameter.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1513
+ className: "mt-1 text-xs text-muted-foreground",
1514
+ children: parameter.description
1515
+ })]
1516
+ }),
1517
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", {
1518
+ className: "grid min-w-0 grid-cols-[minmax(0,7rem)_minmax(0,1fr)] gap-x-3 gap-y-1.5 text-xs",
1519
+ children: [
1520
+ conditional.map(({ name }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1521
+ className: "contents",
1522
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
1523
+ className: "truncate text-muted-foreground",
1524
+ title: name,
1525
+ children: name
1526
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1527
+ className: "min-w-0",
1528
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ValueText, { text: describeValue(conditionalValueFor(parameter.conditionalValues, name)) })
1529
+ })]
1530
+ }, name)),
1531
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
1532
+ className: "text-muted-foreground",
1533
+ children: "Default"
1534
+ }),
1535
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1536
+ className: "min-w-0",
1537
+ children: "value" in parameter.defaultValue ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ValueText, { text: describeValue(parameter.defaultValue) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1538
+ className: "text-muted-foreground",
1539
+ children: "In-app default"
1540
+ })
1541
+ })
1542
+ ]
1543
+ }),
1544
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1545
+ className: "flex gap-1 md:justify-end",
1546
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
1547
+ "aria-label": `Edit ${parameterKey}`,
1548
+ className: "min-h-11 md:min-h-7",
1549
+ onClick: onEdit,
1550
+ size: "sm",
1551
+ variant: "outline",
1552
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pencil, { "data-icon": "inline-start" }), "Edit"]
1553
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1554
+ "aria-label": `Delete ${parameterKey}`,
1555
+ className: "size-11 md:size-7",
1556
+ onClick: onRemove,
1557
+ size: "icon",
1558
+ variant: "ghost",
1559
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2, {})
1560
+ })]
1561
+ })
1562
+ ]
1563
+ });
1564
+ }
1565
+ /** The draft's parameters, with what each device gets for them. */
1566
+ function ParametersCard({ template, onSaveParameter, onRemoveParameter, onAddCondition }) {
1567
+ const [editing, setEditing] = (0, import_react.useState)(null);
1568
+ const [filter, setFilter] = (0, import_react.useState)("");
1569
+ const keys = Object.keys(template.parameters);
1570
+ const shown = keys.filter((key) => key.toLowerCase().includes(filter.trim().toLowerCase()));
1571
+ const editingKey = editing?.key ?? null;
1572
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, {
1573
+ className: "overflow-hidden shadow-sm",
1574
+ children: [
1575
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardHeader, {
1576
+ className: "flex-row flex-wrap items-center justify-between gap-3 space-y-0 border-b px-4 py-3 sm:px-6",
1577
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
1578
+ className: "text-sm",
1579
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "Parameters" })
1580
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1581
+ className: "flex w-full flex-wrap gap-2 sm:w-auto",
1582
+ children: [keys.length > 5 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(InputGroup, {
1583
+ className: "min-h-11 w-full sm:min-h-8 sm:w-56",
1584
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupAddon, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Search, { "aria-hidden": "true" }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupInput, {
1585
+ "aria-label": "Filter parameters by key",
1586
+ className: "text-base sm:text-xs",
1587
+ onChange: (event) => setFilter(event.target.value),
1588
+ placeholder: "Filter by key",
1589
+ value: filter
1590
+ })]
1591
+ }) : null, /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
1592
+ className: "min-h-11 sm:min-h-7",
1593
+ onClick: () => setEditing({ key: null }),
1594
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { "data-icon": "inline-start" }), "Add parameter"]
1595
+ })]
1596
+ })]
1597
+ }),
1598
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, {
1599
+ className: "p-0",
1600
+ children: keys.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Empty, {
1601
+ className: "py-10",
1602
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(EmptyHeader, { children: [
1603
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyMedia, {
1604
+ variant: "icon",
1605
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlidersHorizontal, {})
1606
+ }),
1607
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyTitle, { children: "No parameters" }),
1608
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyDescription, { children: "Add a parameter, give it a default, then publish to change the app without a new build." })
1609
+ ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
1610
+ className: "min-h-11 sm:min-h-7",
1611
+ onClick: () => setEditing({ key: null }),
1612
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { "data-icon": "inline-start" }), "Add parameter"]
1613
+ }) })]
1614
+ }) : shown.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1615
+ className: "flex flex-col items-center gap-2 px-4 py-10 text-center text-xs text-muted-foreground",
1616
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1617
+ className: "font-medium text-foreground",
1618
+ children: "No matching keys"
1619
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1620
+ onClick: () => setFilter(""),
1621
+ size: "sm",
1622
+ variant: "outline",
1623
+ children: "Clear filter"
1624
+ })]
1625
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
1626
+ "aria-label": "Parameters",
1627
+ className: "divide-y",
1628
+ children: shown.map((key) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ParameterRow, {
1629
+ conditions: template.conditions,
1630
+ onEdit: () => setEditing({ key }),
1631
+ onRemove: () => onRemoveParameter(key),
1632
+ parameter: template.parameters[key],
1633
+ parameterKey: key
1634
+ }, key))
1635
+ })
1636
+ }),
1637
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ParameterDialog, {
1638
+ conditions: template.conditions,
1639
+ onAddCondition,
1640
+ onOpenChange: (open) => {
1641
+ if (!open) setEditing(null);
1642
+ },
1643
+ onSave: (key, parameter) => onSaveParameter(editingKey, key, parameter),
1644
+ open: editing !== null,
1645
+ parameter: editingKey === null ? null : template.parameters[editingKey] ?? null,
1646
+ parameterKey: editingKey,
1647
+ takenKeys: keys.filter((key) => key !== editingKey)
1648
+ })
1649
+ ]
1650
+ });
1651
+ }
1652
+ var INITIAL_DEVICE = {
1653
+ platform: "ios",
1654
+ channel: "production",
1655
+ appVersion: "1.0.0",
1656
+ cohort: "1",
1657
+ fingerprintHash: "",
1658
+ at: ""
1659
+ };
1660
+ /** The device after typing pauses, so each keystroke does not ask the server. */
1661
+ var useSettled = (value, delayMs = 300) => {
1662
+ const [settled, setSettled] = (0, import_react.useState)(value);
1663
+ (0, import_react.useEffect)(() => {
1664
+ const timer = setTimeout(() => setSettled(value), delayMs);
1665
+ return () => clearTimeout(timer);
1666
+ }, [value, delayMs]);
1667
+ return settled;
1668
+ };
1669
+ /** What a device with the context entered gets from the draft. */
1670
+ function PreviewCard({ template, isDraft }) {
1671
+ const id = (0, import_react.useId)();
1672
+ const [device, setDevice] = (0, import_react.useState)(INITIAL_DEVICE);
1673
+ const { at, ...deviceContext } = useSettled(device);
1674
+ const atInstant = fromLocalDateTimeInput(at);
1675
+ const preview = useRemoteConfigPreviewQuery({
1676
+ template,
1677
+ context: {
1678
+ ...deviceContext,
1679
+ ...atInstant === void 0 ? {} : { now: Date.parse(atInstant) }
1680
+ }
1681
+ });
1682
+ const keys = Object.keys(template.parameters);
1683
+ const set = (field, value) => setDevice({
1684
+ ...device,
1685
+ [field]: value
1686
+ });
1687
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, {
1688
+ className: "overflow-hidden shadow-sm",
1689
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardHeader, {
1690
+ className: "border-b px-4 py-3 sm:px-6",
1691
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
1692
+ className: "text-sm",
1693
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "Preview a device" })
1694
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardDescription, {
1695
+ className: "text-xs",
1696
+ children: isDraft ? "The values this device would get once your changes are published, now or at the time you pick." : "The values this device gets from the active version, now or at the time you pick."
1697
+ })]
1698
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardContent, {
1699
+ className: "flex flex-col gap-4 p-4 sm:p-6",
1700
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1701
+ className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3",
1702
+ children: [
1703
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1704
+ htmlFor: `${id}-platform`,
1705
+ children: "Platform"
1706
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
1707
+ items: {
1708
+ ios: "iOS",
1709
+ android: "Android"
1710
+ },
1711
+ onValueChange: (value) => {
1712
+ if (value === "ios" || value === "android") set("platform", value);
1713
+ },
1714
+ value: device.platform,
1715
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, {
1716
+ className: "min-h-11 w-full sm:min-h-9",
1717
+ id: `${id}-platform`,
1718
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectValue, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlatformIcon, {
1719
+ className: "size-3.5",
1720
+ platform: device.platform
1721
+ }), device.platform === "ios" ? "iOS" : "Android"] })
1722
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectGroup, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectItem, {
1723
+ value: "ios",
1724
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlatformIcon, {
1725
+ className: "size-3.5",
1726
+ platform: "ios"
1727
+ }), "iOS"]
1728
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectItem, {
1729
+ value: "android",
1730
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlatformIcon, {
1731
+ className: "size-3.5",
1732
+ platform: "android"
1733
+ }), "Android"]
1734
+ })] }) })]
1735
+ })] }),
1736
+ [
1737
+ [
1738
+ "channel",
1739
+ "Channel",
1740
+ "production"
1741
+ ],
1742
+ [
1743
+ "appVersion",
1744
+ "App version",
1745
+ "1.0.0"
1746
+ ],
1747
+ [
1748
+ "cohort",
1749
+ "Cohort",
1750
+ "1-1000 or a name"
1751
+ ],
1752
+ [
1753
+ "fingerprintHash",
1754
+ "Fingerprint",
1755
+ "Optional"
1756
+ ]
1757
+ ].map(([field, label, placeholder]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1758
+ htmlFor: `${id}-${field}`,
1759
+ children: label
1760
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
1761
+ autoComplete: "off",
1762
+ className: "min-h-11 text-base sm:min-h-9 sm:text-xs",
1763
+ id: `${id}-${field}`,
1764
+ onChange: (event) => set(field, event.target.value),
1765
+ placeholder,
1766
+ spellCheck: false,
1767
+ value: device[field]
1768
+ })] }, field)),
1769
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
1770
+ htmlFor: `${id}-at`,
1771
+ children: "Time"
1772
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(InputGroup, {
1773
+ className: "h-11 sm:h-9",
1774
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupInput, {
1775
+ className: "h-11 text-base tabular-nums sm:h-9 sm:text-xs",
1776
+ id: `${id}-at`,
1777
+ onChange: (event) => set("at", event.target.value),
1778
+ type: "datetime-local",
1779
+ value: device.at
1780
+ }), device.at.length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupAddon, {
1781
+ align: "inline-end",
1782
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroupButton, {
1783
+ className: "min-h-11 sm:min-h-6",
1784
+ onClick: () => set("at", ""),
1785
+ size: "xs",
1786
+ children: "Now"
1787
+ })
1788
+ })]
1789
+ })] })
1790
+ ]
1791
+ }), keys.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Empty, {
1792
+ className: "border py-8",
1793
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(EmptyHeader, { children: [
1794
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyMedia, {
1795
+ variant: "icon",
1796
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Smartphone, {})
1797
+ }),
1798
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyTitle, { children: "Nothing to preview" }),
1799
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyDescription, { children: "Add a parameter, and its value for this device shows here." })
1800
+ ] })
1801
+ }) : preview.data?.status === "invalid" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, {
1802
+ variant: "destructive",
1803
+ children: [
1804
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
1805
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "The draft has problems to fix first" }),
1806
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
1807
+ className: "list-disc pl-4",
1808
+ children: preview.data.issues.slice(0, 5).map((issue) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [issue.path === "" ? "" : `${issue.path}: `, issue.message] }, `${issue.path}:${issue.message}`))
1809
+ }) })
1810
+ ]
1811
+ }) : preview.isError ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, {
1812
+ variant: "destructive",
1813
+ children: [
1814
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
1815
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "The preview couldn't be loaded" }),
1816
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: "Check the connection, then change a field to try again." })
1817
+ ]
1818
+ }) : preview.data === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1819
+ className: "flex flex-col gap-2",
1820
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1821
+ className: "sr-only",
1822
+ children: "Loading the preview"
1823
+ }), keys.slice(0, 4).map((key) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-8 w-full" }, key))]
1824
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dl", {
1825
+ "aria-busy": preview.isFetching || void 0,
1826
+ className: "divide-y rounded-lg border",
1827
+ children: keys.map((key) => {
1828
+ const result = preview.data.status === "ok" ? preview.data.parameters[key] : void 0;
1829
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1830
+ className: "grid gap-1 px-3 py-2.5 sm:grid-cols-[minmax(0,14rem)_minmax(0,1fr)_auto] sm:items-center sm:gap-3",
1831
+ children: [
1832
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
1833
+ className: "truncate font-mono text-xs font-medium",
1834
+ children: key
1835
+ }),
1836
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1837
+ className: "min-w-0 truncate font-mono text-xs",
1838
+ children: result === void 0 ? "…" : result.value === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1839
+ className: "font-sans text-muted-foreground",
1840
+ children: "In-app default"
1841
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1842
+ title: result.value,
1843
+ children: result.value.length === 0 ? "\"\"" : result.value
1844
+ })
1845
+ }),
1846
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: result?.condition === void 0 ? null : result.condition === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
1847
+ variant: "outline",
1848
+ children: "Default value"
1849
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
1850
+ variant: "secondary",
1851
+ children: result.condition
1852
+ }) })
1853
+ ]
1854
+ }, key);
1855
+ })
1856
+ })]
1857
+ })]
1858
+ });
1859
+ }
1860
+ /** A changed key, and each of its fields from before to after. */
1861
+ function ChangedKey({ name, fields }) {
1862
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
1863
+ className: "min-w-0",
1864
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1865
+ className: "break-words font-mono",
1866
+ children: name
1867
+ }), fields.length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
1868
+ className: "mt-0.5 flex flex-col gap-0.5",
1869
+ children: fields.map((change) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
1870
+ className: "flex min-w-0 flex-wrap items-center gap-x-1.5 text-muted-foreground",
1871
+ children: [
1872
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: change.field }),
1873
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1874
+ className: "max-w-full truncate font-mono line-through decoration-muted-foreground/60",
1875
+ title: change.before,
1876
+ children: change.before
1877
+ }),
1878
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowRight, {
1879
+ "aria-hidden": "true",
1880
+ className: "size-3 shrink-0"
1881
+ }),
1882
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1883
+ className: "sr-only",
1884
+ children: "to"
1885
+ }),
1886
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1887
+ className: "max-w-full truncate font-mono text-foreground",
1888
+ title: change.after,
1889
+ children: change.after
1890
+ })
1891
+ ]
1892
+ }, change.field))
1893
+ })]
1894
+ });
1895
+ }
1896
+ function ChangeList({ label, changes, describe }) {
1897
+ const rows = [
1898
+ ["Added", changes.added],
1899
+ ["Changed", changes.changed],
1900
+ ["Removed", changes.removed]
1901
+ ];
1902
+ if (rows.every(([, keys]) => keys.length === 0)) return null;
1903
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1904
+ className: "text-xs font-medium",
1905
+ children: label
1906
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dl", {
1907
+ className: "mt-1 grid grid-cols-[5rem_minmax(0,1fr)] gap-x-3 gap-y-1 text-xs",
1908
+ children: rows.map(([title, keys]) => keys.length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1909
+ className: "contents",
1910
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
1911
+ className: "text-muted-foreground",
1912
+ children: title
1913
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
1914
+ className: "min-w-0",
1915
+ children: title === "Changed" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
1916
+ className: "flex flex-col gap-1.5",
1917
+ children: keys.map((key) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChangedKey, {
1918
+ fields: describe(key),
1919
+ name: key
1920
+ }, key))
1921
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1922
+ className: "break-words font-mono",
1923
+ children: keys.join(", ")
1924
+ })
1925
+ })]
1926
+ }, title))
1927
+ })] });
1928
+ }
1929
+ /** Publishes the draft as the next version, after a summary of what changes. */
1930
+ function PublishDialog({ open, onOpenChange, base, baseVersion, draft, onPublished, onLoadLatest, onPublishOver }) {
1931
+ const id = (0, import_react.useId)();
1932
+ const publish = usePublishRemoteConfigMutation();
1933
+ const [description, setDescription] = (0, import_react.useState)("");
1934
+ const [result, setResult] = (0, import_react.useState)(null);
1935
+ const { dateTimeText } = useDateTimeText();
1936
+ const changes = summarizeChanges(base, draft);
1937
+ const describeParameter = (key) => {
1938
+ const before = base.parameters[key];
1939
+ const after = draft.parameters[key];
1940
+ return before === void 0 || after === void 0 ? [] : describeParameterChange(before, after);
1941
+ };
1942
+ const describeCondition = (name) => {
1943
+ const before = base.conditions.find((it) => it.name === name);
1944
+ const after = draft.conditions.find((it) => it.name === name);
1945
+ return before === void 0 || after === void 0 ? [] : describeConditionChange(before, after, dateTimeText);
1946
+ };
1947
+ const tooLong = description.trim().length > 256;
1948
+ const close = (next) => {
1949
+ if (publish.isPending) return;
1950
+ onOpenChange(next);
1951
+ if (!next) {
1952
+ setResult(null);
1953
+ publish.reset();
1954
+ }
1955
+ };
1956
+ const submit = async (event) => {
1957
+ event.preventDefault();
1958
+ if (tooLong) return;
1959
+ try {
1960
+ const outcome = await publish.mutateAsync({
1961
+ template: draft,
1962
+ baseVersion,
1963
+ ...description.trim().length === 0 ? {} : { description: description.trim() }
1964
+ });
1965
+ setResult(outcome);
1966
+ if (outcome.status === "published") {
1967
+ toast.success(`Published version ${outcome.version.version}`);
1968
+ setDescription("");
1969
+ setResult(null);
1970
+ onOpenChange(false);
1971
+ onPublished();
1972
+ }
1973
+ } catch (error) {
1974
+ toast.error(error instanceof Error ? error.message : "Unable to publish");
1975
+ }
1976
+ };
1977
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog$1, {
1978
+ onOpenChange: close,
1979
+ open,
1980
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogContent, {
1981
+ className: "max-h-[calc(100svh-2rem)] overflow-y-auto sm:max-w-lg",
1982
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", {
1983
+ className: "flex flex-col gap-4",
1984
+ noValidate: true,
1985
+ onSubmit: submit,
1986
+ children: [
1987
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogTitle, { children: "Publish changes" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogDescription, { children: [
1988
+ "This becomes version ",
1989
+ baseVersion + 1,
1990
+ ". Devices get its values on their next fetch; earlier versions stay available to roll back to."
1991
+ ] })] }),
1992
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1993
+ className: "flex flex-col gap-3 rounded-lg border p-3",
1994
+ children: [
1995
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChangeList, {
1996
+ changes: changes.parameters,
1997
+ describe: describeParameter,
1998
+ label: "Parameters"
1999
+ }),
2000
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChangeList, {
2001
+ changes: changes.conditions,
2002
+ describe: describeCondition,
2003
+ label: "Conditions"
2004
+ }),
2005
+ changes.reordered ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
2006
+ className: "text-xs text-muted-foreground",
2007
+ children: "Condition priority changed."
2008
+ }) : null
2009
+ ]
2010
+ }),
2011
+ result?.status === "invalid" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, {
2012
+ variant: "destructive",
2013
+ children: [
2014
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
2015
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "The server refused the template" }),
2016
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
2017
+ className: "list-disc pl-4",
2018
+ children: result.issues.slice(0, 6).map((issue) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [issue.path === "" ? "" : `${issue.path}: `, issue.message] }, `${issue.path}:${issue.message}`))
2019
+ }) })
2020
+ ]
2021
+ }) : null,
2022
+ result?.status === "conflict" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, { children: [
2023
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
2024
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertTitle, { children: [
2025
+ "Version ",
2026
+ result.currentVersion,
2027
+ " was published while you edited"
2028
+ ] }),
2029
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: "Load it to review what changed, or publish yours after it, replacing its values." }),
2030
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2031
+ className: "col-span-full mt-2 flex flex-wrap gap-2",
2032
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2033
+ onClick: () => {
2034
+ close(false);
2035
+ onLoadLatest();
2036
+ },
2037
+ size: "sm",
2038
+ type: "button",
2039
+ variant: "outline",
2040
+ children: ["Load version ", result.currentVersion]
2041
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
2042
+ onClick: () => {
2043
+ onPublishOver(result.currentVersion);
2044
+ setResult(null);
2045
+ },
2046
+ size: "sm",
2047
+ type: "button",
2048
+ variant: "ghost",
2049
+ children: "Publish mine after it"
2050
+ })]
2051
+ })
2052
+ ] }) : null,
2053
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, {
2054
+ "data-invalid": tooLong || void 0,
2055
+ children: [
2056
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldLabel, {
2057
+ htmlFor: `${id}-description`,
2058
+ children: "Description"
2059
+ }),
2060
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
2061
+ autoComplete: "off",
2062
+ className: "min-h-11 sm:min-h-9",
2063
+ id: `${id}-description`,
2064
+ onChange: (event) => setDescription(event.target.value),
2065
+ placeholder: "What changed and why",
2066
+ value: description
2067
+ }),
2068
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldDescription, { children: "Shown in the version history." }),
2069
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldError, { children: tooLong ? "Use 256 characters or fewer." : null })
2070
+ ]
2071
+ }),
2072
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
2073
+ disabled: publish.isPending,
2074
+ onClick: () => close(false),
2075
+ type: "button",
2076
+ variant: "outline",
2077
+ children: "Cancel"
2078
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2079
+ disabled: publish.isPending || result?.status === "conflict",
2080
+ type: "submit",
2081
+ children: [
2082
+ publish.isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, {
2083
+ className: "animate-spin",
2084
+ "data-icon": "inline-start"
2085
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Upload, { "data-icon": "inline-start" }),
2086
+ "Publish version ",
2087
+ baseVersion + 1
2088
+ ]
2089
+ })] })
2090
+ ]
2091
+ })
2092
+ })
2093
+ });
2094
+ }
2095
+ /** A publish time: the date and time, with the exact UTC time on hover. */
2096
+ function PublishedAt({ ms }) {
2097
+ const date = new Date(ms);
2098
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("time", {
2099
+ className: "tabular-nums",
2100
+ dateTime: date.toISOString(),
2101
+ title: `${date.toISOString().replace("T", " ").slice(0, 19)} UTC`,
2102
+ children: (0, import_dayjs_min.default)(ms).format("YYYY/MM/DD HH:mm")
2103
+ });
2104
+ }
2105
+ function VersionDialog({ version, onOpenChange }) {
2106
+ const detail = useRemoteConfigVersionQuery(version);
2107
+ const json = detail.data === null || detail.data === void 0 ? null : JSON.stringify(detail.data.template, null, 2);
2108
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog$1, {
2109
+ onOpenChange,
2110
+ open: version !== null,
2111
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogContent, {
2112
+ className: "max-h-[calc(100svh-2rem)] grid-rows-[auto_minmax(0,1fr)] sm:max-w-2xl",
2113
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogTitle, { children: ["Version ", version] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogDescription, { children: "The template as it was published, in the JSON the admin API takes." })] }), detail.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-48 w-full" }) : detail.isError ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, {
2114
+ variant: "destructive",
2115
+ children: [
2116
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
2117
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "This version couldn't be loaded" }),
2118
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: "Check the connection and try again." }),
2119
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertAction, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2120
+ onClick: () => void detail.refetch(),
2121
+ size: "xs",
2122
+ variant: "outline",
2123
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCw, { "data-icon": "inline-start" }), "Retry"]
2124
+ }) })
2125
+ ]
2126
+ }) : json === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
2127
+ className: "text-sm text-muted-foreground",
2128
+ children: [
2129
+ "Version ",
2130
+ version,
2131
+ " no longer exists."
2132
+ ]
2133
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2134
+ className: "flex min-h-0 flex-col gap-2",
2135
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", {
2136
+ className: "min-h-0 overflow-auto rounded-md bg-muted p-3 font-mono text-xs",
2137
+ children: json
2138
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2139
+ className: "self-end",
2140
+ onClick: () => {
2141
+ navigator.clipboard.writeText(json).then(() => toast.success("Template JSON copied"), () => toast.error("Unable to copy the template"));
2142
+ },
2143
+ size: "sm",
2144
+ variant: "outline",
2145
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Clipboard, { "data-icon": "inline-start" }), "Copy JSON"]
2146
+ })]
2147
+ })]
2148
+ })
2149
+ });
2150
+ }
2151
+ /** Published versions, newest first, each viewable and restorable. */
2152
+ function VersionsCard({ activeVersion, hasDraft, onRolledBack, onStartEditing }) {
2153
+ const versions = useRemoteConfigVersionsQuery({ enabled: true });
2154
+ const rollback = useRollbackRemoteConfigMutation();
2155
+ const [viewing, setViewing] = (0, import_react.useState)(null);
2156
+ const [restoring, setRestoring] = (0, import_react.useState)(null);
2157
+ const rows = versions.data?.pages.flatMap((page) => page.versions) ?? [];
2158
+ const restore = async (event) => {
2159
+ event.preventDefault();
2160
+ if (restoring === null) return;
2161
+ try {
2162
+ const result = await rollback.mutateAsync({
2163
+ version: restoring,
2164
+ baseVersion: activeVersion
2165
+ });
2166
+ if (result.status === "published") {
2167
+ toast.success(`Version ${result.version.version} restores version ${restoring}`);
2168
+ onRolledBack();
2169
+ } else if (result.status === "conflict") toast.error(`Version ${result.currentVersion} was published meanwhile. Review it, then roll back again.`);
2170
+ else toast.error("That version no longer exists.");
2171
+ setRestoring(null);
2172
+ } catch (error) {
2173
+ toast.error(error instanceof Error ? error.message : "Unable to roll back");
2174
+ }
2175
+ };
2176
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, {
2177
+ className: "overflow-hidden shadow-sm",
2178
+ children: [
2179
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, {
2180
+ className: "border-b px-4 py-3 sm:px-6",
2181
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
2182
+ className: "text-sm",
2183
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "Versions" })
2184
+ })
2185
+ }),
2186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, {
2187
+ className: "p-0",
2188
+ children: versions.isError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
2189
+ className: "p-4",
2190
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, {
2191
+ variant: "destructive",
2192
+ children: [
2193
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
2194
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "Versions couldn't be loaded" }),
2195
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: "Check the connection and try again." }),
2196
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertAction, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2197
+ onClick: () => void versions.refetch(),
2198
+ size: "xs",
2199
+ variant: "outline",
2200
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCw, { "data-icon": "inline-start" }), "Retry"]
2201
+ }) })
2202
+ ]
2203
+ })
2204
+ }) : versions.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2205
+ className: "flex flex-col gap-3 p-4 sm:p-6",
2206
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2207
+ className: "sr-only",
2208
+ children: "Loading versions"
2209
+ }), Array.from({ length: 3 }, (_, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-10 w-full" }, index))]
2210
+ }) : rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Empty, {
2211
+ className: "py-10",
2212
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(EmptyHeader, { children: [
2213
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyMedia, {
2214
+ variant: "icon",
2215
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(History, {})
2216
+ }),
2217
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyTitle, { children: "No versions yet" }),
2218
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyDescription, { children: "Your first publish becomes version 1. Every publish after it is kept here, ready to roll back to." })
2219
+ ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
2220
+ className: "min-h-11 sm:min-h-7",
2221
+ onClick: onStartEditing,
2222
+ children: "Add a parameter"
2223
+ }) })]
2224
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", {
2225
+ "aria-label": "Versions",
2226
+ className: "divide-y",
2227
+ children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
2228
+ className: "grid gap-2 px-4 py-3 sm:px-6 md:grid-cols-[minmax(0,1fr)_auto] md:items-center",
2229
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2230
+ className: "min-w-0",
2231
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2232
+ className: "flex flex-wrap items-center gap-2",
2233
+ children: [
2234
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
2235
+ className: "text-sm font-medium tabular-nums",
2236
+ children: ["Version ", row.version]
2237
+ }),
2238
+ row.version === activeVersion ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { children: "Active" }) : null,
2239
+ row.updateType === "ROLLBACK" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge, {
2240
+ variant: "outline",
2241
+ children: ["Rollback to v", row.rollbackSource]
2242
+ }) : null
2243
+ ]
2244
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
2245
+ className: "mt-0.5 text-xs text-muted-foreground",
2246
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PublishedAt, { ms: row.createdAtMs }), row.description === null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [" · ", row.description] })]
2247
+ })]
2248
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2249
+ className: "flex gap-1 md:justify-end",
2250
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2251
+ "aria-label": `View version ${row.version}`,
2252
+ className: "min-h-11 md:min-h-7",
2253
+ onClick: () => setViewing(row.version),
2254
+ size: "sm",
2255
+ variant: "ghost",
2256
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Eye, { "data-icon": "inline-start" }), "View"]
2257
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2258
+ "aria-label": `Roll back to version ${row.version}`,
2259
+ className: "min-h-11 md:min-h-7",
2260
+ disabled: row.version === activeVersion,
2261
+ onClick: () => setRestoring(row.version),
2262
+ size: "sm",
2263
+ variant: "outline",
2264
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RotateCcw, { "data-icon": "inline-start" }), "Roll back"]
2265
+ })]
2266
+ })]
2267
+ }, row.version))
2268
+ }), versions.hasNextPage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
2269
+ className: "border-t px-4 py-3 sm:px-6",
2270
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2271
+ className: "min-h-11 w-full sm:min-h-7 sm:w-auto",
2272
+ disabled: versions.isFetchingNextPage,
2273
+ onClick: () => void versions.fetchNextPage(),
2274
+ variant: "outline",
2275
+ children: [versions.isFetchingNextPage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, {
2276
+ className: "animate-spin",
2277
+ "data-icon": "inline-start"
2278
+ }) : null, "Load older versions"]
2279
+ })
2280
+ }) : null] })
2281
+ }),
2282
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VersionDialog, {
2283
+ onOpenChange: (open) => {
2284
+ if (!open) setViewing(null);
2285
+ },
2286
+ version: viewing
2287
+ }),
2288
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialog$1, {
2289
+ onOpenChange: (open) => {
2290
+ if (!rollback.isPending && !open) setRestoring(null);
2291
+ },
2292
+ open: restoring !== null,
2293
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogTitle, { children: [
2294
+ "Roll back to version ",
2295
+ restoring,
2296
+ "?"
2297
+ ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogDescription, { children: [
2298
+ "Version ",
2299
+ activeVersion + 1,
2300
+ " will publish a copy of version",
2301
+ " ",
2302
+ restoring,
2303
+ ", and devices get its values on their next fetch.",
2304
+ hasDraft ? " Your unpublished changes are discarded." : ""
2305
+ ] })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDialogCancel, {
2306
+ disabled: rollback.isPending,
2307
+ children: "Cancel"
2308
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDialogAction, {
2309
+ disabled: rollback.isPending,
2310
+ onClick: (event) => void restore(event),
2311
+ children: [rollback.isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, {
2312
+ className: "animate-spin",
2313
+ "data-icon": "inline-start"
2314
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RotateCcw, { "data-icon": "inline-start" }), "Roll back"]
2315
+ })] })] })
2316
+ })
2317
+ ]
2318
+ });
2319
+ }
2320
+ import_dayjs_min.default.extend(import_relativeTime.default);
2321
+ var REMOTE_CONFIG_TABS = [
2322
+ "parameters",
2323
+ "conditions",
2324
+ "preview",
2325
+ "versions"
2326
+ ];
2327
+ /**
2328
+ * Remote Config: edit the parameters and conditions of a draft, preview what
2329
+ * a device gets, publish the draft as a version, and roll back to an earlier
2330
+ * one.
2331
+ */
2332
+ function RemoteConfigPage({ tab, onTabChange }) {
2333
+ const active = useRemoteConfigQuery();
2334
+ const [draft, setDraft] = (0, import_react.useState)(null);
2335
+ const [restoreChecked, setRestoreChecked] = (0, import_react.useState)(false);
2336
+ const [restored, setRestored] = (0, import_react.useState)(false);
2337
+ const [publishing, setPublishing] = (0, import_react.useState)(false);
2338
+ const published = active.data;
2339
+ const template = draft?.template ?? published?.template;
2340
+ const dirty = draft !== null && published !== void 0 && !isSameTemplate(draft.template, published.template);
2341
+ const stale = dirty && published !== void 0 && draft.baseVersion !== published.version;
2342
+ (0, import_react.useEffect)(() => {
2343
+ if (restoreChecked || published === void 0) return;
2344
+ setRestoreChecked(true);
2345
+ const stored = readStoredDraft(published);
2346
+ if (stored === null) return;
2347
+ setDraft(stored);
2348
+ setRestored(true);
2349
+ }, [published, restoreChecked]);
2350
+ (0, import_react.useEffect)(() => {
2351
+ if (draft === null || !dirty || storeDraft(draft)) return;
2352
+ const warn = (event) => event.preventDefault();
2353
+ window.addEventListener("beforeunload", warn);
2354
+ return () => window.removeEventListener("beforeunload", warn);
2355
+ }, [draft, dirty]);
2356
+ const clearDraft = () => {
2357
+ setDraft(null);
2358
+ setRestored(false);
2359
+ storeDraft(null);
2360
+ };
2361
+ const edit = (change) => {
2362
+ if (published === void 0 || template === void 0) return;
2363
+ setRestored(false);
2364
+ setDraft({
2365
+ baseVersion: draft?.baseVersion ?? published.version,
2366
+ base: draft?.base ?? published.template,
2367
+ template: change(template)
2368
+ });
2369
+ };
2370
+ const counts = {
2371
+ parameters: template === void 0 ? null : Object.keys(template.parameters).length,
2372
+ conditions: template?.conditions.length ?? null
2373
+ };
2374
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2375
+ className: "flex h-svh min-h-0 flex-col",
2376
+ children: [
2377
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", {
2378
+ className: "sticky top-0 z-10 flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 border-b bg-background px-3 py-3 sm:min-h-12 sm:bg-card/70 sm:px-4 sm:backdrop-blur-sm",
2379
+ children: [
2380
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarTrigger, { className: "-ml-1 size-11 lg:size-7" }),
2381
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2382
+ className: "flex items-center gap-1.5",
2383
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlidersHorizontal, {
2384
+ "aria-hidden": "true",
2385
+ className: "size-3.5 text-muted-foreground"
2386
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
2387
+ className: "text-sm font-medium",
2388
+ children: "Remote Config"
2389
+ })]
2390
+ }),
2391
+ published === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
2392
+ className: "text-xs text-muted-foreground",
2393
+ children: published.version === 0 ? "Nothing published yet" : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
2394
+ "Version ",
2395
+ published.version,
2396
+ published.updatedAtMs === null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [" · published ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("time", {
2397
+ dateTime: new Date(published.updatedAtMs).toISOString(),
2398
+ title: (0, import_dayjs_min.default)(published.updatedAtMs).format("YYYY/MM/DD HH:mm:ss"),
2399
+ children: (0, import_dayjs_min.default)(published.updatedAtMs).fromNow()
2400
+ })] })
2401
+ ] })
2402
+ }),
2403
+ dirty ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2404
+ className: "ml-auto flex items-center gap-2",
2405
+ children: [
2406
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
2407
+ className: "hidden sm:inline-flex",
2408
+ variant: "secondary",
2409
+ children: "Unpublished changes"
2410
+ }),
2411
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2412
+ className: "min-h-11 lg:min-h-7",
2413
+ onClick: clearDraft,
2414
+ variant: "ghost",
2415
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Undo2, { "data-icon": "inline-start" }), "Discard"]
2416
+ }),
2417
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2418
+ className: "min-h-11 lg:min-h-7",
2419
+ onClick: () => setPublishing(true),
2420
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Upload, { "data-icon": "inline-start" }), "Publish changes"]
2421
+ })
2422
+ ]
2423
+ }) : null
2424
+ ]
2425
+ }),
2426
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
2427
+ className: "min-h-0 min-w-0 flex-1 overflow-y-auto bg-muted/5 p-3 sm:p-6",
2428
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2429
+ className: "mx-auto flex w-full max-w-5xl flex-col gap-4",
2430
+ children: [
2431
+ active.isError ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, {
2432
+ variant: "destructive",
2433
+ children: [
2434
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
2435
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "Remote Config couldn't be loaded" }),
2436
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: "Check the connection and try again." }),
2437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertAction, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2438
+ onClick: () => void active.refetch(),
2439
+ size: "xs",
2440
+ variant: "outline",
2441
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCw, { "data-icon": "inline-start" }), "Retry"]
2442
+ }) })
2443
+ ]
2444
+ }) : null,
2445
+ restored && dirty && draft !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, { children: [
2446
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(History, {}),
2447
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: "Your unpublished changes are back" }),
2448
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDescription, { children: [
2449
+ "This browser kept them from your last visit, on version",
2450
+ " ",
2451
+ draft.baseVersion,
2452
+ "."
2453
+ ] }),
2454
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertAction, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
2455
+ onClick: clearDraft,
2456
+ size: "xs",
2457
+ variant: "outline",
2458
+ children: "Discard them"
2459
+ }) })
2460
+ ] }) : null,
2461
+ stale && draft !== null && published !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, { children: [
2462
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, {}),
2463
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertTitle, { children: [
2464
+ "Version ",
2465
+ published.version,
2466
+ " was published while you edited"
2467
+ ] }),
2468
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertDescription, { children: [
2469
+ "Your draft started from version ",
2470
+ draft.baseVersion,
2471
+ ". Discard it to review version ",
2472
+ published.version,
2473
+ ", or publish yours after it."
2474
+ ] }),
2475
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertAction, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
2476
+ onClick: clearDraft,
2477
+ size: "xs",
2478
+ variant: "outline",
2479
+ children: ["Load version ", published.version]
2480
+ }) })
2481
+ ] }) : null,
2482
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tabs$1, {
2483
+ onValueChange: (value) => onTabChange(value),
2484
+ value: tab,
2485
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TabsList, {
2486
+ "aria-label": "Remote Config views",
2487
+ className: "max-w-full group-data-horizontal/tabs:h-auto",
2488
+ children: [
2489
+ [
2490
+ "parameters",
2491
+ "Parameters",
2492
+ counts.parameters
2493
+ ],
2494
+ [
2495
+ "conditions",
2496
+ "Conditions",
2497
+ counts.conditions
2498
+ ],
2499
+ [
2500
+ "preview",
2501
+ "Preview",
2502
+ null
2503
+ ],
2504
+ [
2505
+ "versions",
2506
+ "Versions",
2507
+ null
2508
+ ]
2509
+ ].map(([value, label, count]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TabsTrigger, {
2510
+ className: "min-h-11 flex-none px-3 lg:min-h-7",
2511
+ value,
2512
+ children: [label, count === null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2513
+ className: "tabular-nums text-muted-foreground",
2514
+ children: count
2515
+ })]
2516
+ }, value))
2517
+ }), template === void 0 || published === void 0 ? active.isError ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2518
+ className: "flex flex-col gap-3 pt-2",
2519
+ children: [
2520
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
2521
+ className: "sr-only",
2522
+ children: "Loading Remote Config"
2523
+ }),
2524
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-14 w-full" }),
2525
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-14 w-full" }),
2526
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "h-14 w-full" })
2527
+ ]
2528
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
2529
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TabsContent, {
2530
+ value: "parameters",
2531
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ParametersCard, {
2532
+ onAddCondition: (condition) => edit((current) => upsertCondition(current, null, condition)),
2533
+ onRemoveParameter: (key) => edit((current) => removeParameter(current, key)),
2534
+ onSaveParameter: (previousKey, key, parameter) => edit((current) => upsertParameter(current, previousKey, key, parameter)),
2535
+ template
2536
+ })
2537
+ }),
2538
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TabsContent, {
2539
+ value: "conditions",
2540
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConditionsCard, {
2541
+ onMoveCondition: (index, offset) => edit((current) => moveCondition(current, index, offset)),
2542
+ onRemoveCondition: (name) => edit((current) => removeCondition(current, name)),
2543
+ onSaveCondition: (previousName, condition) => edit((current) => upsertCondition(current, previousName, condition)),
2544
+ template
2545
+ })
2546
+ }),
2547
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TabsContent, {
2548
+ value: "preview",
2549
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PreviewCard, {
2550
+ isDraft: dirty,
2551
+ template
2552
+ })
2553
+ }),
2554
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TabsContent, {
2555
+ value: "versions",
2556
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VersionsCard, {
2557
+ activeVersion: published.version,
2558
+ hasDraft: dirty,
2559
+ onRolledBack: clearDraft,
2560
+ onStartEditing: () => onTabChange("parameters")
2561
+ })
2562
+ })
2563
+ ] })]
2564
+ })
2565
+ ]
2566
+ })
2567
+ }),
2568
+ published !== void 0 && draft !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PublishDialog, {
2569
+ base: published.template,
2570
+ baseVersion: draft.baseVersion,
2571
+ draft: draft.template,
2572
+ onLoadLatest: clearDraft,
2573
+ onOpenChange: setPublishing,
2574
+ onPublishOver: (version) => setDraft({
2575
+ ...draft,
2576
+ baseVersion: version,
2577
+ base: published.template
2578
+ }),
2579
+ onPublished: clearDraft,
2580
+ open: publishing
2581
+ }) : null
2582
+ ]
2583
+ });
2584
+ }
2585
+ var $$splitComponentImporter = () => import("./remote-config-Cc5P4J-r.mjs");
2586
+ var $$splitNotFoundComponentImporter = () => import("./remote-config-CjdxcZ9B.mjs");
2587
+ var Route = createFileRoute("/remote-config")({
2588
+ beforeLoad: ({ context }) => requireConsoleFeature(context.queryClient, "remoteConfig"),
2589
+ notFoundComponent: lazyRouteComponent($$splitNotFoundComponentImporter, "notFoundComponent"),
2590
+ component: lazyRouteComponent($$splitComponentImporter, "component"),
2591
+ validateSearch: (search) => REMOTE_CONFIG_TABS.includes(search.tab) && search.tab !== "parameters" ? { tab: search.tab } : {}
2592
+ });
2593
+ //#endregion
2594
+ export { Route as n, RemoteConfigPage as t };