create-backbone-template 0.1.0

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 (182) hide show
  1. package/README.md +33 -0
  2. package/bin/create-backbone-template.js +5 -0
  3. package/package.json +30 -0
  4. package/src/create-backbone-template.js +204 -0
  5. package/template/.agents/skills/agent-browser/SKILL.md +55 -0
  6. package/template/.agents/skills/create-plan/SKILL.md +52 -0
  7. package/template/.agents/skills/create-plan/agents/openai.yaml +4 -0
  8. package/template/.agents/skills/create-pr-presentation/SKILL.md +86 -0
  9. package/template/.agents/skills/create-pr-presentation/agents/openai.yaml +4 -0
  10. package/template/.agents/skills/implement-plan/SKILL.md +26 -0
  11. package/template/.agents/skills/implement-plan/agents/openai.yaml +4 -0
  12. package/template/.agents/skills/review-plan/SKILL.md +38 -0
  13. package/template/.agents/skills/review-plan/agents/openai.yaml +4 -0
  14. package/template/.env.schema +30 -0
  15. package/template/.env.test +6 -0
  16. package/template/.oxlintrc.json +67 -0
  17. package/template/.vscode/extensions.json +3 -0
  18. package/template/.vscode/settings.json +23 -0
  19. package/template/AGENTS.md +55 -0
  20. package/template/Cargo.lock +2648 -0
  21. package/template/Cargo.toml +29 -0
  22. package/template/Justfile +140 -0
  23. package/template/README.md +72 -0
  24. package/template/TODO.md +1 -0
  25. package/template/_gitignore +12 -0
  26. package/template/buf.gen.yaml +7 -0
  27. package/template/buf.yaml +10 -0
  28. package/template/client/.oxfmtrc.json +8 -0
  29. package/template/client/.oxlintrc.json +57 -0
  30. package/template/client/README.md +19 -0
  31. package/template/client/_gitignore +5 -0
  32. package/template/client/index.html +12 -0
  33. package/template/client/package.json +47 -0
  34. package/template/client/packages/design-system/package.json +19 -0
  35. package/template/client/packages/design-system/src/index.ts +2 -0
  36. package/template/client/packages/design-system-basic/package.json +18 -0
  37. package/template/client/packages/design-system-basic/src/button.stories.tsx +50 -0
  38. package/template/client/packages/design-system-basic/src/button.tsx +26 -0
  39. package/template/client/packages/design-system-basic/src/empty-state.stories.tsx +18 -0
  40. package/template/client/packages/design-system-basic/src/empty-state.tsx +17 -0
  41. package/template/client/packages/design-system-basic/src/form-field.stories.tsx +15 -0
  42. package/template/client/packages/design-system-basic/src/form-field.tsx +10 -0
  43. package/template/client/packages/design-system-basic/src/form.stories.tsx +27 -0
  44. package/template/client/packages/design-system-basic/src/form.tsx +9 -0
  45. package/template/client/packages/design-system-basic/src/heading.stories.tsx +14 -0
  46. package/template/client/packages/design-system-basic/src/heading.tsx +25 -0
  47. package/template/client/packages/design-system-basic/src/index.tsx +15 -0
  48. package/template/client/packages/design-system-basic/src/inline.stories.tsx +13 -0
  49. package/template/client/packages/design-system-basic/src/inline.tsx +5 -0
  50. package/template/client/packages/design-system-basic/src/layout.stories.tsx +24 -0
  51. package/template/client/packages/design-system-basic/src/layout.tsx +14 -0
  52. package/template/client/packages/design-system-basic/src/loader.stories.tsx +8 -0
  53. package/template/client/packages/design-system-basic/src/loader.tsx +11 -0
  54. package/template/client/packages/design-system-basic/src/navigation.stories.tsx +16 -0
  55. package/template/client/packages/design-system-basic/src/navigation.tsx +18 -0
  56. package/template/client/packages/design-system-basic/src/notice.stories.tsx +13 -0
  57. package/template/client/packages/design-system-basic/src/notice.tsx +5 -0
  58. package/template/client/packages/design-system-basic/src/stack.stories.tsx +17 -0
  59. package/template/client/packages/design-system-basic/src/stack.tsx +5 -0
  60. package/template/client/packages/design-system-basic/src/styles.css +254 -0
  61. package/template/client/packages/design-system-basic/src/text-input.stories.tsx +13 -0
  62. package/template/client/packages/design-system-basic/src/text-input.tsx +5 -0
  63. package/template/client/packages/design-system-basic/src/text.stories.tsx +21 -0
  64. package/template/client/packages/design-system-basic/src/text.tsx +5 -0
  65. package/template/client/packages/design-system-contract/package.json +15 -0
  66. package/template/client/packages/design-system-contract/src/button.ts +10 -0
  67. package/template/client/packages/design-system-contract/src/empty-state.ts +9 -0
  68. package/template/client/packages/design-system-contract/src/form-field.ts +9 -0
  69. package/template/client/packages/design-system-contract/src/form.ts +9 -0
  70. package/template/client/packages/design-system-contract/src/heading.ts +9 -0
  71. package/template/client/packages/design-system-contract/src/index.ts +13 -0
  72. package/template/client/packages/design-system-contract/src/inline.ts +7 -0
  73. package/template/client/packages/design-system-contract/src/layout.ts +8 -0
  74. package/template/client/packages/design-system-contract/src/loader.ts +7 -0
  75. package/template/client/packages/design-system-contract/src/navigation.ts +13 -0
  76. package/template/client/packages/design-system-contract/src/notice.ts +8 -0
  77. package/template/client/packages/design-system-contract/src/stack.ts +8 -0
  78. package/template/client/packages/design-system-contract/src/text-input.ts +5 -0
  79. package/template/client/packages/design-system-contract/src/text.ts +9 -0
  80. package/template/client/packages/design-system-lint/fixtures/invalid/external-ui-import.tsx +5 -0
  81. package/template/client/packages/design-system-lint/fixtures/invalid/raw-dom-jsx.tsx +3 -0
  82. package/template/client/packages/design-system-lint/fixtures/invalid/two-violations.tsx +7 -0
  83. package/template/client/packages/design-system-lint/fixtures/valid/design-system-only.tsx +13 -0
  84. package/template/client/packages/design-system-lint/package.json +23 -0
  85. package/template/client/packages/design-system-lint/src/check-design-system-architecture.ts +22 -0
  86. package/template/client/packages/design-system-lint/src/design-system-architecture.ts +286 -0
  87. package/template/client/packages/design-system-lint/src/oxlint-plugin.ts +11 -0
  88. package/template/client/packages/design-system-lint/src/page-architecture.ts +382 -0
  89. package/template/client/packages/design-system-lint/src/rules.ts +111 -0
  90. package/template/client/packages/design-system-lint/test/design-system-architecture.test.ts +243 -0
  91. package/template/client/packages/design-system-lint/test/oxlint-fixtures.test.ts +159 -0
  92. package/template/client/packages/design-system-lint/test/page-architecture.test.ts +175 -0
  93. package/template/client/packages/design-system-lint/test/rules.test.ts +65 -0
  94. package/template/client/packages/design-system-lint/tsconfig.json +29 -0
  95. package/template/client/src/App.tsx +77 -0
  96. package/template/client/src/design-system-components.test.tsx +75 -0
  97. package/template/client/src/gen/helloworld/v1/helloworld_pb.ts +63 -0
  98. package/template/client/src/main.tsx +18 -0
  99. package/template/client/src/pages/hello/hello-page.stories.tsx +20 -0
  100. package/template/client/src/pages/hello/hello-page.test.tsx +90 -0
  101. package/template/client/src/pages/hello/hello-page.tsx +126 -0
  102. package/template/client/src/pages/page.ts +20 -0
  103. package/template/client/src/testing/create-preview-events.test.ts +36 -0
  104. package/template/client/src/testing/create-preview-events.ts +30 -0
  105. package/template/client/src/vite-env.d.ts +1 -0
  106. package/template/client/tsconfig.json +32 -0
  107. package/template/client/vite.config.ts +21 -0
  108. package/template/client/vite.ladle.config.ts +5 -0
  109. package/template/e2e/.gherkin-lintrc +20 -0
  110. package/template/e2e/.oxfmtrc.json +15 -0
  111. package/template/e2e/.oxlintrc.json +37 -0
  112. package/template/e2e/_gitignore +4 -0
  113. package/template/e2e/features/helloworld.feature +10 -0
  114. package/template/e2e/package.json +42 -0
  115. package/template/e2e/playwright.config.ts +16 -0
  116. package/template/e2e/support/app-gherkin.ts +4 -0
  117. package/template/e2e/support/fixtures.ts +236 -0
  118. package/template/e2e/support/gherkin-fixtures/duplicate-id.feature +9 -0
  119. package/template/e2e/support/gherkin-fixtures/duplicate-id.spec.ts +7 -0
  120. package/template/e2e/support/gherkin-fixtures/extra-implementation.spec.ts +7 -0
  121. package/template/e2e/support/gherkin-fixtures/extra-step.spec.ts +10 -0
  122. package/template/e2e/support/gherkin-fixtures/happy-path.spec.ts +4 -0
  123. package/template/e2e/support/gherkin-fixtures/missing-id.feature +4 -0
  124. package/template/e2e/support/gherkin-fixtures/missing-id.spec.ts +7 -0
  125. package/template/e2e/support/gherkin-fixtures/missing-implementation.spec.ts +7 -0
  126. package/template/e2e/support/gherkin-fixtures/missing-step.spec.ts +7 -0
  127. package/template/e2e/support/gherkin-fixtures/playwright.config.ts +7 -0
  128. package/template/e2e/support/gherkin-fixtures/scenario-outline.feature +9 -0
  129. package/template/e2e/support/gherkin-fixtures/scenario-outline.spec.ts +7 -0
  130. package/template/e2e/support/gherkin-fixtures/step-mismatch.spec.ts +9 -0
  131. package/template/e2e/support/gherkin-fixtures/valid-implementations.ts +23 -0
  132. package/template/e2e/support/gherkin-fixtures/valid-scenarios.feature +26 -0
  133. package/template/e2e/support/gherkin.test.ts +184 -0
  134. package/template/e2e/support/gherkin.ts +321 -0
  135. package/template/e2e/support/oxlint-plugin.test.ts +328 -0
  136. package/template/e2e/support/oxlint-plugin.ts +485 -0
  137. package/template/e2e/tests/helloworld.spec.ts +39 -0
  138. package/template/e2e/tsconfig.json +26 -0
  139. package/template/e2e/tsconfig.oxlint-plugin.json +12 -0
  140. package/template/package.json +9 -0
  141. package/template/pnpm-lock.yaml +10723 -0
  142. package/template/pnpm-workspace.yaml +8 -0
  143. package/template/pr-slide/README.md +95 -0
  144. package/template/pr-slide/package.json +23 -0
  145. package/template/pr-slide/src/cli.js +262 -0
  146. package/template/pr-slide/src/generate-pr-deck.js +833 -0
  147. package/template/pr-slide/src/git-context.js +91 -0
  148. package/template/pr-slide/src/presentation-paths.js +9 -0
  149. package/template/pr-slide/src/presentations.js +53 -0
  150. package/template/pr-slide/test/generate-pr-deck.test.js +118 -0
  151. package/template/pr-slide/test/presentation-paths.test.js +14 -0
  152. package/template/pr-slide/test/presentations.test.js +50 -0
  153. package/template/proto/helloworld/v1/helloworld.proto +15 -0
  154. package/template/scripts/run-e2e.sh +10 -0
  155. package/template/server/Cargo.toml +26 -0
  156. package/template/server/build.rs +9 -0
  157. package/template/server/dylint/backbone_server_lints/.cargo/config.toml +6 -0
  158. package/template/server/dylint/backbone_server_lints/Cargo.lock +1581 -0
  159. package/template/server/dylint/backbone_server_lints/Cargo.toml +21 -0
  160. package/template/server/dylint/backbone_server_lints/README.md +5 -0
  161. package/template/server/dylint/backbone_server_lints/_gitignore +1 -0
  162. package/template/server/dylint/backbone_server_lints/rust-toolchain +3 -0
  163. package/template/server/dylint/backbone_server_lints/src/lib.rs +612 -0
  164. package/template/server/dylint/backbone_server_lints/ui/lib.rs +4 -0
  165. package/template/server/dylint/backbone_server_lints/ui/lib.stderr +10 -0
  166. package/template/server/dylint/backbone_server_lints/ui/long_file.rs +303 -0
  167. package/template/server/dylint/backbone_server_lints/ui/long_file.stderr +6 -0
  168. package/template/server/dylint/backbone_server_lints/ui/main.rs +59 -0
  169. package/template/server/dylint/backbone_server_lints/ui/main.stderr +85 -0
  170. package/template/server/migrations/20260520120000_create_projects.sql +12 -0
  171. package/template/server/migrations/20260524160000_create_hello_world_inputs.sql +12 -0
  172. package/template/server/src/config.rs +27 -0
  173. package/template/server/src/db/hello_world.rs +34 -0
  174. package/template/server/src/db/hello_world_tests.rs +11 -0
  175. package/template/server/src/db/mod.rs +39 -0
  176. package/template/server/src/lib.rs +10 -0
  177. package/template/server/src/main.rs +43 -0
  178. package/template/server/src/rpc/greeter/mod.rs +31 -0
  179. package/template/server/src/rpc/greeter/say_hello.rs +27 -0
  180. package/template/server/src/rpc/mod.rs +8 -0
  181. package/template/server/src/state.rs +13 -0
  182. package/template/skills-lock.json +11 -0
