diff --git a/site/decodal-site/src/scripts/playground-examples.js b/site/decodal-site/src/scripts/playground-examples.js index 40d3c3b..5aabc7d 100644 --- a/site/decodal-site/src/scripts/playground-examples.js +++ b/site/decodal-site/src/scripts/playground-examples.js @@ -3,6 +3,7 @@ export const playgroundExamples = [ id: 'product-api-production', title: 'Production API deployment', activePath: 'main.dcdl', + entryPath: 'main.dcdl', files: { 'main.dcdl': `let schema = import "./schemas/service.dcdl"; @@ -105,6 +106,7 @@ image = "registry.example.com/checkout-api:2026.06.15"; id: 'tenant-enterprise-plan', title: 'Enterprise tenant contract', activePath: 'main.dcdl', + entryPath: 'main.dcdl', files: { 'main.dcdl': `let schema = import "./schemas/tenant.dcdl"; @@ -192,6 +194,7 @@ Acme = { id: 'event-pipeline-prod', title: 'Event ingestion pipeline', activePath: 'main.dcdl', + entryPath: 'main.dcdl', files: { 'main.dcdl': `let schema = import "./schemas/pipeline.dcdl"; @@ -276,6 +279,7 @@ privacy = ["redact_ip", "hash_user_id"]; id: 'edge-cdn-policy', title: 'Edge CDN routing policy', activePath: 'main.dcdl', + entryPath: 'main.dcdl', files: { 'main.dcdl': `let schema = import "./schemas/edge.dcdl"; @@ -340,6 +344,7 @@ StrictHeaders = { id: 'incident-escalation', title: 'Incident escalation policy', activePath: 'main.dcdl', + entryPath: 'main.dcdl', files: { 'main.dcdl': `let schema = import "./schemas/oncall.dcdl"; diff --git a/site/decodal-site/src/scripts/playground.js b/site/decodal-site/src/scripts/playground.js index f76166c..44128e6 100644 --- a/site/decodal-site/src/scripts/playground.js +++ b/site/decodal-site/src/scripts/playground.js @@ -7,7 +7,7 @@ import { keymap } from '@codemirror/view'; import { decodal } from 'decodal-codemirror'; import { playgroundExamples } from './playground-examples.js'; -const STORAGE_KEY = 'decodal-playground-project-v1'; +const STORAGE_KEY = 'decodal-playground-project-v2'; const starterProject = playgroundExamples[0]; const editorHost = document.getElementById('editor'); @@ -90,6 +90,7 @@ for (const example of playgroundExamples) { exampleSelect.value = starterProject.id; setActiveFile(project.activePath); +updateRunLabel(); renderFileTree(); function loadProject() { @@ -98,12 +99,22 @@ function loadProject() { if (stored && stored.files && typeof stored.activePath === 'string') { const files = normalizeFiles(stored.files); const activePath = files[stored.activePath] === undefined ? Object.keys(files)[0] : stored.activePath; - if (activePath) return { files, activePath }; + if (activePath) { + const storedEntryPath = typeof stored.entryPath === 'string' ? normalizePath(stored.entryPath) : ''; + const entryPath = files[storedEntryPath] === undefined + ? files['main.dcdl'] === undefined ? activePath : 'main.dcdl' + : storedEntryPath; + return { files, activePath, entryPath }; + } } } catch (_error) { // Fall back to the starter project. } - return { files: cloneFiles(starterProject.files), activePath: starterProject.activePath }; + return { + files: cloneFiles(starterProject.files), + activePath: starterProject.activePath, + entryPath: starterProject.entryPath ?? starterProject.activePath, + }; } function cloneFiles(files) { @@ -127,6 +138,7 @@ function loadExampleProject(exampleId) { const example = playgroundExamples.find((item) => item.id === exampleId) ?? starterProject; project.files = cloneFiles(example.files); project.activePath = example.activePath; + project.entryPath = example.entryPath ?? example.activePath; exampleSelect.value = example.id; setActiveFile(project.activePath); output.textContent = ''; @@ -166,16 +178,27 @@ function setActiveFile(path) { } project.activePath = normalized; setEditorText(project.files[normalized]); - activeFile.textContent = normalized; + activeFile.textContent = normalized === project.entryPath ? `${normalized} (entry)` : normalized; deleteFile.disabled = Object.keys(project.files).length <= 1; renderFileTree(); + updateRunLabel(); saveProject(); } +function updateRunLabel() { + const entryPath = project.files[project.entryPath] === undefined ? project.activePath : project.entryPath; + project.entryPath = entryPath; + run.textContent = `Run ${entryPath}`; + run.title = `Materialize ${entryPath}`; +} + function execute() { project.files[project.activePath] = getEditorText(); + const entryPath = project.files[project.entryPath] === undefined ? project.activePath : project.entryPath; + project.entryPath = entryPath; + updateRunLabel(); saveProject(); - const result = JSON.parse(evaluateProject(project.activePath, JSON.stringify(project.files))); + const result = JSON.parse(evaluateProject(entryPath, JSON.stringify(project.files))); output.textContent = result.ok ? result.output : result.error; output.classList.toggle('error', !result.ok); } @@ -224,8 +247,8 @@ function renderTreeList(children) { const button = document.createElement('button'); button.type = 'button'; button.className = child.path === project.activePath ? 'file active' : 'file'; - button.textContent = child.name; - button.title = child.path; + button.textContent = child.path === project.entryPath ? `${child.name} *` : child.name; + button.title = child.path === project.entryPath ? `${child.path} (entry point)` : child.path; button.addEventListener('click', () => setActiveFile(child.path)); item.append(button); } else { @@ -276,5 +299,8 @@ deleteFile.addEventListener('click', () => { if (Object.keys(project.files).length <= 1) return; if (!confirm(`Delete ${project.activePath}?`)) return; delete project.files[project.activePath]; + if (project.files[project.entryPath] === undefined) { + project.entryPath = Object.keys(project.files).sort()[0]; + } setActiveFile(Object.keys(project.files).sort()[0]); });