Files
crumbmissions/crumbblocks/index_v1.html
2025-12-23 20:52:00 +01:00

279 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Crumbblocks Blockly ↔ Crumbforest</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly + Python-Generator + Deutsch -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/python.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{
--bg:#0d0f12;--panel:#12151b;--ink:#e9edf5;--muted:#b7c0d0;--accent:#4caf50;--danger:#e74c3c;
--mono:ui-monospace, Menlo, Monaco, "Courier New", monospace;
}
html,body{height:100%;margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono)}
header{position:sticky;top:0;z-index:5;display:flex;gap:.6rem;align-items:center;padding:.6rem 1rem;background:#0b0d10cc;border-bottom:1px solid #1b212c}
header h2{margin:0;font-size:1rem;color:var(--muted)}
.spacer{flex:1 1 auto}
.btn{border:1px solid #263041;background:#161b22;color:var(--ink);border-radius:8px;padding:.5rem .75rem;cursor:pointer;font-weight:700}
.btn:hover{filter:brightness(1.1)}
.btn.primary{background:var(--accent);color:#0b130c;border-color:#2b6b2e}
.btn.danger{background:var(--danger);border-color:#b13a2e}
.btn.ghost{background:transparent}
#wrap{display:flex;flex-direction:column;gap:.6rem;height:calc(100% - 54px);padding:.6rem 1rem 1rem}
#blocklyDiv{height:62vh;width:100%;border-radius:10px;overflow:hidden;background:#0f1319;border:1px solid #1b212c}
#console{display:grid;grid-template-rows:auto 1fr;gap:.4rem;height:28vh;background:var(--panel);border:1px solid #1b212c;border-radius:10px}
#consoleHead{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;border-bottom:1px solid #1b212c}
#statusDot{width:.6rem;height:.6rem;border-radius:50%;background:#6a6f79;display:inline-block}
#statusText{color:var(--muted);font-size:.9rem}
#output{margin:0;padding:.6rem .8rem;overflow:auto;line-height:1.35;color:#a6ffad;background:#0c0f13;border-radius:0 0 10px 10px;white-space:pre-wrap;word-break:break-word;font-size:.93rem}
@media (max-width:960px){#blocklyDiv{height:56vh}#console{height:32vh}}
.blocklyMainBackground{fill:#0f1319 !important}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbblocks</h2>
<div class="spacer"></div>
<button class="btn" id="btnDemo">Demo laden</button>
<button class="btn ghost" id="btnZoomOut"></button>
<button class="btn ghost" id="btnZoomIn">+</button>
<button class="btn danger" id="btnTrash">Mülleimer</button>
<button class="btn primary" id="btnRun">Ausführen → Terminal</button>
</header>
<div id="wrap">
<div id="blocklyDiv" aria-label="Blockly Arbeitsfläche"></div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Logik" colour="#66b1ff">
<block type="controls_if"></block>
<block type="logic_compare"></block>
<block type="logic_operation"></block>
<block type="logic_boolean"></block>
</category>
<category name="Schleifen" colour="#6cdf6c">
<block type="controls_repeat_ext">
<value name="TIMES"><shadow type="math_number"><field name="NUM">10</field></shadow></value>
</block>
<block type="controls_whileUntil"></block>
</category>
<category name="Mathe" colour="#b68cff">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
</category>
<category name="Text" colour="#ffd866">
<block type="text"></block>
<block type="text_print"></block>
</category>
<sep gap="8"></sep>
<category name="Variablen" custom="VARIABLE" colour="#ff75b5"></category>
<category name="Funktionen" custom="PROCEDURE" colour="#e056fd"></category>
</xml>
<section id="console" aria-label="Ausgabe">
<div id="consoleHead">
<span id="statusDot" title="Status"></span>
<strong>Konsole</strong>
<span id="statusText">bereit</span>
<div class="spacer"></div>
<button class="btn ghost" id="btnClear">Leeren</button>
<button class="btn ghost" id="btnToggle">Ein-/Ausklappen</button>
</div>
<pre id="output">🌲 Terminal wartet …</pre>
</section>
</div>
<script>
// eigenes Dark-Theme
const CrumbDark = Blockly.Theme.defineTheme('CrumbDark', {
base: Blockly.Themes.Dark,
componentStyles: {
workspaceBackgroundColour: '#0f1319',
toolboxBackgroundColour: '#0e1217',
toolboxForegroundColour: '#e9edf5',
flyoutBackgroundColour: '#0f1319',
flyoutForegroundColour: '#e9edf5',
scrollbarColour: '#2a3240',
insertionMarkerColour: '#4caf50',
insertionMarkerOpacity: 0.35,
cursorColour: '#4caf50',
}
});
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: CrumbDark,
renderer: 'zelos',
trashcan: true,
grid: { spacing: 28, length: 2, colour: '#1a2029', snap: true },
move: { scrollbars: true, drag: true, wheel: true },
zoom: { controls: false, wheel: true, startScale: 1.15, maxScale: 2.2, minScale: 0.7, scaleSpeed: 1.2 },
});
window.addEventListener('resize', () => Blockly.svgResize(workspace));
// VORDEFINIERTE VARIABLEN für die Kids
const PRESET_VARS = [
'energie_wh','verbrauch_wh','sensor_temp_c','power_w','wasser_ml','zeit_s'
];
for (const name of PRESET_VARS) {
try { workspace.createVariable(name); } catch(_) {}
}
// UI helpers
const outputEl = document.getElementById('output');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const consoleBox = document.getElementById('console');
function setStatus(color, text){ statusDot.style.background=color; statusText.textContent=text||''; }
function log(line){ outputEl.textContent += (outputEl.textContent.endsWith('\n')?'':'\n')+ line; outputEl.scrollTop = outputEl.scrollHeight; }
function resetOutput(msg='🌲 Terminal wartet …'){ outputEl.textContent = msg; }
// Buttons
document.getElementById('btnZoomIn').onclick = () => workspace.zoomCenter(1);
document.getElementById('btnZoomOut').onclick = () => workspace.zoomCenter(-1);
document.getElementById('btnTrash').onclick = () => workspace.clear();
document.getElementById('btnClear').onclick = () => resetOutput('— leere Ausgabe —');
let collapsed=false;
document.getElementById('btnToggle').onclick = () => { collapsed=!collapsed; consoleBox.style.display = collapsed?'none':'grid'; };
// Demo-Workspace (Wasserkocher) via DOMParser, ohne Blockly.Xml.textToDom
const DEMO_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>energie_wh</variable>
<variable>verbrauch_wh</variable>
<variable>sensor_temp_c</variable>
<variable>power_w</variable>
<variable>wasser_ml</variable>
<variable>zeit_s</variable>
</variables>
<block type="variables_set" x="20" y="20">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE"><block type="math_number"><field name="NUM">20</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">power_w</field>
<value name="VALUE"><block type="math_number"><field name="NUM">800</field></block></value>
<next>
<block type="controls_whileUntil">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare">
<field name="OP">LT</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">100</field></block></value>
</block>
</value>
<statement name="DO">
<block type="variables_set">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">10</field></block></value>
</block>
</value>
<next>
<block type="text_print">
<value name="TEXT">
<block type="text"><field name="TEXT">Erhitze… Temp=</field></block>
</value>
<next>
<block type="text_print">
<value name="TEXT"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
</block>
</next>
</block>
</next>
</block>
</statement>
<next>
<block type="text_print">
<value name="TEXT"><block type="text"><field name="TEXT">Wasser kocht! 🔥</field></block></value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</xml>`.trim();
function loadDemo(){
try{
workspace.clear();
const dom = new DOMParser().parseFromString(DEMO_XML, 'text/xml').documentElement;
// domToWorkspace ist in allen Builds vorhanden
Blockly.Xml.domToWorkspace(dom, workspace);
Blockly.svgResize(workspace);
setStatus('#4caf50','Demo geladen');
resetOutput('🧪 Demo geladen „Wasserkocher“ bereit.');
}catch(e){
setStatus('#e74c3c','Demo-Fehler');
resetOutput('❌ Demo konnte nicht geladen werden:\n' + (e?.message||e));
console.error(e);
}
}
document.getElementById('btnDemo').onclick = loadDemo;
// Run nur aktiv wenn es Blöcke gibt
const btnRun = document.getElementById('btnRun');
function updateRunDisabled(){
const disabled = workspace.getAllBlocks(false).length===0;
btnRun.disabled = disabled;
btnRun.style.opacity = disabled ? .55 : 1;
btnRun.style.cursor = disabled ? 'not-allowed' : 'pointer';
}
workspace.addChangeListener(updateRunDisabled);
updateRunDisabled();
// Ausführen → Bridge (mit Echo-Fallback)
async function runCode(){
const code = Blockly.Python.workspaceToCode(workspace);
resetOutput('📤 Sende an Terminal…\n\n' + code);
setStatus('#f1c40f','sende…');
const ctrl = new AbortController();
const timer = setTimeout(()=>ctrl.abort(), 10000);
async function post(url){
const res = await fetch(url, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ blockcode: code }),
signal: ctrl.signal
});
clearTimeout(timer);
if(!res.ok) throw new Error('HTTP '+res.status+' '+(await res.text()).slice(0,400));
return res.json().catch(()=>({}));
}
try{
let data;
try{
// deine echte Bridge
data = await post('/crumbapi/blockly-terminal');
}catch(_){
// Fallback-Echo (damit Kids sofort Feedback sehen)
data = { ok:true, mode:'echo-fallback', code:code };
}
setStatus('#4caf50','fertig');
log('\n✅ Antwort:\n' + JSON.stringify(data, null, 2));
}catch(err){
setStatus('#e74c3c','Fehler');
log('\n❌ Fehler beim Senden:\n' + (err?.message||err));
}
}
btnRun.addEventListener('click', runCode);
// Startzustand: Demo direkt laden
loadDemo();
</script>
</body>
</html>