Growth Data / Activity heatmap v1.2

FILES
heatmap.html — self-contained card with embedded fonts, styles, sample data, and JavaScript.
heatmap-card.js + heatmap-model.js + card-identity.js — reusable, dependency-free web component.
card-identity.js is generated from the official outlined logo and complete annotation icon catalog.
sample-data.json — two illustrative datasets; replace with your own observations.
ICON-LICENSE.txt — upstream Lucide/Feather notices. Preserve these notices.
fonts/ — Sora and Inter webfonts and licenses (in the component kit).

USE
Serve the component files together and import heatmap-card.js as an ES module.
Create <growth-activity-card></growth-activity-card>, then assign element.data to
an object matching sample-data.json. The default period is the first supplied year.
The component uses Shadow DOM so its styles stay inside the card.

For the standalone example, open heatmap.html in a browser. Its sample data and
fonts are embedded; it does not need network access or a framework.
The page's Copy HTML and Download HTML controls preserve the selected year,
color, light/dark theme, and Show counts setting.

DATA
{ title, metric, source, periods: [{ year, values, signals? }] }
Each period has exactly five rows, Monday–Friday, and twelve columns, January–December.
Each cell is the sum of signals on that weekday across the named month.
Weekends are excluded. Counts are not daily averages or evidence of impact.
Values must be nonnegative integers; null means unavailable, not zero.
Keep source labels factual and clearly label sample or estimated data.
Color thresholds remain fixed: 0, 1–3, 4–7, 8–11, 12+.
CSV exports one row per cell and leaves unavailable counts blank.
Optional signals is a COMPLETE array of dated records for each known bucket:
{ id, date: "2025-02-04", time: "09:00", type: "article", title, summary, source, source_url?, sourceKind? }
Dates are real Monday–Friday dates in that period, formatted YYYY-MM-DD in UTC.
Times are optional HH:mm in UTC. IDs must be unique within the period. Type uses
an annotation ID from /brand/#icons; unknown types retain their label and use the
fallback icon. Source URLs are optional absolute HTTP(S) URLs without credentials. Use source_url
from the tracked annotation (a URL string or the API's { value: URL|null } wrapper).
The legacy url field is accepted when source_url is absent. A supplied source_url
has precedence, including null for an unavailable original link.
Original records show View source with the destination hostname. Missing URLs
show Source link unavailable rather than a fabricated link. Source links open in
a new tab and retain the original path and query.
Use sourceKind: "example" for demo destinations (default: "original"). The included
records are fictional; their links are clearly labeled Example source and point
to public source destinations, not evidence of the fictional events or dates.
Every known cell count must equal its number of dated records; mismatches, invalid
records, and unsafe URLs are rejected before rendering. Null buckets cannot have records.
Omit signals for aggregate-only data: the daily view explains that records are not
included, rather than inventing daily counts. A known zero still means no signals.
Clicking a square opens the first nonempty matching date (or the first date if empty).
The date buttons show all matching weekdays in that month and their exact counts.
Previous date / Next date arrows step through those dates without wrapping. They
are disabled at the first/last date. On narrow cards, the date strip scrolls to
keep the selected date visible; the arrows remain at either end.

OPTIONS
Attributes: theme="light|dark", palette="fern|graphite", show-values (boolean).
CSS variables: --gd-body (host body font), --gd-display (host heading font).
The component emits a bubbling signal-select event when a cell is inspected.
event.detail: { year, month: 1..12, weekday: "Mon".."Fri", value: number|null }.
The day-select event fires when a daily view opens or its selected date changes.
event.detail: { date: "YYYY-MM-DD", count: number|null, records: array|null }.
Records are copied so host listeners cannot mutate the card data. Connect host
navigation or data actions to this event as needed. URL-bearing source labels
open in a new tab; entries without URLs explicitly show Source link unavailable.

ACCESSIBILITY
Native table headers and text labels identify every month, weekday, and count.
Tab enters once. Arrow keys explore; Home/End move within a row; Ctrl+Home/End
move to the first/last cell. Tab exits. Enter/Space opens the daily view and moves
focus to its heading. Date buttons and the previous/next arrows switch days. Escape or Back to heatmap returns
focus to the originating cell. Escape also closes Card actions.
Zero and unavailable states remain distinct. Counts can be shown visually.
The card scrolls horizontally at small widths and honors reduced motion.

HOSTS
These are HTML, SVG, and web-component assets for rendered chat views and dashboards.
Use the host's supported rendering surface and content security policy. The kit
is not a ChatGPT, Claude, or Grok API integration, and plain messages do not render
arbitrary HTML. Data loading and any host-specific actions belong to your application.

BRAND
Every card view, including empty states, must show the shared Growth Data header:
the official Cutout mark and outlined wordmark, with a factual source/sample label.
The outlined lockup stays consistent regardless of host fonts and reverses in dark mode.
Reuse card-identity.js in future cards; do not redraw or typeset a replacement logo.
SVG exports retain this same identity. Use the Growth Data neutral palette and one
meaningful data scale; use the existing annotation icons for event types. Font files
retain their original licenses. Brand guidance: https://growthdata.dev/brand/
Card library: https://growthdata.dev/ui/
