Skip to content

Commit 2b1610c

Browse files
Rich-HarrisRich Harris
and
Rich Harris
authored
better mobile layout (#259)
* mobile toggle * a11y * update lockfile * fix * fix tooltip positions * mobile filetree * debugging * wtf * throw CSS at the problem * remove debugging stuff * make view history-driven --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 4bda38b commit 2b1610c

12 files changed

+368
-153
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@lezer/javascript": "^1.4.1",
4848
"@lezer/lr": "^1.3.3",
4949
"@replit/codemirror-lang-svelte": "^6.0.0",
50-
"@rich_harris/svelte-split-pane": "^1.0.1",
50+
"@rich_harris/svelte-split-pane": "^1.0.2",
5151
"@webcontainer/api": "^1.0.2",
5252
"adm-zip": "^0.5.10",
5353
"ansi-to-html": "^0.7.2",

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 186 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
<script>
22
import Output from './Output.svelte';
3-
import { browser } from '$app/environment';
43
import { afterNavigate, beforeNavigate } from '$app/navigation';
54
import ContextMenu from './filetree/ContextMenu.svelte';
65
import Filetree from './filetree/Filetree.svelte';
76
import { SplitPane } from '@rich_harris/svelte-split-pane';
87
import Icon from '@sveltejs/site-kit/components/Icon.svelte';
9-
import { writable } from 'svelte/store';
108
import Editor from './Editor.svelte';
119
import ImageViewer from './ImageViewer.svelte';
1210
import ScreenToggle from './ScreenToggle.svelte';
@@ -23,20 +21,28 @@
2321
2422
export let data;
2523
26-
let width = browser ? window.innerWidth : 1000;
27-
let selected_view = 0;
28-
2924
let path = data.exercise.path;
25+
let show_editor = false;
26+
let show_filetree = false;
3027
let paused = false;
28+
let w = 1000;
3129
3230
/** @type {import('$lib/types').Stub[]} */
3331
let previous_files = [];
3432
33+
$: mobile = w < 800; // for the things we can't do with media queries
34+
$: completed = is_completed($files, data.exercise.b);
35+
$: files.set(Object.values(data.exercise.a));
36+
$: solution.set(data.exercise.b);
37+
$: selected_name.set(data.exercise.focus);
38+
3539
beforeNavigate(() => {
3640
previous_files = $files;
3741
});
3842
3943
afterNavigate(async () => {
44+
w = window.innerWidth;
45+
4046
const will_delete = previous_files.some((file) => !(file.name in data.exercise.a));
4147
4248
if (data.exercise.path !== path || will_delete) paused = true;
@@ -46,15 +52,6 @@
4652
paused = false;
4753
});
4854
49-
$: mobile = writable(false);
50-
$: $mobile = width < 768;
51-
52-
$: completed = is_completed($files, data.exercise.b);
53-
54-
$: files.set(Object.values(data.exercise.a));
55-
$: solution.set(data.exercise.b);
56-
$: selected_name.set(data.exercise.focus);
57-
5855
/**
5956
* @param {import('$lib/types').Stub[]} files
6057
* @param {Record<string, import('$lib/types').Stub> | null} solution
@@ -103,74 +100,132 @@
103100
<meta property="og:image" content="https://svelte.dev/images/twitter-thumbnail.jpg" />
104101
</svelte:head>
105102
103+
<svelte:window
104+
bind:innerWidth={w}
105+
on:popstate={(e) => {
106+
const q = new URLSearchParams(location.search);
107+
show_editor = q.get('view') === 'editor';
108+
}}
109+
/>
110+
106111
<ContextMenu />
107112
108-
<div class="container" style="--toggle-height: {$mobile ? '4.6rem' : '0px'}">
109-
<SplitPane
110-
type="horizontal"
111-
min={$mobile ? '0px' : '360px'}
112-
max={$mobile ? '100%' : '50%'}
113-
pos={$mobile ? (selected_view === 0 ? '100%' : '0%') : '33%'}
114-
>
115-
<section slot="a" class="content">
116-
<Sidebar
117-
index={data.index}
118-
exercise={data.exercise}
119-
on:select={(e) => {
120-
select_file(e.detail.file);
121-
}}
122-
/>
123-
</section>
124-
125-
<section slot="b" class:hidden={$mobile && selected_view === 0}>
126-
<SplitPane
127-
type="vertical"
128-
min={$mobile ? '0px' : '100px'}
129-
max={$mobile ? '100%' : '50%'}
130-
pos={$mobile ? (selected_view === 1 ? '100%' : '0%') : '50%'}
131-
>
132-
<section slot="a">
133-
<SplitPane type="horizontal" min="120px" max="300px" pos="200px">
134-
<section class="navigator" slot="a">
135-
<Filetree readonly={mobile} exercise={data.exercise} />
136-
137-
<button
138-
class:completed
139-
disabled={!data.exercise.has_solution}
140-
on:click={() => {
141-
reset_files(Object.values(completed ? data.exercise.a : data.exercise.b));
142-
}}
143-
>
144-
{#if completed && data.exercise.has_solution}
145-
reset
113+
<div class="container" class:mobile>
114+
<div class="top" class:offset={show_editor}>
115+
<SplitPane id="main" type="horizontal" min="360px" max="50%" pos="33%">
116+
<section slot="a" class="content">
117+
<Sidebar
118+
index={data.index}
119+
exercise={data.exercise}
120+
on:select={(e) => {
121+
select_file(e.detail.file);
122+
}}
123+
/>
124+
</section>
125+
126+
<section slot="b">
127+
<SplitPane type="vertical" min="100px" max="50%" pos="50%">
128+
<section slot="a">
129+
<SplitPane
130+
id="editor"
131+
type={mobile ? 'vertical' : 'horizontal'}
132+
min="120px"
133+
max="300px"
134+
pos="200px"
135+
>
136+
<section class="navigator" slot="a">
137+
{#if mobile}
138+
<button class="file" on:click={() => (show_filetree = !show_filetree)}>
139+
{$selected_file?.name.replace(
140+
data.exercise.scope.prefix,
141+
data.exercise.scope.name + '/'
142+
) ?? 'Files'}
143+
</button>
146144
{:else}
147-
solve <Icon name="arrow-right" />
145+
<Filetree exercise={data.exercise} />
148146
{/if}
149-
</button>
150-
</section>
151-
152-
<section class="editor-container" slot="b">
153-
<Editor />
154-
<ImageViewer selected={$selected_file} />
155-
</section>
156-
</SplitPane>
157-
</section>
158-
159-
<section slot="b" class="preview">
160-
<Output exercise={data.exercise} {paused} />
161-
</section>
162-
</SplitPane>
163-
</section>
164-
</SplitPane>
165-
{#if $mobile}
166-
<ScreenToggle labels={['Tutorial', 'Input', 'Output']} bind:selected={selected_view} />
167-
{/if}
147+
148+
<button
149+
class="solve"
150+
class:completed
151+
disabled={!data.exercise.has_solution}
152+
on:click={() => {
153+
reset_files(Object.values(completed ? data.exercise.a : data.exercise.b));
154+
}}
155+
>
156+
{#if completed && data.exercise.has_solution}
157+
reset
158+
{:else}
159+
solve <Icon name="arrow-right" />
160+
{/if}
161+
</button>
162+
</section>
163+
164+
<section class="editor-container" slot="b">
165+
<Editor />
166+
<ImageViewer selected={$selected_file} />
167+
168+
{#if mobile && show_filetree}
169+
<div class="mobile-filetree">
170+
<Filetree
171+
mobile
172+
exercise={data.exercise}
173+
on:select={() => (show_filetree = false)}
174+
/>
175+
</div>
176+
{/if}
177+
</section>
178+
</SplitPane>
179+
</section>
180+
181+
<section slot="b" class="preview">
182+
<Output exercise={data.exercise} {paused} />
183+
</section>
184+
</SplitPane>
185+
</section>
186+
</SplitPane>
187+
</div>
188+
189+
<div class="screen-toggle">
190+
<ScreenToggle
191+
on:change={(e) => {
192+
show_editor = e.detail.pressed;
193+
194+
const view = show_editor ? 'editor' : 'tutorial';
195+
history.pushState({}, '', `?view=${view}`);
196+
}}
197+
pressed={show_editor}
198+
/>
199+
</div>
168200
</div>
169201
170202
<style>
171203
.container {
172-
height: calc(100% - var(--toggle-height));
173-
max-height: 100%;
204+
display: flex;
205+
flex-direction: column;
206+
height: 100%;
207+
/** necessary for innerWidth to be correct, so we can determine `mobile` */
208+
width: 100vw;
209+
overflow: hidden;
210+
}
211+
212+
.top {
213+
width: 200vw;
214+
margin-left: -100vw;
215+
height: 0;
216+
flex: 1;
217+
transition: transform 0.2s;
218+
/* we transform the default state, rather than the editor state, because otherwise
219+
the positioning of tooltips is wrong (doesn't take into account transforms) */
220+
transform: translate(50%, 0);
221+
}
222+
223+
.top.offset {
224+
transform: none;
225+
}
226+
227+
.screen-toggle {
228+
height: 4.6rem;
174229
}
175230
176231
.content {
@@ -191,7 +246,7 @@
191246
flex-direction: column;
192247
}
193248
194-
.navigator button {
249+
.navigator .solve {
195250
position: relative;
196251
background: var(--sk-theme-2);
197252
padding: 0.5rem;
@@ -202,15 +257,15 @@
202257
opacity: 1;
203258
}
204259
205-
.navigator button:disabled {
260+
.navigator .solve:disabled {
206261
opacity: 0.5;
207262
}
208263
209-
.navigator button:not(:disabled) {
264+
.navigator .solve:not(:disabled) {
210265
background: var(--sk-theme-1);
211266
}
212267
213-
.navigator button.completed {
268+
.navigator .solve.completed {
214269
background: var(--sk-theme-2);
215270
}
216271
@@ -224,7 +279,58 @@
224279
background-color: var(--sk-back-3);
225280
}
226281
227-
.hidden {
228-
display: none;
282+
.mobile .navigator {
283+
display: flex;
284+
flex-direction: row;
285+
align-items: center;
286+
padding: 1rem;
287+
}
288+
289+
.mobile .navigator .file {
290+
flex: 1;
291+
text-align: left;
292+
}
293+
294+
.mobile .navigator .solve {
295+
width: 9rem;
296+
height: auto;
297+
padding: 0.2rem;
298+
border-radius: 4rem;
299+
border: none;
300+
}
301+
302+
.mobile-filetree {
303+
position: absolute;
304+
top: 0;
305+
width: 100%;
306+
height: 100%;
307+
overflow-y: auto;
308+
}
309+
310+
/* on mobile, override the <SplitPane> controls */
311+
@media (max-width: 799px) {
312+
:global([data-pane='main']) {
313+
--pos: 50% !important;
314+
}
315+
316+
:global([data-pane='editor']) {
317+
--pos: 5.4rem !important;
318+
}
319+
320+
:global([data-pane]) :global(.divider) {
321+
cursor: default;
322+
}
323+
}
324+
325+
@media (min-width: 800px) {
326+
.top {
327+
width: 100vw;
328+
margin: 0;
329+
transform: none;
330+
}
331+
332+
.screen-toggle {
333+
display: none;
334+
}
229335
}
230336
</style>

src/routes/tutorial/[slug]/Editor.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,10 @@
202202
editor_states.clear();
203203
reset($files);
204204
205-
select_state($selected_name);
205+
if (editor_view) {
206+
// could be false if onMount returned early
207+
select_state($selected_name);
208+
}
206209
207210
// clear warnings
208211
warnings.set({});

0 commit comments

Comments
 (0)