Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ db 276,352
and for wireframes `screen`, `button`, `input`, `card`, `list`, `image`.
- `note: ...` under the title is a note for the whole diagram, and `id note: ...` a note for one
shape, one line each: instructions for whoever builds from it, person or agent.
- `id color: red` colours a shape or pen stroke: `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `violet`, `pink` or `grey`.
- `a -> b : label` connects two nodes. The label is optional, and a line may refer to a node
defined further down. `a --> b` is dashed, `a <-> b` has an arrow at each end, and `a <--> b`
is both.
Expand Down
8 changes: 8 additions & 0 deletions backlog/milestones/m-5 - m12-draw-by-hand,-cleanly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
id: m-5
title: 'M12 Draw by hand, cleanly'
---

## Description

Milestone: M12 Draw by hand, cleanly
42 changes: 42 additions & 0 deletions backlog/tasks/fl-92 - Colours-for-shapes-and-strokes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
id: FL-92
title: Colours for shapes and strokes
status: Done
assignee:
- '@raj-khan'
created_date: '2026-09-30 13:40'
updated_date: '2026-09-30 13:52'
labels:
- feature
milestone: m-5
dependencies: []
priority: high
type: feature
ordinal: 22000
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->

Every shape is drawn in its kind's accent today, and every pen stroke in ink. Let people colour them, as Excalidraw does, from a small named palette that reads in light and dark themes. The colour is a name, not a hex, so the .flow text stays readable and an agent can use colour to mean something (red for what is failing).
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria

<!-- AC:BEGIN -->

- [x] #1 A palette of named colours (red, orange, yellow, green, teal, blue, violet, pink, grey) with light and dark values; a coloured shape strokes in the colour and fills with its soft tint
- [x] #2 Selecting one or more shapes shows the swatches; picking one colours them all in one undoable change, and a Default swatch clears it
- [x] #3 The pen has its own colour, shown while the pen is on, used for every new stroke
- [x] #4 .flow writes and reads 'id color: red'; an unknown colour is an error naming the choices
- [x] #5 SVG and PNG export draw the colours; Excalidraw import and export map strokeColor and backgroundColor to the nearest named colour

<!-- AC:END -->

## Final Summary

<!-- SECTION:FINAL_SUMMARY:BEGIN -->

Named palette in src/domain/colors.js (Open Color values, light and dark), stored as data.color and written as 'id color: red' in .flow. Selection toolbar shows swatches for one or more shapes; the pen has its own remembered colour. SVG export and Excalidraw import/export carry colours; format docs, the skill and the draft prompt teach the line.
<!-- SECTION:FINAL_SUMMARY:END -->
34 changes: 34 additions & 0 deletions backlog/tasks/fl-93 - Auto-shapes-a-drawn-box-becomes-a-box.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
id: FL-93
title: 'Auto shapes: a drawn box becomes a box'
status: To Do
assignee:
- '@raj-khan'
created_date: '2026-09-30 13:40'
labels:
- feature
milestone: m-5
dependencies:
- FL-92
priority: high
type: feature
ordinal: 23000
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->

A pen stroke that is plainly a rectangle, an ellipse or a diamond turns into that shape, as in Procreate's QuickShape or Google Keep, so sketching a diagram by hand ends in a diagram an agent reads exactly. Recognised by geometry in src/domain (closedness, corners of the simplified stroke, how much of its box it fills), with no model and no new dependency.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria

<!-- AC:BEGIN -->

- [ ] #1 src/domain/recognize.js classifies a stroke as line, arrow, rectangle, ellipse, diamond or nothing, and is tested on drawn samples including wobbly and unclosed ones
- [ ] #2 With auto shapes on, a closed rectangle becomes a process, an ellipse a terminal and a diamond a decision, at the stroke's box, in the stroke's colour, with its title ready to type
- [ ] #3 One undo brings back the stroke as drawn; a toast offers Keep as drawn
- [ ] #4 Auto shapes is a toggle next to the pen colour, on by default, remembered; scribbles and handwriting stay ink

<!-- AC:END -->
34 changes: 34 additions & 0 deletions backlog/tasks/fl-94 - Drawn-arrows-become-connections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
id: FL-94
title: Drawn arrows become connections
status: To Do
assignee:
- '@raj-khan'
created_date: '2026-09-30 13:40'
labels:
- feature
milestone: m-5
dependencies:
- FL-93
priority: high
type: feature
ordinal: 24000
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->

Drawing an arrow between two shapes should connect them, the way someone sketching expects. An arrow from a shape into empty space completes the thought: a new connected shape where it ends. An arrow that touches no shape straightens into a clean arrow stroke.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria

<!-- AC:BEGIN -->

- [ ] #1 A line or arrow drawn from one shape to another becomes a connection between them, in the drawn direction; a head at both ends connects both ways
- [ ] #2 An arrow from a shape into empty space adds a shape of the same kind at its end, connected, with its title ready to type
- [ ] #3 An arrow or line that touches no shape becomes a straight stroke; an arrow keeps a clean head, written in .flow as 'id arrow: end'
- [ ] #4 Each is one undoable change, with Keep as drawn in the toast

<!-- AC:END -->
5 changes: 5 additions & 0 deletions docs/format.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ <h2>Every rule</h2>
<code>id note: ...</code> a note for one shape, one line each: instructions for whoever
builds from it, person or agent.
</li>
<li>
<code>id color: red</code> colours a shape or pen stroke: <code>red</code>,
<code>orange</code>, <code>yellow</code>, <code>green</code>, <code>teal</code>,
<code>blue</code>, <code>violet</code>, <code>pink</code> or <code>grey</code>.
</li>
<li>
<code>a -&gt; b : label</code> connects two nodes. The label is optional, and a line may
refer to a node defined further down. <code>a --&gt; b</code> is dashed,
Expand Down
2 changes: 2 additions & 0 deletions docs/llms-full.template.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ db 276,352
- `note: ...` under the title is a note for the whole diagram, and
`id note: ...` a note for one shape, one line each: instructions for whoever
builds from it, person or agent.
- `id color: red` colours a shape or pen stroke, one of `red`, `orange`,
`yellow`, `green`, `teal`, `blue`, `violet`, `pink` or `grey`.
- `a -> b : label` connects two nodes; the label is optional, and a line may
refer to a node defined further down. `a --> b` is dashed, `a <-> b` has an
arrow at each end, and `a <--> b` is both.
Expand Down
2 changes: 1 addition & 1 deletion e2e/arrange.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { history } from './helpers.js'

const nodeAt = (page, id) => page.locator(`.vue-flow__node[data-id="${id}"]`)
const NODE = { start: '1', away: 'b6a0c1', welcome: 'b0653a', comment: 'e879e4' }
const toolbar = (page) => page.getByRole('toolbar', { name: 'Arrange the selection' })
const toolbar = (page) => page.getByRole('toolbar', { name: 'Selection' })

test.beforeEach(async ({ page }) => {
await page.goto('/new')
Expand Down
75 changes: 75 additions & 0 deletions e2e/colors.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { expect, test } from '@playwright/test'

import { history } from './helpers.js'

const saved = (page) => page.evaluate(() => JSON.parse(localStorage.getItem('flow:document')))
const colors = async (page) =>
Object.fromEntries((await saved(page)).nodes.map((node) => [node.id, node.data?.color ?? '']))

const DOC = {
version: 3,
title: 'Shop',
nodes: [
{ id: 'api', type: 'process', name: 'API', data: {}, position: { x: 0, y: 0 } },
{ id: 'db', type: 'database', name: 'Orders', data: {}, position: { x: 320, y: 0 } },
{ id: 'web', type: 'screen', name: 'Web', data: {}, position: { x: 0, y: 240 } },
],
edges: [],
}
const node = (page, id) => page.locator(`.vue-flow__node[data-id="${id}"]`)
const toolbar = (page) => page.getByRole('toolbar', { name: 'Selection' })

test.beforeEach(async ({ page }) => {
await page.goto('/new')
await page.evaluate((doc) => localStorage.setItem('flow:document', JSON.stringify(doc)), DOC)
await page.reload()
await expect(node(page, 'web')).toBeVisible()
})

test('colours the selected shapes in one undoable change, and Default clears it', async ({
page,
}) => {
await node(page, 'api').click({ modifiers: ['Shift'] })
await node(page, 'db').click({ modifiers: ['Shift'] })
const colour = toolbar(page).getByRole('radiogroup', { name: 'Colour' })
await expect(colour.getByRole('radio', { name: 'Default' })).toHaveAttribute(
'aria-checked',
'true',
)

await colour.getByRole('radio', { name: 'Blue' }).click()
await expect.poll(() => colors(page)).toEqual({ api: 'blue', db: 'blue', web: '' })
await expect(node(page, 'api').locator('svg[data-color="blue"]')).toHaveCount(1)
await expect(colour.getByRole('radio', { name: 'Blue' })).toHaveAttribute('aria-checked', 'true')

await history(page).getByRole('button', { name: 'Undo' }).click()
await expect.poll(() => colors(page)).toEqual({ api: '', db: '', web: '' })

await history(page).getByRole('button', { name: 'Redo' }).click()
await expect.poll(() => colors(page)).toEqual({ api: 'blue', db: 'blue', web: '' })
await colour.getByRole('radio', { name: 'Default' }).click()
await expect.poll(() => colors(page)).toEqual({ api: '', db: '', web: '' })
})

test('the pen draws in its own colour, remembered', async ({ page }) => {
await page.getByRole('button', { name: 'Pen', exact: true }).click()
const pen = page.getByRole('toolbar', { name: 'Pen' })
await pen.getByRole('radio', { name: 'Red' }).click()

const layer = await page.getByTestId('pen-layer').boundingBox()
const x = layer.x + layer.width - 260
const y = layer.y + 160
await page.mouse.move(x, y)
await page.mouse.down()
for (let step = 1; step <= 12; step += 1) {
await page.mouse.move(x + step * 12, y + Math.sin(step / 2) * 30)
}
await page.mouse.up()

await expect.poll(async () => (await colors(page))['ink-1']).toBe('red')
await page.reload()
await page.getByRole('button', { name: 'Pen', exact: true }).click()
await expect(
page.getByRole('toolbar', { name: 'Pen' }).getByRole('radio', { name: 'Red' }),
).toHaveAttribute('aria-checked', 'true')
})
Loading
Loading