@@ -0,0 +1,77 @@
1
+ import { createClient } from "@connectrpc/connect"
2
+ import { createConnectTransport } from "@connectrpc/connect-web"
3
+ import { Navigation } from "@backbone/design-system"
4
+ import { useMemo, useState } from "react"
5
+ import { Route, Routes } from "react-router"
6
+ import { GreeterService } from "./gen/helloworld/v1/helloworld_pb"
7
+ import {
8
+ HelloPage,
9
+ type HelloPageDynamicProps,
10
+ type HelloPageStaticProps,
11
+ } from "./pages/hello/hello-page"
12
+
13
+ function requireEnv(name: string): string {
14
+ const value = import.meta.env[name]
15
+
16
+ if (value === undefined || value === "") {
17
+ throw new Error(`Missing required environment variable: ${name}`)
18
+ }
19
+
20
+ return value
21
+ }
22
+
23
+ const transport = createConnectTransport({
24
+ baseUrl: requireEnv("VITE_SERVER_URL"),
25
+ })
26
+
27
+ export function App() {
28
+ return (
29
+ <>
30
+ <Navigation currentHref="/" items={[{ href: "/", label: "Hello" }]} />
31
+ <Routes>
32
+ <Route element={<HelloRoute />} path="/" />
33
+ </Routes>
34
+ </>
35
+ )
36
+ }
37
+
38
+ function HelloRoute() {
39
+ const greeter = useMemo(() => createClient(GreeterService, transport), [])
40
+ const [name, setName] = useState("World")
41
+ const [greeting, setGreeting] = useState("Hello, World!")
42
+ const [isCalling, setIsCalling] = useState(false)
43
+ const [error, setError] = useState<string | null>(null)
44
+
45
+ const staticProps: HelloPageStaticProps = {
46
+ eyebrow: "Backbone",
47
+ title: "ConnectRPC helloworld",
48
+ greeting,
49
+ name,
50
+ nameLabel: "Name",
51
+ namePlaceholder: "World",
52
+ submitLabel: isCalling ? "Calling..." : "Say hello",
53
+ isSubmitting: isCalling,
54
+ error,
55
+ }
56
+
57
+ const dynamicProps: HelloPageDynamicProps = {
58
+ onNameChanged: setName,
59
+ onSubmitted: sayHello,
60
+ }
61
+
62
+ async function sayHello() {
63
+ setIsCalling(true)
64
+ setError(null)
65
+
66
+ try {
67
+ const response = await greeter.sayHello({ name })
68
+ setGreeting(response.greeting)
69
+ } catch (caught) {
70
+ setError(caught instanceof Error ? caught.message : "Request failed")
71
+ } finally {
72
+ setIsCalling(false)
73
+ }
74
+ }
75
+
76
+ return <HelloPage {...staticProps} {...dynamicProps} />
77
+ }
@@ -0,0 +1,75 @@
1
+ import assert from "node:assert/strict"
2
+ import { Button, EmptyState, Loader, Navigation } from "@backbone/design-system"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { describe, test } from "vitest"
5
+
6
+ describe("Button", () => {
7
+ test("renders primary, secondary, and danger variants", () => {
8
+ const primary = renderToStaticMarkup(<Button>Say hello</Button>)
9
+ const secondary = renderToStaticMarkup(<Button variant="secondary">View history</Button>)
10
+ const danger = renderToStaticMarkup(<Button variant="danger">Clear history</Button>)
11
+
12
+ assert.match(primary, /ds-button--primary/)
13
+ assert.match(secondary, /ds-button--secondary/)
14
+ assert.match(danger, /ds-button--danger/)
15
+ })
16
+
17
+ test("renders loading state as disabled without hiding the label", () => {
18
+ const html = renderToStaticMarkup(<Button loading>Importing...</Button>)
19
+
20
+ assert.match(html, /ds-button--loading/)
21
+ assert.match(html, /disabled/)
22
+ assert.match(html, /Importing/)
23
+ })
24
+ })
25
+
26
+ describe("EmptyState", () => {
27
+ test("renders title and description", () => {
28
+ const html = renderToStaticMarkup(
29
+ <EmptyState description="Say hello to create the first saved input." title="No inputs yet" />,
30
+ )
31
+
32
+ assert.match(html, /No inputs yet/)
33
+ assert.match(html, /Say hello/)
34
+ })
35
+
36
+ test("renders an optional action", () => {
37
+ const html = renderToStaticMarkup(
38
+ <EmptyState
39
+ action={<Button>Say hello</Button>}
40
+ description="Create a saved hello-world input."
41
+ title="No inputs yet"
42
+ />,
43
+ )
44
+
45
+ assert.match(html, /ds-empty-state__actions/)
46
+ assert.match(html, /Say hello/)
47
+ })
48
+ })
49
+
50
+ describe("Loader", () => {
51
+ test("renders a configurable loading message", () => {
52
+ const html = renderToStaticMarkup(<Loader message="Loading hello-world inputs..." />)
53
+
54
+ assert.match(html, /ds-loader/)
55
+ assert.match(html, /Loading hello-world inputs/)
56
+ })
57
+ })
58
+
59
+ describe("Navigation", () => {
60
+ test("renders navigation links", () => {
61
+ const html = renderToStaticMarkup(
62
+ <Navigation
63
+ currentHref="/hello"
64
+ items={[
65
+ { href: "/hello", label: "Hello" },
66
+ { href: "/history", label: "History" },
67
+ ]}
68
+ />,
69
+ )
70
+
71
+ assert.match(html, /href="\/hello"/)
72
+ assert.match(html, /href="\/history"/)
73
+ assert.match(html, /aria-current="page"/)
74
+ })
75
+ })
@@ -0,0 +1,63 @@
1
+ // @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
2
+ // @generated from file helloworld/v1/helloworld.proto (package helloworld.v1, syntax proto3)
3
+ /* eslint-disable */
4
+
5
+ import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
6
+ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
7
+ import type { Message } from "@bufbuild/protobuf";
8
+
9
+ /**
10
+ * Describes the file helloworld/v1/helloworld.proto.
11
+ */
12
+ export const file_helloworld_v1_helloworld: GenFile = /*@__PURE__*/
13
+ fileDesc("Ch5oZWxsb3dvcmxkL3YxL2hlbGxvd29ybGQucHJvdG8SDWhlbGxvd29ybGQudjEiHwoPU2F5SGVsbG9SZXF1ZXN0EgwKBG5hbWUYASABKAkiJAoQU2F5SGVsbG9SZXNwb25zZRIQCghncmVldGluZxgBIAEoCTJdCg5HcmVldGVyU2VydmljZRJLCghTYXlIZWxsbxIeLmhlbGxvd29ybGQudjEuU2F5SGVsbG9SZXF1ZXN0Gh8uaGVsbG93b3JsZC52MS5TYXlIZWxsb1Jlc3BvbnNlYgZwcm90bzM");
14
+
15
+ /**
16
+ * @generated from message helloworld.v1.SayHelloRequest
17
+ */
18
+ export type SayHelloRequest = Message<"helloworld.v1.SayHelloRequest"> & {
19
+ /**
20
+ * @generated from field: string name = 1;
21
+ */
22
+ name: string;
23
+ };
24
+
25
+ /**
26
+ * Describes the message helloworld.v1.SayHelloRequest.
27
+ * Use `create(SayHelloRequestSchema)` to create a new message.
28
+ */
29
+ export const SayHelloRequestSchema: GenMessage<SayHelloRequest> = /*@__PURE__*/
30
+ messageDesc(file_helloworld_v1_helloworld, 0);
31
+
32
+ /**
33
+ * @generated from message helloworld.v1.SayHelloResponse
34
+ */
35
+ export type SayHelloResponse = Message<"helloworld.v1.SayHelloResponse"> & {
36
+ /**
37
+ * @generated from field: string greeting = 1;
38
+ */
39
+ greeting: string;
40
+ };
41
+
42
+ /**
43
+ * Describes the message helloworld.v1.SayHelloResponse.
44
+ * Use `create(SayHelloResponseSchema)` to create a new message.
45
+ */
46
+ export const SayHelloResponseSchema: GenMessage<SayHelloResponse> = /*@__PURE__*/
47
+ messageDesc(file_helloworld_v1_helloworld, 1);
48
+
49
+ /**
50
+ * @generated from service helloworld.v1.GreeterService
51
+ */
52
+ export const GreeterService: GenService<{
53
+ /**
54
+ * @generated from rpc helloworld.v1.GreeterService.SayHello
55
+ */
56
+ sayHello: {
57
+ methodKind: "unary";
58
+ input: typeof SayHelloRequestSchema;
59
+ output: typeof SayHelloResponseSchema;
60
+ },
61
+ }> = /*@__PURE__*/
62
+ serviceDesc(file_helloworld_v1_helloworld, 0);
63
+
@@ -0,0 +1,18 @@
1
+ import { StrictMode } from "react"
2
+ import { createRoot } from "react-dom/client"
3
+ import { BrowserRouter } from "react-router"
4
+ import { App } from "./App"
5
+
6
+ const rootElement = document.getElementById("root")
7
+
8
+ if (rootElement === null) {
9
+ throw new Error("Root element not found")
10
+ }
11
+
12
+ createRoot(rootElement).render(
13
+ <StrictMode>
14
+ <BrowserRouter>
15
+ <App />
16
+ </BrowserRouter>
17
+ </StrictMode>,
18
+ )
@@ -0,0 +1,20 @@
1
+ import { createPreviewDynamicProps } from "../../testing/create-preview-events"
2
+ import {
3
+ HelloPage,
4
+ type HelloPageDynamicProps,
5
+ helloPageDynamicPropKeys,
6
+ helloPagePreviewStates,
7
+ } from "./hello-page"
8
+
9
+ const { dynamicProps } = createPreviewDynamicProps<HelloPageDynamicProps>(
10
+ helloPageDynamicPropKeys,
11
+ {
12
+ record: true,
13
+ },
14
+ )
15
+
16
+ export const Ready = () => <HelloPage {...helloPagePreviewStates.ready} {...dynamicProps} />
17
+
18
+ export const Calling = () => <HelloPage {...helloPagePreviewStates.calling} {...dynamicProps} />
19
+
20
+ export const Error = () => <HelloPage {...helloPagePreviewStates.error} {...dynamicProps} />
@@ -0,0 +1,90 @@
1
+ import assert from "node:assert/strict"
2
+ import type { ReactElement } from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { describe, test } from "vitest"
5
+ import { HelloPage, type HelloPageDynamicProps, type HelloPageStaticProps } from "./hello-page"
6
+
7
+ type ElementWithProps<Props> = ReactElement<Props>
8
+
9
+ const defaultStaticProps: HelloPageStaticProps = {
10
+ eyebrow: "Backbone",
11
+ title: "ConnectRPC helloworld",
12
+ greeting: "Hello, World!",
13
+ name: "World",
14
+ nameLabel: "Name",
15
+ namePlaceholder: "World",
16
+ submitLabel: "Say hello",
17
+ isSubmitting: false,
18
+ error: null,
19
+ }
20
+
21
+ function createDynamicProps(): HelloPageDynamicProps & {
22
+ calls: Array<{ name: keyof HelloPageDynamicProps; args: unknown[] }>
23
+ } {
24
+ const calls: Array<{ name: keyof HelloPageDynamicProps; args: unknown[] }> = []
25
+
26
+ return {
27
+ calls,
28
+ onNameChanged(value) {
29
+ calls.push({ name: "onNameChanged", args: [value] })
30
+ },
31
+ onSubmitted() {
32
+ calls.push({ name: "onSubmitted", args: [] })
33
+ },
34
+ }
35
+ }
36
+
37
+ describe("HelloPage", () => {
38
+ test("renders from plain params", () => {
39
+ const html = renderToStaticMarkup(
40
+ <HelloPage {...defaultStaticProps} {...createDynamicProps()} />,
41
+ )
42
+
43
+ assert.match(html, /ConnectRPC helloworld/)
44
+ assert.match(html, /Hello, World!/)
45
+ assert.match(html, /value="World"/)
46
+ assert.match(html, /Say hello/)
47
+ })
48
+
49
+ test("wires input and form events", () => {
50
+ const dynamicProps = createDynamicProps()
51
+ const element = HelloPage({ ...defaultStaticProps, ...dynamicProps })
52
+ const layoutProps = element.props as { middle: ReactElement }
53
+ const outerStack = layoutProps.middle as ElementWithProps<{
54
+ children: ReactElement[]
55
+ }>
56
+ const form = outerStack.props.children[1] as ElementWithProps<{
57
+ children: ReactElement
58
+ onSubmit(event: { preventDefault(): void }): void
59
+ }>
60
+ const formField = form.props.children as ElementWithProps<{
61
+ children: ReactElement
62
+ }>
63
+ const inline = formField.props.children as ElementWithProps<{
64
+ children: ReactElement[]
65
+ }>
66
+ const input = inline.props.children[0] as ElementWithProps<{
67
+ onChange(event: { target: { value: string } }): void
68
+ }>
69
+
70
+ input.props.onChange({ target: { value: "Alice" } })
71
+ form.props.onSubmit({ preventDefault() {} })
72
+
73
+ assert.deepEqual(dynamicProps.calls, [
74
+ { name: "onNameChanged", args: ["Alice"] },
75
+ { name: "onSubmitted", args: [] },
76
+ ])
77
+ })
78
+
79
+ test("renders error notice only when error is present", () => {
80
+ const withoutError = renderToStaticMarkup(
81
+ <HelloPage {...defaultStaticProps} {...createDynamicProps()} />,
82
+ )
83
+ const withError = renderToStaticMarkup(
84
+ <HelloPage {...defaultStaticProps} {...createDynamicProps()} error="Request failed" />,
85
+ )
86
+
87
+ assert.doesNotMatch(withoutError, /Request failed/)
88
+ assert.match(withError, /Request failed/)
89
+ })
90
+ })
@@ -0,0 +1,126 @@
1
+ import {
2
+ Button,
3
+ Form,
4
+ FormField,
5
+ Heading,
6
+ Inline,
7
+ Layout,
8
+ Notice,
9
+ Stack,
10
+ Text,
11
+ TextInput,
12
+ } from "@backbone/design-system"
13
+ import type { Page } from "../page"
14
+
15
+ export type HelloPageStaticProps = {
16
+ eyebrow: string
17
+ title: string
18
+ greeting: string
19
+ name: string
20
+ nameLabel: string
21
+ namePlaceholder: string
22
+ submitLabel: string
23
+ isSubmitting: boolean
24
+ error: string | null
25
+ }
26
+
27
+ export type HelloPageDynamicProps = {
28
+ onNameChanged(value: string): void
29
+ onSubmitted(): void
30
+ }
31
+
32
+ export const helloPageDynamicPropKeys = ["onNameChanged", "onSubmitted"] as const
33
+
34
+ export const helloPagePreviewStates = {
35
+ ready: {
36
+ eyebrow: "Backbone",
37
+ title: "ConnectRPC helloworld",
38
+ greeting: "Hello, World!",
39
+ name: "World",
40
+ nameLabel: "Name",
41
+ namePlaceholder: "World",
42
+ submitLabel: "Say hello",
43
+ isSubmitting: false,
44
+ error: null,
45
+ },
46
+ calling: {
47
+ eyebrow: "Backbone",
48
+ title: "ConnectRPC helloworld",
49
+ greeting: "Hello, World!",
50
+ name: "World",
51
+ nameLabel: "Name",
52
+ namePlaceholder: "World",
53
+ submitLabel: "Calling...",
54
+ isSubmitting: true,
55
+ error: null,
56
+ },
57
+ error: {
58
+ eyebrow: "Backbone",
59
+ title: "ConnectRPC helloworld",
60
+ greeting: "Hello, World!",
61
+ name: "World",
62
+ nameLabel: "Name",
63
+ namePlaceholder: "World",
64
+ submitLabel: "Say hello",
65
+ isSubmitting: false,
66
+ error: "Request failed",
67
+ },
68
+ } satisfies Record<string, HelloPageStaticProps>
69
+
70
+ export type HelloPageProps = HelloPageStaticProps & HelloPageDynamicProps
71
+
72
+ export const HelloPage: Page<HelloPageStaticProps, HelloPageDynamicProps> = ({
73
+ eyebrow,
74
+ title,
75
+ greeting,
76
+ name,
77
+ nameLabel,
78
+ namePlaceholder,
79
+ submitLabel,
80
+ isSubmitting,
81
+ error,
82
+ onNameChanged,
83
+ onSubmitted,
84
+ }) => {
85
+ return (
86
+ <Layout
87
+ middle={
88
+ <Stack gap="lg">
89
+ <Stack gap="sm">
90
+ <Text variant="eyebrow">{eyebrow}</Text>
91
+ <Heading id="app-title">{title}</Heading>
92
+ <Text tone="muted" variant="lede">
93
+ {greeting}
94
+ </Text>
95
+ </Stack>
96
+
97
+ <Form
98
+ ariaLabelledBy="app-title"
99
+ onSubmit={(event) => {
100
+ event.preventDefault()
101
+ onSubmitted()
102
+ }}
103
+ >
104
+ <FormField inputId="name" label={nameLabel}>
105
+ <Inline>
106
+ <TextInput
107
+ id="name"
108
+ name="name"
109
+ onChange={(event) => onNameChanged(event.target.value)}
110
+ placeholder={namePlaceholder}
111
+ value={name}
112
+ />
113
+ <Button disabled={isSubmitting} type="submit">
114
+ {submitLabel}
115
+ </Button>
116
+ </Inline>
117
+ </FormField>
118
+ </Form>
119
+
120
+ {error && <Notice tone="danger">{error}</Notice>}
121
+ </Stack>
122
+ }
123
+ middleWidthPx={680}
124
+ />
125
+ )
126
+ }
@@ -0,0 +1,20 @@
1
+ import type { ReactElement } from "react"
2
+
3
+ export type SerializableValue =
4
+ | string
5
+ | number
6
+ | boolean
7
+ | null
8
+ | readonly SerializableValue[]
9
+ | { readonly [key: string]: SerializableValue }
10
+
11
+ type SerializableProps<TProps> = {
12
+ [Key in keyof TProps]: TProps[Key] extends SerializableValue ? TProps[Key] : never
13
+ }
14
+
15
+ export type PageProps<StaticProps, DynamicProps> = StaticProps & DynamicProps
16
+
17
+ export type Page<
18
+ StaticProps extends SerializableProps<StaticProps>,
19
+ DynamicProps extends object,
20
+ > = (props: PageProps<StaticProps, DynamicProps>) => ReactElement
@@ -0,0 +1,36 @@
1
+ import assert from "node:assert/strict"
2
+ import { describe, test } from "vitest"
3
+ import { createPreviewDynamicProps } from "./create-preview-events"
4
+
5
+ type ExampleDynamicProps = {
6
+ onNameChanged(value: string): void
7
+ onSubmitted(): void
8
+ }
9
+
10
+ const dynamicPropKeys = ["onNameChanged", "onSubmitted"] as const
11
+
12
+ describe("createPreviewDynamicProps", () => {
13
+ test("creates default functions for required dynamic prop keys", () => {
14
+ const { dynamicProps } = createPreviewDynamicProps<ExampleDynamicProps>(dynamicPropKeys)
15
+
16
+ assert.equal(typeof dynamicProps.onNameChanged, "function")
17
+ assert.equal(typeof dynamicProps.onSubmitted, "function")
18
+ })
19
+
20
+ test("records dynamic prop names and args", () => {
21
+ const { calls, dynamicProps } = createPreviewDynamicProps<ExampleDynamicProps>(
22
+ dynamicPropKeys,
23
+ {
24
+ record: true,
25
+ },
26
+ )
27
+
28
+ dynamicProps.onNameChanged("Alice")
29
+ dynamicProps.onSubmitted()
30
+
31
+ assert.deepEqual(calls, [
32
+ { name: "onNameChanged", args: ["Alice"] },
33
+ { name: "onSubmitted", args: [] },
34
+ ])
35
+ })
36
+ })
@@ -0,0 +1,30 @@
1
+ type DynamicPropKey<TDynamicProps> = keyof TDynamicProps & string
2
+
3
+ export type PreviewDynamicPropCall<TDynamicProps> = {
4
+ name: DynamicPropKey<TDynamicProps>
5
+ args: unknown[]
6
+ }
7
+
8
+ export type CreatePreviewDynamicPropsOptions = {
9
+ record?: boolean
10
+ }
11
+
12
+ export function createPreviewDynamicProps<TDynamicProps extends object>(
13
+ dynamicPropKeys: readonly DynamicPropKey<TDynamicProps>[],
14
+ options: CreatePreviewDynamicPropsOptions = {},
15
+ ) {
16
+ const calls: Array<PreviewDynamicPropCall<TDynamicProps>> = []
17
+ const dynamicProps = {} as TDynamicProps
18
+
19
+ for (const dynamicPropKey of dynamicPropKeys) {
20
+ Object.assign(dynamicProps, {
21
+ [dynamicPropKey]: (...args: unknown[]) => {
22
+ if (options.record === true) {
23
+ calls.push({ name: dynamicPropKey, args })
24
+ }
25
+ },
26
+ })
27
+ }
28
+
29
+ return { calls, dynamicProps }
30
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
6
+ "skipLibCheck": true,
7
+ "esModuleInterop": true,
8
+ "allowSyntheticDefaultImports": true,
9
+ "strict": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "noUncheckedIndexedAccess": true,
12
+ "noImplicitOverride": true,
13
+ "noPropertyAccessFromIndexSignature": true,
14
+ "noImplicitReturns": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noUncheckedSideEffectImports": true,
19
+ "verbatimModuleSyntax": true,
20
+ "moduleDetection": "force",
21
+ "allowUnreachableCode": false,
22
+ "allowUnusedLabels": false,
23
+ "forceConsistentCasingInFileNames": true,
24
+ "module": "ESNext",
25
+ "moduleResolution": "Bundler",
26
+ "resolveJsonModule": true,
27
+ "isolatedModules": true,
28
+ "noEmit": true,
29
+ "jsx": "react-jsx"
30
+ },
31
+ "include": ["src", "vite.config.ts", "vite.ladle.config.ts"]
32
+ }
@@ -0,0 +1,21 @@
1
+ import react from "@vitejs/plugin-react"
2
+ import { defineConfig } from "vite"
3
+
4
+ const viteDevServerPort = process.env["VITE_DEV_SERVER_PORT"]
5
+ const devServerPort =
6
+ viteDevServerPort === undefined || viteDevServerPort === ""
7
+ ? undefined
8
+ : Number(viteDevServerPort)
9
+
10
+ export default defineConfig({
11
+ plugins: [react()],
12
+ server:
13
+ devServerPort === undefined
14
+ ? {
15
+ host: "127.0.0.1",
16
+ }
17
+ : {
18
+ host: "127.0.0.1",
19
+ port: devServerPort,
20
+ },
21
+ })
@@ -0,0 +1,5 @@
1
+ export default {
2
+ server: {
3
+ host: "127.0.0.1",
4
+ },
5
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "allowed-tags": ["on", { "patterns": ["^@id:[a-z0-9]+(?:[.-][a-z0-9]+)*$"] }],
3
+ "file-name": ["on", { "style": "kebab-case" }],
4
+ "indentation": ["on", { "Feature": 0, "Scenario": 2, "Step": 4, "scenario tag": 2 }],
5
+ "keywords-in-logical-order": "on",
6
+ "name-length": ["on", { "Feature": 80, "Scenario": 80, "Step": 100 }],
7
+ "new-line-at-eof": ["on", "yes"],
8
+ "no-dupe-scenario-names": "on",
9
+ "no-duplicate-tags": "on",
10
+ "no-empty-file": "on",
11
+ "no-files-without-scenarios": "on",
12
+ "no-multiple-empty-lines": "on",
13
+ "no-scenario-outlines-without-examples": "on",
14
+ "no-trailing-spaces": "on",
15
+ "no-unnamed-features": "on",
16
+ "no-unnamed-scenarios": "on",
17
+ "one-space-between-tags": "on",
18
+ "only-one-when": "on",
19
+ "scenario-size": ["on", { "steps-length": 8 }]
20
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "ignorePatterns": [
3
+ "node_modules/**",
4
+ "playwright-report/**",
5
+ "support/dist/**",
6
+ "support/gherkin-fixtures/**",
7
+ "support/gherkin.test.ts",
8
+ "test-results/**"
9
+ ],
10
+ "printWidth": 100,
11
+ "semi": false,
12
+ "singleQuote": false,
13
+ "tabWidth": 2,
14
+ "trailingComma": "all"
15
+ }