Data display
Windowed schedule grid
Rows of resources against a fixed time window, where each block is sized by its overlap with the window rather than by its own duration.
- schedule
- timeline
- grid
- flexbox
- time
<div class="sched-wrap">
<div class="sched-controls">
<button class="sched-btn" id="sched-prev" type="button">← 30 min</button>
<button class="sched-btn" id="sched-next" type="button">30 min →</button>
<output class="sched-window" id="sched-window"></output>
</div>
<div class="sched-panel" id="sched-panel">Select a block to see what it is.</div>
<div class="sched-guide">
<div class="sched-ruler" id="sched-ruler"><div class="sched-spacer"></div></div>
<div class="sched-body" id="sched-body"></div>
</div>
</div>Colors come from shared theme tokens — --surface, --ink, --border, --accent and friends — so this CSS carries no palette
of its own. Use Runnable file to copy the tokens along with it.
.sched-wrap { display: flex; flex-direction: column; gap: 0.9rem; }
.sched-controls { display: flex; align-items: center; gap: 0.6rem; }
.sched-btn {
font: inherit;
font-size: 0.9375rem;
padding: 0.4rem 0.8rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--ink);
cursor: pointer;
}
.sched-window {
font-family: var(--mono);
font-size: 0.9375rem;
color: var(--dim);
margin-left: 0.3rem;
}
.sched-panel {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.8rem 1rem;
background: var(--surface);
min-height: 3rem;
color: var(--dim);
}
.sched-guide {
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
background: var(--surface);
min-width: 560px;
}
.sched-ruler {
display: flex;
border-bottom: 1px solid var(--border);
background: var(--surface-2);
}
.sched-ruler .sched-spacer { flex: 0 0 148px; }
.sched-tick {
flex: 1 0 0;
padding: 0.35rem 0.5rem;
font-family: var(--mono);
font-size: 0.8125rem;
color: var(--dim);
font-variant-numeric: tabular-nums;
}
.sched-tick + .sched-tick { border-left: 1px solid var(--border); }
.sched-body { position: relative; }
.sched-row {
display: flex;
flex-wrap: nowrap;
border-top: 1px solid var(--border);
min-height: 56px;
}
.sched-row:first-child { border-top: none; }
.sched-label {
flex: 0 0 148px;
padding: 0.5rem 0.75rem;
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
justify-content: center;
gap: 1px;
}
.sched-label .name { font-weight: 650; }
.sched-label .kind { font-size: 0.8125rem; color: var(--dim); }
.sched-strip { display: flex; flex: 1; min-width: 0; }
.sched-block {
flex-shrink: 0;
min-width: 0;
align-self: stretch;
border: none;
border-left: 3px solid var(--border);
background: var(--surface);
color: var(--ink);
font-family: inherit;
font-size: 0.9375rem;
text-align: left;
padding: 0.4rem 0.5rem;
overflow: hidden;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 1px;
}
.sched-block:hover { background: var(--surface-2); }
.sched-block[aria-pressed='true'] {
background: var(--surface-2);
box-shadow: inset 0 0 0 1px var(--signal);
}
.sched-block.bare { padding: 0.4rem 0.15rem; }
.sched-block .title {
font-weight: 620;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sched-block .detail {
font-size: 0.8125rem;
color: var(--dim);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sched-block.is-break .title {
font-style: italic;
color: var(--dim);
font-weight: 500;
}
.sched-now {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: var(--bad);
pointer-events: none;
z-index: 2;
}(function () {
// Minutes-since-midnight integers throughout. No Date object touches this
// layout — the sample instant below is a fixed number, not a clock read.
var DAY_START = 480; // 8:00
var DAY_END = 1080; // 18:00
var WINDOW_MINUTES = 120;
var STEP_MINUTES = 30;
var MIN_LABEL_PERCENT = 6;
var GUTTER_PIXELS = 148;
var SAMPLE_NOW_MINUTE = 615; // fixed 10:15 sample, never read from the real clock
var rows = [
{ name: 'Platform', kind: 'Core', color: '#3a6ea5' },
{ name: 'Services', kind: 'API', color: '#b5651d' },
{ name: 'Reports', kind: 'Analytics', color: '#2f6f5e' }
];
var sessionTitles = [
'Schema audit', 'Load test', 'Idle slot', 'Cache layer',
'Incident log', 'Cleanup task', 'Backup jobs', 'Monitoring',
'Query plan', 'Data sync', 'Rate limit', 'Archive sweep'
];
// A small deterministic generator stands in for a real schedule feed, so the
// layout math below runs against rows that both align and drift across
// resources the way a real day of sessions would.
var schedule = [];
for (var rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
var cursor = DAY_START;
var pick = rowIndex;
while (cursor < DAY_END) {
var isBreak = pick % 5 === 4;
var duration = isBreak ? 5 : [30, 45, 60, 90][pick % 4];
if (cursor + duration > DAY_END) duration = DAY_END - cursor;
schedule.push({
row: rowIndex,
title: isBreak ? 'Idle slot' : sessionTitles[(pick * 3 + rowIndex) % sessionTitles.length],
detail: isBreak ? 'Failover drill' : 'Team allocation',
isBreak: isBreak,
start: cursor,
duration: duration
});
cursor += duration;
pick += 1;
}
}
var windowStart = 540; // 9:00, chosen so the sample "now" sits inside it
var selectedKey = null;
var schedBody = document.getElementById('sched-body');
var schedRuler = document.getElementById('sched-ruler');
var schedPanel = document.getElementById('sched-panel');
var schedWindow = document.getElementById('sched-window');
function clockLabel(minute) {
var wrapped = minute % 1440;
var hour = Math.floor(wrapped / 60);
var suffix = hour >= 12 ? 'pm' : 'am';
var display = hour % 12 === 0 ? 12 : hour % 12;
var minutePart = wrapped % 60;
return display + ':' + (minutePart < 10 ? '0' : '') + minutePart + suffix;
}
function entriesInWindow(rowIndex) {
var windowEnd = windowStart + WINDOW_MINUTES;
var matches = [];
for (var index = 0; index < schedule.length; index += 1) {
var entry = schedule[index];
if (entry.row !== rowIndex) continue;
if (entry.start >= windowEnd || entry.start + entry.duration <= windowStart) continue;
matches.push(entry);
}
return matches;
}
function showPanel(entry, row) {
schedPanel.innerHTML = '<strong>' + entry.title + '</strong> · ' + row.name +
', ' + clockLabel(entry.start) + ' · ' + entry.duration + ' min';
}
function renderSchedule() {
schedWindow.textContent = clockLabel(windowStart) + ' \u2013 ' + clockLabel(windowStart + WINDOW_MINUTES);
schedRuler.innerHTML = '<div class="sched-spacer"></div>';
for (var tick = 0; tick < WINDOW_MINUTES / STEP_MINUTES; tick += 1) {
var tickCell = document.createElement('div');
tickCell.className = 'sched-tick';
tickCell.textContent = clockLabel(windowStart + tick * STEP_MINUTES);
schedRuler.appendChild(tickCell);
}
schedBody.innerHTML = '';
for (var rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
var row = rows[rowIndex];
var rowElement = document.createElement('div');
rowElement.className = 'sched-row';
var labelElement = document.createElement('div');
labelElement.className = 'sched-label';
labelElement.innerHTML = '<span class="name">' + row.name + '</span><span class="kind">' + row.kind + '</span>';
rowElement.appendChild(labelElement);
var stripElement = document.createElement('div');
stripElement.className = 'sched-strip';
var visibleEntries = entriesInWindow(rowIndex);
for (var entryIndex = 0; entryIndex < visibleEntries.length; entryIndex += 1) {
var entry = visibleEntries[entryIndex];
var overlapStart = Math.max(entry.start, windowStart);
var overlapEnd = Math.min(entry.start + entry.duration, windowStart + WINDOW_MINUTES);
var widthPercent = ((overlapEnd - overlapStart) / WINDOW_MINUTES) * 100;
var isBare = widthPercent < MIN_LABEL_PERCENT;
var blockElement = document.createElement('button');
blockElement.type = 'button';
blockElement.className = 'sched-block' + (isBare ? ' bare' : '') + (entry.isBreak ? ' is-break' : '');
blockElement.style.flex = '0 0 ' + widthPercent + '%';
blockElement.style.borderLeftColor = entry.isBreak ? 'var(--dim)' : row.color;
blockElement.title = entry.title + ' \u2014 ' + clockLabel(entry.start) + ', ' + entry.duration + ' min';
var entryKey = rowIndex + ':' + entry.start;
blockElement.setAttribute('aria-pressed', selectedKey === entryKey ? 'true' : 'false');
if (!isBare) {
blockElement.innerHTML = '<span class="title">' + entry.title + '</span><span class="detail">' + entry.detail + '</span>';
}
(function (capturedEntry, capturedRow, capturedKey) {
blockElement.addEventListener('click', function () {
selectedKey = capturedKey;
showPanel(capturedEntry, capturedRow);
renderSchedule();
});
})(entry, row, entryKey);
stripElement.appendChild(blockElement);
}
rowElement.appendChild(stripElement);
schedBody.appendChild(rowElement);
}
if (SAMPLE_NOW_MINUTE >= windowStart && SAMPLE_NOW_MINUTE < windowStart + WINDOW_MINUTES) {
var marker = document.createElement('div');
marker.className = 'sched-now';
var fraction = (SAMPLE_NOW_MINUTE - windowStart) / WINDOW_MINUTES;
marker.style.left = 'calc(' + GUTTER_PIXELS + 'px + (100% - ' + GUTTER_PIXELS + 'px) * ' + fraction + ')';
schedBody.appendChild(marker);
}
}
document.getElementById('sched-prev').addEventListener('click', function () {
windowStart = Math.max(DAY_START, windowStart - STEP_MINUTES);
renderSchedule();
});
document.getElementById('sched-next').addEventListener('click', function () {
windowStart = Math.min(DAY_END - WINDOW_MINUTES, windowStart + STEP_MINUTES);
renderSchedule();
});
renderSchedule();
})();Paste this into an agent to rebuild the pattern from scratch.
Build a schedule grid: rows of resources (rooms, tracks, channels, whatever the domain calls them) laid out against a fixed time window, where each entry's block is sized by how much it overlaps the window, not by its own duration.
Reach for this whenever a schedule has to render arbitrary-duration entries inside a fixed viewport — a room booking board, a conference agenda, a channel guide, an on-call rotation. It is wrong for a full-day timeline that scrolls freely (there a duration-proportional layout with absolute positioning is simpler and correct), and wrong when entries never span more than a few minutes (then a plain list beats a grid). The pattern earns its complexity specifically when a window has to stay a fixed width while the entries behind it don't respect that width.
Keep time as minutes-since-midnight integers everywhere in the layout — never construct a `Date` inside the render path. Each row is a flex container; each visible entry is a flex child sized with `flex: 0 0 W%`, where
W = (min(entry.end, windowEnd) - max(entry.start, windowStart)) / windowMinutes * 100
Filter each row to only the entries that overlap `[windowStart, windowStart + windowMinutes)` before you render it. Because you already filtered to overlapping entries, the visible widths in a row always sum to exactly 100% — no absolute positioning, no manual gap math. A window-stepper (a "30 minutes back / forward" pair of buttons) is what makes the clipping visible: watch a block get chopped exactly at the window boundary as you step past it.
Below roughly 6% of window width a block has no room for its label — drop the text entirely and render a bare accent-colored sliver instead of letting text overflow or wrap. Keep the full label reachable anyway by putting it in the native `title` attribute, so a mouse or a screen reader can still get at it.
If you also render a "current time" marker as an absolutely positioned overlay, align it with `left: calc(<label-gutter-px> + (100% - <label-gutter-px>) * fraction)` — the calc is what lets the marker sit correctly against a strip that starts after a fixed-width row-label column, instead of drifting relative to the whole row. Two rules matter for correctness here even though a static demo can't exercise the first one live: never read the real clock during a render — initialize the "now" state to a fixed, deterministic value and only apply the actual time in an effect that runs after mount, or a server-rendered page and the client disagree about where the marker sits and the window visibly jumps on hydration. And if you offer a "jump to now" control that recomputes `windowStart` from the current minute, snap it with `Math.floor(target / step) * step`, never `Math.round` — rounding up can push the window's start past the current minute and eject "now" from the window that was supposed to contain it.
Take every color except the fixed per-row accent stripes from theme custom properties, and check both themes — the accent stripes are legitimate hardcoded sample data, everything else should recolor with the palette.