create-react-starter-kit 1.0.2 → 1.0.3

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 (89) hide show
  1. package/dist/generator/dependencies.d.ts.map +1 -1
  2. package/dist/generator/dependencies.js +20 -2
  3. package/dist/generator/dependencies.js.map +1 -1
  4. package/dist/generator/index.d.ts.map +1 -1
  5. package/dist/generator/index.js +10 -2
  6. package/dist/generator/index.js.map +1 -1
  7. package/dist/templates/README.md.ejs +23 -29
  8. package/dist/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +17 -1
  9. package/dist/templates/__when_e2eTesting_playwright__/tests/e2e/counter.spec.ts.ejs +25 -0
  10. package/dist/templates/__when_e2eTesting_playwright__/tests/e2e/feedback.spec.ts.ejs +20 -0
  11. package/dist/templates/__when_e2eTesting_playwright__/tests/e2e/home.spec.ts.ejs +30 -3
  12. package/dist/templates/__when_e2eTesting_playwright__/tests/e2e/posts.spec.ts.ejs +17 -0
  13. package/dist/templates/__when_formatting_prettier__/.prettierignore +28 -0
  14. package/dist/templates/__when_linting_biome__/biome.json +12 -0
  15. package/dist/templates/__when_unitTesting_jest__/tests/App.test.tsx.ejs +10 -1
  16. package/dist/templates/__when_unitTesting_rtl__/tests/App.test.tsx.ejs +10 -1
  17. package/dist/templates/__when_unitTesting_vitest__/tests/counter.test.tsx.ejs +36 -0
  18. package/dist/templates/__when_unitTesting_vitest__/tests/feedback.test.tsx.ejs +49 -0
  19. package/dist/templates/__when_unitTesting_vitest__/tests/pages.test.tsx.ejs +26 -0
  20. package/dist/templates/__when_unitTesting_vitest__/tests/posts.test.tsx.ejs +58 -0
  21. package/dist/templates/__when_unitTesting_vitest__/tests/test-utils.tsx.ejs +50 -0
  22. package/dist/templates/__when_visualTesting_storybook__/.storybook/main.js.ejs +4 -1
  23. package/dist/templates/__when_visualTesting_storybook__/.storybook/preview.js.ejs +40 -0
  24. package/dist/templates/__when_visualTesting_storybook__/.storybook/preview.tsx.ejs +40 -0
  25. package/dist/templates/__when_visualTesting_storybook__/src/components/StackBadge.stories.tsx.ejs +35 -0
  26. package/dist/templates/__when_visualTesting_storybook__/src/components/ui/UiButton.stories.tsx.ejs +24 -1
  27. package/dist/templates/__when_visualTesting_storybook__/src/features/counter/CounterPanel.stories.tsx.ejs +25 -0
  28. package/dist/templates/__when_visualTesting_storybook__/src/features/feedback/FeedbackForm.stories.tsx.ejs +25 -0
  29. package/dist/templates/__when_visualTesting_storybook__/src/features/posts/PostsPanel.stories.tsx.ejs +25 -0
  30. package/dist/templates/__when_visualTesting_storybook__/src/routes/Pages.stories.tsx.ejs +42 -0
  31. package/dist/templates/docs/architecture.md.ejs +32 -34
  32. package/dist/templates/docs/getting-started.md.ejs +1 -1
  33. package/{templates/src/app → dist/templates/src}/App.tsx.ejs +34 -1
  34. package/dist/templates/src/components/layout/AppLayout.tsx.ejs +35 -12
  35. package/dist/templates/src/features/counter/CounterPanel.tsx.ejs +12 -6
  36. package/dist/templates/src/features/feedback/FeedbackForm.tsx.ejs +26 -7
  37. package/dist/templates/src/main.tsx.ejs +2 -2
  38. package/{templates/src/app → dist/templates/src}/providers.tsx.ejs +1 -2
  39. package/dist/templates/src/routes/AboutPage.tsx.ejs +11 -10
  40. package/dist/templates/src/routes/CounterPage.tsx.ejs +17 -0
  41. package/dist/templates/src/routes/FeedbackPage.tsx.ejs +21 -0
  42. package/dist/templates/src/routes/HomePage.tsx.ejs +43 -21
  43. package/dist/templates/src/routes/PostsPage.tsx.ejs +17 -0
  44. package/dist/templates/src/styles/global.css.ejs +14 -33
  45. package/package.json +1 -1
  46. package/templates/README.md.ejs +23 -29
  47. package/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +17 -1
  48. package/templates/__when_e2eTesting_playwright__/tests/e2e/counter.spec.ts.ejs +25 -0
  49. package/templates/__when_e2eTesting_playwright__/tests/e2e/feedback.spec.ts.ejs +20 -0
  50. package/templates/__when_e2eTesting_playwright__/tests/e2e/home.spec.ts.ejs +30 -3
  51. package/templates/__when_e2eTesting_playwright__/tests/e2e/posts.spec.ts.ejs +17 -0
  52. package/templates/__when_formatting_prettier__/.prettierignore +28 -0
  53. package/templates/__when_linting_biome__/biome.json +12 -0
  54. package/templates/__when_unitTesting_jest__/tests/App.test.tsx.ejs +10 -1
  55. package/templates/__when_unitTesting_rtl__/tests/App.test.tsx.ejs +10 -1
  56. package/templates/__when_unitTesting_vitest__/tests/counter.test.tsx.ejs +36 -0
  57. package/templates/__when_unitTesting_vitest__/tests/feedback.test.tsx.ejs +49 -0
  58. package/templates/__when_unitTesting_vitest__/tests/pages.test.tsx.ejs +26 -0
  59. package/templates/__when_unitTesting_vitest__/tests/posts.test.tsx.ejs +58 -0
  60. package/templates/__when_unitTesting_vitest__/tests/test-utils.tsx.ejs +50 -0
  61. package/templates/__when_visualTesting_storybook__/.storybook/main.js.ejs +4 -1
  62. package/templates/__when_visualTesting_storybook__/.storybook/preview.js.ejs +40 -0
  63. package/templates/__when_visualTesting_storybook__/.storybook/preview.tsx.ejs +40 -0
  64. package/templates/__when_visualTesting_storybook__/src/components/StackBadge.stories.tsx.ejs +35 -0
  65. package/templates/__when_visualTesting_storybook__/src/components/ui/UiButton.stories.tsx.ejs +24 -1
  66. package/templates/__when_visualTesting_storybook__/src/features/counter/CounterPanel.stories.tsx.ejs +25 -0
  67. package/templates/__when_visualTesting_storybook__/src/features/feedback/FeedbackForm.stories.tsx.ejs +25 -0
  68. package/templates/__when_visualTesting_storybook__/src/features/posts/PostsPanel.stories.tsx.ejs +25 -0
  69. package/templates/__when_visualTesting_storybook__/src/routes/Pages.stories.tsx.ejs +42 -0
  70. package/templates/docs/architecture.md.ejs +32 -34
  71. package/templates/docs/getting-started.md.ejs +1 -1
  72. package/{dist/templates/src/app → templates/src}/App.tsx.ejs +34 -1
  73. package/templates/src/components/layout/AppLayout.tsx.ejs +35 -12
  74. package/templates/src/features/counter/CounterPanel.tsx.ejs +12 -6
  75. package/templates/src/features/feedback/FeedbackForm.tsx.ejs +26 -7
  76. package/templates/src/main.tsx.ejs +2 -2
  77. package/{dist/templates/src/app → templates/src}/providers.tsx.ejs +1 -2
  78. package/templates/src/routes/AboutPage.tsx.ejs +11 -10
  79. package/templates/src/routes/CounterPage.tsx.ejs +17 -0
  80. package/templates/src/routes/FeedbackPage.tsx.ejs +21 -0
  81. package/templates/src/routes/HomePage.tsx.ejs +43 -21
  82. package/templates/src/routes/PostsPage.tsx.ejs +17 -0
  83. package/templates/src/styles/global.css.ejs +14 -33
  84. package/dist/templates/__when_unitTesting_vitest__/tests/App.test.tsx.ejs +0 -16
  85. package/dist/templates/__when_visualTesting_storybook__/.storybook/preview.js +0 -8
  86. package/dist/templates/src/components/ui/Toast.tsx.ejs +0 -61
  87. package/templates/__when_unitTesting_vitest__/tests/App.test.tsx.ejs +0 -16
  88. package/templates/__when_visualTesting_storybook__/.storybook/preview.js +0 -8
  89. package/templates/src/components/ui/Toast.tsx.ejs +0 -61
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { PostsPanel } from '@/features/posts/PostsPanel';
5
+ import { renderWithProviders } from './test-utils';
6
+
7
+ vi.mock('@/services/api', async (importOriginal) => {
8
+ const actual = await importOriginal<% if (isTypeScript) { %><typeof import('@/services/api')><% } %>();
9
+ return {
10
+ ...actual,
11
+ fetchPosts: vi.fn(async () => [
12
+ { userId: 1, id: 1, title: 'Alpha post', body: 'body-1' },
13
+ { userId: 1, id: 2, title: 'Beta post', body: 'body-2' },
14
+ ]),
15
+ };
16
+ });
17
+
18
+ <% if (serverState === 'rtk-query') { -%>
19
+ // RTK Query uses its own endpoint; mock still covers the transport used by other modes.
20
+ <% } -%>
21
+
22
+ describe('PostsPanel', () => {
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ <% if (serverState === 'rtk-query') { -%>
28
+ it('renders the posts panel chrome', async () => {
29
+ renderWithProviders(<PostsPanel />);
30
+ expect(screen.getByRole('heading', { name: 'Server state' })).toBeInTheDocument();
31
+ expect(screen.getByRole('button', { name: /Refresh/i })).toBeInTheDocument();
32
+ });
33
+ <% } else { -%>
34
+ it('renders fetched posts', async () => {
35
+ renderWithProviders(<PostsPanel />);
36
+
37
+ await waitFor(() => {
38
+ expect(screen.getByText(/Alpha post/i)).toBeInTheDocument();
39
+ });
40
+ expect(screen.getByText(/Beta post/i)).toBeInTheDocument();
41
+ });
42
+
43
+ it('refreshes posts when Refresh is clicked', async () => {
44
+ const user = userEvent.setup();
45
+ const { fetchPosts } = await import('@/services/api');
46
+ renderWithProviders(<PostsPanel />);
47
+
48
+ await waitFor(() => {
49
+ expect(screen.getByText(/Alpha post/i)).toBeInTheDocument();
50
+ });
51
+
52
+ await user.click(screen.getByRole('button', { name: /Refresh/i }));
53
+ await waitFor(() => {
54
+ expect(fetchPosts).toHaveBeenCalled();
55
+ });
56
+ });
57
+ <% } -%>
58
+ });
@@ -0,0 +1,50 @@
1
+ <% if (isTypeScript) { -%>
2
+ import type { ReactElement, ReactNode } from 'react';
3
+ <% } -%>
4
+ import { render } from '@testing-library/react';
5
+ <% if (router === 'react-router') { -%>
6
+ import { MemoryRouter } from 'react-router-dom';
7
+ <% } -%>
8
+ import { AppProviders } from '@/providers';
9
+
10
+ <% if (isTypeScript) { -%>
11
+ interface Options {
12
+ route?: string;
13
+ }
14
+
15
+ function Wrapper({ children, route = '/' }: { children: ReactNode; route?: string }) {
16
+ <% if (router === 'react-router') { -%>
17
+ return (
18
+ <AppProviders>
19
+ <MemoryRouter initialEntries={[route]}>{children}</MemoryRouter>
20
+ </AppProviders>
21
+ );
22
+ <% } else { -%>
23
+ return <AppProviders>{children}</AppProviders>;
24
+ <% } -%>
25
+ }
26
+
27
+ export function renderWithProviders(ui: ReactElement, options: Options = {}) {
28
+ return render(ui, {
29
+ wrapper: ({ children }) => <Wrapper route={options.route}>{children}</Wrapper>,
30
+ });
31
+ }
32
+ <% } else { -%>
33
+ function Wrapper({ children, route = '/' }) {
34
+ <% if (router === 'react-router') { -%>
35
+ return (
36
+ <AppProviders>
37
+ <MemoryRouter initialEntries={[route]}>{children}</MemoryRouter>
38
+ </AppProviders>
39
+ );
40
+ <% } else { -%>
41
+ return <AppProviders>{children}</AppProviders>;
42
+ <% } -%>
43
+ }
44
+
45
+ export function renderWithProviders(ui, options = {}) {
46
+ return render(ui, {
47
+ wrapper: ({ children }) => <Wrapper route={options.route}>{children}</Wrapper>,
48
+ });
49
+ }
50
+ <% } -%>
@@ -1,11 +1,14 @@
1
1
  /** @type { import('@storybook/<%= buildTool === 'vite' || buildTool === 'esbuild' ? 'react-vite' : 'react-webpack5' %>').StorybookConfig } */
