Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
aacb7e4
feat: scaffold local practice with enforced checks
joshpitzalis Oct 22, 2026
edf4d87
fix: close browser and domain fence gaps
joshpitzalis Oct 22, 2026
04d791e
docs: record approved Strokes product context
joshpitzalis Oct 22, 2026
8b253c5
fix: reject server packages from browser modules
joshpitzalis Oct 22, 2026
3b23af2
docs: choose the Strokes drawing-frame design
joshpitzalis Oct 22, 2026
ceb4957
docs: record delegated practice composition
joshpitzalis Oct 22, 2026
6ed8e98
feat: add local Hindi tracing preview
joshpitzalis Oct 22, 2026
4a1e2c3
fix: verify tracing states and resize alignment
joshpitzalis Oct 22, 2026
f8abbb1
docs: record the implemented practice design
joshpitzalis Oct 22, 2026
4e1c8ac
refactor: colocate practice and use Tailwind
joshpitzalis Oct 24, 2026
cedd2bd
refactor: split tracing practice into a feature slice
joshpitzalis Oct 24, 2026
714f92e
docs: define the reference trace for letter assessment
joshpitzalis Oct 24, 2026
979d2a2
refactor: place tracing inside the writing feature
joshpitzalis Oct 24, 2026
d389cf5
feat: check whole characters with visual feedback
joshpitzalis Oct 24, 2026
9ce5c79
fix: use the device viewport on mobile
joshpitzalis Oct 24, 2026
c864369
test: retain touch session through mobile reset
joshpitzalis Oct 24, 2026
78158a7
test: use one touch stream for drawing and reset
joshpitzalis Oct 24, 2026
ef52e14
test: trace Linux mobile input failure
joshpitzalis Oct 24, 2026
d049540
test: synthesize complete mobile tap gestures
joshpitzalis Oct 24, 2026
fa2f8bf
test: run touch checks with full Chromium
joshpitzalis Oct 24, 2026
c87b9c1
test: use native touch gestures on mobile
joshpitzalis Oct 24, 2026
5086c81
ci: add local Linux browser reproduction
joshpitzalis Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24.21.0
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm check
- run: pnpm test
- run: pnpm build
- uses: actions/upload-artifact@v4
if: failure()
with:
name: browser-results
path: test-results/
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
node_modules
dist
.void
.wrangler
.void-wrangler.jsonc
.env
.env.*
!.env.example
*.tsbuildinfo
.DS_Store
playwright-report/
test-results/
.fence-fixtures-*/

