@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.
- package/TODO.md +21 -0
- package/ai/AGENTS.md +79 -0
- package/ai/README.md +45 -0
- package/ai/claude/launch.json +12 -0
- package/ai/skills/stacks-actions/SKILL.md +128 -0
- package/ai/skills/stacks-ai/SKILL.md +276 -0
- package/ai/skills/stacks-alias/SKILL.md +94 -0
- package/ai/skills/stacks-analytics/SKILL.md +91 -0
- package/ai/skills/stacks-api/SKILL.md +738 -0
- package/ai/skills/stacks-arrays/SKILL.md +277 -0
- package/ai/skills/stacks-auth/SKILL.md +422 -0
- package/ai/skills/stacks-auto-imports/SKILL.md +138 -0
- package/ai/skills/stacks-brandkit/SKILL.md +811 -0
- package/ai/skills/stacks-browse/SKILL.md +127 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +492 -0
- package/ai/skills/stacks-browser/SKILL.md +145 -0
- package/ai/skills/stacks-buddy/SKILL.md +676 -0
- package/ai/skills/stacks-build/SKILL.md +163 -0
- package/ai/skills/stacks-cache/SKILL.md +125 -0
- package/ai/skills/stacks-calendar/SKILL.md +113 -0
- package/ai/skills/stacks-chat/SKILL.md +371 -0
- package/ai/skills/stacks-cli/SKILL.md +112 -0
- package/ai/skills/stacks-cloud/SKILL.md +192 -0
- package/ai/skills/stacks-cms/SKILL.md +217 -0
- package/ai/skills/stacks-collections/SKILL.md +82 -0
- package/ai/skills/stacks-commerce/SKILL.md +114 -0
- package/ai/skills/stacks-composables/SKILL.md +161 -0
- package/ai/skills/stacks-config/SKILL.md +144 -0
- package/ai/skills/stacks-configuration/SKILL.md +154 -0
- package/ai/skills/stacks-cron/SKILL.md +123 -0
- package/ai/skills/stacks-crosswind/SKILL.md +190 -0
- package/ai/skills/stacks-dashboard/SKILL.md +173 -0
- package/ai/skills/stacks-database/SKILL.md +286 -0
- package/ai/skills/stacks-datetime/SKILL.md +269 -0
- package/ai/skills/stacks-dependencies/SKILL.md +111 -0
- package/ai/skills/stacks-deploy/SKILL.md +102 -0
- package/ai/skills/stacks-design-brutalist/SKILL.md +110 -0
- package/ai/skills/stacks-design-minimalist/SKILL.md +115 -0
- package/ai/skills/stacks-design-output/SKILL.md +55 -0
- package/ai/skills/stacks-design-soft/SKILL.md +156 -0
- package/ai/skills/stacks-design-taste/SKILL.md +1012 -0
- package/ai/skills/stacks-design-taste-codex/SKILL.md +294 -0
- package/ai/skills/stacks-desktop/SKILL.md +78 -0
- package/ai/skills/stacks-development/SKILL.md +298 -0
- package/ai/skills/stacks-dns/SKILL.md +149 -0
- package/ai/skills/stacks-docs/SKILL.md +109 -0
- package/ai/skills/stacks-email/SKILL.md +236 -0
- package/ai/skills/stacks-enums/SKILL.md +100 -0
- package/ai/skills/stacks-env/SKILL.md +181 -0
- package/ai/skills/stacks-error-handling/SKILL.md +137 -0
- package/ai/skills/stacks-events/SKILL.md +228 -0
- package/ai/skills/stacks-faker/SKILL.md +158 -0
- package/ai/skills/stacks-git/SKILL.md +158 -0
- package/ai/skills/stacks-guard/SKILL.md +98 -0
- package/ai/skills/stacks-health/SKILL.md +75 -0
- package/ai/skills/stacks-http/SKILL.md +316 -0
- package/ai/skills/stacks-i18n/SKILL.md +198 -0
- package/ai/skills/stacks-image-to-code/SKILL.md +629 -0
- package/ai/skills/stacks-imagegen-mobile/SKILL.md +1474 -0
- package/ai/skills/stacks-imagegen-web/SKILL.md +998 -0
- package/ai/skills/stacks-investigate/SKILL.md +119 -0
- package/ai/skills/stacks-jobs/SKILL.md +136 -0
- package/ai/skills/stacks-lint/SKILL.md +184 -0
- package/ai/skills/stacks-listeners/SKILL.md +99 -0
- package/ai/skills/stacks-logging/SKILL.md +78 -0
- package/ai/skills/stacks-mail/SKILL.md +120 -0
- package/ai/skills/stacks-middleware/SKILL.md +249 -0
- package/ai/skills/stacks-migrations/SKILL.md +105 -0
- package/ai/skills/stacks-models/SKILL.md +267 -0
- package/ai/skills/stacks-new-feature/SKILL.md +206 -0
- package/ai/skills/stacks-notifications/SKILL.md +210 -0
- package/ai/skills/stacks-objects/SKILL.md +205 -0
- package/ai/skills/stacks-office-hours/SKILL.md +134 -0
- package/ai/skills/stacks-orm/SKILL.md +342 -0
- package/ai/skills/stacks-path/SKILL.md +118 -0
- package/ai/skills/stacks-payments/SKILL.md +373 -0
- package/ai/skills/stacks-plan-review/SKILL.md +152 -0
- package/ai/skills/stacks-plugins/SKILL.md +149 -0
- package/ai/skills/stacks-push/SKILL.md +376 -0
- package/ai/skills/stacks-query-builder/SKILL.md +128 -0
- package/ai/skills/stacks-queue/SKILL.md +559 -0
- package/ai/skills/stacks-realtime/SKILL.md +442 -0
- package/ai/skills/stacks-redesign/SKILL.md +182 -0
- package/ai/skills/stacks-registry/SKILL.md +58 -0
- package/ai/skills/stacks-repl/SKILL.md +80 -0
- package/ai/skills/stacks-retro/SKILL.md +122 -0
- package/ai/skills/stacks-review/SKILL.md +135 -0
- package/ai/skills/stacks-router/SKILL.md +231 -0
- package/ai/skills/stacks-routes/SKILL.md +105 -0
- package/ai/skills/stacks-scaffolding/SKILL.md +162 -0
- package/ai/skills/stacks-scheduler/SKILL.md +363 -0
- package/ai/skills/stacks-search-engine/SKILL.md +112 -0
- package/ai/skills/stacks-security/SKILL.md +129 -0
- package/ai/skills/stacks-security-audit/SKILL.md +128 -0
- package/ai/skills/stacks-server/SKILL.md +458 -0
- package/ai/skills/stacks-shell/SKILL.md +62 -0
- package/ai/skills/stacks-slug/SKILL.md +87 -0
- package/ai/skills/stacks-sms/SKILL.md +385 -0
- package/ai/skills/stacks-socials/SKILL.md +303 -0
- package/ai/skills/stacks-storage/SKILL.md +576 -0
- package/ai/skills/stacks-strings/SKILL.md +416 -0
- package/ai/skills/stacks-stx/SKILL.md +176 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +22 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +290 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +4 -0
- package/ai/skills/stacks-technical-diagrams/assets/template.html +1252 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +289 -0
- package/ai/skills/stacks-technical-diagrams/bunfig.toml +1 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +64 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +88 -0
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +75 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +70 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +41 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +62 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +371 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +295 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +91 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +384 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +84 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +257 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +13 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +230 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +88 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +103 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +481 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +68 -0
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +121 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +43 -0
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +221 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +241 -0
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +186 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +332 -0
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +297 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +28 -0
- package/ai/skills/stacks-testing/SKILL.md +129 -0
- package/ai/skills/stacks-tunnel/SKILL.md +125 -0
- package/ai/skills/stacks-types/SKILL.md +178 -0
- package/ai/skills/stacks-ui/SKILL.md +246 -0
- package/ai/skills/stacks-utils/SKILL.md +444 -0
- package/ai/skills/stacks-validation/SKILL.md +371 -0
- package/ai/skills/stacks-whois/SKILL.md +315 -0
- package/assets/world-map-data.ts +121 -0
- package/bootstrap.ts +114 -0
- package/docs/api-examples.md +46 -0
- package/docs/index.md +24 -0
- package/docs/markdown-examples.md +87 -0
- package/functions/analytics.ts +97 -0
- package/functions/api.ts +143 -0
- package/functions/auth.ts +208 -0
- package/functions/billing/payments.ts +149 -0
- package/functions/blog.ts +120 -0
- package/functions/cms/categorizables.ts +107 -0
- package/functions/cms/pages.ts +108 -0
- package/functions/cms/posts.ts +100 -0
- package/functions/cms/taggables.ts +102 -0
- package/functions/commerce/coupons.ts +128 -0
- package/functions/commerce/customers.ts +128 -0
- package/functions/commerce/gift-cards.ts +119 -0
- package/functions/commerce/orders.ts +112 -0
- package/functions/commerce/payments.ts +119 -0
- package/functions/commerce/products/categories.ts +123 -0
- package/functions/commerce/products/manufacturers.ts +123 -0
- package/functions/commerce/products/products.ts +123 -0
- package/functions/commerce/products/reviews.ts +123 -0
- package/functions/commerce/products/units.ts +123 -0
- package/functions/commerce/products/variants.ts +123 -0
- package/functions/commerce/shippings/delivery-routes.ts +123 -0
- package/functions/commerce/shippings/digital-deliveries.ts +119 -0
- package/functions/commerce/shippings/drivers.ts +123 -0
- package/functions/commerce/shippings/license-keys.ts +123 -0
- package/functions/commerce/shippings/shipping-methods.ts +123 -0
- package/functions/commerce/shippings/shipping-rates.ts +119 -0
- package/functions/commerce/shippings/shipping-zones.ts +119 -0
- package/functions/commerce/taxes.ts +119 -0
- package/functions/commerce/waitlist/products.ts +123 -0
- package/functions/commerce/waitlist/restaurants.ts +123 -0
- package/functions/craft.ts +107 -0
- package/functions/dashboard.ts +153 -0
- package/functions/inbox.ts +95 -0
- package/functions/jobs.ts +86 -0
- package/functions/monitoring/errors.ts +295 -0
- package/functions/queue.ts +111 -0
- package/functions/realtime/websockets.ts +96 -0
- package/functions/realtime.ts +52 -0
- package/functions/releases.ts +49 -0
- package/functions/search.ts +126 -0
- package/functions/toasts.ts +95 -0
- package/functions/uploads.ts +216 -0
- package/ide/.gitignore +4 -0
- package/ide/cursor/rules/auto-imports.mdc +574 -0
- package/ide/cursor/rules/code-style.mdc +12 -0
- package/ide/cursor/rules/documentation.mdc +9 -0
- package/ide/cursor/rules/error-handling-validation.mdc +11 -0
- package/ide/cursor/rules/key-conventions.mdc +11 -0
- package/ide/cursor/rules/project-structure.mdc +169 -0
- package/ide/cursor/rules/readme.mdc +318 -0
- package/ide/cursor/rules/syntax-formatting.mdc +9 -0
- package/ide/cursor/rules/testing.mdc +10 -0
- package/ide/cursor/rules/typescript.mdc +9 -0
- package/ide/cursor/rules/ui-styling.mdc +0 -0
- package/ide/dictionary.txt +397 -0
- package/ide/jetbrains/.fleet/settings.json +4 -0
- package/ide/jetbrains/.idea/.gitignore +5 -0
- package/ide/jetbrains/.idea/.name +1 -0
- package/ide/jetbrains/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/ide/jetbrains/.idea/jsLinters/eslint.xml +7 -0
- package/ide/jetbrains/.idea/modules.xml +8 -0
- package/ide/jetbrains/.idea/stacks.iml +22 -0
- package/ide/jetbrains/.idea/vcs.xml +6 -0
- package/ide/jetbrains/.idea/webResources.xml +14 -0
- package/ide/jetbrains/IntelliJ IDEA Global Settings +0 -0
- package/ide/jetbrains/installed.txt +4 -0
- package/ide/jetbrains/options/github-copilot.xml +5 -0
- package/ide/jetbrains/options/nodejs.xml +7 -0
- package/ide/jetbrains/options/templates.xml +124 -0
- package/ide/jetbrains/templates/JavaScript.xml +6 -0
- package/ide/jetbrains/templates/Stacks.xml +8 -0
- package/ide/vscode/.vscode/README.md +23 -0
- package/ide/vscode/.vscode/extensions.json +5 -0
- package/ide/vscode/.vscode/settings.json +176 -0
- package/ide/vscode/.vscode/stacks.code-snippets +181 -0
- package/ide/vscode/LICENSE.md +21 -0
- package/ide/vscode/README.md +36 -0
- package/ide/vscode/logo.png +0 -0
- package/ide/vscode/package.json +79 -0
- package/ide/zed/.zed/settings.json +139 -0
- package/lang/README.md +28 -0
- package/lang/de.yml +14 -0
- package/lang/en.yml +15 -0
- package/package.json +19 -3
- package/project/bootstrap +161 -0
- package/project/buddy +175 -0
- package/project/storage/framework/tsconfig.app.json +50 -0
- package/project/storage/framework/tsconfig.base.json +58 -0
- package/routes/core.ts +78 -0
- package/routes/dashboard-api.ts +233 -0
- package/routes/dashboard.ts +739 -0
- package/routes/email.ts +129 -0
- package/stores/[name].stx +16 -0
- package/stores/git.ts +71 -0
- package/stores/payment.ts +474 -0
- package/stores/queue.ts +43 -0
- package/stores/user.ts +29 -0
- package/stx-components-plugin.ts +25 -0
- package/styles/design-tokens.css +597 -0
- package/types/dashboard.ts +70 -0
- package/types/defaults.ts +438 -0
- package/vcs/github/CODE_OF_CONDUCT.md +83 -0
- package/vcs/github/CONTRIBUTING.md +187 -0
- package/vcs/github/EXPLANATIONS.md +41 -0
- package/vcs/github/FUNDING.yml +1 -0
- package/vcs/github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/vcs/github/ISSUE_TEMPLATE/config.yml +10 -0
- package/vcs/github/ISSUE_TEMPLATE/feature_request.yml +44 -0
- package/vcs/github/SECURITY.md +70 -0
- package/vcs/github/renovate.json +5 -0
- package/vcs/github/stale.yml +14 -0
- package/vcs/github/workflows/README.md +16 -0
- package/vcs/github/workflows/ci.yml +114 -0
- package/vcs/github/workflows/export-size.yml +25 -0
- package/vcs/github/workflows/labeler.yml +13 -0
- package/vcs/github/workflows/release.yml +219 -0
- package/views/[...all].stx +8 -0
- package/views/about.md +3 -0
- package/views/auth/login.stx +1 -0
- package/views/auth/signup.stx +85 -0
- package/views/dashboard/.discovered-models.json +542 -0
- package/views/dashboard/AUDIT.md +904 -0
- package/views/dashboard/[...all].stx +89 -0
- package/views/dashboard/access-tokens/index.stx +177 -0
- package/views/dashboard/actions/index.stx +129 -0
- package/views/dashboard/analytics/blog/index.stx +356 -0
- package/views/dashboard/analytics/browsers/index.stx +263 -0
- package/views/dashboard/analytics/commerce/sales/index.stx +329 -0
- package/views/dashboard/analytics/commerce/web/index.stx +379 -0
- package/views/dashboard/analytics/countries/index.stx +289 -0
- package/views/dashboard/analytics/devices/index.stx +305 -0
- package/views/dashboard/analytics/events/index.stx +536 -0
- package/views/dashboard/analytics/index.stx +88 -0
- package/views/dashboard/analytics/marketing/index.stx +315 -0
- package/views/dashboard/analytics/pages/index.stx +271 -0
- package/views/dashboard/analytics/referrers/index.stx +319 -0
- package/views/dashboard/analytics/web/index.stx +356 -0
- package/views/dashboard/buddy/index.stx +198 -0
- package/views/dashboard/ci/index.stx +462 -0
- package/views/dashboard/cloud/index.stx +452 -0
- package/views/dashboard/commands/index.stx +295 -0
- package/views/dashboard/commerce/categories/index.stx +406 -0
- package/views/dashboard/commerce/coupons/index.stx +485 -0
- package/views/dashboard/commerce/customers/index.stx +410 -0
- package/views/dashboard/commerce/dashboard/index.stx +287 -0
- package/views/dashboard/commerce/delivery/delivery-routes.stx +305 -0
- package/views/dashboard/commerce/delivery/digital-delivery.stx +332 -0
- package/views/dashboard/commerce/delivery/drivers.stx +397 -0
- package/views/dashboard/commerce/delivery/index.stx +18 -0
- package/views/dashboard/commerce/delivery/license-keys.stx +380 -0
- package/views/dashboard/commerce/delivery/shipping-methods.stx +346 -0
- package/views/dashboard/commerce/delivery/shipping-rates.stx +338 -0
- package/views/dashboard/commerce/delivery/shipping-zones.stx +302 -0
- package/views/dashboard/commerce/delivery/tabs.ts +25 -0
- package/views/dashboard/commerce/gift-cards/index.stx +561 -0
- package/views/dashboard/commerce/manufacturers/index.stx +308 -0
- package/views/dashboard/commerce/orders/index.stx +247 -0
- package/views/dashboard/commerce/payments/index.stx +535 -0
- package/views/dashboard/commerce/pos/index.stx +653 -0
- package/views/dashboard/commerce/printers/devices/index.stx +273 -0
- package/views/dashboard/commerce/printers/receipts/index.stx +308 -0
- package/views/dashboard/commerce/products/detail.stx +563 -0
- package/views/dashboard/commerce/products/index.stx +370 -0
- package/views/dashboard/commerce/reviews/index.stx +656 -0
- package/views/dashboard/commerce/reviews/index.ts +0 -0
- package/views/dashboard/commerce/taxes/index.stx +377 -0
- package/views/dashboard/commerce/units/index.stx +317 -0
- package/views/dashboard/commerce/variants/index.stx +319 -0
- package/views/dashboard/commerce/waitlist/products.stx +942 -0
- package/views/dashboard/commerce/waitlist/restaurant.stx +1024 -0
- package/views/dashboard/components/index.stx +330 -0
- package/views/dashboard/composables/index.ts +19 -0
- package/views/dashboard/composables/useChart.ts +77 -0
- package/views/dashboard/composables/useNavigation.ts +99 -0
- package/views/dashboard/composables/useRole.ts +58 -0
- package/views/dashboard/composables/useTheme.ts +74 -0
- package/views/dashboard/content/authors/index.stx +171 -0
- package/views/dashboard/content/blog/index.stx +493 -0
- package/views/dashboard/content/categories/index.stx +169 -0
- package/views/dashboard/content/comments/index.stx +189 -0
- package/views/dashboard/content/dashboard.stx +383 -0
- package/views/dashboard/content/files/index.stx +706 -0
- package/views/dashboard/content/pages/index.stx +174 -0
- package/views/dashboard/content/posts/index.stx +269 -0
- package/views/dashboard/content/seo/index.stx +240 -0
- package/views/dashboard/content/tags/index.stx +167 -0
- package/views/dashboard/data/activity/index.stx +261 -0
- package/views/dashboard/data/dashboard/index.stx +116 -0
- package/views/dashboard/data/subscribers.stx +270 -0
- package/views/dashboard/data/teams.stx +275 -0
- package/views/dashboard/data/users.stx +279 -0
- package/views/dashboard/dependencies/index.stx +178 -0
- package/views/dashboard/deployments/[id].stx +234 -0
- package/views/dashboard/deployments/index.stx +9 -0
- package/views/dashboard/dns/index.stx +163 -0
- package/views/dashboard/environment/index.stx +283 -0
- package/views/dashboard/errors/index.stx +274 -0
- package/views/dashboard/forgot-password.stx +57 -0
- package/views/dashboard/functions/index.stx +159 -0
- package/views/dashboard/health/index.stx +248 -0
- package/views/dashboard/inbox/activity.stx +350 -0
- package/views/dashboard/inbox/index.stx +452 -0
- package/views/dashboard/inbox/settings.stx +290 -0
- package/views/dashboard/index.stx +252 -0
- package/views/dashboard/insights/index.stx +529 -0
- package/views/dashboard/jobs/[id].stx +430 -0
- package/views/dashboard/jobs/history.stx +222 -0
- package/views/dashboard/jobs/index.stx +214 -0
- package/views/dashboard/kanban/[id].stx +899 -0
- package/views/dashboard/kanban/index.stx +260 -0
- package/views/dashboard/layouts/default.stx +565 -0
- package/views/dashboard/layouts/guest.stx +10 -0
- package/views/dashboard/login.stx +48 -0
- package/views/dashboard/logs/index.stx +540 -0
- package/views/dashboard/mailboxes/index.stx +149 -0
- package/views/dashboard/management/permissions/index.stx +490 -0
- package/views/dashboard/marketing/campaigns/index.stx +604 -0
- package/views/dashboard/marketing/lists/index.stx +514 -0
- package/views/dashboard/marketing/reviews/index.stx +475 -0
- package/views/dashboard/marketing/social-posts/index.stx +560 -0
- package/views/dashboard/models/[model].stx +195 -0
- package/views/dashboard/models/index.stx +159 -0
- package/views/dashboard/monitoring/errors/index.stx +415 -0
- package/views/dashboard/notifications/dashboard.stx +383 -0
- package/views/dashboard/notifications/email.stx +336 -0
- package/views/dashboard/notifications/history.stx +373 -0
- package/views/dashboard/notifications/sms.stx +332 -0
- package/views/dashboard/packages/index.stx +195 -0
- package/views/dashboard/queries/[id].stx +381 -0
- package/views/dashboard/queries/history.stx +227 -0
- package/views/dashboard/queries/index.stx +335 -0
- package/views/dashboard/queries/slow.stx +318 -0
- package/views/dashboard/queue/index.stx +427 -0
- package/views/dashboard/realtime/index.stx +429 -0
- package/views/dashboard/register.stx +46 -0
- package/views/dashboard/releases/index.stx +389 -0
- package/views/dashboard/requests/index.stx +462 -0
- package/views/dashboard/serverless/index.stx +475 -0
- package/views/dashboard/servers/[id].stx +299 -0
- package/views/dashboard/servers/index.stx +483 -0
- package/views/dashboard/settings/billing.stx +50 -0
- package/views/dashboard/settings/index.stx +256 -0
- package/views/dashboard/settings/mail.stx +252 -0
- package/views/dashboard/stores/auth.ts +123 -0
- package/views/dashboard/stores/ci.ts +342 -0
- package/views/dashboard/stores/index.ts +129 -0
- package/views/dashboard/stores/kanban.ts +723 -0
- package/views/dashboard/stores/rbac.ts +350 -0
- package/views/dashboard/stx.config.ts +34 -0
- package/views/dashboard/teams/[id].stx +170 -0
- package/views/forum/category.stx +92 -0
- package/views/forum/index.stx +209 -0
- package/views/forum/new-topic.stx +113 -0
- package/views/forum/profile.stx +1 -0
- package/views/forum/topic.stx +143 -0
- package/views/hello-world/[name].stx +47 -0
- package/views/index.stx +21 -0
- 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">☾</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">▾</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">• bucket-one</text>
|
|
585
|
+
<text x="255" y="434" class="t-muted" font-size="8" text-anchor="middle">• bucket-two</text>
|
|
586
|
+
<text x="255" y="448" class="t-muted" font-size="8" text-anchor="middle">• 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>• Item one</li>
|
|
685
|
+
<li>• Item two</li>
|
|
686
|
+
<li>• Item three</li>
|
|
687
|
+
<li>• 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>• Item one</li>
|
|
698
|
+
<li>• Item two</li>
|
|
699
|
+
<li>• Item three</li>
|
|
700
|
+
<li>• 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>• Item one</li>
|
|
711
|
+
<li>• Item two</li>
|
|
712
|
+
<li>• Item three</li>
|
|
713
|
+
<li>• Item four</li>
|
|
714
|
+
</ul>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
<!-- ARCHIFY:CARDS_SLOT_END -->
|
|
718
|
+
|
|
719
|
+
<!-- Footer -->
|
|
720
|
+
<p class="footer">
|
|
721
|
+
[Project Name] • [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>
|