@hot-updater/console 1.0.0-rc.8 → 1.0.0

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