@f-ewald/components 0.7.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -47
- package/custom-elements.json +2236 -966
- package/dist/calendar-entry.d.ts +33 -0
- package/dist/calendar-entry.d.ts.map +1 -0
- package/dist/calendar-entry.js +64 -0
- package/dist/calendar-entry.js.map +1 -0
- package/dist/calendar-month.d.ts +67 -0
- package/dist/calendar-month.d.ts.map +1 -0
- package/dist/calendar-month.js +455 -0
- package/dist/calendar-month.js.map +1 -0
- package/dist/calendar-year.d.ts +43 -0
- package/dist/calendar-year.d.ts.map +1 -0
- package/dist/calendar-year.js +136 -0
- package/dist/calendar-year.js.map +1 -0
- package/dist/form-select.d.ts +21 -2
- package/dist/form-select.d.ts.map +1 -1
- package/dist/form-select.js +431 -27
- package/dist/form-select.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/map-circle.d.ts +3 -0
- package/dist/map-circle.d.ts.map +1 -1
- package/dist/map-circle.js +3 -0
- package/dist/map-circle.js.map +1 -1
- package/dist/tokens.css +4 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +3 -0
- package/dist/tokens.js.map +1 -1
- package/dist/utils/calendar.d.ts +77 -0
- package/dist/utils/calendar.d.ts.map +1 -0
- package/dist/utils/calendar.js +135 -0
- package/dist/utils/calendar.js.map +1 -0
- package/docs/calendar-entry.md +53 -0
- package/docs/calendar-month.md +77 -0
- package/docs/calendar-year.md +53 -0
- package/docs/form-select.md +15 -3
- package/docs/map-circle.md +4 -0
- package/llms.txt +117 -28
- package/package.json +10 -2
- package/dist/map-point.d.ts +0 -31
- package/dist/map-point.d.ts.map +0 -1
- package/dist/map-point.js +0 -83
- package/dist/map-point.js.map +0 -1
- package/docs/map-point.md +0 -44
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
import type { StatusPillColor } from "./status-pill.js";
|
|
3
|
+
/**
|
|
4
|
+
* Declarative metadata for one calendar event, consumed by a parent
|
|
5
|
+
* `calendar-month` or `calendar-year`. Read-only/non-interactive; renders
|
|
6
|
+
* nothing itself.
|
|
7
|
+
*
|
|
8
|
+
* @element calendar-entry
|
|
9
|
+
* @slot title - Plain-text title shown instead of the `label` fallback.
|
|
10
|
+
* @slot detail - Repeatable plain-text details rendered inside the shared body spanning all remaining days.
|
|
11
|
+
* @slot footer - Plain-text ending note pinned to the bottom of the shared body.
|
|
12
|
+
*/
|
|
13
|
+
export declare class CalendarEntry extends LitElement {
|
|
14
|
+
/** Inclusive start date, `"YYYY-MM-DD"`. */
|
|
15
|
+
start: string;
|
|
16
|
+
/** Inclusive end date, `"YYYY-MM-DD"`. Falls back to `start` when unset (single-day entry). */
|
|
17
|
+
end: string;
|
|
18
|
+
/** Fallback title used when no `title` slot is provided. */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Color variant, reusing `status-pill`'s palette. */
|
|
21
|
+
color: StatusPillColor;
|
|
22
|
+
/** Optional link target; the parent renders the entry as an `<a>` when set. */
|
|
23
|
+
href?: string;
|
|
24
|
+
static styles: import("lit").CSSResult[];
|
|
25
|
+
/** Keeps the metadata host hidden while exposing named text slots to the parent calendar. */
|
|
26
|
+
protected render(): import("lit-html").TemplateResult<1>;
|
|
27
|
+
}
|
|
28
|
+
declare global {
|
|
29
|
+
interface HTMLElementTagNameMap {
|
|
30
|
+
"calendar-entry": CalendarEntry;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=calendar-entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar-entry.d.ts","sourceRoot":"","sources":["../src/calendar-entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD;;;;;;;;;GASG;AACH,qBACa,aAAc,SAAQ,UAAU;IAC3C,4CAA4C;IACf,KAAK,SAAM;IAExC,+FAA+F;IAClE,GAAG,SAAM;IAEtC,4DAA4D;IAC/B,KAAK,SAAM;IAExC,sDAAsD;IACzB,KAAK,EAAE,eAAe,CAAa;IAEhE,+EAA+E;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IAE3C,OAAgB,MAAM,4BAOpB;IAEF,6FAA6F;IAC7F,UAAmB,MAAM,yCAExB;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,gBAAgB,EAAE,aAAa,CAAC;KACjC;CACF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, css, html } from "lit";
|
|
8
|
+
import { customElement, property } from "lit/decorators.js";
|
|
9
|
+
import { tokens } from "./tokens.js";
|
|
10
|
+
/**
|
|
11
|
+
* Declarative metadata for one calendar event, consumed by a parent
|
|
12
|
+
* `calendar-month` or `calendar-year`. Read-only/non-interactive; renders
|
|
13
|
+
* nothing itself.
|
|
14
|
+
*
|
|
15
|
+
* @element calendar-entry
|
|
16
|
+
* @slot title - Plain-text title shown instead of the `label` fallback.
|
|
17
|
+
* @slot detail - Repeatable plain-text details rendered inside the shared body spanning all remaining days.
|
|
18
|
+
* @slot footer - Plain-text ending note pinned to the bottom of the shared body.
|
|
19
|
+
*/
|
|
20
|
+
let CalendarEntry = class CalendarEntry extends LitElement {
|
|
21
|
+
constructor() {
|
|
22
|
+
super(...arguments);
|
|
23
|
+
/** Inclusive start date, `"YYYY-MM-DD"`. */
|
|
24
|
+
this.start = "";
|
|
25
|
+
/** Inclusive end date, `"YYYY-MM-DD"`. Falls back to `start` when unset (single-day entry). */
|
|
26
|
+
this.end = "";
|
|
27
|
+
/** Fallback title used when no `title` slot is provided. */
|
|
28
|
+
this.label = "";
|
|
29
|
+
/** Color variant, reusing `status-pill`'s palette. */
|
|
30
|
+
this.color = "neutral";
|
|
31
|
+
}
|
|
32
|
+
static { this.styles = [
|
|
33
|
+
tokens,
|
|
34
|
+
css `
|
|
35
|
+
:host {
|
|
36
|
+
display: none;
|
|
37
|
+
}
|
|
38
|
+
`,
|
|
39
|
+
]; }
|
|
40
|
+
/** Keeps the metadata host hidden while exposing named text slots to the parent calendar. */
|
|
41
|
+
render() {
|
|
42
|
+
return html `<slot name="title"></slot><slot name="detail"></slot><slot name="footer"></slot>`;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
__decorate([
|
|
46
|
+
property({ reflect: true })
|
|
47
|
+
], CalendarEntry.prototype, "start", void 0);
|
|
48
|
+
__decorate([
|
|
49
|
+
property({ reflect: true })
|
|
50
|
+
], CalendarEntry.prototype, "end", void 0);
|
|
51
|
+
__decorate([
|
|
52
|
+
property({ reflect: true })
|
|
53
|
+
], CalendarEntry.prototype, "label", void 0);
|
|
54
|
+
__decorate([
|
|
55
|
+
property({ reflect: true })
|
|
56
|
+
], CalendarEntry.prototype, "color", void 0);
|
|
57
|
+
__decorate([
|
|
58
|
+
property({ reflect: true })
|
|
59
|
+
], CalendarEntry.prototype, "href", void 0);
|
|
60
|
+
CalendarEntry = __decorate([
|
|
61
|
+
customElement("calendar-entry")
|
|
62
|
+
], CalendarEntry);
|
|
63
|
+
export { CalendarEntry };
|
|
64
|
+
//# sourceMappingURL=calendar-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar-entry.js","sourceRoot":"","sources":["../src/calendar-entry.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE5D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;;GASG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,UAAU;IAAtC;;QACL,4CAA4C;QACf,UAAK,GAAG,EAAE,CAAC;QAExC,+FAA+F;QAClE,QAAG,GAAG,EAAE,CAAC;QAEtC,4DAA4D;QAC/B,UAAK,GAAG,EAAE,CAAC;QAExC,sDAAsD;QACzB,UAAK,GAAoB,SAAS,CAAC;IAkBlE,CAAC;aAbiB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;KAIF;KACF,AAPqB,CAOpB;IAEF,6FAA6F;IAC1E,MAAM;QACvB,OAAO,IAAI,CAAA,kFAAkF,CAAC;IAChG,CAAC;CACF,CAAA;AA3B8B;IAA5B,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;4CAAY;AAGX;IAA5B,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;0CAAU;AAGT;IAA5B,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;4CAAY;AAGX;IAA5B,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;4CAAoC;AAGnC;IAA5B,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;2CAAe;AAdhC,aAAa;IADzB,aAAa,CAAC,gBAAgB,CAAC;GACnB,aAAa,CA6BzB","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport type { StatusPillColor } from \"./status-pill.js\";\nimport { tokens } from \"./tokens.js\";\n\n/**\n * Declarative metadata for one calendar event, consumed by a parent\n * `calendar-month` or `calendar-year`. Read-only/non-interactive; renders\n * nothing itself.\n *\n * @element calendar-entry\n * @slot title - Plain-text title shown instead of the `label` fallback.\n * @slot detail - Repeatable plain-text details rendered inside the shared body spanning all remaining days.\n * @slot footer - Plain-text ending note pinned to the bottom of the shared body.\n */\n@customElement(\"calendar-entry\")\nexport class CalendarEntry extends LitElement {\n /** Inclusive start date, `\"YYYY-MM-DD\"`. */\n @property({ reflect: true }) start = \"\";\n\n /** Inclusive end date, `\"YYYY-MM-DD\"`. Falls back to `start` when unset (single-day entry). */\n @property({ reflect: true }) end = \"\";\n\n /** Fallback title used when no `title` slot is provided. */\n @property({ reflect: true }) label = \"\";\n\n /** Color variant, reusing `status-pill`'s palette. */\n @property({ reflect: true }) color: StatusPillColor = \"neutral\";\n\n /** Optional link target; the parent renders the entry as an `<a>` when set. */\n @property({ reflect: true }) href?: string;\n\n static override styles = [\n tokens,\n css`\n :host {\n display: none;\n }\n `,\n ];\n\n /** Keeps the metadata host hidden while exposing named text slots to the parent calendar. */\n protected override render() {\n return html`<slot name=\"title\"></slot><slot name=\"detail\"></slot><slot name=\"footer\"></slot>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"calendar-entry\": CalendarEntry;\n }\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
/**
|
|
3
|
+
* One month rendered as a top-to-bottom list of days — weekends and today
|
|
4
|
+
* highlighted, with declarative `calendar-entry` children shown as colored
|
|
5
|
+
* bars spanning the days they cover. An entry's title uses its first visible
|
|
6
|
+
* day; every remaining visible day becomes one shared body for wrapped
|
|
7
|
+
* details and an optional ending footer. Overlapping entries stack into
|
|
8
|
+
* side-by-side lanes rather than being layered/hidden. Read-only.
|
|
9
|
+
*
|
|
10
|
+
* Lanes are computed independently per instance, from only the entries
|
|
11
|
+
* overlapping this month — an entry spanning a month boundary may therefore
|
|
12
|
+
* land in a different lane index in the adjacent month's `calendar-month`.
|
|
13
|
+
* This is an accepted v1 limitation: cross-month lane continuity would
|
|
14
|
+
* require a shared parent (`calendar-year`) to assign lanes globally.
|
|
15
|
+
*
|
|
16
|
+
* Entry attributes and slotted title/detail text are observed, so a
|
|
17
|
+
* standalone month re-renders when consumers update declarative metadata.
|
|
18
|
+
*
|
|
19
|
+
* @element calendar-month
|
|
20
|
+
* @slot - Declarative `calendar-entry` elements to render for this month.
|
|
21
|
+
*/
|
|
22
|
+
export declare class CalendarMonth extends LitElement {
|
|
23
|
+
/** Calendar year, e.g. `2026`. */
|
|
24
|
+
year: number;
|
|
25
|
+
/** Calendar month, 1-12 (January = 1). */
|
|
26
|
+
month: number;
|
|
27
|
+
private readonly _entryElements;
|
|
28
|
+
/** Bumped when entry elements, attributes, or slotted text change to force a re-render. */
|
|
29
|
+
private _entriesVersion;
|
|
30
|
+
private _entriesObserver?;
|
|
31
|
+
static styles: import("lit").CSSResult[];
|
|
32
|
+
/** Observes declarative entry attributes and slotted text for live updates. */
|
|
33
|
+
connectedCallback(): void;
|
|
34
|
+
/** Releases the entry observer when this month disconnects. */
|
|
35
|
+
disconnectedCallback(): void;
|
|
36
|
+
/** Restores hover/focus classes from the live DOM after Lit updates or reuses cells. */
|
|
37
|
+
protected updated(): void;
|
|
38
|
+
/** Re-renders when declarative `calendar-entry` children are added or removed. */
|
|
39
|
+
private _handleSlotChange;
|
|
40
|
+
/** Finds the laned entry (if any) covering `date` in a given lane. */
|
|
41
|
+
private _entryFor;
|
|
42
|
+
/** Classifies one day of an entry so only the outer endpoints are rounded. */
|
|
43
|
+
private _segmentClass;
|
|
44
|
+
/** Current render key shared by an entry's title and body cells. */
|
|
45
|
+
private _entryKey;
|
|
46
|
+
/** Toggles one interaction class on every rendered cell belonging to an entry. */
|
|
47
|
+
private _setEntryInteraction;
|
|
48
|
+
/** Reconciles classes against the links actually hovered or focused after a render. */
|
|
49
|
+
private _syncEntryInteractions;
|
|
50
|
+
/** Renders a transparent full-cell link without wrapping the visible text. */
|
|
51
|
+
private _renderEntryLink;
|
|
52
|
+
/** Joins the event title, details, and visible footer for tooltips and accessible link names. */
|
|
53
|
+
private _entryBodyText;
|
|
54
|
+
/** Renders the shared details body, with the footer reserved at its bottom edge. */
|
|
55
|
+
private _renderEntryBody;
|
|
56
|
+
/** Renders a row-spanning body cell with whole-line detail and footer budgets. */
|
|
57
|
+
private _renderEntryBodyCell;
|
|
58
|
+
/** Renders a lane's title/body cell, skips rows covered by a body rowspan, or emits an empty spacer. */
|
|
59
|
+
private _renderLaneCell;
|
|
60
|
+
protected render(): import("lit-html").TemplateResult<1>;
|
|
61
|
+
}
|
|
62
|
+
declare global {
|
|
63
|
+
interface HTMLElementTagNameMap {
|
|
64
|
+
"calendar-month": CalendarMonth;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=calendar-month.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar-month.d.ts","sourceRoot":"","sources":["../src/calendar-month.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAoBrD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBACa,aAAc,SAAQ,UAAU;IAC3C,kCAAkC;IACS,IAAI,EAAE,MAAM,CAA4B;IAEnF,0CAA0C;IACC,KAAK,SAAK;IAGrD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAmB;IAElD,2FAA2F;IAClF,OAAO,CAAC,eAAe,CAAK;IAErC,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAE5C,OAAgB,MAAM,4BA+LpB;IAEF,+EAA+E;IACtE,iBAAiB,IAAI,IAAI,CAWjC;IAED,+DAA+D;IACtD,oBAAoB,IAAI,IAAI,CAGpC;IAED,wFAAwF;IACxF,UAAmB,OAAO,IAAI,IAAI,CAEjC;IAED,kFAAkF;IAClF,OAAO,CAAC,iBAAiB;IAIzB,sEAAsE;IACtE,OAAO,CAAC,SAAS;IAIjB,8EAA8E;IAC9E,OAAO,CAAC,aAAa;IASrB,oEAAoE;IACpE,OAAO,CAAC,SAAS;IAIjB,kFAAkF;IAClF,OAAO,CAAC,oBAAoB;IAQ5B,uFAAuF;IACvF,OAAO,CAAC,sBAAsB;IAa9B,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB;IAgBxB,iGAAiG;IACjG,OAAO,CAAC,cAAc;IAMtB,oFAAoF;IACpF,OAAO,CAAC,gBAAgB;IAiCxB,kFAAkF;IAClF,OAAO,CAAC,oBAAoB;IAsB5B,wGAAwG;IACxG,OAAO,CAAC,eAAe;IA8CvB,UAAmB,MAAM,yCAyCxB;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,gBAAgB,EAAE,aAAa,CAAC;KACjC;CACF"}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, css, html, nothing } from "lit";
|
|
8
|
+
import { customElement, property, queryAssignedElements, state } from "lit/decorators.js";
|
|
9
|
+
import { repeat } from "lit/directives/repeat.js";
|
|
10
|
+
import { tokens } from "./tokens.js";
|
|
11
|
+
import { CALENDAR_ENTRY_ATTRIBUTES, WEEKDAY_ABBR, assignLanes, daysInMonth, isWeekend, monthName, overlapsRange, readCalendarEntryElement, resolveEntry, toIsoDate, } from "./utils/calendar.js";
|
|
12
|
+
/**
|
|
13
|
+
* One month rendered as a top-to-bottom list of days — weekends and today
|
|
14
|
+
* highlighted, with declarative `calendar-entry` children shown as colored
|
|
15
|
+
* bars spanning the days they cover. An entry's title uses its first visible
|
|
16
|
+
* day; every remaining visible day becomes one shared body for wrapped
|
|
17
|
+
* details and an optional ending footer. Overlapping entries stack into
|
|
18
|
+
* side-by-side lanes rather than being layered/hidden. Read-only.
|
|
19
|
+
*
|
|
20
|
+
* Lanes are computed independently per instance, from only the entries
|
|
21
|
+
* overlapping this month — an entry spanning a month boundary may therefore
|
|
22
|
+
* land in a different lane index in the adjacent month's `calendar-month`.
|
|
23
|
+
* This is an accepted v1 limitation: cross-month lane continuity would
|
|
24
|
+
* require a shared parent (`calendar-year`) to assign lanes globally.
|
|
25
|
+
*
|
|
26
|
+
* Entry attributes and slotted title/detail text are observed, so a
|
|
27
|
+
* standalone month re-renders when consumers update declarative metadata.
|
|
28
|
+
*
|
|
29
|
+
* @element calendar-month
|
|
30
|
+
* @slot - Declarative `calendar-entry` elements to render for this month.
|
|
31
|
+
*/
|
|
32
|
+
let CalendarMonth = class CalendarMonth extends LitElement {
|
|
33
|
+
constructor() {
|
|
34
|
+
super(...arguments);
|
|
35
|
+
/** Calendar year, e.g. `2026`. */
|
|
36
|
+
this.year = new Date().getFullYear();
|
|
37
|
+
/** Calendar month, 1-12 (January = 1). */
|
|
38
|
+
this.month = 1;
|
|
39
|
+
/** Bumped when entry elements, attributes, or slotted text change to force a re-render. */
|
|
40
|
+
this._entriesVersion = 0;
|
|
41
|
+
}
|
|
42
|
+
static { this.styles = [
|
|
43
|
+
tokens,
|
|
44
|
+
css `
|
|
45
|
+
:host {
|
|
46
|
+
display: block;
|
|
47
|
+
font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);
|
|
48
|
+
font-size: var(--ui-font-size-sm, 0.75rem);
|
|
49
|
+
}
|
|
50
|
+
.month-name {
|
|
51
|
+
margin: 0 0 0.5rem;
|
|
52
|
+
color: var(--ui-text, #0f172a);
|
|
53
|
+
font-size: var(--ui-font-size, 0.875rem);
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
}
|
|
56
|
+
table {
|
|
57
|
+
width: 100%;
|
|
58
|
+
border-collapse: collapse;
|
|
59
|
+
table-layout: fixed;
|
|
60
|
+
}
|
|
61
|
+
.day-row td {
|
|
62
|
+
padding: 0.25rem 0.5rem;
|
|
63
|
+
border-bottom: 1px solid var(--ui-border, #e2e8f0);
|
|
64
|
+
}
|
|
65
|
+
.day-row.weekend {
|
|
66
|
+
background: var(--ui-surface-muted, #f8fafc);
|
|
67
|
+
}
|
|
68
|
+
.day-row.today {
|
|
69
|
+
background: color-mix(in srgb, var(--ui-primary, #4f46e5) 10%, transparent);
|
|
70
|
+
}
|
|
71
|
+
.day-number {
|
|
72
|
+
width: 1.5rem;
|
|
73
|
+
color: var(--ui-text-muted, #64748b);
|
|
74
|
+
font-variant-numeric: tabular-nums;
|
|
75
|
+
text-align: right;
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
}
|
|
78
|
+
.day-row.today .day-number {
|
|
79
|
+
color: var(--ui-primary, #4f46e5);
|
|
80
|
+
font-weight: 700;
|
|
81
|
+
}
|
|
82
|
+
.day-weekday {
|
|
83
|
+
width: 2.25rem;
|
|
84
|
+
color: var(--ui-text-muted, #64748b);
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
}
|
|
87
|
+
.day-row.today .day-weekday {
|
|
88
|
+
color: var(--ui-primary, #4f46e5);
|
|
89
|
+
font-weight: 600;
|
|
90
|
+
}
|
|
91
|
+
.day-row.weekend .day-weekday {
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
}
|
|
94
|
+
.lane-cell {
|
|
95
|
+
padding: 0.1rem 0.4rem;
|
|
96
|
+
}
|
|
97
|
+
.entry-bar {
|
|
98
|
+
border-radius: 0;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
text-overflow: ellipsis;
|
|
101
|
+
transition: box-shadow 120ms ease;
|
|
102
|
+
white-space: nowrap;
|
|
103
|
+
}
|
|
104
|
+
.entry-bar.entry-hovered,
|
|
105
|
+
.entry-bar.entry-focused {
|
|
106
|
+
box-shadow: inset 0 0 0 100vmax var(--ui-hover-overlay, rgb(255 255 255 / 0.32));
|
|
107
|
+
}
|
|
108
|
+
.entry-bar.segment-single {
|
|
109
|
+
border-radius: var(--ui-radius-sm, 0.25rem);
|
|
110
|
+
}
|
|
111
|
+
.entry-bar.segment-start {
|
|
112
|
+
border-radius: var(--ui-radius-sm, 0.25rem) var(--ui-radius-sm, 0.25rem) 0 0;
|
|
113
|
+
}
|
|
114
|
+
.entry-bar.segment-end {
|
|
115
|
+
border-radius: 0 0 var(--ui-radius-sm, 0.25rem) var(--ui-radius-sm, 0.25rem);
|
|
116
|
+
}
|
|
117
|
+
.entry-bar.segment-start,
|
|
118
|
+
.entry-bar.segment-middle {
|
|
119
|
+
border-bottom: 0;
|
|
120
|
+
}
|
|
121
|
+
.entry-bar.neutral {
|
|
122
|
+
background: color-mix(in srgb, var(--ui-text-muted, #64748b) 15%, var(--ui-surface, #fff));
|
|
123
|
+
color: var(--ui-text-muted, #64748b);
|
|
124
|
+
}
|
|
125
|
+
.entry-bar.info {
|
|
126
|
+
background: color-mix(in srgb, #0ea5e9 15%, var(--ui-surface, #fff));
|
|
127
|
+
color: #0ea5e9;
|
|
128
|
+
}
|
|
129
|
+
.entry-bar.primary {
|
|
130
|
+
background: color-mix(in srgb, var(--ui-primary, #4f46e5) 15%, var(--ui-surface, #fff));
|
|
131
|
+
color: var(--ui-primary, #4f46e5);
|
|
132
|
+
}
|
|
133
|
+
.entry-bar.success {
|
|
134
|
+
background: color-mix(in srgb, var(--ui-success, #16a34a) 15%, var(--ui-surface, #fff));
|
|
135
|
+
color: var(--ui-success, #16a34a);
|
|
136
|
+
}
|
|
137
|
+
.entry-bar.warning {
|
|
138
|
+
background: color-mix(in srgb, #d97706 15%, var(--ui-surface, #fff));
|
|
139
|
+
color: #d97706;
|
|
140
|
+
}
|
|
141
|
+
.entry-bar.danger {
|
|
142
|
+
background: color-mix(in srgb, var(--ui-danger, #dc2626) 15%, var(--ui-surface, #fff));
|
|
143
|
+
color: var(--ui-danger, #dc2626);
|
|
144
|
+
}
|
|
145
|
+
.entry-line {
|
|
146
|
+
display: block;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
}
|
|
149
|
+
.day-row td.entry-title-cell {
|
|
150
|
+
position: relative;
|
|
151
|
+
padding: 0;
|
|
152
|
+
}
|
|
153
|
+
.entry-title {
|
|
154
|
+
box-sizing: border-box;
|
|
155
|
+
font-weight: 500;
|
|
156
|
+
padding: 0.15rem 0.4rem;
|
|
157
|
+
pointer-events: none;
|
|
158
|
+
white-space: nowrap;
|
|
159
|
+
text-overflow: ellipsis;
|
|
160
|
+
}
|
|
161
|
+
.entry-details {
|
|
162
|
+
display: -webkit-box;
|
|
163
|
+
min-height: 0;
|
|
164
|
+
max-height: var(--entry-detail-max-height, 1.15em);
|
|
165
|
+
flex: 0 0 auto;
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
-webkit-box-orient: vertical;
|
|
168
|
+
-webkit-line-clamp: var(--entry-detail-lines, 1);
|
|
169
|
+
font-size: var(--ui-font-size-xs, 0.6875rem);
|
|
170
|
+
font-weight: 400;
|
|
171
|
+
line-height: 1.15;
|
|
172
|
+
overflow-wrap: anywhere;
|
|
173
|
+
text-overflow: ellipsis;
|
|
174
|
+
white-space: normal;
|
|
175
|
+
}
|
|
176
|
+
.entry-detail {
|
|
177
|
+
min-height: 1.15em;
|
|
178
|
+
}
|
|
179
|
+
.day-row td.entry-body-cell {
|
|
180
|
+
position: relative;
|
|
181
|
+
padding: 0;
|
|
182
|
+
vertical-align: top;
|
|
183
|
+
}
|
|
184
|
+
.entry-body {
|
|
185
|
+
position: absolute;
|
|
186
|
+
inset: 0;
|
|
187
|
+
box-sizing: border-box;
|
|
188
|
+
display: flex;
|
|
189
|
+
min-height: 0;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
overflow: hidden;
|
|
192
|
+
padding: 0.15rem 0.4rem;
|
|
193
|
+
color: inherit;
|
|
194
|
+
pointer-events: none;
|
|
195
|
+
}
|
|
196
|
+
.entry-footer {
|
|
197
|
+
flex: 0 0 auto;
|
|
198
|
+
min-height: 1.15em;
|
|
199
|
+
margin-top: auto;
|
|
200
|
+
border-top: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
201
|
+
padding-top: 0.15rem;
|
|
202
|
+
font-size: var(--ui-font-size-xs, 0.6875rem);
|
|
203
|
+
font-weight: 500;
|
|
204
|
+
line-height: 1.15;
|
|
205
|
+
overflow: hidden;
|
|
206
|
+
text-overflow: ellipsis;
|
|
207
|
+
white-space: nowrap;
|
|
208
|
+
}
|
|
209
|
+
.entry-link {
|
|
210
|
+
position: absolute;
|
|
211
|
+
z-index: 1;
|
|
212
|
+
inset: 0;
|
|
213
|
+
border-radius: inherit;
|
|
214
|
+
color: inherit;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
text-decoration: none;
|
|
217
|
+
}
|
|
218
|
+
.entry-link:focus-visible {
|
|
219
|
+
outline: 2px solid var(--ui-primary, #4f46e5);
|
|
220
|
+
outline-offset: -2px;
|
|
221
|
+
box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35)) inset;
|
|
222
|
+
}
|
|
223
|
+
@media (forced-colors: active) {
|
|
224
|
+
.entry-link:focus-visible {
|
|
225
|
+
outline-color: Highlight;
|
|
226
|
+
box-shadow: none;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
slot {
|
|
230
|
+
display: none;
|
|
231
|
+
}
|
|
232
|
+
`,
|
|
233
|
+
]; }
|
|
234
|
+
/** Observes declarative entry attributes and slotted text for live updates. */
|
|
235
|
+
connectedCallback() {
|
|
236
|
+
super.connectedCallback();
|
|
237
|
+
this._entriesVersion++;
|
|
238
|
+
this._entriesObserver ??= new MutationObserver(() => this._entriesVersion++);
|
|
239
|
+
this._entriesObserver.observe(this, {
|
|
240
|
+
attributes: true,
|
|
241
|
+
attributeFilter: CALENDAR_ENTRY_ATTRIBUTES,
|
|
242
|
+
characterData: true,
|
|
243
|
+
childList: true,
|
|
244
|
+
subtree: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/** Releases the entry observer when this month disconnects. */
|
|
248
|
+
disconnectedCallback() {
|
|
249
|
+
this._entriesObserver?.disconnect();
|
|
250
|
+
super.disconnectedCallback();
|
|
251
|
+
}
|
|
252
|
+
/** Restores hover/focus classes from the live DOM after Lit updates or reuses cells. */
|
|
253
|
+
updated() {
|
|
254
|
+
this._syncEntryInteractions();
|
|
255
|
+
}
|
|
256
|
+
/** Re-renders when declarative `calendar-entry` children are added or removed. */
|
|
257
|
+
_handleSlotChange() {
|
|
258
|
+
this._entriesVersion++;
|
|
259
|
+
}
|
|
260
|
+
/** Finds the laned entry (if any) covering `date` in a given lane. */
|
|
261
|
+
_entryFor(laned, lane, date) {
|
|
262
|
+
return laned.find((entry) => entry.lane === lane && entry.startDate <= date && date <= entry.endDate);
|
|
263
|
+
}
|
|
264
|
+
/** Classifies one day of an entry so only the outer endpoints are rounded. */
|
|
265
|
+
_segmentClass(entry, date) {
|
|
266
|
+
const startsHere = date.getTime() === entry.startDate.getTime();
|
|
267
|
+
const endsHere = date.getTime() === entry.endDate.getTime();
|
|
268
|
+
if (startsHere && endsHere)
|
|
269
|
+
return "segment-single";
|
|
270
|
+
if (startsHere)
|
|
271
|
+
return "segment-start";
|
|
272
|
+
if (endsHere)
|
|
273
|
+
return "segment-end";
|
|
274
|
+
return "segment-middle";
|
|
275
|
+
}
|
|
276
|
+
/** Current render key shared by an entry's title and body cells. */
|
|
277
|
+
_entryKey(entry) {
|
|
278
|
+
return `${entry.lane}|${entry.start}|${entry.end}|${entry.label}|${entry.href ?? ""}`;
|
|
279
|
+
}
|
|
280
|
+
/** Toggles one interaction class on every rendered cell belonging to an entry. */
|
|
281
|
+
_setEntryInteraction(entryKey, className, active) {
|
|
282
|
+
for (const cell of this.renderRoot.querySelectorAll(".entry-bar")) {
|
|
283
|
+
if (cell.dataset.entryKey === entryKey) {
|
|
284
|
+
cell.classList.toggle(className, active);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** Reconciles classes against the links actually hovered or focused after a render. */
|
|
289
|
+
_syncEntryInteractions() {
|
|
290
|
+
for (const cell of this.renderRoot.querySelectorAll(".entry-bar")) {
|
|
291
|
+
cell.classList.remove("entry-hovered", "entry-focused");
|
|
292
|
+
}
|
|
293
|
+
for (const link of this.renderRoot.querySelectorAll(".entry-link:hover")) {
|
|
294
|
+
this._setEntryInteraction(link.dataset.entryKey ?? "", "entry-hovered", true);
|
|
295
|
+
}
|
|
296
|
+
const activeElement = this.shadowRoot?.activeElement;
|
|
297
|
+
if (activeElement instanceof HTMLElement && activeElement.matches(".entry-link")) {
|
|
298
|
+
this._setEntryInteraction(activeElement.dataset.entryKey ?? "", "entry-focused", true);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** Renders a transparent full-cell link without wrapping the visible text. */
|
|
302
|
+
_renderEntryLink(entry, accessibleText) {
|
|
303
|
+
const entryKey = this._entryKey(entry);
|
|
304
|
+
return entry.href
|
|
305
|
+
? html `<a
|
|
306
|
+
class="entry-link"
|
|
307
|
+
href=${entry.href}
|
|
308
|
+
aria-label=${accessibleText}
|
|
309
|
+
data-entry-key=${entryKey}
|
|
310
|
+
@pointerenter=${() => this._setEntryInteraction(entryKey, "entry-hovered", true)}
|
|
311
|
+
@pointerleave=${() => this._setEntryInteraction(entryKey, "entry-hovered", false)}
|
|
312
|
+
@focus=${() => this._setEntryInteraction(entryKey, "entry-focused", true)}
|
|
313
|
+
@blur=${() => this._setEntryInteraction(entryKey, "entry-focused", false)}
|
|
314
|
+
></a>`
|
|
315
|
+
: nothing;
|
|
316
|
+
}
|
|
317
|
+
/** Joins the event title, details, and visible footer for tooltips and accessible link names. */
|
|
318
|
+
_entryBodyText(entry, showFooter) {
|
|
319
|
+
return [entry.label, ...(entry.details ?? []), showFooter ? entry.footer : undefined]
|
|
320
|
+
.filter((line) => Boolean(line))
|
|
321
|
+
.join("\n");
|
|
322
|
+
}
|
|
323
|
+
/** Renders the shared details body, with the footer reserved at its bottom edge. */
|
|
324
|
+
_renderEntryBody(entry, showFooter, detailLineClamp, showTitleFallback = false) {
|
|
325
|
+
const sourceDetails = entry.details ?? [];
|
|
326
|
+
const footer = showFooter ? entry.footer : undefined;
|
|
327
|
+
const details = showTitleFallback && sourceDetails.length === 0 && !footer
|
|
328
|
+
? [entry.label]
|
|
329
|
+
: sourceDetails;
|
|
330
|
+
const bodyText = this._entryBodyText(entry, showFooter);
|
|
331
|
+
return html `
|
|
332
|
+
${this._renderEntryLink(entry, bodyText)}
|
|
333
|
+
<div class="entry-body" aria-hidden=${entry.href ? "true" : nothing}>
|
|
334
|
+
${detailLineClamp > 0 && details.length > 0
|
|
335
|
+
? html `
|
|
336
|
+
<div
|
|
337
|
+
class="entry-details"
|
|
338
|
+
style=${`--entry-detail-lines: ${detailLineClamp}; --entry-detail-max-height: ${detailLineClamp * 1.15}em`}
|
|
339
|
+
>
|
|
340
|
+
${details.map((detail) => html `<div class="entry-detail">${detail}</div>`)}
|
|
341
|
+
</div>
|
|
342
|
+
`
|
|
343
|
+
: nothing}
|
|
344
|
+
${footer ? html `<div class="entry-footer" title=${footer}>${footer}</div>` : nothing}
|
|
345
|
+
</div>
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
/** Renders a row-spanning body cell with whole-line detail and footer budgets. */
|
|
349
|
+
_renderEntryBodyCell(entry, bodyRows, reachesEventEnd, showTitleFallback = false) {
|
|
350
|
+
const segmentClass = reachesEventEnd ? "segment-end" : "segment-middle";
|
|
351
|
+
const footerLineBudget = reachesEventEnd && entry.footer ? 1 : 0;
|
|
352
|
+
const detailLineClamp = Math.max(0, bodyRows - footerLineBudget);
|
|
353
|
+
const bodyText = this._entryBodyText(entry, reachesEventEnd);
|
|
354
|
+
return html `
|
|
355
|
+
<td
|
|
356
|
+
class="lane-cell entry-bar entry-body-cell ${entry.color} ${segmentClass}"
|
|
357
|
+
data-entry-key=${this._entryKey(entry)}
|
|
358
|
+
rowspan=${bodyRows}
|
|
359
|
+
title=${entry.href ? nothing : bodyText}
|
|
360
|
+
>
|
|
361
|
+
${this._renderEntryBody(entry, reachesEventEnd, detailLineClamp, showTitleFallback)}
|
|
362
|
+
</td>
|
|
363
|
+
`;
|
|
364
|
+
}
|
|
365
|
+
/** Renders a lane's title/body cell, skips rows covered by a body rowspan, or emits an empty spacer. */
|
|
366
|
+
_renderLaneCell(laned, lane, date, monthStart, monthEnd) {
|
|
367
|
+
const entry = this._entryFor(laned, lane, date);
|
|
368
|
+
if (!entry) {
|
|
369
|
+
return html `<td class="lane-cell empty"></td>`;
|
|
370
|
+
}
|
|
371
|
+
const firstVisibleDay = entry.startDate > monthStart ? entry.startDate : monthStart;
|
|
372
|
+
const lastVisibleDay = entry.endDate < monthEnd ? entry.endDate : monthEnd;
|
|
373
|
+
const dayOffset = date.getDate() - firstVisibleDay.getDate();
|
|
374
|
+
const visibleDayCount = lastVisibleDay.getDate() - firstVisibleDay.getDate() + 1;
|
|
375
|
+
const isMultiDayEntry = entry.startDate.getTime() !== entry.endDate.getTime();
|
|
376
|
+
const isEndingOnlyClip = visibleDayCount === 1 &&
|
|
377
|
+
isMultiDayEntry &&
|
|
378
|
+
date.getTime() === entry.endDate.getTime();
|
|
379
|
+
if (isEndingOnlyClip) {
|
|
380
|
+
return this._renderEntryBodyCell(entry, 1, true, true);
|
|
381
|
+
}
|
|
382
|
+
if (dayOffset > 1)
|
|
383
|
+
return nothing;
|
|
384
|
+
if (dayOffset === 1) {
|
|
385
|
+
const bodyRows = visibleDayCount - 1;
|
|
386
|
+
const reachesEventEnd = lastVisibleDay.getTime() === entry.endDate.getTime();
|
|
387
|
+
return this._renderEntryBodyCell(entry, bodyRows, reachesEventEnd);
|
|
388
|
+
}
|
|
389
|
+
return html `
|
|
390
|
+
<td
|
|
391
|
+
class="lane-cell entry-bar entry-title-cell ${entry.color} ${this._segmentClass(entry, date)}"
|
|
392
|
+
data-entry-key=${this._entryKey(entry)}
|
|
393
|
+
title=${entry.href ? nothing : entry.label}
|
|
394
|
+
>
|
|
395
|
+
${this._renderEntryLink(entry, entry.label)}
|
|
396
|
+
<span class="entry-line entry-title" aria-hidden=${entry.href ? "true" : nothing}>
|
|
397
|
+
${entry.label}
|
|
398
|
+
</span>
|
|
399
|
+
</td>
|
|
400
|
+
`;
|
|
401
|
+
}
|
|
402
|
+
render() {
|
|
403
|
+
const totalDays = daysInMonth(this.year, this.month);
|
|
404
|
+
const monthStart = new Date(this.year, this.month - 1, 1);
|
|
405
|
+
const monthEnd = new Date(this.year, this.month - 1, totalDays);
|
|
406
|
+
const resolved = this._entryElements
|
|
407
|
+
.map(readCalendarEntryElement)
|
|
408
|
+
.map(resolveEntry)
|
|
409
|
+
.filter((entry) => entry !== null)
|
|
410
|
+
.filter((entry) => overlapsRange(entry, monthStart, monthEnd));
|
|
411
|
+
const { entries: laned, laneCount } = assignLanes(resolved);
|
|
412
|
+
const todayIso = toIsoDate(new Date());
|
|
413
|
+
const lanes = Array.from({ length: Math.max(laneCount, 1) }, (_, i) => i);
|
|
414
|
+
return html `
|
|
415
|
+
<div class="month">
|
|
416
|
+
<h4 class="month-name">${monthName(this.month)} ${this.year}</h4>
|
|
417
|
+
<table>
|
|
418
|
+
<tbody>
|
|
419
|
+
${repeat(Array.from({ length: totalDays }, (_, i) => i + 1), (d) => d, (d) => {
|
|
420
|
+
const date = new Date(this.year, this.month - 1, d);
|
|
421
|
+
const dow = date.getDay();
|
|
422
|
+
const weekendCls = isWeekend(date) ? "weekend" : "";
|
|
423
|
+
const todayCls = toIsoDate(date) === todayIso ? "today" : "";
|
|
424
|
+
return html `
|
|
425
|
+
<tr class="day-row ${weekendCls} ${todayCls}">
|
|
426
|
+
<td class="day-number">${d}</td>
|
|
427
|
+
<td class="day-weekday">${WEEKDAY_ABBR[dow]}</td>
|
|
428
|
+
${lanes.map((lane) => this._renderLaneCell(laned, lane, date, monthStart, monthEnd))}
|
|
429
|
+
</tr>
|
|
430
|
+
`;
|
|
431
|
+
})}
|
|
432
|
+
</tbody>
|
|
433
|
+
</table>
|
|
434
|
+
<slot @slotchange=${this._handleSlotChange}></slot>
|
|
435
|
+
</div>
|
|
436
|
+
`;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
__decorate([
|
|
440
|
+
property({ type: Number, reflect: true })
|
|
441
|
+
], CalendarMonth.prototype, "year", void 0);
|
|
442
|
+
__decorate([
|
|
443
|
+
property({ type: Number, reflect: true })
|
|
444
|
+
], CalendarMonth.prototype, "month", void 0);
|
|
445
|
+
__decorate([
|
|
446
|
+
queryAssignedElements({ selector: "calendar-entry" })
|
|
447
|
+
], CalendarMonth.prototype, "_entryElements", void 0);
|
|
448
|
+
__decorate([
|
|
449
|
+
state()
|
|
450
|
+
], CalendarMonth.prototype, "_entriesVersion", void 0);
|
|
451
|
+
CalendarMonth = __decorate([
|
|
452
|
+
customElement("calendar-month")
|
|
453
|
+
], CalendarMonth);
|
|
454
|
+
export { CalendarMonth };
|
|
455
|
+
//# sourceMappingURL=calendar-month.js.map
|