|
1 | | -<script> |
| 1 | +<script lang="ts"> |
2 | 2 | import { afterNavigate, beforeNavigate } from '$app/navigation'; |
3 | 3 | import { SplitPane } from '@rich_harris/svelte-split-pane'; |
4 | | - import { reset } from './adapter.svelte'; |
| 4 | + import { adapter_state, reset } from './adapter.svelte'; |
5 | 5 | import Editor from './Editor.svelte'; |
6 | 6 | import ContextMenu from './filetree/ContextMenu.svelte'; |
7 | 7 | import Filetree from './filetree/Filetree.svelte'; |
|
10 | 10 | import { ScreenToggle } from '@sveltejs/site-kit/components'; |
11 | 11 | import Sidebar from './Sidebar.svelte'; |
12 | 12 | import { |
13 | | - create_directories, |
14 | 13 | creating, |
15 | 14 | files, |
16 | 15 | reset_files, |
17 | 16 | selected_file, |
18 | 17 | selected_name, |
19 | 18 | solution |
20 | | - } from './state.js'; |
| 19 | + } from './state.svelte'; |
| 20 | + import { create_directories } from './utils'; |
21 | 21 | import { needs_webcontainers, text_files } from './shared'; |
22 | 22 | import OutputRollup from './OutputRollup.svelte'; |
23 | 23 | import { page } from '$app/stores'; |
24 | 24 | import Controls from './Controls.svelte'; |
| 25 | + import type { Stub } from '$lib/tutorial'; |
| 26 | + import type { Snapshot } from './$types.js'; |
25 | 27 |
|
26 | | - export let data; |
| 28 | + interface Props { |
| 29 | + data: any; |
| 30 | + } |
| 31 | +
|
| 32 | + let { data }: Props = $props(); |
27 | 33 |
|
28 | 34 | let path = data.exercise.path; |
29 | | - let show_editor = false; |
30 | | - let show_filetree = false; |
31 | | - let paused = false; |
32 | | - let w = 1000; |
| 35 | + let show_editor = $state(false); |
| 36 | + let show_filetree = $state(false); |
| 37 | + let paused = $state(false); |
| 38 | + let w = $state(1000); |
33 | 39 |
|
34 | | - /** @type {import('$lib/tutorial').Stub[]} */ |
35 | | - let previous_files = []; |
| 40 | + let previous_files: Stub[] = []; |
36 | 41 |
|
37 | | - /** |
38 | | - * @param {Record<string, string>} map |
39 | | - * @returns {Record<string, import('$lib/tutorial').Stub>} |
40 | | - */ |
41 | | - function create_files(map) { |
42 | | - /** @type {Record<string, import('$lib/tutorial').Stub>} */ |
43 | | - const files = {}; |
| 42 | + function create_files(map: Record<string, string>): Record<string, Stub> { |
| 43 | + const files: Record<string, Stub> = {}; |
44 | 44 |
|
45 | | - /** @type {string[]} */ |
46 | | - const to_delete = []; |
| 45 | + const to_delete: string[] = []; |
47 | 46 |
|
48 | 47 | for (const key in map) { |
49 | 48 | const contents = map[key]; |
|
53 | 52 | } |
54 | 53 |
|
55 | 54 | const parts = key.split('/'); |
56 | | - const basename = /** @type {string} */ (parts.pop()); |
| 55 | + const basename = parts.pop()!; |
57 | 56 | const ext = basename.slice(basename.lastIndexOf('.')); |
58 | 57 |
|
59 | 58 | if (basename === '__delete') { |
|
63 | 62 |
|
64 | 63 | while (parts.length > 0) { |
65 | 64 | const dir = `/${parts.join('/')}`; |
66 | | - const basename = /** @type {string} */ (parts.pop()); |
| 65 | + const basename = parts.pop()!; |
67 | 66 |
|
68 | 67 | files[dir] ??= { |
69 | 68 | type: 'directory', |
|
94 | 93 | return files; |
95 | 94 | } |
96 | 95 |
|
97 | | - $: a = create_files(data.exercise.a); |
98 | | - $: b = create_files({ ...data.exercise.a, ...data.exercise.b }); |
99 | | -
|
100 | | - $: mobile = w < 800; // for the things we can't do with media queries |
101 | | - $: files.set(Object.values(a)); |
102 | | - $: solution.set(b); |
103 | | - $: selected_name.set(data.exercise.focus); |
104 | | - $: completed = is_completed($files, b); |
| 96 | + // for the things we can't do with media queries |
105 | 97 |
|
106 | 98 | beforeNavigate(() => { |
107 | 99 | previous_files = $files; |
|
119 | 111 | paused = false; |
120 | 112 | }); |
121 | 113 |
|
122 | | - /** |
123 | | - * @param {import('$lib/tutorial').Stub[]} files |
124 | | - * @param {Record<string, import('$lib/tutorial').Stub> | null} solution |
125 | | - */ |
126 | | - function is_completed(files, solution) { |
| 114 | + function is_completed(files: Stub[], solution: Record<string, Stub> | null) { |
127 | 115 | if (!solution) return true; |
128 | 116 |
|
129 | 117 | for (const file of files) { |
|
143 | 131 | return true; |
144 | 132 | } |
145 | 133 |
|
146 | | - /** @param {string} code */ |
147 | | - function normalise(code) { |
| 134 | + function normalise(code: string) { |
148 | 135 | // TODO think about more sophisticated normalisation (e.g. truncate multiple newlines) |
149 | 136 | return code.replace(/\s+/g, ' ').trim(); |
150 | 137 | } |
151 | 138 |
|
152 | | - /** @param {string | null} name */ |
153 | | - function select_file(name) { |
| 139 | + function select_file(name: string | null) { |
154 | 140 | const file = name && $files.find((file) => file.name === name); |
155 | 141 |
|
156 | 142 | if (!file && name) { |
|
178 | 164 | show_editor = true; |
179 | 165 | } |
180 | 166 |
|
181 | | - /** @param {string} name */ |
182 | | - function navigate_to_file(name) { |
| 167 | + function navigate_to_file(name: string) { |
183 | 168 | if (name === $selected_name) return; |
184 | 169 |
|
185 | 170 | select_file(name); |
|
190 | 175 | } |
191 | 176 | } |
192 | 177 |
|
193 | | - /** @type {HTMLElement} */ |
194 | | - let sidebar; |
| 178 | + let sidebar = $state() as HTMLElement; |
195 | 179 |
|
196 | | - /** @type {import('./$types').Snapshot<number>} */ |
197 | | - export const snapshot = { |
| 180 | + // TODO this doesn't seem to work any more? |
| 181 | + export const snapshot: Snapshot<number> = { |
198 | 182 | capture: () => { |
199 | 183 | const scroll = sidebar.scrollTop; |
200 | 184 | sidebar.scrollTop = 0; |
|
204 | 188 | sidebar.scrollTop = scroll; |
205 | 189 | } |
206 | 190 | }; |
| 191 | +
|
| 192 | + let a = $derived(create_files(data.exercise.a)); |
| 193 | + let b = $derived(create_files({ ...data.exercise.a, ...data.exercise.b })); |
| 194 | + let mobile = $derived(w < 800); |
| 195 | +
|
| 196 | + $effect(() => { |
| 197 | + files.set(Object.values(a)); |
| 198 | + }); |
| 199 | +
|
| 200 | + $effect(() => { |
| 201 | + solution.set(b); |
| 202 | + }); |
| 203 | +
|
| 204 | + $effect(() => { |
| 205 | + selected_name.set(data.exercise.focus); |
| 206 | + }); |
| 207 | +
|
| 208 | + let completed = $derived(is_completed($files, b)); |
207 | 209 | </script> |
208 | 210 |
|
209 | 211 | <svelte:head> |
|
225 | 227 |
|
226 | 228 | <svelte:window |
227 | 229 | bind:innerWidth={w} |
228 | | - on:popstate={(e) => { |
| 230 | + onpopstate={(e) => { |
229 | 231 | const q = new URLSearchParams(location.search); |
230 | 232 | const file = q.get('file'); |
231 | 233 |
|
|
272 | 274 | max="300px" |
273 | 275 | pos="200px" |
274 | 276 | > |
275 | | - <section class="navigator" slot="a"> |
| 277 | + <section slot="a" class="navigator"> |
276 | 278 | {#if mobile} |
277 | | - <button class="file" on:click={() => (show_filetree = !show_filetree)}> |
| 279 | + <button class="file" onclick={() => (show_filetree = !show_filetree)}> |
278 | 280 | {$selected_file?.name.replace( |
279 | 281 | data.exercise.scope.prefix, |
280 | 282 | data.exercise.scope.name + '/' |
|
290 | 292 | {/if} |
291 | 293 | </section> |
292 | 294 |
|
293 | | - <section class="editor-container" slot="b"> |
294 | | - <Editor exercise={data.exercise} /> |
| 295 | + <section slot="b" class="editor-container"> |
| 296 | + <Editor exercise={data.exercise} warnings={adapter_state.warnings} /> |
295 | 297 | <ImageViewer selected={$selected_file} /> |
296 | 298 |
|
297 | 299 | {#if mobile && show_filetree} |
|
0 commit comments