@aurostack/stacks 0.1.0
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/LICENSE +21 -0
- package/README.md +209 -0
- package/cli/src/extract.mjs +165 -0
- package/cli/src/fsx.mjs +197 -0
- package/cli/src/generate.mjs +235 -0
- package/cli/src/hooks.mjs +55 -0
- package/cli/src/manifest.mjs +163 -0
- package/cli/src/strip.mjs +208 -0
- package/cli/src/tokens.mjs +102 -0
- package/cli/stack.mjs +672 -0
- package/package.json +37 -0
- package/templates/nest-api/files/.bin/db.ts +123 -0
- package/templates/nest-api/files/.bin/dc.ts +74 -0
- package/templates/nest-api/files/.bin/helpers.ts +49 -0
- package/templates/nest-api/files/.bin/secrets.ts +68 -0
- package/templates/nest-api/files/.bin/start.ts +47 -0
- package/templates/nest-api/files/.dockerignore +7 -0
- package/templates/nest-api/files/.env.example +91 -0
- package/templates/nest-api/files/.env.test.example +75 -0
- package/templates/nest-api/files/.forgejo/workflows/build-and-push.yml +41 -0
- package/templates/nest-api/files/.forgejo/workflows/test-e2e.yml +83 -0
- package/templates/nest-api/files/.forgejo/workflows/test.yml +53 -0
- package/templates/nest-api/files/.npmrc.example +1 -0
- package/templates/nest-api/files/.nvmrc +1 -0
- package/templates/nest-api/files/.prettierrc +8 -0
- package/templates/nest-api/files/.swcrc +15 -0
- package/templates/nest-api/files/.watchmanconfig +3 -0
- package/templates/nest-api/files/.yarnrc.yml +1 -0
- package/templates/nest-api/files/Dockerfile +40 -0
- package/templates/nest-api/files/__mocks__/activation-twofa.js +7 -0
- package/templates/nest-api/files/__mocks__/activation.js +7 -0
- package/templates/nest-api/files/__mocks__/forgot-password.js +7 -0
- package/templates/nest-api/files/__mocks__/main-layout.js +7 -0
- package/templates/nest-api/files/__mocks__/password-changed.js +7 -0
- package/templates/nest-api/files/__mocks__/reset-password.js +7 -0
- package/templates/nest-api/files/__mocks__/set-password.js +7 -0
- package/templates/nest-api/files/__mocks__/welcome.js +7 -0
- package/templates/nest-api/files/_gitignore +62 -0
- package/templates/nest-api/files/compose.test.yml +33 -0
- package/templates/nest-api/files/compose.yml +39 -0
- package/templates/nest-api/files/emails/activation-twofa.tsx +58 -0
- package/templates/nest-api/files/emails/activation.tsx +36 -0
- package/templates/nest-api/files/emails/forgot-password.tsx +29 -0
- package/templates/nest-api/files/emails/main-layout.tsx +134 -0
- package/templates/nest-api/files/emails/password-changed.tsx +24 -0
- package/templates/nest-api/files/emails/reset-password.tsx +24 -0
- package/templates/nest-api/files/emails/set-password.tsx +25 -0
- package/templates/nest-api/files/emails/welcome.tsx +39 -0
- package/templates/nest-api/files/eslint.config.mjs +28 -0
- package/templates/nest-api/files/global.d.ts +5 -0
- package/templates/nest-api/files/nest-cli.json +8 -0
- package/templates/nest-api/files/package.json +150 -0
- package/templates/nest-api/files/prisma/schema/auth.prisma +75 -0
- package/templates/nest-api/files/prisma/schema/base.prisma +17 -0
- package/templates/nest-api/files/prisma/schema/enum.prisma +17 -0
- package/templates/nest-api/files/prisma/schema/notification.prisma +14 -0
- package/templates/nest-api/files/prisma.config.ts +13 -0
- package/templates/nest-api/files/public/.gitkeep +0 -0
- package/templates/nest-api/files/seeders/index.ts +27 -0
- package/templates/nest-api/files/seeders/util.ts +88 -0
- package/templates/nest-api/files/src/__tests__/app.controller.spec.ts +29 -0
- package/templates/nest-api/files/src/app.controller.ts +40 -0
- package/templates/nest-api/files/src/app.module.ts +268 -0
- package/templates/nest-api/files/src/app.resolver.ts +32 -0
- package/templates/nest-api/files/src/app.setup.ts +178 -0
- package/templates/nest-api/files/src/common/__tests__/auth.service.spec.ts +31 -0
- package/templates/nest-api/files/src/common/__tests__/cache.service.spec.ts +214 -0
- package/templates/nest-api/files/src/common/__tests__/config.service.spec.ts +56 -0
- package/templates/nest-api/files/src/common/__tests__/generator.service.spec.ts +114 -0
- package/templates/nest-api/files/src/common/__tests__/health.controller.spec.ts +72 -0
- package/templates/nest-api/files/src/common/__tests__/helpers.spec.ts +124 -0
- package/templates/nest-api/files/src/common/__tests__/mail.service.spec.ts +75 -0
- package/templates/nest-api/files/src/common/__tests__/prisma.indicator.spec.ts +39 -0
- package/templates/nest-api/files/src/common/__tests__/prisma.service.spec.ts +15 -0
- package/templates/nest-api/files/src/common/__tests__/records.spec.ts +87 -0
- package/templates/nest-api/files/src/common/__tests__/schemas.spec.ts +142 -0
- package/templates/nest-api/files/src/common/__tests__/throttler-storage.service.spec.ts +80 -0
- package/templates/nest-api/files/src/common/common.module.ts +72 -0
- package/templates/nest-api/files/src/common/constants/index.ts +2 -0
- package/templates/nest-api/files/src/common/constants/options.ts +52 -0
- package/templates/nest-api/files/src/common/constants/pino.ts +24 -0
- package/templates/nest-api/files/src/common/controllers/health.controller.ts +54 -0
- package/templates/nest-api/files/src/common/controllers/index.ts +2 -0
- package/templates/nest-api/files/src/common/controllers/metrics.controller.ts +21 -0
- package/templates/nest-api/files/src/common/decorators/index.ts +2 -0
- package/templates/nest-api/files/src/common/decorators/misc.decorator.ts +49 -0
- package/templates/nest-api/files/src/common/decorators/multi-part.decorator.ts +6 -0
- package/templates/nest-api/files/src/common/dto/filters.dto.ts +25 -0
- package/templates/nest-api/files/src/common/dto/index.ts +1 -0
- package/templates/nest-api/files/src/common/entity/index.ts +2 -0
- package/templates/nest-api/files/src/common/entity/ok.entity.ts +11 -0
- package/templates/nest-api/files/src/common/entity/pagination.entity.ts +24 -0
- package/templates/nest-api/files/src/common/exceptions/index.ts +1 -0
- package/templates/nest-api/files/src/common/exceptions/prisma.exception.ts +57 -0
- package/templates/nest-api/files/src/common/guards/feature-flag.guard.ts +51 -0
- package/templates/nest-api/files/src/common/guards/index.ts +1 -0
- package/templates/nest-api/files/src/common/interceptors/index.ts +1 -0
- package/templates/nest-api/files/src/common/interceptors/metrics.interceptor.ts +37 -0
- package/templates/nest-api/files/src/common/misc/helpers.ts +26 -0
- package/templates/nest-api/files/src/common/misc/index.ts +3 -0
- package/templates/nest-api/files/src/common/misc/origin-builder.ts +56 -0
- package/templates/nest-api/files/src/common/misc/prisma.indicator.ts +24 -0
- package/templates/nest-api/files/src/common/modules/index.ts +3 -0
- package/templates/nest-api/files/src/common/modules/logger.module.ts +104 -0
- package/templates/nest-api/files/src/common/modules/metrics.module.ts +28 -0
- package/templates/nest-api/files/src/common/modules/queue.module.ts +23 -0
- package/templates/nest-api/files/src/common/processors/index.ts +1 -0
- package/templates/nest-api/files/src/common/processors/mail/index.ts +47 -0
- package/templates/nest-api/files/src/common/processors/mail/process.ts +225 -0
- package/templates/nest-api/files/src/common/schemas/fields.ts +76 -0
- package/templates/nest-api/files/src/common/schemas/index.ts +1 -0
- package/templates/nest-api/files/src/common/services/auth-rate-limit.storage.ts +79 -0
- package/templates/nest-api/files/src/common/services/auth.service.ts +16 -0
- package/templates/nest-api/files/src/common/services/cache.service.ts +118 -0
- package/templates/nest-api/files/src/common/services/config.service.ts +388 -0
- package/templates/nest-api/files/src/common/services/feature-flag.service.ts +117 -0
- package/templates/nest-api/files/src/common/services/generator.service.ts +54 -0
- package/templates/nest-api/files/src/common/services/index.ts +10 -0
- package/templates/nest-api/files/src/common/services/logger.service.ts +14 -0
- package/templates/nest-api/files/src/common/services/mail.service.ts +44 -0
- package/templates/nest-api/files/src/common/services/prisma.service.ts +106 -0
- package/templates/nest-api/files/src/common/services/throttler-storage.service.ts +112 -0
- package/templates/nest-api/files/src/common/types/email.ts +55 -0
- package/templates/nest-api/files/src/common/types/index.ts +3 -0
- package/templates/nest-api/files/src/common/types/misc.ts +20 -0
- package/templates/nest-api/files/src/common/types/prisma.ts +18 -0
- package/templates/nest-api/files/src/common/utils/enum.ts +39 -0
- package/templates/nest-api/files/src/common/utils/helpers.ts +139 -0
- package/templates/nest-api/files/src/common/utils/index.ts +5 -0
- package/templates/nest-api/files/src/common/utils/ip.ts +22 -0
- package/templates/nest-api/files/src/common/utils/records.ts +66 -0
- package/templates/nest-api/files/src/common/utils/redis.keys.ts +31 -0
- package/templates/nest-api/files/src/lib/__tests__/access.spec.ts +33 -0
- package/templates/nest-api/files/src/lib/access.ts +114 -0
- package/templates/nest-api/files/src/lib/auth.ts +76 -0
- package/templates/nest-api/files/src/lib/rate-limit.ts +20 -0
- package/templates/nest-api/files/src/main.ts +28 -0
- package/templates/nest-api/files/src/media/__tests__/media.service.spec.ts +60 -0
- package/templates/nest-api/files/src/media/__tests__/s3.service.spec.ts +159 -0
- package/templates/nest-api/files/src/media/entity/file.entity.ts +7 -0
- package/templates/nest-api/files/src/media/entity/index.ts +1 -0
- package/templates/nest-api/files/src/media/media.module.ts +20 -0
- package/templates/nest-api/files/src/media/processors/index.ts +44 -0
- package/templates/nest-api/files/src/media/services/index.ts +2 -0
- package/templates/nest-api/files/src/media/services/media.service.ts +29 -0
- package/templates/nest-api/files/src/media/services/s3.service.ts +168 -0
- package/templates/nest-api/files/src/notifications/controllers/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/controllers/notifications.controller.ts +72 -0
- package/templates/nest-api/files/src/notifications/dto/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/dto/notification.dto.ts +11 -0
- package/templates/nest-api/files/src/notifications/entity/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/entity/notification.entity.ts +37 -0
- package/templates/nest-api/files/src/notifications/notifications.module.ts +11 -0
- package/templates/nest-api/files/src/notifications/services/index.ts +1 -0
- package/templates/nest-api/files/src/notifications/services/notifications.service.ts +98 -0
- package/templates/nest-api/files/src/realtime/__tests__/events.gateway.spec.ts +88 -0
- package/templates/nest-api/files/src/realtime/__tests__/rooms.spec.ts +29 -0
- package/templates/nest-api/files/src/realtime/__tests__/ws-permissions.guard.spec.ts +54 -0
- package/templates/nest-api/files/src/realtime/adapters/redis-io.adapter.ts +57 -0
- package/templates/nest-api/files/src/realtime/constants/index.ts +68 -0
- package/templates/nest-api/files/src/realtime/decorators/index.ts +1 -0
- package/templates/nest-api/files/src/realtime/decorators/ws-permissions.decorator.ts +14 -0
- package/templates/nest-api/files/src/realtime/gateways/events.gateway.ts +187 -0
- package/templates/nest-api/files/src/realtime/guards/index.ts +2 -0
- package/templates/nest-api/files/src/realtime/guards/ws-auth.guard.ts +20 -0
- package/templates/nest-api/files/src/realtime/guards/ws-permissions.guard.ts +35 -0
- package/templates/nest-api/files/src/realtime/misc/session-from-socket.ts +54 -0
- package/templates/nest-api/files/src/realtime/realtime.module.ts +26 -0
- package/templates/nest-api/files/src/realtime/services/presence.service.ts +56 -0
- package/templates/nest-api/files/src/realtime/services/realtime.service.ts +72 -0
- package/templates/nest-api/files/src/users/__tests__/current-user.controller.spec.ts +77 -0
- package/templates/nest-api/files/src/users/__tests__/current-user.service.spec.ts +159 -0
- package/templates/nest-api/files/src/users/__tests__/user.entity.spec.ts +175 -0
- package/templates/nest-api/files/src/users/__tests__/users.controller.spec.ts +89 -0
- package/templates/nest-api/files/src/users/__tests__/users.service.spec.ts +398 -0
- package/templates/nest-api/files/src/users/controllers/current-user.controller.ts +82 -0
- package/templates/nest-api/files/src/users/controllers/index.ts +2 -0
- package/templates/nest-api/files/src/users/controllers/users.controller.ts +40 -0
- package/templates/nest-api/files/src/users/decorators/index.ts +1 -0
- package/templates/nest-api/files/src/users/decorators/permissions.decorator.ts +24 -0
- package/templates/nest-api/files/src/users/dto/avatar.dto.ts +13 -0
- package/templates/nest-api/files/src/users/dto/filters.dto.ts +8 -0
- package/templates/nest-api/files/src/users/dto/index.ts +2 -0
- package/templates/nest-api/files/src/users/entity/index.ts +1 -0
- package/templates/nest-api/files/src/users/entity/user.entity.ts +53 -0
- package/templates/nest-api/files/src/users/services/current-user.service.ts +46 -0
- package/templates/nest-api/files/src/users/services/index.ts +2 -0
- package/templates/nest-api/files/src/users/services/users.service.ts +88 -0
- package/templates/nest-api/files/src/users/users.module.ts +10 -0
- package/templates/nest-api/files/test/app.spec.ts +102 -0
- package/templates/nest-api/files/test/auth.spec.ts +108 -0
- package/templates/nest-api/files/test/current-user.spec.ts +154 -0
- package/templates/nest-api/files/test/factory/app.ts +172 -0
- package/templates/nest-api/files/test/factory/constants.ts +22 -0
- package/templates/nest-api/files/test/factory/dto.ts +27 -0
- package/templates/nest-api/files/test/factory/entity.ts +70 -0
- package/templates/nest-api/files/test/factory/gen.ts +75 -0
- package/templates/nest-api/files/test/factory/util.ts +21 -0
- package/templates/nest-api/files/test/notifications.spec.ts +176 -0
- package/templates/nest-api/files/test/rate-limit.spec.ts +180 -0
- package/templates/nest-api/files/test/setup.ts +1 -0
- package/templates/nest-api/files/test/users.spec.ts +437 -0
- package/templates/nest-api/files/tsconfig.build.json +13 -0
- package/templates/nest-api/files/tsconfig.json +35 -0
- package/templates/nest-api/files/vitest.config.ts +53 -0
- package/templates/nest-api/template.json +452 -0
- package/templates/node-worker/files/.bin/db.ts +123 -0
- package/templates/node-worker/files/.bin/helpers.ts +49 -0
- package/templates/node-worker/files/.bin/secrets.ts +68 -0
- package/templates/node-worker/files/.dockerignore +5 -0
- package/templates/node-worker/files/.env.example +21 -0
- package/templates/node-worker/files/.forgejo/workflows/build-and-push.yml +41 -0
- package/templates/node-worker/files/.nvmrc +1 -0
- package/templates/node-worker/files/.prettierignore +4 -0
- package/templates/node-worker/files/.prettierrc +8 -0
- package/templates/node-worker/files/.yarnrc.yml +5 -0
- package/templates/node-worker/files/Dockerfile +63 -0
- package/templates/node-worker/files/Dockerfile.dev +59 -0
- package/templates/node-worker/files/_gitignore +48 -0
- package/templates/node-worker/files/ecosystem.config.cjs +10 -0
- package/templates/node-worker/files/eslint.config.js +37 -0
- package/templates/node-worker/files/package.json +67 -0
- package/templates/node-worker/files/prisma/_gitignore +1 -0
- package/templates/node-worker/files/prisma/schema/base.prisma +36 -0
- package/templates/node-worker/files/prisma.config.ts +13 -0
- package/templates/node-worker/files/src/config.ts +55 -0
- package/templates/node-worker/files/src/db.ts +118 -0
- package/templates/node-worker/files/src/index.ts +7 -0
- package/templates/node-worker/files/src/logger.ts +45 -0
- package/templates/node-worker/files/src/types.ts +23 -0
- package/templates/node-worker/files/src/utils/browser.ts +16 -0
- package/templates/node-worker/files/src/utils/date.ts +68 -0
- package/templates/node-worker/files/src/utils/helpers.ts +17 -0
- package/templates/node-worker/files/src/utils/number.ts +47 -0
- package/templates/node-worker/files/src/utils/worker.ts +29 -0
- package/templates/node-worker/files/src/workers/constants.ts +6 -0
- package/templates/node-worker/files/src/workers/example/index.ts +27 -0
- package/templates/node-worker/files/src/workers/index.ts +1 -0
- package/templates/node-worker/files/tsconfig.json +30 -0
- package/templates/node-worker/template.json +135 -0
- package/templates/py-worker/files/.dockerignore +176 -0
- package/templates/py-worker/files/.env.example +20 -0
- package/templates/py-worker/files/.forgejo/workflows/build-amd-push.yml +39 -0
- package/templates/py-worker/files/Dockerfile +10 -0
- package/templates/py-worker/files/README.md +3 -0
- package/templates/py-worker/files/_gitignore +176 -0
- package/templates/py-worker/files/config.py +89 -0
- package/templates/py-worker/files/models/decorators.py +42 -0
- package/templates/py-worker/files/models/enums.py +32 -0
- package/templates/py-worker/files/models/mixins.py +61 -0
- package/templates/py-worker/files/requirements.txt +18 -0
- package/templates/py-worker/files/utils/db.py +67 -0
- package/templates/py-worker/files/utils/logger.py +97 -0
- package/templates/py-worker/files/utils/misc.py +45 -0
- package/templates/py-worker/files/utils/worker.py +80 -0
- package/templates/py-worker/files/worker.py +41 -0
- package/templates/py-worker/files/workers/hello/process.py +22 -0
- package/templates/py-worker/template.json +87 -0
- package/templates/react-app/README.md +78 -0
- package/templates/react-app/derive.sh +128 -0
- package/templates/react-app/files/.bin/openapi.js +32 -0
- package/templates/react-app/files/.bin/secrets.js +47 -0
- package/templates/react-app/files/.dockerignore +16 -0
- package/templates/react-app/files/.editorconfig +12 -0
- package/templates/react-app/files/.env.example +34 -0
- package/templates/react-app/files/.forgejo/workflows/build-and-push.yml +55 -0
- package/templates/react-app/files/.forgejo/workflows/ci.yml +39 -0
- package/templates/react-app/files/.husky/pre-commit +1 -0
- package/templates/react-app/files/.nvmrc +1 -0
- package/templates/react-app/files/.prettierignore +6 -0
- package/templates/react-app/files/.prettierrc +9 -0
- package/templates/react-app/files/.yarnrc.yml +2 -0
- package/templates/react-app/files/Dockerfile +52 -0
- package/templates/react-app/files/_gitignore +33 -0
- package/templates/react-app/files/build.sh +53 -0
- package/templates/react-app/files/docker/nginx.conf +134 -0
- package/templates/react-app/files/eslint.config.mjs +75 -0
- package/templates/react-app/files/index.html +62 -0
- package/templates/react-app/files/openapi-ts.config.ts +22 -0
- package/templates/react-app/files/package.json +80 -0
- package/templates/react-app/files/src/app/nav.tsx +32 -0
- package/templates/react-app/files/src/app/providers.tsx +39 -0
- package/templates/react-app/files/src/app/router.tsx +142 -0
- package/templates/react-app/files/src/app/store.ts +20 -0
- package/templates/react-app/files/src/features/auth/components/auth-card.tsx +23 -0
- package/templates/react-app/files/src/features/auth/components/checkbox-field.tsx +32 -0
- package/templates/react-app/files/src/features/auth/components/google-button.tsx +41 -0
- package/templates/react-app/files/src/features/auth/components/or-divider.tsx +9 -0
- package/templates/react-app/files/src/features/auth/components/password-field.tsx +70 -0
- package/templates/react-app/files/src/features/auth/components/submit-button.tsx +16 -0
- package/templates/react-app/files/src/features/auth/lib/auth-actions.ts +68 -0
- package/templates/react-app/files/src/features/auth/lib/password-strength.ts +20 -0
- package/templates/react-app/files/src/features/auth/lib/redirect.ts +6 -0
- package/templates/react-app/files/src/features/auth/lib/schemas.ts +37 -0
- package/templates/react-app/files/src/features/seo/routes.ts +65 -0
- package/templates/react-app/files/src/lib/env.ts +28 -0
- package/templates/react-app/files/src/lib/paths.ts +58 -0
- package/templates/react-app/files/src/main.tsx +19 -0
- package/templates/react-app/files/src/routes/admin/home.tsx +24 -0
- package/templates/react-app/files/src/routes/auth/forgot-password.tsx +93 -0
- package/templates/react-app/files/src/routes/auth/login.tsx +87 -0
- package/templates/react-app/files/src/routes/auth/reset-password.tsx +83 -0
- package/templates/react-app/files/src/routes/auth/signup.tsx +117 -0
- package/templates/react-app/files/src/routes/auth/verified.tsx +31 -0
- package/templates/react-app/files/src/routes/auth/verify-email.tsx +97 -0
- package/templates/react-app/files/src/routes/home.tsx +17 -0
- package/templates/react-app/files/src/routes/legal/privacy.tsx +26 -0
- package/templates/react-app/files/src/routes/legal/terms.tsx +28 -0
- package/templates/react-app/files/src/routes/marketing/home.tsx +18 -0
- package/templates/react-app/files/src/routes/settings.tsx +10 -0
- package/templates/react-app/files/src/shared/api/base-api.ts +24 -0
- package/templates/react-app/files/src/shared/api/base-query.ts +29 -0
- package/templates/react-app/files/src/shared/api/config.ts +21 -0
- package/templates/react-app/files/src/shared/api/index.ts +15 -0
- package/templates/react-app/files/src/shared/api/realtime-events.ts +34 -0
- package/templates/react-app/files/src/shared/api/socket.ts +41 -0
- package/templates/react-app/files/src/shared/api/store.ts +39 -0
- package/templates/react-app/files/src/shared/api/upload-query.ts +115 -0
- package/templates/react-app/files/src/shared/auth/client.ts +27 -0
- package/templates/react-app/files/src/shared/auth/config.ts +28 -0
- package/templates/react-app/files/src/shared/auth/guards.tsx +75 -0
- package/templates/react-app/files/src/shared/auth/hooks.ts +13 -0
- package/templates/react-app/files/src/shared/auth/index.ts +20 -0
- package/templates/react-app/files/src/shared/auth/logout.ts +23 -0
- package/templates/react-app/files/src/shared/auth/provider.tsx +52 -0
- package/templates/react-app/files/src/shared/auth/redirect.ts +65 -0
- package/templates/react-app/files/src/shared/auth/slice.ts +54 -0
- package/templates/react-app/files/src/shared/hooks/index.ts +6 -0
- package/templates/react-app/files/src/shared/layouts/app-shell/app-shell.tsx +225 -0
- package/templates/react-app/files/src/shared/layouts/app-shell/user-menu.tsx +74 -0
- package/templates/react-app/files/src/shared/layouts/auth-layout.tsx +137 -0
- package/templates/react-app/files/src/shared/layouts/error-boundary.tsx +38 -0
- package/templates/react-app/files/src/shared/layouts/index.ts +23 -0
- package/templates/react-app/files/src/shared/layouts/marketing/marketing-layout.tsx +29 -0
- package/templates/react-app/files/src/shared/layouts/marketing/section-heading.tsx +38 -0
- package/templates/react-app/files/src/shared/layouts/marketing/section.tsx +30 -0
- package/templates/react-app/files/src/shared/layouts/states/app-shell-skeleton.tsx +47 -0
- package/templates/react-app/files/src/shared/layouts/states/empty.tsx +22 -0
- package/templates/react-app/files/src/shared/layouts/states/forbidden.tsx +14 -0
- package/templates/react-app/files/src/shared/layouts/states/loading.tsx +11 -0
- package/templates/react-app/files/src/shared/layouts/states/not-found.tsx +21 -0
- package/templates/react-app/files/src/shared/layouts/states/page-skeleton.tsx +39 -0
- package/templates/react-app/files/src/shared/layouts/states/route-fallback.tsx +26 -0
- package/templates/react-app/files/src/shared/layouts/states/state-screen.tsx +22 -0
- package/templates/react-app/files/src/shared/types/README.md +28 -0
- package/templates/react-app/files/src/shared/types/api/index.ts +9 -0
- package/templates/react-app/files/src/shared/types/api/zod.gen.ts +5 -0
- package/templates/react-app/files/src/shared/types/index.ts +4 -0
- package/templates/react-app/files/src/shared/ui/components/brand/logo.tsx +51 -0
- package/templates/react-app/files/src/shared/ui/components/brand/social-icons.tsx +37 -0
- package/templates/react-app/files/src/shared/ui/components/form/form.tsx +120 -0
- package/templates/react-app/files/src/shared/ui/components/ui/alert.tsx +25 -0
- package/templates/react-app/files/src/shared/ui/components/ui/button.tsx +43 -0
- package/templates/react-app/files/src/shared/ui/components/ui/calendar.tsx +78 -0
- package/templates/react-app/files/src/shared/ui/components/ui/card.tsx +44 -0
- package/templates/react-app/files/src/shared/ui/components/ui/checkbox.tsx +16 -0
- package/templates/react-app/files/src/shared/ui/components/ui/date-picker.tsx +76 -0
- package/templates/react-app/files/src/shared/ui/components/ui/input.tsx +22 -0
- package/templates/react-app/files/src/shared/ui/components/ui/label.tsx +19 -0
- package/templates/react-app/files/src/shared/ui/components/ui/modal.tsx +94 -0
- package/templates/react-app/files/src/shared/ui/components/ui/popover.tsx +31 -0
- package/templates/react-app/files/src/shared/ui/components/ui/progress.tsx +33 -0
- package/templates/react-app/files/src/shared/ui/components/ui/search-input.tsx +69 -0
- package/templates/react-app/files/src/shared/ui/components/ui/select.tsx +80 -0
- package/templates/react-app/files/src/shared/ui/components/ui/skeleton.tsx +12 -0
- package/templates/react-app/files/src/shared/ui/components/ui/sonner.tsx +23 -0
- package/templates/react-app/files/src/shared/ui/components/ui/spinner.tsx +8 -0
- package/templates/react-app/files/src/shared/ui/components/ui/textarea.tsx +22 -0
- package/templates/react-app/files/src/shared/ui/index.ts +45 -0
- package/templates/react-app/files/src/shared/ui/lib/utils.ts +7 -0
- package/templates/react-app/files/src/shared/ui/lib/zod-setup.ts +28 -0
- package/templates/react-app/files/src/shared/ui/meta/brand.ts +18 -0
- package/templates/react-app/files/src/shared/ui/meta/meta-context.ts +30 -0
- package/templates/react-app/files/src/shared/ui/meta/meta-provider.tsx +24 -0
- package/templates/react-app/files/src/shared/ui/meta/page-meta.tsx +69 -0
- package/templates/react-app/files/src/shared/ui/meta/use-meta-config.ts +11 -0
- package/templates/react-app/files/src/shared/ui/styles/globals.css +285 -0
- package/templates/react-app/files/src/shared/ui/styles/theme.css +48 -0
- package/templates/react-app/files/src/shared/ui/theme/init-script.ts +23 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-colors.ts +14 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-context.ts +60 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-provider.tsx +101 -0
- package/templates/react-app/files/src/shared/ui/theme/theme-toggle.tsx +40 -0
- package/templates/react-app/files/src/shared/ui/theme/use-theme.ts +11 -0
- package/templates/react-app/files/src/vite-env.d.ts +13 -0
- package/templates/react-app/files/tsconfig.json +26 -0
- package/templates/react-app/files/tsconfig.node.json +25 -0
- package/templates/react-app/files/vite.config.ts +344 -0
- package/templates/react-app/overrides/src/routes/auth/forgot-password.tsx +93 -0
- package/templates/react-app/overrides/src/routes/auth/login.tsx +87 -0
- package/templates/react-app/overrides/src/routes/auth/signup.tsx +117 -0
- package/templates/react-app/overrides/src/routes/auth/verified.tsx +31 -0
- package/templates/react-app/overrides/src/routes/auth/verify-email.tsx +97 -0
- package/templates/react-app/overrides/src/shared/auth/config.ts +28 -0
- package/templates/react-app/overrides/src/shared/auth/guards.tsx +75 -0
- package/templates/react-app/overrides/src/shared/auth/index.ts +20 -0
- package/templates/react-app/overrides/src/shared/auth/redirect.ts +65 -0
- package/templates/react-app/overrides/src/shared/hooks/index.ts +6 -0
- package/templates/react-app/overrides/src/shared/types/README.md +28 -0
- package/templates/react-app/template.json +222 -0
- package/templates/react-monorepo/files/.dockerignore +19 -0
- package/templates/react-monorepo/files/.editorconfig +12 -0
- package/templates/react-monorepo/files/.forgejo/workflows/build-and-push.yml +98 -0
- package/templates/react-monorepo/files/.forgejo/workflows/ci.yml +35 -0
- package/templates/react-monorepo/files/.husky/pre-commit +1 -0
- package/templates/react-monorepo/files/.nvmrc +1 -0
- package/templates/react-monorepo/files/.prettierignore +6 -0
- package/templates/react-monorepo/files/.prettierrc +9 -0
- package/templates/react-monorepo/files/.yarnrc.yml +2 -0
- package/templates/react-monorepo/files/Dockerfile +90 -0
- package/templates/react-monorepo/files/_gitignore +35 -0
- package/templates/react-monorepo/files/apps/admin/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/admin/.env.example +16 -0
- package/templates/react-monorepo/files/apps/admin/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/admin/index.html +57 -0
- package/templates/react-monorepo/files/apps/admin/package.json +33 -0
- package/templates/react-monorepo/files/apps/admin/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/admin/src/app/nav.tsx +14 -0
- package/templates/react-monorepo/files/apps/admin/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/admin/src/app/router.tsx +50 -0
- package/templates/react-monorepo/files/apps/admin/src/app/store.ts +15 -0
- package/templates/react-monorepo/files/apps/admin/src/lib/env.ts +12 -0
- package/templates/react-monorepo/files/apps/admin/src/main.tsx +14 -0
- package/templates/react-monorepo/files/apps/admin/src/routes/home.tsx +17 -0
- package/templates/react-monorepo/files/apps/admin/src/routes/settings.tsx +10 -0
- package/templates/react-monorepo/files/apps/admin/src/vite-env.d.ts +14 -0
- package/templates/react-monorepo/files/apps/admin/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/admin/vite.config.ts +13 -0
- package/templates/react-monorepo/files/apps/auth/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/auth/.env.example +16 -0
- package/templates/react-monorepo/files/apps/auth/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/auth/index.html +57 -0
- package/templates/react-monorepo/files/apps/auth/package.json +32 -0
- package/templates/react-monorepo/files/apps/auth/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/auth/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/auth/src/app/router.tsx +19 -0
- package/templates/react-monorepo/files/apps/auth/src/app/store.ts +15 -0
- package/templates/react-monorepo/files/apps/auth/src/components/auth-card.tsx +23 -0
- package/templates/react-monorepo/files/apps/auth/src/components/checkbox-field.tsx +32 -0
- package/templates/react-monorepo/files/apps/auth/src/components/google-button.tsx +41 -0
- package/templates/react-monorepo/files/apps/auth/src/components/or-divider.tsx +9 -0
- package/templates/react-monorepo/files/apps/auth/src/components/password-field.tsx +70 -0
- package/templates/react-monorepo/files/apps/auth/src/components/submit-button.tsx +16 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/auth-actions.ts +68 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/env.ts +8 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/password-strength.ts +20 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/redirect.ts +6 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/schemas.ts +37 -0
- package/templates/react-monorepo/files/apps/auth/src/main.tsx +14 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/forgot-password.tsx +91 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/login.tsx +82 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/reset-password.tsx +83 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/signup.tsx +117 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/verified.tsx +30 -0
- package/templates/react-monorepo/files/apps/auth/src/routes/verify-email.tsx +96 -0
- package/templates/react-monorepo/files/apps/auth/src/vite-env.d.ts +14 -0
- package/templates/react-monorepo/files/apps/auth/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/auth/vite.config.ts +13 -0
- package/templates/react-monorepo/files/apps/client/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/client/.env.example +16 -0
- package/templates/react-monorepo/files/apps/client/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/client/index.html +57 -0
- package/templates/react-monorepo/files/apps/client/package.json +35 -0
- package/templates/react-monorepo/files/apps/client/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/client/src/app/nav.tsx +14 -0
- package/templates/react-monorepo/files/apps/client/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/client/src/app/router.tsx +50 -0
- package/templates/react-monorepo/files/apps/client/src/app/store.ts +15 -0
- package/templates/react-monorepo/files/apps/client/src/lib/env.ts +12 -0
- package/templates/react-monorepo/files/apps/client/src/main.tsx +19 -0
- package/templates/react-monorepo/files/apps/client/src/routes/home.tsx +17 -0
- package/templates/react-monorepo/files/apps/client/src/routes/settings.tsx +10 -0
- package/templates/react-monorepo/files/apps/client/src/vite-env.d.ts +15 -0
- package/templates/react-monorepo/files/apps/client/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/client/vite.config.ts +120 -0
- package/templates/react-monorepo/files/apps/landing/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/apps/landing/.env.example +20 -0
- package/templates/react-monorepo/files/apps/landing/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/apps/landing/index.html +51 -0
- package/templates/react-monorepo/files/apps/landing/package.json +33 -0
- package/templates/react-monorepo/files/apps/landing/src/App.tsx +3 -0
- package/templates/react-monorepo/files/apps/landing/src/app/providers.tsx +42 -0
- package/templates/react-monorepo/files/apps/landing/src/app/router.tsx +21 -0
- package/templates/react-monorepo/files/apps/landing/src/app/store.ts +18 -0
- package/templates/react-monorepo/files/apps/landing/src/features/seo/routes.ts +65 -0
- package/templates/react-monorepo/files/apps/landing/src/lib/env.ts +10 -0
- package/templates/react-monorepo/files/apps/landing/src/main.tsx +14 -0
- package/templates/react-monorepo/files/apps/landing/src/routes/home.tsx +18 -0
- package/templates/react-monorepo/files/apps/landing/src/vite-env.d.ts +15 -0
- package/templates/react-monorepo/files/apps/landing/tsconfig.json +10 -0
- package/templates/react-monorepo/files/apps/landing/vite.config.ts +169 -0
- package/templates/react-monorepo/files/build.sh +61 -0
- package/templates/react-monorepo/files/docker/nginx.conf +136 -0
- package/templates/react-monorepo/files/package.json +37 -0
- package/templates/react-monorepo/files/packages/.gitkeep +0 -0
- package/templates/react-monorepo/files/packages/api/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/api/package.json +30 -0
- package/templates/react-monorepo/files/packages/api/src/base-api.ts +24 -0
- package/templates/react-monorepo/files/packages/api/src/base-query.ts +29 -0
- package/templates/react-monorepo/files/packages/api/src/config.ts +21 -0
- package/templates/react-monorepo/files/packages/api/src/index.ts +15 -0
- package/templates/react-monorepo/files/packages/api/src/realtime-events.ts +34 -0
- package/templates/react-monorepo/files/packages/api/src/socket.ts +41 -0
- package/templates/react-monorepo/files/packages/api/src/store.ts +39 -0
- package/templates/react-monorepo/files/packages/api/src/upload-query.ts +115 -0
- package/templates/react-monorepo/files/packages/api/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/auth/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/auth/package.json +33 -0
- package/templates/react-monorepo/files/packages/auth/src/client.ts +27 -0
- package/templates/react-monorepo/files/packages/auth/src/config.ts +22 -0
- package/templates/react-monorepo/files/packages/auth/src/guards.tsx +61 -0
- package/templates/react-monorepo/files/packages/auth/src/hooks.ts +13 -0
- package/templates/react-monorepo/files/packages/auth/src/index.ts +20 -0
- package/templates/react-monorepo/files/packages/auth/src/logout.ts +23 -0
- package/templates/react-monorepo/files/packages/auth/src/provider.tsx +52 -0
- package/templates/react-monorepo/files/packages/auth/src/redirect.ts +37 -0
- package/templates/react-monorepo/files/packages/auth/src/slice.ts +54 -0
- package/templates/react-monorepo/files/packages/auth/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/config/README.md +47 -0
- package/templates/react-monorepo/files/packages/config/eslint/base.mjs +83 -0
- package/templates/react-monorepo/files/packages/config/package.json +37 -0
- package/templates/react-monorepo/files/packages/config/tailwind/theme.css +48 -0
- package/templates/react-monorepo/files/packages/config/tsconfig/base.json +22 -0
- package/templates/react-monorepo/files/packages/config/tsconfig/node.json +9 -0
- package/templates/react-monorepo/files/packages/config/tsconfig/react.json +9 -0
- package/templates/react-monorepo/files/packages/config/vite/index.d.ts +18 -0
- package/templates/react-monorepo/files/packages/config/vite/index.mjs +129 -0
- package/templates/react-monorepo/files/packages/hooks/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/hooks/package.json +16 -0
- package/templates/react-monorepo/files/packages/hooks/src/index.ts +3 -0
- package/templates/react-monorepo/files/packages/hooks/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/layouts/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/layouts/package.json +30 -0
- package/templates/react-monorepo/files/packages/layouts/src/app-shell/app-shell.tsx +225 -0
- package/templates/react-monorepo/files/packages/layouts/src/app-shell/user-menu.tsx +74 -0
- package/templates/react-monorepo/files/packages/layouts/src/auth-layout.tsx +137 -0
- package/templates/react-monorepo/files/packages/layouts/src/error-boundary.tsx +38 -0
- package/templates/react-monorepo/files/packages/layouts/src/index.ts +23 -0
- package/templates/react-monorepo/files/packages/layouts/src/marketing/marketing-layout.tsx +29 -0
- package/templates/react-monorepo/files/packages/layouts/src/marketing/section-heading.tsx +38 -0
- package/templates/react-monorepo/files/packages/layouts/src/marketing/section.tsx +30 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/app-shell-skeleton.tsx +47 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/empty.tsx +22 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/forbidden.tsx +14 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/loading.tsx +11 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/not-found.tsx +21 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/page-skeleton.tsx +39 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/route-fallback.tsx +26 -0
- package/templates/react-monorepo/files/packages/layouts/src/states/state-screen.tsx +22 -0
- package/templates/react-monorepo/files/packages/layouts/tsconfig.json +4 -0
- package/templates/react-monorepo/files/packages/types/.bin/openapi.js +32 -0
- package/templates/react-monorepo/files/packages/types/.bin/secrets.js +57 -0
- package/templates/react-monorepo/files/packages/types/README.md +28 -0
- package/templates/react-monorepo/files/packages/types/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/types/openapi-ts.config.ts +22 -0
- package/templates/react-monorepo/files/packages/types/package.json +29 -0
- package/templates/react-monorepo/files/packages/types/src/index.ts +4 -0
- package/templates/react-monorepo/files/packages/types/src/types/api/index.ts +9 -0
- package/templates/react-monorepo/files/packages/types/src/types/api/zod.gen.ts +5 -0
- package/templates/react-monorepo/files/packages/types/tsconfig.json +7 -0
- package/templates/react-monorepo/files/packages/ui/components.json +21 -0
- package/templates/react-monorepo/files/packages/ui/eslint.config.mjs +13 -0
- package/templates/react-monorepo/files/packages/ui/package.json +44 -0
- package/templates/react-monorepo/files/packages/ui/src/components/brand/logo.tsx +51 -0
- package/templates/react-monorepo/files/packages/ui/src/components/brand/social-icons.tsx +37 -0
- package/templates/react-monorepo/files/packages/ui/src/components/form/form.tsx +120 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/alert.tsx +25 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/button.tsx +43 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/calendar.tsx +78 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/card.tsx +44 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/checkbox.tsx +16 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/date-picker.tsx +76 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/input.tsx +22 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/label.tsx +19 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/modal.tsx +94 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/popover.tsx +31 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/progress.tsx +33 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/search-input.tsx +69 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/select.tsx +80 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/skeleton.tsx +12 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/sonner.tsx +23 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/spinner.tsx +8 -0
- package/templates/react-monorepo/files/packages/ui/src/components/ui/textarea.tsx +22 -0
- package/templates/react-monorepo/files/packages/ui/src/index.ts +45 -0
- package/templates/react-monorepo/files/packages/ui/src/lib/utils.ts +7 -0
- package/templates/react-monorepo/files/packages/ui/src/lib/zod-setup.ts +28 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/brand.ts +18 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/meta-context.ts +30 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/meta-provider.tsx +24 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/page-meta.tsx +69 -0
- package/templates/react-monorepo/files/packages/ui/src/meta/use-meta-config.ts +11 -0
- package/templates/react-monorepo/files/packages/ui/src/styles/globals.css +285 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/init-script.ts +23 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-colors.ts +14 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-context.ts +60 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-provider.tsx +101 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/theme-toggle.tsx +40 -0
- package/templates/react-monorepo/files/packages/ui/src/theme/use-theme.ts +11 -0
- package/templates/react-monorepo/files/packages/ui/tsconfig.json +9 -0
- package/templates/react-monorepo/files/turbo.json +24 -0
- package/templates/react-monorepo/template.json +230 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { defineConfig, loadEnv } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Opens the connection to the API origin while the HTML is still parsing.
|
|
8
|
+
*
|
|
9
|
+
* Every authenticated app's first act is a Better Auth session request, and the
|
|
10
|
+
* API is a different origin, so that request pays DNS + TCP + TLS before it can
|
|
11
|
+
* even be sent. Measured against the deployed API: ~420 ms of an ~880 ms
|
|
12
|
+
* `get-session` was connection setup. Preconnecting overlaps it with the bundle
|
|
13
|
+
* download instead of paying it afterwards.
|
|
14
|
+
*
|
|
15
|
+
* `crossorigin` is load-bearing, not decoration. Better Auth and RTK Query both
|
|
16
|
+
* send the session cookie (`credentials: 'include'`), and a credentialed request
|
|
17
|
+
* will not reuse an anonymous socket — omit it and the browser opens a second
|
|
18
|
+
* connection, making the preconnect pure waste. It must also stay a *preconnect*
|
|
19
|
+
* and not become a `preload`: the session response is user-specific and must not
|
|
20
|
+
* be speculatively fetched.
|
|
21
|
+
*
|
|
22
|
+
* Injected here rather than in each `index.html` because `VITE_APP_API_URL` is
|
|
23
|
+
* only known to the build, and HTML can't read env.
|
|
24
|
+
*/
|
|
25
|
+
function apiPreconnect(apiUrl) {
|
|
26
|
+
let origin = '';
|
|
27
|
+
try {
|
|
28
|
+
origin = apiUrl ? new URL(apiUrl).origin : '';
|
|
29
|
+
} catch {
|
|
30
|
+
/* malformed VITE_APP_API_URL — skip rather than emit a broken tag */
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
name: 'acme:api-preconnect',
|
|
34
|
+
transformIndexHtml: {
|
|
35
|
+
order: 'pre',
|
|
36
|
+
handler: () =>
|
|
37
|
+
origin
|
|
38
|
+
? [{ tag: 'link', attrs: { rel: 'preconnect', href: origin, crossorigin: true }, injectTo: 'head-prepend' }]
|
|
39
|
+
: []
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Substitutes `%THEME_LIGHT%` / `%THEME_DARK%` in `index.html`.
|
|
46
|
+
*
|
|
47
|
+
* Every app's `<head>` needs the theme background twice over — the pre-paint
|
|
48
|
+
* script that sets the `dark` class before first paint, and the `html`
|
|
49
|
+
* background that stops a white flash on a dark load — and HTML cannot import.
|
|
50
|
+
* Doing it here keeps `THEME_COLORS` the single source of truth instead of four
|
|
51
|
+
* hardcoded copies drifting apart; that drift is exactly how the client's PWA
|
|
52
|
+
* manifest once ended up `#ffffff` in both themes.
|
|
53
|
+
*
|
|
54
|
+
* The colours are passed in rather than imported: `@acme/config` is the lowest
|
|
55
|
+
* layer and must not depend on `@acme/ui`, which already depends on it.
|
|
56
|
+
*
|
|
57
|
+
* Runs for `vite dev` as well as the build.
|
|
58
|
+
*/
|
|
59
|
+
function injectThemeColors(colors) {
|
|
60
|
+
return {
|
|
61
|
+
name: 'acme:inject-theme-colors',
|
|
62
|
+
transformIndexHtml: (html) => html.replaceAll('%THEME_LIGHT%', colors.light).replaceAll('%THEME_DARK%', colors.dark)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Substitutes `%APP_TITLE%` in `index.html`.
|
|
68
|
+
*
|
|
69
|
+
* The static `<title>` is the fallback a visitor sees before React mounts, and
|
|
70
|
+
* on any route that renders no `PageMeta`. It has to match the suffix that
|
|
71
|
+
* `MetaProvider` appends at runtime, and the brand string is passed in from
|
|
72
|
+
* `@acme/ui/brand` so there is one source rather than a literal per app — four
|
|
73
|
+
* copies of "Acme" drifting from the real product name is precisely what
|
|
74
|
+
* MAI-101 was filed about.
|
|
75
|
+
*
|
|
76
|
+
* Passed in rather than imported for the same reason as the theme colours:
|
|
77
|
+
* `@acme/config` is the lowest layer and must not depend on `@acme/ui`.
|
|
78
|
+
*/
|
|
79
|
+
function injectAppTitle(appTitle) {
|
|
80
|
+
return {
|
|
81
|
+
name: 'acme:inject-app-title',
|
|
82
|
+
transformIndexHtml: (html) => html.replaceAll('%APP_TITLE%', appTitle)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Shared Vite config factory for Acme apps.
|
|
88
|
+
*
|
|
89
|
+
* Usage in an app's `vite.config.ts`:
|
|
90
|
+
* import { defineAppConfig } from '@acme/config/vite';
|
|
91
|
+
* export default defineAppConfig({ rootDir: import.meta.dirname });
|
|
92
|
+
*
|
|
93
|
+
* Reads `PORT` and comma-separated `VITE_APP_ALLOWED_HOSTS` from the app's env,
|
|
94
|
+
* wires the `@` -> `src` alias, and registers the React + Tailwind v4 plugins.
|
|
95
|
+
* Extra app-specific plugins (e.g. the PWA plugin on the client) are appended
|
|
96
|
+
* after the base ones via `plugins`.
|
|
97
|
+
*
|
|
98
|
+
* @param {{ rootDir: string, themeColors?: { light: string, dark: string }, appTitle?: string, plugins?: import('vite').PluginOption[], overrides?: import('vite').UserConfig }} options
|
|
99
|
+
* @returns {import('vite').UserConfigExport}
|
|
100
|
+
*/
|
|
101
|
+
export function defineAppConfig({ rootDir, themeColors, appTitle, plugins = [], overrides = {} }) {
|
|
102
|
+
return defineConfig(({ mode }) => {
|
|
103
|
+
const env = loadEnv(mode, rootDir, '');
|
|
104
|
+
const hosts = env.VITE_APP_ALLOWED_HOSTS;
|
|
105
|
+
const allowedHosts = hosts ? hosts.split(',').map((h) => h.trim()) : [];
|
|
106
|
+
return {
|
|
107
|
+
plugins: [
|
|
108
|
+
react(),
|
|
109
|
+
tailwindcss(),
|
|
110
|
+
apiPreconnect(env.VITE_APP_API_URL),
|
|
111
|
+
...(themeColors ? [injectThemeColors(themeColors)] : []),
|
|
112
|
+
...(appTitle ? [injectAppTitle(appTitle)] : []),
|
|
113
|
+
...plugins
|
|
114
|
+
],
|
|
115
|
+
server: {
|
|
116
|
+
host: true,
|
|
117
|
+
port: env.PORT ? Number(env.PORT) : undefined,
|
|
118
|
+
strictPort: true,
|
|
119
|
+
allowedHosts
|
|
120
|
+
},
|
|
121
|
+
resolve: {
|
|
122
|
+
alias: {
|
|
123
|
+
'@': path.resolve(rootDir, './src')
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
...overrides
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acme/hooks",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@acme/config": "workspace:*"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acme/layouts",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@acme/ui": "workspace:*",
|
|
15
|
+
"lucide-react": "^1.17.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"react": ">=19",
|
|
19
|
+
"react-dom": ">=19",
|
|
20
|
+
"react-router": ">=7"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@acme/config": "workspace:*",
|
|
24
|
+
"@types/react": "^19.2.14",
|
|
25
|
+
"@types/react-dom": "^19.2.3",
|
|
26
|
+
"react": "^19.2.6",
|
|
27
|
+
"react-dom": "^19.2.6",
|
|
28
|
+
"react-router": "^8.2.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { type ReactNode, useState } from 'react';
|
|
2
|
+
import { NavLink, Outlet } from 'react-router';
|
|
3
|
+
import { Menu, X } from 'lucide-react';
|
|
4
|
+
import { Logo, cn } from '@acme/ui';
|
|
5
|
+
import { UserMenu, type ShellUser } from './user-menu';
|
|
6
|
+
|
|
7
|
+
export type NavItem = {
|
|
8
|
+
label: string;
|
|
9
|
+
to: string;
|
|
10
|
+
icon: ReactNode;
|
|
11
|
+
/** Match the route exactly (use for the index/dashboard route). */
|
|
12
|
+
end?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Warm a route's chunk before the click, given its `to`. Wired to hover, keyboard
|
|
17
|
+
* focus and touch-start, so it covers pointer, keyboard and mobile — a phone has
|
|
18
|
+
* no hover, but `touchstart` still lands a few hundred ms before the click.
|
|
19
|
+
* Optional: apps without code-split routes just leave it off.
|
|
20
|
+
*/
|
|
21
|
+
export type PreloadRoute = (to: string) => void;
|
|
22
|
+
|
|
23
|
+
export type ComingSoonItem = {
|
|
24
|
+
label: string;
|
|
25
|
+
icon: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type AppShellProps = {
|
|
29
|
+
nav: NavItem[];
|
|
30
|
+
user: ShellUser;
|
|
31
|
+
onSignOut: () => void;
|
|
32
|
+
/** Brand lockup at the top of the sidebar. Defaults to the Acme Corp mark. */
|
|
33
|
+
brand?: ReactNode;
|
|
34
|
+
/** Locked "Coming soon" nav entries shown below the main nav. */
|
|
35
|
+
comingSoon?: ComingSoonItem[];
|
|
36
|
+
/** Slot pinned to the bottom of the sidebar (e.g. the market ticker). */
|
|
37
|
+
sidebarFooter?: ReactNode;
|
|
38
|
+
/** Topbar search slot. */
|
|
39
|
+
search?: ReactNode;
|
|
40
|
+
/** Topbar action slot to the left of the user menu (e.g. the notification bell). */
|
|
41
|
+
topbarActions?: ReactNode;
|
|
42
|
+
/** Page content. Defaults to the router <Outlet /> when omitted. */
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
/** Warms a route's code-split chunk on nav hover/focus/touch. */
|
|
45
|
+
preloadRoute?: PreloadRoute;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function DefaultBrand() {
|
|
49
|
+
return (
|
|
50
|
+
<div className="px-2">
|
|
51
|
+
<Logo />
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Sidebar({
|
|
57
|
+
nav,
|
|
58
|
+
comingSoon,
|
|
59
|
+
sidebarFooter,
|
|
60
|
+
brand,
|
|
61
|
+
onNavigate,
|
|
62
|
+
preloadRoute
|
|
63
|
+
}: Pick<AppShellProps, 'nav' | 'comingSoon' | 'sidebarFooter' | 'brand' | 'preloadRoute'> & {
|
|
64
|
+
onNavigate?: () => void;
|
|
65
|
+
}) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="flex h-full flex-col p-4">
|
|
68
|
+
<div className="py-2">{brand ?? <DefaultBrand />}</div>
|
|
69
|
+
{/*
|
|
70
|
+
* `min-h-0` is load-bearing: a flex child defaults to `min-height: auto`,
|
|
71
|
+
* which lets it grow past its flex-basis to fit its content instead of
|
|
72
|
+
* scrolling — on a short viewport that pushed `sidebarFooter` off the
|
|
73
|
+
* bottom of the fixed-height sidebar with no way to reach it. Scrollbar
|
|
74
|
+
* is hidden (still scrollable via wheel/touch/keyboard) to match the
|
|
75
|
+
* flat, chrome-free sidebar surface — nav is short enough that a visible
|
|
76
|
+
* track would only appear briefly during scroll anyway.
|
|
77
|
+
*/}
|
|
78
|
+
<div className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
|
79
|
+
<nav className="flex flex-col gap-0.5">
|
|
80
|
+
{nav.map((item) => (
|
|
81
|
+
<NavLink
|
|
82
|
+
key={item.to}
|
|
83
|
+
to={item.to}
|
|
84
|
+
end={item.end}
|
|
85
|
+
onClick={onNavigate}
|
|
86
|
+
onPointerEnter={() => preloadRoute?.(item.to)}
|
|
87
|
+
onFocus={() => preloadRoute?.(item.to)}
|
|
88
|
+
onTouchStart={() => preloadRoute?.(item.to)}
|
|
89
|
+
className={({ isActive }) =>
|
|
90
|
+
cn(
|
|
91
|
+
'flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-semibold transition-colors [&_svg]:size-[18px]',
|
|
92
|
+
isActive ? 'bg-accent text-accent-foreground' : 'text-body hover:bg-secondary'
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
>
|
|
96
|
+
{item.icon}
|
|
97
|
+
{item.label}
|
|
98
|
+
</NavLink>
|
|
99
|
+
))}
|
|
100
|
+
</nav>
|
|
101
|
+
{comingSoon && comingSoon.length > 0 ? (
|
|
102
|
+
<>
|
|
103
|
+
<div className="mx-2 my-3 h-px bg-border" />
|
|
104
|
+
<p className="px-3 pb-1 text-[10px] font-bold uppercase tracking-[0.09em] text-mute">Coming soon</p>
|
|
105
|
+
<div className="flex flex-col gap-0.5">
|
|
106
|
+
{comingSoon.map((item) => (
|
|
107
|
+
<div
|
|
108
|
+
key={item.label}
|
|
109
|
+
className="flex cursor-default items-center justify-between gap-2 rounded-xl px-3 py-2 text-sm font-semibold text-mute [&_svg]:size-4"
|
|
110
|
+
>
|
|
111
|
+
<span className="flex items-center gap-3">
|
|
112
|
+
{item.icon}
|
|
113
|
+
{item.label}
|
|
114
|
+
</span>
|
|
115
|
+
<span className="rounded-full bg-accent px-1.5 py-0.5 text-[8.5px] font-bold uppercase tracking-[0.08em] text-accent-foreground">
|
|
116
|
+
Soon
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</>
|
|
122
|
+
) : null}
|
|
123
|
+
</div>
|
|
124
|
+
{sidebarFooter ? <div className="pt-3">{sidebarFooter}</div> : null}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Authenticated app shell (shared by Client + Admin): fixed sidebar at desktop,
|
|
131
|
+
* a slide-in drawer on mobile, and a sticky topbar with search / actions / user
|
|
132
|
+
* menu. Presentational only — nav and slots are passed in, and sign-out is
|
|
133
|
+
* delegated via `onSignOut`, so it stays decoupled from the auth package.
|
|
134
|
+
*/
|
|
135
|
+
export function AppShell({
|
|
136
|
+
nav,
|
|
137
|
+
user,
|
|
138
|
+
onSignOut,
|
|
139
|
+
brand,
|
|
140
|
+
comingSoon,
|
|
141
|
+
sidebarFooter,
|
|
142
|
+
search,
|
|
143
|
+
topbarActions,
|
|
144
|
+
children,
|
|
145
|
+
preloadRoute
|
|
146
|
+
}: AppShellProps) {
|
|
147
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="min-h-screen bg-background">
|
|
151
|
+
{/* Desktop sidebar */}
|
|
152
|
+
<aside className="fixed inset-y-0 left-0 hidden w-64 border-r border-border bg-card lg:block">
|
|
153
|
+
<Sidebar
|
|
154
|
+
nav={nav}
|
|
155
|
+
comingSoon={comingSoon}
|
|
156
|
+
sidebarFooter={sidebarFooter}
|
|
157
|
+
brand={brand}
|
|
158
|
+
preloadRoute={preloadRoute}
|
|
159
|
+
/>
|
|
160
|
+
</aside>
|
|
161
|
+
|
|
162
|
+
{/* Mobile drawer — always mounted so it animates on both enter and exit. */}
|
|
163
|
+
<div
|
|
164
|
+
className={cn('fixed inset-0 z-50 lg:hidden', !mobileOpen && 'pointer-events-none')}
|
|
165
|
+
aria-hidden={!mobileOpen}
|
|
166
|
+
inert={!mobileOpen}
|
|
167
|
+
>
|
|
168
|
+
<div
|
|
169
|
+
onClick={() => setMobileOpen(false)}
|
|
170
|
+
aria-hidden="true"
|
|
171
|
+
className={cn(
|
|
172
|
+
'absolute inset-0 bg-scrim transition-opacity duration-300 ease-out motion-reduce:transition-none',
|
|
173
|
+
mobileOpen ? 'opacity-100' : 'opacity-0'
|
|
174
|
+
)}
|
|
175
|
+
/>
|
|
176
|
+
<aside
|
|
177
|
+
className={cn(
|
|
178
|
+
'absolute inset-y-0 left-0 w-64 border-r border-border bg-card',
|
|
179
|
+
'transition-transform duration-300 ease-out motion-reduce:transition-none',
|
|
180
|
+
mobileOpen ? 'translate-x-0' : '-translate-x-full'
|
|
181
|
+
)}
|
|
182
|
+
>
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={() => setMobileOpen(false)}
|
|
186
|
+
aria-label="Close menu"
|
|
187
|
+
className="absolute right-3 top-4 flex size-8 items-center justify-center rounded-full text-mute hover:bg-secondary"
|
|
188
|
+
>
|
|
189
|
+
<X className="size-5" />
|
|
190
|
+
</button>
|
|
191
|
+
<Sidebar
|
|
192
|
+
nav={nav}
|
|
193
|
+
comingSoon={comingSoon}
|
|
194
|
+
sidebarFooter={sidebarFooter}
|
|
195
|
+
brand={brand}
|
|
196
|
+
preloadRoute={preloadRoute}
|
|
197
|
+
onNavigate={() => setMobileOpen(false)}
|
|
198
|
+
/>
|
|
199
|
+
</aside>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Main column */}
|
|
203
|
+
<div className="flex min-h-screen flex-col lg:pl-64">
|
|
204
|
+
<header className="sticky top-0 z-40 flex items-center gap-2.5 border-b border-border bg-card px-4 py-2.5">
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={() => setMobileOpen(true)}
|
|
208
|
+
aria-label="Open menu"
|
|
209
|
+
className="flex size-9 items-center justify-center rounded-full text-foreground hover:bg-secondary lg:hidden"
|
|
210
|
+
>
|
|
211
|
+
<Menu className="size-5" />
|
|
212
|
+
</button>
|
|
213
|
+
{search ?? <div className="flex-1" />}
|
|
214
|
+
<div className="ml-auto flex items-center gap-2">
|
|
215
|
+
{topbarActions}
|
|
216
|
+
<UserMenu user={user} onSignOut={onSignOut} />
|
|
217
|
+
</div>
|
|
218
|
+
</header>
|
|
219
|
+
<main className="flex-1">
|
|
220
|
+
<div className="mx-auto w-full max-w-6xl p-4 sm:p-6 lg:p-8">{children ?? <Outlet />}</div>
|
|
221
|
+
</main>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { LogOut } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export type ShellUser = {
|
|
5
|
+
name?: string | null;
|
|
6
|
+
email?: string | null;
|
|
7
|
+
image?: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function initials(user: ShellUser): string {
|
|
11
|
+
const name = (user.name ?? '').trim();
|
|
12
|
+
if (name) {
|
|
13
|
+
const parts = name.split(/\s+/);
|
|
14
|
+
const first = parts[0]?.[0] ?? '';
|
|
15
|
+
const second = parts[1]?.[0] ?? '';
|
|
16
|
+
return (first + second || first).toUpperCase();
|
|
17
|
+
}
|
|
18
|
+
const email = (user.email ?? '').trim();
|
|
19
|
+
return email ? email[0].toUpperCase() : '?';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Avatar button + dropdown (name/email + sign out). Sign-out is delegated via `onSignOut`. */
|
|
23
|
+
export function UserMenu({ user, onSignOut }: { user: ShellUser; onSignOut: () => void }) {
|
|
24
|
+
const [open, setOpen] = useState(false);
|
|
25
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!open) return;
|
|
29
|
+
const onDocClick = (event: MouseEvent) => {
|
|
30
|
+
if (ref.current && !ref.current.contains(event.target as Node)) setOpen(false);
|
|
31
|
+
};
|
|
32
|
+
document.addEventListener('mousedown', onDocClick);
|
|
33
|
+
return () => document.removeEventListener('mousedown', onDocClick);
|
|
34
|
+
}, [open]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="relative" ref={ref}>
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={() => setOpen((value) => !value)}
|
|
41
|
+
aria-haspopup="menu"
|
|
42
|
+
aria-expanded={open}
|
|
43
|
+
aria-label="Account menu"
|
|
44
|
+
className="flex size-9 items-center justify-center overflow-hidden rounded-full bg-primary text-sm font-bold text-primary-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
45
|
+
>
|
|
46
|
+
{user.image ? <img src={user.image} alt="" className="size-9 rounded-full object-cover" /> : initials(user)}
|
|
47
|
+
</button>
|
|
48
|
+
{open ? (
|
|
49
|
+
<div
|
|
50
|
+
role="menu"
|
|
51
|
+
className="absolute right-0 top-11 z-50 w-56 rounded-2xl border border-border bg-card p-2 shadow-[0_12px_32px_rgba(14,15,12,0.18)]"
|
|
52
|
+
>
|
|
53
|
+
<div className="px-3 py-2">
|
|
54
|
+
<p className="truncate text-sm font-semibold text-foreground">{user.name ?? 'Your account'}</p>
|
|
55
|
+
{user.email ? <p className="truncate text-xs text-mute">{user.email}</p> : null}
|
|
56
|
+
</div>
|
|
57
|
+
<div className="my-1 h-px bg-border" />
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
role="menuitem"
|
|
61
|
+
onClick={() => {
|
|
62
|
+
setOpen(false);
|
|
63
|
+
onSignOut();
|
|
64
|
+
}}
|
|
65
|
+
className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-left text-sm font-semibold text-foreground transition-colors hover:bg-secondary"
|
|
66
|
+
>
|
|
67
|
+
<LogOut className="size-4" />
|
|
68
|
+
Sign out
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
) : null}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Logo, LogoMark, ThemeToggle } from '@acme/ui';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Brand-panel watermark, per the design system's Watermark guideline: one large
|
|
6
|
+
* mark, offset, clear of all text, never tiled — "felt, not read".
|
|
7
|
+
*
|
|
8
|
+
* Inline SVG (`LogoMark` fills with `currentColor`), so it costs no request and
|
|
9
|
+
* inverts with the theme for free: ink on the sage panel, near-white on the ink
|
|
10
|
+
* one. The DS quotes 6–8% ink, but that assumes a light surface — the dark panel
|
|
11
|
+
* is far darker, so the same alpha reads ~45% stronger against it. Measured in
|
|
12
|
+
* CIELAB against each panel colour and matched: 0.07 light gives ΔE 5.4, 0.05
|
|
13
|
+
* dark gives ΔE 5.6. Don't "simplify" these to one value.
|
|
14
|
+
*
|
|
15
|
+
* `-z-10` under the panel's `isolate` puts it behind the copy while staying above
|
|
16
|
+
* the panel's own background; without `isolate` the negative z-index would sink
|
|
17
|
+
* below it and disappear.
|
|
18
|
+
*/
|
|
19
|
+
function BrandWatermark() {
|
|
20
|
+
return (
|
|
21
|
+
<div className="pointer-events-none absolute inset-0 -z-10 overflow-hidden" aria-hidden="true">
|
|
22
|
+
{/*
|
|
23
|
+
* Size and offset are load-bearing, and every value here came from looking
|
|
24
|
+
* at it rather than reasoning about it:
|
|
25
|
+
* - 460px left only a corner fragment on-panel, so it read as stray grey
|
|
26
|
+
* rectangles rather than the monogram.
|
|
27
|
+
* - 300px read correctly at 1280 but the dot poked out past the teaser
|
|
28
|
+
* card's right edge; the guideline is explicit that it stays clear.
|
|
29
|
+
* - 240px clears the card at 1280 — but not at 1024, where the panel is
|
|
30
|
+
* only 512px wide. The card is 340px inside 48px padding, so it owns the
|
|
31
|
+
* left 388px and *any* legible mark overlaps it horizontally there. The
|
|
32
|
+
* only way to clear is vertically, hence the smaller `lg` size that fits
|
|
33
|
+
* in the ~155px below the card, going full size from `xl`.
|
|
34
|
+
*
|
|
35
|
+
* Sized in CSS, not the `width` prop, so it can be responsive — the classes
|
|
36
|
+
* override the SVG's width/height attributes and the viewBox keeps the ratio.
|
|
37
|
+
* Re-check both breakpoints if the card's width or the panel padding changes.
|
|
38
|
+
*/}
|
|
39
|
+
<LogoMark
|
|
40
|
+
width={240}
|
|
41
|
+
className="absolute -bottom-4 -right-4 h-auto w-40 text-foreground opacity-[0.07] dark:opacity-[0.05] xl:w-60"
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Product teaser — the brand panel's signature element. Shows a prospective user
|
|
49
|
+
* what the product does instead of decorating around them, and costs nothing:
|
|
50
|
+
* the sparkline is inline SVG, no image request.
|
|
51
|
+
*
|
|
52
|
+
* Replace the copy and the figure with your own; keep the shape. **The numbers
|
|
53
|
+
* are illustrative, not anyone's data**, which is why the visually-hidden
|
|
54
|
+
* "Example" prefix is here — without it a screen reader announces a fabricated
|
|
55
|
+
* figure as fact.
|
|
56
|
+
*
|
|
57
|
+
* Brand green on the line doesn't conflict with "green is for CTAs": `--primary`
|
|
58
|
+
* is the same hex in both themes, so the card needs no per-theme handling, and
|
|
59
|
+
* the surrounding surfaces do.
|
|
60
|
+
*/
|
|
61
|
+
const SPARK = 'M0,44 C24,40 40,46 62,38 C86,29 102,34 124,27 C148,19 166,24 190,17 C214,11 236,14 256,9 L280,6';
|
|
62
|
+
|
|
63
|
+
function ProductTeaser() {
|
|
64
|
+
return (
|
|
65
|
+
<div className="mt-7 max-w-[340px] rounded-[20px] bg-card px-5 py-[18px]">
|
|
66
|
+
<div className="flex items-baseline justify-between gap-2.5">
|
|
67
|
+
<span className="text-[11px] font-semibold uppercase leading-[15px] tracking-[0.07em] text-mute">
|
|
68
|
+
<span className="sr-only">Example: </span>This month
|
|
69
|
+
</span>
|
|
70
|
+
<span className="text-[11.5px] font-semibold leading-4 text-positive">▲ +18.4%</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="mt-1.5 text-[26px] font-extrabold leading-8 tracking-[-0.02em] tabular-nums text-foreground">
|
|
73
|
+
42,830.16
|
|
74
|
+
</div>
|
|
75
|
+
<svg viewBox="0 0 280 56" className="mt-2.5 h-14 w-full" aria-hidden="true">
|
|
76
|
+
{/* Area first so the stroke sits on top of it. */}
|
|
77
|
+
<path d={`${SPARK} L280,56 L0,56 Z`} className="fill-primary opacity-12" />
|
|
78
|
+
<path d={SPARK} fill="none" strokeWidth="2.5" className="stroke-primary" />
|
|
79
|
+
</svg>
|
|
80
|
+
<p className="mt-1.5 text-[10.5px] leading-[15px] text-mute">Illustrative figures · replace with your own</p>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type AuthLayoutProps = {
|
|
86
|
+
children: ReactNode;
|
|
87
|
+
/** Optional brand-panel content (headline / illustration). */
|
|
88
|
+
aside?: ReactNode;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Split auth shell (Wise pattern): sage brand panel on the left at desktop,
|
|
93
|
+
* form panel on the right; stacked to a single centered column on mobile.
|
|
94
|
+
* No shadows — the form sits on the page/card surface via contrast.
|
|
95
|
+
*/
|
|
96
|
+
export function AuthLayout({ children, aside }: AuthLayoutProps) {
|
|
97
|
+
return (
|
|
98
|
+
<div className="min-h-screen w-full bg-background lg:grid lg:grid-cols-2">
|
|
99
|
+
{/*
|
|
100
|
+
* `isolate` so the watermark's -z-10 stays above the panel background.
|
|
101
|
+
*
|
|
102
|
+
* `my-auto` on the content block rather than `justify-between` on the panel:
|
|
103
|
+
* the footer line used to anchor the bottom, so with it gone space-between
|
|
104
|
+
* would drop the headline to the floor.
|
|
105
|
+
*/}
|
|
106
|
+
<aside className="relative isolate hidden flex-col gap-10 bg-brand-panel p-12 lg:flex">
|
|
107
|
+
<BrandWatermark />
|
|
108
|
+
<Logo />
|
|
109
|
+
<div className="my-auto max-w-md">
|
|
110
|
+
{aside ?? (
|
|
111
|
+
<>
|
|
112
|
+
<h1 className="font-display text-4xl font-black leading-[1.06] tracking-tight text-foreground">
|
|
113
|
+
One line on why you exist.
|
|
114
|
+
</h1>
|
|
115
|
+
<p className="mt-4 max-w-sm text-body">
|
|
116
|
+
A sentence of supporting copy, written for someone who has never heard of this product.
|
|
117
|
+
</p>
|
|
118
|
+
<ProductTeaser />
|
|
119
|
+
</>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
</aside>
|
|
123
|
+
{/*
|
|
124
|
+
* The auth app has no topbar, so the toggle rides the form panel. Placed
|
|
125
|
+
* here rather than per-route because every auth screen goes through this
|
|
126
|
+
* layout. `relative` on the panel keeps it inside the right-hand column at
|
|
127
|
+
* desktop instead of over the brand panel.
|
|
128
|
+
*/}
|
|
129
|
+
<main className="relative flex min-h-screen items-center justify-center p-6">
|
|
130
|
+
<div className="absolute right-4 top-4">
|
|
131
|
+
<ThemeToggle />
|
|
132
|
+
</div>
|
|
133
|
+
<div className="w-full max-w-md">{children}</div>
|
|
134
|
+
</main>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
+
import { Button } from '@acme/ui';
|
|
3
|
+
import { StateScreen } from './states/state-screen';
|
|
4
|
+
|
|
5
|
+
type ErrorBoundaryProps = {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ErrorBoundaryState = {
|
|
11
|
+
hasError: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Catches render errors in its subtree and shows a recoverable fallback. */
|
|
15
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
16
|
+
state: ErrorBoundaryState = { hasError: false };
|
|
17
|
+
|
|
18
|
+
static getDerivedStateFromError(): ErrorBoundaryState {
|
|
19
|
+
return { hasError: true };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
componentDidCatch(error: Error, info: ErrorInfo): void {
|
|
23
|
+
console.error('ErrorBoundary caught an error', error, info);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render(): ReactNode {
|
|
27
|
+
if (this.state.hasError) {
|
|
28
|
+
return (
|
|
29
|
+
this.props.fallback ?? (
|
|
30
|
+
<StateScreen title="Something went wrong" description="An unexpected error occurred. Try reloading the page.">
|
|
31
|
+
<Button onClick={() => window.location.reload()}>Reload</Button>
|
|
32
|
+
</StateScreen>
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return this.props.children;
|
|
37
|
+
}
|
|
38
|
+
}
|