aws-vis/index.php

244 lines
9.1 KiB
PHP

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FlowGrid (Vue 2 POC)</title>
<style>
:root{
--ink:#0f172a;
--reqBorder:#2e7d32; --reqFill:#eef7ef;
--recBorder:#8a8a8a; --recFill:#ffffff;
--doneBorder:#9ca3af; --doneInk:#475569;
--modeCol:180px; --gap:12px;
--step:260px; --arrow:34px; --done:110px;
--bg:#f6f7fb;
}
html,body{margin:0;background:var(--bg);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:var(--ink)}
.wrap{margin:24px auto 48px;padding:0 16px}
h1{font-size:22px;margin:0 0 8px}
.grid{display:flex;flex-direction:column;gap:18px}
.lane{display:grid;grid-template-columns:var(--modeCol) 1fr;gap:var(--gap);align-items:center;background:#ffffffcc;padding:12px;border-radius:12px}
.mode{font-weight:700;text-align:center;background:#fff;padding:16px 10px}
.flow{display:grid;align-items:center;gap:8px;padding:8px 0;}
.header {grid-column: span 4; }
.step{border-radius:10px;padding:10px 12px;border:2px solid var(--reqBorder);background:var(--reqFill);min-height:64px}
.step .title{font-weight:700}
.step .sub{font-size:12px;opacity:.8}
.step.rec{border-color:var(--recBorder);border-style:dashed;background:var(--recFill)}
.slot{}
.arrow{font-size:22px;line-height:1;text-align:center}
.arrow.blank{color:transparent}
.done{justify-self:start;border-radius:999px;border:2px dashed var(--doneBorder);padding:10px 14px;color:var(--doneInk);background:#fff;text-align:center}
@media (max-width:900px){
.lane{grid-template-columns:1fr}
.mode{order:-1}
.flow{grid-template-columns:1fr; background:none}
.arrow{display:none}
}
.tag{display:inline-block;padding:2px 8px;border-radius:999px;border:1.5px solid var(--reqBorder);background:var(--reqFill);font-size:12px}
.tag.rec{border-color:var(--recBorder);background:var(--recFill);border-style:dashed}
</style>
</head>
<body>
<div id="app" class="wrap">
<div class="grid">
<!-- Title lane -->
<div class="lane">
<div class="mode">&nbsp;</div>
<div class="flow" :style="{gridTemplateColumns: columns}">
<div class="header"><h1>{{ doc.title }}</h1></div>
</div>
</div>
<!-- Lanes -->
<div class="lane" v-for="(lane, li) in doc.lanes" :key="li">
<div class="mode" v-html="lane.name"></div>
<div class="flow" :style="{gridTemplateColumns: columns}">
<template v-for="(step, si) in lane.steps">
<div class="step" :class="{ rec: step.tag === 'rec' }">
<div class="title">{{ step.code }}</div>
<div class="sub" v-html="formatSub(step)"></div>
</div>
<div class="arrow"></div>
</template>
<!-- padding cells so columns align across lanes -->
<template v-for="n in (maxSteps - lane.steps.length)">
<div class="slot"></div>
<div class="arrow blank"></div>
</template>
<div class="done">Done</div>
</div>
</div>
</div>
</div>
<!-- The DSL spec lives directly in this script tag -->
<script type="text/plain" id="flow-spec">
TITLE: Online Teaching Requirements and Recommendations
VAR: --step=280px; --modeCol=180px
LANE: In Person (with Canvas)
STEP: GOTT 1 | Intro to Online Teaching with Canvas | weeks=2; hours=20; tag=rec
LANE: Online
STEP: GOTT 1 | Intro to Online Teaching with Canvas | weeks=2; hours=20; tag=req
STEP: GOTT 2 | Introduction to Asynchronous Online Teaching and Learning | weeks=4; hours=40; tag=req
LANE: Hybrid
STEP: GOTT 1 | Intro to Online Teaching with Canvas | weeks=2; hours=20; tag=req
STEP: GOTT 2 | Introduction to Asynchronous Online Teaching and Learning | weeks=4; hours=40; tag=req
STEP: GOTT 5 | Essentials of Blended Learning | weeks=2; hours=20; tag=rec
LANE: Online Live
STEP: GOTT 1 | Intro to Online Teaching with Canvas | weeks=2; hours=20; tag=req
STEP: GOTT 2 | Introduction to Asynchronous Online Teaching and Learning | weeks=4; hours=40; tag=req
STEP: GOTT 6 | Introduction to Live Online Teaching and Learning | weeks=2; hours=20; tag=rec
LANE: HyFlex
STEP: GOTT 1 | Intro to Online Teaching with Canvas | weeks=2; hours=20; tag=req
STEP: GOTT 2 | Introduction to Asynchronous Online Teaching and Learning | weeks=4; hours=40; tag=req
STEP: GOTT 6 | Introduction to Live Online Teaching and Learning | weeks=2; hours=20; tag=rec
STEP: HyFlex Tech Training | ~1 hour on-site | desc=~1 hour on-site; tag=rec
</script>
<!-- Vue 2 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
function normalizeTag(tag) {
if (!tag) return null;
const t = String(tag).toLowerCase().trim();
if (t === 'req' || t === 'required') return 'req';
if (t === 'rec' || t === 'recommended') return 'rec';
if (['none','na','n/a','optional'].includes(t)) return null;
return t;
}
function parseSpec(text) {
const lines = text.split(/\r?\n/).map(l => l.trim());
const doc = { title: 'Untitled Diagram', lanes: [], cssVars: {} };
let currentLane = null;
for (const raw of lines) {
if (!raw || raw.startsWith('#')) continue;
let m;
if ((m = raw.match(/^TITLE\s*:\s*(.+)$/i))) {
doc.title = m[1].trim();
continue;
}
if ((m = raw.match(/^VAR\s*:\s*(.+)$/i))) {
const parts = m[1].split(';').map(p => p.trim()).filter(Boolean);
for (const p of parts) {
const eq = p.indexOf('=');
if (eq > -1) {
const k = p.slice(0, eq).trim();
const v = p.slice(eq+1).trim();
doc.cssVars[k] = v;
}
}
continue;
}
if ((m = raw.match(/^LANE\s*:\s*(.+)$/i))) {
currentLane = { name: m[1].trim(), steps: [] };
doc.lanes.push(currentLane);
continue;
}
if ((m = raw.match(/^STEP\s*:\s*(.+)$/i))) {
if (!currentLane) throw new Error('STEP appears before any LANE');
const body = m[1];
const parts = body.split('|').map(s => s.trim());
if (parts.length < 2) throw new Error('STEP needs CODE | LABEL | ...');
const code = parts[0];
const label = parts[1];
const attrsBlob = parts[2] || '';
const kw = {};
if (attrsBlob) {
attrsBlob.split(';').map(s => s.trim()).filter(Boolean).forEach(kv => {
const eq = kv.indexOf('=');
if (eq > -1) {
kw[kv.slice(0,eq).trim().toLowerCase()] = kv.slice(eq+1).trim();
} else if (['req','rec'].includes(kv.toLowerCase())) {
kw['tag'] = kv.toLowerCase();
}
});
}
currentLane.steps.push({
code,
label,
weeks: kw['weeks'] || kw['w'] || null,
hours: kw['hours'] || kw['hrs'] || kw['h'] || null,
tag: normalizeTag(kw['tag']),
desc: kw['desc'] || null,
klass: kw['class'] || kw['klass'] || null
});
continue;
}
throw new Error('Unrecognized line: ' + raw);
}
return doc;
}
new Vue({
el: '#app',
data() {
const specText = document.getElementById('flow-spec').textContent;
const doc = parseSpec(specText);
// Apply CSS custom properties from VAR:
for (const k in doc.cssVars) {
if (k.trim().startsWith('--')) {
document.documentElement.style.setProperty(k.trim(), doc.cssVars[k]);
}
}
return { doc };
},
computed: {
maxSteps() {
if (!this.doc.lanes.length) return 1;
return Math.max.apply(null, this.doc.lanes.map(l => l.steps.length || 0).concat([1]));
},
columns() {
const pairs = Array(this.maxSteps).fill('var(--step) var(--arrow)').join(' ');
return pairs + ' var(--done)';
}
},
methods: {
formatSub(step) {
let core = '';
if (step.desc) {
core = this.escapeHTML(step.desc);
} else {
const bits = [this.escapeHTML(step.label)];
if (step.weeks && step.hours) {
bits.push(` · ${this.escapeHTML(step.weeks)} weeks (~${this.escapeHTML(step.hours)} hrs)`);
} else if (step.weeks) {
bits.push(` · ${this.escapeHTML(step.weeks)} weeks`);
} else if (step.hours) {
bits.push(` · ~${this.escapeHTML(step.hours)} hrs`);
}
core = bits.join('');
}
let tagHTML = '';
if (step.tag === 'req') tagHTML = '<span class="tag">Required</span>';
else if (step.tag === 'rec') tagHTML = '<span class="tag rec">Recommended</span>';
return tagHTML ? core + ' · ' + tagHTML : core;
},
escapeHTML(s) {
const d = document.createElement('div');
d.innerText = String(s);
return d.innerHTML;
}
}
});
</script>
</body>
</html>