Skip to content

Commit f583881

Browse files
committed
feat(WebServer): use proportional viewport units for layout
- Change sidebar width from fixed 350px to 25vw - Change panel height from fixed 200px to 25vh - Update tutorial modal width to reference --sidebar-width - Raise sidebar min-width to 280px to match tutorial min-width - Update sash drag minimum to 280px for consistency - Add test_layout.js with 10 CSS constraint tests
1 parent 12ec84e commit f583881

4 files changed

Lines changed: 241 additions & 7 deletions

File tree

Tools/WebServer/static/css/tutorial.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
}
2020

2121
.tutorial-modal {
22-
width: 560px;
22+
width: calc(var(--sidebar-width) - 10px);
23+
min-width: 280px;
2324
max-width: 90vw;
2425
max-height: 80vh;
2526
display: flex;

Tools/WebServer/static/css/workbench.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@
7171
--sash-size: 4px;
7272
--sash-hover-color: var(--vscode-focus-border);
7373

74-
/* Layout - default values, can be overridden by JS */
75-
--sidebar-width: 350px;
76-
--panel-height: 200px;
74+
/* Layout - default proportions, can be overridden by JS */
75+
--sidebar-width: 25vw;
76+
--panel-height: 25vh;
7777
}
7878

7979
/* Light Theme */
@@ -341,7 +341,7 @@ body {
341341
overflow-y: auto;
342342
overflow-x: hidden;
343343
border-right: 1px solid var(--vscode-border);
344-
min-width: 180px;
344+
min-width: 280px;
345345
}
346346

347347
/* Scrollbar styling - VS Code style */

