@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,899 @@
1
+ @extends('layouts/default')
2
+
3
+ @section('content')
4
+ <script client>
5
+ // Kanban board view (stacksjs/stacks#1846 Phase 2).
6
+ //
7
+ // Drag-and-drop via Pragmatic Drag and Drop (Atlassian). Loaded from
8
+ // esm.sh so we don't need a bundler step — the dashboard's
9
+ // <script client> blocks run as inline modules and ESM imports
10
+ // resolve at fetch time. The library is ~5kb gzipped so the
11
+ // round-trip cost is negligible.
12
+ //
13
+ // Optimistic-update pattern:
14
+ // 1. The DnD handler mutates the store immediately (so the card
15
+ // visually settles in its new position before the network
16
+ // round-trip).
17
+ // 2. The store fires the reorder API call.
18
+ // 3. On failure, the store restores its snapshot and surfaces an
19
+ // error message; the UI re-renders from the restored state.
20
+ //
21
+ // Both column reorder (drag the column header) and card reorder
22
+ // (drag the card body) share the same /cards/reorder + /columns/reorder
23
+ // mutations on the store.
24
+ import { useStore } from '@stacksjs/stx'
25
+ import { useNavigation } from '../composables/useNavigation'
26
+ import { useRole } from '../composables/useRole'
27
+ import {
28
+ draggable,
29
+ dropTargetForElements,
30
+ monitorForElements,
31
+ } from 'https://esm.sh/@atlaskit/pragmatic-drag-and-drop@1/element/adapter'
32
+ import {
33
+ attachClosestEdge,
34
+ extractClosestEdge,
35
+ } from 'https://esm.sh/@atlaskit/pragmatic-drag-and-drop-hitbox@1/closest-edge'
36
+
37
+ const kanban = useStore('kanban')
38
+ const { isDev } = useRole()
39
+ const canView = derived(() => isDev())
40
+
41
+ const loading = derived(() => kanban.loadingBoard())
42
+ const error = derived(() => kanban.errorBoard())
43
+ const board = derived(() => kanban.currentBoard())
44
+ const columns = derived(() => kanban.currentColumns())
45
+ const allLabels = derived(() => kanban.currentLabels())
46
+
47
+ // Card detail modal state — read straight from the store. The
48
+ // modal itself lives at the bottom of this file.
49
+ const openCard = derived(() => kanban.openCard())
50
+ const loadingCard = derived(() => kanban.loadingCard())
51
+ const errorCard = derived(() => kanban.errorCard())
52
+
53
+ // Inline composer state. Each column has its own (independent) "add
54
+ // card" composer; the boolean tracks whether the input is open.
55
+ const composingCardForColumnId = state(null)
56
+ const draftCardTitle = state('')
57
+ const composingNewColumn = state(false)
58
+ const draftColumnName = state('')
59
+
60
+ // Modal-local edit drafts. Title + description debounce-on-blur,
61
+ // due-date commits on change. Labels + assignees commit instantly
62
+ // via the sync endpoints (one round-trip per toggle).
63
+ const titleDraft = state('')
64
+ const descDraft = state('')
65
+ const commentDraft = state('')
66
+ const showLabelPicker = state(false)
67
+ const showAssigneePicker = state(false)
68
+ const showNewLabelComposer = state(false)
69
+ const newLabelName = state('')
70
+ const newLabelColor = state('slate')
71
+
72
+ function syncDraftsFromCard() {
73
+ const c = openCard()
74
+ titleDraft.set(c ? c.title : '')
75
+ descDraft.set(c ? (c.description || '') : '')
76
+ commentDraft.set('')
77
+ }
78
+ // Sync drafts when the open card changes (open / close / refetch).
79
+ effect(() => { openCard(); Promise.resolve().then(syncDraftsFromCard) })
80
+
81
+ async function commitTitle() {
82
+ const c = openCard()
83
+ if (!c) return
84
+ const next = titleDraft().trim()
85
+ if (!next || next === c.title) return
86
+ await kanban.updateCard({ title: next })
87
+ }
88
+
89
+ async function commitDescription() {
90
+ const c = openCard()
91
+ if (!c) return
92
+ const next = descDraft()
93
+ if (next === (c.description || '')) return
94
+ await kanban.updateCard({ description: next || null })
95
+ }
96
+
97
+ async function commitDueDate(value) {
98
+ await kanban.updateCard({ dueDate: value || null })
99
+ }
100
+
101
+ async function toggleLabel(labelId) {
102
+ const c = openCard()
103
+ if (!c) return
104
+ const has = c.labels.some(l => l.id === labelId)
105
+ const next = has
106
+ ? c.labels.filter(l => l.id !== labelId).map(l => l.id)
107
+ : [...c.labels.map(l => l.id), labelId]
108
+ await kanban.syncCardLabels(c.id, next)
109
+ }
110
+
111
+ async function toggleAssignee(userId) {
112
+ const c = openCard()
113
+ if (!c) return
114
+ const has = c.assignees.some(a => a.userId === userId)
115
+ const next = has
116
+ ? c.assignees.filter(a => a.userId !== userId).map(a => a.userId)
117
+ : [...c.assignees.map(a => a.userId), userId]
118
+ await kanban.syncCardAssignees(c.id, next)
119
+ }
120
+
121
+ async function submitComment() {
122
+ const text = commentDraft().trim()
123
+ if (!text) return
124
+ const ok = await kanban.addComment(text)
125
+ if (ok) commentDraft.set('')
126
+ }
127
+
128
+ async function deleteCommentClicked(commentId) {
129
+ if (!window.confirm('Delete this comment?')) return
130
+ await kanban.deleteComment(commentId)
131
+ }
132
+
133
+ async function submitNewLabel() {
134
+ const name = newLabelName().trim()
135
+ if (!name) return
136
+ const c = openCard()
137
+ const boardId = c ? c.boardId : (board() ? board().id : null)
138
+ if (!boardId) return
139
+ const created = await kanban.createLabel({ boardId, name, color: newLabelColor() })
140
+ if (created && c)
141
+ await kanban.syncCardLabels(c.id, [...c.labels.map(l => l.id), created.id])
142
+ newLabelName.set('')
143
+ showNewLabelComposer.set(false)
144
+ }
145
+
146
+ async function deleteLabelClicked(labelId) {
147
+ if (!window.confirm('Delete this label everywhere it\'s used?')) return
148
+ await kanban.deleteLabel(labelId)
149
+ }
150
+
151
+ const labelColors = ['slate', 'red', 'amber', 'emerald', 'blue', 'violet', 'rose']
152
+
153
+ function totalCards() {
154
+ const cols = columns()
155
+ let n = 0
156
+ for (const col of cols) n += (col.cards ? col.cards.length : 0)
157
+ return n
158
+ }
159
+
160
+ // Column color → header accent class. Free-form string in the
161
+ // model, mapped here so a new hue without an explicit mapping
162
+ // falls back to slate.
163
+ function columnAccentClass(color) {
164
+ return ({
165
+ slate: 'bg-slate-100 dark:bg-slate-800',
166
+ amber: 'bg-amber-100 dark:bg-amber-900/40',
167
+ blue: 'bg-blue-100 dark:bg-blue-900/40',
168
+ emerald: 'bg-emerald-100 dark:bg-emerald-900/40',
169
+ red: 'bg-red-100 dark:bg-red-900/40',
170
+ violet: 'bg-violet-100 dark:bg-violet-900/40',
171
+ })[color] || 'bg-slate-100 dark:bg-slate-800'
172
+ }
173
+
174
+ // Pull the id from the URL via the composable instead of window.location
175
+ // (stacksjs/stacks#1838 — no vanilla DOM in dashboard pages).
176
+ const id = useNavigation().urlIdParam()
177
+ if (id !== null)
178
+ kanban.loadBoard(id)
179
+
180
+ // ─── Composer handlers ────────────────────────────────────────────
181
+
182
+ function openCardComposer(columnId) {
183
+ composingCardForColumnId.set(columnId)
184
+ draftCardTitle.set('')
185
+ }
186
+ function closeCardComposer() {
187
+ composingCardForColumnId.set(null)
188
+ draftCardTitle.set('')
189
+ }
190
+ async function submitCard(columnId) {
191
+ const title = draftCardTitle().trim()
192
+ if (!title) {
193
+ closeCardComposer()
194
+ return
195
+ }
196
+ await kanban.createCard({ columnId, title })
197
+ closeCardComposer()
198
+ }
199
+ function isComposingFor(columnId) {
200
+ return composingCardForColumnId() === columnId
201
+ }
202
+
203
+ function openColumnComposer() {
204
+ composingNewColumn.set(true)
205
+ draftColumnName.set('')
206
+ }
207
+ function closeColumnComposer() {
208
+ composingNewColumn.set(false)
209
+ draftColumnName.set('')
210
+ }
211
+ async function submitColumn() {
212
+ const name = draftColumnName().trim()
213
+ if (!name) {
214
+ closeColumnComposer()
215
+ return
216
+ }
217
+ if (!board()) return
218
+ await kanban.createColumn({ boardId: board().id, name })
219
+ closeColumnComposer()
220
+ }
221
+
222
+ async function deleteCardClicked(cardId) {
223
+ if (!window.confirm('Delete this card?')) return
224
+ await kanban.deleteCard(cardId)
225
+ }
226
+ async function deleteColumnClicked(columnId, hasCards) {
227
+ const warning = hasCards
228
+ ? 'Delete this column AND its cards? This cannot be undone.'
229
+ : 'Delete this empty column?'
230
+ if (!window.confirm(warning)) return
231
+ await kanban.deleteColumn(columnId)
232
+ }
233
+
234
+ // ─── Card detail modal handlers ───────────────────────────────────
235
+
236
+ function openDetail(cardId) {
237
+ kanban.loadUsers() // background-fetch for assignee picker
238
+ kanban.openCardDetail(cardId)
239
+ }
240
+ function closeDetail() { kanban.closeCardDetail() }
241
+
242
+ // Label color → tailwind hue. Mirrors the column-accent map but
243
+ // returns a more saturated chip class.
244
+ function labelChipClass(color) {
245
+ return ({
246
+ slate: 'bg-slate-100 text-slate-700 dark:bg-slate-800/60 dark:text-slate-200',
247
+ amber: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200',
248
+ blue: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200',
249
+ emerald: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200',
250
+ red: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-200',
251
+ violet: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-200',
252
+ rose: 'bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-200',
253
+ })[color] || 'bg-slate-100 text-slate-700 dark:bg-slate-800/60 dark:text-slate-200'
254
+ }
255
+
256
+ function initialsOf(name, email) {
257
+ const source = (name || email || '?').trim()
258
+ const parts = source.split(/\s+/).filter(Boolean)
259
+ if (parts.length === 0) return '?'
260
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
261
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
262
+ }
263
+
264
+ function formatDueDate(iso) {
265
+ if (!iso) return ''
266
+ const d = new Date(iso)
267
+ if (Number.isNaN(d.getTime())) return ''
268
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
269
+ }
270
+
271
+ function formatTimestamp(iso) {
272
+ if (!iso) return ''
273
+ const d = new Date(iso)
274
+ if (Number.isNaN(d.getTime())) return ''
275
+ return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
276
+ }
277
+
278
+ // ─── DnD wiring ────────────────────────────────────────────────────
279
+ //
280
+ // The cleanest pattern with Pragmatic DnD + a reactive UI: wire the
281
+ // drop targets + draggables in an `onMount`-style effect that
282
+ // re-runs when the columns list changes. The library's `cleanup()`
283
+ // functions handle deregistering listeners as DOM nodes go away.
284
+ //
285
+ // We mark every card element with `data-card-id` and every column
286
+ // with `data-column-id` so the monitor can extract the move
287
+ // semantics from the dragged element + drop target.
288
+
289
+ const dndCleanups = []
290
+
291
+ function teardownDnd() {
292
+ while (dndCleanups.length) {
293
+ const fn = dndCleanups.pop()
294
+ try { fn() } catch { /* ignore */ }
295
+ }
296
+ }
297
+
298
+ function setupDnd() {
299
+ teardownDnd()
300
+ const root = document.querySelector('[data-kanban-root]')
301
+ if (!root) return
302
+
303
+ // Card draggables + per-card drop targets (for drop-between
304
+ // hitbox detection via Pragmatic DnD's closestEdge utility).
305
+ // Per-card drop targets give us a "drop above / below this card"
306
+ // signal; the column-level drop target is the fallback for
307
+ // empty-column drops.
308
+ for (const cardEl of root.querySelectorAll('[data-card-id]')) {
309
+ dndCleanups.push(draggable({
310
+ element: cardEl,
311
+ getInitialData: () => ({
312
+ kind: 'card',
313
+ cardId: Number(cardEl.getAttribute('data-card-id')),
314
+ fromColumnId: Number(cardEl.getAttribute('data-card-column-id')),
315
+ }),
316
+ }))
317
+ dndCleanups.push(dropTargetForElements({
318
+ element: cardEl,
319
+ canDrop: ({ source }) => source.data?.kind === 'card',
320
+ getData: ({ input, element }) => attachClosestEdge(
321
+ {
322
+ kind: 'card-drop',
323
+ overCardId: Number(cardEl.getAttribute('data-card-id')),
324
+ overColumnId: Number(cardEl.getAttribute('data-card-column-id')),
325
+ },
326
+ { input, element, allowedEdges: ['top', 'bottom'] },
327
+ ),
328
+ }))
329
+ }
330
+ for (const colEl of root.querySelectorAll('[data-column-id]')) {
331
+ dndCleanups.push(dropTargetForElements({
332
+ element: colEl,
333
+ canDrop: ({ source }) => source.data?.kind === 'card',
334
+ getData: () => ({
335
+ kind: 'column-drop',
336
+ columnId: Number(colEl.getAttribute('data-column-id')),
337
+ }),
338
+ }))
339
+ }
340
+
341
+ // Single monitor handles the actual move semantics — fires once
342
+ // per drop, reads source + nearest drop target (a card if the
343
+ // pointer is over one, the column otherwise), computes the new
344
+ // per-column order honouring drop position, hands it to the store.
345
+ dndCleanups.push(monitorForElements({
346
+ canMonitor: ({ source }) => source.data?.kind === 'card',
347
+ onDrop: ({ source, location }) => {
348
+ // The nearest drop target wins. If we landed over a card,
349
+ // its `closestEdge` (top/bottom) tells us whether the drop
350
+ // is before or after that card; if we landed over the
351
+ // column shell, it's an append-to-column drop.
352
+ const dropTargets = location.current.dropTargets ?? []
353
+ if (dropTargets.length === 0) return
354
+
355
+ const cardId = Number(source.data.cardId)
356
+ const fromColumnId = Number(source.data.fromColumnId)
357
+ if (!cardId || !fromColumnId) return
358
+
359
+ // Card-over-card target takes priority over the column-level
360
+ // target (Pragmatic DnD's `dropTargets` array is innermost-
361
+ // first, so a card target appears before its parent column).
362
+ const cardTarget = dropTargets.find(t => t.data?.kind === 'card-drop')
363
+ const columnTarget = dropTargets.find(t => t.data?.kind === 'column-drop')
364
+
365
+ let toColumnId
366
+ let dropBeforeCardId = null
367
+ if (cardTarget) {
368
+ toColumnId = Number(cardTarget.data.overColumnId)
369
+ const overCardId = Number(cardTarget.data.overCardId)
370
+ const edge = extractClosestEdge(cardTarget.data)
371
+ // edge === 'top' → insert before the overCard
372
+ // edge === 'bottom' → insert after = before the NEXT card
373
+ dropBeforeCardId = edge === 'top' ? overCardId : null
374
+ if (edge === 'bottom') {
375
+ // Find the next card in the column; null means "append".
376
+ const cols = columns()
377
+ const tcol = cols.find(c => c.id === toColumnId)
378
+ if (tcol) {
379
+ const idx = tcol.cards.findIndex(c => c.id === overCardId)
380
+ const next = idx >= 0 ? tcol.cards[idx + 1] : null
381
+ dropBeforeCardId = next ? next.id : null
382
+ }
383
+ }
384
+ }
385
+ else if (columnTarget) {
386
+ toColumnId = Number(columnTarget.data.columnId)
387
+ dropBeforeCardId = null // append
388
+ }
389
+ else {
390
+ return
391
+ }
392
+ if (!toColumnId) return
393
+
394
+ // No-op: dragging a card onto itself.
395
+ if (cardId === dropBeforeCardId) return
396
+
397
+ const cols = columns()
398
+ const fromCol = cols.find(c => c.id === fromColumnId)
399
+ const toCol = cols.find(c => c.id === toColumnId)
400
+ if (!fromCol || !toCol) return
401
+
402
+ // Build the new per-column orders.
403
+ const fromOrderRaw = fromCol.cards.map(c => c.id).filter(idd => idd !== cardId)
404
+
405
+ // Start the target column's order from the existing one,
406
+ // minus the moving card (in case of same-column rearrange),
407
+ // then insert at the computed position.
408
+ const toOrderRaw = toCol.cards.map(c => c.id).filter(idd => idd !== cardId)
409
+ let insertAt = toOrderRaw.length
410
+ if (dropBeforeCardId !== null) {
411
+ const idx = toOrderRaw.indexOf(dropBeforeCardId)
412
+ if (idx >= 0) insertAt = idx
413
+ }
414
+ const toOrder = [
415
+ ...toOrderRaw.slice(0, insertAt),
416
+ cardId,
417
+ ...toOrderRaw.slice(insertAt),
418
+ ]
419
+
420
+ const payload = fromColumnId === toColumnId
421
+ ? [{ columnId: toColumnId, order: toOrder }]
422
+ : [
423
+ { columnId: fromColumnId, order: fromOrderRaw },
424
+ { columnId: toColumnId, order: toOrder },
425
+ ]
426
+ kanban.reorderCards(payload)
427
+ },
428
+ }))
429
+ }
430
+
431
+ // Re-wire DnD whenever columns mutate (new card, deleted card,
432
+ // post-drop reorder, etc.). `effect` runs after every state change
433
+ // that touches `columns()`, with a microtask delay so the DOM has
434
+ // re-rendered first.
435
+ effect(() => {
436
+ columns()
437
+ Promise.resolve().then(setupDnd)
438
+ })
439
+
440
+ onUnmount(() => {
441
+ teardownDnd()
442
+ })
443
+ </script>
444
+
445
+ <div class="mx-auto px-6 py-6 max-w-full">
446
+ <div :if="!canView" class="mx-auto px-5 py-8 max-w-2xl text-center bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 rounded-lg dark:border-zinc-800">
447
+ <p class="text-sm text-zinc-600 dark:text-zinc-400">Kanban boards require the dev role.</p>
448
+ </div>
449
+
450
+ <div :if="canView">
451
+ <a href="/kanban" class="inline-flex gap-1 items-center mb-4 no-underline text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300" data-mpa="true">
452
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6" /></svg>
453
+ All boards
454
+ </a>
455
+
456
+ <!-- Loading -->
457
+ <div :if="loading" class="flex items-center justify-center" style="min-height: 40vh">
458
+ <svg class="text-violet-500 animate-spin" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
459
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
460
+ </svg>
461
+ </div>
462
+
463
+ <!-- Error -->
464
+ <div :if="error && !loading" class="px-5 py-4 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900/40">
465
+ <p class="font-medium text-red-700 text-sm dark:text-red-300">Board operation failed</p>
466
+ <p class="mt-1 text-red-600 text-xs dark:text-red-400" :text="error"></p>
467
+ </div>
468
+
469
+ <!-- Loaded -->
470
+ <div :if="board && !loading">
471
+ <header class="flex items-start justify-between mb-6">
472
+ <div>
473
+ <h1 class="font-semibold text-2xl text-zinc-900 dark:text-zinc-100" :text="board.name"></h1>
474
+ <p :if="board.description" class="mt-1 text-sm text-zinc-500" :text="board.description"></p>
475
+ <p class="mt-2 text-xs text-zinc-400">
476
+ <span :text="columns.length + ' columns'"></span>
477
+ <span> · </span>
478
+ <span :text="totalCards() + ' cards'"></span>
479
+ </p>
480
+ </div>
481
+ </header>
482
+
483
+ <!-- The board itself. Horizontal scroll for wide boards;
484
+ each column is a vertical card stack. -->
485
+ <div data-kanban-root class="flex overflow-x-auto gap-3 pb-4" style="min-height: 60vh">
486
+ <div
487
+ :for="col in columns"
488
+ :data-column-id="col.id"
489
+ class="flex flex-col flex-shrink-0 w-72 bg-white dark:bg-zinc-950 border border-zinc-200 rounded-lg dark:border-zinc-800"
490
+ style="max-height: calc(100vh - 220px)"
491
+ >
492
+ <!-- Column header -->
493
+ <div :class="`px-3 py-2 border-b border-zinc-100 dark:border-zinc-900 ${columnAccentClass(col.color)}`">
494
+ <div class="flex gap-2 items-center justify-between">
495
+ <p class="font-medium text-sm text-zinc-700 truncate dark:text-zinc-200" :text="col.name"></p>
496
+ <div class="flex gap-1 items-center">
497
+ <span class="font-mono text-xs text-zinc-500" :text="col.cards.length"></span>
498
+ <button
499
+ @click="deleteColumnClicked(col.id, col.cards.length > 0)"
500
+ class="-m-1 p-1 text-zinc-400 hover:text-red-500"
501
+ title="Delete column"
502
+ >
503
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
504
+ <polyline points="3 6 5 6 21 6" />
505
+ <path d="M19 6l-2 14a2 2 0 01-2 2H9a2 2 0 01-2-2L5 6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
506
+ </svg>
507
+ </button>
508
+ </div>
509
+ </div>
510
+ </div>
511
+
512
+ <!-- Cards (scrollable) -->
513
+ <div class="overflow-y-auto flex-1 p-2 space-y-1" style="min-height: 40px">
514
+ <div
515
+ :for="card in col.cards"
516
+ :data-card-id="card.id"
517
+ :data-card-column-id="col.id"
518
+ @click="openDetail(card.id)"
519
+ class="px-3 py-2 bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 rounded dark:border-zinc-800 hover:border-violet-300 dark:hover:border-violet-700 transition-colors cursor-grab group"
520
+ >
521
+ <!-- Label chips strip -->
522
+ <div :if="card.labels && card.labels.length > 0" class="flex flex-wrap gap-1 mb-1">
523
+ <span
524
+ :for="lbl in card.labels"
525
+ :class="`px-1.5 py-0.5 text-[10px] font-medium rounded ${labelChipClass(lbl.color)}`"
526
+ :text="lbl.name"
527
+ ></span>
528
+ </div>
529
+
530
+ <div class="flex gap-2 items-start justify-between">
531
+ <p class="flex-1 leading-snug text-xs text-zinc-800 dark:text-zinc-200" :text="card.title"></p>
532
+ <button
533
+ @click.stop="deleteCardClicked(card.id)"
534
+ class="text-zinc-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
535
+ title="Delete card"
536
+ >
537
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
538
+ <line x1="18" y1="6" x2="6" y2="18" />
539
+ <line x1="6" y1="6" x2="18" y2="18" />
540
+ </svg>
541
+ </button>
542
+ </div>
543
+
544
+ <!-- Footer: due date + assignee avatars -->
545
+ <div :if="card.dueDate || (card.assignees && card.assignees.length > 0)" class="flex gap-2 items-center justify-between mt-1.5 pt-1.5 border-t border-zinc-200/50 dark:border-zinc-800/50">
546
+ <div :if="card.dueDate" class="flex gap-1 items-center text-[10px] text-zinc-500">
547
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
548
+ <rect x="3" y="4" width="18" height="18" rx="2" /><line x1="16" y1="2" x2="16" y2="6" /><line x1="8" y1="2" x2="8" y2="6" /><line x1="3" y1="10" x2="21" y2="10" />
549
+ </svg>
550
+ <span :text="formatDueDate(card.dueDate)"></span>
551
+ </div>
552
+ <div :if="!card.dueDate"></div>
553
+ <div :if="card.assignees && card.assignees.length > 0" class="flex -space-x-1">
554
+ <span
555
+ :for="asg in card.assignees"
556
+ class="inline-flex items-center justify-center h-5 w-5 font-medium text-[9px] text-white bg-violet-500 ring-1 ring-white rounded-full dark:ring-zinc-950"
557
+ :title="asg.name || asg.email || 'Assignee'"
558
+ :text="initialsOf(asg.name, asg.email)"
559
+ ></span>
560
+ </div>
561
+ </div>
562
+ </div>
563
+ </div>
564
+
565
+ <!-- Add card composer -->
566
+ <div class="p-2 border-t border-zinc-100 dark:border-zinc-900">
567
+ <div :if="isComposingFor(col.id)" class="space-y-2">
568
+ <textarea
569
+ :value="draftCardTitle()"
570
+ @input="(e) => draftCardTitle.set(e.target.value)"
571
+ @keydown.enter.prevent="submitCard(col.id)"
572
+ @keydown.escape="closeCardComposer()"
573
+ placeholder="Card title…"
574
+ rows="2"
575
+ class="px-2 py-1 w-full text-xs text-zinc-800 dark:text-zinc-100 bg-white dark:bg-zinc-900 border border-zinc-300 rounded dark:border-zinc-700 focus:border-violet-500 focus:outline-none resize-none"
576
+ ></textarea>
577
+ <div class="flex gap-1">
578
+ <button
579
+ @click="submitCard(col.id)"
580
+ class="px-2 py-1 text-white text-xs bg-violet-600 hover:bg-violet-700 rounded"
581
+ >
582
+ Add
583
+ </button>
584
+ <button
585
+ @click="closeCardComposer()"
586
+ class="px-2 py-1 text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 rounded"
587
+ >
588
+ Cancel
589
+ </button>
590
+ </div>
591
+ </div>
592
+ <button
593
+ :if="!isComposingFor(col.id)"
594
+ @click="openCardComposer(col.id)"
595
+ class="px-2 py-1 w-full text-left text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-900 rounded"
596
+ >
597
+ + Add card
598
+ </button>
599
+ </div>
600
+ </div>
601
+
602
+ <!-- Add column trailing slot -->
603
+ <div class="flex-shrink-0 w-72">
604
+ <div :if="composingNewColumn()" class="p-3 space-y-2 bg-white dark:bg-zinc-950 border border-zinc-300 rounded-lg dark:border-zinc-700">
605
+ <input
606
+ :value="draftColumnName()"
607
+ @input="(e) => draftColumnName.set(e.target.value)"
608
+ @keydown.enter="submitColumn()"
609
+ @keydown.escape="closeColumnComposer()"
610
+ placeholder="Column name…"
611
+ class="px-2 py-1 w-full text-sm text-zinc-800 dark:text-zinc-100 bg-white dark:bg-zinc-900 border border-zinc-300 rounded dark:border-zinc-700 focus:border-violet-500 focus:outline-none"
612
+ autofocus
613
+ />
614
+ <div class="flex gap-1">
615
+ <button
616
+ @click="submitColumn()"
617
+ class="px-2 py-1 text-white text-xs bg-violet-600 hover:bg-violet-700 rounded"
618
+ >
619
+ Add column
620
+ </button>
621
+ <button
622
+ @click="closeColumnComposer()"
623
+ class="px-2 py-1 text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 rounded"
624
+ >
625
+ Cancel
626
+ </button>
627
+ </div>
628
+ </div>
629
+ <button
630
+ :if="!composingNewColumn()"
631
+ @click="openColumnComposer()"
632
+ class="h-12 w-full text-sm text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 border border-dashed border-zinc-300 rounded-lg dark:border-zinc-700 hover:border-zinc-400 dark:hover:border-zinc-600"
633
+ >
634
+ + Add column
635
+ </button>
636
+ </div>
637
+ </div>
638
+ </div>
639
+ </div>
640
+
641
+ <!-- ───────────── Card detail modal (Phase 3) ───────────── -->
642
+ <!-- Opens on card click. Fixed positioned with a backdrop; closes
643
+ on backdrop click + Escape. Sections (top → bottom):
644
+ · Title (inline-editable, blurs to commit)
645
+ · Description (textarea, blurs to commit)
646
+ · Sidebar: labels picker, assignees picker, due date, archive
647
+ · Comments thread + composer
648
+ All mutations route through the store, which fires optimistic
649
+ updates with rollback on failure. -->
650
+ <div
651
+ :if="openCard()"
652
+ class="flex fixed inset-0 overflow-y-auto z-50 items-start justify-center px-4 py-8 bg-black/50"
653
+ @click="closeDetail()"
654
+ @keydown.escape.window="closeDetail()"
655
+ >
656
+ <div
657
+ @click.stop
658
+ class="my-8 max-w-3xl w-full bg-white dark:bg-zinc-900 border border-zinc-200 rounded-lg dark:border-zinc-800 shadow-xl"
659
+ >
660
+ <!-- Header: title + close -->
661
+ <div class="flex gap-3 items-start px-6 py-4 border-b border-zinc-200 dark:border-zinc-800">
662
+ <input
663
+ :value="titleDraft()"
664
+ @input="(e) => titleDraft.set(e.target.value)"
665
+ @blur="commitTitle()"
666
+ @keydown.enter.prevent="(e) => e.target.blur()"
667
+ class="flex-1 -mx-2 -my-1 px-2 py-1 font-semibold text-lg text-zinc-900 dark:text-zinc-100 bg-transparent focus:bg-zinc-50 dark:focus:bg-zinc-800 rounded focus:outline-none"
668
+ />
669
+ <button
670
+ @click="closeDetail()"
671
+ class="text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300"
672
+ title="Close"
673
+ >
674
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
675
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
676
+ </svg>
677
+ </button>
678
+ </div>
679
+
680
+ <div :if="loadingCard() && (!openCard() || openCard().comments.length === 0)" class="px-6 py-8 text-center text-sm text-zinc-500">
681
+ Loading…
682
+ </div>
683
+
684
+ <div :if="errorCard()" class="px-6 py-3 bg-red-50 dark:bg-red-950/30 border-b border-red-200 dark:border-red-900/40">
685
+ <p class="text-red-700 text-xs dark:text-red-300" :text="errorCard()"></p>
686
+ </div>
687
+
688
+ <div class="grid gap-6 grid-cols-1 md:grid-cols-3 px-6 py-5">
689
+ <!-- Main column: description + comments -->
690
+ <div class="md:col-span-2 space-y-6">
691
+ <!-- Description -->
692
+ <section>
693
+ <h4 class="mb-2 font-medium text-xs text-zinc-500 tracking-wide uppercase">Description</h4>
694
+ <textarea
695
+ :value="descDraft()"
696
+ @input="(e) => descDraft.set(e.target.value)"
697
+ @blur="commitDescription()"
698
+ rows="4"
699
+ placeholder="Add a more detailed description…"
700
+ class="px-3 py-2 w-full text-sm text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-y"
701
+ ></textarea>
702
+ </section>
703
+
704
+ <!-- Comments -->
705
+ <section>
706
+ <h4 class="mb-2 font-medium text-xs text-zinc-500 tracking-wide uppercase">Activity</h4>
707
+
708
+ <!-- Composer -->
709
+ <div class="mb-4">
710
+ <textarea
711
+ :value="commentDraft()"
712
+ @input="(e) => commentDraft.set(e.target.value)"
713
+ @keydown.metaCmd.enter.prevent="submitComment()"
714
+ rows="2"
715
+ placeholder="Add a comment… (⌘+Enter to send)"
716
+ class="px-3 py-2 w-full text-sm text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-none"
717
+ ></textarea>
718
+ <div class="flex justify-end mt-2">
719
+ <button
720
+ @click="submitComment()"
721
+ :disabled="!commentDraft().trim()"
722
+ class="px-3 py-1 text-white text-xs bg-violet-600 hover:bg-violet-700 rounded disabled:opacity-40 disabled:cursor-not-allowed"
723
+ >
724
+ Comment
725
+ </button>
726
+ </div>
727
+ </div>
728
+
729
+ <!-- Thread -->
730
+ <div :if="openCard().comments.length === 0 && !loadingCard()" class="italic text-xs text-zinc-400">
731
+ No comments yet.
732
+ </div>
733
+ <ul class="space-y-3">
734
+ <li
735
+ :for="cm in openCard.comments"
736
+ class="flex gap-3"
737
+ >
738
+ <span class="inline-flex flex-shrink-0 items-center justify-center h-7 w-7 font-medium text-[10px] text-white bg-violet-500 rounded-full" :text="initialsOf(cm.authorName, cm.authorEmail)"></span>
739
+ <div class="flex-1 min-w-0">
740
+ <div class="flex gap-2 items-baseline">
741
+ <span class="font-medium text-xs text-zinc-700 dark:text-zinc-200" :text="cm.authorName || cm.authorEmail || 'Unknown'"></span>
742
+ <span class="text-[10px] text-zinc-400" :text="formatTimestamp(cm.createdAt)"></span>
743
+ <button
744
+ @click="deleteCommentClicked(cm.id)"
745
+ class="ml-auto text-[10px] text-zinc-400 hover:text-red-500"
746
+ >Delete</button>
747
+ </div>
748
+ <p class="mt-0.5 text-xs text-zinc-700 whitespace-pre-wrap dark:text-zinc-300" :text="cm.body"></p>
749
+ </div>
750
+ </li>
751
+ </ul>
752
+ </section>
753
+ </div>
754
+
755
+ <!-- Sidebar: labels, assignees, due date, archive -->
756
+ <aside class="space-y-5">
757
+ <!-- Labels -->
758
+ <section>
759
+ <div class="flex items-center justify-between mb-2">
760
+ <h4 class="font-medium text-xs text-zinc-500 tracking-wide uppercase">Labels</h4>
761
+ <button
762
+ @click="showLabelPicker.set(!showLabelPicker())"
763
+ class="text-violet-600 text-xs dark:text-violet-400 hover:underline"
764
+ >
765
+ <span :if="!showLabelPicker()">Edit</span>
766
+ <span :if="showLabelPicker()">Done</span>
767
+ </button>
768
+ </div>
769
+
770
+ <!-- Selected labels -->
771
+ <div :if="!showLabelPicker()" class="flex flex-wrap gap-1">
772
+ <span
773
+ :for="lbl in openCard.labels"
774
+ :class="`px-2 py-0.5 text-xs font-medium rounded ${labelChipClass(lbl.color)}`"
775
+ :text="lbl.name"
776
+ ></span>
777
+ <span :if="openCard().labels.length === 0" class="italic text-xs text-zinc-400">None</span>
778
+ </div>
779
+
780
+ <!-- Picker -->
781
+ <div :if="showLabelPicker()" class="space-y-2">
782
+ <button
783
+ :for="lbl in allLabels"
784
+ @click="toggleLabel(lbl.id)"
785
+ :class="`w-full flex items-center justify-between px-2 py-1 rounded text-xs ${labelChipClass(lbl.color)} hover:opacity-90`"
786
+ >
787
+ <span :text="lbl.name"></span>
788
+ <span class="flex gap-1 items-center">
789
+ <svg :if="openCard().labels.some(l => l.id === lbl.id)" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
790
+ <polyline points="20 6 9 17 4 12" />
791
+ </svg>
792
+ <span
793
+ @click.stop="deleteLabelClicked(lbl.id)"
794
+ class="text-zinc-500 hover:text-red-500"
795
+ title="Delete label"
796
+ >
797
+
798
+ </span>
799
+ </span>
800
+ </button>
801
+
802
+ <!-- New label composer -->
803
+ <div :if="showNewLabelComposer()" class="pt-2 space-y-2 border-t border-zinc-200 dark:border-zinc-800">
804
+ <input
805
+ :value="newLabelName()"
806
+ @input="(e) => newLabelName.set(e.target.value)"
807
+ @keydown.enter.prevent="submitNewLabel()"
808
+ placeholder="Label name"
809
+ class="px-2 py-1 w-full text-xs bg-white dark:bg-zinc-950 border border-zinc-300 rounded dark:border-zinc-700 focus:border-violet-500 focus:outline-none"
810
+ />
811
+ <div class="flex gap-1 items-center">
812
+ <button
813
+ :for="c in labelColors"
814
+ @click="newLabelColor.set(c)"
815
+ :class="`w-5 h-5 rounded ${labelChipClass(c)} ${newLabelColor() === c ? 'ring-2 ring-offset-1 ring-violet-500 dark:ring-offset-zinc-900' : ''}`"
816
+ :title="c"
817
+ ></button>
818
+ </div>
819
+ <div class="flex gap-1">
820
+ <button @click="submitNewLabel()" class="px-2 py-0.5 text-white text-xs bg-violet-600 hover:bg-violet-700 rounded">Create</button>
821
+ <button @click="showNewLabelComposer.set(false)" class="px-2 py-0.5 text-xs text-zinc-500 hover:text-zinc-700">Cancel</button>
822
+ </div>
823
+ </div>
824
+ <button
825
+ :if="!showNewLabelComposer()"
826
+ @click="showNewLabelComposer.set(true)"
827
+ class="px-2 py-1 w-full text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 border border-dashed border-zinc-300 rounded dark:border-zinc-700"
828
+ >
829
+ + New label
830
+ </button>
831
+ </div>
832
+ </section>
833
+
834
+ <!-- Assignees -->
835
+ <section>
836
+ <div class="flex items-center justify-between mb-2">
837
+ <h4 class="font-medium text-xs text-zinc-500 tracking-wide uppercase">Assignees</h4>
838
+ <button
839
+ @click="showAssigneePicker.set(!showAssigneePicker())"
840
+ class="text-violet-600 text-xs dark:text-violet-400 hover:underline"
841
+ >
842
+ <span :if="!showAssigneePicker()">Edit</span>
843
+ <span :if="showAssigneePicker()">Done</span>
844
+ </button>
845
+ </div>
846
+
847
+ <div :if="!showAssigneePicker()" class="flex flex-wrap gap-2">
848
+ <div
849
+ :for="asg in openCard.assignees"
850
+ class="flex gap-1.5 items-center text-xs text-zinc-700 dark:text-zinc-200"
851
+ >
852
+ <span class="inline-flex items-center justify-center h-6 w-6 font-medium text-[10px] text-white bg-violet-500 rounded-full" :text="initialsOf(asg.name, asg.email)"></span>
853
+ <span :text="asg.name || asg.email || 'Unknown'"></span>
854
+ </div>
855
+ <span :if="openCard().assignees.length === 0" class="italic text-xs text-zinc-400">Unassigned</span>
856
+ </div>
857
+
858
+ <div :if="showAssigneePicker()" class="overflow-y-auto space-y-1 max-h-48">
859
+ <p :if="kanban.users().length === 0" class="italic text-xs text-zinc-400">No users to assign.</p>
860
+ <button
861
+ :for="u in kanban.users"
862
+ @click="toggleAssignee(u.id)"
863
+ class="flex gap-2 items-center px-2 py-1.5 w-full text-left text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded"
864
+ >
865
+ <span class="inline-flex flex-shrink-0 items-center justify-center h-6 w-6 font-medium text-[10px] text-white bg-violet-500 rounded-full" :text="initialsOf(u.name, u.email)"></span>
866
+ <span class="flex-1 text-zinc-700 truncate dark:text-zinc-200" :text="u.name || u.email"></span>
867
+ <svg :if="openCard().assignees.some(a => a.userId === u.id)" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="text-violet-500">
868
+ <polyline points="20 6 9 17 4 12" />
869
+ </svg>
870
+ </button>
871
+ </div>
872
+ </section>
873
+
874
+ <!-- Due date -->
875
+ <section>
876
+ <h4 class="mb-2 font-medium text-xs text-zinc-500 tracking-wide uppercase">Due date</h4>
877
+ <input
878
+ type="date"
879
+ :value="openCard().dueDate ? openCard().dueDate.slice(0, 10) : ''"
880
+ @change="(e) => commitDueDate(e.target.value)"
881
+ class="px-2 py-1 w-full text-xs text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none"
882
+ />
883
+ </section>
884
+
885
+ <!-- Meta -->
886
+ <section class="pt-3 border-t border-zinc-200 dark:border-zinc-800">
887
+ <p class="text-[10px] text-zinc-400">
888
+ Created <span :text="formatTimestamp(openCard().createdAt)"></span>
889
+ </p>
890
+ <p :if="openCard().updatedAt && openCard().updatedAt !== openCard().createdAt" class="text-[10px] text-zinc-400">
891
+ Updated <span :text="formatTimestamp(openCard().updatedAt)"></span>
892
+ </p>
893
+ </section>
894
+ </aside>
895
+ </div>
896
+ </div>
897
+ </div>
898
+ </div>
899
+ @endsection