# `hb-calendar-events` (calendar-events)

**Category:** calendar · **Tags:** calendar · **Package:** `@htmlbricks/hb-calendar-events`

## Summary

`hb-calendar-events` is a Bulma-styled **month calendar** rendered as a full-width table: a **localized weekday header** (short names, order **Monday → Sunday**), a **variable number of week rows**, and **per-day cells** that can show **muted “padding” days** from the previous/next month. Users can **change the visible month**, **select a day** (current month or padding cells), and **click small event chips** mapped from a JSON `events` list. The component dispatches **`changeCalendarDate`**, **`changeSelectedDate`**, and **`calendarEventClick`** on the host for integration with host apps or sibling calendars.

---

## Behavior

- **Visible month** is anchored by the `date` prop: internally, `month`/`year` are derived from that value. The default is **the first day of the current month** (`dayjs().startOf("month")`).
- **Header** (month title + prev/next controls) is shown unless `disable_header` is enabled. Prev/next call `changeMonth(±1)`, update `date`, and dispatch **`changeCalendarDate`** with `{ date }` (a `Date` for the new month anchor).
- **Weekday labels** use `Intl.DateTimeFormat` with `weekday: "short"` and the browser’s primary language (`navigator.languages[0]`, falling back to `"en"`). **Column order** is fixed in markup as Mon–Sat from `startOf("week") + 1..6` days and **Sunday** in the last column (`startOf("week")`).
- **Grid size**: `rows = ceil((daysInMonth + dayOfWeekOfMonthStart) / 7)` where `dayOfWeek` follows **dayjs**’s `day()` (0 = Sunday … 6 = Saturday). The first row mixes **previous-month** padding cells and **current-month** days; the last row mixes **current-month** days and **next-month** padding cells; middle rows are **only** current month.
- **Day selection**: clicking a `<td>` (cell) sets `selected` to the logical calendar date for that cell and dispatches **`changeSelectedDate`** with `{ selectedDate }`. **Padding cells** select dates in the adjacent month (previous or next), not only the visible month.
- **Events**: optional `events` array is **filtered per month** for the visible month, previous month, and next month so padding cells can still show chips for events that fall on those dates. For each day, chips match events whose **calendar day** (`DD`) equals the cell’s day number in the **month that cell represents** (current, previous, or next). Each chip is a `<button>`; its click handler dispatches **`calendarEventClick`** with `{ eventId }` (the event’s `id` string). Chip **`onclick` does not stop propagation**; hosts that also listen on the cell should account for possible bubbling if they attach listeners outside the shadow root.
- **`IEvent.link` and `IEvent.icon`** exist in typings but are **not used** in the current template (no link or icon rendering).
- **`date-holidays`**: the module is imported and `new Holidays("IT")` is constructed, but **no holiday data is read or displayed** in the current markup—do not rely on Italian public holidays in the UI until that logic is wired.

### `disable_header` coercion

Inside `$effect`, if `disable_header` is a string, it is treated as **true** when (case-insensitive) `"true"` or `"yes"`, or when the string is **empty** `""`; otherwise **false**. From HTML attributes, expect the usual **string** forms your custom-element layer maps to this prop.

### `events` and `selected` coercion

- If `events` is a **string**, it is **`JSON.parse`d** into an array (invalid JSON will throw at runtime).
- If `selected` is a **string**, it is parsed with **`dayjs(selected).toDate()`** (pass a value dayjs understands, e.g. ISO 8601).

---

## Graphics and layout

- **Structure**: Optional header `div` (`part="calendar-header"`) plus a **single `<table class="table is-fullwidth hb-calendar-table">`** with `<thead>` (weekdays) and `<tbody>` (day rows).
- **Table**: `width: 100%`, `height: 100%`, `table-layout: fixed`, `min-height: 34.375rem`, `border-collapse: collapse`. Each **day cell** has `vertical-align: baseline`, a **1px** border using `--hb-calendar-cell-border`, and **row height** split evenly via inline `style="height: {100/rows}%;"` on `<td>`.
- **Header** (when enabled): flex row with spacing (`is-flex`, `is-justify-content-space-between`, etc.), default **month + year** text in a title-sized span, and default **small light buttons** `˂` / `˃` for prev/next unless slots override.
- **Cells**: day number in a `.cell-date` div; **padding** days use **`.cell-date-muted`**. **Today** adds **`.cell-today`** (accent color + bold). **Selected** adds **`.cell-selected`** (background). **Hover** on any `<td>` uses `--hb-calendar-hover`.
- **Event chips**: `.cell-event` buttons, full width of the cell, stacked; default background `--hb-calendar-event-button-color` and text `--bulma-white-bis`. If `event.color` is set, inline `background-color` (and transparent border) override the default chip background.

---

## Logic (implementation notes)

| Concern | Implementation |
|--------|------------------|
| Month navigation | `changeMonth` mutates `date`, then `dispatch("changeCalendarDate", { date })`. |
| Event lists | `$derived` filters: `monthsEvent`, `previousMonthEvents`, `nextMonthEvents` by `M` + `YYYY` of `f.date`. |
| Cell click vs chip | Cell `onclick` → `selectDay(...)`; chip `onclick` → `calendarEventClick({ eventId })`. |
| `id` prop | Normalized in `$effect` to `""` when falsy; used as a conventional element id hook if your layer sets it. |

**Note:** The `$effect` block contains `if (typeof date === "string") dayjs(date).startOf("month").toDate();` **without assigning** the result to `date`. Prefer supplying **`date` as a `Date`** (or rely on your framework’s property deserialization) for a predictable visible month.

