@withpotter/pages-admin 0.13.0-next.124 → 0.13.0-next.59

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 (260) hide show
  1. package/dist/admin/_money/cash-postings.js +3 -20
  2. package/dist/admin/_money/cash-postings.js.map +1 -1
  3. package/dist/admin/_money/cash.module.css +0 -10
  4. package/dist/admin/_money/finder-format.js +0 -1
  5. package/dist/admin/_money/finder-format.js.map +1 -1
  6. package/dist/admin/_money/format.js +0 -1
  7. package/dist/admin/_money/format.js.map +1 -1
  8. package/dist/admin/_overview/activation-panel.js +6 -6
  9. package/dist/admin/_overview/activation-panel.js.map +1 -1
  10. package/dist/admin/_overview/attention-groups.js +8 -8
  11. package/dist/admin/_overview/attention-groups.js.map +1 -1
  12. package/dist/admin/_overview/attention-panel.js +33 -37
  13. package/dist/admin/_overview/attention-panel.js.map +1 -1
  14. package/dist/admin/_overview/attention.module.css +3 -0
  15. package/dist/admin/_overview/control-room.js +13 -41
  16. package/dist/admin/_overview/control-room.js.map +1 -1
  17. package/dist/admin/_overview/kpi-row.js +7 -7
  18. package/dist/admin/_overview/kpi-row.js.map +1 -1
  19. package/dist/admin/_overview/leaders-panel.js +1 -1
  20. package/dist/admin/_overview/leaders-panel.js.map +1 -1
  21. package/dist/admin/_overview/overdue-outstanding.js +1 -2
  22. package/dist/admin/_overview/overdue-outstanding.js.map +1 -1
  23. package/dist/admin/_overview/portfolio-panel.js +5 -5
  24. package/dist/admin/_overview/portfolio-panel.js.map +1 -1
  25. package/dist/admin/_overview/position-panel.js +12 -8
  26. package/dist/admin/_overview/position-panel.js.map +1 -1
  27. package/dist/admin/_pos/pos-figures.js +5 -18
  28. package/dist/admin/_pos/pos-figures.js.map +1 -1
  29. package/dist/admin/_pos/pos-rules.js +0 -2
  30. package/dist/admin/_pos/pos-rules.js.map +1 -1
  31. package/dist/admin/_pos/pos-words.js +1 -6
  32. package/dist/admin/_pos/pos-words.js.map +1 -1
  33. package/dist/admin/_pos/use-pos.js +1 -70
  34. package/dist/admin/_pos/use-pos.js.map +1 -1
  35. package/dist/admin/_shell/nav-config.js +3 -3
  36. package/dist/admin/_shell/nav-config.js.map +1 -1
  37. package/dist/admin/_shell/nav-filter.js +2 -4
  38. package/dist/admin/_shell/nav-filter.js.map +1 -1
  39. package/dist/admin/_shell/nav-row.js +2 -5
  40. package/dist/admin/_shell/nav-row.js.map +1 -1
  41. package/dist/admin/_shell/sidebar-footer.js +0 -20
  42. package/dist/admin/_shell/sidebar-footer.js.map +1 -1
  43. package/dist/admin/_shell/sidebar-footer.module.css +0 -17
  44. package/dist/admin/_shell/sidebar.js +15 -25
  45. package/dist/admin/_shell/sidebar.js.map +1 -1
  46. package/dist/admin/_shell/sidebar.module.css +0 -69
  47. package/dist/admin/_shell/use-fluid-hover.js +6 -37
  48. package/dist/admin/_shell/use-fluid-hover.js.map +1 -1
  49. package/dist/admin/_ui/admin.css +0 -7
  50. package/dist/admin/_ui/data-table.js +1 -1
  51. package/dist/admin/_ui/data-table.js.map +1 -1
  52. package/dist/admin/_ui/list-card.js +1 -1
  53. package/dist/admin/_ui/list-card.js.map +1 -1
  54. package/dist/admin/_ui/list-card.module.css +16 -2
  55. package/dist/admin/_ui/money-view.js +0 -1
  56. package/dist/admin/_ui/money-view.js.map +1 -1
  57. package/dist/admin/_ui/page-header.js +4 -31
  58. package/dist/admin/_ui/page-header.js.map +1 -1
  59. package/dist/admin/_ui/panel.module.css +10 -0
  60. package/dist/admin/_ui/stat-card.js +2 -11
  61. package/dist/admin/_ui/stat-card.js.map +1 -1
  62. package/dist/admin/_ui/trend.js +2 -2
  63. package/dist/admin/_ui/trend.js.map +1 -1
  64. package/dist/admin/accounts/account-panel.js +6 -49
  65. package/dist/admin/accounts/account-panel.js.map +1 -1
  66. package/dist/admin/accounts/account-panel.module.css +0 -73
  67. package/dist/admin/accounts/account-row.js +2 -11
  68. package/dist/admin/accounts/account-row.js.map +1 -1
  69. package/dist/admin/accounts/account-vitals.js +13 -15
  70. package/dist/admin/accounts/account-vitals.js.map +1 -1
  71. package/dist/admin/accounts/accounts-table.js +8 -39
  72. package/dist/admin/accounts/accounts-table.js.map +1 -1
  73. package/dist/admin/accounts/accounts-workspace.js +2 -2
  74. package/dist/admin/accounts/accounts-workspace.js.map +1 -1
  75. package/dist/admin/accounts/owner-session.js +6 -0
  76. package/dist/admin/accounts/owner-session.js.map +1 -1
  77. package/dist/admin/accounts/rows.module.css +0 -17
  78. package/dist/admin/audit/status-pill.js +0 -6
  79. package/dist/admin/audit/status-pill.js.map +1 -1
  80. package/dist/admin/audit/status-pill.module.css +0 -4
  81. package/dist/admin/commerce/commerce-overview.js +2 -2
  82. package/dist/admin/commerce/commerce-overview.js.map +1 -1
  83. package/dist/admin/commerce/merchant-meta.js +0 -1
  84. package/dist/admin/commerce/merchant-meta.js.map +1 -1
  85. package/dist/admin/commerce/merchant-view.js +2 -2
  86. package/dist/admin/commerce/merchant-view.js.map +1 -1
  87. package/dist/admin/feedback/prompt-operations.module.css +2 -0
  88. package/dist/admin/feedback/prompt-preview.module.css +2 -0
  89. package/dist/admin/hooks.js +1 -57
  90. package/dist/admin/hooks.js.map +1 -1
  91. package/dist/admin/layout.js +2 -8
  92. package/dist/admin/layout.js.map +1 -1
  93. package/dist/admin/page.js +8 -28
  94. package/dist/admin/page.js.map +1 -1
  95. package/dist/admin/payout-tiers/_tier-dialog.js +21 -31
  96. package/dist/admin/payout-tiers/_tier-dialog.js.map +1 -1
  97. package/dist/admin/payout-tiers/tier-preview-result.js +9 -10
  98. package/dist/admin/payout-tiers/tier-preview-result.js.map +1 -1
  99. package/dist/admin/payout-tiers/tiers-table.js +7 -6
  100. package/dist/admin/payout-tiers/tiers-table.js.map +1 -1
  101. package/dist/admin/payout-tiers/tiers-workspace.js +2 -2
  102. package/dist/admin/payout-tiers/tiers-workspace.js.map +1 -1
  103. package/dist/admin/pos/pos-workspace.js +2 -6
  104. package/dist/admin/pos/pos-workspace.js.map +1 -1
  105. package/dist/admin/pos/shifts-tab.js +9 -16
  106. package/dist/admin/pos/shifts-tab.js.map +1 -1
  107. package/dist/admin/revenue/cohort-panel.js +6 -6
  108. package/dist/admin/revenue/cohort-panel.js.map +1 -1
  109. package/dist/admin/revenue/health-panel.js +21 -22
  110. package/dist/admin/revenue/health-panel.js.map +1 -1
  111. package/dist/admin/revenue/plan-mix-panel.js +3 -3
  112. package/dist/admin/revenue/plan-mix-panel.js.map +1 -1
  113. package/dist/admin/revenue/quality-panel.js +19 -19
  114. package/dist/admin/revenue/quality-panel.js.map +1 -1
  115. package/dist/admin/revenue/revenue-kpis.js +50 -111
  116. package/dist/admin/revenue/revenue-kpis.js.map +1 -1
  117. package/dist/admin/revenue/revenue-overview.js +46 -39
  118. package/dist/admin/revenue/revenue-overview.js.map +1 -1
  119. package/dist/admin/revenue/revenue-retention.js +13 -17
  120. package/dist/admin/revenue/revenue-retention.js.map +1 -1
  121. package/dist/admin/revenue/revenue-workspace.js +6 -53
  122. package/dist/admin/revenue/revenue-workspace.js.map +1 -1
  123. package/dist/admin/revenue/subscriptions-panel.js +14 -14
  124. package/dist/admin/revenue/subscriptions-panel.js.map +1 -1
  125. package/dist/admin/revenue/top-tenants-panel.js +4 -4
  126. package/dist/admin/revenue/top-tenants-panel.js.map +1 -1
  127. package/dist/admin/settings/account-tab.js +25 -1
  128. package/dist/admin/settings/account-tab.js.map +1 -1
  129. package/dist/admin/settings/general-tab.js +2 -2
  130. package/dist/admin/settings/general-tab.js.map +1 -1
  131. package/dist/admin/settings/system-status-card.js +30 -0
  132. package/dist/admin/settings/system-status-card.js.map +1 -0
  133. package/dist/admin/support/queue-card.module.css +4 -1
  134. package/dist/admin/tenants/[id]/commerce/format.js +1 -2
  135. package/dist/admin/tenants/[id]/commerce/format.js.map +1 -1
  136. package/dist/admin/tenants/[id]/commerce/recent-orders.js +1 -1
  137. package/dist/admin/tenants/[id]/commerce/recent-orders.js.map +1 -1
  138. package/dist/admin/tenants/[id]/page.js +10 -37
  139. package/dist/admin/tenants/[id]/page.js.map +1 -1
  140. package/dist/admin/tenants/[id]/tenant-layout.module.css +4 -1
  141. package/dist/admin/tenants/[id]/tenant-overview.js +2 -3
  142. package/dist/admin/tenants/[id]/tenant-overview.js.map +1 -1
  143. package/dist/admin/tenants/[id]/tenant-overview.module.css +1 -1
  144. package/dist/admin/tenants/[id]/tenant-pos.js +16 -64
  145. package/dist/admin/tenants/[id]/tenant-pos.js.map +1 -1
  146. package/dist/admin/tenants/[id]/tenant-wallet-card.js +2 -3
  147. package/dist/admin/tenants/[id]/tenant-wallet-card.js.map +1 -1
  148. package/dist/admin/tenants/[id]/verification/document-viewer.js +52 -3
  149. package/dist/admin/tenants/[id]/verification/document-viewer.js.map +1 -1
  150. package/dist/admin/tenants/[id]/verification/document-viewer.module.css +0 -51
  151. package/dist/admin/tenants/[id]/verification/tenant-verification.js +0 -10
  152. package/dist/admin/tenants/[id]/verification/tenant-verification.js.map +1 -1
  153. package/dist/admin/tenants/tenant-list-row.js +1 -2
  154. package/dist/admin/tenants/tenant-list-row.js.map +1 -1
  155. package/dist/admin/tenants/tenants-table.js +2 -3
  156. package/dist/admin/tenants/tenants-table.js.map +1 -1
  157. package/dist/admin/tenants/tenants-workspace.js +2 -2
  158. package/dist/admin/tenants/tenants-workspace.js.map +1 -1
  159. package/dist/admin/verification/verification-page.js +3 -19
  160. package/dist/admin/verification/verification-page.js.map +1 -1
  161. package/dist/admin/verification/verification.module.css +0 -10
  162. package/dist/admin/verification/waiting-case-actions.js +68 -0
  163. package/dist/admin/verification/waiting-case-actions.js.map +1 -0
  164. package/dist/admin/verification/waiting-cases.js +48 -66
  165. package/dist/admin/verification/waiting-cases.js.map +1 -1
  166. package/package.json +5 -5
  167. package/dist/admin/_pos/olive-balance.js +0 -40
  168. package/dist/admin/_pos/olive-balance.js.map +0 -1
  169. package/dist/admin/_pos/olive-state.js +0 -23
  170. package/dist/admin/_pos/olive-state.js.map +0 -1
  171. package/dist/admin/_pos/payments-model.js +0 -55
  172. package/dist/admin/_pos/payments-model.js.map +0 -1
  173. package/dist/admin/_pos/tenant-operators.js +0 -63
  174. package/dist/admin/_pos/tenant-operators.js.map +0 -1
  175. package/dist/admin/_pos/tenant-outlets.js +0 -77
  176. package/dist/admin/_pos/tenant-outlets.js.map +0 -1
  177. package/dist/admin/_pos/tenant-pos-figures.js +0 -82
  178. package/dist/admin/_pos/tenant-pos-figures.js.map +0 -1
  179. package/dist/admin/_pos/tenant-requests.js +0 -64
  180. package/dist/admin/_pos/tenant-requests.js.map +0 -1
  181. package/dist/admin/_pos/tenant-transactions.js +0 -145
  182. package/dist/admin/_pos/tenant-transactions.js.map +0 -1
  183. package/dist/admin/_shell/nav-fold.js +0 -14
  184. package/dist/admin/_shell/nav-fold.js.map +0 -1
  185. package/dist/admin/_shell/nav-group.js +0 -78
  186. package/dist/admin/_shell/nav-group.js.map +0 -1
  187. package/dist/admin/_shell/use-open-groups.js +0 -64
  188. package/dist/admin/_shell/use-open-groups.js.map +0 -1
  189. package/dist/admin/_ui/page-header.module.css +0 -45
  190. package/dist/admin/accounts/account-sessions.js +0 -70
  191. package/dist/admin/accounts/account-sessions.js.map +0 -1
  192. package/dist/admin/accounts/account-sessions.module.css +0 -55
  193. package/dist/admin/accounts/account-stage-filter.js +0 -29
  194. package/dist/admin/accounts/account-stage-filter.js.map +0 -1
  195. package/dist/admin/accounts/account-stage.js +0 -81
  196. package/dist/admin/accounts/account-stage.js.map +0 -1
  197. package/dist/admin/accounts/hint.js +0 -12
  198. package/dist/admin/accounts/hint.js.map +0 -1
  199. package/dist/admin/payout-tiers/requirement-labels.js +0 -70
  200. package/dist/admin/payout-tiers/requirement-labels.js.map +0 -1
  201. package/dist/admin/pos/transactions-tab.js +0 -214
  202. package/dist/admin/pos/transactions-tab.js.map +0 -1
  203. package/dist/admin/revenue/channel-panel.js +0 -49
  204. package/dist/admin/revenue/channel-panel.js.map +0 -1
  205. package/dist/admin/revenue/channel-rows.js +0 -17
  206. package/dist/admin/revenue/channel-rows.js.map +0 -1
  207. package/dist/admin/revenue/download-button.js +0 -40
  208. package/dist/admin/revenue/download-button.js.map +0 -1
  209. package/dist/admin/revenue/earnings/earnings-colors.js +0 -25
  210. package/dist/admin/revenue/earnings/earnings-colors.js.map +0 -1
  211. package/dist/admin/revenue/earnings/earnings-legend.js +0 -75
  212. package/dist/admin/revenue/earnings/earnings-legend.js.map +0 -1
  213. package/dist/admin/revenue/earnings/earnings-summary.js +0 -308
  214. package/dist/admin/revenue/earnings/earnings-summary.js.map +0 -1
  215. package/dist/admin/revenue/earnings/earnings-table.js +0 -143
  216. package/dist/admin/revenue/earnings/earnings-table.js.map +0 -1
  217. package/dist/admin/revenue/earnings/earnings.module.css +0 -201
  218. package/dist/admin/revenue/earnings/use-earnings.js +0 -27
  219. package/dist/admin/revenue/earnings/use-earnings.js.map +0 -1
  220. package/dist/admin/revenue/ledger-model.js +0 -104
  221. package/dist/admin/revenue/ledger-model.js.map +0 -1
  222. package/dist/admin/revenue/ledger.module.css +0 -144
  223. package/dist/admin/revenue/merchant-headline.js +0 -37
  224. package/dist/admin/revenue/merchant-headline.js.map +0 -1
  225. package/dist/admin/revenue/merchant-payments-view.js +0 -234
  226. package/dist/admin/revenue/merchant-payments-view.js.map +0 -1
  227. package/dist/admin/revenue/method-cards.js +0 -40
  228. package/dist/admin/revenue/method-cards.js.map +0 -1
  229. package/dist/admin/revenue/payment-drawer.js +0 -53
  230. package/dist/admin/revenue/payment-drawer.js.map +0 -1
  231. package/dist/admin/revenue/plan-lines.js +0 -36
  232. package/dist/admin/revenue/plan-lines.js.map +0 -1
  233. package/dist/admin/revenue/plan-payment-drawer.js +0 -42
  234. package/dist/admin/revenue/plan-payment-drawer.js.map +0 -1
  235. package/dist/admin/revenue/plans-panel.js +0 -48
  236. package/dist/admin/revenue/plans-panel.js.map +0 -1
  237. package/dist/admin/revenue/platform-fees.js +0 -9
  238. package/dist/admin/revenue/platform-fees.js.map +0 -1
  239. package/dist/admin/revenue/print-summary-button.js +0 -27
  240. package/dist/admin/revenue/print-summary-button.js.map +0 -1
  241. package/dist/admin/revenue/subscriptions-view.js +0 -213
  242. package/dist/admin/revenue/subscriptions-view.js.map +0 -1
  243. package/dist/admin/revenue/summary-download.js +0 -38
  244. package/dist/admin/revenue/summary-download.js.map +0 -1
  245. package/dist/admin/revenue/summary-print.js +0 -82
  246. package/dist/admin/revenue/summary-print.js.map +0 -1
  247. package/dist/admin/revenue/summary-rows.js +0 -44
  248. package/dist/admin/revenue/summary-rows.js.map +0 -1
  249. package/dist/admin/revenue/url-filters.js +0 -36
  250. package/dist/admin/revenue/url-filters.js.map +0 -1
  251. package/dist/admin/revenue/use-url-filters.js +0 -24
  252. package/dist/admin/revenue/use-url-filters.js.map +0 -1
  253. package/dist/admin/tenants/[id]/use-tenant-wallet.js +0 -23
  254. package/dist/admin/tenants/[id]/use-tenant-wallet.js.map +0 -1
  255. package/dist/admin/tenants/[id]/verification/document-file.js +0 -78
  256. package/dist/admin/tenants/[id]/verification/document-file.js.map +0 -1
  257. package/dist/admin/tenants/[id]/verification/sent-files.js +0 -33
  258. package/dist/admin/tenants/[id]/verification/sent-files.js.map +0 -1
  259. package/dist/admin/verification/preview-dialog.js +0 -111
  260. package/dist/admin/verification/preview-dialog.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_shell/nav-filter.ts"],"sourcesContent":["import {\n NAV_GROUPS,\n type NavAddition,\n type NavGroup,\n type NavItem,\n} from './nav-config';\nimport { moduleOn, type AdminModules } from './nav-modules';\n\nexport const isActive = (item: NavItem, pathname: string) =>\n item.href === '/admin'\n ? pathname === '/admin'\n : (item.activeRoutes ?? [item.href]).some((route) =>\n pathname.startsWith(route),\n );\n\nexport interface AdminSessionLike {\n role: string;\n permissions: string[];\n}\n\nexport const canAccess = (session: AdminSessionLike) => (item: NavItem) => {\n if (session.role === 'superadmin') return true;\n\n if (item.superadmin) return false;\n\n return !item.permission || session.permissions.includes(item.permission);\n};\n\n/**\n * The host's pages placed into their groups: after the Box items in a group (or instead of them), or\n * right after the item named by `after`; a new group goes before Platform.\n */\nexport function withAdditions(\n groups: NavGroup[],\n additions: NavAddition[] = [],\n): NavGroup[] {\n const merged = groups.map((g) => ({ ...g, items: [...g.items] }));\n\n additions.forEach(({ group, items, replace, after }) => {\n const found = merged.find((g) => g.label === group);\n\n if (found) {\n if (replace) found.items = [];\n\n const at = after ? found.items.findIndex((item) => item.href === after) : -1;\n\n if (at === -1) found.items.push(...items);\n else found.items.splice(at + 1, 0, ...items);\n\n return;\n }\n\n const platform = merged.findIndex((g) => g.label === 'Platform');\n const at = platform === -1 ? merged.length : platform;\n\n merged.splice(at, 0, { label: group, items: [...items] });\n });\n\n return merged;\n}\n\n/** The groups this admin may see, modules off removed, empty groups dropped. */\nexport function visibleGroups(\n session: AdminSessionLike,\n modules: AdminModules = {},\n additions: NavAddition[] = [],\n): NavGroup[] {\n const allowed = canAccess(session);\n\n return withAdditions(NAV_GROUPS, additions)\n .map((group) => ({\n ...group,\n items: group.items.filter(\n (item) =>\n moduleOn(modules, item.module) &&\n !(item.unlessModule && moduleOn(modules, item.unlessModule)) &&\n allowed(item),\n ),\n }))\n .filter((group) => group.items.length);\n}\n"],"mappings":"AAAA;AAAA,EACE;AAAA,OAIK;AACP,SAAS,gBAAmC;AAErC,MAAM,WAAW,CAAC,MAAe,aACtC,KAAK,SAAS,WACV,aAAa,YACZ,KAAK,gBAAgB,CAAC,KAAK,IAAI,GAAG;AAAA,EAAK,CAAC,UACvC,SAAS,WAAW,KAAK;AAC3B;AAOC,MAAM,YAAY,CAAC,YAA8B,CAAC,SAAkB;AACzE,MAAI,QAAQ,SAAS,aAAc,QAAO;AAE1C,MAAI,KAAK,WAAY,QAAO;AAE5B,SAAO,CAAC,KAAK,cAAc,QAAQ,YAAY,SAAS,KAAK,UAAU;AACzE;AAMO,SAAS,cACd,QACA,YAA2B,CAAC,GAChB;AACZ,QAAM,SAAS,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;AAEhE,YAAU,QAAQ,CAAC,EAAE,OAAO,OAAO,SAAS,MAAM,MAAM;AACtD,UAAM,QAAQ,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,KAAK;AAElD,QAAI,OAAO;AACT,UAAI,QAAS,OAAM,QAAQ,CAAC;AAE5B,YAAMA,MAAK,QAAQ,MAAM,MAAM,UAAU,CAAC,SAAS,KAAK,SAAS,KAAK,IAAI;AAE1E,UAAIA,QAAO,GAAI,OAAM,MAAM,KAAK,GAAG,KAAK;AAAA,UACnC,OAAM,MAAM,OAAOA,MAAK,GAAG,GAAG,GAAG,KAAK;AAE3C;AAAA,IACF;AAEA,UAAM,WAAW,OAAO,UAAU,CAAC,MAAM,EAAE,UAAU,UAAU;AAC/D,UAAM,KAAK,aAAa,KAAK,OAAO,SAAS;AAE7C,WAAO,OAAO,IAAI,GAAG,EAAE,OAAO,OAAO,OAAO,CAAC,GAAG,KAAK,EAAE,CAAC;AAAA,EAC1D,CAAC;AAED,SAAO;AACT;AAGO,SAAS,cACd,SACA,UAAwB,CAAC,GACzB,YAA2B,CAAC,GAChB;AACZ,QAAM,UAAU,UAAU,OAAO;AAEjC,SAAO,cAAc,YAAY,SAAS,EACvC,IAAI,CAAC,WAAW;AAAA,IACf,GAAG;AAAA,IACH,OAAO,MAAM,MAAM;AAAA,MACjB,CAAC,SACC,SAAS,SAAS,KAAK,MAAM,KAC7B,EAAE,KAAK,gBAAgB,SAAS,SAAS,KAAK,YAAY,MAC1D,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF,EAAE,EACD,OAAO,CAAC,UAAU,MAAM,MAAM,MAAM;AACzC;","names":["at"]}
