Skip to content

Commit 2b4605f

Browse files
refactor: grid layout.
1 parent 6d75fee commit 2b4605f

5 files changed

Lines changed: 540 additions & 152 deletions

File tree

docs/next-steps.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@ Focused follow-up work for `@knighted/develop`.
2222
5. **In-browser component testing**
2323
- Explore authoring and running component-focused tests in-browser (for example, a Vitest-compatible flow) using CDN-delivered tooling.
2424
- Define a lightweight test UX that supports writing tests, running them on demand, and displaying results in-app.
25+
26+
6. **Panel sizing without JS height sync**
27+
- Revisit the current side-layout preview height calculation and investigate a pure CSS replacement.
28+
- Keep existing behavior constraints: preview should not exceed the combined editor stack height in side layouts, and preview content should scroll internally when it overflows.

playwright/app.spec.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,62 @@ const setStylesEditorSource = async (page: Page, source: string) => {
2929
await editorContent.fill(source)
3030
}
3131

32+
const getLongListComponentSource = (count = 1200) =>
33+
[
34+
'const App = () => {',
35+
` const items = Array.from({ length: ${count} }, (_, index) => ({`,
36+
' id: index + 1,',
37+
' title: `List item ${index + 1}`',
38+
' }))',
39+
'',
40+
' return (',
41+
" <section style={{ padding: '12px', fontFamily: 'system-ui, sans-serif' }}>",
42+
" <h2 style={{ margin: '0 0 12px', fontSize: '1rem' }}>Long Preview List</h2>",
43+
" <ul style={{ margin: 0, paddingLeft: '1.25rem' }}>",
44+
' {items.map(item => (',
45+
" <li key={item.id} style={{ marginBottom: '8px' }}>",
46+
' <strong>{item.title}</strong>',
47+
' </li>',
48+
' ))}',
49+
' </ul>',
50+
' </section>',
51+
' )',
52+
'}',
53+
].join('\n')
54+
55+
const getCollapseButton = (page: Page, panelName: 'component' | 'styles' | 'preview') =>
56+
page.locator(`#collapse-${panelName}`)
57+
58+
const expectCollapseButtonState = async (
59+
page: Page,
60+
panelName: 'component' | 'styles' | 'preview',
61+
{
62+
axis,
63+
direction,
64+
collapsed,
65+
disabled,
66+
}: {
67+
axis: 'vertical' | 'horizontal'
68+
direction: 'left' | 'right' | 'none'
69+
collapsed: boolean
70+
disabled?: boolean
71+
},
72+
) => {
73+
const button = getCollapseButton(page, panelName)
74+
75+
await expect(button).toHaveAttribute('data-collapse-axis', axis)
76+
await expect(button).toHaveAttribute('data-collapse-direction', direction)
77+
await expect(button).toHaveAttribute('data-collapsed', collapsed ? 'true' : 'false')
78+
79+
if (disabled !== undefined) {
80+
if (disabled) {
81+
await expect(button).toBeDisabled()
82+
} else {
83+
await expect(button).toBeEnabled()
84+
}
85+
}
86+
}
87+
3288
test('renders default playground preview', async ({ page }) => {
3389
await waitForInitialRender(page)
3490

@@ -54,6 +110,182 @@ test('supports layout and theme toggles', async ({ page }) => {
54110
)
55111
})
56112

113+
test('side layout keeps preview panel height within editor stack height', async ({
114+
page,
115+
}) => {
116+
await waitForInitialRender(page)
117+
118+
await page.getByLabel('Use side preview layout').click()
119+
await expect(page.locator('.app-grid')).toHaveClass(/app-grid--preview-right/)
120+
121+
const metrics = await page.evaluate(() => {
122+
const stack = document.querySelector('.panels-stack--editors')
123+
const previewPanel = document.getElementById('preview-panel')
124+
const stackHeight = stack?.getBoundingClientRect().height ?? 0
125+
const previewHeight = previewPanel?.getBoundingClientRect().height ?? 0
126+
const previewOverflowY = previewPanel ? getComputedStyle(previewPanel).overflowY : ''
127+
return { stackHeight, previewHeight, previewOverflowY }
128+
})
129+
130+
expect(metrics.stackHeight).toBeGreaterThan(0)
131+
expect(metrics.previewHeight).toBeGreaterThan(0)
132+
expect(metrics.previewHeight).toBeLessThanOrEqual(metrics.stackHeight + 2)
133+
expect(metrics.previewOverflowY).toBe('hidden')
134+
})
135+
136+
test('side layout config keeps preview scrolling inside preview host', async ({
137+
page,
138+
}) => {
139+
await waitForInitialRender(page)
140+
141+
await page.getByLabel('Use side preview layout').click()
142+
143+
const scrollConfig = await page.evaluate(() => {
144+
const previewPanel = document.getElementById('preview-panel')
145+
const previewHost = document.getElementById('preview-host')
146+
if (!previewPanel || !previewHost) {
147+
return null
148+
}
149+
150+
const panelStyles = getComputedStyle(previewPanel)
151+
const styles = getComputedStyle(previewHost)
152+
return {
153+
panelOverflowY: panelStyles.overflowY,
154+
panelOverflowX: panelStyles.overflowX,
155+
overflowY: styles.overflowY,
156+
minHeight: styles.minHeight,
157+
}
158+
})
159+
160+
expect(scrollConfig).not.toBeNull()
161+
expect(scrollConfig?.panelOverflowY).toBe('hidden')
162+
expect(scrollConfig?.panelOverflowX).toBe('hidden')
163+
expect(['auto', 'scroll']).toContain(scrollConfig?.overflowY)
164+
expect(scrollConfig?.minHeight).toBe('0px')
165+
})
166+
167+
test('expanded component and styles min-height stay consistent in side layouts', async ({
168+
page,
169+
}) => {
170+
await waitForInitialRender(page)
171+
172+
for (const layoutLabel of ['Use side preview layout', 'Use left preview layout']) {
173+
await page.getByLabel(layoutLabel).click()
174+
175+
const minHeights = await page.evaluate(() => {
176+
const component = document.getElementById('component-panel')
177+
const styles = document.getElementById('styles-panel')
178+
return {
179+
component: component
180+
? Number.parseFloat(getComputedStyle(component).minHeight)
181+
: 0,
182+
styles: styles ? Number.parseFloat(getComputedStyle(styles).minHeight) : 0,
183+
}
184+
})
185+
186+
expect(minHeights.component).toBeGreaterThan(0)
187+
expect(minHeights.styles).toBeGreaterThan(0)
188+
expect(Math.abs(minHeights.component - minHeights.styles)).toBeLessThanOrEqual(1)
189+
}
190+
})
191+
192+
test('panel collapse axis and direction adapt to active layout', async ({ page }) => {
193+
await waitForInitialRender(page)
194+
await expect(page.locator('.app-grid')).toHaveClass(/app-grid/)
195+
196+
await expectCollapseButtonState(page, 'component', {
197+
axis: 'horizontal',
198+
direction: 'left',
199+
collapsed: false,
200+
})
201+
await expectCollapseButtonState(page, 'styles', {
202+
axis: 'horizontal',
203+
direction: 'right',
204+
collapsed: false,
205+
})
206+
await expectCollapseButtonState(page, 'preview', {
207+
axis: 'vertical',
208+
direction: 'none',
209+
collapsed: false,
210+
})
211+
212+
await page.getByLabel('Use side preview layout').click()
213+
await expectCollapseButtonState(page, 'preview', {
214+
axis: 'horizontal',
215+
direction: 'right',
216+
collapsed: false,
217+
})
218+
await expectCollapseButtonState(page, 'component', {
219+
axis: 'vertical',
220+
direction: 'none',
221+
collapsed: false,
222+
})
223+
224+
await page.getByLabel('Use left preview layout').click()
225+
await expectCollapseButtonState(page, 'preview', {
226+
axis: 'horizontal',
227+
direction: 'left',
228+
collapsed: false,
229+
})
230+
})
231+
232+
test('prevents collapsing all three panels at once', async ({ page }) => {
233+
await waitForInitialRender(page)
234+
235+
await getCollapseButton(page, 'component').click()
236+
await getCollapseButton(page, 'styles').click()
237+
238+
await expect(page.locator('#component-panel')).toHaveClass(
239+
/panel--collapsed-horizontal/,
240+
)
241+
await expect(page.locator('#styles-panel')).toHaveClass(/panel--collapsed-horizontal/)
242+
243+
await expectCollapseButtonState(page, 'preview', {
244+
axis: 'vertical',
245+
direction: 'none',
246+
collapsed: false,
247+
disabled: true,
248+
})
249+
await expect(getCollapseButton(page, 'preview')).toHaveAttribute(
250+
'title',
251+
'At least one panel must remain expanded.',
252+
)
253+
254+
await getCollapseButton(page, 'component').click()
255+
await expectCollapseButtonState(page, 'preview', {
256+
axis: 'vertical',
257+
direction: 'none',
258+
collapsed: false,
259+
disabled: false,
260+
})
261+
})
262+
263+
test('does not persist panel collapse state across reload', async ({ page }) => {
264+
await waitForInitialRender(page)
265+
266+
await getCollapseButton(page, 'component').click()
267+
await expect(page.locator('#component-panel')).toHaveClass(
268+
/panel--collapsed-horizontal/,
269+
)
270+
await expectCollapseButtonState(page, 'component', {
271+
axis: 'horizontal',
272+
direction: 'left',
273+
collapsed: true,
274+
})
275+
276+
await page.reload()
277+
await waitForInitialRender(page)
278+
279+
await expect(page.locator('#component-panel')).not.toHaveClass(
280+
/panel--collapsed-horizontal|panel--collapsed-vertical/,
281+
)
282+
await expectCollapseButtonState(page, 'component', {
283+
axis: 'horizontal',
284+
direction: 'left',
285+
collapsed: false,
286+
})
287+
})
288+
57289
test('renders in react mode with css modules', async ({ page }) => {
58290
await waitForInitialRender(page)
59291

src/app.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const appGrid = document.querySelector('.app-grid')
1616
const appGridLayoutButtons = document.querySelectorAll('[data-app-grid-layout]')
1717
const appThemeButtons = document.querySelectorAll('[data-app-theme]')
1818
const panelCollapseButtons = document.querySelectorAll('[data-panel-collapse]')
19+
const editorsStack = document.querySelector('.panels-stack--editors')
1920
const componentPanel = document.getElementById('component-panel')
2021
const stylesPanel = document.getElementById('styles-panel')
2122
const previewPanel = document.getElementById('preview-panel')
@@ -186,6 +187,23 @@ const syncPanelCollapseButtons = () => {
186187
}
187188
}
188189

190+
const syncSidePreviewHeight = () => {
191+
if (!appGrid || !editorsStack) {
192+
return
193+
}
194+
195+
const layout = getCurrentLayout()
196+
if (layout !== 'preview-right' && layout !== 'preview-left') {
197+
appGrid.style.removeProperty('--side-editors-height')
198+
return
199+
}
200+
201+
const height = Math.round(editorsStack.getBoundingClientRect().height)
202+
if (height > 0) {
203+
appGrid.style.setProperty('--side-editors-height', `${height}px`)
204+
}
205+
}
206+
189207
const applyPanelCollapseState = () => {
190208
normalizePanelCollapseState()
191209

@@ -233,6 +251,8 @@ const applyPanelCollapseState = () => {
233251
'app-grid--preview-collapsed-horizontal',
234252
panelCollapseState.preview && previewAxis === 'horizontal',
235253
)
254+
appGrid.classList.toggle('app-grid--component-collapsed', panelCollapseState.component)
255+
appGrid.classList.toggle('app-grid--styles-collapsed', panelCollapseState.styles)
236256
appGrid.classList.toggle(
237257
'app-grid--component-collapsed-horizontal',
238258
panelCollapseState.component && componentAxis === 'horizontal',
@@ -243,6 +263,7 @@ const applyPanelCollapseState = () => {
243263
)
244264

245265
syncPanelCollapseButtons()
266+
syncSidePreviewHeight()
246267
}
247268

248269
const togglePanelCollapse = panelName => {
@@ -663,6 +684,13 @@ window.addEventListener('resize', () => {
663684
applyPanelCollapseState()
664685
})
665686

687+
if (typeof ResizeObserver !== 'undefined' && editorsStack) {
688+
const sidePreviewHeightObserver = new ResizeObserver(() => {
689+
syncSidePreviewHeight()
690+
})
691+
sidePreviewHeightObserver.observe(editorsStack)
692+
}
693+
666694
applyAppGridLayout(getInitialAppGridLayout(), { persist: false })
667695
applyTheme(getInitialTheme(), { persist: false })
668696
applyPanelCollapseState()

0 commit comments

Comments
 (0)