@maestro-js/agent-skills 1.0.0-alpha.2 → 1.0.0-alpha.21
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/dist/bin.js +19 -0
- package/package.json +8 -7
- package/{dist → skills}/choosing-an-npm-package/SKILL.md +5 -4
- package/skills/iconify/SKILL.md +82 -0
- package/skills/iconify/scripts/iconify.js +111 -0
- package/skills/iso-fns/SKILL.md +137 -0
- package/skills/iso-fns/references/api_reference.md +435 -0
- package/skills/maestro-architecture/SKILL.md +552 -0
- package/skills/maestro-examples/SKILL.md +140 -0
- package/skills/maestro-examples/references/cadence/cadence-web/.auth/user.json +15 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/app.css +112 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/auth-layout.tsx +39 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/back-link.tsx +24 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/button-group.tsx +78 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/button-link.tsx +77 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/button.tsx +139 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/checkbox.tsx +210 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/chip.tsx +161 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/container.tsx +85 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/date-range.tsx +141 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/date-time-picker.tsx +199 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/empty-state.tsx +19 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/form-page.tsx +41 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/form.tsx +106 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/animated-popover.tsx +27 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/button-context.ts +13 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/calendar-month-year-picker.tsx +283 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/form-field.tsx +231 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/get-button-classes.ts +141 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/helpers/headless-button.tsx +39 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/icon.tsx +29 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/inline-alert.tsx +57 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/interaction-form.tsx +87 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/menu.tsx +189 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/optional-link.tsx +18 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/page-header.tsx +19 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/rich-text-editor.tsx +352 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/select.tsx +219 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/spinner.tsx +19 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/headless-templated-row-table.ts +553 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/index.tsx +44 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/server-table.ts +146 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-actions.tsx +79 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-container.tsx +176 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-container.type-test.ts +158 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-filters/date-range-filter.tsx +230 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-filters/select-filter.tsx +214 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-filters/sort.tsx +88 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-primitives.tsx +229 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-toolbar.tsx +303 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/table-types.ts +64 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/use-client-table.ts +160 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/use-client-table.type-test.ts +220 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/use-server-table.ts +195 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/table/use-server-table.type-test.ts +177 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/tag-editor.tsx +53 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/tag-field.tsx +253 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/text-field.tsx +147 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/toast.tsx +182 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/components/top-nav.tsx +175 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/entry.client.tsx +21 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/entry.server.tsx +13 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/csrf-middleware.server.ts +17 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/custom-error-status-code-middleware.server.ts +31 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/rate-limiting-middleware.server.ts +36 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/redirect-with-toast.server.ts +14 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/request-context.server.ts +40 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/features/security-headers-middleware.server.ts +10 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/icons/types.ts +36 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/root.tsx +208 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/_index.tsx +5 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/activity.tsx +124 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/archive.tsx +187 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/auth.forgot-password.tsx +108 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/auth.login.tsx +132 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/auth.logout.tsx +17 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/auth.register.tsx +172 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/auth.reset-password.tsx +137 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/contacts.$id.edit.tsx +89 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/contacts.$id.tsx +521 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/contacts.new.tsx +56 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/contacts.tsx +183 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/settings.tsx +158 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes/users.tsx +263 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/routes.ts +4 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/services.server.ts +1 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/colors.ts +47 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/compose-event-handlers.ts +13 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/compose-refs.ts +35 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/format-date.ts +10 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/overridable-component-helper.ts +31 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/sanitize-html.ts +16 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/tw.ts +16 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-hydrated.ts +12 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-prevent-default.ts +30 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-query-state.ts +629 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-render-props.ts +42 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-spin-delay.ts +27 -0
- package/skills/maestro-examples/references/cadence/cadence-web/app/utils/use-stable-accessor.ts +14 -0
- package/skills/maestro-examples/references/cadence/cadence-web/instrument.server.mjs +29 -0
- package/skills/maestro-examples/references/cadence/cadence-web/package.json +60 -0
- package/skills/maestro-examples/references/cadence/cadence-web/playwright.config.ts +39 -0
- package/skills/maestro-examples/references/cadence/cadence-web/react-router.config.ts +12 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tailwind.config.ts +7 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/activity/filter-activity.spec.ts +93 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/activity/recent-activity.spec.ts +70 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/archive/archive-view.spec.ts +62 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth/login-invalid.spec.ts +68 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth/login-valid.spec.ts +45 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth/logout.spec.ts +46 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth/password-reset.spec.ts +105 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth/register.spec.ts +39 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/auth.setup.ts +48 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/authorization/member-ownership.spec.ts +104 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/authorization/viewer-read-only.spec.ts +103 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/constants.ts +11 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/archive-restore.spec.ts +77 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/create-contact.spec.ts +56 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/delete-contact.spec.ts +53 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/edit-contact.spec.ts +60 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/filter-by-last-contacted.spec.ts +85 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/filter-by-tag.spec.ts +68 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/global-search.spec.ts +57 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/sort-contacts.spec.ts +60 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/tags.spec.ts +85 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/contacts/view-detail.spec.ts +58 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/interactions/delete-interaction.spec.ts +59 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/interactions/edit-interaction.spec.ts +68 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/interactions/log-email.spec.ts +57 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/interactions/log-note.spec.ts +56 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/interactions/rich-text-formatting.spec.ts +97 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/organization/delete.spec.ts +70 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/organization/edit-name.spec.ts +51 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/users/change-role.spec.ts +62 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/users/invite-user.spec.ts +56 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tests/users/remove-user.spec.ts +58 -0
- package/skills/maestro-examples/references/cadence/cadence-web/tsconfig.json +22 -0
- package/skills/maestro-examples/references/cadence/cadence-web/vite.config.ts +37 -0
- package/skills/maestro-examples/references/cadence/config/agent-container.ts +4 -0
- package/skills/maestro-examples/references/cadence/config/cadence-web.ts +12 -0
- package/skills/maestro-examples/references/cadence/config/db-migrate.ts +9 -0
- package/skills/maestro-examples/references/cadence/config/queue-processor.ts +11 -0
- package/skills/maestro-examples/references/cadence/config/services.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000001_organizations-and-users.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000002_contacts.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000003_interactions.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000004_remember-me-tokens.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000005_password-reset-tokens.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/20260404000006_queue-and-recurring-jobs.ts +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000001_organizations-and-users-down.sql +2 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000001_organizations-and-users-up.sql +24 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000002_contacts-down.sql +1 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000002_contacts-up.sql +16 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000003_interactions-down.sql +1 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000003_interactions-up.sql +15 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000004_remember-me-tokens-down.sql +1 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000004_remember-me-tokens-up.sql +12 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000005_password-reset-tokens-down.sql +1 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000005_password-reset-tokens-up.sql +11 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000006_queue-and-recurring-jobs-down.sql +2 -0
- package/skills/maestro-examples/references/cadence/infrastructure/db-migrations/sqls/20260404000006_queue-and-recurring-jobs-up.sql +21 -0
- package/skills/maestro-examples/references/cadence/package.json +17 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/package.json +17 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/src/contact/index.ts +57 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/src/index.ts +4 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/src/interaction/index.ts +45 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/src/organization/index.ts +32 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/src/user/index.ts +69 -0
- package/skills/maestro-examples/references/cadence/packages/schemas/tsconfig.json +9 -0
- package/skills/maestro-examples/references/cadence/packages/services/package.json +43 -0
- package/skills/maestro-examples/references/cadence/packages/services/run-tests.ts +59 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/auth/auth-db-provider.ts +202 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/auth/auth-use-cases.ts +155 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/auth/index.ts +17 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/auth/profile-db-provider.ts +17 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/contact/contact-db-provider.test.ts +120 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/contact/contact-db-provider.ts +145 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/contact/contact-use-cases.ts +196 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/contact/index.ts +29 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/index.ts +23 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/interaction/index.ts +25 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/interaction/interaction-db-provider.test.ts +128 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/interaction/interaction-db-provider.ts +98 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/interaction/interaction-use-cases.ts +171 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/maestro.ts +218 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/organization/index.ts +8 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/organization/organization-db-provider.test.ts +53 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/organization/organization-db-provider.ts +60 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/organization/organization-use-cases.ts +88 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/permissions.ts +28 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/contact-seed.ts +30 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/index.ts +13 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/interaction-seed.ts +29 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/organization-seed.ts +25 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/seed-pipe.ts +38 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/testing/user-seed.ts +27 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/user/index.ts +27 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/user/user-db-provider.test.ts +100 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/user/user-db-provider.ts +69 -0
- package/skills/maestro-examples/references/cadence/packages/services/src/user/user-use-cases.ts +162 -0
- package/skills/maestro-examples/references/cadence/packages/services/tsconfig.json +9 -0
- package/skills/maestro-examples/references/cadence/scripts/foreign-key-delete-behavior.ts +106 -0
- package/skills/maestro-examples/references/cadence/specs/app.md +72 -0
- package/skills/maestro-examples/references/cadence/specs/data-models.md +80 -0
- package/skills/maestro-examples/references/cadence/specs/multi-tenant-security.md +239 -0
- package/skills/maestro-examples/references/cadence/specs/rich-text-editor.md +108 -0
- package/skills/maestro-examples/references/cadence/specs/table.md +151 -0
- package/skills/maestro-examples/references/cadence/specs/use-cases.md +62 -0
- package/skills/maestro-examples/references/cadence/tsconfig.json +29 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/app.css +152 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/alert-dialog.tsx +153 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/avatar.tsx +57 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/button-group.tsx +78 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/button-link.tsx +77 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/button.tsx +139 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/channel-card.tsx +40 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/checkbox-group.tsx +175 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/checkbox.tsx +210 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/chip.tsx +161 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/container.tsx +85 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/date-picker.tsx +214 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/date-time-picker.tsx +199 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/dialog.tsx +114 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/enum-chip.tsx +33 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/file-input.tsx +248 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/form.tsx +106 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/headless-file-input.tsx +365 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/animated-popover.tsx +27 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/button-context.ts +13 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/calendar-month-year-picker.tsx +283 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/form-field.tsx +231 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/get-button-classes.ts +141 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/headless-button.tsx +39 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/helpers/pdf-dist.client.ts +9 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/icon.tsx +29 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/image-grid.tsx +96 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/image-input.tsx +268 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/inline-alert.tsx +57 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/like-button.tsx +48 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/menu.tsx +189 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/pdf-input.tsx +252 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/pdf.tsx +89 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/post-card.tsx +147 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/post-composer.tsx +166 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/radio-group.tsx +115 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/select.tsx +219 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/spinner.tsx +19 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/tag-field.tsx +253 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/text-area.tsx +151 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/text-field.tsx +147 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/components/toast.tsx +179 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/entry.client.tsx +21 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/entry.server.tsx +12 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/csrf-hidden-input.tsx +6 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/csrf-middleware.server.ts +17 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/custom-error-status-code-middleware.server.ts +18 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/rate-limiting-middleware.server.ts +36 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/redirect-with-toast.server.ts +14 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/request-context.server.ts +40 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/features/security-headers-middleware.server.ts +10 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/icons/types.ts +26 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/root.tsx +164 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_auth/forgot-password.tsx +90 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_auth/login.tsx +104 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_auth/logout.tsx +44 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_auth/reset-password.tsx +130 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_auth/signup.tsx +91 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_browse/_layout.tsx +36 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_browse/c.new.tsx +47 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/_browse/index.tsx +3 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/api/hide-post.$postId.tsx +30 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/api/like.$postId.tsx +22 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug/_layout.tsx +108 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug/post.new.tsx +59 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug.edit.tsx +53 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug.manage/_layout.tsx +123 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug.manage/add-moderator.tsx +48 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug.manage/transfer-ownership.tsx +71 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/c.$slug.post.$postId.tsx +119 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/files.$.tsx +22 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/health.tsx +3 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes/settings.tsx +128 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/routes.ts +4 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/services.server.ts +1 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/colors.ts +47 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/compose-refs.ts +35 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/enum-colors.ts +4 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/file-input.ts +52 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/tw.ts +16 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-element-size.ts +35 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-number-input.ts +143 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-pagination.ts +41 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-prevent-default.ts +30 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-profile.ts +7 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-render-props.ts +42 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-spin-delay.ts +27 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-stable-accessor.ts +14 -0
- package/skills/maestro-examples/references/chorus/chorus-web/app/utils/use-tab-indicator.ts +107 -0
- package/skills/maestro-examples/references/chorus/chorus-web/environment.d.ts +15 -0
- package/skills/maestro-examples/references/chorus/chorus-web/instrument.server.mjs +29 -0
- package/skills/maestro-examples/references/chorus/chorus-web/package.json +54 -0
- package/skills/maestro-examples/references/chorus/chorus-web/playwright.config.ts +39 -0
- package/skills/maestro-examples/references/chorus/chorus-web/react-router.config.ts +12 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/auth-landing.test.ts +8 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/auth-login.test.ts +40 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/auth-logout.test.ts +8 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/auth-signup.test.ts +15 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/auth.setup.ts +42 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-browse.test.ts +43 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-create.test.ts +34 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-edit.test.ts +33 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-manage-moderators.test.ts +59 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-manage-permissions.test.ts +55 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/channel-transfer.test.ts +47 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/constants.ts +7 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/forgot-password.test.ts +33 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/hide-post-author.test.ts +62 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/hide-post-nonauthor.test.ts +36 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/like-toggle.test.ts +60 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/like-unauth.test.ts +37 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/post-create.test.ts +30 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/post-listing.test.ts +33 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/post-reply.test.ts +48 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/post-view-unauth.test.ts +37 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/settings-duplicate.test.ts +28 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/settings-update.test.ts +34 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/settings-view.test.ts +17 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tests/signup-validation.test.ts +51 -0
- package/skills/maestro-examples/references/chorus/chorus-web/tsconfig.json +14 -0
- package/skills/maestro-examples/references/chorus/chorus-web/vite.config.ts +38 -0
- package/skills/maestro-examples/references/chorus/config/app-a.ts +7 -0
- package/skills/maestro-examples/references/chorus/config/chorus-web.ts +7 -0
- package/skills/maestro-examples/references/chorus/config/db-migrate.ts +9 -0
- package/skills/maestro-examples/references/chorus/config/queue-processor.ts +7 -0
- package/skills/maestro-examples/references/chorus/config/services.ts +15 -0
- package/skills/maestro-examples/references/chorus/infrastructure/cdk/index.ts +60 -0
- package/skills/maestro-examples/references/chorus/infrastructure/cdk.context.json +5 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260303005902_init_authentication.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260308234835_add-maestro-queue-table.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260315051819_channels.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260315052202_channel-moderators.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260315052539_posts.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260315052734_post-images.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/20260315053110_likes.ts +16 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260303005902_init_authentication-down.sql +3 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260303005902_init_authentication-up.sql +40 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260308234835_add-maestro-queue-table-down.sql +3 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260308234835_add-maestro-queue-table-up.sql +23 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315051819_channels-down.sql +1 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315051819_channels-up.sql +11 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052202_channel-moderators-down.sql +1 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052202_channel-moderators-up.sql +7 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052539_posts-down.sql +1 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052539_posts-up.sql +14 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052734_post-images-down.sql +1 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315052734_post-images-up.sql +7 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315053110_likes-down.sql +1 -0
- package/skills/maestro-examples/references/chorus/infrastructure/db-migrations/sqls/20260315053110_likes-up.sql +8 -0
- package/skills/maestro-examples/references/chorus/infrastructure/package.json +20 -0
- package/skills/maestro-examples/references/chorus/infrastructure/tsconfig.json +8 -0
- package/skills/maestro-examples/references/chorus/package.json +17 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/other/generate-email-modules.ts +22 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/other/generate-email-previews.ts +18 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/package.json +27 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/components/button.tsx +19 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/components/root-container.tsx +29 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/components/tailwind-wrapper.tsx +113 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/email-modules.ts +9 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/emails/password-reset.tsx +41 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/index.ts +3 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/render-email-template.ts +24 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/src/utils/tw.ts +5 -0
- package/skills/maestro-examples/references/chorus/packages/email-templates/tsconfig.json +11 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/package.json +17 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/src/channel/index.ts +41 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/src/index.ts +4 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/src/like/index.ts +16 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/src/post/index.ts +44 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/src/profile/index.ts +80 -0
- package/skills/maestro-examples/references/chorus/packages/schemas/tsconfig.json +9 -0
- package/skills/maestro-examples/references/chorus/packages/services/environment.d.ts +7 -0
- package/skills/maestro-examples/references/chorus/packages/services/package.json +47 -0
- package/skills/maestro-examples/references/chorus/packages/services/run-tests.ts +59 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/auth/auth-use-cases.ts +178 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/auth/authenticatable-db-provider.ts +59 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/auth/index.ts +29 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/auth/password-reset-token-db-provider.ts +58 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/auth/remember-me-token-db-provider.ts +82 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/channel/channel-db-provider.test.ts +85 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/channel/channel-db-provider.ts +158 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/channel/channel-use-cases.ts +303 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/channel/index.ts +29 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/index.ts +21 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/like/index.ts +6 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/like/like-db-provider.test.ts +47 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/like/like-db-provider.ts +53 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/like/like-use-cases.ts +31 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/maestro.ts +215 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/permissions.ts +16 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/post/index.ts +25 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/post/post-db-provider.test.ts +68 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/post/post-db-provider.ts +219 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/post/post-use-cases.ts +184 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/post/redact-hidden-post.ts +4 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/profile/index.ts +17 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/profile/profile-db-provider.test.ts +57 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/profile/profile-db-provider.ts +75 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/profile/profile-use-cases.ts +61 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/channel-seed.ts +27 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/index.ts +13 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/like-seed.ts +21 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/post-seed.ts +27 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/profile-seed.ts +28 -0
- package/skills/maestro-examples/references/chorus/packages/services/src/testing/seed-pipe.ts +38 -0
- package/skills/maestro-examples/references/chorus/packages/services/tsconfig.json +9 -0
- package/skills/maestro-examples/references/chorus/scripts/foreign-key-delete-behavior.ts +111 -0
- package/skills/maestro-examples/references/chorus/specs/app.md +73 -0
- package/skills/maestro-examples/references/chorus/specs/data-models.md +48 -0
- package/skills/maestro-examples/references/chorus/specs/database-schema.md +59 -0
- package/skills/maestro-examples/references/chorus/specs/use-cases.md +33 -0
- package/skills/maestro-examples/references/chorus/tsconfig.json +29 -0
- package/skills/maestro-examples/references/setlist/IMPLEMENTATION_PLAN.md +88 -0
- package/skills/maestro-examples/references/setlist/config/db-migrate.ts +9 -0
- package/skills/maestro-examples/references/setlist/config/queue-processor.ts +11 -0
- package/skills/maestro-examples/references/setlist/config/services.ts +16 -0
- package/skills/maestro-examples/references/setlist/config/setlist-web.ts +12 -0
- package/skills/maestro-examples/references/setlist/infrastructure/cdk/index.ts +56 -0
- package/skills/maestro-examples/references/setlist/infrastructure/cdk.context.json +5 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/20260316000000_init.ts +16 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/20260317000000_add-queue-and-recurring-job-tables.ts +22 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/sqls/20260316000000_init-down.sql +5 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/sqls/20260316000000_init-up.sql +68 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/sqls/20260317000000_add-queue-and-recurring-job-tables-down.sql +2 -0
- package/skills/maestro-examples/references/setlist/infrastructure/db-migrations/sqls/20260317000000_add-queue-and-recurring-job-tables-up.sql +21 -0
- package/skills/maestro-examples/references/setlist/infrastructure/package.json +20 -0
- package/skills/maestro-examples/references/setlist/infrastructure/tsconfig.json +8 -0
- package/skills/maestro-examples/references/setlist/package.json +19 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/other/generate-email-modules.ts +22 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/other/generate-email-previews.ts +18 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/package.json +27 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/components/button.tsx +19 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/components/root-container.tsx +29 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/components/tailwind-wrapper.tsx +113 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/email-modules.ts +9 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/emails/magic-link-login.tsx +49 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/index.ts +3 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/render-email-template.ts +24 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/src/utils/tw.ts +5 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/tsconfig.json +11 -0
- package/skills/maestro-examples/references/setlist/packages/email-templates/tsup.config.ts +10 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/package.json +21 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/area/index.ts +28 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/index.ts +6 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/magic-link/index.ts +20 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/position.ts +137 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/project/index.ts +44 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/task/index.ts +41 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/src/user/index.ts +28 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/tests/position.test.ts +106 -0
- package/skills/maestro-examples/references/setlist/packages/schemas/tsconfig.json +9 -0
- package/skills/maestro-examples/references/setlist/packages/services/package.json +40 -0
- package/skills/maestro-examples/references/setlist/packages/services/run-tests.ts +59 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/area/area-db-provider.test.ts +29 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/area/area-db-provider.ts +81 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/area/area-use-cases.ts +130 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/area/index.ts +27 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/auth-db-provider.test.ts +34 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/auth-db-provider.ts +24 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/auth-use-cases.ts +110 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/index.ts +18 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/magic-link-db-provider.test.ts +52 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/magic-link-db-provider.ts +59 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/user-db-provider.test.ts +33 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/user-db-provider.ts +55 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/auth/verify-magic-link-credentials.ts +39 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/index.ts +20 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/maestro.ts +175 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/project/index.ts +29 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/project/project-db-provider.test.ts +46 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/project/project-db-provider.ts +114 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/project/project-use-cases.ts +183 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/task/index.ts +39 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/task/task-db-provider.test.ts +53 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/task/task-db-provider.ts +219 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/task/task-use-cases.ts +303 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/area-seed.ts +25 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/index.ts +13 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/project-seed.ts +29 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/seed-pipe.ts +38 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/task-seed.ts +31 -0
- package/skills/maestro-examples/references/setlist/packages/services/src/testing/user-seed.ts +24 -0
- package/skills/maestro-examples/references/setlist/packages/services/tsconfig.json +9 -0
- package/skills/maestro-examples/references/setlist/queue-processor/package.json +17 -0
- package/skills/maestro-examples/references/setlist/queue-processor/src/index.ts +29 -0
- package/skills/maestro-examples/references/setlist/queue-processor/src/recurring-jobs.ts +13 -0
- package/skills/maestro-examples/references/setlist/queue-processor/tsconfig.json +4 -0
- package/skills/maestro-examples/references/setlist/scripts/foreign-key-delete-behavior.ts +107 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/app.css +158 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/alert-dialog.tsx +153 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/app-dnd-provider.tsx +180 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/area-form.tsx +113 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/button-group.tsx +78 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/button.tsx +139 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/checkbox.tsx +210 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/context-menu.tsx +78 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/date-picker.tsx +214 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/dnd-collision-detection.ts +28 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/dnd-utils.ts +11 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/drag-ghost.tsx +24 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/form.tsx +107 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/animated-popover.tsx +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/button-context.ts +13 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/calendar-month-year-picker.tsx +283 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/form-field.tsx +231 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/get-button-classes.ts +141 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/helpers/headless-button.tsx +39 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/icon.tsx +29 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/inline-alert.tsx +57 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/project-form.tsx +153 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/select.tsx +219 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/helpers.ts +73 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/index.ts +6 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/inline-inputs.tsx +128 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/sidebar.tsx +333 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/sortable-rows.tsx +256 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/sidebar/use-sidebar-dnd.ts +261 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/spinner.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-inline-create.tsx +51 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/index.ts +3 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/task-date-helpers.ts +16 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/task-expanded-detail.tsx +273 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/task-keyboard-helpers.ts +5 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/task-list.tsx +134 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/task-row.tsx +100 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/types.ts +49 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/use-expanded-cache.ts +22 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/use-sticky-task.ts +24 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/task-list/use-task-reorder.ts +140 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/text-area.tsx +151 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/text-field.tsx +147 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/components/toast.tsx +179 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/entry.client.tsx +21 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/entry.server.tsx +13 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/csrf-middleware.server.ts +17 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/custom-error-status-code-middleware.server.ts +31 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/rate-limiting-middleware.server.ts +36 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/redirect-with-toast.server.ts +14 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/request-context.server.ts +52 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/features/security-headers-middleware.server.ts +10 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/hooks/use-area-actions.ts +44 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/hooks/use-csrf-token.ts +6 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/hooks/use-project-actions.ts +80 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/hooks/use-task-actions.ts +65 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/root.tsx +218 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/areas.$id.delete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/areas.$id.edit.tsx +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/areas.$id.reorder.tsx +30 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/areas.tsx +23 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.complete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.delete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.edit.tsx +32 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.move.tsx +23 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.reorder.tsx +30 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.$id.uncomplete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/projects.tsx +23 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.complete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.delete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.edit.tsx +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.move.tsx +30 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.reorder.tsx +32 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.$id.uncomplete.tsx +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/api/tasks.tsx +23 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/areas.$id.tsx +155 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/auth.login.tsx +98 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/auth.logout.tsx +17 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/auth.verify.tsx +154 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/completed.tsx +121 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/inbox.tsx +68 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/projects.$id.tsx +217 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/today.tsx +86 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes/upcoming.tsx +83 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/routes.ts +4 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/services.server.ts +1 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/colors.ts +47 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/compose-refs.ts +35 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/tw.ts +16 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-dismiss.ts +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-fetcher-complete.ts +17 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-prevent-default.ts +30 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-render-props.ts +42 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-spin-delay.ts +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/app/utils/use-stable-accessor.ts +14 -0
- package/skills/maestro-examples/references/setlist/setlist-web/environment.d.ts +8 -0
- package/skills/maestro-examples/references/setlist/setlist-web/instrument.server.mjs +29 -0
- package/skills/maestro-examples/references/setlist/setlist-web/knip.json +3 -0
- package/skills/maestro-examples/references/setlist/setlist-web/package.json +52 -0
- package/skills/maestro-examples/references/setlist/setlist-web/playwright.config.ts +39 -0
- package/skills/maestro-examples/references/setlist/setlist-web/react-router.config.ts +12 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-create.test.ts +37 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-delete.test.ts +45 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-deleted-not-in-sidebar.test.ts +44 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-detail-view.test.ts +46 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-edit-title.test.ts +48 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-expand-collapse.test.ts +50 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-list-in-sidebar.test.ts +37 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/areas-reorder-dnd.test.ts +82 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-logout.test.ts +18 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-reject-invalid-token.test.ts +35 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-reject-used-token.test.ts +104 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-request-magic-link.test.ts +29 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-verify-via-pasted-token.test.ts +52 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth-verify-via-url.test.ts +48 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/auth.setup.ts +55 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/constants.ts +13 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/dnd-inbox-to-area.test.ts +74 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/dnd-inbox-to-project.test.ts +73 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/dnd-move-to-inbox.test.ts +51 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/dnd-project-to-area.test.ts +76 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/dnd-project-to-project.test.ts +75 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/home.test.ts +27 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-complete.test.ts +53 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-completed-not-in-sidebar.test.ts +46 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-create-in-area.test.ts +55 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-create-top-level.test.ts +37 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-delete.test.ts +55 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-edit-notes-date.test.ts +69 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-edit-title.test.ts +48 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-move-into-area.test.ts +72 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-move-out-of-area.test.ts +76 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-reorder-top-level.test.ts +82 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-reorder-within-area.test.ts +91 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-uncomplete.test.ts +39 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/projects-view-tasks.test.ts +48 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-complete-removes-from-active-views.test.ts +52 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-complete.test.ts +46 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-create-in-inbox.test.ts +19 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-create-in-project.test.ts +44 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-delete.test.ts +52 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-edit.test.ts +73 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-reorder-in-project.test.ts +81 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-reorder-inbox.test.ts +80 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-reorder-today.test.ts +103 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-uncomplete.test.ts +51 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/tasks-view-details.test.ts +51 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-active-sidebar-highlight.test.ts +38 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-inbox-unassigned-only.test.ts +43 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-navigate-to-completed.test.ts +59 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-navigate-to-inbox.test.ts +21 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-navigate-to-today.test.ts +42 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-navigate-to-upcoming.test.ts +59 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-sidebar-built-in-views.test.ts +23 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tests/views-today-shows-overdue.test.ts +48 -0
- package/skills/maestro-examples/references/setlist/setlist-web/tsconfig.json +22 -0
- package/skills/maestro-examples/references/setlist/setlist-web/vite.config.ts +37 -0
- package/skills/maestro-examples/references/setlist/specs/app.md +69 -0
- package/skills/maestro-examples/references/setlist/specs/data-models.md +92 -0
- package/skills/maestro-examples/references/setlist/specs/database-schema.md +76 -0
- package/skills/maestro-examples/references/setlist/specs/drag-and-drop.md +89 -0
- package/skills/maestro-examples/references/setlist/specs/magic-link-login.md +300 -0
- package/skills/maestro-examples/references/setlist/specs/position-ordering.md +329 -0
- package/skills/maestro-examples/references/setlist/specs/sidebar.md +65 -0
- package/skills/maestro-examples/references/setlist/specs/use-cases.md +55 -0
- package/skills/maestro-examples/references/setlist/tsconfig.json +28 -0
- package/{dist → skills}/maestro-packages/SKILL.md +3 -3
- package/skills/maestro-packages/permissions.md +215 -0
- package/skills/maestro-packages/queue.md +442 -0
- package/skills/maestro-packages/recurring-jobs.md +238 -0
- package/skills/react-use-effect/SKILL.md +65 -0
- package/skills/react-use-effect/references/patterns.md +283 -0
- package/dist/maestro-packages/queue.md +0 -284
- package/dist/maestro-packages/recurring-jobs.md +0 -353
- package/dist/maestro-packages/tracing.md +0 -240
- /package/{dist → skills}/maestro-cli/SKILL.md +0 -0
- /package/{dist → skills}/maestro-packages/broadcast.md +0 -0
- /package/{dist → skills}/maestro-packages/cache-control.md +0 -0
- /package/{dist → skills}/maestro-packages/cache.md +0 -0
- /package/{dist → skills}/maestro-packages/config.md +0 -0
- /package/{dist → skills}/maestro-packages/context.md +0 -0
- /package/{dist → skills}/maestro-packages/crypt.md +0 -0
- /package/{dist → skills}/maestro-packages/db.md +0 -0
- /package/{dist → skills}/maestro-packages/events.md +0 -0
- /package/{dist → skills}/maestro-packages/exceptions.md +0 -0
- /package/{dist → skills}/maestro-packages/form.md +0 -0
- /package/{dist → skills}/maestro-packages/hash.md +0 -0
- /package/{dist → skills}/maestro-packages/helpers.md +0 -0
- /package/{dist → skills}/maestro-packages/log.md +0 -0
- /package/{dist → skills}/maestro-packages/mail.md +0 -0
- /package/{dist → skills}/maestro-packages/metrics.md +0 -0
- /package/{dist → skills}/maestro-packages/rate-limiting.md +0 -0
- /package/{dist → skills}/maestro-packages/react-router-file-routes.md +0 -0
- /package/{dist → skills}/maestro-packages/sql.md +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Magic Link Login
|
|
2
|
+
|
|
3
|
+
Authentication uses magic links — no passwords. A user enters their email, receives a link via email, and clicking it logs them in. The email also includes the raw token as a copyable string so the user can paste it into a login form on a different device.
|
|
4
|
+
|
|
5
|
+
This spec covers the maestro service setup, the magic-link DB provider, and the two authentication use cases (A1: Request Magic Link, A2: Verify Magic Link).
|
|
6
|
+
|
|
7
|
+
## Maestro Service Setup
|
|
8
|
+
|
|
9
|
+
Magic-link auth uses `Auth.login()` with a `verifyCredentials` callback that implements the full magic-link token verification flow (decode, lookup, hash check, expiry check, mark as used).
|
|
10
|
+
|
|
11
|
+
### Hash
|
|
12
|
+
|
|
13
|
+
Register two Hash providers: a default `sha256` for general hashing (remember-me tokens, etc.) and a `bcrypt` provider for hashing magic-link verifiers.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
Hash.Provider.register(
|
|
17
|
+
'default',
|
|
18
|
+
Hash.Provider.create(Hash.drivers.sha256({ key: process.env.HASH_KEY!, previousKeys: [] }))
|
|
19
|
+
)
|
|
20
|
+
Hash.Provider.register('bcrypt', Hash.Provider.create(Hash.drivers.bcrypt({ rounds: 12 })))
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Crypt
|
|
24
|
+
|
|
25
|
+
Crypt is required by Auth for encrypting remember-me cookies.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
Crypt.Provider.register(
|
|
29
|
+
'default',
|
|
30
|
+
Crypt.Provider.create({
|
|
31
|
+
key: process.env.CRYPT_KEY!,
|
|
32
|
+
previousKeys: []
|
|
33
|
+
})
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Auth
|
|
38
|
+
|
|
39
|
+
`Credentials` is `{ token: string }` — the raw magic-link token. `verifyCredentials` decodes it, verifies the hash, checks expiry, marks the token as used, and returns the authenticatable.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
type Credentials = { token: string }
|
|
43
|
+
type DeviceContext = ReturnType<(typeof Helpers)['getDeviceContext']>
|
|
44
|
+
|
|
45
|
+
Auth.Provider.register(
|
|
46
|
+
'default',
|
|
47
|
+
Auth.Provider.create<number, Credentials, DeviceContext>({
|
|
48
|
+
name: 'default',
|
|
49
|
+
verifyCredentials: async ({ token }) => {
|
|
50
|
+
const decoded = decodeMagicLinkToken(token)
|
|
51
|
+
if (!decoded) return null
|
|
52
|
+
|
|
53
|
+
const record = await magicLinkDbProvider.getBySelector(decoded.selector)
|
|
54
|
+
if (!record) return null
|
|
55
|
+
|
|
56
|
+
const isValid = await Hash.provider('bcrypt').verify({
|
|
57
|
+
plaintext: decoded.verifier,
|
|
58
|
+
hash: record.verifierHash
|
|
59
|
+
})
|
|
60
|
+
if (!isValid) return null
|
|
61
|
+
|
|
62
|
+
if (record.expirationDate < instantFns.now()) {
|
|
63
|
+
throw new MagicLinkExpiredTokenError()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await magicLinkDbProvider.markAsUsed(decoded.selector)
|
|
67
|
+
|
|
68
|
+
const userRows = await Db.select('SELECT credentialsInvalidBefore FROM users WHERE id = ?', [record.userId])
|
|
69
|
+
return { id: record.userId, credentialsInvalidBefore: (userRows[0]?.credentialsInvalidBefore as Iso.Instant) ?? null }
|
|
70
|
+
},
|
|
71
|
+
getAuthenticatableById: async (id) => {
|
|
72
|
+
const rows = await Db.select('SELECT id, credentialsInvalidBefore FROM users WHERE id = ?', [id])
|
|
73
|
+
const row = rows[0]
|
|
74
|
+
if (!row) return null
|
|
75
|
+
return { id: row.id as number, credentialsInvalidBefore: (row.credentialsInvalidBefore as Iso.Instant) ?? null }
|
|
76
|
+
},
|
|
77
|
+
updateCredentialsInvalidBefore: async (id, date) => {
|
|
78
|
+
await Db.update('UPDATE users SET credentialsInvalidBefore = ? WHERE id = ?', [instantFns.formatISO9075(date), id])
|
|
79
|
+
},
|
|
80
|
+
hash: Hash,
|
|
81
|
+
crypt: Crypt
|
|
82
|
+
})
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`credentialsInvalidBefore` is stored on the `users` table as a nullable `DATETIME(3)`. It starts as `NULL` and is set when `logoutOtherDevices` is called — Auth uses it to invalidate sessions and remember-me tokens created before that timestamp.
|
|
87
|
+
|
|
88
|
+
Error behavior through `Auth.login()`:
|
|
89
|
+
- Invalid format / not found / hash mismatch → `verifyCredentials` returns `null` → `Auth.login()` returns `{ id: null, cookie: null }`
|
|
90
|
+
- Expired token → `verifyCredentials` throws `MagicLinkExpiredTokenError` → propagates to the caller (no try/catch in `Auth.login`)
|
|
91
|
+
- Valid → marks as used, returns authenticatable → `Auth.login()` establishes a session via `privateLogin`
|
|
92
|
+
|
|
93
|
+
### Mail
|
|
94
|
+
|
|
95
|
+
Register a Mail provider for sending magic-link emails.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
Mail.Provider.register(
|
|
99
|
+
'default',
|
|
100
|
+
Mail.Provider.create({
|
|
101
|
+
queue: Queue,
|
|
102
|
+
driver: Mail.drivers.ses({ /* ... */ }),
|
|
103
|
+
name: 'default',
|
|
104
|
+
globalFrom: 'noreply@setlist.example.com',
|
|
105
|
+
logger: Log
|
|
106
|
+
})
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Magic-Link DB Provider
|
|
111
|
+
|
|
112
|
+
A set of CRUD functions for the `magicLinks` table, following the same pattern as chorus's `password-reset-token-db-provider.ts`. These are plain functions — not a maestro service — consumed directly by the use cases.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { Db } from '@maestro-js/db'
|
|
116
|
+
import { isoZ } from '@maestro-js/iso-zod'
|
|
117
|
+
import { instantFns } from 'iso-fns'
|
|
118
|
+
import { z } from 'zod'
|
|
119
|
+
|
|
120
|
+
const magicLinkRecordModel = z.object({
|
|
121
|
+
id: z.number(),
|
|
122
|
+
userId: z.number(),
|
|
123
|
+
selector: z.string(),
|
|
124
|
+
verifierHash: z.string(),
|
|
125
|
+
usedDate: isoZ.instant().nullable(),
|
|
126
|
+
expirationDate: isoZ.instant(),
|
|
127
|
+
createdDate: isoZ.instant()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
type MagicLinkRecord = z.infer<typeof magicLinkRecordModel>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### create
|
|
134
|
+
|
|
135
|
+
Insert a new magic-link record.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
async function create(record: {
|
|
139
|
+
userId: number
|
|
140
|
+
selector: string
|
|
141
|
+
verifierHash: string
|
|
142
|
+
expirationDate: Iso.Instant
|
|
143
|
+
createdDate: Iso.Instant
|
|
144
|
+
}): Promise<void> {
|
|
145
|
+
await Db.insert(
|
|
146
|
+
'INSERT INTO magicLinks (userId, selector, verifierHash, expirationDate, createdDate) VALUES (?, ?, ?, ?, ?)',
|
|
147
|
+
[record.userId, record.selector, record.verifierHash, instantFns.formatISO9075(record.expirationDate), instantFns.formatISO9075(record.createdDate)]
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### getBySelector
|
|
153
|
+
|
|
154
|
+
Look up an unused magic-link record by its selector (the non-secret half of the token). Returns `null` if the token has already been used.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
async function getBySelector(selector: string): Promise<MagicLinkRecord | null> {
|
|
158
|
+
const rows = await Db.select('SELECT * FROM magicLinks WHERE selector = ? AND usedDate IS NULL', [selector])
|
|
159
|
+
const row = rows[0]
|
|
160
|
+
if (!row) return null
|
|
161
|
+
return magicLinkRecordModel.parse(row)
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### getByUserId
|
|
166
|
+
|
|
167
|
+
Look up the most recent magic-link record for a user (used for throttle checks).
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
async function getByUserId(userId: number): Promise<MagicLinkRecord | null> {
|
|
171
|
+
const rows = await Db.select('SELECT * FROM magicLinks WHERE userId = ? ORDER BY createdDate DESC LIMIT 1', [userId])
|
|
172
|
+
const row = rows[0]
|
|
173
|
+
if (!row) return null
|
|
174
|
+
return magicLinkRecordModel.parse(row)
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### markAsUsed
|
|
179
|
+
|
|
180
|
+
Mark a magic-link record as used by setting its `usedDate`. The token can no longer be verified again, but the record is retained.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
async function markAsUsed(selector: string): Promise<void> {
|
|
184
|
+
await Db.update('UPDATE magicLinks SET usedDate = ? WHERE selector = ?', [instantFns.formatISO9075(instantFns.now()), selector])
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Export
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
export const magicLinkDbProvider = {
|
|
192
|
+
create,
|
|
193
|
+
getBySelector,
|
|
194
|
+
getByUserId,
|
|
195
|
+
markAsUsed
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Use Case A1: Request Magic Link
|
|
200
|
+
|
|
201
|
+
The user enters their email address. If a user with that email exists, a magic-link token is generated and emailed. The response is always the same (success) regardless of whether the email exists — this prevents email enumeration.
|
|
202
|
+
|
|
203
|
+
### Flow
|
|
204
|
+
|
|
205
|
+
1. Look up the user by email.
|
|
206
|
+
2. If the user doesn't exist, return success (no email sent — prevents enumeration).
|
|
207
|
+
3. **Throttle check**: call `magicLinkDbProvider.getByUserId(user.id)`. If a record exists and was created less than 60 seconds ago, throw a throttle error.
|
|
208
|
+
4. **Generate token**: create a `selector` (16 random bytes, base64url) and `verifier` (32 random bytes, base64url) using `crypto.randomBytes()`.
|
|
209
|
+
5. **Hash the verifier**: `await Hash.provider('bcrypt').hash(verifier)`.
|
|
210
|
+
6. **Store the record**: call `magicLinkDbProvider.create()` with the userId, selector, verifierHash, expirationDate (now + 15 minutes), and createdDate (now). Previous tokens are not deleted — they expire naturally or are already marked as used.
|
|
211
|
+
7. **Build the raw token**: `${selector}|${verifier}` — this is the `selector|verifier` format used by `@maestro-js/password`.
|
|
212
|
+
8. **Send the email**: queue a magic-link email containing the login URL and the raw token.
|
|
213
|
+
|
|
214
|
+
### Token Generation
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import crypto from 'node:crypto'
|
|
218
|
+
|
|
219
|
+
const selector = crypto.randomBytes(16).toString('base64url')
|
|
220
|
+
const verifier = crypto.randomBytes(32).toString('base64url')
|
|
221
|
+
const verifierHash = await Hash.provider('bcrypt').hash(verifier)
|
|
222
|
+
|
|
223
|
+
const now = instantFns.now()
|
|
224
|
+
const expirationDate = instantFns.add(now, { minutes: 15 })
|
|
225
|
+
|
|
226
|
+
await magicLinkDbProvider.create({
|
|
227
|
+
userId: user.id,
|
|
228
|
+
selector,
|
|
229
|
+
verifierHash,
|
|
230
|
+
expirationDate,
|
|
231
|
+
createdDate: now
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const token = `${selector}|${verifier}`
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Email Content
|
|
238
|
+
|
|
239
|
+
Use the `email-templates` package pattern from chorus — a `magic-link-login` template that receives `{ loginUrl: string, token: string, expiresInMinutes: number }`. The email contains a clickable login URL and the raw token as a copyable string for cross-device use.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const html = await renderEmailTemplate('magic-link-login', {
|
|
243
|
+
loginUrl: `${APP_URL}/auth/verify?token=${encodeURIComponent(token)}`,
|
|
244
|
+
token,
|
|
245
|
+
expiresInMinutes: 15
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
Mail.queue({ to: user.email, subject: 'Your login link', content: { html } })
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Rate Limiting
|
|
252
|
+
|
|
253
|
+
Throttle repeat requests per email to prevent abuse. If a magic-link record for the user was created less than 60 seconds ago, reject the request. This mirrors the `throttleSeconds` pattern in `@maestro-js/password`'s `passwordResetOptions`.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const existing = await magicLinkDbProvider.getByUserId(user.id)
|
|
257
|
+
if (existing) {
|
|
258
|
+
const throttleExpiry = instantFns.add(existing.createdDate, { seconds: 60 })
|
|
259
|
+
if (instantFns.now() < throttleExpiry) {
|
|
260
|
+
throw new MagicLinkThrottledError()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Use Case A2: Verify Magic Link
|
|
266
|
+
|
|
267
|
+
The user clicks the magic-link URL or pastes the raw token into the login form. The token is validated and the user is logged in.
|
|
268
|
+
|
|
269
|
+
### Flow
|
|
270
|
+
|
|
271
|
+
1. **Call `Auth.login()`** with the raw token as credentials. `verifyCredentials` handles decoding, lookup, hash verification, expiry check, and marking the token as used.
|
|
272
|
+
2. **Return**: the `{ id, cookie }` result from `Auth.login()`.
|
|
273
|
+
|
|
274
|
+
### Token Decode Helper
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
function decodeMagicLinkToken(token: string): { selector: string; verifier: string } | null {
|
|
278
|
+
const [selector, verifier] = token.split('|')
|
|
279
|
+
if (!selector || !verifier) return null
|
|
280
|
+
return { selector, verifier }
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Login
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const result = await Auth.login({ credentials: { token }, session, deviceContext })
|
|
288
|
+
|
|
289
|
+
return result // { id, cookie } or { id: null, cookie: null }
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Security Considerations
|
|
293
|
+
|
|
294
|
+
- **No email enumeration**: A1 always returns success regardless of whether the email exists in the database.
|
|
295
|
+
- **Token format**: `selector|verifier` — the selector is stored in plaintext for lookup; the verifier is bcrypt-hashed. A database leak does not expose usable tokens.
|
|
296
|
+
- **Bcrypt for verifier hash**: bcrypt is used (not sha256) because magic-link verifiers are high-entropy but short-lived secrets — bcrypt's cost factor adds defense-in-depth against brute force if the database is compromised.
|
|
297
|
+
- **15-minute expiry**: tokens expire quickly to limit the window of vulnerability.
|
|
298
|
+
- **Single-use tokens**: the token is marked as used (`usedDate` is set) before establishing the session. `getBySelector` filters out used tokens, so a token cannot be verified twice. The record is retained rather than hard-deleted.
|
|
299
|
+
- **Rate limiting**: repeat requests for the same email are throttled to one per 60 seconds to prevent abuse and inbox flooding.
|
|
300
|
+
- **HTTPS only**: the magic-link URL must be served over HTTPS. Session cookies are set with `Secure` and `HttpOnly` flags.
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Position Ordering
|
|
2
|
+
|
|
3
|
+
How string-based position fields work and how to compute new positions when reordering items.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Every orderable entity uses a `VARCHAR` column that sorts lexicographically. Rather than integer indices (which require shifting every sibling on insert), a string position slots between any two adjacent items by generating a key that compares between them. A reorder only writes **one row**.
|
|
8
|
+
|
|
9
|
+
### Position columns
|
|
10
|
+
|
|
11
|
+
| Entity | Column | Scope |
|
|
12
|
+
| ------- | ------------------- | ------------------------------------------------------- |
|
|
13
|
+
| Area | `position` | All areas for a user (sidebar) |
|
|
14
|
+
| Project | `position` | Sidebar ordering — shared position space with areas |
|
|
15
|
+
| Task | `listPosition` | Within its container (inbox, project, or area) |
|
|
16
|
+
| Task | `scheduledPosition` | Within its scheduled date (Today view, Upcoming groups) |
|
|
17
|
+
|
|
18
|
+
All of these columns follow the same algorithm. The examples below use `listPosition` on tasks, but the logic applies identically to every position column.
|
|
19
|
+
|
|
20
|
+
## The Algorithm
|
|
21
|
+
|
|
22
|
+
Based on [rocicorp/fractional-indexing](https://github.com/rocicorp/fractional-indexing) (CC0 / public domain). We vendor this as a local utility — no npm package needed.
|
|
23
|
+
|
|
24
|
+
### Key structure
|
|
25
|
+
|
|
26
|
+
A position key has two parts: an **integer part** and a **fractional part**.
|
|
27
|
+
|
|
28
|
+
The integer part is a variable-length prefix whose first character (the "head") encodes its length:
|
|
29
|
+
- `a`–`z`: lowercase heads have length `charCode - 'a' + 2` (so `a` = 2 chars, `b` = 3, ..., `z` = 28)
|
|
30
|
+
- `A`–`Z`: uppercase heads have length `'Z' - charCode + 2` (so `Z` = 2, `Y` = 3, ..., `A` = 28)
|
|
31
|
+
|
|
32
|
+
Uppercase integers sort before lowercase ones, giving a total order from smallest (`A` + 26 digits) to largest (`z` + 26 digits), with `a0` as the natural midpoint.
|
|
33
|
+
|
|
34
|
+
The fractional part is everything after the integer prefix. It provides fine-grained ordering within an integer bucket. No trailing zeros are allowed.
|
|
35
|
+
|
|
36
|
+
### Digit base
|
|
37
|
+
|
|
38
|
+
Keys use base-62 digits: `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`. These are in ascending character code order, so standard string comparison (`localeCompare` or SQL `ORDER BY`) sorts them correctly.
|
|
39
|
+
|
|
40
|
+
### `generateKeyBetween(a, b)`
|
|
41
|
+
|
|
42
|
+
The main entry point. Takes two nullable position strings and returns a new string that sorts between them.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
generateKeyBetween(a: string | null, b: string | null): string
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `a` — position of the item that should sort **before** the result (or `null` for start-of-list)
|
|
49
|
+
- `b` — position of the item that should sort **after** the result (or `null` for end-of-list)
|
|
50
|
+
|
|
51
|
+
**How it picks a key:**
|
|
52
|
+
|
|
53
|
+
1. **Both null** → return `"a0"` (the default midpoint)
|
|
54
|
+
2. **`a` is null** → find the integer part of `b` and decrement it. If `b` is already at its integer part (no fractional suffix), step to the previous integer
|
|
55
|
+
3. **`b` is null** → find the integer part of `a` and increment it. If the integer overflows, extend the fractional part instead
|
|
56
|
+
4. **Both non-null, same integer part** → compute a fractional midpoint between their fractional suffixes
|
|
57
|
+
5. **Both non-null, different integer parts** → increment `a`'s integer. If the result is less than `b`, use it directly; otherwise extend `a`'s fractional part
|
|
58
|
+
|
|
59
|
+
The `midpoint` helper finds a string that sorts between two fractional strings by walking their digits and picking the midpoint digit. When consecutive digits leave no room, it recurses deeper — this is what allows infinite subdivision without collisions.
|
|
60
|
+
|
|
61
|
+
## Implementation
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const BASE_62_DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
|
65
|
+
|
|
66
|
+
function midpoint(a: string, b: string | null, digits: string = BASE_62_DIGITS): string {
|
|
67
|
+
const zero = digits[0]
|
|
68
|
+
if (b != null && a >= b) throw new Error(a + ' >= ' + b)
|
|
69
|
+
if (a.slice(-1) === zero || (b && b.slice(-1) === zero)) throw new Error('trailing zero')
|
|
70
|
+
|
|
71
|
+
if (b) {
|
|
72
|
+
let n = 0
|
|
73
|
+
while ((a[n] || zero) === b[n]) n++
|
|
74
|
+
if (n > 0) return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const digitA = a ? digits.indexOf(a[0]) : 0
|
|
78
|
+
const digitB = b != null ? digits.indexOf(b[0]) : digits.length
|
|
79
|
+
|
|
80
|
+
if (digitB - digitA > 1) {
|
|
81
|
+
return digits[Math.round(0.5 * (digitA + digitB))]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (b && b.length > 1) {
|
|
85
|
+
return b.slice(0, 1)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return digits[digitA] + midpoint(a.slice(1), null, digits)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getIntegerLength(head: string): number {
|
|
92
|
+
if (head >= 'a' && head <= 'z') return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2
|
|
93
|
+
if (head >= 'A' && head <= 'Z') return 'Z'.charCodeAt(0) - head.charCodeAt(0) + 2
|
|
94
|
+
throw new Error('invalid order key head: ' + head)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getIntegerPart(key: string): string {
|
|
98
|
+
const len = getIntegerLength(key[0])
|
|
99
|
+
if (len > key.length) throw new Error('invalid order key: ' + key)
|
|
100
|
+
return key.slice(0, len)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateInteger(int: string): void {
|
|
104
|
+
if (int.length !== getIntegerLength(int[0])) throw new Error('invalid integer part of order key: ' + int)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateOrderKey(key: string, digits: string = BASE_62_DIGITS): void {
|
|
108
|
+
if (key === 'A' + digits[0].repeat(26)) throw new Error('invalid order key: ' + key)
|
|
109
|
+
const i = getIntegerPart(key)
|
|
110
|
+
const f = key.slice(i.length)
|
|
111
|
+
if (f.slice(-1) === digits[0]) throw new Error('invalid order key: ' + key)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function incrementInteger(x: string, digits: string = BASE_62_DIGITS): string | null {
|
|
115
|
+
validateInteger(x)
|
|
116
|
+
const [head, ...digs] = x.split('')
|
|
117
|
+
let carry = true
|
|
118
|
+
for (let i = digs.length - 1; carry && i >= 0; i--) {
|
|
119
|
+
const d = digits.indexOf(digs[i]) + 1
|
|
120
|
+
if (d === digits.length) {
|
|
121
|
+
digs[i] = digits[0]
|
|
122
|
+
} else {
|
|
123
|
+
digs[i] = digits[d]
|
|
124
|
+
carry = false
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (carry) {
|
|
128
|
+
if (head === 'Z') return 'a' + digits[0]
|
|
129
|
+
if (head === 'z') return null
|
|
130
|
+
const h = String.fromCharCode(head.charCodeAt(0) + 1)
|
|
131
|
+
if (h > 'a') digs.push(digits[0])
|
|
132
|
+
else digs.pop()
|
|
133
|
+
return h + digs.join('')
|
|
134
|
+
}
|
|
135
|
+
return head + digs.join('')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function decrementInteger(x: string, digits: string = BASE_62_DIGITS): string | null {
|
|
139
|
+
validateInteger(x)
|
|
140
|
+
const [head, ...digs] = x.split('')
|
|
141
|
+
let borrow = true
|
|
142
|
+
for (let i = digs.length - 1; borrow && i >= 0; i--) {
|
|
143
|
+
const d = digits.indexOf(digs[i]) - 1
|
|
144
|
+
if (d === -1) {
|
|
145
|
+
digs[i] = digits.slice(-1)
|
|
146
|
+
} else {
|
|
147
|
+
digs[i] = digits[d]
|
|
148
|
+
borrow = false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (borrow) {
|
|
152
|
+
if (head === 'a') return 'Z' + digits.slice(-1)
|
|
153
|
+
if (head === 'A') return null
|
|
154
|
+
const h = String.fromCharCode(head.charCodeAt(0) - 1)
|
|
155
|
+
if (h < 'Z') digs.push(digits.slice(-1))
|
|
156
|
+
else digs.pop()
|
|
157
|
+
return h + digs.join('')
|
|
158
|
+
}
|
|
159
|
+
return head + digs.join('')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function generateKeyBetween(a: string | null, b: string | null): string {
|
|
163
|
+
const digits = BASE_62_DIGITS
|
|
164
|
+
if (a != null) validateOrderKey(a, digits)
|
|
165
|
+
if (b != null) validateOrderKey(b, digits)
|
|
166
|
+
if (a != null && b != null && a >= b) throw new Error(a + ' >= ' + b)
|
|
167
|
+
|
|
168
|
+
if (a == null) {
|
|
169
|
+
if (b == null) return 'a' + digits[0]
|
|
170
|
+
const ib = getIntegerPart(b)
|
|
171
|
+
const fb = b.slice(ib.length)
|
|
172
|
+
if (ib === 'A' + digits[0].repeat(26)) return ib + midpoint('', fb, digits)
|
|
173
|
+
if (ib < b) return ib
|
|
174
|
+
const res = decrementInteger(ib, digits)
|
|
175
|
+
if (res == null) throw new Error('cannot decrement any more')
|
|
176
|
+
return res
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (b == null) {
|
|
180
|
+
const ia = getIntegerPart(a)
|
|
181
|
+
const fa = a.slice(ia.length)
|
|
182
|
+
const i = incrementInteger(ia, digits)
|
|
183
|
+
return i == null ? ia + midpoint(fa, null, digits) : i
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const ia = getIntegerPart(a)
|
|
187
|
+
const fa = a.slice(ia.length)
|
|
188
|
+
const ib = getIntegerPart(b)
|
|
189
|
+
const fb = b.slice(ib.length)
|
|
190
|
+
if (ia === ib) return ia + midpoint(fa, fb, digits)
|
|
191
|
+
const i = incrementInteger(ia, digits)
|
|
192
|
+
if (i == null) throw new Error('cannot increment any more')
|
|
193
|
+
if (i < b) return i
|
|
194
|
+
return ia + midpoint(fa, null, digits)
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Cases
|
|
199
|
+
|
|
200
|
+
There are four cases when computing a position. In every case, `previousItem` is the item directly before the target drop position and `nextItem` is the item directly after.
|
|
201
|
+
|
|
202
|
+
### 1. Insert between two items
|
|
203
|
+
|
|
204
|
+
The most common reorder — dragging an item between two existing neighbors.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// Items: [A:"a0", B:"a1", C:"a2"]
|
|
208
|
+
// Drop task between A and B
|
|
209
|
+
const position = generateKeyBetween('a0', 'a1') // → "a0V"
|
|
210
|
+
// Result: [A:"a0", NEW:"a0V", B:"a1", C:"a2"]
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 2. Insert at the start
|
|
214
|
+
|
|
215
|
+
Dragging an item to the top of a list. `previousItem` is `null`, `nextItem` is the current first item.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// Items: [A:"a0", B:"a1", C:"a2"]
|
|
219
|
+
// Drop task before A (beginning of list)
|
|
220
|
+
const position = generateKeyBetween(null, 'a0') // → "Zz"
|
|
221
|
+
// Result: [NEW:"Zz", A:"a0", B:"a1", C:"a2"]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 3. Insert at the end
|
|
225
|
+
|
|
226
|
+
Dragging an item to the bottom, or appending a newly created item. `previousItem` is the current last item, `nextItem` is `null`.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// Items: [A:"a0", B:"a1", C:"a2"]
|
|
230
|
+
// Drop task after C (end of list)
|
|
231
|
+
const position = generateKeyBetween('a2', null) // → "a3"
|
|
232
|
+
// Result: [A:"a0", B:"a1", C:"a2", NEW:"a3"]
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 4. Insert into an empty list
|
|
236
|
+
|
|
237
|
+
Creating the first item in a container, or moving an item into an empty project/area. Both `previousItem` and `nextItem` are `null`.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
// Items: []
|
|
241
|
+
// First task in an empty project
|
|
242
|
+
const position = generateKeyBetween(null, null) // → "a0"
|
|
243
|
+
// Result: [NEW:"a0"]
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Deriving previousItem and nextItem from a drag event
|
|
247
|
+
|
|
248
|
+
The frontend's `onDragEnd` handler receives the `active` item (being dragged) and the `over` item (the drop target). Translate these into `previousItem` / `nextItem` by finding the insertion index in the **filtered** list (excluding the dragged item):
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
function onDragEnd(active, over, items) {
|
|
252
|
+
// Remove the dragged item from the list to get the remaining order
|
|
253
|
+
const filtered = items.filter(item => item.id !== active.id)
|
|
254
|
+
const overIndex = filtered.findIndex(item => item.id === over.id)
|
|
255
|
+
|
|
256
|
+
// Determine drop direction: did the item move up or down?
|
|
257
|
+
const oldIndex = items.findIndex(item => item.id === active.id)
|
|
258
|
+
const rawOverIndex = items.findIndex(item => item.id === over.id)
|
|
259
|
+
const insertAfter = rawOverIndex > oldIndex
|
|
260
|
+
|
|
261
|
+
let previousItem: Item | null
|
|
262
|
+
let nextItem: Item | null
|
|
263
|
+
|
|
264
|
+
if (insertAfter) {
|
|
265
|
+
// Dropped below the "over" item
|
|
266
|
+
previousItem = filtered[overIndex] ?? null
|
|
267
|
+
nextItem = filtered[overIndex + 1] ?? null
|
|
268
|
+
} else {
|
|
269
|
+
// Dropped above the "over" item
|
|
270
|
+
previousItem = filtered[overIndex - 1] ?? null
|
|
271
|
+
nextItem = filtered[overIndex] ?? null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const position = generateKeyBetween(
|
|
275
|
+
previousItem?.listPosition ?? null,
|
|
276
|
+
nextItem?.listPosition ?? null
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
// Optimistic update + API call
|
|
280
|
+
reorderTask(active.id, { listPosition: position })
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## New item creation
|
|
285
|
+
|
|
286
|
+
When creating a new task, project, or area, append it to the end of its target list:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
// Fetch the last item in the container
|
|
290
|
+
const lastTask = tasks.at(-1)
|
|
291
|
+
const listPosition = generateKeyBetween(lastTask?.listPosition ?? null, null)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
To prepend to the beginning instead (e.g. Inbox where new items appear at top):
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const firstTask = tasks.at(0)
|
|
298
|
+
const listPosition = generateKeyBetween(null, firstTask?.listPosition ?? null)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Cross-container moves
|
|
302
|
+
|
|
303
|
+
When a task moves between containers (e.g. from inbox to a project), the `listPosition` must be recomputed relative to the **destination** list, not the source. The task's old `listPosition` is irrelevant once the container changes.
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
// Task dragged from Inbox onto Project #5
|
|
307
|
+
// Get the tasks already in Project #5, ordered by listPosition
|
|
308
|
+
const projectTasks = tasks
|
|
309
|
+
.filter(t => t.projectId === 5)
|
|
310
|
+
.sort((a, b) => a.listPosition.localeCompare(b.listPosition))
|
|
311
|
+
|
|
312
|
+
// Append to end of the project's task list
|
|
313
|
+
const lastTask = projectTasks.at(-1)
|
|
314
|
+
const listPosition = generateKeyBetween(lastTask?.listPosition ?? null, null)
|
|
315
|
+
|
|
316
|
+
reorderTask(task.id, { listPosition, projectId: 5, areaId: null })
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Querying in order
|
|
320
|
+
|
|
321
|
+
Position columns sort correctly with a plain `ORDER BY` since they are lexicographic strings:
|
|
322
|
+
|
|
323
|
+
```sql
|
|
324
|
+
SELECT * FROM tasks
|
|
325
|
+
WHERE userId = ? AND projectId = ? AND completedDate IS NULL AND deletedDate IS NULL
|
|
326
|
+
ORDER BY listPosition
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
No casting or special collation is needed.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Sidebar
|
|
2
|
+
|
|
3
|
+
The sidebar is the primary navigation surface. It has two sections: a fixed set of built-in views at the top, and a user-managed list of areas and projects below.
|
|
4
|
+
|
|
5
|
+
## Built-in Views
|
|
6
|
+
|
|
7
|
+
Four fixed links at the top of the sidebar, in this order:
|
|
8
|
+
|
|
9
|
+
1. **Inbox** — tasks not assigned to a project, area, or scheduled date (V1)
|
|
10
|
+
2. **Today** — tasks scheduled for today + overdue tasks (V2)
|
|
11
|
+
3. **Upcoming** — tasks grouped by scheduled date, looking forward (V3)
|
|
12
|
+
4. **Completed** — all completed tasks and projects, reverse chronological (V4)
|
|
13
|
+
|
|
14
|
+
These are not reorderable or removable.
|
|
15
|
+
|
|
16
|
+
## User Content
|
|
17
|
+
|
|
18
|
+
Below the built-in views, a single mixed list of **areas** and **top-level projects** (projects with no `areaId`), sorted by their shared `position` column. Areas and top-level projects interleave freely based on position value.
|
|
19
|
+
|
|
20
|
+
### Area Rendering
|
|
21
|
+
|
|
22
|
+
- An area row shows its title
|
|
23
|
+
- Areas are expandable/collapsible — expanding an area reveals its child projects nested underneath
|
|
24
|
+
- Collapsed state persists across sessions
|
|
25
|
+
- An area's child projects are sorted by `position` within the area
|
|
26
|
+
|
|
27
|
+
### Project Rendering
|
|
28
|
+
|
|
29
|
+
- A project row shows its title
|
|
30
|
+
- Projects with an `areaId` appear nested under their parent area (visible only when the area is expanded)
|
|
31
|
+
- Projects with no `areaId` appear at the top level alongside areas
|
|
32
|
+
- Completed projects are excluded from the sidebar entirely — they move to the Completed view (V4)
|
|
33
|
+
- Deleted projects and areas are excluded
|
|
34
|
+
|
|
35
|
+
## Active State
|
|
36
|
+
|
|
37
|
+
The currently active view, project, or area is visually highlighted in the sidebar. Only one item is active at a time. Clicking a sidebar item navigates to it and updates the highlight.
|
|
38
|
+
|
|
39
|
+
## Interactions
|
|
40
|
+
|
|
41
|
+
### Navigation
|
|
42
|
+
|
|
43
|
+
- Click a built-in view link to navigate to that view
|
|
44
|
+
- Click an area to navigate to its detail view (standalone tasks + projects)
|
|
45
|
+
- Click a project to navigate to its task list (PR1)
|
|
46
|
+
|
|
47
|
+
### Drag-and-Drop
|
|
48
|
+
|
|
49
|
+
All drag-and-drop behavior references the shared position space described in [position-ordering.md](position-ordering.md) and the implementation in [drag-and-drop.md](drag-and-drop.md).
|
|
50
|
+
|
|
51
|
+
- **Reorder areas** in the sidebar (AR5) — drag an area to a new position among other top-level items
|
|
52
|
+
- **Reorder top-level projects** in the sidebar (PR8) — drag a project to a new position among other top-level items
|
|
53
|
+
- **Move a project into an area** (PR4) — drag a project onto an area to nest it; updates `projects.areaId` and `projects.position`
|
|
54
|
+
- **Move a project out of an area** (PR4) — drag a nested project out to the top level; sets `areaId` to null and updates position
|
|
55
|
+
- **Move a task to a project or area** (T4) — drag a task from any list view onto a sidebar project or area to reassign it
|
|
56
|
+
|
|
57
|
+
### Create
|
|
58
|
+
|
|
59
|
+
- Inline creation for new areas (AR2) and new projects (PR2) from the sidebar
|
|
60
|
+
- New items are appended to the end of the list by default
|
|
61
|
+
|
|
62
|
+
### Context Menu
|
|
63
|
+
|
|
64
|
+
- Right-click an area for options: edit title (AR3), delete (AR4)
|
|
65
|
+
- Right-click a project for options: edit (PR3), complete (PR5), delete (PR7)
|