@stacksjs/defaults 0.70.54

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 (286) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +2 -0
  3. package/package.json +20 -0
  4. package/resources/assets/fonts/013717e9.woff +0 -0
  5. package/resources/assets/fonts/03355817.woff2 +0 -0
  6. package/resources/assets/fonts/0380de7e.woff2 +0 -0
  7. package/resources/assets/fonts/0ba5d32a.woff +0 -0
  8. package/resources/assets/fonts/0bd57ddf.woff2 +0 -0
  9. package/resources/assets/fonts/19de0a60.woff2 +0 -0
  10. package/resources/assets/fonts/20b87e84.woff +0 -0
  11. package/resources/assets/fonts/4bbc67e1.woff +0 -0
  12. package/resources/assets/fonts/576fb0d7.woff +0 -0
  13. package/resources/assets/fonts/5ea10ee5.woff2 +0 -0
  14. package/resources/assets/fonts/6413fc0a.woff +0 -0
  15. package/resources/assets/fonts/72c114cf.woff +0 -0
  16. package/resources/assets/fonts/73c57e73.woff2 +0 -0
  17. package/resources/assets/fonts/813dca6c.woff +0 -0
  18. package/resources/assets/fonts/8a9b0995.woff2 +0 -0
  19. package/resources/assets/fonts/90b4832f.woff +0 -0
  20. package/resources/assets/fonts/9394ab85.woff2 +0 -0
  21. package/resources/assets/fonts/9b3c7d7f.woff2 +0 -0
  22. package/resources/assets/fonts/Monaco.ttf +0 -0
  23. package/resources/assets/fonts/b1b36d74.woff +0 -0
  24. package/resources/assets/fonts/b5d74798.woff2 +0 -0
  25. package/resources/assets/fonts/b9f81a4d.woff +0 -0
  26. package/resources/assets/fonts/c9784008.woff2 +0 -0
  27. package/resources/assets/fonts/cf692227.woff +0 -0
  28. package/resources/assets/fonts/d1c6ab51.woff2 +0 -0
  29. package/resources/assets/fonts/e2eb62fc.woff +0 -0
  30. package/resources/assets/fonts/fe21f8d8.woff2 +0 -0
  31. package/resources/assets/fonts/nps/CampmateScript-Regular.woff2 +0 -0
  32. package/resources/assets/fonts/nps/NPS_2026-variable.woff2 +0 -0
  33. package/resources/assets/fonts/nps/RedwoodSerif-Regular.woff2 +0 -0
  34. package/resources/assets/fonts/nps/SequoiaSans-Regular.woff2 +0 -0
  35. package/resources/assets/fonts/nps/Switchback-Regular.woff2 +0 -0
  36. package/resources/assets/images/app-icon.png +0 -0
  37. package/resources/assets/images/bg-img.png +0 -0
  38. package/resources/assets/images/park-ridge.svg +15 -0
  39. package/resources/assets/images/topography.svg +1 -0
  40. package/resources/assets/scripts/main.ts +2153 -0
  41. package/resources/assets/styles/README.md +19 -0
  42. package/resources/assets/styles/docs/main.css +1 -0
  43. package/resources/assets/styles/docs/overrides.css +1 -0
  44. package/resources/assets/styles/docs/vars.css +8 -0
  45. package/resources/assets/styles/main.css +1419 -0
  46. package/resources/components/Button.stx +70 -0
  47. package/resources/components/Buttons/BaseButton.stx +39 -0
  48. package/resources/components/Buttons/Counter.stx +28 -0
  49. package/resources/components/Buttons/ToggleDark.stx +11 -0
  50. package/resources/components/Container.stx +10 -0
  51. package/resources/components/Dashboard/.gitkeep +0 -0
  52. package/resources/components/Dashboard/Auth/ForgotPassword.stx +127 -0
  53. package/resources/components/Dashboard/Auth/Login.stx +212 -0
  54. package/resources/components/Dashboard/Auth/Register.stx +306 -0
  55. package/resources/components/Dashboard/Avatar.stx +123 -0
  56. package/resources/components/Dashboard/Billing/ActivePlan.stx +57 -0
  57. package/resources/components/Dashboard/Billing/CardBrands.stx +32 -0
  58. package/resources/components/Dashboard/Billing/CardForm.stx +34 -0
  59. package/resources/components/Dashboard/Billing/OneTimePayment.stx +28 -0
  60. package/resources/components/Dashboard/Billing/PaymentForm.stx +33 -0
  61. package/resources/components/Dashboard/Billing/PaymentMethod.stx +66 -0
  62. package/resources/components/Dashboard/Billing/PaymentMethodList.stx +52 -0
  63. package/resources/components/Dashboard/Billing/Plans.stx +136 -0
  64. package/resources/components/Dashboard/Buttons/AppButton.stx +48 -0
  65. package/resources/components/Dashboard/Buttons/IconButton.stx +20 -0
  66. package/resources/components/Dashboard/Card.stx +78 -0
  67. package/resources/components/Dashboard/Chart.stx +75 -0
  68. package/resources/components/Dashboard/CodeEditor.stx +41 -0
  69. package/resources/components/Dashboard/Commerce/BulkActionBar.stx +70 -0
  70. package/resources/components/Dashboard/Commerce/CategoriesTable.stx +133 -0
  71. package/resources/components/Dashboard/Commerce/CouponsTable.stx +161 -0
  72. package/resources/components/Dashboard/Commerce/CustomersTable.stx +142 -0
  73. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +80 -0
  74. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryTable.stx +85 -0
  75. package/resources/components/Dashboard/Commerce/Delivery/DriversTable.stx +99 -0
  76. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysTable.stx +92 -0
  77. package/resources/components/Dashboard/Commerce/Delivery/LicenseTemplatesTable.stx +88 -0
  78. package/resources/components/Dashboard/Commerce/Delivery/Pagination.stx +122 -0
  79. package/resources/components/Dashboard/Commerce/Delivery/SearchFilter.stx +23 -0
  80. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +80 -0
  81. package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +59 -0
  82. package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesTable.stx +71 -0
  83. package/resources/components/Dashboard/Commerce/Delivery/TabNavigation.stx +36 -0
  84. package/resources/components/Dashboard/Commerce/Forms/CouponsForm.stx +191 -0
  85. package/resources/components/Dashboard/Commerce/OrdersTable.stx +131 -0
  86. package/resources/components/Dashboard/Commerce/ProductsTable.stx +170 -0
  87. package/resources/components/Dashboard/CreateRecordModal.stx +363 -0
  88. package/resources/components/Dashboard/DashboardLayout.stx +155 -0
  89. package/resources/components/Dashboard/Deployments/ActivityFeed.stx +54 -0
  90. package/resources/components/Dashboard/Deployments/DeployScript.stx +23 -0
  91. package/resources/components/Dashboard/Deployments/DeploymentHistory.stx +97 -0
  92. package/resources/components/Dashboard/Deployments/DeploymentList.stx +94 -0
  93. package/resources/components/Dashboard/Deployments/LiveTerminalOutput.stx +18 -0
  94. package/resources/components/Dashboard/Elements/Dropdown.stx +57 -0
  95. package/resources/components/Dashboard/Email/EmailActivityChart.stx +111 -0
  96. package/resources/components/Dashboard/Email/EmailCompose.stx +72 -0
  97. package/resources/components/Dashboard/Email/EmailDetail.stx +105 -0
  98. package/resources/components/Dashboard/Email/EmailList.stx +117 -0
  99. package/resources/components/Dashboard/Email/EmailSearch.stx +40 -0
  100. package/resources/components/Dashboard/Email/EmailSidebar.stx +133 -0
  101. package/resources/components/Dashboard/GlobalSearch.stx +124 -0
  102. package/resources/components/Dashboard/MobileSidebar.stx +47 -0
  103. package/resources/components/Dashboard/Modals/BaseModal.stx +50 -0
  104. package/resources/components/Dashboard/Modals/Popups/Alert.stx +112 -0
  105. package/resources/components/Dashboard/Modals/Popups/Toast.stx +70 -0
  106. package/resources/components/Dashboard/Modals/ToastWrapper.stx +35 -0
  107. package/resources/components/Dashboard/Monitoring/ErrorDetailModal.stx +228 -0
  108. package/resources/components/Dashboard/Monitoring/ErrorsTable.stx +184 -0
  109. package/resources/components/Dashboard/Navbar.stx +194 -0
  110. package/resources/components/Dashboard/NavbarModern.stx +188 -0
  111. package/resources/components/Dashboard/NotificationErrorModal.stx +105 -0
  112. package/resources/components/Dashboard/NotificationStatusBadge.stx +20 -0
  113. package/resources/components/Dashboard/Pagination.stx +90 -0
  114. package/resources/components/Dashboard/Queue/QueueTable.stx +127 -0
  115. package/resources/components/Dashboard/Settings/Forms/AIForm.stx +126 -0
  116. package/resources/components/Dashboard/Settings/Forms/AnalyticsForm.stx +52 -0
  117. package/resources/components/Dashboard/Settings/Forms/AppForm.stx +157 -0
  118. package/resources/components/Dashboard/Settings/Forms/CLIForm.stx +66 -0
  119. package/resources/components/Dashboard/Settings/Forms/CacheForm.stx +253 -0
  120. package/resources/components/Dashboard/Settings/Forms/CloudForm.stx +441 -0
  121. package/resources/components/Dashboard/Settings/Forms/DNSForm.stx +307 -0
  122. package/resources/components/Dashboard/Settings/Forms/DatabaseForm.stx +168 -0
  123. package/resources/components/Dashboard/Settings/Forms/EmailForm.stx +100 -0
  124. package/resources/components/Dashboard/Settings/Forms/FileSystemForm.stx +259 -0
  125. package/resources/components/Dashboard/Settings/Forms/HashingForm.stx +119 -0
  126. package/resources/components/Dashboard/Settings/Forms/LibraryForm.stx +389 -0
  127. package/resources/components/Dashboard/Settings/Forms/QueueForm.stx +219 -0
  128. package/resources/components/Dashboard/Settings/Forms/SearchEngineForm.stx +48 -0
  129. package/resources/components/Dashboard/Settings/Forms/SecurityForm.stx +143 -0
  130. package/resources/components/Dashboard/Settings/Forms/ServicesForm.stx +194 -0
  131. package/resources/components/Dashboard/Settings/Forms/StorageForm.stx +191 -0
  132. package/resources/components/Dashboard/Settings/Forms/TeamsForm.stx +33 -0
  133. package/resources/components/Dashboard/Settings/SettingsFormManager.stx +181 -0
  134. package/resources/components/Dashboard/SettingsHeader.stx +88 -0
  135. package/resources/components/Dashboard/SettingsSidebar.stx +234 -0
  136. package/resources/components/Dashboard/Sidebar.stx +23 -0
  137. package/resources/components/Dashboard/SidebarModern.stx +249 -0
  138. package/resources/components/Dashboard/Skeleton/LoadingCard.stx +15 -0
  139. package/resources/components/Dashboard/Skeleton/LoadingDetails.stx +26 -0
  140. package/resources/components/Dashboard/Table.stx +82 -0
  141. package/resources/components/Dashboard/TerminalContainer.stx +97 -0
  142. package/resources/components/Dashboard/Tooltip.stx +166 -0
  143. package/resources/components/Dashboard/Transaction/index.stx +88 -0
  144. package/resources/components/Dashboard/UI/ActivityTable.stx +54 -0
  145. package/resources/components/Dashboard/UI/Avatar.stx +102 -0
  146. package/resources/components/Dashboard/UI/Badge.stx +66 -0
  147. package/resources/components/Dashboard/UI/Button.stx +73 -0
  148. package/resources/components/Dashboard/UI/Card.stx +51 -0
  149. package/resources/components/Dashboard/UI/ChartCard.stx +36 -0
  150. package/resources/components/Dashboard/UI/Checkbox.stx +74 -0
  151. package/resources/components/Dashboard/UI/ConfirmDialog.stx +74 -0
  152. package/resources/components/Dashboard/UI/DataTable.stx +45 -0
  153. package/resources/components/Dashboard/UI/Dropdown.stx +72 -0
  154. package/resources/components/Dashboard/UI/DropdownItem.stx +80 -0
  155. package/resources/components/Dashboard/UI/EmptyState.stx +50 -0
  156. package/resources/components/Dashboard/UI/FilterBar.stx +48 -0
  157. package/resources/components/Dashboard/UI/Input.stx +77 -0
  158. package/resources/components/Dashboard/UI/Modal.stx +75 -0
  159. package/resources/components/Dashboard/UI/PageHeader.stx +69 -0
  160. package/resources/components/Dashboard/UI/PageLayout.stx +84 -0
  161. package/resources/components/Dashboard/UI/Pagination.stx +126 -0
  162. package/resources/components/Dashboard/UI/QuickLinks.stx +31 -0
  163. package/resources/components/Dashboard/UI/RouterLink.stx +31 -0
  164. package/resources/components/Dashboard/UI/Select.stx +84 -0
  165. package/resources/components/Dashboard/UI/ServiceHealth.stx +38 -0
  166. package/resources/components/Dashboard/UI/StatsCard.stx +79 -0
  167. package/resources/components/Dashboard/UI/Table.stx +143 -0
  168. package/resources/components/Dashboard/UI/Tabs.stx +45 -0
  169. package/resources/components/Dashboard/UI/Textarea.stx +93 -0
  170. package/resources/components/Dashboard/UI/Toggle.stx +84 -0
  171. package/resources/components/Dashboard/UI/WindowControls.stx +135 -0
  172. package/resources/components/Dashboard/Widget.stx +89 -0
  173. package/resources/components/Dashboard/WindowControls.stx +119 -0
  174. package/resources/components/Dashboard/index.ts +106 -0
  175. package/resources/components/Docs/Demo/ComboboxCode.md +114 -0
  176. package/resources/components/Docs/Demo/ComboboxDemo.stx +91 -0
  177. package/resources/components/Docs/Demo/DialogCode.md +42 -0
  178. package/resources/components/Docs/Demo/DialogDemo.stx +76 -0
  179. package/resources/components/Docs/Demo/DocsPlayground.stx +39 -0
  180. package/resources/components/Docs/Demo/DropdownCode.md +109 -0
  181. package/resources/components/Docs/Demo/DropdownDemo.stx +82 -0
  182. package/resources/components/Docs/Demo/Hero.stx +100 -0
  183. package/resources/components/Docs/Demo/ListboxCode.md +87 -0
  184. package/resources/components/Docs/Demo/ListboxDemo.stx +59 -0
  185. package/resources/components/Docs/Demo/NotificationCode.md +173 -0
  186. package/resources/components/Docs/Demo/NotificationDemo.stx +99 -0
  187. package/resources/components/Docs/Demo/PopoverCode.md +161 -0
  188. package/resources/components/Docs/Demo/PopoverDemo.stx +96 -0
  189. package/resources/components/Docs/Demo/RadioGroupCode.md +98 -0
  190. package/resources/components/Docs/Demo/RadioGroupDemo.stx +53 -0
  191. package/resources/components/Docs/Demo/StepperCode.md +216 -0
  192. package/resources/components/Docs/Demo/StepperDemo.stx +137 -0
  193. package/resources/components/Docs/Demo/SwitchCode.md +24 -0
  194. package/resources/components/Docs/Demo/SwitchDemo.stx +28 -0
  195. package/resources/components/Docs/Home.stx +7 -0
  196. package/resources/components/Docs/HomeContributors.stx +23 -0
  197. package/resources/components/Docs/HomeSponsors.stx +17 -0
  198. package/resources/components/Docs/HomeTeam.stx +18 -0
  199. package/resources/components/Docs/TeamMember.stx +79 -0
  200. package/resources/components/Email/EmailButton.stx +32 -0
  201. package/resources/components/Email/EmailDivider.stx +20 -0
  202. package/resources/components/Email/EmailImage.stx +24 -0
  203. package/resources/components/Email/EmailLayout.stx +55 -0
  204. package/resources/components/Email/EmailLink.stx +22 -0
  205. package/resources/components/Email/EmailSection.stx +20 -0
  206. package/resources/components/Email/EmailText.stx +30 -0
  207. package/resources/components/Forum/ForumBreadcrumb.stx +38 -0
  208. package/resources/components/Forum/ForumCategory.stx +16 -0
  209. package/resources/components/Forum/ForumCategoryItem.stx +46 -0
  210. package/resources/components/Forum/ForumLayout.stx +36 -0
  211. package/resources/components/Forum/ForumPagination.stx +39 -0
  212. package/resources/components/Forum/ForumPost.stx +71 -0
  213. package/resources/components/Forum/ForumReplyForm.stx +76 -0
  214. package/resources/components/Forum/ForumSidebar.stx +86 -0
  215. package/resources/components/Forum/ForumTopicItem.stx +77 -0
  216. package/resources/components/Forum/ForumTopicList.stx +23 -0
  217. package/resources/components/HelloWorld.stx +253 -0
  218. package/resources/components/Logo.stx +10 -0
  219. package/resources/components/Marketing/CallToAction.stx +30 -0
  220. package/resources/components/Marketing/ComingSoon.stx +155 -0
  221. package/resources/components/Marketing/Faqs.stx +99 -0
  222. package/resources/components/Marketing/Feature.stx +32 -0
  223. package/resources/components/Marketing/Fields.stx +28 -0
  224. package/resources/components/Marketing/Footer.stx +33 -0
  225. package/resources/components/Marketing/Header.stx +51 -0
  226. package/resources/components/Marketing/Hero.stx +46 -0
  227. package/resources/components/Marketing/Layout.stx +26 -0
  228. package/resources/components/Marketing/NavLink.stx +13 -0
  229. package/resources/components/Marketing/Plan.stx +43 -0
  230. package/resources/components/Marketing/Pricing.stx +120 -0
  231. package/resources/components/Marketing/PrimaryFeatures.stx +80 -0
  232. package/resources/components/Marketing/PrimaryFeatures2.stx +98 -0
  233. package/resources/components/Marketing/SecondaryFeatures.stx +80 -0
  234. package/resources/components/Marketing/Testimonials.stx +120 -0
  235. package/resources/components/Pagination.stx +149 -0
  236. package/resources/components/Playground.stx +9 -0
  237. package/resources/components/README.md +87 -0
  238. package/resources/components/Storefront/AnnouncementBar.stx +17 -0
  239. package/resources/components/Storefront/CartDrawer.stx +227 -0
  240. package/resources/components/Storefront/CheckoutSteps.stx +50 -0
  241. package/resources/components/Storefront/Footer.stx +81 -0
  242. package/resources/components/Storefront/Header.stx +66 -0
  243. package/resources/components/Storefront/OrderSummary.stx +79 -0
  244. package/resources/components/Storefront/ProductCard.stx +63 -0
  245. package/resources/components/Storefront/ProductGallery.stx +88 -0
  246. package/resources/components/Storefront/QuantitySelector.stx +97 -0
  247. package/resources/components/index.ts +13 -0
  248. package/resources/emails/email-verification.stx +46 -0
  249. package/resources/emails/layouts/base.html +113 -0
  250. package/resources/emails/password-changed.stx +55 -0
  251. package/resources/emails/password-reset.stx +43 -0
  252. package/resources/emails/subscription-confirmation.stx +99 -0
  253. package/resources/emails/welcome.stx +48 -0
  254. package/resources/functions/README.md +28 -0
  255. package/resources/functions/counter.ts +13 -0
  256. package/resources/functions/dark.ts +4 -0
  257. package/resources/functions/dashboard/analytics.ts +238 -0
  258. package/resources/functions/dashboard/config-io.ts +316 -0
  259. package/resources/functions/dashboard/data.ts +375 -0
  260. package/resources/functions/dashboard/library.ts +250 -0
  261. package/resources/functions/dashboard/sidebar.ts +537 -0
  262. package/resources/functions/dashboard/system.ts +150 -0
  263. package/resources/layouts/403.stx +188 -0
  264. package/resources/layouts/404.stx +34 -0
  265. package/resources/layouts/README.md +14 -0
  266. package/resources/layouts/dashboard/default.stx +20 -0
  267. package/resources/layouts/dashboard/guest.stx +10 -0
  268. package/resources/layouts/default.stx +3 -0
  269. package/resources/layouts/maintenance.stx +11 -0
  270. package/resources/layouts/storefront.stx +73 -0
  271. package/resources/layouts/system-tray/default.stx +3 -0
  272. package/resources/modules/.gitkeep +0 -0
  273. package/resources/plugins/.gitkeep +0 -0
  274. package/resources/plugins/preloader.ts +308 -0
  275. package/resources/types/README.md +3 -0
  276. package/resources/views/cart.stx +149 -0
  277. package/resources/views/checkout/contact.stx +101 -0
  278. package/resources/views/checkout/payment.stx +114 -0
  279. package/resources/views/checkout/shipping.stx +120 -0
  280. package/resources/views/coming-soon.stx +9 -0
  281. package/resources/views/dashboard/custom-page.stx +45 -0
  282. package/resources/views/dashboard/index.stx +24 -0
  283. package/resources/views/emails/welcome.stx +0 -0
  284. package/resources/views/errors/tester.stx +26 -0
  285. package/resources/views/index.stx +2865 -0
  286. package/resources/views/orders/[id].stx +130 -0
