This commit is contained in:
2026-03-04 14:16:51 +01:00
parent 1cca200eda
commit 765aa83fb6
24 changed files with 1370 additions and 424 deletions

View File

@@ -1,46 +1,338 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<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">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>z_flutter</title>
<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>
<!--
You can customize the "flutter_bootstrap.js" script.
This is useful to provide a custom configuration to the Flutter loader
or to give the user feedback during the initialization process.
<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>
For more details:
* https://docs.flutter.dev/platform-integration/web/initialization
-->
<script src="flutter_bootstrap.js" async></script>
</body>
</html>