2
2
  const config = {
3
3
  stories: ['../src/**/*.stories.@(<%= isTypeScript ? 'ts|tsx' : 'js|jsx' %>)'],
4
- addons: [],
4
+ addons: ['@storybook/addon-essentials'],
5
5
  framework: {
6
6
  name: '@storybook/<%= buildTool === 'vite' || buildTool === 'esbuild' ? 'react-vite' : 'react-webpack5' %>',
7
7
  options: {},
8
8
  },
9
+ docs: {
10
+ autodocs: 'tag',
11
+ },
9
12
  };
10
13
 
11
14
  export default config;
@@ -0,0 +1,40 @@
1
+ import { AppProviders } from '../src/providers';
2
+ <% if (router === 'react-router') { -%>
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ <% } -%>
5
+ import '../src/styles/global.<%= styling === 'sass' && uiLibrary !== 'shadcn' ? 'scss' : 'css' %>';
6
+
7
+ /** @type { import('@storybook/react').Preview } */
8
+ const preview = {
9
+ parameters: {
10
+ layout: 'padded',
11
+ controls: {
12
+ matchers: {
13
+ color: /(background|color)$/i,
14
+ date: /Date$/i,
15
+ },
16
+ },
17
+ docs: {
18
+ toc: true,
19
+ },
20
+ },
21
+ decorators: [
22
+ (Story) => (
23
+ <AppProviders>
24
+ <% if (router === 'react-router') { -%>
25
+ <MemoryRouter>
26
+ <div style={{ maxWidth: 960, margin: '0 auto' }}>
27
+ <Story />
28
+ </div>
29
+ </MemoryRouter>
30
+ <% } else { -%>
31
+ <div style={{ maxWidth: 960, margin: '0 auto' }}>
32
+ <Story />
33
+ </div>
34
+ <% } -%>
35
+ </AppProviders>
36
+ ),
37
+ ],
38
+ };
39
+
40
+ export default preview;
@@ -0,0 +1,40 @@
1
+ import { AppProviders } from '../src/providers';
2
+ <% if (router === 'react-router') { -%>
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ <% } -%>
5
+ import '../src/styles/global.<%= styling === 'sass' && uiLibrary !== 'shadcn' ? 'scss' : 'css' %>';
6
+
7
+ /** @type { import('@storybook/react').Preview } */
8
+ const preview = {
9
+ parameters: {
10
+ layout: 'padded',
11
+ controls: {
12
+ matchers: {
13
+ color: /(background|color)$/i,
14
+ date: /Date$/i,
15
+ },
16
+ },
17
+ docs: {
18
+ toc: true,
19
+ },
20
+ },
21
+ decorators: [
22
+ (Story) => (
23
+ <AppProviders>
24
+ <% if (router === 'react-router') { -%>
25
+ <MemoryRouter>
26
+ <div style={{ maxWidth: 960, margin: '0 auto' }}>
27
+ <Story />
28
+ </div>
29
+ </MemoryRouter>
30
+ <% } else { -%>
31
+ <div style={{ maxWidth: 960, margin: '0 auto' }}>
32
+ <Story />
33
+ </div>
34
+ <% } -%>
35
+ </AppProviders>
36
+ ),
37
+ ],
38
+ };
39
+
40
+ export default preview;
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { StackBadge } from '@/components/StackBadge';
3
+
4
+ const meta = {
5
+ title: 'Components/StackBadge',
6
+ component: StackBadge,
7
+ tags: ['autodocs'],
8
+ args: {
9
+ label: 'Build',
10
+ value: '<%= stackLabels.buildTool %>',
11
+ },
12
+ } satisfies Meta<typeof StackBadge>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {};
18
+
19
+ export const Language: Story = {
20
+ args: {
21
+ label: 'Language',
22
+ value: '<%= stackLabels.language %>',
23
+ },
24
+ };
25
+
26
+ export const Row: Story = {
27
+ render: () => (
28
+ <div className="stack-row">
29
+ <StackBadge label="Build" value="<%= stackLabels.buildTool %>" />
30
+ <StackBadge label="Router" value="<%= stackLabels.router %>" />
31
+ <StackBadge label="State" value="<%= stackLabels.stateManagement %>" />
32
+ <StackBadge label="Data" value="<%= stackLabels.serverState %>" />
33
+ </div>
34
+ ),
35
+ };
@@ -1,15 +1,38 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { UiButton } from '@/components/ui/UiButton';
3
3
 
