WeekView
A week of hour columns with events drawn at their times, side by side when they overlap, and all-day events across the top.
Demo
Press an event.
4
5
6
all day
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/week_view.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/week_view.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/week_view.qwe
let week_view_demo_events = [ { id: 1, title: "Stand-up", start: "2026-11-02T09:00", end: "2026-11-02T09:15" }, { id: 2, title: "Design review", start: "2026-11-03T14:00", end: "2026-11-03T15:30", place: "Room 2", href: "#week_view" }, { id: 3, title: "Lunch with Ana", start: "2026-11-03T12:30", end: "2026-11-03T13:30", tone: "success" }, { id: 4, title: "1:1 Ben", start: "2026-11-04T10:00", end: "2026-11-04T10:30", tone: "info" }, { id: 5, title: "Planning", start: "2026-11-04T10:00", end: "2026-11-04T11:30", tone: "warning" }, { id: 6, title: "Dentist", start: "2026-11-04T11:00", end: "2026-11-04T12:00", tone: "danger", place: "Rua Augusta 10" }, { id: 7, title: "Release", start: "2026-11-06T16:00", end: "2026-11-06T19:00", tone: "accent" }, { id: 8, title: "Offsite", day: "2026-11-05" }, { id: 9, title: "Ana out", day: "2026-11-05", tone: "neutral" },]component WeekViewDemoLive() { state opened = null <div.demo-stack> <WeekView start="2026-11-02" today="2026-11-04" now="2026-11-04T14:20" events={week_view_demo_events} on_open={(id) => { opened = id }}/> <p.demo-note>{opened == null ? "Press an event." : `Opened event ${opened}.`}</p> </div>}view WeekViewDemo() = <div.demo-stack> <WeekViewDemoLive/> <div id="week-view-demo-short"> <WeekView start="2026-11-04" days={3} from_hour={9} to_hour={13} hour_height={40} label="Three days" events={week_view_demo_events | filter((e) => e.id == 4 || e.id == 7 || e.id == 8)}/> </div> </div>Source
ui/week_view.qwe
// WeekView: a week as columns of hours with the events drawn where their time is,// side by side when they overlap, and all-day events in a row across the top. Today's// column is marked, and a line shows the time now when it is given.//// <WeekView start="2026-11-02" today="2026-11-04" events={[// { id: 1, title: "Stand-up", start: "2026-11-02T09:00", end: "2026-11-02T09:15", tone: "accent" },// { id: 2, title: "Design review", start: "2026-11-03T14:00", end: "2026-11-03T15:30", place: "Room 2", href: "/events/2" },// { id: 3, title: "Offsite", day: "2026-11-05" },// ]}/>//// start: the first day shown ("YYYY-MM-DD"); days: how many (7). events: a list of// { title, start, end?, day?, id?, place?, tone?, href? }: start and end are// "YYYY-MM-DDTHH:MM" (an end left out is an hour after the start); an event with `day`// and no start is all day. tone: "accent" (default), "success", "warning", "danger",// "info" or "neutral". href makes the event a link; otherwise, inside one of your// components, on_open(id) is called with its id when it is pressed.// from_hour, to_hour: the hours shown (8 to 18); an event past them is cut at the edge,// and one wholly outside them is not drawn.// hour_height: pixels per hour (48). today: the day marked ("YYYY-MM-DD"); now: the// time to draw a line at ("YYYY-MM-DDTHH:MM"). label: what screen readers call it// ("Week"); each event is heard with its day, time and place.import ui/theme.qwe use ui_clslet week_view_css = css`.ui-week-view { display: grid; grid-template-columns: 52px minmax(0, 1fr); min-width: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); font: 12px/1.3 var(--ui-font); color: var(--ui-fg); overflow: hidden; --wv-h: 48px; }.ui-week-view-corner { border-bottom: 1px solid var(--ui-border); }.ui-week-view-head { display: grid; grid-template-columns: repeat(var(--wv-days, 7), minmax(0, 1fr)); border-bottom: 1px solid var(--ui-border); }.ui-week-view-day { display: grid; justify-items: center; gap: 2px; padding: 8px 4px; border-left: 1px solid var(--ui-border); color: var(--ui-muted); font-weight: 500; text-transform: uppercase; letter-spacing: .04em; }.ui-week-view-day b { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; color: var(--ui-fg); font-size: 15px; font-weight: 600; letter-spacing: 0; }.ui-week-view-today b { background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-week-view-allday { display: grid; grid-template-columns: repeat(var(--wv-days, 7), minmax(0, 1fr)); min-height: 8px; border-bottom: 1px solid var(--ui-border); }.ui-week-view-allday-cell { display: grid; gap: 2px; padding: 3px; border-left: 1px solid var(--ui-border); }.ui-week-view-allday-label { display: flex; align-items: center; justify-content: flex-end; padding: 4px 6px; border-bottom: 1px solid var(--ui-border); color: var(--ui-muted); }.ui-week-view-scroll { grid-column: 1 / -1; display: grid; grid-template-columns: 52px minmax(0, 1fr); max-height: 560px; overflow-y: auto; }.ui-week-view-hours { position: relative; height: calc(var(--wv-hours) * var(--wv-h)); }.ui-week-view-hour { position: absolute; right: 6px; width: 100%; transform: translateY(-50%); color: var(--ui-muted); text-align: right; font-variant-numeric: tabular-nums; }.ui-week-view-body { position: relative; display: grid; grid-template-columns: repeat(var(--wv-days, 7), minmax(0, 1fr)); height: calc(var(--wv-hours) * var(--wv-h)); background-image: linear-gradient(var(--ui-border) 1px, transparent 1px); background-size: 100% var(--wv-h); }.ui-week-view-col { position: relative; border-left: 1px solid var(--ui-border); }.ui-week-view-today.ui-week-view-col { background: color-mix(in srgb, var(--ui-accent) 5%, transparent); }.ui-week-view-event { position: absolute; display: grid; align-content: start; gap: 1px; overflow: hidden; margin: 1px; padding: 3px 6px; border: 0; border-left: 3px solid var(--t); border-radius: 4px; background: color-mix(in srgb, var(--t) 16%, var(--ui-surface)); color: var(--ui-fg); font: inherit; text-align: left; text-decoration: none; cursor: pointer; --t: var(--ui-accent);}.ui-week-view-event:hover { background: color-mix(in srgb, var(--t) 28%, var(--ui-surface)); }.ui-week-view-event:focus-visible { outline: none; box-shadow: var(--ui-focus); z-index: 2; }.ui-week-view-event b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-week-view-event span { color: var(--ui-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-week-view-success { --t: var(--ui-success); }.ui-week-view-warning { --t: var(--ui-warning); }.ui-week-view-danger { --t: var(--ui-danger); }.ui-week-view-info { --t: var(--ui-info); }.ui-week-view-neutral { --t: var(--ui-strong); }.ui-week-view-allday .ui-week-view-event { position: static; margin: 0; padding: 2px 6px; }.ui-week-view-now { position: absolute; left: 0; right: 0; z-index: 1; height: 2px; background: var(--ui-danger); pointer-events: none; }.ui-week-view-now::before { content: ""; position: absolute; left: -4px; top: -3px; width: 8px; height: 8px; border-radius: 50%; background: var(--ui-danger); }@media (max-width: 640px) { .ui-week-view { grid-template-columns: 40px minmax(0, 1fr); } .ui-week-view-scroll { grid-template-columns: 40px minmax(0, 1fr); } .ui-week-view-day { font-size: 10px; } .ui-week-view-event span { display: none; } }`// The minutes since midnight of "YYYY-MM-DDTHH:MM" (or "HH:MM").fn week_view_minutes(t: str) -> int { let hm = len(t) > 5 ? slice(t, 11, 16) : t (int(slice(hm, 0, 2)) ?? 0) * 60 + (int(slice(hm, 3, 5)) ?? 0)}// n with two digits: 7 is "07".fn week_view_two(n: int) -> str = n < 10 ? `0${n}` : str(n)// "HH:MM" from minutes since midnight.fn week_view_clock(m: int) -> str = `${week_view_two(floor(m / 60))}:${week_view_two(m % 60)}`// Events of one day placed side by side where they overlap: each gets { ...e, from,// to (minutes), lane, lanes } where lanes is how many columns its cluster needs.fn week_view_lay(events: list) -> list { let sorted = events | map((e) => { ...e, from: week_view_minutes(e.start), to: e.end != null ? week_view_minutes(e.end) : week_view_minutes(e.start) + 60 }) | sort_by((e) => e.from * 10000 - e.to) let placed = sorted | reduce({ out: [], ends: [], cluster: [], cluster_end: -1 }, (acc, e) => { // A new cluster starts when this event begins after every earlier one has ended. let fresh = e.from >= acc.cluster_end let ends = fresh ? [] : acc.ends let free = ends | enumerate | find((p) => p[1] <= e.from) let lane = free != null ? free[0] : len(ends) let next_ends = free != null ? set(ends, lane, e.to) : ends | push(e.to) let flushed = fresh ? acc.out + (acc.cluster | map((c) => { ...c, lanes: len(acc.ends) })) : acc.out { out: flushed, ends: next_ends, cluster: (fresh ? [] : acc.cluster) | push({ ...e, lane }), cluster_end: fresh ? e.to : max([acc.cluster_end, e.to]) } }) placed.out + (placed.cluster | map((c) => { ...c, lanes: len(placed.ends) }))}view WeekView(start: str, events: list = [], days: int = 7, from_hour: int = 8, to_hour: int = 18, hour_height: int = 48, today: str? = null, now: str? = null, label: str = "Week", on_open = null, class: str? = null) { style global week_view_css let hours = to_hour - from_hour let dates = range(days) | map((i) => date.add_days(start, i)) // Timed events, less those wholly outside the hours shown. let timed = events | filter((e) => e.start != null) | filter((e) => week_view_minutes(e.start) < to_hour * 60 && (e.end != null ? week_view_minutes(e.end) : week_view_minutes(e.start) + 60) > from_hour * 60) let allday = events | filter((e) => e.start == null && e.day != null) let any_allday = len(allday) > 0 let px = (m: int) => (m - from_hour * 60) / 60 * hour_height let event = (e: map, d: str, styles: str?) => { let said = `${e.title}, ${date.format(d, "dddd D MMMM")}${e.from != null ? `, ${week_view_clock(e.from)} to ${week_view_clock(e.to)}` : ", all day"}${e.place != null ? `, ${e.place}` : ""}` let cls = ui_cls(["ui-week-view-event", e.tone != null && e.tone != "accent" && `ui-week-view-${e.tone}`]) let body = [<b>{e.title}</b>, e.from != null && <span>{week_view_clock(e.from)}–{week_view_clock(e.to)}{e.place != null ? ` · ${e.place}` : ""}</span>] e.href != null ? <a class={cls} href={e.href} style={styles} aria-label={said} data-id={e.id}>{body}</a> : <button class={cls} type="button" style={styles} aria-label={said} data-id={e.id} @click={() => { on_open && on_open(e.id) }}>{body}</button> } <div class={ui_cls(["ui-week-view", class])} role="group" aria-label={label} style={`--wv-days: ${days}; --wv-hours: ${hours}; --wv-h: ${hour_height}px`}> <div.ui-week-view-corner></div> <div.ui-week-view-head> {for d in dates => <div key={d} class={ui_cls(["ui-week-view-day", d == today && "ui-week-view-today"])}> <span aria-hidden="true">{date.format(d, "ddd")}</span> <b aria-label={date.format(d, "dddd D MMMM")}>{date.parts(d).day}</b> </div>} </div> {any_allday && <div.ui-week-view-allday-label>all day</div>} {any_allday && <div.ui-week-view-allday> {for d in dates => <div.ui-week-view-allday-cell key={d}> {for e in allday | filter((e) => e.day == d) => event(e, d, null)} </div>} </div>} <div.ui-week-view-scroll> <div.ui-week-view-hours aria-hidden="true"> {for h in range(from_hour + 1, to_hour) => <span.ui-week-view-hour key={h} style={`top: ${(h - from_hour) * hour_height}px`}>{week_view_two(h)}:00</span>} </div> <div.ui-week-view-body> {for d in dates => <div key={d} class={ui_cls(["ui-week-view-col", d == today && "ui-week-view-today"])}> {for e in week_view_lay(timed | filter((e) => slice(e.start, 0, 10) == d)) => { let top = max([px(e.from), 0]) let bottom = min([px(e.to), hours * hour_height]) let w = 100 / e.lanes event(e, d, `top: ${top}px; height: ${max([bottom - top, 18])}px; left: ${round(e.lane * w, 2)}%; width: ${round(w, 2)}%`) }} {now != null && slice(now, 0, 10) == d && week_view_minutes(now) >= from_hour * 60 && week_view_minutes(now) <= to_hour * 60 && <div.ui-week-view-now style={`top: ${px(week_view_minutes(now))}px`} role="img" aria-label={`Now, ${slice(now, 11, 16)}`}></div>} </div>} </div> </div> </div>}Related
DayViewOne day as a column of hours with its events drawn at their times, side by side when they overlap, and all-day events above.7 views · 7 downloadsAgendaListEvents grouped by day with the date at the left, each with its time, title and place; today and tomorrow named.6 views · 6 downloadsEventCardAn event as a card: a date block, title, time, place, a line about it, an optional picture and badge, and a button.7 views · 6 downloadsYearCalendarA year as twelve small months, with dates and runs of dates marked in colours, today ringed, and a key.7 views · 4 downloadsGanttChartTasks as bars over dates, filled by progress, in groups with spanning bars, with milestones and a today line.7 views · 7 downloadsAvailabilityGridDays across and hours down: click or drag across the cells to mark when you are available; the slots go in a hidden input.7 views · 7 downloads
Thread
Thread 0 posts
On WeekView: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.