@skyf0xx/hedgehog-core-full-stack-app 1.0.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.
Files changed (137) hide show
  1. package/CLAUDE.core.md +147 -0
  2. package/LICENSE +21 -0
  3. package/README.md +41 -0
  4. package/agents/backend-eng.md +189 -0
  5. package/agents/front-end-eng.md +176 -0
  6. package/agents/ux-planner.md +174 -0
  7. package/hedgehog-core.yaml +20 -0
  8. package/package.json +41 -0
  9. package/skills/hedgehog-bootstrap/SKILL.md +324 -0
  10. package/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +411 -0
  11. package/skills/hedgehog-loop/SKILL.md +643 -0
  12. package/skills/link-workspace-packages/SKILL.md +41 -0
  13. package/skills/nx-generate/SKILL.md +165 -0
  14. package/skills/nx-run-tasks/SKILL.md +52 -0
  15. package/skills/nx-workspace/SKILL.md +261 -0
  16. package/skills/nx-workspace/references/AFFECTED.md +27 -0
  17. package/vendor-skills/GSAP/ATTRIBUTION.md +30 -0
  18. package/vendor-skills/GSAP/LICENSE +21 -0
  19. package/vendor-skills/GSAP/gsap-core/SKILL.md +254 -0
  20. package/vendor-skills/GSAP/gsap-frameworks/SKILL.md +266 -0
  21. package/vendor-skills/GSAP/gsap-performance/SKILL.md +79 -0
  22. package/vendor-skills/GSAP/gsap-plugins/SKILL.md +433 -0
  23. package/vendor-skills/GSAP/gsap-react/SKILL.md +136 -0
  24. package/vendor-skills/GSAP/gsap-scrolltrigger/SKILL.md +296 -0
  25. package/vendor-skills/GSAP/gsap-timeline/SKILL.md +107 -0
  26. package/vendor-skills/GSAP/gsap-utils/SKILL.md +284 -0
  27. package/vendor-skills/GSAP/llms.txt +39 -0
  28. package/workspace/.env.example +5 -0
  29. package/workspace/.github/workflows/phase-gate.yml +17 -0
  30. package/workspace/.prettierignore +5 -0
  31. package/workspace/.prettierrc +3 -0
  32. package/workspace/.vscode/extensions.json +3 -0
  33. package/workspace/apps/api/eslint.config.mjs +3 -0
  34. package/workspace/apps/api/package.json +117 -0
  35. package/workspace/apps/api/src/app/app.module.spec.ts +15 -0
  36. package/workspace/apps/api/src/app/app.module.ts +15 -0
  37. package/workspace/apps/api/src/app/feature-modules.ts +8 -0
  38. package/workspace/apps/api/src/app/health.controller.ts +9 -0
  39. package/workspace/apps/api/src/assets/.gitkeep +0 -0
  40. package/workspace/apps/api/src/main.ts +32 -0
  41. package/workspace/apps/api/tsconfig.app.json +22 -0
  42. package/workspace/apps/api/tsconfig.json +13 -0
  43. package/workspace/apps/api/tsconfig.spec.json +36 -0
  44. package/workspace/apps/api/vitest.config.mts +18 -0
  45. package/workspace/apps/api/webpack.config.cjs +25 -0
  46. package/workspace/apps/api-e2e/eslint.config.mjs +3 -0
  47. package/workspace/apps/api-e2e/package.json +28 -0
  48. package/workspace/apps/api-e2e/src/api/api.spec.ts +11 -0
  49. package/workspace/apps/api-e2e/src/support/global-setup.ts +17 -0
  50. package/workspace/apps/api-e2e/src/support/test-setup.ts +6 -0
  51. package/workspace/apps/api-e2e/tsconfig.json +10 -0
  52. package/workspace/apps/api-e2e/tsconfig.spec.json +28 -0
  53. package/workspace/apps/api-e2e/vitest.config.mts +20 -0
  54. package/workspace/apps/web/.env.example +12 -0
  55. package/workspace/apps/web/.prettierrc.js +10 -0
  56. package/workspace/apps/web/.swcrc +30 -0
  57. package/workspace/apps/web/components.json +21 -0
  58. package/workspace/apps/web/eslint.config.mjs +12 -0
  59. package/workspace/apps/web/index.d.ts +7 -0
  60. package/workspace/apps/web/next-env.d.ts +7 -0
  61. package/workspace/apps/web/next.config.js +9 -0
  62. package/workspace/apps/web/package.json +38 -0
  63. package/workspace/apps/web/postcss.config.mjs +7 -0
  64. package/workspace/apps/web/public/.gitkeep +0 -0
  65. package/workspace/apps/web/public/favicon.ico +0 -0
  66. package/workspace/apps/web/src/app/global.css +83 -0
  67. package/workspace/apps/web/src/app/layout.tsx +34 -0
  68. package/workspace/apps/web/src/app/module-routes.ts +13 -0
  69. package/workspace/apps/web/src/app/page.tsx +34 -0
  70. package/workspace/apps/web/src/app/providers.tsx +12 -0
  71. package/workspace/apps/web/src/components/theme-toggle.spec.tsx +20 -0
  72. package/workspace/apps/web/src/components/theme-toggle.tsx +32 -0
  73. package/workspace/apps/web/src/components/ui/button.tsx +60 -0
  74. package/workspace/apps/web/src/lib/utils.ts +6 -0
  75. package/workspace/apps/web/src/test-setup.ts +1 -0
  76. package/workspace/apps/web/tsconfig.json +62 -0
  77. package/workspace/apps/web/tsconfig.spec.json +37 -0
  78. package/workspace/apps/web/vitest.config.mts +27 -0
  79. package/workspace/apps/web-e2e/eslint.config.mjs +12 -0
  80. package/workspace/apps/web-e2e/package.json +10 -0
  81. package/workspace/apps/web-e2e/playwright.config.mts +75 -0
  82. package/workspace/apps/web-e2e/src/example.spec.ts +7 -0
  83. package/workspace/apps/web-e2e/tsconfig.json +26 -0
  84. package/workspace/commitlint.config.cjs +3 -0
  85. package/workspace/core.yaml +68 -0
  86. package/workspace/docker-compose.yml +14 -0
  87. package/workspace/eslint.config.mjs +15 -0
  88. package/workspace/gitignore.template +24 -0
  89. package/workspace/lefthook.yml +52 -0
  90. package/workspace/nx.json +90 -0
  91. package/workspace/package.json +99 -0
  92. package/workspace/packages/config/eslint-base.js +205 -0
  93. package/workspace/packages/config/package.json +28 -0
  94. package/workspace/packages/config/prettier.js +13 -0
  95. package/workspace/packages/config/src/env.schema.spec.ts +38 -0
  96. package/workspace/packages/config/src/env.schema.ts +18 -0
  97. package/workspace/packages/config/src/index.ts +1 -0
  98. package/workspace/packages/config/tsconfig.json +13 -0
  99. package/workspace/packages/config/tsconfig.lib.json +27 -0
  100. package/workspace/packages/config/tsconfig.spec.json +34 -0
  101. package/workspace/packages/config/vitest.config.mts +18 -0
  102. package/workspace/packages/db/package.json +27 -0
  103. package/workspace/packages/db/src/index.ts +2 -0
  104. package/workspace/packages/db/src/lib/db.spec.ts +48 -0
  105. package/workspace/packages/db/src/lib/db.ts +54 -0
  106. package/workspace/packages/db/src/schema/index.ts +4 -0
  107. package/workspace/packages/db/tsconfig.json +13 -0
  108. package/workspace/packages/db/tsconfig.lib.json +27 -0
  109. package/workspace/packages/db/tsconfig.spec.json +34 -0
  110. package/workspace/packages/db/vitest.config.mts +18 -0
  111. package/workspace/pnpm-lock.yaml +23719 -0
  112. package/workspace/pnpm-workspace.yaml +7 -0
  113. package/workspace/tools/generate-feature-modules.cjs +104 -0
  114. package/workspace/tools/generate-module-routes.cjs +89 -0
  115. package/workspace/tools/generators/contract/generator.ts +336 -0
  116. package/workspace/tools/generators/contract/schema.json +24 -0
  117. package/workspace/tools/generators/controller/generator.ts +390 -0
  118. package/workspace/tools/generators/controller/schema.json +24 -0
  119. package/workspace/tools/generators/fields.ts +160 -0
  120. package/workspace/tools/generators/generators.json +42 -0
  121. package/workspace/tools/generators/hook/generator.ts +291 -0
  122. package/workspace/tools/generators/hook/schema.json +19 -0
  123. package/workspace/tools/generators/lib-shell.ts +124 -0
  124. package/workspace/tools/generators/naming.ts +84 -0
  125. package/workspace/tools/generators/package.json +6 -0
  126. package/workspace/tools/generators/repository/generator.ts +298 -0
  127. package/workspace/tools/generators/repository/schema.json +15 -0
  128. package/workspace/tools/generators/schema/generator.ts +169 -0
  129. package/workspace/tools/generators/schema/schema.json +20 -0
  130. package/workspace/tools/generators/screen/generator.ts +218 -0
  131. package/workspace/tools/generators/screen/schema.json +15 -0
  132. package/workspace/tools/generators/service/generator.ts +270 -0
  133. package/workspace/tools/generators/service/schema.json +19 -0
  134. package/workspace/tools/phase-gate.cjs +77 -0
  135. package/workspace/tsconfig.base.json +21 -0
  136. package/workspace/tsconfig.json +25 -0
  137. package/workspace/vitest.workspace.ts +4 -0
