Skip to content

Commit d394a2e

Browse files
alicodingclaude
andauthored
feat: creation is the toolbar -- Table tool + size picker join Card/Note/Area, the + Add menu is retired, placement decides containment (goal 0139) (#293)
Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2Sd Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 18db38b commit d394a2e

25 files changed

Lines changed: 361 additions & 277 deletions

frontend/e2e/atlas-kind-authoring.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { chromium, expect, test } from '@playwright/test'
2+
import { openPlacementPopover } from './fixtures/atlasBoard'
23
import { mkdtempSync, rmSync } from 'node:fs'
34
import { tmpdir } from 'node:os'
45
import path from 'node:path'
@@ -77,9 +78,8 @@ test('create a card kind with a choice field, see it in the create picker, edit
7778
// The picker offers it: close the dialog, arm a card placement.
7879
await page.keyboard.press('Escape')
7980
await expect(dialog(page)).not.toBeVisible()
80-
await page.getByTestId('atlas-add-button').click()
81-
await page.getByTestId('atlas-add-child').click()
82-
await page.getByTestId('atlas-create-kind').click()
81+
await openPlacementPopover(page)
82+
await page.getByTestId('atlas-placement-kind').click()
8383
await expect(page.getByText('ZzE2eSignal', { exact: false }).first()).toBeVisible()
8484
await page.keyboard.press('Escape')
8585
await page.keyboard.press('Escape')

frontend/e2e/atlas-page-edit.spec.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {
99
type SpawnedServer,
1010
} from './fixtures/server'
1111
import { deleteViaPageMenu } from './fixtures/atlasPage'
12-
import { ATLAS_KIND_TOPIC, selectKind } from './fixtures/kindPicker'
13-
import { clickBreadcrumbSegment, clickFrameGutter, groupCard, noteCard, openCard } from './fixtures/atlasBoard'
12+
import { ATLAS_KIND_TOPIC } from './fixtures/kindPicker'
13+
import { clickBreadcrumbSegment, clickFrameGutter, groupCard, noteCard, openCard, createCardViaTray, openPlacementPopover } from './fixtures/atlasBoard'
1414

1515
// Atlas card page read-is-edit + chip navigation (goal 0081 slice A5,
1616
// LOCKED design §5b): every field editable in place with a per-save
@@ -243,11 +243,7 @@ test('atlas card page: read-is-edit fields, kind-gated mirror controls, page lin
243243
// card, delete it, confirm it's gone. ---
244244
await clickBreadcrumbSegment(page, page.getByTestId('atlas-breadcrumb').getByText('My space', { exact: true }), 'My space')
245245
const title = 'ZzE2eAtlasPageEditDelete'
246-
await page.getByTestId('atlas-add-button').click()
247-
await page.getByTestId('atlas-add-child').click()
248-
await selectKind(page, ATLAS_KIND_TOPIC, 'atlas-create-kind')
249-
await page.getByTestId('atlas-create-title').fill(title)
250-
await page.getByRole('button', { name: 'Create' }).click()
246+
await createCardViaTray(page, title, { kindID: ATLAS_KIND_TOPIC })
251247
const throwaway = noteCard(page, title)
252248
await expect(throwaway).toBeVisible()
253249
await openCard(page, throwaway)
@@ -259,15 +255,11 @@ test('atlas card page: read-is-edit fields, kind-gated mirror controls, page lin
259255
// --- Rider (c): AtlasCreateMenu's kind picker (its own legacy
260256
// native <select>) is now the shared KindPicker -- every option
261257
// shows its own one-line description, not just a bare name. ---
262-
await page.getByTestId('atlas-add-button').click()
263-
await page.getByTestId('atlas-add-child').click()
264-
await page.getByTestId('atlas-create-kind').click()
265-
await expect(page.getByTestId(`atlas-create-kind-option-${ATLAS_KIND_TOPIC}`)).toContainText('Something being tracked or worked through.')
266-
// Closed by clicking Cancel directly (registers as an outside
267-
// click for KindPicker's own dropdown too), not Escape -- the
268-
// picker's hand-rolled Escape listener doesn't stop propagation,
269-
// so it would also close the dialog underneath it.
270-
await page.getByRole('button', { name: 'Cancel' }).click()
258+
await openPlacementPopover(page)
259+
await page.getByTestId('atlas-placement-kind').click()
260+
await expect(page.getByTestId(`atlas-placement-kind-option-${ATLAS_KIND_TOPIC}`)).toContainText('Something being tracked or worked through.')
261+
await page.keyboard.press('Escape')
262+
await page.keyboard.press('Escape')
271263
} finally {
272264
await server?.stop()
273265
rmSync(dir, { recursive: true, force: true })

frontend/e2e/atlas-perspectives.spec.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
type SpawnedServer,
1010
} from './fixtures/server'
1111
import { contextMenu } from './fixtures/contextMenu'
12-
import { clickBreadcrumbSegment, groupCard, noteCard, openCard } from './fixtures/atlasBoard'
12+
import { clickBreadcrumbSegment, groupCard, noteCard, openCard, createCardViaTray } from './fixtures/atlasBoard'
1313
import { deleteViaPageMenu } from './fixtures/atlasPage'
14-
import { ATLAS_KIND_TOPIC, selectKind } from './fixtures/kindPicker'
14+
import { ATLAS_KIND_TOPIC } from './fixtures/kindPicker'
1515

1616
// Perspectives (ADR-0041, goal 0095): the switcher (which absorbed the
1717
// old Lens popover), board membership filtering incl. the ancestry-
@@ -174,11 +174,7 @@ test('authoring a card while a perspective is active adds it automatically; the
174174
await createPerspective(page, 'Authoring')
175175

176176
const title = 'ZzE2ePerspectiveAuthored'
177-
await page.getByTestId('atlas-add-button').click()
178-
await page.getByTestId('atlas-add-child').click()
179-
await selectKind(page, ATLAS_KIND_TOPIC, 'atlas-create-kind')
180-
await page.getByTestId('atlas-create-title').fill(title)
181-
await page.getByRole('button', { name: 'Create' }).click()
177+
await createCardViaTray(page, title, { kindID: ATLAS_KIND_TOPIC })
182178

183179
// Authoring-adds-to-active (server-side, keyed off the session's
184180
// own ActivePerspectiveID) -- the new card renders immediately on
@@ -266,11 +262,7 @@ test('Compare shows the diff between two user-authored perspectives', async ({},
266262
const oldTitle = 'ZzE2eCompareOld'
267263
const newTitle = 'ZzE2eCompareNew'
268264
for (const title of [oldTitle, newTitle]) {
269-
await page.getByTestId('atlas-add-button').click()
270-
await page.getByTestId('atlas-add-child').click()
271-
await selectKind(page, ATLAS_KIND_TOPIC, 'atlas-create-kind')
272-
await page.getByTestId('atlas-create-title').fill(title)
273-
await page.getByRole('button', { name: 'Create' }).click()
265+
await createCardViaTray(page, title, { kindID: ATLAS_KIND_TOPIC })
274266
await expect(noteCard(page, title)).toBeVisible()
275267
}
276268

frontend/e2e/atlas-projections.spec.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { test, expect } from './fixtures/server'
22
import { deleteViaPageMenu } from './fixtures/atlasPage'
3-
import { ATLAS_KIND_DOCUMENT, selectKind } from './fixtures/kindPicker'
4-
import { openCard } from './fixtures/atlasBoard'
3+
import { ATLAS_KIND_DOCUMENT } from './fixtures/kindPicker'
4+
import { openCard, createCardViaTray } from './fixtures/atlasBoard'
55

66
// Atlas projections (docs/goals/0064, ADR-0038): mirror-content
77
// rendering, the traceability matrix, and coverage -- each proven
@@ -36,11 +36,7 @@ test('a card with a Mirror path pointing at a markdown file renders its content
3636
await page.getByRole('link', { name: 'Atlas' }).click()
3737
await expect(page.getByTestId('atlas-board')).toBeVisible()
3838

39-
await page.getByTestId('atlas-add-button').click()
40-
await page.getByTestId('atlas-add-child').click()
41-
await selectKind(page, ATLAS_KIND_DOCUMENT, 'atlas-create-kind')
42-
await page.getByTestId('atlas-create-title').fill(title)
43-
await page.getByRole('button', { name: 'Create' }).click()
39+
await createCardViaTray(page, title, { kindID: ATLAS_KIND_DOCUMENT })
4440

4541
const newCard = noteCard(page, title)
4642
await expect(newCard).toBeVisible()

frontend/e2e/atlas-recognized-sources.spec.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { test, expect } from './fixtures/server'
22
import { deleteViaPageMenu } from './fixtures/atlasPage'
3-
import { ATLAS_KIND_DOCUMENT, selectKind } from './fixtures/kindPicker'
4-
import { openCard } from './fixtures/atlasBoard'
3+
import { ATLAS_KIND_DOCUMENT } from './fixtures/kindPicker'
4+
import { openCard, createCardViaTray } from './fixtures/atlasBoard'
55
import { noteCard } from './fixtures/atlasCards'
66

77
// Recognized sources (goal 0126): a card whose Source host matches a
@@ -19,11 +19,7 @@ test('a Source matching a configured Integration offers its declared workflows',
1919
await page.getByRole('link', { name: 'Atlas' }).click()
2020
await expect(page.getByTestId('atlas-board')).toBeVisible()
2121

22-
await page.getByTestId('atlas-add-button').click()
23-
await page.getByTestId('atlas-add-child').click()
24-
await selectKind(page, ATLAS_KIND_DOCUMENT, 'atlas-create-kind')
25-
await page.getByTestId('atlas-create-title').fill(title)
26-
await page.getByRole('button', { name: 'Create' }).click()
22+
await createCardViaTray(page, title, { kindID: ATLAS_KIND_DOCUMENT })
2723

2824
const card = noteCard(page, title)
2925
await expect(card).toBeVisible()

frontend/e2e/atlas-table-projection.spec.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ test('a table card projects a List live on the board and its page', async ({ pag
2020
await page.getByRole('link', { name: 'Atlas' }).click()
2121
await expect(page.getByTestId('atlas-board')).toBeVisible()
2222

23-
await page.getByTestId('atlas-add-button').click()
24-
await page.getByTestId('atlas-add-table').click()
23+
await page.getByTestId('atlas-tray-table').click()
24+
await page.getByTestId('atlas-table-from-list').click()
2525
await page.getByTestId('entity-ref-field').selectOption({ label: 'Example: Country codes' })
2626
// Picking the List prefilled the title with its label.
2727
await expect(page.getByTestId('atlas-create-title')).toHaveValue('Example: Country codes')
@@ -55,8 +55,8 @@ test('a table card projects a List live on the board and its page', async ({ pag
5555
test('auto-arrange keeps the table face at its real footprint', async ({ page }) => {
5656
await page.goto('/')
5757
await page.getByRole('link', { name: 'Atlas' }).click()
58-
await page.getByTestId('atlas-add-button').click()
59-
await page.getByTestId('atlas-add-table').click()
58+
await page.getByTestId('atlas-tray-table').click()
59+
await page.getByTestId('atlas-table-from-list').click()
6060
await page.getByTestId('entity-ref-field').selectOption({ label: 'Example: Country codes' })
6161
await selectKind(page, ATLAS_KIND_DOCUMENT, 'atlas-create-kind')
6262
await page.getByTestId('atlas-create-title').fill('ZzE2eProjectionArrangeCard')
@@ -90,8 +90,8 @@ test('boundary inserts, cell edits, and column rename all work in place on the c
9090
await page.getByTestId('save-list').click()
9191

9292
await page.getByRole('link', { name: 'Atlas' }).click()
93-
await page.getByTestId('atlas-add-button').click()
94-
await page.getByTestId('atlas-add-table').click()
93+
await page.getByTestId('atlas-tray-table').click()
94+
await page.getByTestId('atlas-table-from-list').click()
9595
await page.getByTestId('entity-ref-field').selectOption({ label: 'ZzE2eProjectionEditList' })
9696
await selectKind(page, ATLAS_KIND_DOCUMENT, 'atlas-create-kind')
9797
await page.getByRole('button', { name: 'Create' }).click()
@@ -154,8 +154,8 @@ test('boundary inserts, cell edits, and column rename all work in place on the c
154154
test('an options column renders pills, edits as a select, and the pills density tints rows', async ({ page }) => {
155155
await page.goto('/')
156156
await page.getByRole('link', { name: 'Atlas' }).click()
157-
await page.getByTestId('atlas-add-button').click()
158-
await page.getByTestId('atlas-add-table').click()
157+
await page.getByTestId('atlas-tray-table').click()
158+
await page.getByTestId('atlas-table-from-list').click()
159159
await page.getByTestId('entity-ref-field').selectOption({ label: 'Example: Task tracker' })
160160
await selectKind(page, ATLAS_KIND_DOCUMENT, 'atlas-create-kind')
161161
await page.getByTestId('atlas-create-title').fill('ZzE2ePillsCard')
@@ -204,8 +204,7 @@ test('New table creates a sized grid instantly from the size picker', async ({ p
204204
await page.getByRole('link', { name: 'Atlas' }).click()
205205
await expect(page.getByTestId('atlas-board')).toBeVisible()
206206

207-
await page.getByTestId('atlas-add-button').click()
208-
await page.getByTestId('atlas-add-table-new').click()
207+
await page.getByTestId('atlas-tray-table').click()
209208
await expect(page.getByTestId('atlas-table-size-picker')).toBeVisible()
210209
await page.getByTestId('atlas-table-size-3x2').hover()
211210
await expect(page.getByTestId('atlas-table-size-label')).toContainText('3 × 2')
@@ -246,8 +245,8 @@ test('New table creates a sized grid instantly from the size picker', async ({ p
246245
test('a hovered header stacks above the neighboring sticky header', async ({ page }) => {
247246
await page.goto('/')
248247
await page.getByRole('link', { name: 'Atlas' }).click()
249-
await page.getByTestId('atlas-add-button').click()
250-
await page.getByTestId('atlas-add-table').click()
248+
await page.getByTestId('atlas-tray-table').click()
249+
await page.getByTestId('atlas-table-from-list').click()
251250
await page.getByTestId('entity-ref-field').selectOption({ label: 'Example: Country codes' })
252251
await selectKind(page, ATLAS_KIND_DOCUMENT, 'atlas-create-kind')
253252
await page.getByRole('button', { name: 'Create' }).click()
@@ -269,8 +268,8 @@ test('a hovered header stacks above the neighboring sticky header', async ({ pag
269268
test('resizing a table card persists its footprint across reload', async ({ page }) => {
270269
await page.goto('/')
271270
await page.getByRole('link', { name: 'Atlas' }).click()
272-
await page.getByTestId('atlas-add-button').click()
273-
await page.getByTestId('atlas-add-table').click()
271+
await page.getByTestId('atlas-tray-table').click()
272+
await page.getByTestId('atlas-table-from-list').click()
274273
await page.getByTestId('entity-ref-field').selectOption({ label: 'Example: Country codes' })
275274
await selectKind(page, ATLAS_KIND_DOCUMENT, 'atlas-create-kind')
276275
await page.getByRole('button', { name: 'Create' }).click()
@@ -320,8 +319,8 @@ test('resizing a table card persists its footprint across reload', async ({ page
320319
test('the last boundary insert dot is not clipped by the card edge', async ({ page }) => {
321320
await page.goto('/')
322321
await page.getByRole('link', { name: 'Atlas' }).click()
323-
await page.getByTestId('atlas-add-button').click()
324-
await page.getByTestId('atlas-add-table').click()
322+
await page.getByTestId('atlas-tray-table').click()
323+
await page.getByTestId('atlas-table-from-list').click()
325324
await page.getByTestId('entity-ref-field').selectOption({ label: 'Example: Country codes' })
326325
await selectKind(page, ATLAS_KIND_DOCUMENT, 'atlas-create-kind')
327326
await page.getByRole('button', { name: 'Create' }).click()

frontend/e2e/atlas.spec.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { test, expect } from './fixtures/server'
22
import { deleteViaPageMenu } from './fixtures/atlasPage'
33
import { ATLAS_KIND_TOPIC, selectKind } from './fixtures/kindPicker'
4-
import { clickBreadcrumbSegment, openCard } from './fixtures/atlasBoard'
4+
import { clickBreadcrumbSegment, openCard, createCardViaTray } from './fixtures/atlasBoard'
55

66
// Exercises the Atlas surface's one-map board (docs/adr/0038,
77
// goal 0072 slice A: AtlasShelves retired, every level renders through
@@ -108,8 +108,9 @@ test('creating a sibling of the auto-entered root surfaces the "All spaces" meta
108108
// (a sibling of "My space", ParentID "") -- the only path a second
109109
// root card can be created through, and the one that must surface
110110
// the meta level once it exists.
111-
await page.getByTestId('atlas-add-button').click()
112-
await page.getByTestId('atlas-add-sibling').click()
111+
// Element-relative position clear of cards, tray, and minimap.
112+
await page.getByTestId('atlas-board').click({ button: 'right', position: { x: 180, y: 420 } })
113+
await page.getByText('New space…', { exact: true }).click()
113114
await selectKind(page, ATLAS_KIND_TOPIC, 'atlas-create-kind')
114115
await page.getByTestId('atlas-create-title').fill(title)
115116
await page.getByRole('button', { name: 'Create' }).click()
@@ -248,11 +249,7 @@ test('create a child card, edit + persist it via the card page, then delete it',
248249

249250
// Sibling-vs-child is always an explicit choice -- "Add inside this
250251
// card" lands the new card as a CHILD of the currently viewed space.
251-
await page.getByTestId('atlas-add-button').click()
252-
await page.getByTestId('atlas-add-child').click()
253-
await selectKind(page, ATLAS_KIND_TOPIC, 'atlas-create-kind')
254-
await page.getByTestId('atlas-create-title').fill(title)
255-
await page.getByRole('button', { name: 'Create' }).click()
252+
await createCardViaTray(page, title, { kindID: ATLAS_KIND_TOPIC })
256253

257254
const newCard = noteCard(page, title)
258255
await expect(newCard).toBeVisible()
@@ -341,14 +338,12 @@ test('a sibling card created into a Free-mode space lands clear of both leaf not
341338
await exampleArea.getByTestId('atlas-group-header').click()
342339
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('Example area')
343340

341+
// Placement decides containment (goal 0139): a "sibling of the
342+
// viewed area" is just a create at the parent level -- navigate up,
343+
// then create with the tray.
344344
const title = 'ZzE2eAtlasSiblingCard'
345-
await page.getByTestId('atlas-add-button').click()
346-
await page.getByTestId('atlas-add-sibling').click()
347-
await selectKind(page, ATLAS_KIND_TOPIC, 'atlas-create-kind')
348-
await page.getByTestId('atlas-create-title').fill(title)
349-
await page.getByRole('button', { name: 'Create' }).click()
350-
351345
await clickBreadcrumbSegment(page, page.getByTestId('atlas-breadcrumb').getByText('My space', { exact: true }), 'My space')
346+
await createCardViaTray(page, title, { kindID: ATLAS_KIND_TOPIC })
352347
const newCard = noteCard(page, title)
353348
await expect(newCard).toBeVisible()
354349

frontend/e2e/context-menu.spec.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { test, expect } from './fixtures/server'
2+
import { openPlacementPopover } from './fixtures/atlasBoard'
23
import { groupCard, noteCard } from './fixtures/atlasCards'
34
import { contextMenu, rightClickEmptyArea } from './fixtures/contextMenu'
45
import { clickRowAction } from './inventoryRow'
@@ -147,18 +148,14 @@ test('right-click on the Atlas board pane offers direct-placement Add card/Add n
147148
await expect(menu).not.toBeVisible()
148149
})
149150

150-
test('the Atlas toolbar\'s "+ Add" button still reaches the child-create dialog', async ({ page }) => {
151+
test('the creation tray\'s Card tool reaches the placement popover (goal 0139: no + Add menu)', async ({ page }) => {
151152
await page.goto('/')
152153
await page.getByRole('link', { name: 'Atlas' }).click()
153154
await expect(page.getByTestId('atlas-board')).toBeVisible()
154155

155-
await page.getByTestId('atlas-add-button').click()
156-
await page.getByTestId('atlas-add-child').click()
157-
const dialog = page.getByRole('dialog')
158-
await expect(dialog).toBeVisible()
159-
await expect(dialog).toContainText('Add inside this card')
160-
await dialog.getByRole('button', { name: 'Cancel' }).click()
161-
await expect(dialog).not.toBeVisible()
156+
const popover = await openPlacementPopover(page)
157+
await page.keyboard.press('Escape')
158+
await expect(popover).not.toBeVisible()
162159
})
163160

164161
test('right-click on the canvas pane: no menu in view mode; Add step… opens the palette in edit mode', async ({ page }) => {

frontend/e2e/fixtures/atlasBoard.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,37 @@ export async function clickBreadcrumbSegment(page: Page, segment: Locator, label
190190
await segment.click()
191191
await page.getByTestId('atlas-breadcrumb-siblings').getByText(label, { exact: true }).click()
192192
}
193+
194+
// Toolbar-first creation (goal 0139, the + Add menu's replacement):
195+
// arm the Card tool, click a free canvas point, complete the
196+
// placement popover. The default point sits low-left, clear of the
197+
// seeded board's cards/notes, the bottom-center tray (a card landing
198+
// under the tray can't be clicked), and the minimap. Kind is the
199+
// picker's testId contract (atlas-placement-kind); omit to keep the
200+
// last-used default.
201+
export async function openPlacementPopover(page: Page, at?: { x: number; y: number }) {
202+
await page.getByTestId('atlas-tray-card').click()
203+
// Candidate points are tried until one is genuinely empty pane --
204+
// an earlier create in the same test may already occupy a spot.
205+
const candidates = at ? [at] : [{ x: 400, y: 500 }, { x: 300, y: 620 }, { x: 1000, y: 640 }, { x: 250, y: 250 }]
206+
let point = candidates[0]
207+
for (const c of candidates) {
208+
const isPane = await page.evaluate(([px, py]) => document.elementFromPoint(px, py)?.classList?.contains('react-flow__pane') ?? false, [c.x, c.y])
209+
if (isPane) {
210+
point = c
211+
break
212+
}
213+
}
214+
await page.mouse.click(point.x, point.y)
215+
const popover = page.getByTestId('atlas-placement-popover')
216+
await expect(popover).toBeVisible()
217+
return popover
218+
}
219+
220+
export async function createCardViaTray(page: Page, title: string, opts?: { kindID?: string; at?: { x: number; y: number } }) {
221+
const popover = await openPlacementPopover(page, opts?.at)
222+
if (opts?.kindID) await selectKind(page, opts.kindID, 'atlas-placement-kind')
223+
await popover.getByTestId('atlas-placement-title').fill(title)
224+
await popover.getByTestId('atlas-placement-title').press('Enter')
225+
await expect(popover).toHaveCount(0)
226+
}

0 commit comments

Comments
 (0)