march-hare 0.13.0 → 0.13.2

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 (131) hide show
  1. package/README.md +45 -1
  2. package/dist/action/index.d.ts +2 -2
  3. package/dist/action/utils.d.ts +2 -2
  4. package/dist/actions/index.d.ts +2 -2
  5. package/dist/actions/types.d.ts +3 -3
  6. package/dist/actions/utils.d.ts +3 -3
  7. package/dist/app/index.d.ts +7 -7
  8. package/dist/app/types.d.ts +5 -5
  9. package/dist/boundary/components/broadcast/index.d.ts +2 -2
  10. package/dist/boundary/components/broadcast/types.d.ts +1 -1
  11. package/dist/boundary/components/consumer/components/partition/index.d.ts +1 -1
  12. package/dist/boundary/components/consumer/components/partition/types.d.ts +1 -1
  13. package/dist/boundary/components/consumer/index.d.ts +5 -5
  14. package/dist/boundary/components/consumer/types.d.ts +1 -1
  15. package/dist/boundary/components/consumer/utils.d.ts +1 -1
  16. package/dist/boundary/components/env/index.d.ts +3 -27
  17. package/dist/boundary/components/env/types.d.ts +24 -2
  18. package/dist/boundary/components/env/utils.d.ts +1 -1
  19. package/dist/boundary/components/scope/index.d.ts +2 -2
  20. package/dist/boundary/components/scope/types.d.ts +1 -1
  21. package/dist/boundary/components/scope/utils.d.ts +1 -1
  22. package/dist/boundary/components/sharing/index.d.ts +1 -1
  23. package/dist/boundary/components/tap/index.d.ts +3 -3
  24. package/dist/boundary/components/tap/types.d.ts +2 -2
  25. package/dist/boundary/components/tap/utils.d.ts +1 -1
  26. package/dist/boundary/components/tasks/index.d.ts +2 -2
  27. package/dist/boundary/components/tasks/utils.d.ts +1 -1
  28. package/dist/boundary/index.d.ts +1 -1
  29. package/dist/boundary/types.d.ts +2 -2
  30. package/dist/cache/index.d.ts +91 -9
  31. package/dist/cache/types.d.ts +1 -1
  32. package/dist/cli/bin/mh.js +10 -0
  33. package/dist/cli/lib/banner/index.js +14 -0
  34. package/dist/cli/lib/commands/app/index.js +37 -0
  35. package/dist/cli/lib/commands/feature/index.js +55 -0
  36. package/dist/cli/lib/commands/index.js +89 -0
  37. package/dist/cli/lib/commands/init/index.js +29 -0
  38. package/dist/cli/lib/commands/shared/index.js +56 -0
  39. package/dist/cli/lib/index.js +56 -0
  40. package/dist/cli/lib/parser/index.js +24 -0
  41. package/dist/cli/lib/prompt/index.js +61 -0
  42. package/dist/cli/lib/runner/index.js +46 -0
  43. package/dist/cli/lib/runner/types.js +1 -0
  44. package/dist/cli/lib/runner/utils.js +60 -0
  45. package/dist/cli/lib/types.js +1 -0
  46. package/dist/cli/lib/utils.js +20 -0
  47. package/dist/cli/templates/app/action/actions.ts.ejs.t +10 -0
  48. package/dist/cli/templates/app/action/types.ts.ejs.t +7 -0
  49. package/dist/cli/templates/app/integration/index.integration.tsx.ejs.t +13 -0
  50. package/dist/cli/templates/app/page/actions.ts.ejs.t +14 -0
  51. package/dist/cli/templates/app/page/index.tsx.ejs.t +20 -0
  52. package/dist/cli/templates/app/page/styles.ts.ejs.t +35 -0
  53. package/dist/cli/templates/app/page/types.ts.ejs.t +12 -0
  54. package/dist/cli/templates/feature/action/actions.ts.ejs.t +10 -0
  55. package/dist/cli/templates/feature/action/types.ts.ejs.t +7 -0
  56. package/dist/cli/templates/feature/multicast/types.ts.ejs.t +7 -0
  57. package/dist/cli/templates/feature/presentational/index.tsx.ejs.t +14 -0
  58. package/dist/cli/templates/feature/presentational/types.ts.ejs.t +12 -0
  59. package/dist/cli/templates/feature/presentational/utils.ts.ejs.t +8 -0
  60. package/dist/cli/templates/feature/stateful/actions.ts.ejs.t +16 -0
  61. package/dist/cli/templates/feature/stateful/index.tsx.ejs.t +19 -0
  62. package/dist/cli/templates/feature/stateful/types.ts.ejs.t +16 -0
  63. package/dist/cli/templates/feature/stateful/utils.ts.ejs.t +8 -0
  64. package/dist/cli/templates/feature/unit/index.test.tsx.ejs.t +21 -0
  65. package/dist/cli/templates/init/new/README.md.ejs.t +48 -0
  66. package/dist/cli/templates/init/new/eslint.config.js.ejs.t +88 -0
  67. package/dist/cli/templates/init/new/gitignore.ejs.t +9 -0
  68. package/dist/cli/templates/init/new/index.html.ejs.t +18 -0
  69. package/dist/cli/templates/init/new/package.json.ejs.t +54 -0
  70. package/dist/cli/templates/init/new/playwright.config.ts.ejs.t +17 -0
  71. package/dist/cli/templates/init/new/prettierrc.ejs.t +8 -0
  72. package/dist/cli/templates/init/new/src.app.index.tsx.ejs.t +14 -0
  73. package/dist/cli/templates/init/new/src.app.pages.home.actions.ts.ejs.t +16 -0
  74. package/dist/cli/templates/init/new/src.app.pages.home.index.tsx.ejs.t +30 -0
  75. package/dist/cli/templates/init/new/src.app.pages.home.integration.tsx.ejs.t +28 -0
  76. package/dist/cli/templates/init/new/src.app.pages.home.styles.ts.ejs.t +45 -0
  77. package/dist/cli/templates/init/new/src.app.pages.home.types.ts.ejs.t +12 -0
  78. package/dist/cli/templates/init/new/src.app.utils.ts.ejs.t +9 -0
  79. package/dist/cli/templates/init/new/src.features.greet.actions.ts.ejs.t +20 -0
  80. package/dist/cli/templates/init/new/src.features.greet.index.test.tsx.ejs.t +21 -0
  81. package/dist/cli/templates/init/new/src.features.greet.index.tsx.ejs.t +24 -0
  82. package/dist/cli/templates/init/new/src.features.greet.types.ts.ejs.t +18 -0
  83. package/dist/cli/templates/init/new/src.features.greet.utils.ts.ejs.t +8 -0
  84. package/dist/cli/templates/init/new/src.index.tsx.ejs.t +8 -0
  85. package/dist/cli/templates/init/new/src.shared.components.button.index.test.tsx.ejs.t +13 -0
  86. package/dist/cli/templates/init/new/src.shared.components.button.index.tsx.ejs.t +10 -0
  87. package/dist/cli/templates/init/new/src.shared.components.button.types.ts.ejs.t +6 -0
  88. package/dist/cli/templates/init/new/src.shared.resources.index.ts.ejs.t +4 -0
  89. package/dist/cli/templates/init/new/src.shared.theme.index.ts.ejs.t +51 -0
  90. package/dist/cli/templates/init/new/src.shared.types.index.ts.ejs.t +23 -0
  91. package/dist/cli/templates/init/new/src.test-setup.ts.ejs.t +10 -0
  92. package/dist/cli/templates/init/new/src.vite-env.d.ts.ejs.t +4 -0
  93. package/dist/cli/templates/init/new/tests.home.e2e.ts.ejs.t +14 -0
  94. package/dist/cli/templates/init/new/tsconfig.json.ejs.t +29 -0
  95. package/dist/cli/templates/init/new/vite.config.ts.ejs.t +17 -0
  96. package/dist/cli/templates/init/new/vitest.config.ts.ejs.t +24 -0
  97. package/dist/cli/templates/shared/component/index.tsx.ejs.t +9 -0
  98. package/dist/cli/templates/shared/component/types.ts.ejs.t +8 -0
  99. package/dist/cli/templates/shared/resource/index.ts.ejs.t +15 -0
  100. package/dist/cli/templates/shared/resource/types.ts.ejs.t +10 -0
  101. package/dist/cli/templates/shared/type-broadcast/types.ts.ejs.t +7 -0
  102. package/dist/cli/templates/shared/type-payload/types.ts.ejs.t +9 -0
  103. package/dist/cli/templates/shared/unit-component/index.test.tsx.ejs.t +13 -0
  104. package/dist/cli/templates/shared/unit-resource/index.test.ts.ejs.t +15 -0
  105. package/dist/cli/templates/shared/unit-util/index.test.ts.ejs.t +11 -0
  106. package/dist/cli/templates/shared/util/index.ts.ejs.t +6 -0
  107. package/dist/coalesce/index.d.ts +1 -1
  108. package/dist/context/index.d.ts +1 -1
  109. package/dist/error/types.d.ts +1 -1
  110. package/dist/error/utils.d.ts +1 -1
  111. package/dist/index.d.ts +17 -17
  112. package/dist/march-hare.js +9 -8
  113. package/dist/march-hare.js.map +1 -1
  114. package/dist/march-hare.umd.cjs +1 -1
  115. package/dist/march-hare.umd.cjs.map +1 -1
  116. package/dist/resource/index.d.ts +6 -5
  117. package/dist/resource/types.d.ts +3 -3
  118. package/dist/resource/utils.d.ts +12 -5
  119. package/dist/scope/index.d.ts +3 -3
  120. package/dist/scope/types.d.ts +2 -2
  121. package/dist/scope/utils.d.ts +6 -4
  122. package/dist/shared/index.d.ts +4 -4
  123. package/dist/types/index.d.ts +5 -5
  124. package/dist/utils/index.d.ts +3 -3
  125. package/dist/utils/types.d.ts +1 -1
  126. package/dist/utils/utils.d.ts +1 -1
  127. package/dist/with/index.d.ts +3 -3
  128. package/dist/with/types.d.ts +2 -2
  129. package/dist/with/utils.d.ts +3 -3
  130. package/package.json +18 -4
  131. package/src/cli/README.md +314 -0
