Skip to content

Commit 6d75fee

Browse files
feat: collapsible panels.
1 parent 212028d commit 6d75fee

4 files changed

Lines changed: 504 additions & 3 deletions

File tree

src/app.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ const statusNode = document.getElementById('status')
1515
const appGrid = document.querySelector('.app-grid')
1616
const appGridLayoutButtons = document.querySelectorAll('[data-app-grid-layout]')
1717
const appThemeButtons = document.querySelectorAll('[data-app-theme]')
18+
const panelCollapseButtons = document.querySelectorAll('[data-panel-collapse]')
19+
const componentPanel = document.getElementById('component-panel')
20+
const stylesPanel = document.getElementById('styles-panel')
21+
const previewPanel = document.getElementById('preview-panel')
1822
const renderMode = document.getElementById('render-mode')
1923
const autoRenderToggle = document.getElementById('auto-render')
2024
const typecheckButton = document.getElementById('typecheck-button')
@@ -71,6 +75,185 @@ const layoutTheme = createLayoutThemeController({
7175
const { applyAppGridLayout, applyTheme, getInitialAppGridLayout, getInitialTheme } =
7276
layoutTheme
7377

78+
const panelMap = {
79+
component: componentPanel,
80+
styles: stylesPanel,
81+
preview: previewPanel,
82+
}
83+
84+
const getCurrentLayout = () => {
85+
if (appGrid.classList.contains('app-grid--preview-right')) {
86+
return 'preview-right'
87+
}
88+
89+
if (appGrid.classList.contains('app-grid--preview-left')) {
90+
return 'preview-left'
91+
}
92+
93+
return 'default'
94+
}
95+
96+
const isCompactViewport = () => window.matchMedia('(max-width: 900px)').matches
97+
98+
const getPanelCollapseAxis = panelName => {
99+
if (isCompactViewport()) {
100+
return 'vertical'
101+
}
102+
103+
const layout = getCurrentLayout()
104+
105+
if (panelName === 'preview') {
106+
return layout === 'default' ? 'vertical' : 'horizontal'
107+
}
108+
109+
if (panelName === 'component' || panelName === 'styles') {
110+
return layout === 'default' ? 'horizontal' : 'vertical'
111+
}
112+
113+
return 'vertical'
114+
}
115+
116+
const getPanelCollapseDirection = panelName => {
117+
const axis = getPanelCollapseAxis(panelName)
118+
if (axis !== 'horizontal') {
119+
return 'none'
120+
}
121+
122+
const layout = getCurrentLayout()
123+
124+
if (panelName === 'preview') {
125+
return layout === 'preview-left' ? 'left' : 'right'
126+
}
127+
128+
if (panelName === 'component') {
129+
return 'left'
130+
}
131+
132+
if (panelName === 'styles') {
133+
return 'right'
134+
}
135+
136+
return 'right'
137+
}
138+
139+
const panelCollapseState = {
140+
component: false,
141+
styles: false,
142+
preview: false,
143+
}
144+
145+
const normalizePanelCollapseState = () => {
146+
const collapsedPanels = Object.entries(panelCollapseState)
147+
.filter(([, isCollapsed]) => isCollapsed)
148+
.map(([panelName]) => panelName)
149+
150+
if (collapsedPanels.length === Object.keys(panelCollapseState).length) {
151+
panelCollapseState.preview = false
152+
}
153+
}
154+
155+
const syncPanelCollapseButtons = () => {
156+
const collapsedCount = Object.values(panelCollapseState).filter(Boolean).length
157+
158+
for (const button of panelCollapseButtons) {
159+
const panelName = button.dataset.panelCollapse
160+
if (!panelName || !(panelName in panelMap)) {
161+
continue
162+
}
163+
164+
const axis = getPanelCollapseAxis(panelName)
165+
const direction = getPanelCollapseDirection(panelName)
166+
const isCollapsed = panelCollapseState[panelName] === true
167+
const panelTitle = `${panelName.charAt(0).toUpperCase()}${panelName.slice(1)}`
168+
const canCollapse = isCollapsed || collapsedCount < 2
169+
170+
button.dataset.collapseAxis = axis
171+
button.dataset.collapseDirection = direction
172+
button.dataset.collapsed = isCollapsed ? 'true' : 'false'
173+
button.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true')
174+
button.disabled = !canCollapse
175+
button.setAttribute('aria-disabled', canCollapse ? 'false' : 'true')
176+
button.setAttribute(
177+
'aria-label',
178+
`${isCollapsed ? 'Expand' : 'Collapse'} ${panelTitle.toLowerCase()} panel`,
179+
)
180+
button.setAttribute(
181+
'title',
182+
canCollapse
183+
? `${isCollapsed ? 'Expand' : 'Collapse'} ${panelTitle.toLowerCase()} panel`
184+
: 'At least one panel must remain expanded.',
185+
)
186+
}
187+
}
188+
189+
const applyPanelCollapseState = () => {
190+
normalizePanelCollapseState()
191+
192+
const previewAxis = getPanelCollapseAxis('preview')
193+
const componentAxis = getPanelCollapseAxis('component')
194+
const stylesAxis = getPanelCollapseAxis('styles')
195+
196+
if (componentPanel) {
197+
const isCollapsed = panelCollapseState.component
198+
componentPanel.classList.toggle(
199+
'panel--collapsed-vertical',
200+
isCollapsed && componentAxis === 'vertical',
201+
)
202+
componentPanel.classList.toggle(
203+
'panel--collapsed-horizontal',
204+
isCollapsed && componentAxis === 'horizontal',
205+
)
206+
}
207+
208+
if (stylesPanel) {
209+
const isCollapsed = panelCollapseState.styles
210+
stylesPanel.classList.toggle(
211+
'panel--collapsed-vertical',
212+
isCollapsed && stylesAxis === 'vertical',
213+
)
214+
stylesPanel.classList.toggle(
215+
'panel--collapsed-horizontal',
216+
isCollapsed && stylesAxis === 'horizontal',
217+
)
218+
}
219+
220+
if (previewPanel) {
221+
const isCollapsed = panelCollapseState.preview
222+
previewPanel.classList.toggle(
223+
'panel--collapsed-vertical',
224+
isCollapsed && previewAxis === 'vertical',
225+
)
226+
previewPanel.classList.toggle(
227+
'panel--collapsed-horizontal',
228+
isCollapsed && previewAxis === 'horizontal',
229+
)
230+
}
231+
232+
appGrid.classList.toggle(
233+
'app-grid--preview-collapsed-horizontal',
234+
panelCollapseState.preview && previewAxis === 'horizontal',
235+
)
236+
appGrid.classList.toggle(
237+
'app-grid--component-collapsed-horizontal',
238+
panelCollapseState.component && componentAxis === 'horizontal',
239+
)
240+
appGrid.classList.toggle(
241+
'app-grid--styles-collapsed-horizontal',
242+
panelCollapseState.styles && stylesAxis === 'horizontal',
243+
)
244+
245+
syncPanelCollapseButtons()
246+
}
247+
248+
const togglePanelCollapse = panelName => {
249+
if (!(panelName in panelCollapseState)) {
250+
return
251+
}
252+
253+
panelCollapseState[panelName] = !panelCollapseState[panelName]
254+
applyPanelCollapseState()
255+
}
256+
74257
const diagnosticsUi = createDiagnosticsUiController({
75258
diagnosticsToggle,
76259
diagnosticsDrawer,
@@ -451,6 +634,7 @@ for (const button of appGridLayoutButtons) {
451634
return
452635
}
453636
applyAppGridLayout(nextLayout)
637+
applyPanelCollapseState()
454638
})
455639
}
456640

@@ -464,8 +648,24 @@ for (const button of appThemeButtons) {
464648
})
465649
}
466650

