@oss-ma/tpl 1.0.27 → 1.0.29
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/package.json +10 -2
- package/resources/templates/react-ts/files/.env.example +9 -0
- package/resources/templates/react-ts/files/package.json +19 -17
- package/resources/templates/react-ts/files/src/app/App.tsx +7 -5
- package/resources/templates/react-ts/files/src/app/index.css +24 -0
- package/resources/templates/react-ts/files/src/app/main.tsx +25 -7
- package/resources/templates/react-ts/files/src/env.d.ts +12 -0
- package/resources/templates/react-ts/files/src/features/example/ExampleFeature.tsx +54 -0
- package/resources/templates/react-ts/files/src/features/example/store.test.ts +30 -0
- package/resources/templates/react-ts/files/src/features/example/store.ts +15 -0
- package/resources/templates/react-ts/files/src/pages/HomePage.tsx +13 -0
- package/resources/templates/react-ts/files/src/pages/NotFoundPage.tsx +13 -0
- package/resources/templates/react-ts/files/src/shared/ui/Button.test.tsx +28 -0
- package/resources/templates/react-ts/files/src/shared/ui/Button.tsx +26 -14
- package/resources/templates/react-ts/files/src/tests/setup.ts +1 -0
- package/resources/templates/react-ts/files/tsconfig.json +11 -8
- package/resources/templates/react-ts/files/vite.config.ts +17 -1
- package/resources/templates/react-ts/files/src/features/example/Example.tsx +0 -14
- package/resources/templates/react-ts/files/tsconfig.node.json +0 -10
- package/resources/templates/react-ts/files/vitest.config.js +0 -9
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-ma/tpl",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"description": "Generate, enforce and maintain clean project architectures",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/aos01/oss-ma"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/aos01/oss-ma#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/aos01/oss-ma/issues"
|
|
13
|
+
},
|
|
6
14
|
"bin": {
|
|
7
15
|
"tpl": "dist/index.js"
|
|
8
16
|
},
|
|
@@ -33,4 +41,4 @@
|
|
|
33
41
|
"tsx": "^4.16.2",
|
|
34
42
|
"typescript": "^5.5.4"
|
|
35
43
|
}
|
|
36
|
-
}
|
|
44
|
+
}
|
|
@@ -7,50 +7,52 @@
|
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "tsc -b && vite build",
|
|
9
9
|
"preview": "vite preview",
|
|
10
|
-
|
|
11
10
|
"test": "vitest run",
|
|
12
|
-
"test:watch": "vitest",
|
|
13
|
-
|
|
11
|
+
"test:watch": "vitest --ui",
|
|
12
|
+
"test:coverage": "vitest run --coverage",
|
|
14
13
|
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint . --fix",
|
|
15
15
|
"format": "prettier -w .",
|
|
16
16
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
17
|
-
|
|
18
|
-
"gen:feature": "node scripts/gen-feature.mjs",
|
|
19
|
-
|
|
20
17
|
"prepare": "husky",
|
|
21
18
|
"audit": "npm audit --audit-level=high"
|
|
22
19
|
},
|
|
23
20
|
"dependencies": {
|
|
21
|
+
"@tanstack/react-query": "^5.56.0",
|
|
24
22
|
"react": "^18.3.0",
|
|
25
|
-
"react-dom": "^18.3.0"
|
|
23
|
+
"react-dom": "^18.3.0",
|
|
24
|
+
"react-router-dom": "^6.26.0",
|
|
25
|
+
"zustand": "^4.5.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@commitlint/cli": "^19.3.0",
|
|
29
29
|
"@commitlint/config-conventional": "^19.2.0",
|
|
30
30
|
"@eslint/js": "^9.0.0",
|
|
31
|
+
"@tanstack/react-query-devtools": "^5.56.0",
|
|
32
|
+
"@testing-library/jest-dom": "^6.5.0",
|
|
33
|
+
"@testing-library/react": "^16.0.0",
|
|
34
|
+
"@testing-library/user-event": "^14.5.0",
|
|
31
35
|
"@types/react": "^18.3.0",
|
|
32
36
|
"@types/react-dom": "^18.3.0",
|
|
37
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
38
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
33
39
|
"@vitejs/plugin-react": "^4.3.0",
|
|
40
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
41
|
+
"@vitest/ui": "^2.0.0",
|
|
34
42
|
"eslint": "^9.0.0",
|
|
35
43
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
36
44
|
"eslint-plugin-react-refresh": "^0.4.0",
|
|
37
45
|
"globals": "^15.0.0",
|
|
38
46
|
"husky": "^9.0.0",
|
|
47
|
+
"jsdom": "^26.0.0",
|
|
39
48
|
"lint-staged": "^15.2.0",
|
|
40
49
|
"prettier": "^3.3.0",
|
|
41
50
|
"typescript": "^5.5.0",
|
|
42
51
|
"vite": "^5.4.0",
|
|
43
|
-
"vitest": "^2.0.0"
|
|
44
|
-
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
45
|
-
"@typescript-eslint/parser": "^8.0.0",
|
|
46
|
-
"jsdom": "^26.0.0"
|
|
52
|
+
"vitest": "^2.0.0"
|
|
47
53
|
},
|
|
48
54
|
"lint-staged": {
|
|
49
|
-
"*.{ts,tsx,js,jsx,json,md,yml,yaml}": [
|
|
50
|
-
|
|
51
|
-
],
|
|
52
|
-
"*.{ts,tsx,js,jsx}": [
|
|
53
|
-
"eslint --fix"
|
|
54
|
-
]
|
|
55
|
+
"*.{ts,tsx,js,jsx,json,md,yml,yaml}": ["prettier -w"],
|
|
56
|
+
"*.{ts,tsx,js,jsx}": ["eslint --fix"]
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Routes, Route } from "react-router-dom";
|
|
2
|
+
import { HomePage } from "@/pages/HomePage";
|
|
3
|
+
import { NotFoundPage } from "@/pages/NotFoundPage";
|
|
2
4
|
|
|
3
5
|
export function App() {
|
|
4
6
|
return (
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
</
|
|
7
|
+
<Routes>
|
|
8
|
+
<Route path="/" element={<HomePage />} />
|
|
9
|
+
<Route path="*" element={<NotFoundPage />} />
|
|
10
|
+
</Routes>
|
|
9
11
|
);
|
|
10
12
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
*,
|
|
2
|
+
*::before,
|
|
3
|
+
*::after {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:root {
|
|
10
|
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
11
|
+
line-height: 1.5;
|
|
12
|
+
font-weight: 400;
|
|
13
|
+
color-scheme: light dark;
|
|
14
|
+
color: rgba(255, 255, 255, 0.87);
|
|
15
|
+
background-color: #242424;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#root {
|
|
23
|
+
min-height: 100vh;
|
|
24
|
+
}
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { BrowserRouter } from "react-router-dom";
|
|
4
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
|
+
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
6
|
+
import { App } from "@/app/App";
|
|
7
|
+
import "@/app/index.css";
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
const queryClient = new QueryClient({
|
|
10
|
+
defaultOptions: {
|
|
11
|
+
queries: {
|
|
12
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
13
|
+
retry: 1,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
createRoot(document.getElementById("root")!).render(
|
|
19
|
+
<StrictMode>
|
|
20
|
+
<QueryClientProvider client={queryClient}>
|
|
21
|
+
<BrowserRouter>
|
|
22
|
+
<App />
|
|
23
|
+
</BrowserRouter>
|
|
24
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
25
|
+
</QueryClientProvider>
|
|
26
|
+
</StrictMode>
|
|
9
27
|
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
readonly VITE_APP_NAME: string;
|
|
5
|
+
readonly VITE_APP_VERSION: string;
|
|
6
|
+
readonly VITE_API_BASE_URL: string;
|
|
7
|
+
readonly VITE_ENABLE_DEVTOOLS: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ImportMeta {
|
|
11
|
+
readonly env: ImportMetaEnv;
|
|
12
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { useCounterStore } from "./store";
|
|
3
|
+
import { Button } from "@/shared/ui/Button";
|
|
4
|
+
|
|
5
|
+
interface Post {
|
|
6
|
+
id: number;
|
|
7
|
+
title: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function fetchPosts(): Promise<Post[]> {
|
|
11
|
+
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=3");
|
|
12
|
+
if (!res.ok) throw new Error("Failed to fetch posts");
|
|
13
|
+
return res.json();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ExampleFeature() {
|
|
17
|
+
const { count, increment, decrement, reset } = useCounterStore();
|
|
18
|
+
|
|
19
|
+
const { data: posts, isLoading, isError } = useQuery({
|
|
20
|
+
queryKey: ["posts"],
|
|
21
|
+
queryFn: fetchPosts,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<section style={{ marginTop: "2rem" }}>
|
|
26
|
+
{/* Zustand counter */}
|
|
27
|
+
<div style={{ marginBottom: "2rem" }}>
|
|
28
|
+
<h2>Counter (Zustand)</h2>
|
|
29
|
+
<p style={{ fontSize: "2rem", margin: "0.5rem 0" }}>{count}</p>
|
|
30
|
+
<div style={{ display: "flex", gap: "0.5rem" }}>
|
|
31
|
+
<Button onClick={decrement}>−</Button>
|
|
32
|
+
<Button onClick={increment}>+</Button>
|
|
33
|
+
<Button onClick={reset} variant="secondary">Reset</Button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{/* React Query fetch */}
|
|
38
|
+
<div>
|
|
39
|
+
<h2>Posts (TanStack Query)</h2>
|
|
40
|
+
{isLoading && <p>Loading...</p>}
|
|
41
|
+
{isError && <p>Error loading posts.</p>}
|
|
42
|
+
{posts && (
|
|
43
|
+
<ul style={{ marginTop: "0.5rem", paddingLeft: "1.2rem" }}>
|
|
44
|
+
{posts.map((post) => (
|
|
45
|
+
<li key={post.id} style={{ marginBottom: "0.25rem" }}>
|
|
46
|
+
{post.title}
|
|
47
|
+
</li>
|
|
48
|
+
))}
|
|
49
|
+
</ul>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { useCounterStore } from "@/features/example/store";
|
|
3
|
+
|
|
4
|
+
describe("useCounterStore", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
useCounterStore.getState().reset();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("starts at 0", () => {
|
|
10
|
+
expect(useCounterStore.getState().count).toBe(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("increments", () => {
|
|
14
|
+
useCounterStore.getState().increment();
|
|
15
|
+
expect(useCounterStore.getState().count).toBe(1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("decrements", () => {
|
|
19
|
+
useCounterStore.getState().increment();
|
|
20
|
+
useCounterStore.getState().decrement();
|
|
21
|
+
expect(useCounterStore.getState().count).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("resets to 0", () => {
|
|
25
|
+
useCounterStore.getState().increment();
|
|
26
|
+
useCounterStore.getState().increment();
|
|
27
|
+
useCounterStore.getState().reset();
|
|
28
|
+
expect(useCounterStore.getState().count).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
interface CounterState {
|
|
4
|
+
count: number;
|
|
5
|
+
increment: () => void;
|
|
6
|
+
decrement: () => void;
|
|
7
|
+
reset: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useCounterStore = create<CounterState>((set) => ({
|
|
11
|
+
count: 0,
|
|
12
|
+
increment: () => set((state) => ({ count: state.count + 1 })),
|
|
13
|
+
decrement: () => set((state) => ({ count: state.count - 1 })),
|
|
14
|
+
reset: () => set({ count: 0 }),
|
|
15
|
+
}));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ExampleFeature } from "@/features/example/ExampleFeature";
|
|
2
|
+
|
|
3
|
+
export function HomePage() {
|
|
4
|
+
return (
|
|
5
|
+
<main style={{ padding: "2rem", maxWidth: 800, margin: "0 auto" }}>
|
|
6
|
+
<h1>{{appName}}</h1>
|
|
7
|
+
<p style={{ marginTop: "0.5rem", opacity: 0.7 }}>
|
|
8
|
+
React + TypeScript + Vite
|
|
9
|
+
</p>
|
|
10
|
+
<ExampleFeature />
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Link } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
export function NotFoundPage() {
|
|
4
|
+
return (
|
|
5
|
+
<main style={{ padding: "2rem", textAlign: "center" }}>
|
|
6
|
+
<h1>404</h1>
|
|
7
|
+
<p>Page not found.</p>
|
|
8
|
+
<Link to="/" style={{ marginTop: "1rem", display: "inline-block" }}>
|
|
9
|
+
Go home
|
|
10
|
+
</Link>
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { Button } from "@/shared/ui/Button";
|
|
4
|
+
|
|
5
|
+
describe("Button", () => {
|
|
6
|
+
it("renders children", () => {
|
|
7
|
+
render(<Button>Click me</Button>);
|
|
8
|
+
expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("calls onClick when clicked", () => {
|
|
12
|
+
const handler = vi.fn();
|
|
13
|
+
render(<Button onClick={handler}>Click</Button>);
|
|
14
|
+
fireEvent.click(screen.getByRole("button"));
|
|
15
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("applies secondary variant", () => {
|
|
19
|
+
render(<Button variant="secondary">Cancel</Button>);
|
|
20
|
+
const btn = screen.getByRole("button");
|
|
21
|
+
expect(btn).toHaveStyle({ background: "#3a3a3a" });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("is disabled when disabled prop is set", () => {
|
|
25
|
+
render(<Button disabled>Disabled</Button>);
|
|
26
|
+
expect(screen.getByRole("button")).toBeDisabled();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4
|
+
variant?: "primary" | "secondary";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Button({
|
|
8
|
+
variant = "primary",
|
|
9
|
+
children,
|
|
10
|
+
style,
|
|
11
|
+
...props
|
|
12
|
+
}: ButtonProps) {
|
|
13
|
+
const base: React.CSSProperties = {
|
|
14
|
+
padding: "0.4rem 1rem",
|
|
15
|
+
borderRadius: 6,
|
|
16
|
+
border: "none",
|
|
17
|
+
cursor: "pointer",
|
|
18
|
+
fontWeight: 500,
|
|
19
|
+
fontSize: "0.9rem",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const variants: Record<string, React.CSSProperties> = {
|
|
23
|
+
primary: { background: "#646cff", color: "#fff" },
|
|
24
|
+
secondary: { background: "#3a3a3a", color: "#fff" },
|
|
25
|
+
};
|
|
5
26
|
|
|
6
|
-
export function Button({ children, onClick }: Props) {
|
|
7
27
|
return (
|
|
8
|
-
<button
|
|
9
|
-
onClick={onClick}
|
|
10
|
-
style={{
|
|
11
|
-
padding: "8px 12px",
|
|
12
|
-
borderRadius: 10,
|
|
13
|
-
border: "1px solid #ddd",
|
|
14
|
-
cursor: "pointer"
|
|
15
|
-
}}
|
|
16
|
-
>
|
|
28
|
+
<button style={{ ...base, ...variants[variant], ...style }} {...props}>
|
|
17
29
|
{children}
|
|
18
30
|
</button>
|
|
19
31
|
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
3
|
+
"target": "ES2020",
|
|
4
4
|
"useDefineForClassFields": true,
|
|
5
|
-
"lib": ["
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
6
|
"module": "ESNext",
|
|
7
7
|
"skipLibCheck": true,
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"resolveJsonModule": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
11
10
|
"isolatedModules": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
12
|
"noEmit": true,
|
|
13
13
|
"jsx": "react-jsx",
|
|
14
|
-
|
|
15
14
|
"strict": true,
|
|
16
15
|
"noUnusedLocals": true,
|
|
17
16
|
"noUnusedParameters": true,
|
|
18
|
-
"noFallthroughCasesInSwitch": true
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"baseUrl": ".",
|
|
19
|
+
"paths": {
|
|
20
|
+
"@/*": ["./src/*"]
|
|
21
|
+
}
|
|
19
22
|
},
|
|
20
|
-
"include": ["src"
|
|
23
|
+
"include": ["src"]
|
|
21
24
|
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { defineConfig } from "vite";
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
3
|
+
import { resolve } from "path";
|
|
3
4
|
|
|
4
5
|
export default defineConfig({
|
|
5
|
-
plugins: [react()]
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
"@": resolve(__dirname, "./src"),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
test: {
|
|
13
|
+
globals: true,
|
|
14
|
+
environment: "jsdom",
|
|
15
|
+
setupFiles: ["./src/tests/setup.ts"],
|
|
16
|
+
coverage: {
|
|
17
|
+
provider: "v8",
|
|
18
|
+
reporter: ["text", "html"],
|
|
19
|
+
exclude: ["node_modules/", "src/tests/"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
6
22
|
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { Button } from "../../shared/ui/Button.js";
|
|
3
|
-
|
|
4
|
-
export function Example() {
|
|
5
|
-
const [msg, setMsg] = useState<string | null>(null);
|
|
6
|
-
|
|
7
|
-
return (
|
|
8
|
-
<section style={{ marginTop: 16 }}>
|
|
9
|
-
<p>Feature module example.</p>
|
|
10
|
-
<Button onClick={() => setMsg("Hello!")}>Click</Button>
|
|
11
|
-
{msg && <p style={{ marginTop: 8 }}>{msg}</p>}
|
|
12
|
-
</section>
|
|
13
|
-
);
|
|
14
|
-
}
|