1
+ {"version":3,"sources":["../../../src/admin/_shell/nav-filter.ts"],"sourcesContent":["import {\n NAV_GROUPS,\n type NavAddition,\n type NavGroup,\n type NavItem,\n} from './nav-config';\nimport { moduleOn, type AdminModules } from './nav-modules';\n\nexport const isActive = (item: NavItem, pathname: string) =>\n item.href === '/admin'\n ? pathname === '/admin'\n : (item.activeRoutes ?? [item.href]).some((route) =>\n pathname.startsWith(route),\n );\n\nexport interface AdminSessionLike {\n role: string;\n permissions: string[];\n}\n\nexport const canAccess = (session: AdminSessionLike) => (item: NavItem) => {\n if (session.role === 'superadmin') return true;\n\n if (item.superadmin) return false;\n\n return !item.permission || session.permissions.includes(item.permission);\n};\n\n/** The host's pages placed into their groups: after the Box items in a group (or instead of them), a new group before Platform. */\nexport function withAdditions(\n groups: NavGroup[],\n additions: NavAddition[] = [],\n): NavGroup[] {\n const merged = groups.map((g) => ({ ...g, items: [...g.items] }));\n\n additions.forEach(({ group, items, replace }) => {\n const found = merged.find((g) => g.label === group);\n\n if (found) {\n if (replace) found.items = [];\n found.items.push(...items);\n return;\n }\n\n const platform = merged.findIndex((g) => g.label === 'Platform');\n const at = platform === -1 ? merged.length : platform;\n\n merged.splice(at, 0, { label: group, items: [...items] });\n });\n\n return merged;\n}\n\n/** The groups this admin may see, modules off removed, empty groups dropped. */\nexport function visibleGroups(\n session: AdminSessionLike,\n modules: AdminModules = {},\n additions: NavAddition[] = [],\n): NavGroup[] {\n const allowed = canAccess(session);\n\n return withAdditions(NAV_GROUPS, additions)\n .map((group) => ({\n ...group,\n items: group.items.filter(\n (item) =>\n moduleOn(modules, item.module) &&\n !(item.unlessModule && moduleOn(modules, item.unlessModule)) &&\n allowed(item),\n ),\n }))\n .filter((group) => group.items.length);\n}\n"],"mappings":"AAAA;AAAA,EACE;AAAA,OAIK;AACP,SAAS,gBAAmC;AAErC,MAAM,WAAW,CAAC,MAAe,aACtC,KAAK,SAAS,WACV,aAAa,YACZ,KAAK,gBAAgB,CAAC,KAAK,IAAI,GAAG;AAAA,EAAK,CAAC,UACvC,SAAS,WAAW,KAAK;AAC3B;AAOC,MAAM,YAAY,CAAC,YAA8B,CAAC,SAAkB;AACzE,MAAI,QAAQ,SAAS,aAAc,QAAO;AAE1C,MAAI,KAAK,WAAY,QAAO;AAE5B,SAAO,CAAC,KAAK,cAAc,QAAQ,YAAY,SAAS,KAAK,UAAU;AACzE;AAGO,SAAS,cACd,QACA,YAA2B,CAAC,GAChB;AACZ,QAAM,SAAS,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;AAEhE,YAAU,QAAQ,CAAC,EAAE,OAAO,OAAO,QAAQ,MAAM;AAC/C,UAAM,QAAQ,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,KAAK;AAElD,QAAI,OAAO;AACT,UAAI,QAAS,OAAM,QAAQ,CAAC;AAC5B,YAAM,MAAM,KAAK,GAAG,KAAK;AACzB;AAAA,IACF;AAEA,UAAM,WAAW,OAAO,UAAU,CAAC,MAAM,EAAE,UAAU,UAAU;AAC/D,UAAM,KAAK,aAAa,KAAK,OAAO,SAAS;AAE7C,WAAO,OAAO,IAAI,GAAG,EAAE,OAAO,OAAO,OAAO,CAAC,GAAG,KAAK,EAAE,CAAC;AAAA,EAC1D,CAAC;AAED,SAAO;AACT;AAGO,SAAS,cACd,SACA,UAAwB,CAAC,GACzB,YAA2B,CAAC,GAChB;AACZ,QAAM,UAAU,UAAU,OAAO;AAEjC,SAAO,cAAc,YAAY,SAAS,EACvC,IAAI,CAAC,WAAW;AAAA,IACf,GAAG;AAAA,IACH,OAAO,MAAM,MAAM;AAAA,MACjB,CAAC,SACC,SAAS,SAAS,KAAK,MAAM,KAC7B,EAAE,KAAK,gBAAgB,SAAS,SAAS,KAAK,YAAY,MAC1D,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF,EAAE,EACD,OAAO,CAAC,UAAU,MAAM,MAAM,MAAM;AACzC;","names":[]}
@@ -24,11 +24,9 @@ function NavRow({
24
24
  item,
25
25
  active,
26
26
  collapsed,
27
- canSupport,
28
- badges
27
+ canSupport
29
28
  }) {
30
29
  const Icon = item.icon;
31
- const Badge = badges?.[item.href];
32
30
  return /* @__PURE__ */ jsx(RailTip, { label: item.label, on: collapsed, children: /* @__PURE__ */ jsxs(
33
31
  Link,
34
32
  {
@@ -49,8 +47,7 @@ function NavRow({
49
47
  }
50
48
  ),
51
49
  /* @__PURE__ */ jsx("span", { className: styles.linkText, children: /* @__PURE__ */ jsx("span", { className: styles.linkLabel, children: item.label }) }),
52
- item.inbox && /* @__PURE__ */ jsx("span", { className: styles.badgeSlot, children: /* @__PURE__ */ jsx(NavBadge, { kind: item.inbox, canSupport }) }),
53
- Badge && !item.inbox && /* @__PURE__ */ jsx("span", { className: styles.badgeSlot, children: /* @__PURE__ */ jsx(Badge, {}) })
50
+ item.inbox && /* @__PURE__ */ jsx("span", { className: styles.badgeSlot, children: /* @__PURE__ */ jsx(NavBadge, { kind: item.inbox, canSupport }) })
54
51
  ]
55
52
  }
56
53
  ) });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_shell/nav-row.tsx"],"sourcesContent":["'use client';\n\nimport type { ComponentType, ReactElement } from 'react';\nimport Link from 'next/link';\nimport {\n TooltipContent,\n TooltipProvider,\n TooltipRoot,\n TooltipTrigger,\n} from '@withpotter/ui-kit/tooltip';\nimport { NavBadge } from '../_inbox/nav-badge';\nimport type { NavItem } from './nav-config';\nimport styles from './sidebar.module.css';\n\n/**\n * The collapsed rail is icons only, so the name arrives as a tooltip on hover\n * and keyboard focus (the merchant rail's RailLabel, without the drop shadow).\n * Expanded, the visible label is the name and this passes straight through.\n */\nexport function RailTip({\n label,\n on,\n children,\n}: {\n label: string;\n on: boolean;\n children: ReactElement;\n}) {\n if (!on) return children;\n\n return (\n <TooltipProvider delayDuration={200}>\n <TooltipRoot>\n <TooltipTrigger asChild>{children}</TooltipTrigger>\n <TooltipContent side=\"right\" className=\"shadow-none\">\n {label}\n </TooltipContent>\n </TooltipRoot>\n </TooltipProvider>\n );\n}\n\n/**\n * One sidebar row: a link to its page. A host can show its own count beside the label: `badges` maps\n * an item's href to a component that renders one `span` (it sits in the same slot as the inbox\n * count, so it is styled and moved to the corner in the collapsed rail the same way). An item that\n * has an inbox count keeps that count.\n */\nexport function NavRow({\n item,\n active,\n collapsed,\n canSupport,\n badges,\n}: {\n item: NavItem;\n active: boolean;\n collapsed: boolean;\n canSupport: boolean;\n badges?: Record<string, ComponentType>;\n}) {\n const Icon = item.icon;\n const Badge = badges?.[item.href];\n\n return (\n <RailTip label={item.label} on={collapsed}>\n <Link\n href={item.href}\n className={styles.link}\n data-fluid-item\n data-active={active}\n aria-current={active ? 'page' : undefined}\n aria-label={collapsed ? item.label : undefined}\n >\n <Icon\n size={16}\n strokeWidth={active ? 2 : 1.75}\n className={styles.icon}\n aria-hidden\n />\n <span className={styles.linkText}>\n <span className={styles.linkLabel}>{item.label}</span>\n </span>\n {item.inbox && (\n <span className={styles.badgeSlot}>\n <NavBadge kind={item.inbox} canSupport={canSupport} />\n </span>\n )}\n {Badge && !item.inbox && (\n <span className={styles.badgeSlot}>\n <Badge />\n </span>\n )}\n </Link>\n </RailTip>\n );\n}\n"],"mappings":";AAgCM,SACE,KADF;AA7BN,OAAO,UAAU;AACjB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AAEzB,OAAO,YAAY;AAOZ,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,MAAI,CAAC,GAAI,QAAO;AAEhB,SACE,oBAAC,mBAAgB,eAAe,KAC9B,+BAAC,eACC;AAAA,wBAAC,kBAAe,SAAO,MAAE,UAAS;AAAA,IAClC,oBAAC,kBAAe,MAAK,SAAQ,WAAU,eACpC,iBACH;AAAA,KACF,GACF;AAEJ;AAQO,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,OAAO,KAAK;AAClB,QAAM,QAAQ,SAAS,KAAK,IAAI;AAEhC,SACE,oBAAC,WAAQ,OAAO,KAAK,OAAO,IAAI,WAC9B;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,KAAK;AAAA,MACX,WAAW,OAAO;AAAA,MAClB,mBAAe;AAAA,MACf,eAAa;AAAA,MACb,gBAAc,SAAS,SAAS;AAAA,MAChC,cAAY,YAAY,KAAK,QAAQ;AAAA,MAErC;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAM;AAAA,YACN,aAAa,SAAS,IAAI;AAAA,YAC1B,WAAW,OAAO;AAAA,YAClB,eAAW;AAAA;AAAA,QACb;AAAA,QACA,oBAAC,UAAK,WAAW,OAAO,UACtB,8BAAC,UAAK,WAAW,OAAO,WAAY,eAAK,OAAM,GACjD;AAAA,QACC,KAAK,SACJ,oBAAC,UAAK,WAAW,OAAO,WACtB,8BAAC,YAAS,MAAM,KAAK,OAAO,YAAwB,GACtD;AAAA,QAED,SAAS,CAAC,KAAK,SACd,oBAAC,UAAK,WAAW,OAAO,WACtB,8BAAC,SAAM,GACT;AAAA;AAAA;AAAA,EAEJ,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_shell/nav-row.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactElement } from 'react';\nimport Link from 'next/link';\nimport {\n TooltipContent,\n TooltipProvider,\n TooltipRoot,\n TooltipTrigger,\n} from '@withpotter/ui-kit/tooltip';\nimport { NavBadge } from '../_inbox/nav-badge';\nimport type { NavItem } from './nav-config';\nimport styles from './sidebar.module.css';\n\n/**\n * The collapsed rail is icons only, so the name arrives as a tooltip on hover\n * and keyboard focus (the merchant rail's RailLabel, without the drop shadow).\n * Expanded, the visible label is the name and this passes straight through.\n */\nexport function RailTip({\n label,\n on,\n children,\n}: {\n label: string;\n on: boolean;\n children: ReactElement;\n}) {\n if (!on) return children;\n\n return (\n <TooltipProvider delayDuration={200}>\n <TooltipRoot>\n <TooltipTrigger asChild>{children}</TooltipTrigger>\n <TooltipContent side=\"right\" className=\"shadow-none\">\n {label}\n </TooltipContent>\n </TooltipRoot>\n </TooltipProvider>\n );\n}\n\n/** One sidebar row: a link to its page. */\nexport function NavRow({\n item,\n active,\n collapsed,\n canSupport,\n}: {\n item: NavItem;\n active: boolean;\n collapsed: boolean;\n canSupport: boolean;\n}) {\n const Icon = item.icon;\n\n return (\n <RailTip label={item.label} on={collapsed}>\n <Link\n href={item.href}\n className={styles.link}\n data-fluid-item\n data-active={active}\n aria-current={active ? 'page' : undefined}\n aria-label={collapsed ? item.label : undefined}\n >\n <Icon\n size={16}\n strokeWidth={active ? 2 : 1.75}\n className={styles.icon}\n aria-hidden\n />\n <span className={styles.linkText}>\n <span className={styles.linkLabel}>{item.label}</span>\n </span>\n {item.inbox && (\n <span className={styles.badgeSlot}>\n <NavBadge kind={item.inbox} canSupport={canSupport} />\n </span>\n )}\n </Link>\n </RailTip>\n );\n}\n"],"mappings":";AAgCM,SACE,KADF;AA7BN,OAAO,UAAU;AACjB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AAEzB,OAAO,YAAY;AAOZ,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,MAAI,CAAC,GAAI,QAAO;AAEhB,SACE,oBAAC,mBAAgB,eAAe,KAC9B,+BAAC,eACC;AAAA,wBAAC,kBAAe,SAAO,MAAE,UAAS;AAAA,IAClC,oBAAC,kBAAe,MAAK,SAAQ,WAAU,eACpC,iBACH;AAAA,KACF,GACF;AAEJ;AAGO,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,OAAO,KAAK;AAElB,SACE,oBAAC,WAAQ,OAAO,KAAK,OAAO,IAAI,WAC9B;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,KAAK;AAAA,MACX,WAAW,OAAO;AAAA,MAClB,mBAAe;AAAA,MACf,eAAa;AAAA,MACb,gBAAc,SAAS,SAAS;AAAA,MAChC,cAAY,YAAY,KAAK,QAAQ;AAAA,MAErC;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAM;AAAA,YACN,aAAa,SAAS,IAAI;AAAA,YAC1B,WAAW,OAAO;AAAA,YAClB,eAAW;AAAA;AAAA,QACb;AAAA,QACA,oBAAC,UAAK,WAAW,OAAO,UACtB,8BAAC,UAAK,WAAW,OAAO,WAAY,eAAK,OAAM,GACjD;AAAA,QACC,KAAK,SACJ,oBAAC,UAAK,WAAW,OAAO,WACtB,8BAAC,YAAS,MAAM,KAAK,OAAO,YAAwB,GACtD;AAAA;AAAA;AAAA,EAEJ,GACF;AAEJ;","names":[]}
