Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
83 changes: 83 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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/<addon>.ts → entry file (thin re-export)
src/<addon>/ → implementation + tests
package.json exports → ./<addon> map
dist/alpineflow-<addon>.esm.js → built output
```

Currently shipped: `whiteboard`, `collab`, `dagre`, `force`, `hierarchy`, `elk`, `workflow`, `schema`.

Adding a new addon means: creating `src/<name>/` with index.ts + tests, adding `lib/<name>.ts`, extending `package.json` exports + the `build:addons` script, and adding a doc page at `docs/addons/<name>.md`.

All addons use the `registerAddon('<name>', { 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/<kebab-topic>` 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:<command>` 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
52 changes: 52 additions & 0 deletions docs/addons/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<x-flow :nodes="$nodes" :edges="$edges">
<x-slot:node>
<template x-if="node.type === 'flow-wait'">
<div x-flow-wait class="flow-wait-node"></div>
</template>
</x-slot:node>
</x-flow>
```

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 --}}
<template x-if="node.type === 'flow-wait'">
<div x-flow-wait></div>
</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).
Expand Down
27 changes: 27 additions & 0 deletions src/plugin/directives/flow-condition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
9 changes: 9 additions & 0 deletions src/schema/inspector/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down