|
1 | 1 | <script>
|
2 | 2 | import Output from './Output.svelte';
|
3 |
| - import { browser } from '$app/environment'; |
4 | 3 | import { afterNavigate, beforeNavigate } from '$app/navigation';
|
5 | 4 | import ContextMenu from './filetree/ContextMenu.svelte';
|
6 | 5 | import Filetree from './filetree/Filetree.svelte';
|
7 | 6 | import { SplitPane } from '@rich_harris/svelte-split-pane';
|
8 | 7 | import Icon from '@sveltejs/site-kit/components/Icon.svelte';
|
9 |
| - import { writable } from 'svelte/store'; |
10 | 8 | import Editor from './Editor.svelte';
|
11 | 9 | import ImageViewer from './ImageViewer.svelte';
|
12 | 10 | import ScreenToggle from './ScreenToggle.svelte';
|
|
23 | 21 |
|
24 | 22 | export let data;
|
25 | 23 |
|
26 |
| - let width = browser ? window.innerWidth : 1000; |
27 |
| - let selected_view = 0; |
28 |
| -
|
29 | 24 | let path = data.exercise.path;
|
| 25 | + let show_editor = false; |
| 26 | + let show_filetree = false; |
30 | 27 | let paused = false;
|
| 28 | + let w = 1000; |
31 | 29 |
|
32 | 30 | /** @type {import('$lib/types').Stub[]} */
|
33 | 31 | let previous_files = [];
|
34 | 32 |
|
| 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 | +
|
35 | 39 | beforeNavigate(() => {
|
36 | 40 | previous_files = $files;
|
37 | 41 | });
|
38 | 42 |
|
39 | 43 | afterNavigate(async () => {
|
| 44 | + w = window.innerWidth; |
| 45 | +
|
40 | 46 | const will_delete = previous_files.some((file) => !(file.name in data.exercise.a));
|
41 | 47 |
|
42 | 48 | if (data.exercise.path !== path || will_delete) paused = true;
|
|
46 | 52 | paused = false;
|
47 | 53 | });
|
48 | 54 |
|
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 |
| -
|
58 | 55 | /**
|
59 | 56 | * @param {import('$lib/types').Stub[]} files
|
60 | 57 | * @param {Record<string, import('$lib/types').Stub> | null} solution
|
|
103 | 100 | <meta property="og:image" content="https://svelte.dev/images/twitter-thumbnail.jpg" />
|
104 | 101 | </svelte:head>
|
105 | 102 |
|
| 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 | +
|
106 | 111 | <ContextMenu />
|
107 | 112 |
|
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> |
146 | 144 | {:else}
|
147 |
| - solve <Icon name="arrow-right" /> |
| 145 | + <Filetree exercise={data.exercise} /> |
148 | 146 | {/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> |
168 | 200 | </div>
|
169 | 201 |
|
170 | 202 | <style>
|
171 | 203 | .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; |
174 | 229 | }
|
175 | 230 |
|
176 | 231 | .content {
|
|
191 | 246 | flex-direction: column;
|
192 | 247 | }
|
193 | 248 |
|
194 |
| - .navigator button { |
| 249 | + .navigator .solve { |
195 | 250 | position: relative;
|
196 | 251 | background: var(--sk-theme-2);
|
197 | 252 | padding: 0.5rem;
|
|
202 | 257 | opacity: 1;
|
203 | 258 | }
|
204 | 259 |
|
205 |
| - .navigator button:disabled { |
| 260 | + .navigator .solve:disabled { |
206 | 261 | opacity: 0.5;
|
207 | 262 | }
|
208 | 263 |
|
209 |
| - .navigator button:not(:disabled) { |
| 264 | + .navigator .solve:not(:disabled) { |
210 | 265 | background: var(--sk-theme-1);
|
211 | 266 | }
|
212 | 267 |
|
213 |
| - .navigator button.completed { |
| 268 | + .navigator .solve.completed { |
214 | 269 | background: var(--sk-theme-2);
|
215 | 270 | }
|
216 | 271 |
|
|
224 | 279 | background-color: var(--sk-back-3);
|
225 | 280 | }
|
226 | 281 |
|
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 | + } |
229 | 335 | }
|
230 | 336 | </style>
|
0 commit comments