Scheduling

YearCalendar

A year as twelve small months, with dates and runs of dates marked in colours, today ringed, and a key.

#schedule#calendar#data#no-js

Demo

Preview
  • Public holiday
  • Leave
  • Travel
  • Deadline
January
SMTWTFS
12
3456789
10111213141516
17181920212223
24252627282930
31
February
SMTWTFS
123456
78910111213
14151617181920
21222324252627
28
March
SMTWTFS
123456
78910111213
14151617181920
21222324252627
28293031
April
SMTWTFS
123
45678910
11121314151617
18192021222324
252627282930
May
SMTWTFS
1
2345678
9101112131415
16171819202122
23242526272829
3031
June
SMTWTFS
12345
6789101112
13141516171819
20212223242526
27282930
July
SMTWTFS
123
45678910
11121314151617
18192021222324
25262728293031
August
SMTWTFS
1234567
891011121314
15161718192021
22232425262728
293031
September
SMTWTFS
1234
567891011
12131415161718
19202122232425
2627282930
October
SMTWTFS
12
3456789
10111213141516
17181920212223
24252627282930
31
November
SMTWTFS
123456
78910111213
14151617181920
21222324252627
282930
December
SMTWTFS
1234
567891011
12131415161718
19202122232425
262728293031

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/year_calendar.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/year_calendar.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/year_calendar.qwe
view YearCalendarDemo() =    <div.demo-stack>        <YearCalendar id="year-calendar-demo" year={2026} today="2026-10-28" href="#year_calendar-{day}"                      marks={[{ day: "2026-01-01", tone: "danger", label: "New Year's Day" }, { day: "2026-04-03", tone: "danger", label: "Good Friday" },                              { day: "2026-04-25", tone: "danger", label: "Freedom Day" }, { day: "2026-12-25", tone: "danger", label: "Christmas" },                              { day: "2026-08-03", to: "2026-08-14", tone: "success", label: "Summer leave" },                              { day: "2026-03-16", to: "2026-03-18", tone: "info", label: "Conference" },                              { day: "2026-11-20", tone: "warning", label: "Release" }]}                      legend={[{ tone: "danger", label: "Public holiday" }, { tone: "success", label: "Leave" },                               { tone: "info", label: "Travel" }, { tone: "warning", label: "Deadline" }]}/>        <div id="year-calendar-demo-sunday" style="max-width: 240px">            <YearCalendar year={2027} week_start={7} label="2027, weeks from Sunday"/>        </div>    </div>

Source

