339 lines
11 KiB
HTML
339 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<base href="$FLUTTER_BASE_HREF">
|
|
<meta charset="UTF-8">
|
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
|
<meta name="description" content="Zendegi Timeline - Dev Mode">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
|
<meta name="apple-mobile-web-app-title" content="z_flutter">
|
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
|
<title>Zendegi Timeline (Dev)</title>
|
|
<link rel="manifest" href="manifest.json">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
background: #1a1a2e;
|
|
color: #e0e0e0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
}
|
|
#toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
background: #16213e;
|
|
border-bottom: 1px solid #0f3460;
|
|
font-size: 13px;
|
|
flex-shrink: 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
#toolbar .group { display: flex; align-items: center; gap: 4px; }
|
|
#toolbar label { color: #8899aa; }
|
|
#toolbar button {
|
|
padding: 4px 10px;
|
|
border: 1px solid #0f3460;
|
|
border-radius: 4px;
|
|
background: #1a1a2e;
|
|
color: #e0e0e0;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
#toolbar button:hover { background: #0f3460; }
|
|
#toolbar .sep { width: 1px; height: 20px; background: #0f3460; }
|
|
#event-log {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
max-height: 180px;
|
|
overflow-y: auto;
|
|
background: #0d1117;
|
|
border-top: 1px solid #0f3460;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
padding: 6px 10px;
|
|
z-index: 100;
|
|
}
|
|
#event-log .entry { padding: 2px 0; border-bottom: 1px solid #161b22; }
|
|
#event-log .type { color: #58a6ff; }
|
|
#event-log .time { color: #6e7681; margin-right: 8px; }
|
|
#event-log .payload { color: #8b949e; }
|
|
#flutter-container { flex: 1; min-height: 0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="toolbar">
|
|
<div class="group">
|
|
<label>Theme:</label>
|
|
<button id="btn-light">Light</button>
|
|
<button id="btn-dark">Dark</button>
|
|
</div>
|
|
<div class="sep"></div>
|
|
<div class="group">
|
|
<label>Data:</label>
|
|
<button id="btn-few">Few items</button>
|
|
<button id="btn-many">Many items</button>
|
|
<button id="btn-empty">Empty</button>
|
|
</div>
|
|
<div class="sep"></div>
|
|
<div class="group">
|
|
<button id="btn-push">Push state</button>
|
|
<button id="btn-clear-log">Clear log</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="flutter-container"></div>
|
|
<div id="event-log"></div>
|
|
|
|
<script>
|
|
// -----------------------------------------------------------------------
|
|
// Test data generators
|
|
// -----------------------------------------------------------------------
|
|
|
|
function makeId() {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
function iso(year, month, day) {
|
|
return new Date(year, month - 1, day).toISOString();
|
|
}
|
|
|
|
function buildFewItems() {
|
|
const g1 = makeId();
|
|
const g2 = makeId();
|
|
const g3 = makeId();
|
|
|
|
const items = {};
|
|
const addItem = (groupId, title, start, end, lane, desc) => {
|
|
const id = makeId();
|
|
items[id] = {
|
|
id, groupId, title, description: desc ?? null,
|
|
start, end: end ?? null, lane,
|
|
};
|
|
};
|
|
|
|
// Work group
|
|
addItem(g1, "Project Alpha", iso(2026, 1, 5), iso(2026, 3, 15), 1, "Main project");
|
|
addItem(g1, "Project Beta", iso(2026, 2, 10), iso(2026, 5, 20), 2, "Secondary project");
|
|
addItem(g1, "Code Review", iso(2026, 3, 1), iso(2026, 3, 5), 1);
|
|
addItem(g1, "Sprint Planning", iso(2026, 1, 15), null, 3); // point event
|
|
|
|
// Personal group
|
|
addItem(g2, "Vacation", iso(2026, 4, 1), iso(2026, 4, 14), 1, "Spring break");
|
|
addItem(g2, "Birthday", iso(2026, 6, 12), null, 1); // point event
|
|
addItem(g2, "Move apartments", iso(2026, 3, 20), iso(2026, 3, 25), 2);
|
|
|
|
// Learning group
|
|
addItem(g3, "Flutter course", iso(2026, 1, 1), iso(2026, 2, 28), 1);
|
|
addItem(g3, "Rust book", iso(2026, 2, 15), iso(2026, 4, 30), 2);
|
|
addItem(g3, "Conference talk", iso(2026, 5, 10), null, 1); // point event
|
|
|
|
return {
|
|
timeline: { id: makeId(), title: "My Timeline" },
|
|
groups: {
|
|
[g1]: { id: g1, title: "Work", sortOrder: 0 },
|
|
[g2]: { id: g2, title: "Personal", sortOrder: 1 },
|
|
[g3]: { id: g3, title: "Learning", sortOrder: 2 },
|
|
},
|
|
items,
|
|
groupOrder: [g1, g2, g3],
|
|
selectedItemId: null,
|
|
darkMode: true,
|
|
};
|
|
}
|
|
|
|
function buildManyItems() {
|
|
const groupCount = 5;
|
|
const groupIds = Array.from({ length: groupCount }, makeId);
|
|
const groupNames = ["Engineering", "Design", "Marketing", "Operations", "Research"];
|
|
const groups = {};
|
|
for (let i = 0; i < groupCount; i++) {
|
|
groups[groupIds[i]] = { id: groupIds[i], title: groupNames[i], sortOrder: i };
|
|
}
|
|
|
|
const items = {};
|
|
const baseDate = new Date(2026, 0, 1);
|
|
let itemIndex = 0;
|
|
for (const gId of groupIds) {
|
|
for (let lane = 1; lane <= 3; lane++) {
|
|
for (let j = 0; j < 4; j++) {
|
|
const id = makeId();
|
|
const startOffset = j * 45 + lane * 5 + Math.floor(Math.random() * 10);
|
|
const duration = 14 + Math.floor(Math.random() * 30);
|
|
const start = new Date(baseDate);
|
|
start.setDate(start.getDate() + startOffset);
|
|
const end = new Date(start);
|
|
end.setDate(end.getDate() + duration);
|
|
const isPoint = Math.random() < 0.15;
|
|
items[id] = {
|
|
id, groupId: gId,
|
|
title: `Task ${++itemIndex}`,
|
|
description: null,
|
|
start: start.toISOString(),
|
|
end: isPoint ? null : end.toISOString(),
|
|
lane,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
timeline: { id: makeId(), title: "Large Timeline" },
|
|
groups, items,
|
|
groupOrder: groupIds,
|
|
selectedItemId: null,
|
|
darkMode: true,
|
|
};
|
|
}
|
|
|
|
function buildEmpty() {
|
|
const g1 = makeId();
|
|
return {
|
|
timeline: { id: makeId(), title: "Empty Timeline" },
|
|
groups: { [g1]: { id: g1, title: "Untitled Group", sortOrder: 0 } },
|
|
items: {},
|
|
groupOrder: [g1],
|
|
selectedItemId: null,
|
|
darkMode: true,
|
|
};
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Bridge state
|
|
// -----------------------------------------------------------------------
|
|
|
|
let currentState = buildFewItems();
|
|
let updateStateCallback = null;
|
|
|
|
window.__zendegi__ = {
|
|
getState: () => JSON.stringify(currentState),
|
|
onEvent: (json) => {
|
|
const event = JSON.parse(json);
|
|
logEvent(event);
|
|
handleEvent(event);
|
|
},
|
|
set updateState(cb) { updateStateCallback = cb; },
|
|
get updateState() { return updateStateCallback; },
|
|
};
|
|
|
|
function pushState(state) {
|
|
currentState = state;
|
|
if (updateStateCallback) {
|
|
updateStateCallback(JSON.stringify(state));
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Event handling
|
|
// -----------------------------------------------------------------------
|
|
|
|
function handleEvent(event) {
|
|
switch (event.type) {
|
|
case "content_height": {
|
|
const container = document.getElementById("flutter-container");
|
|
if (container) container.style.height = event.payload.height + "px";
|
|
break;
|
|
}
|
|
case "entry_moved": {
|
|
const { entryId, newStart, newEnd, newGroupId, newLane } = event.payload;
|
|
const item = currentState.items[entryId];
|
|
if (item) {
|
|
currentState.items[entryId] = {
|
|
...item,
|
|
start: newStart,
|
|
end: newEnd,
|
|
groupId: newGroupId,
|
|
lane: newLane,
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
case "entry_resized": {
|
|
const { entryId, newStart, newEnd, groupId, lane } = event.payload;
|
|
const item = currentState.items[entryId];
|
|
if (item) {
|
|
currentState.items[entryId] = {
|
|
...item,
|
|
start: newStart,
|
|
end: newEnd,
|
|
groupId: groupId,
|
|
lane: lane,
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
case "item_selected":
|
|
currentState.selectedItemId = event.payload.itemId;
|
|
break;
|
|
case "item_deselected":
|
|
currentState.selectedItemId = null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Event log
|
|
// -----------------------------------------------------------------------
|
|
|
|
function logEvent(event) {
|
|
const log = document.getElementById("event-log");
|
|
const entry = document.createElement("div");
|
|
entry.className = "entry";
|
|
const now = new Date().toLocaleTimeString("en-GB", { hour12: false });
|
|
const payloadStr = event.payload ? " " + JSON.stringify(event.payload) : "";
|
|
entry.innerHTML =
|
|
`<span class="time">${now}</span>` +
|
|
`<span class="type">${event.type}</span>` +
|
|
`<span class="payload">${payloadStr}</span>`;
|
|
log.appendChild(entry);
|
|
log.scrollTop = log.scrollHeight;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Toolbar actions
|
|
// -----------------------------------------------------------------------
|
|
|
|
document.getElementById("btn-dark").addEventListener("click", () => {
|
|
currentState.darkMode = true;
|
|
document.body.style.background = "#1a1a2e";
|
|
pushState(currentState);
|
|
});
|
|
|
|
document.getElementById("btn-light").addEventListener("click", () => {
|
|
currentState.darkMode = false;
|
|
document.body.style.background = "#f0f0f0";
|
|
document.body.style.color = "#333";
|
|
pushState(currentState);
|
|
});
|
|
|
|
document.getElementById("btn-few").addEventListener("click", () => {
|
|
pushState(buildFewItems());
|
|
});
|
|
|
|
document.getElementById("btn-many").addEventListener("click", () => {
|
|
pushState(buildManyItems());
|
|
});
|
|
|
|
document.getElementById("btn-empty").addEventListener("click", () => {
|
|
pushState(buildEmpty());
|
|
});
|
|
|
|
document.getElementById("btn-push").addEventListener("click", () => {
|
|
pushState(currentState);
|
|
});
|
|
|
|
document.getElementById("btn-clear-log").addEventListener("click", () => {
|
|
document.getElementById("event-log").innerHTML = "";
|
|
});
|
|
</script>
|
|
|
|
<script src="flutter_bootstrap.js" async></script>
|
|
</body>
|
|
</html>
|