@warlock.js/core 5.34.3 → 5.36.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/CHANGELOG.md +69 -0
- package/esm/cli/cli-commands.manager.mjs +1 -1
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -0
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/build.command.mjs +30 -3
- package/esm/cli/commands/build.command.mjs.map +1 -1
- package/esm/cli/commands/check/check-command.action.mjs +1 -0
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
- package/esm/cli/commands/check.command.mjs +5 -0
- package/esm/cli/commands/check.command.mjs.map +1 -1
- package/esm/cli/commands/explain/explain-command.action.mjs +21 -0
- package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -1
- package/esm/cli/commands/explain.command.mjs +14 -4
- package/esm/cli/commands/explain.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +19 -1
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/web.generator.mjs +169 -0
- package/esm/cli/commands/generate/generators/web.generator.mjs.map +1 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs +231 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs.map +1 -0
- package/esm/cli/commands/inspect/inspect-command.action.mjs +4 -3
- package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -1
- package/esm/cli/commands/inspect.command.mjs +13 -8
- package/esm/cli/commands/inspect.command.mjs.map +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -1
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -0
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/load-config-files.mjs +4 -0
- package/esm/config/load-config-files.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +11 -1
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +16 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +13 -4
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/loader/loader-port-acks.mjs +21 -9
- package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs +31 -12
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +15 -1
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +1 -1
- package/esm/dev-server/special-file-patterns.mjs +4 -1
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +1 -1
- package/esm/e2e/cdp.d.mts.map +1 -1
- package/esm/e2e/cdp.mjs +9 -2
- package/esm/e2e/cdp.mjs.map +1 -1
- package/esm/e2e/index.d.mts +2 -2
- package/esm/e2e/page.d.mts +14 -1
- package/esm/e2e/page.d.mts.map +1 -1
- package/esm/e2e/page.mjs +81 -8
- package/esm/e2e/page.mjs.map +1 -1
- package/esm/e2e/types.d.mts +49 -0
- package/esm/e2e/types.d.mts.map +1 -1
- package/esm/e2e/types.mjs.map +1 -1
- package/esm/errors/error-codes.d.mts +72 -0
- package/esm/errors/error-codes.d.mts.map +1 -1
- package/esm/errors/error-codes.mjs +72 -0
- package/esm/errors/error-codes.mjs.map +1 -1
- package/esm/errors/index.d.mts +2 -2
- package/esm/errors/index.mjs +2 -2
- package/esm/generations/add-command.action.mjs +2 -0
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/design-system-payload.mjs +53 -0
- package/esm/generations/design-system-payload.mjs.map +1 -0
- package/esm/generations/features/design-system.feature.mjs +38 -0
- package/esm/generations/features/design-system.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +3 -1
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs +13 -2
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +48 -13
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs +3 -3
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +9 -5
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/error-mapper.d.mts +1 -1
- package/esm/http/error-mapper.mjs +3 -3
- package/esm/http/error-mapper.mjs.map +1 -1
- package/esm/http/error-response.d.mts +3 -0
- package/esm/http/error-response.d.mts.map +1 -1
- package/esm/http/error-response.mjs +8 -2
- package/esm/http/error-response.mjs.map +1 -1
- package/esm/http/errors/errors.d.mts +8 -0
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +13 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +3 -2
- package/esm/http/index.mjs +3 -2
- package/esm/http/list-endpoint.d.mts +35 -0
- package/esm/http/list-endpoint.d.mts.map +1 -0
- package/esm/http/list-endpoint.mjs +28 -0
- package/esm/http/list-endpoint.mjs.map +1 -0
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +12 -2
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/server.mjs +1 -1
- package/esm/index.d.mts +10 -6
- package/esm/index.mjs +5 -2
- package/esm/localization/index.mjs +3 -1
- package/esm/localization/translation-usage-graph.d.mts +49 -0
- package/esm/localization/translation-usage-graph.d.mts.map +1 -0
- package/esm/localization/translation-usage-graph.mjs +128 -0
- package/esm/localization/translation-usage-graph.mjs.map +1 -0
- package/esm/localization/translation-usage.d.mts +85 -0
- package/esm/localization/translation-usage.d.mts.map +1 -0
- package/esm/localization/translation-usage.mjs +307 -0
- package/esm/localization/translation-usage.mjs.map +1 -0
- package/esm/openapi/security.mjs +12 -4
- package/esm/openapi/security.mjs.map +1 -1
- package/esm/openapi/types.d.mts +2 -0
- package/esm/openapi/types.d.mts.map +1 -1
- package/esm/production/build-budgets.d.mts +64 -0
- package/esm/production/build-budgets.d.mts.map +1 -0
- package/esm/production/build-budgets.mjs +109 -0
- package/esm/production/build-budgets.mjs.map +1 -0
- package/esm/production/production-builder.mjs +13 -1
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +1 -1
- package/esm/production/resolve-build-config.d.mts.map +1 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +5 -3
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/restful/restful.d.mts +1 -1
- package/esm/router/router.d.mts +2 -0
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +4 -1
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +19 -0
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tests/fakes/storage-fake.d.mts +3 -1
- package/esm/tests/fakes/storage-fake.d.mts.map +1 -1
- package/esm/tests/fakes/storage-fake.mjs +3 -3
- package/esm/tests/fakes/storage-fake.mjs.map +1 -1
- package/esm/tests/fakes/test-fakes.d.mts +3 -1
- package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
- package/esm/tests/fakes/test-fakes.mjs +3 -2
- package/esm/tests/fakes/test-fakes.mjs.map +1 -1
- package/esm/tests/index.d.mts +4 -2
- package/esm/tests/index.mjs +3 -1
- package/esm/tests/load-unit-config.mjs +24 -0
- package/esm/tests/load-unit-config.mjs.map +1 -0
- package/esm/tests/response-contract.d.mts +32 -0
- package/esm/tests/response-contract.d.mts.map +1 -0
- package/esm/tests/response-contract.mjs +116 -0
- package/esm/tests/response-contract.mjs.map +1 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +10 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-lifecycle-state.mjs.map +1 -1
- package/esm/tests/test-locales.mjs +33 -0
- package/esm/tests/test-locales.mjs.map +1 -0
- package/esm/tests/test-runtime-scope.d.mts +15 -0
- package/esm/tests/test-runtime-scope.d.mts.map +1 -0
- package/esm/tests/test-runtime-scope.mjs +80 -0
- package/esm/tests/test-runtime-scope.mjs.map +1 -0
- package/esm/tests/vitest-setup.d.mts +13 -3
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +146 -18
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/tooling/check/affected-files.d.mts +5 -0
- package/esm/tooling/check/affected-files.d.mts.map +1 -0
- package/esm/tooling/check/affected-files.mjs +56 -0
- package/esm/tooling/check/affected-files.mjs.map +1 -0
- package/esm/tooling/check/api-route-references.d.mts +16 -0
- package/esm/tooling/check/api-route-references.d.mts.map +1 -0
- package/esm/tooling/check/api-route-references.mjs +146 -0
- package/esm/tooling/check/api-route-references.mjs.map +1 -0
- package/esm/tooling/check/changed-files.d.mts +2 -0
- package/esm/tooling/check/changed-files.d.mts.map +1 -1
- package/esm/tooling/check/changed-files.mjs +2 -2
- package/esm/tooling/check/changed-files.mjs.map +1 -1
- package/esm/tooling/check/conventions-step.d.mts +7 -3
- package/esm/tooling/check/conventions-step.d.mts.map +1 -1
- package/esm/tooling/check/conventions-step.mjs +59 -8
- package/esm/tooling/check/conventions-step.mjs.map +1 -1
- package/esm/tooling/check/fixes.d.mts +8 -0
- package/esm/tooling/check/fixes.d.mts.map +1 -0
- package/esm/tooling/check/fixes.mjs +31 -0
- package/esm/tooling/check/fixes.mjs.map +1 -0
- package/esm/tooling/check/index.d.mts +12 -0
- package/esm/tooling/check/index.mjs +3 -2
- package/esm/tooling/check/run-check.d.mts +2 -0
- package/esm/tooling/check/run-check.d.mts.map +1 -1
- package/esm/tooling/check/run-check.mjs +23 -5
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/check/run-tests.mjs +1 -1
- package/esm/tooling/check/source-ast.d.mts +1 -0
- package/esm/tooling/check/translation-conventions.mjs +32 -0
- package/esm/tooling/check/translation-conventions.mjs.map +1 -0
- package/esm/tooling/check/web-conventions.d.mts +10 -0
- package/esm/tooling/check/web-conventions.d.mts.map +1 -0
- package/esm/tooling/check/web-conventions.mjs +126 -0
- package/esm/tooling/check/web-conventions.mjs.map +1 -0
- package/esm/tooling/index.d.mts +7 -2
- package/esm/tooling/index.mjs +4 -2
- package/esm/tooling/inspect/collect-files.mjs +172 -0
- package/esm/tooling/inspect/collect-files.mjs.map +1 -0
- package/esm/tooling/inspect/collect-pages.mjs +3 -2
- package/esm/tooling/inspect/collect-pages.mjs.map +1 -1
- package/esm/tooling/inspect/collect-routes.mjs +1 -1
- package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
- package/esm/tooling/inspect/explain-file.d.mts +12 -0
- package/esm/tooling/inspect/explain-file.d.mts.map +1 -0
- package/esm/tooling/inspect/explain-file.mjs +45 -0
- package/esm/tooling/inspect/explain-file.mjs.map +1 -0
- package/esm/tooling/inspect/index.d.mts +3 -2
- package/esm/tooling/inspect/index.mjs +2 -1
- package/esm/tooling/inspect/inspect-app.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-app.mjs +9 -3
- package/esm/tooling/inspect/inspect-app.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs +34 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts +17 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.mjs +2 -1
- package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -1
- package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -1
- package/esm/tooling/mcp/mcp-tools.mjs +8 -2
- package/esm/tooling/mcp/mcp-tools.mjs.map +1 -1
- package/esm/tooling/problem.d.mts +4 -0
- package/esm/tooling/problem.d.mts.map +1 -1
- package/esm/tooling/problem.mjs +1 -0
- package/esm/tooling/problem.mjs.map +1 -1
- package/esm/tooling/syntax/ast.d.mts +2 -0
- package/esm/tooling/typecheck/run-typecheck.mjs +5 -1
- package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -1
- package/esm/upgrade/codemods/index.mjs +2 -0
- package/esm/upgrade/codemods/index.mjs.map +1 -1
- package/esm/upgrade/codemods/vite-vitest.mjs +170 -0
- package/esm/upgrade/codemods/vite-vitest.mjs.map +1 -0
- package/esm/upgrade/notes.mjs +4 -0
- package/esm/upgrade/notes.mjs.map +1 -1
- package/esm/vite/index.d.mts +3 -1
- package/esm/vite/index.mjs +3 -1
- package/esm/vite/test-setup-reporter.d.mts +16 -0
- package/esm/vite/test-setup-reporter.d.mts.map +1 -0
- package/esm/vite/test-setup-reporter.mjs +55 -0
- package/esm/vite/test-setup-reporter.mjs.map +1 -0
- package/esm/vite/test-workers.d.mts +5 -0
- package/esm/vite/test-workers.d.mts.map +1 -0
- package/esm/vite/test-workers.mjs +38 -0
- package/esm/vite/test-workers.mjs.map +1 -0
- package/esm/warlock-config/index.d.mts +2 -1
- package/esm/warlock-config/types.d.mts +3 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +874 -9
- package/llms.txt +2 -0
- package/package.json +15 -14
- package/schemas/inspect-report.schema.json +47 -1
- package/skills/build-budgets/SKILL.md +51 -0
- package/skills/build-restful/SKILL.md +56 -0
- package/skills/configure-app/SKILL.md +7 -0
- package/skills/create-module/SKILL.md +9 -0
- package/skills/generate-web-ui/SKILL.md +71 -0
- package/skills/index.md +2 -2
- package/skills/register-route/SKILL.md +13 -0
- package/skills/run-app/SKILL.md +1 -1
- package/skills/send-mail/SKILL.md +25 -0
- package/skills/send-response/SKILL.md +50 -0
- package/skills/test-browser/SKILL.md +53 -0
- package/skills/test-http/SKILL.md +91 -0
- package/skills/test-service/SKILL.md +161 -3
- package/skills/upgrade-from-5-29/SKILL.md +109 -0
- package/skills/use-localization/SKILL.md +25 -0
- package/skills/warlock-check/SKILL.md +75 -1
- package/skills/warlock-inspect/SKILL.md +45 -2
- package/skills/warlock-mcp/SKILL.md +4 -0
- package/skills/write-cli-command/SKILL.md +18 -0
- package/esm/production/build-app-production.mjs +0 -14
- package/esm/production/build-app-production.mjs.map +0 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region ../../packages/core/src/generations/design-system-payload.json
|
|
2
|
+
var dependencies = {
|
|
3
|
+
"@mongez/react-form": "4.0.0",
|
|
4
|
+
"radix-ui": "1.7.0",
|
|
5
|
+
"cmdk": "1.1.1",
|
|
6
|
+
"react-day-picker": "9.14.0",
|
|
7
|
+
"date-fns": "4.4.0",
|
|
8
|
+
"cn": "0.4.0",
|
|
9
|
+
"lucide-react": "1.52.0",
|
|
10
|
+
"class-variance-authority": "0.7.1",
|
|
11
|
+
"clsx": "2.1.1",
|
|
12
|
+
"tailwind-merge": "3.7.0"
|
|
13
|
+
};
|
|
14
|
+
var files = {
|
|
15
|
+
"src/web/components/ui/button.tsx": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"cn\";\nimport { Slot } from \"radix-ui\";\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean;\n }) {\n const Comp = asChild ? Slot.Root : \"button\";\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n />\n );\n}\n\nexport { Button, buttonVariants };\n",
|
|
16
|
+
"src/web/components/ui/calendar.tsx": "\"use client\";\n\nimport * as React from \"react\";\nimport { useCalendarDayButton } from \"web/design-system/hooks/use-calendar-day-button\";\nimport { cn } from \"cn\";\nimport { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport { DayPicker, getDefaultClassNames, type DayButton } from \"react-day-picker\";\n\nimport { Button, buttonVariants } from \"web/components/ui/button\";\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n captionLayout = \"label\",\n buttonVariant = \"ghost\",\n formatters,\n components,\n ...props\n}: React.ComponentProps<typeof DayPicker> & {\n buttonVariant?: React.ComponentProps<typeof Button>[\"variant\"];\n}) {\n const defaultClassNames = getDefaultClassNames();\n\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n className={cn(\n \"group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent\",\n String.raw`rtl:**:[.rdp-button\\_next>svg]:rotate-180`,\n String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n className,\n )}\n captionLayout={captionLayout}\n formatters={{\n formatMonthDropdown: (date) => date.toLocaleString(\"default\", { month: \"short\" }),\n ...formatters,\n }}\n classNames={{\n root: cn(\"w-fit\", defaultClassNames.root),\n months: cn(\"relative flex flex-col gap-4 md:flex-row\", defaultClassNames.months),\n month: cn(\"flex w-full flex-col gap-4\", defaultClassNames.month),\n nav: cn(\n \"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1\",\n defaultClassNames.nav,\n ),\n button_previous: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) p-0 select-none aria-disabled:opacity-50\",\n defaultClassNames.button_previous,\n ),\n button_next: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) p-0 select-none aria-disabled:opacity-50\",\n defaultClassNames.button_next,\n ),\n month_caption: cn(\n \"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)\",\n defaultClassNames.month_caption,\n ),\n dropdowns: cn(\n \"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium\",\n defaultClassNames.dropdowns,\n ),\n dropdown_root: cn(\n \"relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50\",\n defaultClassNames.dropdown_root,\n ),\n dropdown: cn(\"absolute inset-0 bg-popover opacity-0\", defaultClassNames.dropdown),\n caption_label: cn(\n \"font-medium select-none\",\n captionLayout === \"label\"\n ? \"text-sm\"\n : \"flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground\",\n defaultClassNames.caption_label,\n ),\n month_grid: cn(\"w-full border-collapse\", defaultClassNames.month_grid),\n weekdays: cn(\"flex\", defaultClassNames.weekdays),\n weekday: cn(\n \"flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none\",\n defaultClassNames.weekday,\n ),\n week: cn(\"mt-2 flex w-full\", defaultClassNames.week),\n week_number_header: cn(\"w-(--cell-size) select-none\", defaultClassNames.week_number_header),\n week_number: cn(\n \"text-[0.8rem] text-muted-foreground select-none\",\n defaultClassNames.week_number,\n ),\n day: cn(\n \"group/day relative aspect-square h-full w-full p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-md\",\n props.showWeekNumber\n ? \"[&:nth-child(2)[data-selected=true]_button]:rounded-l-md\"\n : \"[&:first-child[data-selected=true]_button]:rounded-l-md\",\n defaultClassNames.day,\n ),\n range_start: cn(\"rounded-l-md bg-accent\", defaultClassNames.range_start),\n range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n range_end: cn(\"rounded-r-md bg-accent\", defaultClassNames.range_end),\n today: cn(\n \"rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none\",\n defaultClassNames.today,\n ),\n outside: cn(\n \"text-muted-foreground aria-selected:text-muted-foreground\",\n defaultClassNames.outside,\n ),\n disabled: cn(\"text-muted-foreground opacity-50\", defaultClassNames.disabled),\n hidden: cn(\"invisible\", defaultClassNames.hidden),\n ...classNames,\n }}\n components={{\n Root: ({ className, rootRef, ...props }) => {\n return <div data-slot=\"calendar\" ref={rootRef} className={cn(className)} {...props} />;\n },\n Chevron: ({ className, orientation, ...props }) => {\n if (orientation === \"left\") {\n return <ChevronLeftIcon className={cn(\"size-4\", className)} {...props} />;\n }\n\n if (orientation === \"right\") {\n return <ChevronRightIcon className={cn(\"size-4\", className)} {...props} />;\n }\n\n return <ChevronDownIcon className={cn(\"size-4\", className)} {...props} />;\n },\n DayButton: CalendarDayButton,\n WeekNumber: ({ children, ...props }) => {\n return (\n <td {...props}>\n <div className=\"flex size-(--cell-size) items-center justify-center text-center\">\n {children}\n </div>\n </td>\n );\n },\n ...components,\n }}\n {...props}\n />\n );\n}\n\nfunction CalendarDayButton({\n className,\n day,\n modifiers,\n ...props\n}: React.ComponentProps<typeof DayButton>) {\n const defaultClassNames = getDefaultClassNames();\n\n const ref = useCalendarDayButton(modifiers.focused);\n\n return (\n <Button\n ref={ref}\n variant=\"ghost\"\n size=\"icon\"\n data-day={day.date.toLocaleDateString()}\n data-selected-single={\n modifiers.selected &&\n !modifiers.range_start &&\n !modifiers.range_end &&\n !modifiers.range_middle\n }\n data-range-start={modifiers.range_start}\n data-range-end={modifiers.range_end}\n data-range-middle={modifiers.range_middle}\n className={cn(\n \"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70\",\n defaultClassNames.day,\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Calendar, CalendarDayButton };\n",
|
|
17
|
+
"src/web/components/ui/checkbox.tsx": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"cn\";\nimport { CheckIcon } from \"lucide-react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\n\nfunction Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n return (\n <CheckboxPrimitive.Root\n data-slot=\"checkbox\"\n className={cn(\n \"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary\",\n className,\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className=\"grid place-content-center text-current transition-none\"\n >\n <CheckIcon className=\"size-3.5\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n );\n}\n\nexport { Checkbox };\n",
|
|
18
|
+
"src/web/components/ui/command.tsx": "\"use client\";\n\nimport * as React from \"react\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { cn } from \"cn\";\nimport { SearchIcon } from \"lucide-react\";\n\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"web/components/ui/dialog\";\n\nfunction Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {\n return (\n <CommandPrimitive\n data-slot=\"command\"\n className={cn(\n \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CommandDialog({\n title = \"Command Palette\",\n description = \"Search for a command to run...\",\n children,\n className,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof Dialog> & {\n title?: string;\n description?: string;\n className?: string;\n showCloseButton?: boolean;\n}) {\n return (\n <Dialog {...props}>\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <DialogContent\n className={cn(\"overflow-hidden p-0\", className)}\n showCloseButton={showCloseButton}\n >\n <Command className=\"**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction CommandInput({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Input>) {\n return (\n <div data-slot=\"command-input-wrapper\" className=\"flex h-9 items-center gap-2 border-b px-3\">\n <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n <CommandPrimitive.Input\n data-slot=\"command-input\"\n className={cn(\n \"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n className,\n )}\n {...props}\n />\n </div>\n );\n}\n\nfunction CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {\n return (\n <CommandPrimitive.List\n data-slot=\"command-list\"\n className={cn(\"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\", className)}\n {...props}\n />\n );\n}\n\nfunction CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n return (\n <CommandPrimitive.Empty\n data-slot=\"command-empty\"\n className=\"py-6 text-center text-sm\"\n {...props}\n />\n );\n}\n\nfunction CommandGroup({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Group>) {\n return (\n <CommandPrimitive.Group\n data-slot=\"command-group\"\n className={cn(\n \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CommandSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n return (\n <CommandPrimitive.Separator\n data-slot=\"command-separator\"\n className={cn(\"-mx-1 h-px bg-border\", className)}\n {...props}\n />\n );\n}\n\nfunction CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {\n return (\n <CommandPrimitive.Item\n data-slot=\"command-item\"\n className={cn(\n \"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CommandShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"command-shortcut\"\n className={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Command,\n CommandDialog,\n CommandInput,\n CommandList,\n CommandEmpty,\n CommandGroup,\n CommandItem,\n CommandShortcut,\n CommandSeparator,\n};\n",
|
|
19
|
+
"src/web/components/ui/dialog.tsx": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"cn\";\nimport { XIcon } from \"lucide-react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\n\nimport { Button } from \"web/components/ui/button\";\n\nfunction Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n showCloseButton?: boolean;\n}) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n className={cn(\n \"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg\",\n className,\n )}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close\n data-slot=\"dialog-close\"\n className=\"absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n >\n <XIcon />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogFooter({\n className,\n showCloseButton = false,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & {\n showCloseButton?: boolean;\n}) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close asChild>\n <Button variant=\"outline\">Close</Button>\n </DialogPrimitive.Close>\n )}\n </div>\n );\n}\n\nfunction DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn(\"text-lg leading-none font-semibold\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n",
|
|
20
|
+
"src/web/components/ui/input.tsx": "import * as React from \"react\";\nimport { cn } from \"cn\";\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n return (\n <input\n type={type}\n data-slot=\"input\"\n className={cn(\n \"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Input };\n",
|
|
21
|
+
"src/web/components/ui/label.tsx": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"cn\";\nimport { Label as LabelPrimitive } from \"radix-ui\";\n\nfunction Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {\n return (\n <LabelPrimitive.Root\n data-slot=\"label\"\n className={cn(\n \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Label };\n",
|
|
22
|
+
"src/web/components/ui/LICENSE.md": "MIT License\n\nCopyright (c) 2023 shadcn\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
|
|
23
|
+
"src/web/components/ui/popover.tsx": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"cn\";\nimport { Popover as PopoverPrimitive } from \"radix-ui\";\n\nfunction Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />;\n}\n\nfunction PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />;\n}\n\nfunction PopoverContent({\n className,\n align = \"center\",\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n data-slot=\"popover-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n className,\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n );\n}\n\nfunction PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />;\n}\n\nfunction PopoverHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"popover-header\"\n className={cn(\"flex flex-col gap-1 text-sm\", className)}\n {...props}\n />\n );\n}\n\nfunction PopoverTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n return <div data-slot=\"popover-title\" className={cn(\"font-medium\", className)} {...props} />;\n}\n\nfunction PopoverDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"popover-description\"\n className={cn(\"text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Popover,\n PopoverTrigger,\n PopoverContent,\n PopoverAnchor,\n PopoverHeader,\n PopoverTitle,\n PopoverDescription,\n};\n",
|
|
24
|
+
"src/web/components/ui/radio-group.tsx": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"cn\";\nimport { CircleIcon } from \"lucide-react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\n\nfunction RadioGroup({\n className,\n ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n return (\n <RadioGroupPrimitive.Root\n data-slot=\"radio-group\"\n className={cn(\"grid gap-3\", className)}\n {...props}\n />\n );\n}\n\nfunction RadioGroupItem({\n className,\n ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n return (\n <RadioGroupPrimitive.Item\n data-slot=\"radio-group-item\"\n className={cn(\n \"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40\",\n className,\n )}\n {...props}\n >\n <RadioGroupPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n className=\"relative flex items-center justify-center\"\n >\n <CircleIcon className=\"absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary\" />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n );\n}\n\nexport { RadioGroup, RadioGroupItem };\n",
|
|
25
|
+
"src/web/components/ui/switch.tsx": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"cn\";\nimport { Switch as SwitchPrimitive } from \"radix-ui\";\n\nfunction Switch({\n className,\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root> & {\n size?: \"sm\" | \"default\";\n}) {\n return (\n <SwitchPrimitive.Root\n data-slot=\"switch\"\n data-size={size}\n className={cn(\n \"peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80\",\n className,\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n data-slot=\"switch-thumb\"\n className={cn(\n \"pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground\",\n )}\n />\n </SwitchPrimitive.Root>\n );\n}\n\nexport { Switch };\n",
|
|
26
|
+
"src/web/components/ui/textarea.tsx": "import * as React from \"react\";\nimport { cn } from \"cn\";\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n \"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Textarea };\n",
|
|
27
|
+
"src/web/design-system/components/checkbox-input.tsx": "import { Checkbox } from \"web/components/ui/checkbox\";\nimport { useControl } from \"../hooks/use-control\";\nimport type { CheckProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function CheckboxInput(props: CheckProps) {\n const field = useControl(props, \"checkbox\");\n return (\n <FieldFrame {...props} error={field.error}>\n <Checkbox\n {...field.binding}\n disabled={field.disabled || props.readOnly}\n checked={Boolean(field.checked)}\n onCheckedChange={(checked) => field.setChecked(checked === true)}\n aria-labelledby={`${props.id}-label`}\n />\n </FieldFrame>\n );\n}\n",
|
|
28
|
+
"src/web/design-system/components/date-picker-input.tsx": "import { Button } from \"web/components/ui/button\";\nimport { Calendar } from \"web/components/ui/calendar\";\n\nimport { Popover, PopoverContent, PopoverTrigger } from \"web/components/ui/popover\";\nimport { useDateControl } from \"../hooks/use-date-control\";\nimport type { DateProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function DatePickerInput(props: DateProps) {\n const field = useDateControl(props, false);\n return (\n <FieldFrame {...props} error={field.error}>\n <Popover modal open={field.open} onOpenChange={field.setOpen}>\n <PopoverTrigger asChild>\n <Button\n {...field.binding}\n type=\"button\"\n variant=\"outline\"\n disabled={field.disabled || props.readOnly}\n aria-haspopup=\"dialog\"\n aria-controls={`${props.id}-dialog`}\n aria-labelledby={`${props.id}-label ${props.id}-value`}\n >\n <span id={`${props.id}-value`}>\n {field.value || props.placeholder || \"Choose date\"}\n </span>\n </Button>\n </PopoverTrigger>\n <PopoverContent\n id={`${props.id}-dialog`}\n aria-modal=\"true\"\n dir={props.dir}\n align=\"start\"\n aria-labelledby={`${props.id}-label`}\n onCloseAutoFocus={field.onBlur}\n >\n <Calendar\n id={`${props.id}-calendar`}\n mode=\"single\"\n autoFocus\n dir={props.dir}\n selected={field.selected}\n defaultMonth={field.selected}\n onSelect={field.select}\n />\n\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"ghost\" onClick={field.clear}>\n {props.clearLabel ?? \"Clear\"}\n </Button>\n <Button type=\"button\" variant=\"ghost\" onClick={() => field.setOpen(false)}>\n {props.closeLabel ?? \"Close\"}\n </Button>\n </div>\n </PopoverContent>\n </Popover>\n </FieldFrame>\n );\n}\n",
|
|
29
|
+
"src/web/design-system/components/date-time-picker-input.tsx": "import { Button } from \"web/components/ui/button\";\nimport { Calendar } from \"web/components/ui/calendar\";\nimport { Input } from \"web/components/ui/input\";\nimport { Label } from \"web/components/ui/label\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"web/components/ui/popover\";\nimport { useDateControl } from \"../hooks/use-date-control\";\nimport type { DateProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function DateTimePickerInput(props: DateProps) {\n const field = useDateControl(props, true);\n return (\n <FieldFrame {...props} error={field.error}>\n <Popover modal open={field.open} onOpenChange={field.setOpen}>\n <PopoverTrigger asChild>\n <Button\n {...field.binding}\n type=\"button\"\n variant=\"outline\"\n disabled={field.disabled || props.readOnly}\n aria-haspopup=\"dialog\"\n aria-controls={`${props.id}-dialog`}\n aria-labelledby={`${props.id}-label ${props.id}-value`}\n >\n <span id={`${props.id}-value`}>\n {field.value || props.placeholder || \"Choose date\"}\n </span>\n </Button>\n </PopoverTrigger>\n <PopoverContent\n id={`${props.id}-dialog`}\n aria-modal=\"true\"\n dir={props.dir}\n align=\"start\"\n aria-labelledby={`${props.id}-label`}\n onCloseAutoFocus={field.onBlur}\n >\n <Calendar\n id={`${props.id}-calendar`}\n mode=\"single\"\n autoFocus\n dir={props.dir}\n selected={field.selected}\n defaultMonth={field.selected}\n onSelect={field.select}\n />\n <Label htmlFor={`${props.id}-time`}>{props.timeLabel ?? \"Time\"}</Label>\n <Input\n id={`${props.id}-time`}\n type=\"time\"\n value={field.time}\n disabled={!field.selected}\n onChange={(event) => field.changeTime(event.target.value)}\n />\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"ghost\" onClick={field.clear}>\n {props.clearLabel ?? \"Clear\"}\n </Button>\n <Button type=\"button\" variant=\"ghost\" onClick={() => field.setOpen(false)}>\n {props.closeLabel ?? \"Close\"}\n </Button>\n </div>\n </PopoverContent>\n </Popover>\n </FieldFrame>\n );\n}\n",
|
|
30
|
+
"src/web/design-system/components/email-input.tsx": "import { Input } from \"web/components/ui/input\";\nimport { useControl } from \"../hooks/use-control\";\nimport type { ControlProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function EmailInput(props: ControlProps) {\n const field = useControl(props, \"email\");\n return (\n <FieldFrame {...props} error={field.error}>\n <Input\n {...field.binding}\n type=\"email\"\n value={typeof field.value === \"string\" ? field.value : \"\"}\n onChange={(event) => field.changeValue(event.target.value)}\n placeholder={props.placeholder}\n readOnly={props.readOnly}\n autoComplete={props.autoComplete}\n dir={props.dir}\n />\n </FieldFrame>\n );\n}\n",
|
|
31
|
+
"src/web/design-system/components/field-frame.tsx": "import type { ReactNode } from \"react\";\nimport { Label } from \"web/components/ui/label\";\nimport type { ControlProps } from \"../types/control-props\";\n\nexport default function FieldFrame({\n id,\n label,\n hint,\n error,\n children,\n dir,\n className,\n}: Pick<ControlProps, \"id\" | \"label\" | \"hint\" | \"error\" | \"dir\" | \"className\"> & {\n children: ReactNode;\n}) {\n return (\n <div dir={dir} className={className ?? \"grid gap-2 text-start\"}>\n {label != null ? (\n <Label id={`${id}-label`} htmlFor={id}>\n {label}\n </Label>\n ) : null}\n {children}\n {hint ? (\n <p id={`${id}-hint`} className=\"text-sm text-muted-foreground\">\n {hint}\n </p>\n ) : null}\n {error ? (\n <p id={`${id}-error`} role=\"alert\" className=\"text-sm text-destructive\">\n {error}\n </p>\n ) : null}\n </div>\n );\n}\n",
|
|
32
|
+
"src/web/design-system/components/file-input.tsx": "import { Input } from \"web/components/ui/input\";\nimport { useFileControl } from \"../hooks/use-file-control\";\nimport type { ControlProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function FileInput(props: ControlProps) {\n const field = useFileControl(props);\n return (\n <FieldFrame {...props} error={field.error}>\n <Input\n {...field.binding}\n type=\"file\"\n disabled={field.disabled || props.readOnly}\n accept={props.accept}\n multiple={props.multiple}\n onChange={(event) =>\n field.changeValue(\n props.multiple ? Array.from(event.target.files ?? []) : (event.target.files?.[0] ?? \"\"),\n )\n }\n />\n </FieldFrame>\n );\n}\n",
|
|
33
|
+
"src/web/design-system/components/multi-select-input.tsx": "import { Button } from \"web/components/ui/button\";\nimport { Input } from \"web/components/ui/input\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"web/components/ui/popover\";\nimport { useSelectControl } from \"../hooks/use-select-control\";\nimport type { SelectProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function MultiSelectInput(props: SelectProps) {\n const field = useSelectControl(props, true);\n return (\n <FieldFrame {...props} error={field.error}>\n <Popover open={field.open} onOpenChange={field.setOpen}>\n <PopoverTrigger asChild>\n <Button\n {...field.binding}\n type=\"button\"\n variant=\"outline\"\n disabled={field.disabled || props.readOnly}\n aria-labelledby={`${props.id}-label ${props.id}-value`}\n aria-haspopup=\"dialog\"\n aria-controls={`${props.id}-dialog`}\n >\n <span id={`${props.id}-value`}>\n {props.options\n .filter((option) => field.selected.includes(option.value))\n .map((option) => option.label)\n .join(\", \") ||\n props.placeholder ||\n \"Choose\"}\n </span>\n </Button>\n </PopoverTrigger>\n <PopoverContent\n id={`${props.id}-dialog`}\n dir={props.dir}\n align=\"start\"\n aria-labelledby={`${props.id}-label`}\n onCloseAutoFocus={field.onBlur}\n >\n <Input\n id={`${props.id}-search`}\n role=\"combobox\"\n aria-label={props.searchLabel ?? \"Search options\"}\n aria-expanded={field.open}\n aria-controls={`${props.id}-listbox`}\n aria-autocomplete=\"list\"\n aria-activedescendant={\n field.activeOption\n ? `${props.id}-option-${encodeURIComponent(field.activeOption.value)}`\n : undefined\n }\n value={field.search}\n onChange={(event) => field.setSearch(event.target.value)}\n onKeyDown={field.onKeyDown}\n />\n <div\n id={`${props.id}-listbox`}\n role=\"listbox\"\n aria-labelledby={`${props.id}-label`}\n aria-multiselectable={true}\n className=\"max-h-60 overflow-auto py-2\"\n >\n {field.filtered.map((option) => (\n <Button\n key={option.value}\n id={`${props.id}-option-${encodeURIComponent(option.value)}`}\n type=\"button\"\n role=\"option\"\n tabIndex={-1}\n aria-selected={field.selected.includes(option.value)}\n aria-disabled={option.disabled}\n disabled={option.disabled}\n variant=\"ghost\"\n className=\"w-full justify-start aria-selected:bg-accent data-[active=true]:ring-2 data-[active=true]:ring-ring\"\n data-active={field.activeOption === option}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => field.select(option.value)}\n >\n {field.selected.includes(option.value) ? \"✓ \" : \"\"}\n {option.label}\n </Button>\n ))}\n </div>\n {field.filtered.length === 0 ? (\n <p role=\"status\">{props.emptyLabel ?? \"No options found\"}</p>\n ) : null}\n <Button type=\"button\" variant=\"ghost\" onClick={field.clear}>\n {props.clearLabel ?? \"Clear\"}\n </Button>\n </PopoverContent>\n </Popover>\n </FieldFrame>\n );\n}\n",
|
|
34
|
+
"src/web/design-system/components/password-input.tsx": "import { Input } from \"web/components/ui/input\";\nimport { useControl } from \"../hooks/use-control\";\nimport type { ControlProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function PasswordInput(props: ControlProps) {\n const field = useControl(props, \"password\");\n return (\n <FieldFrame {...props} error={field.error}>\n <Input\n {...field.binding}\n type=\"password\"\n value={typeof field.value === \"string\" ? field.value : \"\"}\n onChange={(event) => field.changeValue(event.target.value)}\n placeholder={props.placeholder}\n readOnly={props.readOnly}\n autoComplete={props.autoComplete}\n dir={props.dir}\n />\n </FieldFrame>\n );\n}\n",
|
|
35
|
+
"src/web/design-system/components/radio-input.tsx": "import { RadioGroup, RadioGroupItem } from \"web/components/ui/radio-group\";\nimport { Label } from \"web/components/ui/label\";\nimport { useControl } from \"../hooks/use-control\";\nimport type { RadioProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function RadioInput(props: RadioProps) {\n // Register the group once as a scalar; individual Radix items own keyboard focus.\n const field = useControl(props);\n return (\n <FieldFrame {...props} error={field.error}>\n <RadioGroup\n {...field.binding}\n ref={(element) => {\n field.visibleElementRef.current = element;\n }}\n dir={props.dir}\n disabled={field.disabled || props.readOnly}\n value={typeof field.value === \"string\" ? field.value : \"\"}\n onValueChange={field.changeValue}\n aria-labelledby={`${props.id}-label`}\n >\n {props.options.map((option) => (\n <div key={option.value} className=\"flex items-center gap-2\">\n <RadioGroupItem\n ref={\n option === props.options.find((item) => !item.disabled) ? field.inputRef : undefined\n }\n id={`${props.id}-option-${encodeURIComponent(option.value)}`}\n value={option.value}\n disabled={option.disabled}\n />\n <Label htmlFor={`${props.id}-option-${encodeURIComponent(option.value)}`}>\n {option.label}\n </Label>\n </div>\n ))}\n </RadioGroup>\n </FieldFrame>\n );\n}\n",
|
|
36
|
+
"src/web/design-system/components/select-input.tsx": "import { Button } from \"web/components/ui/button\";\nimport { Input } from \"web/components/ui/input\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"web/components/ui/popover\";\nimport { useSelectControl } from \"../hooks/use-select-control\";\nimport type { SelectProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function SelectInput(props: SelectProps) {\n const field = useSelectControl(props, false);\n return (\n <FieldFrame {...props} error={field.error}>\n <Popover open={field.open} onOpenChange={field.setOpen}>\n <PopoverTrigger asChild>\n <Button\n {...field.binding}\n type=\"button\"\n variant=\"outline\"\n disabled={field.disabled || props.readOnly}\n aria-labelledby={`${props.id}-label ${props.id}-value`}\n aria-haspopup=\"dialog\"\n aria-controls={`${props.id}-dialog`}\n >\n <span id={`${props.id}-value`}>\n {props.options\n .filter((option) => field.selected.includes(option.value))\n .map((option) => option.label)\n .join(\", \") ||\n props.placeholder ||\n \"Choose\"}\n </span>\n </Button>\n </PopoverTrigger>\n <PopoverContent\n id={`${props.id}-dialog`}\n dir={props.dir}\n align=\"start\"\n aria-labelledby={`${props.id}-label`}\n onCloseAutoFocus={field.onBlur}\n >\n <Input\n id={`${props.id}-search`}\n role=\"combobox\"\n aria-label={props.searchLabel ?? \"Search options\"}\n aria-expanded={field.open}\n aria-controls={`${props.id}-listbox`}\n aria-autocomplete=\"list\"\n aria-activedescendant={\n field.activeOption\n ? `${props.id}-option-${encodeURIComponent(field.activeOption.value)}`\n : undefined\n }\n value={field.search}\n onChange={(event) => field.setSearch(event.target.value)}\n onKeyDown={field.onKeyDown}\n />\n <div\n id={`${props.id}-listbox`}\n role=\"listbox\"\n aria-labelledby={`${props.id}-label`}\n aria-multiselectable={false}\n className=\"max-h-60 overflow-auto py-2\"\n >\n {field.filtered.map((option) => (\n <Button\n key={option.value}\n id={`${props.id}-option-${encodeURIComponent(option.value)}`}\n type=\"button\"\n role=\"option\"\n tabIndex={-1}\n aria-selected={field.selected.includes(option.value)}\n aria-disabled={option.disabled}\n disabled={option.disabled}\n variant=\"ghost\"\n className=\"w-full justify-start aria-selected:bg-accent data-[active=true]:ring-2 data-[active=true]:ring-ring\"\n data-active={field.activeOption === option}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => field.select(option.value)}\n >\n {field.selected.includes(option.value) ? \"✓ \" : \"\"}\n {option.label}\n </Button>\n ))}\n </div>\n {field.filtered.length === 0 ? (\n <p role=\"status\">{props.emptyLabel ?? \"No options found\"}</p>\n ) : null}\n <Button type=\"button\" variant=\"ghost\" onClick={field.clear}>\n {props.clearLabel ?? \"Clear\"}\n </Button>\n </PopoverContent>\n </Popover>\n </FieldFrame>\n );\n}\n",
|
|
37
|
+
"src/web/design-system/components/submit-button.tsx": "import { Button } from \"web/components/ui/button\";\nimport { useSubmitControl } from \"../hooks/use-submit-control\";\nimport type { ControlProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function SubmitButton(props: ControlProps) {\n const field = useSubmitControl(props);\n return (\n <FieldFrame {...props} label={null} error={field.error}>\n <Button\n {...field.binding}\n type=\"submit\"\n disabled={field.disabled}\n aria-busy={field.isSubmitting}\n >\n {props.label}\n </Button>\n </FieldFrame>\n );\n}\n",
|
|
38
|
+
"src/web/design-system/components/switch-input.tsx": "import { Switch } from \"web/components/ui/switch\";\nimport { useControl } from \"../hooks/use-control\";\nimport type { CheckProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function SwitchInput(props: CheckProps) {\n const field = useControl(props, \"checkbox\");\n return (\n <FieldFrame {...props} error={field.error}>\n <Switch\n {...field.binding}\n disabled={field.disabled || props.readOnly}\n checked={Boolean(field.checked)}\n onCheckedChange={(checked) => field.setChecked(checked === true)}\n aria-labelledby={`${props.id}-label`}\n />\n </FieldFrame>\n );\n}\n",
|
|
39
|
+
"src/web/design-system/components/text-input.tsx": "import { Input } from \"web/components/ui/input\";\nimport { useControl } from \"../hooks/use-control\";\nimport type { ControlProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function TextInput(props: ControlProps) {\n const field = useControl(props, \"text\");\n return (\n <FieldFrame {...props} error={field.error}>\n <Input\n {...field.binding}\n type=\"text\"\n value={typeof field.value === \"string\" ? field.value : \"\"}\n onChange={(event) => field.changeValue(event.target.value)}\n placeholder={props.placeholder}\n readOnly={props.readOnly}\n autoComplete={props.autoComplete}\n dir={props.dir}\n />\n </FieldFrame>\n );\n}\n",
|
|
40
|
+
"src/web/design-system/components/textarea-input.tsx": "import { Textarea } from \"web/components/ui/textarea\";\nimport { useControl } from \"../hooks/use-control\";\nimport type { ControlProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function TextareaInput(props: ControlProps) {\n const field = useControl(props, \"textarea\");\n return (\n <FieldFrame {...props} error={field.error}>\n <Textarea\n {...field.binding}\n rows={props.rows}\n value={typeof field.value === \"string\" ? field.value : \"\"}\n onChange={(event) => field.changeValue(event.target.value)}\n placeholder={props.placeholder}\n readOnly={props.readOnly}\n autoComplete={props.autoComplete}\n dir={props.dir}\n />\n </FieldFrame>\n );\n}\n",
|
|
41
|
+
"src/web/design-system/components/time-picker-input.tsx": "import { Input } from \"web/components/ui/input\";\nimport { useControl } from \"../hooks/use-control\";\nimport type { ControlProps } from \"../types/control-props\";\nimport FieldFrame from \"./field-frame\";\n\nexport default function TimePickerInput(props: ControlProps) {\n const field = useControl(props, \"time\");\n return (\n <FieldFrame {...props} error={field.error}>\n <Input\n {...field.binding}\n type=\"time\"\n value={typeof field.value === \"string\" ? field.value : \"\"}\n onChange={(event) => field.changeValue(event.target.value)}\n placeholder={props.placeholder}\n readOnly={props.readOnly}\n autoComplete={props.autoComplete}\n dir={props.dir}\n />\n </FieldFrame>\n );\n}\n",
|
|
42
|
+
"src/web/design-system/hooks/use-calendar-day-button.ts": "import { useEffect, useRef } from \"react\";\n\nexport function useCalendarDayButton(focused: boolean) {\n const ref = useRef<HTMLButtonElement>(null);\n useEffect(() => {\n if (focused) ref.current?.focus();\n }, [focused]);\n return ref;\n}\n",
|
|
43
|
+
"src/web/design-system/hooks/use-control.ts": "import { useFormControl } from \"@mongez/react-form\";\nimport type { ControlProps } from \"../types/control-props\";\n\n/** Explicit ids keep SSR, hydration, portal labels and repeated forms aligned. */\nexport function useControl(props: ControlProps, type = \"text\", multiple = false) {\n const control = useFormControl(\n { ...props, type, multiple },\n { multiple, isCollectable: type === \"submit\" ? () => false : undefined },\n );\n const error = props.error ?? control.error;\n const describedBy =\n [props.hint ? `${props.id}-hint` : \"\", error ? `${props.id}-error` : \"\"]\n .filter(Boolean)\n .join(\" \") || undefined;\n const binding = {\n id: props.id,\n name: props.name,\n disabled: control.disabled,\n onBlur: control.onBlur,\n ref: (element: HTMLElement | null) => {\n control.inputRef.current = element;\n control.visibleElementRef.current = element;\n },\n \"aria-invalid\": Boolean(error),\n \"aria-required\": props.required || undefined,\n \"aria-describedby\": describedBy,\n };\n return { ...control, error, binding };\n}\n",
|
|
44
|
+
"src/web/design-system/hooks/use-date-control.ts": "import { useState } from \"react\";\nimport { useControl } from \"./use-control\";\nimport type { DateProps } from \"../types/control-props\";\n\nexport function calendarDate(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}-${String(date.getDate()).padStart(2, \"0\")}`;\n}\nexport function parseCalendarDate(value: string): Date | undefined {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return undefined;\n const [year = 0, month = 0, day = 0] = value.split(\"-\").map(Number);\n const date = new Date(0);\n date.setFullYear(year, month - 1, day);\n date.setHours(12, 0, 0, 0);\n return calendarDate(date) === value ? date : undefined;\n}\nexport function useDateControl(props: DateProps, withTime = false) {\n const field = useControl(props);\n const [open, setOpen] = useState(false);\n const value = typeof field.value === \"string\" ? field.value : \"\";\n const [datePart = \"\", time = \"\"] = value.split(\"T\");\n return {\n ...field,\n open,\n setOpen,\n value,\n time,\n selected: parseCalendarDate(datePart),\n select: (date: Date | undefined) => {\n if (field.disabled || props.readOnly) return;\n field.changeValue(date ? calendarDate(date) + (withTime ? `T${time || \"00:00\"}` : \"\") : \"\");\n if (!withTime) setOpen(false);\n },\n changeTime: (next: string) => {\n if (!field.disabled && !props.readOnly)\n field.changeValue(next && parseCalendarDate(datePart) ? `${datePart}T${next}` : \"\");\n },\n clear: () => {\n if (!field.disabled && !props.readOnly) field.changeValue(\"\");\n setOpen(false);\n },\n };\n}\n",
|
|
45
|
+
"src/web/design-system/hooks/use-file-control.ts": "import { useEffect } from \"react\";\nimport { useControl } from \"./use-control\";\nimport type { ControlProps } from \"../types/control-props\";\n\nexport function useFileControl(props: ControlProps) {\n const field = useControl(\n { ...props, value: undefined, defaultValue: props.multiple ? [] : \"\" },\n \"file\",\n props.multiple,\n );\n useEffect(() => {\n const clear = () => {\n const input: HTMLInputElement | undefined = field.inputRef.current;\n if (input) input.value = \"\";\n };\n const reset = field.formControl.onReset(clear);\n const cleared = field.formControl.onClear(clear);\n return () => {\n reset.unsubscribe();\n cleared.unsubscribe();\n };\n }, [field.formControl, field.inputRef]);\n return field;\n}\n",
|
|
46
|
+
"src/web/design-system/hooks/use-select-control.ts": "import { useState, type KeyboardEvent } from \"react\";\nimport { useControl } from \"./use-control\";\nimport type { SelectProps } from \"../types/control-props\";\n\nexport function selectedValues(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((item): item is string => typeof item === \"string\")\n : typeof value === \"string\" && value !== \"\"\n ? [value]\n : [];\n}\nexport function toggleSelection(values: readonly string[], value: string): string[] {\n return values.includes(value) ? values.filter((item) => item !== value) : [...values, value];\n}\nexport function useSelectControl(props: SelectProps, multiple = false) {\n const field = useControl(props, \"text\", multiple);\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const [active, setActive] = useState(0);\n const selected = selectedValues(field.value);\n const filtered = props.options.filter((option) =>\n option.label.toLocaleLowerCase().includes(search.toLocaleLowerCase()),\n );\n const enabled = filtered.filter((option) => !option.disabled);\n const activeOption = enabled[Math.min(active, Math.max(0, enabled.length - 1))];\n const select = (value: string) => {\n if (field.disabled || props.readOnly) return;\n field.changeValue(multiple ? toggleSelection(selected, value) : value);\n if (!multiple) setOpen(false);\n };\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if ([\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(event.key)) {\n event.preventDefault();\n const current = activeOption ? enabled.indexOf(activeOption) : 0;\n const next =\n event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? enabled.length - 1\n : (current + (event.key === \"ArrowDown\" ? 1 : -1) + enabled.length) %\n Math.max(1, enabled.length);\n setActive(Math.max(0, next));\n const option = enabled[Math.max(0, next)];\n if (option)\n document\n .getElementById(`${props.id}-option-${encodeURIComponent(option.value)}`)\n ?.scrollIntoView({ block: \"nearest\" });\n } else if (\n (event.key === \"Enter\" || (multiple && event.key === \" \" && search === \"\")) &&\n activeOption\n ) {\n event.preventDefault();\n select(activeOption.value);\n }\n };\n return {\n ...field,\n open,\n setOpen: (next: boolean) => {\n setOpen(next && !field.disabled && !props.readOnly);\n setSearch(\"\");\n setActive(0);\n },\n search,\n setSearch: (next: string) => {\n setSearch(next);\n setActive(0);\n },\n selected,\n filtered,\n activeOption,\n select,\n onKeyDown,\n clear: () => {\n if (!field.disabled && !props.readOnly) field.changeValue(multiple ? [] : \"\");\n },\n };\n}\n",
|
|
47
|
+
"src/web/design-system/hooks/use-submit-control.ts": "import { useSubmitButton } from \"@mongez/react-form\";\nimport { useControl } from \"./use-control\";\nimport type { ControlProps } from \"../types/control-props\";\n\nexport function useSubmitControl(props: ControlProps) {\n const field = useControl(props, \"submit\");\n const submit = useSubmitButton();\n return {\n ...field,\n disabled: field.disabled || submit.disabled || submit.isSubmitting,\n isSubmitting: submit.isSubmitting,\n };\n}\n",
|
|
48
|
+
"src/web/design-system/types/control-props.ts": "import type { FormControlProps } from \"@mongez/react-form\";\nimport type { ReactNode } from \"react\";\n\nexport type ControlProps = Pick<\n FormControlProps,\n | \"name\"\n | \"rules\"\n | \"schema\"\n | \"validate\"\n | \"validateOn\"\n | \"onChange\"\n | \"onError\"\n | \"errors\"\n | \"errorKeys\"\n | \"disabled\"\n | \"readOnly\"\n | \"required\"\n> & {\n id: string;\n label: ReactNode;\n hint?: ReactNode;\n error?: ReactNode;\n value?: unknown;\n defaultValue?: unknown;\n placeholder?: string;\n className?: string;\n dir?: \"ltr\" | \"rtl\";\n autoComplete?: string;\n rows?: number;\n accept?: string;\n multiple?: boolean;\n};\nexport type Choice = { value: string; label: string; disabled?: boolean };\nexport type SelectProps = ControlProps & {\n options: readonly Choice[];\n searchLabel?: string;\n emptyLabel?: string;\n clearLabel?: string;\n};\nexport type RadioProps = ControlProps & { options: readonly Choice[] };\nexport type CheckProps = ControlProps & { checked?: boolean; defaultChecked?: boolean };\nexport type DateProps = ControlProps & {\n clearLabel?: string;\n closeLabel?: string;\n timeLabel?: string;\n};\n"
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { dependencies, files };
|
|
53
|
+
//# sourceMappingURL=design-system-payload.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-system-payload.mjs","names":[],"sources":["../../../../../../../../packages/core/src/generations/design-system-payload.json"],"sourcesContent":[""],"mappings":""}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { rootPath, srcPath } from "../../utils/paths.mjs";
|
|
2
|
+
import "../../utils/index.mjs";
|
|
3
|
+
import { recordSkippedFile } from "../generation-run.mjs";
|
|
4
|
+
import { ensureDirectoryAsync, fileExistsAsync, getFileAsync, getJsonFileAsync, putFileAsync } from "../generation-fs.mjs";
|
|
5
|
+
import { dependencies, files } from "../design-system-payload.mjs";
|
|
6
|
+
import { shadcnFeature } from "./shadcn.feature.mjs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
|
|
9
|
+
//#region ../../packages/core/src/generations/features/design-system.feature.ts
|
|
10
|
+
/** Run before dependency installation so refusal leaves the app untouched. */
|
|
11
|
+
async function assertDesignSystemTailwind() {
|
|
12
|
+
const manifest = await getJsonFileAsync(rootPath("package.json"));
|
|
13
|
+
const css = await fileExistsAsync(srcPath("web/app.css")) ? await getFileAsync(srcPath("web/app.css")) : "";
|
|
14
|
+
if (!(manifest.dependencies?.tailwindcss ?? manifest.devDependencies?.tailwindcss) || !/@import\s+["']tailwindcss["']/.test(css.replace(/\/\*[\s\S]*?\*\//g, ""))) throw new Error("The design system needs Tailwind CSS in src/web/app.css. Please run `warlock add tailwind` first, then `warlock add design-system`.");
|
|
15
|
+
}
|
|
16
|
+
const designSystemFeature = {
|
|
17
|
+
description: "Accessible shadcn/ui form controls wired to Mongez React Form; requires an existing Tailwind setup.",
|
|
18
|
+
requires: ["web"],
|
|
19
|
+
dependencies,
|
|
20
|
+
async onExecuting(options) {
|
|
21
|
+
await assertDesignSystemTailwind();
|
|
22
|
+
await shadcnFeature.onExecuting?.(options);
|
|
23
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
24
|
+
const target = rootPath(relativePath);
|
|
25
|
+
if (await fileExistsAsync(target)) {
|
|
26
|
+
recordSkippedFile(target, "exists");
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
await ensureDirectoryAsync(dirname(target));
|
|
30
|
+
await putFileAsync(target, content);
|
|
31
|
+
}
|
|
32
|
+
console.log("Design system controls are ready in src/web/design-system. Existing files were preserved.");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { assertDesignSystemTailwind, designSystemFeature };
|
|
38
|
+
//# sourceMappingURL=design-system.feature.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-system.feature.mjs","names":["payload.dependencies","payload.files"],"sources":["../../../../../../../../../packages/core/src/generations/features/design-system.feature.ts"],"sourcesContent":["import payload from \"../design-system-payload.json\" with { type: \"json\" };\nimport {\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n getJsonFileAsync,\n putFileAsync,\n recordSkippedFile,\n} from \"../generation-fs\";\nimport { dirname } from \"node:path\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport { shadcnFeature } from \"./shadcn.feature\";\nimport type { FeatureDefinition } from \"./types\";\n\n/** Run before dependency installation so refusal leaves the app untouched. */\nexport async function assertDesignSystemTailwind(): Promise<void> {\n const manifest = await getJsonFileAsync<{\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n }>(rootPath(\"package.json\"));\n const css = (await fileExistsAsync(srcPath(\"web/app.css\")))\n ? await getFileAsync(srcPath(\"web/app.css\"))\n : \"\";\n const hasPackage = manifest.dependencies?.tailwindcss ?? manifest.devDependencies?.tailwindcss;\n if (!hasPackage || !/@import\\s+[\"']tailwindcss[\"']/.test(css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\"))) {\n throw new Error(\n \"The design system needs Tailwind CSS in src/web/app.css. Please run `warlock add tailwind` first, then `warlock add design-system`.\",\n );\n }\n}\n\nexport const designSystemFeature: FeatureDefinition = {\n description:\n \"Accessible shadcn/ui form controls wired to Mongez React Form; requires an existing Tailwind setup.\",\n requires: [\"web\"],\n dependencies: payload.dependencies,\n async onExecuting(options) {\n await assertDesignSystemTailwind();\n await shadcnFeature.onExecuting?.(options);\n for (const [relativePath, content] of Object.entries(payload.files)) {\n const target = rootPath(relativePath);\n if (await fileExistsAsync(target)) {\n recordSkippedFile(target, \"exists\");\n continue;\n }\n await ensureDirectoryAsync(dirname(target));\n await putFileAsync(target, content);\n }\n console.log(\n \"Design system controls are ready in src/web/design-system. Existing files were preserved.\",\n );\n },\n};\n"],"mappings":";;;;;;;;;;AAeA,eAAsB,6BAA4C;CAChE,MAAM,WAAW,MAAM,iBAGpB,SAAS,cAAc,CAAC;CAC3B,MAAM,MAAO,MAAM,gBAAgB,QAAQ,aAAa,CAAC,IACrD,MAAM,aAAa,QAAQ,aAAa,CAAC,IACzC;CAEJ,IAAI,EADe,SAAS,cAAc,eAAe,SAAS,iBAAiB,gBAChE,CAAC,gCAAgC,KAAK,IAAI,QAAQ,qBAAqB,EAAE,CAAC,GAC3F,MAAM,IAAI,MACR,qIACF;AAEJ;AAEA,MAAa,sBAAyC;CACpD,aACE;CACF,UAAU,CAAC,KAAK;CACFA;CACd,MAAM,YAAY,SAAS;EACzB,MAAM,2BAA2B;EACjC,MAAM,cAAc,cAAc,OAAO;EACzC,KAAK,MAAM,CAAC,cAAc,YAAY,OAAO,QAAQC,KAAa,GAAG;GACnE,MAAM,SAAS,SAAS,YAAY;GACpC,IAAI,MAAM,gBAAgB,MAAM,GAAG;IACjC,kBAAkB,QAAQ,QAAQ;IAClC;GACF;GACA,MAAM,qBAAqB,QAAQ,MAAM,CAAC;GAC1C,MAAM,aAAa,QAAQ,OAAO;EACpC;EACA,QAAQ,IACN,2FACF;CACF;AACF"}
|
|
@@ -11,6 +11,8 @@ import { aiFeature } from "./ai.feature.mjs";
|
|
|
11
11
|
import { authGoogleFeature } from "./auth-google.feature.mjs";
|
|
12
12
|
import { authPasskeysFeature } from "./auth-passkeys.feature.mjs";
|
|
13
13
|
import { bullBoardFeature } from "./bull-board.feature.mjs";
|
|
14
|
+
import { shadcnFeature } from "./shadcn.feature.mjs";
|
|
15
|
+
import { designSystemFeature } from "./design-system.feature.mjs";
|
|
14
16
|
import { devtoolsFeature } from "./devtools.feature.mjs";
|
|
15
17
|
import { heraldFeature } from "./herald.feature.mjs";
|
|
16
18
|
import { imageFeature } from "./image.feature.mjs";
|
|
@@ -27,7 +29,6 @@ import { redisFeature } from "./redis.feature.mjs";
|
|
|
27
29
|
import { s3Feature } from "./s3.feature.mjs";
|
|
28
30
|
import { schedulerFeature } from "./scheduler.feature.mjs";
|
|
29
31
|
import { sesFeature } from "./ses.feature.mjs";
|
|
30
|
-
import { shadcnFeature } from "./shadcn.feature.mjs";
|
|
31
32
|
import { sitemapFeature } from "./sitemap.feature.mjs";
|
|
32
33
|
import { socketFeature } from "./socket.feature.mjs";
|
|
33
34
|
import { tailwindFeature } from "./tailwind.feature.mjs";
|
|
@@ -59,6 +60,7 @@ const featuresMap = {
|
|
|
59
60
|
web: webFeature,
|
|
60
61
|
tailwind: tailwindFeature,
|
|
61
62
|
shadcn: shadcnFeature,
|
|
63
|
+
"design-system": designSystemFeature,
|
|
62
64
|
sitemap: sitemapFeature,
|
|
63
65
|
devtools: devtoolsFeature,
|
|
64
66
|
process: processFeature,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/index.ts"],"sourcesContent":["import { accessFeature } from \"./access.feature\";\nimport { aiAnthropicFeature } from \"./ai-anthropic.feature\";\nimport { aiBedrockFeature } from \"./ai-bedrock.feature\";\nimport { aiGoogleFeature } from \"./ai-google.feature\";\nimport { aiOllamaFeature } from \"./ai-ollama.feature\";\nimport { aiOpenaiFeature } from \"./ai-openai.feature\";\nimport { aiPanopticFeature } from \"./ai-panoptic.feature\";\nimport { aiToolsFeature } from \"./ai-tools.feature\";\nimport { aiWorkspaceFeature } from \"./ai-workspace.feature\";\nimport { aiFeature } from \"./ai.feature\";\nimport { authGoogleFeature } from \"./auth-google.feature\";\nimport { authPasskeysFeature } from \"./auth-passkeys.feature\";\nimport { bullBoardFeature } from \"./bull-board.feature\";\nimport { devtoolsFeature } from \"./devtools.feature\";\nimport { heraldFeature } from \"./herald.feature\";\nimport { imageFeature } from \"./image.feature\";\nimport { mailFeature } from \"./mail.feature\";\nimport { mongodbFeature } from \"./mongodb.feature\";\nimport { mysqlFeature } from \"./mysql.feature\";\nimport { notificationsFeature } from \"./notifications.feature\";\nimport { postgresFeature } from \"./postgres.feature\";\nimport { processFeature } from \"./process.feature\";\nimport { queueFeature } from \"./queue.feature\";\nimport { reactEmailFeature } from \"./react-email.feature\";\nimport { reactFeature } from \"./react.feature\";\nimport { redisFeature } from \"./redis.feature\";\nimport { s3Feature } from \"./s3.feature\";\nimport { schedulerFeature } from \"./scheduler.feature\";\nimport { sesFeature } from \"./ses.feature\";\nimport { shadcnFeature } from \"./shadcn.feature\";\nimport { sitemapFeature } from \"./sitemap.feature\";\nimport { socketFeature } from \"./socket.feature\";\nimport { tailwindFeature } from \"./tailwind.feature\";\nimport { testFeature } from \"./test.feature\";\nimport type { FeatureDefinition } from \"./types\";\nimport { webFeature } from \"./web.feature\";\n\nexport type { FeatureDefinition } from \"./types\";\n\n/**\n * The feature registry `warlock add` dispatches against.\n *\n * This file is an INDEX, nothing more: every entry lives in its own module\n * alongside the `onExecuting` body it runs. Key order is load-bearing — it is\n * the order `--list` prints and the order the \"not allowed\" error lists — so\n * add new features in the place they should appear, not alphabetically.\n */\nexport const featuresMap: Record<string, FeatureDefinition> = {\n \"react-email\": reactEmailFeature,\n react: reactFeature,\n image: imageFeature,\n mail: mailFeature,\n ses: sesFeature,\n mongodb: mongodbFeature,\n scheduler: schedulerFeature,\n // swagger / postman intentionally omitted — those packages do not exist yet;\n // they will ship together in the unified @warlock.js/api-docs package.\n postgres: postgresFeature,\n mysql: mysqlFeature,\n redis: redisFeature,\n s3: s3Feature,\n test: testFeature,\n web: webFeature,\n // Directly after `web`, and only there: it `requires` it, it is useless\n // without it, and a reader scanning `--list` for the page stack should meet\n // the two together rather than find styling filed between queues and sockets.\n tailwind: tailwindFeature,\n // Immediately after `tailwind`, for the same reason `tailwind` follows `web`:\n // it `requires` it, it appends to the stylesheet that feature creates, and the\n // three of them are one stack a reader should meet in build order.\n shadcn: shadcnFeature,\n // Directly after the web/tailwind/shadcn stack, for the same reason: it\n // `requires` web (it reads the page registry `listRoutablePages()`\n // exposes) and a reader scanning `--list` for the page stack should meet it\n // there rather than filed between queues and sockets.\n sitemap: sitemapFeature,\n devtools: devtoolsFeature,\n process: processFeature,\n herald: heraldFeature,\n queue: queueFeature,\n // Directly after `queue`, for the same reason `tailwind` follows `web`: it\n // needs queue already installed and configured, and a reader scanning\n // `--list` for job-queue features should meet the two together.\n \"bull-board\": bullBoardFeature,\n socket: socketFeature,\n notifications: notificationsFeature,\n access: accessFeature,\n // Login methods for @warlock.js/auth — \"<package>-<vendor>\" like the ai-* entries.\n \"auth-google\": authGoogleFeature,\n \"auth-passkeys\": authPasskeysFeature,\n ai: aiFeature,\n \"ai-openai\": aiOpenaiFeature,\n \"ai-google\": aiGoogleFeature,\n \"ai-anthropic\": aiAnthropicFeature,\n \"ai-bedrock\": aiBedrockFeature,\n \"ai-ollama\": aiOllamaFeature,\n \"ai-tools\": aiToolsFeature,\n \"ai-panoptic\": aiPanopticFeature,\n \"ai-workspace\": aiWorkspaceFeature,\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/index.ts"],"sourcesContent":["import { accessFeature } from \"./access.feature\";\nimport { aiAnthropicFeature } from \"./ai-anthropic.feature\";\nimport { aiBedrockFeature } from \"./ai-bedrock.feature\";\nimport { aiGoogleFeature } from \"./ai-google.feature\";\nimport { aiOllamaFeature } from \"./ai-ollama.feature\";\nimport { aiOpenaiFeature } from \"./ai-openai.feature\";\nimport { aiPanopticFeature } from \"./ai-panoptic.feature\";\nimport { aiToolsFeature } from \"./ai-tools.feature\";\nimport { aiWorkspaceFeature } from \"./ai-workspace.feature\";\nimport { aiFeature } from \"./ai.feature\";\nimport { authGoogleFeature } from \"./auth-google.feature\";\nimport { authPasskeysFeature } from \"./auth-passkeys.feature\";\nimport { bullBoardFeature } from \"./bull-board.feature\";\nimport { designSystemFeature } from \"./design-system.feature\";\nimport { devtoolsFeature } from \"./devtools.feature\";\nimport { heraldFeature } from \"./herald.feature\";\nimport { imageFeature } from \"./image.feature\";\nimport { mailFeature } from \"./mail.feature\";\nimport { mongodbFeature } from \"./mongodb.feature\";\nimport { mysqlFeature } from \"./mysql.feature\";\nimport { notificationsFeature } from \"./notifications.feature\";\nimport { postgresFeature } from \"./postgres.feature\";\nimport { processFeature } from \"./process.feature\";\nimport { queueFeature } from \"./queue.feature\";\nimport { reactEmailFeature } from \"./react-email.feature\";\nimport { reactFeature } from \"./react.feature\";\nimport { redisFeature } from \"./redis.feature\";\nimport { s3Feature } from \"./s3.feature\";\nimport { schedulerFeature } from \"./scheduler.feature\";\nimport { sesFeature } from \"./ses.feature\";\nimport { shadcnFeature } from \"./shadcn.feature\";\nimport { sitemapFeature } from \"./sitemap.feature\";\nimport { socketFeature } from \"./socket.feature\";\nimport { tailwindFeature } from \"./tailwind.feature\";\nimport { testFeature } from \"./test.feature\";\nimport type { FeatureDefinition } from \"./types\";\nimport { webFeature } from \"./web.feature\";\n\nexport type { FeatureDefinition } from \"./types\";\n\n/**\n * The feature registry `warlock add` dispatches against.\n *\n * This file is an INDEX, nothing more: every entry lives in its own module\n * alongside the `onExecuting` body it runs. Key order is load-bearing — it is\n * the order `--list` prints and the order the \"not allowed\" error lists — so\n * add new features in the place they should appear, not alphabetically.\n */\nexport const featuresMap: Record<string, FeatureDefinition> = {\n \"react-email\": reactEmailFeature,\n react: reactFeature,\n image: imageFeature,\n mail: mailFeature,\n ses: sesFeature,\n mongodb: mongodbFeature,\n scheduler: schedulerFeature,\n // swagger / postman intentionally omitted — those packages do not exist yet;\n // they will ship together in the unified @warlock.js/api-docs package.\n postgres: postgresFeature,\n mysql: mysqlFeature,\n redis: redisFeature,\n s3: s3Feature,\n test: testFeature,\n web: webFeature,\n // Directly after `web`, and only there: it `requires` it, it is useless\n // without it, and a reader scanning `--list` for the page stack should meet\n // the two together rather than find styling filed between queues and sockets.\n tailwind: tailwindFeature,\n // Immediately after `tailwind`, for the same reason `tailwind` follows `web`:\n // it `requires` it, it appends to the stylesheet that feature creates, and the\n // three of them are one stack a reader should meet in build order.\n shadcn: shadcnFeature,\n \"design-system\": designSystemFeature,\n // Directly after the web/tailwind/shadcn stack, for the same reason: it\n // `requires` web (it reads the page registry `listRoutablePages()`\n // exposes) and a reader scanning `--list` for the page stack should meet it\n // there rather than filed between queues and sockets.\n sitemap: sitemapFeature,\n devtools: devtoolsFeature,\n process: processFeature,\n herald: heraldFeature,\n queue: queueFeature,\n // Directly after `queue`, for the same reason `tailwind` follows `web`: it\n // needs queue already installed and configured, and a reader scanning\n // `--list` for job-queue features should meet the two together.\n \"bull-board\": bullBoardFeature,\n socket: socketFeature,\n notifications: notificationsFeature,\n access: accessFeature,\n // Login methods for @warlock.js/auth — \"<package>-<vendor>\" like the ai-* entries.\n \"auth-google\": authGoogleFeature,\n \"auth-passkeys\": authPasskeysFeature,\n ai: aiFeature,\n \"ai-openai\": aiOpenaiFeature,\n \"ai-google\": aiGoogleFeature,\n \"ai-anthropic\": aiAnthropicFeature,\n \"ai-bedrock\": aiBedrockFeature,\n \"ai-ollama\": aiOllamaFeature,\n \"ai-tools\": aiToolsFeature,\n \"ai-panoptic\": aiPanopticFeature,\n \"ai-workspace\": aiWorkspaceFeature,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,cAAiD;CAC5D,eAAe;CACf,OAAO;CACP,OAAO;CACP,MAAM;CACN,KAAK;CACL,SAAS;CACT,WAAW;CAGX,UAAU;CACV,OAAO;CACP,OAAO;CACP,IAAI;CACJ,MAAM;CACN,KAAK;CAIL,UAAU;CAIV,QAAQ;CACR,iBAAiB;CAKjB,SAAS;CACT,UAAU;CACV,SAAS;CACT,QAAQ;CACR,OAAO;CAIP,cAAc;CACd,QAAQ;CACR,eAAe;CACf,QAAQ;CAER,eAAe;CACf,iBAAiB;CACjB,IAAI;CACJ,aAAa;CACb,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,aAAa;CACb,YAAY;CACZ,eAAe;CACf,gBAAgB;AAClB"}
|
|
@@ -2,7 +2,7 @@ import { srcPath } from "../../utils/paths.mjs";
|
|
|
2
2
|
import "../../utils/index.mjs";
|
|
3
3
|
import { recordSkippedFile } from "../generation-run.mjs";
|
|
4
4
|
import { directoryExistsAsync, ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from "../generation-fs.mjs";
|
|
5
|
-
import { listNotificationsControllerStub, listNotificationsSchemaStub, markAllNotificationsReadControllerStub, markNotificationReadControllerStub, markNotificationReadServiceStub, notificationControllerSpecStub,
|
|
5
|
+
import { createNotificationLocalesStub, listNotificationsControllerStub, listNotificationsSchemaStub, markAllNotificationsReadControllerStub, markNotificationReadControllerStub, markNotificationReadServiceStub, notificationControllerSpecStub, notificationMigrationStub, notificationModelStub, notificationResourceStub, notificationRoutesStub, notificationTestFixtureStub, notificationsConfigStub, notificationsRequestTypeStub, unreadNotificationsCountControllerStub } from "../stubs.mjs";
|
|
6
6
|
import { migrationTimestamp } from "./shared/migration-timestamp.mjs";
|
|
7
7
|
import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
|
|
8
8
|
import { dirname } from "node:path";
|
|
@@ -63,6 +63,7 @@ async function writeModuleFile(relativePath, content, skipped) {
|
|
|
63
63
|
* that renames its group, is left for the app to update by hand.
|
|
64
64
|
*/
|
|
65
65
|
async function registerNotificationLocales(skipped) {
|
|
66
|
+
const notificationLocalesStub = createNotificationLocalesStub();
|
|
66
67
|
const localesPath = srcPath("app/notifications/utils/locales.json");
|
|
67
68
|
if (!await fileExistsAsync(localesPath)) {
|
|
68
69
|
await writeModuleFile("utils/locales.json", notificationLocalesStub, skipped);
|
|
@@ -83,8 +84,18 @@ async function registerNotificationLocales(skipped) {
|
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
const missing = Object.keys(defaults).filter((key) => dictionary[key] === void 0);
|
|
87
|
+
const existing = dictionary.notFound;
|
|
88
|
+
const translations = defaults.notFound;
|
|
89
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing) && translations && typeof translations === "object" && !Array.isArray(translations)) {
|
|
90
|
+
const current = existing;
|
|
91
|
+
for (const [code, fallback] of Object.entries(translations)) {
|
|
92
|
+
if (current[code] !== void 0) continue;
|
|
93
|
+
current[code] = fallback;
|
|
94
|
+
missing.push(`notFound.${code}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
86
97
|
if (missing.length === 0) return;
|
|
87
|
-
for (const key of
|
|
98
|
+
for (const key of Object.keys(defaults)) if (dictionary[key] === void 0) dictionary[key] = defaults[key];
|
|
88
99
|
await putFileAsync(localesPath, `${JSON.stringify(dictionary, null, 2)}\n`);
|
|
89
100
|
console.log(`${colors.green("✓")} Added ${missing.join(", ")} to src/app/notifications/utils/locales.json`);
|
|
90
101
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/notifications.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { dirname } from \"node:path\";\nimport {\n directoryExistsAsync,\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n putFileAsync,\n recordSkippedFile,\n} from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { srcPath } from \"../../utils\";\nimport {\n listNotificationsControllerStub,\n listNotificationsSchemaStub,\n markAllNotificationsReadControllerStub,\n markNotificationReadControllerStub,\n markNotificationReadServiceStub,\n notificationControllerSpecStub,\n notificationTestFixtureStub,\n notificationLocalesStub,\n notificationMigrationStub,\n notificationModelStub,\n notificationResourceStub,\n notificationRoutesStub,\n notificationsConfigStub,\n notificationsRequestTypeStub,\n unreadNotificationsCountControllerStub,\n} from \"../stubs\";\nimport { migrationTimestamp } from \"./shared/migration-timestamp\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\n/**\n * The module's controllers, with the route each one serves. A spec per\n * controller is written next to it when the app has the test feature.\n */\nconst notificationControllers = [\n {\n file: \"list-notifications.controller\",\n stub: listNotificationsControllerStub,\n method: \"GET\",\n path: \"/notifications\",\n },\n {\n file: \"unread-notifications-count.controller\",\n stub: unreadNotificationsCountControllerStub,\n method: \"GET\",\n path: \"/notifications/unread-count\",\n },\n {\n file: \"mark-notification-read.controller\",\n stub: markNotificationReadControllerStub,\n method: \"PATCH\",\n path: \"/notifications/1/read\",\n },\n {\n file: \"mark-all-notifications-read.controller\",\n stub: markAllNotificationsReadControllerStub,\n method: \"PATCH\",\n path: \"/notifications/read-all\",\n },\n] as const;\n\n/** The model's import path in the ejected config, before and since 5.30. */\nconst MODEL_IMPORT = \"app/notifications/models/notification/notification.model\";\nconst LEGACY_MODEL_IMPORT = \"app/notifications/notification.model\";\n\n/**\n * Write one module file unless it already exists. A file the app already has\n * is the app's own code: it is reported and left alone, never overwritten.\n */\nasync function writeModuleFile(relativePath: string, content: string, skipped: string[]) {\n const path = srcPath(\"app/notifications\", relativePath);\n\n if (await fileExistsAsync(path)) {\n skipped.push(relativePath);\n recordSkippedFile(path, \"exists\");\n return;\n }\n\n await ensureDirectoryAsync(dirname(path));\n await putFileAsync(path, content);\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/${relativePath}`);\n}\n\n/**\n * Add the module's `notFound` message to an existing utils/locales.json, or\n * create the file. Existing keys win; a file that is not a JSON object, or\n * that renames its group, is left for the app to update by hand.\n */\nasync function registerNotificationLocales(skipped: string[]) {\n const localesPath = srcPath(\"app/notifications/utils/locales.json\");\n\n if (!(await fileExistsAsync(localesPath))) {\n await writeModuleFile(\"utils/locales.json\", notificationLocalesStub, skipped);\n return;\n }\n\n const defaults = JSON.parse(notificationLocalesStub) as Record<string, unknown>;\n let dictionary: Record<string, unknown>;\n\n try {\n const parsed: unknown = JSON.parse(await getFileAsync(localesPath));\n\n if (parsed === null || Array.isArray(parsed) || typeof parsed !== \"object\") throw new Error();\n\n dictionary = parsed as Record<string, unknown>;\n } catch {\n console.log(\n `${colors.yellowBright(\"notifications\")} locale JSON is invalid; add notFound to src/app/notifications/utils/locales.json manually.`,\n );\n return;\n }\n\n // A root `$group` renames the file's namespace, so the service's\n // t(\"notifications.notFound\") would not find a key merged here.\n if (dictionary.$group !== undefined && dictionary.$group !== \"notifications\") {\n console.log(\n `${colors.yellowBright(\"notifications\")} locale JSON declares a different $group; add notifications.notFound manually.`,\n );\n return;\n }\n\n const missing = Object.keys(defaults).filter((key) => dictionary[key] === undefined);\n\n if (missing.length === 0) return;\n\n for (const key of missing) dictionary[key] = defaults[key];\n\n await putFileAsync(localesPath, `${JSON.stringify(dictionary, null, 2)}\\n`);\n console.log(\n `${colors.green(\"✓\")} Added ${missing.join(\", \")} to src/app/notifications/utils/locales.json`,\n );\n}\n\n/**\n * The config ejected just before this runs imports the 5.30 model path. In an\n * app scaffolded by 5.29 the model is still at the module root, so point the\n * import there instead of at a file that does not exist.\n */\nasync function pointConfigAtLegacyModel() {\n const configPath = srcPath(\"config/notifications.ts\");\n\n if (!(await fileExistsAsync(configPath))) return;\n\n const config = await getFileAsync(configPath);\n\n if (!config.includes(`\"${MODEL_IMPORT}\"`)) return;\n\n await putFileAsync(configPath, config.replace(`\"${MODEL_IMPORT}\"`, `\"${LEGACY_MODEL_IMPORT}\"`));\n console.log(\n `${colors.green(\"✓\")} src/config/notifications.ts imports the existing model at ${LEGACY_MODEL_IMPORT}`,\n );\n}\n\nasync function completeNotificationsInstallation(_options: CommandActionData) {\n const modelPath = srcPath(\"app/notifications/models/notification/notification.model.ts\");\n // Where 5.29 and earlier wrote the model.\n const legacyModelPath = srcPath(\"app/notifications/notification.model.ts\");\n\n // The model file is the sentinel for \"notifications already scaffolded\" —\n // its presence means the migration was created too (timestamped, so we must\n // not re-emit a duplicate on a second run).\n if (await fileExistsAsync(legacyModelPath)) {\n if (!(await fileExistsAsync(modelPath))) await pointConfigAtLegacyModel();\n\n console.log(\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded (5.29 layout), skipping...`,\n );\n return;\n }\n\n if (await fileExistsAsync(modelPath)) {\n console.log(`${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping...`);\n return;\n }\n\n const skipped: string[] = [];\n // An app that already has its own notifications module may keep its model\n // anywhere in it; a second Notification model would collide with it.\n const moduleExists = await directoryExistsAsync(srcPath(\"app/notifications\"));\n\n // 1. Model + migration under models/notification/, like every other module.\n // The migration's MM-DD-YYYY_HH-MM-SS prefix lets cascade infer its\n // createdAt and order it deterministically.\n if (moduleExists) {\n console.log(\n `${colors.yellowBright(\"src/app/notifications\")} already exists: not adding a Notification model or migration. config/notifications.ts must import your model.`,\n );\n } else {\n await writeModuleFile(\n \"models/notification/notification.model.ts\",\n notificationModelStub,\n skipped,\n );\n await writeModuleFile(\n `models/notification/migrations/${migrationTimestamp()}-notification.migration.ts`,\n notificationMigrationStub,\n skipped,\n );\n }\n\n // 2. One responsibility per file: output resource, validated list query,\n // guarded request type, service, translations.\n await writeModuleFile(\"resources/notification.resource.ts\", notificationResourceStub, skipped);\n await writeModuleFile(\n \"schema/list-notifications.schema.ts\",\n listNotificationsSchemaStub,\n skipped,\n );\n await writeModuleFile(\n \"types/notifications-request.type.ts\",\n notificationsRequestTypeStub,\n skipped,\n );\n await writeModuleFile(\n \"services/mark-notification-read.service.ts\",\n markNotificationReadServiceStub,\n skipped,\n );\n await registerNotificationLocales(skipped);\n\n // 3. HTTP surface — list, unread count, mark read, mark all read, one\n // controller per file, gated by authMiddleware in routes.ts.\n const withSpecs = await fileExistsAsync(srcPath(\"test-setup.ts\"));\n if (withSpecs) {\n await writeModuleFile(\"tests/notifications.fixture.ts\", notificationTestFixtureStub, skipped);\n }\n\n for (const controller of notificationControllers) {\n await writeModuleFile(`controllers/${controller.file}.ts`, controller.stub, skipped);\n\n if (withSpecs) {\n await writeModuleFile(\n `controllers/${controller.file}.spec.ts`,\n notificationControllerSpecStub(controller.method, controller.path),\n skipped,\n );\n }\n }\n\n await writeModuleFile(\"routes.ts\", notificationRoutesStub, skipped);\n\n if (skipped.length > 0) {\n console.log(\n `${colors.yellowBright(\"Kept your existing files\")} (not overwritten): ${skipped\n .map((file) => `src/app/notifications/${file}`)\n .join(\", \")}`,\n );\n }\n}\n\nexport const notificationsFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds src/app/notifications: the Notification model + migration, a resource, a validated list query, and the recipient-scoped list / unread-count / mark-read / mark-all-read endpoints (one controller per file, a spec each when tests are set up)\",\n // The ejected config wires a `mail` channel by default (needs nodemailer,\n // via the `mail` feature); the scaffolded routes are gated by\n // `authMiddleware`, so `@warlock.js/auth` is pulled in too.\n requires: [\"mail\"],\n dependencies: {\n \"@warlock.js/notifications\": INSTALLED_WARLOCK_VERSION,\n \"@warlock.js/auth\": INSTALLED_WARLOCK_VERSION,\n },\n ejectConfig: {\n content: notificationsConfigStub,\n name: \"notifications\",\n },\n onExecuting: completeNotificationsInstallation,\n};\n"],"mappings":";;;;;;;;;;;;;;;AAoCA,MAAM,0BAA0B;CAC9B;EACE,MAAM;EACN,MAAM;EACN,QAAQ;EACR,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;EACN,QAAQ;EACR,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;EACN,QAAQ;EACR,MAAM;CACR;CACA;EACE,MAAM;EACN,MAAM;EACN,QAAQ;EACR,MAAM;CACR;AACF;;AAGA,MAAM,eAAe;AACrB,MAAM,sBAAsB;;;;;AAM5B,eAAe,gBAAgB,cAAsB,SAAiB,SAAmB;CACvF,MAAM,OAAO,QAAQ,qBAAqB,YAAY;CAEtD,IAAI,MAAM,gBAAgB,IAAI,GAAG;EAC/B,QAAQ,KAAK,YAAY;EACzB,kBAAkB,MAAM,QAAQ;EAChC;CACF;CAEA,MAAM,qBAAqB,QAAQ,IAAI,CAAC;CACxC,MAAM,aAAa,MAAM,OAAO;CAChC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,iCAAiC,cAAc;AAClF;;;;;;AAOA,eAAe,4BAA4B,SAAmB;CAC5D,MAAM,cAAc,QAAQ,sCAAsC;CAElE,IAAI,CAAE,MAAM,gBAAgB,WAAW,GAAI;EACzC,MAAM,gBAAgB,sBAAsB,yBAAyB,OAAO;EAC5E;CACF;CAEA,MAAM,WAAW,KAAK,MAAM,uBAAuB;CACnD,IAAI;CAEJ,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,MAAM,aAAa,WAAW,CAAC;EAElE,IAAI,WAAW,QAAQ,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,UAAU,MAAM,IAAI,MAAM;EAE5F,aAAa;CACf,QAAQ;EACN,QAAQ,IACN,GAAG,OAAO,aAAa,eAAe,EAAE,4FAC1C;EACA;CACF;CAIA,IAAI,WAAW,WAAW,UAAa,WAAW,WAAW,iBAAiB;EAC5E,QAAQ,IACN,GAAG,OAAO,aAAa,eAAe,EAAE,+EAC1C;EACA;CACF;CAEA,MAAM,UAAU,OAAO,KAAK,QAAQ,CAAC,CAAC,QAAQ,QAAQ,WAAW,SAAS,MAAS;CAEnF,IAAI,QAAQ,WAAW,GAAG;CAE1B,KAAK,MAAM,OAAO,SAAS,WAAW,OAAO,SAAS;CAEtD,MAAM,aAAa,aAAa,GAAG,KAAK,UAAU,YAAY,MAAM,CAAC,EAAE,GAAG;CAC1E,QAAQ,IACN,GAAG,OAAO,MAAM,GAAG,EAAE,SAAS,QAAQ,KAAK,IAAI,EAAE,6CACnD;AACF;;;;;;AAOA,eAAe,2BAA2B;CACxC,MAAM,aAAa,QAAQ,yBAAyB;CAEpD,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;CAE1C,MAAM,SAAS,MAAM,aAAa,UAAU;CAE5C,IAAI,CAAC,OAAO,SAAS,IAAI,aAAa,EAAE,GAAG;CAE3C,MAAM,aAAa,YAAY,OAAO,QAAQ,IAAI,aAAa,IAAI,IAAI,oBAAoB,EAAE,CAAC;CAC9F,QAAQ,IACN,GAAG,OAAO,MAAM,GAAG,EAAE,6DAA6D,qBACpF;AACF;AAEA,eAAe,kCAAkC,UAA6B;CAC5E,MAAM,YAAY,QAAQ,6DAA6D;CAEvF,MAAM,kBAAkB,QAAQ,yCAAyC;CAKzE,IAAI,MAAM,gBAAgB,eAAe,GAAG;EAC1C,IAAI,CAAE,MAAM,gBAAgB,SAAS,GAAI,MAAM,yBAAyB;EAExE,QAAQ,IACN,GAAG,OAAO,aAAa,uBAAuB,EAAE,+CAClD;EACA;CACF;CAEA,IAAI,MAAM,gBAAgB,SAAS,GAAG;EACpC,QAAQ,IAAI,GAAG,OAAO,aAAa,uBAAuB,EAAE,iCAAiC;EAC7F;CACF;CAEA,MAAM,UAAoB,CAAC;CAQ3B,IAAI,MALuB,qBAAqB,QAAQ,mBAAmB,CAAC,GAM1E,QAAQ,IACN,GAAG,OAAO,aAAa,uBAAuB,EAAE,+GAClD;MACK;EACL,MAAM,gBACJ,6CACA,uBACA,OACF;EACA,MAAM,gBACJ,kCAAkC,mBAAmB,EAAE,6BACvD,2BACA,OACF;CACF;CAIA,MAAM,gBAAgB,sCAAsC,0BAA0B,OAAO;CAC7F,MAAM,gBACJ,uCACA,6BACA,OACF;CACA,MAAM,gBACJ,uCACA,8BACA,OACF;CACA,MAAM,gBACJ,8CACA,iCACA,OACF;CACA,MAAM,4BAA4B,OAAO;CAIzC,MAAM,YAAY,MAAM,gBAAgB,QAAQ,eAAe,CAAC;CAChE,IAAI,WACF,MAAM,gBAAgB,kCAAkC,6BAA6B,OAAO;CAG9F,KAAK,MAAM,cAAc,yBAAyB;EAChD,MAAM,gBAAgB,eAAe,WAAW,KAAK,MAAM,WAAW,MAAM,OAAO;EAEnF,IAAI,WACF,MAAM,gBACJ,eAAe,WAAW,KAAK,WAC/B,+BAA+B,WAAW,QAAQ,WAAW,IAAI,GACjE,OACF;CAEJ;CAEA,MAAM,gBAAgB,aAAa,wBAAwB,OAAO;CAElE,IAAI,QAAQ,SAAS,GACnB,QAAQ,IACN,GAAG,OAAO,aAAa,0BAA0B,EAAE,sBAAsB,QACtE,KAAK,SAAS,yBAAyB,MAAM,CAAC,CAC9C,KAAK,IAAI,GACd;AAEJ;AAEA,MAAa,uBAA0C;CACrD,aACE;CAIF,UAAU,CAAC,MAAM;CACjB,cAAc;EACZ,6BAA6B;EAC7B,oBAAoB;CACtB;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;CACA,aAAa;AACf"}
|
|
1
|
+
{"version":3,"file":"notifications.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/notifications.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { dirname } from \"node:path\";\nimport {\n directoryExistsAsync,\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n putFileAsync,\n recordSkippedFile,\n} from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { srcPath } from \"../../utils\";\nimport {\n listNotificationsControllerStub,\n listNotificationsSchemaStub,\n markAllNotificationsReadControllerStub,\n markNotificationReadControllerStub,\n markNotificationReadServiceStub,\n notificationControllerSpecStub,\n notificationTestFixtureStub,\n createNotificationLocalesStub,\n notificationMigrationStub,\n notificationModelStub,\n notificationResourceStub,\n notificationRoutesStub,\n notificationsConfigStub,\n notificationsRequestTypeStub,\n unreadNotificationsCountControllerStub,\n} from \"../stubs\";\nimport { migrationTimestamp } from \"./shared/migration-timestamp\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\n/**\n * The module's controllers, with the route each one serves. A spec per\n * controller is written next to it when the app has the test feature.\n */\nconst notificationControllers = [\n {\n file: \"list-notifications.controller\",\n stub: listNotificationsControllerStub,\n method: \"GET\",\n path: \"/notifications\",\n },\n {\n file: \"unread-notifications-count.controller\",\n stub: unreadNotificationsCountControllerStub,\n method: \"GET\",\n path: \"/notifications/unread-count\",\n },\n {\n file: \"mark-notification-read.controller\",\n stub: markNotificationReadControllerStub,\n method: \"PATCH\",\n path: \"/notifications/1/read\",\n },\n {\n file: \"mark-all-notifications-read.controller\",\n stub: markAllNotificationsReadControllerStub,\n method: \"PATCH\",\n path: \"/notifications/read-all\",\n },\n] as const;\n\n/** The model's import path in the ejected config, before and since 5.30. */\nconst MODEL_IMPORT = \"app/notifications/models/notification/notification.model\";\nconst LEGACY_MODEL_IMPORT = \"app/notifications/notification.model\";\n\n/**\n * Write one module file unless it already exists. A file the app already has\n * is the app's own code: it is reported and left alone, never overwritten.\n */\nasync function writeModuleFile(relativePath: string, content: string, skipped: string[]) {\n const path = srcPath(\"app/notifications\", relativePath);\n\n if (await fileExistsAsync(path)) {\n skipped.push(relativePath);\n recordSkippedFile(path, \"exists\");\n return;\n }\n\n await ensureDirectoryAsync(dirname(path));\n await putFileAsync(path, content);\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/${relativePath}`);\n}\n\n/**\n * Add the module's `notFound` message to an existing utils/locales.json, or\n * create the file. Existing keys win; a file that is not a JSON object, or\n * that renames its group, is left for the app to update by hand.\n */\nasync function registerNotificationLocales(skipped: string[]) {\n const notificationLocalesStub = createNotificationLocalesStub();\n const localesPath = srcPath(\"app/notifications/utils/locales.json\");\n\n if (!(await fileExistsAsync(localesPath))) {\n await writeModuleFile(\"utils/locales.json\", notificationLocalesStub, skipped);\n return;\n }\n\n const defaults = JSON.parse(notificationLocalesStub) as Record<string, unknown>;\n let dictionary: Record<string, unknown>;\n\n try {\n const parsed: unknown = JSON.parse(await getFileAsync(localesPath));\n\n if (parsed === null || Array.isArray(parsed) || typeof parsed !== \"object\") throw new Error();\n\n dictionary = parsed as Record<string, unknown>;\n } catch {\n console.log(\n `${colors.yellowBright(\"notifications\")} locale JSON is invalid; add notFound to src/app/notifications/utils/locales.json manually.`,\n );\n return;\n }\n\n // A root `$group` renames the file's namespace, so the service's\n // t(\"notifications.notFound\") would not find a key merged here.\n if (dictionary.$group !== undefined && dictionary.$group !== \"notifications\") {\n console.log(\n `${colors.yellowBright(\"notifications\")} locale JSON declares a different $group; add notifications.notFound manually.`,\n );\n return;\n }\n\n const missing = Object.keys(defaults).filter((key) => dictionary[key] === undefined);\n const existing = dictionary.notFound;\n const translations = defaults.notFound;\n if (\n existing &&\n typeof existing === \"object\" &&\n !Array.isArray(existing) &&\n translations &&\n typeof translations === \"object\" &&\n !Array.isArray(translations)\n ) {\n const current = existing as Record<string, unknown>;\n for (const [code, fallback] of Object.entries(translations)) {\n if (current[code] !== undefined) continue;\n current[code] = fallback;\n missing.push(`notFound.${code}`);\n }\n }\n\n if (missing.length === 0) return;\n\n for (const key of Object.keys(defaults)) {\n if (dictionary[key] === undefined) dictionary[key] = defaults[key];\n }\n\n await putFileAsync(localesPath, `${JSON.stringify(dictionary, null, 2)}\\n`);\n console.log(\n `${colors.green(\"✓\")} Added ${missing.join(\", \")} to src/app/notifications/utils/locales.json`,\n );\n}\n\n/**\n * The config ejected just before this runs imports the 5.30 model path. In an\n * app scaffolded by 5.29 the model is still at the module root, so point the\n * import there instead of at a file that does not exist.\n */\nasync function pointConfigAtLegacyModel() {\n const configPath = srcPath(\"config/notifications.ts\");\n\n if (!(await fileExistsAsync(configPath))) return;\n\n const config = await getFileAsync(configPath);\n\n if (!config.includes(`\"${MODEL_IMPORT}\"`)) return;\n\n await putFileAsync(configPath, config.replace(`\"${MODEL_IMPORT}\"`, `\"${LEGACY_MODEL_IMPORT}\"`));\n console.log(\n `${colors.green(\"✓\")} src/config/notifications.ts imports the existing model at ${LEGACY_MODEL_IMPORT}`,\n );\n}\n\nasync function completeNotificationsInstallation(_options: CommandActionData) {\n const modelPath = srcPath(\"app/notifications/models/notification/notification.model.ts\");\n // Where 5.29 and earlier wrote the model.\n const legacyModelPath = srcPath(\"app/notifications/notification.model.ts\");\n\n // The model file is the sentinel for \"notifications already scaffolded\" —\n // its presence means the migration was created too (timestamped, so we must\n // not re-emit a duplicate on a second run).\n if (await fileExistsAsync(legacyModelPath)) {\n if (!(await fileExistsAsync(modelPath))) await pointConfigAtLegacyModel();\n\n console.log(\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded (5.29 layout), skipping...`,\n );\n return;\n }\n\n if (await fileExistsAsync(modelPath)) {\n console.log(`${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping...`);\n return;\n }\n\n const skipped: string[] = [];\n // An app that already has its own notifications module may keep its model\n // anywhere in it; a second Notification model would collide with it.\n const moduleExists = await directoryExistsAsync(srcPath(\"app/notifications\"));\n\n // 1. Model + migration under models/notification/, like every other module.\n // The migration's MM-DD-YYYY_HH-MM-SS prefix lets cascade infer its\n // createdAt and order it deterministically.\n if (moduleExists) {\n console.log(\n `${colors.yellowBright(\"src/app/notifications\")} already exists: not adding a Notification model or migration. config/notifications.ts must import your model.`,\n );\n } else {\n await writeModuleFile(\n \"models/notification/notification.model.ts\",\n notificationModelStub,\n skipped,\n );\n await writeModuleFile(\n `models/notification/migrations/${migrationTimestamp()}-notification.migration.ts`,\n notificationMigrationStub,\n skipped,\n );\n }\n\n // 2. One responsibility per file: output resource, validated list query,\n // guarded request type, service, translations.\n await writeModuleFile(\"resources/notification.resource.ts\", notificationResourceStub, skipped);\n await writeModuleFile(\n \"schema/list-notifications.schema.ts\",\n listNotificationsSchemaStub,\n skipped,\n );\n await writeModuleFile(\n \"types/notifications-request.type.ts\",\n notificationsRequestTypeStub,\n skipped,\n );\n await writeModuleFile(\n \"services/mark-notification-read.service.ts\",\n markNotificationReadServiceStub,\n skipped,\n );\n await registerNotificationLocales(skipped);\n\n // 3. HTTP surface — list, unread count, mark read, mark all read, one\n // controller per file, gated by authMiddleware in routes.ts.\n const withSpecs = await fileExistsAsync(srcPath(\"test-setup.ts\"));\n if (withSpecs) {\n await writeModuleFile(\"tests/notifications.fixture.ts\", notificationTestFixtureStub, skipped);\n }\n\n for (const controller of notificationControllers) {\n await writeModuleFile(`controllers/${controller.file}.ts`, controller.stub, skipped);\n\n if (withSpecs) {\n await writeModuleFile(\n `controllers/${controller.file}.spec.ts`,\n notificationControllerSpecStub(controller.method, controller.path),\n skipped,\n );\n }\n }\n\n await writeModuleFile(\"routes.ts\", notificationRoutesStub, skipped);\n\n if (skipped.length > 0) {\n console.log(\n `${colors.yellowBright(\"Kept your existing files\")} (not overwritten): ${skipped\n .map((file) => `src/app/notifications/${file}`)\n .join(\", \")}`,\n );\n }\n}\n\nexport const notificationsFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds src/app/notifications: the Notification model + migration, a resource, a validated list query, and the recipient-scoped list / unread-count / mark-read / mark-all-read endpoints (one controller per file, a spec each when tests are set up)\",\n // The ejected config wires a `mail` channel by default (needs nodemailer,\n // via the `mail` feature); the scaffolded routes are gated by\n // `authMiddleware`, so `@warlock.js/auth` is pulled in too.\n requires: [\"mail\"],\n dependencies: {\n \"@warlock.js/notifications\": INSTALLED_WARLOCK_VERSION,\n \"@warlock.js/auth\": INSTALLED_WARLOCK_VERSION,\n },\n ejectConfig: {\n content: notificationsConfigStub,\n name: \"notifications\",\n },\n onExecuting: completeNotificationsInstallation,\n};\n"],"mappings":"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"}
|
|
@@ -53,31 +53,66 @@ export async function teardown() {
|
|
|
53
53
|
* setupTest() owns closing it in the same runtime context.
|
|
54
54
|
*/
|
|
55
55
|
import { setupTest, teardownTest } from "@warlock.js/core/tests";
|
|
56
|
-
import { afterAll } from "vitest";
|
|
56
|
+
import { afterAll, expect } from "vitest";
|
|
57
57
|
|
|
58
|
-
await setupTest({ importModule: (file) => import(file) });
|
|
58
|
+
await setupTest({ importModule: (file) => import(file), file: expect.getState().testPath });
|
|
59
59
|
|
|
60
60
|
afterAll(teardownTest);
|
|
61
61
|
`);
|
|
62
62
|
console.log(`${colors.green("✓")} Created src/test-setup.ts`);
|
|
63
63
|
} else recordSkippedFile(testSetupPath, "exists");
|
|
64
|
+
const runnerPath = srcPath("test-runner.ts");
|
|
65
|
+
if (!await fileExistsAsync(runnerPath)) await putFileAsync(runnerPath, `import { configureTestWorker } from "@warlock.js/core/tests";
|
|
66
|
+
import { TestRunner } from "vitest";
|
|
67
|
+
|
|
68
|
+
// Vitest awaits onCleanupWorkerContext callbacks before recycling the fork.
|
|
69
|
+
export default class WarlockTestRunner extends TestRunner {
|
|
70
|
+
public constructor(config: ConstructorParameters<typeof TestRunner>[0]) {
|
|
71
|
+
super(config);
|
|
72
|
+
configureTestWorker(
|
|
73
|
+
{ pool: this.pool, watch: config.watch, isolate: config.isolate },
|
|
74
|
+
(cleanup) => this.onCleanupWorkerContext?.(cleanup),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
`);
|
|
79
|
+
else recordSkippedFile(runnerPath, "exists");
|
|
64
80
|
const viteConfigPath = rootPath("vite.config.ts");
|
|
65
81
|
if (!await fileExistsAsync(viteConfigPath)) {
|
|
66
|
-
await putFileAsync(viteConfigPath, `import { lowerStage3Decorators } from "@warlock.js/core/vite";
|
|
82
|
+
await putFileAsync(viteConfigPath, `import { lowerStage3Decorators, recommendedTestWorkers, TestSetupReporter } from "@warlock.js/core/vite";
|
|
67
83
|
import mongezVite from "@mongez/vite";
|
|
68
84
|
import { defineConfig } from "vitest/config";
|
|
69
85
|
|
|
70
86
|
export default defineConfig({
|
|
71
|
-
// lowerStage3Decorators MUST come first: it lowers native (@RegisterModel, …)
|
|
72
|
-
// decorators with esbuild before oxc / the SSR rewrite can mangle them, so
|
|
73
|
-
// decorated Cascade models load under Vitest.
|
|
74
87
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
75
88
|
test: {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
maxWorkers: recommendedTestWorkers(),
|
|
90
|
+
reporters: ["default", new TestSetupReporter()],
|
|
91
|
+
projects: [
|
|
92
|
+
{
|
|
93
|
+
extends: true,
|
|
94
|
+
test: {
|
|
95
|
+
name: "unit",
|
|
96
|
+
environment: "node",
|
|
97
|
+
include: ["src/**/*.spec.ts"],
|
|
98
|
+
exclude: ["src/**/tests/**"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
extends: true,
|
|
103
|
+
test: {
|
|
104
|
+
name: "integration",
|
|
105
|
+
environment: "node",
|
|
106
|
+
pool: "forks",
|
|
107
|
+
// Required only when opting into tests.lifetime: "worker".
|
|
108
|
+
isolate: true,
|
|
109
|
+
runner: "./src/test-runner.ts",
|
|
110
|
+
globalSetup: "./src/test-global-setup.ts",
|
|
111
|
+
setupFiles: ["./src/test-setup.ts"],
|
|
112
|
+
include: ["src/**/*.test.ts", "src/**/tests/**/*.{test,spec}.ts", "tests/**/*.{test,spec}.ts"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
81
116
|
},
|
|
82
117
|
});
|
|
83
118
|
`);
|
|
@@ -96,8 +131,8 @@ const testFeature = {
|
|
|
96
131
|
devDependencies: {
|
|
97
132
|
"@mongez/vite": "^2.0.4",
|
|
98
133
|
vite: VITE_RANGE,
|
|
99
|
-
vitest: "^
|
|
100
|
-
"@vitest/coverage-v8": "^
|
|
134
|
+
vitest: "^5.0.3",
|
|
135
|
+
"@vitest/coverage-v8": "^5.0.3"
|
|
101
136
|
}
|
|
102
137
|
};
|
|
103
138
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/test.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, putFileAsync, recordSkippedFile } from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport type { FeatureDefinition } from \"./types\";\nimport { VITE_RANGE } from \"./web.feature\";\n\nasync function completeTestInstallation(options: CommandActionData) {\n // Create test-global-setup.ts (runs once before all tests)\n const testGlobalSetupPath = srcPath(\"test-global-setup.ts\");\n const testGlobalSetupExists = await fileExistsAsync(testGlobalSetupPath);\n\n if (!testGlobalSetupExists) {\n await putFileAsync(\n testGlobalSetupPath,\n `/**\n * Global Test Setup\n *\n * Runs ONCE before all test workers.\n * Starts the HTTP server for integration tests.\n *\n * port: 0 lets the OS pick a free port, so tests run while \\`warlock dev\\`\n * holds the configured one; the test helpers (testGet, actingAs, ...) are\n * told the bound port automatically.\n */\nimport { startHttpTestServer, stopHttpTestServer } from \"@warlock.js/core/tests\";\n\nexport async function setup() {\n await startHttpTestServer({ port: 0 });\n}\n\nexport async function teardown() {\n await stopHttpTestServer();\n}\n`,\n );\n console.log(`${colors.green(\"✓\")} Created src/test-global-setup.ts`);\n } else {\n recordSkippedFile(testGlobalSetupPath, \"exists\");\n }\n\n // Create test-setup.ts (runs before EVERY test file)\n const testSetupPath = srcPath(\"test-setup.ts\");\n const testSetupExists = await fileExistsAsync(testSetupPath);\n\n if (!testSetupExists) {\n await putFileAsync(\n testSetupPath,\n `/**\n * Test Setup - runs before EVERY test file\n *\n * Vitest runs setupFiles before each test file and rebuilds the module\n * registry with it, so this pair boots and closes the test runtime once per\n * test file.\n *\n * setupTest() gets no connectors option on purpose: an explicit connectors\n * value outranks tests.connectors from src/config/tests.ts, so passing one\n * here would erase your project config. Omitting it leaves the config in\n * charge.\n *\n * importModule makes Warlock load your app modules (config, routes, ...)\n * through THIS file's import(), which Vitest handles. Tests and the framework\n * then share one copy of every class, so \"owner instanceof User\" works.\n *\n * afterAll(teardownTest) is the other half of the pair: whoever calls\n * setupTest() owns closing it in the same runtime context.\n */\nimport { setupTest, teardownTest } from \"@warlock.js/core/tests\";\nimport { afterAll } from \"vitest\";\n\nawait setupTest({ importModule: (file) => import(file) });\n\nafterAll(teardownTest);\n`,\n );\n console.log(`${colors.green(\"✓\")} Created src/test-setup.ts`);\n } else {\n recordSkippedFile(testSetupPath, \"exists\");\n }\n\n // Create vite.config.ts\n const viteConfigPath = rootPath(\"vite.config.ts\");\n const viteConfigExists = await fileExistsAsync(viteConfigPath);\n\n if (!viteConfigExists) {\n await putFileAsync(\n viteConfigPath,\n `import { lowerStage3Decorators } from \"@warlock.js/core/vite\";\nimport mongezVite from \"@mongez/vite\";\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n
|
|
1
|
+
{"version":3,"file":"test.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/test.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, putFileAsync, recordSkippedFile } from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport type { FeatureDefinition } from \"./types\";\nimport { VITE_RANGE } from \"./web.feature\";\n\nasync function completeTestInstallation(options: CommandActionData) {\n // Create test-global-setup.ts (runs once before all tests)\n const testGlobalSetupPath = srcPath(\"test-global-setup.ts\");\n const testGlobalSetupExists = await fileExistsAsync(testGlobalSetupPath);\n\n if (!testGlobalSetupExists) {\n await putFileAsync(\n testGlobalSetupPath,\n `/**\n * Global Test Setup\n *\n * Runs ONCE before all test workers.\n * Starts the HTTP server for integration tests.\n *\n * port: 0 lets the OS pick a free port, so tests run while \\`warlock dev\\`\n * holds the configured one; the test helpers (testGet, actingAs, ...) are\n * told the bound port automatically.\n */\nimport { startHttpTestServer, stopHttpTestServer } from \"@warlock.js/core/tests\";\n\nexport async function setup() {\n await startHttpTestServer({ port: 0 });\n}\n\nexport async function teardown() {\n await stopHttpTestServer();\n}\n`,\n );\n console.log(`${colors.green(\"✓\")} Created src/test-global-setup.ts`);\n } else {\n recordSkippedFile(testGlobalSetupPath, \"exists\");\n }\n\n // Create test-setup.ts (runs before EVERY test file)\n const testSetupPath = srcPath(\"test-setup.ts\");\n const testSetupExists = await fileExistsAsync(testSetupPath);\n\n if (!testSetupExists) {\n await putFileAsync(\n testSetupPath,\n `/**\n * Test Setup - runs before EVERY test file\n *\n * Vitest runs setupFiles before each test file and rebuilds the module\n * registry with it, so this pair boots and closes the test runtime once per\n * test file.\n *\n * setupTest() gets no connectors option on purpose: an explicit connectors\n * value outranks tests.connectors from src/config/tests.ts, so passing one\n * here would erase your project config. Omitting it leaves the config in\n * charge.\n *\n * importModule makes Warlock load your app modules (config, routes, ...)\n * through THIS file's import(), which Vitest handles. Tests and the framework\n * then share one copy of every class, so \"owner instanceof User\" works.\n *\n * afterAll(teardownTest) is the other half of the pair: whoever calls\n * setupTest() owns closing it in the same runtime context.\n */\nimport { setupTest, teardownTest } from \"@warlock.js/core/tests\";\nimport { afterAll, expect } from \"vitest\";\n\nawait setupTest({ importModule: (file) => import(file), file: expect.getState().testPath });\n\nafterAll(teardownTest);\n`,\n );\n console.log(`${colors.green(\"✓\")} Created src/test-setup.ts`);\n } else {\n recordSkippedFile(testSetupPath, \"exists\");\n }\n\n const runnerPath = srcPath(\"test-runner.ts\");\n if (!(await fileExistsAsync(runnerPath))) {\n await putFileAsync(\n runnerPath,\n `import { configureTestWorker } from \"@warlock.js/core/tests\";\nimport { TestRunner } from \"vitest\";\n\n// Vitest awaits onCleanupWorkerContext callbacks before recycling the fork.\nexport default class WarlockTestRunner extends TestRunner {\n public constructor(config: ConstructorParameters<typeof TestRunner>[0]) {\n super(config);\n configureTestWorker(\n { pool: this.pool, watch: config.watch, isolate: config.isolate },\n (cleanup) => this.onCleanupWorkerContext?.(cleanup),\n );\n }\n}\n`,\n );\n } else {\n recordSkippedFile(runnerPath, \"exists\");\n }\n\n // Create vite.config.ts\n const viteConfigPath = rootPath(\"vite.config.ts\");\n const viteConfigExists = await fileExistsAsync(viteConfigPath);\n\n if (!viteConfigExists) {\n await putFileAsync(\n viteConfigPath,\n `import { lowerStage3Decorators, recommendedTestWorkers, TestSetupReporter } from \"@warlock.js/core/vite\";\nimport mongezVite from \"@mongez/vite\";\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n plugins: [lowerStage3Decorators(), mongezVite()],\n test: {\n maxWorkers: recommendedTestWorkers(),\n reporters: [\"default\", new TestSetupReporter()],\n projects: [\n {\n extends: true,\n test: {\n name: \"unit\",\n environment: \"node\",\n include: [\"src/**/*.spec.ts\"],\n exclude: [\"src/**/tests/**\"],\n },\n },\n {\n extends: true,\n test: {\n name: \"integration\",\n environment: \"node\",\n pool: \"forks\",\n // Required only when opting into tests.lifetime: \"worker\".\n isolate: true,\n runner: \"./src/test-runner.ts\",\n globalSetup: \"./src/test-global-setup.ts\",\n setupFiles: [\"./src/test-setup.ts\"],\n include: [\"src/**/*.test.ts\", \"src/**/tests/**/*.{test,spec}.ts\", \"tests/**/*.{test,spec}.ts\"],\n },\n },\n ],\n },\n});\n`,\n );\n console.log(`${colors.green(\"✓\")} Created vite.config.ts`);\n } else {\n recordSkippedFile(viteConfigPath, \"exists\");\n }\n}\n\nexport const testFeature: FeatureDefinition = {\n description: \"Installs warlock test for testing\",\n onExecuting: completeTestInstallation,\n script: {\n test: \"vitest run\",\n \"test:coverage\": \"vitest run --coverage\",\n \"test:ui\": \"vitest --ui\",\n \"test:watch\": \"vitest --watch\",\n },\n devDependencies: {\n \"@mongez/vite\": \"^2.0.4\",\n vite: VITE_RANGE,\n vitest: \"^5.0.3\",\n \"@vitest/coverage-v8\": \"^5.0.3\",\n },\n};\n"],"mappings":";;;;;;;;AAOA,eAAe,yBAAyB,SAA4B;CAElE,MAAM,sBAAsB,QAAQ,sBAAsB;CAG1D,IAAI,CAAC,MAF+B,gBAAgB,mBAAmB,GAE3C;EAC1B,MAAM,aACJ,qBACA;;;;;;;;;;;;;;;;;;;CAoBF;EACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,kCAAkC;CACrE,OACE,kBAAkB,qBAAqB,QAAQ;CAIjD,MAAM,gBAAgB,QAAQ,eAAe;CAG7C,IAAI,CAAC,MAFyB,gBAAgB,aAAa,GAErC;EACpB,MAAM,aACJ,eACA;;;;;;;;;;;;;;;;;;;;;;;;;CA0BF;EACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2BAA2B;CAC9D,OACE,kBAAkB,eAAe,QAAQ;CAG3C,MAAM,aAAa,QAAQ,gBAAgB;CAC3C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GACpC,MAAM,aACJ,YACA;;;;;;;;;;;;;CAcF;MAEA,kBAAkB,YAAY,QAAQ;CAIxC,MAAM,iBAAiB,SAAS,gBAAgB;CAGhD,IAAI,CAAC,MAF0B,gBAAgB,cAAc,GAEtC;EACrB,MAAM,aACJ,gBACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCF;EACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,wBAAwB;CAC3D,OACE,kBAAkB,gBAAgB,QAAQ;AAE9C;AAEA,MAAa,cAAiC;CAC5C,aAAa;CACb,aAAa;CACb,QAAQ;EACN,MAAM;EACN,iBAAiB;EACjB,WAAW;EACX,cAAc;CAChB;CACA,iBAAiB;EACf,gBAAgB;EAChB,MAAM;EACN,QAAQ;EACR,uBAAuB;CACzB;AACF"}
|
|
@@ -9,8 +9,8 @@ import { resolveContactScaffold } from "./shared/resolve-contact-scaffold.mjs";
|
|
|
9
9
|
import { colors } from "@mongez/copper";
|
|
10
10
|
|
|
11
11
|
//#region ../../packages/core/src/generations/features/web.feature.ts
|
|
12
|
-
/** The one vite range shared by every feature (web's peer range is
|
|
13
|
-
const VITE_RANGE = "^
|
|
12
|
+
/** The one vite range shared by every feature (web's peer range is `^8.0.0`). */
|
|
13
|
+
const VITE_RANGE = "^8.3.3";
|
|
14
14
|
/**
|
|
15
15
|
* Register the WebConnector in `warlock.config.ts`, and ONLY there.
|
|
16
16
|
*
|
|
@@ -111,7 +111,7 @@ const webFeature = {
|
|
|
111
111
|
"@types/react": "^19.2.7",
|
|
112
112
|
"@types/react-dom": "^19.2.3",
|
|
113
113
|
vite: VITE_RANGE,
|
|
114
|
-
"@vitejs/plugin-react": "^
|
|
114
|
+
"@vitejs/plugin-react": "^6.1.2"
|
|
115
115
|
},
|
|
116
116
|
onExecuting: completeWebInstallation
|
|
117
117
|
};
|