Skip to content

Commit d22f9c9

Browse files
refactor: fixed layout.
1 parent 3b3867f commit d22f9c9

12 files changed

Lines changed: 292 additions & 588 deletions

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ browser acts as the runtime host for render, lint, and typecheck flows.
2525
`@knighted/develop` lets you:
2626

2727
- write component code in the browser
28+
- manage dynamic workspace tabs with add, rename, remove, and entry-role protection
2829
- switch render mode between DOM and React
2930
- switch style mode between native CSS, CSS Modules, Less, and Sass
3031
- run in-browser lint and type diagnostics
@@ -41,6 +42,10 @@ browser acts as the runtime host for render, lint, and typecheck flows.
4142

4243
- GitHub PAT setup and usage: [docs/byot.md](docs/byot.md)
4344

45+
## Editor Architecture
46+
47+
- Workspace-first editor architecture and migration notes: [docs/editor-workspace-architecture.md](docs/editor-workspace-architecture.md)
48+
4449
## Fine-Grained PAT Quick Setup
4550

4651
For PR/BYOT and AI chat flows, use a fine-grained GitHub PAT and follow the
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Editor Workspace Architecture
2+
3+
This document describes the current workspace-first editor model in `@knighted/develop`.
4+
5+
## Overview
6+
7+
- Workspace tabs are the source of truth for editor identity and content.
8+
- Tab metadata controls behavior, including entry resolution via `role: 'entry'`.
9+
- The default workspace starts with:
10+
- `src/components/App.tsx` (entry tab)
11+
- `src/styles/app.css` (style tab)
12+
- Only one editor panel is visible at a time in the current UI.
13+
- The preview compiles from the resolved entry tab and hydrates workspace module tabs.
14+
15+
## Tab UX
16+
17+
- Selecting any tab activates and reveals the matching editor content.
18+
- Add tab now requests an explicit tab name during creation.
19+
- Rename is first-class with a dedicated rename action on each tab.
20+
- Remove is available for non-entry tabs only.
21+
22+
## Persistence Model
23+
24+
- Workspace tab state is stored in IndexedDB workspace records.
25+
- Tab records persist tab role (`entry` or `module`) so entry behavior survives reload.
26+
- Local storage is intentionally limited to app/theme controls and GitHub integration state:
27+
- BYOT token and selected repository
28+
- PR drawer repository-scoped configuration
29+
- layout/theme preferences
30+
31+
## Migration Summary
32+
33+
### Removed
34+
35+
- Legacy DOM and CSS selectors tied to fixed `component-panel` and `styles-panel` naming.
36+
- Panel-era CSS branches that depended on those legacy class names.
37+
38+
### Intentionally Legacy (for integration boundaries)
39+
40+
- Distinct component/styles tool controls remain because diagnostics, lint actions, and PR sync flows still expose separate component and styles actions.
41+
- Collapse control keys still use `component` and `styles` internally for stable behavior and test coverage.
42+
43+
### Follow-up
44+
45+
- Introduce an optional dual-editor split/pin layout without changing tab identity and persistence semantics.

playwright/github-byot-ai.spec.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -389,20 +389,22 @@ test('AI chat proposals can be confirmed, applied, and undone for component and
389389
page.getByRole('button', { name: 'Undo last Component apply' }),
390390
).toBeVisible()
391391
await expect(page.getByRole('button', { name: 'Undo last Styles apply' })).toBeVisible()
392-
await expect(page.locator('.component-panel .cm-content').first()).toContainText(
393-
'Updated',
394-
)
395-
await expect(page.locator('.styles-panel .cm-content').first()).toContainText(
396-
'rgb(10 20 30)',
397-
)
392+
await expect(
393+
page.locator('.editor-panel[data-editor-kind="component"] .cm-content').first(),
394+
).toContainText('Updated')
395+
await expect(
396+
page.locator('.editor-panel[data-editor-kind="styles"] .cm-content').first(),
397+
).toContainText('rgb(10 20 30)')
398398

399399
await page.getByRole('button', { name: 'Undo last Component apply' }).click()
400-
await expect(page.locator('.component-panel .cm-content').first()).toContainText(
401-
'Before',
402-
)
400+
await expect(
401+
page.locator('.editor-panel[data-editor-kind="component"] .cm-content').first(),
402+
).toContainText('Before')
403403

404404
await page.getByRole('button', { name: 'Undo last Styles apply' }).click()
405-
await expect(page.locator('.styles-panel .cm-content').first()).toContainText('red')
405+
await expect(
406+
page.locator('.editor-panel[data-editor-kind="styles"] .cm-content').first(),
407+
).toContainText('red')
406408
})
407409