4
+ /**
5
+ * Shared button adapter used across demos.
6
+ * Prefer this over raw HTML buttons so UI library swaps stay localized.
7
+ */
4
8
  const meta = {
5
- title: 'UI/UiButton',
9
+ title: 'Components/UiButton',
6
10
  component: UiButton,
11
+ tags: ['autodocs'],
7
12
  args: {
8
13
  children: 'Click me',
9
14
  },
15
+ argTypes: {
16
+ onClick: { action: 'clicked' },
17
+ children: { control: 'text' },
18
+ },
10
19
  } satisfies Meta<typeof UiButton>;
11
20
 
12
21
  export default meta;
13
22
  type Story = StoryObj<typeof meta>;
14
23
 
15
24
  export const Primary: Story = {};
25
+
26
+ export const SubmitLabel: Story = {
27
+ args: {
28
+ children: 'Send feedback',
29
+ type: 'submit',
30
+ },
31
+ };
32
+
33
+ export const Disabled: Story = {
34
+ args: {
35
+ children: 'Disabled',
36
+ disabled: true,
37
+ },
38
+ };
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { CounterPanel } from '@/features/counter/CounterPanel';
3
+
4
+ /**
5
+ * Client-state demo used on `/counter`.
6
+ * Interact with +/- to verify store wiring in isolation.
7
+ */
8
+ const meta = {
9
+ title: 'Features/CounterPanel',
10
+ component: CounterPanel,
11
+ tags: ['autodocs'],
12
+ parameters: {
13
+ docs: {
14
+ description: {
15
+ component:
16
+ 'Demonstrates <%= stackLabels.stateManagement %> for ephemeral UI state. Prefer this over putting server entities into client stores.',
17
+ },
18
+ },
19
+ },
20
+ } satisfies Meta<typeof CounterPanel>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {};
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { FeedbackForm } from '@/features/feedback/FeedbackForm';
3
+
4
+ /**
5
+ * Forms demo used on `/feedback`.
6
+ * Try empty submit for validation, then a valid payload for success status.
7
+ */
8
+ const meta = {
9
+ title: 'Features/FeedbackForm',
10
+ component: FeedbackForm,
11
+ tags: ['autodocs'],
12
+ parameters: {
13
+ docs: {
14
+ description: {
15
+ component:
16
+ 'Built with <%= stackLabels.forms %><% if (validation !== 'none') { %> and <%= stackLabels.validation %><% } %>. Submits to the demo REST API.',
17
+ },
18
+ },
19
+ },
20
+ } satisfies Meta<typeof FeedbackForm>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Empty: Story = {};
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { PostsPanel } from '@/features/posts/PostsPanel';
3
+
4
+ /**
5
+ * Server-state demo used on `/posts`.
6
+ * Needs network access to JSONPlaceholder (or your `VITE_API_BASE_URL`).
7
+ */
8
+ const meta = {
9
+ title: 'Features/PostsPanel',
10
+ component: PostsPanel,
11
+ tags: ['autodocs'],
12
+ parameters: {
13
+ docs: {
14
+ description: {
15
+ component:
16
+ 'Fetches posts with <%= stackLabels.serverState === 'None' ? stackLabels.apiLayer : stackLabels.serverState %>. Use Refresh to exercise refetch behavior.',
17
+ },
18
+ },
19
+ },
20
+ } satisfies Meta<typeof PostsPanel>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {};
@@ -0,0 +1,42 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { HomePage } from '@/routes/HomePage';
3
+ import { AboutPage } from '@/routes/AboutPage';
4
+ import { CounterPage } from '@/routes/CounterPage';
5
+ import { PostsPage } from '@/routes/PostsPage';
6
+ import { FeedbackPage } from '@/routes/FeedbackPage';
7
+
8
+ const meta = {
9
+ title: 'Pages/Routes',
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ docs: {
13
+ description: {
14
+ component:
15
+ 'Route-level pages composed from feature panels. Useful for visual review of layout and copy.',
16
+ },
17
+ },
18
+ },
19
+ } satisfies Meta;
20
+
21
+ export default meta;
22
+ type Story = StoryObj;
23
+
24
+ export const Home: Story = {
25
+ render: () => <HomePage />,
26
+ };
27
+
28
+ export const About: Story = {
29
+ render: () => <AboutPage />,
30
+ };
31
+
32
+ export const Counter: Story = {
33
+ render: () => <CounterPage />,
34
+ };
35
+
36
+ export const Posts: Story = {
37
+ render: () => <PostsPage />,
38
+ };
39
+
40
+ export const Feedback: Story = {
41
+ render: () => <FeedbackPage />,
42
+ };
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Why this stack
4
4
 
