diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be214d..7bc3b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -229,6 +229,18 @@ Workflow addon — lazy canvas resolution. - `flowRunButton`, `flowStopButton`, `flowResetButton`, `flowReplayControls`, and `flowExecutionLog` Alpine.data factories now lazy re-resolve their cached `_canvas` reference if a required canvas method is missing at click/getter time. Previously, when a button was rendered as a sibling/ancestor of the `.flow-container` (DOM-order: button before canvas), the button's `init()` cached a stale empty proxy from `Alpine.$data()` and click-time method calls threw `TypeError: this._canvas.run is not a function`. The factories are now tolerant of arbitrary DOM order. - `flowExecutionLog.filteredEvents` reads from the canvas at getter time instead of caching the source array reference at init — the visible log now stays in sync with the canvas's `executionLog`/sourceExpr regardless of mount order. +--- + +Audit follow-ups — docs, tests, and internal notes. + +### Docs +- `docs/addons/workflow.md` — added an `x-flow-wait` directive section (sibling to the existing `x-flow-condition` directive section). Documents the rendered DOM, the `node.data` shape (`durationMs`, `label`, `icon`), the duration-format table, and the pairing with `flow-wait` workflow nodes. +- `CLAUDE.md` — committed the repo-internal working notes (tech stack, build/test commands, branching rules) for human and AI contributors working inside the package. + +### Internal +- `src/plugin/directives/flow-condition.test.ts` — added five error-path tests covering invalid `_branchTaken` values, missing/non-object `condition` shapes, and garbage direction expressions. +- `src/schema/inspector/shared.ts` — `findCanvasScope()` JSDoc now documents the cached-at-init assumption with a forward reference to the workflow addon's `ensureCanvas` pattern for future portal/tab-switched inspector placements. + ## v0.1.2-alpha — 2026-04-03 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..79d5a8b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,83 @@ +# AlpineFlow — Internal Working Notes + +These are notes for developers (human or AI) working **inside** this repo. The public API is documented in `docs/` and on https://artisanflow.dev. This file is committed to the public repository — keep it factual about how the package is built and tested, not about downstream/consumer-site workflows. + +## Tech stack + +- **TypeScript** (strict) +- **Vite** for the bundler (three configs: `vite.config.ts`, `vite.bundle.config.ts`, `vite.addons.config.ts`) +- **Vitest** for testing — jsdom for unit tests (`vitest.config.ts`), Playwright for browser tests (`vitest.browser.config.ts`) +- **No** ESLint / Prettier / Biome — formatting is managed by Vite + tsc, plus convention. Match sibling-file style. + +## Build + +```bash +npm run build # full pipeline: vite build && tsc && build:css && build:bundle && build:addons +npm run build:bundle # core bundle only +npm run build:addons # all 8 subpath addons (whiteboard, dagre, force, hierarchy, elk, collab, workflow, schema) +npm run build:css # copy structural + theme CSS into dist/ +``` + +The full `build` is the source of truth — always run it before claiming a change is shipped. Dist files in `dist/` are committed (downstream consumers symlink/copy this directory). + +After every build, verify the new symbols are actually in the bundle (`grep` the output, don't trust the build to be silent on tree-shaking elimination). + +## Test + +```bash +npm run test # vitest run — fast, jsdom, runs everything in src/**/*.test.ts +npm run test:watch # vitest interactive +npm run test:browser # Playwright-driven browser tests +npm run test:all # both +npm run bench # vitest bench --run +``` + +**Every change requires test coverage.** Pure logic → vitest. DOM-touching directives → vitest with jsdom. End-to-end UX → browser test. + +Filter to a single file when iterating: `npm run test -- src/workflow/run.test.ts`. + +## Subpath addons + +``` +lib/.ts → entry file (thin re-export) +src// → implementation + tests +package.json exports → ./ map +dist/alpineflow-.esm.js → built output +``` + +Currently shipped: `whiteboard`, `collab`, `dagre`, `force`, `hierarchy`, `elk`, `workflow`, `schema`. + +Adding a new addon means: creating `src//` with index.ts + tests, adding `lib/.ts`, extending `package.json` exports + the `build:addons` script, and adding a doc page at `docs/addons/.md`. + +All addons use the `registerAddon('', { setup(canvas) { … } })` pattern from `src/core/registry.ts` to attach methods/state onto canvas instances. + +## Theming & CSS + +- Structural rules live in `css/structural.css` — pure layout, no colors +- Theme defaults live in `css/theme-default.css` — uses CSS custom properties +- **Reuse existing tokens** (`--flow-node-bg`, `--flow-handle-color`, `--flow-text-muted`, etc.). Do not add new CSS variables without explicit owner approval — every variable becomes part of the theming contract for consumers +- Both files are copied into `dist/` by `build:css` + +## Branching & releases + +- **`dev`** is the integration branch +- **Feature workflow:** cut a `feature/` branch off `dev`, do the work there, open a PR back to `dev` once tests pass. The PR gets reviewed and merged into `dev` (typically by the owner). Don't push commits directly to `dev` +- **`main`** mirrors the latest tagged release. **Never push to main directly. Never merge dev → main without owner approval** +- **Never tag a version**. Tags are cut by the owner after manual verification across alpineflow + wireflow + the consuming site +- **CHANGELOG.md** is append-only. Group entries under the current alpha version block; keep `### Added` / `### Changed (alpha-breaking)` / `### Fixed` / `### Docs` subsections consistent with prior entries + +## Conventions + +- Strict TypeScript — explicit return types on exported functions, no implicit `any` +- Tests colocated next to source (`src/foo/bar.ts` → `src/foo/bar.test.ts`) +- Match sibling-file style (imports order, comment density, error handling) +- Docstrings on exported APIs; sparse inline comments — only when the *why* is non-obvious +- No new dependencies (`dependencies` or `peerDependencies`) without owner approval +- New canvas methods that need to be server-callable from Livewire/wireflow must include a matching `flow:` listener in `src/core/wire-bridge.ts` so wireflow's `WithWireFlow` trait can dispatch them + +## Don't + +- Don't bypass the build (`tsc --noEmit` is fine for typecheck during dev, but the dist is what consumers use — always run the full `npm run build` before claiming done) +- Don't skip tests with `it.skip` / `it.only` in committed code +- Don't add a workaround in a consumer when the right fix is in core/addon +- Don't change the dependency graph or peer-dependency requirements without checking the impact on downstream wireflow + artisanflow diff --git a/docs/addons/workflow.md b/docs/addons/workflow.md index 277c03b..6620b24 100644 --- a/docs/addons/workflow.md +++ b/docs/addons/workflow.md @@ -303,6 +303,58 @@ In WireFlow Blade templates, use from Alpine scope: Server-side `$this->flowSetNodeState()` and `$this->flowResetStates()` complement the addon for server-driven state pushes. The addon's `$flow.run()` handles client-side orchestration. +## Wait node template — `x-flow-wait` directive + +Renders a workflow wait node with a header (optional icon, label, formatted duration), a top target handle, and a bottom source handle. + +```blade + + + + + +``` + +Reads from `node.data`: + +- `durationMs?: number` — wait duration in milliseconds; rendered as a compact human-readable string +- `label?: string` — header label; defaults to `Wait` +- `icon?: string` — optional emoji or single character placed before the label + +The directive owns the element's children entirely. Consumers who want a fully custom wait-node render should skip `x-flow-wait` and stamp their own DOM. + +### Duration formatting + +| Range | Output | Example | +| --- | --- | --- | +| `< 1s` | `{n}ms` | `500ms` | +| `< 1m`, integer seconds | `{n}s` | `3s` | +| `< 1m`, fractional | `{n.n}s` | `2.5s` | +| `≥ 1m`, exact minutes | `{n}m` | `2m` | +| `≥ 1m`, with seconds | `{n}m {s}s` | `1m 30s` | + +Non-numeric or negative values render an empty duration string. textContent only — no innerHTML anywhere. + +### Pairs with `flow-wait` workflow nodes + +This directive renders the visual node; the workflow engine handles execution timing. Together they form the full surface for wait steps: + +```js +// Data side: workflow engine pauses for durationMs without firing onEnter/onExit +{ id: 'cooldown', type: 'flow-wait', data: { durationMs: 2000, label: 'Cool-down' } } +``` + +```blade +{{-- View side: the directive renders the matching node template --}} + +``` + +`validateWorkflow()` flags wait nodes with non-numeric or missing `data.durationMs` via the `wait-missing-duration` issue code. + ## Condition node template — `x-flow-condition` directive Renders a workflow condition node with a header, a pretty-printed expression body, and three handles (target + true/false sources). diff --git a/src/plugin/directives/flow-condition.test.ts b/src/plugin/directives/flow-condition.test.ts index f5b87c4..cac3a6d 100644 --- a/src/plugin/directives/flow-condition.test.ts +++ b/src/plugin/directives/flow-condition.test.ts @@ -109,4 +109,31 @@ describe('x-flow-condition directive', () => { const el = mount({ condition: { field: 'x', op: 'equals', value: 1 } }); expect(el.hasAttribute('data-flow-condition-branch-taken')).toBe(false); }); + + it('ignores invalid _branchTaken values (anything other than "true"/"false")', () => { + const el = mount({ + condition: { field: 'x', op: 'equals', value: 1 }, + _branchTaken: 'maybe', + }); + expect(el.hasAttribute('data-flow-condition-branch-taken')).toBe(false); + }); + + it('renders empty body when neither condition nor evaluate is set', () => { + const el = mount({ label: 'Empty' }); + expect(el.querySelector('.flow-condition-body')?.textContent).toBe(''); + }); + + it('renders empty body when condition is a non-object value', () => { + // String/number/null in condition slot must not blow up prettyPrintCondition. + const el = mount({ condition: 'not-an-object' as unknown as object }); + expect(el.querySelector('.flow-condition-body')?.textContent).toBe(''); + }); + + it('falls back to default horizontal direction when expression evaluates to garbage', () => { + // The directive expression here is a literal string that doesn't parse as + // 'horizontal' or 'vertical'. Should fall through to the data fallback, + // then to the 'horizontal' default. + const el = mount({ condition: { field: 'x', op: 'equals', value: 1 } }, "'sideways'"); + expect(el.getAttribute('data-flow-condition-direction')).toBe('horizontal'); + }); }); diff --git a/src/schema/inspector/shared.ts b/src/schema/inspector/shared.ts index e3202d7..593f7aa 100644 --- a/src/schema/inspector/shared.ts +++ b/src/schema/inspector/shared.ts @@ -56,6 +56,15 @@ export function parseRowId( * 3. Multiple canvases without an ancestor match — log a one-shot warning * and bail out; the caller must nest the inspector inside the target * canvas (an explicit selector prop is on the v0.2.2 roadmap). + * + * Inspector directives currently call this once at init and close over the + * returned scope. That works because inspectors render inside or alongside an + * already-initialized canvas in every shipped pattern. If a future caller + * mounts an inspector before its canvas's Alpine setup runs (e.g. portal- + * teleported or tab-switched), the cached `Alpine.$data()` proxy can go stale + * and method calls will throw — mirror the workflow addon's `ensureCanvas` + * pattern (re-resolve at method-call time) before placing inspectors in such + * positions. */ export function findCanvasScope( Alpine: Alpine,