@stacksjs/defaults 0.70.206 → 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,1252 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>[PROJECT NAME] Architecture Diagram</title>
7
+ <script>
8
+ // Resolve the theme before first paint so light-preference users don't
9
+ // see a dark flash. The toolbar script below re-applies with UI state.
10
+ (function () {
11
+ try {
12
+ var theme = null;
13
+ try {
14
+ var param = new URLSearchParams(window.location.search).get('theme');
15
+ if (param === 'light' || param === 'dark') theme = param;
16
+ } catch (_) {}
17
+ if (!theme) {
18
+ try { theme = localStorage.getItem('technical-diagrams-theme'); } catch (_) {}
19
+ }
20
+ if (theme !== 'light' && theme !== 'dark') {
21
+ theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
22
+ }
23
+ document.documentElement.setAttribute('data-theme', theme);
24
+ } catch (_) {}
25
+ })();
26
+ </script>
27
+ <style>
28
+ /* ==========================================================
29
+ THEME VARIABLES - switch by toggling [data-theme] on <html>
30
+ ========================================================== */
31
+ :root,
32
+ [data-theme="dark"] {
33
+ --bg: #020617;
34
+ --grid: #1e293b;
35
+ --text: #ffffff;
36
+ --text-muted: #94a3b8;
37
+ --text-dim: #475569;
38
+ /* UI chrome (menu hints, footer) needs more contrast than SVG .t-dim accents */
39
+ --text-faint: #7d8da1;
40
+
41
+ --panel: rgba(15, 23, 42, 0.5);
42
+ --panel-border: #1e293b;
43
+ --lane-fill: rgba(15, 23, 42, 0.22);
44
+ --lane-stroke: #334155;
45
+
46
+ --arrow: #64748b;
47
+ --arrow-emphasis: #34d399;
48
+
49
+ /* Opaque mask color used behind semi-transparent component fills
50
+ so the arrows drawn underneath are properly hidden. */
51
+ --mask: #0f172a;
52
+
53
+ --frontend-fill: rgba(8, 51, 68, 0.4);
54
+ --frontend-stroke: #22d3ee;
55
+ --backend-fill: rgba(6, 78, 59, 0.4);
56
+ --backend-stroke: #34d399;
57
+ --database-fill: rgba(76, 29, 149, 0.4);
58
+ --database-stroke: #a78bfa;
59
+ --cloud-fill: rgba(120, 53, 15, 0.3);
60
+ --cloud-stroke: #fbbf24;
61
+ --security-fill: rgba(136, 19, 55, 0.4);
62
+ --security-stroke: #fb7185;
63
+ --messagebus-fill: rgba(251, 146, 60, 0.3);
64
+ --messagebus-stroke:#fb923c;
65
+ --external-fill: rgba(30, 41, 59, 0.5);
66
+ --external-stroke: #94a3b8;
67
+
68
+ --toolbar-bg: rgba(15, 23, 42, 0.8);
69
+ --toolbar-border: #334155;
70
+ --toolbar-text: #e2e8f0;
71
+ --toolbar-hover: rgba(15, 23, 42, 0.95);
72
+ --toolbar-menu-bg: #0f172a;
73
+ }
74
+
75
+ [data-theme="light"] {
76
+ --bg: #f8fafc;
77
+ --grid: #e2e8f0;
78
+ --text: #0f172a;
79
+ --text-muted: #64748b;
80
+ --text-dim: #94a3b8;
81
+ --text-faint: #64748b;
82
+
83
+ --panel: #ffffff;
84
+ --panel-border: #e2e8f0;
85
+ --lane-fill: rgba(248, 250, 252, 0.65);
86
+ --lane-stroke: #cbd5e1;
87
+
88
+ --arrow: #94a3b8;
89
+ --arrow-emphasis: #059669;
90
+
91
+ --mask: #ffffff;
92
+
93
+ --frontend-fill: rgba(34, 211, 238, 0.15);
94
+ --frontend-stroke: #0891b2;
95
+ --backend-fill: rgba(52, 211, 153, 0.18);
96
+ --backend-stroke: #059669;
97
+ --database-fill: rgba(167, 139, 250, 0.2);
98
+ --database-stroke: #7c3aed;
99
+ --cloud-fill: rgba(251, 191, 36, 0.18);
100
+ --cloud-stroke: #d97706;
101
+ --security-fill: rgba(251, 113, 133, 0.15);
102
+ --security-stroke: #e11d48;
103
+ --messagebus-fill: rgba(251, 146, 60, 0.15);
104
+ --messagebus-stroke:#ea580c;
105
+ --external-fill: rgba(148, 163, 184, 0.18);
106
+ --external-stroke: #64748b;
107
+
108
+ --toolbar-bg: rgba(255, 255, 255, 0.92);
109
+ --toolbar-border: #cbd5e1;
110
+ --toolbar-text: #334155;
111
+ --toolbar-hover: #ffffff;
112
+ --toolbar-menu-bg: #ffffff;
113
+ }
114
+
115
+ * { margin: 0; padding: 0; box-sizing: border-box; }
116
+
117
+ body {
118
+ font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', 'Liberation Mono', 'Noto Sans Mono CJK SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', monospace;
119
+ background: var(--bg);
120
+ min-height: 100vh;
121
+ padding: 2rem;
122
+ color: var(--text);
123
+ transition: background 0.2s ease, color 0.2s ease;
124
+ }
125
+
126
+ .container { max-width: 1200px; margin: 0 auto; }
127
+
128
+ .header { margin-bottom: 2rem; }
129
+
130
+ .header-row {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 1rem;
134
+ margin-bottom: 0.5rem;
135
+ }
136
+
137
+ .pulse-dot {
138
+ width: 12px;
139
+ height: 12px;
140
+ background: var(--frontend-stroke);
141
+ border-radius: 50%;
142
+ animation: pulse 2s infinite;
143
+ }
144
+
145
+ @keyframes pulse {
146
+ 0%, 100% { opacity: 1; }
147
+ 50% { opacity: 0.5; }
148
+ }
149
+
150
+ h1 {
151
+ font-size: 1.5rem;
152
+ font-weight: 700;
153
+ letter-spacing: -0.025em;
154
+ }
155
+
156
+ .subtitle {
157
+ color: var(--text-muted);
158
+ font-size: 0.875rem;
159
+ margin-left: 1.75rem;
160
+ }
161
+
162
+ .diagram-container {
163
+ background: var(--panel);
164
+ border-radius: 1rem;
165
+ border: 1px solid var(--panel-border);
166
+ padding: 1.5rem;
167
+ overflow-x: auto;
168
+ }
169
+
170
+ svg {
171
+ width: 100%;
172
+ min-width: min(900px, 100%);
173
+ display: block;
174
+ }
175
+
176
+ .cards {
177
+ display: grid;
178
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
179
+ gap: 1rem;
180
+ margin-top: 2rem;
181
+ }
182
+
183
+ .card {
184
+ background: var(--panel);
185
+ border-radius: 0.75rem;
186
+ border: 1px solid var(--panel-border);
187
+ padding: 1.25rem;
188
+ }
189
+
190
+ .card-header {
191
+ display: flex;
192
+ align-items: center;
193
+ gap: 0.5rem;
194
+ margin-bottom: 0.75rem;
195
+ }
196
+
197
+ .card-dot {
198
+ width: 8px;
199
+ height: 8px;
200
+ border-radius: 50%;
201
+ }
202
+
203
+ .card-dot.cyan { background: var(--frontend-stroke); }
204
+ .card-dot.emerald { background: var(--backend-stroke); }
205
+ .card-dot.violet { background: var(--database-stroke); }
206
+ .card-dot.amber { background: var(--cloud-stroke); }
207
+ .card-dot.rose { background: var(--security-stroke); }
208
+ .card-dot.orange { background: var(--messagebus-stroke); }
209
+ .card-dot.slate { background: var(--external-stroke); }
210
+
211
+ .card h3 { font-size: 0.875rem; font-weight: 600; color: var(--text); }
212
+
213
+ .card ul {
214
+ list-style: none;
215
+ color: var(--text-muted);
216
+ font-size: 0.75rem;
217
+ }
218
+
219
+ .card li { margin-bottom: 0.375rem; }
220
+
221
+ .footer {
222
+ text-align: center;
223
+ margin-top: 1.5rem;
224
+ color: var(--text-faint);
225
+ font-size: 0.75rem;
226
+ }
227
+
228
+ /* ==========================================================
229
+ Print stylesheet - hide interactive controls, force light,
230
+ drop the grid so the diagram doesn't waste ink, use landscape
231
+ so wide diagrams fit, pack summary cards into 2 columns to
232
+ avoid an orphan card on a trailing page.
233
+ ========================================================== */
234
+ @page { size: landscape; margin: 1.5cm; }
235
+
236
+ @media print {
237
+ /* Force the FULL light palette - including component fills/strokes and
238
+ lane/arrow colors - so printing from dark theme doesn't put neon
239
+ strokes and translucent dark fills on white paper. */
240
+ :root, [data-theme="dark"], [data-theme="light"] {
241
+ --bg: #ffffff;
242
+ --grid: transparent;
243
+ --panel: #ffffff;
244
+ --panel-border: #e2e8f0;
245
+ --text: #0f172a;
246
+ --text-muted: #475569;
247
+ --text-dim: #94a3b8;
248
+ --text-faint: #64748b;
249
+ --mask: #ffffff;
250
+ --lane-fill: rgba(248, 250, 252, 0.65);
251
+ --lane-stroke: #cbd5e1;
252
+ --arrow: #94a3b8;
253
+ --arrow-emphasis: #059669;
254
+ --frontend-fill: rgba(34, 211, 238, 0.15);
255
+ --frontend-stroke: #0891b2;
256
+ --backend-fill: rgba(52, 211, 153, 0.18);
257
+ --backend-stroke: #059669;
258
+ --database-fill: rgba(167, 139, 250, 0.2);
259
+ --database-stroke: #7c3aed;
260
+ --cloud-fill: rgba(251, 191, 36, 0.18);
261
+ --cloud-stroke: #d97706;
262
+ --security-fill: rgba(251, 113, 133, 0.15);
263
+ --security-stroke: #e11d48;
264
+ --messagebus-fill: rgba(251, 146, 60, 0.15);
265
+ --messagebus-stroke:#ea580c;
266
+ --external-fill: rgba(148, 163, 184, 0.18);
267
+ --external-stroke: #64748b;
268
+ }
269
+ body { background: #ffffff !important; padding: 0; }
270
+ .container { max-width: none; }
271
+ .toolbar, .technical-diagram-toast, .no-print { display: none !important; }
272
+ .diagram-container, .card { box-shadow: none; border-color: #e2e8f0; }
273
+ .cards { grid-template-columns: 1fr 1fr; }
274
+ /* Ensure elements break sensibly across pages */
275
+ .card { break-inside: avoid; page-break-inside: avoid; }
276
+ .diagram-container { break-inside: avoid; page-break-inside: avoid; }
277
+ h1, .subtitle { color: #0f172a; }
278
+ }
279
+
280
+ /* ==========================================================
281
+ TOOLBAR (theme toggle + export menu)
282
+ ========================================================== */
283
+ .toolbar {
284
+ position: fixed;
285
+ top: 1rem;
286
+ right: 1rem;
287
+ display: flex;
288
+ gap: 0.5rem;
289
+ z-index: 100;
290
+ }
291
+ .toolbar button {
292
+ background: var(--toolbar-bg);
293
+ color: var(--toolbar-text);
294
+ border: 1px solid var(--toolbar-border);
295
+ padding: 0.5rem 0.875rem;
296
+ border-radius: 0.5rem;
297
+ font-family: inherit;
298
+ font-size: 0.75rem;
299
+ font-weight: 500;
300
+ cursor: pointer;
301
+ backdrop-filter: blur(8px);
302
+ transition: background 0.15s, border-color 0.15s;
303
+ display: inline-flex;
304
+ align-items: center;
305
+ gap: 0.375rem;
306
+ }
307
+ .toolbar button:hover {
308
+ background: var(--toolbar-hover);
309
+ border-color: var(--arrow);
310
+ }
311
+ .toolbar button:focus-visible {
312
+ outline: 2px solid var(--frontend-stroke);
313
+ outline-offset: 2px;
314
+ }
315
+ .toolbar .export-wrap { position: relative; }
316
+ .toolbar .export-menu {
317
+ position: absolute;
318
+ top: calc(100% + 0.375rem);
319
+ right: 0;
320
+ background: var(--toolbar-menu-bg);
321
+ border: 1px solid var(--toolbar-border);
322
+ border-radius: 0.5rem;
323
+ padding: 0.375rem;
324
+ min-width: 160px;
325
+ display: none;
326
+ flex-direction: column;
327
+ box-shadow: 0 4px 20px rgba(0,0,0,0.25);
328
+ }
329
+ .toolbar .export-menu.open { display: flex; }
330
+ .toolbar .export-menu button {
331
+ background: transparent;
332
+ border: 0;
333
+ justify-content: space-between;
334
+ width: 100%;
335
+ text-align: left;
336
+ padding: 0.5rem 0.75rem;
337
+ }
338
+ .toolbar .export-menu button:hover {
339
+ background: var(--toolbar-hover);
340
+ }
341
+ .toolbar .export-menu .hint {
342
+ color: var(--text-faint);
343
+ font-size: 0.625rem;
344
+ }
345
+ .toolbar .export-menu hr {
346
+ border: 0;
347
+ border-top: 1px solid var(--toolbar-border);
348
+ margin: 0.25rem 0.375rem;
349
+ opacity: 0.5;
350
+ }
351
+
352
+ /* Toast - brief feedback for actions like "copied" */
353
+ .technical-diagram-toast {
354
+ position: fixed;
355
+ top: 1rem;
356
+ left: 50%;
357
+ transform: translateX(-50%) translateY(-8px);
358
+ background: var(--toolbar-menu-bg);
359
+ color: var(--text);
360
+ border: 1px solid var(--toolbar-border);
361
+ padding: 0.5rem 1rem;
362
+ border-radius: 0.5rem;
363
+ font-family: inherit;
364
+ font-size: 0.75rem;
365
+ opacity: 0;
366
+ transition: opacity 0.2s, transform 0.2s;
367
+ z-index: 200;
368
+ pointer-events: none;
369
+ box-shadow: 0 4px 20px rgba(0,0,0,0.25);
370
+ }
371
+ .technical-diagram-toast.show {
372
+ opacity: 1;
373
+ transform: translateX(-50%) translateY(0);
374
+ }
375
+
376
+ @media (max-width: 720px) {
377
+ body { padding: 1rem; }
378
+ .toolbar {
379
+ position: static;
380
+ justify-content: flex-end;
381
+ margin-bottom: 1rem;
382
+ }
383
+ .header { margin-bottom: 1.25rem; }
384
+ .header-row {
385
+ gap: 0.75rem;
386
+ align-items: flex-start;
387
+ }
388
+ h1 {
389
+ font-size: 1.25rem;
390
+ line-height: 1.25;
391
+ }
392
+ .subtitle {
393
+ margin-left: 0;
394
+ font-size: 0.8125rem;
395
+ line-height: 1.55;
396
+ }
397
+ .diagram-container {
398
+ padding: 0.75rem;
399
+ border-radius: 0.75rem;
400
+ }
401
+ .cards {
402
+ grid-template-columns: 1fr;
403
+ margin-top: 1rem;
404
+ }
405
+ }
406
+
407
+ /* ==========================================================
408
+ SVG SEMANTIC CLASSES - use these instead of inline fill/stroke
409
+ ========================================================== */
410
+ .c-grid { stroke: var(--grid); fill: none; }
411
+ .c-mask { fill: var(--mask); stroke: none; }
412
+
413
+ .c-frontend { fill: var(--frontend-fill); stroke: var(--frontend-stroke); }
414
+ .c-backend { fill: var(--backend-fill); stroke: var(--backend-stroke); }
415
+ .c-database { fill: var(--database-fill); stroke: var(--database-stroke); }
416
+ .c-cloud { fill: var(--cloud-fill); stroke: var(--cloud-stroke); }
417
+ .c-security { fill: var(--security-fill); stroke: var(--security-stroke); }
418
+ .c-messagebus { fill: var(--messagebus-fill); stroke: var(--messagebus-stroke); }
419
+ .c-external { fill: var(--external-fill); stroke: var(--external-stroke); }
420
+
421
+ /* Text helpers */
422
+ .t-primary { fill: var(--text); }
423
+ .t-muted { fill: var(--text-muted); }
424
+ .t-dim { fill: var(--text-dim); }
425
+ .t-frontend { fill: var(--frontend-stroke); }
426
+ .t-backend { fill: var(--backend-stroke); }
427
+ .t-database { fill: var(--database-stroke); }
428
+ .t-cloud { fill: var(--cloud-stroke); }
429
+ .t-security { fill: var(--security-stroke); }
430
+ .t-messagebus { fill: var(--messagebus-stroke); }
431
+ .t-external { fill: var(--external-stroke); }
432
+
433
+ /* Arrows */
434
+ .a-default { stroke: var(--arrow); fill: none; }
435
+ .a-emphasis { stroke: var(--arrow-emphasis); fill: none; }
436
+ .a-security { stroke: var(--security-stroke); fill: none; stroke-dasharray: 5,5; }
437
+ .a-dashed { stroke: var(--database-stroke); fill: none; stroke-dasharray: 4,4; }
438
+
439
+ /* Arrowhead markers reference these */
440
+ .m-default { fill: var(--arrow); }
441
+ .m-emphasis { fill: var(--arrow-emphasis); }
442
+ .m-security { fill: var(--security-stroke); }
443
+ .m-dashed { fill: var(--database-stroke); }
444
+
445
+ /* Dashed boundary variants */
446
+ .c-security-group { fill: transparent; stroke: var(--security-stroke); stroke-dasharray: 4,4; }
447
+ .c-region { fill: rgba(251, 191, 36, 0.05); stroke: var(--cloud-stroke); stroke-dasharray: 8,4; }
448
+ .c-lane { fill: var(--lane-fill); stroke: var(--lane-stroke); stroke-dasharray: 6,6; }
449
+
450
+ /* Optional trace animation, enabled only by meta.animation = "trace".
451
+ Static diagrams do not carry data-animation and render exactly as before. */
452
+ svg[data-animation="trace"] [data-animate="edge"] {
453
+ stroke-dasharray: 10 8;
454
+ stroke-dashoffset: 54;
455
+ animation: technical-diagram-edge-flow 2.4s linear infinite;
456
+ animation-delay: calc(var(--step, 0) * 160ms);
457
+ }
458
+ svg[data-animation="trace"] [data-animate="node"] {
459
+ transform-box: fill-box;
460
+ transform-origin: center;
461
+ animation: technical-diagram-node-pulse 3.6s ease-in-out infinite;
462
+ animation-delay: calc(var(--step, 0) * 160ms);
463
+ }
464
+ @keyframes technical-diagram-edge-flow {
465
+ to { stroke-dashoffset: 0; }
466
+ }
467
+ @keyframes technical-diagram-node-pulse {
468
+ 0%, 72%, 100% { filter: none; stroke-width: 1.5; }
469
+ 18%, 36% {
470
+ filter: drop-shadow(0 0 8px var(--arrow-emphasis));
471
+ stroke-width: 2.4;
472
+ }
473
+ }
474
+ @media (prefers-reduced-motion: reduce) {
475
+ svg[data-animation="trace"] [data-animate] {
476
+ animation: none !important;
477
+ stroke-dashoffset: 0;
478
+ }
479
+ }
480
+ </style>
481
+ </head>
482
+ <body>
483
+ <!-- Toolbar: theme toggle + export menu (present on every generated diagram)
484
+ Keyboard: T toggles theme, E opens export menu (when no input focused) -->
485
+ <div class="toolbar" role="toolbar" aria-label="Diagram actions">
486
+ <button id="btn-theme" type="button"
487
+ title="Toggle theme (T)"
488
+ aria-label="Toggle color theme"
489
+ aria-pressed="false">
490
+ <span id="theme-icon" aria-hidden="true">&#9790;</span>
491
+ <span id="theme-label">Dark</span>
492
+ </button>
493
+ <div class="export-wrap">
494
+ <button id="btn-export" type="button"
495
+ title="Export diagram (E)"
496
+ aria-label="Export diagram"
497
+ aria-haspopup="menu"
498
+ aria-expanded="false"
499
+ aria-controls="export-menu">
500
+ Export <span aria-hidden="true">&#9662;</span>
501
+ </button>
502
+ <div class="export-menu" id="export-menu" role="menu" aria-label="Export">
503
+ <button data-action="copy" type="button" role="menuitem" tabindex="-1">Copy to clipboard <span class="hint">PNG</span></button>
504
+ <hr role="separator">
505
+ <button data-format="png" type="button" role="menuitem" tabindex="-1">Download PNG</button>
506
+ <button data-format="jpeg" type="button" role="menuitem" tabindex="-1">Download JPEG</button>
507
+ <button data-format="webp" type="button" role="menuitem" tabindex="-1">Download WebP</button>
508
+ <hr role="separator">
509
+ <button data-format="svg" type="button" role="menuitem" tabindex="-1">Download SVG <span class="hint">vector</span></button>
510
+ </div>
511
+ </div>
512
+ </div>
513
+
514
+ <div class="container">
515
+ <!-- Header -->
516
+ <div class="header">
517
+ <div class="header-row">
518
+ <div class="pulse-dot"></div>
519
+ <h1>[PROJECT NAME] Architecture</h1>
520
+ </div>
521
+ <p class="subtitle">[Subtitle description]</p>
522
+ </div>
523
+
524
+ <!-- Main Diagram -->
525
+ <div class="diagram-container">
526
+ <!-- ARCHIFY:SVG_SLOT_START -->
527
+ <svg viewBox="0 0 1000 680" role="img" aria-label="[PROJECT NAME] architecture diagram">
528
+ <!-- Definitions -->
529
+ <defs>
530
+ <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
531
+ <polygon points="0 0, 10 3.5, 0 7" class="m-default" />
532
+ </marker>
533
+ <marker id="arrowhead-emphasis" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
534
+ <polygon points="0 0, 10 3.5, 0 7" class="m-emphasis" />
535
+ </marker>
536
+ <marker id="arrowhead-security" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
537
+ <polygon points="0 0, 10 3.5, 0 7" class="m-security" />
538
+ </marker>
539
+ <marker id="arrowhead-dashed" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
540
+ <polygon points="0 0, 10 3.5, 0 7" class="m-dashed" />
541
+ </marker>
542
+ <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
543
+ <path d="M 40 0 L 0 0 0 40" class="c-grid" stroke-width="0.5"/>
544
+ </pattern>
545
+ </defs>
546
+
547
+ <!-- Background Grid -->
548
+ <rect width="100%" height="100%" fill="url(#grid)" />
549
+
550
+ <!-- =================================================================
551
+ COMPONENT EXAMPLES - use CSS classes, not inline fill/stroke
552
+ (so theme toggle works). Each box pattern:
553
+ 1. opaque mask rect (.c-mask) to hide arrows underneath
554
+ 2. styled rect (.c-<type>) with 1.5px stroke
555
+ 3. text (.t-primary / .t-muted)
556
+ ================================================================= -->
557
+
558
+ <!-- External/Generic Component -->
559
+ <rect x="30" y="280" width="100" height="50" rx="6" class="c-mask"/>
560
+ <rect x="30" y="280" width="100" height="50" rx="6" class="c-external" stroke-width="1.5"/>
561
+ <text x="80" y="300" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Users</text>
562
+ <text x="80" y="316" class="t-muted" font-size="9" text-anchor="middle">Browser/Mobile</text>
563
+
564
+ <!-- Security Component -->
565
+ <rect x="30" y="80" width="100" height="60" rx="6" class="c-mask"/>
566
+ <rect x="30" y="80" width="100" height="60" rx="6" class="c-security" stroke-width="1.5"/>
567
+ <text x="80" y="105" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Auth Provider</text>
568
+ <text x="80" y="121" class="t-muted" font-size="9" text-anchor="middle">OAuth 2.0</text>
569
+
570
+ <!-- Region/Cloud Boundary -->
571
+ <rect x="160" y="40" width="820" height="620" rx="12" class="c-region" stroke-width="1"/>
572
+ <text x="172" y="58" class="t-cloud" font-size="10" font-weight="600">AWS Region: us-west-2</text>
573
+
574
+ <!-- AWS/Cloud Service -->
575
+ <rect x="200" y="280" width="110" height="50" rx="6" class="c-mask"/>
576
+ <rect x="200" y="280" width="110" height="50" rx="6" class="c-cloud" stroke-width="1.5"/>
577
+ <text x="255" y="300" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">CloudFront</text>
578
+ <text x="255" y="316" class="t-muted" font-size="9" text-anchor="middle">CDN</text>
579
+
580
+ <!-- Multi-line AWS Component (S3 Buckets example) -->
581
+ <rect x="200" y="380" width="110" height="100" rx="6" class="c-mask"/>
582
+ <rect x="200" y="380" width="110" height="100" rx="6" class="c-cloud" stroke-width="1.5"/>
583
+ <text x="255" y="400" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">S3 Buckets</text>
584
+ <text x="255" y="420" class="t-muted" font-size="8" text-anchor="middle">&bull; bucket-one</text>
585
+ <text x="255" y="434" class="t-muted" font-size="8" text-anchor="middle">&bull; bucket-two</text>
586
+ <text x="255" y="448" class="t-muted" font-size="8" text-anchor="middle">&bull; bucket-three</text>
587
+ <text x="255" y="466" class="t-cloud" font-size="7" text-anchor="middle">OAI Protected</text>
588
+
589
+ <!-- Security Group (dashed boundary). NOTE: extra vertical headroom
590
+ so the sg-name label has ~12px breathing room above the inner
591
+ component. Pattern: boundary y = inner.y - 30, h = inner.h + 50. -->
592
+ <rect x="350" y="250" width="120" height="100" rx="8" class="c-security-group" stroke-width="1"/>
593
+ <text x="358" y="268" class="t-security" font-size="8">sg-name :port</text>
594
+
595
+ <!-- Component inside security group -->
596
+ <rect x="360" y="280" width="100" height="50" rx="6" class="c-mask"/>
597
+ <rect x="360" y="280" width="100" height="50" rx="6" class="c-cloud" stroke-width="1.5"/>
598
+ <text x="410" y="300" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Load Balancer</text>
599
+ <text x="410" y="316" class="t-muted" font-size="9" text-anchor="middle">HTTPS :443</text>
600
+
601
+ <!-- Backend Component -->
602
+ <rect x="510" y="280" width="110" height="50" rx="6" class="c-mask"/>
603
+ <rect x="510" y="280" width="110" height="50" rx="6" class="c-backend" stroke-width="1.5"/>
604
+ <text x="565" y="300" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">API Server</text>
605
+ <text x="565" y="316" class="t-muted" font-size="9" text-anchor="middle">FastAPI :8000</text>
606
+
607
+ <!-- Database Component -->
608
+ <rect x="700" y="280" width="120" height="50" rx="6" class="c-mask"/>
609
+ <rect x="700" y="280" width="120" height="50" rx="6" class="c-database" stroke-width="1.5"/>
610
+ <text x="760" y="300" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Database</text>
611
+ <text x="760" y="316" class="t-muted" font-size="9" text-anchor="middle">PostgreSQL</text>
612
+
613
+ <!-- Frontend Component -->
614
+ <rect x="200" y="520" width="200" height="110" rx="8" class="c-mask"/>
615
+ <rect x="200" y="520" width="200" height="110" rx="8" class="c-frontend" stroke-width="1.5"/>
616
+ <text x="300" y="545" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Frontend</text>
617
+ <text x="300" y="565" class="t-muted" font-size="9" text-anchor="middle">React + TypeScript</text>
618
+ <text x="300" y="580" class="t-muted" font-size="9" text-anchor="middle">Additional detail</text>
619
+ <text x="300" y="595" class="t-muted" font-size="9" text-anchor="middle">More info</text>
620
+ <text x="300" y="615" class="t-frontend" font-size="8" text-anchor="middle">domain.example.com</text>
621
+
622
+ <!-- =================================================================
623
+ ARROW EXAMPLES (use .a-* classes and stroke-width on the line)
624
+ ================================================================= -->
625
+
626
+ <!-- Standard arrow with label -->
627
+ <line x1="130" y1="305" x2="198" y2="305" class="a-default" stroke-width="1.5" marker-end="url(#arrowhead)"/>
628
+ <text x="164" y="299" class="t-muted" font-size="9" text-anchor="middle">HTTPS</text>
629
+
630
+ <!-- Simple arrow (no label) -->
631
+ <line x1="310" y1="305" x2="358" y2="305" class="a-default" stroke-width="1.5" marker-end="url(#arrowhead)"/>
632
+
633
+ <!-- Vertical arrow -->
634
+ <line x1="255" y1="330" x2="255" y2="378" class="a-default" stroke-width="1.5" marker-end="url(#arrowhead)"/>
635
+ <text x="270" y="358" class="t-muted" font-size="9">OAI</text>
636
+
637
+ <!-- Straight arrows between services -->
638
+ <line x1="460" y1="305" x2="508" y2="305" class="a-default" stroke-width="1.5" marker-end="url(#arrowhead)"/>
639
+ <line x1="620" y1="305" x2="698" y2="305" class="a-default" stroke-width="1.5" marker-end="url(#arrowhead)"/>
640
+ <text x="655" y="299" class="t-muted" font-size="9">TLS</text>
641
+
642
+ <!-- Curved dashed path for auth/security flow -->
643
+ <path d="M 80 140 L 80 200 Q 80 220 100 220 L 200 220 Q 220 220 220 240 L 220 278"
644
+ class="a-security" stroke-width="1.5"/>
645
+ <text x="150" y="210" class="t-security" font-size="8">JWT + PKCE</text>
646
+
647
+ <!-- =================================================================
648
+ LEGEND
649
+ ================================================================= -->
650
+ <text x="720" y="70" class="t-primary" font-size="10" font-weight="600">Legend</text>
651
+
652
+ <rect x="720" y="82" width="16" height="10" rx="2" class="c-frontend" stroke-width="1"/>
653
+ <text x="742" y="90" class="t-muted" font-size="8">Frontend</text>
654
+
655
+ <rect x="720" y="98" width="16" height="10" rx="2" class="c-backend" stroke-width="1"/>
656
+ <text x="742" y="106" class="t-muted" font-size="8">Backend</text>
657
+
658
+ <rect x="720" y="114" width="16" height="10" rx="2" class="c-cloud" stroke-width="1"/>
659
+ <text x="742" y="122" class="t-muted" font-size="8">Cloud Service</text>
660
+
661
+ <rect x="720" y="130" width="16" height="10" rx="2" class="c-database" stroke-width="1"/>
662
+ <text x="742" y="138" class="t-muted" font-size="8">Database</text>
663
+
664
+ <rect x="720" y="146" width="16" height="10" rx="2" class="c-security" stroke-width="1"/>
665
+ <text x="742" y="154" class="t-muted" font-size="8">Security</text>
666
+
667
+ <line x1="720" y1="168" x2="736" y2="168" class="a-security" stroke-width="1"/>
668
+ <text x="742" y="171" class="t-muted" font-size="8">Auth Flow</text>
669
+
670
+ <rect x="720" y="178" width="16" height="10" rx="2" class="c-security-group" stroke-width="1"/>
671
+ <text x="742" y="186" class="t-muted" font-size="8">Security Group</text>
672
+ </svg>
673
+ <!-- ARCHIFY:SVG_SLOT_END -->
674
+ </div>
675
+
676
+ <!-- ARCHIFY:CARDS_SLOT_START -->
677
+ <div class="cards">
678
+ <div class="card">
679
+ <div class="card-header">
680
+ <div class="card-dot rose"></div>
681
+ <h3>Card Title 1</h3>
682
+ </div>
683
+ <ul>
684
+ <li>&bull; Item one</li>
685
+ <li>&bull; Item two</li>
686
+ <li>&bull; Item three</li>
687
+ <li>&bull; Item four</li>
688
+ </ul>
689
+ </div>
690
+
691
+ <div class="card">
692
+ <div class="card-header">
693
+ <div class="card-dot amber"></div>
694
+ <h3>Card Title 2</h3>
695
+ </div>
696
+ <ul>
697
+ <li>&bull; Item one</li>
698
+ <li>&bull; Item two</li>
699
+ <li>&bull; Item three</li>
700
+ <li>&bull; Item four</li>
701
+ </ul>
702
+ </div>
703
+
704
+ <div class="card">
705
+ <div class="card-header">
706
+ <div class="card-dot violet"></div>
707
+ <h3>Card Title 3</h3>
708
+ </div>
709
+ <ul>
710
+ <li>&bull; Item one</li>
711
+ <li>&bull; Item two</li>
712
+ <li>&bull; Item three</li>
713
+ <li>&bull; Item four</li>
714
+ </ul>
715
+ </div>
716
+ </div>
717
+ <!-- ARCHIFY:CARDS_SLOT_END -->
718
+
719
+ <!-- Footer -->
720
+ <p class="footer">
721
+ [Project Name] &bull; [Additional metadata]
722
+ </p>
723
+ </div>
724
+
725
+ <script>
726
+ /* ============================================================
727
+ Theme toggle - persists to localStorage, respects system pref
728
+ ============================================================ */
729
+ var TechnicalDiagram = {};
730
+
731
+ TechnicalDiagram.theme = (function () {
732
+ var STORAGE_KEY = 'technical-diagrams-theme';
733
+ var html = document.documentElement;
734
+ var btn = document.getElementById('btn-theme');
735
+ var icon = document.getElementById('theme-icon');
736
+ var label = document.getElementById('theme-label');
737
+
738
+ // localStorage can throw (blocked cookies, sandboxed iframes); the whole
739
+ // script element dies on an uncaught error, so guard every access.
740
+ function readStored() {
741
+ try { return localStorage.getItem(STORAGE_KEY); } catch (_) { return null; }
742
+ }
743
+ function writeStored(value) {
744
+ try { localStorage.setItem(STORAGE_KEY, value); } catch (_) {}
745
+ }
746
+ function urlOverride() {
747
+ // URL param wins (useful for deterministic screenshots / share links)
748
+ try {
749
+ var param = new URLSearchParams(window.location.search).get('theme');
750
+ if (param === 'light' || param === 'dark') return param;
751
+ } catch (_) {}
752
+ return null;
753
+ }
754
+
755
+ function resolveInitial() {
756
+ var fromUrl = urlOverride();
757
+ if (fromUrl) return fromUrl;
758
+ var saved = readStored();
759
+ if (saved === 'light' || saved === 'dark') return saved;
760
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
761
+ }
762
+
763
+ function apply(theme) {
764
+ html.setAttribute('data-theme', theme);
765
+ icon.textContent = theme === 'dark' ? '\u263E' : '\u263C';
766
+ label.textContent = theme === 'dark' ? 'Dark' : 'Light';
767
+ btn.setAttribute('aria-pressed', theme === 'light' ? 'true' : 'false');
768
+ }
769
+
770
+ function toggle() {
771
+ var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
772
+ apply(next);
773
+ writeStored(next);
774
+ }
775
+
776
+ apply(resolveInitial());
777
+ btn.addEventListener('click', toggle);
778
+
779
+ // Follow live OS theme changes while the user has no explicit preference.
780
+ try {
781
+ var media = window.matchMedia('(prefers-color-scheme: light)');
782
+ var onChange = function (e) {
783
+ var saved = readStored();
784
+ if (urlOverride() || saved === 'light' || saved === 'dark') return;
785
+ apply(e.matches ? 'light' : 'dark');
786
+ };
787
+ if (media.addEventListener) media.addEventListener('change', onChange);
788
+ else if (media.addListener) media.addListener(onChange);
789
+ } catch (_) {}
790
+
791
+ return { toggle: toggle };
792
+ })();
793
+
794
+ /* ============================================================
795
+ Export - PNG / JPEG / WebP / SVG (download) and Copy PNG (clipboard)
796
+
797
+ Raster exports are always rendered at 4x source resolution for
798
+ maximum sharpness. The trick: we set the serialized SVG's
799
+ `width`/`height` to viewBox * 4 so the browser rasterizes the
800
+ vectors at that resolution natively. drawImage then draws at
801
+ the image's natural size (no upscaling = no blur).
802
+
803
+ JPEG/WebP paint the current theme's background explicitly since
804
+ those formats have no alpha channel.
805
+ ============================================================ */
806
+ (function () {
807
+ var RASTER_SCALE = 4;
808
+
809
+ function diagramFilename() {
810
+ var title = (document.title || 'diagram')
811
+ .replace(/\s+Architecture(\s+Diagram)?$/i, '')
812
+ .replace(/\s+Diagram$/i, '')
813
+ .trim();
814
+ return title.replace(/[^a-z0-9_\-]+/gi, '-')
815
+ .toLowerCase()
816
+ .replace(/^-+|-+$/g, '') || 'diagram';
817
+ }
818
+
819
+ function currentBg() {
820
+ return getComputedStyle(document.body).backgroundColor || '#ffffff';
821
+ }
822
+
823
+ /**
824
+ * Serialize the main SVG into a standalone document.
825
+ *
826
+ * Two modes:
827
+ * - Default (opts.autoTheme=false) - locks the serialized SVG to the
828
+ * viewer's current theme. Used by the raster pipeline
829
+ * (PNG/JPEG/WebP/clipboard) because canvas rasterization needs
830
+ * deterministic colors; a raster cannot react to
831
+ * prefers-color-scheme after encoding.
832
+ * - autoTheme=true - emits BOTH dark and light variable sets plus a
833
+ * `@media (prefers-color-scheme)` rule so the resulting SVG
834
+ * self-themes when embedded in GitHub READMEs or other hosts that
835
+ * expose a color scheme. Used for "Download SVG".
836
+ */
837
+ function serializeSvg(scale, opts) {
838
+ // scale: integer multiplier for intrinsic SVG pixel dimensions used by
839
+ // the raster path. Defaults to 1 (natural size) for SVG download.
840
+ scale = scale || 1;
841
+ opts = opts || {};
842
+ var autoTheme = opts.autoTheme === true;
843
+
844
+ var svg = document.querySelector('.diagram-container svg');
845
+ var clone = svg.cloneNode(true);
846
+
847
+ var vb = svg.viewBox.baseVal;
848
+ // Scale width/height so the browser rasterizes the vectors at target
849
+ // resolution directly. viewBox stays unchanged so coordinates don't
850
+ // shift. This is the key to sharp rasters - do NOT scale later via
851
+ // canvas upscaling.
852
+ clone.setAttribute('width', vb.width * scale);
853
+ clone.setAttribute('height', vb.height * scale);
854
+ clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
855
+
856
+ // Only the SVG-relevant rules: semantic classes, markers, and the
857
+ // theme variable blocks. Toolbar/cards/print CSS can never apply
858
+ // inside a standalone SVG and would only bloat the export.
859
+ var hostStyle = (function () {
860
+ var out = [];
861
+ Array.prototype.forEach.call(document.styleSheets, function (sheet) {
862
+ var rules;
863
+ try { rules = sheet.cssRules; } catch (_) { return; } // cross-origin
864
+ if (!rules) return;
865
+ Array.prototype.forEach.call(rules, function (rule) {
866
+ if (rule.type !== 1) return; // plain style rules only
867
+ var sel = rule.selectorText || '';
868
+ if (/(^|,)\s*(svg|:root|\[data-theme|\.c-|\.t-|\.a-|\.m-)/.test(sel)) {
869
+ out.push(rule.cssText);
870
+ }
871
+ });
872
+ });
873
+ return out.join('\n');
874
+ })();
875
+
876
+ // Derive the variable list from the stylesheet so newly added theme
877
+ // variables can never be missed by the export pipeline again
878
+ // (--lane-fill/--lane-stroke once were).
879
+ var varNames = (function () {
880
+ var seen = {};
881
+ var names = [];
882
+ (hostStyle.match(/--[a-zA-Z0-9-]+(?=\s*:)/g) || []).forEach(function (n) {
883
+ if (!seen[n]) { seen[n] = true; names.push(n); }
884
+ });
885
+ return names;
886
+ })();
887
+
888
+ // Resolve the full variable set for a given data-theme via an
889
+ // off-DOM probe, independent of what the viewer is currently set to.
890
+ function resolveVars(themeAttr) {
891
+ var probe = document.createElement('div');
892
+ probe.setAttribute('data-theme', themeAttr);
893
+ probe.style.cssText = 'position:absolute;width:0;height:0;visibility:hidden;';
894
+ document.body.appendChild(probe);
895
+ try {
896
+ var c = getComputedStyle(probe);
897
+ return varNames.map(function (n) {
898
+ return n + ': ' + c.getPropertyValue(n).trim() + ';';
899
+ }).join(' ');
900
+ } finally {
901
+ document.body.removeChild(probe);
902
+ }
903
+ }
904
+
905
+ // Prepend a local()-only @font-face block for each weight so that the
906
+ // sandboxed image-rendering context used during PNG/JPEG/WebP export
907
+ // can find JetBrains Mono if the user has it installed locally, and
908
+ // falls through cleanly to the monospace stack otherwise. The usual
909
+ // Google-Fonts <link> in the host document is unreachable from the
910
+ // serialized SVG, so url()-sourced faces would just hang.
911
+ var fontFallback = [400, 500, 600, 700].map(function (w) {
912
+ return "@font-face { font-family: 'JetBrains Mono'; font-weight: " + w +
913
+ "; src: local('JetBrains Mono'), local('JetBrainsMono-Regular'); }";
914
+ }).join('\n');
915
+
916
+ var style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
917
+ var bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
918
+ bgRect.setAttribute('width', '100%');
919
+ bgRect.setAttribute('height', '100%');
920
+
921
+ if (autoTheme) {
922
+ // Dual-theme SVG. Dark is the default (so hosts without
923
+ // prefers-color-scheme still render), light swaps in via media
924
+ // query, and svg[data-theme="..."] still lets downstream
925
+ // consumers force a specific theme.
926
+ var darkVars = resolveVars('dark');
927
+ var lightVars = resolveVars('light');
928
+
929
+ style.textContent =
930
+ fontFallback + "\n" +
931
+ "svg { font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', 'Liberation Mono', 'Noto Sans Mono CJK SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', monospace; }\n" +
932
+ hostStyle + "\n" +
933
+ ":root, svg { " + darkVars + " }\n" +
934
+ "@media (prefers-color-scheme: light) { :root, svg { " + lightVars + " } }\n" +
935
+ "svg[data-theme=\"light\"] { " + lightVars + " }\n" +
936
+ "svg[data-theme=\"dark\"] { " + darkVars + " }\n" +
937
+ "rect.c-bg-rect { fill: var(--bg); }\n";
938
+
939
+ // Don't lock the serialized SVG to the viewer's current theme.
940
+ clone.removeAttribute('data-theme');
941
+ // Background follows the CSS variable, not a fixed color, so it
942
+ // swaps with the media query.
943
+ bgRect.setAttribute('class', 'c-bg-rect');
944
+ } else {
945
+ // Raster path: lock to the viewer's current theme.
946
+ var theme = document.documentElement.getAttribute('data-theme') || 'dark';
947
+ var themeHost = document.querySelector('[data-theme="' + theme + '"]') || document.documentElement;
948
+ var computed = getComputedStyle(themeHost);
949
+ var vars = varNames.map(function (n) {
950
+ return n + ': ' + computed.getPropertyValue(n).trim() + ';';
951
+ }).join(' ');
952
+
953
+ // IMPORTANT: inject the resolved variables AFTER hostStyle,
954
+ // otherwise hostStyle's ":root, [data-theme=\"dark\"] { ... }" rule
955
+ // overrides our chosen theme via later-in-cascade equal-specificity.
956
+ // Keep this order.
957
+ style.textContent =
958
+ fontFallback + "\n" +
959
+ "svg { font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', 'Liberation Mono', 'Noto Sans Mono CJK SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', monospace; }\n" +
960
+ hostStyle + "\n" +
961
+ ":root, svg { " + vars + " }\n";
962
+
963
+ bgRect.setAttribute('fill', computed.getPropertyValue('--bg').trim() || '#ffffff');
964
+ }
965
+
966
+ clone.insertBefore(style, clone.firstChild);
967
+ clone.insertBefore(bgRect, style.nextSibling);
968
+
969
+ return {
970
+ svgString: new XMLSerializer().serializeToString(clone),
971
+ width: vb.width * scale,
972
+ height: vb.height * scale
973
+ };
974
+ }
975
+
976
+ function download(blob, filename) {
977
+ var url = URL.createObjectURL(blob);
978
+ var a = document.createElement('a');
979
+ a.href = url;
980
+ a.download = filename;
981
+ document.body.appendChild(a);
982
+ a.click();
983
+ a.remove();
984
+ setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
985
+ }
986
+
987
+ // Conservative upper bound on total canvas pixels. Older Safari on iOS
988
+ // silently produces a blank canvas above ~16 Mpx; Chrome / Firefox /
989
+ // desktop Safari are far higher but start failing on memory-constrained
990
+ // devices. We pick the largest integer scale in {4,3,2,1} whose target
991
+ // pixel count fits under this cap.
992
+ var MAX_CANVAS_PIXELS = 16 * 1024 * 1024;
993
+
994
+ function pickSafeScale(vbW, vbH) {
995
+ for (var s = RASTER_SCALE; s >= 1; s--) {
996
+ if (vbW * s * vbH * s <= MAX_CANVAS_PIXELS) return s;
997
+ }
998
+ return 1;
999
+ }
1000
+
1001
+ function rasterize(format) {
1002
+ // Serialize at a safe scale (RASTER_SCALE=4 by default, reduced if the
1003
+ // resulting canvas would exceed MAX_CANVAS_PIXELS). The SVG itself is
1004
+ // rasterized at target resolution natively; drawImage draws at natural
1005
+ // size - no upsampling blur.
1006
+ var svg = document.querySelector('.diagram-container svg');
1007
+ var vb = svg.viewBox.baseVal;
1008
+ var scale = pickSafeScale(vb.width, vb.height);
1009
+ var data = serializeSvg(scale);
1010
+ var svgBlob = new Blob([data.svgString], { type: 'image/svg+xml;charset=utf-8' });
1011
+ var svgUrl = URL.createObjectURL(svgBlob);
1012
+
1013
+ return new Promise(function (resolve, reject) {
1014
+ var img = new Image();
1015
+ img.onload = function () {
1016
+ var canvas = document.createElement('canvas');
1017
+ canvas.width = data.width;
1018
+ canvas.height = data.height;
1019
+ var ctx = canvas.getContext('2d');
1020
+ if (format === 'jpeg') {
1021
+ ctx.fillStyle = currentBg();
1022
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1023
+ }
1024
+ // Draw at natural size - SVG was already rasterized at target res.
1025
+ ctx.drawImage(img, 0, 0);
1026
+ URL.revokeObjectURL(svgUrl);
1027
+ var mime = format === 'jpeg' ? 'image/jpeg' :
1028
+ format === 'webp' ? 'image/webp' : 'image/png';
1029
+ var quality = format === 'png' ? undefined : 0.95;
1030
+ canvas.toBlob(function (blob) {
1031
+ if (!blob) reject(new Error('toBlob returned null for ' + format));
1032
+ else resolve(blob);
1033
+ }, mime, quality);
1034
+ };
1035
+ img.onerror = function (e) {
1036
+ URL.revokeObjectURL(svgUrl);
1037
+ reject(e);
1038
+ };
1039
+ img.src = svgUrl;
1040
+ });
1041
+ }
1042
+
1043
+ var menu = document.getElementById('export-menu');
1044
+ var btn = document.getElementById('btn-export');
1045
+ var items = function () {
1046
+ return Array.prototype.slice.call(menu.querySelectorAll('button[role="menuitem"]'));
1047
+ };
1048
+
1049
+ // ---- Clipboard support ----------------------------------------------
1050
+ function canCopyImage() {
1051
+ return typeof ClipboardItem !== 'undefined' &&
1052
+ navigator.clipboard &&
1053
+ typeof navigator.clipboard.write === 'function';
1054
+ }
1055
+
1056
+ // ---- Raster format detection ----------------------------------------
1057
+ // canvas.toBlob('image/webp') silently returns a PNG on browsers without
1058
+ // WebP encoding (older Safari), so detect explicitly.
1059
+ function supports(format) {
1060
+ if (format === 'svg' || format === 'png') return true;
1061
+ var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp';
1062
+ try {
1063
+ var c = document.createElement('canvas');
1064
+ c.width = c.height = 2;
1065
+ return c.toDataURL(mime).indexOf('data:' + mime) === 0;
1066
+ } catch (_) { return false; }
1067
+ }
1068
+
1069
+ // Gray out unsupported items.
1070
+ items().forEach(function (it) {
1071
+ if (it.dataset.format && !supports(it.dataset.format)) {
1072
+ it.disabled = true;
1073
+ it.title = 'Not supported by this browser';
1074
+ it.style.opacity = '0.5';
1075
+ }
1076
+ if (it.dataset.action === 'copy' && !canCopyImage()) {
1077
+ it.disabled = true;
1078
+ it.title = 'Clipboard image write not supported by this browser';
1079
+ it.style.opacity = '0.5';
1080
+ }
1081
+ });
1082
+
1083
+ // ---- Toast ----------------------------------------------------------
1084
+ // A live region only announces CHANGES to an existing node, so the toast
1085
+ // element is created once up front and updated in place.
1086
+ var toastEl = document.createElement('div');
1087
+ toastEl.className = 'technical-diagram-toast';
1088
+ toastEl.setAttribute('role', 'status');
1089
+ document.body.appendChild(toastEl);
1090
+ var toastTimer = null;
1091
+
1092
+ function toast(msg) {
1093
+ toastEl.textContent = '';
1094
+ requestAnimationFrame(function () {
1095
+ toastEl.textContent = msg;
1096
+ toastEl.classList.add('show');
1097
+ });
1098
+ clearTimeout(toastTimer);
1099
+ toastTimer = setTimeout(function () { toastEl.classList.remove('show'); }, 1500);
1100
+ }
1101
+
1102
+ function open(focusLast) {
1103
+ menu.classList.add('open');
1104
+ btn.setAttribute('aria-expanded', 'true');
1105
+ var available = items().filter(function (i) { return !i.disabled; });
1106
+ var target = focusLast ? available[available.length - 1] : available[0];
1107
+ if (target) target.focus();
1108
+ }
1109
+ function close(focusTrigger) {
1110
+ menu.classList.remove('open');
1111
+ btn.setAttribute('aria-expanded', 'false');
1112
+ if (focusTrigger) btn.focus();
1113
+ }
1114
+ function isOpen() { return menu.classList.contains('open'); }
1115
+
1116
+ btn.addEventListener('click', function (e) {
1117
+ e.stopPropagation();
1118
+ if (isOpen()) close(false);
1119
+ else open();
1120
+ });
1121
+ // APG menu-button pattern: ArrowDown/ArrowUp on the trigger opens the menu.
1122
+ btn.addEventListener('keydown', function (e) {
1123
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
1124
+ e.preventDefault();
1125
+ if (!isOpen()) open(e.key === 'ArrowUp');
1126
+ }
1127
+ });
1128
+ document.addEventListener('click', function (e) {
1129
+ if (!menu.contains(e.target) && e.target !== btn) close(false);
1130
+ });
1131
+
1132
+ // Keyboard navigation inside the menu.
1133
+ // Menu items: Up/Down, Home/End.
1134
+ // Esc closes + returns focus to trigger; Tab closes.
1135
+ menu.addEventListener('keydown', function (e) {
1136
+ if (e.key === 'Escape') { e.preventDefault(); close(true); return; }
1137
+ if (e.key === 'Tab') { close(false); return; }
1138
+
1139
+ var available = items().filter(function (i) { return !i.disabled; });
1140
+ var current = available.indexOf(document.activeElement);
1141
+ switch (e.key) {
1142
+ case 'ArrowDown':
1143
+ e.preventDefault();
1144
+ if (available.length) available[(current + 1 + available.length) % available.length].focus();
1145
+ break;
1146
+ case 'ArrowUp':
1147
+ e.preventDefault();
1148
+ if (available.length) available[(current - 1 + available.length) % available.length].focus();
1149
+ break;
1150
+ case 'Home':
1151
+ e.preventDefault();
1152
+ if (available[0]) available[0].focus();
1153
+ break;
1154
+ case 'End':
1155
+ e.preventDefault();
1156
+ if (available.length) available[available.length - 1].focus();
1157
+ break;
1158
+ }
1159
+ });
1160
+
1161
+ function runExport(format) {
1162
+ var base = diagramFilename();
1163
+ close(true);
1164
+ return (format === 'svg'
1165
+ ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) {
1166
+ download(new Blob([d.svgString], { type: 'image/svg+xml;charset=utf-8' }), base + '.svg');
1167
+ })
1168
+ : rasterize(format).then(function (blob) {
1169
+ download(blob, base + '.' + format);
1170
+ })
1171
+ ).catch(function (err) {
1172
+ console.error(err);
1173
+ alert('Export failed: ' + (err && err.message ? err.message : format));
1174
+ });
1175
+ }
1176
+
1177
+ function runCopy() {
1178
+ close(true);
1179
+ if (!canCopyImage()) {
1180
+ alert('Clipboard image write not supported in this browser.');
1181
+ return;
1182
+ }
1183
+ // WebKit requires ClipboardItem to be constructed synchronously inside
1184
+ // the user gesture - pass the pending Promise<Blob> as the item value.
1185
+ // Browsers that reject promise values throw synchronously; fall back
1186
+ // to awaiting the blob first (works in Chromium/Firefox).
1187
+ var blobPromise = rasterize('png');
1188
+ var writePromise;
1189
+ try {
1190
+ writePromise = navigator.clipboard.write([new ClipboardItem({ 'image/png': blobPromise })]);
1191
+ } catch (_) {
1192
+ writePromise = blobPromise.then(function (blob) {
1193
+ return navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
1194
+ });
1195
+ }
1196
+ return writePromise.then(function () {
1197
+ toast('Copied PNG to clipboard');
1198
+ }).catch(function (err) {
1199
+ console.error(err);
1200
+ alert('Copy failed: ' + (err && err.message ? err.message : 'unknown'));
1201
+ });
1202
+ }
1203
+
1204
+ menu.addEventListener('click', function (e) {
1205
+ var copyBtn = e.target.closest('button[data-action="copy"]');
1206
+ if (copyBtn && !copyBtn.disabled) { runCopy(); return; }
1207
+
1208
+ var formatBtn = e.target.closest('button[data-format]');
1209
+ if (formatBtn && !formatBtn.disabled) { runExport(formatBtn.dataset.format); }
1210
+ });
1211
+
1212
+ TechnicalDiagram.exportMenu = { open: open, close: close, isOpen: isOpen, run: runExport };
1213
+
1214
+ // Auto-open on page load for demo/screenshot purposes: ?openExport=1
1215
+ // Wait for fonts (so the menu doesn't flash before typography lands)
1216
+ // and paint before opening. Fallback timeout for browsers without the
1217
+ // Font Loading API.
1218
+ try {
1219
+ if (new URLSearchParams(window.location.search).get('openExport') === '1') {
1220
+ var openWhenReady = function () {
1221
+ requestAnimationFrame(function () { requestAnimationFrame(open); });
1222
+ };
1223
+ if (document.fonts && document.fonts.ready) {
1224
+ document.fonts.ready.then(openWhenReady);
1225
+ } else {
1226
+ setTimeout(openWhenReady, 200);
1227
+ }
1228
+ }
1229
+ } catch (_) {}
1230
+ })();
1231
+
1232
+ /* ============================================================
1233
+ Global keyboard shortcuts
1234
+ T -> toggle theme
1235
+ E -> open export menu
1236
+ Ignored when focus is inside an input/textarea/contenteditable.
1237
+ ============================================================ */
1238
+ document.addEventListener('keydown', function (e) {
1239
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
1240
+ var t = e.target;
1241
+ if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
1242
+ if (e.key === 't' || e.key === 'T') {
1243
+ e.preventDefault();
1244
+ TechnicalDiagram.theme.toggle();
1245
+ } else if (e.key === 'e' || e.key === 'E') {
1246
+ e.preventDefault();
1247
+ if (!TechnicalDiagram.exportMenu.isOpen()) TechnicalDiagram.exportMenu.open();
1248
+ }
1249
+ });
1250
+ </script>
1251
+ </body>
1252
+ </html>