Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/svelte.dev/src/lib/icons/download-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/svelte.dev/src/lib/icons/download-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/svelte.dev/src/lib/icons/user-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/svelte.dev/src/lib/icons/user-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 9 additions & 11 deletions apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@

let repl = $state() as Repl;
let name = $state(data.gist.name);
let zen_mode = $state(false);
let modified_count = $state(0);
let modified = $state(false);
let version = data.version;
let setting_hash: any = null;

Expand Down Expand Up @@ -51,7 +50,7 @@

if (!hash) {
repl?.set({
files: data.gist.components
files: structuredClone(data.gist.components)
});

return;
Expand Down Expand Up @@ -91,12 +90,12 @@
}

function handle_change({ files }: { files: File[] }) {
const old_count = modified_count;
modified_count = files.filter((c) => c.modified).length;
const was_modified = modified;
modified = files.some((c) => c.modified);

if (
old_count === 0 &&
modified_count > 0 &&
!was_modified &&
modified &&
name === data.gist.name &&
data.examples.some((section) =>
section.examples.some((example) => example.slug === data.gist.id)
Expand Down Expand Up @@ -130,7 +129,7 @@
}}
/>

<div class="repl-outer {zen_mode ? 'zen-mode' : ''}">
<div class="repl-outer">
<AppControls
examples={data.examples}
user={data.user}
Expand All @@ -139,8 +138,7 @@
saved={handle_save}
{repl}
bind:name
bind:zen_mode
bind:modified_count
bind:modified
/>

{#if browser}
Expand All @@ -158,7 +156,7 @@
remove={handle_change}
blur={() => {
// Only change hash on editor blur to not pollute everyone's browser history
if (modified_count !== 0) {
if (modified) {
const json = JSON.stringify({ files: repl.toJSON().files });
change_hash(json);
}
Expand Down
168 changes: 106 additions & 62 deletions apps/svelte.dev/src/routes/(authed)/playground/[id]/AppControls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,22 @@
import { get_app_context } from '../../app-context';
import type { Gist, User } from '$lib/db/types';
import type { File } from '@sveltejs/repl';
import { browser } from '$app/environment';

interface Props {
examples: Array<{ title: string; examples: any[] }>;
user: User | null;
repl: Repl;
gist: Gist;
name: string;
zen_mode: boolean;
modified_count: number;
modified: boolean;
forked: (value: { gist: Gist }) => void;
saved: () => void;
}

let {
name = $bindable(),
zen_mode = $bindable(),
modified_count = $bindable(),
modified = $bindable(),
user,
repl,
gist,
Expand All @@ -41,6 +40,7 @@
let downloading = $state(false);
let justSaved = $state(false);
let justForked = $state(false);
let select: HTMLSelectElement;

function wait(ms: number) {
return new Promise((f) => setTimeout(f, ms));
Expand Down Expand Up @@ -84,7 +84,7 @@
const gist = await r.json();
forked({ gist });

modified_count = 0;
modified = false;
repl.markSaved();

if (intentWasSave) {
Expand Down Expand Up @@ -146,7 +146,7 @@
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
}

modified_count = 0;
modified = false;
repl.markSaved();
saved();
justSaved = true;
Expand Down Expand Up @@ -209,6 +209,14 @@ export default app;`

downloading = false;
}

// modifying an app should reset the `<select>`, so that
// the example can be reselected
$effect(() => {
if (modified) {
select.value = '';
}
});
</script>

<svelte:window on:keydown={handleKeydown} />
Expand All @@ -217,10 +225,11 @@ export default app;`
<div class="examples-select">
<span class="raised icon"><Icon name="menu" /></span>
<select
bind:this={select}
title="examples"
value={gist.id}
onchange={e => {
goto(`/playground/${(e.target as HTMLSelectElement).value}`);
onchange={(e) => {
goto(`/playground/${e.currentTarget.value}`);
}}
>
<option value="untitled">Create new</option>
Expand All @@ -237,52 +246,55 @@ export default app;`

<input
bind:value={name}
onchange={() => (modified = true)}
onfocus={(e) => e.currentTarget.select()}
use:enter={(e) => (e.currentTarget as HTMLInputElement).blur()}
/>

<div class="buttons">
<button class="raised icon" onclick={() => (zen_mode = !zen_mode)} title="fullscreen editor">
{#if zen_mode}
<Icon size={18} name="close" />
{:else}
<Icon size={18} name="maximize" />
{/if}
</button>

<button class="raised icon" disabled={downloading} onclick={download} title="download zip file">
<Icon size={18} name="download" />
</button>

<button class="raised icon" disabled={saving || !user} onclick={() => fork(false)} title="fork">
<button
class="raised icon"
disabled={saving || !user}
onclick={() => fork(false)}
aria-label={user ? 'fork' : 'log in to fork'}
>
{#if justForked}
<Icon size={18} name="check" />
{:else}
<Icon size={18} name="git-branch" />
{/if}
</button>

<button class="raised icon" disabled={saving || !user} onclick={save} title="save">
<button
class="raised icon"
disabled={saving || !user}
onclick={save}
aria-label={user
? `save (${browser && navigator.platform === 'MacIntel' ? '⌘' : 'Ctrl'}+S)`
: 'log in to save'}
>
{#if justSaved}
<Icon size={18} name="check" />
{:else}
<Icon size={18} name="save" />
{#if modified_count}
<div class="badge">{modified_count}</div>
{#if modified}
<span class="badge"></span>
{/if}
{/if}
</button>

<button
class="raised icon download"
disabled={downloading}
onclick={download}
aria-label="download zip file"
></button>

{#if user}
<UserMenu {user} />
{:else}
<button
class="raised icon"
onclick={(e) => (e.preventDefault(), login())}
style="width: auto; padding: 0 0.4rem"
>
<Icon name="log-in" />
<span>&nbsp;Log in to save</span>
<button class="raised icon login" onclick={login}>
<span>log in</span>
</button>
{/if}
</div>
Expand Down Expand Up @@ -319,11 +331,15 @@ export default app;`

.examples-select {
position: relative;
}

.examples-select:has(select:focus-visible) .raised.icon {
outline: 2px solid hsla(var(--sk-theme-1-hsl), 0.6);
border-radius: var(--sk-border-radius);
&:has(select:focus-visible) .raised.icon {
outline: 2px solid hsla(var(--sk-theme-1-hsl), 0.6);
border-radius: var(--sk-border-radius);
}

span {
pointer-events: none;
}
}

select {
Expand All @@ -343,6 +359,7 @@ export default app;`
justify-content: center;
width: 3.2rem;
height: 3.2rem;
user-select: none;
}

.icon {
Expand All @@ -351,18 +368,62 @@ export default app;`
font-size: var(--sk-font-size-ui-small);
color: var(--sk-text-3);
line-height: 1;
background: 50% 50% no-repeat;
background-size: 1.8rem;
z-index: 999;

&[aria-label]:hover::before {
content: '';
width: 1rem;
height: 1rem;
position: absolute;
background: var(--sk-text-3);
top: calc(100% + 0.5rem);
rotate: 45deg;
}

&[aria-label]:hover::after {
content: attr(aria-label);
position: absolute;
top: calc(100% + 1rem);
background: var(--sk-text-3);
color: var(--sk-back-4);
padding: 0.5em 0.5em;
border-radius: var(--sk-border-radius);
}

&.login {
width: auto;
background-image: url($lib/icons/user-light.svg);
background-position: 0.4rem 50%;
padding: 0 0.4rem 0 2.8rem;

:root.dark & {
background-image: url($lib/icons/user-dark.svg);
}
}

&.download {
background-image: url($lib/icons/download-light.svg);

:root.dark & {
background-image: url($lib/icons/download-dark.svg);
}
}
}

.icon:hover,
.icon:focus-visible {
opacity: 1;
}

/* TODO use lucide-svelte, so we don't need all this customisation? */
.icon:disabled {
opacity: 0.3;
}
color: #ccc;

.icon[title^='fullscreen'] {
display: none;
:root.dark & {
color: #555;
}
}

input {
Expand All @@ -378,30 +439,13 @@ export default app;`
font-size: var(--sk-font-size-ui-medium);
}

button span {
display: none;
}

.badge {
background: #ff3e00;
border-radius: 100%;
font-size: 10px;
padding: 0;
width: 15px;
height: 15px;
line-height: 15px;
position: absolute;
top: 10px;
right: 0px;
}

@media (min-width: 600px) {
.icon[title^='fullscreen'] {
display: inline;
}

button span {
display: inline-block;
}
background: var(--sk-theme-1);
border-radius: 50%;
width: 1rem;
height: 1rem;
top: -0.2rem;
right: -0.2rem;
}
</style>
Loading
Loading