march-hare 0.13.0 → 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.
Files changed (129) hide show
  1. package/README.md +25 -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 +2 -2
  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 +16 -16
  112. package/dist/march-hare.js.map +1 -1
  113. package/dist/march-hare.umd.cjs.map +1 -1
  114. package/dist/resource/index.d.ts +4 -4
  115. package/dist/resource/types.d.ts +3 -3
  116. package/dist/resource/utils.d.ts +4 -4
  117. package/dist/scope/index.d.ts +3 -3
  118. package/dist/scope/types.d.ts +2 -2
  119. package/dist/scope/utils.d.ts +2 -2
  120. package/dist/shared/index.d.ts +4 -4
  121. package/dist/types/index.d.ts +5 -5
  122. package/dist/utils/index.d.ts +3 -3
  123. package/dist/utils/types.d.ts +1 -1
  124. package/dist/utils/utils.d.ts +1 -1
  125. package/dist/with/index.d.ts +3 -3
  126. package/dist/with/types.d.ts +2 -2
  127. package/dist/with/utils.d.ts +3 -3
  128. package/package.json +18 -4
  129. 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,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
+ });
@@ -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,6 @@
1
+ ---
2
+ to: src/shared/components/button/types.ts
3
+ ---
4
+ import type { ButtonProps } from "antd";
5
+
6
+ export type Props = ButtonProps;
@@ -0,0 +1,4 @@
1
+ ---
2
+ to: src/shared/resources/index.ts
3
+ ---
4
+ export {};
@@ -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,10 @@
1
+ ---
2
+ to: src/test-setup.ts
3
+ ---
4
+ import { cleanup } from "@testing-library/react";
5
+ import { afterEach } from "vitest";
6
+ import "@testing-library/jest-dom/vitest";
7
+
8
+ afterEach(() => {
9
+ cleanup();
10
+ });
@@ -0,0 +1,4 @@
1
+ ---
2
+ to: src/vite-env.d.ts
3
+ ---
4
+ /// <reference types="vite/client" />
@@ -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
+ });