feat(editor): scaffold editor package

UI-only feature package for the ADR-029 editor rebuild: tsconfig
rootDir '.', vitest jsdom base with '@' alias and honest L0 baseline
thresholds, conformance ESLint active, React 19, @xyflow/react +
zustand wired against the pre-approved 2026-07-12 library traces.
Minimal feature.manifest.ts (empty useCases — no binders; declares
requiredCores runner-protocol + baseline coverage band) so the editor
participates in pnpm conformance without weakening any lint rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 22:00:34 +02:00
parent e9edad69b4
commit c42eca5b80
12 changed files with 527 additions and 1 deletions

21
packages/editor/AGENTS.md Normal file
View File

@@ -0,0 +1,21 @@
# @repo/editor
Owns the rebuilt Veect editor UI (ADR-029): the React Flow board shell, the iframe frame node that hosts the runner's preview-adapter origin, the editor-side selection overlay, the canvas-protocol client (pinned-`targetOrigin` postMessage both directions, ADR-028), and the minimal zustand store (registry + selection only — DesignDoc v1 arrives with the `design-doc-and-editor-foundation` epic).
**Boundary tag:** feature. May import from core (`core-runner-protocol`, `core-shared`), feature, and tooling packages. Imported by apps (`apps/web-next` hosts the board from story 10).
**Public surface:**
- `.` (root) — contracts only: canvas-protocol schemas + types, the feature manifest.
- `./ui` — UI artifacts: board, frame node, selection overlay components, the editor store hook.
**Conventions:**
- UI-only feature package — `useCases` is empty in `src/feature.manifest.ts`; there are no DI binders. Manifest-first ordering applies the moment a use case appears.
- Every component in `src/ui/` has sibling `.stories.tsx` + `.test.tsx` files (`component-must-have-story` / `component-must-have-test`) — no exemption zone (ADR-029).
- Chrome (selection/hover rings) renders as editor-side overlays positioned from agent-reported geometry — never inside the iframe document (ADR-028).
- Board pan/zoom and frame drag are pure React Flow transforms — no canvas-protocol round-trip on drag (<16 ms/frame budget).
- The canvas-protocol message shapes agent editor are local zod schemas in `src/canvas-protocol/` for now; they reconcile with the adapter's agent script (walking-skeleton story 05) and `@repo/core-runner-protocol` in story 10. `render-frame` already comes from `@repo/core-runner-protocol`.
- The prototype under `docs/product/reference/project/veect-codebase/` is reference only never vendored (ADR-029).
**See:** `docs/decisions/adr-028-iframe-canvas.md`, `docs/decisions/adr-029-designdoc-v1-and-editor-rebuild.md`, `docs/work/epics/walking-skeleton/09-editor-package/_story.md`.

View File

@@ -0,0 +1,3 @@
import baseConfig from "@repo/core-eslint/base";
export default baseConfig;

View File

@@ -0,0 +1,40 @@
{
"name": "@repo/editor",
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
".": "./src/index.ts",
"./ui": "./src/ui/index.ts"
},
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"test": "vitest run --passWithNoTests",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/core-runner-protocol": "workspace:*",
"@repo/core-shared": "workspace:*",
"@xyflow/react": "^12.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^3.24.0",
"zustand": "^5.0.0"
},
"devDependencies": {
"@repo/core-eslint": "workspace:*",
"@repo/core-testing": "workspace:*",
"@repo/core-typescript": "workspace:*",
"@storybook/react": "^8.6.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitest/coverage-v8": "^3.2.4",
"jsdom": "^25.0.0",
"typescript": "^5.8.0",
"vitest": "^3.0.0"
}
}

View File