@@ -0,0 +1,14 @@
1
+ ---
2
+ to: src/features/<%= name %>/index.tsx
3
+ ---
4
+ import * as React from "react";
5
+ import type { Props } from "./types.ts";
6
+ import { scope } from "./utils.ts";
7
+
8
+ export function <%= pascalName %>({ label }: Props): React.ReactElement {
9
+ return (
10
+ <scope.Boundary>
11
+ <span>{label}</span>
12
+ </scope.Boundary>
13
+ );
14
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ to: src/features/<%= name %>/types.ts
3
+ ---
4
+ import { Action, Distribution } from "march-hare";
5
+
6
+ export type Props = {
7
+ label: string;
8
+ };
9
+
10
+ export class Multicast {
11
+ static Update = Action<string>("<%= pascalName %>.Update", Distribution.Multicast);
12
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ to: src/features/<%= name %>/utils.ts
3
+ ---
4
+ import { shared } from "march-hare";
5
+ import { type Envs } from "@shared/types/index.ts";
6
+ import type { Multicast } from "./types.ts";
7
+
8
+ export const scope = shared.Scope<Envs, typeof Multicast>();
@@ -0,0 +1,16 @@
1
+ ---
2
+ to: src/features/<%= name %>/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.Tick, (context) => {
12
+ context.actions.produce(({ model }) => void (model.count += 1));
13
+ });
14
+
15
+ return actions;
16
+ }
@@ -0,0 +1,19 @@
1
+ ---
2
+ to: src/features/<%= name %>/index.tsx
3
+ ---
4
+ import * as React from "react";
5
+ import { useActions } from "./actions.ts";
6
+ import { scope } from "./utils.ts";
7
+ import { Actions } from "./types.ts";
8
+
9
+ export function <%= pascalName %>(): React.ReactElement {
10
+ const [model, actions] = useActions();
11
+
12
+ return (
13
+ <scope.Boundary>
14
+ <button onClick={() => actions.dispatch(Actions.Tick)}>
15
+ <%= pascalName %> ({model.count})
16
+ </button>
17
+ </scope.Boundary>
18
+ );
19
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ to: src/features/<%= name %>/types.ts
3
+ ---
4
+ import { Action, Distribution } from "march-hare";
5
+
6
+ export type Model = {
7
+ count: number;
8
+ };
9
+
10
+ export class Actions {
11
+ static Tick = Action("<%= pascalName %>.Tick");
12
+ }
13
+
14
+ export class Multicast {
15
+ static Update = Action<number>("<%= pascalName %>.Update", Distribution.Multicast);
16
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ to: src/features/<%= name %>/utils.ts
3
+ ---
4
+ import { shared } from "march-hare";
5
+ import { type Envs } from "@shared/types/index.ts";
6
+ import type { Multicast } from "./types.ts";
7
+
8
+ export const scope = shared.Scope<Envs, typeof Multicast>();
@@ -0,0 +1,21 @@
1
+ ---
2
+ to: src/features/<%= name %>/index.test.tsx
3
+ ---
4
+ import { describe, expect, it } from "vitest";
5
+ import { render } from "@testing-library/react";
6
+ import { App } from "march-hare";
7
+ import { <%= pascalName %> } 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("<%= pascalName %>", () => {
13
+ it("renders inside an app boundary", () => {
14
+ render(
15
+ <app.Boundary>
16
+ <<%= pascalName %> />
17
+ </app.Boundary>,
18
+ );
19
+ expect(document.body).toBeInTheDocument();
20
+ });
21
+ });
@@ -0,0 +1,48 @@
1
+ ---
2
+ to: README.md
3
+ ---
4
+ # <%= title(name) %>
5
+
6
+ <%= description %>
7
+
8
+ Built with [March Hare](https://github.com/Wildhoney/MarchHare) and the
9
+ [`@march-hare/cli`](https://github.com/Wildhoney/MarchHare/tree/main/src/cli)
10
+ scaffolder.
11
+
12
+ ## Layout
13
+
14
+ ```
15
+ src/
16
+ ├── app/ ← the host. One App() declaration, one Boundary, all routes.
17
+ ├── features/ ← user-facing behaviours. Each feature owns its own scope.
18
+ └── shared/ ← reusable building blocks: types, resources, theme, utils.
19
+ ```
20
+
21
+ Layer boundaries are enforced by `eslint-plugin-boundaries` — imports
22
+ flow strictly downward (`app → features → shared`).
23
+
24
+ ## Scripts
25
+
26
+ | Command | What it does |
27
+ | -------------------- | ----------------------------------------- |
28
+ | `yarn dev` | Start the Vite dev server |
29
+ | `yarn test` | Run unit + integration tests with Vitest |
30
+ | `yarn test:e2e` | Run Playwright end-to-end tests |
31
+ | `yarn typecheck` | TypeScript with no emit |
32
+ | `yarn lint` | ESLint |
33
+ | `yarn checks` | Format, lint, typecheck, and test |
34
+
35
+ ## Scaffolding more
36
+
37
+ With the CLI installed globally (`npm i -g @march-hare/cli`) or linked
38
+ locally, drop new pages, features, and shared modules in:
39
+
40
+ ```bash
41
+ mh # interactive menu
42
+ mh feature new # add a feature
43
+ mh app new # add a page
44
+ mh shared component # add a shared component
45
+ ```
46
+
47
+ See the [CLI README](https://github.com/Wildhoney/MarchHare/tree/main/src/cli)
48
+ for the full list.
@@ -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,9 @@
1
+ ---
2
+ to: .gitignore
3
+ ---
4
+ node_modules/
5
+ dist/
6
+ test-results/
7
+ playwright-report/
8
+ .DS_Store
9
+ *.log
@@ -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,8 @@
1
+ ---
2
+ to: .prettierrc
3
+ ---
4
+ {
5
+ "tabWidth": 2,
6
+ "printWidth": 80,
7
+ "singleQuote": false
8
+ }
@@ -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,12 @@
1
+ ---
2
+ to: src/app/pages/home/types.ts
3
+ ---
4
+ import { Broadcast } from "@shared/types/index.ts";
5
+
6
+ export type Model = {
7
+ greeting: string | null;
8
+ };
9
+
10
+ export class Actions {
11
+ static Broadcast = Broadcast;
12
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ to: src/app/utils.ts
3
+ ---
4
+ import { App } from "march-hare";
5
+ import { Env } from "@shared/types/index.ts";
6
+
7
+ export const app = App<Env.<%= env %>>({
8
+ env: { apiBase: "<%= apiBase %>" },
9
+ });
@@ -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,8 @@
1
+ ---
2
+ to: src/features/greet/utils.ts
3
+ ---
4
+ import { shared } from "march-hare";
5
+ import { type Envs } from "@shared/types/index.ts";
6
+ import type { Multicast } from "./types.ts";
7
+
8
+ export const scope = shared.Scope<Envs, typeof Multicast>();
@@ -0,0 +1,8 @@
1
+ ---
2
+ to: src/index.tsx
3
+ ---
4
+ import * as ReactDOM from "react-dom/client";
5
+ import { Root } from "./app/index.tsx";
6
+
7
+ const root = ReactDOM.createRoot(document.body);
8
+ root.render(<Root />);
@@ -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
+ });