651+
for (const button of panelCollapseButtons) {
652+
button.addEventListener('click', () => {
653+
const panelName = button.dataset.panelCollapse
654+
if (!panelName) {
655+
return
656+
}
657+
658+
togglePanelCollapse(panelName)
659+
})
660+
}
661+
662+
window.addEventListener('resize', () => {
663+
applyPanelCollapseState()
664+
})
665+
467666
applyAppGridLayout(getInitialAppGridLayout(), { persist: false })
468667
applyTheme(getInitialTheme(), { persist: false })
668+
applyPanelCollapseState()
469669

470670
updateRenderButtonVisibility()
471671
renderDiagnosticsScope('component')

src/index.html

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,26 @@ <h1>
129129
</div>
130130
</div>
131131

132-
<section class="panel component-panel">
132+
<section class="panel component-panel" id="component-panel">
133133
<div class="panel-header panel-header--grid">
134134
<h2>Component</h2>
135135
<div class="controls controls--quick-actions panel-header-quick-actions">
136+
<button
137+
class="panel-collapse-toggle"
138+
id="collapse-component"
139+
type="button"
140+
data-panel-collapse="component"
141+
data-collapse-axis="vertical"
142+
data-collapsed="false"
143+
aria-expanded="true"
144+
aria-controls="component-panel"
145+
title="Collapse component panel"
146+
>
147+
<svg viewBox="0 0 24 24" aria-hidden="true">
148+
<path d="m6 9 6 6 6-6"></path>
149+
</svg>
150+
<span class="panel-collapse-toggle__label">Collapse</span>
151+
</button>
136152
<button
137153
class="icon-button"
138154
id="copy-component"
@@ -186,10 +202,26 @@ <h2>Component</h2>
186202
<textarea id="jsx-editor" spellcheck="false"></textarea>
187203
</section>
188204

