march-hare 0.12.1 → 0.13.1
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/README.md +66 -25
- package/dist/action/index.d.ts +2 -2
- package/dist/action/utils.d.ts +2 -2
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/types.d.ts +3 -3
- package/dist/actions/utils.d.ts +3 -3
- package/dist/app/index.d.ts +33 -87
- package/dist/app/types.d.ts +79 -26
- package/dist/boundary/components/broadcast/index.d.ts +2 -2
- package/dist/boundary/components/broadcast/types.d.ts +1 -1
- package/dist/boundary/components/consumer/components/partition/index.d.ts +1 -1
- package/dist/boundary/components/consumer/components/partition/types.d.ts +1 -1
- package/dist/boundary/components/consumer/index.d.ts +5 -5
- package/dist/boundary/components/consumer/types.d.ts +1 -1
- package/dist/boundary/components/consumer/utils.d.ts +1 -1
- package/dist/boundary/components/env/index.d.ts +3 -16
- package/dist/boundary/components/env/types.d.ts +24 -2
- package/dist/boundary/components/env/utils.d.ts +1 -1
- package/dist/boundary/components/scope/index.d.ts +2 -2
- package/dist/boundary/components/scope/types.d.ts +1 -1
- package/dist/boundary/components/scope/utils.d.ts +1 -1
- package/dist/boundary/components/sharing/index.d.ts +3 -3
- package/dist/boundary/components/tap/index.d.ts +3 -3
- package/dist/boundary/components/tap/types.d.ts +2 -2
- package/dist/boundary/components/tap/utils.d.ts +1 -1
- package/dist/boundary/components/tasks/index.d.ts +2 -2
- package/dist/boundary/components/tasks/utils.d.ts +1 -1
- package/dist/boundary/index.d.ts +3 -3
- package/dist/boundary/types.d.ts +3 -3
- package/dist/cache/index.d.ts +68 -12
- package/dist/cache/types.d.ts +33 -19
- package/dist/cli/bin/mh.js +10 -0
- package/dist/cli/lib/banner/index.js +14 -0
- package/dist/cli/lib/commands/app/index.js +37 -0
- package/dist/cli/lib/commands/feature/index.js +55 -0
- package/dist/cli/lib/commands/index.js +89 -0
- package/dist/cli/lib/commands/init/index.js +29 -0
- package/dist/cli/lib/commands/shared/index.js +56 -0
- package/dist/cli/lib/index.js +56 -0
- package/dist/cli/lib/parser/index.js +24 -0
- package/dist/cli/lib/prompt/index.js +61 -0
- package/dist/cli/lib/runner/index.js +46 -0
- package/dist/cli/lib/runner/types.js +1 -0
- package/dist/cli/lib/runner/utils.js +60 -0
- package/dist/cli/lib/types.js +1 -0
- package/dist/cli/lib/utils.js +20 -0
- package/dist/cli/templates/app/action/actions.ts.ejs.t +10 -0
- package/dist/cli/templates/app/action/types.ts.ejs.t +7 -0
- package/dist/cli/templates/app/integration/index.integration.tsx.ejs.t +13 -0
- package/dist/cli/templates/app/page/actions.ts.ejs.t +14 -0
- package/dist/cli/templates/app/page/index.tsx.ejs.t +20 -0
- package/dist/cli/templates/app/page/styles.ts.ejs.t +35 -0
- package/dist/cli/templates/app/page/types.ts.ejs.t +12 -0
- package/dist/cli/templates/feature/action/actions.ts.ejs.t +10 -0
- package/dist/cli/templates/feature/action/types.ts.ejs.t +7 -0
- package/dist/cli/templates/feature/multicast/types.ts.ejs.t +7 -0
- package/dist/cli/templates/feature/presentational/index.tsx.ejs.t +14 -0
- package/dist/cli/templates/feature/presentational/types.ts.ejs.t +12 -0
- package/dist/cli/templates/feature/presentational/utils.ts.ejs.t +8 -0
- package/dist/cli/templates/feature/stateful/actions.ts.ejs.t +16 -0
- package/dist/cli/templates/feature/stateful/index.tsx.ejs.t +19 -0
- package/dist/cli/templates/feature/stateful/types.ts.ejs.t +16 -0
- package/dist/cli/templates/feature/stateful/utils.ts.ejs.t +8 -0
- package/dist/cli/templates/feature/unit/index.test.tsx.ejs.t +21 -0
- package/dist/cli/templates/init/new/README.md.ejs.t +48 -0
- package/dist/cli/templates/init/new/eslint.config.js.ejs.t +88 -0
- package/dist/cli/templates/init/new/gitignore.ejs.t +9 -0
- package/dist/cli/templates/init/new/index.html.ejs.t +18 -0
- package/dist/cli/templates/init/new/package.json.ejs.t +54 -0
- package/dist/cli/templates/init/new/playwright.config.ts.ejs.t +17 -0
- package/dist/cli/templates/init/new/prettierrc.ejs.t +8 -0
- package/dist/cli/templates/init/new/src.app.index.tsx.ejs.t +14 -0
- package/dist/cli/templates/init/new/src.app.pages.home.actions.ts.ejs.t +16 -0
- package/dist/cli/templates/init/new/src.app.pages.home.index.tsx.ejs.t +30 -0
- package/dist/cli/templates/init/new/src.app.pages.home.integration.tsx.ejs.t +28 -0
- package/dist/cli/templates/init/new/src.app.pages.home.styles.ts.ejs.t +45 -0
- package/dist/cli/templates/init/new/src.app.pages.home.types.ts.ejs.t +12 -0
- package/dist/cli/templates/init/new/src.app.utils.ts.ejs.t +9 -0
- package/dist/cli/templates/init/new/src.features.greet.actions.ts.ejs.t +20 -0
- package/dist/cli/templates/init/new/src.features.greet.index.test.tsx.ejs.t +21 -0
- package/dist/cli/templates/init/new/src.features.greet.index.tsx.ejs.t +24 -0
- package/dist/cli/templates/init/new/src.features.greet.types.ts.ejs.t +18 -0
- package/dist/cli/templates/init/new/src.features.greet.utils.ts.ejs.t +8 -0
- package/dist/cli/templates/init/new/src.index.tsx.ejs.t +8 -0
- package/dist/cli/templates/init/new/src.shared.components.button.index.test.tsx.ejs.t +13 -0
- package/dist/cli/templates/init/new/src.shared.components.button.index.tsx.ejs.t +10 -0
- package/dist/cli/templates/init/new/src.shared.components.button.types.ts.ejs.t +6 -0
- package/dist/cli/templates/init/new/src.shared.resources.index.ts.ejs.t +4 -0
- package/dist/cli/templates/init/new/src.shared.theme.index.ts.ejs.t +51 -0
- package/dist/cli/templates/init/new/src.shared.types.index.ts.ejs.t +23 -0
- package/dist/cli/templates/init/new/src.test-setup.ts.ejs.t +10 -0
- package/dist/cli/templates/init/new/src.vite-env.d.ts.ejs.t +4 -0
- package/dist/cli/templates/init/new/tests.home.e2e.ts.ejs.t +14 -0
- package/dist/cli/templates/init/new/tsconfig.json.ejs.t +29 -0
- package/dist/cli/templates/init/new/vite.config.ts.ejs.t +17 -0
- package/dist/cli/templates/init/new/vitest.config.ts.ejs.t +24 -0
- package/dist/cli/templates/shared/component/index.tsx.ejs.t +9 -0
- package/dist/cli/templates/shared/component/types.ts.ejs.t +8 -0
- package/dist/cli/templates/shared/resource/index.ts.ejs.t +15 -0
- package/dist/cli/templates/shared/resource/types.ts.ejs.t +10 -0
- package/dist/cli/templates/shared/type-broadcast/types.ts.ejs.t +7 -0
- package/dist/cli/templates/shared/type-payload/types.ts.ejs.t +9 -0
- package/dist/cli/templates/shared/unit-component/index.test.tsx.ejs.t +13 -0
- package/dist/cli/templates/shared/unit-resource/index.test.ts.ejs.t +15 -0
- package/dist/cli/templates/shared/unit-util/index.test.ts.ejs.t +11 -0
- package/dist/cli/templates/shared/util/index.ts.ejs.t +6 -0
- package/dist/coalesce/index.d.ts +1 -1
- package/dist/context/index.d.ts +2 -2
- package/dist/error/index.d.ts +18 -1
- package/dist/error/types.d.ts +1 -18
- package/dist/error/utils.d.ts +1 -1
- package/dist/index.d.ts +16 -14
- package/dist/march-hare.js +7 -6
- package/dist/march-hare.js.map +1 -0
- package/dist/march-hare.umd.cjs +2 -1
- package/dist/march-hare.umd.cjs.map +1 -0
- package/dist/resource/index.d.ts +32 -61
- package/dist/resource/types.d.ts +45 -22
- package/dist/resource/utils.d.ts +31 -3
- package/dist/scope/index.d.ts +4 -64
- package/dist/scope/types.d.ts +8 -8
- package/dist/scope/utils.d.ts +12 -0
- package/dist/shared/index.d.ts +12 -21
- package/dist/types/index.d.ts +114 -29
- package/dist/utils/index.d.ts +3 -3
- package/dist/utils/types.d.ts +1 -3
- package/dist/utils/utils.d.ts +1 -3
- package/dist/with/index.d.ts +17 -62
- package/dist/with/types.d.ts +66 -0
- package/dist/with/utils.d.ts +61 -0
- package/package.json +21 -4
- package/src/cli/README.md +314 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: eslint.config.js
|
|
3
|
+
---
|
|
4
|
+
import js from "@eslint/js";
|
|
5
|
+
import globals from "globals";
|
|
6
|
+
import tseslint from "typescript-eslint";
|
|
7
|
+
import pluginReact from "eslint-plugin-react";
|
|
8
|
+
import pluginImport from "eslint-plugin-import";
|
|
9
|
+
import pluginBoundaries from "eslint-plugin-boundaries";
|
|
10
|
+
import { defineConfig } from "eslint/config";
|
|
11
|
+
|
|
12
|
+
export default defineConfig([
|
|
13
|
+
{
|
|
14
|
+
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
15
|
+
plugins: { js },
|
|
16
|
+
extends: ["js/recommended"],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
20
|
+
languageOptions: { globals: globals.browser },
|
|
21
|
+
},
|
|
22
|
+
tseslint.configs.recommended,
|
|
23
|
+
pluginReact.configs.flat.recommended,
|
|
24
|
+
{
|
|
25
|
+
plugins: { import: pluginImport },
|
|
26
|
+
settings: {
|
|
27
|
+
react: { version: "detect" },
|
|
28
|
+
},
|
|
29
|
+
rules: {
|
|
30
|
+
"react/react-in-jsx-scope": "off",
|
|
31
|
+
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
|
|
32
|
+
"@typescript-eslint/no-non-null-assertion": "error",
|
|
33
|
+
"@typescript-eslint/no-namespace": "off",
|
|
34
|
+
"@typescript-eslint/no-unused-vars": [
|
|
35
|
+
"error",
|
|
36
|
+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
37
|
+
],
|
|
38
|
+
"import/no-default-export": "error",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
files: ["src/**/*.{ts,tsx,d.ts}"],
|
|
43
|
+
plugins: { boundaries: pluginBoundaries },
|
|
44
|
+
settings: {
|
|
45
|
+
"import/resolver": {
|
|
46
|
+
typescript: { project: "./tsconfig.json" },
|
|
47
|
+
node: true,
|
|
48
|
+
},
|
|
49
|
+
"boundaries/include": ["src/**/*"],
|
|
50
|
+
"boundaries/elements": [
|
|
51
|
+
{ type: "app", pattern: "src/app", mode: "folder" },
|
|
52
|
+
{
|
|
53
|
+
type: "features",
|
|
54
|
+
pattern: "src/features/*",
|
|
55
|
+
mode: "folder",
|
|
56
|
+
capture: ["slice"],
|
|
57
|
+
},
|
|
58
|
+
{ type: "shared", pattern: "src/shared", mode: "folder" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
rules: {
|
|
62
|
+
"import/no-default-export": "off",
|
|
63
|
+
"boundaries/dependencies": [
|
|
64
|
+
"error",
|
|
65
|
+
{
|
|
66
|
+
default: "disallow",
|
|
67
|
+
message:
|
|
68
|
+
"${file.type} cannot import ${dependency.type} — layering is top-down (app → features → shared).",
|
|
69
|
+
rules: [
|
|
70
|
+
{ from: { type: "app" }, allow: { to: { type: ["app", "features", "shared"] } } },
|
|
71
|
+
{ from: { type: "features" }, allow: { to: { type: "shared" } } },
|
|
72
|
+
{ from: { type: "shared" }, allow: { to: { type: "shared" } } },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
files: ["**/*.test.{ts,tsx}", "**/*.integration.{ts,tsx}", "tests/**/*.{ts,tsx}"],
|
|
80
|
+
rules: {
|
|
81
|
+
"@typescript-eslint/no-floating-promises": "off",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
files: ["vite.config.ts", "vitest.config.ts", "playwright.config.ts", "eslint.config.js"],
|
|
86
|
+
rules: { "import/no-default-export": "off" },
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: index.html
|
|
3
|
+
---
|
|
4
|
+
<!doctype html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9
|
+
<title><%= title(name) %></title>
|
|
10
|
+
<style>
|
|
11
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
12
|
+
html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: #f5f5f5; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<script type="module" src="/src/index.tsx"></script>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: package.json
|
|
3
|
+
---
|
|
4
|
+
{
|
|
5
|
+
"name": "<%= name %>",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"description": "<%= description %>",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"private": true,
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "vite",
|
|
12
|
+
"build": "vite build",
|
|
13
|
+
"preview": "vite preview",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"test:e2e": "playwright test",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"lint": "eslint src/",
|
|
19
|
+
"fmt": "prettier --write .",
|
|
20
|
+
"checks": "npm run fmt && npm run lint && npm run typecheck && npm run test"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@emotion/css": "^11.13.5",
|
|
24
|
+
"@mobily/ts-belt": "^3.0.0",
|
|
25
|
+
"antd": "^6.3.0",
|
|
26
|
+
"immer": "^10.0.0",
|
|
27
|
+
"ky": "^2.0.2",
|
|
28
|
+
"march-hare": "^0.13.0",
|
|
29
|
+
"react": "^19.0.0",
|
|
30
|
+
"react-dom": "^19.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@eslint/js": "^9.39.3",
|
|
34
|
+
"@playwright/test": "^1.58.2",
|
|
35
|
+
"@testing-library/dom": "^10.4.1",
|
|
36
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
37
|
+
"@testing-library/react": "^16.3.2",
|
|
38
|
+
"@types/react": "^19.2.14",
|
|
39
|
+
"@types/react-dom": "^19.2.3",
|
|
40
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
41
|
+
"eslint": "^9.39.3",
|
|
42
|
+
"eslint-import-resolver-typescript": "^4.4.5",
|
|
43
|
+
"eslint-plugin-boundaries": "^6.0.2",
|
|
44
|
+
"eslint-plugin-import": "^2.32.0",
|
|
45
|
+
"eslint-plugin-react": "^7.37.5",
|
|
46
|
+
"globals": "^17.3.0",
|
|
47
|
+
"happy-dom": "^20.6.1",
|
|
48
|
+
"prettier": "^3.8.1",
|
|
49
|
+
"typescript": "^6.0.3",
|
|
50
|
+
"typescript-eslint": "^8.55.0",
|
|
51
|
+
"vite": "^7.3.1",
|
|
52
|
+
"vitest": "^4.0.18"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: playwright.config.ts
|
|
3
|
+
---
|
|
4
|
+
import { defineConfig } from "@playwright/test";
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
testDir: "./tests",
|
|
8
|
+
testMatch: "*.e2e.ts",
|
|
9
|
+
use: {
|
|
10
|
+
baseURL: "http://localhost:5173",
|
|
11
|
+
},
|
|
12
|
+
webServer: {
|
|
13
|
+
command: "vite dev",
|
|
14
|
+
port: 5173,
|
|
15
|
+
reuseExistingServer: !process.env.CI,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/app/index.tsx
|
|
3
|
+
---
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { app } from "./utils.ts";
|
|
6
|
+
import { HomePage } from "./pages/home/index.tsx";
|
|
7
|
+
|
|
8
|
+
export function Root(): React.ReactElement {
|
|
9
|
+
return (
|
|
10
|
+
<app.Boundary>
|
|
11
|
+
<HomePage />
|
|
12
|
+
</app.Boundary>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/app/pages/home/actions.ts
|
|
3
|
+
---
|
|
4
|
+
import { app } from "../../utils.ts";
|
|
5
|
+
import { Actions, type Model } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
export function useActions() {
|
|
8
|
+
const context = app.useContext<Model, typeof Actions>();
|
|
9
|
+
const actions = context.useActions({ greeting: null });
|
|
10
|
+
|
|
11
|
+
actions.useAction(Actions.Broadcast.Greeted, (context, message) => {
|
|
12
|
+
context.actions.produce(({ model }) => void (model.greeting = message));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return actions;
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/app/pages/home/index.tsx
|
|
3
|
+
---
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { GreetButton } from "@features/greet/index.tsx";
|
|
6
|
+
import { useActions } from "./actions.ts";
|
|
7
|
+
import * as styles from "./styles.ts";
|
|
8
|
+
|
|
9
|
+
export function HomePage(): React.ReactElement {
|
|
10
|
+
const [model] = useActions();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<main className={styles.layout}>
|
|
14
|
+
<header className={styles.header}>
|
|
15
|
+
<h1 className={styles.title}><%= title(name) %></h1>
|
|
16
|
+
<p className={styles.tagline}>
|
|
17
|
+
Click the button below to dispatch your first March Hare action.
|
|
18
|
+
</p>
|
|
19
|
+
</header>
|
|
20
|
+
|
|
21
|
+
<GreetButton />
|
|
22
|
+
|
|
23
|
+
{model.greeting ? (
|
|
24
|
+
<p className={styles.greeting}>{model.greeting}</p>
|
|
25
|
+
) : (
|
|
26
|
+
<p className={styles.empty}>No greeting yet — press the button.</p>
|
|
27
|
+
)}
|
|
28
|
+
</main>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/app/pages/home/index.integration.tsx
|
|
3
|
+
---
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { render, screen, act, fireEvent, waitFor } from "@testing-library/react";
|
|
6
|
+
import { Root } from "@app/index.tsx";
|
|
7
|
+
|
|
8
|
+
describe("HomePage", () => {
|
|
9
|
+
it("renders the heading", () => {
|
|
10
|
+
render(<Root />);
|
|
11
|
+
expect(screen.getByRole("heading", { name: "<%= title(name) %>" })).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("shows the empty state until the button is clicked", () => {
|
|
15
|
+
render(<Root />);
|
|
16
|
+
expect(screen.getByText(/No greeting yet/)).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("broadcasts a greeting on click", async () => {
|
|
20
|
+
render(<Root />);
|
|
21
|
+
await act(async () => {
|
|
22
|
+
fireEvent.click(screen.getByRole("button", { name: /Say hello/ }));
|
|
23
|
+
});
|
|
24
|
+
await waitFor(() => {
|
|
25
|
+
expect(screen.queryByText(/No greeting yet/)).not.toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/app/pages/home/styles.ts
|
|
3
|
+
---
|
|
4
|
+
import { css } from "@emotion/css";
|
|
5
|
+
import { colour, font, spacing } from "@shared/theme/index.ts";
|
|
6
|
+
|
|
7
|
+
export const layout = css`
|
|
8
|
+
min-height: 100vh;
|
|
9
|
+
padding: ${spacing.xxl} ${spacing.l};
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: ${spacing.xl};
|
|
14
|
+
font-family: ${font.family};
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export const header = css`
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: ${spacing.xs};
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const title = css`
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-size: ${font.size.xxl};
|
|
27
|
+
font-weight: ${font.weight.bold};
|
|
28
|
+
color: ${colour.text.primary};
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const tagline = css`
|
|
32
|
+
margin: 0;
|
|
33
|
+
color: ${colour.text.secondary};
|
|
34
|
+
font-size: ${font.size.m};
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
export const greeting = css`
|
|
38
|
+
font-size: ${font.size.l};
|
|
39
|
+
color: ${colour.text.primary};
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export const empty = css`
|
|
43
|
+
color: ${colour.text.muted};
|
|
44
|
+
font-style: italic;
|
|
45
|
+
`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/greet/actions.ts
|
|
3
|
+
---
|
|
4
|
+
import { Actions, type Model } from "./types.ts";
|
|
5
|
+
import { scope } from "./utils.ts";
|
|
6
|
+
|
|
7
|
+
export function useActions() {
|
|
8
|
+
const context = scope.useContext<Model, typeof Actions>();
|
|
9
|
+
const actions = context.useActions({ count: 0 });
|
|
10
|
+
|
|
11
|
+
actions.useAction(Actions.Click, async (context) => {
|
|
12
|
+
context.actions.produce(({ model }) => void (model.count += 1));
|
|
13
|
+
await context.actions.dispatch(
|
|
14
|
+
Actions.Broadcast.Greeted,
|
|
15
|
+
`Hello from <%= title(name) %> #${context.model.count + 1}`,
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return actions;
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/greet/index.test.tsx
|
|
3
|
+
---
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { render, screen } from "@testing-library/react";
|
|
6
|
+
import { App } from "march-hare";
|
|
7
|
+
import { GreetButton } from "./index.tsx";
|
|
8
|
+
import { type Envs } from "@shared/types/index.ts";
|
|
9
|
+
|
|
10
|
+
const app = App<Envs>({ env: { apiBase: "https://api.example.test" } });
|
|
11
|
+
|
|
12
|
+
describe("GreetButton", () => {
|
|
13
|
+
it("renders the Say hello button", () => {
|
|
14
|
+
render(
|
|
15
|
+
<app.Boundary>
|
|
16
|
+
<GreetButton />
|
|
17
|
+
</app.Boundary>,
|
|
18
|
+
);
|
|
19
|
+
expect(screen.getByRole("button", { name: "Say hello" })).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/greet/index.tsx
|
|
3
|
+
---
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { Button } from "@shared/components/button/index.tsx";
|
|
6
|
+
import { useActions } from "./actions.ts";
|
|
7
|
+
import { scope } from "./utils.ts";
|
|
8
|
+
import { Actions } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export function GreetButton(): React.ReactElement {
|
|
11
|
+
const [, actions] = useActions();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<scope.Boundary>
|
|
15
|
+
<Button
|
|
16
|
+
type="primary"
|
|
17
|
+
size="large"
|
|
18
|
+
onClick={() => actions.dispatch(Actions.Click)}
|
|
19
|
+
>
|
|
20
|
+
Say hello
|
|
21
|
+
</Button>
|
|
22
|
+
</scope.Boundary>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/greet/types.ts
|
|
3
|
+
---
|
|
4
|
+
import { Action, Distribution } from "march-hare";
|
|
5
|
+
import { Broadcast } from "@shared/types/index.ts";
|
|
6
|
+
|
|
7
|
+
export type Model = {
|
|
8
|
+
count: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class Actions {
|
|
12
|
+
static Click = Action("Greet.Click");
|
|
13
|
+
static Broadcast = Broadcast;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Multicast {
|
|
17
|
+
static Pulse = Action<number>("Greet.Pulse", Distribution.Multicast);
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/shared/components/button/index.test.tsx
|
|
3
|
+
---
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { render, screen } from "@testing-library/react";
|
|
6
|
+
import { Button } from "./index.tsx";
|
|
7
|
+
|
|
8
|
+
describe("Button", () => {
|
|
9
|
+
it("renders its children", () => {
|
|
10
|
+
render(<Button>Click me</Button>);
|
|
11
|
+
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/shared/components/button/index.tsx
|
|
3
|
+
---
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { Button as AntButton } from "antd";
|
|
6
|
+
import type { Props } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
export function Button(props: Props): React.ReactElement {
|
|
9
|
+
return <AntButton {...props} />;
|
|
10
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/shared/theme/index.ts
|
|
3
|
+
---
|
|
4
|
+
export const colour = <const>{
|
|
5
|
+
text: {
|
|
6
|
+
primary: "#1f1f1f",
|
|
7
|
+
secondary: "#6b6b6b",
|
|
8
|
+
muted: "#9b9b9b",
|
|
9
|
+
},
|
|
10
|
+
surface: {
|
|
11
|
+
card: "#ffffff",
|
|
12
|
+
placeholder: "#f0f0f0",
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const spacing = <const>{
|
|
17
|
+
xs: "8px",
|
|
18
|
+
s: "12px",
|
|
19
|
+
m: "20px",
|
|
20
|
+
l: "24px",
|
|
21
|
+
xl: "32px",
|
|
22
|
+
xxl: "48px",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const radius = <const>{
|
|
26
|
+
card: "16px",
|
|
27
|
+
pill: "50%",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const shadow = <const>{
|
|
31
|
+
card: "0 1px 2px rgba(0, 0, 0, 0.04), 0 4px 16px rgba(0, 0, 0, 0.06)",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const font = <const>{
|
|
35
|
+
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
36
|
+
size: {
|
|
37
|
+
s: "14px",
|
|
38
|
+
m: "15px",
|
|
39
|
+
l: "16px",
|
|
40
|
+
xl: "22px",
|
|
41
|
+
xxl: "36px",
|
|
42
|
+
},
|
|
43
|
+
weight: {
|
|
44
|
+
regular: 400,
|
|
45
|
+
semibold: 600,
|
|
46
|
+
bold: 700,
|
|
47
|
+
},
|
|
48
|
+
letterSpacing: {
|
|
49
|
+
tight: "-0.02em",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/shared/types/index.ts
|
|
3
|
+
---
|
|
4
|
+
import { Action, Distribution } from "march-hare";
|
|
5
|
+
|
|
6
|
+
export namespace Env {
|
|
7
|
+
export type <%= env %> = {
|
|
8
|
+
apiBase: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type Envs = Env.<%= env %>;
|
|
13
|
+
|
|
14
|
+
export namespace Payload {
|
|
15
|
+
export type Greeting = {
|
|
16
|
+
message: string;
|
|
17
|
+
at: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export namespace Broadcast {
|
|
22
|
+
export const Greeted = Action<string>("Greeted", Distribution.Broadcast);
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: tests/home.e2e.ts
|
|
3
|
+
---
|
|
4
|
+
import { test, expect } from "@playwright/test";
|
|
5
|
+
|
|
6
|
+
test("clicking Say hello renders a greeting", async ({ page }) => {
|
|
7
|
+
await page.goto("/");
|
|
8
|
+
|
|
9
|
+
await expect(page.getByText(/No greeting yet/)).toBeVisible();
|
|
10
|
+
|
|
11
|
+
await page.getByRole("button", { name: /Say hello/ }).click();
|
|
12
|
+
|
|
13
|
+
await expect(page.getByText(/Hello from/)).toBeVisible();
|
|
14
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: tsconfig.json
|
|
3
|
+
---
|
|
4
|
+
{
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"target": "es2020",
|
|
7
|
+
"module": "esnext",
|
|
8
|
+
"lib": ["es2024", "esnext.temporal", "dom", "dom.iterable"],
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noUnusedLocals": true,
|
|
13
|
+
"noUnusedParameters": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"allowImportingTsExtensions": true,
|
|
18
|
+
"isolatedModules": true,
|
|
19
|
+
"moduleDetection": "force",
|
|
20
|
+
"useDefineForClassFields": true,
|
|
21
|
+
"paths": {
|
|
22
|
+
"@app/*": ["./src/app/*"],
|
|
23
|
+
"@features/*": ["./src/features/*"],
|
|
24
|
+
"@shared/*": ["./src/shared/*"]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"include": ["src", "tests"],
|
|
28
|
+
"exclude": ["**/*.test.ts", "**/*.test.tsx", "**/*.integration.ts", "**/*.integration.tsx"]
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: vite.config.ts
|
|
3
|
+
---
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { defineConfig } from "vite";
|
|
6
|
+
import react from "@vitejs/plugin-react";
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: {
|
|
11
|
+
"@app": resolve(__dirname, "src/app"),
|
|
12
|
+
"@features": resolve(__dirname, "src/features"),
|
|
13
|
+
"@shared": resolve(__dirname, "src/shared"),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
plugins: [react()],
|
|
17
|
+
});
|