From 6d75fee7b72c4dcf8c264ec238ce72e9806ed939 Mon Sep 17 00:00:00 2001 From: KCM Date: Tue, 17 Mar 2026 11:48:57 -0500 Subject: [PATCH 1/6] feat: collapsible panels. --- src/app.js | 200 ++++++++++++++++++++++++++++++++++ src/index.html | 54 ++++++++- src/styles/layout-shell.css | 47 ++++++++ src/styles/panels-editor.css | 206 +++++++++++++++++++++++++++++++++++ 4 files changed, 504 insertions(+), 3 deletions(-) diff --git a/src/app.js b/src/app.js index 8f4b3fb..3b7b00d 100644 --- a/src/app.js +++ b/src/app.js @@ -15,6 +15,10 @@ const statusNode = document.getElementById('status') const appGrid = document.querySelector('.app-grid') const appGridLayoutButtons = document.querySelectorAll('[data-app-grid-layout]') const appThemeButtons = document.querySelectorAll('[data-app-theme]') +const panelCollapseButtons = document.querySelectorAll('[data-panel-collapse]') +const componentPanel = document.getElementById('component-panel') +const stylesPanel = document.getElementById('styles-panel') +const previewPanel = document.getElementById('preview-panel') const renderMode = document.getElementById('render-mode') const autoRenderToggle = document.getElementById('auto-render') const typecheckButton = document.getElementById('typecheck-button') @@ -71,6 +75,185 @@ const layoutTheme = createLayoutThemeController({ const { applyAppGridLayout, applyTheme, getInitialAppGridLayout, getInitialTheme } = layoutTheme +const panelMap = { + component: componentPanel, + styles: stylesPanel, + preview: previewPanel, +} + +const getCurrentLayout = () => { + if (appGrid.classList.contains('app-grid--preview-right')) { + return 'preview-right' + } + + if (appGrid.classList.contains('app-grid--preview-left')) { + return 'preview-left' + } + + return 'default' +} + +const isCompactViewport = () => window.matchMedia('(max-width: 900px)').matches + +const getPanelCollapseAxis = panelName => { + if (isCompactViewport()) { + return 'vertical' + } + + const layout = getCurrentLayout() + + if (panelName === 'preview') { + return layout === 'default' ? 'vertical' : 'horizontal' + } + + if (panelName === 'component' || panelName === 'styles') { + return layout === 'default' ? 'horizontal' : 'vertical' + } + + return 'vertical' +} + +const getPanelCollapseDirection = panelName => { + const axis = getPanelCollapseAxis(panelName) + if (axis !== 'horizontal') { + return 'none' + } + + const layout = getCurrentLayout() + + if (panelName === 'preview') { + return layout === 'preview-left' ? 'left' : 'right' + } + + if (panelName === 'component') { + return 'left' + } + + if (panelName === 'styles') { + return 'right' + } + + return 'right' +} + +const panelCollapseState = { + component: false, + styles: false, + preview: false, +} + +const normalizePanelCollapseState = () => { + const collapsedPanels = Object.entries(panelCollapseState) + .filter(([, isCollapsed]) => isCollapsed) + .map(([panelName]) => panelName) + + if (collapsedPanels.length === Object.keys(panelCollapseState).length) { + panelCollapseState.preview = false + } +} + +const syncPanelCollapseButtons = () => { + const collapsedCount = Object.values(panelCollapseState).filter(Boolean).length + + for (const button of panelCollapseButtons) { + const panelName = button.dataset.panelCollapse + if (!panelName || !(panelName in panelMap)) { + continue + } + + const axis = getPanelCollapseAxis(panelName) + const direction = getPanelCollapseDirection(panelName) + const isCollapsed = panelCollapseState[panelName] === true + const panelTitle = `${panelName.charAt(0).toUpperCase()}${panelName.slice(1)}` + const canCollapse = isCollapsed || collapsedCount < 2 + + button.dataset.collapseAxis = axis + button.dataset.collapseDirection = direction + button.dataset.collapsed = isCollapsed ? 'true' : 'false' + button.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true') + button.disabled = !canCollapse + button.setAttribute('aria-disabled', canCollapse ? 'false' : 'true') + button.setAttribute( + 'aria-label', + `${isCollapsed ? 'Expand' : 'Collapse'} ${panelTitle.toLowerCase()} panel`, + ) + button.setAttribute( + 'title', + canCollapse + ? `${isCollapsed ? 'Expand' : 'Collapse'} ${panelTitle.toLowerCase()} panel` + : 'At least one panel must remain expanded.', + ) + } +} + +const applyPanelCollapseState = () => { + normalizePanelCollapseState() + + const previewAxis = getPanelCollapseAxis('preview') + const componentAxis = getPanelCollapseAxis('component') + const stylesAxis = getPanelCollapseAxis('styles') + + if (componentPanel) { + const isCollapsed = panelCollapseState.component + componentPanel.classList.toggle( + 'panel--collapsed-vertical', + isCollapsed && componentAxis === 'vertical', + ) + componentPanel.classList.toggle( + 'panel--collapsed-horizontal', + isCollapsed && componentAxis === 'horizontal', + ) + } + + if (stylesPanel) { + const isCollapsed = panelCollapseState.styles + stylesPanel.classList.toggle( + 'panel--collapsed-vertical', + isCollapsed && stylesAxis === 'vertical', + ) + stylesPanel.classList.toggle( + 'panel--collapsed-horizontal', + isCollapsed && stylesAxis === 'horizontal', + ) + } + + if (previewPanel) { + const isCollapsed = panelCollapseState.preview + previewPanel.classList.toggle( + 'panel--collapsed-vertical', + isCollapsed && previewAxis === 'vertical', + ) + previewPanel.classList.toggle( + 'panel--collapsed-horizontal', + isCollapsed && previewAxis === 'horizontal', + ) + } + + appGrid.classList.toggle( + 'app-grid--preview-collapsed-horizontal', + panelCollapseState.preview && previewAxis === 'horizontal', + ) + appGrid.classList.toggle( + 'app-grid--component-collapsed-horizontal', + panelCollapseState.component && componentAxis === 'horizontal', + ) + appGrid.classList.toggle( + 'app-grid--styles-collapsed-horizontal', + panelCollapseState.styles && stylesAxis === 'horizontal', + ) + + syncPanelCollapseButtons() +} + +const togglePanelCollapse = panelName => { + if (!(panelName in panelCollapseState)) { + return + } + + panelCollapseState[panelName] = !panelCollapseState[panelName] + applyPanelCollapseState() +} + const diagnosticsUi = createDiagnosticsUiController({ diagnosticsToggle, diagnosticsDrawer, @@ -451,6 +634,7 @@ for (const button of appGridLayoutButtons) { return } applyAppGridLayout(nextLayout) + applyPanelCollapseState() }) } @@ -464,8 +648,24 @@ for (const button of appThemeButtons) { }) } +for (const button of panelCollapseButtons) { + button.addEventListener('click', () => { + const panelName = button.dataset.panelCollapse + if (!panelName) { + return + } + + togglePanelCollapse(panelName) + }) +} + +window.addEventListener('resize', () => { + applyPanelCollapseState() +}) + applyAppGridLayout(getInitialAppGridLayout(), { persist: false }) applyTheme(getInitialTheme(), { persist: false }) +applyPanelCollapseState() updateRenderButtonVisibility() renderDiagnosticsScope('component') diff --git a/src/index.html b/src/index.html index 7172ab3..d3fd6d4 100644 --- a/src/index.html +++ b/src/index.html @@ -129,10 +129,26 @@

-
+

Component

+

-
+

Styles

+
-
+

Preview

@@ -256,6 +288,22 @@

Preview

> i +
diff --git a/src/styles/layout-shell.css b/src/styles/layout-shell.css index 1878230..6dc5fca 100644 --- a/src/styles/layout-shell.css +++ b/src/styles/layout-shell.css @@ -78,6 +78,30 @@ padding: 24px; } +.app-grid.app-grid--component-collapsed-horizontal:not(.app-grid--preview-right):not( + .app-grid--preview-left + ) { + grid-template-columns: 72px minmax(320px, 1fr); +} + +.app-grid.app-grid--styles-collapsed-horizontal:not(.app-grid--preview-right):not( + .app-grid--preview-left + ) { + grid-template-columns: minmax(320px, 1fr) 72px; +} + +.app-grid.app-grid--component-collapsed-horizontal.app-grid--styles-collapsed-horizontal:not( + .app-grid--preview-right + ):not(.app-grid--preview-left) { + grid-template-columns: 72px 72px minmax(0, 1fr); + grid-template-rows: auto auto minmax(320px, 1fr); + grid-template-areas: + 'layout-controls layout-controls layout-controls' + 'component styles .' + 'preview preview preview'; + min-height: max(520px, calc(100dvh - 210px)); +} + .app-grid--preview-right { grid-template-areas: 'layout-controls layout-controls' @@ -85,6 +109,10 @@ 'styles preview'; } +.app-grid--preview-right.app-grid--preview-collapsed-horizontal { + grid-template-columns: minmax(320px, 1fr) 72px; +} + .app-grid--preview-left { grid-template-areas: 'layout-controls layout-controls' @@ -92,6 +120,10 @@ 'preview styles'; } +.app-grid--preview-left.app-grid--preview-collapsed-horizontal { + grid-template-columns: 72px minmax(320px, 1fr); +} + .app-grid-layout-controls { grid-area: layout-controls; display: inline-flex; @@ -178,6 +210,21 @@ 'preview'; } + .app-grid--preview-right.app-grid--preview-collapsed-horizontal, + .app-grid--preview-left.app-grid--preview-collapsed-horizontal { + grid-template-columns: minmax(0, 1fr); + } + + .app-grid.app-grid--component-collapsed-horizontal, + .app-grid.app-grid--styles-collapsed-horizontal { + grid-template-columns: minmax(0, 1fr); + } + + .app-grid.app-grid--component-collapsed-horizontal.app-grid--styles-collapsed-horizontal { + grid-template-rows: auto; + min-height: 0; + } + .app-grid-layout-controls { justify-self: start; } diff --git a/src/styles/panels-editor.css b/src/styles/panels-editor.css index 21c7e49..7cd3a76 100644 --- a/src/styles/panels-editor.css +++ b/src/styles/panels-editor.css @@ -74,6 +74,77 @@ gap: 10px; } +.panel-collapse-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--border-control); + background: var(--surface-control); + color: var(--icon-color); + border-radius: 999px; + width: 32px; + height: 32px; + padding: 0; + cursor: pointer; +} + +.panel-collapse-toggle:hover { + background: var(--surface-control-hover); +} + +.panel-collapse-toggle:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.panel-collapse-toggle:disabled:hover { + background: var(--surface-control); +} + +.panel-collapse-toggle:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 1px; +} + +.panel-collapse-toggle svg { + width: 14px; + height: 14px; + stroke: currentColor; + fill: none; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + transform: rotate(180deg); +} + +.panel-collapse-toggle[data-collapse-axis='vertical'][data-collapsed='true'] svg { + transform: rotate(0deg); +} + +.panel-collapse-toggle[data-collapse-axis='horizontal'][data-collapse-direction='right'][data-collapsed='false'] + svg { + transform: rotate(-90deg); +} + +.panel-collapse-toggle[data-collapse-axis='horizontal'][data-collapse-direction='right'][data-collapsed='true'] + svg { + transform: rotate(90deg); +} + +.panel-collapse-toggle[data-collapse-axis='horizontal'][data-collapse-direction='left'][data-collapsed='false'] + svg { + transform: rotate(90deg); +} + +.panel-collapse-toggle[data-collapse-axis='horizontal'][data-collapse-direction='left'][data-collapsed='true'] + svg { + transform: rotate(-90deg); +} + +.panel-collapse-toggle__label { + display: none; +} + .controls label { display: flex; flex-direction: column; @@ -143,7 +214,142 @@ textarea:focus { overflow: auto; } +.panel.panel--collapsed-vertical { + min-height: auto; +} + +.component-panel.panel--collapsed-vertical, +.styles-panel.panel--collapsed-vertical { + align-self: start; +} + +.panel.preview.panel--collapsed-vertical { + min-height: 0; +} + +.component-panel.panel--collapsed-vertical .editor-host, +.component-panel.panel--collapsed-vertical textarea, +.styles-panel.panel--collapsed-vertical .editor-host, +.styles-panel.panel--collapsed-vertical textarea, +.styles-panel.panel--collapsed-vertical .panel-footer, +.preview-panel.panel--collapsed-vertical .preview-host { + display: none; +} + +.component-panel.panel--collapsed-vertical .panel-header-main-actions, +.styles-panel.panel--collapsed-vertical .panel-header-main-actions, +.component-panel.panel--collapsed-vertical .panel-header .icon-button, +.styles-panel.panel--collapsed-vertical .panel-header .icon-button, +.preview-panel.panel--collapsed-vertical .controls label, +.preview-panel.panel--collapsed-vertical .controls .hint-icon { + display: none; +} + +.component-panel.panel--collapsed-vertical .panel-header--grid, +.styles-panel.panel--collapsed-vertical .panel-header--grid { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.preview-panel.panel--collapsed-vertical .panel-header .controls { + justify-content: center; +} + +.preview-panel.panel--collapsed-horizontal { + min-height: auto; +} + +.preview-panel.panel--collapsed-horizontal .preview-host, +.preview-panel.panel--collapsed-horizontal .controls label, +.preview-panel.panel--collapsed-horizontal .controls .hint-icon { + display: none; +} + +.preview-panel.panel--collapsed-horizontal .panel-header { + height: 100%; + padding: 12px 8px; + flex-direction: column; + justify-content: space-between; + align-items: center; +} + +.preview-panel.panel--collapsed-horizontal .panel-header h2 { + writing-mode: vertical-rl; + transform: rotate(180deg); + font-size: 0.8rem; +} + +.preview-panel.panel--collapsed-horizontal .panel-header .controls { + width: 100%; + justify-content: center; +} + +.preview-panel.panel--collapsed-horizontal .panel-collapse-toggle { + width: 100%; + min-width: 0; + justify-content: center; +} + +.preview-panel.panel--collapsed-horizontal .panel-collapse-toggle__label { + display: none; +} + +.component-panel.panel--collapsed-horizontal, +.styles-panel.panel--collapsed-horizontal { + min-height: auto; +} + +.component-panel.panel--collapsed-horizontal .editor-host, +.component-panel.panel--collapsed-horizontal textarea, +.component-panel.panel--collapsed-horizontal .panel-header-main-actions, +.component-panel.panel--collapsed-horizontal .panel-header .icon-button, +.styles-panel.panel--collapsed-horizontal .editor-host, +.styles-panel.panel--collapsed-horizontal textarea, +.styles-panel.panel--collapsed-horizontal .panel-header-main-actions, +.styles-panel.panel--collapsed-horizontal .panel-header .icon-button, +.styles-panel.panel--collapsed-horizontal .panel-footer { + display: none; +} + +.component-panel.panel--collapsed-horizontal .panel-header, +.styles-panel.panel--collapsed-horizontal .panel-header { + height: 100%; + padding: 12px 8px; + border-bottom: none; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.component-panel.panel--collapsed-horizontal .panel-header h2, +.styles-panel.panel--collapsed-horizontal .panel-header h2 { + writing-mode: vertical-rl; + transform: rotate(180deg); + font-size: 0.8rem; +} + +.component-panel.panel--collapsed-horizontal .panel-header-quick-actions, +.styles-panel.panel--collapsed-horizontal .panel-header-quick-actions { + width: 100%; + justify-content: center; +} + +.component-panel.panel--collapsed-horizontal .panel-collapse-toggle, +.styles-panel.panel--collapsed-horizontal .panel-collapse-toggle { + width: 100%; + min-width: 0; +} + @media (max-width: 900px) { + .panel-collapse-toggle { + width: 32px; + height: 32px; + } + .component-panel, .styles-panel { max-height: none; From 2b4605f705090130d10ae1cdbf5997a27462569c Mon Sep 17 00:00:00 2001 From: KCM Date: Tue, 17 Mar 2026 13:14:28 -0500 Subject: [PATCH 2/6] refactor: grid layout. --- docs/next-steps.md | 4 + playwright/app.spec.ts | 232 +++++++++++++++++++++++++++++++ src/app.js | 28 ++++ src/index.html | 268 ++++++++++++++++++------------------ src/styles/layout-shell.css | 160 ++++++++++++++++++--- 5 files changed, 540 insertions(+), 152 deletions(-) diff --git a/docs/next-steps.md b/docs/next-steps.md index 99677f4..172733e 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -22,3 +22,7 @@ Focused follow-up work for `@knighted/develop`. 5. **In-browser component testing** - Explore authoring and running component-focused tests in-browser (for example, a Vitest-compatible flow) using CDN-delivered tooling. - Define a lightweight test UX that supports writing tests, running them on demand, and displaying results in-app. + +6. **Panel sizing without JS height sync** + - Revisit the current side-layout preview height calculation and investigate a pure CSS replacement. + - 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. diff --git a/playwright/app.spec.ts b/playwright/app.spec.ts index d52b2ea..3064bb3 100644 --- a/playwright/app.spec.ts +++ b/playwright/app.spec.ts @@ -29,6 +29,62 @@ const setStylesEditorSource = async (page: Page, source: string) => { await editorContent.fill(source) } +const getLongListComponentSource = (count = 1200) => + [ + 'const App = () => {', + ` const items = Array.from({ length: ${count} }, (_, index) => ({`, + ' id: index + 1,', + ' title: `List item ${index + 1}`', + ' }))', + '', + ' return (', + "
", + "

Long Preview List

", + "
    ", + ' {items.map(item => (', + "
  • ", + ' {item.title}', + '
  • ', + ' ))}', + '
', + '
', + ' )', + '}', + ].join('\n') + +const getCollapseButton = (page: Page, panelName: 'component' | 'styles' | 'preview') => + page.locator(`#collapse-${panelName}`) + +const expectCollapseButtonState = async ( + page: Page, + panelName: 'component' | 'styles' | 'preview', + { + axis, + direction, + collapsed, + disabled, + }: { + axis: 'vertical' | 'horizontal' + direction: 'left' | 'right' | 'none' + collapsed: boolean + disabled?: boolean + }, +) => { + const button = getCollapseButton(page, panelName) + + await expect(button).toHaveAttribute('data-collapse-axis', axis) + await expect(button).toHaveAttribute('data-collapse-direction', direction) + await expect(button).toHaveAttribute('data-collapsed', collapsed ? 'true' : 'false') + + if (disabled !== undefined) { + if (disabled) { + await expect(button).toBeDisabled() + } else { + await expect(button).toBeEnabled() + } + } +} + test('renders default playground preview', async ({ page }) => { await waitForInitialRender(page) @@ -54,6 +110,182 @@ test('supports layout and theme toggles', async ({ page }) => { ) }) +test('side layout keeps preview panel height within editor stack height', async ({ + page, +}) => { + await waitForInitialRender(page) + + await page.getByLabel('Use side preview layout').click() + await expect(page.locator('.app-grid')).toHaveClass(/app-grid--preview-right/) + + const metrics = await page.evaluate(() => { + const stack = document.querySelector('.panels-stack--editors') + const previewPanel = document.getElementById('preview-panel') + const stackHeight = stack?.getBoundingClientRect().height ?? 0 + const previewHeight = previewPanel?.getBoundingClientRect().height ?? 0 + const previewOverflowY = previewPanel ? getComputedStyle(previewPanel).overflowY : '' + return { stackHeight, previewHeight, previewOverflowY } + }) + + expect(metrics.stackHeight).toBeGreaterThan(0) + expect(metrics.previewHeight).toBeGreaterThan(0) + expect(metrics.previewHeight).toBeLessThanOrEqual(metrics.stackHeight + 2) + expect(metrics.previewOverflowY).toBe('hidden') +}) + +test('side layout config keeps preview scrolling inside preview host', async ({ + page, +}) => { + await waitForInitialRender(page) + + await page.getByLabel('Use side preview layout').click() + + const scrollConfig = await page.evaluate(() => { + const previewPanel = document.getElementById('preview-panel') + const previewHost = document.getElementById('preview-host') + if (!previewPanel || !previewHost) { + return null + } + + const panelStyles = getComputedStyle(previewPanel) + const styles = getComputedStyle(previewHost) + return { + panelOverflowY: panelStyles.overflowY, + panelOverflowX: panelStyles.overflowX, + overflowY: styles.overflowY, + minHeight: styles.minHeight, + } + }) + + expect(scrollConfig).not.toBeNull() + expect(scrollConfig?.panelOverflowY).toBe('hidden') + expect(scrollConfig?.panelOverflowX).toBe('hidden') + expect(['auto', 'scroll']).toContain(scrollConfig?.overflowY) + expect(scrollConfig?.minHeight).toBe('0px') +}) + +test('expanded component and styles min-height stay consistent in side layouts', async ({ + page, +}) => { + await waitForInitialRender(page) + + for (const layoutLabel of ['Use side preview layout', 'Use left preview layout']) { + await page.getByLabel(layoutLabel).click() + + const minHeights = await page.evaluate(() => { + const component = document.getElementById('component-panel') + const styles = document.getElementById('styles-panel') + return { + component: component + ? Number.parseFloat(getComputedStyle(component).minHeight) + : 0, + styles: styles ? Number.parseFloat(getComputedStyle(styles).minHeight) : 0, + } + }) + + expect(minHeights.component).toBeGreaterThan(0) + expect(minHeights.styles).toBeGreaterThan(0) + expect(Math.abs(minHeights.component - minHeights.styles)).toBeLessThanOrEqual(1) + } +}) + +test('panel collapse axis and direction adapt to active layout', async ({ page }) => { + await waitForInitialRender(page) + await expect(page.locator('.app-grid')).toHaveClass(/app-grid/) + + await expectCollapseButtonState(page, 'component', { + axis: 'horizontal', + direction: 'left', + collapsed: false, + }) + await expectCollapseButtonState(page, 'styles', { + axis: 'horizontal', + direction: 'right', + collapsed: false, + }) + await expectCollapseButtonState(page, 'preview', { + axis: 'vertical', + direction: 'none', + collapsed: false, + }) + + await page.getByLabel('Use side preview layout').click() + await expectCollapseButtonState(page, 'preview', { + axis: 'horizontal', + direction: 'right', + collapsed: false, + }) + await expectCollapseButtonState(page, 'component', { + axis: 'vertical', + direction: 'none', + collapsed: false, + }) + + await page.getByLabel('Use left preview layout').click() + await expectCollapseButtonState(page, 'preview', { + axis: 'horizontal', + direction: 'left', + collapsed: false, + }) +}) + +test('prevents collapsing all three panels at once', async ({ page }) => { + await waitForInitialRender(page) + + await getCollapseButton(page, 'component').click() + await getCollapseButton(page, 'styles').click() + + await expect(page.locator('#component-panel')).toHaveClass( + /panel--collapsed-horizontal/, + ) + await expect(page.locator('#styles-panel')).toHaveClass(/panel--collapsed-horizontal/) + + await expectCollapseButtonState(page, 'preview', { + axis: 'vertical', + direction: 'none', + collapsed: false, + disabled: true, + }) + await expect(getCollapseButton(page, 'preview')).toHaveAttribute( + 'title', + 'At least one panel must remain expanded.', + ) + + await getCollapseButton(page, 'component').click() + await expectCollapseButtonState(page, 'preview', { + axis: 'vertical', + direction: 'none', + collapsed: false, + disabled: false, + }) +}) + +test('does not persist panel collapse state across reload', async ({ page }) => { + await waitForInitialRender(page) + + await getCollapseButton(page, 'component').click() + await expect(page.locator('#component-panel')).toHaveClass( + /panel--collapsed-horizontal/, + ) + await expectCollapseButtonState(page, 'component', { + axis: 'horizontal', + direction: 'left', + collapsed: true, + }) + + await page.reload() + await waitForInitialRender(page) + + await expect(page.locator('#component-panel')).not.toHaveClass( + /panel--collapsed-horizontal|panel--collapsed-vertical/, + ) + await expectCollapseButtonState(page, 'component', { + axis: 'horizontal', + direction: 'left', + collapsed: false, + }) +}) + test('renders in react mode with css modules', async ({ page }) => { await waitForInitialRender(page) diff --git a/src/app.js b/src/app.js index 3b7b00d..c610a94 100644 --- a/src/app.js +++ b/src/app.js @@ -16,6 +16,7 @@ const appGrid = document.querySelector('.app-grid') const appGridLayoutButtons = document.querySelectorAll('[data-app-grid-layout]') const appThemeButtons = document.querySelectorAll('[data-app-theme]') const panelCollapseButtons = document.querySelectorAll('[data-panel-collapse]') +const editorsStack = document.querySelector('.panels-stack--editors') const componentPanel = document.getElementById('component-panel') const stylesPanel = document.getElementById('styles-panel') const previewPanel = document.getElementById('preview-panel') @@ -186,6 +187,23 @@ const syncPanelCollapseButtons = () => { } } +const syncSidePreviewHeight = () => { + if (!appGrid || !editorsStack) { + return + } + + const layout = getCurrentLayout() + if (layout !== 'preview-right' && layout !== 'preview-left') { + appGrid.style.removeProperty('--side-editors-height') + return + } + + const height = Math.round(editorsStack.getBoundingClientRect().height) + if (height > 0) { + appGrid.style.setProperty('--side-editors-height', `${height}px`) + } +} + const applyPanelCollapseState = () => { normalizePanelCollapseState() @@ -233,6 +251,8 @@ const applyPanelCollapseState = () => { 'app-grid--preview-collapsed-horizontal', panelCollapseState.preview && previewAxis === 'horizontal', ) + appGrid.classList.toggle('app-grid--component-collapsed', panelCollapseState.component) + appGrid.classList.toggle('app-grid--styles-collapsed', panelCollapseState.styles) appGrid.classList.toggle( 'app-grid--component-collapsed-horizontal', panelCollapseState.component && componentAxis === 'horizontal', @@ -243,6 +263,7 @@ const applyPanelCollapseState = () => { ) syncPanelCollapseButtons() + syncSidePreviewHeight() } const togglePanelCollapse = panelName => { @@ -663,6 +684,13 @@ window.addEventListener('resize', () => { applyPanelCollapseState() }) +if (typeof ResizeObserver !== 'undefined' && editorsStack) { + const sidePreviewHeightObserver = new ResizeObserver(() => { + syncSidePreviewHeight() + }) + sidePreviewHeightObserver.observe(editorsStack) +} + applyAppGridLayout(getInitialAppGridLayout(), { persist: false }) applyTheme(getInitialTheme(), { persist: false }) applyPanelCollapseState() diff --git a/src/index.html b/src/index.html index d3fd6d4..afd0dd2 100644 --- a/src/index.html +++ b/src/index.html @@ -129,144 +129,150 @@

-
-
-

Component

-
- - - -
-
-
- - + - - +
+
+
+ + + + +
-
- -
+ +

-
-
-

Styles

-
- - - -
-
-
- +
+
+

Styles

+
+ + + +
+
+
+ +
-
- - -
+ + +
+
diff --git a/src/styles/layout-shell.css b/src/styles/layout-shell.css index 6dc5fca..463daf1 100644 --- a/src/styles/layout-shell.css +++ b/src/styles/layout-shell.css @@ -68,45 +68,93 @@ } .app-grid { + --panel-max-height-default: clamp(360px, calc(100dvh - 290px), 680px); + --panel-max-height-side: clamp(420px, calc(100dvh - 230px), 860px); display: grid; grid-template-columns: repeat(2, minmax(320px, 1fr)); grid-template-areas: 'layout-controls layout-controls' - 'component styles' + 'editors editors' 'preview preview'; gap: 18px; padding: 24px; } +.panels-stack { + min-width: 0; +} + +.panels-stack--editors { + grid-area: editors; + display: grid; + grid-template-columns: repeat(2, minmax(320px, 1fr)); + gap: 18px; + min-width: 0; +} + .app-grid.app-grid--component-collapsed-horizontal:not(.app-grid--preview-right):not( .app-grid--preview-left - ) { + ) + .panels-stack--editors { grid-template-columns: 72px minmax(320px, 1fr); } .app-grid.app-grid--styles-collapsed-horizontal:not(.app-grid--preview-right):not( .app-grid--preview-left - ) { + ) + .panels-stack--editors { grid-template-columns: minmax(320px, 1fr) 72px; } .app-grid.app-grid--component-collapsed-horizontal.app-grid--styles-collapsed-horizontal:not( .app-grid--preview-right ):not(.app-grid--preview-left) { - grid-template-columns: 72px 72px minmax(0, 1fr); grid-template-rows: auto auto minmax(320px, 1fr); grid-template-areas: - 'layout-controls layout-controls layout-controls' - 'component styles .' - 'preview preview preview'; + 'layout-controls layout-controls' + 'editors editors' + 'preview preview'; min-height: max(520px, calc(100dvh - 210px)); } +.app-grid.app-grid--component-collapsed-horizontal.app-grid--styles-collapsed-horizontal:not( + .app-grid--preview-right + ):not(.app-grid--preview-left) + .panels-stack--editors { + grid-template-columns: 72px 72px; + justify-content: start; +} + .app-grid--preview-right { + grid-template-columns: repeat(2, minmax(320px, 1fr)); + grid-template-rows: auto minmax(0, 1fr); grid-template-areas: 'layout-controls layout-controls' - 'component preview' - 'styles preview'; + 'editors preview'; + min-height: max(520px, calc(100dvh - 210px)); +} + +.app-grid--preview-right .panels-stack--editors { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); + height: 100%; + min-height: 0; +} + +.app-grid--preview-right.app-grid--component-collapsed:not(.app-grid--styles-collapsed) + .panels-stack--editors { + grid-template-rows: auto minmax(0, 1fr); +} + +.app-grid--preview-right.app-grid--styles-collapsed:not(.app-grid--component-collapsed) + .panels-stack--editors { + grid-template-rows: minmax(0, 1fr) auto; +} + +.app-grid--preview-right.app-grid--component-collapsed.app-grid--styles-collapsed + .panels-stack--editors { + grid-template-rows: auto auto; + align-content: start; } .app-grid--preview-right.app-grid--preview-collapsed-horizontal { @@ -114,10 +162,35 @@ } .app-grid--preview-left { + grid-template-columns: repeat(2, minmax(320px, 1fr)); + grid-template-rows: auto minmax(0, 1fr); grid-template-areas: 'layout-controls layout-controls' - 'preview component' - 'preview styles'; + 'preview editors'; + min-height: max(520px, calc(100dvh - 210px)); +} + +.app-grid--preview-left .panels-stack--editors { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); + height: 100%; + min-height: 0; +} + +.app-grid--preview-left.app-grid--component-collapsed:not(.app-grid--styles-collapsed) + .panels-stack--editors { + grid-template-rows: auto minmax(0, 1fr); +} + +.app-grid--preview-left.app-grid--styles-collapsed:not(.app-grid--component-collapsed) + .panels-stack--editors { + grid-template-rows: minmax(0, 1fr) auto; +} + +.app-grid--preview-left.app-grid--component-collapsed.app-grid--styles-collapsed + .panels-stack--editors { + grid-template-rows: auto auto; + align-content: start; } .app-grid--preview-left.app-grid--preview-collapsed-horizontal { @@ -184,40 +257,85 @@ } .component-panel { - grid-area: component; - max-height: min(64vh, 620px); + max-height: var(--panel-max-height-default); min-height: 0; } .styles-panel { - grid-area: styles; - max-height: min(64vh, 620px); + max-height: var(--panel-max-height-default); min-height: 0; } .preview-panel { + max-height: var(--panel-max-height-default); grid-area: preview; position: relative; } +.app-grid--preview-right .preview-panel, +.app-grid--preview-left .preview-panel { + min-height: 0; + height: var(--side-editors-height, auto); + max-height: var(--side-editors-height, var(--panel-max-height-side)); + overflow: hidden; +} + +.app-grid--preview-right .preview-host, +.app-grid--preview-left .preview-host { + min-height: 0; +} + +.app-grid--preview-right .component-panel, +.app-grid--preview-right .styles-panel, +.app-grid--preview-left .component-panel, +.app-grid--preview-left .styles-panel { + min-height: 0; + max-height: var(--panel-max-height-side); +} + +.app-grid--preview-right + .component-panel:not(.panel--collapsed-vertical):not(.panel--collapsed-horizontal), +.app-grid--preview-right + .styles-panel:not(.panel--collapsed-vertical):not(.panel--collapsed-horizontal), +.app-grid--preview-left + .component-panel:not(.panel--collapsed-vertical):not(.panel--collapsed-horizontal), +.app-grid--preview-left + .styles-panel:not(.panel--collapsed-vertical):not(.panel--collapsed-horizontal) { + min-height: 360px; +} + @media (max-width: 900px) { .app-grid { grid-template-columns: minmax(0, 1fr); grid-template-areas: 'layout-controls' - 'component' - 'styles' + 'editors' 'preview'; } - .app-grid--preview-right.app-grid--preview-collapsed-horizontal, - .app-grid--preview-left.app-grid--preview-collapsed-horizontal { + .component-panel, + .styles-panel, + .preview-panel, + .app-grid--preview-right .component-panel, + .app-grid--preview-right .styles-panel, + .app-grid--preview-right .preview-panel, + .app-grid--preview-left .component-panel, + .app-grid--preview-left .styles-panel, + .app-grid--preview-left .preview-panel { + max-height: none; + } + + .panels-stack--editors { grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto; + height: auto; } - .app-grid.app-grid--component-collapsed-horizontal, - .app-grid.app-grid--styles-collapsed-horizontal { + .app-grid--preview-right.app-grid--preview-collapsed-horizontal, + .app-grid--preview-left.app-grid--preview-collapsed-horizontal { grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto; + min-height: 0; } .app-grid.app-grid--component-collapsed-horizontal.app-grid--styles-collapsed-horizontal { From 5d4aff9a5f72eae2ccdec03b442affae5c38cff7 Mon Sep 17 00:00:00 2001 From: KCM Date: Tue, 17 Mar 2026 13:38:29 -0500 Subject: [PATCH 3/6] refactor: address comments. --- docs/next-steps.md | 5 +++++ eslint.config.js | 3 +++ playwright/app.spec.ts | 23 ----------------------- src/app.js | 14 +++++++++++--- src/index.html | 23 ++++++++++++++++------- src/styles/panels-editor.css | 15 ++++++++++++++- 6 files changed, 49 insertions(+), 34 deletions(-) diff --git a/docs/next-steps.md b/docs/next-steps.md index 172733e..8e286a4 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -26,3 +26,8 @@ Focused follow-up work for `@knighted/develop`. 6. **Panel sizing without JS height sync** - Revisit the current side-layout preview height calculation and investigate a pure CSS replacement. - 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. + +7. **CDN failure recovery UX** + - Detect transient CDN/module loading failures and surface a clear recovery action in-app. + - Add a user-triggered retry path (for example, Reload page / Force reload) when runtime bootstrap imports fail. + - Consider an optional automatic one-time retry before showing recovery controls, while avoiding infinite reload loops. diff --git a/eslint.config.js b/eslint.config.js index 7adedfa..b368ba7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,6 +20,9 @@ export default [ sourceType: 'module', ecmaVersion: 'latest', }, + rules: { + 'no-unused-vars': 'error', + }, }, { ...playwrightConfig, diff --git a/playwright/app.spec.ts b/playwright/app.spec.ts index 3064bb3..34cdfd4 100644 --- a/playwright/app.spec.ts +++ b/playwright/app.spec.ts @@ -29,29 +29,6 @@ const setStylesEditorSource = async (page: Page, source: string) => { await editorContent.fill(source) } -const getLongListComponentSource = (count = 1200) => - [ - 'const App = () => {', - ` const items = Array.from({ length: ${count} }, (_, index) => ({`, - ' id: index + 1,', - ' title: `List item ${index + 1}`', - ' }))', - '', - ' return (', - "
", - "

Long Preview List

", - "
    ", - ' {items.map(item => (', - "
  • ", - ' {item.title}', - '
  • ', - ' ))}', - '
', - '
', - ' )', - '}', - ].join('\n') - const getCollapseButton = (page: Page, panelName: 'component' | 'styles' | 'preview') => page.locator(`#collapse-${panelName}`) diff --git a/src/app.js b/src/app.js index c610a94..daa69e3 100644 --- a/src/app.js +++ b/src/app.js @@ -82,6 +82,8 @@ const panelMap = { preview: previewPanel, } +const compactViewportMediaQuery = window.matchMedia('(max-width: 900px)') + const getCurrentLayout = () => { if (appGrid.classList.contains('app-grid--preview-right')) { return 'preview-right' @@ -94,7 +96,7 @@ const getCurrentLayout = () => { return 'default' } -const isCompactViewport = () => window.matchMedia('(max-width: 900px)').matches +const isCompactViewport = () => compactViewportMediaQuery.matches const getPanelCollapseAxis = panelName => { if (isCompactViewport()) { @@ -680,9 +682,15 @@ for (const button of panelCollapseButtons) { }) } -window.addEventListener('resize', () => { +const handleCompactViewportChange = () => { applyPanelCollapseState() -}) +} + +if (typeof compactViewportMediaQuery.addEventListener === 'function') { + compactViewportMediaQuery.addEventListener('change', handleCompactViewportChange) +} else { + compactViewportMediaQuery.onchange = handleCompactViewportChange +} if (typeof ResizeObserver !== 'undefined' && editorsStack) { const sidePreviewHeightObserver = new ResizeObserver(() => { diff --git a/src/index.html b/src/index.html index afd0dd2..d769ca8 100644 --- a/src/index.html +++ b/src/index.html @@ -142,7 +142,8 @@

Component

data-collapse-axis="vertical" data-collapsed="false" aria-expanded="true" - aria-controls="component-panel" + aria-controls="component-panel-content" + aria-label="Collapse component panel" title="Collapse component panel" >

Component

- +
+ +
@@ -217,7 +220,8 @@

Styles

data-collapse-axis="vertical" data-collapsed="false" aria-expanded="true" - aria-controls="styles-panel" + aria-controls="styles-panel-content" + aria-label="Collapse styles panel" title="Collapse styles panel" >

Styles

- - +
+ + +
@@ -302,7 +308,8 @@

Preview

data-collapse-axis="vertical" data-collapsed="false" aria-expanded="true" - aria-controls="preview-panel" + aria-controls="preview-panel-content" + aria-label="Collapse preview panel" title="Collapse preview panel" >

Preview

-
+
+
+