---

## Custom element tag

```text
hb-calendar-events
```

---

## Properties / attributes (snake_case)

From **`types/webcomponent.type.d.ts`**. In plain HTML, **attributes are strings**; booleans are typically **`yes` / `no`** in this project’s web-component conventions—**and** this component’s `$effect` accepts string forms for `disable_header` as described above. **`events`** should be a **JSON string** representing an array of event objects.

| Name | Type (authoring) | Role |
|------|------------------|------|
| `id` | `string` (optional) | Optional identifier; coerced to `""` when missing in `$effect`. |
| `date` | `Date` (optional) | Month anchor; default start of **current** month. Prefer a real `Date` instance. |
| `events` | `IEvent[]` or JSON **string** | Events to show as chips; optional. Parsed from string when needed. |
| `selected` | `Date` or parseable **string** (optional) | Highlights the matching day (including padding months). |
| `disable_header` | `boolean` or string (optional) | When true, the entire header block (nav + slots) is omitted. |

### `IEvent` shape (each item in `events`)

| Field | Required | Notes |
|-------|----------|--------|
| `date` | yes | Event occurrence; matched by month/year/day for chip placement. |
| `label` | yes | Chip button text. |
| `id` | yes | Passed back as `eventId` in **`calendarEventClick`**. |
| `link` | no | Typing only; not rendered. |
| `icon` | no | Typing only; not rendered. |
| `color` | no | If set, applied as inline `background-color` on the chip. |

---

## Events (host `CustomEvent`)

All dispatched on the **custom element host** via `new CustomEvent(name, { detail })`, so by default they **do not bubble** and are **not composed** through the shadow boundary—listen on the **`hb-calendar-events` element** itself.

| Event name | `detail` (TypeScript) | When |
|------------|------------------------|------|
| `calendarEventClick` | `{ eventId: string }` | User activates an event chip (`id` → `eventId`). |
| `changeCalendarDate` | `{ date: Date }` | Visible month changes (prev/next). |
| `changeSelectedDate` | `{ selectedDate: Date }` | User selects a day cell (any row, including padding). |

---

## CSS custom properties

Documented in **`extra/docs.ts`** (`styleSetup.vars`); defaults in **`styles/webcomponent.scss`** fall back to Bulma tokens.

| Variable | Role |
|----------|------|
| `--hb-calendar-event-button-color` | Default background for event chips (default: `--bulma-link`). |
| `--hb-calendar-cell-border` | 1px cell border color (default: `--bulma-border`). |
| `--hb-calendar-hover` | `<td>` hover background (default: `--bulma-background-lighter`). |
| `--hb-calendar-selected` | Selected cell background (default: `--bulma-info`). |
| `--hb-calendar-today` | “Today” day number color (default: `--bulma-primary`). |
| `--bulma-radius` | Chip border radius (docs default `0.375rem`; SCSS fallback `0.25rem`). |
| `--bulma-white-bis` | Chip text on default chip background. |
| `--bulma-text-weak` | Muted weekday header tone. |

---

## CSS parts (`::part`)

| Part | Target |
|------|--------|
| `calendar-header` | Outer header strip (month navigation area). |
| `calendar-current-time-header` | Inner span wrapping the default month/year line (and `calendar_month` slot). |
| `cell` | Each day `<td>` in the grid (current, padding, selected, today). |

---

## Slots

Names and descriptions match **`extra/docs.ts`** (`htmlSlots`).

| Slot | Purpose |
|------|---------|
| `header` | Wraps default header content: replace entire header row layout while keeping the outer `calendar-header` part on the parent when `disable_header` is false. |
| `calendar_month` | Replace the default **month name + year** text inside the title span. |
| `header_month_icon_prev` | Replace default **previous month** control (default: small `˂` button). |
| `header_month_icon_next` | Replace default **next month** control (default: small `˃` button). |

---

## Typings

Authoring types for consumers and wrappers live in:

`src/wc/calendar-events/types/webcomponent.type.d.ts`

- **`IEvent`** — one calendar entry (date, label, id, optional link/icon/color).
- **`Component`** — props: `id`, `date`, `events`, `selected`, `disable_header`.
- **`Events`** — maps custom event names to their `detail` shapes: `calendarEventClick`, `changeCalendarDate`, `changeSelectedDate`.

After a full web-component build, generated DOM / Svelte element typings may also list this tag under `types/html-elements.d.ts` and `types/svelte-elements.d.ts` in the package output.

---

## Minimal example

```html
<hb-calendar-events
  events='[{"id":"launch","label":"Launch","date":"2026-03-15T10:00:00.000Z"}]'
></hb-calendar-events>
```

With **explicit month anchor**, **selection**, **no header**, and a listener (vanilla JS):

```html
<hb-calendar-events
  id="cal-1"
  disable_header="yes"
  date="2026-03-01T00:00:00.000Z"
  selected="2026-03-15T00:00:00.000Z"
  events='[{"id":"a","label":"Ship","date":"2026-03-15T12:00:00.000Z","color":"#2574fc"}]'
></hb-calendar-events>

<script>
  const el = document.getElementById("cal-1");
  el.addEventListener("changeSelectedDate", (e) => {
    console.log("selected", e.detail.selectedDate);
  });
  el.addEventListener("calendarEventClick", (e) => {
    console.log("event", e.detail.eventId);
  });
</script>
```

Adjust attribute serialization (`yes`/`no`, JSON escaping) to match how your **custom element** layer maps host attributes to component props.