189-
<section class="panel styles-panel">
205+
<section class="panel styles-panel" id="styles-panel">
190206
<div class="panel-header panel-header--grid">
191207
<h2>Styles</h2>
192208
<div class="controls controls--quick-actions panel-header-quick-actions">
209+
<button
210+
class="panel-collapse-toggle"
211+
id="collapse-styles"
212+
type="button"
213+
data-panel-collapse="styles"
214+
data-collapse-axis="vertical"
215+
data-collapsed="false"
216+
aria-expanded="true"
217+
aria-controls="styles-panel"
218+
title="Collapse styles panel"
219+
>
220+
<svg viewBox="0 0 24 24" aria-hidden="true">
221+
<path d="m6 9 6 6 6-6"></path>
222+
</svg>
223+
<span class="panel-collapse-toggle__label">Collapse</span>
224+
</button>
193225
<button
194226
class="icon-button"
195227
id="copy-styles"
@@ -236,7 +268,7 @@ <h2>Styles</h2>
236268
<div class="panel-footer" id="style-warning"></div>
237269
</section>
238270

239-
<section class="panel preview preview-panel">
271+
<section class="panel preview preview-panel" id="preview-panel">
240272
<div class="panel-header">
241273
<h2>Preview</h2>
242274
<div class="controls">
@@ -256,6 +288,22 @@ <h2>Preview</h2>
256288
>
257289
i
258290
</button>
291+
<button
292+
class="panel-collapse-toggle"
293+
id="collapse-preview"
294+
type="button"
295+
data-panel-collapse="preview"
296+
data-collapse-axis="vertical"
297+
data-collapsed="false"
298+
aria-expanded="true"
299+
aria-controls="preview-panel"
300+
title="Collapse preview panel"
301+
>
302+
<svg viewBox="0 0 24 24" aria-hidden="true">
303+
<path d="m6 9 6 6 6-6"></path>
304+
</svg>
305+
<span class="panel-collapse-toggle__label">Collapse</span>
306+
</button>
259307
</div>
260308
</div>
261309
<div class="preview-host" id="preview-host"></div>

src/styles/layout-shell.css

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,52 @@
7878
padding: 24px;
7979
}
8080

81+
.app-grid.app-grid--component-collapsed-horizontal:not(.app-grid--preview-right):not(
82+
.app-grid--preview-left
83+
) {
84+
grid-template-columns: 72px minmax(320px, 1fr);
85+
}
86+
87+
.app-grid.app-grid--styles-collapsed-horizontal:not(.app-grid--preview-right):not(
88+
.app-grid--preview-left
89+
) {
90+
grid-template-columns: minmax(320px, 1fr) 72px;
91+
}
92+
93+
.app-grid.app-grid--component-collapsed-horizontal.app-grid--styles-collapsed-horizontal:not(
94+
.app-grid--preview-right
95+
):not(.app-grid--preview-left) {
96+
grid-template-columns: 72px 72px minmax(0, 1fr);
97+
grid-template-rows: auto auto minmax(320px, 1fr);
98+
grid-template-areas:
99+
'layout-controls layout-controls layout-controls'
100+
'component styles .'
101+
'preview preview preview';
102+
min-height: max(520px, calc(100dvh - 210px));
103+
}
104+
81105
.app-grid--preview-right {
82106
grid-template-areas:
83107
'layout-controls layout-controls'
84108
'component preview'
85109
'styles preview';
86110
}
87111

112+
.app-grid--preview-right.app-grid--preview-collapsed-horizontal {
113+
grid-template-columns: minmax(320px, 1fr) 72px;
114+
}
115+
88116
.app-grid--preview-left {
89117
grid-template-areas:
90118
'layout-controls layout-controls'
91119
'preview component'
92120
'preview styles';
93121
}
94122

123+
.app-grid--preview-left.app-grid--preview-collapsed-horizontal {
124+
grid-template-columns: 72px minmax(320px, 1fr);
125+
}
126+
95127
.app-grid-layout-controls {
96128
grid-area: layout-controls;
97129
display: inline-flex;
@@ -178,6 +210,21 @@
178210
'preview';
179211
}
180212

213+
.app-grid--preview-right.app-grid--preview-collapsed-horizontal,
214+
.app-grid--preview-left.app-grid--preview-collapsed-horizontal {
215+
grid-template-columns: minmax(0, 1fr);
216+
}
217+
218+
.app-grid.app-grid--component-collapsed-horizontal,
219+
.app-grid.app-grid--styles-collapsed-horizontal {
220+
grid-template-columns: minmax(0, 1fr);
221+
}
222+
223+
.app-grid.app-grid--component-collapsed-horizontal.app-grid--styles-collapsed-horizontal {
224+
grid-template-rows: auto;
225+
min-height: 0;
226+
}
227+
181228
.app-grid-layout-controls {
182229
justify-self: start;
183230
}

0 commit comments

Comments
 (0)