ui/year_calendar.qwe
// YearCalendar: a whole year as twelve small months, with dates marked in colours// (holidays, bookings, days off) and today ringed; a key under it says what each colour// means. Each month is a table, and a marked day is named with what it is.////   <YearCalendar year={2026} today="2026-10-28" marks={[//       { day: "2026-12-25", tone: "danger", label: "Christmas" },//       { day: "2026-08-03", to: "2026-08-14", tone: "success", label: "Holiday" },//   ]} legend={[{ tone: "danger", label: "Public holiday" }, { tone: "success", label: "Leave" }]}/>//// year: the year shown. marks: a list of { day ("YYYY-MM-DD"), to? (the last day of a run// of days), tone? ("accent" default, "success", "warning", "danger", "info"), label? }.// today: the day ringed ("YYYY-MM-DD"). week_start: 1 (Monday, default) or 7 (Sunday).// legend: a list of { tone, label } shown under the months. href: a link for each day,// with {day} replaced by its date ("/day/{day}"), to open it. label: what screen readers// call it (the year).import ui/theme.qwe use ui_clslet year_calendar_css = css`.ui-year-calendar { display: grid; gap: 14px; min-width: 0; font: 12px/1.2 var(--ui-font); color: var(--ui-fg); }.ui-year-calendar-months { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 18px 22px; }.ui-year-calendar-month { width: 100%; border-collapse: collapse; table-layout: fixed; }.ui-year-calendar-month caption { padding: 0 0 6px 4px; font-size: 13px; font-weight: 600; text-align: left; }.ui-year-calendar-month th { padding: 2px 0; color: var(--ui-muted); font-size: 10px; font-weight: 600; text-align: center; }.ui-year-calendar-month td { padding: 1px; text-align: center; }.ui-year-calendar-day { display: grid; place-items: center; width: 100%; aspect-ratio: 1; max-width: 26px; margin: auto; border-radius: 50%; color: inherit; font-variant-numeric: tabular-nums; text-decoration: none; --t: var(--ui-accent); }a.ui-year-calendar-day:hover { background: var(--ui-subtle); }a.ui-year-calendar-day:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-year-calendar-weekend { color: var(--ui-muted); }.ui-year-calendar-marked { background: color-mix(in srgb, var(--t) 22%, transparent); color: var(--ui-fg); font-weight: 600; }a.ui-year-calendar-marked:hover { background: color-mix(in srgb, var(--t) 36%, transparent); }.ui-year-calendar-today { box-shadow: inset 0 0 0 2px var(--ui-accent); font-weight: 700; }.ui-year-calendar-success { --t: var(--ui-success); }.ui-year-calendar-warning { --t: var(--ui-warning); }.ui-year-calendar-danger { --t: var(--ui-danger); }.ui-year-calendar-info { --t: var(--ui-info); }.ui-year-calendar-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 12px; }.ui-year-calendar-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-year-calendar-legend i { width: 12px; height: 12px; border-radius: 50%; background: color-mix(in srgb, var(--t) 40%, transparent); --t: var(--ui-accent); }`let year_calendar_months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]let year_calendar_weekdays = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]// The marks as a map of each date they cover to the mark: runs are spread over their days.fn year_calendar_by_day(marks: list) -> map =    marks | reduce({}, (acc, m) => {        let n = m.to != null ? date.days_between(m.day, m.to) : 0        range(0, n + 1) | reduce(acc, (a, i) => set(a, date.add_days(m.day, i), m))    })view YearCalendar(year: int, marks: list = [], today: str? = null, week_start: int = 1, legend: list = [], href: str? = null,                  label: str? = null, id: str? = null, class: str? = null) {    style global year_calendar_css    let by_day = year_calendar_by_day(marks)    let heads = range(0, 7) | map((i) => year_calendar_weekdays[(i + week_start - 1) % 7])    <section class={ui_cls(["ui-year-calendar", class])} id={id} aria-label={label ?? str(year)}>        <div.ui-year-calendar-months>            {for m in range(1, 13) => {                let first = date.of(year, m, 1)                let lead = (date.weekday(first) - week_start + 7) % 7                let n = date.days_in_month(year, m)                let cells = (range(0, lead) | map((_i) => null)) + range(1, n + 1)                let weeks = range(0, ceil(len(cells) / 7)) | map((r) => slice(cells, r * 7, r * 7 + 7))                <table.ui-year-calendar-month key={m}>                    <caption>{year_calendar_months[m - 1]}</caption>                    <thead><tr>{for h in heads => <th key={h} scope="col" abbr={h}>{slice(h, 0, 1)}</th>}</tr></thead>                    <tbody>                        {for [r, wk] in enumerate(weeks) => <tr key={r}>                            {for [c, d] in enumerate(wk) => {                                let iso = d == null ? null : date.of(year, m, d)                                let mark = iso == null ? null : get(by_day, iso)                                let wd = iso == null ? 0 : date.weekday(iso)                                let cls = ui_cls(["ui-year-calendar-day", wd >= 6 && "ui-year-calendar-weekend",                                                  mark != null && "ui-year-calendar-marked",                                                  mark != null && mark.tone != null && mark.tone != "accent" && `ui-year-calendar-${mark.tone}`,                                                  iso == today && "ui-year-calendar-today"])                                let said = iso == null ? null : `${d} ${year_calendar_months[m - 1]}${iso == today ? ", today" : ""}${mark != null && mark.label != null ? `, ${mark.label}` : ""}`                                <td key={c}>                                    {iso != null && (href != null                                        ? <a class={cls} href={replace(href, "{day}", iso)} data-day={iso} aria-label={said} title={mark?.label}>{d}</a>                                        : <span class={cls} data-day={iso} aria-label={said} title={mark?.label} role={mark != null || iso == today ? "img" : null}>{d}</span>)}                                </td>                            }}                        </tr>}                    </tbody>                </table>            }}        </div>        {len(legend) > 0 && <ul.ui-year-calendar-legend aria-label="Key">            {for l in legend => <li key={l.label}><i class={l.tone != null && l.tone != "accent" ? `ui-year-calendar-${l.tone}` : null} aria-hidden="true"></i>{l.label}</li>}        </ul>}    </section>}
Thread
74Useful? Like it, or add to its thread below.

Thread 0 posts

On YearCalendar: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.