5
- You selected a coherent default-friendly stack during scaffolding. The generator wires providers, scripts, and a demo page so packages are not orphaned in `package.json`.
5
+ You selected a coherent default-friendly stack during scaffolding. The generator wires providers, scripts, and demo routes so packages are not orphaned in `package.json`.
6
6
 
7
7
  | Concern | Choice | Role |
8
8
  |---------|--------|------|
@@ -19,56 +19,54 @@ You selected a coherent default-friendly stack during scaffolding. The generator
19
19
 
20
20
  ## Folder organization (feature-based)
21
21
 
22
- This scaffold uses a **feature-based / domain-driven** layout — the pattern most large React teams settle on as apps grow (similar in spirit to how companies structure product domains: keep code that changes together close together).
23
-
24
22
  ```text
25
23
  src/
26
- app/ # bootstrap, providers, root App shell
27
- assets/ # images, fonts, static media imported from code
28
- components/ # shared, feature-agnostic UI (layout/, ui/)
29
- config/ # env helpers and app-level constants
30
- features/ # domain modules (counter, posts, feedback, …)
31
- hooks/ # shared hooks
32
- lib/ # third-party wrappers / cross-cutting helpers
33
- routes/ # route-level pages
34
- services/ # API clients / transport
35
- store/ # global client state (when selected)
36
- styles/ # global styles / tokens
37
- types/ # shared TypeScript types
38
- utils/ # pure helpers
24
+ main.<%= jsxExt %> # bootstrap + render
25
+ providers.<%= jsxExt %>
26
+ App.<%= jsxExt %> # route tree
27
+ assets/
28
+ components/ # shared UI (layout/, ui/)
29
+ config/
30
+ features/ # domain modules
31
+ hooks/
32
+ lib/
33
+ routes/ # / /counter /posts /feedback /about
34
+ services/
35
+ store/
36
+ styles/
37
+ types/
38
+ utils/
39
39
  ```