Tools/WebServer/static/js/ui/sash.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ function initSashResize() {
7777
if (isResizingSidebar) {
7878
const delta = e.clientX - startX;
7979
const newWidth = startWidth + delta;
80-
if (newWidth >= 150) {
80+
if (newWidth >= 280) {
8181
document.documentElement.style.setProperty(
8282
'--sidebar-width',
8383
newWidth + 'px',
@@ -102,7 +102,7 @@ function initSashResize() {
102102
const newWidth = startWidth + deltaX;
103103
const newHeight = startHeight + deltaY;
104104

105-
if (newWidth >= 150) {
105+
if (newWidth >= 280) {
106106
document.documentElement.style.setProperty(
107107
'--sidebar-width',
108108
newWidth + 'px',
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/**
2+
* Tests for layout constraints between workbench.css and tutorial.css
3+
*
4+
* Ensures sidebar default width >= tutorial modal width,
5+
* and layout proportions are within reasonable bounds.
6+
*/
7+
const fs = require('fs');
8+
const path = require('path');
9+
const { describe, it, assertTrue, assertEqual } = require('./framework');
10+
11+
// Parse CSS files at module load time
12+
const cssDir = path.join(__dirname, '..', '..', 'static', 'css');
13+
const workbenchCSS = fs.readFileSync(
14+
path.join(cssDir, 'workbench.css'),
15+
'utf-8',
16+
);
17+
const tutorialCSS = fs.readFileSync(path.join(cssDir, 'tutorial.css'), 'utf-8');
18+
19+
/**
20+
* Extract a CSS custom property default value from :root
21+
* e.g. "--sidebar-width: 25vw;" → "25vw"
22+
*/
23+
function extractCSSVar(css, varName) {
24+
const re = new RegExp(
25+
`${varName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}\\s*:\\s*([^;]+);`,
26+
);
27+
const m = css.match(re);
28+
return m ? m[1].trim() : null;
29+
}
30+
31+
/**
32+
* Extract a CSS property value from a selector block.
33+
* Uses a line-start anchor to avoid matching compound selectors
34+
* like ".tutorial-overlay .tutorial-modal" when looking for ".tutorial-modal".
35+
*/
36+
function extractPropertyFromBlock(css, selector, property) {
37+
// Split into lines and find the block that starts with exactly this selector
38+
const lines = css.split('\n');
39+
let inBlock = false;
40+
let braceDepth = 0;
41+
let blockContent = '';
42+
43+
for (const line of lines) {
44+
if (!inBlock) {
45+
const trimmed = line.trim();
46+
// Match line that is exactly "selector {" (not part of a compound selector)
47+
if (trimmed === selector + ' {' || trimmed === selector + '{') {
48+
inBlock = true;
49+
braceDepth = 1;
50+
blockContent = '';
51+
continue;
52+
}
53+
} else {
54+
for (const ch of line) {
55+
if (ch === '{') braceDepth++;
56+
if (ch === '}') braceDepth--;
57+
}
58+
if (braceDepth <= 0) break;
59+
blockContent += line + '\n';
60+
}
61+
}
62+
63+
if (!blockContent) return null;
64+
const propRe = new RegExp(`${property}\\s*:\\s*([^;]+);`);
65+
const propMatch = blockContent.match(propRe);
66+
return propMatch ? propMatch[1].trim() : null;
67+
}
68+
69+
/**
70+
* Parse a CSS value with unit, returns { value, unit }
71+
* e.g. "25vw" → { value: 25, unit: "vw" }
72+
* e.g. "350px" → { value: 350, unit: "px" }
73+
*/
74+
function parseCSSValue(str) {
75+
if (!str) return null;
76+
const m = str.match(/^([\d.]+)(px|vw|vh|%|em|rem)$/);
77+
if (!m) return null;
78+
return { value: parseFloat(m[1]), unit: m[2] };
79+
}
80+
81+
module.exports = function () {
82+
/* ===========================
83+
CSS Variable Extraction
84+
=========================== */
85+
86+
describe('Layout - CSS Variables Exist', () => {
87+
it('workbench.css defines --sidebar-width', () => {
88+
const val = extractCSSVar(workbenchCSS, '--sidebar-width');
89+
assertTrue(val !== null, '--sidebar-width not found in workbench.css');
90+
});
91+
92+
it('workbench.css defines --panel-height', () => {
93+
const val = extractCSSVar(workbenchCSS, '--panel-height');
94+
assertTrue(val !== null, '--panel-height not found in workbench.css');
95+
});
96+
97+
it('tutorial.css defines .tutorial-modal width', () => {
98+
const val = extractPropertyFromBlock(
99+
tutorialCSS,
100+
'.tutorial-modal',
101+
'width',
102+
);
103+
assertTrue(val !== null, '.tutorial-modal width not found');
104+
});
105+
106+
it('tutorial.css defines .tutorial-modal min-width', () => {
107+
const val = extractPropertyFromBlock(
108+
tutorialCSS,
109+
'.tutorial-modal',
110+
'min-width',
111+
);
112+
assertTrue(val !== null, '.tutorial-modal min-width not found');
113+
});
114+
});
115+
116+
/* ===========================
117+
Proportional Layout Checks
118+
=========================== */
119+
120+
describe('Layout - Sidebar Proportions', () => {
121+
it('sidebar width uses viewport-relative unit (vw)', () => {
122+
const val = extractCSSVar(workbenchCSS, '--sidebar-width');
123+
const parsed = parseCSSValue(val);
124+
assertTrue(
125+
parsed !== null && parsed.unit === 'vw',
126+
`Expected vw unit, got: ${val}`,
127+
);
128+
});
129+
130+
it('sidebar width is between 20vw and 40vw', () => {
131+
const val = extractCSSVar(workbenchCSS, '--sidebar-width');
132+
const parsed = parseCSSValue(val);
133+
assertTrue(parsed !== null, `Cannot parse: ${val}`);
134+
assertTrue(
135+
parsed.value >= 20 && parsed.value <= 40,
136+
`Sidebar ${parsed.value}vw out of [20, 40] range`,
137+
);
138+
});
139+
});
140+
141+
describe('Layout - Panel Proportions', () => {
142+
it('panel height uses viewport-relative unit (vh)', () => {
143+
const val = extractCSSVar(workbenchCSS, '--panel-height');
144+
const parsed = parseCSSValue(val);
145+
assertTrue(
146+
parsed !== null && parsed.unit === 'vh',
147+
`Expected vh unit, got: ${val}`,
148+
);
149+
});
150+
151+
it('panel height is between 15vh and 40vh', () => {
152+
const val = extractCSSVar(workbenchCSS, '--panel-height');
153+
const parsed = parseCSSValue(val);
154+
assertTrue(parsed !== null, `Cannot parse: ${val}`);
155+
assertTrue(
156+
parsed.value >= 15 && parsed.value <= 40,
157+
`Panel ${parsed.value}vh out of [15, 40] range`,
158+
);
159+
});
160+
});
161+
162+
/* ===========================
163+
Sidebar >= Tutorial Constraint
164+
=========================== */
165+
166+
describe('Layout - Sidebar >= Tutorial Width', () => {
167+
it('tutorial modal width is derived from --sidebar-width (not fixed px)', () => {
168+
const val = extractPropertyFromBlock(
169+
tutorialCSS,
170+
'.tutorial-modal',
171+
'width',
172+
);
173+
assertTrue(
174+
val.includes('var(--sidebar-width)'),
175+
`Tutorial width should reference --sidebar-width, got: ${val}`,
176+
);
177+
});
178+
179+
it('tutorial modal min-width is less than sidebar min-width', () => {
180+
// tutorial min-width
181+
const tutorialMinStr = extractPropertyFromBlock(
182+
tutorialCSS,
183+
'.tutorial-modal',
184+
'min-width',
185+
);
186+
const tutorialMin = parseCSSValue(tutorialMinStr);
187+
assertTrue(
188+
tutorialMin !== null && tutorialMin.unit === 'px',
189+
`Cannot parse tutorial min-width: ${tutorialMinStr}`,
190+
);
191+
192+
// sidebar min-width from .sidebar block
193+
const sidebarBlock = workbenchCSS.match(/\.sidebar\s*\{([^}]+)\}/);
194+
assertTrue(sidebarBlock !== null, '.sidebar block not found');
195+
const sidebarMinMatch = sidebarBlock[1].match(/min-width\s*:\s*([^;]+);/);
196+
assertTrue(sidebarMinMatch !== null, 'sidebar min-width not found');
197+
const sidebarMin = parseCSSValue(sidebarMinMatch[1].trim());
198+
assertTrue(
199+
sidebarMin !== null && sidebarMin.unit === 'px',
200+
`Cannot parse sidebar min-width: ${sidebarMinMatch[1]}`,
201+
);
202+
203+
assertTrue(
204+
tutorialMin.value <= sidebarMin.value,
205+
`Tutorial min-width (${tutorialMin.value}px) must be <= sidebar min-width (${sidebarMin.value}px)`,
206+
);
207+
});
208+
209+
it('sidebar min-width (CSS) >= sash drag minimum (JS)', () => {
210+
// sidebar CSS min-width
211+
const sidebarBlock = workbenchCSS.match(/\.sidebar\s*\{([^}]+)\}/);
212+
const sidebarMinMatch = sidebarBlock[1].match(/min-width\s*:\s*([^;]+);/);
213+
const sidebarMin = parseCSSValue(sidebarMinMatch[1].trim());
214+
215+
// sash.js drag minimum (hardcoded 150)
216+
const sashJS = fs.readFileSync(
217+
path.join(__dirname, '..', '..', 'static', 'js', 'ui', 'sash.js'),
218+
'utf-8',
219+
);
220+
const dragMinMatch = sashJS.match(/newWidth\s*>=\s*(\d+)/);
221+
assertTrue(
222+
dragMinMatch !== null,
223+
'Sash drag minimum not found in sash.js',
224+
);
225+
const dragMin = parseInt(dragMinMatch[1], 10);
226+
227+
assertTrue(
228+
sidebarMin.value >= dragMin,
229+
`Sidebar CSS min-width (${sidebarMin.value}px) must be >= sash drag min (${dragMin}px)`,
230+
);
231+
});
232+
});
233+
};

0 commit comments

Comments
 (0)