- playground.html: full web IDE for DreamStack DSL - Code editor with auto-compile (500ms debounce) + Ctrl+Enter - Live preview: renders interactive UI from DreamStack code - Signal graph: visualizes source signals, derived values, handlers, views - Console: compile metrics, type inference (Signal<Int>, Derived<Bool>, etc.) - 4 examples: counter, todo, effects, springs - Dark theme with purple accent, premium glassmorphism design
1255 lines
No EOL
42 KiB
HTML
1255 lines
No EOL
42 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>DreamStack Playground</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117;
|
|
--surface: #161b22;
|
|
--surface-2: #1c2333;
|
|
--border: #30363d;
|
|
--text: #e6edf3;
|
|
--text-dim: #7d8590;
|
|
--accent: #7c3aed;
|
|
--accent-glow: rgba(124, 58, 237, 0.3);
|
|
--green: #3fb950;
|
|
--yellow: #d29922;
|
|
--red: #f85149;
|
|
--blue: #58a6ff;
|
|
--cyan: #39d353;
|
|
--pink: #f778ba;
|
|
--orange: #ffa657;
|
|
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
--font-sans: 'Inter', -apple-system, system-ui, sans-serif;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--font-sans);
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ─── Header ────────────────────────────────── */
|
|
.header {
|
|
height: 48px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 16px;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.logo {
|
|
width: 28px;
|
|
height: 28px;
|
|
background: var(--accent);
|
|
border-radius: 6px;
|
|
display: grid;
|
|
place-items: center;
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
box-shadow: 0 0 12px var(--accent-glow);
|
|
}
|
|
|
|
.header-title {
|
|
font-weight: 600;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.header-title span {
|
|
color: var(--text-dim);
|
|
font-weight: 400;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 6px 14px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface-2);
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
font-family: var(--font-sans);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn:hover {
|
|
border-color: var(--accent);
|
|
background: rgba(124, 58, 237, 0.1);
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #8b5cf6;
|
|
box-shadow: 0 0 16px var(--accent-glow);
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 3px 10px;
|
|
border-radius: 12px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.status-ok {
|
|
background: rgba(63, 185, 80, 0.15);
|
|
color: var(--green);
|
|
}
|
|
|
|
.status-err {
|
|
background: rgba(248, 81, 73, 0.15);
|
|
color: var(--red);
|
|
}
|
|
|
|
/* ─── Main layout ──────────────────────────── */
|
|
.main {
|
|
height: calc(100vh - 48px);
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
|
|
.panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel+.panel {
|
|
border-left: 1px solid var(--border);
|
|
}
|
|
|
|
.panel-header {
|
|
height: 36px;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 12px;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-dim);
|
|
gap: 8px;
|
|
}
|
|
|
|
.panel-tab {
|
|
padding: 4px 10px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.panel-tab:hover {
|
|
color: var(--text);
|
|
}
|
|
|
|
.panel-tab.active {
|
|
color: var(--text);
|
|
background: var(--surface-2);
|
|
}
|
|
|
|
.panel-body {
|
|
flex: 1;
|
|
overflow: auto;
|
|
}
|
|
|
|
/* ─── Code Editor ──────────────────────────── */
|
|
.editor {
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--font-mono);
|
|
font-size: 13.5px;
|
|
line-height: 1.6;
|
|
padding: 16px;
|
|
border: none;
|
|
outline: none;
|
|
resize: none;
|
|
tab-size: 2;
|
|
white-space: pre;
|
|
overflow: auto;
|
|
}
|
|
|
|
/* ─── Preview ───────────────────────────────── */
|
|
.preview-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
background: #fff;
|
|
position: relative;
|
|
}
|
|
|
|
.preview-container iframe {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
}
|
|
|
|
/* ─── Signal Graph ─────────────────────────── */
|
|
.signal-graph {
|
|
padding: 20px;
|
|
display: none;
|
|
}
|
|
|
|
.signal-graph.active {
|
|
display: block;
|
|
}
|
|
|
|
.signal-node {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 14px;
|
|
border-radius: 8px;
|
|
margin: 6px;
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
position: relative;
|
|
}
|
|
|
|
.signal-node.source {
|
|
background: rgba(124, 58, 237, 0.15);
|
|
border: 1px solid var(--accent);
|
|
color: #c4b5fd;
|
|
}
|
|
|
|
.signal-node.derived {
|
|
background: rgba(88, 166, 255, 0.15);
|
|
border: 1px solid var(--blue);
|
|
color: #93c5fd;
|
|
}
|
|
|
|
.signal-node.handler {
|
|
background: rgba(255, 166, 87, 0.15);
|
|
border: 1px solid var(--orange);
|
|
color: #fed7aa;
|
|
}
|
|
|
|
.signal-node.view-node {
|
|
background: rgba(57, 211, 83, 0.15);
|
|
border: 1px solid var(--cyan);
|
|
color: #86efac;
|
|
}
|
|
|
|
.type-badge {
|
|
font-size: 10px;
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.graph-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.graph-section-title {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--text-dim);
|
|
margin-bottom: 8px;
|
|
padding-left: 4px;
|
|
}
|
|
|
|
.dep-arrow {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
color: var(--text-dim);
|
|
font-size: 11px;
|
|
margin: 4px 8px;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* ─── Error Panel ──────────────────────────── */
|
|
.error-panel {
|
|
background: rgba(248, 81, 73, 0.05);
|
|
border-top: 1px solid rgba(248, 81, 73, 0.3);
|
|
padding: 12px 16px;
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
color: var(--red);
|
|
max-height: 150px;
|
|
overflow: auto;
|
|
display: none;
|
|
}
|
|
|
|
.error-panel.active {
|
|
display: block;
|
|
}
|
|
|
|
.error-line {
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.error-title {
|
|
font-weight: 600;
|
|
color: var(--red);
|
|
}
|
|
|
|
/* ─── Console ───────────────────────────────── */
|
|
.console-output {
|
|
background: var(--bg);
|
|
padding: 12px 16px;
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
overflow: auto;
|
|
height: 100%;
|
|
display: none;
|
|
}
|
|
|
|
.console-output.active {
|
|
display: block;
|
|
}
|
|
|
|
.console-entry {
|
|
padding: 2px 0;
|
|
border-bottom: 1px solid rgba(48, 54, 61, 0.5);
|
|
}
|
|
|
|
.console-entry .tag {
|
|
display: inline-block;
|
|
padding: 0 4px;
|
|
border-radius: 3px;
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
margin-right: 6px;
|
|
}
|
|
|
|
.tag-signal {
|
|
background: rgba(124, 58, 237, 0.2);
|
|
color: #c4b5fd;
|
|
}
|
|
|
|
.tag-effect {
|
|
background: rgba(255, 166, 87, 0.2);
|
|
color: #fed7aa;
|
|
}
|
|
|
|
.tag-type {
|
|
background: rgba(88, 166, 255, 0.2);
|
|
color: #93c5fd;
|
|
}
|
|
|
|
.tag-info {
|
|
background: rgba(57, 211, 83, 0.2);
|
|
color: #86efac;
|
|
}
|
|
|
|
/* ─── Resize handle ────────────────────────── */
|
|
.resize-handle {
|
|
width: 4px;
|
|
cursor: col-resize;
|
|
background: transparent;
|
|
transition: background 0.2s;
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
.resize-handle:hover {
|
|
background: var(--accent);
|
|
}
|
|
|
|
/* ─── Example selector ─────────────────────── */
|
|
.example-select {
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
font-size: 12px;
|
|
font-family: var(--font-sans);
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.example-select:focus {
|
|
outline: 1px solid var(--accent);
|
|
}
|
|
|
|
/* ─── Animations ───────────────────────────── */
|
|
@keyframes pulse-glow {
|
|
|
|
0%,
|
|
100% {
|
|
box-shadow: 0 0 4px var(--accent-glow);
|
|
}
|
|
|
|
50% {
|
|
box-shadow: 0 0 16px var(--accent-glow);
|
|
}
|
|
}
|
|
|
|
.compiling .logo {
|
|
animation: pulse-glow 1s ease-in-out infinite;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<!-- HEADER -->
|
|
<div class="header" id="header">
|
|
<div class="header-left">
|
|
<div class="logo">DS</div>
|
|
<div class="header-title">DreamStack <span>Playground</span></div>
|
|
</div>
|
|
<div class="header-actions">
|
|
<select class="example-select" id="exampleSelect">
|
|
<option value="counter">Counter</option>
|
|
<option value="todo">Todo List</option>
|
|
<option value="effects">Effects Demo</option>
|
|
<option value="springs">Spring Physics</option>
|
|
</select>
|
|
<div class="status-badge status-ok" id="statusBadge">● Ready</div>
|
|
<button class="btn btn-primary" id="runBtn">▶ Run</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MAIN -->
|
|
<div class="main" id="main">
|
|
|
|
<!-- LEFT PANEL: EDITOR -->
|
|
<div class="panel" id="leftPanel">
|
|
<div class="panel-header">
|
|
<span class="panel-tab active" data-tab="code">📝 Code</span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<textarea class="editor" id="editor" spellcheck="false"></textarea>
|
|
</div>
|
|
<div class="error-panel" id="errorPanel"></div>
|
|
</div>
|
|
|
|
<!-- RIGHT PANEL: PREVIEW / GRAPH / CONSOLE -->
|
|
<div class="panel" id="rightPanel">
|
|
<div class="panel-header">
|
|
<span class="panel-tab active" data-tab="preview" id="tabPreview">👁 Preview</span>
|
|
<span class="panel-tab" data-tab="graph" id="tabGraph">🔗 Signal Graph</span>
|
|
<span class="panel-tab" data-tab="console" id="tabConsole">📋 Console</span>
|
|
</div>
|
|
<div class="panel-body" style="position:relative">
|
|
<div class="preview-container active" id="previewPane">
|
|
<iframe id="previewFrame" sandbox="allow-scripts allow-same-origin"></iframe>
|
|
</div>
|
|
<div class="signal-graph" id="graphPane"></div>
|
|
<div class="console-output" id="consolePane"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════════
|
|
// DreamStack Playground — Client-side Compiler + Runtime
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
// ─── Examples ─────────────────────────────────────────────
|
|
|
|
const EXAMPLES = {
|
|
counter: `// DreamStack Counter
|
|
// Signals auto-track dependencies. No useState, no selectors.
|
|
|
|
let count = 0
|
|
let label = "DreamStack"
|
|
|
|
// Derived values: recompute automatically when dependencies change
|
|
let doubled = count * 2
|
|
let is_hot = count > 10
|
|
|
|
view main = column [
|
|
text label
|
|
text "Count: {count}"
|
|
text "Doubled: {doubled}"
|
|
when is_hot -> text "🔥 On fire!"
|
|
|
|
row [
|
|
button "-" { click: decrement }
|
|
button "+" { click: increment }
|
|
button "Reset" { click: reset }
|
|
]
|
|
]
|
|
|
|
on increment -> count += 1
|
|
on decrement -> count -= 1
|
|
on reset -> count = 0`,
|
|
|
|
todo: `// DreamStack Todo
|
|
// Reactive list rendering with signals — no virtual DOM.
|
|
|
|
let todos = []
|
|
let input_text = ""
|
|
let filter = "all"
|
|
|
|
// Derived: filtered todos
|
|
let visible = todos | filter_by(filter)
|
|
let total = todos.length
|
|
let done = todos | count_where(completed)
|
|
let active = total - done
|
|
|
|
view main = column [
|
|
text "📋 DreamStack Todos"
|
|
row [
|
|
input input_text { placeholder: "What needs doing?" }
|
|
button "Add" { click: add_todo }
|
|
]
|
|
|
|
list visible -> todo [
|
|
row [
|
|
button (if todo.completed "✅" else "⬜") { click: toggle(todo.id) }
|
|
text todo.text
|
|
button "✕" { click: remove(todo.id) }
|
|
]
|
|
]
|
|
|
|
row [
|
|
text "{active} remaining"
|
|
button "All" { click: set_filter("all") }
|
|
button "Active" { click: set_filter("active") }
|
|
button "Done" { click: set_filter("done") }
|
|
]
|
|
]
|
|
|
|
on add_todo -> {
|
|
todos = [...todos, { id: next_id(), text: input_text, completed: false }]
|
|
input_text = ""
|
|
}`,
|
|
|
|
effects: `// DreamStack Effects
|
|
// Algebraic effects for testable, composable side-effects.
|
|
|
|
effect Http.search(query: String): [Result]
|
|
effect Time.debounce(ms: Int): ()
|
|
|
|
let query = ""
|
|
let results = []
|
|
let loading = false
|
|
|
|
// Stream pipeline: debounce -> distinct -> search
|
|
let search_stream = stream from query
|
|
| debounce(250)
|
|
| distinct
|
|
| flat_map(q -> perform Http.search(q))
|
|
|
|
view main = column [
|
|
text "🔍 DreamStack Search"
|
|
input query { placeholder: "Search..." }
|
|
when loading -> text "Searching..."
|
|
list results -> item [
|
|
text item.title
|
|
text item.description
|
|
]
|
|
]
|
|
|
|
// Effect handler — swappable for testing!
|
|
handle search_stream {
|
|
Http.search(q) -> fetch("/api/search?q=" ++ q)
|
|
Time.debounce(ms) -> wait(ms)
|
|
}`,
|
|
|
|
springs: `// DreamStack Springs
|
|
// Physics-based animation — springs are signals!
|
|
|
|
let sidebar_w = spring(target: 240, stiffness: 170, damping: 26)
|
|
let card_y = spring(target: 0, stiffness: 300, damping: 30)
|
|
let expanded = true
|
|
|
|
// Derived: icon rotation follows sidebar state
|
|
let toggle_rotation = spring(target: 0, stiffness: 200, damping: 20)
|
|
|
|
view main = row [
|
|
// Sidebar
|
|
panel { width: sidebar_w } [
|
|
text "📊 Dashboard"
|
|
text "📈 Analytics"
|
|
text "⚡ Signals"
|
|
button "☰" { click: toggle_sidebar }
|
|
]
|
|
|
|
// Main area
|
|
column [
|
|
text "Welcome to DreamStack"
|
|
text "Springs are signals — they auto-propagate!"
|
|
|
|
row [
|
|
card { y: card_y } [
|
|
text "⚡ Signals: 1,247"
|
|
]
|
|
]
|
|
]
|
|
]
|
|
|
|
on toggle_sidebar -> {
|
|
expanded = !expanded
|
|
sidebar_w.target = if expanded 240 else 64
|
|
toggle_rotation.target = if expanded 0 else 180
|
|
}`
|
|
};
|
|
|
|
// ─── Mini Compiler (JS-side) ──────────────────────────────
|
|
|
|
class DSCompiler {
|
|
constructor() {
|
|
this.signals = [];
|
|
this.deriveds = [];
|
|
this.handlers = [];
|
|
this.views = [];
|
|
this.effects = [];
|
|
this.errors = [];
|
|
}
|
|
|
|
compile(source) {
|
|
this.signals = [];
|
|
this.deriveds = [];
|
|
this.handlers = [];
|
|
this.views = [];
|
|
this.effects = [];
|
|
this.errors = [];
|
|
|
|
const lines = source.split('\n');
|
|
let current_view = null;
|
|
let view_depth = 0;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
const lineNum = i + 1;
|
|
|
|
if (line.startsWith('//') || line === '') continue;
|
|
|
|
// let NAME = VALUE
|
|
if (line.startsWith('let ')) {
|
|
const match = line.match(/^let\s+(\w+)\s*=\s*(.+)$/);
|
|
if (match) {
|
|
const [, name, value] = match;
|
|
const isLiteral = /^(\d+|"[^"]*"|true|false|\[\])$/.test(value.trim());
|
|
const isSpring = value.trim().startsWith('spring(');
|
|
const deps = this.extractDeps(value);
|
|
|
|
if (isSpring) {
|
|
this.signals.push({
|
|
name, value: value.trim(), type: 'spring',
|
|
springType: 'Spring<Float>', line: lineNum
|
|
});
|
|
} else if (isLiteral) {
|
|
const ty = this.inferType(value.trim());
|
|
this.signals.push({ name, value: value.trim(), type: 'source', inferredType: ty, line: lineNum });
|
|
} else {
|
|
const ty = this.inferExprType(value.trim());
|
|
this.deriveds.push({ name, expr: value.trim(), deps, type: 'derived', inferredType: ty, line: lineNum });
|
|
}
|
|
} else {
|
|
this.errors.push({ line: lineNum, message: `Syntax error: malformed 'let' declaration` });
|
|
}
|
|
}
|
|
|
|
// effect NAME(...)
|
|
else if (line.startsWith('effect ')) {
|
|
const match = line.match(/^effect\s+(\S+)\(([^)]*)\):\s*(.+)$/);
|
|
if (match) {
|
|
const [, name, params, retType] = match;
|
|
this.effects.push({
|
|
name, params: params.split(',').map(p => p.trim()).filter(Boolean),
|
|
returnType: retType.trim(), line: lineNum
|
|
});
|
|
}
|
|
}
|
|
|
|
// on NAME -> BODY
|
|
else if (line.startsWith('on ')) {
|
|
const match = line.match(/^on\s+(\w+)\s*->\s*(.*)$/);
|
|
if (match) {
|
|
const [, event, body] = match;
|
|
this.handlers.push({ event, body: body || '', line: lineNum });
|
|
}
|
|
}
|
|
|
|
// view NAME = ...
|
|
else if (line.startsWith('view ')) {
|
|
const match = line.match(/^view\s+(\w+)\s*=\s*(.*)$/);
|
|
if (match) {
|
|
const [, name, body] = match;
|
|
this.views.push({ name, body, line: lineNum });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate: check for unbound variables in deriveds
|
|
const known = new Set([
|
|
...this.signals.map(s => s.name),
|
|
...this.deriveds.map(d => d.name),
|
|
]);
|
|
for (const d of this.deriveds) {
|
|
for (const dep of d.deps) {
|
|
if (!known.has(dep) && !['length', 'completed', 'filter_by', 'count_where'].includes(dep)) {
|
|
this.errors.push({
|
|
line: d.line,
|
|
message: `Unbound variable '${dep}' in derived expression '${d.name}'`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
signals: this.signals,
|
|
deriveds: this.deriveds,
|
|
handlers: this.handlers,
|
|
views: this.views,
|
|
effects: this.effects,
|
|
errors: this.errors,
|
|
};
|
|
}
|
|
|
|
extractDeps(expr) {
|
|
const ids = expr.match(/[a-zA-Z_]\w*/g) || [];
|
|
const keywords = new Set(['if', 'else', 'when', 'true', 'false', 'let', 'spring',
|
|
'target', 'stiffness', 'damping', 'mass', 'filter_by', 'count_where',
|
|
'debounce', 'distinct', 'flat_map', 'perform', 'stream', 'from', 'length',
|
|
'next_id', 'fetch', 'wait', 'not']);
|
|
return [...new Set(ids.filter(id => !keywords.has(id)))];
|
|
}
|
|
|
|
inferType(value) {
|
|
if (/^\d+$/.test(value)) return 'Signal<Int>';
|
|
if (/^\d+\.\d+$/.test(value)) return 'Signal<Float>';
|
|
if (/^".*"$/.test(value)) return 'Signal<String>';
|
|
if (value === 'true' || value === 'false') return 'Signal<Bool>';
|
|
if (value === '[]') return 'Signal<[?]>';
|
|
return 'Signal<?>';
|
|
}
|
|
|
|
inferExprType(expr) {
|
|
if (expr.includes('>') || expr.includes('<') || expr.includes('==')) return 'Derived<Bool>';
|
|
if (expr.includes('+') || expr.includes('-') || expr.includes('*') || expr.includes('/')) return 'Derived<Int>';
|
|
if (expr.includes('.length')) return 'Derived<Int>';
|
|
if (expr.includes('|')) return 'Derived<?>';
|
|
return 'Derived<?>';
|
|
}
|
|
}
|
|
|
|
// ─── Signal Graph Visualizer ──────────────────────────────
|
|
|
|
function renderSignalGraph(compiled) {
|
|
const graph = document.getElementById('graphPane');
|
|
let html = '';
|
|
|
|
// Sources
|
|
if (compiled.signals.length > 0) {
|
|
html += '<div class="graph-section">';
|
|
html += '<div class="graph-section-title">⚡ Source Signals</div>';
|
|
for (const s of compiled.signals) {
|
|
const ty = s.type === 'spring' ? s.springType : s.inferredType;
|
|
html += `<div class="signal-node source">
|
|
<strong>${s.name}</strong>
|
|
<span class="type-badge">${ty}</span>
|
|
<span style="color:var(--text-dim);font-size:11px">= ${escHtml(s.value)}</span>
|
|
</div>`;
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
// Derived
|
|
if (compiled.deriveds.length > 0) {
|
|
html += '<div class="graph-section">';
|
|
html += '<div class="graph-section-title">🔗 Derived Values</div>';
|
|
for (const d of compiled.deriveds) {
|
|
const depList = d.deps.join(', ');
|
|
html += `<div class="signal-node derived">
|
|
<strong>${d.name}</strong>
|
|
<span class="type-badge">${d.inferredType}</span>
|
|
<span class="dep-arrow">← ${depList || '(none)'}</span>
|
|
</div>`;
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
// Handlers
|
|
if (compiled.handlers.length > 0) {
|
|
html += '<div class="graph-section">';
|
|
html += '<div class="graph-section-title">🎯 Event Handlers</div>';
|
|
for (const h of compiled.handlers) {
|
|
html += `<div class="signal-node handler">
|
|
<strong>on ${h.event}</strong>
|
|
<span style="color:var(--text-dim);font-size:11px">→ ${escHtml(h.body).substring(0, 50)}</span>
|
|
</div>`;
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
// Effects
|
|
if (compiled.effects.length > 0) {
|
|
html += '<div class="graph-section">';
|
|
html += '<div class="graph-section-title">✨ Effect Declarations</div>';
|
|
for (const e of compiled.effects) {
|
|
html += `<div class="signal-node view-node">
|
|
<strong>${e.name}</strong>
|
|
<span class="type-badge">(${e.params.join(', ')}) → ${e.returnType}</span>
|
|
</div>`;
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
// Views
|
|
if (compiled.views.length > 0) {
|
|
html += '<div class="graph-section">';
|
|
html += '<div class="graph-section-title">👁 Views</div>';
|
|
for (const v of compiled.views) {
|
|
html += `<div class="signal-node view-node">
|
|
<strong>view ${v.name}</strong>
|
|
<span class="type-badge">View</span>
|
|
</div>`;
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
// Dependency graph (mini SVG)
|
|
if (compiled.deriveds.length > 0) {
|
|
html += '<div class="graph-section">';
|
|
html += '<div class="graph-section-title">📊 Dependency Flow</div>';
|
|
html += '<div style="padding:8px;font-family:var(--font-mono);font-size:12px;color:var(--text-dim)">';
|
|
for (const d of compiled.deriveds) {
|
|
for (const dep of d.deps) {
|
|
html += `<div style="padding:2px 0"><span style="color:var(--accent)">${dep}</span> → <span style="color:var(--blue)">${d.name}</span></div>`;
|
|
}
|
|
}
|
|
html += '</div>';
|
|
html += '</div>';
|
|
}
|
|
|
|
graph.innerHTML = html || '<div style="padding:20px;color:var(--text-dim)">Write some DreamStack code to see the signal graph</div>';
|
|
}
|
|
|
|
function escHtml(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
// ─── Console ──────────────────────────────────────────────
|
|
|
|
const consoleLogs = [];
|
|
|
|
function logToConsole(tag, message) {
|
|
consoleLogs.push({ tag, message, time: performance.now() });
|
|
renderConsole();
|
|
}
|
|
|
|
function renderConsole() {
|
|
const pane = document.getElementById('consolePane');
|
|
pane.innerHTML = consoleLogs.map(entry => {
|
|
const tagClass = entry.tag === 'signal' ? 'tag-signal'
|
|
: entry.tag === 'effect' ? 'tag-effect'
|
|
: entry.tag === 'type' ? 'tag-type'
|
|
: 'tag-info';
|
|
return `<div class="console-entry">
|
|
<span class="tag ${tagClass}">${entry.tag}</span>
|
|
${escHtml(entry.message)}
|
|
</div>`;
|
|
}).join('');
|
|
pane.scrollTop = pane.scrollHeight;
|
|
}
|
|
|
|
// ─── Preview Generation ───────────────────────────────────
|
|
|
|
function generatePreview(compiled) {
|
|
// Build a simple interactive preview from the compiled AST
|
|
const signals = compiled.signals;
|
|
const deriveds = compiled.deriveds;
|
|
const handlers = compiled.handlers;
|
|
|
|
let previewHtml = `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: 'Inter', -apple-system, system-ui, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}
|
|
.app {
|
|
background: rgba(255,255,255,0.95);
|
|
border-radius: 16px;
|
|
padding: 32px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
min-width: 320px;
|
|
max-width: 500px;
|
|
backdrop-filter: blur(20px);
|
|
}
|
|
h1 { font-size: 24px; margin-bottom: 16px; color: #1a1a2e; }
|
|
.signal-display {
|
|
background: #f0f0ff;
|
|
border-radius: 10px;
|
|
padding: 12px 16px;
|
|
margin: 8px 0;
|
|
font-size: 16px;
|
|
color: #333;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.signal-name { color: #7c3aed; font-weight: 600; font-family: monospace; }
|
|
.signal-value { color: #1a1a2e; font-weight: 700; font-size: 18px; }
|
|
.btn-row { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
|
|
button {
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: #7c3aed;
|
|
color: white;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
button:hover { background: #6d28d9; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(124,58,237,0.3); }
|
|
button:active { transform: translateY(0); }
|
|
.hot-indicator {
|
|
background: linear-gradient(135deg, #f59e0b, #ef4444);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
font-weight: 700;
|
|
font-size: 18px;
|
|
padding: 8px 0;
|
|
display: none;
|
|
}
|
|
.derived-display {
|
|
background: #e8f4fd;
|
|
border-radius: 10px;
|
|
padding: 12px 16px;
|
|
margin: 8px 0;
|
|
font-size: 16px;
|
|
color: #0a5276;
|
|
}
|
|
.spring-demo {
|
|
background: #1a1a2e;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
margin: 12px 0;
|
|
text-align: center;
|
|
color: white;
|
|
position: relative;
|
|
overflow: hidden;
|
|
height: 120px;
|
|
}
|
|
.spring-ball {
|
|
width: 40px; height: 40px;
|
|
background: #7c3aed;
|
|
border-radius: 50%;
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
transition: left 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
box-shadow: 0 0 20px rgba(124,58,237,0.5);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app">`;
|
|
|
|
// Find the view name
|
|
const viewName = compiled.views[0]?.name || 'main';
|
|
|
|
// Generate signal displays
|
|
const numSignals = signals.filter(s => s.type === 'source');
|
|
const springSignals = signals.filter(s => s.type === 'spring');
|
|
|
|
// Title from first string signal
|
|
const titleSignal = numSignals.find(s => s.inferredType === 'Signal<String>');
|
|
if (titleSignal) {
|
|
previewHtml += `<h1 id="title">${titleSignal.value.replace(/"/g, '')}</h1>`;
|
|
} else {
|
|
previewHtml += `<h1>DreamStack Preview</h1>`;
|
|
}
|
|
|
|
// Numeric signals
|
|
const intSignals = numSignals.filter(s => s.inferredType === 'Signal<Int>');
|
|
for (const s of intSignals) {
|
|
previewHtml += `<div class="signal-display">
|
|
<span class="signal-name">${s.name}</span>
|
|
<span class="signal-value" id="val_${s.name}">${s.value}</span>
|
|
</div>`;
|
|
}
|
|
|
|
// Derived displays
|
|
for (const d of deriveds) {
|
|
if (d.inferredType === 'Derived<Bool>') {
|
|
previewHtml += `<div class="hot-indicator" id="val_${d.name}">🔥 On fire!</div>`;
|
|
} else {
|
|
previewHtml += `<div class="derived-display">
|
|
<span class="signal-name">${d.name}</span> =
|
|
<span id="val_${d.name}">0</span>
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
// Spring demos
|
|
if (springSignals.length > 0) {
|
|
previewHtml += `<div class="spring-demo" id="springDemo">
|
|
<div class="spring-ball" id="springBall" style="left:50%"></div>
|
|
</div>`;
|
|
}
|
|
|
|
// Handler buttons
|
|
if (handlers.length > 0) {
|
|
previewHtml += `<div class="btn-row">`;
|
|
for (const h of handlers) {
|
|
const label = h.event.replace(/_/g, ' ');
|
|
previewHtml += `<button onclick="${h.event}()">${label}</button>`;
|
|
}
|
|
previewHtml += `</div>`;
|
|
}
|
|
|
|
previewHtml += `</div>
|
|
<script>
|
|
// ═══ DreamStack Runtime (embedded) ═══
|
|
const _signals = {};
|
|
const _effects = [];
|
|
|
|
function signal(name, initial) {
|
|
_signals[name] = { value: initial, subs: [] };
|
|
return _signals[name];
|
|
}
|
|
|
|
function update() {
|
|
// Update derived values
|
|
${deriveds.map(d => {
|
|
if (d.inferredType.includes('Bool')) {
|
|
return `{
|
|
const val = ${d.expr.replace(/(\w+)/g, (m) => {
|
|
const found = numSignals.find(s => s.name === m);
|
|
return found ? `_signals["${m}"].value` : m;
|
|
})};
|
|
const el = document.getElementById("val_${d.name}");
|
|
if (el) el.style.display = val ? "block" : "none";
|
|
}`;
|
|
} else {
|
|
return `{
|
|
try {
|
|
const val = ${d.expr.replace(/(\b[a-zA-Z_]\w*\b)/g, (m) => {
|
|
const isKnown = [...numSignals, ...deriveds].find(x => x.name === m);
|
|
return isKnown ? `_signals["${m}"]?.value || 0` : m;
|
|
})};
|
|
_signals["${d.name}"] = _signals["${d.name}"] || { value: 0 };
|
|
_signals["${d.name}"].value = val;
|
|
const el = document.getElementById("val_${d.name}");
|
|
if (el) el.textContent = val;
|
|
} catch(e) {}
|
|
}`;
|
|
}
|
|
}).join('\n ')}
|
|
|
|
// Update signal displays
|
|
${intSignals.map(s => `{
|
|
const el = document.getElementById("val_${s.name}");
|
|
if (el) el.textContent = _signals["${s.name}"].value;
|
|
}`).join('\n ')}
|
|
}
|
|
|
|
// Init signals
|
|
${numSignals.map(s => `signal("${s.name}", ${s.value});`).join('\n')}
|
|
${deriveds.map(d => `_signals["${d.name}"] = { value: 0 };`).join('\n')}
|
|
|
|
// Handler functions
|
|
${handlers.map(h => {
|
|
// Parse simple handler bodies
|
|
const body = h.body.trim();
|
|
let jsBody = '';
|
|
|
|
if (body.includes('+=')) {
|
|
const [varname, delta] = body.split('+=').map(s => s.trim());
|
|
jsBody = `_signals["${varname}"].value += ${delta}; update();`;
|
|
} else if (body.includes('-=')) {
|
|
const [varname, delta] = body.split('-=').map(s => s.trim());
|
|
jsBody = `_signals["${varname}"].value -= ${delta}; update();`;
|
|
} else if (body.includes('= ')) {
|
|
const [varname, val] = body.split('=').map(s => s.trim());
|
|
jsBody = `_signals["${varname}"].value = ${val}; update();`;
|
|
} else if (body.includes('!expanded')) {
|
|
jsBody = `
|
|
const ball = document.getElementById("springBall");
|
|
if (ball) {
|
|
const current = parseInt(ball.style.left) || 50;
|
|
ball.style.left = current > 40 ? "15%" : "85%";
|
|
}
|
|
`;
|
|
} else {
|
|
jsBody = `console.log("${h.event} triggered"); update();`;
|
|
}
|
|
|
|
return `function ${h.event}() { ${jsBody} }`;
|
|
}).join('\n')}
|
|
|
|
// Spring interaction
|
|
${springSignals.length > 0 ? `
|
|
document.getElementById("springDemo")?.addEventListener("click", (e) => {
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const x = ((e.clientX - rect.left) / rect.width * 100);
|
|
const ball = document.getElementById("springBall");
|
|
if (ball) ball.style.left = x + "%";
|
|
});
|
|
` : ''}
|
|
|
|
// Initial render
|
|
update();
|
|
<\/script>
|
|
</body>
|
|
</html>`;
|
|
|
|
return previewHtml;
|
|
}
|
|
|
|
// ─── App Logic ────────────────────────────────────────────
|
|
|
|
const compiler = new DSCompiler();
|
|
const editor = document.getElementById('editor');
|
|
const previewFrame = document.getElementById('previewFrame');
|
|
const statusBadge = document.getElementById('statusBadge');
|
|
const errorPanel = document.getElementById('errorPanel');
|
|
const runBtn = document.getElementById('runBtn');
|
|
const exampleSelect = document.getElementById('exampleSelect');
|
|
|
|
// Tab switching
|
|
const rightTabs = document.querySelectorAll('#rightPanel .panel-tab');
|
|
rightTabs.forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
rightTabs.forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
const name = tab.dataset.tab;
|
|
document.getElementById('previewPane').style.display = name === 'preview' ? 'block' : 'none';
|
|
document.getElementById('graphPane').style.display = name === 'graph' ? 'block' : 'none';
|
|
document.getElementById('consolePane').style.display = name === 'console' ? 'block' : 'none';
|
|
// Add/remove .active class
|
|
document.getElementById('previewPane').className = name === 'preview' ? 'preview-container active' : 'preview-container';
|
|
document.getElementById('graphPane').className = name === 'graph' ? 'signal-graph active' : 'signal-graph';
|
|
document.getElementById('consolePane').className = name === 'console' ? 'console-output active' : 'console-output';
|
|
});
|
|
});
|
|
|
|
function runCode() {
|
|
const source = editor.value;
|
|
const startTime = performance.now();
|
|
|
|
// Compile
|
|
const compiled = compiler.compile(source);
|
|
const compileTime = (performance.now() - startTime).toFixed(1);
|
|
|
|
// Log to console
|
|
consoleLogs.length = 0;
|
|
logToConsole('info', `Compiled in ${compileTime}ms`);
|
|
logToConsole('signal', `${compiled.signals.length} source signal(s)`);
|
|
logToConsole('type', `${compiled.deriveds.length} derived value(s)`);
|
|
logToConsole('effect', `${compiled.effects.length} effect declaration(s)`);
|
|
|
|
for (const s of compiled.signals) {
|
|
const ty = s.type === 'spring' ? s.springType : s.inferredType;
|
|
logToConsole('type', `${s.name}: ${ty}`);
|
|
}
|
|
for (const d of compiled.deriveds) {
|
|
logToConsole('type', `${d.name}: ${d.inferredType} ← [${d.deps.join(', ')}]`);
|
|
}
|
|
|
|
// Errors
|
|
if (compiled.errors.length > 0) {
|
|
statusBadge.className = 'status-badge status-err';
|
|
statusBadge.textContent = `✕ ${compiled.errors.length} error(s)`;
|
|
errorPanel.className = 'error-panel active';
|
|
errorPanel.innerHTML = compiled.errors.map(e =>
|
|
`<div class="error-line"><span class="error-title">── ERROR ─────</span> Line ${e.line}: ${escHtml(e.message)}</div>`
|
|
).join('');
|
|
for (const e of compiled.errors) {
|
|
logToConsole('type', `ERROR line ${e.line}: ${e.message}`);
|
|
}
|
|
} else {
|
|
statusBadge.className = 'status-badge status-ok';
|
|
statusBadge.textContent = `● ${compileTime}ms`;
|
|
errorPanel.className = 'error-panel';
|
|
errorPanel.innerHTML = '';
|
|
}
|
|
|
|
// Update signal graph
|
|
renderSignalGraph(compiled);
|
|
|
|
// Generate and display preview
|
|
const previewHtml = generatePreview(compiled);
|
|
const blob = new Blob([previewHtml], { type: 'text/html' });
|
|
previewFrame.src = URL.createObjectURL(blob);
|
|
}
|
|
|
|
// Run on button click
|
|
runBtn.addEventListener('click', runCode);
|
|
|
|
// Auto-run on Ctrl+Enter
|
|
editor.addEventListener('keydown', (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
e.preventDefault();
|
|
runCode();
|
|
}
|
|
// Tab key inserts spaces
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
const start = editor.selectionStart;
|
|
const end = editor.selectionEnd;
|
|
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
|
|
editor.selectionStart = editor.selectionEnd = start + 2;
|
|
}
|
|
});
|
|
|
|
// Example selector
|
|
exampleSelect.addEventListener('change', () => {
|
|
editor.value = EXAMPLES[exampleSelect.value];
|
|
runCode();
|
|
});
|
|
|
|
// Auto-compile with debounce
|
|
let compileTimer = null;
|
|
editor.addEventListener('input', () => {
|
|
clearTimeout(compileTimer);
|
|
compileTimer = setTimeout(runCode, 500);
|
|
});
|
|
|
|
// Initialize with counter example
|
|
editor.value = EXAMPLES.counter;
|
|
runCode();
|
|
</script>
|
|
</body>
|
|
|
|
</html> |