40
40
 
41
41
  **Rules of thumb**
42
42
 
43
43
  - Put new product work in `src/features/<domain>/`.
44
44
  - Keep `components/` dumb and reusable.
45
- - Import with the `@/` alias (e.g. `import { apiGet } from '@/services/api'`).
45
+ - Import with the `@/` alias.
46
46
  - Prefer a feature's public surface over deep cross-feature imports.
47
47
 
48
- ## Path aliases
48
+ ## Routes
49
49
 
50
- `@/*` maps to `src/*` in TypeScript/JS config and in your bundler (`<%= stackLabels.buildTool %>`).
50
+ | Path | Purpose |
51
+ |------|---------|
52
+ | `/` | Home overview |
53
+ | `/counter` | Client state demo |
54
+ | `/posts` | Server state / data fetching |
55
+ | `/feedback` | Forms + validation |
56
+ | `/about` | Orientation |
51
57
 
52
- ```ts
53
- import { HomePage } from '@/routes/HomePage';
54
- import { useToast } from '@/components/ui/Toast';
55
- ```
58
+ ## Path aliases
59
+
60
+ `@/*` maps to `src/*` in TypeScript/JS config and in your bundler.
56
61
 
57
62
  ## Runtime composition
58
63
 
59
- `src/main.<%= jsxExt %>` → providers (`src/app/providers.<%= jsxExt %>`) router (`src/app/App.<%= jsxExt %>`) → layouts/pages.
64
+ `src/main.<%= jsxExt %>` → `AppProviders` → `App` (router) → layout → pages.
60
65
 
