Skip to content

Commit e204d17

Browse files
authored
Merge pull request #55 from intent-framework/example/canonical-invite-app
example: add canonical invite app
2 parents 4dd1240 + 7e391d0 commit e204d17

13 files changed

Lines changed: 235 additions & 9 deletions

File tree

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,25 @@ This repository currently contains:
204204

205205
The core package must remain platformless. It should not import DOM, React, router internals, server framework code, or native APIs.
206206

207-
## Run the demo
207+
## Run the canonical example
208208

209209
Install dependencies:
210210

211211
```sh
212212
pnpm install
213213
```
214214

215-
Run the web demo:
215+
Run the canonical example (matches the Quickstart one-to-one):
216+
217+
```sh
218+
pnpm dev:canonical
219+
```
220+
221+
Open the local URL printed by Vite.
222+
223+
The example shows a single InviteMember screen — an email ask, a primary invite action blocked until the email is valid, and `inspectScreen()` output displayed on the page.
224+
225+
For a more complete demo with routing, resources, and diagnostics, see:
216226

217227
```sh
218228
pnpm dev:web-basic

docs/MVP-Checkpoint.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ It now has:
2828
## What is proven
2929

3030
- An npm-first quickstart guides new developers through install, definition, rendering, testing, and graph inspection.
31+
- A canonical runnable example (`examples/canonical-invite`) matches the quickstart one-to-one.
3132
- Screens can be authored semantically.
3233
- DOM can materialize the graph without being the source of truth.
3334
- Actions can be independently executable.
@@ -133,9 +134,12 @@ Do not jump into these without a very specific narrow PR:
133134
- Full DevTools package
134135
- Backend framework
135136

137+
## Proven
138+
139+
- ✅ Canonical example app (`examples/canonical-invite`) — new developers can clone and run after the quickstart. See [Canonical Invite Example](../examples/canonical-invite).
140+
136141
## Recommended next moves
137142

138-
1. Add a canonical example app that new developers can clone and run after the quickstart.
139-
2. Add a tiny diagnostics/dev-inspection page or command that prints `inspectScreen()` output for demo screens without `MutationObserver`.
140-
3. Improve resource semantics documentation: runtime-scoped resources, reload, stale, invalidation, context.
141-
4. Add one or two graph diagnostics that catch real authoring mistakes before adding any new package.
143+
1. Add a tiny diagnostics/dev-inspection page or command that prints `inspectScreen()` output for demo screens without `MutationObserver`.
144+
2. Improve resource semantics documentation: runtime-scoped resources, reload, stale, invalidation, context.
145+
3. Add one or two graph diagnostics that catch real authoring mistakes before adding any new package.

docs/Quickstart.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const InviteMember = screen("InviteMember", $ => {
3434

3535
const emailAsk = $.ask("Email", email)
3636
.required("Email is required")
37-
.validate(value => value.includes("@"), "Enter a valid email")
37+
.validate(value => value.includes("@") ? true : "Enter a valid email")
3838

3939
const invite = $.act("Invite member")
4040
.primary()
@@ -218,4 +218,12 @@ Intent does not replace components for everything. It replaces components as the
218218

219219
---
220220

221-
**Next steps:** See the [web demo](../examples/web-basic) for a full team invite flow with routing, resources, and diagnostics. Read the [Specification](Specification.md) for the architecture. Check the [MVP Checkpoint](MVP-Checkpoint.md) for current boundaries.
221+
**Next steps:**
222+
223+
1. Run the [canonical example](../examples/canonical-invite) — it matches this Quickstart one-to-one:
224+
```sh
225+
pnpm dev:canonical
226+
```
227+
2. See the [web demo](../examples/web-basic) for a full team invite flow with routing, resources, and diagnostics.
228+
3. Read the [Specification](Specification.md) for the architecture.
229+
4. Check the [MVP Checkpoint](MVP-Checkpoint.md) for current boundaries.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Canonical Invite Example
2+
3+
A minimal Intent application demonstrating the core pattern:
4+
5+
```txt
6+
Screen intent → Semantic graph → Materialized output (DOM, tests, diagnostics)
7+
```
8+
9+
This example matches the [Quickstart](../../docs/Quickstart.md) one-to-one.
10+
11+
## Run
12+
13+
```sh
14+
pnpm install
15+
pnpm dev:canonical
16+
```
17+
18+
Open the local URL printed by Vite.
19+
20+
## What it proves
21+
22+
- A semantic screen can be defined without DOM, components, or markup.
23+
- Asks can validate input and block actions with human-readable reasons.
24+
- The DOM renderer materializes the graph into real, inspectable HTML.
25+
- Actions produce feedback visible to the user.
26+
- The semantic graph can be inspected via `inspectScreen()`.
27+
- Tests can assert product behavior without touching the DOM.
28+
29+
## Test
30+
31+
```sh
32+
pnpm test
33+
```
34+
35+
## Compare
36+
37+
For a more complete example with routing, resources, and multiple screens, see [`examples/web-basic`](../web-basic).
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Intent — Invite Member Example</title>
7+
<style>
8+
body { font-family: system-ui, sans-serif; max-width: 650px; margin: 2rem auto; padding: 0 1rem; line-height: 1.5; }
9+
h1:first-of-type { font-size: 1.25rem; }
10+
h2 { font-size: 1rem; margin-top: 2rem; }
11+
label { display: block; font-weight: 600; margin-top: 1rem; margin-bottom: 0.25rem; }
12+
input { width: 100%; box-sizing: border-box; padding: 0.5rem; font-size: 1rem; border: 1px solid #ccc; border-radius: 4px; }
13+
button { margin-top: 0.75rem; padding: 0.5rem 1rem; font-size: 1rem; cursor: pointer; border: 1px solid #aaa; border-radius: 4px; background: #fff; }
14+
button.primary { font-weight: 700; background: #1a73e8; color: #fff; border-color: #1a73e8; }
15+
button:disabled { opacity: 0.5; cursor: not-allowed; }
16+
output { display: block; margin-top: 0.5rem; font-style: italic; color: #555; }
17+
pre { background: #f5f5f5; padding: 0.75rem; border-radius: 4px; font-size: 0.8rem; overflow-x: auto; }
18+
.proves { background: #e8f0fe; padding: 0.75rem; border-radius: 4px; margin-top: 1.5rem; }
19+
.proves ul { margin: 0.25rem 0; padding-left: 1.25rem; }
20+
</style>
21+
</head>
22+
<body>
23+
<h1>Intent: Invite Member</h1>
24+
<p>A canonical runnable example. See <a href="../../docs/Quickstart.md">Quickstart</a> for the walkthrough.</p>
25+
<div id="root"></div>
26+
<div class="proves">
27+
<strong>What this proves:</strong>
28+
<ul>
29+
<li>A semantic screen can be defined without DOM, components, or markup.</li>
30+
<li>Asks can validate input and block actions with human-readable reasons.</li>
31+
<li>The DOM renderer materializes the graph into real, inspectable HTML.</li>
32+
<li>Actions produce feedback visible to the user.</li>
33+
<li>The semantic graph can be inspected via <code>inspectScreen()</code>.</li>
34+
<li>Tests can assert product behavior without touching the DOM.</li>
35+
</ul>
36+
</div>
37+
<h2>Semantic Graph (<code>inspectScreen()</code>)</h2>
38+
<pre id="inspect">Loading...</pre>
39+
<script type="module" src="/src/main.ts"></script>
40+
</body>
41+
</html>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "canonical-invite",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "vite build",
9+
"test": "vitest run",
10+
"typecheck": "tsc --noEmit",
11+
"lint": "tsc --noEmit"
12+
},
13+
"dependencies": {
14+
"@intent-framework/core": "workspace:*",
15+
"@intent-framework/dom": "workspace:*"
16+
},
17+
"devDependencies": {
18+
"@intent-framework/testing": "workspace:*",
19+
"typescript": "^5.7.0",
20+
"vite": "^6.0.0",
21+
"vitest": "^3.1.1"
22+
}
23+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, it, expect } from "vitest"
2+
import { testScreen } from "@intent-framework/testing"
3+
import { InviteMember } from "./InviteMember.js"
4+
5+
describe("InviteMember", () => {
6+
it("invite is blocked before valid email", async () => {
7+
await testScreen(InviteMember, async app => {
8+
app.act("Invite member").toBeBlockedBy("Enter a valid email first")
9+
})
10+
})
11+
12+
it("invite becomes enabled after valid email", async () => {
13+
await testScreen(InviteMember, async app => {
14+
await app.answer("Email", "ada@example.com")
15+
app.act("Invite member").toBeEnabled()
16+
})
17+
})
18+
19+
it("invite execution produces feedback", async () => {
20+
await testScreen(InviteMember, async app => {
21+
await app.answer("Email", "ada@example.com")
22+
await app.act("Invite member").run()
23+
expect(app.feedback()).toBe("Invite sent!")
24+
})
25+
})
26+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { screen } from "@intent-framework/core"
2+
3+
export const InviteMember = screen("InviteMember", $ => {
4+
const email = $.state.text("email")
5+
6+
const emailAsk = $.ask("Email", email)
7+
.required("Email is required")
8+
.validate(value => value.includes("@") ? true : "Enter a valid email")
9+
10+
const invite = $.act("Invite member")
11+
.primary()
12+
.when(emailAsk.valid, "Enter a valid email first")
13+
.does(() => {
14+
console.log("invite", email.value)
15+
})
16+
.feedback({
17+
pending: "Sending invite...",
18+
success: "Invite sent!",
19+
failure: "Could not send invite.",
20+
})
21+
22+
$.surface("main").contains(emailAsk, invite)
23+
})
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { renderDom } from "@intent-framework/dom"
2+
import { inspectScreen } from "@intent-framework/core"
3+
import { InviteMember } from "./InviteMember.js"
4+
5+
const root = document.getElementById("root")!
6+
renderDom(InviteMember, { target: root, showScreenName: true })
7+
8+
const inspect = document.getElementById("inspect")!
9+
const graph = inspectScreen(InviteMember)
10+
inspect.textContent = JSON.stringify(graph, null, 2)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"lib": ["ES2022", "DOM"],
5+
"noUnusedLocals": false,
6+
"noUnusedParameters": false
7+
},
8+
"include": ["src"]
9+
}

0 commit comments

Comments
 (0)