|
1 | 1 | <script lang="ts">
|
2 |
| - import { forcefocus } from '@sveltejs/site-kit/actions'; |
3 |
| - import { tick } from 'svelte'; |
4 | 2 | import RunesInfo from './RunesInfo.svelte';
|
5 | 3 | import Migrate from './Migrate.svelte';
|
6 | 4 | import type { Workspace, File } from 'editor';
|
| 5 | + import { tick } from 'svelte'; |
7 | 6 |
|
8 | 7 | interface Props {
|
9 | 8 | runes: boolean;
|
|
14 | 13 |
|
15 | 14 | let { runes, onchange, workspace, can_migrate }: Props = $props();
|
16 | 15 |
|
17 |
| - let editing_name: string | null = $state(null); |
18 |
| - let input_value = $state(''); |
19 |
| -
|
20 |
| - function select_file(filename: string) { |
21 |
| - if (workspace.current.name !== filename) { |
22 |
| - editing_name = null; |
23 |
| - workspace.select(filename); |
24 |
| - } |
25 |
| - } |
26 |
| -
|
27 |
| - function edit_tab(file: File) { |
28 |
| - if (workspace.current.name === file.name) { |
29 |
| - editing_name = file.name; |
30 |
| - input_value = file.name; |
31 |
| - } |
32 |
| - } |
| 16 | + let input = $state() as HTMLInputElement; |
| 17 | + let input_value = $state(workspace.current.name); |
33 | 18 |
|
34 |
| - async function close_edit() { |
35 |
| - if (input_value === editing_name) { |
| 19 | + async function close_edit(file: File) { |
| 20 | + if (input_value === file.name || input_value === '') { |
36 | 21 | // nothing to do
|
37 |
| - editing_name = null; |
| 22 | + input_value = file.name; |
38 | 23 | return;
|
39 | 24 | }
|
40 | 25 |
|
41 |
| - const edited_file = (workspace.files as File[]).find((val) => val.name === editing_name); |
42 |
| - if (!edited_file) return; // TODO can this happen? |
| 26 | + const deconflicted = deconflict(input_value, file); |
43 | 27 |
|
44 |
| - const deconflicted = deconflict(input_value, edited_file); |
45 |
| -
|
46 |
| - workspace.rename(edited_file, deconflicted); |
| 28 | + workspace.rename(file, deconflicted); |
47 | 29 | workspace.focus();
|
48 |
| -
|
49 |
| - editing_name = null; |
50 | 30 | }
|
51 | 31 |
|
52 | 32 | function deconflict(name: string, file?: File) {
|
|
70 | 50 | onchange();
|
71 | 51 | }
|
72 | 52 |
|
73 |
| - function add_new() { |
| 53 | + async function add_new() { |
74 | 54 | const basename = deconflict(`Component.svelte`);
|
75 | 55 |
|
76 | 56 | const file = workspace.add({
|
|
81 | 61 | text: true
|
82 | 62 | });
|
83 | 63 |
|
84 |
| - editing_name = file.name; |
85 | 64 | input_value = file.name;
|
86 |
| -
|
87 | 65 | onchange();
|
| 66 | +
|
| 67 | + await tick(); |
| 68 | + input.focus(); |
88 | 69 | }
|
89 | 70 |
|
90 | 71 | // drag and drop
|
|
97 | 78 | <div class="file-tabs">
|
98 | 79 | {#each workspace.files as File[] as file, index (file.name)}
|
99 | 80 | <div
|
100 |
| - id={file.name} |
101 | 81 | class="button"
|
| 82 | + class:editable={file.name !== 'App.svelte'} |
102 | 83 | role="button"
|
103 | 84 | tabindex="0"
|
104 |
| - class:active={file.name === workspace.current.name} |
105 |
| - class:draggable={file.name !== editing_name && index !== 0} |
| 85 | + aria-current={file === workspace.current} |
106 | 86 | class:drag-over={file === dragover}
|
107 |
| - onclick={() => select_file(file.name)} |
108 |
| - onkeyup={(e) => e.key === ' ' && select_file(file.name)} |
109 |
| - draggable={file.name !== editing_name} |
| 87 | + onclick={() => { |
| 88 | + workspace.select(file.name); |
| 89 | + input_value = file.name; |
| 90 | + }} |
| 91 | + onkeyup={(e) => e.key === ' ' && workspace.select(file.name)} |
| 92 | + draggable="true" |
110 | 93 | ondragstart={() => (dragging = file)}
|
111 | 94 | ondragover={(e) => (e.preventDefault(), (dragover = file))}
|
112 | 95 | ondragleave={(e) => (e.preventDefault(), (dragover = null))}
|
|
120 | 103 | >
|
121 | 104 | <i class="drag-handle"></i>
|
122 | 105 |
|
123 |
| - {#if file.name === 'App.svelte'} |
124 |
| - <div class="uneditable"> |
125 |
| - App.svelte{#if workspace.modified[file.name]}*{/if} |
126 |
| - </div> |
127 |
| - {:else if file.name === editing_name} |
128 |
| - <span class="input-sizer"> |
129 |
| - <span style="color: transparent">{input_value}</span> |
130 |
| - </span> |
| 106 | + <span class="filename"> |
| 107 | + {(file === workspace.current && file.name !== 'App.svelte' ? input_value : file.name) + |
| 108 | + (workspace.modified[file.name] ? '*' : '') || ' '} |
| 109 | + </span> |
131 | 110 |
|
| 111 | + {#if file === workspace.current && file.name !== 'App.svelte'} |
132 | 112 | <!-- svelte-ignore a11y_autofocus -->
|
133 | 113 | <input
|
134 |
| - use:forcefocus |
135 | 114 | spellcheck={false}
|
| 115 | + bind:this={input} |
136 | 116 | bind:value={input_value}
|
137 | 117 | onfocus={async (event) => {
|
138 | 118 | const input = event.currentTarget;
|
139 |
| - await tick(); |
140 |
| - input.select(); |
| 119 | + setTimeout(() => { |
| 120 | + input.select(); |
| 121 | + }); |
141 | 122 | }}
|
142 |
| - onblur={close_edit} |
| 123 | + onblur={() => close_edit(file)} |
143 | 124 | onkeydown={(e) => {
|
144 | 125 | if (e.key === 'Enter') {
|
145 | 126 | e.preventDefault();
|
146 | 127 | e.currentTarget.blur();
|
147 | 128 | }
|
| 129 | + |
| 130 | + if (e.key === 'Escape') { |
| 131 | + input_value = file.name; |
| 132 | + e.currentTarget.blur(); |
| 133 | + } |
148 | 134 | }}
|
149 | 135 | />
|
150 |
| - {:else} |
151 |
| - <div |
152 |
| - class="editable" |
153 |
| - title="edit component name" |
154 |
| - onclick={() => edit_tab(file)} |
155 |
| - onkeyup={(e) => e.key === ' ' && edit_tab(file)} |
156 |
| - > |
157 |
| - {file.name}{#if workspace.modified[file.name]}*{/if} |
158 |
| - </div> |
159 | 136 |
|
160 | 137 | <span
|
161 | 138 | class="remove"
|
|
166 | 143 | }}
|
167 | 144 | onkeyup={(e) => e.key === ' ' && remove_file(file)}
|
168 | 145 | >
|
169 |
| - <svg width="12" height="12" viewBox="0 0 24 24"> |
| 146 | + <svg viewBox="0 0 24 24"> |
170 | 147 | <line stroke="#999" x1="18" y1="6" x2="6" y2="18" />
|
171 | 148 | <line stroke="#999" x1="6" y1="6" x2="18" y2="18" />
|
172 | 149 | </svg>
|
|
176 | 153 | {/each}
|
177 | 154 | </div>
|
178 | 155 |
|
179 |
| - <button class="add-new" onclick={add_new} aria-label="add new component" title="add new component" |
| 156 | + <button |
| 157 | + class="raised add-new" |
| 158 | + onclick={add_new} |
| 159 | + aria-label="add new component" |
| 160 | + title="add new component" |
180 | 161 | ></button>
|
181 | 162 |
|
182 | 163 | <div class="runes">
|
|
189 | 170 | .component-selector {
|
190 | 171 | position: relative;
|
191 | 172 | display: flex;
|
| 173 | + gap: 0.5rem; |
| 174 | + align-items: center; |
192 | 175 | padding: 0 1rem 0 0;
|
193 | 176 |
|
194 | 177 | /* fake border (allows tab borders to appear above it) */
|
|
206 | 189 | .file-tabs {
|
207 | 190 | border: none;
|
208 | 191 | margin: 0;
|
| 192 | + height: 100%; |
209 | 193 | white-space: nowrap;
|
210 | 194 | overflow-x: auto;
|
211 | 195 | overflow-y: hidden;
|
212 | 196 | }
|
213 | 197 |
|
214 |
| - .file-tabs .button, |
215 |
| - .add-new { |
| 198 | + .file-tabs .button { |
216 | 199 | position: relative;
|
217 | 200 | display: inline-flex;
|
218 | 201 | align-items: center;
|
|
221 | 204 | border: none;
|
222 | 205 | padding: 0 1rem;
|
223 | 206 | height: 100%;
|
224 |
| - aspect-ratio: 1; |
225 |
| - margin: 0; |
226 |
| - border-radius: 0; |
227 | 207 | cursor: pointer;
|
228 | 208 | }
|
229 | 209 |
|
230 |
| - .add-new { |
231 |
| - background: url(./file-new.svg) 50% 50% no-repeat; |
232 |
| - background-size: 1em; |
233 |
| - } |
234 |
| -
|
235 | 210 | .file-tabs .button {
|
236 |
| - padding: 0 1rem 0 2em; |
| 211 | + --padding-left: 2.6rem; |
| 212 | + --padding-right: 1.4rem; |
| 213 | + padding: 0 var(--padding-right) 0 var(--padding-left); |
| 214 | +
|
| 215 | + &.editable { |
| 216 | + --padding-right: 1.8rem; |
| 217 | + } |
237 | 218 |
|
238 | 219 | .drag-handle {
|
239 | 220 | cursor: move;
|
|
246 | 227 | background-size: 1em;
|
247 | 228 | }
|
248 | 229 |
|
249 |
| - &.active { |
250 |
| - border-bottom: 1px solid var(--sk-fg-accent); |
| 230 | + .remove { |
| 231 | + position: absolute; |
| 232 | + display: none; |
| 233 | + top: 0; |
| 234 | + right: 0; |
| 235 | + padding: 0 0.2rem; |
| 236 | + width: 1.6rem; |
| 237 | + height: 100%; |
| 238 | + cursor: pointer; |
| 239 | +
|
| 240 | + svg { |
| 241 | + width: 100%; |
| 242 | + height: 100%; |
| 243 | + } |
251 | 244 | }
|
252 |
| - } |
253 | 245 |
|
254 |
| - .editable, |
255 |
| - .uneditable, |
256 |
| - .input-sizer, |
257 |
| - input { |
258 |
| - display: inline-block; |
259 |
| - position: relative; |
260 |
| - line-height: 1; |
| 246 | + &.drag-over { |
| 247 | + background: var(--sk-bg-4); |
| 248 | + } |
| 249 | +
|
| 250 | + &[aria-current='true'] { |
| 251 | + border-bottom: 1px solid var(--sk-fg-accent); |
| 252 | +
|
| 253 | + &.editable .filename { |
| 254 | + cursor: text; |
| 255 | + } |
| 256 | +
|
| 257 | + .remove { |
| 258 | + display: block; |
| 259 | + } |
| 260 | + } |
261 | 261 | }
|
262 | 262 |
|
263 | 263 | input {
|
264 | 264 | position: absolute;
|
265 |
| - width: 100%; |
| 265 | + width: calc(100% - var(--padding-left) - var(--padding-right)); |
266 | 266 | border: none;
|
267 |
| - color: var(--sk-fg-accent); |
268 | 267 | outline: none;
|
269 |
| - background-color: transparent; |
| 268 | + background-color: inherit; |
| 269 | + color: inherit; |
270 | 270 | top: 0;
|
271 |
| - left: 0; |
| 271 | + left: var(--padding-left); |
272 | 272 | height: 100%;
|
273 | 273 | display: flex;
|
274 | 274 | align-items: center;
|
275 | 275 | justify-content: center;
|
276 | 276 | font-family: var(--sk-font-family-ui);
|
277 | 277 | font: var(--sk-font-ui-small); /* TODO can we just inherit */
|
278 |
| - padding: 0 1rem 1px 2em; |
279 | 278 | box-sizing: border-box;
|
280 |
| - } |
281 | 279 |
|
282 |
| - .duplicate { |
283 |
| - color: var(--sk-fg-accent); |
284 |
| - } |
285 |
| -
|
286 |
| - .remove { |
287 |
| - position: absolute; |
288 |
| - display: none; |
289 |
| - right: 1px; |
290 |
| - top: 4px; |
291 |
| - width: 16px; |
292 |
| - text-align: right; |
293 |
| - padding: 12px 0 12px 5px; |
294 |
| - font-size: 8px; |
295 |
| - cursor: pointer; |
296 |
| - } |
297 |
| -
|
298 |
| - .file-tabs .button.active .editable { |
299 |
| - cursor: text; |
300 |
| - } |
301 |
| -
|
302 |
| - .file-tabs .button.active .remove { |
303 |
| - display: block; |
304 |
| - } |
305 |
| -
|
306 |
| - .file-tabs .button.drag-over { |
307 |
| - background: #67677814; |
308 |
| - } |
309 |
| -
|
310 |
| - .file-tabs .button.drag-over { |
311 |
| - cursor: move; |
| 280 | + &:focus { |
| 281 | + color: var(--sk-fg-accent); |
| 282 | + } |
312 | 283 | }
|
313 | 284 |
|
314 | 285 | .add-new {
|
315 |
| - padding: 12px 10px 8px 8px; |
316 |
| - height: 40px; |
317 |
| - text-align: center; |
| 286 | + height: 3.2rem; |
| 287 | + aspect-ratio: 1; |
| 288 | + background: url(./file-new.svg) 50% 50% no-repeat; |
| 289 | + background-size: 1em; |
318 | 290 | }
|
319 | 291 |
|
320 | 292 | .runes {
|
|
0 commit comments