61
- Providers are ordered deliberately: state → server cache → UI theme → toasts.
66
+ Providers are ordered: client state → server cache → UI theme.
62
67
 
63
68
  ## Boundaries
64
69
 
65
- - **UI** must not call `fetch`/`axios` directly in deep leaves — go through `services/`.
66
- - **Schemas** live in `lib/validation` so forms and API payloads share rules.
70
+ - **UI** should call HTTP through `services/`.
71
+ - **Schemas** live in `lib/validation`.
67
72
  - **Env access** stays in `config/`, `services/`, and `lib/monitoring`.
68
-
69
- ## Production readiness patterns
70
-
71
- - Code-split heavy routes when the app grows.
72
- - Fail closed on missing monitoring DSN (already gated).
73
- - Keep lint/format/test scripts green in CI.
74
- - Document public env vars only in `.env.example`.
@@ -38,7 +38,7 @@ Only variables prefixed for your bundler (e.g. `VITE_`) are exposed to the brows
38
38
  ## Extending the app
39
39
 
40
40
  - Add a domain under `src/features/<name>/`.
41
- - Register routes in `src/app/App.<%= jsxExt %>`.
41
+ - Register routes in `src/App.<%= jsxExt %>`.
42
42
  - Put shared UI in `src/components/`.
43
43
  - Put HTTP clients in `src/services/`.
