diff --git a/site/decodal-site/src/pages/playground.astro b/site/decodal-site/src/pages/playground.astro
index c5b8b57..47f733c 100644
--- a/site/decodal-site/src/pages/playground.astro
+++ b/site/decodal-site/src/pages/playground.astro
@@ -9,6 +9,11 @@ import ManualLayout from '../layouts/ManualLayout.astro';
Virtual files are evaluated in the browser through WebAssembly. Use import paths such as ./schemas/service.dcdl.
+
+
Loading WASM...
diff --git a/site/decodal-site/src/scripts/playground-examples.js b/site/decodal-site/src/scripts/playground-examples.js
new file mode 100644
index 0000000..9e3859d
--- /dev/null
+++ b/site/decodal-site/src/scripts/playground-examples.js
@@ -0,0 +1,174 @@
+export const playgroundExamples = [
+ {
+ id: 'service-deployment',
+ title: 'Service deployment',
+ activePath: 'main.dcdl',
+ files: {
+ 'main.dcdl': `let
+ schema = import "./schemas/service.dcdl";
+ prod = import "./env/prod.dcdl";
+ shared = import "./shared/tags.dcdl";
+in
+ schema.Service & prod.ServicePatch & {
+ name = "api";
+ image = "registry.example.com/api:2026-06-24";
+ port = 9000 + 443;
+ public = prod.is_public && 9000 + 443 > 9000;
+ tags = shared.common ++ ["api", "edge"];
+ }
+`,
+ 'schemas/service.dcdl': `Service = {
+ name = String;
+ image = String;
+ port = Int & > 443 default 8443;
+ replicas = Int & > 0 default 2;
+ public = Bool default false;
+ feature.metrics = Bool default true;
+ feature.tracing = Bool default false;
+};
+`,
+ 'env/prod.dcdl': `is_public = true;
+
+ServicePatch = {
+ replicas = 3;
+ feature.tracing = true;
+};
+`,
+ 'shared/tags.dcdl': `common = ["decodal", "prod"];
+`,
+ },
+ },
+ {
+ id: 'access-policy',
+ title: 'Access policy',
+ activePath: 'main.dcdl',
+ files: {
+ 'main.dcdl': `let
+ policy = import "./policy/base.dcdl";
+ teams = import "./policy/teams.dcdl";
+ env = "prod";
+in
+{
+ service = "payments";
+ readers = policy.defaultReaders ++ teams.observability;
+ writers = policy.defaultWriters ++ teams.payments;
+ admins = match env {
+ "prod": policy.breakglassAdmins;
+ _: teams.platform;
+ };
+ audit.required = env == "prod";
+ audit.retention_days = match env {
+ "prod": 365;
+ _: 30;
+ };
+}
+`,
+ 'policy/base.dcdl': `defaultReaders = ["group:engineering", "group:support"];
+defaultWriters = ["group:platform"];
+breakglassAdmins = ["user:oncall-primary", "user:oncall-secondary"];
+`,
+ 'policy/teams.dcdl': `platform = ["group:platform-admins"];
+payments = ["group:payments-api", "group:payments-sre"];
+observability = ["group:observability"];
+`,
+ },
+ },
+ {
+ id: 'edge-worker',
+ title: 'Edge worker config',
+ activePath: 'main.dcdl',
+ files: {
+ 'main.dcdl': `let
+ schema = import "./schemas/worker.dcdl";
+ routes = import "./routes.dcdl";
+ production = true;
+in
+ schema.Worker & {
+ name = "decodal-docs";
+ compatibility_date = "2026-06-15";
+ workers_dev = !production;
+ route.host = routes.primary_host;
+ route.paths = routes.docs_paths ++ routes.playground_paths;
+ cache.ttl_seconds = 60 * 60;
+ cache.bypass = !production;
+ }
+`,
+ 'schemas/worker.dcdl': `Worker = {
+ name = String;
+ compatibility_date = String;
+ workers_dev = Bool default false;
+ route.host = String;
+ cache.ttl_seconds = Int & >= 0 default 300;
+ cache.bypass = Bool default false;
+};
+`,
+ 'routes.dcdl': `primary_host = "decodal.example.com";
+docs_paths = ["/docs/*", "/assets/*"];
+playground_paths = ["/playground/*"];
+`,
+ },
+ },
+ {
+ id: 'data-pipeline',
+ title: 'Data pipeline',
+ activePath: 'main.dcdl',
+ files: {
+ 'main.dcdl': `let
+ schema = import "./schemas/pipeline.dcdl";
+ presets = import "./presets/batch.dcdl";
+ env = "staging";
+in
+ schema.Pipeline & presets.Batch & {
+ name = "events-rollup";
+ source.topic = "events.raw";
+ sink.table = "analytics.events_daily";
+ workers = match env {
+ "prod": 8;
+ "staging": 3;
+ _: 1;
+ };
+ alerts.enabled = env != "dev";
+ transforms = presets.standardTransforms ++ ["dedupe", "aggregate_daily"];
+ }
+`,
+ 'schemas/pipeline.dcdl': `Pipeline = {
+ name = String;
+ source.topic = String;
+ sink.table = String;
+ workers = Int & > 0 default 1;
+ batch.size = Int & >= 100 default 1000;
+ batch.timeout_seconds = Int & > 0 default 60;
+ alerts.enabled = Bool default false;
+};
+`,
+ 'presets/batch.dcdl': `standardTransforms = ["parse_json", "validate_schema", "enrich_metadata"];
+
+Batch = {
+ batch.size = 5000;
+ batch.timeout_seconds = 30 + 30;
+};
+`,
+ },
+ },
+ {
+ id: 'diagnostics',
+ title: 'Diagnostics: invalid port',
+ activePath: 'main.dcdl',
+ files: {
+ 'main.dcdl': `let
+ schema = import "./schemas/service.dcdl";
+in
+ schema.Service & {
+ name = "broken-api";
+ port = 442;
+ }
+`,
+ 'schemas/service.dcdl': `Service = {
+ name = String;
+ port = Int & > 443 default 8443;
+ replicas = Int & > 0 default 2;
+};
+`,
+ },
+ },
+];
diff --git a/site/decodal-site/src/scripts/playground.js b/site/decodal-site/src/scripts/playground.js
index f52e597..b0b79b1 100644
--- a/site/decodal-site/src/scripts/playground.js
+++ b/site/decodal-site/src/scripts/playground.js
@@ -1,26 +1,9 @@
import init, { evaluateProject } from '../wasm/decodal_wasm.js';
import { highlightDecodal } from '../lib/highlight.js';
+import { playgroundExamples } from './playground-examples.js';
const STORAGE_KEY = 'decodal-playground-project-v1';
-
-const starterFiles = {
- 'main.dcdl': `let
- schema = import "./schemas/service.dcdl";
-in
- schema.Service & {
- name = "api";
- port = 9000 + 443;
- tags = ["web"] ++ ["prod"];
- feature.enable = 9000 + 443 > 9000 && true;
- }
-`,
- 'schemas/service.dcdl': `Service = {
- name = String;
- port = Int & > 443 default 8443;
- feature.enable = Bool default true;
-};
-`,
-};
+const starterProject = playgroundExamples[0];
const source = document.getElementById('source');
const sourceHighlight = document.getElementById('source-highlight');
@@ -31,9 +14,19 @@ const fileTree = document.getElementById('file-tree');
const activeFile = document.getElementById('active-file');
const newFile = document.getElementById('new-file');
const deleteFile = document.getElementById('delete-file');
+const exampleSelect = document.getElementById('example-select');
+const loadExample = document.getElementById('load-example');
const project = loadProject();
+for (const example of playgroundExamples) {
+ const option = document.createElement('option');
+ option.value = example.id;
+ option.textContent = example.title;
+ exampleSelect.append(option);
+}
+exampleSelect.value = starterProject.id;
+
setActiveFile(project.activePath);
renderFileTree();
updateHighlight();
@@ -49,7 +42,11 @@ function loadProject() {
} catch (_error) {
// Fall back to the starter project.
}
- return { files: { ...starterFiles }, activePath: 'main.dcdl' };
+ return { files: cloneFiles(starterProject.files), activePath: starterProject.activePath };
+}
+
+function cloneFiles(files) {
+ return Object.fromEntries(Object.entries(files).map(([path, source]) => [path, source]));
}
function normalizeFiles(files) {
@@ -65,6 +62,17 @@ function saveProject() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(project));
}
+function loadExampleProject(exampleId) {
+ const example = playgroundExamples.find((item) => item.id === exampleId) ?? starterProject;
+ project.files = cloneFiles(example.files);
+ project.activePath = example.activePath;
+ exampleSelect.value = example.id;
+ setActiveFile(project.activePath);
+ output.textContent = '';
+ output.classList.remove('error');
+ execute();
+}
+
function normalizePath(path) {
const parts = [];
for (const part of String(path).replaceAll('\\', '/').split('/')) {
@@ -165,6 +173,12 @@ try {
}
run.addEventListener('click', execute);
+loadExample.addEventListener('click', () => {
+ const example = playgroundExamples.find((item) => item.id === exampleSelect.value);
+ if (!example) return;
+ if (!confirm(`Load example "${example.title}"? This replaces the current virtual files.`)) return;
+ loadExampleProject(example.id);
+});
newFile.addEventListener('click', () => {
const path = normalizePath(prompt('New virtual file path', 'schemas/types.dcdl') ?? '');
if (!path) return;
@@ -172,7 +186,7 @@ newFile.addEventListener('click', () => {
setActiveFile(path);
return;
}
- project.files[path] = path.endsWith('.dcdl') ? '' : '// Decodal source\n';
+ project.files[path] = 'value = "new";\n';
setActiveFile(path);
});
deleteFile.addEventListener('click', () => {
diff --git a/site/decodal-site/src/style.css b/site/decodal-site/src/style.css
index d8b8ed8..4d6ee42 100644
--- a/site/decodal-site/src/style.css
+++ b/site/decodal-site/src/style.css
@@ -281,7 +281,30 @@ main.playground {
.playground-actions {
align-items: center;
display: flex;
- gap: 12px;
+ flex-wrap: wrap;
+ gap: 8px;
+ justify-content: flex-end;
+}
+
+.example-picker {
+ align-items: center;
+ color: var(--muted);
+ display: flex;
+ font-size: 12px;
+ font-weight: 700;
+ gap: 6px;
+ text-transform: uppercase;
+}
+
+.example-picker select {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ color: var(--text);
+ font: inherit;
+ min-width: 180px;
+ padding: 7px 10px;
+ text-transform: none;
}
button {