.impeccable/questions/
.keepup/map.html
204 changes: 204 additions & 0 deletions .impeccable/design.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
{
"schemaVersion": 2,
"generatedAt": "2026-10-22T05:37:28.158120+00:00",
"title": "Design System: Strokes",
"extensions": {
"colorMeta": {
"primary": {
"role": "primary",
"displayName": "Primary",
"canonical": "#2348d1",
"tonalRamp": [
"oklch(15% 0.2 265)",
"oklch(27% 0.2 265)",
"oklch(39% 0.2 265)",
"oklch(51% 0.2 265)",
"oklch(63% 0.2 265)",
"oklch(75% 0.2 265)",
"oklch(87% 0.2 265)",
"oklch(95% 0.2 265)"
]
},
"primary-deep": {
"role": "primary",
"displayName": "Primary Deep",
"canonical": "#17349e",
"tonalRamp": [
"oklch(15% 0.18 265)",
"oklch(27% 0.18 265)",
"oklch(39% 0.18 265)",
"oklch(51% 0.18 265)",
"oklch(63% 0.18 265)",
"oklch(75% 0.18 265)",
"oklch(87% 0.18 265)",
"oklch(95% 0.18 265)"
]
},
"ground": {
"role": "neutral",
"displayName": "Ground",
"canonical": "#edf1f6",
"tonalRamp": [
"oklch(15% 0.015 265)",
"oklch(27% 0.015 265)",
"oklch(39% 0.015 265)",
"oklch(51% 0.015 265)",
"oklch(63% 0.015 265)",
"oklch(75% 0.015 265)",
"oklch(87% 0.015 265)",
"oklch(95% 0.015 265)"
]
},
"surface": {
"role": "neutral",
"displayName": "Surface",
"canonical": "#ffffff",
"tonalRamp": [
"oklch(15% 0 265)",
"oklch(27% 0 265)",
"oklch(39% 0 265)",
"oklch(51% 0 265)",
"oklch(63% 0 265)",
"oklch(75% 0 265)",
"oklch(87% 0 265)",
"oklch(95% 0 265)"
]
},
"ink": {
"role": "neutral",
"displayName": "Ink",
"canonical": "#15223b",
"tonalRamp": [
"oklch(15% 0.04 265)",
"oklch(27% 0.04 265)",
"oklch(39% 0.04 265)",
"oklch(51% 0.04 265)",
"oklch(63% 0.04 265)",
"oklch(75% 0.04 265)",
"oklch(87% 0.04 265)",
"oklch(95% 0.04 265)"
]
},
"muted": {
"role": "neutral",
"displayName": "Muted",
"canonical": "#526079",
"tonalRamp": [
"oklch(15% 0.04 265)",
"oklch(27% 0.04 265)",
"oklch(39% 0.04 265)",
"oklch(51% 0.04 265)",
"oklch(63% 0.04 265)",
"oklch(75% 0.04 265)",
"oklch(87% 0.04 265)",
"oklch(95% 0.04 265)"
]
},
"rule": {
"role": "neutral",
"displayName": "Rule",
"canonical": "#c9d3e3",
"tonalRamp": [
"oklch(15% 0.025 265)",
"oklch(27% 0.025 265)",
"oklch(39% 0.025 265)",
"oklch(51% 0.025 265)",
"oklch(63% 0.025 265)",
"oklch(75% 0.025 265)",
"oklch(87% 0.025 265)",
"oklch(95% 0.025 265)"
]
},
"guide": {
"role": "primary",
"displayName": "Guide",
"canonical": "#c6d2f6",
"tonalRamp": [
"oklch(15% 0.08 265)",
"oklch(27% 0.08 265)",
"oklch(39% 0.08 265)",
"oklch(51% 0.08 265)",
"oklch(63% 0.08 265)",
"oklch(75% 0.08 265)",
"oklch(87% 0.08 265)",
"oklch(95% 0.08 265)"
]
},
"grid": {
"role": "neutral",
"displayName": "Grid",
"canonical": "#dce3ee",
"tonalRamp": [
"oklch(15% 0.02 265)",
"oklch(27% 0.02 265)",
"oklch(39% 0.02 265)",
"oklch(51% 0.02 265)",
"oklch(63% 0.02 265)",
"oklch(75% 0.02 265)",
"oklch(87% 0.02 265)",
"oklch(95% 0.02 265)"
]
},
"primary-soft": {
"role": "primary",
"displayName": "Primary Soft",
"canonical": "#e3e9ff",
"tonalRamp": [
"oklch(15% 0.055 265)",
"oklch(27% 0.055 265)",
"oklch(39% 0.055 265)",
"oklch(51% 0.055 265)",
"oklch(63% 0.055 265)",
"oklch(75% 0.055 265)",
"oklch(87% 0.055 265)",
"oklch(95% 0.055 265)"
]
},
"primary-pressed": {
"role": "primary",
"displayName": "Primary Pressed",
"canonical": "#d3ddff",
"tonalRamp": [
"oklch(15% 0.07 265)",
"oklch(27% 0.07 265)",
"oklch(39% 0.07 265)",
"oklch(51% 0.07 265)",
"oklch(63% 0.07 265)",
"oklch(75% 0.07 265)",
"oklch(87% 0.07 265)",
"oklch(95% 0.07 265)"
]
}
},
"shadows": [],
"motion": [],
"breakpoints": [
{
"name": "phone",
"value": "760px"
}
]
},
"components": [
{
"name": "Reveal button",
"kind": "button",
"html": "<button type=\"button\">Reveal</button>",
"css": "button { font: 600 16px system-ui, sans-serif; min-height:48px; padding:10px 16px; border:1px solid #2348d1; border-radius:8px; background:#2348d1; color:white; cursor:pointer; } button:hover { background:#17349e; } button:focus-visible { outline:3px solid #15223b; outline-offset:4px; } button:disabled { opacity:.5; cursor:wait; }"
}
],
"narrative": {
"northStar": "Printmaker's proof",
"overview": "A cool white working field, cobalt ink, registration corners, and explicit directed-stroke proofs. The user delegated the visual choice on 2026-10-22. The reference metaphor explains aligned drawing and repeated attempts; it introduces no printing terminology into learner instructions.",
"rules": [],
"dos": [
"Do prioritize the pad under ordinary indoor and daylight phone use.",
"Do keep human review and development-fixture status honest and readable.",
"Do use semantic HTML and SVG for all instructions, controls, and drawing."
],
"donts": [
"Don't add streaks, scores, mastery claims, a lesson catalog, or persistent history.",
"Don't use imagery or animation where it would compete with the drawing task."
]
}
}
26 changes: 26 additions & 0 deletions .impeccable/surfaces/src-features-practice-browser-practice-tsx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
version: 1
slug: "src-features-practice-browser-practice-tsx"
primary_target: "src/features/practice/components/practice.tsx"
related_targets: []
---