44
44
  - Import with `@/` (maps to `src/`), e.g. `import { env } from '@/config/env'`.
@@ -3,6 +3,9 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
3
3
  import { AppLayout } from '@/components/layout/AppLayout';
4
4
  import { HomePage } from '@/routes/HomePage';
5
5
  import { AboutPage } from '@/routes/AboutPage';
6
+ import { CounterPage } from '@/routes/CounterPage';
7
+ import { PostsPage } from '@/routes/PostsPage';
8
+ import { FeedbackPage } from '@/routes/FeedbackPage';
6
9
 
7
10
  export function App() {
8
11
  return (
@@ -11,6 +14,9 @@ export function App() {
11
14
  <Route element={<AppLayout />}>
12
15
  <Route path="/" element={<HomePage />} />
13
16
  <Route path="/about" element={<AboutPage />} />
17
+ <Route path="/counter" element={<CounterPage />} />
18
+ <Route path="/posts" element={<PostsPage />} />
19
+ <Route path="/feedback" element={<FeedbackPage />} />
14
20
  <Route path="*" element={<Navigate to="/" replace />} />
15
21
  </Route>
16
22
  </Routes>
@@ -28,6 +34,9 @@ import {
28
34
  import { AppLayout } from '@/components/layout/AppLayout';
29
35
  import { HomePage } from '@/routes/HomePage';
30
36
  import { AboutPage } from '@/routes/AboutPage';
37
+ import { CounterPage } from '@/routes/CounterPage';
38
+ import { PostsPage } from '@/routes/PostsPage';
39
+ import { FeedbackPage } from '@/routes/FeedbackPage';
31
40
 
32
41
  function RootComponent() {
33
42
  return (
@@ -53,7 +62,31 @@ const aboutRoute = createRoute({
53
62
  component: AboutPage,
54
63
  });
55
64
 
56
- const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]);
65
+ const counterRoute = createRoute({
66
+ getParentRoute: () => rootRoute,
67
+ path: '/counter',
68
+ component: CounterPage,
69
+ });
70
+
71
+ const postsRoute = createRoute({
72
+ getParentRoute: () => rootRoute,
73
+ path: '/posts',
74
+ component: PostsPage,
75
+ });
76
+
77
+ const feedbackRoute = createRoute({
78
+ getParentRoute: () => rootRoute,
79
+ path: '/feedback',
80
+ component: FeedbackPage,
81
+ });
82
+
83
+ const routeTree = rootRoute.addChildren([
84
+ indexRoute,
85
+ aboutRoute,
86
+ counterRoute,
87
+ postsRoute,
88
+ feedbackRoute,
89
+ ]);
57
90
 
58
91
  const router = createRouter({ routeTree });
59
92
 
@@ -13,6 +13,14 @@ interface AppLayoutProps {
13
13
  }
14
14
  <% } -%>
15
15
 
16
+ const links = [
17
+ { to: '/', label: 'Home', end: true },
18
+ { to: '/counter', label: 'Counter' },
19
+ { to: '/posts', label: 'Posts' },
20
+ { to: '/feedback', label: 'Feedback' },
21
+ { to: '/about', label: 'About' },
22
+ ] as const;
23
+
16
24
  export function AppLayout(<% if (router === 'tanstack-router') { %>{ children }<% if (isTypeScript) { %>: AppLayoutProps<% } %><% } %>) {
17
25
  <% if (router === 'tanstack-router') { -%>
18
26
  const pathname = useRouterState({ select: (s) => s.location.pathname });
@@ -30,19 +38,34 @@ export function AppLayout(<% if (router === 'tanstack-router') { %>{ children }<
30
38
  </div>
31
39
  <nav className="app-nav" aria-label="Primary">
32
40
  <% if (router === 'react-router') { -%>
33
- <NavLink to="/" end className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}>
34
- Home
35
- </NavLink>
36
- <NavLink to="/about" className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}>
37
- About
38
- </NavLink>
41
+ {links.map((link) => (
42
+ <NavLink
43
+ key={link.to}
44
+ to={link.to}
45
+ end={'end' in link ? link.end : false}
46
+ className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
47
+ >
48
+ {link.label}
49
+ </NavLink>
50
+ ))}
39
51
  <% } else { -%>
40
- <Link to="/" className={pathname === '/' ? 'nav-link active' : 'nav-link'}>
41
- Home
42
- </Link>
43
- <Link to="/about" className={pathname === '/about' ? 'nav-link active' : 'nav-link'}>
44
- About
45
- </Link>
52
+ {links.map((link) => (
53
+ <Link
54
+ key={link.to}
55
+ to={link.to}
56
+ className={
57
+ link.to === '/'
58
+ ? pathname === '/'
59
+ ? 'nav-link active'
60
+ : 'nav-link'
61
+ : pathname.startsWith(link.to)
62
+ ? 'nav-link active'
63
+ : 'nav-link'
64
+ }
65
+ >
66
+ {link.label}
67
+ </Link>
68
+ ))}
46
69
  <% } -%>
47
70
  </nav>
48
71
  </header>