408410
test('AI chat shows a single apply action when both editor proposals are available', async ({

playwright/helpers/app-test-helpers.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,24 @@ export const expectPreviewHasRenderedContent = async (page: Page) => {
7171
}
7272

7373
export const setComponentEditorSource = async (page: Page, source: string) => {
74-
const editorContent = page.locator('.component-panel .cm-content').first()
74+
await page.getByRole('tab', { name: 'Open tab App.tsx' }).click()
75+
const editorContent = page
76+
.locator('.editor-panel[data-editor-kind="component"] .cm-content')
77+
.first()
7578
await editorContent.fill(source)
7679
}
7780

7881
export const setStylesEditorSource = async (page: Page, source: string) => {
79-
const editorContent = page.locator('.styles-panel .cm-content').first()
82+
await page.getByRole('tab', { name: 'Open tab app.css' }).click()
83+
const editorContent = page
84+
.locator('.editor-panel[data-editor-kind="styles"] .cm-content')
85+
.first()
8086
await editorContent.fill(source)
8187
}
8288

8389
export const getActiveComponentEditorLineNumber = async (page: Page) => {
8490
return page
85-
.locator('#component-panel .cm-activeLineGutter')
91+
.locator('#editor-panel-component .cm-activeLineGutter')
8692
.first()
8793
.innerText()
8894
.then(text => text.trim())
@@ -105,7 +111,7 @@ export const runStylesLint = async (page: Page) => {
105111

106112
export const getActiveStylesEditorLineNumber = async (page: Page) => {
107113
return page
108-
.locator('#styles-panel .cm-activeLineGutter')
114+
.locator('#editor-panel-styles .cm-activeLineGutter')
109115
.first()
110116
.innerText()
111117
.then(text => text.trim())
@@ -123,6 +129,12 @@ export const ensurePanelToolsVisible = async (
123129
page: Page,
124130
panelName: 'component' | 'styles',
125131
) => {
132+
if (panelName === 'styles') {
133+
await page.getByRole('tab', { name: 'Open tab app.css' }).click()
134+
} else {
135+
await page.getByRole('tab', { name: 'Open tab App.tsx' }).click()
136+
}
137+
126138
const button = getToolsButton(page, panelName)
127139
const isPressed = await button.getAttribute('aria-pressed')
128140
if (isPressed !== 'true') {

playwright/layout-panels.spec.ts

Lines changed: 54 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,9 @@ test('renders default playground preview', async ({ page }) => {
1515
await expectPreviewHasRenderedContent(page)
1616
})
1717

18-
test('supports layout and theme toggles', async ({ page }) => {
18+
test('supports theme toggles', async ({ page }) => {
1919
await waitForInitialRender(page)
2020

21-
await page.getByLabel('Use side preview layout').click()
22-
await expect(page.getByRole('main')).toHaveClass(/app-grid--preview-right/)
23-
2421
await page.getByLabel('Use light theme').click()
2522
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')
2623

@@ -33,14 +30,11 @@ test('supports layout and theme toggles', async ({ page }) => {
3330
expect(previewBackgroundColor).toBe('rgb(36, 86, 168)')
3431
})
3532

36-
test('side layout keeps preview panel height within editor stack height', async ({
33+
test('fixed layout keeps preview panel height within editor stack height', async ({
3734
page,
3835
}) => {
3936
await waitForInitialRender(page)
4037

41-
await page.getByLabel('Use side preview layout').click()
42-
await expect(page.getByRole('main')).toHaveClass(/app-grid--preview-right/)
43-
4438
const metrics = await page.evaluate(() => {
4539
const stack = document.querySelector('.panels-stack--editors')
4640
const previewPanel = document.getElementById('preview-panel')
@@ -56,13 +50,9 @@ test('side layout keeps preview panel height within editor stack height', async
5650
expect(metrics.previewOverflowY).toBe('hidden')
5751
})
5852

59-
test('side layout config keeps preview scrolling inside preview host', async ({
60-
page,
61-
}) => {
53+
test('fixed layout keeps preview scrolling inside preview host', async ({ page }) => {
6254
await waitForInitialRender(page)
6355

64-
await page.getByLabel('Use side preview layout').click()
65-
6656
const scrollConfig = await page.evaluate(() => {
6757
const previewPanel = document.getElementById('preview-panel')
6858
const previewHost = document.getElementById('preview-host')
@@ -87,85 +77,61 @@ test('side layout config keeps preview scrolling inside preview host', async ({
8777
expect(scrollConfig?.minHeight).toBe('0px')
8878
})
8979

90-
test('expanded component and styles can shrink consistently in side layouts', async ({
80+
test('expanded component and styles can shrink consistently in fixed layout', async ({
9181
page,
9282
}) => {
9383
await waitForInitialRender(page)
9484

95-
for (const layoutLabel of ['Use side preview layout', 'Use left preview layout']) {
96-
await page.getByLabel(layoutLabel).click()
97-
98-
const minHeights = await page.evaluate(() => {
99-
const component = document.getElementById('component-panel')
100-
const styles = document.getElementById('styles-panel')
101-
return {
102-
component: component
103-
? Number.parseFloat(getComputedStyle(component).minHeight)
104-
: 0,
105-
styles: styles ? Number.parseFloat(getComputedStyle(styles).minHeight) : 0,
106-
}
107-
})
108-
109-
expect(minHeights.component).toBeGreaterThanOrEqual(0)
110-
expect(minHeights.styles).toBeGreaterThanOrEqual(0)
111-
expect(Math.abs(minHeights.component - minHeights.styles)).toBeLessThanOrEqual(1)
112-
}
85+
const minHeights = await page.evaluate(() => {
86+
const component = document.getElementById('editor-panel-component')
87+
const styles = document.getElementById('editor-panel-styles')
88+
return {
89+
component: component ? Number.parseFloat(getComputedStyle(component).minHeight) : 0,
90+
styles: styles ? Number.parseFloat(getComputedStyle(styles).minHeight) : 0,
91+
}
92+
})
93+
94+
expect(minHeights.component).toBeGreaterThanOrEqual(0)
95+
expect(minHeights.styles).toBeGreaterThanOrEqual(0)
96+
expect(Math.abs(minHeights.component - minHeights.styles)).toBeLessThanOrEqual(1)
11397
})
11498

115-
test('panel collapse axis and direction adapt to active layout', async ({ page }) => {
99+
test('panel collapse axis and direction match fixed layout', async ({ page }) => {
116100
await waitForInitialRender(page)
117-
await expect(page.getByRole('main')).toHaveClass(/app-grid/)
101+
await expect(page.getByRole('main')).toHaveClass(/app-grid--preview-right/)
118102

119103
await expectCollapseButtonState(page, 'component', {
120-
axis: 'horizontal',
121-
direction: 'left',
122-
collapsed: false,
123-
})
124-
await expectCollapseButtonState(page, 'styles', {
125-
axis: 'horizontal',
126-
direction: 'right',
127-
collapsed: false,
128-
})
129-
await expectCollapseButtonState(page, 'preview', {
130104
axis: 'vertical',
131105
direction: 'none',
132106
collapsed: false,
133107
})
134-
135-
await page.getByLabel('Use side preview layout').click()
136-
await expectCollapseButtonState(page, 'preview', {
137-
axis: 'horizontal',
138-
direction: 'right',
139-
collapsed: false,
140-
})
141-
await expectCollapseButtonState(page, 'component', {
108+
await expectCollapseButtonState(page, 'styles', {
142109
axis: 'vertical',
143110
direction: 'none',
144111
collapsed: false,
145112
})
146-
147-
await page.getByLabel('Use left preview layout').click()
148113
await expectCollapseButtonState(page, 'preview', {
149114
axis: 'horizontal',
150-
direction: 'left',
115+
direction: 'right',
151116
collapsed: false,
152117
})
153118
})
154119

155120
test('prevents collapsing all three panels at once', async ({ page }) => {
156121
await waitForInitialRender(page)
157-
const componentPanel = page.getByRole('region', { name: 'Component' })
158-
const stylesPanel = page.getByRole('region', { name: 'Styles' })
122+
const componentPanel = page.locator('#editor-panel-component')
123+
const stylesPanel = page.locator('#editor-panel-styles')
159124

160125
await getCollapseButton(page, 'component').click()
126+
await page.getByRole('tab', { name: 'Open tab app.css' }).click()
161127
await getCollapseButton(page, 'styles').click()
162128

163-
await expect(componentPanel).toHaveClass(/panel--collapsed-horizontal/)
164-
await expect(stylesPanel).toHaveClass(/panel--collapsed-horizontal/)
129+
await expect(componentPanel).toHaveClass(/panel--collapsed-vertical/)
130+
await expect(stylesPanel).toHaveClass(/panel--collapsed-vertical/)
165131

166132
await expectCollapseButtonState(page, 'preview', {
167-
axis: 'vertical',
168-
direction: 'none',
133+
axis: 'horizontal',
134+
direction: 'right',
169135
collapsed: false,
170136
disabled: true,
171137
})
@@ -174,24 +140,25 @@ test('prevents collapsing all three panels at once', async ({ page }) => {
174140
'At least one panel must remain expanded.',
175141
)
176142

143+
await page.getByRole('tab', { name: 'Open tab App.tsx' }).click()
177144
await getCollapseButton(page, 'component').click()
178145
await expectCollapseButtonState(page, 'preview', {
179-
axis: 'vertical',
180-
direction: 'none',
146+
axis: 'horizontal',
147+
direction: 'right',
181148
collapsed: false,
182149
disabled: false,
183150
})
184151
})
185152

186153
test('does not persist panel collapse state across reload', async ({ page }) => {
187154
await waitForInitialRender(page)
188-
const componentPanel = page.getByRole('region', { name: 'Component' })
155+
const componentPanel = page.locator('#editor-panel-component')
189156

190157
await getCollapseButton(page, 'component').click()
191-
await expect(componentPanel).toHaveClass(/panel--collapsed-horizontal/)
158+
await expect(componentPanel).toHaveClass(/panel--collapsed-vertical/)
192159
await expectCollapseButtonState(page, 'component', {
193-
axis: 'horizontal',
194-
direction: 'left',
160+
axis: 'vertical',
161+
direction: 'none',
195162
collapsed: true,
196163
})
197164

@@ -202,8 +169,8 @@ test('does not persist panel collapse state across reload', async ({ page }) =>
202169
/panel--collapsed-horizontal|panel--collapsed-vertical/,
203170
)
204171
await expectCollapseButtonState(page, 'component', {
205-
axis: 'horizontal',
206-
direction: 'left',
172+
axis: 'vertical',
173+
direction: 'none',
207174
collapsed: false,
208175
})
209176
})
@@ -213,8 +180,8 @@ test('gear tools toggles default inactive and switch active/inactive per panel',
213180
}) => {
214181
await waitForInitialRender(page)
215182

216-
const componentPanel = page.getByRole('region', { name: 'Component' })
217-
const stylesPanel = page.getByRole('region', { name: 'Styles' })
183+
const componentPanel = page.locator('#editor-panel-component')
184+
const stylesPanel = page.locator('#editor-panel-styles')
218185
const componentTools = getToolsButton(page, 'component')
219186
const stylesTools = getToolsButton(page, 'styles')
220187

@@ -233,8 +200,24 @@ test('gear tools toggles default inactive and switch active/inactive per panel',
233200
await expect(componentTools).toHaveAttribute('aria-pressed', 'false')
234201
await expect(componentTools).toHaveAttribute('title', 'Show component tools')
235202

203+
await page.getByRole('tab', { name: 'Open tab app.css' }).click()
236204
await stylesTools.click()
237205
await expect(stylesPanel).not.toHaveClass(/panel--tools-hidden/)
238206
await expect(stylesTools).toHaveAttribute('aria-pressed', 'true')
239207
await expect(stylesTools).toHaveAttribute('title', 'Hide styles tools')
240208
})
209+
210+
test('fixed layout keeps inactive editor panel hidden', async ({ page }) => {
211+
await waitForInitialRender(page)
212+
213+
const componentPanel = page.locator('#editor-panel-component')
214+
const stylesPanel = page.locator('#editor-panel-styles')
215+
216+
const assertEntryPanelVisible = async () => {
217+
await page.getByRole('tab', { name: 'Open tab App.tsx' }).click()
218+
await expect(componentPanel).toBeVisible()
219+
await expect(stylesPanel).toBeHidden()
220+
}
221+
222+
await assertEntryPanelVisible()
223+
})

playwright/rendering-modes.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,10 +486,9 @@ test('auto render supports export default named component without App redeclarat
486486
await expect(page.locator('#preview-host pre')).toHaveCount(0)
487487
})
488488

489-
test('persists layout and theme across reload', async ({ page }) => {
489+
test('persists theme across reload with fixed layout', async ({ page }) => {
490490
await waitForInitialRender(page)
491491

492-
await page.getByLabel('Use side preview layout').click()
493492
await page.getByLabel('Use light theme').click()
494493
await expect(page.getByRole('main')).toHaveClass(/app-grid--preview-right/)
495494
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')

0 commit comments

Comments
 (0)