fix gitignore

pull/1890/head
Rich Harris 7 years ago
parent 3200352bc4
commit e57637b3ca

1
.gitignore vendored

@ -18,7 +18,6 @@ node_modules
/store.umd.js
/yarn-error.log
_actual*.*
_*/
/site/cypress/screenshots/
/site/__sapper__/

5
site/.gitignore vendored

@ -1,5 +0,0 @@
.DS_Store
node_modules
yarn-error.log
/cypress/screenshots/
/__sapper__/

@ -0,0 +1,286 @@
<script>
import { createEventDispatcher } from 'svelte';
import ExampleSelector from './ExampleSelector.html';
import UserMenu from './UserMenu.html';
import Icon from '../../../components/icon.html';
import * as doNotZip from 'do-not-zip';
import downloadBlob from '../_utils/downloadBlob.js';
import { user } from '../../../user.js';
const dispatch = createEventDispatcher();
export let examples;
export let gist;
export let name;
export let components;
export let json5;
export let zen_mode;
export let bundle;
let saving = false;
let downloading = false;
let justSaved = false;
let justForked = false;
const isMac = typeof navigator !== 'undefined' && navigator.platform === 'MacIntel';
function wait(ms) {
return new Promise(f => setTimeout(f, ms));
}
let canSave;
$: canSave = !!$user && !!gist && $user.id == gist.owner.id; // comparing number and string
function handleKeydown(event) {
if (event.which === 83 && (isMac ? event.metaKey : event.ctrlKey)) {
event.preventDefault();
save();
}
}
function login(event) {
event.preventDefault();
const loginWindow = window.open(`${window.location.origin}/auth/login`, 'login', 'width=600,height=400');
const handleLogin = event => {
loginWindow.close();
user.set(event.data.user);
window.removeEventListener('message', handleLogin);
};
window.addEventListener('message', handleLogin);
}
async function fork(intentWasSave) {
saving = true;
try {
const r = await fetch(`gist/create`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify({
name,
components,
json5
})
});
if (r.status < 200 || r.status >= 300) {
const { error } = await r.json();
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
}
const gist = await r.json();
dispatch('forked', { gist });
if (intentWasSave) {
justSaved = true;
await wait(600);
justSaved = false;
} else {
justForked = true;
await wait(600);
justForked = false;
}
} catch (err) {
if (navigator.onLine) {
alert(err.message);
} else {
alert(`It looks like you're offline! Find the internet and try again`);
}
}
saving = false;
}
async function save() {
if (saving) return;
if (!canSave) {
fork(true);
return;
}
saving = true;
try {
const files = {};
// null out any deleted files
const set = new Set(components.map(m => `${m.name}.${m.type}`));
Object.keys(gist.files).forEach(file => {
if (/\.(html|js)$/.test(file)) {
if (!set.has(file)) files[file] = null;
}
});
components.forEach(module => {
const file = `${module.name}.${module.type}`;
if (!module.source.trim()) {
throw new Error(`GitHub does not allow saving gists with empty files - ${file}`);
}
if (!gist.files[files] || module.source !== gist.files[file].content) {
files[file] = { content: module.source };
}
});
if (!gist.files['data.json5'] || json5 !== gist.files['data.json5'].content) {
files['data.json5'] = { content: json5 };
}
// data.json has been deprecated in favour of data.json5
if (gist.files['data.json']) gist.files['data.json'] = null;
const r = await fetch(`gist/${gist.id}`, {
method: 'PATCH',
credentials: 'include',
body: JSON.stringify({
description: name,
files
})
});
if (r.status < 200 || r.status >= 300) {
const { error } = await r.json();
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
}
const result = await r.json();
justSaved = true;
await wait(600);
justSaved = false;
} catch (err) {
if (navigator.onLine) {
alert(err.message);
} else {
alert(`It looks like you're offline! Find the internet and try again`);
}
}
saving = false;
}
async function download() {
downloading = true;
const files = await (await fetch('/svelte-app.json')).json();
if (bundle.imports.length > 0) {
const idx = files.findIndex(({ path }) => path === 'package.json');
const pkg = JSON.parse(files[idx].data);
const deps = {};
bundle.imports.forEach(mod => {
const match = /^(@[^\/]+\/)?[^@\/]+/.exec(mod);
deps[match[0]] = 'latest';
});
pkg.dependencies = deps;
files[idx].data = JSON.stringify(pkg, null, ' ');
}
files.push(...components.map(component => ({ path: `src/${component.name}.${component.type}`, data: component.source })));
files.push({
path: `src/main.js`, data: `import App from './App.html';
var app = new App({
target: document.body,
props: ${JSON.stringify(data, null, '\t').replace(/\n/g, '\n\t')}
});
export default app;` });
downloadBlob(doNotZip.toBlob(files), 'svelte-app.zip');
downloading = false;
}
</script>
<svelte:window on:keydown={handleKeydown} />
<div class="app-controls">
<div class="icon" style="position: relative; width: 3.2rem; margin-top:-.5rem">
<Icon name="menu" />
</div>
<ExampleSelector {examples} bind:name on:select />
<div style="flex: 1 0 auto" />
<div style="text-align: right; margin-right:.8rem">
{#if $user}
<UserMenu />
{:else}
<a class="icon" on:click={login} href="auth/login" title="Login to save">
<Icon name="log-in" />
</a>
{/if}
<button class="icon" on:click="{() => zen_mode = !zen_mode}" title="fullscreen editor">
{#if zen_mode}
<Icon name="close" />
{:else}
<Icon name="maximize" />
{/if}
</button>
<button class="icon" disabled={downloading} on:click={download} title="download zip file">
<Icon name="download" />
</button>
{#if $user}
<button class="icon" disabled="{saving || !$user}" on:click={fork} title="fork">
{#if justForked}
<Icon name="check" />
{:else}
<Icon name="git-branch" />
{/if}
</button>
<button class="icon" disabled="{saving || !$user}" on:click={save} title="save">
{#if justSaved}
<Icon name="check" />
{:else}
<Icon name="save" />
{/if}
</button>
{/if}
{#if gist}
<a class="icon" href={gist.html_url} title="link to gist">
<Icon name="save" />
</a>
{/if}
</div>
</div>
<style>
.app-controls {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: var(--app-controls-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: .6rem var(--side-nav);
background-color: var(--second);
color: white;
}
.icon {
padding: 0 .8rem;
opacity: .5;
transition: opacity .3s;
}
.icon:hover { opacity: 1 }
.icon:disabled { opacity: .3 }
.icon[title^='fullscreen'] { display: none }
@media (min-width: 768px) {
.icon[title^='fullscreen'] { display: inline }
}
</style>

@ -0,0 +1,224 @@
<script>
import { onMount, beforeUpdate, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let mode;
export let code;
export let readonly;
export let error;
export let errorLoc;
export let warningCount = 0;
let w;
let h;
export function resize() {
editor.refresh();
}
const modes = {
json: {
name: 'javascript',
json: true
},
handlebars: {
name: 'handlebars',
base: 'text/html'
}
};
const refs = {};
let CodeMirror;
let editor;
let updating = false;
let marker;
let error_line;
let destroyed = false;
$: if (CodeMirror) {
createEditor(mode);
}
$: if (editor && !updating && code != null) {
updating = true;
editor.setValue(code);
}
$: if (editor && w && h) {
editor.refresh();
}
$: {
if (marker) marker.clear();
if (errorLoc) {
const line = errorLoc.line - 1;
const ch = errorLoc.column;
marker = editor.markText({ line, ch }, { line, ch: ch + 1 }, {
className: 'error-loc'
});
error_line = line;
} else {
error_line = null;
}
}
let previous_error_line;
$: if (editor) {
if (previous_error_line != null) {
editor.removeLineClass(previousLine, 'wrap', 'error-line')
}
if (error_line && (error_line !== previous_error_line)) {
editor.addLineClass(error_line, 'wrap', 'error-line');
previous_error_line = error_line;
}
}
onMount(() => {
import(/* webpackChunkName: "codemirror" */ './_codemirror.js').then(mod => {
CodeMirror = mod.default;
});
return () => {
destroyed = true;
if (editor) editor.toTextArea();
}
});
beforeUpdate(() => {
updating = false;
});
function createEditor(mode) {
if (destroyed) return;
if (editor) {
editor.toTextArea();
}
editor = CodeMirror.fromTextArea(refs.editor, {
lineNumbers: true,
lineWrapping: true,
indentWithTabs: true,
indentUnit: 2,
tabSize: 2,
value: code,
mode: modes[mode] || {
name: mode
},
readOnly: readonly
});
editor.on('change', instance => {
if (!updating) {
updating = true;
code = instance.getValue();
}
});
editor.refresh();
}
</script>
<style>
.codemirror-container {
width: 100%;
}
.codemirror-container :global(.CodeMirror) {
height: 100%;
min-height: 60px;
background: var(--background);
font: 300 var(--code-fs)/1.7 var(--font-mono);
color: var(--base);
}
@media (min-width: 768px) {
.codemirror-container {
height: 100%;
border: none;
}
.codemirror-container :global(.CodeMirror) {
height: 100%;
}
}
.codemirror-container :global(.CodeMirror-gutters) {
padding: 0 1.6rem 0 .8rem;
border: none;
}
.codemirror-container .message {
position: absolute;
bottom: 2.4rem;
left: 2.4rem;
z-index: 20;
}
.codemirror-container :global(.error-loc) {
position: relative;
border-bottom: 2px solid #da106e;
}
.codemirror-container :global(.error-line) {
background-color: rgba(200, 0, 0, .05);
}
.loading,
.error {
text-align: center;
color: #999;
font-weight: 300;
margin: 2.4rem 0 0 0;
}
textarea { width: 100%; border: none }
</style>
<!--
-----------------------------------------------
syntax-highlighting [prism]
NOTE
- just started to transfer colors from prism to codemirror
-----------------------------------------------
-->
<div class='codemirror-container' bind:offsetWidth={w} bind:offsetHeight={h}>
<textarea tabindex='2' bind:this={refs.editor}></textarea>
{#if error}
<p class='error message'>
{#if error.loc}
<strong>
{#if error.filename}
<span
class='filename'
on:click="{() => dispatch('navigate', { filename: error.filename })}"
>{error.filename}</span>
{/if}
({error.loc.line}:{error.loc.column})
</strong>
{/if}
{error.message}
</p>
{:elseif warningCount > 0}
<p class='warning message'>
Compiled, but with {warningCount} {warningCount === 1 ? 'warning' : 'warnings'} — check the console for details
</p>
{/if}
</div>
{#if !CodeMirror}
<p class='loading'>loading editor...</p>
{/if}
<!-- TODO -->
<!-- {:catch err}
<p class='error'>error loading CodeMirror</p>
{/await} -->

@ -0,0 +1,77 @@
<script>
import { createEventDispatcher } from 'svelte';
import { enter } from './events.js';
export let examples;
export let name;
const dispatch = createEventDispatcher();
</script>
<div class='select-wrapper'>
<select on:change="{e => dispatch('select', { slug: e.target.value })}">
<option value={null} disabled>Select an example</option>
{#each examples as group}
<optgroup label={group.name}>
{#each group.examples as example}
<option value={example.slug}>{example.title}</option>
{/each}
</optgroup>
{/each}
</select>
<div class='visible'>
<span class='widther'>{name}</span>
<input bind:value={name} on:focus="{e => e.target.select()}" use:enter="{e => e.target.blur()}">
</div>
</div>
<style>
.select-wrapper {
position: relative;
display: block;
float: left;
margin: 0 0 0 -4rem;
padding: 0 0 0 4.8rem;
font-family: var(--font-ui);
}
select {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
/* debug only */
/* background-color: rgba(221, 255, 205, .377); */
/* opacity: .5; */
}
.visible {
position: relative;
padding: .5rem 1.6rem .5rem .4em;
font-size: 1.3rem;
}
span {
color: transparent;
}
input {
position: absolute;
paddding: 0;
width: 100%;
height: 100%;
top: 0;
left: .4em;
font-size: inherit;
font-family: inherit;
color: inherit;
border: none;
background: none;
opacity: .7;
}
</style>

@ -0,0 +1,206 @@
<script>
import { createEventDispatcher } from 'svelte';
import Icon from '../../../../components/icon.html';
import { enter } from '../events.js';
const dispatch = createEventDispatcher();
export let components;
export let selectedComponent;
// let previous_components;
// $: {
// // bit of a hack...
// if (components !== previous_components) {
// selectedComponent = components[0];
// previous_components = components;
// }
// }
function selectComponent(component, selectedComponent) {
if (selectedComponent != component) {
selectedComponent.edit = false;
}
dispatch('select', { component });
}
function editTab(component, selectedComponent) {
if (selectedComponent === component) {
selectedComponent.edit = true; // TODO can we make this local state?
}
}
function closeEdit(selectedComponent) {
const match = /(.+)\.(html|js)$/.exec(selectedComponent.name);
selectedComponent.name = match ? match[1] : selectedComponent.name;
if (match && match[2]) selectedComponent.type = match[2];
selectedComponent.edit = false;
components = components; // TODO necessary?
}
function remove(component) {
let result = confirm(`Are you sure you want to delete ${component.name}.${component.type}?`);
if (result) dispatch('remove');
}
function selectInput(event) {
setTimeout(() => {
event.target.select();
});
}
</script>
<style>
.component-selector {
position: relative;
border-bottom: 1px solid #eee;
}
.file-tabs {
border: none;
padding: 0 0 0 5rem;
margin: 0;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
height: 100%;
}
.file-tabs button {
position: relative;
font: 300 1.2rem/1.5 var(--font-ui);
color: var(--second);
border-bottom: var(--border-w) solid transparent;
padding: 1.2rem 1.2rem 0.8rem 0.5rem;
margin: 0 0.5rem 0 0;
}
.file-tabs button.active {
border-bottom: var(--border-w) solid var(--prime);
}
.editable, .uneditable, .input-sizer, input {
display: inline-block;
position: relative;
line-height: 1;
}
.input-sizer {
color: #ccc;
}
input {
position: absolute;
width: 100%;
left: 0.5rem;
top: 1.2rem;
/* padding: 0 0.4rem; */
/* font-size: 1rem; */
font: 300 1.2rem/1.5 var(--font-ui);
border: none;
color: var(--flash);
outline: none;
line-height: 1;
background-color: transparent;
}
.editable {
/* margin-right: 2.4rem; */
}
.uneditable {
/* padding-left: 1.2rem; */
}
.remove {
position: absolute;
display: none;
right: .1rem;
top: .4rem;
width: 1.6rem;
text-align: right;
padding: 1.2em 0 1.2em .5em;
font-size: 0.8rem;
cursor: pointer;
}
.remove:hover {
color: var(--flash);
}
.file-tabs button.active .editable {
cursor: text;
}
.file-tabs button.active .remove {
display: inline-block;
}
.add-new {
position: absolute;
left: 0;
top: 0;
width: 5rem;
height: 100%;
text-align: center;
}
.add-new:hover {
color: var(--flash);
background-color: white;
}
</style>
<!-- WUT -->
<div class="component-selector">
<div class="file-tabs" on:dblclick="{() => dispatch('create')}">
{#each components as component}
<button
id={component.name}
class:active="{component === selectedComponent}"
data-name={component.name}
on:click="{() => selectComponent(component, selectedComponent)}"
on:dblclick="{e => e.stopPropagation()}"
>
{#if component.name == 'App'}
<div class="uneditable">
App.html
</div>
{:else}
{#if component.edit}
<span class="input-sizer">{component.name + (/\./.test(component.name) ? '' : '.html')}</span>
<input
autofocus
bind:value={component.name}
on:focus={selectInput}
on:blur="{() => closeEdit(selectedComponent)}"
use:enter="{e => e.target.blur()}"
>
{:else}
<div
class="editable"
title="edit component name"
on:click="{() => editTab(component, selectedComponent)}"
>
{component.name}.{component.type}
</div>
<span class="remove" on:click="{() => remove(component)}">
<Icon name="close"/>
<!-- &times; -->
</span>
{/if}
{/if}
</button>
{/each}
</div>
<button class="add-new" on:click="{() => dispatch('create')}" title="add new component">
<Icon name="plus" />
</button>
</div>

@ -0,0 +1,37 @@
<script>
import CodeMirror from '../CodeMirror.html';
export let selectedComponent;
export let error;
export let errorLoc;
export let warningCount = 0;
</script>
<style>
.editor-wrapper {
z-index: 5;
background: var(--back-light);
}
@media (min-width: 768px) {
.editor-wrapper {
/* make it easier to interact with scrollbar */
padding-right: 8px;
height: auto;
/* height: 100%; */
}
}
</style>
<div class="editor-wrapper">
{#if selectedComponent}
<CodeMirror
mode="{selectedComponent.type === 'js' ? 'javascript' : 'handlebars'}"
bind:code={selectedComponent.source}
{error}
{errorLoc}
{warningCount}
on:navigate
/>
{/if}
</div>

@ -0,0 +1,27 @@
<script>
import ComponentSelector from './ComponentSelector.html';
import ModuleEditor from './ModuleEditor.html';
export let components;
export let selectedComponent;
export let sourceError;
export let sourceErrorLoc;
export let runtimeErrorLoc;
export let warningCount;
</script>
<ComponentSelector
{components}
bind:selectedComponent
on:create
on:remove
on:select
/>
<ModuleEditor
bind:selectedComponent
error={sourceError}
errorLoc="{sourceErrorLoc || runtimeErrorLoc}"
{warningCount}
on:navigate
/>

@ -0,0 +1,386 @@
<script>
import { onMount, createEventDispatcher } from 'svelte';
import getLocationFromStack from '../../_utils/getLocationFromStack.js';
import { decode } from 'sourcemap-codec';
const dispatch = createEventDispatcher();
export let bundle;
export let dom;
export let ssr;
export let data;
export let sourceError;
export let error;
const refs = {};
const importCache = {};
let pendingImports = 0;
let pending = false;
function fetchImport(id, curl) {
return new Promise((fulfil, reject) => {
curl([`https://bundle.run/${id}`]).then(module => {
importCache[id] = module;
fulfil(module);
}, err => {
console.error(err.stack);
reject(new Error(`Error loading ${id} from bundle.run`));
});
});
}
const namespaceSpecifier = /\*\s+as\s+(\w+)/;
const namedSpecifiers = /\{(.+)\}/;
function parseSpecifiers(specifiers) {
specifiers = specifiers.trim();
let match = namespaceSpecifier.exec(specifiers);
if (match) {
return {
namespace: true,
name: match[1]
};
}
let names = [];
specifiers = specifiers.replace(namedSpecifiers, (match, str) => {
names = str.split(',').map(name => {
const split = name.split('as');
const exported = split[0].trim();
const local = (split[1] || exported).trim();
return { local, exported };
});
return '';
});
match = /\w+/.exec(specifiers);
return {
namespace: false,
names,
default: match ? match[0] : null
};
}
let createComponent;
let init;
onMount(() => {
let component;
refs.child.addEventListener('load', () => {
const iframe = refs.child;
const body = iframe.contentDocument.body;
const evalInIframe = iframe.contentWindow.eval;
// intercept links, so that we can use #hashes inside the iframe
body.addEventListener('click', event => {
if (event.which !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
// ensure target is a link
let el = event.target;
while (el && el.nodeName !== 'A') el = el.parentNode;
if (!el || el.nodeName !== 'A') return;
if (el.hasAttribute('download') || el.getAttribute('rel') === 'external' || el.target) return;
event.preventDefault();
if (el.href.startsWith(top.location.href)) {
const hash = el.href.replace(top.location.href, '');
if (hash[0] === '#') {
iframe.contentWindow.location.hash = hash;
return;
}
}
window.open(el.href, '_blank');
});
let promise = null;
let updating = false;
let toDestroy = null;
const init = () => {
if (sourceError) return;
const imports = [];
const missingImports = bundle.imports.filter(x => !importCache[x]);
const removeStyles = () => {
const styles = iframe.contentDocument.querySelectorAll('style');
let i = styles.length;
while (i--) styles[i].parentNode.removeChild(styles[i]);
};
const ready = () => {
error = null;
if (toDestroy) {
removeStyles();
toDestroy.$destroy();
toDestroy = null;
}
bundle.imports.forEach(x => {
const module = importCache[x];
const name = bundle.importMap.get(x);
iframe.contentWindow[name] = module;
});
if (ssr) { // this only gets generated if component uses lifecycle hooks
pending = true;
createHtml();
} else {
pending = false;
createComponent();
}
};
const createHtml = () => {
try {
evalInIframe(`${ssr.code}
var rendered = SvelteComponent.render(${JSON.stringify(data)});
if (rendered.css.code) {
var style = document.createElement('style');
style.textContent = rendered.css.code;
document.head.appendChild(style);
}
document.body.innerHTML = rendered.html;
`)
} catch (e) {
const loc = getLocationFromStack(e.stack, ssr.map);
if (loc) {
e.filename = loc.source;
e.loc = { line: loc.line, column: loc.column };
}
error = e;
}
};
const createComponent = () => {
// remove leftover styles from SSR renderer
if (ssr) removeStyles();
try {
evalInIframe(`${dom.code}
document.body.innerHTML = '';
window.location.hash = '';
window._svelteTransitionManager = null;
var component = new SvelteComponent({
target: document.body,
data: ${JSON.stringify(data)}
});`);
component = window.app = window.component = iframe.contentWindow.component;
// component.on('state', ({ current }) => {
// if (updating) return;
// updating = true;
// this.fire('data', { current });
// updating = false;
// });
} catch (e) {
// TODO show in UI
component = null;
const loc = getLocationFromStack(e.stack, dom.map);
if (loc) {
e.filename = loc.source;
e.loc = { line: loc.line, column: loc.column };
}
error = e;
}
};
pendingImports = missingImports.length;
if (missingImports.length) {
let cancelled = false;
promise = Promise.all(
missingImports.map(id => fetchImport(id, iframe.contentWindow.curl).then(module => {
pendingImports -= 1;
return module;
}))
);
promise.cancel = () => cancelled = true;
promise
.then(() => {
if (cancelled) return;
ready();
})
.catch(e => {
if (cancelled) return;
error = e;
});
} else {
ready();
}
run = () => {
pending = false;
// TODO do we need to clear out SSR HTML?
createComponent();
};
}
bundle_handler = bundle => {
if (!bundle) return; // TODO can this ever happen?
if (promise) promise.cancel();
toDestroy = component;
component = null;
if (data !== undefined) init();
};
data_handler = data => {
if (updating) return;
try {
if (component) {
error = null;
updating = true;
component.$set(data);
updating = false;
} else {
init();
}
} catch (e) {
const loc = getLocationFromStack(e.stack, bundle.map);
if (loc) {
e.filename = loc.source;
e.loc = { line: loc.line, column: loc.column };
}
error = e;
}
};
});
});
function noop(){}
let run = noop;
let bundle_handler = noop;
let data_handler = noop;
$: bundle_handler(bundle);
$: data_handler(data);
</script>
<style>
.iframe-container {
border-top: 1px solid #ccc;
background-color: white;
}
iframe {
width: 100%;
height: calc(100vh -3em);
/* height: calc(100vh - var(--nav-h)); */
border: none;
display: block;
}
@media (min-width: 768px) {
.iframe-container {
border: none;
height: 100%;
}
iframe {
height: 100%;
}
}
.greyed-out {
filter: grayscale(50%) blur(1px);
opacity: .25;
}
.overlay {
position: absolute;
top: 0;
width: 100%;
height: 100%;
padding: 1em;
pointer-events: none;
}
.overlay p {
pointer-events: all;
}
.pending {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center;
pointer-events: all;
}
.pending button {
position: absolute;
margin-top: 6rem;
}
</style>
<div class="iframe-container">
<iframe title="Result" bind:this={refs.child} class="{error || pending || pendingImports ? 'greyed-out' : ''}" srcdoc='
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="/repl-viewer.css">
</head>
<body>
<script src="/curl.js"></script>
<script>curl.config(&#123; dontAddFileExt: /./ });</script>
</body>
</html>
'></iframe>
</div>
<div class="overlay">
{#if error}
<p class="error message">
{#if error.loc}
<strong>
{#if error.filename}
<span class="filename" on:click="{() => dispatch('navigate', { filename: error.filename })}">{error.filename}</span>
{/if}
({error.loc.line}:{error.loc.column})
</strong>
{/if}
{error.message}
</p>
{:elseif pending}
<div class="pending" on:click={run}>
<button class="bg-second white">Click to run</button>
</div>
{:elseif pendingImports}
<p class="info message">loading {pendingImports} {pendingImports === 1 ? 'dependency' : 'dependencies'} from
https://bundle.run</p>
{/if}
</div>

@ -0,0 +1,101 @@
<script>
import SplitPane from '../SplitPane.html';
import Viewer from './Viewer.html';
import CodeMirror from '../CodeMirror.html';
export let bundle
export let compiled;
export let dom;
export let ssr;
export let data;
export let json5;
export let sourceError;
export let runtimeError;
export let dataError;
export let dataErrorLoc;
let view = 'result';
</script>
<style>
.view-toggle {
height: var(--pane-controls-h);
border-bottom: 1px solid #eee;
}
button {
/* width: 50%;
height: 100%; */
text-align: left;
font: 300 1.2rem/1.5 var(--font-ui);
border-bottom: var(--border-w) solid transparent;
}
button.active {
border-bottom: var(--border-w) solid var(--prime);
color: var(--second);
}
div {
height: 100%;
}
</style>
<div class="view-toggle">
<button
class:active="{view === 'result'}"
on:click="{() => view = 'result'}"
>Result</button>
<button
class:active="{view === 'code'}"
on:click="{() => view = 'code'}"
>Compiled code</button>
</div>
{#if view === 'result'}
<SplitPane type="vertical">
<div slot="a">
{#if bundle}
<Viewer
{bundle}
{dom}
{ssr}
{data}
{sourceError}
bind:error={runtimeError}
on:data="{e => updateData(e.detail.current)}"
on:navigate={navigate}
/>
{:else}
<p class="loading">loading Svelte compiler...</p>
{/if}
</div>
<section slot="b">
<CodeMirror
mode="json"
bind:code={json5}
error={dataError}
errorLoc={dataErrorLoc}
/>
</section>
</SplitPane>
{:else}
<SplitPane type="vertical">
<div slot="a">
<CodeMirror
mode="javascript"
code={compiled}
error={sourceError}
errorLoc={sourceErrorLoc}
readonly
/>
</div>
<section slot="b">
compiler options go here
</section>
</SplitPane>
{/if}

@ -0,0 +1,272 @@
<script>
import { onMount } from 'svelte';
import * as fleece from 'golden-fleece';
import SplitPane from './SplitPane.html';
import CodeMirror from './CodeMirror.html';
import Input from './Input/index.html';
import Output from './Output/index.html';
export let version;
export let components;
export let selectedComponent;
export let data;
export let json5;
let bundle = null;
let dom;
let ssr;
let sourceError = null;
let runtimeError = null;
let dataError = null;
let dataErrorLoc = null;
let warningCount = 0;
let compiled = '';
let uid = 0;
let sourceErrorLoc, runtimeErrorLoc;
let worker;
onMount(async () => {
worker = new Worker('/repl-worker.js');
function listener(event) {
switch (event.data.type) {
case 'version':
version = event.data.version;
break;
case 'bundled':
({ bundle, dom, ssr, warningCount, error: sourceError } = event.data.result);
runtimeError = null;
break;
case 'compiled':
compiled = event.data.result;
break;
}
}
worker.addEventListener('message', listener);
worker.postMessage({ type: 'init', version });
return () => {
worker.removeEventListener('message', listener);
worker.terminate();
};
});
function createComponent() {
const newComponent = {
name: uid++ ? `Component${uid}` : 'Component1',
type: 'html',
source: '',
edit: true
};
components = components.concat(newComponent);
setTimeout(() => {
document.getElementById(newComponent.name).scrollIntoView(false);
selectedComponent = newComponent;
});
}
function removeComponent() {
const component = selectedComponent;
if (component.name === 'App') {
// App.html can't be removed
component.source = '';
selectedComponent = component;
} else {
const index = components.indexOf(component);
if (~index) {
components = components.slice(0, index).concat(components.slice(index + 1));
} else {
console.error(`Could not find component! That's... odd`);
}
selectedComponent = components[index] || components[components.length - 1];
}
}
function updateData(current) {
const data = fleece.evaluate(json5);
for (const key in data) {
data[key] = current[key];
}
json5 = fleece.patch(json5, data);
}
function navigate(filename) {
const name = filename.replace(/\.html$/, '');
if (selectedComponent.name === name) return;
selectedComponent = components.find(c => c.name === name);
}
$: if (sourceError && selectedComponent) {
sourceErrorLoc = sourceError.filename === `${selectedComponent.name}.${selectedComponent.type}`
? sourceError.start
: null;
}
$: if (runtimeError && selectedComponent) {
runtimeErrorLoc = runtimeError.filename === `${selectedComponent.name}.${selectedComponent.type}`
? runtimeError.start
: null;
}
$: try {
data= fleece.evaluate(json5);
dataError = null;
dataErrorLoc = null;
} catch (err) {
dataError = err;
dataErrorLoc = err && err.loc;
}
$: if (worker && components.length > 0) {
worker.postMessage({ type: 'bundle', components });
}
let last_selected_component;
$: {
// slightly counterintuitively, we only want to rebundle if
// this is the *same* component — not if we've just selected
// a different one
// if (selectedComponent === last_selected_component) {
// if (worker && components.length > 0) {
// console.log(`2`, components);
// worker.postMessage({ type: 'bundle', components });
// }
// }
// recompile the currently selected component
if (selectedComponent) {
if (selectedComponent.type === 'html') {
worker.postMessage({ type: 'compile', component: selectedComponent });
} else {
compiled = selectedComponent.source;
}
}
last_selected_component = selectedComponent;
}
</script>
<style>
.repl-inner { height: 100% }
.repl-inner :global(section) {
position: relative;
padding: 4.2rem 0 0 0;
height: 100%;
}
.repl-inner :global(section) > :global(*):first-child {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4.2rem;
}
.repl-inner :global(section) > :global(*):last-child {
width: 100%;
height: 100%;
}
.repl-inner :global(.message) {
position: relative;
border-radius: var(--border-r);
margin: 0;
padding: 1.2rem 1.6rem 1.2rem 4.4rem;
vertical-align: middle;
font: 300 1.2rem/1.7 var(--font-ui);
color: white;
}
.repl-inner :global(.message::before) {
content: '!';
position: absolute;
left: 1.2rem;
top: 1.1rem;
width: 1rem;
height: 1rem;
text-align: center;
line-height: 1;
padding: .4rem;
border-radius: 50%;
color: white;
border: .2rem solid white;
}
.repl-inner :global(.error.message) {
background-color: #da106e;
}
.repl-inner :global(.warning.message) {
background-color: #e47e0a;
}
.repl-inner :global(.info.message) {
background-color: var(--second);
animation: fade-in .4s .2s both;
}
.repl-inner :global(.error) :global(.filename) {
cursor: pointer;
}
@media (min-width: 768px) {
.show-if-mobile { display: none }
.repl-outer.zen-mode {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 11;
}
section { height: 100% }
}
</style>
<div class="repl-inner">
<SplitPane type="horizontal">
<section slot=a>
<Input
{components}
bind:selectedComponent
error={sourceError}
errorLoc="{sourceErrorLoc || runtimeErrorLoc}"
{warningCount}
on:create={createComponent}
on:remove={removeComponent}
on:select="{e => selectedComponent = e.detail.component}"
/>
</section>
<section slot=b style='height: 100%;'>
<Output
{compiled}
{bundle}
{ssr}
{dom}
{data}
{json5}
{sourceError}
{runtimeError}
{dataError}
{dataErrorLoc}
/>
</section>
</SplitPane>
</div>

@ -0,0 +1,160 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let type;
const refs = {};
const side = type === 'horizontal' ? 'left' : 'top';
const dimension = type === 'horizontal' ? 'width' : 'height';
let pos = 50;
let dragging = false;
function setPos(event) {
const { top, bottom, left, right } = refs.container.getBoundingClientRect();
pos = 100 * (type === 'vertical'
? (event.clientY - top) / (bottom - top)
: (event.clientX - left) / (right - left));
dispatch('change');
}
function drag(node, callback) {
const mousedown = event => {
if (event.which !== 1) return;
event.preventDefault();
dragging = true;
const onmouseup = () => {
dragging = false;
window.removeEventListener('mousemove', callback, false);
window.removeEventListener('mouseup', onmouseup, false);
};
window.addEventListener('mousemove', callback, false);
window.addEventListener('mouseup', onmouseup, false);
}
node.addEventListener('mousedown', mousedown, false);
return {
destroy() {
node.removeEventListener('mousedown', onmousedown, false);
}
};
}
</script>
<style>
.container {
position: relative;
width: 100%;
height: 100%;
}
.pane {
float: left;
width: 100%;
height: 100%;
}
.mousecatcher {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,.01);
}
.divider {
position: absolute;
z-index: 10;
display: none;
}
.divider::after {
content: '';
position: absolute;
/* background-color: #eee; */
background-color: var(--second);
}
.horizontal {
padding: 0 8px;
width: 0;
height: 100%;
cursor: ew-resize;
}
.horizontal::after {
left: 8px;
top: 0;
width: 1px;
height: 100%;
}
.vertical {
padding: 8px 0;
width: 100%;
height: 0;
cursor: ns-resize;
}
.vertical::after {
top: 8px;
left: 0;
width: 100%;
height: 1px;
}
@media (max-width: 767px) {
.pane {
/* override divider-set dimensions */
width: 100% !important;
height: auto !important;
}
}
@media (min-width: 768px) {
.left, .right, .divider {
display: block;
}
.left, .right {
height: 100%;
float: left;
}
.top, .bottom {
position: absolute;
width: 100%;
}
.top { top: 0; }
.bottom { bottom: 0; }
}
</style>
<div class="container" bind:this={refs.container}>
<div class="pane" style="{dimension}: {pos}%;">
<slot name="a"></slot>
</div>
<div class="pane" style="{dimension}: {100 - pos}%;">
<slot name="b"></slot>
</div>
<div class="{type} divider" style="{side}: calc({pos}% - 8px)" use:drag={setPos}></div>
</div>
{#if dragging}
<div class="mousecatcher"></div>
{/if}

@ -0,0 +1,66 @@
<script>
import { user, logout } from '../../../user.js';
let showMenu = false;
let name;
$: name = $user.displayName || $user.username;
</script>
<div class="user" on:mouseenter="{() => showMenu = true}" on:mouseleave="{() => showMenu = false}">
<span>{name}</span>
<img alt="{name} avatar" src="{$user.photo}">
{#if showMenu}
<div class="menu">
<button on:click={logout}>log out</button>
</div>
{/if}
</div>
<style>
.user {
position: relative;
float: right;
display: block;
padding: .5em 2.2em 0 .8em;
line-height: 1;
z-index: 99;
height: 2.5em;
}
span {
/* position: relative; padding: 0 2em 0 0; */
line-height: 1;
display: inline-block;
padding: .05em 0;
font-family: Rajdhani;
font-weight: 400;
}
img {
position: absolute;
top: .2em;
right: 0;
width: 1.6em;
height: 1.6em;
border-radius: 50%;
}
.menu {
position: absolute;
min-width: calc(100% + .1em);
background-color: #f4f4f4;
padding: .5em;
z-index: 99;
top: calc(2.5em - 1px);
border-bottom: 1px solid #eee;
border-left: 1px solid #eee;
right: 0;
text-align: left;
}
.menu button {
background-color: transparent;
}
</style>

@ -0,0 +1,10 @@
const CodeMirror = require('codemirror');
require('./codemirror.css');
require('codemirror/mode/javascript/javascript.js');
require('codemirror/mode/shell/shell.js');
require('codemirror/mode/handlebars/handlebars.js');
require('codemirror/mode/htmlmixed/htmlmixed.js');
require('codemirror/mode/xml/xml.js');
require('codemirror/mode/css/css.js');
module.exports = CodeMirror;

@ -0,0 +1,350 @@
/* BASICS */
.CodeMirror {
/* copied colors over from prism */
--background: var(--back-light);
--base: #9b978b;
--comment: #afbfcf;
--keyword: #5ba3d3;
--function: #db794b;
--string: #b69b61;
--number: #86af75;
--tags: var(--function);
--important: var(--string);
/* Set height, width, borders, and global font properties here */
/* see prism.css */
height: 300px;
direction: ltr;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: var(--comment);
white-space: nowrap;
opacity: .6;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, .5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue}
.cm-s-default .cm-quote {color: #090}
.cm-negative {color: #d44}
.cm-positive {color: #292}
.cm-header, .cm-strong {font-weight: bold}
.cm-em {font-style: italic}
.cm-link {text-decoration: underline}
.cm-strikethrough {text-decoration: line-through}
.cm-s-default .cm-atom,
.cm-s-default .cm-def,
.cm-s-default .cm-property,
.cm-s-default .cm-variable-2,
.cm-s-default .cm-variable-3,
.cm-s-default .cm-punctuation {color: var(--base)}
.cm-s-default .cm-hr,
.cm-s-default .cm-comment {color: var(--comment)}
.cm-s-default .cm-attribute,
.cm-s-default .cm-keyword {color: var(--keyword)}
.cm-s-default .cm-variable,
.cm-s-default .cm-bracket,
.cm-s-default .cm-tag {color: var(--tags)}
.cm-s-default .cm-number {color: var(--number)}
.cm-s-default .cm-string {color: var(--string)}
.cm-s-default .cm-string-2 {color: #f50}
.cm-s-default .cm-type {color: #085}
.cm-s-default .cm-meta {color: #555}
.cm-s-default .cm-qualifier {color: #555}
.cm-s-default .cm-builtin {color: #30a}
.cm-s-default .cm-link {color: var(--flash)}
.cm-s-default .cm-error {color: #ff008c}
.cm-invalidchar {color: #ff008c}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
padding: .1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

@ -0,0 +1,62 @@
.module-name {
position: relative;
display: block;
background-color: #ff00d4;
}
.panel-header {
/* padding: 0 40px .5em 0; */
background-color: #ff00d4;
}
.dropdown {
position: relative;
display: block;
float: left;
padding: 0 2em 0 0;
background-color: #ff00d4;
}
.dropdown::after {
content: '▼';
position: absolute;
right: 1rem;
top: .55rem;
font-size: .8em;
color: #999;
pointer-events: none;
background-color: #ff00d4;
}
.input-wrapper {
position: relative;
display: block;
float: left;
line-height: 1;
/* margin: 0 .3em 0 0; */
background-color: #ff00d4;
}
.file-tabs li.active {
/* background-color: var(--back-light); */
background-color: #ff00d4;
}
.widther {
display: block;
font-family: inherit;
font-size: inherit;
border: 1px solid #eee;
padding: calc(.5em - 1px) .25em;
line-height: 1;
background-color: #ff00d4;
}
.file-extension {
display: inline-block;
padding: calc(.5em - 1px) 0;
color: var(--prime);
left: -.2em;
pointer-events: none;
background-color: #ff00d4;
}

@ -0,0 +1,19 @@
export function keyEvent(code) {
return function (node, callback) {
node.addEventListener('keydown', handleKeydown);
function handleKeydown(event) {
if (event.keyCode === code) {
callback.call(this, event);
}
}
return {
destroy() {
node.removeEventListener('keydown', handleKeydown);
}
};
}
}
export const enter = keyEvent(13);

@ -0,0 +1,11 @@
export default (blob, filename) => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(url);
link.remove();
};

@ -0,0 +1,31 @@
import { decode } from 'sourcemap-codec';
export default function getLocationFromStack(stack, map) {
if (!stack) return;
const last = stack.split('\n')[1];
const match = /<anonymous>:(\d+):(\d+)\)$/.exec(last);
if (!match) return null;
const line = +match[1];
const column = +match[2];
return trace({ line, column }, map);
}
function trace(loc, map) {
const mappings = decode(map.mappings);
const segments = mappings[loc.line - 1];
for (let i = 0; i < segments.length; i += 1) {
const segment = segments[i];
if (segment[0] === loc.column) {
const [, sourceIndex, line, column] = segment;
const source = map.sources[sourceIndex].slice(2);
return { source, line: line + 1, column };
}
}
return null;
}
Loading…
Cancel
Save