@@ -0,0 +1,25 @@
1
+ const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
2
+ const { join } = require('path');
3
+
4
+ module.exports = {
5
+ output: {
6
+ path: join(__dirname, 'dist'),
7
+ clean: true,
8
+ ...(process.env.NODE_ENV !== 'production' && {
9
+ devtoolModuleFilenameTemplate: '[absolute-resource-path]',
10
+ }),
11
+ },
12
+ plugins: [
13
+ new NxAppWebpackPlugin({
14
+ target: 'node',
15
+ compiler: 'tsc',
16
+ main: './src/main.ts',
17
+ tsConfig: './tsconfig.app.json',
18
+ assets: ['./src/assets'],
19
+ optimization: false,
20
+ outputHashing: 'none',
21
+ generatePackageJson: false,
22
+ sourceMap: true,
23
+ }),
24
+ ],
25
+ };
@@ -0,0 +1,3 @@
1
+ import baseConfig from '../../eslint.config.mjs';
2
+
3
+ export default [...baseConfig];
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "api-e2e",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "nx": {
6
+ "implicitDependencies": [
7
+ "api"
8
+ ],
9
+ "targets": {
10
+ "e2e": {
11
+ "executor": "nx:run-commands",
12
+ "outputs": [
13
+ "{projectRoot}/test-output/vitest/coverage"
14
+ ],
15
+ "dependsOn": [
16
+ "api:build",
17
+ "api:serve"
18
+ ],
19
+ "options": {
20
+ "command": "vitest run --config apps/api-e2e/vitest.config.mts"
21
+ }
22
+ }
23
+ },
24
+ "tags": [
25
+ "scope:api"
26
+ ]
27
+ }
28
+ }
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import axios from 'axios';
3
+
4
+ describe('GET /api/health', () => {
5
+ it('should return ok status', async () => {
6
+ const res = await axios.get(`/api/health`);
7
+
8
+ expect(res.status).toBe(200);
9
+ expect(res.data).toEqual({ status: 'ok' });
10
+ });
11
+ });
@@ -0,0 +1,17 @@
1
+ import { killPort, waitForPortOpen } from '@nx/node/utils';
2
+
3
+ // Start services that the app needs to run (e.g. database, docker-compose,
4
+ // etc.). Vitest's `globalSetup` runs this once before the suite and, if a
5
+ // function is returned, runs it once after the suite as teardown.
6
+ export default async function setup() {
7
+ console.log('\nSetting up...\n');
8
+
9
+ const host = process.env.HOST ?? 'localhost';
10
+ const port = process.env.PORT ? Number(process.env.PORT) : 3333;
11
+ await waitForPortOpen(port, { host });
12
+
13
+ return async function teardown() {
14
+ console.log('\nTearing down...\n');
15
+ await killPort(port);
16
+ };
17
+ }
@@ -0,0 +1,6 @@
1
+ import axios from 'axios';
2
+
3
+ // Configure axios for tests to use.
4
+ const host = process.env.HOST ?? 'localhost';
5
+ const port = process.env.PORT ?? '3333';
6
+ axios.defaults.baseURL = `http://${host}:${port}`;
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "./tsconfig.spec.json"
8
+ }
9
+ ]
10
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/vitest",
5
+ "composite": true,
6
+ "declaration": true,
7
+ "esModuleInterop": true,
8
+ "noUnusedLocals": false,
9
+ "noImplicitAny": false,
10
+ "types": [
11
+ "vitest/globals",
12
+ "vitest/importMeta",
13
+ "vite/client",
14
+ "node",
15
+ "vitest"
16
+ ],
17
+ "forceConsistentCasingInFileNames": true
18
+ },
19
+ "include": [
20
+ "vite.config.ts",
21
+ "vite.config.mts",
22
+ "vitest.config.ts",
23
+ "vitest.config.mts",
24
+ "src/**/*.ts",
25
+ "src/**/*.d.ts"
26
+ ],
27
+ "references": []
28
+ }
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig(() => ({
4
+ root: __dirname,
5
+ cacheDir: '../../node_modules/.vite/apps/api-e2e',
6
+ test: {
7
+ name: 'api-e2e',
8
+ watch: false,
9
+ globals: true,
10
+ environment: 'node',
11
+ globalSetup: ['./src/support/global-setup.ts'],
12
+ setupFiles: ['./src/support/test-setup.ts'],
13
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
14
+ reporters: ['default'],
15
+ coverage: {
16
+ reportsDirectory: './test-output/vitest/coverage',
17
+ provider: 'v8' as const,
18
+ },
19
+ },
20
+ }));
@@ -0,0 +1,12 @@
1
+ # Copy to apps/web/.env.local for local dev. Next.js loads env files from
2
+ # the app directory, not the workspace root — the root .env.example is
3
+ # apps/api's, and nothing in this file reaches the server.
4
+ #
5
+ # The /api suffix is apps/api's global route prefix (app.setGlobalPrefix in
6
+ # apps/api/src/main.ts). Without it every request lands one path segment
7
+ # short and 404s against a route that plainly exists. 3333 is apps/api's
8
+ # dev port; apps/web itself runs on 3000.
9
+ #
10
+ # NEXT_PUBLIC_ is required: this value is read in the browser, so Next has
11
+ # to inline it at build time.
12
+ NEXT_PUBLIC_API_BASE_URL=http://localhost:3333/api
@@ -0,0 +1,10 @@
1
+ import base from '../../packages/config/prettier.js';
2
+
3
+ export default {
4
+ ...base,
5
+ plugins: ['prettier-plugin-tailwindcss'],
6
+ // Tailwind v4 has no tailwind.config.js to autodetect — the plugin
7
+ // requires this explicit path (resolved relative to this file) to
8
+ // locate the design system.
9
+ tailwindStylesheet: './src/app/global.css',
10
+ };
@@ -0,0 +1,30 @@
1
+ {
2
+ "jsc": {
3
+ "target": "es2017",
4
+ "parser": {
5
+ "syntax": "typescript",
6
+ "decorators": true,
7
+ "dynamicImport": true
8
+ },
9
+ "transform": {
10
+ "decoratorMetadata": true,
11
+ "legacyDecorator": true
12
+ },
13
+ "keepClassNames": true,
14
+ "externalHelpers": true,
15
+ "loose": true
16
+ },
17
+ "module": {
18
+ "type": "commonjs"
19
+ },
20
+ "sourceMaps": true,
21
+ "exclude": [
22
+ "jest.config.[ct]s",
23
+ ".*\\.spec.tsx?$",
24
+ ".*\\.test.tsx?$",
25
+ "./src/jest-setup.ts$",
26
+ "./**/jest-setup.ts$",
27
+ ".*.js$",
28
+ ".*.d.ts$"
29
+ ]
30
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/app/global.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
@@ -0,0 +1,12 @@
1
+ import nextEslintPluginNext from '@next/eslint-plugin-next';
2
+ import nx from '@nx/eslint-plugin';
3
+ import baseConfig from '../../eslint.config.mjs';
4
+
5
+ export default [
6
+ { plugins: { '@next/next': nextEslintPluginNext } },
7
+ ...nx.configs['flat/react-typescript'],
8
+ ...baseConfig,
9
+ {
10
+ ignores: ['.next/**/*', '**/out-tsc', '.prettierrc.js'],
11
+ },
12
+ ];
@@ -0,0 +1,7 @@
1
+ /// <reference types="@nx/next/typings/style.d.ts" />
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ declare module '*.svg' {
4
+ const content: any;
5
+ export const ReactComponent: any;
6
+ export default content;
7
+ }
@@ -0,0 +1,7 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/types/routes.d.ts";
4
+ import "./.next/types/root-params.d.ts";
5
+
6
+ // NOTE: This file should not be edited
7
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,9 @@
1
+ //@ts-check
2
+
3
+ /** @type {import('next').NextConfig} */
4
+ const nextConfig = {
5
+ // Next.js options go here
6
+ // See: https://nextjs.org/docs/app/api-reference/config/next-config-js
7
+ };
8
+
9
+ export default nextConfig;
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "web",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "dependencies": {
7
+ "@radix-ui/react-slot": "^1.3.1",
8
+ "@tailwindcss/postcss": "^4.3.3",
9
+ "@tanstack/react-query": "^5.101.4",
10
+ "class-variance-authority": "^0.7.1",
11
+ "clsx": "^2.1.1",
12
+ "lucide-react": "^1.25.0",
13
+ "next": "~16.3.0",
14
+ "postcss": "^8.5.23",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0",
17
+ "tailwind-merge": "^3.6.0",
18
+ "tailwindcss": "^4.3.3"
19
+ },
20
+ "devDependencies": {
21
+ "prettier-plugin-tailwindcss": "^0.7.4"
22
+ },
23
+ "nx": {
24
+ "targets": {
25
+ "generate-module-routes": {
26
+ "executor": "nx:run-commands",
27
+ "cache": true,
28
+ "inputs": ["{projectRoot}/src/app/*/page.tsx"],
29
+ "outputs": ["{projectRoot}/src/app/module-routes.ts"],
30
+ "options": {
31
+ "command": "node tools/generate-module-routes.cjs",
32
+ "cwd": "{workspaceRoot}"
33
+ }
34
+ }
35
+ },
36
+ "tags": ["scope:web"]
37
+ }
38
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ };
6
+
7
+ export default config;
File without changes
@@ -0,0 +1,83 @@
1
+ @import 'tailwindcss';
2
+
3
+ @custom-variant dark (&:is(.dark *));
4
+
5
+ :root {
6
+ --background: oklch(1 0 0);
7
+ --foreground: oklch(0.145 0 0);
8
+ --card: oklch(1 0 0);
9
+ --card-foreground: oklch(0.145 0 0);
10
+ --popover: oklch(1 0 0);
11
+ --popover-foreground: oklch(0.145 0 0);
12
+ --primary: oklch(0.205 0 0);
13
+ --primary-foreground: oklch(0.985 0 0);
14
+ --secondary: oklch(0.97 0 0);
15
+ --secondary-foreground: oklch(0.205 0 0);
16
+ --muted: oklch(0.97 0 0);
17
+ --muted-foreground: oklch(0.556 0 0);
18
+ --accent: oklch(0.97 0 0);
19
+ --accent-foreground: oklch(0.205 0 0);
20
+ --destructive: oklch(0.577 0.245 27.325);
21
+ --destructive-foreground: oklch(0.577 0.245 27.325);
22
+ --border: oklch(0.922 0 0);
23
+ --input: oklch(0.922 0 0);
24
+ --ring: oklch(0.708 0 0);
25
+ --radius: 0.625rem;
26
+ }
27
+
28
+ .dark {
29
+ --background: oklch(0.145 0 0);
30
+ --foreground: oklch(0.985 0 0);
31
+ --card: oklch(0.205 0 0);
32
+ --card-foreground: oklch(0.985 0 0);
33
+ --popover: oklch(0.205 0 0);
34
+ --popover-foreground: oklch(0.985 0 0);
35
+ --primary: oklch(0.922 0 0);
36
+ --primary-foreground: oklch(0.205 0 0);
37
+ --secondary: oklch(0.269 0 0);
38
+ --secondary-foreground: oklch(0.985 0 0);
39
+ --muted: oklch(0.269 0 0);
40
+ --muted-foreground: oklch(0.708 0 0);
41
+ --accent: oklch(0.269 0 0);
42
+ --accent-foreground: oklch(0.985 0 0);
43
+ --destructive: oklch(0.704 0.191 22.216);
44
+ --destructive-foreground: oklch(0.704 0.191 22.216);
45
+ --border: oklch(1 0 0 / 10%);
46
+ --input: oklch(1 0 0 / 15%);
47
+ --ring: oklch(0.556 0 0);
48
+ }
49
+
50
+ @theme inline {
51
+ --color-background: var(--background);
52
+ --color-foreground: var(--foreground);
53
+ --color-card: var(--card);
54
+ --color-card-foreground: var(--card-foreground);
55
+ --color-popover: var(--popover);
56
+ --color-popover-foreground: var(--popover-foreground);
57
+ --color-primary: var(--primary);
58
+ --color-primary-foreground: var(--primary-foreground);
59
+ --color-secondary: var(--secondary);
60
+ --color-secondary-foreground: var(--secondary-foreground);
61
+ --color-muted: var(--muted);
62
+ --color-muted-foreground: var(--muted-foreground);
63
+ --color-accent: var(--accent);
64
+ --color-accent-foreground: var(--accent-foreground);
65
+ --color-destructive: var(--destructive);
66
+ --color-destructive-foreground: var(--destructive-foreground);
67
+ --color-border: var(--border);
68
+ --color-input: var(--input);
69
+ --color-ring: var(--ring);
70
+ --radius-sm: calc(var(--radius) - 4px);
71
+ --radius-md: calc(var(--radius) - 2px);
72
+ --radius-lg: var(--radius);
73
+ --radius-xl: calc(var(--radius) + 4px);
74
+ }
75
+
76
+ @layer base {
77
+ * {
78
+ @apply border-border outline-ring/50;
79
+ }
80
+ body {
81
+ @apply bg-background text-foreground;
82
+ }
83
+ }
@@ -0,0 +1,34 @@
1
+ import './global.css';
2
+ import { Providers } from './providers';
3
+
4
+ export const metadata = {
5
+ title: 'Welcome to web',
6
+ description: 'Generated by create-nx-workspace',
7
+ };
8
+
9
+ const themeInitScript = `
10
+ (function () {
11
+ var stored = localStorage.getItem('theme');
12
+ var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
13
+ if (stored === 'dark' || (!stored && prefersDark)) {
14
+ document.documentElement.classList.add('dark');
15
+ }
16
+ })();
17
+ `;
18
+
19
+ export default function RootLayout({
20
+ children,
21
+ }: {
22
+ children: React.ReactNode;
23
+ }) {
24
+ return (
25
+ <html lang="en" suppressHydrationWarning>
26
+ <head>
27
+ <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
28
+ </head>
29
+ <body>
30
+ <Providers>{children}</Providers>
31
+ </body>
32
+ </html>
33
+ );
34
+ }
@@ -0,0 +1,13 @@
1
+ // GENERATED FILE — do not hand-edit. Regenerated by
2
+ // tools/generate-module-routes.cjs (the generate-module-routes Nx
3
+ // target, which build, typecheck and test depend on) from every
4
+ // apps/web/src/app/*/page.tsx file on disk. A module's screen layer
5
+ // only ever creates its own page.tsx inside its own directory —
6
+ // never edits this file or the root page.
7
+
8
+ export interface ModuleRoute {
9
+ href: string;
10
+ label: string;
11
+ }
12
+
13
+ export const moduleRoutes: ModuleRoute[] = [];
@@ -0,0 +1,34 @@
1
+ import Link from 'next/link';
2
+ import { Button } from '@/components/ui/button';
3
+ import { ThemeToggle } from '@/components/theme-toggle';
4
+ import { moduleRoutes } from './module-routes';
5
+
6
+ // The index of everything built so far. `moduleRoutes` is generated from
7
+ // the route directories on disk (tools/generate-module-routes.cjs), so a
8
+ // module's screen layer only writes its own apps/web/src/app/<module>/
9
+ // and this page picks it up — no task ever edits this file, which is what
10
+ // keeps it outside every module-scoped layer's ALLOWED SCOPE.
11
+ export default function Index() {
12
+ return (
13
+ <div className="flex min-h-screen flex-col items-center justify-center gap-6 p-8">
14
+ <div className="flex items-center gap-4">
15
+ <h1 className="text-2xl font-semibold">Hedgehog</h1>
16
+ <ThemeToggle />
17
+ </div>
18
+
19
+ {moduleRoutes.length === 0 ? (
20
+ <p className="text-muted-foreground max-w-prose text-center text-sm">
21
+ No modules yet. Each one you build gets a route here.
22
+ </p>
23
+ ) : (
24
+ <nav className="flex flex-wrap items-center justify-center gap-3">
25
+ {moduleRoutes.map((route) => (
26
+ <Button key={route.href} asChild variant="outline">
27
+ <Link href={route.href}>{route.label}</Link>
28
+ </Button>
29
+ ))}
30
+ </nav>
31
+ )}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import { useState } from 'react';
5
+
6
+ export function Providers({ children }: { children: React.ReactNode }) {
7
+ const [queryClient] = useState(() => new QueryClient());
8
+
9
+ return (
10
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
11
+ );
12
+ }
@@ -0,0 +1,20 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { ThemeToggle } from './theme-toggle';
5
+
6
+ describe('ThemeToggle', () => {
7
+ it('renders a toggle button and flips the document theme class on click', async () => {
8
+ const user = userEvent.setup();
9
+ render(<ThemeToggle />);
10
+
11
+ const toggle = screen.getByRole('button', { name: /toggle theme/i });
12
+ expect(toggle).toBeInTheDocument();
13
+
14
+ await user.click(toggle);
15
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
16
+
17
+ await user.click(toggle);
18
+ expect(document.documentElement.classList.contains('dark')).toBe(false);
19
+ });
20
+ });
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { Moon, Sun } from 'lucide-react';
4
+ import { useEffect, useState } from 'react';
5
+
6
+ import { Button } from '@/components/ui/button';
7
+
8
+ export function ThemeToggle() {
9
+ const [isDark, setIsDark] = useState(false);
10
+
11
+ useEffect(() => {
12
+ setIsDark(document.documentElement.classList.contains('dark'));
13
+ }, []);
14
+
15
+ function toggle() {
16
+ const next = !isDark;
17
+ document.documentElement.classList.toggle('dark', next);
18
+ localStorage.setItem('theme', next ? 'dark' : 'light');
19
+ setIsDark(next);
20
+ }
21
+
22
+ return (
23
+ <Button
24
+ variant="ghost"
25
+ size="icon"
26
+ onClick={toggle}
27
+ aria-label="Toggle theme"
28
+ >
29
+ {isDark ? <Sun /> : <Moon />}
30
+ </Button>
31
+ );
32
+ }
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+
3
+ import { Slot } from '@radix-ui/react-slot';
4
+ import { cva, type VariantProps } from 'class-variance-authority';
5
+ import * as React from 'react';
6
+
7
+ import { cn } from '@/lib/utils';
8
+
9
+ const buttonVariants = cva(
10
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default:
15
+ 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
16
+ destructive:
17
+ 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20',
18
+ outline:
19
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
20
+ secondary:
21
+ 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
22
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
23
+ link: 'text-primary underline-offset-4 hover:underline',
24
+ },
25
+ size: {
26
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
27
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
28
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
29
+ icon: 'size-9',
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: 'default',
34
+ size: 'default',
35
+ },
36
+ },
37
+ );
38
+
39
+ function Button({
40
+ className,
41
+ variant,
42
+ size,
43
+ asChild = false,
44
+ ...props
45
+ }: React.ComponentProps<'button'> &
46
+ VariantProps<typeof buttonVariants> & {
47
+ asChild?: boolean;
48
+ }) {
49
+ const Comp = asChild ? Slot : 'button';
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ export { Button, buttonVariants };
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom/vitest';