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