<!--
THESIS: One letter takes shape in one shared drawing frame; the pad owns attention.
OWN-WORLD: Cool white workspace, cobalt ink, pale blue reference, crisp registration corners, system sans controls.
STORY: Identify the letter, trace it, reveal the ordered reference, and retry; all input stays temporary.
FIRST VIEWPORT: Compact app header and letter row, large square pad, stable feedback and Clear/Reveal/Retry below. Desktop adds a narrow right reference rail; phone stacks it beneath.
FORM: Printmaker's proof, grounded direction5, seed2bcf69ec; exemplar-and-proof staging. Delegated compositionA is the north star.
-->

# Practice surface

Mode: Operate. Scope: the single-letter practice page and its responsive controls.

The user delegated direction and composition on 2026-10-22. Keep the square pad large enough for finger drawing and the control row adjacent. Numbered reference hints appear on Reveal; use the same checked-in directed paths as the faint guide. Avoid duplicated hint panels.

Composition reference: /Users/jxsh/.codex/generated_images/01a161ab-fc56-7072-819f-9052df880277/exec-23f5a8b9-c24e-4c90-92c7-ab78138bc58f.png. The other two generated compositions are alternatives only. These are preview-only mockups created with the built-in image tool; no raster asset belongs in the app.

Implementation inventory: semantic HTML for labels, instructions, feedback and buttons; SVG for authored lesson geometry, guide, directed hints and learner ink; CSS for registration corners, responsive layout and states. No photos, textures, raster text, icon library, or external fonts needed. Ignore invented completion/audio affordances in the generated comps. Do not copy their proposed teaching order as reviewed evidence.

Open gates: original lesson review, calibrated matching, and actual iPhone/Android/stylus checks. A tracing-only development lesson must say review pending and must not imply automatic grading or checked completion.
21 changes: 21 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"useTabs": true,
"ignorePatterns": [
"node_modules",
".agents",
".claude",
".void",
"dist",
"test-results",
"playwright-report"
],
"overrides": [
{
"files": ["*.css"],
"options": {
"useTabs": false
}
}
]
}
63 changes: 63 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"typescript",
"react",
"jsx-a11y"
],
"jsPlugins": [
"./scripts/oxlint-boundaries.mjs",
"oxlint-tailwindcss"
],
"categories": {
"correctness": "error"
},
"rules": {
"strokes-boundaries/no-platform-in-domain": "error",
"typescript/no-explicit-any": "error",
"typescript/no-non-null-assertion": "error",
"typescript/consistent-type-imports": "error",
"tailwindcss/no-conflicting-classes": "error",
"tailwindcss/no-unknown-classes": "error",
"tailwindcss/no-dynamic-classes": "error",
"tailwindcss/no-duplicate-classes": "error",
"tailwindcss/no-deprecated-classes": "error"
},
"overrides": [
{
"files": [
"src/**/domain/**/*.{ts,tsx}",
"src/pages/**/*.{ts,tsx}",
".fence-fixtures-*/domain/*.ts"
],
"rules": {
"no-restricted-globals": [
"error",
"window",
"document",
"navigator",
"localStorage",
"sessionStorage",
"fetch",
"PointerEvent",
"ResizeObserver",
"require"
]
}
}
],
"ignorePatterns": [
"node_modules",
".agents",
".claude",
".void",
"dist",
"test-results",
"playwright-report"
],
"settings": {
"tailwindcss": {
"entryPoint": "src/styles.css"
}
}
}
3 changes: 3 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["oxc.oxc-vscode"]
}
9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"biome.enabled": false,
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "never"
}
}
Loading
Loading