@stacksjs/defaults 0.70.207 → 0.70.208

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 (408) hide show
  1. package/TODO.md +21 -0
  2. package/ai/AGENTS.md +79 -0
  3. package/ai/README.md +45 -0
  4. package/ai/claude/launch.json +12 -0
  5. package/ai/skills/stacks-actions/SKILL.md +128 -0
  6. package/ai/skills/stacks-ai/SKILL.md +276 -0
  7. package/ai/skills/stacks-alias/SKILL.md +94 -0
  8. package/ai/skills/stacks-analytics/SKILL.md +91 -0
  9. package/ai/skills/stacks-api/SKILL.md +738 -0
  10. package/ai/skills/stacks-arrays/SKILL.md +277 -0
  11. package/ai/skills/stacks-auth/SKILL.md +422 -0
  12. package/ai/skills/stacks-auto-imports/SKILL.md +138 -0
  13. package/ai/skills/stacks-brandkit/SKILL.md +811 -0
  14. package/ai/skills/stacks-browse/SKILL.md +127 -0
  15. package/ai/skills/stacks-browse/scripts/browse.ts +492 -0
  16. package/ai/skills/stacks-browser/SKILL.md +145 -0
  17. package/ai/skills/stacks-buddy/SKILL.md +676 -0
  18. package/ai/skills/stacks-build/SKILL.md +163 -0
  19. package/ai/skills/stacks-cache/SKILL.md +125 -0
  20. package/ai/skills/stacks-calendar/SKILL.md +113 -0
  21. package/ai/skills/stacks-chat/SKILL.md +371 -0
  22. package/ai/skills/stacks-cli/SKILL.md +112 -0
  23. package/ai/skills/stacks-cloud/SKILL.md +192 -0
  24. package/ai/skills/stacks-cms/SKILL.md +217 -0
  25. package/ai/skills/stacks-collections/SKILL.md +82 -0
  26. package/ai/skills/stacks-commerce/SKILL.md +114 -0
  27. package/ai/skills/stacks-composables/SKILL.md +161 -0
  28. package/ai/skills/stacks-config/SKILL.md +144 -0
  29. package/ai/skills/stacks-configuration/SKILL.md +154 -0
  30. package/ai/skills/stacks-cron/SKILL.md +123 -0
  31. package/ai/skills/stacks-crosswind/SKILL.md +190 -0
  32. package/ai/skills/stacks-dashboard/SKILL.md +173 -0
  33. package/ai/skills/stacks-database/SKILL.md +286 -0
  34. package/ai/skills/stacks-datetime/SKILL.md +269 -0
  35. package/ai/skills/stacks-dependencies/SKILL.md +111 -0
  36. package/ai/skills/stacks-deploy/SKILL.md +102 -0
  37. package/ai/skills/stacks-design-brutalist/SKILL.md +110 -0
  38. package/ai/skills/stacks-design-minimalist/SKILL.md +115 -0
  39. package/ai/skills/stacks-design-output/SKILL.md +55 -0
  40. package/ai/skills/stacks-design-soft/SKILL.md +156 -0
  41. package/ai/skills/stacks-design-taste/SKILL.md +1012 -0
  42. package/ai/skills/stacks-design-taste-codex/SKILL.md +294 -0
  43. package/ai/skills/stacks-desktop/SKILL.md +78 -0
  44. package/ai/skills/stacks-development/SKILL.md +298 -0
  45. package/ai/skills/stacks-dns/SKILL.md +149 -0
  46. package/ai/skills/stacks-docs/SKILL.md +109 -0
  47. package/ai/skills/stacks-email/SKILL.md +236 -0
  48. package/ai/skills/stacks-enums/SKILL.md +100 -0
  49. package/ai/skills/stacks-env/SKILL.md +181 -0
  50. package/ai/skills/stacks-error-handling/SKILL.md +137 -0
  51. package/ai/skills/stacks-events/SKILL.md +228 -0
  52. package/ai/skills/stacks-faker/SKILL.md +158 -0
  53. package/ai/skills/stacks-git/SKILL.md +158 -0
  54. package/ai/skills/stacks-guard/SKILL.md +98 -0
  55. package/ai/skills/stacks-health/SKILL.md +75 -0
  56. package/ai/skills/stacks-http/SKILL.md +316 -0
  57. package/ai/skills/stacks-i18n/SKILL.md +198 -0
  58. package/ai/skills/stacks-image-to-code/SKILL.md +629 -0
  59. package/ai/skills/stacks-imagegen-mobile/SKILL.md +1474 -0
  60. package/ai/skills/stacks-imagegen-web/SKILL.md +998 -0
  61. package/ai/skills/stacks-investigate/SKILL.md +119 -0
  62. package/ai/skills/stacks-jobs/SKILL.md +136 -0
  63. package/ai/skills/stacks-lint/SKILL.md +184 -0
  64. package/ai/skills/stacks-listeners/SKILL.md +99 -0
  65. package/ai/skills/stacks-logging/SKILL.md +78 -0
  66. package/ai/skills/stacks-mail/SKILL.md +120 -0
  67. package/ai/skills/stacks-middleware/SKILL.md +249 -0
  68. package/ai/skills/stacks-migrations/SKILL.md +105 -0
  69. package/ai/skills/stacks-models/SKILL.md +267 -0
  70. package/ai/skills/stacks-new-feature/SKILL.md +206 -0
  71. package/ai/skills/stacks-notifications/SKILL.md +210 -0
  72. package/ai/skills/stacks-objects/SKILL.md +205 -0
  73. package/ai/skills/stacks-office-hours/SKILL.md +134 -0
  74. package/ai/skills/stacks-orm/SKILL.md +342 -0
  75. package/ai/skills/stacks-path/SKILL.md +118 -0
  76. package/ai/skills/stacks-payments/SKILL.md +373 -0
  77. package/ai/skills/stacks-plan-review/SKILL.md +152 -0
  78. package/ai/skills/stacks-plugins/SKILL.md +149 -0
  79. package/ai/skills/stacks-push/SKILL.md +376 -0
  80. package/ai/skills/stacks-query-builder/SKILL.md +128 -0
  81. package/ai/skills/stacks-queue/SKILL.md +559 -0
  82. package/ai/skills/stacks-realtime/SKILL.md +442 -0
  83. package/ai/skills/stacks-redesign/SKILL.md +182 -0
  84. package/ai/skills/stacks-registry/SKILL.md +58 -0
  85. package/ai/skills/stacks-repl/SKILL.md +80 -0
  86. package/ai/skills/stacks-retro/SKILL.md +122 -0
  87. package/ai/skills/stacks-review/SKILL.md +135 -0
  88. package/ai/skills/stacks-router/SKILL.md +231 -0
  89. package/ai/skills/stacks-routes/SKILL.md +105 -0
  90. package/ai/skills/stacks-scaffolding/SKILL.md +162 -0
  91. package/ai/skills/stacks-scheduler/SKILL.md +363 -0
  92. package/ai/skills/stacks-search-engine/SKILL.md +112 -0
  93. package/ai/skills/stacks-security/SKILL.md +129 -0
  94. package/ai/skills/stacks-security-audit/SKILL.md +128 -0
  95. package/ai/skills/stacks-server/SKILL.md +458 -0
  96. package/ai/skills/stacks-shell/SKILL.md +62 -0
  97. package/ai/skills/stacks-slug/SKILL.md +87 -0
  98. package/ai/skills/stacks-sms/SKILL.md +385 -0
  99. package/ai/skills/stacks-socials/SKILL.md +303 -0
  100. package/ai/skills/stacks-storage/SKILL.md +576 -0
  101. package/ai/skills/stacks-strings/SKILL.md +416 -0
  102. package/ai/skills/stacks-stx/SKILL.md +176 -0
  103. package/ai/skills/stacks-technical-diagrams/LICENSE +22 -0
  104. package/ai/skills/stacks-technical-diagrams/SKILL.md +290 -0
  105. package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +4 -0
  106. package/ai/skills/stacks-technical-diagrams/assets/template.html +1252 -0
  107. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +289 -0
  108. package/ai/skills/stacks-technical-diagrams/bunfig.toml +1 -0
  109. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +64 -0
  110. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +88 -0
  111. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +75 -0
  112. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +70 -0
  113. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +41 -0
  114. package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +62 -0
  115. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +371 -0
  116. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +82 -0
  117. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +295 -0
  118. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +91 -0
  119. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +384 -0
  120. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +84 -0
  121. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +257 -0
  122. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +42 -0
  123. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +13 -0
  124. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +230 -0
  125. package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +40 -0
  126. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +88 -0
  127. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +40 -0
  128. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +103 -0
  129. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +481 -0
  130. package/ai/skills/stacks-technical-diagrams/schemas/README.md +68 -0
  131. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +121 -0
  132. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +43 -0
  133. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +221 -0
  134. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +241 -0
  135. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +186 -0
  136. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +332 -0
  137. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +297 -0
  138. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +28 -0
  139. package/ai/skills/stacks-testing/SKILL.md +129 -0
  140. package/ai/skills/stacks-tunnel/SKILL.md +125 -0
  141. package/ai/skills/stacks-types/SKILL.md +178 -0
  142. package/ai/skills/stacks-ui/SKILL.md +246 -0
  143. package/ai/skills/stacks-utils/SKILL.md +444 -0
  144. package/ai/skills/stacks-validation/SKILL.md +371 -0
  145. package/ai/skills/stacks-whois/SKILL.md +315 -0
  146. package/assets/world-map-data.ts +121 -0
  147. package/bootstrap.ts +114 -0
  148. package/docs/api-examples.md +46 -0
  149. package/docs/index.md +24 -0
  150. package/docs/markdown-examples.md +87 -0
  151. package/functions/analytics.ts +97 -0
  152. package/functions/api.ts +143 -0
  153. package/functions/auth.ts +208 -0
  154. package/functions/billing/payments.ts +149 -0
  155. package/functions/blog.ts +120 -0
  156. package/functions/cms/categorizables.ts +107 -0
  157. package/functions/cms/pages.ts +108 -0
  158. package/functions/cms/posts.ts +100 -0
  159. package/functions/cms/taggables.ts +102 -0
  160. package/functions/commerce/coupons.ts +128 -0
  161. package/functions/commerce/customers.ts +128 -0
  162. package/functions/commerce/gift-cards.ts +119 -0
  163. package/functions/commerce/orders.ts +112 -0
  164. package/functions/commerce/payments.ts +119 -0
  165. package/functions/commerce/products/categories.ts +123 -0
  166. package/functions/commerce/products/manufacturers.ts +123 -0
  167. package/functions/commerce/products/products.ts +123 -0
  168. package/functions/commerce/products/reviews.ts +123 -0
  169. package/functions/commerce/products/units.ts +123 -0
  170. package/functions/commerce/products/variants.ts +123 -0
  171. package/functions/commerce/shippings/delivery-routes.ts +123 -0
  172. package/functions/commerce/shippings/digital-deliveries.ts +119 -0
  173. package/functions/commerce/shippings/drivers.ts +123 -0
  174. package/functions/commerce/shippings/license-keys.ts +123 -0
  175. package/functions/commerce/shippings/shipping-methods.ts +123 -0
  176. package/functions/commerce/shippings/shipping-rates.ts +119 -0
  177. package/functions/commerce/shippings/shipping-zones.ts +119 -0
  178. package/functions/commerce/taxes.ts +119 -0
  179. package/functions/commerce/waitlist/products.ts +123 -0
  180. package/functions/commerce/waitlist/restaurants.ts +123 -0
  181. package/functions/craft.ts +107 -0
  182. package/functions/dashboard.ts +153 -0
  183. package/functions/inbox.ts +95 -0
  184. package/functions/jobs.ts +86 -0
  185. package/functions/monitoring/errors.ts +295 -0
  186. package/functions/queue.ts +111 -0
  187. package/functions/realtime/websockets.ts +96 -0
  188. package/functions/realtime.ts +52 -0
  189. package/functions/releases.ts +49 -0
  190. package/functions/search.ts +126 -0
  191. package/functions/toasts.ts +95 -0
  192. package/functions/uploads.ts +216 -0
  193. package/ide/.gitignore +4 -0
  194. package/ide/cursor/rules/auto-imports.mdc +574 -0
  195. package/ide/cursor/rules/code-style.mdc +12 -0
  196. package/ide/cursor/rules/documentation.mdc +9 -0
  197. package/ide/cursor/rules/error-handling-validation.mdc +11 -0
  198. package/ide/cursor/rules/key-conventions.mdc +11 -0
  199. package/ide/cursor/rules/project-structure.mdc +169 -0
  200. package/ide/cursor/rules/readme.mdc +318 -0
  201. package/ide/cursor/rules/syntax-formatting.mdc +9 -0
  202. package/ide/cursor/rules/testing.mdc +10 -0
  203. package/ide/cursor/rules/typescript.mdc +9 -0
  204. package/ide/cursor/rules/ui-styling.mdc +0 -0
  205. package/ide/dictionary.txt +397 -0
  206. package/ide/jetbrains/.fleet/settings.json +4 -0
  207. package/ide/jetbrains/.idea/.gitignore +5 -0
  208. package/ide/jetbrains/.idea/.name +1 -0
  209. package/ide/jetbrains/.idea/inspectionProfiles/Project_Default.xml +6 -0
  210. package/ide/jetbrains/.idea/jsLinters/eslint.xml +7 -0
  211. package/ide/jetbrains/.idea/modules.xml +8 -0
  212. package/ide/jetbrains/.idea/stacks.iml +22 -0
  213. package/ide/jetbrains/.idea/vcs.xml +6 -0
  214. package/ide/jetbrains/.idea/webResources.xml +14 -0
  215. package/ide/jetbrains/IntelliJ IDEA Global Settings +0 -0
  216. package/ide/jetbrains/installed.txt +4 -0
  217. package/ide/jetbrains/options/github-copilot.xml +5 -0
  218. package/ide/jetbrains/options/nodejs.xml +7 -0
  219. package/ide/jetbrains/options/templates.xml +124 -0
  220. package/ide/jetbrains/templates/JavaScript.xml +6 -0
  221. package/ide/jetbrains/templates/Stacks.xml +8 -0
  222. package/ide/vscode/.vscode/README.md +23 -0
  223. package/ide/vscode/.vscode/extensions.json +5 -0
  224. package/ide/vscode/.vscode/settings.json +176 -0
  225. package/ide/vscode/.vscode/stacks.code-snippets +181 -0
  226. package/ide/vscode/LICENSE.md +21 -0
  227. package/ide/vscode/README.md +36 -0
  228. package/ide/vscode/logo.png +0 -0
  229. package/ide/vscode/package.json +79 -0
  230. package/ide/zed/.zed/settings.json +139 -0
  231. package/lang/README.md +28 -0
  232. package/lang/de.yml +14 -0
  233. package/lang/en.yml +15 -0
  234. package/package.json +19 -3
  235. package/project/bootstrap +161 -0
  236. package/project/buddy +175 -0
  237. package/project/storage/framework/tsconfig.app.json +50 -0
  238. package/project/storage/framework/tsconfig.base.json +58 -0
  239. package/routes/core.ts +78 -0
  240. package/routes/dashboard-api.ts +233 -0
  241. package/routes/dashboard.ts +739 -0
  242. package/routes/email.ts +129 -0
  243. package/stores/[name].stx +16 -0
  244. package/stores/git.ts +71 -0
  245. package/stores/payment.ts +474 -0
  246. package/stores/queue.ts +43 -0
  247. package/stores/user.ts +29 -0
  248. package/stx-components-plugin.ts +25 -0
  249. package/styles/design-tokens.css +597 -0
  250. package/types/dashboard.ts +70 -0
  251. package/types/defaults.ts +438 -0
  252. package/vcs/github/CODE_OF_CONDUCT.md +83 -0
  253. package/vcs/github/CONTRIBUTING.md +187 -0
  254. package/vcs/github/EXPLANATIONS.md +41 -0
  255. package/vcs/github/FUNDING.yml +1 -0
  256. package/vcs/github/ISSUE_TEMPLATE/bug_report.yml +49 -0
  257. package/vcs/github/ISSUE_TEMPLATE/config.yml +10 -0
  258. package/vcs/github/ISSUE_TEMPLATE/feature_request.yml +44 -0
  259. package/vcs/github/SECURITY.md +70 -0
  260. package/vcs/github/renovate.json +5 -0
  261. package/vcs/github/stale.yml +14 -0
  262. package/vcs/github/workflows/README.md +16 -0
  263. package/vcs/github/workflows/ci.yml +114 -0
  264. package/vcs/github/workflows/export-size.yml +25 -0
  265. package/vcs/github/workflows/labeler.yml +13 -0
  266. package/vcs/github/workflows/release.yml +219 -0
  267. package/views/[...all].stx +8 -0
  268. package/views/about.md +3 -0
  269. package/views/auth/login.stx +1 -0
  270. package/views/auth/signup.stx +85 -0
  271. package/views/dashboard/.discovered-models.json +542 -0
  272. package/views/dashboard/AUDIT.md +904 -0
  273. package/views/dashboard/[...all].stx +89 -0
  274. package/views/dashboard/access-tokens/index.stx +177 -0
  275. package/views/dashboard/actions/index.stx +129 -0
  276. package/views/dashboard/analytics/blog/index.stx +356 -0
  277. package/views/dashboard/analytics/browsers/index.stx +263 -0
  278. package/views/dashboard/analytics/commerce/sales/index.stx +329 -0
  279. package/views/dashboard/analytics/commerce/web/index.stx +379 -0
  280. package/views/dashboard/analytics/countries/index.stx +289 -0
  281. package/views/dashboard/analytics/devices/index.stx +305 -0
  282. package/views/dashboard/analytics/events/index.stx +536 -0
  283. package/views/dashboard/analytics/index.stx +88 -0
  284. package/views/dashboard/analytics/marketing/index.stx +315 -0
  285. package/views/dashboard/analytics/pages/index.stx +271 -0
  286. package/views/dashboard/analytics/referrers/index.stx +319 -0
  287. package/views/dashboard/analytics/web/index.stx +356 -0
  288. package/views/dashboard/buddy/index.stx +198 -0
  289. package/views/dashboard/ci/index.stx +462 -0
  290. package/views/dashboard/cloud/index.stx +452 -0
  291. package/views/dashboard/commands/index.stx +295 -0
  292. package/views/dashboard/commerce/categories/index.stx +406 -0
  293. package/views/dashboard/commerce/coupons/index.stx +485 -0
  294. package/views/dashboard/commerce/customers/index.stx +410 -0
  295. package/views/dashboard/commerce/dashboard/index.stx +287 -0
  296. package/views/dashboard/commerce/delivery/delivery-routes.stx +305 -0
  297. package/views/dashboard/commerce/delivery/digital-delivery.stx +332 -0
  298. package/views/dashboard/commerce/delivery/drivers.stx +397 -0
  299. package/views/dashboard/commerce/delivery/index.stx +18 -0
  300. package/views/dashboard/commerce/delivery/license-keys.stx +380 -0
  301. package/views/dashboard/commerce/delivery/shipping-methods.stx +346 -0
  302. package/views/dashboard/commerce/delivery/shipping-rates.stx +338 -0
  303. package/views/dashboard/commerce/delivery/shipping-zones.stx +302 -0
  304. package/views/dashboard/commerce/delivery/tabs.ts +25 -0
  305. package/views/dashboard/commerce/gift-cards/index.stx +561 -0
  306. package/views/dashboard/commerce/manufacturers/index.stx +308 -0
  307. package/views/dashboard/commerce/orders/index.stx +247 -0
  308. package/views/dashboard/commerce/payments/index.stx +535 -0
  309. package/views/dashboard/commerce/pos/index.stx +653 -0
  310. package/views/dashboard/commerce/printers/devices/index.stx +273 -0
  311. package/views/dashboard/commerce/printers/receipts/index.stx +308 -0
  312. package/views/dashboard/commerce/products/detail.stx +563 -0
  313. package/views/dashboard/commerce/products/index.stx +370 -0
  314. package/views/dashboard/commerce/reviews/index.stx +656 -0
  315. package/views/dashboard/commerce/reviews/index.ts +0 -0
  316. package/views/dashboard/commerce/taxes/index.stx +377 -0
  317. package/views/dashboard/commerce/units/index.stx +317 -0
  318. package/views/dashboard/commerce/variants/index.stx +319 -0
  319. package/views/dashboard/commerce/waitlist/products.stx +942 -0
  320. package/views/dashboard/commerce/waitlist/restaurant.stx +1024 -0
  321. package/views/dashboard/components/index.stx +330 -0
  322. package/views/dashboard/composables/index.ts +19 -0
  323. package/views/dashboard/composables/useChart.ts +77 -0
  324. package/views/dashboard/composables/useNavigation.ts +99 -0
  325. package/views/dashboard/composables/useRole.ts +58 -0
  326. package/views/dashboard/composables/useTheme.ts +74 -0
  327. package/views/dashboard/content/authors/index.stx +171 -0
  328. package/views/dashboard/content/blog/index.stx +493 -0
  329. package/views/dashboard/content/categories/index.stx +169 -0
  330. package/views/dashboard/content/comments/index.stx +189 -0
  331. package/views/dashboard/content/dashboard.stx +383 -0
  332. package/views/dashboard/content/files/index.stx +706 -0
  333. package/views/dashboard/content/pages/index.stx +174 -0
  334. package/views/dashboard/content/posts/index.stx +269 -0
  335. package/views/dashboard/content/seo/index.stx +240 -0
  336. package/views/dashboard/content/tags/index.stx +167 -0
  337. package/views/dashboard/data/activity/index.stx +261 -0
  338. package/views/dashboard/data/dashboard/index.stx +116 -0
  339. package/views/dashboard/data/subscribers.stx +270 -0
  340. package/views/dashboard/data/teams.stx +275 -0
  341. package/views/dashboard/data/users.stx +279 -0
  342. package/views/dashboard/dependencies/index.stx +178 -0
  343. package/views/dashboard/deployments/[id].stx +234 -0
  344. package/views/dashboard/deployments/index.stx +9 -0
  345. package/views/dashboard/dns/index.stx +163 -0
  346. package/views/dashboard/environment/index.stx +283 -0
  347. package/views/dashboard/errors/index.stx +274 -0
  348. package/views/dashboard/forgot-password.stx +57 -0
  349. package/views/dashboard/functions/index.stx +159 -0
  350. package/views/dashboard/health/index.stx +248 -0
  351. package/views/dashboard/inbox/activity.stx +350 -0
  352. package/views/dashboard/inbox/index.stx +452 -0
  353. package/views/dashboard/inbox/settings.stx +290 -0
  354. package/views/dashboard/index.stx +252 -0
  355. package/views/dashboard/insights/index.stx +529 -0
  356. package/views/dashboard/jobs/[id].stx +430 -0
  357. package/views/dashboard/jobs/history.stx +222 -0
  358. package/views/dashboard/jobs/index.stx +214 -0
  359. package/views/dashboard/kanban/[id].stx +899 -0
  360. package/views/dashboard/kanban/index.stx +260 -0
  361. package/views/dashboard/layouts/default.stx +565 -0
  362. package/views/dashboard/layouts/guest.stx +10 -0
  363. package/views/dashboard/login.stx +48 -0
  364. package/views/dashboard/logs/index.stx +540 -0
  365. package/views/dashboard/mailboxes/index.stx +149 -0
  366. package/views/dashboard/management/permissions/index.stx +490 -0
  367. package/views/dashboard/marketing/campaigns/index.stx +604 -0
  368. package/views/dashboard/marketing/lists/index.stx +514 -0
  369. package/views/dashboard/marketing/reviews/index.stx +475 -0
  370. package/views/dashboard/marketing/social-posts/index.stx +560 -0
  371. package/views/dashboard/models/[model].stx +195 -0
  372. package/views/dashboard/models/index.stx +159 -0
  373. package/views/dashboard/monitoring/errors/index.stx +415 -0
  374. package/views/dashboard/notifications/dashboard.stx +383 -0
  375. package/views/dashboard/notifications/email.stx +336 -0
  376. package/views/dashboard/notifications/history.stx +373 -0
  377. package/views/dashboard/notifications/sms.stx +332 -0
  378. package/views/dashboard/packages/index.stx +195 -0
  379. package/views/dashboard/queries/[id].stx +381 -0
  380. package/views/dashboard/queries/history.stx +227 -0
  381. package/views/dashboard/queries/index.stx +335 -0
  382. package/views/dashboard/queries/slow.stx +318 -0
  383. package/views/dashboard/queue/index.stx +427 -0
  384. package/views/dashboard/realtime/index.stx +429 -0
  385. package/views/dashboard/register.stx +46 -0
  386. package/views/dashboard/releases/index.stx +389 -0
  387. package/views/dashboard/requests/index.stx +462 -0
  388. package/views/dashboard/serverless/index.stx +475 -0
  389. package/views/dashboard/servers/[id].stx +299 -0
  390. package/views/dashboard/servers/index.stx +483 -0
  391. package/views/dashboard/settings/billing.stx +50 -0
  392. package/views/dashboard/settings/index.stx +256 -0
  393. package/views/dashboard/settings/mail.stx +252 -0
  394. package/views/dashboard/stores/auth.ts +123 -0
  395. package/views/dashboard/stores/ci.ts +342 -0
  396. package/views/dashboard/stores/index.ts +129 -0
  397. package/views/dashboard/stores/kanban.ts +723 -0
  398. package/views/dashboard/stores/rbac.ts +350 -0
  399. package/views/dashboard/stx.config.ts +34 -0
  400. package/views/dashboard/teams/[id].stx +170 -0
  401. package/views/forum/category.stx +92 -0
  402. package/views/forum/index.stx +209 -0
  403. package/views/forum/new-topic.stx +113 -0
  404. package/views/forum/profile.stx +1 -0
  405. package/views/forum/topic.stx +143 -0
  406. package/views/hello-world/[name].stx +47 -0
  407. package/views/index.stx +21 -0
  408. package/views/system-tray/index.stx +147 -0
