@f-ewald/components 0.6.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 +101 -44
- package/custom-elements.json +2570 -877
- 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/dropdown-button.d.ts +37 -0
- package/dist/dropdown-button.d.ts.map +1 -0
- package/dist/dropdown-button.js +219 -0
- package/dist/dropdown-button.js.map +1 -0
- package/dist/form-select.d.ts +27 -2
- package/dist/form-select.d.ts.map +1 -1
- package/dist/form-select.js +440 -27
- package/dist/form-select.js.map +1 -1
- package/dist/frame-box.d.ts +22 -0
- package/dist/frame-box.d.ts.map +1 -0
- package/dist/frame-box.js +68 -0
- package/dist/frame-box.js.map +1 -0
- package/dist/icon-button.d.ts +27 -0
- package/dist/icon-button.d.ts.map +1 -0
- package/dist/icon-button.js +83 -0
- package/dist/icon-button.js.map +1 -0
- package/dist/icons.d.ts +1 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +1 -0
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -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/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +82 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/popover-panel.d.ts +3 -0
- package/dist/popover-panel.d.ts.map +1 -1
- package/dist/popover-panel.js +11 -0
- package/dist/popover-panel.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/dropdown-button.md +61 -0
- package/docs/form-select.md +21 -3
- package/docs/frame-box.md +43 -0
- package/docs/icon-button.md +52 -0
- package/docs/map-circle.md +4 -0
- package/docs/mcp-evaluation.md +18 -11
- package/docs/popover-panel.md +5 -1
- package/llms.txt +194 -28
- package/package.json +19 -4
- 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
package/dist/map-circle.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-circle.d.ts","sourceRoot":"","sources":["../src/map-circle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAO5C
|
|
1
|
+
{"version":3,"file":"map-circle.d.ts","sourceRoot":"","sources":["../src/map-circle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAO5C;;;;;;;;;;;;;GAaG;AACH,qBACa,SAAU,SAAQ,UAAU;IACvC,OAAgB,MAAM,4BA8BpB;IAEF,4FAA4F;IAChF,KAAK,SAAa;IAC9B,+BAA+B;IACH,IAAI,SAAM;IACtC,wFAAwF;IACnC,SAAS,SAAK;IACnE,sFAAsF;IAC1C,WAAW,UAAS;IAEhE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4C;IAE3D,MAAM,yCAuBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,YAAY,EAAE,SAAS,CAAC;KACzB;CACF"}
|
package/dist/map-circle.js
CHANGED
|
@@ -15,6 +15,9 @@ let gradientIdCounter = 0;
|
|
|
15
15
|
* need to visually "point" at their exact coordinate. Purely a visual
|
|
16
16
|
* primitive — it has no `mapbox-gl` (or any mapping library) dependency;
|
|
17
17
|
* the consumer positions it, e.g. via `new mapboxgl.Marker({ element: el })`.
|
|
18
|
+
* It can also replace the former `<map-point>` dense-layer primitive: use
|
|
19
|
+
* `size="14" ring-width="3"`, leave the slot empty, and rasterize one marker
|
|
20
|
+
* per color for use as a map `icon-image`.
|
|
18
21
|
*
|
|
19
22
|
* @element map-circle
|
|
20
23
|
* @slot - Optional badge content shown centered on the circle — a rank
|
package/dist/map-circle.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-circle.js","sourceRoot":"","sources":["../src/map-circle.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;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,IAAI,iBAAiB,GAAG,CAAC,CAAC;AAE1B
|
|
1
|
+
{"version":3,"file":"map-circle.js","sourceRoot":"","sources":["../src/map-circle.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;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,IAAI,iBAAiB,GAAG,CAAC,CAAC;AAE1B;;;;;;;;;;;;;GAaG;AAEI,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,UAAU;IAAlC;;QAiCL,4FAA4F;QAChF,UAAK,GAAG,SAAS,CAAC;QAC9B,+BAA+B;QACH,SAAI,GAAG,EAAE,CAAC;QACtC,wFAAwF;QACnC,cAAS,GAAG,CAAC,CAAC;QACnE,sFAAsF;QAC1C,gBAAW,GAAG,KAAK,CAAC;QAE/C,YAAO,GAAG,mBAAmB,iBAAiB,EAAE,EAAE,CAAC;IA0BtE,CAAC;aAnEiB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;KA2BF;KACF,AA9BqB,CA8BpB;IAaO,MAAM;QACb,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC;QAC/C,OAAO,IAAI,CAAA;;gBAEC,IAAI,CAAC,IAAI;iBACR,IAAI,CAAC,IAAI;;;;;;+BAMK,IAAI,CAAC,OAAO;2CACA,KAAK;6CACH,IAAI;;;oDAGG,IAAI,CAAC,OAAO,oCAAoC,IAAI,CAAC,SAAS;;8CAEpE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;;;KAGpE,CAAC;IACJ,CAAC;CACF,CAAA;AAlCa;IAAX,QAAQ,EAAE;wCAAmB;AAEF;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;uCAAW;AAEe;IAApD,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;4CAAe;AAEvB;IAA3C,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;8CAAqB;AAxCrD,SAAS;IADrB,aAAa,CAAC,YAAY,CAAC;GACf,SAAS,CAoErB","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { tokens } from \"./tokens.js\";\nimport { mixHex } from \"./utils/color.js\";\n\nlet gradientIdCounter = 0;\n\n/**\n * A plain circular map marker: a light-to-dark gradient fill with a white\n * outer ring, no point/tail (unlike `<map-pin>`) — for markers that don't\n * need to visually \"point\" at their exact coordinate. Purely a visual\n * primitive — it has no `mapbox-gl` (or any mapping library) dependency;\n * the consumer positions it, e.g. via `new mapboxgl.Marker({ element: el })`.\n * It can also replace the former `<map-point>` dense-layer primitive: use\n * `size=\"14\" ring-width=\"3\"`, leave the slot empty, and rasterize one marker\n * per color for use as a map `icon-image`.\n *\n * @element map-circle\n * @slot - Optional badge content shown centered on the circle — a rank\n * number, an emoji, a small icon.\n */\n@customElement(\"map-circle\")\nexport class MapCircle extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-block;\n position: relative;\n line-height: 0;\n }\n svg {\n display: block;\n transition: transform 120ms ease, filter 120ms ease;\n }\n :host([highlighted]) svg {\n transform: scale(1.12);\n filter: brightness(1.08) drop-shadow(0 0 5px rgb(0 0 0 / 0.35));\n }\n .content {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n color: #fff;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n font-weight: 700;\n line-height: 1;\n text-shadow: 0 1px 2px rgb(0 0 0 / 0.35);\n pointer-events: none;\n white-space: nowrap;\n }\n `,\n ];\n\n /** Fill color; the gradient's light (top) and dark (bottom) stops are derived from this. */\n @property() color = \"#4f46e5\";\n /** Diameter, in CSS pixels. */\n @property({ type: Number }) size = 18;\n /** White outer ring thickness, in the same viewBox units as `size` (scales with it). */\n @property({ type: Number, attribute: \"ring-width\" }) ringWidth = 4;\n /** Scales and glows the circle — a generic emphasis state (e.g. hover, selection). */\n @property({ type: Boolean, reflect: true }) highlighted = false;\n\n private readonly _gradId = `map-circle-grad-${gradientIdCounter++}`;\n\n override render() {\n const light = mixHex(this.color, \"#ffffff\", 30);\n const dark = mixHex(this.color, \"#000000\", 30);\n return html`\n <svg\n width=${this.size}\n height=${this.size}\n viewBox=\"0 0 32 32\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <defs>\n <linearGradient id=${this._gradId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=${light} />\n <stop offset=\"100%\" stop-color=${dark} />\n </linearGradient>\n </defs>\n <circle cx=\"16\" cy=\"16\" r=\"13\" fill=\"url(#${this._gradId})\" stroke=\"#ffffff\" stroke-width=${this.ringWidth} />\n </svg>\n <div class=\"content\" style=\"font-size:${Math.round(this.size * 0.4)}px\">\n <slot></slot>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"map-circle\": MapCircle;\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stdio MCP server exposing the `@f-ewald/components` catalog to AI coding
|
|
4
|
+
* assistants (Claude Code, etc.) working in a consuming project.
|
|
5
|
+
*
|
|
6
|
+
* Two tools, backed by the same `custom-elements.json` and `docs/*.md` this
|
|
7
|
+
* package already generates and ships via `npm run docs`:
|
|
8
|
+
* - `list_components`: every tag name + one-line description.
|
|
9
|
+
* - `get_component_docs`: the full generated Markdown doc for one tag.
|
|
10
|
+
*
|
|
11
|
+
* No new data source — this is just a different transport over data that
|
|
12
|
+
* already exists. See `docs/mcp-evaluation.md` for the design rationale.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
// This file compiles to dist/mcp-server.js; custom-elements.json and docs/
|
|
22
|
+
// ship alongside dist/ at the package root, whether run from the repo
|
|
23
|
+
// checkout or from an installed node_modules/@f-ewald/components.
|
|
24
|
+
const packageRoot = path.join(__dirname, "..");
|
|
25
|
+
const docsDir = path.join(packageRoot, "docs");
|
|
26
|
+
/** Reads the package's own version out of package.json, for server metadata. */
|
|
27
|
+
async function readPackageVersion() {
|
|
28
|
+
const text = await readFile(path.join(packageRoot, "package.json"), "utf8");
|
|
29
|
+
return JSON.parse(text).version;
|
|
30
|
+
}
|
|
31
|
+
/** Extracts {tag, one-line summary} for every custom element, sorted by tag. */
|
|
32
|
+
async function listComponents() {
|
|
33
|
+
const text = await readFile(path.join(packageRoot, "custom-elements.json"), "utf8");
|
|
34
|
+
const manifest = JSON.parse(text);
|
|
35
|
+
const components = [];
|
|
36
|
+
for (const mod of manifest.modules) {
|
|
37
|
+
for (const decl of mod.declarations ?? []) {
|
|
38
|
+
if (!decl.customElement)
|
|
39
|
+
continue;
|
|
40
|
+
const summary = (decl.description ?? "").split("\n\n")[0].replace(/\n/g, " ").trim();
|
|
41
|
+
components.push({ tag: decl.tagName, summary });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
components.sort((a, b) => a.tag.localeCompare(b.tag));
|
|
45
|
+
return components;
|
|
46
|
+
}
|
|
47
|
+
const server = new McpServer({
|
|
48
|
+
name: "f-ewald-components",
|
|
49
|
+
version: await readPackageVersion(),
|
|
50
|
+
});
|
|
51
|
+
server.registerTool("list_components", {
|
|
52
|
+
title: "List components",
|
|
53
|
+
description: "Lists every web component tag in @f-ewald/components with a one-line " +
|
|
54
|
+
"description. Call get_component_docs(tag) for the full usage example, " +
|
|
55
|
+
"attributes/properties, events, slots, and CSS custom properties of a " +
|
|
56
|
+
"specific tag.",
|
|
57
|
+
}, async () => ({
|
|
58
|
+
content: [{ type: "text", text: JSON.stringify(await listComponents(), null, 2) }],
|
|
59
|
+
}));
|
|
60
|
+
server.registerTool("get_component_docs", {
|
|
61
|
+
title: "Get component docs",
|
|
62
|
+
description: "Returns the full generated Markdown documentation (install snippet, " +
|
|
63
|
+
"usage example, attributes/properties, events, slots, CSS custom " +
|
|
64
|
+
"properties) for one @f-ewald/components tag. Use list_components " +
|
|
65
|
+
"first to find valid tags.",
|
|
66
|
+
inputSchema: { tag: z.string().describe('Component tag name, e.g. "ui-button".') },
|
|
67
|
+
}, async ({ tag }) => {
|
|
68
|
+
try {
|
|
69
|
+
const text = await readFile(path.join(docsDir, `${tag}.md`), "utf8");
|
|
70
|
+
return { content: [{ type: "text", text }] };
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
const validTags = (await listComponents()).map((c) => c.tag).join(", ");
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text", text: `No docs found for tag "${tag}". Valid tags: ${validTags}` }],
|
|
76
|
+
isError: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const transport = new StdioServerTransport();
|
|
81
|
+
await server.connect(transport);
|
|
82
|
+
//# sourceMappingURL=mcp-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-server.js","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * Stdio MCP server exposing the `@f-ewald/components` catalog to AI coding\n * assistants (Claude Code, etc.) working in a consuming project.\n *\n * Two tools, backed by the same `custom-elements.json` and `docs/*.md` this\n * package already generates and ships via `npm run docs`:\n * - `list_components`: every tag name + one-line description.\n * - `get_component_docs`: the full generated Markdown doc for one tag.\n *\n * No new data source — this is just a different transport over data that\n * already exists. See `docs/mcp-evaluation.md` for the design rationale.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport path from \"node:path\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n// This file compiles to dist/mcp-server.js; custom-elements.json and docs/\n// ship alongside dist/ at the package root, whether run from the repo\n// checkout or from an installed node_modules/@f-ewald/components.\nconst packageRoot = path.join(__dirname, \"..\");\nconst docsDir = path.join(packageRoot, \"docs\");\n\n/** One row of the `list_components` result. */\ninterface ComponentSummary {\n tag: string;\n summary: string;\n}\n\n/** Reads the package's own version out of package.json, for server metadata. */\nasync function readPackageVersion(): Promise<string> {\n const text = await readFile(path.join(packageRoot, \"package.json\"), \"utf8\");\n return (JSON.parse(text) as { version: string }).version;\n}\n\n/** Extracts {tag, one-line summary} for every custom element, sorted by tag. */\nasync function listComponents(): Promise<ComponentSummary[]> {\n const text = await readFile(path.join(packageRoot, \"custom-elements.json\"), \"utf8\");\n const manifest = JSON.parse(text) as {\n modules: { declarations?: { customElement?: boolean; tagName: string; description?: string }[] }[];\n };\n const components: ComponentSummary[] = [];\n for (const mod of manifest.modules) {\n for (const decl of mod.declarations ?? []) {\n if (!decl.customElement) continue;\n const summary = (decl.description ?? \"\").split(\"\\n\\n\")[0]!.replace(/\\n/g, \" \").trim();\n components.push({ tag: decl.tagName, summary });\n }\n }\n components.sort((a, b) => a.tag.localeCompare(b.tag));\n return components;\n}\n\nconst server = new McpServer({\n name: \"f-ewald-components\",\n version: await readPackageVersion(),\n});\n\nserver.registerTool(\n \"list_components\",\n {\n title: \"List components\",\n description:\n \"Lists every web component tag in @f-ewald/components with a one-line \" +\n \"description. Call get_component_docs(tag) for the full usage example, \" +\n \"attributes/properties, events, slots, and CSS custom properties of a \" +\n \"specific tag.\",\n },\n async () => ({\n content: [{ type: \"text\", text: JSON.stringify(await listComponents(), null, 2) }],\n }),\n);\n\nserver.registerTool(\n \"get_component_docs\",\n {\n title: \"Get component docs\",\n description:\n \"Returns the full generated Markdown documentation (install snippet, \" +\n \"usage example, attributes/properties, events, slots, CSS custom \" +\n \"properties) for one @f-ewald/components tag. Use list_components \" +\n \"first to find valid tags.\",\n inputSchema: { tag: z.string().describe('Component tag name, e.g. \"ui-button\".') },\n },\n async ({ tag }) => {\n try {\n const text = await readFile(path.join(docsDir, `${tag}.md`), \"utf8\");\n return { content: [{ type: \"text\", text }] };\n } catch {\n const validTags = (await listComponents()).map((c) => c.tag).join(\", \");\n return {\n content: [{ type: \"text\", text: `No docs found for tag \"${tag}\". Valid tags: ${validTags}` }],\n isError: true,\n };\n }\n },\n);\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n"]}
|
package/dist/popover-panel.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ import { LitElement, type PropertyValues } from "lit";
|
|
|
12
12
|
*
|
|
13
13
|
* @element popover-panel
|
|
14
14
|
* @fires panel-close - User clicked the close (✕) button, pressed Escape, or clicked outside/the backdrop.
|
|
15
|
+
* @slot title - Overrides the plain `heading` text with custom markup.
|
|
16
|
+
* @slot actions - Extra header controls (e.g. an icon+label link) rendered between the title and the close button.
|
|
17
|
+
* @slot - Popover body content.
|
|
15
18
|
*/
|
|
16
19
|
export declare class PopoverPanel extends LitElement {
|
|
17
20
|
#private;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"popover-panel.d.ts","sourceRoot":"","sources":["../src/popover-panel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAKjE
|
|
1
|
+
{"version":3,"file":"popover-panel.d.ts","sourceRoot":"","sources":["../src/popover-panel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAKjE;;;;;;;;;;;;;;;;GAgBG;AACH,qBACa,YAAa,SAAQ,UAAU;;IAC1C,gDAAgD;IACJ,IAAI,UAAS;IACzD,8CAA8C;IAClC,OAAO,SAAM;IACzB,uFAAuF;IAC3C,QAAQ,UAAS;IAE7D,OAAgB,MAAM,4BA8EpB;IAEO,oBAAoB,IAAI,IAAI,CAIpC;IAED,UAAmB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CASxD;IAqBQ,MAAM,yCAed;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,eAAe,EAAE,YAAY,CAAC;KAC/B;CACF"}
|
package/dist/popover-panel.js
CHANGED
|
@@ -21,6 +21,9 @@ import { tokens } from "./tokens.js";
|
|
|
21
21
|
*
|
|
22
22
|
* @element popover-panel
|
|
23
23
|
* @fires panel-close - User clicked the close (✕) button, pressed Escape, or clicked outside/the backdrop.
|
|
24
|
+
* @slot title - Overrides the plain `heading` text with custom markup.
|
|
25
|
+
* @slot actions - Extra header controls (e.g. an icon+label link) rendered between the title and the close button.
|
|
26
|
+
* @slot - Popover body content.
|
|
24
27
|
*/
|
|
25
28
|
let PopoverPanel = class PopoverPanel extends LitElement {
|
|
26
29
|
constructor() {
|
|
@@ -102,6 +105,13 @@ let PopoverPanel = class PopoverPanel extends LitElement {
|
|
|
102
105
|
text-overflow: ellipsis;
|
|
103
106
|
padding-right: 0.5rem;
|
|
104
107
|
}
|
|
108
|
+
.panel-actions {
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
gap: 0.5rem;
|
|
112
|
+
flex: 0 0 auto;
|
|
113
|
+
margin-right: 0.25rem;
|
|
114
|
+
}
|
|
105
115
|
.close-btn {
|
|
106
116
|
background: none;
|
|
107
117
|
border: none;
|
|
@@ -148,6 +158,7 @@ let PopoverPanel = class PopoverPanel extends LitElement {
|
|
|
148
158
|
<slot name="title">
|
|
149
159
|
${this.heading ? html `<span class="panel-title">${this.heading}</span>` : null}
|
|
150
160
|
</slot>
|
|
161
|
+
<span class="panel-actions"><slot name="actions"></slot></span>
|
|
151
162
|
<button class="close-btn" aria-label="Close popover" @click=${() => this.#close()}>
|
|
152
163
|
${iconX(18)}
|
|
153
164
|
</button>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"popover-panel.js","sourceRoot":"","sources":["../src/popover-panel.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAuB,MAAM,KAAK,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC
|
|
1
|
+
{"version":3,"file":"popover-panel.js","sourceRoot":"","sources":["../src/popover-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, type PropertyValues } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { iconX } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\n/**\n * Generic anchored popover shell: a floating card positioned relative to its\n * nearest `position: relative` ancestor (place it next to its trigger button\n * inside such a wrapper), as opposed to `slide-panel`'s fixed screen-edge\n * drawer. Closes on outside click or Escape. Header chrome and close button\n * match `slide-panel`'s API (`heading`, `panel-close`) so either can be\n * swapped in for the other with no consumer-side changes beyond the wrapper.\n *\n * Set `centered` to render as a screen-centered modal with a translucent\n * backdrop instead of the default anchored placement.\n *\n * @element popover-panel\n * @fires panel-close - User clicked the close (✕) button, pressed Escape, or clicked outside/the backdrop.\n * @slot title - Overrides the plain `heading` text with custom markup.\n * @slot actions - Extra header controls (e.g. an icon+label link) rendered between the title and the close button.\n * @slot - Popover body content.\n */\n@customElement(\"popover-panel\")\nexport class PopoverPanel extends LitElement {\n /** Whether the popover is currently visible. */\n @property({ type: Boolean, reflect: true }) open = false;\n /** Title text shown in the popover header. */\n @property() heading = \"\";\n /** Render as a screen-centered modal with a backdrop instead of anchored placement. */\n @property({ type: Boolean, reflect: true }) centered = false;\n\n static override styles = [\n tokens,\n css`\n :host {\n display: none;\n position: absolute;\n top: calc(100% + 0.5rem);\n right: 0;\n z-index: 20;\n }\n :host([open]) {\n display: block;\n }\n :host([centered][open]) {\n position: fixed;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgb(0 0 0 / 0.35);\n z-index: 30;\n }\n :host([centered]) .panel {\n width: 400px;\n }\n .panel {\n width: 320px;\n max-width: calc(100vw - 2rem);\n background: var(--ui-surface, #fff);\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: var(--ui-radius, 0.5rem);\n box-shadow: var(--ui-shadow-lg, 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1));\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n .panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 0.625rem;\n border-bottom: 1px solid var(--ui-border, #e2e8f0);\n flex: 0 0 auto;\n }\n .panel-title {\n font-weight: 600;\n font-size: var(--ui-font-size, 0.875rem);\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n color: var(--ui-text, #0f172a);\n flex: 1 1 auto;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n padding-right: 0.5rem;\n }\n .panel-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex: 0 0 auto;\n margin-right: 0.25rem;\n }\n .close-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0.25rem 0.5rem;\n color: var(--ui-text-muted, #64748b);\n border-radius: var(--ui-radius-sm, 0.25rem);\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n }\n .close-btn:hover {\n background: var(--ui-surface-muted, #f8fafc);\n }\n `,\n ];\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n window.removeEventListener(\"mousedown\", this.#onWindowMousedown, true);\n window.removeEventListener(\"keydown\", this.#onWindowKeydown);\n }\n\n protected override updated(changed: PropertyValues): void {\n if (!changed.has(\"open\")) return;\n if (this.open) {\n window.addEventListener(\"mousedown\", this.#onWindowMousedown, true);\n window.addEventListener(\"keydown\", this.#onWindowKeydown);\n } else {\n window.removeEventListener(\"mousedown\", this.#onWindowMousedown, true);\n window.removeEventListener(\"keydown\", this.#onWindowKeydown);\n }\n }\n\n #onWindowMousedown = (e: MouseEvent): void => {\n // In centered mode the host itself renders the backdrop, so a mousedown\n // landing directly on the host (not on .panel or its content) is a\n // backdrop click and must close like an outside click would.\n if (this.centered && e.target === this) {\n this.#close();\n return;\n }\n if (!e.composedPath().includes(this)) this.#close();\n };\n\n #onWindowKeydown = (e: KeyboardEvent): void => {\n if (e.key === \"Escape\") this.#close();\n };\n\n #close(): void {\n this.dispatchEvent(new CustomEvent(\"panel-close\", { bubbles: true, composed: true }));\n }\n\n override render() {\n return html`\n <div class=\"panel\">\n <div class=\"panel-header\">\n <slot name=\"title\">\n ${this.heading ? html`<span class=\"panel-title\">${this.heading}</span>` : null}\n </slot>\n <span class=\"panel-actions\"><slot name=\"actions\"></slot></span>\n <button class=\"close-btn\" aria-label=\"Close popover\" @click=${() => this.#close()}>\n ${iconX(18)}\n </button>\n </div>\n <slot></slot>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"popover-panel\": PopoverPanel;\n }\n}\n"]}
|
package/dist/tokens.css
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
--ui-border: #e2e8f0;
|
|
11
11
|
--ui-surface: #ffffff;
|
|
12
12
|
--ui-surface-muted: #f8fafc;
|
|
13
|
+
--ui-hover-overlay: rgb(255 255 255 / 0.32);
|
|
13
14
|
--ui-overlay: rgb(15 23 42 / 0.45);
|
|
14
15
|
--ui-radius: 0.5rem;
|
|
15
16
|
--ui-radius-sm: 0.25rem;
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
--ui-font: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
20
21
|
--ui-font-size: 0.875rem;
|
|
21
22
|
--ui-font-size-sm: 0.75rem;
|
|
23
|
+
--ui-font-size-xs: 0.6875rem;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
@media (prefers-color-scheme: dark) {
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
--ui-border: #334155;
|
|
35
37
|
--ui-surface: #0f172a;
|
|
36
38
|
--ui-surface-muted: #1e293b;
|
|
39
|
+
--ui-hover-overlay: rgb(255 255 255 / 0.12);
|
|
37
40
|
--ui-overlay: rgb(2 6 23 / 0.6);
|
|
38
41
|
--ui-focus-ring: 0 0 0 3px rgb(99 102 241 / 0.45);
|
|
39
42
|
}
|
|
@@ -51,6 +54,7 @@
|
|
|
51
54
|
--ui-border: #334155;
|
|
52
55
|
--ui-surface: #0f172a;
|
|
53
56
|
--ui-surface-muted: #1e293b;
|
|
57
|
+
--ui-hover-overlay: rgb(255 255 255 / 0.12);
|
|
54
58
|
--ui-overlay: rgb(2 6 23 / 0.6);
|
|
55
59
|
--ui-focus-ring: 0 0 0 3px rgb(99 102 241 / 0.45);
|
|
56
60
|
}
|
package/dist/tokens.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA8B9C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAclD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,MAAM,yBAAQ,CAAC"}
|
package/dist/tokens.js
CHANGED
|
@@ -19,6 +19,7 @@ export const tokenValues = {
|
|
|
19
19
|
"--ui-border": "#e2e8f0", // slate-200
|
|
20
20
|
"--ui-surface": "#ffffff", // white
|
|
21
21
|
"--ui-surface-muted": "#f8fafc", // slate-50
|
|
22
|
+
"--ui-hover-overlay": "rgb(255 255 255 / 0.32)",
|
|
22
23
|
"--ui-overlay": "rgb(15 23 42 / 0.45)", // slate-900 / 45%
|
|
23
24
|
// Shape / depth
|
|
24
25
|
"--ui-radius": "0.5rem", // rounded-lg
|
|
@@ -30,6 +31,7 @@ export const tokenValues = {
|
|
|
30
31
|
"--ui-font": 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
|
31
32
|
"--ui-font-size": "0.875rem",
|
|
32
33
|
"--ui-font-size-sm": "0.75rem",
|
|
34
|
+
"--ui-font-size-xs": "0.6875rem",
|
|
33
35
|
};
|
|
34
36
|
/**
|
|
35
37
|
* Dark-mode overrides for the same `--ui-*` custom properties, applied by
|
|
@@ -50,6 +52,7 @@ export const darkTokenValues = {
|
|
|
50
52
|
"--ui-border": "#334155", // slate-700
|
|
51
53
|
"--ui-surface": "#0f172a", // slate-900
|
|
52
54
|
"--ui-surface-muted": "#1e293b", // slate-800
|
|
55
|
+
"--ui-hover-overlay": "rgb(255 255 255 / 0.12)",
|
|
53
56
|
"--ui-overlay": "rgb(2 6 23 / 0.6)", // slate-950 / 60%
|
|
54
57
|
"--ui-focus-ring": "0 0 0 3px rgb(99 102 241 / 0.45)",
|
|
55
58
|
};
|
package/dist/tokens.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAA2B;IACjD,SAAS;IACT,cAAc,EAAE,SAAS,EAAE,aAAa;IACxC,oBAAoB,EAAE,SAAS,EAAE,aAAa;IAC9C,aAAa,EAAE,SAAS,EAAE,UAAU;IACpC,mBAAmB,EAAE,SAAS,EAAE,UAAU;IAC1C,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,WAAW,EAAE,SAAS,EAAE,YAAY;IACpC,iBAAiB,EAAE,SAAS,EAAE,YAAY;IAC1C,aAAa,EAAE,SAAS,EAAE,YAAY;IACtC,cAAc,EAAE,SAAS,EAAE,QAAQ;IACnC,oBAAoB,EAAE,SAAS,EAAE,WAAW;IAC5C,cAAc,EAAE,sBAAsB,EAAE,kBAAkB;IAE1D,gBAAgB;IAChB,aAAa,EAAE,QAAQ,EAAE,aAAa;IACtC,gBAAgB,EAAE,SAAS,EAAE,UAAU;IACvC,aAAa,EACX,kEAAkE,EAAE,YAAY;IAClF,gBAAgB,EACd,qEAAqE,EAAE,YAAY;IACrF,iBAAiB,EAAE,iCAAiC;IAEpD,OAAO;IACP,WAAW,EACT,oHAAoH;IACtH,gBAAgB,EAAE,UAAU;IAC5B,mBAAmB,EAAE,SAAS;
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAA2B;IACjD,SAAS;IACT,cAAc,EAAE,SAAS,EAAE,aAAa;IACxC,oBAAoB,EAAE,SAAS,EAAE,aAAa;IAC9C,aAAa,EAAE,SAAS,EAAE,UAAU;IACpC,mBAAmB,EAAE,SAAS,EAAE,UAAU;IAC1C,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,WAAW,EAAE,SAAS,EAAE,YAAY;IACpC,iBAAiB,EAAE,SAAS,EAAE,YAAY;IAC1C,aAAa,EAAE,SAAS,EAAE,YAAY;IACtC,cAAc,EAAE,SAAS,EAAE,QAAQ;IACnC,oBAAoB,EAAE,SAAS,EAAE,WAAW;IAC5C,oBAAoB,EAAE,yBAAyB;IAC/C,cAAc,EAAE,sBAAsB,EAAE,kBAAkB;IAE1D,gBAAgB;IAChB,aAAa,EAAE,QAAQ,EAAE,aAAa;IACtC,gBAAgB,EAAE,SAAS,EAAE,UAAU;IACvC,aAAa,EACX,kEAAkE,EAAE,YAAY;IAClF,gBAAgB,EACd,qEAAqE,EAAE,YAAY;IACrF,iBAAiB,EAAE,iCAAiC;IAEpD,OAAO;IACP,WAAW,EACT,oHAAoH;IACtH,gBAAgB,EAAE,UAAU;IAC5B,mBAAmB,EAAE,SAAS;IAC9B,mBAAmB,EAAE,WAAW;CACjC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAA2B;IACrD,cAAc,EAAE,SAAS,EAAE,aAAa;IACxC,oBAAoB,EAAE,SAAS,EAAE,aAAa;IAC9C,aAAa,EAAE,SAAS,EAAE,UAAU;IACpC,mBAAmB,EAAE,SAAS,EAAE,UAAU;IAC1C,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,WAAW,EAAE,SAAS,EAAE,YAAY;IACpC,iBAAiB,EAAE,SAAS,EAAE,YAAY;IAC1C,aAAa,EAAE,SAAS,EAAE,YAAY;IACtC,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,oBAAoB,EAAE,SAAS,EAAE,YAAY;IAC7C,oBAAoB,EAAE,yBAAyB;IAC/C,cAAc,EAAE,mBAAmB,EAAE,kBAAkB;IACvD,iBAAiB,EAAE,kCAAkC;CACtD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAA,EAAE,CAAC","sourcesContent":["import { css } from \"lit\";\n\n/**\n * Design token values, sourced from Tailwind v4's default theme palette.\n * Each entry maps a `--ui-*` custom property name to its default value.\n * These are the same values baked in as `var(--ui-*, <fallback>)` fallbacks\n * throughout component styles, so components render correctly with zero\n * external CSS. Consumers may override any of these by setting the custom\n * property on `:root` or an ancestor element (see `tokens.css`).\n */\nexport const tokenValues: Record<string, string> = {\n // Colors\n \"--ui-primary\": \"#4f46e5\", // indigo-600\n \"--ui-primary-hover\": \"#4338ca\", // indigo-700\n \"--ui-danger\": \"#dc2626\", // red-600\n \"--ui-danger-hover\": \"#b91c1c\", // red-700\n \"--ui-success\": \"#16a34a\", // green-600\n \"--ui-text\": \"#0f172a\", // slate-900\n \"--ui-text-muted\": \"#64748b\", // slate-500\n \"--ui-border\": \"#e2e8f0\", // slate-200\n \"--ui-surface\": \"#ffffff\", // white\n \"--ui-surface-muted\": \"#f8fafc\", // slate-50\n \"--ui-hover-overlay\": \"rgb(255 255 255 / 0.32)\",\n \"--ui-overlay\": \"rgb(15 23 42 / 0.45)\", // slate-900 / 45%\n\n // Shape / depth\n \"--ui-radius\": \"0.5rem\", // rounded-lg\n \"--ui-radius-sm\": \"0.25rem\", // rounded\n \"--ui-shadow\":\n \"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)\", // shadow-md\n \"--ui-shadow-lg\":\n \"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)\", // shadow-xl\n \"--ui-focus-ring\": \"0 0 0 3px rgb(79 70 229 / 0.35)\",\n\n // Type\n \"--ui-font\":\n 'ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"',\n \"--ui-font-size\": \"0.875rem\",\n \"--ui-font-size-sm\": \"0.75rem\",\n \"--ui-font-size-xs\": \"0.6875rem\",\n};\n\n/**\n * Dark-mode overrides for the same `--ui-*` custom properties, applied by\n * `tokens.css` via `@media (prefers-color-scheme: dark)` and `[data-theme]`\n * (see generate-tokens-css.mjs). Same Tailwind slate scale as the light\n * palette, inverted, with primary/danger/success lightened one step (500\n * instead of 600) for sufficient contrast against dark surfaces. Shape/type\n * tokens are theme-independent and not repeated here.\n */\nexport const darkTokenValues: Record<string, string> = {\n \"--ui-primary\": \"#6366f1\", // indigo-500\n \"--ui-primary-hover\": \"#818cf8\", // indigo-400\n \"--ui-danger\": \"#ef4444\", // red-500\n \"--ui-danger-hover\": \"#f87171\", // red-400\n \"--ui-success\": \"#22c55e\", // green-500\n \"--ui-text\": \"#f1f5f9\", // slate-100\n \"--ui-text-muted\": \"#94a3b8\", // slate-400\n \"--ui-border\": \"#334155\", // slate-700\n \"--ui-surface\": \"#0f172a\", // slate-900\n \"--ui-surface-muted\": \"#1e293b\", // slate-800\n \"--ui-hover-overlay\": \"rgb(255 255 255 / 0.12)\",\n \"--ui-overlay\": \"rgb(2 6 23 / 0.6)\", // slate-950 / 60%\n \"--ui-focus-ring\": \"0 0 0 3px rgb(99 102 241 / 0.45)\",\n};\n\n/**\n * Historically a shared `:host { --ui-x: var(--ui-x, fallback); } ` block,\n * re-declaring every token on each component's own host element as a\n * \"materialize the inherited value, or this fallback\" trick. That pattern\n * turned out to be a genuine bug, not a working default: browsers treat\n * `--ui-x: var(--ui-x, ...)` on `:host` as a self-referencing declaration\n * that computes to the guaranteed-invalid value REGARDLESS of what an\n * ancestor (e.g. `:root`) set — the host's own `:host` rule wins the cascade\n * over the ancestor's, so this dropped whatever the app actually set and\n * silently substituted the component's own fallback everywhere. It went\n * unnoticed because every component's fallback happened to equal the\n * intended light-mode value — dark mode (a real, different value at\n * `:root`) exposed it immediately (colors rendering as if no theme were\n * applied at all). Fix: don't redeclare tokens on `:host` — plain\n * inheritance already carries `:root`'s values into every shadow root\n * correctly, and every component already has its own `var(--ui-x, fallback)`\n * at each point of use, so nothing here is needed for the \"zero external\n * CSS\" default to keep working. Kept as an empty export so the existing\n * `static override styles = [tokens, css\\`...\\`]` import in every component\n * keeps compiling.\n */\nexport const tokens = css``;\n"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { StatusPillColor } from "../status-pill.js";
|
|
2
|
+
import type { CalendarEntry } from "../calendar-entry.js";
|
|
3
|
+
/** Plain data extracted from a `calendar-entry` element's properties. */
|
|
4
|
+
export interface CalendarEntryData {
|
|
5
|
+
start: string;
|
|
6
|
+
end: string;
|
|
7
|
+
label: string;
|
|
8
|
+
details?: string[];
|
|
9
|
+
footer?: string;
|
|
10
|
+
color: StatusPillColor;
|
|
11
|
+
href?: string;
|
|
12
|
+
}
|
|
13
|
+
/** A `CalendarEntryData` with its date strings resolved to local `Date`s. */
|
|
14
|
+
export interface ResolvedCalendarEntry extends CalendarEntryData {
|
|
15
|
+
startDate: Date;
|
|
16
|
+
endDate: Date;
|
|
17
|
+
}
|
|
18
|
+
/** A `ResolvedCalendarEntry` assigned a stacking column by `assignLanes`. */
|
|
19
|
+
export interface LanedEntry extends ResolvedCalendarEntry {
|
|
20
|
+
lane: number;
|
|
21
|
+
}
|
|
22
|
+
/** Attributes whose mutations can change a rendered calendar entry. */
|
|
23
|
+
export declare const CALENDAR_ENTRY_ATTRIBUTES: string[];
|
|
24
|
+
/** Number of days in `month` (1-12) of `year`, leap years included. */
|
|
25
|
+
export declare function daysInMonth(year: number, month: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* Parses `"YYYY-MM-DD"` as a local-midnight `Date`. Returns `null` for
|
|
28
|
+
* anything that doesn't match or doesn't round-trip (e.g. "2026-02-30") —
|
|
29
|
+
* deliberately never uses `new Date(iso)`/`.toISOString()`, both of which
|
|
30
|
+
* are UTC-based and would silently shift the date in negative-UTC-offset
|
|
31
|
+
* timezones.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseIsoDate(iso: string): Date | null;
|
|
34
|
+
/** Formats a `Date`'s local year/month/day as `"YYYY-MM-DD"`. */
|
|
35
|
+
export declare function toIsoDate(date: Date): string;
|
|
36
|
+
/** Locale-aware full month name for `month` (1-12). */
|
|
37
|
+
export declare function monthName(month: number): string;
|
|
38
|
+
/** Sunday-first weekday abbreviations, indexed by `Date.getDay()` (0 = Sunday). */
|
|
39
|
+
export declare const WEEKDAY_ABBR: readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* Whether `date` falls on a Saturday or Sunday, per JS's `Date.getDay()`
|
|
42
|
+
* convention (Sunday = 0 .. Saturday = 6) — note this differs from Python's
|
|
43
|
+
* `calendar` module (Monday = 0), which the original reference
|
|
44
|
+
* implementation this component's layout is based on used.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isWeekend(date: Date): boolean;
|
|
47
|
+
/** Reads a `calendar-entry` element's properties and named text slots into plain data. */
|
|
48
|
+
export declare function readCalendarEntryElement(el: CalendarEntry): CalendarEntryData;
|
|
49
|
+
/**
|
|
50
|
+
* Resolves a `CalendarEntryData`'s date strings to `Date`s. Returns `null`
|
|
51
|
+
* when `start` is missing or unparseable — such entries are dropped
|
|
52
|
+
* silently rather than thrown, since this reads declarative markup a
|
|
53
|
+
* consumer authored, not validated user input. A blank `end` defaults to
|
|
54
|
+
* `start` (single-day entry); an `end` before `start` is clamped up to
|
|
55
|
+
* `start` rather than treated as an error.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveEntry(data: CalendarEntryData): ResolvedCalendarEntry | null;
|
|
58
|
+
/** Whether `entry`'s inclusive date range intersects the inclusive `[rangeStart, rangeEnd]` range. */
|
|
59
|
+
export declare function overlapsRange(entry: ResolvedCalendarEntry, rangeStart: Date, rangeEnd: Date): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Assigns each entry a 0-based stacking lane via greedy interval
|
|
62
|
+
* partitioning ("meeting rooms II"): entries are sorted by start date, then
|
|
63
|
+
* each is placed in the lowest-numbered lane whose current occupant ends
|
|
64
|
+
* strictly before the entry starts. Because entry ranges are
|
|
65
|
+
* inclusive-inclusive, an entry ending on day X and one starting on day X
|
|
66
|
+
* both occupy day X and must not share a lane — hence the strict `<`.
|
|
67
|
+
*
|
|
68
|
+
* Lane assignment is meant to be computed independently per `calendar-month`
|
|
69
|
+
* (from only the entries overlapping that month); an entry spanning a month
|
|
70
|
+
* boundary may therefore land in a different lane in adjacent months. This
|
|
71
|
+
* is an accepted v1 limitation, not a bug.
|
|
72
|
+
*/
|
|
73
|
+
export declare function assignLanes(entries: ResolvedCalendarEntry[]): {
|
|
74
|
+
entries: LanedEntry[];
|
|
75
|
+
laneCount: number;
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=calendar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../src/utils/calendar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,eAAe,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,UAAW,SAAQ,qBAAqB;IACvD,IAAI,EAAE,MAAM,CAAC;CACd;AAID,uEAAuE;AACvE,eAAO,MAAM,yBAAyB,UAAqD,CAAC;AAE5F,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAYrD;AAED,iEAAiE;AACjE,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAK5C;AAID,uDAAuD;AACvD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,mFAAmF;AACnF,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAsD,CAAC;AAEjG;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAG7C;AAQD,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,aAAa,GAAG,iBAAiB,CAY7E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,iBAAiB,GAAG,qBAAqB,GAAG,IAAI,CAWlF;AAED,sGAAsG;AACtG,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAG,OAAO,CAErG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,qBAAqB,EAAE,GAAG;IAC7D,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,CAyBA"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
2
|
+
/** Attributes whose mutations can change a rendered calendar entry. */
|
|
3
|
+
export const CALENDAR_ENTRY_ATTRIBUTES = ["start", "end", "label", "color", "href", "slot"];
|
|
4
|
+
/** Number of days in `month` (1-12) of `year`, leap years included. */
|
|
5
|
+
export function daysInMonth(year, month) {
|
|
6
|
+
return new Date(year, month, 0).getDate();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parses `"YYYY-MM-DD"` as a local-midnight `Date`. Returns `null` for
|
|
10
|
+
* anything that doesn't match or doesn't round-trip (e.g. "2026-02-30") —
|
|
11
|
+
* deliberately never uses `new Date(iso)`/`.toISOString()`, both of which
|
|
12
|
+
* are UTC-based and would silently shift the date in negative-UTC-offset
|
|
13
|
+
* timezones.
|
|
14
|
+
*/
|
|
15
|
+
export function parseIsoDate(iso) {
|
|
16
|
+
const match = ISO_DATE_PATTERN.exec(iso);
|
|
17
|
+
if (!match)
|
|
18
|
+
return null;
|
|
19
|
+
const year = Number(match[1]);
|
|
20
|
+
const month = Number(match[2]);
|
|
21
|
+
const day = Number(match[3]);
|
|
22
|
+
const date = new Date(year, month - 1, day);
|
|
23
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return date;
|
|
27
|
+
}
|
|
28
|
+
/** Formats a `Date`'s local year/month/day as `"YYYY-MM-DD"`. */
|
|
29
|
+
export function toIsoDate(date) {
|
|
30
|
+
const year = String(date.getFullYear()).padStart(4, "0");
|
|
31
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
32
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
33
|
+
return `${year}-${month}-${day}`;
|
|
34
|
+
}
|
|
35
|
+
const monthNameFormatter = new Intl.DateTimeFormat(undefined, { month: "long" });
|
|
36
|
+
/** Locale-aware full month name for `month` (1-12). */
|
|
37
|
+
export function monthName(month) {
|
|
38
|
+
return monthNameFormatter.format(new Date(2000, month - 1, 1));
|
|
39
|
+
}
|
|
40
|
+
/** Sunday-first weekday abbreviations, indexed by `Date.getDay()` (0 = Sunday). */
|
|
41
|
+
export const WEEKDAY_ABBR = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
42
|
+
/**
|
|
43
|
+
* Whether `date` falls on a Saturday or Sunday, per JS's `Date.getDay()`
|
|
44
|
+
* convention (Sunday = 0 .. Saturday = 6) — note this differs from Python's
|
|
45
|
+
* `calendar` module (Monday = 0), which the original reference
|
|
46
|
+
* implementation this component's layout is based on used.
|
|
47
|
+
*/
|
|
48
|
+
export function isWeekend(date) {
|
|
49
|
+
const day = date.getDay();
|
|
50
|
+
return day === 0 || day === 6;
|
|
51
|
+
}
|
|
52
|
+
/** Reads normalized plain-text content from direct children assigned to a named slot. */
|
|
53
|
+
function slottedText(el, slotName) {
|
|
54
|
+
return Array.from(el.querySelectorAll(`:scope > [slot="${slotName}"]`))
|
|
55
|
+
.map((node) => (node.textContent ?? "").replace(/\s+/g, " ").trim());
|
|
56
|
+
}
|
|
57
|
+
/** Reads a `calendar-entry` element's properties and named text slots into plain data. */
|
|
58
|
+
export function readCalendarEntryElement(el) {
|
|
59
|
+
const title = slottedText(el, "title").find((line) => line.length > 0);
|
|
60
|
+
const footer = slottedText(el, "footer").find((line) => line.length > 0);
|
|
61
|
+
return {
|
|
62
|
+
start: el.start,
|
|
63
|
+
end: el.end,
|
|
64
|
+
label: title ?? el.label,
|
|
65
|
+
details: slottedText(el, "detail"),
|
|
66
|
+
footer,
|
|
67
|
+
color: el.color,
|
|
68
|
+
href: el.href,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolves a `CalendarEntryData`'s date strings to `Date`s. Returns `null`
|
|
73
|
+
* when `start` is missing or unparseable — such entries are dropped
|
|
74
|
+
* silently rather than thrown, since this reads declarative markup a
|
|
75
|
+
* consumer authored, not validated user input. A blank `end` defaults to
|
|
76
|
+
* `start` (single-day entry); an `end` before `start` is clamped up to
|
|
77
|
+
* `start` rather than treated as an error.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveEntry(data) {
|
|
80
|
+
const startDate = parseIsoDate(data.start);
|
|
81
|
+
if (!startDate)
|
|
82
|
+
return null;
|
|
83
|
+
const endDate = parseIsoDate(data.end) ?? startDate;
|
|
84
|
+
return {
|
|
85
|
+
...data,
|
|
86
|
+
details: data.details ?? [],
|
|
87
|
+
startDate,
|
|
88
|
+
endDate: endDate < startDate ? startDate : endDate,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Whether `entry`'s inclusive date range intersects the inclusive `[rangeStart, rangeEnd]` range. */
|
|
92
|
+
export function overlapsRange(entry, rangeStart, rangeEnd) {
|
|
93
|
+
return entry.startDate <= rangeEnd && entry.endDate >= rangeStart;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Assigns each entry a 0-based stacking lane via greedy interval
|
|
97
|
+
* partitioning ("meeting rooms II"): entries are sorted by start date, then
|
|
98
|
+
* each is placed in the lowest-numbered lane whose current occupant ends
|
|
99
|
+
* strictly before the entry starts. Because entry ranges are
|
|
100
|
+
* inclusive-inclusive, an entry ending on day X and one starting on day X
|
|
101
|
+
* both occupy day X and must not share a lane — hence the strict `<`.
|
|
102
|
+
*
|
|
103
|
+
* Lane assignment is meant to be computed independently per `calendar-month`
|
|
104
|
+
* (from only the entries overlapping that month); an entry spanning a month
|
|
105
|
+
* boundary may therefore land in a different lane in adjacent months. This
|
|
106
|
+
* is an accepted v1 limitation, not a bug.
|
|
107
|
+
*/
|
|
108
|
+
export function assignLanes(entries) {
|
|
109
|
+
const sorted = entries
|
|
110
|
+
.map((entry, index) => ({ entry, index }))
|
|
111
|
+
.sort((a, b) => {
|
|
112
|
+
const startDiff = a.entry.startDate.getTime() - b.entry.startDate.getTime();
|
|
113
|
+
if (startDiff !== 0)
|
|
114
|
+
return startDiff;
|
|
115
|
+
const endDiff = a.entry.endDate.getTime() - b.entry.endDate.getTime();
|
|
116
|
+
if (endDiff !== 0)
|
|
117
|
+
return endDiff;
|
|
118
|
+
return a.index - b.index;
|
|
119
|
+
});
|
|
120
|
+
const laneEnds = [];
|
|
121
|
+
const laned = [];
|
|
122
|
+
for (const { entry } of sorted) {
|
|
123
|
+
let lane = laneEnds.findIndex((end) => end < entry.startDate);
|
|
124
|
+
if (lane === -1) {
|
|
125
|
+
lane = laneEnds.length;
|
|
126
|
+
laneEnds.push(entry.endDate);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
laneEnds[lane] = entry.endDate;
|
|
130
|
+
}
|
|
131
|
+
laned.push({ ...entry, lane });
|
|
132
|
+
}
|
|
133
|
+
return { entries: laned, laneCount: laneEnds.length };
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=calendar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar.js","sourceRoot":"","sources":["../../src/utils/calendar.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StatusPillColor } from \"../status-pill.js\";\nimport type { CalendarEntry } from \"../calendar-entry.js\";\n\n/** Plain data extracted from a `calendar-entry` element's properties. */\nexport interface CalendarEntryData {\n start: string;\n end: string;\n label: string;\n details?: string[];\n footer?: string;\n color: StatusPillColor;\n href?: string;\n}\n\n/** A `CalendarEntryData` with its date strings resolved to local `Date`s. */\nexport interface ResolvedCalendarEntry extends CalendarEntryData {\n startDate: Date;\n endDate: Date;\n}\n\n/** A `ResolvedCalendarEntry` assigned a stacking column by `assignLanes`. */\nexport interface LanedEntry extends ResolvedCalendarEntry {\n lane: number;\n}\n\nconst ISO_DATE_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/** Attributes whose mutations can change a rendered calendar entry. */\nexport const CALENDAR_ENTRY_ATTRIBUTES = [\"start\", \"end\", \"label\", \"color\", \"href\", \"slot\"];\n\n/** Number of days in `month` (1-12) of `year`, leap years included. */\nexport function daysInMonth(year: number, month: number): number {\n return new Date(year, month, 0).getDate();\n}\n\n/**\n * Parses `\"YYYY-MM-DD\"` as a local-midnight `Date`. Returns `null` for\n * anything that doesn't match or doesn't round-trip (e.g. \"2026-02-30\") —\n * deliberately never uses `new Date(iso)`/`.toISOString()`, both of which\n * are UTC-based and would silently shift the date in negative-UTC-offset\n * timezones.\n */\nexport function parseIsoDate(iso: string): Date | null {\n const match = ISO_DATE_PATTERN.exec(iso);\n if (!match) return null;\n\n const year = Number(match[1]);\n const month = Number(match[2]);\n const day = Number(match[3]);\n const date = new Date(year, month - 1, day);\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\n/** Formats a `Date`'s local year/month/day as `\"YYYY-MM-DD\"`. */\nexport function toIsoDate(date: Date): string {\n const year = String(date.getFullYear()).padStart(4, \"0\");\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\nconst monthNameFormatter = new Intl.DateTimeFormat(undefined, { month: \"long\" });\n\n/** Locale-aware full month name for `month` (1-12). */\nexport function monthName(month: number): string {\n return monthNameFormatter.format(new Date(2000, month - 1, 1));\n}\n\n/** Sunday-first weekday abbreviations, indexed by `Date.getDay()` (0 = Sunday). */\nexport const WEEKDAY_ABBR: readonly string[] = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\n\n/**\n * Whether `date` falls on a Saturday or Sunday, per JS's `Date.getDay()`\n * convention (Sunday = 0 .. Saturday = 6) — note this differs from Python's\n * `calendar` module (Monday = 0), which the original reference\n * implementation this component's layout is based on used.\n */\nexport function isWeekend(date: Date): boolean {\n const day = date.getDay();\n return day === 0 || day === 6;\n}\n\n/** Reads normalized plain-text content from direct children assigned to a named slot. */\nfunction slottedText(el: CalendarEntry, slotName: string): string[] {\n return Array.from(el.querySelectorAll<HTMLElement>(`:scope > [slot=\"${slotName}\"]`))\n .map((node) => (node.textContent ?? \"\").replace(/\\s+/g, \" \").trim());\n}\n\n/** Reads a `calendar-entry` element's properties and named text slots into plain data. */\nexport function readCalendarEntryElement(el: CalendarEntry): CalendarEntryData {\n const title = slottedText(el, \"title\").find((line) => line.length > 0);\n const footer = slottedText(el, \"footer\").find((line) => line.length > 0);\n return {\n start: el.start,\n end: el.end,\n label: title ?? el.label,\n details: slottedText(el, \"detail\"),\n footer,\n color: el.color,\n href: el.href,\n };\n}\n\n/**\n * Resolves a `CalendarEntryData`'s date strings to `Date`s. Returns `null`\n * when `start` is missing or unparseable — such entries are dropped\n * silently rather than thrown, since this reads declarative markup a\n * consumer authored, not validated user input. A blank `end` defaults to\n * `start` (single-day entry); an `end` before `start` is clamped up to\n * `start` rather than treated as an error.\n */\nexport function resolveEntry(data: CalendarEntryData): ResolvedCalendarEntry | null {\n const startDate = parseIsoDate(data.start);\n if (!startDate) return null;\n\n const endDate = parseIsoDate(data.end) ?? startDate;\n return {\n ...data,\n details: data.details ?? [],\n startDate,\n endDate: endDate < startDate ? startDate : endDate,\n };\n}\n\n/** Whether `entry`'s inclusive date range intersects the inclusive `[rangeStart, rangeEnd]` range. */\nexport function overlapsRange(entry: ResolvedCalendarEntry, rangeStart: Date, rangeEnd: Date): boolean {\n return entry.startDate <= rangeEnd && entry.endDate >= rangeStart;\n}\n\n/**\n * Assigns each entry a 0-based stacking lane via greedy interval\n * partitioning (\"meeting rooms II\"): entries are sorted by start date, then\n * each is placed in the lowest-numbered lane whose current occupant ends\n * strictly before the entry starts. Because entry ranges are\n * inclusive-inclusive, an entry ending on day X and one starting on day X\n * both occupy day X and must not share a lane — hence the strict `<`.\n *\n * Lane assignment is meant to be computed independently per `calendar-month`\n * (from only the entries overlapping that month); an entry spanning a month\n * boundary may therefore land in a different lane in adjacent months. This\n * is an accepted v1 limitation, not a bug.\n */\nexport function assignLanes(entries: ResolvedCalendarEntry[]): {\n entries: LanedEntry[];\n laneCount: number;\n} {\n const sorted = entries\n .map((entry, index) => ({ entry, index }))\n .sort((a, b) => {\n const startDiff = a.entry.startDate.getTime() - b.entry.startDate.getTime();\n if (startDiff !== 0) return startDiff;\n const endDiff = a.entry.endDate.getTime() - b.entry.endDate.getTime();\n if (endDiff !== 0) return endDiff;\n return a.index - b.index;\n });\n\n const laneEnds: Date[] = [];\n const laned: LanedEntry[] = [];\n for (const { entry } of sorted) {\n let lane = laneEnds.findIndex((end) => end < entry.startDate);\n if (lane === -1) {\n lane = laneEnds.length;\n laneEnds.push(entry.endDate);\n } else {\n laneEnds[lane] = entry.endDate;\n }\n laned.push({ ...entry, lane });\n }\n\n return { entries: laned, laneCount: laneEnds.length };\n}\n"]}
|