OpeningHours
A week's opening hours with today marked and whether it is open now, closing soon, or when it opens next.
Demo
Opening hours
Closed · opens 09:00
| Monday (today) | 09:00–13:0014:00–18:00 |
|---|---|
| Tuesday | 09:00–18:00 |
| Wednesday | 09:00–18:00 |
| Thursday | 09:00–20:00 |
| Friday | 09:00–18:00 |
| Saturday | 10:00–02:00 |
| Sunday | ClosedBank holidays too |
Closed · opens 14:00
Closes soon · closes 20:00
Open · closes 02:00
Shut · opens tomorrow 09:00
Closed · opens tomorrow 10:00
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/opening_hours.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/opening_hours.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/opening_hours.qwe
let opening_hours_demo_week = [ { day: 1, open: "09:00", close: "13:00" }, { day: 1, open: "14:00", close: "18:00" }, { day: 2, open: "09:00", close: "18:00" }, { day: 3, open: "09:00", close: "18:00" }, { day: 4, open: "09:00", close: "20:00" }, { day: 5, open: "09:00", close: "18:00" }, { day: 6, open: "10:00", close: "02:00" },]view OpeningHoursDemo() = <div.demo-row style="align-items: flex-start; gap: 28px"> <div style="flex: 1 1 280px; max-width: 340px"> <OpeningHours id="opening-hours-demo-now" zone="Europe/Lisbon" hours={opening_hours_demo_week} notes={{ "7": "Bank holidays too" }}/> </div> <div.demo-stack style="flex: 1 1 240px"> <OpeningHours id="opening-hours-demo-lunch" compact={true} title={null} hours={opening_hours_demo_week} at="2026-11-02T13:20"/> <OpeningHours id="opening-hours-demo-soon" compact={true} title={null} hours={opening_hours_demo_week} at="2026-11-05T19:45"/> <OpeningHours id="opening-hours-demo-late" compact={true} title={null} hours={opening_hours_demo_week} at="2026-11-08T01:15"/> <OpeningHours id="opening-hours-demo-sunday" compact={true} title={null} hours={opening_hours_demo_week} at="2026-11-08T12:00" closed_label="Shut"/> <OpeningHours id="opening-hours-demo-friday" compact={true} title={null} hours={opening_hours_demo_week} at="2026-11-06T19:00"/> </div> </div>Source
ui/opening_hours.qwe
// OpeningHours: a week's opening hours, a row a day, today's row marked, and above them// whether it is open now: "Open · closes 18:00", "Closes soon · 17:45", or "Closed ·// opens Tue 09:00". A day can have more than one span (a lunch break), and a span may// close after midnight. The status is worked out when the page is made, in the place's// own time zone.//// <OpeningHours zone="Europe/Lisbon" hours={[// { day: 1, open: "09:00", close: "13:00" }, { day: 1, open: "14:00", close: "18:00" },// { day: 2, open: "09:00", close: "18:00" }, { day: 6, open: "10:00", close: "02:00" },// ]}/>//// hours: a list of { day (1 Monday to 7 Sunday), open, close ("HH:MM") }; a day with none// is closed. zone: the place's time zone, an IANA name ("Europe/Paris"; "UTC" by default).// at: the moment to judge by instead of now, "YYYY-MM-DDTHH:MM" in the place's own time.// soon: minutes before closing to say "Closes soon" (30). notes: a map of day number to a// line beside it ({ "7": "Bank holidays too" }). closed_label ("Closed"). title: a heading// ("Opening hours"; null for none). compact: just the status line, no table.import ui/theme.qwe use ui_clslet opening_hours_css = css`.ui-opening-hours { display: grid; gap: 10px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-opening-hours-title { margin: 0; font-size: 15px; font-weight: 600; }.ui-opening-hours-status { display: flex; align-items: center; gap: 8px; margin: 0; font-weight: 600; --t: var(--ui-success); }.ui-opening-hours-status::before { content: ""; flex: none; width: 9px; height: 9px; border-radius: 50%; background: var(--t); box-shadow: 0 0 0 3px color-mix(in srgb, var(--t) 22%, transparent); }.ui-opening-hours-status span { color: var(--ui-muted); font-weight: 400; }.ui-opening-hours-soon { --t: var(--ui-warning); }.ui-opening-hours-closed { --t: var(--ui-danger); }.ui-opening-hours-table { width: 100%; border-collapse: collapse; }.ui-opening-hours-table th, .ui-opening-hours-table td { padding: 6px 8px; border-top: 1px solid var(--ui-border); text-align: left; vertical-align: top; font-weight: 400; }.ui-opening-hours-table tr:first-child th, .ui-opening-hours-table tr:first-child td { border-top: 0; }.ui-opening-hours-table td { text-align: right; font-variant-numeric: tabular-nums; }.ui-opening-hours-table td span { display: block; }.ui-opening-hours-table small { display: block; color: var(--ui-muted); font-size: 12px; }.ui-opening-hours-off { color: var(--ui-muted); }.ui-opening-hours-today th, .ui-opening-hours-today td { font-weight: 600; background: var(--ui-accent-soft); }.ui-opening-hours-today th { border-radius: var(--ui-radius-sm) 0 0 var(--ui-radius-sm); }.ui-opening-hours-today td { border-radius: 0 var(--ui-radius-sm) var(--ui-radius-sm) 0; }`let opening_hours_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]// The minutes since midnight of "HH:MM".fn opening_hours_minutes(t: str) -> int = (int(slice(t, 0, 2)) ?? 0) * 60 + (int(slice(t, 3, 5)) ?? 0)// The spans open in the week as minutes since Monday 00:00: [from, to], a span closing at// or before it opens running into the next day (Sunday's into Monday, round the week).fn opening_hours_spans(hours: list) -> list = hours | map((h) => { let base = (h.day - 1) * 1440 let a = opening_hours_minutes(h.open) let b = opening_hours_minutes(h.close) { from: base + a, to: base + (b <= a ? b + 1440 : b), day: h.day, open: h.open, close: h.close } }) | sort_by((s) => s.from)// Open now? { open, soon, until, next } for the minute of the week `w`.fn opening_hours_status(spans: list, w: int, soon: int) -> map { let week = 7 * 1440 let inside = find(spans, (s) => (w >= s.from && w < s.to) || (s.to > week && w + week >= s.from && w + week < s.to)) if inside != null { let end = inside.to > week && w < inside.from ? inside.to - week : inside.to return { open: true, soon: end - w <= soon, until: inside.close, next: null } } let ahead = find(spans, (s) => s.from > w) ?? (len(spans) > 0 ? spans[0] : null) { open: false, soon: false, until: null, next: ahead }}view OpeningHours(hours: list = [], zone: str = "UTC", at: str? = null, soon: int = 30, notes: map = {}, closed_label: str = "Closed", title: str? = "Opening hours", compact: bool = false, id: str? = null, class: str? = null) { style global opening_hours_css let local = at ?? slice(date.to_zone(date.now(), zone), 0, 16) let today = date.weekday(slice(local, 0, 10)) let w = (today - 1) * 1440 + opening_hours_minutes(slice(local, 11, 16)) let spans = opening_hours_spans(hours) let st = opening_hours_status(spans, w, soon) let next_text = st.next == null ? "" : (st.next.day == today && st.next.from > w ? `opens ${st.next.open}` : (st.next.day == today % 7 + 1 ? `opens tomorrow ${st.next.open}` : `opens ${slice(opening_hours_days[st.next.day - 1], 0, 3)} ${st.next.open}`)) <div class={ui_cls(["ui-opening-hours", class])} id={id}> {title != null && <h3.ui-opening-hours-title>{title}</h3>} <p class={ui_cls(["ui-opening-hours-status", st.soon && "ui-opening-hours-soon", !st.open && "ui-opening-hours-closed"])} role="status"> {st.open ? (st.soon ? "Closes soon" : "Open") : closed_label}{" "} <span>{st.open ? `· closes ${st.until}` : (next_text != "" ? `· ${next_text}` : "")}</span> </p> {!compact && <table.ui-opening-hours-table> <tbody> {for d in range(1, 8) => { let mine = hours | filter((h) => h.day == d) | sort_by((h) => h.open) let note = get(notes, str(d)) <tr key={d} class={d == today ? "ui-opening-hours-today" : null} aria-current={d == today ? "date" : null}> <th scope="row">{opening_hours_days[d - 1]}{d == today && <span.ui-sr> (today)</span>}</th> <td> {len(mine) == 0 ? <span.ui-opening-hours-off>{closed_label}</span> : for h in mine => <span key={h.open}>{h.open}–{h.close}</span>} {note != null && <small>{note}</small>} </td> </tr> }} </tbody> </table>} </div>}Related
AgendaListEvents grouped by day with the date at the left, each with its time, title and place; today and tomorrow named.6 views · 6 downloadsDayViewOne 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 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 downloadsRelativeTime"3 minutes ago" or "in 2 hours" from an ISO time, kept up to date in the browser, with the full date on hover.6 views · 4 downloadsStopwatchA stopwatch to the hundredth: Start, Lap, Stop and Reset, with the laps listed and the fastest and slowest marked.7 views · 5 downloadsTimerA countdown set in minutes with Start, Pause and Reset, a ring that empties, presets, and a silent alert at zero.7 views · 7 downloads
Thread
Thread 0 posts
On OpeningHours: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.