@@ -0,0 +1,40 @@
import { describe, it, expect } from "vitest";
import {
DEFAULT_COVERAGE_BANDS,
getCoverageBands,
} from "@repo/core-shared/conformance";
import { editorManifest } from "@/feature.manifest";
describe("editorManifest", () => {
it("declares the editor feature name", () => {
expect(editorManifest.name).toBe("editor");
});
it("declares no use cases — the editor is a UI-only feature (ADR-029)", () => {
expect(editorManifest.useCases).toEqual({});
});
it("requires the runner-protocol core (render-frame schema source)", () => {
expect(editorManifest.requiredCores).toEqual(["runner-protocol"]);
});
it("declares no realtime channels and no jobs", () => {
expect(editorManifest.realtimeChannels).toEqual([]);
expect(editorManifest.jobs).toEqual([]);
});
it("declares the default baseline coverage band — drift guard against vitest.config.ts, which inherits the same numbers from nodeVitestConfig", () => {
expect(editorManifest.coverage.bands.baseline).toEqual(
DEFAULT_COVERAGE_BANDS.baseline,
);
});
it("resolves layer bands from defaults via getCoverageBands (no layer dirs exist yet)", () => {
expect(getCoverageBands(editorManifest)).toEqual({
baseline: DEFAULT_COVERAGE_BANDS.baseline,
entities: DEFAULT_COVERAGE_BANDS.entities,
"use-cases": DEFAULT_COVERAGE_BANDS["use-cases"],
controllers: DEFAULT_COVERAGE_BANDS.controllers,
});
});
});

View File

@@ -0,0 +1,31 @@
import { defineFeature } from "@repo/core-shared/conformance";
/**
* The editor feature's conformance manifest.
*
* The editor is a UI-only feature package (ADR-028/029): it owns the React
* Flow board, the iframe frame node, and the canvas-protocol client — no
* Clean Architecture use cases, so `useCases` is empty and there are no
* DI binders. `feature-must-have-manifest` / `usecase-must-be-wired` only
* fire on use-case files and binders, so the manifest is declarative
* headroom rather than a lint requirement — it exists so the editor
* participates in `pnpm conformance` and declares its coverage bands
* (ADR-020) and core dependency (`core-runner-protocol`) explicitly.
*
* When the editor grows request-path domain logic (e.g. design-doc
* mutations), those use cases land here first — manifest-first ordering.
*/
export const editorManifest = defineFeature({
name: "editor",
requiredCores: ["runner-protocol"],
useCases: {},
realtimeChannels: [],
jobs: [],
coverage: {
bands: {
baseline: { statements: 80, branches: 75, functions: 80, lines: 80 },
},
},
} as const);
export type EditorManifest = typeof editorManifest;

View File

@@ -0,0 +1,4 @@
// Public contract surface of @repo/editor (types, schemas, manifest).
// UI artifacts (board, frame node, overlay, store hook) live behind
// `@repo/editor/ui` — see src/ui/index.ts.
export { editorManifest, type EditorManifest } from "./feature.manifest";

View File

@@ -0,0 +1,5 @@
// Public UI surface of @repo/editor: the React Flow board, iframe frame
// node, selection overlay, and the editor store hook land here as the
// walking-skeleton slices ship. Apps import components/hooks from
// `@repo/editor/ui` and contracts from `@repo/editor`.
export {};

View File

@@ -0,0 +1,13 @@
{
"extends": "@repo/core-typescript/react-library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": ".",
"types": ["vitest/globals", "@testing-library/jest-dom"],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.stories.ts"]
}

View File

@@ -0,0 +1,4 @@
{
"extends": ["//"],
"tags": ["feature"]
}

View File

@@ -0,0 +1,23 @@
import path from "node:path";
import { mergeConfig } from "vitest/config";
import { jsdomVitestConfig } from "@repo/core-typescript/vitest.base.jsdom";
// Coverage thresholds are the honest L0 baseline inherited from
// `nodeVitestConfig` (80/75/80/80 — DEFAULT_COVERAGE_BANDS.baseline, ADR-020).
// The editor has no entities / use-cases / controllers layers yet, so no
// per-layer band globs apply; `feature.manifest.ts` declares the same
// baseline band and `feature.manifest.test.ts` guards against drift.
export default mergeConfig(jsdomVitestConfig, {
resolve: {
alias: { "@": path.resolve(__dirname, "./src") },
},
test: {
coverage: {
exclude: [
// Storybook stories — excluded from vitest by design; exercised by
// the Storybook test runner (`pnpm test:stories`).
"src/**/*.stories.{ts,tsx}",
],
},
},
});