@@ -9,8 +9,6 @@ function SidebarFooter({
9
9
  collapsed,
10
10
  settingsActive,
11
11
  showSettings,
12
- actions = [],
13
- pathname,
14
12
  onSearch
15
13
  }) {
16
14
  const { resolvedTheme, toggleTheme } = useTheme();
@@ -47,24 +45,6 @@ function SidebarFooter({
47
45
  ]
48
46
  }
49
47
  ) }),
50
- actions.map(({ href, label, icon: Icon, badge: Badge }) => {
51
- const active = pathname === href || pathname.startsWith(`${href}/`);
52
- return /* @__PURE__ */ jsx(RailTip, { label, on: collapsed, children: /* @__PURE__ */ jsxs(
53
- Link,
54
- {
55
- href,
56
- className: styles.theme,
57
- "data-active": active,
58
- "aria-current": active ? "page" : void 0,
59
- "aria-label": label,
60
- title: collapsed ? void 0 : label,
61
- children: [
62
- /* @__PURE__ */ jsx(Icon, { size: 16, "aria-hidden": true }),
63
- Badge && /* @__PURE__ */ jsx("span", { className: styles.badge, children: /* @__PURE__ */ jsx(Badge, {}) })
64
- ]
65
- }
66
- ) }, href);
67
- }),
68
48
  /* @__PURE__ */ jsx(RailTip, { label: themeLabel, on: collapsed, children: /* @__PURE__ */ jsx(
69
49
  "button",
70
50
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_shell/sidebar-footer.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport type { ComponentType } from 'react';\nimport { Moon, Search, Settings, Sun, type LucideIcon } from 'lucide-react';\nimport { useTheme } from '@withpotter/lib/theme-context';\nimport { RailTip } from './nav-row';\nimport styles from './sidebar-footer.module.css';\n\n/**\n * An icon a host adds to the foot, beside the theme switch: a link with a\n * name for screen readers and a tooltip, and an optional `badge` drawn on its\n * corner (a dot or a count; it draws nothing when there is nothing to show).\n */\nexport type FooterAction = {\n href: string;\n label: string;\n icon: LucideIcon;\n badge?: ComponentType;\n /** Shown to superadmins only, like a menu item marked `superadmin`. */\n superadmin?: boolean;\n};\n\n/**\n * The pinned foot of the sidebar: jump anywhere (⌘K), and Settings with the\n * host's icons and the theme switch beside it. In the rail each is its own\n * 40px target.\n */\nexport function SidebarFooter({\n collapsed,\n settingsActive,\n showSettings,\n actions = [],\n pathname,\n onSearch,\n}: {\n collapsed: boolean;\n settingsActive: boolean;\n showSettings: boolean;\n actions?: FooterAction[];\n pathname: string;\n onSearch: () => void;\n}) {\n const { resolvedTheme, toggleTheme } = useTheme();\n const dark = resolvedTheme === 'dark';\n const themeLabel = `Switch to ${dark ? 'light' : 'dark'} mode`;\n\n return (\n <div className={styles.foot} data-collapsed={collapsed}>\n <RailTip label=\"Search\" on={collapsed}>\n <button\n type=\"button\"\n className={styles.row}\n onClick={onSearch}\n aria-label={collapsed ? 'Search' : undefined}\n aria-keyshortcuts=\"Meta+K\"\n >\n <Search size={16} className={styles.icon} aria-hidden />\n <span className={styles.label}>Search</span>\n <kbd className={styles.kbd}>⌘K</kbd>\n </button>\n </RailTip>\n\n <div className={styles.split}>\n {showSettings && (\n <RailTip label=\"Settings\" on={collapsed}>\n <Link\n href=\"/admin/settings\"\n className={styles.row}\n data-active={settingsActive}\n aria-current={settingsActive ? 'page' : undefined}\n aria-label={collapsed ? 'Settings' : undefined}\n >\n <Settings size={16} className={styles.icon} aria-hidden />\n <span className={styles.label}>Settings</span>\n </Link>\n </RailTip>\n )}\n {actions.map(({ href, label, icon: Icon, badge: Badge }) => {\n const active = pathname === href || pathname.startsWith(`${href}/`);\n return (\n <RailTip key={href} label={label} on={collapsed}>\n <Link\n href={href}\n className={styles.theme}\n data-active={active}\n aria-current={active ? 'page' : undefined}\n aria-label={label}\n title={collapsed ? undefined : label}\n >\n <Icon size={16} aria-hidden />\n {Badge && (\n <span className={styles.badge}>\n <Badge />\n </span>\n )}\n </Link>\n </RailTip>\n );\n })}\n <RailTip label={themeLabel} on={collapsed}>\n <button\n type=\"button\"\n className={styles.theme}\n onClick={toggleTheme}\n aria-label={themeLabel}\n title={collapsed ? undefined : themeLabel}\n >\n {dark ? <Sun size={16} /> : <Moon size={16} />}\n </button>\n </RailTip>\n </div>\n </div>\n );\n}\n"],"mappings":";AAkDQ,SAOE,KAPF;AAhDR,OAAO,UAAU;AAEjB,SAAS,MAAM,QAAQ,UAAU,WAA4B;AAC7D,SAAS,gBAAgB;AACzB,SAAS,eAAe;AACxB,OAAO,YAAY;AAqBZ,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU,CAAC;AAAA,EACX;AAAA,EACA;AACF,GAOG;AACD,QAAM,EAAE,eAAe,YAAY,IAAI,SAAS;AAChD,QAAM,OAAO,kBAAkB;AAC/B,QAAM,aAAa,aAAa,OAAO,UAAU,MAAM;AAEvD,SACE,qBAAC,SAAI,WAAW,OAAO,MAAM,kBAAgB,WAC3C;AAAA,wBAAC,WAAQ,OAAM,UAAS,IAAI,WAC1B;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAW,OAAO;AAAA,QAClB,SAAS;AAAA,QACT,cAAY,YAAY,WAAW;AAAA,QACnC,qBAAkB;AAAA,QAElB;AAAA,8BAAC,UAAO,MAAM,IAAI,WAAW,OAAO,MAAM,eAAW,MAAC;AAAA,UACtD,oBAAC,UAAK,WAAW,OAAO,OAAO,oBAAM;AAAA,UACrC,oBAAC,SAAI,WAAW,OAAO,KAAK,qBAAE;AAAA;AAAA;AAAA,IAChC,GACF;AAAA,IAEA,qBAAC,SAAI,WAAW,OAAO,OACpB;AAAA,sBACC,oBAAC,WAAQ,OAAM,YAAW,IAAI,WAC5B;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAW,OAAO;AAAA,UAClB,eAAa;AAAA,UACb,gBAAc,iBAAiB,SAAS;AAAA,UACxC,cAAY,YAAY,aAAa;AAAA,UAErC;AAAA,gCAAC,YAAS,MAAM,IAAI,WAAW,OAAO,MAAM,eAAW,MAAC;AAAA,YACxD,oBAAC,UAAK,WAAW,OAAO,OAAO,sBAAQ;AAAA;AAAA;AAAA,MACzC,GACF;AAAA,MAED,QAAQ,IAAI,CAAC,EAAE,MAAM,OAAO,MAAM,MAAM,OAAO,MAAM,MAAM;AAC1D,cAAM,SAAS,aAAa,QAAQ,SAAS,WAAW,GAAG,IAAI,GAAG;AAClE,eACE,oBAAC,WAAmB,OAAc,IAAI,WACpC;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,WAAW,OAAO;AAAA,YAClB,eAAa;AAAA,YACb,gBAAc,SAAS,SAAS;AAAA,YAChC,cAAY;AAAA,YACZ,OAAO,YAAY,SAAY;AAAA,YAE/B;AAAA,kCAAC,QAAK,MAAM,IAAI,eAAW,MAAC;AAAA,cAC3B,SACC,oBAAC,UAAK,WAAW,OAAO,OACtB,8BAAC,SAAM,GACT;AAAA;AAAA;AAAA,QAEJ,KAfY,IAgBd;AAAA,MAEJ,CAAC;AAAA,MACD,oBAAC,WAAQ,OAAO,YAAY,IAAI,WAC9B;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAW,OAAO;AAAA,UAClB,SAAS;AAAA,UACT,cAAY;AAAA,UACZ,OAAO,YAAY,SAAY;AAAA,UAE9B,iBAAO,oBAAC,OAAI,MAAM,IAAI,IAAK,oBAAC,QAAK,MAAM,IAAI;AAAA;AAAA,MAC9C,GACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_shell/sidebar-footer.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { Moon, Search, Settings, Sun } from 'lucide-react';\nimport { useTheme } from '@withpotter/lib/theme-context';\nimport { RailTip } from './nav-row';\nimport styles from './sidebar-footer.module.css';\n\n/**\n * The pinned foot of the sidebar: jump anywhere (⌘K), and Settings with the\n * theme switch beside it. In the rail each is its own 40px target.\n */\nexport function SidebarFooter({\n collapsed,\n settingsActive,\n showSettings,\n onSearch,\n}: {\n collapsed: boolean;\n settingsActive: boolean;\n showSettings: boolean;\n onSearch: () => void;\n}) {\n const { resolvedTheme, toggleTheme } = useTheme();\n const dark = resolvedTheme === 'dark';\n const themeLabel = `Switch to ${dark ? 'light' : 'dark'} mode`;\n\n return (\n <div className={styles.foot} data-collapsed={collapsed}>\n <RailTip label=\"Search\" on={collapsed}>\n <button\n type=\"button\"\n className={styles.row}\n onClick={onSearch}\n aria-label={collapsed ? 'Search' : undefined}\n aria-keyshortcuts=\"Meta+K\"\n >\n <Search size={16} className={styles.icon} aria-hidden />\n <span className={styles.label}>Search</span>\n <kbd className={styles.kbd}>⌘K</kbd>\n </button>\n </RailTip>\n\n <div className={styles.split}>\n {showSettings && (\n <RailTip label=\"Settings\" on={collapsed}>\n <Link\n href=\"/admin/settings\"\n className={styles.row}\n data-active={settingsActive}\n aria-current={settingsActive ? 'page' : undefined}\n aria-label={collapsed ? 'Settings' : undefined}\n >\n <Settings size={16} className={styles.icon} aria-hidden />\n <span className={styles.label}>Settings</span>\n </Link>\n </RailTip>\n )}\n <RailTip label={themeLabel} on={collapsed}>\n <button\n type=\"button\"\n className={styles.theme}\n onClick={toggleTheme}\n aria-label={themeLabel}\n title={collapsed ? undefined : themeLabel}\n >\n {dark ? <Sun size={16} /> : <Moon size={16} />}\n </button>\n </RailTip>\n </div>\n </div>\n );\n}\n"],"mappings":";AA8BQ,SAOE,KAPF;AA5BR,OAAO,UAAU;AACjB,SAAS,MAAM,QAAQ,UAAU,WAAW;AAC5C,SAAS,gBAAgB;AACzB,SAAS,eAAe;AACxB,OAAO,YAAY;AAMZ,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,EAAE,eAAe,YAAY,IAAI,SAAS;AAChD,QAAM,OAAO,kBAAkB;AAC/B,QAAM,aAAa,aAAa,OAAO,UAAU,MAAM;AAEvD,SACE,qBAAC,SAAI,WAAW,OAAO,MAAM,kBAAgB,WAC3C;AAAA,wBAAC,WAAQ,OAAM,UAAS,IAAI,WAC1B;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAW,OAAO;AAAA,QAClB,SAAS;AAAA,QACT,cAAY,YAAY,WAAW;AAAA,QACnC,qBAAkB;AAAA,QAElB;AAAA,8BAAC,UAAO,MAAM,IAAI,WAAW,OAAO,MAAM,eAAW,MAAC;AAAA,UACtD,oBAAC,UAAK,WAAW,OAAO,OAAO,oBAAM;AAAA,UACrC,oBAAC,SAAI,WAAW,OAAO,KAAK,qBAAE;AAAA;AAAA;AAAA,IAChC,GACF;AAAA,IAEA,qBAAC,SAAI,WAAW,OAAO,OACpB;AAAA,sBACC,oBAAC,WAAQ,OAAM,YAAW,IAAI,WAC5B;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAW,OAAO;AAAA,UAClB,eAAa;AAAA,UACb,gBAAc,iBAAiB,SAAS;AAAA,UACxC,cAAY,YAAY,aAAa;AAAA,UAErC;AAAA,gCAAC,YAAS,MAAM,IAAI,WAAW,OAAO,MAAM,eAAW,MAAC;AAAA,YACxD,oBAAC,UAAK,WAAW,OAAO,OAAO,sBAAQ;AAAA;AAAA;AAAA,MACzC,GACF;AAAA,MAEF,oBAAC,WAAQ,OAAO,YAAY,IAAI,WAC9B;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAW,OAAO;AAAA,UAClB,SAAS;AAAA,UACT,cAAY;AAAA,UACZ,OAAO,YAAY,SAAY;AAAA,UAE9B,iBAAO,oBAAC,OAAI,MAAM,IAAI,IAAK,oBAAC,QAAK,MAAM,IAAI;AAAA;AAAA,MAC9C,GACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
@@ -42,27 +42,10 @@
42
42
  }
43
43
 
44
44
  .theme {
45
- position: relative;
46
45
  flex: none;
47
46
  justify-content: center;
48
47
  width: 32px;
49
48
  height: 32px;
50
- text-decoration: none;
51
- }
52
-
53
- .theme[data-active='true'] {
54
- border-color: var(--border-default);
55
- background: var(--bg-base);
56
- color: var(--text-primary);
57
- }
58
-
59
- /* A host's count or dot, on the icon's top right corner. */
60
- .badge {
61
- position: absolute;
62
- top: 2px;
63
- right: 2px;
64
- display: flex;
65
- pointer-events: none;
66
49
  }
67
50
 
68
51
  .row:hover,
@@ -1,8 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { isActive } from "./nav-filter";
4
- import { NavGroupView } from "./nav-group";
5
- import { useOpenGroups } from "./use-open-groups";
4
+ import { NavRow } from "./nav-row";
6
5
  import { SidebarFooter } from "./sidebar-footer";
7
6
  import { useFluidHover } from "./use-fluid-hover";
8
7
  import { UserMenu } from "./user-menu";
@@ -15,9 +14,7 @@ function Sidebar({
15
14
  collapsed,
16
15
  email,
17
16
  role,
18
- onSearch,
19
- badges,
20
- footerActions
17
+ onSearch
21
18
  }) {
22
19
  const hover = useFluidHover();
23
20
  const settings = groups.flatMap((group) => group.items).find((item) => item.href === FOOT_HREF);
@@ -25,10 +22,6 @@ function Sidebar({
25
22
  ...group,
26
23
  items: group.items.filter((item) => item.href !== FOOT_HREF)
27
24
  })).filter((group) => group.items.length);
28
- const activeGroup = navGroups.find(
29
- (group) => group.items.some((item) => isActive(item, pathname))
30
- )?.label ?? null;
31
- const { choices, setGroupOpen } = useOpenGroups(pathname, activeGroup);
32
25
  return /* @__PURE__ */ jsxs("aside", { className: styles.sidebar, "data-collapsed": collapsed, children: [
33
26
  /* @__PURE__ */ jsx(UserMenu, { email, role, collapsed }),
34
27
  /* @__PURE__ */ jsxs(
@@ -38,7 +31,6 @@ function Sidebar({
38
31
  "aria-label": "Admin",
39
32
  onPointerMove: hover.onPointerMove,
40
33
  onPointerLeave: hover.onPointerLeave,
41
- onClick: hover.onClick,
42
34
  children: [
43
35
  /* @__PURE__ */ jsx(
44
36
  "span",
@@ -52,19 +44,19 @@ function Sidebar({
52
44
  } : void 0
53
45
  }
54
46
  ),
55
- navGroups.map((group, index) => /* @__PURE__ */ jsx(
56
- NavGroupView,
57
- {
58
- group,
59
- pathname,
60
- collapsed,
61
- canSupport,
62
- choices,
63
- onToggle: setGroupOpen,
64
- badges
65
- },
66
- group.label ?? index
67
- ))
47
+ navGroups.map((group, index) => /* @__PURE__ */ jsxs("div", { className: styles.group, children: [
48
+ group.label && /* @__PURE__ */ jsx("div", { className: styles.groupLabel, children: group.label }),
49
+ group.items.map((item) => /* @__PURE__ */ jsx(
50
+ NavRow,
51
+ {
52
+ item,
53
+ active: isActive(item, pathname),
54
+ collapsed,
55
+ canSupport
56
+ },
57
+ item.href
58
+ ))
59
+ ] }, group.label ?? index))
68
60
  ]
69
61
  }
70
62
  ),
@@ -74,8 +66,6 @@ function Sidebar({
74
66
  collapsed,
75
67
  showSettings: Boolean(settings),
76
68
  settingsActive: settings ? isActive(settings, pathname) : false,
77
- actions: footerActions,
78
- pathname,
79
69
  onSearch
80
70
  }
81
71
  )
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_shell/sidebar.tsx"],"sourcesContent":["'use client';\n\nimport type { ComponentType } from 'react';\nimport type { NavGroup } from './nav-config';\nimport { isActive } from './nav-filter';\nimport { NavGroupView } from './nav-group';\nimport { useOpenGroups } from './use-open-groups';\nimport { SidebarFooter, type FooterAction } from './sidebar-footer';\nimport { useFluidHover } from './use-fluid-hover';\nimport { UserMenu } from './user-menu';\nimport styles from './sidebar.module.css';\n\n/** Settings lives in the pinned foot, beside the theme switch. */\nconst FOOT_HREF = '/admin/settings';\n\nexport function Sidebar({\n groups,\n pathname,\n canSupport,\n collapsed,\n email,\n role,\n onSearch,\n badges,\n footerActions,\n}: {\n groups: NavGroup[];\n pathname: string;\n canSupport: boolean;\n collapsed: boolean;\n email: string;\n role: string;\n onSearch: () => void;\n /** A host's own count per item, by href. */\n badges?: Record<string, ComponentType>;\n /** Icons a host adds to the foot, beside the theme switch. */\n footerActions?: FooterAction[];\n}) {\n const hover = useFluidHover();\n const settings = groups\n .flatMap((group) => group.items)\n .find((item) => item.href === FOOT_HREF);\n const navGroups = groups\n .map((group) => ({\n ...group,\n items: group.items.filter((item) => item.href !== FOOT_HREF),\n }))\n .filter((group) => group.items.length);\n const activeGroup =\n navGroups.find((group) =>\n group.items.some((item) => isActive(item, pathname)),\n )?.label ?? null;\n const { choices, setGroupOpen } = useOpenGroups(pathname, activeGroup);\n\n return (\n <aside className={styles.sidebar} data-collapsed={collapsed}>\n <UserMenu email={email} role={role} collapsed={collapsed} />\n\n <nav\n className={styles.nav}\n aria-label=\"Admin\"\n onPointerMove={hover.onPointerMove}\n onPointerLeave={hover.onPointerLeave}\n onClick={hover.onClick}\n >\n <span\n aria-hidden\n className={styles.glide}\n data-visible={Boolean(hover.rect)}\n style={\n hover.rect\n ? {\n transform: `translateY(${hover.rect.top}px)`,\n height: hover.rect.height,\n }\n : undefined\n }\n />\n {navGroups.map((group, index) => (\n <NavGroupView\n key={group.label ?? index}\n group={group}\n pathname={pathname}\n collapsed={collapsed}\n canSupport={canSupport}\n choices={choices}\n onToggle={setGroupOpen}\n badges={badges}\n />\n ))}\n </nav>\n\n <SidebarFooter\n collapsed={collapsed}\n showSettings={Boolean(settings)}\n settingsActive={settings ? isActive(settings, pathname) : false}\n actions={footerActions}\n pathname={pathname}\n onSearch={onSearch}\n />\n </aside>\n );\n}\n"],"mappings":";AAwDM,cAEA,YAFA;AApDN,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,qBAAwC;AACjD,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,OAAO,YAAY;AAGnB,MAAM,YAAY;AAEX,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAYG;AACD,QAAM,QAAQ,cAAc;AAC5B,QAAM,WAAW,OACd,QAAQ,CAAC,UAAU,MAAM,KAAK,EAC9B,KAAK,CAAC,SAAS,KAAK,SAAS,SAAS;AACzC,QAAM,YAAY,OACf,IAAI,CAAC,WAAW;AAAA,IACf,GAAG;AAAA,IACH,OAAO,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,SAAS,SAAS;AAAA,EAC7D,EAAE,EACD,OAAO,CAAC,UAAU,MAAM,MAAM,MAAM;AACvC,QAAM,cACJ,UAAU;AAAA,IAAK,CAAC,UACd,MAAM,MAAM,KAAK,CAAC,SAAS,SAAS,MAAM,QAAQ,CAAC;AAAA,EACrD,GAAG,SAAS;AACd,QAAM,EAAE,SAAS,aAAa,IAAI,cAAc,UAAU,WAAW;AAErE,SACE,qBAAC,WAAM,WAAW,OAAO,SAAS,kBAAgB,WAChD;AAAA,wBAAC,YAAS,OAAc,MAAY,WAAsB;AAAA,IAE1D;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,OAAO;AAAA,QAClB,cAAW;AAAA,QACX,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QACtB,SAAS,MAAM;AAAA,QAEf;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAW,OAAO;AAAA,cAClB,gBAAc,QAAQ,MAAM,IAAI;AAAA,cAChC,OACE,MAAM,OACF;AAAA,gBACE,WAAW,cAAc,MAAM,KAAK,GAAG;AAAA,gBACvC,QAAQ,MAAM,KAAK;AAAA,cACrB,IACA;AAAA;AAAA,UAER;AAAA,UACC,UAAU,IAAI,CAAC,OAAO,UACrB;AAAA,YAAC;AAAA;AAAA,cAEC;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA,UAAU;AAAA,cACV;AAAA;AAAA,YAPK,MAAM,SAAS;AAAA,UAQtB,CACD;AAAA;AAAA;AAAA,IACH;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,cAAc,QAAQ,QAAQ;AAAA,QAC9B,gBAAgB,WAAW,SAAS,UAAU,QAAQ,IAAI;AAAA,QAC1D,SAAS;AAAA,QACT;AAAA,QACA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_shell/sidebar.tsx"],"sourcesContent":["'use client';\n\nimport type { NavGroup } from './nav-config';\nimport { isActive } from './nav-filter';\nimport { NavRow } from './nav-row';\nimport { SidebarFooter } from './sidebar-footer';\nimport { useFluidHover } from './use-fluid-hover';\nimport { UserMenu } from './user-menu';\nimport styles from './sidebar.module.css';\n\n/** Settings lives in the pinned foot, beside the theme switch. */\nconst FOOT_HREF = '/admin/settings';\n\nexport function Sidebar({\n groups,\n pathname,\n canSupport,\n collapsed,\n email,\n role,\n onSearch,\n}: {\n groups: NavGroup[];\n pathname: string;\n canSupport: boolean;\n collapsed: boolean;\n email: string;\n role: string;\n onSearch: () => void;\n}) {\n const hover = useFluidHover();\n const settings = groups\n .flatMap((group) => group.items)\n .find((item) => item.href === FOOT_HREF);\n const navGroups = groups\n .map((group) => ({\n ...group,\n items: group.items.filter((item) => item.href !== FOOT_HREF),\n }))\n .filter((group) => group.items.length);\n\n return (\n <aside className={styles.sidebar} data-collapsed={collapsed}>\n <UserMenu email={email} role={role} collapsed={collapsed} />\n\n <nav\n className={styles.nav}\n aria-label=\"Admin\"\n onPointerMove={hover.onPointerMove}\n onPointerLeave={hover.onPointerLeave}\n >\n <span\n aria-hidden\n className={styles.glide}\n data-visible={Boolean(hover.rect)}\n style={\n hover.rect\n ? {\n transform: `translateY(${hover.rect.top}px)`,\n height: hover.rect.height,\n }\n : undefined\n }\n />\n {navGroups.map((group, index) => (\n <div key={group.label ?? index} className={styles.group}>\n {group.label && (\n <div className={styles.groupLabel}>{group.label}</div>\n )}\n {group.items.map((item) => (\n <NavRow\n key={item.href}\n item={item}\n active={isActive(item, pathname)}\n collapsed={collapsed}\n canSupport={canSupport}\n />\n ))}\n </div>\n ))}\n </nav>\n\n <SidebarFooter\n collapsed={collapsed}\n showSettings={Boolean(settings)}\n settingsActive={settings ? isActive(settings, pathname) : false}\n onSearch={onSearch}\n />\n </aside>\n );\n}\n"],"mappings":";AA2CM,cAsBI,YAtBJ;AAxCN,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,OAAO,YAAY;AAGnB,MAAM,YAAY;AAEX,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,QAAQ,cAAc;AAC5B,QAAM,WAAW,OACd,QAAQ,CAAC,UAAU,MAAM,KAAK,EAC9B,KAAK,CAAC,SAAS,KAAK,SAAS,SAAS;AACzC,QAAM,YAAY,OACf,IAAI,CAAC,WAAW;AAAA,IACf,GAAG;AAAA,IACH,OAAO,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,SAAS,SAAS;AAAA,EAC7D,EAAE,EACD,OAAO,CAAC,UAAU,MAAM,MAAM,MAAM;AAEvC,SACE,qBAAC,WAAM,WAAW,OAAO,SAAS,kBAAgB,WAChD;AAAA,wBAAC,YAAS,OAAc,MAAY,WAAsB;AAAA,IAE1D;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,OAAO;AAAA,QAClB,cAAW;AAAA,QACX,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QAEtB;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAW,OAAO;AAAA,cAClB,gBAAc,QAAQ,MAAM,IAAI;AAAA,cAChC,OACE,MAAM,OACF;AAAA,gBACE,WAAW,cAAc,MAAM,KAAK,GAAG;AAAA,gBACvC,QAAQ,MAAM,KAAK;AAAA,cACrB,IACA;AAAA;AAAA,UAER;AAAA,UACC,UAAU,IAAI,CAAC,OAAO,UACrB,qBAAC,SAA+B,WAAW,OAAO,OAC/C;AAAA,kBAAM,SACL,oBAAC,SAAI,WAAW,OAAO,YAAa,gBAAM,OAAM;AAAA,YAEjD,MAAM,MAAM,IAAI,CAAC,SAChB;AAAA,cAAC;AAAA;AAAA,gBAEC;AAAA,gBACA,QAAQ,SAAS,MAAM,QAAQ;AAAA,gBAC/B;AAAA,gBACA;AAAA;AAAA,cAJK,KAAK;AAAA,YAKZ,CACD;AAAA,eAZO,MAAM,SAAS,KAazB,CACD;AAAA;AAAA;AAAA,IACH;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,cAAc,QAAQ,QAAQ;AAAA,QAC9B,gBAAgB,WAAW,SAAS,UAAU,QAAQ,IAAI;AAAA,QAC1D;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
@@ -64,75 +64,6 @@
64
64
  white-space: nowrap;
65
65
  }
66
66
 
67
- /* A heading that folds its group: the label's look, as a button. */
68
- .groupToggle {
69
- display: flex;
70
- align-items: center;
71
- justify-content: space-between;
72
- width: 100%;
73
- padding-right: 8px;
74
- border: 0;
75
- background: none;
76
- font-family: inherit;
77
- text-align: left;
78
- cursor: pointer;
79
- }
80
-
81
- .groupToggle:hover {
82
- color: var(--text-secondary);
83
- }
84
-
85
- .groupToggle:focus-visible {
86
- outline: 2px solid var(--focus-ring);
87
- outline-offset: 1px;
88
- border-radius: 6px;
89
- }
90
-
91
- .groupChevron {
92
- flex: none;
93
- opacity: 0;
94
- transition:
95
- transform var(--adm-fast),
96
- opacity var(--adm-fast);
97
- }
98
-
99
- /* The chevron arrives on hover, as on the merchant rail, and stays while the
100
- group is open, rotated, to say the rows below belong to this heading. A
101
- keyboard has no pointer and touch has no hover, so those keep it too. */
102
- .groupToggle:hover .groupChevron,
103
- .groupToggle:focus-visible .groupChevron,
104
- .groupToggle[aria-expanded='true'] .groupChevron {
105
- opacity: 1;
106
- }
107
-
108
- .groupToggle[aria-expanded='true'] .groupChevron {
109
- transform: rotate(180deg);
110
- }
111
-
112
- @media (hover: none) {
113
- .groupChevron {
114
- opacity: 1;
115
- }
116
- }
117
-
118
- /* A folding group has no gap of its own between heading and rows: that 2px
119
- would pop in and out as the rows mount, so it lives inside the rows' padding
120
- and grows with their height. */
121
- .groupFold {
122
- gap: 0;
123
- }
124
-
125
- /* The rows of a folding group, laid out like the group's own. The padding keeps
126
- a focus ring from being clipped by the closing box; the side and bottom
127
- margins take it back, and the top padding is the gap under the heading. */
128
- .groupRows {
129
- display: flex;
130
- flex-direction: column;
131
- gap: 2px;
132
- padding: 2px;
133
- margin: 0 -2px -2px;
134
- }
135
-
136
67
  .link {
137
68
  position: relative;
138
69
  display: flex;
@@ -1,21 +1,15 @@
1
1
  "use client";
2
- import {
3
- useCallback,
4
- useEffect,
5
- useRef,
6
- useState
7
- } from "react";
2
+ import { useCallback, useState } from "react";
8
3
  function useFluidHover() {
9
4
  const [rect, setRect] = useState(null);
10
- const pointerY = useRef(null);
11
- const frame = useRef(0);
12
- const measure = useCallback((container, y) => {
5
+ const onPointerMove = useCallback((event) => {
6
+ const container = event.currentTarget;
13
7
  const items = container.querySelectorAll("[data-fluid-item]");
14
8
  let nearest = null;
15
9
  let best = Infinity;
16
10
  items.forEach((item) => {
17
11
  const box2 = item.getBoundingClientRect();
18
- const distance = Math.abs(y - (box2.top + box2.height / 2));
12
+ const distance = Math.abs(event.clientY - (box2.top + box2.height / 2));
19
13
  if (distance < best) {
20
14
  best = distance;
21
15
  nearest = box2;
@@ -28,33 +22,8 @@ function useFluidHover() {
28
22
  (current) => current && current.top === top && current.height === box.height ? current : { top, height: box.height }
29
23
  );
30
24
  }, []);
31
- const onPointerMove = useCallback(
32
- (event) => {
33
- pointerY.current = event.clientY;
34
- measure(event.currentTarget, event.clientY);
35
- },
36
- [measure]
37
- );
38
- const onPointerLeave = useCallback(() => {
39
- pointerY.current = null;
40
- setRect(null);
41
- }, []);
42
- const onClick = useCallback(
43
- (event) => {
44
- const container = event.currentTarget;
45
- const until = performance.now() + 320;
46
- cancelAnimationFrame(frame.current);
47
- const follow = () => {
48
- if (pointerY.current !== null) measure(container, pointerY.current);
49
- if (performance.now() < until)
50
- frame.current = requestAnimationFrame(follow);
51
- };
52
- frame.current = requestAnimationFrame(follow);
53
- },
54
- [measure]
55
- );
56
- useEffect(() => () => cancelAnimationFrame(frame.current), []);
57
- return { rect, onPointerMove, onPointerLeave, onClick };
25
+ const onPointerLeave = useCallback(() => setRect(null), []);
26
+ return { rect, onPointerMove, onPointerLeave };
58
27
  }
59
28
  export {
60
29
  useFluidHover
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_shell/use-fluid-hover.ts"],"sourcesContent":["'use client';\n\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type MouseEvent,\n type PointerEvent,\n} from 'react';\n\ntype Rect = { top: number; height: number } | null;\n\n/**\n * Fluid hover: one highlight that glides to the item nearest the pointer\n * (not just the one under it), so the target is lit before the click lands.\n * Items opt in with `data-fluid-item`; bind the handlers on their scrolling\n * container. The glide is a CSS spring on `transform`, so it retargets\n * smoothly when the pointer changes direction.\n */\nexport function useFluidHover() {\n const [rect, setRect] = useState<Rect>(null);\n const pointerY = useRef<number | null>(null);\n const frame = useRef(0);\n\n const measure = useCallback((container: HTMLElement, y: number) => {\n const items = container.querySelectorAll<HTMLElement>('[data-fluid-item]');\n let nearest: DOMRect | null = null;\n let best = Infinity;\n\n items.forEach((item) => {\n const box = item.getBoundingClientRect();\n const distance = Math.abs(y - (box.top + box.height / 2));\n\n if (distance < best) {\n best = distance;\n nearest = box;\n }\n });\n\n if (!nearest) return;\n\n const box: DOMRect = nearest;\n const top =\n box.top - container.getBoundingClientRect().top + container.scrollTop;\n\n setRect((current) =>\n current && current.top === top && current.height === box.height\n ? current\n : { top, height: box.height },\n );\n }, []);\n\n const onPointerMove = useCallback(\n (event: PointerEvent<HTMLElement>) => {\n pointerY.current = event.clientY;\n measure(event.currentTarget, event.clientY);\n },\n [measure],\n );\n\n const onPointerLeave = useCallback(() => {\n pointerY.current = null;\n setRect(null);\n }, []);\n\n /**\n * A group opening or closing moves the rows under a still pointer, and no\n * pointer event follows, so the highlight would stay where the row was. After\n * a press, follow the rows for the length of the move.\n */\n const onClick = useCallback(\n (event: MouseEvent<HTMLElement>) => {\n const container = event.currentTarget;\n const until = performance.now() + 320;\n cancelAnimationFrame(frame.current);\n\n const follow = () => {\n if (pointerY.current !== null) measure(container, pointerY.current);\n if (performance.now() < until)\n frame.current = requestAnimationFrame(follow);\n };\n\n frame.current = requestAnimationFrame(follow);\n },\n [measure],\n );\n\n useEffect(() => () => cancelAnimationFrame(frame.current), []);\n\n return { rect, onPointerMove, onPointerLeave, onClick };\n}\n"],"mappings":";AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAWA,SAAS,gBAAgB;AAC9B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAe,IAAI;AAC3C,QAAM,WAAW,OAAsB,IAAI;AAC3C,QAAM,QAAQ,OAAO,CAAC;AAEtB,QAAM,UAAU,YAAY,CAAC,WAAwB,MAAc;AACjE,UAAM,QAAQ,UAAU,iBAA8B,mBAAmB;AACzE,QAAI,UAA0B;AAC9B,QAAI,OAAO;AAEX,UAAM,QAAQ,CAAC,SAAS;AACtB,YAAMA,OAAM,KAAK,sBAAsB;AACvC,YAAM,WAAW,KAAK,IAAI,KAAKA,KAAI,MAAMA,KAAI,SAAS,EAAE;AAExD,UAAI,WAAW,MAAM;AACnB,eAAO;AACP,kBAAUA;AAAA,MACZ;AAAA,IACF,CAAC;AAED,QAAI,CAAC,QAAS;AAEd,UAAM,MAAe;AACrB,UAAM,MACJ,IAAI,MAAM,UAAU,sBAAsB,EAAE,MAAM,UAAU;AAE9D;AAAA,MAAQ,CAAC,YACP,WAAW,QAAQ,QAAQ,OAAO,QAAQ,WAAW,IAAI,SACrD,UACA,EAAE,KAAK,QAAQ,IAAI,OAAO;AAAA,IAChC;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,gBAAgB;AAAA,IACpB,CAAC,UAAqC;AACpC,eAAS,UAAU,MAAM;AACzB,cAAQ,MAAM,eAAe,MAAM,OAAO;AAAA,IAC5C;AAAA,IACA,CAAC,OAAO;AAAA,EACV;AAEA,QAAM,iBAAiB,YAAY,MAAM;AACvC,aAAS,UAAU;AACnB,YAAQ,IAAI;AAAA,EACd,GAAG,CAAC,CAAC;AAOL,QAAM,UAAU;AAAA,IACd,CAAC,UAAmC;AAClC,YAAM,YAAY,MAAM;AACxB,YAAM,QAAQ,YAAY,IAAI,IAAI;AAClC,2BAAqB,MAAM,OAAO;AAElC,YAAM,SAAS,MAAM;AACnB,YAAI,SAAS,YAAY,KAAM,SAAQ,WAAW,SAAS,OAAO;AAClE,YAAI,YAAY,IAAI,IAAI;AACtB,gBAAM,UAAU,sBAAsB,MAAM;AAAA,MAChD;AAEA,YAAM,UAAU,sBAAsB,MAAM;AAAA,IAC9C;AAAA,IACA,CAAC,OAAO;AAAA,EACV;AAEA,YAAU,MAAM,MAAM,qBAAqB,MAAM,OAAO,GAAG,CAAC,CAAC;AAE7D,SAAO,EAAE,MAAM,eAAe,gBAAgB,QAAQ;AACxD;","names":["box"]}
1
+ {"version":3,"sources":["../../../src/admin/_shell/use-fluid-hover.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useState, type PointerEvent } from 'react';\n\ntype Rect = { top: number; height: number } | null;\n\n/**\n * Fluid hover: one highlight that glides to the item nearest the pointer\n * (not just the one under it), so the target is lit before the click lands.\n * Items opt in with `data-fluid-item`; bind the handlers on their scrolling\n * container. The glide is a CSS spring on `transform`, so it retargets\n * smoothly when the pointer changes direction.\n */\nexport function useFluidHover() {\n const [rect, setRect] = useState<Rect>(null);\n\n const onPointerMove = useCallback((event: PointerEvent<HTMLElement>) => {\n const container = event.currentTarget;\n const items = container.querySelectorAll<HTMLElement>('[data-fluid-item]');\n let nearest: DOMRect | null = null;\n let best = Infinity;\n\n items.forEach((item) => {\n const box = item.getBoundingClientRect();\n const distance = Math.abs(event.clientY - (box.top + box.height / 2));\n\n if (distance < best) {\n best = distance;\n nearest = box;\n }\n });\n\n if (!nearest) return;\n\n const box: DOMRect = nearest;\n const top =\n box.top - container.getBoundingClientRect().top + container.scrollTop;\n\n setRect((current) =>\n current && current.top === top && current.height === box.height\n ? current\n : { top, height: box.height },\n );\n }, []);\n\n const onPointerLeave = useCallback(() => setRect(null), []);\n\n return { rect, onPointerMove, onPointerLeave };\n}\n"],"mappings":";AAEA,SAAS,aAAa,gBAAmC;AAWlD,SAAS,gBAAgB;AAC9B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAe,IAAI;AAE3C,QAAM,gBAAgB,YAAY,CAAC,UAAqC;AACtE,UAAM,YAAY,MAAM;AACxB,UAAM,QAAQ,UAAU,iBAA8B,mBAAmB;AACzE,QAAI,UAA0B;AAC9B,QAAI,OAAO;AAEX,UAAM,QAAQ,CAAC,SAAS;AACtB,YAAMA,OAAM,KAAK,sBAAsB;AACvC,YAAM,WAAW,KAAK,IAAI,MAAM,WAAWA,KAAI,MAAMA,KAAI,SAAS,EAAE;AAEpE,UAAI,WAAW,MAAM;AACnB,eAAO;AACP,kBAAUA;AAAA,MACZ;AAAA,IACF,CAAC;AAED,QAAI,CAAC,QAAS;AAEd,UAAM,MAAe;AACrB,UAAM,MACJ,IAAI,MAAM,UAAU,sBAAsB,EAAE,MAAM,UAAU;AAE9D;AAAA,MAAQ,CAAC,YACP,WAAW,QAAQ,QAAQ,OAAO,QAAQ,WAAW,IAAI,SACrD,UACA,EAAE,KAAK,QAAQ,IAAI,OAAO;AAAA,IAChC;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,iBAAiB,YAAY,MAAM,QAAQ,IAAI,GAAG,CAAC,CAAC;AAE1D,SAAO,EAAE,MAAM,eAAe,eAAe;AAC/C;","names":["box"]}
@@ -209,13 +209,6 @@
209
209
  min-width: 0;
210
210
  }
211
211
 
212
- /* A colour key along the top edge, matching the chart slice the card stands for. */
213
- .adm-stat[data-accent] {
214
- box-shadow:
215
- inset 0 3px 0 var(--adm-stat-accent),
216
- var(--adm-card-shadow);
217
- }
218
-
219
212
  .adm-stat-head {
220
213
  display: grid;
221
214
  gap: 2px;
@@ -32,7 +32,7 @@ function DataTable({
32
32
  {
33
33
  tone: "error",
34
34
  title: errorTitle,
35
- description: "Please try again. If it keeps happening, tell the engineering team."
35
+ description: "Please retry. If this persists, check the API logs."
36
36
  }
37
37
  ) }) }),
38
38
  !isLoading && !isError && rows && rows.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: colCount, style: { padding: 0 }, children: /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_ui/data-table.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { EmptyState } from './empty-state';\nimport { Skeleton } from './skeleton';\nimport { tableStyles } from './styles';\n\nexport interface Column<T> {\n /** Unique key for the column. */\n key: string;\n /** Header label. */\n header: string;\n /** Cell renderer. Defaults to String(row[key]). */\n render?: (row: T) => ReactNode;\n /** Render muted (secondary text colour). */\n muted?: boolean;\n /** Prevent wrapping. */\n nowrap?: boolean;\n width?: number | string;\n}\n\n/**\n * Generic admin data table. Owns the loading (skeleton), error and empty\n * states so list pages never hand-roll a <table> again. Supports an optional\n * expandable sub-row per record.\n */\nexport function DataTable<T>({\n columns,\n rows,\n getRowKey,\n isLoading,\n isError,\n errorTitle = 'Failed to load',\n emptyTitle = 'Nothing here yet',\n emptyDescription,\n emptyIcon,\n skeletonRows = 6,\n renderExpanded,\n isExpanded,\n onRowClick,\n}: {\n columns: Column<T>[];\n rows: T[] | undefined;\n getRowKey: (row: T) => string;\n isLoading?: boolean;\n isError?: boolean;\n errorTitle?: string;\n emptyTitle?: string;\n emptyDescription?: string;\n emptyIcon?: React.ComponentType<{ size?: number; color?: string }>;\n skeletonRows?: number;\n renderExpanded?: (row: T) => ReactNode;\n isExpanded?: (row: T) => boolean;\n /** When provided, the whole row becomes clickable (excluding links/buttons). */\n onRowClick?: (row: T) => void;\n}) {\n const colCount = columns.length;\n\n return (\n <div className=\"adm-table-card\">\n <table style={tableStyles.table}>\n <thead>\n <tr style={tableStyles.headRow}>\n {columns.map((c) => (\n <th key={c.key} style={{ ...tableStyles.th, width: c.width }}>\n {c.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {isLoading &&\n Array.from({ length: skeletonRows }).map((_, i) => (\n <tr key={`sk-${i}`}>\n {columns.map((c) => (\n <td key={c.key} style={tableStyles.cell}>\n <Skeleton\n width={`${50 + ((i + c.key.length) % 4) * 12}%`}\n />\n </td>\n ))}\n </tr>\n ))}\n\n {!isLoading && isError && (\n <tr>\n <td colSpan={colCount} style={{ padding: 0 }}>\n <EmptyState\n tone=\"error\"\n title={errorTitle}\n description=\"Please try again. If it keeps happening, tell the engineering team.\"\n />\n </td>\n </tr>\n )}\n\n {!isLoading && !isError && rows && rows.length === 0 && (\n <tr>\n <td colSpan={colCount} style={{ padding: 0 }}>\n <EmptyState\n icon={emptyIcon}\n title={emptyTitle}\n description={emptyDescription}\n />\n </td>\n </tr>\n )}\n\n {!isLoading &&\n !isError &&\n rows?.map((row) => (\n <RowGroup\n key={getRowKey(row)}\n row={row}\n columns={columns}\n renderExpanded={renderExpanded}\n expanded={isExpanded?.(row) ?? false}\n colCount={colCount}\n onRowClick={onRowClick}\n />\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction RowGroup<T>({\n row,\n columns,\n renderExpanded,\n expanded,\n colCount,\n onRowClick,\n}: {\n row: T;\n columns: Column<T>[];\n renderExpanded?: (row: T) => ReactNode;\n expanded: boolean;\n colCount: number;\n onRowClick?: (row: T) => void;\n}) {\n const clickable = !!onRowClick;\n return (\n <>\n <tr\n onClick={\n clickable\n ? (e) => {\n // Let inner links/buttons handle their own clicks.\n if ((e.target as HTMLElement).closest('a,button')) return;\n onRowClick!(row);\n }\n : undefined\n }\n style={clickable ? { cursor: 'pointer' } : undefined}\n onMouseEnter={\n clickable\n ? (e) => (e.currentTarget.style.background = 'var(--bg-subtle)')\n : undefined\n }\n onMouseLeave={\n clickable ? (e) => (e.currentTarget.style.background = '') : undefined\n }\n >\n {columns.map((c) => (\n <td\n key={c.key}\n style={{\n ...(c.muted ? tableStyles.cellMuted : tableStyles.cell),\n whiteSpace: c.nowrap ? 'nowrap' : undefined,\n }}\n >\n {c.render\n ? c.render(row)\n : String((row as Record<string, unknown>)[c.key] ?? '—')}\n </td>\n ))}\n </tr>\n {expanded && renderExpanded && (\n <tr>\n <td\n colSpan={colCount}\n style={{ ...tableStyles.cell, background: 'var(--bg-subtle)' }}\n >\n {renderExpanded(row)}\n </td>\n </tr>\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_ui/data-table.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { EmptyState } from './empty-state';\nimport { Skeleton } from './skeleton';\nimport { tableStyles } from './styles';\n\nexport interface Column<T> {\n /** Unique key for the column. */\n key: string;\n /** Header label. */\n header: string;\n /** Cell renderer. Defaults to String(row[key]). */\n render?: (row: T) => ReactNode;\n /** Render muted (secondary text colour). */\n muted?: boolean;\n /** Prevent wrapping. */\n nowrap?: boolean;\n width?: number | string;\n}\n\n/**\n * Generic admin data table. Owns the loading (skeleton), error and empty\n * states so list pages never hand-roll a <table> again. Supports an optional\n * expandable sub-row per record.\n */\nexport function DataTable<T>({\n columns,\n rows,\n getRowKey,\n isLoading,\n isError,\n errorTitle = 'Failed to load',\n emptyTitle = 'Nothing here yet',\n emptyDescription,\n emptyIcon,\n skeletonRows = 6,\n renderExpanded,\n isExpanded,\n onRowClick,\n}: {\n columns: Column<T>[];\n rows: T[] | undefined;\n getRowKey: (row: T) => string;\n isLoading?: boolean;\n isError?: boolean;\n errorTitle?: string;\n emptyTitle?: string;\n emptyDescription?: string;\n emptyIcon?: React.ComponentType<{ size?: number; color?: string }>;\n skeletonRows?: number;\n renderExpanded?: (row: T) => ReactNode;\n isExpanded?: (row: T) => boolean;\n /** When provided, the whole row becomes clickable (excluding links/buttons). */\n onRowClick?: (row: T) => void;\n}) {\n const colCount = columns.length;\n\n return (\n <div className=\"adm-table-card\">\n <table style={tableStyles.table}>\n <thead>\n <tr style={tableStyles.headRow}>\n {columns.map((c) => (\n <th key={c.key} style={{ ...tableStyles.th, width: c.width }}>\n {c.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {isLoading &&\n Array.from({ length: skeletonRows }).map((_, i) => (\n <tr key={`sk-${i}`}>\n {columns.map((c) => (\n <td key={c.key} style={tableStyles.cell}>\n <Skeleton\n width={`${50 + ((i + c.key.length) % 4) * 12}%`}\n />\n </td>\n ))}\n </tr>\n ))}\n\n {!isLoading && isError && (\n <tr>\n <td colSpan={colCount} style={{ padding: 0 }}>\n <EmptyState\n tone=\"error\"\n title={errorTitle}\n description=\"Please retry. If this persists, check the API logs.\"\n />\n </td>\n </tr>\n )}\n\n {!isLoading && !isError && rows && rows.length === 0 && (\n <tr>\n <td colSpan={colCount} style={{ padding: 0 }}>\n <EmptyState\n icon={emptyIcon}\n title={emptyTitle}\n description={emptyDescription}\n />\n </td>\n </tr>\n )}\n\n {!isLoading &&\n !isError &&\n rows?.map((row) => (\n <RowGroup\n key={getRowKey(row)}\n row={row}\n columns={columns}\n renderExpanded={renderExpanded}\n expanded={isExpanded?.(row) ?? false}\n colCount={colCount}\n onRowClick={onRowClick}\n />\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction RowGroup<T>({\n row,\n columns,\n renderExpanded,\n expanded,\n colCount,\n onRowClick,\n}: {\n row: T;\n columns: Column<T>[];\n renderExpanded?: (row: T) => ReactNode;\n expanded: boolean;\n colCount: number;\n onRowClick?: (row: T) => void;\n}) {\n const clickable = !!onRowClick;\n return (\n <>\n <tr\n onClick={\n clickable\n ? (e) => {\n // Let inner links/buttons handle their own clicks.\n if ((e.target as HTMLElement).closest('a,button')) return;\n onRowClick!(row);\n }\n : undefined\n }\n style={clickable ? { cursor: 'pointer' } : undefined}\n onMouseEnter={\n clickable\n ? (e) => (e.currentTarget.style.background = 'var(--bg-subtle)')\n : undefined\n }\n onMouseLeave={\n clickable ? (e) => (e.currentTarget.style.background = '') : undefined\n }\n >\n {columns.map((c) => (\n <td\n key={c.key}\n style={{\n ...(c.muted ? tableStyles.cellMuted : tableStyles.cell),\n whiteSpace: c.nowrap ? 'nowrap' : undefined,\n }}\n >\n {c.render\n ? c.render(row)\n : String((row as Record<string, unknown>)[c.key] ?? '—')}\n </td>\n ))}\n </tr>\n {expanded && renderExpanded && (\n <tr>\n <td\n colSpan={colCount}\n style={{ ...tableStyles.cell, background: 'var(--bg-subtle)' }}\n >\n {renderExpanded(row)}\n </td>\n </tr>\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -53,7 +53,7 @@ function ListCard({
53
53
  {
54
54
  tone: "error",
55
55
  title: errorTitle,
56
- description: "Please try again. If it keeps happening, tell the engineering team.",
56
+ description: "Please retry. If this persists, check the API logs.",
57
57
  action: onRetry && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: onRetry, children: "Try again" })
58
58
  }
59
59
  )
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_ui/list-card.tsx"],"sourcesContent":["'use client';\n\nimport type { ComponentType, ReactNode } from 'react';\nimport { Button, TableShell } from '@withpotter/ui-kit';\nimport { EmptyState } from './empty-state';\nimport { Skeleton } from './skeleton';\nimport styles from './list-card.module.css';\n\n/** What an empty cell shows: an en dash, never a blank. */\nexport const EMPTY = '–';\n\n/**\n * A list table in its own card: title, search and filters in the toolbar,\n * rows scrolling under a sticky header, the pager in the footer. Owns the\n * loading, error and empty rows so each list page only declares columns.\n */\nexport function ListCard({\n title,\n count,\n controls,\n head,\n columns,\n minWidth = 760,\n selection,\n isLoading,\n isError,\n onRetry,\n errorTitle,\n isEmpty,\n emptyIcon,\n emptyTitle,\n emptyDescription,\n footer,\n children,\n}: {\n title: string;\n count?: number;\n controls?: ReactNode;\n /** The header <tr> of <Th> cells. */\n head: ReactNode;\n /** Column count, for skeleton and state rows. */\n columns: number;\n minWidth?: number;\n /** Takes the header row's place while rows are ticked. */\n selection?: ReactNode;\n isLoading?: boolean;\n isError?: boolean;\n onRetry?: () => void;\n errorTitle: string;\n isEmpty?: boolean;\n emptyIcon?: ComponentType<{ size?: number; color?: string }>;\n emptyTitle: string;\n emptyDescription?: string;\n footer?: ReactNode;\n children: ReactNode;\n}) {\n const state = (body: ReactNode) => (\n <tr>\n <td colSpan={columns} className=\"!p-0\">\n {body}\n </td>\n </tr>\n );\n\n return (\n <section\n className={`adm-section ${styles.card}`}\n aria-label={title}\n aria-busy={isLoading || undefined}\n >\n <TableShell\n bare\n minWidth={minWidth}\n head={head}\n selection={selection}\n columnCount={columns}\n toolbar={\n <>\n <h2 className={styles.title}>\n {title}\n {count !== undefined && (\n <span className={styles.count}>{count}</span>\n )}\n </h2>\n {controls}\n </>\n }\n footer={footer}\n >\n {isLoading\n ? Array.from({ length: 8 }, (_, row) => (\n <tr key={`sk-${row}`} aria-hidden>\n {Array.from({ length: columns }, (_, col) => (\n <td key={col}>\n <Skeleton width={`${48 + ((row + col) % 4) * 12}%`} />\n </td>\n ))}\n </tr>\n ))\n : isError\n ? state(\n <EmptyState\n tone=\"error\"\n title={errorTitle}\n description=\"Please try again. If it keeps happening, tell the engineering team.\"\n action={\n onRetry && (\n <Button size=\"sm\" variant=\"secondary\" onClick={onRetry}>\n Try again\n </Button>\n )\n }\n />,\n )\n : isEmpty\n ? state(\n <EmptyState\n icon={emptyIcon}\n title={emptyTitle}\n description={emptyDescription}\n />,\n )\n : children}\n </TableShell>\n </section>\n );\n}\n\n/** Count on the left, Previous · Page n of m · Next on the right. */\nexport function ListPager({\n page,\n totalPages,\n total,\n noun,\n onPage,\n}: {\n page: number;\n totalPages: number;\n total?: number;\n noun: string;\n onPage: (next: number) => void;\n}) {\n return (\n <div className={styles.footer}>\n <span>\n {total != null\n ? `${new Intl.NumberFormat().format(total)} ${noun}`\n : ''}\n </span>\n <div className={styles.pages}>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => onPage(Math.max(1, page - 1))}\n disabled={page <= 1}\n >\n Previous\n </Button>\n <span>\n Page {page} of {totalPages}\n </span>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => onPage(Math.min(totalPages, page + 1))}\n disabled={page >= totalPages}\n >\n Next\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AA0DM,SAmBI,UAnBJ,KAoBM,YApBN;AAvDN,SAAS,QAAQ,kBAAkB;AACnC,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,OAAO,YAAY;AAGZ,MAAM,QAAQ;AAOd,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAqBG;AACD,QAAM,QAAQ,CAAC,SACb,oBAAC,QACC,8BAAC,QAAG,SAAS,SAAS,WAAU,QAC7B,gBACH,GACF;AAGF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,eAAe,OAAO,IAAI;AAAA,MACrC,cAAY;AAAA,MACZ,aAAW,aAAa;AAAA,MAExB;AAAA,QAAC;AAAA;AAAA,UACC,MAAI;AAAA,UACJ;AAAA,UACA;AAAA,UACA;AAAA,UACA,aAAa;AAAA,UACb,SACE,iCACE;AAAA,iCAAC,QAAG,WAAW,OAAO,OACnB;AAAA;AAAA,cACA,UAAU,UACT,oBAAC,UAAK,WAAW,OAAO,OAAQ,iBAAM;AAAA,eAE1C;AAAA,YACC;AAAA,aACH;AAAA,UAEF;AAAA,UAEC,sBACG,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,QAC5B,oBAAC,QAAqB,eAAW,MAC9B,gBAAM,KAAK,EAAE,QAAQ,QAAQ,GAAG,CAACA,IAAG,QACnC,oBAAC,QACC,8BAAC,YAAS,OAAO,GAAG,MAAO,MAAM,OAAO,IAAK,EAAE,KAAK,KAD7C,GAET,CACD,KALM,MAAM,GAAG,EAMlB,CACD,IACD,UACE;AAAA,YACE;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,OAAO;AAAA,gBACP,aAAY;AAAA,gBACZ,QACE,WACE,oBAAC,UAAO,MAAK,MAAK,SAAQ,aAAY,SAAS,SAAS,uBAExD;AAAA;AAAA,YAGN;AAAA,UACF,IACA,UACE;AAAA,YACE;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,OAAO;AAAA,gBACP,aAAa;AAAA;AAAA,YACf;AAAA,UACF,IACA;AAAA;AAAA,MACV;AAAA;AAAA,EACF;AAEJ;AAGO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE,qBAAC,SAAI,WAAW,OAAO,QACrB;AAAA,wBAAC,UACE,mBAAS,OACN,GAAG,IAAI,KAAK,aAAa,EAAE,OAAO,KAAK,CAAC,IAAI,IAAI,KAChD,IACN;AAAA,IACA,qBAAC,SAAI,WAAW,OAAO,OACrB;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,SAAS,MAAM,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,UAC3C,UAAU,QAAQ;AAAA,UACnB;AAAA;AAAA,MAED;AAAA,MACA,qBAAC,UAAK;AAAA;AAAA,QACE;AAAA,QAAK;AAAA,QAAK;AAAA,SAClB;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,SAAS,MAAM,OAAO,KAAK,IAAI,YAAY,OAAO,CAAC,CAAC;AAAA,UACpD,UAAU,QAAQ;AAAA,UACnB;AAAA;AAAA,MAED;AAAA,OACF;AAAA,KACF;AAEJ;","names":["_"]}
1
+ {"version":3,"sources":["../../../src/admin/_ui/list-card.tsx"],"sourcesContent":["'use client';\n\nimport type { ComponentType, ReactNode } from 'react';\nimport { Button, TableShell } from '@withpotter/ui-kit';\nimport { EmptyState } from './empty-state';\nimport { Skeleton } from './skeleton';\nimport styles from './list-card.module.css';\n\n/** What an empty cell shows: an en dash, never a blank. */\nexport const EMPTY = '–';\n\n/**\n * A list table in its own card: title, search and filters in the toolbar,\n * rows scrolling under a sticky header, the pager in the footer. Owns the\n * loading, error and empty rows so each list page only declares columns.\n */\nexport function ListCard({\n title,\n count,\n controls,\n head,\n columns,\n minWidth = 760,\n selection,\n isLoading,\n isError,\n onRetry,\n errorTitle,\n isEmpty,\n emptyIcon,\n emptyTitle,\n emptyDescription,\n footer,\n children,\n}: {\n title: string;\n count?: number;\n controls?: ReactNode;\n /** The header <tr> of <Th> cells. */\n head: ReactNode;\n /** Column count, for skeleton and state rows. */\n columns: number;\n minWidth?: number;\n /** Takes the header row's place while rows are ticked. */\n selection?: ReactNode;\n isLoading?: boolean;\n isError?: boolean;\n onRetry?: () => void;\n errorTitle: string;\n isEmpty?: boolean;\n emptyIcon?: ComponentType<{ size?: number; color?: string }>;\n emptyTitle: string;\n emptyDescription?: string;\n footer?: ReactNode;\n children: ReactNode;\n}) {\n const state = (body: ReactNode) => (\n <tr>\n <td colSpan={columns} className=\"!p-0\">\n {body}\n </td>\n </tr>\n );\n\n return (\n <section\n className={`adm-section ${styles.card}`}\n aria-label={title}\n aria-busy={isLoading || undefined}\n >\n <TableShell\n bare\n minWidth={minWidth}\n head={head}\n selection={selection}\n columnCount={columns}\n toolbar={\n <>\n <h2 className={styles.title}>\n {title}\n {count !== undefined && (\n <span className={styles.count}>{count}</span>\n )}\n </h2>\n {controls}\n </>\n }\n footer={footer}\n >\n {isLoading\n ? Array.from({ length: 8 }, (_, row) => (\n <tr key={`sk-${row}`} aria-hidden>\n {Array.from({ length: columns }, (_, col) => (\n <td key={col}>\n <Skeleton width={`${48 + ((row + col) % 4) * 12}%`} />\n </td>\n ))}\n </tr>\n ))\n : isError\n ? state(\n <EmptyState\n tone=\"error\"\n title={errorTitle}\n description=\"Please retry. If this persists, check the API logs.\"\n action={\n onRetry && (\n <Button size=\"sm\" variant=\"secondary\" onClick={onRetry}>\n Try again\n </Button>\n )\n }\n />,\n )\n : isEmpty\n ? state(\n <EmptyState\n icon={emptyIcon}\n title={emptyTitle}\n description={emptyDescription}\n />,\n )\n : children}\n </TableShell>\n </section>\n );\n}\n\n/** Count on the left, Previous · Page n of m · Next on the right. */\nexport function ListPager({\n page,\n totalPages,\n total,\n noun,\n onPage,\n}: {\n page: number;\n totalPages: number;\n total?: number;\n noun: string;\n onPage: (next: number) => void;\n}) {\n return (\n <div className={styles.footer}>\n <span>\n {total != null\n ? `${new Intl.NumberFormat().format(total)} ${noun}`\n : ''}\n </span>\n <div className={styles.pages}>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => onPage(Math.max(1, page - 1))}\n disabled={page <= 1}\n >\n Previous\n </Button>\n <span>\n Page {page} of {totalPages}\n </span>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => onPage(Math.min(totalPages, page + 1))}\n disabled={page >= totalPages}\n >\n Next\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AA0DM,SAmBI,UAnBJ,KAoBM,YApBN;AAvDN,SAAS,QAAQ,kBAAkB;AACnC,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,OAAO,YAAY;AAGZ,MAAM,QAAQ;AAOd,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAqBG;AACD,QAAM,QAAQ,CAAC,SACb,oBAAC,QACC,8BAAC,QAAG,SAAS,SAAS,WAAU,QAC7B,gBACH,GACF;AAGF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,eAAe,OAAO,IAAI;AAAA,MACrC,cAAY;AAAA,MACZ,aAAW,aAAa;AAAA,MAExB;AAAA,QAAC;AAAA;AAAA,UACC,MAAI;AAAA,UACJ;AAAA,UACA;AAAA,UACA;AAAA,UACA,aAAa;AAAA,UACb,SACE,iCACE;AAAA,iCAAC,QAAG,WAAW,OAAO,OACnB;AAAA;AAAA,cACA,UAAU,UACT,oBAAC,UAAK,WAAW,OAAO,OAAQ,iBAAM;AAAA,eAE1C;AAAA,YACC;AAAA,aACH;AAAA,UAEF;AAAA,UAEC,sBACG,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,QAC5B,oBAAC,QAAqB,eAAW,MAC9B,gBAAM,KAAK,EAAE,QAAQ,QAAQ,GAAG,CAACA,IAAG,QACnC,oBAAC,QACC,8BAAC,YAAS,OAAO,GAAG,MAAO,MAAM,OAAO,IAAK,EAAE,KAAK,KAD7C,GAET,CACD,KALM,MAAM,GAAG,EAMlB,CACD,IACD,UACE;AAAA,YACE;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,OAAO;AAAA,gBACP,aAAY;AAAA,gBACZ,QACE,WACE,oBAAC,UAAO,MAAK,MAAK,SAAQ,aAAY,SAAS,SAAS,uBAExD;AAAA;AAAA,YAGN;AAAA,UACF,IACA,UACE;AAAA,YACE;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,OAAO;AAAA,gBACP,aAAa;AAAA;AAAA,YACf;AAAA,UACF,IACA;AAAA;AAAA,MACV;AAAA;AAAA,EACF;AAEJ;AAGO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE,qBAAC,SAAI,WAAW,OAAO,QACrB;AAAA,wBAAC,UACE,mBAAS,OACN,GAAG,IAAI,KAAK,aAAa,EAAE,OAAO,KAAK,CAAC,IAAI,IAAI,KAChD,IACN;AAAA,IACA,qBAAC,SAAI,WAAW,OAAO,OACrB;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,SAAS,MAAM,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,UAC3C,UAAU,QAAQ;AAAA,UACnB;AAAA;AAAA,MAED;AAAA,MACA,qBAAC,UAAK;AAAA;AAAA,QACE;AAAA,QAAK;AAAA,QAAK;AAAA,SAClB;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,SAAS,MAAM,OAAO,KAAK,IAAI,YAAY,OAAO,CAAC,CAAC;AAAA,UACpD,UAAU,QAAQ;AAAA,UACnB;AAAA;AAAA,MAED;AAAA,OACF;AAAA,KACF;AAEJ;","names":["_"]}
@@ -31,13 +31,23 @@
31
31
  color: var(--text-secondary);
32
32
  }
33
33
 
34
- /* The list grows with its rows; only a wide table scrolls, sideways. */
34
+ /* The table's own scroller holds both axes, so the header can stick to it. */
35
35
  .card div:has(> table),
36
36
  .fill div:has(> table) {
37
- overflow-x: auto;
37
+ max-height: calc(var(--adm-panel-h) * 1.4);
38
+ overflow: auto;
39
+ overscroll-behavior: contain;
38
40
  scrollbar-width: thin;
39
41
  }
40
42
 
43
+ /* Inside a Panel: the table fills the body and scrolls on its own. */
44
+ .fill,
45
+ .fill > div,
46
+ .fill div:has(> table) {
47
+ height: 100%;
48
+ max-height: 100%;
49
+ }
50
+
41
51
  .note {
42
52
  margin: 0;
43
53
  padding: 0 20px;
@@ -45,8 +55,12 @@
45
55
 
46
56
  .card thead th,
47
57
  .fill thead th {
58
+ position: sticky;
59
+ top: 0;
60
+ z-index: 1;
48
61
  white-space: nowrap;
49
62
  background: var(--bg-base);
63
+ box-shadow: inset 0 -1px 0 var(--border-subtle);
50
64
  }
51
65
 
52
66
  .footer {
@@ -4,7 +4,6 @@ function formatMoney(amount, currency, decimals = 0) {
4
4
  try {
5
5
  return new Intl.NumberFormat("en-US", {
6
6
  style: "currency",
7
- currencyDisplay: "narrowSymbol",
8
7
  currency,
9
8
  maximumFractionDigits: decimals
10
9
  }).format(amount);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/_ui/money-view.ts"],"sourcesContent":["/**\n * How a page shows money that comes back once per currency.\n *\n * Amounts in different currencies are never added together and never\n * converted. A headline figure shows one line per currency; a chart or a\n * leaderboard needs one currency, so it follows the currency filter, or the\n * only currency there is, and otherwise asks for one to be picked.\n */\n\n/** An amount in its own currency's major unit: 120000, 'NGN' → \"₦120,000\". */\nexport function formatMoney(amount: number, currency: string, decimals = 0): string {\n const plain = () =>\n new Intl.NumberFormat('en-US', { maximumFractionDigits: decimals }).format(amount);\n // XXX is ISO \"no currency\": the engine's label for a row with none.\n if (currency === 'XXX') return `${plain()} (no currency)`;\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency,\n maximumFractionDigits: decimals,\n }).format(amount);\n } catch {\n return `${currency} ${plain()}`;\n }\n}\n\n/** A formatter for one currency, for charts and tables that take `(n) => string`. */\nexport const moneyIn =\n (currency: string, decimals = 0) =>\n (amount: number): string =>\n formatMoney(amount, currency, decimals);\n\n/**\n * The one currency a chart or leaderboard shows: the filter's choice, else\n * the only currency in the data, else null (several, none chosen).\n */\nexport function focusCurrency(currencies: readonly string[], selected: string): string | null {\n if (selected) return selected;\n return currencies.length === 1 ? currencies[0] : null;\n}\n\n/** The rows for the chosen currency; every row when none is chosen. */\nexport function shownRows<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n): T[] {\n return (rows ?? []).filter((r) => !selected || r.currency === selected);\n}\n\n/** The row for one currency, if the data has it. */\nexport function rowFor<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n currency: string | null,\n): T | undefined {\n return currency ? (rows ?? []).find((r) => r.currency === currency) : undefined;\n}\n\n/** One formatted line per shown currency, A→Z as the API sends them. */\nexport function moneyLines<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n amountOf: (row: T) => number,\n decimals = 0,\n): string[] {\n return shownRows(rows, selected).map((r) => formatMoney(amountOf(r), r.currency, decimals));\n}\n\n/** What an empty money figure says: \"No NGN\" when filtered, else the fallback. */\nexport function noneText(selected: string, fallback = '–'): string {\n return selected ? `No ${selected}` : fallback;\n}\n\n/** The line under a chart that cannot add currencies together. */\nexport const PICK_CURRENCY = 'Pick a currency to see this. Amounts in different currencies are never added together.';\n"],"mappings":"AAUO,SAAS,YAAY,QAAgB,UAAkB,WAAW,GAAW;AAClF,QAAM,QAAQ,MACZ,IAAI,KAAK,aAAa,SAAS,EAAE,uBAAuB,SAAS,CAAC,EAAE,OAAO,MAAM;AAEnF,MAAI,aAAa,MAAO,QAAO,GAAG,MAAM,CAAC;AACzC,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP,iBAAiB;AAAA,MACjB;AAAA,MACA,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,MAAM;AAAA,EAClB,QAAQ;AACN,WAAO,GAAG,QAAQ,IAAI,MAAM,CAAC;AAAA,EAC/B;AACF;AAGO,MAAM,UACX,CAAC,UAAkB,WAAW,MAC9B,CAAC,WACC,YAAY,QAAQ,UAAU,QAAQ;AAMnC,SAAS,cAAc,YAA+B,UAAiC;AAC5F,MAAI,SAAU,QAAO;AACrB,SAAO,WAAW,WAAW,IAAI,WAAW,CAAC,IAAI;AACnD;AAGO,SAAS,UACd,MACA,UACK;AACL,UAAQ,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ;AACxE;AAGO,SAAS,OACd,MACA,UACe;AACf,SAAO,YAAY,QAAQ,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,IAAI;AACxE;AAGO,SAAS,WACd,MACA,UACA,UACA,WAAW,GACD;AACV,SAAO,UAAU,MAAM,QAAQ,EAAE,IAAI,CAAC,MAAM,YAAY,SAAS,CAAC,GAAG,EAAE,UAAU,QAAQ,CAAC;AAC5F;AAGO,SAAS,SAAS,UAAkB,WAAW,UAAa;AACjE,SAAO,WAAW,MAAM,QAAQ,KAAK;AACvC;AAGO,MAAM,gBAAgB;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/_ui/money-view.ts"],"sourcesContent":["/**\n * How a page shows money that comes back once per currency.\n *\n * Amounts in different currencies are never added together and never\n * converted. A headline figure shows one line per currency; a chart or a\n * leaderboard needs one currency, so it follows the currency filter, or the\n * only currency there is, and otherwise asks for one to be picked.\n */\n\n/** An amount in its own currency's major unit: 120000, 'NGN' → \"NGN 120,000\". */\nexport function formatMoney(amount: number, currency: string, decimals = 0): string {\n const plain = () =>\n new Intl.NumberFormat('en-US', { maximumFractionDigits: decimals }).format(amount);\n // XXX is ISO \"no currency\": the engine's label for a row with none.\n if (currency === 'XXX') return `${plain()} (no currency)`;\n try {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n maximumFractionDigits: decimals,\n }).format(amount);\n } catch {\n return `${currency} ${plain()}`;\n }\n}\n\n/** A formatter for one currency, for charts and tables that take `(n) => string`. */\nexport const moneyIn =\n (currency: string, decimals = 0) =>\n (amount: number): string =>\n formatMoney(amount, currency, decimals);\n\n/**\n * The one currency a chart or leaderboard shows: the filter's choice, else\n * the only currency in the data, else null (several, none chosen).\n */\nexport function focusCurrency(currencies: readonly string[], selected: string): string | null {\n if (selected) return selected;\n return currencies.length === 1 ? currencies[0] : null;\n}\n\n/** The rows for the chosen currency; every row when none is chosen. */\nexport function shownRows<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n): T[] {\n return (rows ?? []).filter((r) => !selected || r.currency === selected);\n}\n\n/** The row for one currency, if the data has it. */\nexport function rowFor<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n currency: string | null,\n): T | undefined {\n return currency ? (rows ?? []).find((r) => r.currency === currency) : undefined;\n}\n\n/** One formatted line per shown currency, A→Z as the API sends them. */\nexport function moneyLines<T extends { currency: string }>(\n rows: readonly T[] | undefined,\n selected: string,\n amountOf: (row: T) => number,\n decimals = 0,\n): string[] {\n return shownRows(rows, selected).map((r) => formatMoney(amountOf(r), r.currency, decimals));\n}\n\n/** What an empty money figure says: \"No NGN\" when filtered, else the fallback. */\nexport function noneText(selected: string, fallback = '–'): string {\n return selected ? `No ${selected}` : fallback;\n}\n\n/** The line under a chart that cannot add currencies together. */\nexport const PICK_CURRENCY = 'Pick a currency to see this. Amounts in different currencies are never added together.';\n"],"mappings":"AAUO,SAAS,YAAY,QAAgB,UAAkB,WAAW,GAAW;AAClF,QAAM,QAAQ,MACZ,IAAI,KAAK,aAAa,SAAS,EAAE,uBAAuB,SAAS,CAAC,EAAE,OAAO,MAAM;AAEnF,MAAI,aAAa,MAAO,QAAO,GAAG,MAAM,CAAC;AACzC,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,SAAS;AAAA,MACpC,OAAO;AAAA,MACP;AAAA,MACA,uBAAuB;AAAA,IACzB,CAAC,EAAE,OAAO,MAAM;AAAA,EAClB,QAAQ;AACN,WAAO,GAAG,QAAQ,IAAI,MAAM,CAAC;AAAA,EAC/B;AACF;AAGO,MAAM,UACX,CAAC,UAAkB,WAAW,MAC9B,CAAC,WACC,YAAY,QAAQ,UAAU,QAAQ;AAMnC,SAAS,cAAc,YAA+B,UAAiC;AAC5F,MAAI,SAAU,QAAO;AACrB,SAAO,WAAW,WAAW,IAAI,WAAW,CAAC,IAAI;AACnD;AAGO,SAAS,UACd,MACA,UACK;AACL,UAAQ,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,aAAa,QAAQ;AACxE;AAGO,SAAS,OACd,MACA,UACe;AACf,SAAO,YAAY,QAAQ,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,IAAI;AACxE;AAGO,SAAS,WACd,MACA,UACA,UACA,WAAW,GACD;AACV,SAAO,UAAU,MAAM,QAAQ,EAAE,IAAI,CAAC,MAAM,YAAY,SAAS,CAAC,GAAG,EAAE,UAAU,QAAQ,CAAC;AAC5F;AAGO,SAAS,SAAS,UAAkB,WAAW,UAAa;AACjE,SAAO,WAAW,MAAM,QAAQ,KAAK;AACvC;AAGO,MAAM,gBAAgB;","names":[]}