@@ -0,0 +1,723 @@
1
+ import { defineStore, derived, registerStoresClient, state } from '@stacksjs/stx'
2
+
3
+ interface BoardSummary {
4
+ id: number
5
+ uuid: string | null
6
+ name: string
7
+ description: string | null
8
+ icon: string
9
+ color: string
10
+ position: number
11
+ archived: boolean
12
+ cardCount: number
13
+ createdAt: string | null
14
+ updatedAt: string | null
15
+ }
16
+
17
+ interface CardLabel { id: number, name: string, color: string }
18
+ interface CardAssignee { userId: number, name: string | null, email: string | null }
19
+ interface CardComment {
20
+ id: number
21
+ uuid: string | null
22
+ userId: number | null
23
+ body: string
24
+ authorName: string | null
25
+ authorEmail: string | null
26
+ createdAt: string | null
27
+ updatedAt: string | null
28
+ }
29
+
30
+ interface CardRecord {
31
+ id: number
32
+ uuid: string | null
33
+ columnId: number
34
+ boardId: number
35
+ title: string
36
+ description: string | null
37
+ position: number
38
+ createdByUserId: number | null
39
+ dueDate: string | null
40
+ archived: boolean
41
+ createdAt: string | null
42
+ updatedAt: string | null
43
+ // Optional pivot-derived fields. BoardShowAction populates them
44
+ // (Phase 3); CardStoreAction's optimistic insert leaves them empty.
45
+ labels?: CardLabel[]
46
+ assignees?: CardAssignee[]
47
+ }
48
+
49
+ interface UserSummary { id: number, name: string | null, email: string | null }
50
+
51
+ interface CardDetail extends CardRecord {
52
+ labels: CardLabel[]
53
+ assignees: CardAssignee[]
54
+ comments: CardComment[]
55
+ }
56
+
57
+ interface ColumnRecord {
58
+ id: number
59
+ uuid: string | null
60
+ boardId: number
61
+ name: string
62
+ position: number
63
+ cardLimit: number | null
64
+ color: string
65
+ cards: CardRecord[]
66
+ }
67
+
68
+ interface LabelRecord {
69
+ id: number
70
+ boardId: number
71
+ name: string
72
+ color: string
73
+ }
74
+
75
+ interface BoardDetail {
76
+ id: number
77
+ uuid: string | null
78
+ name: string
79
+ description: string | null
80
+ icon: string
81
+ color: string
82
+ position: number
83
+ archived: boolean
84
+ createdAt: string | null
85
+ updatedAt: string | null
86
+ }
87
+
88
+ /**
89
+ * Kanban dashboard store (stacksjs/stacks#1846).
90
+ *
91
+ * Phase 1 surface — list of boards for the index page plus the
92
+ * fetch-by-id helper that the board detail page (Phase 2) will use.
93
+ * Drag-and-drop reorder, write mutations, and label/assignee ops land
94
+ * in Phase 2 / Phase 3.
95
+ *
96
+ * Persistence: only the last-viewed board id is cached in
97
+ * sessionStorage so the user lands back where they left off when they
98
+ * SPA-nav between unrelated dashboard pages. The actual data is always
99
+ * re-fetched.
100
+ */
101
+ export const kanbanStore = defineStore('kanban', () => {
102
+ const boards = state<BoardSummary[]>([])
103
+ const currentBoard = state<BoardDetail | null>(null)
104
+ const currentColumns = state<ColumnRecord[]>([])
105
+ const currentLabels = state<LabelRecord[]>([])
106
+ const lastViewedBoardId = state<number | null>(null)
107
+ const loading = state(true)
108
+ const loadingBoard = state(false)
109
+ const error = state<string | null>(null)
110
+ const errorBoard = state<string | null>(null)
111
+
112
+ // Phase 3 — card detail modal state. Holding card + comments here
113
+ // (instead of one of column.cards) keeps the modal independent of
114
+ // its source: opening from a card click on the board uses the
115
+ // already-loaded preview; refresh-mid-modal hits /cards/:id and
116
+ // hydrates from the network. Either way the modal reads from
117
+ // `openCard` / `openCardComments`.
118
+ const openCard = state<CardDetail | null>(null)
119
+ const loadingCard = state(false)
120
+ const errorCard = state<string | null>(null)
121
+ const users = state<UserSummary[]>([])
122
+ const loadingUsers = state(false)
123
+
124
+ const hasBoards = derived(() => boards().length > 0)
125
+
126
+ async function loadBoards(): Promise<void> {
127
+ loading.set(true)
128
+ error.set(null)
129
+ try {
130
+ const res = await fetch('/api/dashboard/kanban/boards', { headers: { accept: 'application/json' } })
131
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
132
+ const data = await res.json() as { boards: BoardSummary[], error?: string }
133
+ if (data.error) {
134
+ error.set(data.error)
135
+ boards.set([])
136
+ return
137
+ }
138
+ boards.set(data.boards ?? [])
139
+ }
140
+ catch (e) {
141
+ error.set(e instanceof Error ? e.message : String(e))
142
+ }
143
+ finally {
144
+ loading.set(false)
145
+ }
146
+ }
147
+
148
+ async function loadBoard(id: number): Promise<void> {
149
+ loadingBoard.set(true)
150
+ errorBoard.set(null)
151
+ lastViewedBoardId.set(id)
152
+ try {
153
+ const res = await fetch(`/api/dashboard/kanban/boards/${id}`, { headers: { accept: 'application/json' } })
154
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
155
+ const data = await res.json() as {
156
+ board?: BoardDetail
157
+ columns?: ColumnRecord[]
158
+ labels?: LabelRecord[]
159
+ error?: string
160
+ }
161
+ if (data.error || !data.board) {
162
+ errorBoard.set(data.error ?? 'Board not found')
163
+ currentBoard.set(null)
164
+ currentColumns.set([])
165
+ currentLabels.set([])
166
+ return
167
+ }
168
+ currentBoard.set(data.board)
169
+ currentColumns.set(data.columns ?? [])
170
+ currentLabels.set(data.labels ?? [])
171
+ }
172
+ catch (e) {
173
+ errorBoard.set(e instanceof Error ? e.message : String(e))
174
+ }
175
+ finally {
176
+ loadingBoard.set(false)
177
+ }
178
+ }
179
+
180
+ // ─── Mutations (Phase 2) ───────────────────────────────────────────
181
+ //
182
+ // Every mutation follows the same shape:
183
+ // 1. Apply optimistically — update the local store immediately so
184
+ // the UI feels snappy.
185
+ // 2. Fire the network request.
186
+ // 3. On failure, restore the pre-mutation snapshot + set an error
187
+ // message. Phase 3 wires a toast helper into the rollback path
188
+ // so the user actually sees the failure.
189
+ //
190
+ // The mutations return the new entity (or null on failure) so
191
+ // callers can chain further work or surface "saved!" toasts.
192
+
193
+ async function createBoard(input: { name: string, description?: string, color?: string, icon?: string }): Promise<BoardSummary | null> {
194
+ const snapshot = boards()
195
+ try {
196
+ const res = await fetch('/api/dashboard/kanban/boards', {
197
+ method: 'POST',
198
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
199
+ body: JSON.stringify(input),
200
+ })
201
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
202
+ const data = await res.json() as { board?: BoardSummary, error?: string }
203
+ if (data.error || !data.board) throw new Error(data.error ?? 'Create failed')
204
+ boards.set([...snapshot, data.board])
205
+ return data.board
206
+ }
207
+ catch (e) {
208
+ error.set(e instanceof Error ? e.message : String(e))
209
+ return null
210
+ }
211
+ }
212
+
213
+ async function deleteBoard(id: number): Promise<boolean> {
214
+ const snapshot = boards()
215
+ // Optimistic remove.
216
+ boards.set(snapshot.filter(b => b.id !== id))
217
+ try {
218
+ const res = await fetch(`/api/dashboard/kanban/boards/${id}`, { method: 'DELETE', headers: { accept: 'application/json' } })
219
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
220
+ return true
221
+ }
222
+ catch (e) {
223
+ boards.set(snapshot)
224
+ error.set(e instanceof Error ? e.message : String(e))
225
+ return false
226
+ }
227
+ }
228
+
229
+ async function createColumn(input: { boardId: number, name: string, color?: string }): Promise<ColumnRecord | null> {
230
+ const snapshot = currentColumns()
231
+ try {
232
+ const res = await fetch('/api/dashboard/kanban/columns', {
233
+ method: 'POST',
234
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
235
+ body: JSON.stringify(input),
236
+ })
237
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
238
+ const data = await res.json() as { column?: ColumnRecord, error?: string }
239
+ if (data.error || !data.column) throw new Error(data.error ?? 'Create failed')
240
+ currentColumns.set([...snapshot, data.column])
241
+ return data.column
242
+ }
243
+ catch (e) {
244
+ errorBoard.set(e instanceof Error ? e.message : String(e))
245
+ return null
246
+ }
247
+ }
248
+
249
+ async function deleteColumn(columnId: number): Promise<boolean> {
250
+ const snapshot = currentColumns()
251
+ currentColumns.set(snapshot.filter(c => c.id !== columnId))
252
+ try {
253
+ const res = await fetch(`/api/dashboard/kanban/columns/${columnId}`, { method: 'DELETE', headers: { accept: 'application/json' } })
254
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
255
+ return true
256
+ }
257
+ catch (e) {
258
+ currentColumns.set(snapshot)
259
+ errorBoard.set(e instanceof Error ? e.message : String(e))
260
+ return false
261
+ }
262
+ }
263
+
264
+ async function createCard(input: { columnId: number, title: string, description?: string }): Promise<CardRecord | null> {
265
+ const snapshot = currentColumns()
266
+ try {
267
+ const res = await fetch('/api/dashboard/kanban/cards', {
268
+ method: 'POST',
269
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
270
+ body: JSON.stringify(input),
271
+ })
272
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
273
+ const data = await res.json() as { card?: CardRecord, error?: string }
274
+ if (data.error || !data.card) throw new Error(data.error ?? 'Create failed')
275
+ currentColumns.set(snapshot.map(col =>
276
+ col.id === input.columnId
277
+ ? { ...col, cards: [...col.cards, data.card!] }
278
+ : col,
279
+ ))
280
+ return data.card
281
+ }
282
+ catch (e) {
283
+ errorBoard.set(e instanceof Error ? e.message : String(e))
284
+ return null
285
+ }
286
+ }
287
+
288
+ async function deleteCard(cardId: number): Promise<boolean> {
289
+ const snapshot = currentColumns()
290
+ currentColumns.set(snapshot.map(col => ({
291
+ ...col,
292
+ cards: col.cards.filter(c => c.id !== cardId),
293
+ })))
294
+ try {
295
+ const res = await fetch(`/api/dashboard/kanban/cards/${cardId}`, { method: 'DELETE', headers: { accept: 'application/json' } })
296
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
297
+ return true
298
+ }
299
+ catch (e) {
300
+ currentColumns.set(snapshot)
301
+ errorBoard.set(e instanceof Error ? e.message : String(e))
302
+ return false
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Reorder cards across one or more columns of the current board.
308
+ * Called by the drag-and-drop handler in `kanban/[id].stx` after a
309
+ * card move. Sends the full per-column state so neighbour
310
+ * positions never drift — see the CardsReorderAction docstring.
311
+ *
312
+ * @param payload one entry per affected column with that column's
313
+ * new ordered list of card ids
314
+ */
315
+ async function reorderCards(payload: Array<{ columnId: number, order: number[] }>): Promise<boolean> {
316
+ const snapshot = currentColumns()
317
+
318
+ // Apply optimistically — derive the new column state from the
319
+ // payload + existing cards.
320
+ const cardById = new Map<number, CardRecord>()
321
+ for (const col of snapshot)
322
+ for (const c of col.cards) cardById.set(c.id, c)
323
+
324
+ const next = snapshot.map((col) => {
325
+ const entry = payload.find(p => p.columnId === col.id)
326
+ if (!entry) return col
327
+ const reordered = entry.order
328
+ .map((id, i) => {
329
+ const card = cardById.get(id)
330
+ if (!card) return null
331
+ return { ...card, columnId: col.id, position: i }
332
+ })
333
+ .filter((c): c is CardRecord => c !== null)
334
+ return { ...col, cards: reordered }
335
+ })
336
+ currentColumns.set(next)
337
+
338
+ try {
339
+ const res = await fetch('/api/dashboard/kanban/cards/reorder', {
340
+ method: 'POST',
341
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
342
+ body: JSON.stringify({ columns: payload }),
343
+ })
344
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
345
+ const data = await res.json() as { moved?: number, error?: string }
346
+ if (data.error) throw new Error(data.error)
347
+ return true
348
+ }
349
+ catch (e) {
350
+ currentColumns.set(snapshot)
351
+ errorBoard.set(e instanceof Error ? e.message : String(e))
352
+ return false
353
+ }
354
+ }
355
+
356
+ async function reorderColumns(boardId: number, order: number[]): Promise<boolean> {
357
+ const snapshot = currentColumns()
358
+ // Optimistic reorder by sorting against the supplied id list.
359
+ const byId = new Map(snapshot.map(c => [c.id, c] as const))
360
+ const next = order.map((id, i) => {
361
+ const col = byId.get(id)
362
+ return col ? { ...col, position: i } : null
363
+ }).filter((c): c is ColumnRecord => c !== null)
364
+ currentColumns.set(next)
365
+
366
+ try {
367
+ const res = await fetch('/api/dashboard/kanban/columns/reorder', {
368
+ method: 'POST',
369
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
370
+ body: JSON.stringify({ boardId, order }),
371
+ })
372
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
373
+ const data = await res.json() as { reordered?: number, error?: string }
374
+ if (data.error) throw new Error(data.error)
375
+ return true
376
+ }
377
+ catch (e) {
378
+ currentColumns.set(snapshot)
379
+ errorBoard.set(e instanceof Error ? e.message : String(e))
380
+ return false
381
+ }
382
+ }
383
+
384
+ // ─── Phase 3 mutations (card detail) ─────────────────────────────
385
+
386
+ /**
387
+ * Helper: project a CardRecord (board view) onto a partial
388
+ * CardDetail (modal view). Used when the modal opens against a
389
+ * card already loaded into `currentColumns` — we fill in the
390
+ * pivot-derived shape and fire `/cards/:id` in the background to
391
+ * pick up comments.
392
+ */
393
+ function cardToDetail(c: CardRecord): CardDetail {
394
+ return {
395
+ ...c,
396
+ labels: c.labels ?? [],
397
+ assignees: c.assignees ?? [],
398
+ comments: [], // hydrated by openCardDetail
399
+ }
400
+ }
401
+
402
+ function findCardInColumns(cardId: number): CardRecord | null {
403
+ for (const col of currentColumns())
404
+ for (const card of col.cards)
405
+ if (card.id === cardId) return card
406
+ return null
407
+ }
408
+
409
+ async function openCardDetail(cardId: number): Promise<void> {
410
+ loadingCard.set(true)
411
+ errorCard.set(null)
412
+ // Seed from the board view if we have it so the modal renders
413
+ // immediately; the fetch fills in comments and refreshes labels
414
+ // / assignees from the canonical store.
415
+ const seed = findCardInColumns(cardId)
416
+ if (seed)
417
+ openCard.set(cardToDetail(seed))
418
+
419
+ try {
420
+ const res = await fetch(`/api/dashboard/kanban/cards/${cardId}`, { headers: { accept: 'application/json' } })
421
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
422
+ const data = await res.json() as {
423
+ card?: CardRecord
424
+ labels?: CardLabel[]
425
+ assignees?: CardAssignee[]
426
+ comments?: CardComment[]
427
+ error?: string
428
+ }
429
+ if (data.error || !data.card)
430
+ throw new Error(data.error ?? 'Card not found')
431
+ openCard.set({
432
+ ...data.card,
433
+ labels: data.labels ?? [],
434
+ assignees: data.assignees ?? [],
435
+ comments: data.comments ?? [],
436
+ })
437
+ }
438
+ catch (e) {
439
+ errorCard.set(e instanceof Error ? e.message : String(e))
440
+ }
441
+ finally {
442
+ loadingCard.set(false)
443
+ }
444
+ }
445
+
446
+ function closeCardDetail(): void {
447
+ openCard.set(null)
448
+ errorCard.set(null)
449
+ }
450
+
451
+ /**
452
+ * Apply an in-place patch to the open card AND to the matching card
453
+ * inside `currentColumns` so the board view re-renders alongside
454
+ * the modal. Reduces double-bookkeeping at every callsite.
455
+ */
456
+ function patchOpenCard(partial: Partial<CardDetail>): void {
457
+ const oc = openCard()
458
+ if (!oc) return
459
+ openCard.set({ ...oc, ...partial })
460
+
461
+ currentColumns.set(currentColumns().map(col => ({
462
+ ...col,
463
+ cards: col.cards.map(c => c.id === oc.id ? { ...c, ...partial } : c),
464
+ })))
465
+ }
466
+
467
+ async function updateCard(input: { title?: string, description?: string | null, dueDate?: string | null, archived?: boolean }): Promise<boolean> {
468
+ const oc = openCard()
469
+ if (!oc) return false
470
+ const snapshot = { ...oc }
471
+ patchOpenCard(input as Partial<CardDetail>)
472
+ try {
473
+ const res = await fetch(`/api/dashboard/kanban/cards/${oc.id}`, {
474
+ method: 'PATCH',
475
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
476
+ body: JSON.stringify(input),
477
+ })
478
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
479
+ const data = await res.json() as { card?: CardRecord, error?: string }
480
+ if (data.error || !data.card) throw new Error(data.error ?? 'Update failed')
481
+ // Server-canonical state wins (handles trimming etc.).
482
+ patchOpenCard(data.card)
483
+ return true
484
+ }
485
+ catch (e) {
486
+ // Roll back to the pre-patch snapshot.
487
+ openCard.set(snapshot)
488
+ currentColumns.set(currentColumns().map(col => ({
489
+ ...col,
490
+ cards: col.cards.map(c => c.id === snapshot.id ? snapshot : c),
491
+ })))
492
+ errorCard.set(e instanceof Error ? e.message : String(e))
493
+ return false
494
+ }
495
+ }
496
+
497
+ async function loadUsers(): Promise<void> {
498
+ if (loadingUsers() || users().length > 0) return
499
+ loadingUsers.set(true)
500
+ try {
501
+ const res = await fetch('/api/dashboard/kanban/users', { headers: { accept: 'application/json' } })
502
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
503
+ const data = await res.json() as { users?: UserSummary[] }
504
+ users.set(data.users ?? [])
505
+ }
506
+ catch {
507
+ // Soft-fail — assignee picker just shows empty.
508
+ }
509
+ finally {
510
+ loadingUsers.set(false)
511
+ }
512
+ }
513
+
514
+ async function syncCardLabels(cardId: number, labelIds: number[]): Promise<boolean> {
515
+ const oc = openCard()
516
+ if (!oc || oc.id !== cardId) return false
517
+ const snapshot = oc.labels
518
+ // Optimistic: keep only the labels in the new set, pulled from
519
+ // the current open card's labels OR the board palette.
520
+ const boardLabels = currentLabels()
521
+ const optimistic = labelIds.map((id) => {
522
+ const fromCard = oc.labels.find(l => l.id === id)
523
+ if (fromCard) return fromCard
524
+ const fromBoard = boardLabels.find(l => l.id === id)
525
+ return fromBoard ? { id: fromBoard.id, name: fromBoard.name, color: fromBoard.color } : null
526
+ }).filter((l): l is CardLabel => l !== null)
527
+ patchOpenCard({ labels: optimistic })
528
+
529
+ try {
530
+ const res = await fetch(`/api/dashboard/kanban/cards/${cardId}/labels`, {
531
+ method: 'POST',
532
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
533
+ body: JSON.stringify({ labelIds }),
534
+ })
535
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
536
+ const data = await res.json() as { labels?: CardLabel[], error?: string }
537
+ if (data.error) throw new Error(data.error)
538
+ // Canonical reconciliation.
539
+ patchOpenCard({ labels: data.labels ?? [] })
540
+ return true
541
+ }
542
+ catch (e) {
543
+ patchOpenCard({ labels: snapshot })
544
+ errorCard.set(e instanceof Error ? e.message : String(e))
545
+ return false
546
+ }
547
+ }
548
+
549
+ async function syncCardAssignees(cardId: number, userIds: number[]): Promise<boolean> {
550
+ const oc = openCard()
551
+ if (!oc || oc.id !== cardId) return false
552
+ const snapshot = oc.assignees
553
+ // Optimistic — pull names from the loaded users list when
554
+ // available; fall back to a minimal record so the avatar
555
+ // initial renders something.
556
+ const allUsers = users()
557
+ const optimistic = userIds.map((id) => {
558
+ const u = allUsers.find(x => x.id === id)
559
+ return u
560
+ ? { userId: u.id, name: u.name, email: u.email }
561
+ : { userId: id, name: null, email: null }
562
+ })
563
+ patchOpenCard({ assignees: optimistic })
564
+
565
+ try {
566
+ const res = await fetch(`/api/dashboard/kanban/cards/${cardId}/assignees`, {
567
+ method: 'POST',
568
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
569
+ body: JSON.stringify({ userIds }),
570
+ })
571
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
572
+ const data = await res.json() as { assignees?: CardAssignee[], error?: string }
573
+ if (data.error) throw new Error(data.error)
574
+ patchOpenCard({ assignees: data.assignees ?? [] })
575
+ return true
576
+ }
577
+ catch (e) {
578
+ patchOpenCard({ assignees: snapshot })
579
+ errorCard.set(e instanceof Error ? e.message : String(e))
580
+ return false
581
+ }
582
+ }
583
+
584
+ async function addComment(body: string): Promise<boolean> {
585
+ const oc = openCard()
586
+ if (!oc) return false
587
+ const trimmed = body.trim()
588
+ if (!trimmed) return false
589
+
590
+ try {
591
+ const res = await fetch(`/api/dashboard/kanban/cards/${oc.id}/comments`, {
592
+ method: 'POST',
593
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
594
+ body: JSON.stringify({ body: trimmed }),
595
+ })
596
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
597
+ const data = await res.json() as { comment?: CardComment, error?: string }
598
+ if (data.error || !data.comment) throw new Error(data.error ?? 'Comment failed')
599
+ patchOpenCard({ comments: [...oc.comments, data.comment] })
600
+ return true
601
+ }
602
+ catch (e) {
603
+ errorCard.set(e instanceof Error ? e.message : String(e))
604
+ return false
605
+ }
606
+ }
607
+
608
+ async function deleteComment(commentId: number): Promise<boolean> {
609
+ const oc = openCard()
610
+ if (!oc) return false
611
+ const snapshot = oc.comments
612
+ patchOpenCard({ comments: oc.comments.filter(c => c.id !== commentId) })
613
+ try {
614
+ const res = await fetch(`/api/dashboard/kanban/comments/${commentId}`, { method: 'DELETE', headers: { accept: 'application/json' } })
615
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
616
+ return true
617
+ }
618
+ catch (e) {
619
+ patchOpenCard({ comments: snapshot })
620
+ errorCard.set(e instanceof Error ? e.message : String(e))
621
+ return false
622
+ }
623
+ }
624
+
625
+ // ─── Label CRUD (board scope) ──────────────────────────────────────
626
+
627
+ async function createLabel(input: { boardId: number, name: string, color?: string }): Promise<LabelRecord | null> {
628
+ try {
629
+ const res = await fetch('/api/dashboard/kanban/labels', {
630
+ method: 'POST',
631
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
632
+ body: JSON.stringify(input),
633
+ })
634
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
635
+ const data = await res.json() as { label?: LabelRecord, error?: string }
636
+ if (data.error || !data.label) throw new Error(data.error ?? 'Create failed')
637
+ currentLabels.set([...currentLabels(), data.label])
638
+ return data.label
639
+ }
640
+ catch (e) {
641
+ errorCard.set(e instanceof Error ? e.message : String(e))
642
+ return null
643
+ }
644
+ }
645
+
646
+ async function deleteLabel(labelId: number): Promise<boolean> {
647
+ const snapshot = currentLabels()
648
+ currentLabels.set(snapshot.filter(l => l.id !== labelId))
649
+ // Also strip the label from any cards on the board (mirrors what
650
+ // LabelDestroyAction does server-side).
651
+ const colSnapshot = currentColumns()
652
+ currentColumns.set(colSnapshot.map(col => ({
653
+ ...col,
654
+ cards: col.cards.map(c => ({
655
+ ...c,
656
+ labels: (c.labels ?? []).filter(l => l.id !== labelId),
657
+ })),
658
+ })))
659
+ const oc = openCard()
660
+ if (oc)
661
+ patchOpenCard({ labels: oc.labels.filter(l => l.id !== labelId) })
662
+
663
+ try {
664
+ const res = await fetch(`/api/dashboard/kanban/labels/${labelId}`, { method: 'DELETE', headers: { accept: 'application/json' } })
665
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
666
+ return true
667
+ }
668
+ catch (e) {
669
+ currentLabels.set(snapshot)
670
+ currentColumns.set(colSnapshot)
671
+ errorCard.set(e instanceof Error ? e.message : String(e))
672
+ return false
673
+ }
674
+ }
675
+
676
+ return {
677
+ boards,
678
+ currentBoard,
679
+ currentColumns,
680
+ currentLabels,
681
+ lastViewedBoardId,
682
+ loading,
683
+ loadingBoard,
684
+ error,
685
+ errorBoard,
686
+ hasBoards,
687
+ loadBoards,
688
+ loadBoard,
689
+ createBoard,
690
+ deleteBoard,
691
+ createColumn,
692
+ deleteColumn,
693
+ createCard,
694
+ deleteCard,
695
+ reorderCards,
696
+ reorderColumns,
697
+ // Phase 3
698
+ openCard,
699
+ loadingCard,
700
+ errorCard,
701
+ users,
702
+ loadingUsers,
703
+ openCardDetail,
704
+ closeCardDetail,
705
+ updateCard,
706
+ loadUsers,
707
+ syncCardLabels,
708
+ syncCardAssignees,
709
+ addComment,
710
+ deleteComment,
711
+ createLabel,
712
+ deleteLabel,
713
+ }
714
+ }, {
715
+ persist: {
716
+ storage: 'sessionStorage',
717
+ key: 'stacks-dashboard-kanban',
718
+ pick: ['lastViewedBoardId'],
719
+ },
720
+ })
721
+
722
+ if (typeof window !== 'undefined')
723
+ registerStoresClient({ kanbanStore })