@@ -0,0 +1,537 @@
1
+ /**
2
+ * Dashboard sidebar nav HTML builder.
3
+ *
4
+ * Generates the entire navigation tree (8 sections + each leaf item's
5
+ * inline SVG icon + a dynamic row per user-defined model) as a single
6
+ * HTML string. The layout's `<script server>` calls this and passes
7
+ * the result to `<Sidebar :nav-html="…" />`, where it's rendered as
8
+ * raw HTML (`{{{ navHtml }}}`).
9
+ *
10
+ * Why this lives in the layout-side rather than inside Sidebar.stx:
11
+ * STX's @foreach evaluator in the Sidebar component template does NOT
12
+ * resolve identifiers from the component's own `<script>` block, from
13
+ * defineProps defaults, or from prop bindings reliably (it fails with
14
+ * "not iterable" or "unexpected token" depending on the source). A
15
+ * static HTML string passed as a prop and rendered via the triple-brace
16
+ * raw interpolation IS reliable in every context, so we sidestep the
17
+ * scope problem entirely by building the markup here on the server
18
+ * side.
19
+ */
20
+ import { existsSync, readFileSync } from 'node:fs'
21
+ import { resolve } from 'node:path'
22
+
23
+ type ModelCategory = 'userland' | 'data' | 'commerce' | 'content' | 'marketing' | 'system'
24
+
25
+ /**
26
+ * Per-model dashboard configuration shape (stacksjs/stacks#1843).
27
+ * Mirror of `DashboardModelOptions` in `@stacksjs/types` — duplicated here
28
+ * because this file is consumed from STX server-script context where the
29
+ * full type imports aren't available. Keep in sync with the source.
30
+ */
31
+ interface DashboardModelOptionsLite {
32
+ enabled?: boolean
33
+ label?: string
34
+ icon?: string
35
+ section?: string
36
+ roles?: string[]
37
+ description?: string
38
+ }
39
+
40
+ interface DiscoveredModel {
41
+ id: string
42
+ name: string
43
+ icon?: string
44
+ hasDedicatedPage?: boolean
45
+ category?: ModelCategory
46
+ /** Per-model dashboard config from `defineModel({ dashboard: … })`. */
47
+ dashboard?: DashboardModelOptionsLite
48
+ }
49
+
50
+ interface DataRowToggles {
51
+ dashboard: boolean
52
+ activity: boolean
53
+ users: boolean
54
+ teams: boolean
55
+ subscribers: boolean
56
+ allModels: boolean
57
+ }
58
+
59
+ interface DashboardSectionToggles {
60
+ library: boolean
61
+ content: boolean
62
+ commerce: boolean
63
+ marketing: boolean
64
+ analytics: boolean
65
+ management: boolean
66
+ utilities: boolean
67
+ /** CI tracking surface (stacksjs/stacks#1844). Lives under management. */
68
+ ci?: boolean
69
+ data: DataRowToggles
70
+ }
71
+
72
+ interface DiscoveredManifest {
73
+ models: DiscoveredModel[]
74
+ sections: DashboardSectionToggles
75
+ }
76
+
77
+ const DEFAULT_DATA_TOGGLES: DataRowToggles = {
78
+ dashboard: true,
79
+ activity: true,
80
+ users: true,
81
+ teams: true,
82
+ subscribers: true,
83
+ allModels: true,
84
+ }
85
+
86
+ const DEFAULT_TOGGLES: DashboardSectionToggles = {
87
+ library: true,
88
+ content: true,
89
+ commerce: true,
90
+ marketing: true,
91
+ analytics: true,
92
+ management: true,
93
+ utilities: true,
94
+ data: DEFAULT_DATA_TOGGLES,
95
+ }
96
+
97
+ const SVG_OPEN = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">'
98
+ const SVG_CLOSE = '</svg>'
99
+
100
+ // Path data for each icon used by the sidebar. Keys are short logical
101
+ // names; the leaf-item builder below uses these directly. All icons are
102
+ // 20×20, stroke-based, currentColor — restyle by changing one CSS rule
103
+ // rather than editing each path.
104
+ const ICON_PATHS: Record<string, string> = {
105
+ 'home': '<path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1m-2 0h2"/>',
106
+ 'puzzle': '<path d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H6a1 1 0 01-1-1v-3a1 1 0 00-1-1H3a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"/>',
107
+ 'function': '<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/>',
108
+ 'list-number': '<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/>',
109
+ 'package': '<path d="M16.5 9.4l-9-5.19M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
110
+ 'dashboard': '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="4" rx="1"/><rect x="14" y="10" width="7" height="11" rx="1"/><rect x="3" y="13" width="7" height="8" rx="1"/>',
111
+ 'files': '<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/>',
112
+ 'file': '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>',
113
+ 'post': '<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>',
114
+ 'tags': '<path d="M7 7h.01M2 12.5l9.1 9.1a1.5 1.5 0 002.1 0l7.3-7.3a1.5 1.5 0 000-2.1L11.4 3H4a1 1 0 00-1 1v7.4a1.5 1.5 0 00.4 1z"/>',
115
+ 'tag': '<path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/>',
116
+ 'comment': '<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>',
117
+ 'user-edit': '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/><path d="M16 3.13a4 4 0 010 7.75"/>',
118
+ 'seo': '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><path d="M11 8v6M8 11h6"/>',
119
+ 'rocket': '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 00-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 012-3.95A12.88 12.88 0 0122 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 01-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 3 0 3 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-3 0-3"/>',
120
+ 'api': '<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/>',
121
+ 'link': '<path d="M15 7h3a5 5 0 010 10h-3m-6 0H6A5 5 0 016 7h3"/><line x1="8" y1="12" x2="16" y2="12"/>',
122
+ 'bolt': '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
123
+ 'terminal': '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>',
124
+ 'queue': '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
125
+ 'briefcase': '<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v16"/>',
126
+ 'search': '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
127
+ 'bell': '<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/>',
128
+ 'cart': '<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"/>',
129
+ 'user': '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>',
130
+ 'orders': '<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 14l2 2 4-4"/>',
131
+ 'coupon': '<path d="M2 9a3 3 0 003 3v4a2 2 0 002 2h14a2 2 0 002-2v-4a3 3 0 000-6V8a2 2 0 00-2-2H7a2 2 0 00-2 2v1a3 3 0 00-3 3z"/><path d="M13 9v2M13 13h.01"/>',
132
+ 'gift': '<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 010-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 000-5C13 2 12 7 12 7z"/>',
133
+ 'invoice': '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/>',
134
+ 'truck': '<rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/>',
135
+ 'percent': '<line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/>',
136
+ 'activity': '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
137
+ 'check-circle': '<circle cx="12" cy="12" r="10"/><polyline points="9 12 11 14 15 10"/>',
138
+ 'users': '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/>',
139
+ 'group': '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/>',
140
+ 'mail': '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22 6 12 13 2 6"/>',
141
+ 'table': '<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/>',
142
+ 'list-settings': '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><circle cx="4" cy="6" r="2"/><circle cx="4" cy="12" r="2"/><circle cx="4" cy="18" r="2"/>',
143
+ 'clock': '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
144
+ 'star': '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
145
+ 'globe': '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>',
146
+ 'document': '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/>',
147
+ 'target': '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
148
+ 'sale-tag': '<path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/><line x1="10" y1="14" x2="14" y2="10"/>',
149
+ 'megaphone': '<path d="M3 11l18-5v12L3 13v-2z"/><path d="M11.6 16.8a3 3 0 11-5.8-1.6"/>',
150
+ 'cloud': '<path d="M18 10h-1.26A8 8 0 109 20h9a5 5 0 000-10z"/>',
151
+ 'server': '<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>',
152
+ 'zap': '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
153
+ 'globe-search': '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10M12 2a15.3 15.3 0 00-4 10 15.3 15.3 0 004 10"/>',
154
+ 'lock': '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/>',
155
+ 'mailbox': '<path d="M22 17H2a3 3 0 003 3h14a3 3 0 003-3zm0 0V9a3 3 0 00-3-3H5a3 3 0 00-3 3v8"/><path d="M6 6v11"/><path d="M2 12h4"/>',
156
+ 'log': '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/>',
157
+ 'settings': '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/>',
158
+ }
159
+
160
+ function svg(key: string): string {
161
+ const path = ICON_PATHS[key] ?? ICON_PATHS.file
162
+ return SVG_OPEN + path + SVG_CLOSE
163
+ }
164
+
165
+ interface NavItem {
166
+ to: string
167
+ icon: string
168
+ text: string
169
+ mpa?: boolean
170
+ /**
171
+ * Role-gate metadata for sidebar items (stacksjs/stacks#1843).
172
+ * When set, the rendered `<a>` carries `data-required-roles="…"` and
173
+ * the client-side `useRole()` filter hides the row from viewers who
174
+ * don't hold a matching role. Unauthenticated viewers (dev dashboard)
175
+ * see all rows per the permissive default in `useRole`.
176
+ */
177
+ roles?: string[]
178
+ }
179
+
180
+ const SECTION_TOGGLE_ONCLICK = "(function(btn){var sec=btn.closest('.sidebar-section');var key=sec.getAttribute('data-section');sec.classList.toggle('collapsed');try{var c={};var s=localStorage.getItem('sidebar-collapsed');if(s)c=JSON.parse(s);c[key]=sec.classList.contains('collapsed');localStorage.setItem('sidebar-collapsed',JSON.stringify(c));}catch(e){}})(this)"
181
+
182
+ /**
183
+ * HTML-encode a string for safe insertion into element attributes (`role`
184
+ * names, labels) or text content. Models can ship arbitrary `dashboard`
185
+ * config, so we don't trust the values to be HTML-safe by construction.
186
+ */
187
+ function escapeHtml(s: string): string {
188
+ return s
189
+ .replace(/&/g, '&amp;')
190
+ .replace(/</g, '&lt;')
191
+ .replace(/>/g, '&gt;')
192
+ .replace(/"/g, '&quot;')
193
+ .replace(/'/g, '&#39;')
194
+ }
195
+
196
+ function renderItem(item: NavItem): string {
197
+ const mpaAttr = item.mpa ? ' data-mpa="true"' : ''
198
+ // Roles encoded as a comma-separated list on `data-required-roles`. The
199
+ // client-side filter in `useRole()` parses this and removes the row if
200
+ // the viewer doesn't hold any of the listed roles. Static rows omit the
201
+ // attribute entirely, so they're always visible.
202
+ const rolesAttr = item.roles && item.roles.length > 0
203
+ ? ` data-required-roles="${escapeHtml(item.roles.join(','))}"`
204
+ : ''
205
+ return `<li${rolesAttr ? ` data-required-roles-li="${escapeHtml((item.roles ?? []).join(','))}"` : ''}><a href="${escapeHtml(item.to)}" class="sidebar-link"${mpaAttr}${rolesAttr}><span class="sidebar-icon">${svg(item.icon)}</span><span>${escapeHtml(item.text)}</span></a></li>`
206
+ }
207
+
208
+ function renderSection(key: string, label: string, items: NavItem[]): string {
209
+ if (items.length === 0) return ''
210
+ const lis = items.map(renderItem).join('')
211
+ return `<div class="sidebar-section" data-section="${key}">`
212
+ + `<button class="sidebar-section-label" type="button" onclick="${SECTION_TOGGLE_ONCLICK}"><span>${label}</span><div class="sidebar-chevron"></div></button>`
213
+ + `<ul class="sidebar-section-list">${lis}</ul>`
214
+ + '</div>'
215
+ }
216
+
217
+ /**
218
+ * Pull the discovered-models manifest from the framework defaults dir.
219
+ * Handles both the modern envelope (`{ models, sections }`) and the legacy
220
+ * bare-array form to keep dashboards working across versions while a stale
221
+ * `.discovered-models.json` is still on disk.
222
+ *
223
+ * Returns the default empty payload when the file is absent (fresh project
224
+ * before the first dashboard run regenerates it).
225
+ */
226
+ export function loadDiscoveredManifest(): DiscoveredManifest {
227
+ const manifestPath = resolve(process.cwd(), 'storage/framework/defaults/views/dashboard/.discovered-models.json')
228
+ if (!existsSync(manifestPath)) return { models: [], sections: DEFAULT_TOGGLES }
229
+ try {
230
+ const parsed = JSON.parse(readFileSync(manifestPath, 'utf8')) as DiscoveredModel[] | { models?: DiscoveredModel[], sections?: Partial<DashboardSectionToggles> }
231
+ if (Array.isArray(parsed)) {
232
+ return {
233
+ models: parsed.sort((a, b) => (a.name || '').localeCompare(b.name || '')),
234
+ sections: DEFAULT_TOGGLES,
235
+ }
236
+ }
237
+ const models = Array.isArray(parsed.models) ? parsed.models.sort((a, b) => (a.name || '').localeCompare(b.name || '')) : []
238
+ return {
239
+ models,
240
+ sections: { ...DEFAULT_TOGGLES, ...(parsed.sections ?? {}) },
241
+ }
242
+ }
243
+ catch {
244
+ return { models: [], sections: DEFAULT_TOGGLES }
245
+ }
246
+ }
247
+
248
+ /** Backward-compat: prior callers only needed the model list. */
249
+ export function loadDiscoveredModels(): DiscoveredModel[] {
250
+ return loadDiscoveredManifest().models
251
+ }
252
+
253
+ /**
254
+ * Pick discovered-model rows that belong to a category and turn them into
255
+ * web-sidebar nav items. The row visually lives under the categorized
256
+ * section but always links to `/models/<id>` — that's the dashboard's
257
+ * dynamic ORM viewer (`views/dashboard/models/[model].stx`), and the
258
+ * only catch-all that resolves arbitrary slugs. `dedicated` skips models
259
+ * the section already surfaces as a hand-built static row (e.g.
260
+ * `/commerce/customers` already exists, so the Customer model row would
261
+ * just duplicate it).
262
+ */
263
+ /**
264
+ * The section a model surfaces under, after honouring the model's
265
+ * `dashboard.section` override. When no override is set, falls back to
266
+ * the path-derived category. Returns `undefined` when the model is
267
+ * explicitly disabled so callers can skip the row.
268
+ */
269
+ function effectiveCategory(m: DiscoveredModel): string | undefined {
270
+ if (m.dashboard?.enabled === false)
271
+ return undefined
272
+ if (m.dashboard?.section)
273
+ return m.dashboard.section
274
+ return m.category ?? 'data'
275
+ }
276
+
277
+ function categoryNavItems(
278
+ models: DiscoveredModel[],
279
+ category: ModelCategory,
280
+ dedicated: Set<string>,
281
+ ): NavItem[] {
282
+ return models
283
+ .filter(m => effectiveCategory(m) === category)
284
+ .filter(m => !m.hasDedicatedPage && !dedicated.has(m.id))
285
+ .map((m): NavItem => ({
286
+ to: `/models/${m.id}`,
287
+ icon: m.dashboard?.icon ?? m.icon ?? 'table',
288
+ text: m.dashboard?.label ?? m.name,
289
+ mpa: true,
290
+ ...(m.dashboard?.roles && m.dashboard.roles.length > 0
291
+ ? { roles: m.dashboard.roles }
292
+ : {}),
293
+ }))
294
+ }
295
+
296
+ /**
297
+ * Build the entire sidebar nav HTML. Models are bucketed into the section
298
+ * matching their `category`:
299
+ * - `userland` / `data` → Data
300
+ * - `commerce` → Commerce (skipped entirely if disabled in config)
301
+ * - `content` → Content (skipped if disabled)
302
+ * - `marketing` → Marketing (skipped if disabled)
303
+ * - `system` → App
304
+ *
305
+ * Models with a dedicated dashboard page never get a dynamic row.
306
+ */
307
+ export function buildSidebarNavHtml(
308
+ discoveredModels: DiscoveredModel[] = [],
309
+ toggles: DashboardSectionToggles = DEFAULT_TOGGLES,
310
+ ): string {
311
+ const sections: Array<[string, string, NavItem[]]> = []
312
+
313
+ // Library section: views live at the project root (e.g. `/functions`,
314
+ // `/packages`, `/releases`). The historical `/library/*` URLs in the
315
+ // sidebar 404'd because no matching files exist under that prefix.
316
+ // `/components` is dropped — that path is taken by the STX components
317
+ // directory; there's no dashboard page for it.
318
+ if (toggles.library) {
319
+ sections.push(['library', 'library', [
320
+ { to: '/functions', icon: 'function', text: 'Functions' },
321
+ { to: '/releases', icon: 'list-number', text: 'Releases' },
322
+ { to: '/packages', icon: 'package', text: 'Packages' },
323
+ ]])
324
+ }
325
+
326
+ if (toggles.content) {
327
+ sections.push(['content', 'content', [
328
+ { to: '/content/dashboard', icon: 'dashboard', text: 'Dashboard' },
329
+ { to: '/content/files', icon: 'files', text: 'Files' },
330
+ { to: '/content/pages', icon: 'file', text: 'Pages' },
331
+ { to: '/content/posts', icon: 'post', text: 'Posts' },
332
+ { to: '/content/categories', icon: 'tags', text: 'Categories' },
333
+ { to: '/content/tags', icon: 'tag', text: 'Tags' },
334
+ { to: '/content/comments', icon: 'comment', text: 'Comments' },
335
+ { to: '/content/authors', icon: 'user-edit', text: 'Authors' },
336
+ { to: '/content/seo', icon: 'seo', text: 'SEO' },
337
+ ...categoryNavItems(discoveredModels, 'content', new Set([
338
+ 'page', 'post', 'category', 'tag', 'comment', 'author',
339
+ ])),
340
+ ]])
341
+ }
342
+
343
+ // App section: every page lives at a flat root path (`/jobs`, `/queue`,
344
+ // `/inbox`, etc.). The historical `/app/*` prefix never matched a real
345
+ // file. Notifications resolves to the dashboard variant
346
+ // (`/notifications/dashboard`); the bare `/notifications` redirects.
347
+ sections.push(['app', 'app', [
348
+ { to: '/deployments', icon: 'rocket', text: 'Deployments' },
349
+ { to: '/requests', icon: 'api', text: 'Requests' },
350
+ { to: '/realtime', icon: 'link', text: 'Realtime' },
351
+ { to: '/actions', icon: 'bolt', text: 'Actions' },
352
+ { to: '/commands', icon: 'terminal', text: 'Commands' },
353
+ { to: '/queue', icon: 'queue', text: 'Queue' },
354
+ { to: '/jobs', icon: 'briefcase', text: 'Jobs' },
355
+ { to: '/jobs/history', icon: 'clock', text: 'Job History' },
356
+ { to: '/queries', icon: 'search', text: 'Queries' },
357
+ { to: '/queries/slow', icon: 'clock', text: 'Slow Queries' },
358
+ { to: '/queries/history', icon: 'log', text: 'Query History' },
359
+ { to: '/errors', icon: 'log', text: 'Errors' },
360
+ { to: '/notifications/dashboard', icon: 'bell', text: 'Notifications' },
361
+ { to: '/inbox', icon: 'mail', text: 'Inbox' },
362
+ { to: '/inbox/activity', icon: 'activity', text: 'Mail Activity' },
363
+ { to: '/inbox/settings', icon: 'settings', text: 'Mail Settings' },
364
+ ...categoryNavItems(discoveredModels, 'system', new Set()),
365
+ ]])
366
+
367
+ // Data section: configurable basic rows + ALL userland models + any
368
+ // root-level framework `data` models. Per-row toggles let projects hide
369
+ // the basic rows they don't need; userland models always appear.
370
+ // Per-model rows link to `/models/<id>` (dynamic ORM viewer); the
371
+ // built-in dashboard / activity / users / teams / subscribers rows keep
372
+ // their bespoke pages under `/data/<row>`.
373
+ const dataRows = toggles.data ?? DEFAULT_DATA_TOGGLES
374
+ const dataNav: NavItem[] = []
375
+ if (dataRows.dashboard) dataNav.push({ to: '/data/dashboard', icon: 'dashboard', text: 'Dashboard' })
376
+ if (dataRows.activity) dataNav.push({ to: '/data/activity', icon: 'activity', text: 'Activity' })
377
+ if (dataRows.users) dataNav.push({ to: '/data/users', icon: 'users', text: 'Users' })
378
+ if (dataRows.teams) dataNav.push({ to: '/data/teams', icon: 'group', text: 'Teams' })
379
+ if (dataRows.subscribers) dataNav.push({ to: '/data/subscribers', icon: 'mail', text: 'Subscribers' })
380
+ if (dataRows.allModels) dataNav.push({ to: '/models', icon: 'table', text: 'All Models' })
381
+ for (const m of discoveredModels) {
382
+ // Skip when:
383
+ // - explicit `dashboard.enabled === false` (effectiveCategory returns undefined)
384
+ // - pinned to a different section via `dashboard.section`
385
+ // - already has a dedicated dashboard page
386
+ const eff = effectiveCategory(m)
387
+ if (eff !== 'userland' && eff !== 'data') continue
388
+ if (m.hasDedicatedPage) continue
389
+ dataNav.push({
390
+ to: `/models/${m.id}`,
391
+ icon: m.dashboard?.icon ?? m.icon ?? 'table',
392
+ text: m.dashboard?.label ?? m.name,
393
+ mpa: true,
394
+ ...(m.dashboard?.roles && m.dashboard.roles.length > 0
395
+ ? { roles: m.dashboard.roles }
396
+ : {}),
397
+ })
398
+ }
399
+ sections.push(['data', 'data', dataNav])
400
+
401
+ if (toggles.commerce) {
402
+ sections.push(['commerce', 'commerce', [
403
+ { to: '/commerce/dashboard', icon: 'dashboard', text: 'Dashboard' },
404
+ { to: '/commerce/pos', icon: 'cart', text: 'POS' },
405
+ { to: '/commerce/customers', icon: 'user', text: 'Customers' },
406
+ { to: '/commerce/orders', icon: 'orders', text: 'Orders' },
407
+ { to: '/commerce/products', icon: 'package', text: 'Products' },
408
+ { to: '/commerce/categories', icon: 'tags', text: 'Categories' },
409
+ { to: '/commerce/manufacturers', icon: 'briefcase', text: 'Manufacturers' },
410
+ { to: '/commerce/units', icon: 'list-number', text: 'Units' },
411
+ { to: '/commerce/variants', icon: 'puzzle', text: 'Variants' },
412
+ { to: '/commerce/reviews', icon: 'star', text: 'Reviews' },
413
+ { to: '/commerce/coupons', icon: 'coupon', text: 'Coupons' },
414
+ { to: '/commerce/gift-cards', icon: 'gift', text: 'Gift Cards' },
415
+ { to: '/commerce/payments', icon: 'invoice', text: 'Payments' },
416
+ { to: '/commerce/taxes', icon: 'percent', text: 'Taxes' },
417
+ { to: '/commerce/printers/devices', icon: 'package', text: 'Printers' },
418
+ { to: '/commerce/printers/receipts', icon: 'invoice', text: 'Receipts' },
419
+ { to: '/commerce/waitlist/products', icon: 'clock', text: 'Product Waitlist' },
420
+ { to: '/commerce/waitlist/restaurant', icon: 'clock', text: 'Restaurant Waitlist' },
421
+ ...categoryNavItems(discoveredModels, 'commerce', new Set([
422
+ 'customer', 'order', 'product', 'product-variant', 'product-unit',
423
+ 'manufacturer', 'review', 'coupon', 'gift-card', 'payment', 'category',
424
+ 'cart', 'cart-item', 'order-item', 'tax-rate',
425
+ 'shipping-method', 'shipping-rate', 'shipping-zone',
426
+ 'delivery-route', 'driver', 'license-key', 'digital-delivery',
427
+ 'print-device', 'receipt',
428
+ 'waitlist-product', 'waitlist-restaurant',
429
+ ])),
430
+ ]])
431
+
432
+ // Delivery section: routes/digital views ship under their full names
433
+ // (`delivery-routes.stx`, `digital-delivery.stx`), the abbreviated
434
+ // sidebar URLs from before never resolved.
435
+ sections.push(['delivery', 'delivery', [
436
+ { to: '/commerce/delivery', icon: 'truck', text: 'Overview' },
437
+ { to: '/commerce/delivery/shipping-methods', icon: 'truck', text: 'Shipping Methods' },
438
+ { to: '/commerce/delivery/shipping-rates', icon: 'percent', text: 'Shipping Rates' },
439
+ { to: '/commerce/delivery/shipping-zones', icon: 'globe', text: 'Shipping Zones' },
440
+ { to: '/commerce/delivery/drivers', icon: 'user', text: 'Drivers' },
441
+ { to: '/commerce/delivery/delivery-routes', icon: 'globe', text: 'Routes' },
442
+ { to: '/commerce/delivery/license-keys', icon: 'lock', text: 'License Keys' },
443
+ { to: '/commerce/delivery/digital-delivery', icon: 'package', text: 'Digital Delivery' },
444
+ ]])
445
+ }
446
+
447
+ if (toggles.marketing) {
448
+ sections.push(['marketing', 'marketing', [
449
+ { to: '/marketing/lists', icon: 'list-settings', text: 'Lists' },
450
+ { to: '/marketing/social-posts', icon: 'clock', text: 'Social Posts' },
451
+ { to: '/marketing/campaigns', icon: 'rocket', text: 'Campaigns' },
452
+ { to: '/marketing/reviews', icon: 'star', text: 'Reviews' },
453
+ ...categoryNavItems(discoveredModels, 'marketing', new Set(['campaign'])),
454
+ ]])
455
+ }
456
+
457
+ // Analytics section: commerce/sales sub-pages live under
458
+ // `analytics/commerce/{web,sales}.stx`. The bare `/analytics/commerce`
459
+ // and `/analytics/sales` URLs from before never resolved. `/goals`
460
+ // had no view file at all and is dropped.
461
+ if (toggles.analytics) {
462
+ sections.push(['analytics', 'analytics', [
463
+ { to: '/analytics/web', icon: 'globe', text: 'Web' },
464
+ { to: '/analytics/pages', icon: 'document', text: 'Top Pages' },
465
+ { to: '/analytics/referrers', icon: 'link', text: 'Referrers' },
466
+ { to: '/analytics/countries', icon: 'globe-search', text: 'Countries' },
467
+ { to: '/analytics/devices', icon: 'package', text: 'Devices' },
468
+ { to: '/analytics/browsers', icon: 'globe', text: 'Browsers' },
469
+ { to: '/analytics/events', icon: 'bolt', text: 'Events' },
470
+ { to: '/analytics/blog', icon: 'document', text: 'Blog' },
471
+ { to: '/analytics/commerce/web', icon: 'cart', text: 'Commerce' },
472
+ { to: '/analytics/commerce/sales', icon: 'sale-tag', text: 'Sales' },
473
+ { to: '/analytics/marketing', icon: 'megaphone', text: 'Marketing' },
474
+ ]])
475
+ }
476
+
477
+ // Management section: every page is at the root path; only
478
+ // `permissions` lives under a `/management/` subdir. The bare
479
+ // `/management/<x>` URLs from before never resolved.
480
+ if (toggles.management) {
481
+ const managementItems: NavItem[] = [
482
+ { to: '/cloud', icon: 'cloud', text: 'Cloud' },
483
+ { to: '/servers', icon: 'server', text: 'Servers' },
484
+ { to: '/serverless', icon: 'zap', text: 'Serverless' },
485
+ { to: '/dns', icon: 'globe-search', text: 'DNS' },
486
+ { to: '/management/permissions', icon: 'lock', text: 'Permissions' },
487
+ { to: '/mailboxes', icon: 'mailbox', text: 'Mailboxes' },
488
+ { to: '/logs', icon: 'log', text: 'Logs' },
489
+ { to: '/health', icon: 'activity', text: 'Health' },
490
+ { to: '/insights', icon: 'star', text: 'Insights' },
491
+ // Kanban surface (stacksjs/stacks#1846). Role-gated to admin+dev
492
+ // via data-required-roles; the client filter in useRole() hides
493
+ // the row for client-role viewers. Permissive default applies on
494
+ // the localhost dev dashboard.
495
+ { to: '/kanban', icon: 'table', text: 'Kanban', mpa: true, roles: ['admin', 'dev'] },
496
+ ]
497
+ // CI tracking (stacksjs/stacks#1844) — opt-in via `ci.enabled` in
498
+ // config/dashboard.ts. The page additionally checks `useRole().isDev()`
499
+ // (stub for #1843) before rendering.
500
+ if (toggles.ci)
501
+ managementItems.push({ to: '/ci', icon: 'check-circle', text: 'CI' })
502
+ sections.push(['management', 'management', managementItems])
503
+ }
504
+
505
+ // Utilities section: real paths are flat (`/buddy`, `/environment`,
506
+ // `/access-tokens`) or under `/settings/*` for billing/mail. The
507
+ // historical `/utilities/*` URLs were uniformly 404s.
508
+ if (toggles.utilities) {
509
+ sections.push(['utilities', 'utilities', [
510
+ { to: '/buddy', icon: 'terminal', text: 'Buddy CLI' },
511
+ { to: '/environment', icon: 'settings', text: 'Environment' },
512
+ { to: '/access-tokens', icon: 'lock', text: 'Access Tokens' },
513
+ { to: '/settings/billing', icon: 'invoice', text: 'Billing' },
514
+ { to: '/settings/mail', icon: 'mail', text: 'Mail Settings' },
515
+ ]])
516
+ }
517
+
518
+ return sections.map(([key, label, items]) => renderSection(key, label, items)).join('')
519
+ }
520
+
521
+ /**
522
+ * Build all three pre-rendered chunks the Sidebar component consumes,
523
+ * in one call. Returned with intentionally-uncommon keys (`top`, `nav`,
524
+ * `bottom`) so they don't collide with auto-imported names that STX's
525
+ * global-scope auto-imports may surface in the component template's
526
+ * lookup chain — that collision was the cause of the previous bug
527
+ * where the rendered output dumped the function source text instead
528
+ * of its return value.
529
+ */
530
+ export function buildSidebarChunks(): { top: string, nav: string, bottom: string } {
531
+ const manifest = loadDiscoveredManifest()
532
+ return {
533
+ top: `<a href="/" class="sidebar-link sidebar-link-home"><span class="sidebar-icon">${svg('home')}</span><span>Home</span></a>`,
534
+ nav: buildSidebarNavHtml(manifest.models, manifest.sections),
535
+ bottom: `<a href="/settings/billing" class="sidebar-link"><span class="sidebar-icon">${svg('settings')}</span><span>Settings</span></a>`,
536
+ }
537
+ }