From 0ec06c83976c634c761d4c099f12915aadeb795f Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:07:16 -0700 Subject: [PATCH 01/22] test: stabilize browser fixtures and assertions --- playwright.config.mjs | 12 ++- tests/e2e/day-selection-feedback.spec.js | 24 ++--- tests/e2e/element-branding.spec.js | 5 +- tests/e2e/helpers.js | 36 +++++++ tests/e2e/responsive-layout.spec.js | 15 +-- tests/e2e/screen-reader-semantics.spec.js | 4 +- tests/e2e/swipe-gestures.spec.js | 110 +++++----------------- 7 files changed, 94 insertions(+), 112 deletions(-) diff --git a/playwright.config.mjs b/playwright.config.mjs index ce3bb9d..52d942a 100644 --- a/playwright.config.mjs +++ b/playwright.config.mjs @@ -22,6 +22,7 @@ export default defineConfig({ timeout: 5_000 }, forbidOnly: Boolean(process.env["CI"]), + failOnFlakyTests: true, fullyParallel: true, globalSetup: "./tests/e2e/global-setup.js", outputDir: "test-results/playwright", @@ -34,19 +35,26 @@ export default defineConfig({ }, { name: "firefox", + grepInvert: /@chromium-input/u, use: { ...devices["Desktop Firefox"] } }, { name: "webkit", + grepInvert: /@chromium-input/u, use: { ...devices["Desktop Safari"] } } ], reporter: process.env["CI"] - ? [["github"], ["line"]] + ? [ + ["github"], + ["line"], + ["json", { outputFile: "test-results/browser-results.json" }], + ["html", { open: "never", outputFolder: "test-results/browser-report" }] + ] : [["list"]], retries: process.env["CI"] ? 1 : 0, testDir: "./tests/e2e", @@ -61,5 +69,5 @@ export default defineConfig({ trace: "retain-on-failure", video: "off" }, - workers: 2 + workers: process.env["CI"] ? 1 : 2 }); diff --git a/tests/e2e/day-selection-feedback.spec.js b/tests/e2e/day-selection-feedback.spec.js index ec48fff..a7aef66 100644 --- a/tests/e2e/day-selection-feedback.spec.js +++ b/tests/e2e/day-selection-feedback.spec.js @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; -import { expectExampleReady, expectOnlyOneGridTabStop } from "./helpers.js"; +import { expectLibraryFixtureReady, expectOnlyOneGridTabStop } from "./helpers.js"; const ANIMATION_NAME = "lfc-day-selection-reveal"; const CONFIRM_ANIMATION_NAME = "lfc-day-selection-confirm"; @@ -8,11 +8,11 @@ const FEEDBACK_ANIMATION_NAMES = [ANIMATION_NAME, CONFIRM_ANIMATION_NAME].sort() const TARGET_DATE = "2026-08-31"; const SECOND_TARGET_DATE = "2026-08-30"; -test.describe("when motion is allowed", () => { +test.describe("when motion is allowed", { tag: "@firefox-regression" }, () => { test.use({ reducedMotion: "no-preference" }); test("an immediate pointer click produces one coherent selection transition with a seamless cleanup", async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); + await expectLibraryFixtureReady(page, { calendar: true }); const grid = page.getByRole("grid"); const target = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`); await armSelectionProbe(target); @@ -84,7 +84,7 @@ test.describe("when motion is allowed", () => { }); test("native Today navigation stays settled while direct current-day selection transitions", async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); + await expectLibraryFixtureReady(page, { calendar: true }); const grid = page.getByRole("grid"); const title = page.locator(".lfc-calendar-title-label-full"); await page.locator(".lfc-calendar-nav-button-next").click(); @@ -124,7 +124,7 @@ test.describe("when motion is allowed", () => { }); test("rapid reselection cancels stale feedback and leaves only the latest transition", async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); + await expectLibraryFixtureReady(page, { calendar: true }); const grid = page.getByRole("grid"); const firstTarget = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`); await armSelectionProbe(firstTarget); @@ -153,7 +153,7 @@ test.describe("when motion is allowed", () => { }); test("keyboard activation receives the same transition and immediate semantics", async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); + await expectLibraryFixtureReady(page, { calendar: true }); const grid = page.getByRole("grid"); const target = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`); await target.focus(); @@ -189,15 +189,11 @@ test.describe("when motion is allowed", () => { }); }); -test.describe("with trusted touch input", () => { +test.describe("with trusted touch input", { tag: "@chromium-input" }, () => { test.use({ hasTouch: true, reducedMotion: "no-preference" }); - test.skip( - ({ browserName }) => browserName !== "chromium", - "Trusted touch injection uses Chromium CDP, which Playwright does not expose for Firefox or WebKit." - ); test("a tap hands off pressed-day feedback to the selection transition", async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); + await expectLibraryFixtureReady(page, { calendar: true }); const grid = page.getByRole("grid"); const target = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`); await armSelectionProbe(target); @@ -239,7 +235,7 @@ test.describe("when reduced motion is requested", () => { test.use({ reducedMotion: "reduce" }); test("pointer and keyboard selection render their settled state immediately", async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); + await expectLibraryFixtureReady(page, { calendar: true }); const grid = page.getByRole("grid"); const pointerTarget = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`); await pointerTarget.click(); @@ -268,7 +264,7 @@ test.describe("when forced colors are active", () => { test.use({ forcedColors: "active", reducedMotion: "reduce" }); test("selected and focused state remains visibly outlined without decorative feedback", async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); + await expectLibraryFixtureReady(page, { calendar: true }); const grid = page.getByRole("grid"); const target = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`); await target.focus(); diff --git a/tests/e2e/element-branding.spec.js b/tests/e2e/element-branding.spec.js index 13a3bd3..b8b4255 100644 --- a/tests/e2e/element-branding.spec.js +++ b/tests/e2e/element-branding.spec.js @@ -1,8 +1,9 @@ import { expect, test } from "@playwright/test"; +import { expectLibraryFixtureReady } from "./helpers.js"; + test("a forwarding DOM proxy cannot cross the public host boundary", async ({ page }) => { - const response = await page.goto("/examples/basic/", { waitUntil: "domcontentloaded" }); - expect(response?.ok()).toBe(true); + await expectLibraryFixtureReady(page); const result = await page.evaluate(async () => { const { createCalendar } = await import("/dist/index.js"); diff --git a/tests/e2e/helpers.js b/tests/e2e/helpers.js index 21f4ac0..0cfebbf 100644 --- a/tests/e2e/helpers.js +++ b/tests/e2e/helpers.js @@ -2,6 +2,17 @@ import AxeBuilder from "@axe-core/playwright"; import { expect } from "@playwright/test"; const READY_SELECTOR = 'html[data-test-ready="true"]'; +const LIBRARY_FIXTURE_ROUTE = "/examples/basic/?library-fixture=true"; +const LIBRARY_FIXTURE_DOCUMENT = ` + + + + +Calendar browser fixture + + +
+`; const WCAG_TAGS = Object.freeze([ "wcag2a", "wcag2aa", @@ -10,6 +21,31 @@ const WCAG_TAGS = Object.freeze([ "wcag22aa" ]); +export async function expectLibraryFixtureReady(page, { calendar = false } = {}) { + //Keep the repository server's CSP and security headers; replace only the example document. + await page.route((url) => url.searchParams.get("library-fixture") === "true", async (route) => { + const response = await route.fetch(); + await route.fulfill({ body: LIBRARY_FIXTURE_DOCUMENT, response }); + }, { times: 1 }); + const response = await page.goto(LIBRARY_FIXTURE_ROUTE, { waitUntil: "load" }); + expect(response?.ok(), "Expected the isolated library document to load.").toBe(true); + await expect(page.locator(READY_SELECTOR)).toHaveCount(1); + if (calendar) { + await page.evaluate(async () => { + const { createCalendar } = await import("/dist/index.js"); + const host = document.querySelector("[data-my-calendar]"); + const instance = createCalendar(host, { + events: [], + initialDate: "2026-08-06", + now: () => new Date("2026-08-07T02:00:00.000Z"), + onDaySelect: () => {} + }); + instance.render(); + }); + await expect(page.getByRole("grid")).toBeVisible(); + } +} + export async function expectExampleReady(page, route) { const response = await page.goto(route, { waitUntil: "commit" }); expect(response?.ok(), `Expected ${route} to return a successful response.`).toBe(true); diff --git a/tests/e2e/responsive-layout.spec.js b/tests/e2e/responsive-layout.spec.js index 77cb579..71ba23f 100644 --- a/tests/e2e/responsive-layout.spec.js +++ b/tests/e2e/responsive-layout.spec.js @@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test"; import { expectExampleReady, + expectLibraryFixtureReady, gridEventActions } from "./helpers.js"; @@ -463,7 +464,7 @@ async function expectCompactToolbarVisualLayout(page, layout) { async function prepareWeekLayoutFixturePage(page) { await page.setViewportSize({ height: COMPACT_VIEWPORT_HEIGHT, width: 1_100 }); - await expectExampleReady(page, "/examples/advanced/"); + await expectLibraryFixtureReady(page); } async function mountWeekLayoutFixture( @@ -733,7 +734,7 @@ async function mountResponsiveMultipleEventFixture( width = 768 } = {} ) { - await expectExampleReady(page, "/examples/advanced/"); + await expectLibraryFixtureReady(page); await page.addStyleTag({ content: ` .my-responsive-marker { @@ -1376,7 +1377,7 @@ test("short month labels use the strict 24rem content-box boundary without runti expect(stability).toEqual({ callsStable: true, nodesStable: true, providerCalls: 1 }); }); -test("default equal rows grow intrinsically and remain stable across responsive layout", async ({ page }) => { +test("default equal rows grow intrinsically and remain stable across responsive layout", { tag: "@firefox-regression" }, async ({ page }) => { await prepareWeekLayoutFixturePage(page); const { host } = await mountWeekLayoutFixture(page); const initial = await getWeekLayoutGeometry(host); @@ -1412,9 +1413,11 @@ test("default equal rows grow intrinsically and remain stable across responsive expect(grown.anchorMarkerBlockSize).toBeGreaterThanOrEqual(192); expectEqualWeekHeights(grown); expectWeekLayoutContained(grown); - expect(Math.min(...grown.weekHeights) - Math.max(...initial.weekHeights)) - .toBeGreaterThan(48); - expect(grown.calendarBlockSize - initial.calendarBlockSize).toBeGreaterThan(6 * 48); + const rowGrowth = grown.weekHeights.map((height, index) => height - initial.weekHeights[index]); + expect(Math.min(...rowGrowth), "Every equal row must grow to contain the taller marker.") + .toBeGreaterThan(0); + expect(grown.calendarBlockSize - initial.calendarBlockSize) + .toBeCloseTo(rowGrowth.reduce((total, growth) => total + growth, 0), 0); for (const width of [390, 768]) { await setWeekLayoutFixtureWidth(page, width); diff --git a/tests/e2e/screen-reader-semantics.spec.js b/tests/e2e/screen-reader-semantics.spec.js index cd641a2..6348ce6 100644 --- a/tests/e2e/screen-reader-semantics.spec.js +++ b/tests/e2e/screen-reader-semantics.spec.js @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; -import { expectExampleReady, expectOnlyOneGridTabStop } from "./helpers.js"; +import { expectExampleReady, expectLibraryFixtureReady, expectOnlyOneGridTabStop } from "./helpers.js"; const ANNOUNCEMENT_FIXTURE_SELECTOR = "#my-screen-reader-announcement-fixture"; const FAILURE_FIXTURE_SELECTOR = "#my-screen-reader-failure-fixture"; @@ -163,7 +163,7 @@ test.describe("screen-reader-facing browser semantics", () => { }); test("routes progress and failures through polite and assertive live regions without stealing focus", async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); + await expectLibraryFixtureReady(page); const announcementHost = await mountAnnouncementFixture(page); const politeLive = announcementHost.getByRole("status"); const assertiveLive = announcementHost.getByRole("alert"); diff --git a/tests/e2e/swipe-gestures.spec.js b/tests/e2e/swipe-gestures.spec.js index cbd9381..02f0f45 100644 --- a/tests/e2e/swipe-gestures.spec.js +++ b/tests/e2e/swipe-gestures.spec.js @@ -1,13 +1,11 @@ import { expect, test } from "@playwright/test"; -import { expectExampleReady } from "./helpers.js"; +import { expectExampleReady, expectLibraryFixtureReady } from "./helpers.js"; const HOST_SELECTOR = "[data-my-calendar]"; const MONTH_SELECTOR = "[data-my-state-month]"; const VIEWPORT_SELECTOR = ".lfc-calendar-swipe-viewport"; const WHEEL_BURST_MAXIMUM_SPAN_MILLISECONDS = 120; -const CHROMIUM_INPUT_REASON = - "Trusted touch and pen injection uses Chromium CDP, which Playwright does not expose for Firefox or WebKit."; async function createChromiumInputClient(page, reducedMotion) { const client = await page.context().newCDPSession(page); @@ -19,10 +17,6 @@ async function createChromiumInputClient(page, reducedMotion) { return client; } -function skipWithoutChromiumInput(browserName) { - test.skip(browserName !== "chromium", CHROMIUM_INPUT_REASON); -} - async function dispatchTouch(client, type, points) { await client.send("Input.dispatchTouchEvent", { touchPoints: points.map(({ id = 0, x, y }) => ({ @@ -72,6 +66,7 @@ async function runTouchGesture(page, client, start, deltaX, deltaY, cancel = fal } async function mountCalendarFixture(page, options = {}) { + await expectLibraryFixtureReady(page); await page.evaluate(async (fixtureOptions) => { const { createCalendar } = await import("/dist/index.js"); const fixture = document.createElement("section"); @@ -218,11 +213,8 @@ function pagingLaneLabels(host, direction) { test.describe("native month pager", () => { test.use({ reducedMotion: "no-preference" }); - test.beforeEach(async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); - }); - test("renders hidden semantic lanes around one live grid and settles the current snap", async ({ page }) => { + await expectExampleReady(page, "/examples/advanced/"); const host = page.locator(HOST_SELECTOR); const viewport = host.locator(VIEWPORT_SELECTOR); await expect(host).toHaveAttribute("data-lfc-swipe-enabled", "true"); @@ -332,7 +324,7 @@ test.describe("native month pager", () => { await expect(viewport).not.toHaveAttribute("aria-hidden"); }); - test("scrollend requests one month while current and bounded snaps do not", async ({ page }) => { + test("scrollend requests one month while current and bounded snaps do not", { tag: "@firefox-regression" }, async ({ page }) => { const host = await mountCalendarFixture(page, { maxDate: "2026-09-30", minDate: "2026-08-01" @@ -401,7 +393,7 @@ test.describe("native month pager", () => { await expect(nextLabels.compact).toHaveText(septemberCompact); }); - test("layout geometry remains exact through an ancestor transform and RTL recentering", async ({ page }) => { + test("layout geometry remains exact through an ancestor transform and RTL recentering", { tag: "@firefox-regression" }, async ({ page }) => { const host = await mountCalendarFixture(page); const ruleInstalled = await page.evaluate(() => { for (const sheet of document.styleSheets) { @@ -467,7 +459,7 @@ test.describe("native month pager", () => { )).toBe(5); }); - test("wheel burst timestamps remain one transaction across pager rerenders", async ({ page }) => { + test("wheel burst timestamps remain one transaction across pager rerenders", { tag: "@firefox-regression" }, async ({ page }) => { const host = await mountCalendarFixture(page); for (const wheelTimeIncrement of [WHEEL_BURST_MAXIMUM_SPAN_MILLISECONDS + 1, 10, 10]) { await setPagerPosition(host, "next", true, 1, wheelTimeIncrement); @@ -484,59 +476,8 @@ test.describe("native month pager", () => { await expectPagerClean(host); }); - test("a trusted rapid three-event horizontal wheel burst commits exactly one page", async ({ page }) => { - const host = await mountCalendarFixture(page); - const point = await gesturePoint(host.locator( - '.lfc-calendar-day-button[data-lfc-date="2026-08-13"]' - ), 16); - await page.evaluate(() => { - const burst = []; - Object.defineProperty(window, "__lfcTrustedWheelBurst", { - configurable: true, - value: burst - }); - document.addEventListener("wheel", (event) => { - if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) { - burst.push({ - isTrusted: event.isTrusted, - timeStamp: event.timeStamp - }); - } - }, { capture: true, passive: true }); - }); - await page.mouse.move(point.x, point.y); - for (let eventIndex = 0; eventIndex < 3; eventIndex += 1) { - await page.mouse.wheel(90, 0); - } - - await expect.poll(() => page.evaluate(() => - window.__lfcTrustedWheelBurst.length - )).toBe(3); - const trustedBurst = await page.evaluate(() => window.__lfcTrustedWheelBurst); - expect(trustedBurst).toHaveLength(3); - expect(trustedBurst.every((event) => event.isTrusted)).toBe(true); - const maximumObservedGap = Math.max( - ...trustedBurst.slice(1).map((event, eventIndex) => - event.timeStamp - trustedBurst[eventIndex].timeStamp - ) - ); - test.skip( - maximumObservedGap > WHEEL_BURST_MAXIMUM_SPAN_MILLISECONDS, - "The runner did not deliver the trusted wheel events within one burst window." - ); - await expect.poll(() => page.evaluate(() => - window.__lfcSwipeFixture.calendar.getState().displayedMonth.month - )).toBe(9); - await page.waitForTimeout(300); - expect(await page.evaluate(() => ({ - month: window.__lfcSwipeFixture.calendar.getState().displayedMonth.month, - requests: window.__lfcSwipeFixture.observations.requests.length - }))).toEqual({ month: 9, requests: 2 }); - await expectPagerClean(host); - }); - - test("trusted touch returns short pulls, commits once in LTR and RTL, and stops at bounds", async ({ browserName, page }) => { - skipWithoutChromiumInput(browserName); + test("trusted touch returns short pulls, commits once in LTR and RTL, and stops at bounds", { tag: "@chromium-input" }, async ({ page }) => { + await expectExampleReady(page, "/examples/advanced/"); const client = await createChromiumInputClient(page, "no-preference"); const host = page.locator(HOST_SELECTOR); const viewport = host.locator(VIEWPORT_SELECTOR); @@ -576,8 +517,8 @@ test.describe("native month pager", () => { await expect(actionResult).toHaveText(rtlAction ?? ""); }); - test("trusted vertical, multi-touch, and canceled gestures preserve the current month", async ({ browserName, page }) => { - skipWithoutChromiumInput(browserName); + test("trusted vertical, multi-touch, and canceled gestures preserve the current month", { tag: "@chromium-input" }, async ({ page }) => { + await expectExampleReady(page, "/examples/advanced/"); const client = await createChromiumInputClient(page, "no-preference"); const host = page.locator(HOST_SELECTOR); const viewport = host.locator(VIEWPORT_SELECTOR); @@ -610,6 +551,7 @@ test.describe("native month pager", () => { }); test("trusted wheel pages horizontally and leaves vertical wheel unhandled", async ({ page }) => { + await expectExampleReady(page, "/examples/advanced/"); const host = page.locator(HOST_SELECTOR); let point = await gesturePoint(page.locator( '.lfc-calendar-day-button[data-lfc-date="2026-08-13"]' @@ -648,10 +590,9 @@ test.describe("native month pager", () => { await expectPagerClean(host); }); - test("trusted pen input is not intercepted", async ({ browserName, page }) => { - skipWithoutChromiumInput(browserName); - const client = await createChromiumInputClient(page, "no-preference"); + test("trusted pen input is not intercepted", { tag: "@chromium-input" }, async ({ page }) => { const host = await mountCalendarFixture(page); + const client = await createChromiumInputClient(page, "no-preference"); const viewport = host.locator(VIEWPORT_SELECTOR); await viewport.evaluate((element) => { @@ -675,23 +616,23 @@ test.describe("native month pager", () => { await dispatchPen(client, "mousePressed", point, true); await dispatchPen(client, "mouseMoved", { x: point.x + 80, y: point.y }, true); await dispatchPen(client, "mouseReleased", { x: point.x + 80, y: point.y }, false); - const penOutcome = await page.evaluate(() => ({ + const penOutcome = await host.evaluate((element) => ({ events: window.__lfcPenEvents, - hasInlineStyle: document.querySelector("[data-my-calendar]")?.matches("[style]") || - document.querySelector("[data-my-calendar] [style]") !== null + hasInlineStyle: element.matches("[style]") || element.querySelector("[style]") !== null })); expect(penOutcome.events.length).toBeGreaterThanOrEqual(3); expect(penOutcome.events.every((event) => event.isTrusted && !event.defaultPrevented && !event.hasCapture )).toBe(true); expect(penOutcome.hasInlineStyle).toBe(false); - await expect(page.locator(MONTH_SELECTOR)).toHaveText("2026-08-01"); + expect(await page.evaluate(() => + window.__lfcSwipeFixture.calendar.getState().displayedMonth.month + )).toBe(8); }); - test("held touch cannot double-navigate across refetch or programmatic navigation", async ({ browserName, page }) => { - skipWithoutChromiumInput(browserName); - const client = await createChromiumInputClient(page, "no-preference"); + test("held touch cannot double-navigate across refetch or programmatic navigation", { tag: "@chromium-input" }, async ({ page }) => { const host = await mountCalendarFixture(page); + const client = await createChromiumInputClient(page, "no-preference"); const viewport = host.locator(VIEWPORT_SELECTOR); let point = await gesturePoint(viewport); @@ -770,10 +711,9 @@ test.describe("native month pager", () => { }))).toEqual({ month: 8, requests: 1, state: null }); }); - test("swipe false ignores trusted touch paging", async ({ browserName, page }) => { - skipWithoutChromiumInput(browserName); - const client = await createChromiumInputClient(page, "no-preference"); + test("swipe false ignores trusted touch paging", { tag: "@chromium-input" }, async ({ page }) => { const host = await mountCalendarFixture(page, { swipe: false }); + const client = await createChromiumInputClient(page, "no-preference"); const point = await gesturePoint(host.locator(VIEWPORT_SELECTOR)); await runTouchGesture(page, client, point, -120, 0); await page.waitForTimeout(200); @@ -788,11 +728,9 @@ test.describe("native month pager", () => { test.describe("native pager with reduced motion", () => { test.use({ reducedMotion: "reduce" }); - test("tracks directly and has no post-terminal snap interpolation or authored animation", async ({ browserName, page }) => { - skipWithoutChromiumInput(browserName); - await expectExampleReady(page, "/examples/advanced/"); - const client = await createChromiumInputClient(page, "reduce"); + test("tracks directly and has no post-terminal snap interpolation or authored animation", { tag: "@chromium-input" }, async ({ page }) => { const host = await mountCalendarFixture(page); + const client = await createChromiumInputClient(page, "reduce"); const viewport = host.locator(VIEWPORT_SELECTOR); expect(await page.evaluate(() => ({ noPreference: matchMedia("(prefers-reduced-motion: no-preference)").matches, From 571191300b8834df45897bb2745bb4d3137eb948 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:09:31 -0700 Subject: [PATCH 02/22] ci: reject flaky browser results --- .github/workflows/ci.yml | 53 ++++++++++++++++++++++- scripts/qualify-firefox.mjs | 46 ++++++++++++++++++++ scripts/tests/workflow-contracts.test.mjs | 28 +++++++++++- 3 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 scripts/qualify-firefox.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 472e752..cf13eb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,8 +55,8 @@ jobs: - name: Run the complete local quality gate run: npm run check - - name: Upload browser diagnostics on failure - if: failure() + - name: Upload browser results and failure diagnostics + if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: browser-diagnostics-${{ github.run_id }} @@ -65,3 +65,52 @@ jobs: playwright-report/ test-results/ retention-days: 7 + + firefox-qualification: + name: Qualify Firefox (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - name: Check out source without persisted credentials + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up exact Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ env.LFC_NODE_VERSION }} + package-manager-cache: false + + - name: Set up exact npm + shell: bash + working-directory: ${{ runner.temp }} + run: npm install --global "npm@${LFC_NPM_VERSION}" --ignore-scripts + + - name: Install locked development tooling + run: npm ci --ignore-scripts + + - name: Install pinned Firefox + run: npx playwright install --with-deps firefox + + - name: Build package and examples + run: npm run build + + - name: Repeat regressions and qualify three complete Firefox runs + env: + DEBUG: pw:browser + run: node scripts/qualify-firefox.mjs + + - name: Upload Firefox qualification evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: firefox-qualification-${{ matrix.os }}-${{ github.run_id }} + if-no-files-found: ignore + path: test-results/firefox-qualification/ + retention-days: 7 diff --git a/scripts/qualify-firefox.mjs b/scripts/qualify-firefox.mjs new file mode 100644 index 0000000..5b30997 --- /dev/null +++ b/scripts/qualify-firefox.mjs @@ -0,0 +1,46 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { assertSupportedNodeVersion } from "./lib/node-version.mjs"; +import { REPOSITORY_ROOT, run } from "./lib/process.mjs"; + +assertSupportedNodeVersion("Firefox qualification"); + +const reportRoot = join(REPOSITORY_ROOT, "test-results", "firefox-qualification"); +const runs = [ + { name: "regressions-20", arguments: ["--grep=@firefox-regression", "--repeat-each=20"] }, + ...Array.from({ length: 3 }, (_value, index) => ({ + name: `complete-${String(index + 1)}`, + arguments: [] + })) +]; +await mkdir(reportRoot, { recursive: true }); +for (const qualification of runs) { + const output = join(reportRoot, qualification.name); + await mkdir(output, { recursive: true }); + await writeFile(join(output, "environment.json"), `${JSON.stringify({ + arch: process.arch, + node: process.version, + platform: process.platform, + retries: 0, + workers: 1 + }, null, 2)}\n`); + await run(process.execPath, [ + join(REPOSITORY_ROOT, "node_modules", "@playwright", "test", "cli.js"), + "test", + "--project=firefox", + "--workers=1", + "--retries=0", + "--max-failures=1", + "--reporter=line,json,html", + `--output=${join(output, "artifacts")}`, + ...qualification.arguments + ], { + env: { + ...process.env, + PLAYWRIGHT_HTML_OPEN: "never", + PLAYWRIGHT_HTML_OUTPUT_DIR: join(output, "html"), + PLAYWRIGHT_JSON_OUTPUT_FILE: join(output, "results.json") + } + }); +} diff --git a/scripts/tests/workflow-contracts.test.mjs b/scripts/tests/workflow-contracts.test.mjs index 164741c..5aa4eb6 100644 --- a/scripts/tests/workflow-contracts.test.mjs +++ b/scripts/tests/workflow-contracts.test.mjs @@ -608,7 +608,7 @@ void test("release gates and Playwright retain the complete browser-engine matri void test("workflow artifacts use bounded purpose-specific retention", async () => { const expectations = new Map([ - ["ci.yml", [7]], + ["ci.yml", [7, 7]], ["deploy-examples.yml", [1, 1]], ["prepare-alpha.yml", [1]], ["publish-alpha.yml", [7, 30, 30]], @@ -658,3 +658,29 @@ void test("workflow dependency caches stay disabled", async () => { ); } }); + +void test("browser gates reject flakes and retain reproducible Firefox qualification evidence", async () => { + const source = await workflow("ci.yml"); + const qualification = job(source, "firefox-qualification"); + assert.equal(playwrightConfiguration.failOnFlakyTests, true); + assert.match(qualification, /os: \[ubuntu-latest, windows-latest\]/u); + assert.match(qualification, /node scripts\/qualify-firefox\.mjs/u); + assert.match(qualification, /if: always\(\)[\s\S]*?path: test-results\/firefox-qualification\//u); + const config = await readFile(join(REPOSITORY_ROOT, "playwright.config.mjs"), "utf8"); + assert.match(config, /workers: process\.env\["CI"\] \? 1 : 2/u); + assert.match(config, /retries: process\.env\["CI"\] \? 1 : 0/u); + for (const project of playwrightConfiguration.projects) { + if (project.name === "chromium") { + assert.equal(project.grepInvert, undefined); + } else { + assert.equal(project.grepInvert?.test("@chromium-input"), true); + } + } + const qualifier = await readFile(join(REPOSITORY_ROOT, "scripts", "qualify-firefox.mjs"), "utf8"); + assert.match(qualifier, /--repeat-each=20/u); + assert.match(qualifier, /length: 3/u); + assert.match(qualifier, /--retries=0/u); + assert.match(qualifier, /--workers=1/u); + assert.match(qualifier, /PLAYWRIGHT_JSON_OUTPUT_FILE/u); + assert.match(qualifier, /PLAYWRIGHT_HTML_OUTPUT_DIR/u); +}); From a7e2985f998abc65ad0a76555676c668fbd9ee03 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:33:18 -0700 Subject: [PATCH 03/22] feat: adopt Vite Baseline compatibility policy --- ACCESSIBILITY.md | 2 + CONTRIBUTING.md | 1 + DESIGN.md | 6 +- docs/browser-support.md | 34 +- docs/code-style.md | 4 + examples/advanced/theme.css | 9 + examples/example.css | 43 ++ examples/index.css | 1 + package-lock.json | 754 ++++++++++++++++++++++++- package.json | 8 +- scripts/lib/browser-targets.mjs | 82 +++ scripts/report-browser-targets.mjs | 8 + scripts/tests/browser-targets.test.mjs | 36 ++ src/internal/dom/month-picker.ts | 86 ++- src/internal/dom/palette.ts | 73 +++ src/internal/dom/structure.ts | 2 +- src/internal/runtime/coordinator.ts | 5 + src/styles/agenda.css | 1 + src/styles/base.css | 2 + src/styles/month-grid.css | 20 +- src/styles/preferences.css | 26 - src/styles/responsive.css | 4 + src/styles/tokens.css | 88 ++- src/styles/toolbar.css | 12 + tests/e2e/baseline-fallbacks.js | 19 + tests/e2e/browser-baseline.spec.js | 80 +++ tests/e2e/theme-integration.spec.js | 153 +++++ 27 files changed, 1456 insertions(+), 103 deletions(-) create mode 100644 scripts/lib/browser-targets.mjs create mode 100644 scripts/report-browser-targets.mjs create mode 100644 scripts/tests/browser-targets.test.mjs create mode 100644 src/internal/dom/palette.ts create mode 100644 tests/e2e/baseline-fallbacks.js create mode 100644 tests/e2e/browser-baseline.spec.js create mode 100644 tests/e2e/theme-integration.spec.js diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md index 994d066..c4bcae5 100644 --- a/ACCESSIBILITY.md +++ b/ACCESSIBILITY.md @@ -43,6 +43,8 @@ Replacing event input keeps package-owned focus on the same day or event occurre The presented month and year is a native button inside the configured `h1` through `h6`; a dedicated visible date span alone labels the grid. The button opens the default `popover="auto"` surface exposed as a dialog, synchronizes its labelled month and year controls, and focuses Month. A successful Jump accepts only an in-range month, while Cancel makes no date change; both paths close the popover and return focus to the month/year trigger. Invalid form input remains open for correction. Escape also dismisses the popover and restores trigger focus. Pointer/outside light-dismiss closes it without stealing focus from the user's new target. The package-owned labels are localized through `chooseMonthYear`, `jumpToMonthYear`, `month`, `year`, `jump`, and `cancel`. +Where Popover is unavailable, the same picker uses a native modal `` to meet the [Baseline Widely available policy](docs/browser-support.md). Native modality keeps focus within the picker and makes the rest of the document inert. Jump, Cancel, Escape, and backdrop dismissal restore the trigger; invalid input remains open. Theme changes preserve the open surface, field values, selection, and focus in both implementations. Test the fallback independently by disabling the optional capability; current-engine success alone does not verify it. + Previous, Next, Today, swipe, month/year jump, grid keyboard movement, day activation, and focus movement all honor the same inclusive bounds. Previous, Next, and Today remain in the normal Tab order and expose `aria-disabled="true"` when their action has no in-range destination; their guarded handlers make activation a no-op. Disabled filler-day buttons cannot receive focus or invoke day actions. Bounds do not shorten event-source requests: each rendered month still uses the complete 42-day provider range, including visible disabled dates outside the configured range. The current date uses `aria-current="date"`; the chosen agenda date uses `aria-selected`. They are independent states and are not communicated by color or motion alone. Directly activating a different day in the displayed month commits selection, agenda, ARIA, and focus before any decorative feedback begins. The [canonical day-state and motion treatment](DESIGN.md#month-grid-and-day-states) keeps selection and focus outlines visible throughout; reduced motion and other selection paths show the settled treatment immediately. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c06fa6..4748277 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,7 @@ Small fixes may go directly to a pull request. Discuss large public-API, date-mo - Install the exact development dependencies from `package-lock.json` and use the repository-pinned Chromium, Firefox, and WebKit binaries. The [contributor command reference](CONTRIBUTOR_COMMANDS.md#set-up-the-repository) owns the copyable setup commands. - Keep text files UTF-8 with LF line endings. Git attributes and EditorConfig enforce this across supported editors and operating systems. - Do not add runtime, peer, optional, or bundled dependencies; install hooks; remote assets; CDNs; fonts; or icons. Exact-pin and justify any new development dependency or browser tooling in the pull request. +- Target [Vite's Baseline Widely available preset](docs/browser-support.md) for the library, examples, and published site. Resolve `baseline-widely-available` through Vite's public API; the support window is fixed per major, not a daily rolling query. Review weekly Dependabot Vite updates, including majors, through CI. Any capability outside the effective targets needs feature detection and a tested supported fallback. Use focused checks while developing, then run the [complete repository gate](CONTRIBUTOR_COMMANDS.md#run-the-final-gate) before submission. Automated checks do not replace affected manual accessibility or compatibility review. diff --git a/DESIGN.md b/DESIGN.md index 748e6d2..da854f4 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -286,7 +286,7 @@ For implementation work, start with the relevant component section, then check t ## Colors -The light palette resembles clean ledger paper rather than pure white. The dark palette keeps the same semantic relationships instead of merely inverting the light values. `prefers-color-scheme: dark` selects the dark defaults; application overrides participate in the normal cascade. +The light palette resembles clean ledger paper rather than pure white. The dark palette keeps the same semantic relationships instead of merely inverting the light values. The host defaults to `color-scheme: light dark` and follows system preference. Applications can force `light` or `dark`, or set `inherit` on the host to follow ancestor themes. Native `light-dark()` and its tested Baseline fallback select the same palette without resetting focused controls. Application token overrides participate in the normal cascade. ### Core surfaces @@ -363,7 +363,7 @@ Interactive day and compact event targets never fall below 24 by 24 CSS pixels a Litefold Calendar's core interface is flat. Hierarchy comes from adjacent cool-toned surfaces, one-CSS-pixel rules, inset outlines, and the browser top layer—not floating cards or ornamental depth. The root, grid, agenda, status panel, controls, and month/year picker use borders rather than drop shadows. Event hover uses a restrained inset rule; selected and focused days use outlines that remain visible throughout feedback. -The month/year picker uses the native popover top layer with a backdrop mixed from 20% current ink and transparency. It does not gain a drop shadow, blur, glass treatment, gradient, or simulated window chrome. Application-owned example dialogs may use their application's elevation system and are outside the package visual contract. +The month/year picker uses the native popover top layer, or a native modal dialog where Popover is unavailable, with a backdrop mixed from 20% current ink and transparency. It does not gain a drop shadow, blur, glass treatment, gradient, or simulated window chrome. Application-owned example dialogs may use their application's elevation system and are outside the package visual contract. ## Shapes @@ -386,7 +386,7 @@ Previous/Next are transparent glyph controls with the standard component radius. ### Month/year picker -Use the native non-modal `popover="auto"` surface, native Month ``, a filled primary Show month action, and a quiet Cancel action. The picker is at most `24rem` wide, scrolls within the dynamic viewport when necessary, and stacks its fields only at the smallest container. Opening, validation, dismissal, and focus behavior are canonical in the [built-in month-and-year jump](docs/api.md#built-in-month-and-year-jump) and [interaction model](ACCESSIBILITY.md#interaction-model). +Use the native non-modal `popover="auto"` surface with a native modal `` fallback, native Month ``, a filled primary Show month action, and a quiet Cancel action. The picker is at most `24rem` wide, scrolls within the dynamic viewport when necessary, and stacks its fields only at the smallest container. Opening, validation, dismissal, and focus behavior are canonical in the [built-in month-and-year jump](docs/api.md#built-in-month-and-year-jump) and [interaction model](ACCESSIBILITY.md#interaction-model). ### Month grid and day states diff --git a/docs/browser-support.md b/docs/browser-support.md index 00587e8..876ab3b 100644 --- a/docs/browser-support.md +++ b/docs/browser-support.md @@ -1,18 +1,22 @@ # Browser support -Litefold Calendar targets current standards-based browsers and ships no polyfills or legacy build. The support window rolls forward with stable browser releases instead of being pinned to package-specific version numbers. +Litefold Calendar targets **Baseline Widely available** across the library, examples, and published documentation site. The package has no runtime dependencies, global polyfills, or separate legacy build. ## Supported release window -The current and immediately previous stable major versions are supported: +The shared build target is Vite's `baseline-widely-available` preset. Vite fixes the preset's Baseline date for each major release; targets do not advance merely because another day passes. The repository resolves the preset through Vite's public API and uses its effective browser targets for JavaScript and CSS, without maintaining a separate browser-version list. See [Vite's target policy](https://vite.dev/config/build-options#build-target). + +Baseline Widely available describes capabilities interoperable across the core browser set for at least 30 months. The support commitment uses the installed Vite preset's fixed window, so a current Baseline badge alone does not prove support in every resolved target. See [MDN's Baseline definition](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility). + +Run `npm run browsers:report` to see the preset identifier, resolution date, resolved JavaScript/CSS targets, and the installed Vite and esbuild versions under `dataVersions`. Builds write the same evidence to `.cache/browser-targets.json`; release and screenshot evidence use the effective targets. Weekly Dependabot updates include Vite major updates, which require review and CI before they can change the supported window. The recorded resolution date is evidence of when the report was generated, not a rolling compatibility cutoff. | Environment | Supported browsers | | --- | --- | -| Desktop | Google Chrome, Chromium-based Microsoft Edge, Mozilla Firefox, and Apple Safari on macOS | -| Mobile | Google Chrome on Android and Apple Safari on iOS or iPadOS | -| Embedded system webviews | Android System WebView and `WKWebView` on vendor-supported Android, iOS, and iPadOS releases | +| Desktop | Vite-preset targets for Google Chrome, Chromium-based Microsoft Edge, Mozilla Firefox, and Apple Safari on macOS | +| Mobile | Equivalent preset capabilities in Google Chrome on Android and Apple Safari on iOS or iPadOS | +| Embedded system webviews | Vendor-supported Android System WebView and `WKWebView` with equivalent required capabilities; verify the embedding environment separately | -“Current” means the stable major release generally available when a defect is evaluated. Beta, Dev, Canary, Nightly, and Safari Technology Preview releases are useful for early testing but are not supported releases. Patch updates within a supported major should be installed before reporting a defect. +Install available security and patch updates. Beta, Dev, Canary, Nightly, and Safari Technology Preview releases are useful for early testing but do not define the support target. Baseline is a feature-interoperability policy; it does not certify every webview, device, or assistive-technology combination. This policy defines where the project accepts compatibility defects. Automated coverage gates every release; dated browser, device, and assistive-technology results are tracked in the [accessibility test record](../ACCESSIBILITY.md#assistive-technology-record) and repeated when relevant behavior changes. @@ -26,17 +30,21 @@ Use `npm run test:browser` for the complete matrix or `npm run test:browser:chro ## Required platform -The package relies directly on these modern platform capabilities: +The package relies directly on these Baseline platform capabilities: | Area | Required capabilities | | --- | --- | | JavaScript | ECMAScript modules, promises, `AbortController`, URL parsing, and `Intl` | -| DOM and input | Standard DOM, native semantic elements, the Popover API, Pointer/Touch/Wheel Events, and native scrolling | -| CSS | Custom properties, Grid and subgrid, Scroll Snap, logical and intrinsic sizing, cascade layers, `:where()`, `:has()`, animations, and inline-size container queries and units | +| DOM and input | Standard DOM, native semantic elements and modal dialogs, Pointer/Touch/Wheel Events, and native scrolling | +| CSS | Custom properties, Grid and subgrid, Scroll Snap, logical and intrinsic sizing, cascade layers, `:where()`, `:has()`, `:dir()`, animations, and inline-size container queries and units | The optional [classic-script recipe](integration-guide.md#classic-script-entry-point) also requires dynamic `import()`. -The displayed month/year button opens a package-owned `popover="auto"` with native form controls; it does not require ``. Locale-derived week starts use either `Intl.Locale#getWeekInfo()` or `Intl.Locale#weekInfo` and fall back to Sunday only when neither returns a usable `firstDay`. The package does not download compatibility code or modify global browser APIs. +Capabilities outside the resolved preset require feature detection and a tested supported path. The displayed month/year button uses `popover="auto"` when available and a native modal `` otherwise. Both provide labelled Month and Year controls, validation, Jump, Cancel, Escape, light dismissal, and focus restoration. The fallback uses native modality; other page controls remain inert until dismissal. It does not require `` or modify global APIs. + +The palette uses `light-dark()` when available. Otherwise a small local controller mirrors the effective host `color-scheme` into the same CSS palette, observing system preference and live host/ancestor theme attributes. It does not rerender, refetch, reset picker input, or move focus. The default host scheme remains `light dark`; applications can set `color-scheme: light`, `dark`, or `inherit` on their host. Use `inherit` when an ancestor controls application themes. Removing the override resumes the automatic default. Application token overrides and forced-color/increased-contrast rules remain in the normal cascade. + +Locale-derived week starts use either `Intl.Locale#getWeekInfo()` or `Intl.Locale#weekInfo` and fall back to Sunday only when neither returns a usable `firstDay`. `ResizeObserver` is an optional direct-input enhancement. When available, it clears an in-progress paging gesture after the viewport resizes. Without it, the ordinary calendar and its toolbar, keyboard, picker, and public navigation routes remain available. @@ -54,7 +62,7 @@ See the [direct-input accessibility contract](../ACCESSIBILITY.md#responsive-and ## Experimental WebMCP availability -WebMCP is not a required platform capability and is outside the rolling browser-support commitment above. Its `document.modelContext` API is experimental, selectively enabled, and may be unavailable in an otherwise supported browser. +WebMCP is not a required platform capability and is outside the Baseline support commitment above. Its `document.modelContext` API is experimental, selectively enabled, and may be unavailable in an otherwise supported browser. WebMCP is an optional extension imported from `@tryagaindev/litefold-calendar/extensions/webmcp`. Omitting `webMcp()` from `CalendarOptions.extensions` leaves it inactive; configuring it in a browser without the API produces a progressive no-op. In both cases, the ordinary calendar remains available. @@ -64,11 +72,11 @@ Use exact feature detection rather than browser-name or version inference. When The project does not support: -- Browsers outside the rolling two-major-version window. +- Browsers outside the installed Vite preset's resolved target. - Browser components outside the supported table, including in-app browsers that modify their engine, inject incompatible scripts, or cannot reproduce a defect in the corresponding supported browser or system webview. -- Polyfill-dependent operation, a legacy JavaScript build, CommonJS, `nomodule`, or transpilation performed by this package. +- Polyfill-dependent operation, a separate legacy JavaScript build, CommonJS, or `nomodule`. The package build transforms syntax for the resolved targets but cannot supply missing platform APIs. An unsupported browser may happen to work, but compatibility is not tested or guaranteed there. Applications may transpile their own code, but modifying or wrapping the distributed package does not expand the project's support commitment. diff --git a/docs/code-style.md b/docs/code-style.md index fb0d805..3b5e577 100644 --- a/docs/code-style.md +++ b/docs/code-style.md @@ -22,6 +22,10 @@ Use diagrams when they make a relationship materially easier to understand, whil - Do not use Mermaid frontmatter, embedded HTML, initialization/configuration directives, or `click` directives. Put navigation in ordinary Markdown links so repository checks can validate it. - Diagram stable concepts, public vocabulary, and ownership boundaries. Do not duplicate exhaustive module inventories, private selectors, incidental DOM order, or other volatile implementation details that would create a second source of truth. +## Browser compatibility + +Apply the [Baseline Widely available policy](browser-support.md) to library code, examples, and published-site code. Resolve the shared `baseline-widely-available` preset through Vite's public API; its support window is fixed per Vite major rather than updated daily. Do not copy resolved browser versions into source or prose. Review Vite dependency updates, including majors, through CI. Check new platform features against the installed preset's effective targets; a current Baseline badge alone is insufficient. Features outside those targets require capability detection and an independently tested supported implementation. Keep compatibility work local to the missing capability and release its listeners during teardown; do not add broad layout-observer systems or global polyfills. WebMCP remains an optional progressive extension. + ## TypeScript contracts - Keep strict TypeScript and `exactOptionalPropertyTypes`; omit an optional property instead of assigning `undefined`. diff --git a/examples/advanced/theme.css b/examples/advanced/theme.css index 5b49a40..50bb5fb 100644 --- a/examples/advanced/theme.css +++ b/examples/advanced/theme.css @@ -5,6 +5,14 @@ --my-task-color: light-dark(#006F8B, #70D8F4); } + @supports not (color: light-dark(white, black)) { + :root { + --my-appointment-color: var(--my-internal-light, #00766C) var(--my-internal-dark, #4DE0B5); + --my-milestone-color: var(--my-internal-light, #7048B8) var(--my-internal-dark, #C6A7FF); + --my-task-color: var(--my-internal-light, #006F8B) var(--my-internal-dark, #70D8F4); + } + } + .my-controls, .my-observation { align-content: start; @@ -101,6 +109,7 @@ } .my-layers label { + align-items: center; align-items: safe center; display: inline-flex; gap: 0.375rem; diff --git a/examples/example.css b/examples/example.css index b5e5ff2..551d7b6 100644 --- a/examples/example.css +++ b/examples/example.css @@ -33,6 +33,47 @@ font-family: var(--my-font-family); } + @supports not (color: light-dark(white, black)) { + :root { + --my-internal-light: initial; + --my-internal-dark: ; + --my-color: var(--my-internal-light, #102A36) var(--my-internal-dark, #EAF7F7); + --my-muted-color: var(--my-internal-light, #405F68) var(--my-internal-dark, #A8C8CC); + --my-page-background: var(--my-internal-light, #EDF5F5) var(--my-internal-dark, #031017); + --my-surface-background: var(--my-internal-light, #F7FCFC) var(--my-internal-dark, #061A23); + --my-subtle-background: var(--my-internal-light, #E9F4F5) var(--my-internal-dark, #0C2A34); + --my-border-color: var(--my-internal-light, #56737B) var(--my-internal-dark, #6E929A); + --my-primary-color: var(--my-internal-light, #047857) var(--my-internal-dark, #62E3A8); + --my-primary-contrast-color: var(--my-internal-light, #F4FFFB) var(--my-internal-dark, #062A20); + --my-focus-color: var(--my-internal-light, #4E46C7) var(--my-internal-dark, #C0B6FF); + --my-selected-background: var(--my-internal-light, #D4F4E5) var(--my-internal-dark, #12483F); + --my-selected-color: var(--my-internal-light, #0A3B32) var(--my-internal-dark, #D5FFF1); + --my-today-border-color: var(--my-internal-light, #071E26) var(--my-internal-dark, #062A20); + --my-event-background: var(--my-internal-light, #EFEEFF) var(--my-internal-dark, #282853); + --my-event-color: var(--my-internal-light, #262050) var(--my-internal-dark, #F1EEFF); + --my-event-border-color: var(--my-internal-light, #665CC3) var(--my-internal-dark, #A39AFF); + --my-event-accent-color: var(--my-internal-light, #A52A78) var(--my-internal-dark, #FF88C8); + --my-warning-background: var(--my-internal-light, #FFF7ED) var(--my-internal-dark, #431407); + --my-warning-color: var(--my-internal-light, #7C2D12) var(--my-internal-dark, #FFEDD5); + --my-warning-border-color: var(--my-internal-light, #C2410C) var(--my-internal-dark, #FDBA74); + --my-error-background: var(--my-internal-light, #FEF2F2) var(--my-internal-dark, #450A0A); + --my-error-color: var(--my-internal-light, #7F1D1D) var(--my-internal-dark, #FEE2E2); + --my-error-border-color: var(--my-internal-light, #B91C1C) var(--my-internal-dark, #FCA5A5); + } + + @media (prefers-color-scheme: dark) { + :root:not([data-my-theme="light"]) { + --my-internal-light: ; + --my-internal-dark: initial; + } + } + + :root[data-my-theme="dark"] { + --my-internal-light: ; + --my-internal-dark: initial; + } + } + :root[data-my-theme="light"] { color-scheme: light; } @@ -129,6 +170,7 @@ .my-control-row, .my-field-row { + align-items: center; align-items: safe center; display: flex; flex-wrap: wrap; @@ -181,6 +223,7 @@ } .my-check { + align-items: center; align-items: safe center; display: inline-flex; gap: 0.5rem; diff --git a/examples/index.css b/examples/index.css index 7b3cca3..b112453 100644 --- a/examples/index.css +++ b/examples/index.css @@ -221,6 +221,7 @@ } .my-card-run { + align-items: center; align-items: safe center; display: flex; gap: 0.75rem; diff --git a/package-lock.json b/package-lock.json index 1be2980..1dbc984 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,6 @@ "name": "@tryagaindev/litefold-calendar", "version": "0.5.0-alpha.0", "license": "MIT", - "devEngines": { - "runtime": { - "name": "node", - "version": "24.x", - "onFail": "warn" - }, - "packageManager": { - "name": "npm", - "version": ">=11.0.0", - "onFail": "warn" - } - }, "devDependencies": { "@axe-core/playwright": "4.13.0", "@eslint/js": "10.0.1", @@ -37,7 +25,20 @@ "stylelint": "17.15.0", "stylelint-config-standard": "40.0.0", "typescript": "6.0.3", - "typescript-eslint": "8.69.0" + "typescript-eslint": "8.69.0", + "vite": "8.3.0" + }, + "devEngines": { + "runtime": { + "name": "node", + "version": "24.x", + "onFail": "warn" + }, + "packageManager": { + "name": "npm", + "version": ">=11.0.0", + "onFail": "warn" + } } }, "node_modules/@asamuzakjp/css-color": { @@ -1098,6 +1099,16 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, "node_modules/@playwright/test": { "version": "1.62.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", @@ -1114,6 +1125,286 @@ "node": ">=20" } }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -1912,6 +2203,16 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -2773,9 +3074,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -2891,6 +3192,279 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4081,9 +4655,9 @@ } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -4101,7 +4675,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4314,6 +4888,40 @@ "node": ">=0.10.0" } }, + "node_modules/rolldown": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.149.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5082,6 +5690,112 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index 5240d21..4eaf2a0 100644 --- a/package.json +++ b/package.json @@ -99,12 +99,13 @@ "test:browser:webkit": "npm run build && playwright test --project=webkit", "test:examples": "npm run build && npm run test:examples:built", "test:examples:built": "node scripts/run-advanced-smoke.mjs", - "test:tooling": "node --test scripts/tests/build-pages.test.mjs scripts/tests/calendar-workflow-measurement.test.mjs scripts/tests/check-sbom.test.mjs scripts/tests/dynamic-update-measurement.test.mjs scripts/tests/example-metadata.test.mjs scripts/tests/markdown-diagrams.test.mjs scripts/tests/markdown-heading.test.mjs scripts/tests/node-version.test.mjs scripts/tests/ownership.test.mjs scripts/tests/package-entries.test.mjs scripts/tests/packed-extension-verification.test.mjs scripts/tests/publish-alpha-policy.test.mjs scripts/tests/release-artifacts.test.mjs scripts/tests/release-state.test.mjs scripts/tests/screenshot-contract.test.mjs scripts/tests/semver.test.mjs scripts/tests/serve-repository.test.mjs scripts/tests/styles.test.mjs scripts/tests/workflow-contracts.test.mjs", + "test:tooling": "node --test scripts/tests/build-pages.test.mjs scripts/tests/calendar-workflow-measurement.test.mjs scripts/tests/check-sbom.test.mjs scripts/tests/dynamic-update-measurement.test.mjs scripts/tests/example-metadata.test.mjs scripts/tests/markdown-diagrams.test.mjs scripts/tests/markdown-heading.test.mjs scripts/tests/node-version.test.mjs scripts/tests/ownership.test.mjs scripts/tests/package-entries.test.mjs scripts/tests/packed-extension-verification.test.mjs scripts/tests/publish-alpha-policy.test.mjs scripts/tests/release-artifacts.test.mjs scripts/tests/release-state.test.mjs scripts/tests/screenshot-contract.test.mjs scripts/tests/semver.test.mjs scripts/tests/serve-repository.test.mjs scripts/tests/styles.test.mjs scripts/tests/workflow-contracts.test.mjs scripts/tests/browser-targets.test.mjs", "test:unit": "node scripts/run-unit-tests.mjs", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit", "typecheck:examples": "npm run build:package && npm run typecheck:examples:built", "typecheck:examples:built": "tsc -p examples/advanced/tsconfig.json --noEmit", - "check:license": "node scripts/check-license.mjs" + "check:license": "node scripts/check-license.mjs", + "browsers:report": "node scripts/report-browser-targets.mjs" }, "devDependencies": { "@axe-core/playwright": "4.13.0", @@ -123,7 +124,8 @@ "stylelint": "17.15.0", "stylelint-config-standard": "40.0.0", "typescript": "6.0.3", - "typescript-eslint": "8.69.0" + "typescript-eslint": "8.69.0", + "vite": "8.3.0" }, "publishConfig": { "access": "public", diff --git a/scripts/lib/browser-targets.mjs b/scripts/lib/browser-targets.mjs new file mode 100644 index 0000000..6d0640e --- /dev/null +++ b/scripts/lib/browser-targets.mjs @@ -0,0 +1,82 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { mkdir, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; + +import { resolveConfig } from "vite"; + +import { REPOSITORY_ROOT } from "./process.mjs"; + +const require = createRequire(import.meta.url); +export const BROWSER_TARGET_PRESET = "baseline-widely-available"; + +//Use Vite's public resolver, avoiding copied browser versions or private constants. +const presetConfiguration = await resolveConfig({ + configFile: false, + envFile: false, + logLevel: "silent", + root: REPOSITORY_ROOT, + build: { target: BROWSER_TARGET_PRESET } +}, "build"); + +function packageVersion(name) { + let directory = dirname(require.resolve(name)); + for (;;) { + try { + const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8")); + if (manifest.name === name) { return manifest.version; } + } catch (error) { + if (error.code !== "ENOENT") { throw error; } + } + const parent = dirname(directory); + if (parent === directory) { throw new Error(`Cannot resolve ${name} package version.`); } + directory = parent; + } +} + +/** Validates the UTC resolution date retained with one build's target evidence. */ +export function normalizeBrowserTargetDate(date) { + if (typeof date !== "string" || !/^\d{4}-\d{2}-\d{2}$/u.test(date) || + !Number.isFinite(Date.parse(`${date}T00:00:00.000Z`)) || + new Date(`${date}T00:00:00.000Z`).toISOString().slice(0, 10) !== date) { + throw new TypeError("Browser target date must be a valid YYYY-MM-DD UTC date."); + } + return date; +} + +function resolvedTargets(value) { + if (!Array.isArray(value) || value.length === 0 || + value.some((target) => typeof target !== "string" || !/^[a-z]+\d+(?:\.\d+)*$/u.test(target))) { + throw new Error("Vite did not resolve the Baseline preset to explicit browser targets."); + } + return Object.freeze([...value]); +} + +/** Reports the installed Vite preset; the date records resolution, not a moving target. */ +export function resolveBrowserTargets({ + date = process.env.LFC_BROWSER_TARGET_DATE ?? new Date().toISOString().slice(0, 10) +} = {}) { + const resolvedAt = normalizeBrowserTargetDate(date); + const dataVersions = Object.freeze({ vite: packageVersion("vite"), esbuild: packageVersion("esbuild") }); + const javascript = resolvedTargets(presetConfiguration.build.target); + const css = resolvedTargets(presetConfiguration.build.cssTarget); + return Object.freeze({ query: BROWSER_TARGET_PRESET, + effectiveQuery: `Vite ${dataVersions.vite}: ${BROWSER_TARGET_PRESET}`, resolvedAt, + dataVersions, browsers: javascript, javascript, css }); +} + +/** Fingerprints effective compatibility inputs without making evidence stale every midnight. */ +export function browserTargetsFingerprint(targets = resolveBrowserTargets()) { + return createHash("sha256").update(JSON.stringify({ query: targets.query, + dataVersions: targets.dataVersions, browsers: targets.browsers, + javascript: targets.javascript, css: targets.css })).digest("hex"); +} + +/** Records this build's resolution outside the directory reserved for release bundles. */ +export async function writeBrowserTargetReport(targets = resolveBrowserTargets(), + path = join(REPOSITORY_ROOT, ".cache", "browser-targets.json")) { + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, `${JSON.stringify(targets, null, 2)}\n`); + return targets; +} \ No newline at end of file diff --git a/scripts/report-browser-targets.mjs b/scripts/report-browser-targets.mjs new file mode 100644 index 0000000..143ab1f --- /dev/null +++ b/scripts/report-browser-targets.mjs @@ -0,0 +1,8 @@ +import { resolveBrowserTargets, writeBrowserTargetReport } from "./lib/browser-targets.mjs"; + +if (process.argv.length > 2) { + throw new Error("Use LFC_BROWSER_TARGET_DATE=YYYY-MM-DD to reproduce an earlier resolution."); +} +const report = resolveBrowserTargets(); +await writeBrowserTargetReport(report); +console.log(JSON.stringify(report, null, 2)); diff --git a/scripts/tests/browser-targets.test.mjs b/scripts/tests/browser-targets.test.mjs new file mode 100644 index 0000000..43303b3 --- /dev/null +++ b/scripts/tests/browser-targets.test.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { resolveConfig } from "vite"; + +import { BROWSER_TARGET_PRESET, browserTargetsFingerprint, normalizeBrowserTargetDate, + resolveBrowserTargets } from "../lib/browser-targets.mjs"; + +void test("browser target date rejects normalized invalid dates and timestamps", () => { + assert.equal(normalizeBrowserTargetDate("2024-02-29"), "2024-02-29"); + for (const value of ["2025-02-29", "2026-13-01", "2026-09-12T00:00:00Z", "", null]) { + assert.throws(() => normalizeBrowserTargetDate(value), TypeError); + } +}); + +void test("JavaScript and CSS evidence follows Vite's public preset resolution", async () => { + const report = resolveBrowserTargets({ date: "2026-09-12" }); + const config = await resolveConfig({ configFile: false, envFile: false, logLevel: "silent", + build: { target: BROWSER_TARGET_PRESET } }, "build"); + assert.equal(report.query, "baseline-widely-available"); + assert.equal(report.resolvedAt, "2026-09-12"); + assert.deepEqual(report.javascript, config.build.target); + assert.deepEqual(report.css, config.build.cssTarget); + assert.ok(report.browsers.length > 0); + for (const name of ["vite", "esbuild"]) { + assert.match(report.dataVersions[name], /^\d+\.\d+\.\d+$/u); + } + assert.ok(Object.isFrozen(report) && Object.isFrozen(report.javascript)); +}); + +void test("a new resolution date does not change preset targets or screenshot freshness", () => { + const report = resolveBrowserTargets({ date: "2026-09-12" }); + const later = resolveBrowserTargets({ date: "2026-09-13" }); + assert.equal(browserTargetsFingerprint(report), browserTargetsFingerprint(later)); + assert.notEqual(browserTargetsFingerprint(report), browserTargetsFingerprint({ ...report, + javascript: [...report.javascript, "chrome999"] })); +}); \ No newline at end of file diff --git a/src/internal/dom/month-picker.ts b/src/internal/dom/month-picker.ts index b3a5840..cce14cc 100644 --- a/src/internal/dom/month-picker.ts +++ b/src/internal/dom/month-picker.ts @@ -5,7 +5,7 @@ import type { CalendarMonthTitleElements } from "./month-title.js"; /** Stable package-owned elements used to control and render the month-and-year picker. */ export interface CalendarMonthPickerElements extends CalendarMonthTitleElements { - readonly monthPicker: HTMLDivElement; + readonly monthPicker: HTMLDivElement | HTMLDialogElement; readonly monthPickerCancelButton: HTMLButtonElement; readonly monthPickerForm: HTMLFormElement; readonly monthPickerMonth: HTMLSelectElement; @@ -22,7 +22,7 @@ interface CalendarMonthPickerOptions { readonly messages: Readonly; readonly minYear: number; readonly monthNameFormatter: Intl.DateTimeFormat; - readonly onCancel: (this: void, event: MouseEvent) => void; + readonly onCancel: (this: void, event: Event) => void; readonly onBeforeToggle: (this: void, event: Event) => void; readonly onSubmit: (this: void, event: SubmitEvent) => void; readonly onTitleClick: (this: void, event: MouseEvent) => void; @@ -45,7 +45,7 @@ interface CalendarMonthPickerControllerOptions { ) => Readonly | null; } -/** Owns native Popover state, validation, synchronization, and focus restoration. */ +/** Owns native Popover or dialog state, validation, synchronization, and focus restoration. */ export class CalendarMonthPickerController { private isOpen = false; private restoreFocus = false; @@ -74,7 +74,7 @@ export class CalendarMonthPickerController { }, 0); }; - public readonly handleCancel = (event: MouseEvent): void => { + public readonly handleCancel = (event: Event): void => { if (event.defaultPrevented || !this.options.canContinue()) { return; } @@ -125,22 +125,32 @@ export class CalendarMonthPickerController { return; } try { - elements.monthPicker.showPopover(); + if (isDialogPicker(elements.monthPicker)) { + this.sync(elements); + elements.monthPicker.showModal(); + this.handleToggle(); + } else { + elements.monthPicker.showPopover(); + } } catch { - //The supported browser contract provides Popover; a changed host surface remains inert. + //A detached or application-modified surface cannot be opened. + this.finishClose(); } }; public readonly handleToggle = (): void => { const elements = this.options.getElements(); if (elements !== null && this.isPopoverOpen(elements)) { + const wasOpen = this.isOpen; this.isOpen = true; if (!this.options.canContinue()) { this.hide(false); return; } elements.titleButton.setAttribute("aria-expanded", "true"); - this.sync(elements); + if (!wasOpen) { + this.sync(elements); + } this.options.document.addEventListener("keydown", this.handleDocumentKeydown, true); const active = this.options.document.activeElement; if (active === null || active === this.options.document.body || active === elements.titleButton) { @@ -174,10 +184,14 @@ export class CalendarMonthPickerController { this.restoreFocus = this.restoreFocus || restoreFocus; let hideFailed = false; try { - elements.monthPicker.hidePopover(); + if (isDialogPicker(elements.monthPicker)) { + elements.monthPicker.close(); + } else { + elements.monthPicker.hidePopover(); + } } catch { hideFailed = true; - //The supported browser contract provides Popover; teardown remains safe if host state changed first. + //Teardown remains safe if the application changed the surface first. } if (!hideFailed && this.isPopoverOpen(elements)) { this.restoreFocus = false; @@ -233,6 +247,9 @@ export class CalendarMonthPickerController { } private isPopoverOpen(elements: CalendarMonthPickerElements): boolean { + if (isDialogPicker(elements.monthPicker)) { + return elements.monthPicker.open; + } try { return elements.monthPicker.matches(":popover-open"); } catch { @@ -263,7 +280,6 @@ export function createCalendarMonthPicker( titleButton.setAttribute("aria-controls", monthPickerId); titleButton.setAttribute("aria-expanded", "false"); titleButton.setAttribute("aria-haspopup", "dialog"); - titleButton.setAttribute("popovertarget", monthPickerId); titleButton.addEventListener("click", options.onTitleClick); const titleLabel = options.document.createElement("span"); titleLabel.className = "lfc-calendar-title-label"; @@ -279,11 +295,24 @@ export function createCalendarMonthPicker( titleButton.append(titleLabel); title.append(titleButton); - const monthPicker = options.document.createElement("div"); + const supportsPopover = typeof titleButton.showPopover === "function" && + typeof titleButton.hidePopover === "function"; + const monthPicker = options.document.createElement(supportsPopover ? "div" : "dialog"); monthPicker.className = "lfc-calendar-month-picker"; monthPicker.id = monthPickerId; monthPicker.setAttribute("aria-labelledby", `${options.instanceName}-month-picker-title`); - monthPicker.setAttribute("popover", "auto"); + if (supportsPopover) { + monthPicker.setAttribute("popover", "auto"); + titleButton.setAttribute("popovertarget", monthPickerId); + } else if (isDialogPicker(monthPicker)) { + monthPicker.setAttribute("aria-modal", "true"); + if (Reflect.has(monthPicker, "closedBy")) { + monthPicker.setAttribute("closedby", "any"); + } + monthPicker.addEventListener("cancel", options.onCancel); + monthPicker.addEventListener("close", options.onToggle); + installDialogLightDismiss(monthPicker, options.onCancel); + } monthPicker.setAttribute("role", "dialog"); const monthPickerTitle = createHeading(options.document, Math.min(6, options.headingLevel + 1)); monthPickerTitle.className = "lfc-calendar-month-picker-title"; @@ -342,8 +371,10 @@ export function createCalendarMonthPicker( const monthPickerCancelButton = options.document.createElement("button"); monthPickerCancelButton.className = "lfc-calendar-month-picker-cancel"; monthPickerCancelButton.type = "button"; - monthPickerCancelButton.setAttribute("popovertarget", monthPickerId); - monthPickerCancelButton.setAttribute("popovertargetaction", "hide"); + if (supportsPopover) { + monthPickerCancelButton.setAttribute("popovertarget", monthPickerId); + monthPickerCancelButton.setAttribute("popovertargetaction", "hide"); + } monthPickerCancelButton.textContent = options.messages.cancel; monthPickerCancelButton.addEventListener("click", options.onCancel); monthPickerActions.append(jumpButton, monthPickerCancelButton); @@ -371,3 +402,30 @@ export function createCalendarMonthPicker( function createHeading(document: Document, level: number): HTMLHeadingElement { return document.createElement(`h${level.toString()}`) as HTMLHeadingElement; } + +function isDialogPicker(picker: HTMLDivElement | HTMLDialogElement): picker is HTMLDialogElement { + return picker.localName === "dialog"; +} + +function installDialogLightDismiss(picker: HTMLDialogElement, onCancel: (event: Event) => void): void { + if (Reflect.has(picker, "closedBy")) { + return; + } + let pressedBackdrop = false; + const isBackdrop = (event: MouseEvent): boolean => { + if (event.target !== picker) { + return false; + } + const rect = picker.getBoundingClientRect(); + return event.clientX < rect.left || event.clientX > rect.right || + event.clientY < rect.top || event.clientY > rect.bottom; + }; + picker.addEventListener("pointerdown", (event) => { pressedBackdrop = isBackdrop(event); }); + picker.addEventListener("pointercancel", () => { pressedBackdrop = false; }); + picker.addEventListener("click", (event) => { + if (pressedBackdrop && isBackdrop(event)) { + onCancel(event); + } + pressedBackdrop = false; + }); +} diff --git a/src/internal/dom/palette.ts b/src/internal/dom/palette.ts new file mode 100644 index 0000000..f640894 --- /dev/null +++ b/src/internal/dom/palette.ts @@ -0,0 +1,73 @@ +import type { HostWindow } from "./environment.js"; + +/** Mirrors the effective host color scheme only when native light-dark() is unavailable. */ +export class CalendarPalette { + private readonly preference: MediaQueryList | null; + private readonly observer: MutationObserver | null; + private readonly connectionObserver: MutationObserver | null; + private disconnected = false; + + public constructor(private readonly host: HTMLElement, private readonly window: HostWindow | null) { + const needsFallback = typeof window?.CSS === "object" && !window.CSS.supports("color", "light-dark(white, black)"); + this.preference = needsFallback ? window.matchMedia("(prefers-color-scheme: dark)") : null; + this.observer = needsFallback ? new window.MutationObserver(this.handleMutation) : null; + this.connectionObserver = needsFallback ? new window.MutationObserver(this.handleConnection) : null; + if (needsFallback) { + this.observeAncestors(); + this.preference?.addEventListener("change", this.sync); + host.ownerDocument.addEventListener("load", this.sync, true); + this.sync(); + } + } + + /** Releases listeners and removes only package-owned fallback state. */ + public disconnect(): void { + this.disconnected = true; + this.observer?.disconnect(); + this.connectionObserver?.disconnect(); + this.preference?.removeEventListener("change", this.sync); + this.host.ownerDocument.removeEventListener("load", this.sync, true); + this.host.classList.remove("lfc-palette-light", "lfc-palette-dark"); + } + + private observeAncestors(): void { + this.observer?.disconnect(); + this.connectionObserver?.disconnect(); + for (let ancestor: HTMLElement | null = this.host; ancestor !== null; ancestor = ancestor.parentElement) { + //Attribute changes can select application theme rules; child-list changes can reparent the host. + this.observer?.observe(ancestor, { attributes: true, childList: true }); + } + if (!this.host.isConnected) { + //A document-wide observation lasts only until a detached host is attached again. + this.connectionObserver?.observe(this.host.ownerDocument, { childList: true, subtree: true }); + } + } + + private readonly handleMutation = (): void => { + if (this.disconnected) { + return; + } + this.observeAncestors(); + this.sync(); + }; + + private readonly handleConnection = (): void => { + if (this.host.isConnected) { + this.handleMutation(); + } + }; + + private readonly sync = (): void => { + if (this.disconnected || this.window === null) { + return; + } + const schemes = this.window.getComputedStyle(this.host).colorScheme.split(/\s+/u); + const dark = schemes.includes("dark") && + (!schemes.includes("light") || this.preference?.matches === true); + for (const [name, enabled] of [["lfc-palette-dark", dark], ["lfc-palette-light", !dark]] as const) { + if (this.host.classList.contains(name) !== enabled) { + this.host.classList.toggle(name, enabled); + } + } + }; +} diff --git a/src/internal/dom/structure.ts b/src/internal/dom/structure.ts index 9006d95..b7da8ed 100644 --- a/src/internal/dom/structure.ts +++ b/src/internal/dom/structure.ts @@ -53,7 +53,7 @@ interface CalendarStructureOptions { readonly minYear: number; readonly monthNameFormatter: Intl.DateTimeFormat; readonly onMonthPickerBeforeToggle: (this: void, event: Event) => void; - readonly onMonthPickerCancel: (this: void, event: MouseEvent) => void; + readonly onMonthPickerCancel: (this: void, event: Event) => void; readonly onMonthPickerSubmit: (this: void, event: SubmitEvent) => void; readonly onMonthPickerTitleClick: (this: void, event: MouseEvent) => void; readonly onMonthPickerToggle: (this: void, event: Event) => void; diff --git a/src/internal/runtime/coordinator.ts b/src/internal/runtime/coordinator.ts index 69ef289..9c38914 100644 --- a/src/internal/runtime/coordinator.ts +++ b/src/internal/runtime/coordinator.ts @@ -34,6 +34,7 @@ import { CalendarAnnouncementPresenter } from "../dom/announcement.js"; import { presentCalendarIssue } from "../dom/issue-region.js"; import { createCalendarStructure, type CalendarDom } from "../dom/structure.js"; import { CalendarMonthPickerController } from "../dom/month-picker.js"; +import { CalendarPalette } from "../dom/palette.js"; import { CalendarMonthTitleRenderer } from "../dom/month-title.js"; import { createAgendaPresentation, type AgendaEventEntry } from "../dom/agenda.js"; import { installEventActionListeners as installNativeEventActionListeners } from "../dom/event-structure.js"; @@ -176,6 +177,7 @@ export class MonthCalendar implements Calendar { private dayButtons = new Map(); private displayedMonth: CalendarDate; private dom: CalendarDom | null = null; + private palette: CalendarPalette | null = null; private eventReplacementSequence = 0; private focusedDate: CalendarDate; private generation = 0; @@ -455,6 +457,7 @@ export class MonthCalendar implements Calendar { try { this.host.classList.add(ROOT_CLASS); + this.palette = new CalendarPalette(this.host, this.window); this.host.setAttribute("data-litefold-calendar", ""); if (this.swipeEnabled) { this.host.setAttribute("data-lfc-swipe-enabled", "true"); @@ -482,6 +485,8 @@ export class MonthCalendar implements Calendar { this.isDestroyed = true; this.isRendered = false; this.monthPickerController.hide(false); + this.palette?.disconnect(); + this.palette = null; this.generation += 1; this.registeredExtensions?.stop(); this.actionGenerations.clear(); diff --git a/src/styles/agenda.css b/src/styles/agenda.css index 23c5ead..ad5cc55 100644 --- a/src/styles/agenda.css +++ b/src/styles/agenda.css @@ -57,6 +57,7 @@ } :where(.litefold-calendar) .lfc-calendar-agenda-event { + align-items: center; align-items: safe center; background: var(--lfc-event-background); border: var(--lfc-internal-hairline) solid var(--lfc-event-border-color); diff --git a/src/styles/base.css b/src/styles/base.css index 2b1e2c9..8f280d0 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -29,6 +29,7 @@ .lfc-calendar-month-picker-jump, .lfc-calendar-month-picker-cancel ) { + align-items: center; align-items: safe center; appearance: none; background: var(--lfc-surface-background); @@ -36,6 +37,7 @@ border-radius: var(--lfc-border-radius); cursor: pointer; display: inline-flex; + justify-content: center; justify-content: safe center; max-inline-size: 100%; min-block-size: max(2.75rem, var(--lfc-control-min-size)); diff --git a/src/styles/month-grid.css b/src/styles/month-grid.css index ba79630..1a1a49a 100644 --- a/src/styles/month-grid.css +++ b/src/styles/month-grid.css @@ -55,12 +55,24 @@ :where(.litefold-calendar) .lfc-calendar-weeks[data-lfc-grid-event-placement="center"] { - --lfc-internal-grid-event-block-alignment: safe center; + --lfc-internal-grid-event-block-alignment: center; } :where(.litefold-calendar) .lfc-calendar-weeks[data-lfc-grid-event-placement="bottom"] { - --lfc-internal-grid-event-block-alignment: safe end; + --lfc-internal-grid-event-block-alignment: end; + } + + @supports (justify-content: safe center) { + :where(.litefold-calendar) + .lfc-calendar-weeks[data-lfc-grid-event-placement="center"] { + --lfc-internal-grid-event-block-alignment: safe center; + } + + :where(.litefold-calendar) + .lfc-calendar-weeks[data-lfc-grid-event-placement="bottom"] { + --lfc-internal-grid-event-block-alignment: safe end; + } } :where(.litefold-calendar) .lfc-calendar-week { @@ -216,6 +228,7 @@ } :where(.litefold-calendar) .lfc-calendar-event-overflow { + align-items: center; align-items: safe center; background: transparent; border: 0; @@ -245,8 +258,10 @@ } :where(.litefold-calendar) .lfc-calendar-event-overflow-content { + align-items: center; align-items: safe center; display: inline-flex; + justify-content: center; justify-content: safe center; min-inline-size: 0; } @@ -258,6 +273,7 @@ } :where(.litefold-calendar) .lfc-calendar-event-summary { + align-items: center; align-items: safe center; background: var(--lfc-event-background); border: var(--lfc-internal-hairline) solid var(--lfc-event-border-color); diff --git a/src/styles/preferences.css b/src/styles/preferences.css index de71172..b7b376a 100644 --- a/src/styles/preferences.css +++ b/src/styles/preferences.css @@ -94,32 +94,6 @@ transition-property: none; } - @media (prefers-color-scheme: dark) { - :where(.litefold-calendar) { - --lfc-color: #eaf7f7; - --lfc-muted-color: #a8c8cc; - --lfc-background: #061a23; - --lfc-surface-background: #0c2a34; - --lfc-border-color: #6e929a; - --lfc-accent-color: #62e3a8; - --lfc-accent-contrast-color: #062a20; - --lfc-focus-ring-color: #c0b6ff; - --lfc-selected-background: #12483f; - --lfc-selected-color: #d5fff1; - --lfc-today-border-color: #062a20; - --lfc-event-background: #282853; - --lfc-event-color: #f1eeff; - --lfc-event-border-color: #a39aff; - --lfc-event-accent-color: #ff88c8; - --lfc-warning-background: #431407; - --lfc-warning-color: #ffedd5; - --lfc-warning-border-color: #fdba74; - --lfc-error-background: #450a0a; - --lfc-error-color: #fee2e2; - --lfc-error-border-color: #fca5a5; - } - } - @media (prefers-contrast: more) { :where(.litefold-calendar) { --lfc-border-color: currentcolor; diff --git a/src/styles/responsive.css b/src/styles/responsive.css index bf68c90..89bab77 100644 --- a/src/styles/responsive.css +++ b/src/styles/responsive.css @@ -30,6 +30,7 @@ :where(.litefold-calendar) .lfc-calendar-title-button { inline-size: 100%; + justify-content: center; justify-content: safe center; padding-inline: 0; } @@ -178,6 +179,7 @@ :where(.litefold-calendar) .lfc-calendar-event-overflow-cluster > .lfc-calendar-event-overflow.lfc-is-compact { + justify-content: center; justify-content: safe center; } @@ -269,9 +271,11 @@ } :where(.litefold-calendar) .lfc-calendar-grid-more.lfc-is-compact-primary { + align-items: center; align-items: safe center; display: inline-flex; inline-size: var(--lfc-internal-compact-control-inline-size); + justify-content: center; justify-content: safe center; min-block-size: max(1.5rem, var(--lfc-grid-event-min-block-size)); min-inline-size: min(100%, 1.5rem); diff --git a/src/styles/tokens.css b/src/styles/tokens.css index a5ee016..78a0ade 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -13,27 +13,27 @@ --lfc-grid-event-gap: 0.125rem; --lfc-grid-event-font-size: 0.75em; --lfc-grid-event-min-block-size: 1.5rem; - --lfc-color: #102a36; - --lfc-muted-color: #405f68; - --lfc-background: #f7fcfc; - --lfc-surface-background: #e9f4f5; - --lfc-border-color: #56737b; - --lfc-accent-color: #047857; - --lfc-accent-contrast-color: #f4fffb; - --lfc-focus-ring-color: #4e46c7; - --lfc-selected-background: #d4f4e5; - --lfc-selected-color: #0a3b32; - --lfc-today-border-color: #071e26; - --lfc-event-background: #efeeff; - --lfc-event-color: #262050; - --lfc-event-border-color: #665cc3; - --lfc-event-accent-color: #a52a78; - --lfc-warning-background: #fff7ed; - --lfc-warning-color: #7c2d12; - --lfc-warning-border-color: #c2410c; - --lfc-error-background: #fef2f2; - --lfc-error-color: #7f1d1d; - --lfc-error-border-color: #b91c1c; + --lfc-color: light-dark(#102a36, #eaf7f7); + --lfc-muted-color: light-dark(#405f68, #a8c8cc); + --lfc-background: light-dark(#f7fcfc, #061a23); + --lfc-surface-background: light-dark(#e9f4f5, #0c2a34); + --lfc-border-color: light-dark(#56737b, #6e929a); + --lfc-accent-color: light-dark(#047857, #62e3a8); + --lfc-accent-contrast-color: light-dark(#f4fffb, #062a20); + --lfc-focus-ring-color: light-dark(#4e46c7, #c0b6ff); + --lfc-selected-background: light-dark(#d4f4e5, #12483f); + --lfc-selected-color: light-dark(#0a3b32, #d5fff1); + --lfc-today-border-color: light-dark(#071e26, #062a20); + --lfc-event-background: light-dark(#efeeff, #282853); + --lfc-event-color: light-dark(#262050, #f1eeff); + --lfc-event-border-color: light-dark(#665cc3, #a39aff); + --lfc-event-accent-color: light-dark(#a52a78, #ff88c8); + --lfc-warning-background: light-dark(#fff7ed, #431407); + --lfc-warning-color: light-dark(#7c2d12, #ffedd5); + --lfc-warning-border-color: light-dark(#c2410c, #fdba74); + --lfc-error-background: light-dark(#fef2f2, #450a0a); + --lfc-error-color: light-dark(#7f1d1d, #fee2e2); + --lfc-error-border-color: light-dark(#b91c1c, #fca5a5); /* stylelint-disable-next-line unit-disallowed-list -- Design-documented one-CSS-pixel hairline. */ --lfc-internal-hairline: 1px; /* stylelint-disable-next-line unit-disallowed-list -- Canonical visually-hidden sentinel. */ @@ -84,4 +84,50 @@ overflow: clip; text-align: start; } + + /* Baseline engines without light-dark() resolve the same palette from the host color scheme. */ + @supports not (color: light-dark(white, black)) { + :where(.litefold-calendar) { + --lfc-internal-light: initial; + --lfc-internal-dark: ; + --lfc-color: var(--lfc-internal-light, #102a36) var(--lfc-internal-dark, #eaf7f7); + --lfc-muted-color: var(--lfc-internal-light, #405f68) var(--lfc-internal-dark, #a8c8cc); + --lfc-background: var(--lfc-internal-light, #f7fcfc) var(--lfc-internal-dark, #061a23); + --lfc-surface-background: var(--lfc-internal-light, #e9f4f5) var(--lfc-internal-dark, #0c2a34); + --lfc-border-color: var(--lfc-internal-light, #56737b) var(--lfc-internal-dark, #6e929a); + --lfc-accent-color: var(--lfc-internal-light, #047857) var(--lfc-internal-dark, #62e3a8); + --lfc-accent-contrast-color: var(--lfc-internal-light, #f4fffb) var(--lfc-internal-dark, #062a20); + --lfc-focus-ring-color: var(--lfc-internal-light, #4e46c7) var(--lfc-internal-dark, #c0b6ff); + --lfc-selected-background: var(--lfc-internal-light, #d4f4e5) var(--lfc-internal-dark, #12483f); + --lfc-selected-color: var(--lfc-internal-light, #0a3b32) var(--lfc-internal-dark, #d5fff1); + --lfc-today-border-color: var(--lfc-internal-light, #071e26) var(--lfc-internal-dark, #062a20); + --lfc-event-background: var(--lfc-internal-light, #efeeff) var(--lfc-internal-dark, #282853); + --lfc-event-color: var(--lfc-internal-light, #262050) var(--lfc-internal-dark, #f1eeff); + --lfc-event-border-color: var(--lfc-internal-light, #665cc3) var(--lfc-internal-dark, #a39aff); + --lfc-event-accent-color: var(--lfc-internal-light, #a52a78) var(--lfc-internal-dark, #ff88c8); + --lfc-warning-background: var(--lfc-internal-light, #fff7ed) var(--lfc-internal-dark, #431407); + --lfc-warning-color: var(--lfc-internal-light, #7c2d12) var(--lfc-internal-dark, #ffedd5); + --lfc-warning-border-color: var(--lfc-internal-light, #c2410c) var(--lfc-internal-dark, #fdba74); + --lfc-error-background: var(--lfc-internal-light, #fef2f2) var(--lfc-internal-dark, #450a0a); + --lfc-error-color: var(--lfc-internal-light, #7f1d1d) var(--lfc-internal-dark, #fee2e2); + --lfc-error-border-color: var(--lfc-internal-light, #b91c1c) var(--lfc-internal-dark, #fca5a5); + } + + @media (prefers-color-scheme: dark) { + :where(.litefold-calendar) { + --lfc-internal-light: ; + --lfc-internal-dark: initial; + } + } + + :where(.litefold-calendar):where(.lfc-palette-light) { + --lfc-internal-light: initial; + --lfc-internal-dark: ; + } + + :where(.litefold-calendar):where(.lfc-palette-dark) { + --lfc-internal-light: ; + --lfc-internal-dark: initial; + } + } } diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index 956479a..b463633 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -1,5 +1,6 @@ @layer lfc { :where(.litefold-calendar) .lfc-calendar-toolbar { + align-items: center; align-items: safe center; background: var(--lfc-background); border-block-end: var(--lfc-internal-hairline) solid var(--lfc-border-color); @@ -11,6 +12,7 @@ } :where(.litefold-calendar) .lfc-calendar-navigation { + align-items: center; align-items: safe center; display: grid; flex: 1 1 32rem; @@ -21,6 +23,7 @@ } :where(.litefold-calendar) .lfc-calendar-month-stepper { + align-items: center; align-items: safe center; display: flex; gap: 0.125rem; @@ -40,6 +43,7 @@ } :where(.litefold-calendar) .lfc-calendar-title-button { + align-items: center; align-items: safe center; appearance: none; background: transparent; @@ -113,6 +117,11 @@ gap: var(--lfc-gap); } + :where(.litefold-calendar) dialog.lfc-calendar-month-picker[open] { + display: grid; + gap: var(--lfc-gap); + } + :where(.litefold-calendar) .lfc-calendar-month-picker::backdrop { background: color-mix(in oklab, var(--lfc-color) 20%, transparent); } @@ -158,6 +167,7 @@ } :where(.litefold-calendar) .lfc-calendar-month-picker-actions { + align-items: center; align-items: safe center; display: flex; flex-wrap: wrap; @@ -177,6 +187,7 @@ } :where(.litefold-calendar) .lfc-calendar-toolbar-end { + align-items: center; align-items: safe center; display: flex; flex: 0 1 auto; @@ -279,6 +290,7 @@ } :where(.litefold-calendar) .lfc-calendar-status-actions { + align-items: center; align-items: safe center; display: flex; flex-wrap: wrap; diff --git a/tests/e2e/baseline-fallbacks.js b/tests/e2e/baseline-fallbacks.js new file mode 100644 index 0000000..6f64a06 --- /dev/null +++ b/tests/e2e/baseline-fallbacks.js @@ -0,0 +1,19 @@ +/** Exercises only the shipped non-Baseline enhancements' fallback paths. */ +export async function useBaselineFallbacks(page) { + await page.addInitScript(() => { + for (const method of ["showPopover", "hidePopover", "togglePopover"]) { + Object.defineProperty(HTMLElement.prototype, method, { configurable: true, value: undefined }); + } + Reflect.deleteProperty(HTMLDialogElement.prototype, "closedBy"); + const supports = CSS.supports.bind(CSS); + CSS.supports = (...arguments_) => arguments_.some((value) => value.includes("light-dark(")) + ? false : supports(...arguments_); + }); + await page.route("**/*.css", async (route) => { + const response = await route.fetch(); + const body = (await response.text()) + .replaceAll("light-dark(", "lfc-unsupported-color(") + .replaceAll(":popover-open", ":lfc-unsupported-popover"); + await route.fulfill({ response, body }); + }); +} diff --git a/tests/e2e/browser-baseline.spec.js b/tests/e2e/browser-baseline.spec.js new file mode 100644 index 0000000..765ec29 --- /dev/null +++ b/tests/e2e/browser-baseline.spec.js @@ -0,0 +1,80 @@ +import { expect, test } from "@playwright/test"; + +import { useBaselineFallbacks } from "./baseline-fallbacks.js"; +import { expectLibraryFixtureReady } from "./helpers.js"; + +test.beforeEach(async ({ page }) => { + await useBaselineFallbacks(page); + await expectLibraryFixtureReady(page); + await page.evaluate(async () => { + const { createCalendar } = await import("/dist/index.js"); + const calendar = createCalendar(document.querySelector("[data-my-calendar]"), { + events: [], + initialDate: "2026-08-31", + maxDate: "2027-02-28", + minDate: "2026-02-01" + }); + calendar.render(); + window.__lfcBaseline = calendar; + }); +}); + +test("dialog fallback validates navigation and restores focus after every dismissal", async ({ page }) => { + const host = page.locator("[data-my-calendar]"); + const title = host.locator(".lfc-calendar-title-button"); + const picker = host.getByRole("dialog", { includeHidden: true }); + await expect(picker).toBeHidden(); + await title.click(); + await expect(picker).toBeVisible(); + await expect(title).toHaveAttribute("aria-expanded", "true"); + const month = picker.getByRole("combobox", { name: "Month", exact: true }); + const year = picker.getByLabel("Year", { exact: true }); + await expect(month).toBeFocused(); + await title.evaluate((element) => { element.focus(); }); + await expect(title).not.toBeFocused(); + await year.fill("2028"); + await picker.locator("button[type=submit]").click(); + await expect(picker).toBeVisible(); + await year.fill("2026"); + await month.selectOption("9"); + await picker.locator("button[type=submit]").click(); + await expect(picker).toBeHidden(); + await expect(title).toBeFocused(); + await expect(title).toContainText("September"); + await expect(host.locator('[role="gridcell"][aria-selected="true"] > button')) + .toHaveAttribute("data-lfc-date", "2026-09-30"); + for (const dismissal of ["Escape", "Cancel", "backdrop"]) { + await title.click(); + await month.selectOption("11"); + if (dismissal === "Escape") { + await page.keyboard.press("Escape"); + } else if (dismissal === "Cancel") { + await picker.getByRole("button", { name: "Cancel", exact: true }).click(); + } else { + await page.mouse.click(1, 1); + } + await expect(picker).toBeHidden(); + await expect(title).toHaveAttribute("aria-expanded", "false"); + await expect(title).toBeFocused(); + await expect(title).toContainText("September"); + } +}); + +test("destroy closes the fallback modal and releases the document for reused host content", async ({ page }) => { + const host = page.locator("[data-my-calendar]"); + await host.locator(".lfc-calendar-title-button").click(); + await expect(host.getByRole("dialog")).toBeVisible(); + await page.evaluate(() => window.__lfcBaseline.destroy()); + await expect(page.locator("dialog[open]")).toHaveCount(0); + await host.evaluate((element) => { + const button = document.createElement("button"); + button.textContent = "Reused host"; + element.replaceChildren(button); + }); + const replacement = host.getByRole("button", { name: "Reused host" }); + await replacement.focus(); + await expect(replacement).toBeFocused(); + await page.emulateMedia({ colorScheme: "dark" }); + await expect(replacement).toBeFocused(); + await expect(host).not.toHaveClass(/lfc-palette/u); +}); diff --git a/tests/e2e/theme-integration.spec.js b/tests/e2e/theme-integration.spec.js new file mode 100644 index 0000000..d380463 --- /dev/null +++ b/tests/e2e/theme-integration.spec.js @@ -0,0 +1,153 @@ +import { expect, test } from "@playwright/test"; + +import { useBaselineFallbacks } from "./baseline-fallbacks.js"; +import { expectLibraryFixtureReady } from "./helpers.js"; + +test.use({ bypassCSP: true }); + +async function mountThemeFixture(page, detached = false) { + await expectLibraryFixtureReady(page); + await page.addStyleTag({ content: ` + @layer lfc, my.application; + @layer my.application { + .my-light { color-scheme: light; } + .my-dark { color-scheme: dark; } + .my-inherit { color-scheme: inherit; } + body[data-my-theme="dark"] { color-scheme: dark; } + body[data-my-theme="light"] { color-scheme: light; } + } + ` }); + await page.evaluate(async (deferMount) => { + const { createCalendar } = await import("/dist/index.js"); + const host = document.querySelector("[data-my-calendar]"); + if (deferMount) { host.remove(); } + const observations = { requests: 0 }; + const calendar = createCalendar(host, { + initialDate: "2026-08-06", + events: () => { + observations.requests += 1; + return [{ id: "theme-event", start: "2026-08-06T09:00", title: "Theme review" }]; + } + }); + calendar.render(); + window.__lfcTheme = { calendar, host, observations }; + }, detached); +} + +async function expectPalette(host, scheme) { + const dark = scheme === "dark"; + const background = dark ? "rgb(6, 26, 35)" : "rgb(247, 252, 252)"; + await expect(host).toHaveCSS("background-color", background); + await expect(host).toHaveCSS("color", dark ? "rgb(234, 247, 247)" : "rgb(16, 42, 54)"); + await expect(host.locator(".lfc-calendar-agenda")).toHaveCSS("background-color", + dark ? "rgb(12, 42, 52)" : "rgb(233, 244, 245)"); + await expect(host.locator(".lfc-calendar-agenda-event")).toHaveCSS("background-color", + dark ? "rgb(40, 40, 83)" : "rgb(239, 238, 255)"); + await expect(host.locator(".lfc-calendar-month-picker")).toHaveCSS("background-color", background); +} + +for (const fallback of [false, true]) { + test.describe(fallback ? "Baseline palette" : "native palette", () => { + test.beforeEach(async ({ page }) => { + if (fallback) { await useBaselineFallbacks(page); } + }); + + test("system and host schemes preserve focused picker state and resume automatic mode", async ({ page }) => { + await mountThemeFixture(page); + const host = page.locator("[data-my-calendar]"); + await host.locator(".lfc-calendar-title-button").click(); + const year = host.getByLabel("Year", { exact: true }); + await year.fill("2027"); + for (const scheme of ["dark", "light", "dark"]) { + await page.emulateMedia({ colorScheme: scheme }); + await expectPalette(host, scheme); + const forced = scheme === "dark" ? "light" : "dark"; + await host.evaluate((element, value) => element.classList.add(`my-${value}`), forced); + await expectPalette(host, forced); + await host.evaluate((element, value) => element.classList.remove(`my-${value}`), forced); + await expectPalette(host, scheme); + await expect(year).toBeFocused(); + await expect(year).toHaveValue("2027"); + } + expect(await page.evaluate(() => window.__lfcTheme.observations.requests)).toBe(1); + }); + + test("inherited ancestor themes preserve picker fields and application token overrides", async ({ page }) => { + await mountThemeFixture(page); + const host = page.locator("[data-my-calendar]"); + await host.evaluate((element) => element.classList.add("my-inherit")); + await host.locator(".lfc-calendar-title-button").click(); + const year = host.getByLabel("Year", { exact: true }); + await year.fill("2027"); + for (const scheme of ["dark", "light", "dark"]) { + await page.locator("body").evaluate((element, value) => { element.dataset.myTheme = value; }, scheme); + await expectPalette(host, scheme); + await expect(year).toBeFocused(); + await expect(year).toHaveValue("2027"); + } + await page.addStyleTag({ content: "@layer my.application { [data-my-calendar] { --lfc-background: #ffffff; } }" }); + await expect(host).toHaveCSS("background-color", "rgb(255, 255, 255)"); + await expect(year).toBeFocused(); + expect(await page.evaluate(() => window.__lfcTheme.observations.requests)).toBe(1); + }); + + test("detached mounting and reparenting pick up the current inherited theme", async ({ page }) => { + await mountThemeFixture(page, true); + await page.evaluate(() => { + const { host } = window.__lfcTheme; + host.classList.add("my-inherit"); + document.body.dataset.myTheme = "dark"; + document.querySelector("main").append(host); + }); + const host = page.locator("[data-my-calendar]"); + await expectPalette(host, "dark"); + await page.evaluate(() => { + const parent = document.createElement("section"); + parent.className = "my-light"; + document.body.append(parent); + parent.append(window.__lfcTheme.host); + }); + await expectPalette(host, "light"); + await page.evaluate(() => window.__lfcTheme.calendar.destroy()); + await expect(host).not.toHaveClass(/lfc-palette/u); + }); + + test("example palettes support explicit and automatic themes", async ({ page }) => { + await mountThemeFixture(page); + await page.addStyleTag({ url: "/examples/example.css" }); + await page.addStyleTag({ url: "/examples/advanced/theme.css" }); + const host = page.locator("[data-my-calendar]"); + await host.evaluate((element) => element.classList.add("my-calendar")); + for (const system of ["dark", "light"]) { + await page.emulateMedia({ colorScheme: system }); + for (const theme of ["dark", "light", "auto"]) { + await page.locator("html").evaluate((element, value) => { element.dataset.myTheme = value; }, theme); + const effective = theme === "auto" ? system : theme; + await expectPalette(host, effective); + await expect(page.locator("body")).toHaveCSS("background-color", + effective === "dark" ? "rgb(3, 16, 23)" : "rgb(237, 245, 245)"); + const appointmentColor = await page.locator("html").evaluate((element) => { + const probe = document.createElement("span"); + probe.style.color = "var(--my-appointment-color)"; + element.append(probe); + const color = getComputedStyle(probe).color; + probe.remove(); + return color; + }); + expect(appointmentColor).toBe(effective === "dark" ? "rgb(77, 224, 181)" : "rgb(0, 118, 108)"); + } + } + }); + + test("increased contrast retains semantic foreground and border roles", async ({ page }) => { + await page.emulateMedia({ colorScheme: "dark", contrast: "more" }); + await mountThemeFixture(page); + test.skip(!await page.evaluate(() => matchMedia("(prefers-contrast: more)").matches), + "This browser build cannot emulate increased contrast."); + const host = page.locator("[data-my-calendar]"); + const color = await host.evaluate((element) => getComputedStyle(element).color); + await expect(host).toHaveCSS("border-top-color", color); + await expect(host.locator(".lfc-calendar-agenda-event .lfc-calendar-time")).toHaveCSS("color", color); + }); + }); +} From 8492c57b073295f889e2392e9d27d62eb0f50ae8 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:37:49 -0700 Subject: [PATCH 04/22] build: adopt Vite library mode and example development --- docs/dynamic-event-updates-adr.md | 2 +- package.json | 5 +- scripts/build-package.mjs | 11 +- scripts/check-package-policy.mjs | 175 ++------------- scripts/lib/calendar-workflow-measurement.mjs | 6 +- scripts/lib/distribution-provenance.mjs | 35 +++ scripts/lib/dynamic-update-measurement.mjs | 10 +- scripts/lib/package-bundle-policy.mjs | 201 ++++++++++++++++++ scripts/lib/packed-browser-verification.mjs | 43 ++++ scripts/lib/packed-extension-verification.mjs | 73 ++++--- scripts/lib/styles.mjs | 8 +- scripts/measure-calendar-workflows.mjs | 7 +- scripts/measure-dynamic-event-updates.mjs | 22 +- .../calendar-workflow-measurement.test.mjs | 11 +- .../tests/dynamic-update-measurement.test.mjs | 16 +- scripts/tests/package-bundle-policy.test.mjs | 125 +++++++++++ .../packed-extension-verification.test.mjs | 45 +++- scripts/tests/vite-build.test.mjs | 68 ++++++ tsconfig.build.json | 3 +- vite.config.mjs | 37 ++++ vite.examples.config.mjs | 71 +++++++ 21 files changed, 744 insertions(+), 230 deletions(-) create mode 100644 scripts/lib/distribution-provenance.mjs create mode 100644 scripts/lib/package-bundle-policy.mjs create mode 100644 scripts/tests/package-bundle-policy.test.mjs create mode 100644 scripts/tests/vite-build.test.mjs create mode 100644 vite.config.mjs create mode 100644 vite.examples.config.mjs diff --git a/docs/dynamic-event-updates-adr.md b/docs/dynamic-event-updates-adr.md index 17c9183..ca530c0 100644 --- a/docs/dynamic-event-updates-adr.md +++ b/docs/dynamic-event-updates-adr.md @@ -123,7 +123,7 @@ npm run build:package node scripts/measure-calendar-workflows.mjs --json ``` -The report also includes raw and maximum-compression gzip byte counts for `dist/index.js`, `dist/internal/runtime/coordinator.js`, and every JavaScript module beneath `dist/`. Preserve a complete prior `dist/` directory outside the candidate build, then request baseline deltas: +The report also includes raw and maximum-compression gzip byte counts for `dist/index.js`, its complete static import graph, and every JavaScript module beneath `dist/`. Graph totals compress each emitted module separately, including Vite's private shared chunks. Preserve a complete prior `dist/` directory outside the candidate build, then request baseline deltas: ```sh npm run measure:dynamic-events -- --baseline .cache/dynamic-update-baseline/dist diff --git a/package.json b/package.json index 4eaf2a0..f100a6a 100644 --- a/package.json +++ b/package.json @@ -99,13 +99,14 @@ "test:browser:webkit": "npm run build && playwright test --project=webkit", "test:examples": "npm run build && npm run test:examples:built", "test:examples:built": "node scripts/run-advanced-smoke.mjs", - "test:tooling": "node --test scripts/tests/build-pages.test.mjs scripts/tests/calendar-workflow-measurement.test.mjs scripts/tests/check-sbom.test.mjs scripts/tests/dynamic-update-measurement.test.mjs scripts/tests/example-metadata.test.mjs scripts/tests/markdown-diagrams.test.mjs scripts/tests/markdown-heading.test.mjs scripts/tests/node-version.test.mjs scripts/tests/ownership.test.mjs scripts/tests/package-entries.test.mjs scripts/tests/packed-extension-verification.test.mjs scripts/tests/publish-alpha-policy.test.mjs scripts/tests/release-artifacts.test.mjs scripts/tests/release-state.test.mjs scripts/tests/screenshot-contract.test.mjs scripts/tests/semver.test.mjs scripts/tests/serve-repository.test.mjs scripts/tests/styles.test.mjs scripts/tests/workflow-contracts.test.mjs scripts/tests/browser-targets.test.mjs", + "test:tooling": "node --test scripts/tests/build-pages.test.mjs scripts/tests/calendar-workflow-measurement.test.mjs scripts/tests/check-sbom.test.mjs scripts/tests/dynamic-update-measurement.test.mjs scripts/tests/example-metadata.test.mjs scripts/tests/markdown-diagrams.test.mjs scripts/tests/markdown-heading.test.mjs scripts/tests/node-version.test.mjs scripts/tests/ownership.test.mjs scripts/tests/package-entries.test.mjs scripts/tests/packed-extension-verification.test.mjs scripts/tests/publish-alpha-policy.test.mjs scripts/tests/release-artifacts.test.mjs scripts/tests/release-state.test.mjs scripts/tests/screenshot-contract.test.mjs scripts/tests/semver.test.mjs scripts/tests/serve-repository.test.mjs scripts/tests/styles.test.mjs scripts/tests/workflow-contracts.test.mjs scripts/tests/browser-targets.test.mjs scripts/tests/vite-build.test.mjs scripts/tests/package-bundle-policy.test.mjs", "test:unit": "node scripts/run-unit-tests.mjs", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit", "typecheck:examples": "npm run build:package && npm run typecheck:examples:built", "typecheck:examples:built": "tsc -p examples/advanced/tsconfig.json --noEmit", "check:license": "node scripts/check-license.mjs", - "browsers:report": "node scripts/report-browser-targets.mjs" + "browsers:report": "node scripts/report-browser-targets.mjs", + "dev": "vite --config vite.examples.config.mjs" }, "devDependencies": { "@axe-core/playwright": "4.13.0", diff --git a/scripts/build-package.mjs b/scripts/build-package.mjs index 2b5e2b3..7123081 100644 --- a/scripts/build-package.mjs +++ b/scripts/build-package.mjs @@ -1,6 +1,10 @@ import { access, copyFile, mkdir, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; +import { build } from "vite"; + +import { createLibraryConfig } from "../vite.config.mjs"; +import { resolveBrowserTargets, writeBrowserTargetReport } from "./lib/browser-targets.mjs"; import { REPOSITORY_ROOT, runTsc } from "./lib/process.mjs"; import { extractExtensionEntries, readPackageManifest } from "./lib/package-entries.mjs"; import { composeDistributedStyles } from "./lib/styles.mjs"; @@ -9,14 +13,17 @@ const DIST_DIRECTORY = join(REPOSITORY_ROOT, "dist"); const SOURCE_STYLE_TYPES = join(REPOSITORY_ROOT, "src", "styles.css.d.ts"); const extensionEntries = extractExtensionEntries(await readPackageManifest()); -const packageStyles = await composeDistributedStyles(); +const targets = resolveBrowserTargets(); +const packageStyles = await composeDistributedStyles({ targets: targets.css }); await rm(DIST_DIRECTORY, { force: true, recursive: true }); +await build(await createLibraryConfig(targets)); await runTsc(["-p", "tsconfig.build.json", "--pretty", "false"]); await mkdir(DIST_DIRECTORY, { recursive: true }); await Promise.all([ writeFile(join(DIST_DIRECTORY, "styles.css"), packageStyles, "utf8"), copyFile(SOURCE_STYLE_TYPES, join(DIST_DIRECTORY, "styles.css.d.ts")) ]); +await writeBrowserTargetReport(targets); await Promise.all([ access(join(DIST_DIRECTORY, "index.js")), @@ -29,4 +36,4 @@ await Promise.all([ ]) ]); -console.log("Built dependency-free ESM, declarations, and styles."); +console.log(`Built Vite ESM, declarations, and explicit styles for ${targets.effectiveQuery}.`); diff --git a/scripts/check-package-policy.mjs b/scripts/check-package-policy.mjs index 99e123c..1019d91 100644 --- a/scripts/check-package-policy.mjs +++ b/scripts/check-package-policy.mjs @@ -10,6 +10,7 @@ import { SUPPORTED_NODE_SELECTOR } from "./lib/node-version.mjs"; import { normalizeNpmPackResult } from "./lib/npm-pack-result.mjs"; +import { inspectDistributionInventory, inspectPackageBundles } from "./lib/package-bundle-policy.mjs"; import { expectedExtensionExport, extractExtensionEntries, @@ -148,20 +149,6 @@ const PACKED_MODULES = (await listFiles(join(REPOSITORY_ROOT, "src"))) const PACKED_MODULE_SET = new Set(PACKED_MODULES); -const ALLOWED_PACKED_FILES = new Set([ - "LICENSE", - "README.md", - "package.json", - "dist/styles.css", - "dist/styles.css.d.ts", - ...PACKED_MODULES.flatMap((moduleName) => [ - `dist/${moduleName}.d.ts`, - `dist/${moduleName}.d.ts.map`, - `dist/${moduleName}.js`, - `dist/${moduleName}.js.map` - ]) -]); - function inspectExtensionCatalog(extensionEntries) { const exportedIds = new Set(extensionEntries.map((entry) => entry.id)); const sourceIds = new Set(); @@ -1209,139 +1196,9 @@ async function inspectWorkflowTree( } function inspectPackedFiles(packResult) { - for (const file of packResult.files ?? []) { - const path = - String(file.path) - .replaceAll("\\", "/"); - - if (!ALLOWED_PACKED_FILES.has(path)) { - addError( - `Unexpected packed file: ${path}.` - ); - } - } - - const packedPaths = - new Set( - (packResult.files ?? []).map( - (file) => - String(file.path) - .replaceAll("\\", "/") - ) - ); - - for (const required of - ALLOWED_PACKED_FILES) { - if (!packedPaths.has(required)) { - addError( - `Required packed file is missing: ${required}.` - ); - } - } -} - -function inspectSourceMap(path, source) { - const packagePath = - relative(REPOSITORY_ROOT, path) - .replaceAll("\\", "/"); - - const mapExtension = - packagePath.endsWith(".d.ts.map") - ? ".d.ts.map" - : packagePath.endsWith(".js.map") - ? ".js.map" - : ""; - - const moduleName = - packagePath.startsWith("dist/") && - mapExtension.length > 0 - ? packagePath.slice( - "dist/".length, - -mapExtension.length - ) - : ""; - - if (!PACKED_MODULE_SET.has(moduleName)) { - addError( - `${packagePath} is not an expected published source map.` - ); - return; - } - - let sourceMap; - - try { - sourceMap = - JSON.parse(source); - } catch { - addError( - `${packagePath} is not valid JSON.` - ); - return; - } - - if (sourceMap === null || - typeof sourceMap !== "object" || - Array.isArray(sourceMap)) { - addError( - `${packagePath} must contain a source map object.` - ); - return; - } - - const expectedSource = - relative( - dirname(path), - join( - REPOSITORY_ROOT, - "src", - `${moduleName}.ts` - ) - ).replaceAll("\\", "/"); - - const outputExtension = - mapExtension === ".d.ts.map" - ? ".d.ts" - : ".js"; - - const outputFile = - `${moduleName.split("/").at(-1) ?? moduleName}${outputExtension}`; - - if (sourceMap.version !== 3) { - addError( - `${packagePath} must use source map version 3.` - ); - } - - if (sourceMap.file !== outputFile) { - addError( - `${packagePath} must identify ${outputFile} as its generated file.` - ); - } - - if (sourceMap.sourceRoot !== "") { - addError( - `${packagePath} must use an empty sourceRoot.` - ); - } - - if (!isExactValue( - sourceMap.sources, - [expectedSource] - )) { - addError( - `${packagePath} sources must resolve only to ${expectedSource}.` - ); - } - - if (mapExtension === ".js.map" && - (!Array.isArray(sourceMap.sourcesContent) || - sourceMap.sourcesContent.length !== 1 || - typeof sourceMap.sourcesContent[0] !== "string" || - sourceMap.sourcesContent[0].length === 0)) { - addError( - `${packagePath} must embed sourcesContent for every source.` - ); + const paths = (packResult.files ?? []).map((file) => String(file.path).replaceAll("\\", "/")); + for (const error of inspectDistributionInventory(paths, PACKED_MODULES, publicJavaScript, { packed: true })) { + addError(error); } } @@ -1367,6 +1224,8 @@ try { inspectExtensionCatalog(extensionEntries); +const publicJavaScript = ["dist/index.js", ...extensionEntries.map((entry) => entry.distJavaScript)]; + const developmentNodeRange = packageJson.devEngines?.runtime?.version; @@ -1788,6 +1647,7 @@ if (process.argv.includes("--built")) { } } + const builtContent = new Map(); for (const path of builtFiles) { const extension = extname(path); @@ -1798,6 +1658,8 @@ if (process.argv.includes("--built")) { "utf8" ); + builtContent.set(relative(REPOSITORY_ROOT, path).replaceAll("\\", "/"), source); + inspectRuntimeLiterals( path, source @@ -1808,12 +1670,6 @@ if (process.argv.includes("--built")) { path, source ); - } else if (path.endsWith(".js.map") || - path.endsWith(".d.ts.map")) { - inspectSourceMap( - path, - source - ); } else if (path.endsWith(".d.ts")) { inspectScript( path, @@ -1829,6 +1685,19 @@ if (process.argv.includes("--built")) { ); } } + + const sourceContent = new Map(await Promise.all(PACKED_MODULES.map(async (moduleName) => { + const path = `src/${moduleName}.ts`; + return [path, await readFile(join(REPOSITORY_ROOT, path), "utf8")]; + }))); + for (const error of inspectPackageBundles({ + files: builtContent, + sources: sourceContent, + sourceModules: PACKED_MODULES, + publicJavaScript + })) { + addError(error); + } } if (process.argv.includes("--pack")) { diff --git a/scripts/lib/calendar-workflow-measurement.mjs b/scripts/lib/calendar-workflow-measurement.mjs index e2cb7fa..e96c2e4 100644 --- a/scripts/lib/calendar-workflow-measurement.mjs +++ b/scripts/lib/calendar-workflow-measurement.mjs @@ -1,5 +1,5 @@ import { readFile } from "node:fs/promises"; -import { dirname, extname, relative, resolve, sep } from "node:path"; +import { dirname, extname, isAbsolute, relative, resolve, sep } from "node:path"; import { gzipSync } from "node:zlib"; import ts from "typescript"; @@ -101,7 +101,7 @@ export async function measureReachableJavaScriptGraph(entryPath, rootDirectory) ); for (const specifier of readStaticModuleSpecifiers(source)) { if (!specifier.startsWith("./") && !specifier.startsWith("../")) { - continue; + throw new Error(`Measured JavaScript graph must not import external module ${specifier}.`); } const resolvedPath = resolve(dirname(path), specifier); pending.push(extname(resolvedPath) === "" ? `${resolvedPath}.js` : resolvedPath); @@ -233,7 +233,7 @@ function readStaticModuleSpecifiers(sourceFile) { function assertWithinRoot(path, root) { const rootRelativePath = relative(root, path); - if (rootRelativePath === ".." || rootRelativePath.startsWith(`..${sep}`) || resolve(path) === root) { + if (isAbsolute(rootRelativePath) || rootRelativePath === ".." || rootRelativePath.startsWith(`..${sep}`) || resolve(path) === root) { throw new Error(`Reachable module ${path} must remain beneath ${root}.`); } } diff --git a/scripts/lib/distribution-provenance.mjs b/scripts/lib/distribution-provenance.mjs new file mode 100644 index 0000000..136ce30 --- /dev/null +++ b/scripts/lib/distribution-provenance.mjs @@ -0,0 +1,35 @@ +import { readFile } from "node:fs/promises"; +import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path"; + +/** Reads the original TypeScript sources represented by a shipped JavaScript module. */ +export async function readDistributionSourceProvenance(modulePath, packageDirectory) { + const mapPath = `${modulePath}.map`; + const sourceMap = JSON.parse(await readFile(mapPath, "utf8")); + if (sourceMap === null || typeof sourceMap !== "object" || sourceMap.version !== 3 || + sourceMap.file !== basename(modulePath) || + (sourceMap.sourceRoot !== undefined && sourceMap.sourceRoot !== "") || + !Array.isArray(sourceMap.sources) || !Array.isArray(sourceMap.sourcesContent) || + sourceMap.sources.length !== sourceMap.sourcesContent.length || + typeof sourceMap.mappings !== "string") { + throw new Error(`${mapPath} must contain a companion source map with embedded source provenance.`); + } + return Object.freeze(sourceMap.sources.map((source, index) => { + if (typeof source !== "string" || isAbsolute(source) || source.includes("\\") || + typeof sourceMap.sourcesContent[index] !== "string") { + throw new Error(`${mapPath} must contain relative TypeScript sources with embedded content.`); + } + const path = relative(packageDirectory, resolve(dirname(mapPath), source)).replaceAll(sep, "/"); + if (!/^src\/.+\.ts$/u.test(path) || path.endsWith(".d.ts")) { + throw new Error(`${mapPath} source ${source} must resolve beneath the package src/ directory.`); + } + return path; + })); +} + +/** Identifies optional extension code independently of bundled output filenames. */ +export function extensionIdsFromSources(sources) { + return new Set(sources.flatMap((source) => { + const match = /^src\/extensions\/([^/]+)\//u.exec(source); + return match === null ? [] : [match[1]]; + })); +} diff --git a/scripts/lib/dynamic-update-measurement.mjs b/scripts/lib/dynamic-update-measurement.mjs index d683235..08a8259 100644 --- a/scripts/lib/dynamic-update-measurement.mjs +++ b/scripts/lib/dynamic-update-measurement.mjs @@ -120,8 +120,8 @@ export function compareDistributionSizes(current, baseline = null) { /** * Creates the three distribution rows emitted by the opt-in measurement command. * - * @param {{coordinator: Uint8Array, entry: Uint8Array, javascript: readonly Uint8Array[]}} current - * @param {{coordinator: Uint8Array | null, entry: Uint8Array | null, javascript: readonly Uint8Array[] | null} | null} baseline + * @param {{coreImportGraph: {fileCount: number, gzipBytes: number, rawBytes: number}, entry: Uint8Array, javascript: readonly Uint8Array[]}} current + * @param {{coreImportGraph: {fileCount: number, gzipBytes: number, rawBytes: number} | null, entry: Uint8Array | null, javascript: readonly Uint8Array[] | null} | null} baseline * @returns {readonly object[]} */ export function createDistributionMeasurements(current, baseline = null) { @@ -136,9 +136,9 @@ export function createDistributionMeasurements(current, baseline = null) { ...summarizeDistributionSize(current.entry, baseline?.entry ?? null) }), Object.freeze({ - key: "coordinator", - label: "dist/internal/runtime/coordinator.js", - ...summarizeDistributionSize(current.coordinator, baseline?.coordinator ?? null) + key: "coreImportGraph", + label: `Core static import graph (${String(current.coreImportGraph.fileCount)} modules)`, + ...compareDistributionSizes(current.coreImportGraph, baseline?.coreImportGraph ?? null) }), Object.freeze({ key: "javascriptTotal", diff --git a/scripts/lib/package-bundle-policy.mjs b/scripts/lib/package-bundle-policy.mjs new file mode 100644 index 0000000..5aba1d4 --- /dev/null +++ b/scripts/lib/package-bundle-policy.mjs @@ -0,0 +1,201 @@ +import { posix } from "node:path"; + +import ts from "typescript"; + +const PRIVATE_CHUNK = /^dist\/chunks\/[A-Za-z0-9_$.-]+-[A-Za-z0-9_-]{8,}\.js$/u; +const BASE_FILES = ["LICENSE", "README.md", "package.json", "dist/styles.css", "dist/styles.css.d.ts"]; + +export function requiredPackageFiles(sourceModules, publicJavaScript) { + return new Set([ + ...BASE_FILES, + ...sourceModules.flatMap((name) => [`dist/${name}.d.ts`, `dist/${name}.d.ts.map`]), + ...publicJavaScript.flatMap((path) => [path, `${path}.map`]) + ]); +} + +export function inspectDistributionInventory(paths, sourceModules, publicJavaScript, { packed = false } = {}) { + const errors = []; + const actual = new Set(paths); + const required = requiredPackageFiles(sourceModules, publicJavaScript); + for (const path of actual) { + if (!required.has(path) && !PRIVATE_CHUNK.test(path.replace(/\.map$/u, ""))) { + errors.push(`Unexpected ${packed ? "packed" : "built"} file: ${path}.`); + } + if (path.endsWith(".js") && !actual.has(`${path}.map`)) { + errors.push(`${path} requires a companion JavaScript source map.`); + } + if (path.endsWith(".js.map") && !actual.has(path.slice(0, -4))) { + errors.push(`${path} has no corresponding JavaScript output.`); + } + } + for (const path of required) { + if ((packed || path.startsWith("dist/")) && !actual.has(path)) { + errors.push(`Required ${packed ? "packed" : "built"} file is missing: ${path}.`); + } + } + return errors; +} + +function readSourceMap(path, content, sources, errors) { + let sourceMap; + try { + sourceMap = JSON.parse(content); + } catch { + errors.push(`${path} is not valid JSON.`); + return []; + } + if (sourceMap === null || typeof sourceMap !== "object" || Array.isArray(sourceMap)) { + errors.push(`${path} must contain a source map object.`); + return []; + } + if (sourceMap.version !== 3 || !Array.isArray(sourceMap.names) || typeof sourceMap.mappings !== "string") { + errors.push(`${path} must contain a version 3 source map with names and mappings.`); + } + if (sourceMap.file !== posix.basename(path.slice(0, -4))) { + errors.push(`${path} must identify its companion output as the generated file.`); + } + if (sourceMap.sourceRoot !== undefined && sourceMap.sourceRoot !== "") { + errors.push(`${path} must use an empty sourceRoot.`); + } + if (!Array.isArray(sourceMap.sources) || !sourceMap.sources.every((value) => typeof value === "string")) { + errors.push(`${path} must list relative source paths.`); + return []; + } + const resolvedSources = sourceMap.sources.map((source) => posix.join(posix.dirname(path), source)); + for (let index = 0; index < sourceMap.sources.length; index += 1) { + const source = sourceMap.sources[index]; + const resolved = resolvedSources[index]; + if (source.includes("\\") || posix.isAbsolute(source) || !sources.has(resolved)) { + errors.push(`${path} source ${JSON.stringify(source)} must resolve to a repository src/ TypeScript module.`); + continue; + } + if (path.endsWith(".js.map") && sourceMap.sourcesContent?.[index] !== sources.get(resolved)) { + errors.push(`${path} must embed matching sourcesContent for ${resolved}.`); + } + } + if (path.endsWith(".js.map") && + (!Array.isArray(sourceMap.sourcesContent) || sourceMap.sourcesContent.length !== sourceMap.sources.length)) { + errors.push(`${path} must embed sourcesContent for every source.`); + } + if (path.endsWith(".d.ts.map")) { + const expectedSource = `src/${path.slice("dist/".length, -".d.ts.map".length)}.ts`; + if (resolvedSources.length !== 1 || resolvedSources[0] !== expectedSource) { + errors.push(`${path} sources must resolve only to ${expectedSource}.`); + } + } + return resolvedSources.filter((source) => sources.has(source)); +} + +function getModuleReferences(sourceFile) { + const references = []; + const visit = (node) => { + if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier !== undefined) { + references.push(node.moduleSpecifier); + } else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && + node.moduleReference.expression !== undefined) { + references.push(node.moduleReference.expression); + } else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) { + references.push(node.argument.literal); + } else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) { + references.push(node.arguments[0]); + } + ts.forEachChild(node, visit); + }; + visit(sourceFile); + return references; +} + +function resolveModuleReferences(path, sourceFile, files, errors) { + const targets = []; + for (const reference of getModuleReferences(sourceFile)) { + if (reference === undefined || !ts.isStringLiteralLike(reference)) { + errors.push(`${path} contains a non-literal module reference.`); + continue; + } + const specifier = reference.text; + if ((!specifier.startsWith("./") && !specifier.startsWith("../")) || specifier.includes("\\")) { + errors.push(`${path} references non-relative module ${JSON.stringify(specifier)}.`); + continue; + } + let target = posix.join(posix.dirname(path), specifier); + if (path.endsWith(".d.ts") && target.endsWith(".js")) { + target = `${target.slice(0, -3)}.d.ts`; + } + if (!target.startsWith("dist/") || !files.has(target) || + (path.endsWith(".js") && !target.endsWith(".js"))) { + errors.push(`${path} module ${JSON.stringify(specifier)} must resolve to a shipped ${path.endsWith(".js") ? "JavaScript" : "declaration"} module inside dist/.`); + continue; + } + targets.push(target); + } + return targets; +} + +function visitGraph(entry, graph) { + const visited = new Set(); + const pending = [entry]; + while (pending.length > 0) { + const path = pending.pop(); + if (visited.has(path)) { + continue; + } + visited.add(path); + pending.push(...(graph.get(path) ?? [])); + } + return visited; +} + +//Validate bytes that will ship, without relying on private source-to-output filenames. +export function inspectPackageBundles({ files, sources, sourceModules, publicJavaScript }) { + const errors = inspectDistributionInventory([...files.keys()], sourceModules, publicJavaScript); + const provenance = new Map(); + const graph = new Map(); + for (const [path, content] of files) { + if (path.endsWith(".js.map") || path.endsWith(".d.ts.map")) { + provenance.set(path.slice(0, -4), readSourceMap(path, content, sources, errors)); + } + } + for (const [path, content] of files) { + if (!path.endsWith(".js") && !path.endsWith(".d.ts")) { + continue; + } + const declaration = path.endsWith(".d.ts"); + const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true, + declaration ? ts.ScriptKind.TS : ts.ScriptKind.JS); + const references = resolveModuleReferences(path, sourceFile, files, errors); + if (!declaration) { + graph.set(path, references); + if (sourceFile.parseDiagnostics.length > 0) { + errors.push(`${path} must contain valid JavaScript.`); + } + const sourceMapComments = [...content.matchAll(/\/\/[#@]\s*sourceMappingURL=(\S+)/gu)]; + if (sourceMapComments.length !== 1 || sourceMapComments[0][1] !== `${posix.basename(path)}.map`) { + errors.push(`${path} must reference only its companion source map.`); + } + const forwardingOnly = sourceFile.statements.every((statement) => + ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement) || ts.isEmptyStatement(statement)); + if (!forwardingOnly && (provenance.get(path)?.length ?? 0) === 0) { + errors.push(`${path} contains runtime code without repository source provenance.`); + } + } + } + const reachable = new Set(); + for (const entry of publicJavaScript) { + const extensionId = /^dist\/extensions\/([^/]+)\/index\.js$/u.exec(entry)?.[1]; + for (const path of visitGraph(entry, graph)) { + reachable.add(path); + for (const source of provenance.get(path) ?? []) { + const sourceExtension = /^src\/extensions\/([^/]+)\//u.exec(source)?.[1]; + if (sourceExtension !== undefined && sourceExtension !== extensionId) { + errors.push(`${entry} reaches optional extension ${JSON.stringify(sourceExtension)} source ${source} through ${path}.`); + } + } + } + } + for (const path of graph.keys()) { + if (!reachable.has(path)) { + errors.push(`${path} is not reachable from a public package entry.`); + } + } + return errors; +} diff --git a/scripts/lib/packed-browser-verification.mjs b/scripts/lib/packed-browser-verification.mjs index 01a0365..df69855 100644 --- a/scripts/lib/packed-browser-verification.mjs +++ b/scripts/lib/packed-browser-verification.mjs @@ -5,6 +5,7 @@ import { JSDOM } from "jsdom"; const DOM_GLOBAL_NAMES = [ "AbortController", + "AbortSignal", "CustomEvent", "DOMException", "Element", @@ -51,12 +52,31 @@ export async function verifyPackedBrowserInteraction(installedPackage) { if (typeof packageModule.createCalendar !== "function") { throw new Error("Packed root module must export createCalendar()."); } + const extensionPath = join(installedPackage, "dist", "extensions", "webmcp", "index.js"); + const { webMcp } = await import(pathToFileURL(extensionPath).href); + if (typeof webMcp !== "function") { + throw new Error("Packed WebMCP entry must export webMcp()."); + } const dom = new JSDOM('
', { pretendToBeVisual: true, url: "https://example.test/calendar" }); const restoreGlobals = installDomGlobals(dom); + const registeredTools = new Map(); + const registrationSignals = []; + Object.defineProperty(dom.window.document, "modelContext", { + value: { + registerTool(tool, { signal }) { + if (signal.aborted || registeredTools.has(tool.name)) { + throw new Error("Packed WebMCP registration must be live and unique."); + } + registeredTools.set(tool.name, tool); + registrationSignals.push(signal); + signal.addEventListener("abort", () => { registeredTools.delete(tool.name); }, { once: true }); + } + } + }); try { const host = dom.window.document.querySelector("#calendar"); if (!(host instanceof dom.window.HTMLElement)) { @@ -64,6 +84,7 @@ export async function verifyPackedBrowserInteraction(installedPackage) { } let activation = null; const calendar = packageModule.createCalendar(host, { + extensions: [webMcp({ toolNamePrefix: "packed-calendar" })], events: [{ id: "packed-byte-event", start: "2026-07-14T09:00", @@ -84,6 +105,19 @@ export async function verifyPackedBrowserInteraction(installedPackage) { if (calendar.getState().phase !== "ready") { throw new Error(`Packed calendar did not become ready; phase was ${calendar.getState().phase}.`); } + for (let attempt = 0; attempt < 200 && registeredTools.size !== 2; attempt += 1) { + await new Promise((resolvePromise) => { setTimeout(resolvePromise, 0); }); + } + if (registeredTools.size !== 2 || !registeredTools.has("packed-calendar-get-events") || + !registeredTools.has("packed-calendar-navigate") || registrationSignals[0] !== registrationSignals[1]) { + throw new Error("Packed core and WebMCP must share registration identity and register both tools."); + } + const getEvents = registeredTools.get("packed-calendar-get-events"); + const initialEvents = await getEvents.execute({ date: "2026-07-14" }); + if (initialEvents.ok !== true || initialEvents.events?.length !== 1 || + initialEvents.events[0].title !== "Packed byte interaction") { + throw new Error("Packed WebMCP must read the core calendar's loaded events."); + } const initialAction = host.querySelector( ".lfc-calendar-agenda button[data-lfc-event-id='packed-byte-event']" @@ -116,6 +150,12 @@ export async function verifyPackedBrowserInteraction(installedPackage) { if (!(action instanceof dom.window.HTMLButtonElement)) { throw new Error("Packed calendar did not refetch the latest setEvents() snapshot."); } + const replacementEvents = await getEvents.execute({ date: "2026-07-14" }); + if (replacementEvents.ok !== true || replacementEvents.events?.length !== 1 || + replacementEvents.events[0].title !== "Packed replacement interaction" || + JSON.stringify(replacementEvents.state) !== JSON.stringify(calendar.getState())) { + throw new Error("Packed WebMCP must observe the same current state after core setEvents() and refetchEvents()."); + } const nativeEvent = new dom.window.MouseEvent("click", { bubbles: true, cancelable: true }); action.dispatchEvent(nativeEvent); if (activation?.event?.id !== "packed-replacement" || activation.nativeEvent !== nativeEvent || @@ -125,6 +165,9 @@ export async function verifyPackedBrowserInteraction(installedPackage) { calendar.destroy(); calendar.destroy(); + if (registeredTools.size !== 0 || registrationSignals.some((signal) => !signal.aborted)) { + throw new Error("Packed calendar destroy() must release every registered WebMCP tool."); + } if (host.childNodes.length !== 0 || host.classList.contains("litefold-calendar") || host.hasAttribute("data-litefold-calendar") || action.isConnected) { throw new Error("Packed calendar destroy() did not release its rendered host."); diff --git a/scripts/lib/packed-extension-verification.mjs b/scripts/lib/packed-extension-verification.mjs index 965af8f..05d15a0 100644 --- a/scripts/lib/packed-extension-verification.mjs +++ b/scripts/lib/packed-extension-verification.mjs @@ -1,10 +1,11 @@ import { readFile } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { pathToFileURL } from "node:url"; import { build } from "esbuild"; import { extractExtensionEntries } from "./package-entries.mjs"; +import { extensionIdsFromSources, readDistributionSourceProvenance } from "./distribution-provenance.mjs"; function assertExactExtensionSet(actual, expected, description) { const actualIds = [...actual].sort((left, right) => left.localeCompare(right, "en")); @@ -16,23 +17,20 @@ function assertExactExtensionSet(actual, expected, description) { } } -function bundledExtensionIds(metafile, packageName) { - const packageMarker = `/node_modules/${packageName}/dist/extensions/`; +function bundledExtensionIds(metafile, provenance) { const ids = new Set(); - - for (const inputPath of Object.keys(metafile.inputs)) { - const normalizedPath = `/${inputPath.replaceAll("\\", "/")}`; - const markerIndex = normalizedPath.lastIndexOf(packageMarker); - if (markerIndex < 0) { - continue; - } - - const relativeExtensionPath = normalizedPath.slice( - markerIndex + packageMarker.length - ); - const id = relativeExtensionPath.split("/", 1)[0]; - if (id !== undefined && id.length > 0) { - ids.add(id); + for (const output of Object.values(metafile.outputs)) { + for (const [inputPath, contribution] of Object.entries(output.inputs)) { + if (contribution.bytesInOutput === 0 || !inputPath.startsWith("packed-package:")) { + continue; + } + const sources = provenance.get(inputPath.slice("packed-package:".length).replaceAll("\\", "/")); + if (sources === undefined) { + throw new Error(`Bundled input ${inputPath} is missing verified source provenance.`); + } + for (const id of extensionIdsFromSources(sources)) { + ids.add(id); + } } } @@ -78,8 +76,18 @@ async function inspectExtensionFactories(installedPackage, extensionEntries) { return Object.freeze(factories); } -function packedPackagePlugin(installedPackage, packageJson) { +function packedPackagePlugin(installedPackage, packageJson, provenance) { const packageNamePattern = packageJson.name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); + const resolvePackedModule = (from, specifier) => { + const path = resolve(from, specifier); + const packagePath = relative(join(installedPackage, "dist"), path); + if ((!specifier.startsWith("./") && !specifier.startsWith("../")) || + isAbsolute(packagePath) || packagePath === ".." || packagePath.startsWith(`..${sep}`) || + !path.endsWith(".js")) { + throw new Error(`Packed JavaScript import ${specifier} must remain inside dist/.`); + } + return { namespace: "packed-package", path }; + }; return { name: "packed-package", @@ -98,26 +106,24 @@ function packedPackagePlugin(installedPackage, packageJson) { return { errors: [{ text: `Packed package does not export ${args.path}.` }] }; } - return { - namespace: "packed-package", - path: resolve(installedPackage, importTarget) - }; + return resolvePackedModule(installedPackage, importTarget); } ); buildContext.onResolve( { filter: /^\./, namespace: "packed-package" }, - (args) => ({ - namespace: "packed-package", - path: resolve(dirname(args.importer), args.path) - }) + (args) => resolvePackedModule(dirname(args.importer), args.path) ); buildContext.onLoad( { filter: /\.js$/, namespace: "packed-package" }, - async (args) => ({ - contents: await readFile(args.path, "utf8"), - loader: "js", - resolveDir: dirname(args.path) - }) + async (args) => { + provenance.set(args.path.replaceAll("\\", "/"), + await readDistributionSourceProvenance(args.path, installedPackage)); + return { + contents: await readFile(args.path, "utf8"), + loader: "js", + resolveDir: dirname(args.path) + }; + } ); } }; @@ -130,6 +136,7 @@ async function bundleFixture( name, source ) { + const provenance = new Map(); return build({ absWorkingDir: fixtureDirectory, bundle: true, @@ -137,7 +144,7 @@ async function bundleFixture( logLevel: "silent", metafile: true, platform: "browser", - plugins: [packedPackagePlugin(installedPackage, packageJson)], + plugins: [packedPackagePlugin(installedPackage, packageJson, provenance)], splitting: true, stdin: { contents: source, @@ -148,7 +155,7 @@ async function bundleFixture( treeShaking: true, write: false, outdir: join(fixtureDirectory, ".extension-bundles", name) - }).then((result) => bundledExtensionIds(result.metafile, packageJson.name)); + }).then((result) => bundledExtensionIds(result.metafile, provenance)); } function extensionImport(packageName, factory, localName = factory.factoryName) { diff --git a/scripts/lib/styles.mjs b/scripts/lib/styles.mjs index ee8d77e..aaaeda1 100644 --- a/scripts/lib/styles.mjs +++ b/scripts/lib/styles.mjs @@ -4,6 +4,7 @@ import { join } from "node:path"; import { transform } from "esbuild"; import { REPOSITORY_ROOT } from "./process.mjs"; +import { resolveBrowserTargets } from "./browser-targets.mjs"; const STYLE_SOURCE_DIRECTORY = join(REPOSITORY_ROOT, "src", "styles"); const LAYER_START = "@layer lfc {\n"; @@ -123,7 +124,7 @@ export async function composeStyles({ } /** Minifies a composed stylesheet for distribution while preserving a trailing newline. */ -export async function minifyStyles(source) { +export async function minifyStyles(source, targets = resolveBrowserTargets().css) { if (typeof source !== "string") { throw new TypeError("Styles to minify must be a string."); } @@ -132,12 +133,13 @@ export async function minifyStyles(source) { legalComments: "none", loader: "css", minify: true, - sourcemap: false + sourcemap: false, + target: [...targets] }); return `${result.code.trimEnd()}\n`; } /** Composes the canonical source modules and minifies only the public distributed stylesheet. */ export async function composeDistributedStyles(options = {}) { - return minifyStyles(await composeStyles(options)); + return minifyStyles(await composeStyles(options), options.targets); } diff --git a/scripts/measure-calendar-workflows.mjs b/scripts/measure-calendar-workflows.mjs index f8ad9bd..a12d523 100644 --- a/scripts/measure-calendar-workflows.mjs +++ b/scripts/measure-calendar-workflows.mjs @@ -17,6 +17,7 @@ import { parseWorkflowMeasurementArguments } from "./lib/calendar-workflow-measurement.mjs"; import { summarizeDurations } from "./lib/dynamic-update-measurement.mjs"; +import { extensionIdsFromSources, readDistributionSourceProvenance } from "./lib/distribution-provenance.mjs"; const execFileAsync = promisify(execFile); const require = createRequire(import.meta.url); @@ -57,9 +58,11 @@ const [rootImportGraph, stylesheet] = await Promise.all([ measureReachableJavaScriptGraph(DIST_ENTRY, DIST_DIRECTORY), measureDistributionAsset(DIST_STYLES) ]); +const rootSources = (await Promise.all(rootImportGraph.files.map((path) => + readDistributionSourceProvenance(join(DIST_DIRECTORY, path), REPOSITORY_ROOT)))).flat(); assert.equal( - rootImportGraph.files.some((path) => path.startsWith("extensions/")), - false, + extensionIdsFromSources(rootSources).size, + 0, "The root import graph must exclude optional extensions." ); diff --git a/scripts/measure-dynamic-event-updates.mjs b/scripts/measure-dynamic-event-updates.mjs index 4a89435..cdd7599 100644 --- a/scripts/measure-dynamic-event-updates.mjs +++ b/scripts/measure-dynamic-event-updates.mjs @@ -7,6 +7,8 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { JSDOM } from "jsdom"; +import { measureReachableJavaScriptGraph } from "./lib/calendar-workflow-measurement.mjs"; + import { createEventSnapshots, createDistributionMeasurements, @@ -21,7 +23,6 @@ import { const REPOSITORY_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const DIST_DIRECTORY = join(REPOSITORY_ROOT, "dist"); const DIST_ENTRY = join(DIST_DIRECTORY, "index.js"); -const DIST_COORDINATOR = join(DIST_DIRECTORY, "internal", "runtime", "coordinator.js"); const require = createRequire(import.meta.url); const JSDOM_VERSION = require("jsdom/package.json").version; const READY_ATTEMPTS = 200; @@ -350,12 +351,12 @@ async function readRequiredFile(path, correctiveAction) { async function measureDistribution(baselinePath) { const currentGraph = await readJavaScriptGraph(DIST_DIRECTORY); - const [currentEntry, currentCoordinator] = await Promise.all([ + const [currentEntry, currentCoreGraph] = await Promise.all([ readRequiredFile(DIST_ENTRY, "Run npm run build:package before measuring dynamic event updates."), - readRequiredFile(DIST_COORDINATOR, "Run npm run build:package before measuring dynamic event updates.") + measureReachableJavaScriptGraph(DIST_ENTRY, DIST_DIRECTORY) ]); const baseline = await resolveBaseline(baselinePath); - const [baselineEntry, baselineCoordinator, baselineGraph] = await Promise.all([ + const [baselineEntry, baselineCoreGraph, baselineGraph] = await Promise.all([ baseline === null ? Promise.resolve(null) : readRequiredFile( @@ -364,10 +365,7 @@ async function measureDistribution(baselinePath) { ), baseline?.directory === null || baseline === null ? Promise.resolve(null) - : readRequiredFile( - join(baseline.directory, "internal", "runtime", "coordinator.js"), - "The baseline dist directory must contain internal/runtime/coordinator.js." - ), + : measureReachableJavaScriptGraph(baseline.entry, baseline.directory), baseline?.directory === null || baseline === null ? Promise.resolve(null) : readJavaScriptGraph(baseline.directory) @@ -377,14 +375,14 @@ async function measureDistribution(baselinePath) { gzipLevel: 9, measurements: createDistributionMeasurements( { - coordinator: currentCoordinator, + coreImportGraph: distributionGraphSize(currentCoreGraph), entry: currentEntry, javascript: currentGraph.bytes }, baseline === null ? null : { - coordinator: baselineCoordinator, + coreImportGraph: baselineCoreGraph === null ? null : distributionGraphSize(baselineCoreGraph), entry: baselineEntry, javascript: baselineGraph?.bytes ?? null } @@ -392,6 +390,10 @@ async function measureDistribution(baselinePath) { }; } +function distributionGraphSize(graph) { + return { fileCount: graph.fileCount, gzipBytes: graph.separateGzipBytes, rawBytes: graph.rawBytes }; +} + async function resolveBaseline(path) { if (path === null) { return null; diff --git a/scripts/tests/calendar-workflow-measurement.test.mjs b/scripts/tests/calendar-workflow-measurement.test.mjs index 20fc4f7..6f80f43 100644 --- a/scripts/tests/calendar-workflow-measurement.test.mjs +++ b/scripts/tests/calendar-workflow-measurement.test.mjs @@ -34,17 +34,18 @@ void test("root import graph excludes unreferenced optional modules", async (con const directory = await mkdtemp(join(tmpdir(), "lfc-workflow-graph-")); context.after(async () => { await rm(directory, { force: true, recursive: true }); }); await mkdir(join(directory, "extensions")); + await mkdir(join(directory, "chunks")); const sources = new Map([ ["index.js", 'export { createCalendar } from "./calendar.js";\nexport { CalendarError } from "./errors.js";\n'], - ["calendar.js", 'import { helper } from "./internal.js";\nexport const createCalendar = helper;\n'], + ["calendar.js", 'import { helper } from "./chunks/internal-a1234567.js";\nexport const createCalendar = helper;\n'], ["errors.js", "export class CalendarError extends Error {}\n"], - ["internal.js", 'import "node:fs";\nexport const helper = () => undefined;\nexport const loadOptional = () => import("./extensions/webmcp.js");\n'], + [join("chunks", "internal-a1234567.js"), 'export const helper = () => undefined;\nexport const loadOptional = () => import("../extensions/webmcp.js");\n'], [join("extensions", "webmcp.js"), "export const optional = true;\n"] ]); await Promise.all([...sources].map(([path, source]) => writeFile(join(directory, path), source, "utf8"))); const graph = await measureReachableJavaScriptGraph(join(directory, "index.js"), directory); - assert.deepEqual(graph.files, ["calendar.js", "errors.js", "index.js", "internal.js"]); + assert.deepEqual(graph.files, ["calendar.js", "chunks/internal-a1234567.js", "errors.js", "index.js"]); assert.equal( graph.rawBytes, [...sources].filter(([path]) => !path.includes("extensions")) @@ -52,6 +53,10 @@ void test("root import graph excludes unreferenced optional modules", async (con ); assert.ok(graph.separateGzipBytes > 0); assert.ok(graph.combinedGzipBytes > 0); + await writeFile(join(directory, "index.js"), 'import "node:fs";\n', "utf8"); + await assert.rejects(measureReachableJavaScriptGraph(join(directory, "index.js"), directory), /external module/u); + await writeFile(join(directory, "index.js"), 'export * from "../outside.js";\n', "utf8"); + await assert.rejects(measureReachableJavaScriptGraph(join(directory, "index.js"), directory), /must remain beneath/u); }); void test("workflow operation contract catches redundant renders without timing assertions", () => { diff --git a/scripts/tests/dynamic-update-measurement.test.mjs b/scripts/tests/dynamic-update-measurement.test.mjs index 0f58d48..406e172 100644 --- a/scripts/tests/dynamic-update-measurement.test.mjs +++ b/scripts/tests/dynamic-update-measurement.test.mjs @@ -64,21 +64,21 @@ void test("distribution size reports raw and gzip deltas", () => { assert.throws(() => summarizeDistributionFiles([]), /at least one/u); }); -void test("distribution report keeps entry, coordinator, and complete JavaScript graph rows", () => { +void test("distribution report compares public entry, core import graph, and complete JavaScript graph rows", () => { const current = Buffer.from("current calendar module\n", "utf8"); const baseline = Buffer.from("baseline module\n", "utf8"); const rows = createDistributionMeasurements( - { coordinator: current, entry: current, javascript: [current, current] }, - { coordinator: baseline, entry: baseline, javascript: [baseline] } + { coreImportGraph: summarizeDistributionFiles([current, current]), entry: current, javascript: [current, current, current] }, + { coreImportGraph: summarizeDistributionFiles([baseline]), entry: baseline, javascript: [baseline, baseline] } ); assert.deepEqual(rows.map(({ key, label }) => ({ key, label })), [ { key: "entry", label: "dist/index.js" }, - { key: "coordinator", label: "dist/internal/runtime/coordinator.js" }, - { key: "javascriptTotal", label: "dist JavaScript total (2 modules)" } + { key: "coreImportGraph", label: "Core static import graph (2 modules)" }, + { key: "javascriptTotal", label: "dist JavaScript total (3 modules)" } ]); - assert.equal(rows[1]?.delta?.rawBytes, current.byteLength - baseline.byteLength); - assert.equal(rows[2]?.current.rawBytes, current.byteLength * 2); - assert.equal(rows[2]?.baseline?.rawBytes, baseline.byteLength); + assert.equal(rows[1]?.delta?.rawBytes, current.byteLength * 2 - baseline.byteLength); + assert.equal(rows[2]?.current.rawBytes, current.byteLength * 3); + assert.equal(rows[2]?.baseline?.rawBytes, baseline.byteLength * 2); }); void test("measurement arguments preserve the minimum protocol", () => { diff --git a/scripts/tests/package-bundle-policy.test.mjs b/scripts/tests/package-bundle-policy.test.mjs new file mode 100644 index 0000000..74f41f0 --- /dev/null +++ b/scripts/tests/package-bundle-policy.test.mjs @@ -0,0 +1,125 @@ +import assert from "node:assert/strict"; +import { posix } from "node:path"; +import test from "node:test"; + +import { + inspectDistributionInventory, + inspectPackageBundles, + requiredPackageFiles +} from "../lib/package-bundle-policy.mjs"; + +const SHARED_CHUNK = "dist/chunks/registry-AbCd0123.js"; +const EXTENSION_ENTRY = "dist/extensions/webmcp/index.js"; + +function setJavaScript(fixture, path, content, sourcePaths) { + fixture.files.set(path, `${content}\n//# sourceMappingURL=${posix.basename(path)}.map\n`); + fixture.files.set(`${path}.map`, JSON.stringify({ + file: posix.basename(path), + mappings: "AAAA", + names: [], + sources: sourcePaths.map((source) => posix.relative(posix.dirname(path), source)), + sourcesContent: sourcePaths.map((source) => fixture.sources.get(source)), + version: 3 + })); +} + +function fixture() { + const sourceModules = ["index", "types", "internal/registry", "extensions/webmcp/index"]; + const publicJavaScript = ["dist/index.js", EXTENSION_ENTRY]; + const sources = new Map(sourceModules.map((name) => [`src/${name}.ts`, `//Source: ${name}\n`])); + const files = new Map([["dist/styles.css", ""], ["dist/styles.css.d.ts", "export {};\n"]]); + for (const name of sourceModules) { + const path = `dist/${name}.d.ts`; + files.set(path, "export {};\n"); + files.set(`${path}.map`, JSON.stringify({ + file: posix.basename(path), + mappings: "AAAA", + names: [], + sourceRoot: "", + sources: [posix.relative(posix.dirname(path), `src/${name}.ts`)], + version: 3 + })); + } + const result = { files, publicJavaScript, sourceModules, sources }; + setJavaScript(result, "dist/index.js", `export { registry } from "./chunks/${posix.basename(SHARED_CHUNK)}";`, ["src/index.ts"]); + setJavaScript(result, SHARED_CHUNK, "export const registry = new WeakMap();", ["src/internal/registry.ts"]); + setJavaScript(result, EXTENSION_ENTRY, + `import { registry } from "../../chunks/${posix.basename(SHARED_CHUNK)}"; export const extension = () => registry;`, + ["src/extensions/webmcp/index.ts"]); + return result; +} + +void test("package bundle policy accepts shared bundled ESM with source declarations and embedded provenance", () => { + const data = fixture(); + assert.deepEqual(inspectPackageBundles(data), []); + const packed = [...data.files.keys(), "LICENSE", "README.md", "package.json"]; + assert.deepEqual(inspectDistributionInventory(packed, data.sourceModules, data.publicJavaScript, { packed: true }), []); + assert.equal(requiredPackageFiles(data.sourceModules, data.publicJavaScript).has("dist/internal/registry.js"), false); +}); + +void test("package bundle policy rejects extension code concealed in a core-reachable private chunk", () => { + const data = fixture(); + setJavaScript(data, SHARED_CHUNK, "export const registry = new WeakMap();", ["src/extensions/webmcp/index.ts"]); + assert.ok(inspectPackageBundles(data).some((error) => + error.includes("dist/index.js reaches optional extension") && error.includes(SHARED_CHUNK))); +}); + +void test("package bundle policy rejects sibling-extension source reachable from another extension", () => { + const data = fixture(); + data.sources.set("src/extensions/other/index.ts", "export {};\n"); + setJavaScript(data, EXTENSION_ENTRY, "export const extension = () => undefined;", ["src/extensions/other/index.ts"]); + assert.ok(inspectPackageBundles(data).some((error) => error.includes(`${EXTENSION_ENTRY} reaches optional extension "other"`))); +}); + +void test("package bundle policy rejects altered source content and source-map paths outside repository source", () => { + const data = fixture(); + const map = JSON.parse(data.files.get(`${SHARED_CHUNK}.map`)); + map.sourcesContent[0] = "export const registry = false;\n"; + data.files.set(`${SHARED_CHUNK}.map`, JSON.stringify(map)); + assert.ok(inspectPackageBundles(data).some((error) => error.includes("matching sourcesContent"))); + map.sources[0] = "../../node_modules/unexpected/index.ts"; + data.files.set(`${SHARED_CHUNK}.map`, JSON.stringify(map)); + assert.ok(inspectPackageBundles(data).some((error) => error.includes("repository src/ TypeScript module"))); +}); + +void test("package bundle policy resolves static, dynamic, and declaration imports against shipped output", () => { + const data = fixture(); + setJavaScript(data, "dist/index.js", 'export const load = () => import("./missing.js");', ["src/index.ts"]); + data.files.set("dist/index.d.ts", 'export type { Missing } from "./missing.js";\n'); + const errors = inspectPackageBundles(data); + assert.ok(errors.some((error) => error.includes("shipped JavaScript module inside dist/"))); + assert.ok(errors.some((error) => error.includes("shipped declaration module inside dist/"))); + data.files.set("dist/index.d.ts", 'export type { Value } from "./types.js";\n'); + assert.equal(inspectPackageBundles(data).some((error) => error.includes("shipped declaration")), false); +}); + +void test("package bundle policy rejects orphan runtime outputs and incomplete map pairs", () => { + const data = fixture(); + const orphan = "dist/chunks/unused-EfGh4567.js"; + setJavaScript(data, orphan, "export const unused = true;", ["src/types.ts"]); + assert.ok(inspectPackageBundles(data).some((error) => error.includes(`${orphan} is not reachable`))); + data.files.delete(`${orphan}.map`); + assert.ok(inspectPackageBundles(data).some((error) => error.includes(`${orphan} requires a companion`))); + data.files.delete(orphan); + data.files.delete("dist/index.js"); + assert.ok(inspectPackageBundles(data).some((error) => error.includes("dist/index.js.map has no corresponding"))); +}); + +void test("package bundle policy accepts source-free forwarding entries but rejects unmapped runtime code", () => { + const data = fixture(); + setJavaScript(data, "dist/index.js", `export { registry } from "./chunks/${posix.basename(SHARED_CHUNK)}";`, []); + assert.deepEqual(inspectPackageBundles(data), []); + setJavaScript(data, "dist/index.js", 'export const registry = new WeakMap();', []); + assert.ok(inspectPackageBundles(data).some((error) => error.includes("runtime code without repository source provenance"))); +}); + +void test("package bundle policy rejects unexpected artifacts and wrong source-map identity", () => { + const data = fixture(); + data.files.set("dist/accidental.json", "{}"); + const map = JSON.parse(data.files.get(`${SHARED_CHUNK}.map`)); + map.file = "different.js"; + data.files.set(`${SHARED_CHUNK}.map`, JSON.stringify(map)); + const errors = inspectPackageBundles(data); + assert.ok(errors.some((error) => error.includes("Unexpected built file: dist/accidental.json"))); + assert.ok(errors.some((error) => error.includes("companion output as the generated file"))); +}); diff --git a/scripts/tests/packed-extension-verification.test.mjs b/scripts/tests/packed-extension-verification.test.mjs index 6485d9c..dc82b55 100644 --- a/scripts/tests/packed-extension-verification.test.mjs +++ b/scripts/tests/packed-extension-verification.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { dirname, join } from "node:path"; +import { basename, dirname, join, relative, sep } from "node:path"; import test from "node:test"; import { expectedExtensionExport } from "../lib/package-entries.mjs"; @@ -10,9 +10,21 @@ import { } from "../lib/packed-extension-verification.mjs"; import { REPOSITORY_ROOT, runTsc } from "../lib/process.mjs"; -async function writeModule(path, source) { +async function writeModule(path, source, sourceModule) { await mkdir(dirname(path), { recursive: true }); await writeFile(path, source, "utf8"); + if (path.endsWith(".js")) { + const normalizedPath = path.replaceAll(sep, "/"); + const distIndex = normalizedPath.lastIndexOf("/dist/"); + const originalPath = sourceModule === undefined + ? `${normalizedPath.slice(0, distIndex)}/src/${normalizedPath.slice(distIndex + 6, -3)}.ts` + : `${normalizedPath.slice(0, distIndex)}/${sourceModule}`; + await writeFile(`${path}.map`, JSON.stringify({ + file: basename(path), mappings: "AAAA", names: [], + sources: [relative(dirname(path), originalPath).replaceAll(sep, "/")], + sourcesContent: [source], version: 3 + }), "utf8"); + } } void test("packed extension verification proves isolated bundles and public aggregate types", async () => { @@ -75,7 +87,12 @@ void test("packed extension verification proves isolated bundles and public aggr ); await writeModule( join(installedPackage, "dist", "extensions", "alpha", "index.js"), - "export function alpha() { return Object.freeze({}); }\n" + 'export { alpha } from "../../chunks/alpha-a1234567.js";\n' + ); + await writeModule( + join(installedPackage, "dist", "chunks", "alpha-a1234567.js"), + "export function alpha() { return Object.freeze({}); }\n", + "src/extensions/alpha/implementation.ts" ); await writeModule( join(installedPackage, "dist", "extensions", "alpha", "index.d.ts"), @@ -135,6 +152,28 @@ void test("packed extension verification proves isolated bundles and public aggr }, null, 2)}\n` ); await runTsc(["-p", "tsconfig.json", "--pretty", "false"], { cwd: fixtureDirectory }); + await writeModule(join(installedPackage, "dist", "index.js"), + 'import { alpha } from "./chunks/alpha-a1234567.js";\nexport function createCalendar() { return undefined; }\n'); + await verifyPackedExtensions(fixtureDirectory, installedPackage, packageJson); + + await writeModule( + join(installedPackage, "dist", "chunks", "hidden-b7654321.js"), + "export const extra = 1;\n", + "src/extensions/beta/implementation.ts" + ); + await writeModule( + join(installedPackage, "dist", "index.js"), + 'import { extra } from "./chunks/hidden-b7654321.js";\nexport function createCalendar() { return extra; }\n' + ); + await assert.rejects(verifyPackedExtensions(fixtureDirectory, installedPackage, packageJson), + /Core-only fixture bundled extensions \[beta\]/u); + await writeModule(join(installedPackage, "dist", "index.js"), + "export function createCalendar() { return undefined; }\n"); + await writeFile(join(installedPackage, "dist", "chunks", "alpha-a1234567.js.map"), + JSON.stringify({ file: "alpha-a1234567.js", mappings: "", sources: ["../../../outside.ts"], + sourcesContent: ["export const x = 1;"], version: 3 }), "utf8"); + await assert.rejects(verifyPackedExtensions(fixtureDirectory, installedPackage, packageJson), + /must resolve beneath the package src/u); } finally { await rm(fixtureDirectory, { force: true, recursive: true }); } diff --git a/scripts/tests/vite-build.test.mjs b/scripts/tests/vite-build.test.mjs new file mode 100644 index 0000000..42bdc64 --- /dev/null +++ b/scripts/tests/vite-build.test.mjs @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import test from "node:test"; + +import { createServer } from "vite"; + +import { createLibraryConfig } from "../../vite.config.mjs"; +import examplesConfig, { sourceExamplesPlugin } from "../../vite.examples.config.mjs"; +import { REPOSITORY_ROOT } from "../lib/process.mjs"; + +void test("Vite preserves public ESM entries, shared chunks, and standalone CSS", async () => { + const config = await createLibraryConfig(); + assert.deepEqual(config.build.lib.formats, ["es"]); + assert.equal(config.build.lib.entry.index, join(REPOSITORY_ROOT, "src/index.ts")); + assert.equal(config.build.lib.entry["extensions/webmcp/index"], join(REPOSITORY_ROOT, "src/extensions/webmcp/index.ts")); + assert.equal(config.build.lib.fileName("es", "extensions/webmcp/index"), "extensions/webmcp/index.js"); + assert.equal(config.build.rolldownOptions.output.chunkFileNames, "chunks/[name]-[hash].js"); + assert.equal(config.build.target, "baseline-widely-available"); + assert.equal(config.build.sourcemap, true); + assert.equal(config.build.cssTarget.length > 0, true); + const tsconfig = JSON.parse(await readFile(join(REPOSITORY_ROOT, "tsconfig.build.json"), "utf8")); + assert.equal(tsconfig.compilerOptions.emitDeclarationOnly, true); + assert.equal(tsconfig.compilerOptions.declarationMap, true); +}); + +void test("Vite examples resolve package imports and style requests to source", async () => { + const server = await createServer({ + ...examplesConfig, + configFile: false, + server: { middlewareMode: true, watch: null }, + optimizeDeps: { noDiscovery: true } + }); + try { + const core = await server.transformRequest("/dist/index.js"); + assert.match(core.code, /src\/calendar\.ts/u); + const extension = await server.transformRequest("/dist/extensions/webmcp/index.js"); + assert.match(extension.code, /webMcp/u); + const styles = await server.transformRequest("/dist/styles.css?direct"); + assert.match(styles.code, /@layer lfc/u); + const advanced = await server.transformIndexHtml("/examples/advanced/index.html", ''); + assert.match(advanced, /src="\.\/main\.ts"/u); + } finally { + await server.close(); + } +}); + +void test("Vite only rewrites the TypeScript example and preserves classic dynamic import", () => { + const plugin = sourceExamplesPlugin(); + const html = ''; + assert.equal(plugin.transformIndexHtml.handler(html, { path: "/examples/classic-script/" }), html); + assert.equal(plugin.transformIndexHtml.handler(html, { path: "/examples/remote-data/" }), html); +}); + +void test("Vite source development identifies the working copy instead of a stale built commit", async () => { + const plugin = sourceExamplesPlugin(); + let middleware; + plugin.configureServer({ middlewares: { use(handler) { middleware = handler; } } }); + const metadata = await new Promise((resolve, reject) => { + middleware({ url: "/examples/metadata.json" }, { + setHeader() {}, + end(body) { resolve(JSON.parse(body)); } + }, reject); + }); + assert.equal(metadata.channel, "local"); + assert.equal(metadata.commit, null); + assert.equal(typeof metadata.version, "string"); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json index 029bc0a..0e9aee6 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -6,8 +6,7 @@ "outDir": "dist", "declaration": true, "declarationMap": true, - "inlineSources": true, - "sourceMap": true, + "emitDeclarationOnly": true, "stripInternal": true, "noEmit": false }, diff --git a/vite.config.mjs b/vite.config.mjs new file mode 100644 index 0000000..4887dc9 --- /dev/null +++ b/vite.config.mjs @@ -0,0 +1,37 @@ +import { join } from "node:path"; + +import { defineConfig } from "vite"; + +import { resolveBrowserTargets } from "./scripts/lib/browser-targets.mjs"; +import { extractExtensionEntries, readPackageManifest } from "./scripts/lib/package-entries.mjs"; +import { REPOSITORY_ROOT } from "./scripts/lib/process.mjs"; + +/** Builds all public entry points together so private extension registration stays shared. */ +export async function createLibraryConfig(targets = resolveBrowserTargets()) { + const extensions = extractExtensionEntries(await readPackageManifest()); + return defineConfig({ + configFile: false, + root: REPOSITORY_ROOT, + build: { + emptyOutDir: false, + lib: { + entry: Object.fromEntries([ + ["index", join(REPOSITORY_ROOT, "src/index.ts")], + ...extensions.map((entry) => [entry.sourceModule, join(REPOSITORY_ROOT, entry.sourceEntry)]) + ]), + formats: ["es"], + fileName: (_format, entryName) => `${entryName}.js` + }, + minify: false, + outDir: join(REPOSITORY_ROOT, "dist"), + rolldownOptions: { + output: { chunkFileNames: "chunks/[name]-[hash].js" } + }, + sourcemap: true, + target: "baseline-widely-available", + cssTarget: [...targets.css] + } + }); +} + +export default createLibraryConfig(); diff --git a/vite.examples.config.mjs b/vite.examples.config.mjs new file mode 100644 index 0000000..ca00988 --- /dev/null +++ b/vite.examples.config.mjs @@ -0,0 +1,71 @@ +import { dirname, join, resolve } from "node:path"; + +import { defineConfig } from "vite"; + +import { REPOSITORY_ROOT } from "./scripts/lib/process.mjs"; +import { composeStyles } from "./scripts/lib/styles.mjs"; +import { createExampleMetadata, serializeExampleMetadata } from "./scripts/lib/example-metadata.mjs"; +import { readPackageManifest } from "./scripts/lib/package-entries.mjs"; + +const VIRTUAL_STYLES = "\0lfc-source-styles.css"; + +/** Serves source modules during local development; verification still uses built artifacts. */ +export function sourceExamplesPlugin() { + return { + name: "litefold-source-examples", + enforce: "pre", + configureServer(server) { + server.middlewares.use((request, response, next) => { + if (request.url?.split("?", 1)[0] !== "/examples/metadata.json") { next(); return; } + void readPackageManifest().then((manifest) => { + response.setHeader("Content-Type", "application/json; charset=utf-8"); + response.setHeader("Cache-Control", "no-store"); + response.end(serializeExampleMetadata(createExampleMetadata({ + channel: "local", commit: null, version: manifest.version + }))); + }).catch(next); + }); + }, + resolveId(source, importer) { + const path = source.startsWith("/") + ? join(REPOSITORY_ROOT, source) + : importer === undefined ? null : resolve(dirname(importer), source); + if (path === join(REPOSITORY_ROOT, "dist", "styles.css")) { + return VIRTUAL_STYLES; + } + if (path === join(REPOSITORY_ROOT, "dist", "index.js")) { + return join(REPOSITORY_ROOT, "src", "index.ts"); + } + const extension = path?.replaceAll("\\", "/") + .match(/\/dist\/extensions\/([a-z][a-z0-9-]*)\/index\.js$/u)?.[1]; + if (extension !== undefined && path.startsWith(join(REPOSITORY_ROOT, "dist"))) { + return join(REPOSITORY_ROOT, "src", "extensions", extension, "index.ts"); + } + }, + load(id) { + if (id === VIRTUAL_STYLES) { return composeStyles(); } + }, + transformIndexHtml: { + order: "pre", + handler(html, context) { + return context.path === "/examples/advanced/index.html" || context.path === "/examples/advanced/" + ? html.replace('src="./main.js"', 'src="./main.ts"') + : html; + } + }, + handleHotUpdate(context) { + if (context.file.startsWith(join(REPOSITORY_ROOT, "src", "styles"))) { + const module = context.server.moduleGraph.getModuleById(VIRTUAL_STYLES); + if (module !== undefined) { context.server.moduleGraph.invalidateModule(module); } + context.server.ws.send({ type: "full-reload" }); + return []; + } + } + }; +} + +export default defineConfig({ + root: REPOSITORY_ROOT, + plugins: [sourceExamplesPlugin()], + server: { host: "127.0.0.1", open: "/examples/", port: 4173, strictPort: true } +}); From de4ccb57968ed2c94c3c8f56a0cd6e1c36b0871b Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:44:02 -0700 Subject: [PATCH 05/22] feat: add responsive day event-count presentation --- ACCESSIBILITY.md | 8 +- DESIGN.md | 26 +- examples/advanced/main.ts | 10 + src/index.ts | 2 + src/internal/dom/grid-focus.ts | 42 +- src/internal/runtime/configuration.ts | 29 +- src/internal/runtime/coordinator.ts | 35 +- src/internal/runtime/day-grid-actions.ts | 51 ++- .../runtime/event-overflow-presentation.ts | 76 +++- src/internal/runtime/message-configuration.ts | 2 + src/internal/runtime/render-hook-visuals.ts | 5 +- src/messages.ts | 6 + src/styles/month-grid.css | 12 + src/styles/responsive.css | 49 +++ src/types.ts | 32 ++ tests/calendar-core-features.test.ts | 1 + .../calendar-event-overflow-integrity.test.ts | 6 + tests/calendar-event-overflow.test.ts | 4 + tests/calendar-grid-event-display.test.ts | 366 ++++++++++++++++++ tests/calendar-render-hooks.test.ts | 55 +-- tests/e2e/grid-event-display.spec.js | 200 ++++++++++ tests/e2e/responsive-layout.spec.js | 2 + tests/public-api.type-test.ts | 28 ++ 23 files changed, 945 insertions(+), 102 deletions(-) create mode 100644 tests/calendar-grid-event-display.test.ts create mode 100644 tests/e2e/grid-event-display.spec.js diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md index c4bcae5..873e75d 100644 --- a/ACCESSIBILITY.md +++ b/ACCESSIBILITY.md @@ -35,7 +35,11 @@ Event representation follows native semantics: Direct event activation on either surface never selects the day or invokes `onDaySelect`. The selected-day agenda remains below the grid at every width and uses ordered-list markup. -Grid overflow is a native action named from its date and hidden count; its default label is “View {count} more {eventLabel} for {date}”. `renderEventOverflow` may replace its pre-rendered compact and wide visual content, but each custom slot is `aria-hidden` and noninteractive. The native button, accessible label, activation, and agenda-focus transfer remain package-owned; `context.text` retains the package-formatted fallback value even when custom content replaces that fallback visually. Activating overflow selects the represented date, resets agenda expansion, focuses the agenda heading, and does not invoke `onDaySelect`. +Day counts and grid overflow share a native action. Whenever either container-size variant presents a total count, its stable accessible label uses the full count and date: “View {count} {eventLabel} for {date}”. If the wide variant presents remaining events, its localized visible wording is also included in that name, such as “View 4 events for Tuesday, July 14, 2026, 1 more”. Otherwise the label describes the hidden count with “View {count} more {eventLabel} for {date}”. `renderEventOverflow` customizes only `aria-hidden`, noninteractive visual content; count actions cannot be suppressed by returning `null`. + +Activation invokes `onEventOverflowActivate` with an immutable occurrence snapshot before selection or DOM replacement. Synchronous `nativeEvent.preventDefault()` transfers interaction and focus ownership to the application, which may open its own event chooser and restore focus when it closes. Without cancellation, activation selects the day, resets agenda expansion, focuses the agenda heading, and does not invoke `onDaySelect`. Application navigation, rerendering, or destruction during the callback invalidates the original action and prevents stale default work; asynchronous failures use the action-error channel. + +At the supported year limits, a spillover occurrence may belong to a month whose complete six-week grid cannot be rendered. Its application callback still runs, but default navigation leaves selection unchanged, matching the day proxy's boundary behavior. Replacing event input keeps package-owned focus on the same day or event occurrence when it still exists. If a focused event disappears, focus returns to its owning day; focus outside the calendar is not moved. @@ -69,7 +73,7 @@ Returning `"handled"` from `onError` explicitly transfers both visible and acces Responsive breakpoints, visual composition, week-row sizing, event-stack placement, and the supported design floor are canonical in the [responsive design](DESIGN.md#responsive-model). Layout uses CSS container queries and intrinsic grid sizing rather than viewport width or JavaScript measurement. `weekRowSizing` and `gridEventPlacement` change geometry only: the date stays at the cell's block start, and moving the event/overflow stack never changes DOM order, managed action order, or focus behavior. At the supported floor, reflow must not introduce persistent two-dimensional content scrolling; narrower containers are robustness targets rather than design claims. The pager's transient native horizontal movement is direct input, not a second content axis or adjacent interactive view. Verify the complete page at 400% zoom with the calendar host at the supported design floor. -At every width, the first actionable event retains its complete accessible name; visually hidden later actions remain reachable through managed action navigation; and agenda rows retain titles, times, overflow details, and visible/total progress. In equal week-row mode, package-owned, normally visible compact primary-event, count, and compact-primary overflow roots share one full slot size; focus-only later summaries remain intrinsically sized so their complete content and focus indicator can surface. Compact day counts are decorative, pointer-transparent, unfocusable, and excluded from action order. Pointer activation selects the day. If the overflow button carries the compact count, including when `maxGridEventsPerDay` is `0`, it remains one native action with package-owned naming and focus transfer. Neither presentation replaces the exact total in the day button's accessible name or changes F2, Arrow, Escape, Tab, pointer, or agenda behavior. Render-hook marker and leading content must remain noninteractive and must not obscure names, focus indicators, or adjacent actions. +In count presentation, individual summaries are hidden from new keyboard focus entry and from the accessibility tree. F2 and Up/Down Arrow attempt actions in canonical order and verify that the browser accepted focus, skipping CSS-hidden controls without measuring layout. An already focused summary stays visible across a container-size transition until focus leaves it; the shared count/overflow action also keeps its identity and accessible name across resizing. The count action provides access to all occurrences, including static events and events beyond summary or agenda-page limits. In legacy `"events"` presentation, visually hidden later actions remain available through managed navigation. Every visible action keeps its complete accessible name and at least the supported target size. Agenda rows retain titles, times, details, and visible/total progress in every mode. Static and interactive agenda rows keep marker or leading content, localized time, title, details, and trailing content in that DOM order. Their [visual alignment and narrow reflow](DESIGN.md#events-and-agenda) preserve usable reading space in both text directions. diff --git a/DESIGN.md b/DESIGN.md index da854f4..31c0eb0 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -350,8 +350,10 @@ By default, the six week tracks have equal intrinsic height: every row matches t The date number and badge remain at the cell's block start. `gridEventPlacement` aligns the complete event-summary and overflow stack within the remaining block space at every width: `"top"` is the default, with `"center"` and `"bottom"` as alternatives. Center and bottom use safe alignment and fall back toward the top when the stack cannot fit; a content-filled cell may have no visible placement difference. -- **Above `42rem`:** Toolbar content stays on the primary row, weekday labels use the wide size, and every capped event action consumes the available day-cell width. A native grid-overflow action shows its localized package content or `renderEventOverflow`-supplied wide visual content without changing the action itself. -- **At or below `42rem`:** Built-in navigation occupies the first row and application `toolbarEnd` content the second. Gaps and margins contract. Day badges disappear. The first actionable grid event uses a marker-only presentation inside a 24–44 CSS-pixel action target; later actions are visually hidden unless focused. A day with more than one total event occurrence shows a locale-aware social-style count. When a primary marker/action and passive count coexist, two equal, gap-free auto-fit grid blocks keep them aligned within the event stack. Their centers evenly divide the available inline area when both compact-control-size tracks, 44 CSS pixels by default, fit; otherwise the blocks become centered, equal full-width rows. They therefore remain stacked through supported phone widths and share one row only near the compact ceiling. The count reports additional occurrences with a sign when the marker is visible, and the unsigned total when the marker is suppressed or no compact primary visual exists. A markerless standalone total uses one centered block. If the native overflow action is the compact-primary control, including `maxGridEventsPerDay: 0`, its compact variant remains one package-owned action instead of creating a separate cue. Full event information remains in the agenda. +- **Above `42rem`:** Toolbar content stays on the primary row and weekday labels use the wide size. By default, capped event summaries fill the day width and remaining occurrences use a native overflow action. `gridEventDisplay.wide` can instead show a localized total-count button for every nonempty day (`"count"`) or only days with multiple occurrences (`"count-when-multiple"`). +- **At or below `42rem`:** Navigation occupies the first row, application `toolbarEnd` content the second, and day badges disappear. The default `gridEventDisplay.compact: "count-when-multiple"` presents multiple occurrences as one bordered, recognizable total-count button, independent of the summary cap. `"count"` also uses this button for single-event days. A one-event day otherwise retains its existing event representation. Counts use localized compact number formatting and a complete, exact accessible total. An empty day has no count action. + +`gridEventDisplay: { compact: "events" }` restores the prior marker-plus-overflow presentation. Its first actionable event uses a marker-only 24–44 CSS-pixel target; later actions remain available on focus. A passive cue reports the signed additional count with a marker or the unsigned total without one. The package arranges marker and cue in equal auto-fit blocks, stacked when the full compact targets cannot fit beside each other. A zero summary cap suppresses individual summaries in every mode, while total-count actions remain available. All loaded normalized occurrences contribute to counts, including multi-day occurrences and visually suppressed markers. - **Below `24rem`:** The toolbar title and decorative pager lanes switch their visible month/year text from the full month to the locale's `month: "short", year: "numeric"` form. The full title, trigger name, grid name, and live text remain complete, while the compact title and entire pager lanes are excluded from accessibility. Agenda title and supporting event content span the row beneath marker and time so long text keeps useful measure. - **At or below `20rem`:** Previous/Next and the month title occupy the first row, Today the second, and application content the third. Picker fields stack, the status panel becomes one column, and narrow weekday labels replace short labels without changing their full accessible names. These are additional structural changes; month/year abbreviation already follows the below-`24rem` rule. @@ -396,23 +398,9 @@ Today is a primary-filled circular number with its own border. Selection colors ### Events and agenda -Grid events are compact lavender slips with a violet outline, magenta -logical-leading rule, preserved marker slot, and ellipsized text slots. At -compact widths the first action uses a marker-only presentation within the full -compact target. Its relationship to the day-level overflow cue, track sizing, -stacking, markerless total, and `maxGridEventsPerDay: 0` fallback is defined -once in the [responsive model](#responsive-model). - -The compact cue is a bare semibold locale-aware number with no pill, background, -border, or shadow. It shows the signed additional count when the primary visual -represents one event and the unsigned total when no primary visual exists. The -package owns placement according to `gridEventPlacement`; no overflow-layout -selector or CSS token is public, and render hooks replace only visual content -within the assigned block. Accessible -naming and focus behavior follow the -[interaction model](ACCESSIBILITY.md#interaction-model). - -Wide grid overflow remains an explicit native count/action rather than silently dropping events. `renderEventOverflow` may replace applicable pre-rendered compact and wide visual content through one discriminated hook; `context.text` supplies the package-formatted visual fallback, and mandatory native-action variants retain that fallback for `null` or invalid output. Custom content cannot replace the action, accessible label, target geometry, or agenda-focus behavior. +Grid summaries use the event palette, logical-leading accent rule, marker slot, and ellipsized text. Total-count buttons use the same event palette with a visible border and no leading accent. Compact count buttons retain the full compact control target; wide counts display localized text such as “3 events.” The [responsive model](#responsive-model) defines when count and individual-event presentations apply. + +The legacy passive compact overflow cue remains a bare semibold number. The package owns placement according to `gridEventPlacement`; render hooks replace only noninteractive visual content within the assigned block. `renderEventOverflow` exposes `display: "count" | "overflow"` in addition to the compact/wide variant. Count contexts set `visibleEventCount: 0` and `overflowCount: eventCount`. Mandatory actions keep their fallback for `null` or invalid output. The [interaction model](ACCESSIBILITY.md#interaction-model) defines application override and focus ownership. The agenda is a rounded surface panel immediately below the grid. Its ordered event rows reuse the event palette and visually align leading content, localized time, title, supporting details, and application trailing content. The canonical action and render-hook semantics are defined by the [interaction model](ACCESSIBILITY.md#interaction-model) and [render-hook API](docs/api.md#customize-rendering-calendarrenderhooks). diff --git a/examples/advanced/main.ts b/examples/advanced/main.ts index 439edc4..5a214c4 100644 --- a/examples/advanced/main.ts +++ b/examples/advanced/main.ts @@ -80,6 +80,8 @@ const ADVANCED_MESSAGES = Object.freeze({ renderHookErrorMessage: "Some schedule details could not be displayed.", renderHookErrorTitle: "Some schedule details are unavailable", gridEventInstructions: "Use arrow keys to move between dates and Enter or Space to select. Press F2 on a date to move to its visible event actions; use Up and Down Arrow between actions, and Escape or F2 to return.", + gridEventCount: "{count} {eventLabel}", + gridEventCountLabel: "View {count} {eventLabel} for {date}", gridMore: "{count} additional", gridMoreLabel: "View {count} more {eventLabel} for {date}", internalErrorMessage: "The schedule encountered an unexpected error.", @@ -629,6 +631,7 @@ const advancedRenderHooks = Object.freeze({ }, renderEventOverflow: ({ dateString, + display, document: ownerDocument, elements, eventCount, @@ -641,6 +644,7 @@ const advancedRenderHooks = Object.freeze({ const content = ownerDocument.createElement("span"); content.className = `my-event-overflow-${variant}`; content.dataset["testDate"] = dateString; + content.dataset["testDisplay"] = display; content.dataset["testEventCount"] = String(eventCount); content.dataset["testOverflowCount"] = String(overflowCount); content.dataset["testSurface"] = surface; @@ -674,6 +678,8 @@ const calendarOptions = { extensions: [webMcp({ toolNamePrefix: "my-schedule" })], fallbackElement, firstDay: 1, + //Explicitly demonstrate the package defaults; use "count" to include single-item dates too. + gridEventDisplay: { compact: "count-when-multiple", wide: "events" }, gridEventPlacement: "bottom", headingLevel: 3, icons: { @@ -748,6 +754,10 @@ const calendarOptions = { `Event menu for ${event.title} on ${dateString} from ${surface} with ${nativeEvent.type} on ${element.localName} at ${clientX}, ${clientY}.` ); }, + onEventOverflowActivate: ({ dateString, element, eventCount, nativeEvent }) => { + //Observing activation preserves the default agenda behavior; cancellation belongs to an app-owned chooser. + reportAction(`Viewing ${String(eventCount)} items on ${dateString} with ${nativeEvent.type} on ${element.localName}.`); + }, onStateChange: updateState, renderHooks: [advancedRenderHooks], sourceEventLimit: 100, diff --git a/src/index.ts b/src/index.ts index b2408fe..75aa214 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ export type { CalendarEventContextMenuAvailability, CalendarEventElements, CalendarEventOverflowContext, + CalendarEventOverflowActivation, CalendarEventOverflowElements, CalendarEventRenderContext, CalendarEventSource, @@ -40,6 +41,7 @@ export type { CalendarEvents, CalendarExtension, CalendarGridEventPlacement, + CalendarGridEventDisplay, CalendarRenderCleanup, CalendarRenderContext, CalendarRenderHooks, diff --git a/src/internal/dom/grid-focus.ts b/src/internal/dom/grid-focus.ts index b8ed69a..22a90a3 100644 --- a/src/internal/dom/grid-focus.ts +++ b/src/internal/dom/grid-focus.ts @@ -54,12 +54,8 @@ export function enterGridActions( host: HTMLElement ): void { const actions = elements.gridActionsByDate.get(dateString) ?? []; - const firstAction = actions.find((action) => action.isConnected && host.contains(action)); - if (firstAction === undefined) { - return; - } setDayProxyTabStop(dateString, elements); - firstAction.focus({ preventScroll: true }); + focusFirstEligible(actions, host); } /** Handles action-mode movement and returns whether the key was consumed. */ @@ -94,11 +90,10 @@ export function handleGridActionKeydown( return false; } event.preventDefault(); - const targetIndex = actionIndex + (event.key === "ArrowDown" ? 1 : -1); - const target = actions[targetIndex]; - if (target !== undefined) { - target.focus({ preventScroll: true }); - } + const candidates = event.key === "ArrowDown" + ? actions.slice(actionIndex + 1) + : actions.slice(0, actionIndex).reverse(); + focusFirstEligible(candidates, host); return true; } @@ -167,7 +162,34 @@ export function restoreCalendarFocus( if (token.date !== undefined && (isGridActionToken(token) || (element === null && dateFallback !== null))) { setDayProxyTabStop(token.date, elements); } + focusRestorationTarget(target, token.date, elements, host); +} + +function focusRestorationTarget( + target: HTMLElement, + date: string | undefined, + elements: Readonly, + host: HTMLElement +): void { target.focus({ preventScroll: true }); + if (target.ownerDocument.activeElement !== target && date !== undefined && + !focusFirstEligible(elements.gridActionsByDate.get(date) ?? [], host)) { + elements.dayButtons.get(date)?.focus({ preventScroll: true }); + } +} + +/** Lets the browser reject CSS-hidden controls without duplicating responsive layout decisions. */ +function focusFirstEligible(actions: readonly CalendarEventActionElement[], host: HTMLElement): boolean { + for (const action of actions) { + if (!action.isConnected || !host.contains(action)) { + continue; + } + action.focus({ preventScroll: true }); + if (action.ownerDocument.activeElement === action) { + return true; + } + } + return false; } function setDayProxyTabStop(dateString: string, elements: Readonly): void { diff --git a/src/internal/runtime/configuration.ts b/src/internal/runtime/configuration.ts index f5ead67..660b62b 100644 --- a/src/internal/runtime/configuration.ts +++ b/src/internal/runtime/configuration.ts @@ -5,6 +5,7 @@ import type { CalendarIcons } from "../../icons.js"; import type { CalendarEventTimeDisplay, CalendarGridEventPlacement, + CalendarGridEventDisplay, CalendarOptions, CalendarWeekRowSizing } from "../../types.js"; @@ -24,6 +25,7 @@ const CALENDAR_OPTION_SCHEMA = Object.freeze({ events: "value", extensions: "value", gridEventPlacement: "value", + gridEventDisplay: "value", renderHooks: "value", fallbackElement: "value", firstDay: "value", @@ -42,6 +44,7 @@ const CALENDAR_OPTION_SCHEMA = Object.freeze({ onDaySelect: "callback", onError: "callback", onEventActivate: "callback", + onEventOverflowActivate: "callback", onEventContextMenu: "callback", onStateChange: "callback", sourceEventLimit: "value", @@ -62,7 +65,7 @@ export type CalendarOptionsSnapshot = Readonly< CalendarOptions & Required, "gridEventPlacement" | "weekRowSizing" - >> + >> & { readonly gridEventDisplay: Readonly<{ compact: CalendarGridEventDisplay; wide: CalendarGridEventDisplay }> } >; export function createConfigurationError(message: string, cause?: unknown): LitefoldCalendarError { @@ -163,6 +166,7 @@ export function snapshotCalendarOptions( } normalizeEventTimeDisplay(snapshot["eventTimeDisplay"]); snapshot["gridEventPlacement"] = normalizeGridEventPlacement(snapshot["gridEventPlacement"]); + snapshot["gridEventDisplay"] = normalizeGridEventDisplay(snapshot["gridEventDisplay"]); snapshot["weekRowSizing"] = normalizeWeekRowSizing(snapshot["weekRowSizing"]); return Object.freeze(snapshot) as CalendarOptionsSnapshot; } @@ -196,6 +200,29 @@ export function normalizeEventTimeDisplay(value: unknown): CalendarEventTimeDisp return value; } +/** Resolves and snapshots independent container-size presentation settings. */ +export function normalizeGridEventDisplay( + value: unknown +): Readonly<{ compact: CalendarGridEventDisplay; wide: CalendarGridEventDisplay }> { + if (value !== undefined && !isConfigurationRecord(value)) { + throw createConfigurationError("gridEventDisplay must be an object when supplied."); + } + if (value !== undefined) { + assertKnownConfigurationKeys(value, new Set(["compact", "wide"]), "gridEventDisplay"); + } + const resolve = (variant: "compact" | "wide", fallback: CalendarGridEventDisplay): CalendarGridEventDisplay => { + const mode = value === undefined ? undefined : readConfigurationValue(value, variant, `gridEventDisplay.${variant}`); + if (mode === undefined) { + return fallback; + } + if (mode !== "events" && mode !== "count" && mode !== "count-when-multiple") { + throw createConfigurationError(`gridEventDisplay.${variant} must be "events", "count", or "count-when-multiple".`); + } + return mode; + }; + return Object.freeze({ compact: resolve("compact", "count-when-multiple"), wide: resolve("wide", "events") }); +} + /** Resolves vertical placement of the complete event stack within each month-grid day cell. */ export function normalizeGridEventPlacement(value: unknown): CalendarGridEventPlacement { if (value === undefined) { diff --git a/src/internal/runtime/coordinator.ts b/src/internal/runtime/coordinator.ts index 9c38914..376d8bb 100644 --- a/src/internal/runtime/coordinator.ts +++ b/src/internal/runtime/coordinator.ts @@ -52,7 +52,8 @@ import { import { createDayCellElements, renderMonthWeeks, renderWeekdayHeadings } from "../dom/month-grid.js"; import { DayGridActionCollector, - installGridOverflowActionListeners + installGridOverflowActionListeners, + setDayActionShortcuts } from "./day-grid-actions.js"; import { resolveCalendarIcons } from "./icon-configuration.js"; import { IntegrationNodeController } from "./integration-nodes.js"; @@ -388,6 +389,7 @@ export class MonthCalendar implements Calendar { this.eventOverflowPresenter = new CalendarEventOverflowPresenter({ document: this.document, gridEventLimit: this.gridEventLimit, + gridEventDisplay: resolvedOptions.gridEventDisplay, locale, messages: this.messages, numberFormatter: this.numberFormatter @@ -824,13 +826,7 @@ export class MonthCalendar implements Calendar { const accessibleLabel = isAllowed ? this.eventText.getDayAccessibleLabel(fullDateText, events.length) : fullDateText; - const { - badge, - button, - cell, - number, - summaries - } = createDayCellElements({ + const { badge, button, cell, number, summaries } = createDayCellElements({ accessibleLabel, dateString, dayNumber: this.dayFormatter.format(toUtcDate(date)), @@ -888,8 +884,13 @@ export class MonthCalendar implements Calendar { const { button: gridMore } = eventOverflow.grid; installGridOverflowActionListeners({ action: gridMore, + date, + events: () => events.map((event) => event.event), + invokeAction: (action) => { this.invokeAction("onEventOverflowActivate", action); }, isCurrent: () => this.canUseRenderedAction(gridMore, renderGeneration), - onActivate: () => { + onActivate: this.options.onEventOverflowActivate, + onDefault: () => { + if (this.bounds.getDateNavigationFailure(date) !== null) { return; } this.selectDate(date, "gridMore"); if (this.canContinueInteraction()) { this.dom?.agendaTitle.focus({ preventScroll: true }); @@ -901,11 +902,9 @@ export class MonthCalendar implements Calendar { } }); } - if (eventOverflow.compact !== null) { - this.renderHookVisuals.renderEventOverflow(eventOverflow.compact); - if (!this.isRenderGenerationCurrent(renderGeneration)) { - return cell; - } + this.renderHookVisuals.renderEventOverflow(eventOverflow.compact); + if (!this.isRenderGenerationCurrent(renderGeneration)) { + return cell; } if (eventOverflow.grid !== null) { const { button: gridMore, wide } = eventOverflow.grid; @@ -949,13 +948,7 @@ export class MonthCalendar implements Calendar { } this.handleDayKeydown(event, date, button); }); - const dayShortcuts = [ - ...(gridActionSnapshot.length > 0 ? ["F2"] : []), - ...(this.options.onDayContextMenu === undefined ? [] : ["Shift+F10"]) - ]; - if (dayShortcuts.length > 0) { - button.setAttribute("aria-keyshortcuts", dayShortcuts.join(" ")); - } + setDayActionShortcuts(button, gridActionSnapshot.length > 0, this.options.onDayContextMenu !== undefined); if (this.options.onDayContextMenu !== undefined) { button.addEventListener("contextmenu", (jsEvent) => { if (jsEvent.defaultPrevented || !this.canUseRenderedAction(button, renderGeneration) || diff --git a/src/internal/runtime/day-grid-actions.ts b/src/internal/runtime/day-grid-actions.ts index c7c5bb3..1957b87 100644 --- a/src/internal/runtime/day-grid-actions.ts +++ b/src/internal/runtime/day-grid-actions.ts @@ -1,20 +1,55 @@ -import type { CalendarEventActionElement } from "../../types.js"; +import type { + CalendarAction, + CalendarDate, + CalendarEvent, + CalendarEventActionElement, + CalendarEventOverflowActivation +} from "../../types.js"; +import { formatCalendarDate } from "../domain/civil-date.js"; import type { EventRepresentationElements } from "../dom/event-representation.js"; -interface GridOverflowActionListenerOptions { +/** Advertises only keyboard actions installed on this day proxy. */ +export function setDayActionShortcuts(button: HTMLButtonElement, hasEvents: boolean, hasContext: boolean): void { + const shortcuts = [...(hasEvents ? ["F2"] : []), ...(hasContext ? ["Shift+F10"] : [])]; + if (shortcuts.length > 0) { + button.setAttribute("aria-keyshortcuts", shortcuts.join(" ")); + } +} + +interface GridOverflowActionListenerOptions { readonly action: HTMLButtonElement; + readonly date: CalendarDate; + readonly events: () => readonly CalendarEvent[]; + readonly invokeAction: (action: () => unknown) => void; readonly isCurrent: () => boolean; - readonly onActivate: () => void; + readonly onActivate: CalendarAction> | undefined; + readonly onDefault: () => void; readonly onKeydown: (event: KeyboardEvent) => void; } /** Installs package-owned overflow behavior before consumer visual hooks inspect the action. */ -export function installGridOverflowActionListeners( - options: Readonly +export function installGridOverflowActionListeners( + options: Readonly> ): void { - options.action.addEventListener("click", () => { - if (options.isCurrent()) { - options.onActivate(); + options.action.addEventListener("click", (event) => { + if (!options.isCurrent()) { + return; + } + const onActivate = options.onActivate; + if (onActivate !== undefined) { + const events = Object.freeze([...options.events()]); + const context = Object.freeze({ + date: Object.freeze({ ...options.date }), + dateString: formatCalendarDate(options.date), + element: options.action, + eventCount: events.length, + events, + nativeEvent: event + }); + options.invokeAction(() => onActivate(context)); + } + if (!event.defaultPrevented && options.isCurrent()) { + options.onDefault(); } }, { capture: true }); options.action.addEventListener("keydown", (event) => { diff --git a/src/internal/runtime/event-overflow-presentation.ts b/src/internal/runtime/event-overflow-presentation.ts index ea9f3b2..48f43cd 100644 --- a/src/internal/runtime/event-overflow-presentation.ts +++ b/src/internal/runtime/event-overflow-presentation.ts @@ -1,5 +1,5 @@ import { formatCalendarMessage, type CalendarMessages } from "../../messages.js"; -import type { CalendarDate } from "../../types.js"; +import type { CalendarDate, CalendarGridEventDisplay } from "../../types.js"; import type { EventRepresentationElements } from "../dom/event-representation.js"; import { createEventOverflowElements } from "../dom/event-overflow.js"; @@ -13,6 +13,7 @@ const COMPACT_NUMBER_FORMAT_OPTIONS = Object.freeze({ interface EventOverflowPresenterOptions { readonly document: Document; readonly gridEventLimit: number; + readonly gridEventDisplay: Readonly<{ compact: CalendarGridEventDisplay; wide: CalendarGridEventDisplay }>; readonly locale: string | undefined; readonly messages: Readonly; readonly numberFormatter: Intl.NumberFormat; @@ -29,13 +30,14 @@ interface PrepareDayEventOverflowOptions { interface PreparedGridOverflow { readonly button: HTMLButtonElement; - readonly wide: Readonly; + readonly wide: Readonly | null; } interface PreparedEventOverflowVariantBase { readonly content: HTMLElement; readonly date: CalendarDate; readonly dateString: string; + readonly display: "overflow" | "count"; readonly eventCount: number; readonly overflowCount: number; readonly root: HTMLElement; @@ -78,20 +80,54 @@ export class CalendarEventOverflowPresenter { public prepareAndPlace( options: Readonly ): Readonly { + const compactCount = this.showsCount("compact", options.eventCount); + const wideCount = this.showsCount("wide", options.eventCount); + options.summaries.setAttribute("data-lfc-compact-display", compactCount ? "count" : "events"); + options.summaries.setAttribute("data-lfc-wide-display", wideCount ? "count" : "events"); options.compactPrimary?.root.classList.add("lfc-is-compact-primary"); const grid = this.createGridOverflow(options); - const compact = this.createCompactOverflow(options, grid?.button ?? null); - if (grid !== null) { + const compactAction = compactCount || options.eventCount > this.options.gridEventLimit + ? grid?.button ?? null : null; + const compact = this.createCompactOverflow(options, compactAction); + if (grid !== null && grid.wide !== null) { grid.button.append(grid.wide.root); } + grid?.button.classList.toggle("lfc-has-wide-overflow", grid.wide !== null); return Object.freeze({ compact, grid }); } + private showsCount(variant: "compact" | "wide", eventCount: number): boolean { + const mode = this.options.gridEventDisplay[variant]; + return eventCount > 0 && (mode === "count" || (mode === "count-when-multiple" && eventCount > 1)); + } + private createCompactOverflow( options: Readonly, gridAction: HTMLButtonElement | null ): Readonly | null { - if (options.eventCount <= 1 && (gridAction === null || options.compactPrimary !== null)) { + if (this.showsCount("compact", options.eventCount) && gridAction !== null) { + const text = this.getCompactNumberFormatter(false).format(options.eventCount); + const overflow = createEventOverflowElements(this.options.document, "compact", text); + overflow.root.classList.add("lfc-is-count"); + gridAction.classList.add("lfc-is-compact-primary"); + gridAction.append(overflow.root); + return Object.freeze({ + action: gridAction, + content: overflow.content, + date: options.date, + dateString: options.dateString, + display: "count", + eventCount: options.eventCount, + overflowCount: options.eventCount, + placementBoundary: null, + root: overflow.root, + text, + variant: "compact", + visibleEventCount: 0 + }); + } + if (options.eventCount <= 1 && + (options.eventCount <= this.options.gridEventLimit || options.compactPrimary !== null)) { return null; } const visibleEventCount = options.compactPrimary !== null && @@ -108,6 +144,7 @@ export class CalendarEventOverflowPresenter { content: overflow.content, date: options.date, dateString: options.dateString, + display: "overflow", eventCount: options.eventCount, overflowCount, placementBoundary: action === null @@ -123,7 +160,10 @@ export class CalendarEventOverflowPresenter { private createGridOverflow( options: Readonly ): Readonly | null { - if (options.eventCount <= this.options.gridEventLimit) { + const compactCount = this.showsCount("compact", options.eventCount); + const wideCount = this.showsCount("wide", options.eventCount); + const hasOverflow = options.eventCount > this.options.gridEventLimit; + if (!compactCount && !wideCount && !hasOverflow) { return null; } const button = this.options.document.createElement("button"); @@ -133,25 +173,35 @@ export class CalendarEventOverflowPresenter { button.setAttribute("aria-keyshortcuts", "F2"); button.setAttribute("data-lfc-date", options.dateString); button.classList.toggle("lfc-is-compact-primary", options.compactPrimary === null); + button.classList.toggle("lfc-is-wide-count-only", wideCount && !compactCount && !hasOverflow); - const overflowCount = options.eventCount - this.options.gridEventLimit; + const overflowCount = wideCount ? options.eventCount : Math.max(0, options.eventCount - this.options.gridEventLimit); const formattedCount = this.options.numberFormatter.format(overflowCount); - const text = formatCalendarMessage(this.options.messages.gridMore, { count: formattedCount }); - const wide = createEventOverflowElements(this.options.document, "wide", text); - button.setAttribute("aria-label", formatCalendarMessage(this.options.messages.gridMoreLabel, { + const text = formatCalendarMessage(wideCount ? this.options.messages.gridEventCount : this.options.messages.gridMore, { count: formattedCount, + eventLabel: overflowCount === 1 ? this.options.messages.event : this.options.messages.events + }); + const wide = createEventOverflowElements(this.options.document, "wide", text); + wide.root.classList.toggle("lfc-is-count", wideCount); + const accessibleCount = compactCount || wideCount ? options.eventCount : overflowCount; + const accessibleLabel = formatCalendarMessage( + compactCount || wideCount ? this.options.messages.gridEventCountLabel : this.options.messages.gridMoreLabel, { + count: this.options.numberFormatter.format(accessibleCount), date: options.fullDateText, - eventLabel: overflowCount === 1 + eventLabel: accessibleCount === 1 ? this.options.messages.event : this.options.messages.events - })); + }); + button.setAttribute("aria-label", compactCount && !wideCount && hasOverflow + ? `${accessibleLabel}, ${text}` : accessibleLabel); return Object.freeze({ button, - wide: Object.freeze({ + wide: !wideCount && !hasOverflow ? null : Object.freeze({ action: button, content: wide.content, date: options.date, dateString: options.dateString, + display: wideCount ? "count" : "overflow", eventCount: options.eventCount, overflowCount, root: wide.root, diff --git a/src/internal/runtime/message-configuration.ts b/src/internal/runtime/message-configuration.ts index d4f4b78..9198ade 100644 --- a/src/internal/runtime/message-configuration.ts +++ b/src/internal/runtime/message-configuration.ts @@ -26,6 +26,8 @@ const CALENDAR_MESSAGE_TOKENS = Object.freeze({ renderHookErrorMessage: [], renderHookErrorTitle: [], gridMore: ["count"], + gridEventCount: ["count", "eventLabel"], + gridEventCountLabel: ["count", "date", "eventLabel"], gridEventInstructions: [], gridMoreLabel: ["count", "date", "eventLabel"], internalErrorMessage: [], diff --git a/src/internal/runtime/render-hook-visuals.ts b/src/internal/runtime/render-hook-visuals.ts index dc4d57d..684ca5b 100644 --- a/src/internal/runtime/render-hook-visuals.ts +++ b/src/internal/runtime/render-hook-visuals.ts @@ -132,8 +132,8 @@ export class RenderHookVisualRenderer { } /** Renders one pre-rendered compact or wide overflow variant through its singleton hook. */ - public renderEventOverflow(options: Readonly): void { - if (this.options.isDestroyed()) { + public renderEventOverflow(options: Readonly | null): void { + if (options === null || this.options.isDestroyed()) { return; } const runtime = this.options.renderHooks.find((candidate) => @@ -151,6 +151,7 @@ export class RenderHookVisualRenderer { const context = Object.freeze({ date: Object.freeze({ ...options.date }), dateString: options.dateString, + display: options.display, document: this.options.document, elements: Object.freeze({ action: options.action, diff --git a/src/messages.ts b/src/messages.ts index bbc4849..63d32e4 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -30,6 +30,10 @@ export interface CalendarMessages { readonly renderHookErrorTitle: string; /** Grid overflow template. Supports `{count}`. */ readonly gridMore: string; + /** Total-count button template. Supports `{count}` and `{eventLabel}`. */ + readonly gridEventCount: string; + /** Accessible total-count label. Supports `{count}`, `{date}`, and `{eventLabel}`. */ + readonly gridEventCountLabel: string; /** Instructions for entering and leaving visible event actions in the managed grid. */ readonly gridEventInstructions: string; /** Accessible grid-overflow label. Supports `{count}`, `{date}`, and `{eventLabel}`. */ @@ -96,6 +100,8 @@ export const DEFAULT_CALENDAR_MESSAGES: Readonly = Object.free renderHookErrorMessage: "Some calendar details could not be displayed.", renderHookErrorTitle: "Some details are unavailable", gridMore: "{count} more", + gridEventCount: "{count} {eventLabel}", + gridEventCountLabel: "View {count} {eventLabel} for {date}", gridEventInstructions: "Use arrow keys to move between dates and Enter or Space to select. Press F2 on a date to move to its visible event actions; use Up and Down Arrow between actions, and Escape or F2 to return.", gridMoreLabel: "View {count} more {eventLabel} for {date}", internalErrorMessage: "The calendar encountered an unexpected error.", diff --git a/src/styles/month-grid.css b/src/styles/month-grid.css index 1a1a49a..c1a7e8f 100644 --- a/src/styles/month-grid.css +++ b/src/styles/month-grid.css @@ -266,6 +266,10 @@ min-inline-size: 0; } + :where(.litefold-calendar) .lfc-calendar-event-overflow.lfc-is-count { + color: inherit; + } + :where(.litefold-calendar) .lfc-calendar-event-overflow.lfc-has-custom-event-overflow .lfc-event-overflow-default-content { @@ -362,4 +366,12 @@ min-inline-size: 0; overflow-wrap: anywhere; } + + :where(.litefold-calendar) .lfc-calendar-grid-more:has(.lfc-calendar-event-overflow.lfc-is-count) { + background: var(--lfc-event-background); + border: var(--lfc-internal-hairline) solid var(--lfc-event-border-color); + border-radius: var(--lfc-border-radius); + color: var(--lfc-event-color); + } + } diff --git a/src/styles/responsive.css b/src/styles/responsive.css index 89bab77..17aed84 100644 --- a/src/styles/responsive.css +++ b/src/styles/responsive.css @@ -235,6 +235,12 @@ display: none; } + :where(.litefold-calendar) + .lfc-calendar-grid-more.lfc-is-wide-count-only:focus + .lfc-calendar-event-overflow.lfc-is-wide { + display: inline-flex; + } + :where(.litefold-calendar) .lfc-calendar-grid-more:not(.lfc-is-compact-primary):focus > .lfc-calendar-event-overflow.lfc-is-wide:not( @@ -288,6 +294,35 @@ .lfc-calendar-grid-more.lfc-is-compact-primary { aspect-ratio: 1; } + + /* A focused summary survives resizing; hidden summaries reject new focus entry. */ + :where(.litefold-calendar) .lfc-calendar-grid-more.lfc-is-wide-count-only:not(:focus), + :where(.litefold-calendar) + .lfc-calendar-day-summaries[data-lfc-compact-display="count"] + .lfc-calendar-event-summary:not(:focus) { + display: none; + } + + :where(.litefold-calendar) + .lfc-calendar-day-summaries[data-lfc-compact-display="count"] + .lfc-calendar-grid-more { + grid-row: auto; + max-inline-size: 100%; + } + + :where(.litefold-calendar) + .lfc-calendar-day-summaries[data-lfc-compact-display="count"] + .lfc-calendar-event-summary.lfc-is-compact-primary:focus { + aspect-ratio: auto; + inline-size: 100%; + } + + :where(.litefold-calendar) + .lfc-calendar-day-summaries[data-lfc-compact-display="count"] + .lfc-calendar-event-summary.lfc-is-compact-primary:focus + > .lfc-calendar-event-title { + display: block; + } } @container lfc-calendar (inline-size < 24rem) { @@ -406,6 +441,20 @@ } @container lfc-calendar (inline-size > 42rem) { + :where(.litefold-calendar) + .lfc-calendar-day-summaries[data-lfc-wide-display="count"] + .lfc-calendar-event-summary:not(:focus), + :where(.litefold-calendar) + .lfc-calendar-grid-more:not(.lfc-has-wide-overflow, :focus) { + display: none; + } + + :where(.litefold-calendar) + .lfc-calendar-grid-more:not(.lfc-has-wide-overflow):focus + .lfc-calendar-event-overflow.lfc-is-compact { + display: inline-flex; + } + :where(.litefold-calendar) .lfc-calendar-toolbar-end { flex: 0 1 auto; margin-inline-start: auto; diff --git a/src/types.ts b/src/types.ts index 1c5733b..23aca3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -169,6 +169,9 @@ export type CalendarEventTimeDisplay = "all" | "grid" | "agenda" | "none"; /** Vertical placement of the event stack within a month-grid day cell. */ export type CalendarGridEventPlacement = "top" | "center" | "bottom"; +/** Presentation of each day's events at one calendar-container size. */ +export type CalendarGridEventDisplay = "events" | "count" | "count-when-multiple"; + /** Package-owned element references supplied for event-overflow inspection and placement. */ export interface CalendarEventOverflowElements< TAction extends HTMLButtonElement | null = HTMLButtonElement | null @@ -183,6 +186,8 @@ export interface CalendarEventOverflowElements< /** Values supplied when rendering a compact event-overflow cue. */ export interface CalendarCompactEventOverflowContext extends CalendarRenderContext<"day"> { + /** Whether this visual describes hidden occurrences or the total count for the day. */ + readonly display: "overflow" | "count"; /** Structured Gregorian date for the rendered day. */ readonly date: CalendarDate; /** Strict `YYYY-MM-DD` form of `date`. */ @@ -203,6 +208,8 @@ export interface CalendarCompactEventOverflowContext extends CalendarRenderConte /** Values supplied when rendering wide content for the native grid-overflow action. */ export interface CalendarWideEventOverflowContext extends CalendarRenderContext<"grid-summary"> { + /** Whether this visual describes hidden occurrences or the total count for the day. */ + readonly display: "overflow" | "count"; /** Structured Gregorian date for the rendered day. */ readonly date: CalendarDate; /** Strict `YYYY-MM-DD` form of `date`. */ @@ -334,6 +341,22 @@ export interface CalendarEventActivation { readonly surface: CalendarEventSurface; } +/** Day-count or overflow activation, delivered before calendar selection or DOM replacement. */ +export interface CalendarEventOverflowActivation { + /** Immutable structured occurrence date represented by the action. */ + readonly date: CalendarDate; + /** Strict `YYYY-MM-DD` form of `date`. */ + readonly dateString: string; + /** Current native count or overflow button; applications own focus when cancelling. */ + readonly element: HTMLButtonElement; + /** Immutable ordered snapshot of all normalized occurrences on the day; metadata remains opaque. */ + readonly events: readonly CalendarEvent[]; + /** Total occurrence count, independent of summary limits and agenda pagination. */ + readonly eventCount: number; + /** Call `preventDefault()` synchronously to replace the default agenda interaction. */ + readonly nativeEvent: MouseEvent; +} + /** Information supplied for an application context action on an event occurrence. */ export interface CalendarEventContextMenu extends Omit, "nativeEvent"> { @@ -412,6 +435,13 @@ export interface CalendarOptions { readonly eventTimeDisplay?: CalendarEventTimeDisplay; /** Vertical placement of the month-grid event stack; defaults to `"top"`. */ readonly gridEventPlacement?: CalendarGridEventPlacement; + /** Container-based presentation; defaults to compact `count-when-multiple` and wide `events`. */ + readonly gridEventDisplay?: { + /** Presentation at calendar-container widths at or below `42rem`. */ + readonly compact?: CalendarGridEventDisplay; + /** Presentation at calendar-container widths above `42rem`. */ + readonly wide?: CalendarGridEventDisplay; + }; /** Synchronous bridge to an application-owned live announcer. */ readonly onAnnounce?: (this: void, announcement: Readonly) => undefined; /** Handles pointer or keyboard context gestures on a day. */ @@ -420,6 +450,8 @@ export interface CalendarOptions { readonly onDaySelect?: CalendarAction; /** Handles activation of a native event link or button on either rendering surface. */ readonly onEventActivate?: CalendarAction>; + /** Count/overflow action before selection; synchronous cancellation transfers interaction ownership. */ + readonly onEventOverflowActivate?: CalendarAction>; /** Handles context gestures and primary activation when context is an unlinked occurrence's only action. */ readonly onEventContextMenu?: CalendarAction>; /** Immediate static events or an abort-aware provider whose return shape selects each request's timing. */ diff --git a/tests/calendar-core-features.test.ts b/tests/calendar-core-features.test.ts index 8826292..dc2d88a 100644 --- a/tests/calendar-core-features.test.ts +++ b/tests/calendar-core-features.test.ts @@ -269,6 +269,7 @@ void test("F2 action mode is non-wrapping and exits to the day proxy or agenda h initialDate: "2026-07-14", locale: "ar-EG", maxGridEventsPerDay: 2, + gridEventDisplay: { compact: "events" }, messages: { gridMoreLabel: "Open {count} {eventLabel} on {date}" }, onDaySelect: () => { daySelections += 1; }, onEventActivate: (activation) => { activation.nativeEvent.preventDefault(); } diff --git a/tests/calendar-event-overflow-integrity.test.ts b/tests/calendar-event-overflow-integrity.test.ts index 24082fd..9f864a5 100644 --- a/tests/calendar-event-overflow-integrity.test.ts +++ b/tests/calendar-event-overflow-integrity.test.ts @@ -22,6 +22,7 @@ for (const mutation of [ const errors: LitefoldCalendarError[] = []; let originalActionLabel: string | null = null; const calendar = createCalendar(host, { + gridEventDisplay: { compact: "events" }, events: eventsForDate("2026-07-14", 2, `package-mutation-${mutation}`), initialDate: "2026-07-14", maxGridEventsPerDay: 1, @@ -63,6 +64,7 @@ for (const mutation of ["remove cluster", "mutate summaries", "mutate day cell"] const errors: LitefoldCalendarError[] = []; let placementReached = false; const calendar = createCalendar(host, { + gridEventDisplay: { compact: "events" }, events: eventsForDate("2026-07-14", 2, `compact-placement-${mutation}`), initialDate: "2026-07-14", maxGridEventsPerDay: 1, @@ -128,6 +130,7 @@ for (const mutation of ["overflow attributes", "fallback text node value"] as co let originalActionLabel: string | null = null; let packageTextReached = false; const calendar = createCalendar(host, { + gridEventDisplay: { compact: "events" }, events: eventsForDate("2026-07-14", 2, `connected-${mutation}`), initialDate: "2026-07-14", maxGridEventsPerDay: 1, @@ -182,6 +185,7 @@ void test("later event hooks may mutate earlier consumer-owned output", async (c const errors: LitefoldCalendarError[] = []; let earlierOutput: HTMLElement | null = null; const calendar = createCalendar(host, { + gridEventDisplay: { compact: "events" }, events: [event("consumer-owned", "2026-07-14T09:00", "Consumer owned")], initialDate: "2026-07-14", onError: (error) => { errors.push(error); }, @@ -228,6 +232,7 @@ void test("native grid-overflow activation precedes action-backed compact consum let consumerClicks = 0; let consumerKeydowns = 0; const calendar = createCalendar(host, { + gridEventDisplay: { compact: "events" }, events: eventsForDate("2026-07-15", 2, "listener-order"), initialDate: "2026-07-14", maxGridEventsPerDay: 0, @@ -292,6 +297,7 @@ void test("a rejected renderEventOverflow Promise is observed, quarantined, and process.on("unhandledRejection", trackUnhandledRejection); context.after(() => { process.off("unhandledRejection", trackUnhandledRejection); }); const calendar = createCalendar(host, { + gridEventDisplay: { compact: "events" }, events: eventsForDate("2026-07-14", 2, "asynchronous-overflow"), initialDate: "2026-07-14", maxGridEventsPerDay: 1, diff --git a/tests/calendar-event-overflow.test.ts b/tests/calendar-event-overflow.test.ts index 72d0240..1379a0f 100644 --- a/tests/calendar-event-overflow.test.ts +++ b/tests/calendar-event-overflow.test.ts @@ -27,6 +27,7 @@ void test("compact overflow text uses locale-aware social number formatting", as const compactTexts = new Map(); const calendar = createCalendar(host, { events, + gridEventDisplay: { compact: "events" }, initialDate: "2026-07-14", locale, maxGridEventsPerDay: 1, @@ -77,6 +78,7 @@ void test("multi-day occurrences contribute to each day's adaptive overflow coun const { host } = setupDom(context); const compactCounts = new Map(); const calendar = createCalendar(host, { + gridEventDisplay: { compact: "events" }, events: [ { end: "2026-07-17", id: "spanning", start: "2026-07-14", title: "Spanning" }, event("day-14", "2026-07-14T09:00", "Day 14"), @@ -120,6 +122,7 @@ void test("removed split overflow hook names are rejected as unknown configurati ] as const) { assert.throws( () => createCalendar(host, { + gridEventDisplay: { compact: "events" }, events: [], renderHooks: [{ id: `removed-${removedHook}`, [removedHook]: () => null }] }), @@ -143,6 +146,7 @@ void test("cross-variant quarantine restores default content removed during conn ); let captured: LitefoldCalendarError | undefined; const calendar = createCalendar(host, { + gridEventDisplay: { compact: "events" }, events: eventsForDate("2026-07-14", 2, "restore-default"), initialDate: "2026-07-14", maxGridEventsPerDay: 1, diff --git a/tests/calendar-grid-event-display.test.ts b/tests/calendar-grid-event-display.test.ts new file mode 100644 index 0000000..effb51a --- /dev/null +++ b/tests/calendar-grid-event-display.test.ts @@ -0,0 +1,366 @@ +import assert from "node:assert/strict"; +import test, { type TestContext } from "node:test"; + +import { + createCalendar, + LitefoldCalendarError, + type Calendar, + type CalendarEventInput, + type CalendarEventOverflowActivation, + type CalendarEventOverflowContext, + type CalendarGridEventDisplay +} from "../src/index.js"; +import { createDom, dispatchClick, getHost, installDom, waitFor } from "./helpers/dom.js"; + +const TARGET_DATE = "2026-07-14"; +const MODES = ["events", "count", "count-when-multiple"] as const; + +void test("grid event display defaults count multiple compact events and preserve wide summaries", async (context) => { + const { host } = setupDom(context); + const contexts: CalendarEventOverflowContext[] = []; + const calendar = createCalendar(host, { + events: [...eventsForDate(TARGET_DATE, 2), ...eventsForDate("2026-07-15", 1)], + initialDate: TARGET_DATE, + renderHooks: [{ + id: "default-counts", + renderEventOverflow: (value) => { contexts.push(value); return undefined; } + }] + }); + calendar.render(); + await ready(calendar); + + assert.equal(contexts.find((value) => value.dateString === TARGET_DATE && value.variant === "compact")?.display, "count"); + assert.equal(contexts.some((value) => value.variant === "wide" && value.display === "count"), false); + assert.equal(contexts.some((value) => value.dateString === "2026-07-15" && value.display === "count"), false); + assert.equal(getCountButton(host, TARGET_DATE).getAttribute("type"), "button"); + assert.equal(getDay(host, "2026-07-16").querySelector(".lfc-calendar-grid-more"), null); +}); + +for (const mode of MODES) { + for (const cap of [0, 1, 3]) { + void test(`${mode} counts honor zero, singleton, multiple and cap boundaries with summary cap ${cap.toString()}`, async (context) => { + const { host } = setupDom(context); + const contexts: CalendarEventOverflowContext[] = []; + const calendar = createCalendar(host, { + events: [1, 2, 3, 4].flatMap((count) => eventsForDate(`2026-07-${(13 + count).toString()}`, count)), + gridEventDisplay: { compact: mode, wide: mode }, + initialDate: TARGET_DATE, + maxGridEventsPerDay: cap, + onEventActivate: () => undefined, + renderHooks: [{ + id: "mode-matrix", + renderEventOverflow: (value) => { contexts.push(value); return undefined; } + }] + }); + calendar.render(); + await ready(calendar); + + assert.equal(getDay(host, "2026-07-13").querySelector(".lfc-calendar-grid-more"), null); + for (const count of [1, 2, 3, 4]) { + const dateString = `2026-07-${(13 + count).toString()}`; + const isCount = mode === "count" || (mode === "count-when-multiple" && count > 1); + const countContexts = contexts.filter((value) => value.dateString === dateString && value.display === "count"); + assert.deepEqual(countContexts.map((value) => value.variant).sort(), isCount ? ["compact", "wide"] : []); + for (const value of countContexts) { + assert.equal(value.eventCount, count); + assert.equal(value.visibleEventCount, 0); + assert.equal(value.overflowCount, count); + } + if (isCount) { + assert.match(getCountButton(host, dateString).getAttribute("aria-label") ?? "", new RegExp(count.toString())); + } + if (cap === 0) { + assert.equal(getDay(host, dateString).querySelectorAll("[data-lfc-event-id]").length, 0); + } + } + }); + } +} + +void test("compact and wide display settings are independent", async (context) => { + const { host } = setupDom(context); + const contexts: CalendarEventOverflowContext[] = []; + const calendar = createCalendar(host, { + events: eventsForDate(TARGET_DATE, 3), + gridEventDisplay: { compact: "events", wide: "count" }, + initialDate: TARGET_DATE, + maxGridEventsPerDay: 1, + onEventActivate: () => undefined, + renderHooks: [{ + id: "independent-display", + renderEventOverflow: (value) => { contexts.push(value); return undefined; } + }] + }); + calendar.render(); + await ready(calendar); + + assert.equal(contexts.find((value) => value.variant === "compact")?.display, "overflow"); + assert.equal(contexts.find((value) => value.variant === "wide")?.display, "count"); + assert.equal(contexts.find((value) => value.variant === "compact")?.text, "+2"); + assert.equal(contexts.find((value) => value.variant === "wide")?.text, "3 events"); +}); + +void test("count display includes normalized multiday occurrences despite suppressed markers and paged agenda", async (context) => { + const { host } = setupDom(context); + const contexts: CalendarEventOverflowContext[] = []; + const calendar = createCalendar(host, { + agendaPageSize: 10, + events: [ + { id: "spanning", start: TARGET_DATE, end: "2026-07-17", title: "Spanning" }, + ...eventsForDate(TARGET_DATE, 12), + ...eventsForDate("2026-07-15", 1) + ], + gridEventDisplay: { compact: "count", wide: "count" }, + initialDate: TARGET_DATE, + maxGridEventsPerDay: 1, + renderHooks: [{ + id: "missing-markers", + renderEventMarker: () => null, + renderEventOverflow: (value) => { contexts.push(value); return undefined; } + }] + }); + calendar.render(); + await ready(calendar); + + assert.deepEqual(contexts.filter((value) => value.variant === "compact").map((value) => [value.dateString, value.eventCount]), [ + [TARGET_DATE, 13], ["2026-07-15", 2], ["2026-07-16", 1] + ]); + assert.equal(host.querySelectorAll(".lfc-calendar-agenda-event").length, 10); + assert.equal(getDay(host, "2026-07-17").querySelector(".lfc-calendar-grid-more"), null); +}); + +void test("count labels localize the full total without an overflow plus sign", async (context) => { + const { host } = setupDom(context); + const contexts: CalendarEventOverflowContext[] = []; + const calendar = createCalendar(host, { + events: eventsForDate(TARGET_DATE, 12), + gridEventDisplay: { compact: "count", wide: "count" }, + initialDate: TARGET_DATE, + locale: "ar-EG", + renderHooks: [{ + id: "localized-counts", + renderEventOverflow: (value) => { contexts.push(value); return undefined; } + }] + }); + calendar.render(); + await ready(calendar); + + const localized = new Intl.NumberFormat("ar-EG").format(12); + assert.equal(contexts.find((value) => value.variant === "compact")?.text, localized); + assert.equal(getCountButton(host, TARGET_DATE).getAttribute("aria-label")?.includes(localized), true); + assert.ok(contexts.every((value) => !value.text.includes("+"))); +}); + +void test("count actions survive null and failing overflow hooks", async (context) => { + const { host } = setupDom(context); + for (const result of ["null", "throw"] as const) { + const errors: LitefoldCalendarError[] = []; + const calendar = createCalendar(host, { + events: eventsForDate(TARGET_DATE, 2), + gridEventDisplay: { compact: "count", wide: "count" }, + initialDate: TARGET_DATE, + onError: (error) => { errors.push(error); }, + renderHooks: [{ + id: "required-count", + renderEventOverflow: () => { + if (result === "throw") { throw new Error("private count render failure"); } + return null; + } + }] + }); + calendar.render(); + await waitFor(() => calendar.getState().phase === (result === "throw" ? "degraded" : "ready")); + const action = getCountButton(host, TARGET_DATE); + assert.equal(action.querySelector(".lfc-is-compact .lfc-event-overflow-default-content")?.textContent, "2"); + assert.equal(action.querySelector(".lfc-is-wide .lfc-event-overflow-default-content")?.textContent, "2 events"); + assert.equal(errors.length, result === "throw" ? 1 : 0); + assert.doesNotMatch(host.textContent ?? "", /private count render failure/u); + calendar.destroy(); + } +}); + +void test("count activation supplies immutable snapshots before selection and resets agenda paging", async (context) => { + const { dom, host } = setupDom(context); + let activation: CalendarEventOverflowActivation | undefined; + let selectedInsideAction: number | undefined; + let connectedInsideAction: boolean | undefined; + let daySelections = 0; + const calendar = createCalendar(host, { + agendaPageSize: 10, + events: [...eventsForDate(TARGET_DATE, 13), ...eventsForDate("2026-07-15", 12)], + gridEventDisplay: { compact: "count", wide: "count" }, + initialDate: TARGET_DATE, + onDaySelect: () => { daySelections += 1; }, + onEventOverflowActivate: (value) => { + activation = value; + selectedInsideAction = calendar.getState().selectedDate.day; + connectedInsideAction = value.element.isConnected; + } + }); + calendar.render(); + await ready(calendar); + const more = host.querySelector(".lfc-calendar-agenda-more"); + assert.ok(more); + dispatchClick(dom, more); + assert.equal(host.querySelectorAll(".lfc-calendar-agenda-event").length, 13); + const action = getCountButton(host, "2026-07-15"); + dispatchClick(dom, action); + + assert.ok(activation); + assert.equal(Object.isFrozen(activation), true); + assert.equal(Object.isFrozen(activation.date), true); + assert.equal(Object.isFrozen(activation.events), true); + assert.ok(activation.events.every(Object.isFrozen)); + assert.equal(activation.dateString, "2026-07-15"); + assert.deepEqual(activation.date, { day: 15, month: 7, year: 2026 }); + assert.equal(activation.eventCount, 12); + assert.deepEqual(activation.events.map((event) => event.id).sort(), eventsForDate("2026-07-15", 12).map((event) => event.id).sort()); + assert.equal(activation.element, action); + assert.equal(activation.nativeEvent.type, "click"); + assert.equal(selectedInsideAction, 14); + assert.equal(connectedInsideAction, true); + assert.equal(calendar.getState().selectedDate.day, 15); + assert.equal(host.querySelectorAll(".lfc-calendar-agenda-event").length, 10); + assert.equal(dom.window.document.activeElement, host.querySelector(".lfc-calendar-agenda-title")); + assert.equal(daySelections, 0); +}); + +void test("synchronous cancellation preserves selection and application focus ownership", async (context) => { + const { dom, host } = setupDom(context); + const appButton = dom.window.document.createElement("button"); + appButton.textContent = "Application dialog action"; + dom.window.document.body.append(appButton); + const calendar = createCalendar(host, { + events: eventsForDate("2026-07-15", 2), + initialDate: TARGET_DATE, + onEventOverflowActivate: ({ nativeEvent }) => { + nativeEvent.preventDefault(); + appButton.focus(); + } + }); + calendar.render(); + await ready(calendar); + const action = getCountButton(host, "2026-07-15"); + dispatchClick(dom, action); + + assert.equal(calendar.getState().selectedDate.day, 14); + assert.equal(getCountButton(host, "2026-07-15"), action); + assert.equal(dom.window.document.activeElement, appButton); +}); + +for (const failure of ["throw", "reject"] as const) { + void test(`overflow action ${failure} reports a safe action error`, async (context) => { + const { dom, host } = setupDom(context); + const errors: LitefoldCalendarError[] = []; + const calendar = createCalendar(host, { + events: eventsForDate(TARGET_DATE, 2), + initialDate: TARGET_DATE, + onError: (error) => { errors.push(error); }, + onEventOverflowActivate: ({ nativeEvent }) => { + nativeEvent.preventDefault(); + if (failure === "throw") { throw new Error("private count action failure"); } + return Promise.reject(new Error("private count action failure")); + } + }); + calendar.render(); + await ready(calendar); + dispatchClick(dom, getCountButton(host, TARGET_DATE)); + await waitFor(() => errors.some((error) => error.code === "action-failed")); + assert.doesNotMatch(host.textContent ?? "", /private count action failure/u); + }); +} + +void test("destroying the calendar during overflow activation cannot restore stale selection or DOM", async (context) => { + const { dom, host } = setupDom(context); + const calendar = createCalendar(host, { + events: eventsForDate("2026-07-15", 2), + initialDate: TARGET_DATE, + onEventOverflowActivate: () => { calendar.destroy(); } + }); + calendar.render(); + await ready(calendar); + dispatchClick(dom, getCountButton(host, "2026-07-15")); + assert.equal(calendar.getState().phase, "destroyed"); + assert.equal(host.childElementCount, 0); +}); + +void test("grid display settings are snapshotted and invalid modes are rejected before mounting", async (context) => { + const { host } = setupDom(context); + const display: { compact: CalendarGridEventDisplay; wide: CalendarGridEventDisplay } = { compact: "count", wide: "count" }; + const calendar = createCalendar(host, { events: eventsForDate(TARGET_DATE, 1), gridEventDisplay: display, initialDate: TARGET_DATE }); + display.compact = "events"; + display.wide = "events"; + calendar.render(); + await ready(calendar); + assert.equal(getCountButton(host, TARGET_DATE).querySelectorAll(".lfc-is-count").length, 2); + calendar.destroy(); + + for (const invalid of [null, "count", [], { compact: "total" }, { wide: "total" }, { compact: null }]) { + assert.throws(() => createCalendar(host, { events: [], gridEventDisplay: invalid } as never), + (error: unknown) => error instanceof LitefoldCalendarError && error.code === "invalid-configuration"); + assert.equal(host.childElementCount, 0); + } +}); + +for (const boundary of [ + { initialDate: "0001-02-15", outsideDate: "0001-01-28", supportedDate: "0001-02-14" }, + { initialDate: "9999-11-15", outsideDate: "9999-12-01", supportedDate: "9999-11-14" } +]) { + void test(`count activation preserves supported month bounds at ${boundary.outsideDate}`, async (context) => { + const { dom, host } = setupDom(context); + const uncaughtErrors: unknown[] = []; + const activations: string[] = []; + dom.window.addEventListener("error", (event) => { + uncaughtErrors.push(event.error); + event.preventDefault(); + }); + const calendar = createCalendar(host, { + events: [...eventsForDate(boundary.outsideDate, 2), ...eventsForDate(boundary.supportedDate, 2)], + firstDay: 0, + initialDate: boundary.initialDate, + onEventOverflowActivate: ({ dateString }) => { activations.push(dateString); } + }); + calendar.render(); + await ready(calendar); + const before = calendar.getState(); + dispatchClick(dom, getCountButton(host, boundary.outsideDate)); + assert.deepEqual(uncaughtErrors, []); + assert.deepEqual(calendar.getState().displayedMonth, before.displayedMonth); + assert.deepEqual(calendar.getState().selectedDate, before.selectedDate); + assert.deepEqual(activations, [boundary.outsideDate]); + dispatchClick(dom, getCountButton(host, boundary.supportedDate)); + assert.equal(calendar.getState().selectedDate.day, 14); + assert.deepEqual(activations, [boundary.outsideDate, boundary.supportedDate]); + assert.equal(dom.window.document.activeElement, host.querySelector(".lfc-calendar-agenda-title")); + }); +} + +function setupDom(context: TestContext): { dom: ReturnType; host: HTMLElement } { + const dom = createDom(); + context.after(installDom(dom)); + return { dom, host: getHost(dom) }; +} + +function eventsForDate(dateString: string, count: number): CalendarEventInput[] { + return Array.from({ length: count }, (_, index) => ({ + id: `${dateString}-${index.toString()}`, + start: `${dateString}T09:00`, + title: `Event ${index.toString()}` + })); +} + +function getDay(host: HTMLElement, dateString: string): HTMLElement { + const cell = host.querySelector(`.lfc-calendar-day-button[data-lfc-date='${dateString}']`)?.closest("[role='gridcell']"); + assert.ok(cell, `Expected the ${dateString} day cell.`); + return cell; +} + +function getCountButton(host: HTMLElement, dateString: string): HTMLButtonElement { + const button = getDay(host, dateString).querySelector(".lfc-calendar-grid-more"); + assert.ok(button, `Expected the ${dateString} count action.`); + return button; +} + +async function ready(calendar: Calendar): Promise { + await waitFor(() => calendar.getState().phase === "ready", "ready calendar"); +} diff --git a/tests/calendar-render-hooks.test.ts b/tests/calendar-render-hooks.test.ts index c891d6e..a6defd6 100644 --- a/tests/calendar-render-hooks.test.ts +++ b/tests/calendar-render-hooks.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test, { type TestContext } from "node:test"; import { - createCalendar, + createCalendar as createCalendarWithDefaults, type Calendar, type CalendarEventOverflowContext, type CalendarEventInput, @@ -15,6 +15,9 @@ import { waitFor } from "./helpers/dom.js"; +const createLegacyCalendar: typeof createCalendarWithDefaults = (host, options) => + createCalendarWithDefaults(host, { ...options, gridEventDisplay: { compact: "events" } }); + void test("render-hook sets are ordered, isolated, and completely cleaned up", async (context) => { const { dom, host } = setupDom(context); const hookOrder: string[] = []; @@ -54,7 +57,7 @@ void test("render-hook sets are ordered, isolated, and completely cleaned up", a } } ]; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: async () => [event("render-hook-event", "2026-07-14", "Extended")], renderHooks, initialDate: "2026-07-14", @@ -85,7 +88,7 @@ void test("day badges render into the visual-only badge slot", async (context) = const { host } = setupDom(context); let badgeElement: HTMLElement | undefined; let badgeNode: Node | undefined; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: async () => [], renderHooks: [{ dayDidMount: ({ dateString, elements }) => { @@ -118,7 +121,7 @@ void test("day badges render into the visual-only badge slot", async (context) = void test("a failing day badge is quarantined with the public hook name", async (context) => { const { host } = setupDom(context); let captured: LitefoldCalendarError | undefined; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: async () => [], renderHooks: [{ id: "failing-day-badge", @@ -144,7 +147,7 @@ void test("a failing day badge is quarantined with the public hook name", async void test("renderEventMarker replaces or suppresses the built-in marker", async (context) => { const { host } = setupDom(context); const surfaces: string[] = []; - const replacementCalendar = createCalendar(host, { + const replacementCalendar = createLegacyCalendar(host, { events: [event("marker-event", "2026-07-14T09:00", "Marker event")], renderHooks: [{ id: "replacement-marker", @@ -166,7 +169,7 @@ void test("renderEventMarker replaces or suppresses the built-in marker", async assert.deepEqual(surfaces.sort(), ["agenda", "grid-summary"]); replacementCalendar.destroy(); - const suppressionCalendar = createCalendar(host, { + const suppressionCalendar = createLegacyCalendar(host, { events: [event("suppressed-marker", "2026-07-14T09:00", "Suppressed marker")], renderHooks: [{ id: "suppressed-marker", @@ -187,7 +190,7 @@ void test("a quarantined marker renderer restores every built-in marker fallback const { host } = setupDom(context); let calls = 0; let captured: LitefoldCalendarError | undefined; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: [event("marker-fallback", "2026-07-14T09:00", "Marker fallback")], renderHooks: [{ id: "failing-marker", @@ -225,7 +228,7 @@ void test("a marker that reparents itself while connecting cannot suppress built } dom.window.customElements.define("lfc-reparenting-marker", ReparentingMarkerElement); let captured: LitefoldCalendarError | undefined; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: [event("reparenting-marker", "2026-07-14T09:00", "Reparenting marker")], renderHooks: [{ id: "reparenting-marker", @@ -256,7 +259,7 @@ void test("connected render-hook output cannot add interactive descendants", asy } dom.window.customElements.define("lfc-interactive-marker", InteractiveMarkerElement); let captured: LitefoldCalendarError | undefined; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: [event("interactive-marker", "2026-07-14T09:00", "Interactive marker")], renderHooks: [{ id: "interactive-marker", @@ -281,7 +284,7 @@ void test("connected render-hook output cannot add interactive descendants", asy void test("built-in event-overflow variants use adaptive counts independently of the grid cap", async (context) => { const { host } = setupDom(context); - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: [ event("single", "2026-07-13T09:00", "Single event"), ...eventsForDate("2026-07-14", 4, "multiple") @@ -308,7 +311,7 @@ void test("built-in event-overflow variants use adaptive counts independently of void test("renderEventOverflow receives both frozen variants and stable element references", async (context) => { const { dom, host } = setupDom(context); const contexts: Readonly[] = []; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: eventsForDate("2026-07-14", 4, "context"), initialDate: "2026-07-14", maxGridEventsPerDay: 2, @@ -365,7 +368,7 @@ void test("renderEventOverflow receives both frozen variants and stable element void test("renderEventOverflow skips out-of-range structural days", async (context) => { const { host } = setupDom(context); const calls: string[] = []; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: [ ...eventsForDate("2026-07-13", 2, "outside"), ...eventsForDate("2026-07-14", 2, "inside") @@ -395,7 +398,7 @@ void test("renderEventOverflow skips out-of-range structural days", async (conte void test("renderEventOverflow supports custom, null, and undefined results in both variants", async (context) => { const { host } = setupDom(context); const contexts: Readonly[] = []; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: [ ...eventsForDate("2026-07-14", 2, "custom"), ...eventsForDate("2026-07-15", 2, "null"), @@ -458,7 +461,7 @@ void test("renderEventOverflow supports custom, null, and undefined results in b void test("a suppressed or absent primary marker makes the compact count standalone", async (context) => { const { host } = setupDom(context); const compactContexts: Readonly[] = []; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: eventsForDate("2026-07-14", 2, "markerless"), initialDate: "2026-07-14", onEventActivate: () => undefined, @@ -490,7 +493,7 @@ void test("a suppressed or absent primary marker makes the compact count standal void test("static multi-event days show a standalone total without creating a wide action", async (context) => { const { host } = setupDom(context); const contexts: Readonly[] = []; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: eventsForDate("2026-07-14", 2, "static"), initialDate: "2026-07-14", maxGridEventsPerDay: 2, @@ -519,7 +522,7 @@ void test("static multi-event days show a standalone total without creating a wi void test("a compact-primary overflow action retains its fallback when customization returns null", async (context) => { const { host } = setupDom(context); const contexts: Readonly[] = []; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: eventsForDate("2026-07-14", 2, "zero-cap"), initialDate: "2026-07-14", maxGridEventsPerDay: 0, @@ -559,7 +562,7 @@ void test("a wide overflow failure quarantines the unified hook and restores eve const { host } = setupDom(context); const calls: string[] = []; let captured: LitefoldCalendarError | undefined; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: [ ...eventsForDate("2026-07-14", 2, "first"), ...eventsForDate("2026-07-15", 3, "second") @@ -611,7 +614,7 @@ void test("one node cannot be reused across compact and wide overflow variants", const shared = dom.window.document.createElement("span"); shared.className = "shared-event-overflow"; let captured: LitefoldCalendarError | undefined; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: eventsForDate("2026-07-14", 2, "shared"), initialDate: "2026-07-14", maxGridEventsPerDay: 1, @@ -637,7 +640,7 @@ for (const invalidVariant of ["compact", "wide"] as const) { void test(`interactive ${invalidVariant} overflow output quarantines the unified hook`, async (context) => { const { host } = setupDom(context); const errors: LitefoldCalendarError[] = []; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: eventsForDate("2026-07-14", 2, `interactive-${invalidVariant}`), initialDate: "2026-07-14", maxGridEventsPerDay: 1, @@ -676,7 +679,7 @@ for (const outputKind of ["empty fragment", "comment", "template"] as const) { template.content.append(ownerDocument.createElement("span")); return template; }; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: eventsForDate("2026-07-14", 2, `nonvisual-${outputKind}`), initialDate: "2026-07-14", maxGridEventsPerDay: 1, @@ -699,7 +702,7 @@ for (const outputKind of ["empty fragment", "comment", "template"] as const) { void test("a template plus visible fragment child is accepted as overflow content", async (context) => { const { host } = setupDom(context); - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: eventsForDate("2026-07-14", 2, "visible-fragment"), initialDate: "2026-07-14", maxGridEventsPerDay: 1, @@ -746,7 +749,7 @@ for (const mutation of ["append-sibling", "remove-ancestor"] as const) { } dom.window.customElements.define(tagName, MutatingOverflowElement); const errors: LitefoldCalendarError[] = []; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: eventsForDate("2026-07-15", 2, `mutating-${mutation}`), initialDate: "2026-07-14", maxGridEventsPerDay: 1, @@ -784,7 +787,7 @@ void test("connected custom overflow output may extend only its own noninteracti } } dom.window.customElements.define("lfc-self-contained-overflow", SelfContainedOverflowElement); - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: eventsForDate("2026-07-14", 2, "self-contained"), initialDate: "2026-07-14", maxGridEventsPerDay: 1, @@ -806,7 +809,7 @@ void test("connected custom overflow output may extend only its own noninteracti void test("renderEventLeading contains text nodes in a compact-hideable wrapper", async (context) => { const { dom, host } = setupDom(context); - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: [event("text-leading", "2026-07-14T09:00", "Text leading")], renderHooks: [{ id: "text-leading", @@ -838,7 +841,7 @@ void test("cross-document render-hook nodes are rejected without taking down cor foreignDom.window.close(); }); let captured: LitefoldCalendarError | undefined; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: async () => [event("safe", "2026-07-14", "Core event")], renderHooks: [{ id: "foreign-node", @@ -859,7 +862,7 @@ void test("cross-document render-hook nodes are rejected without taking down cor void test("render-hook cleanup failures quarantine the hook set while preserving calendar data", async (context) => { const { host } = setupDom(context); const errors: LitefoldCalendarError[] = []; - const calendar = createCalendar(host, { + const calendar = createLegacyCalendar(host, { events: async () => [event("cleanup", "2026-07-14", "Preserved event")], renderHooks: [{ eventDidMount: () => () => { diff --git a/tests/e2e/grid-event-display.spec.js b/tests/e2e/grid-event-display.spec.js new file mode 100644 index 0000000..47ee9a1 --- /dev/null +++ b/tests/e2e/grid-event-display.spec.js @@ -0,0 +1,200 @@ +import { expect, test } from "@playwright/test"; + +import { expectLibraryFixtureReady, expectOnlyOneGridTabStop } from "./helpers.js"; + +const TARGET_DATE = "2026-07-14"; + +test("compact multiple-event counts are the only new keyboard entry", async ({ page }) => { + await mountCalendar(page, { width: 320 }); + const { action, count, day, grid } = targets(page); + await expect(count).toBeVisible(); + await expect(count.locator(".lfc-is-compact")).toHaveText("2"); + await expect(action).toBeHidden(); + await day.focus(); + await day.press("F2"); + await expect(count).toBeFocused(); + await expectOnlyOneGridTabStop(grid); + await page.keyboard.press("ArrowUp"); + await expect(count).toBeFocused(); + await page.keyboard.press("ArrowDown"); + await expect(count).toBeFocused(); + await page.keyboard.press("Enter"); + await expect(page.locator(".lfc-calendar-agenda-title")).toBeFocused(); +}); + +for (const width of [320, 900]) { + test(`all-event count mode includes singletons at container width ${String(width)}`, async ({ page }) => { + await mountCalendar(page, { count: 1, display: { compact: "count", wide: "count" }, width }); + const { action, count } = targets(page); + await expect(count).toBeVisible(); + await expect(count).toHaveAccessibleName(/1 event.+July 14, 2026/u); + await expect(count.locator(width === 320 ? ".lfc-is-compact" : ".lfc-is-wide")).toHaveText(width === 320 ? "1" : "1 event"); + await expect(action).toBeHidden(); + await expect(page.locator('.lfc-calendar-grid-more[data-lfc-date="2026-07-15"]')).toHaveCount(0); + }); +} + +test("a wide-only singleton count creates no empty compact action", async ({ page }) => { + await mountCalendar(page, { actionable: false, count: 1, display: { compact: "events", wide: "count" }, width: 320 }); + const { count, day } = targets(page); + await expect(count).toBeHidden(); + await day.focus(); + await day.press("F2"); + await expect(day).toBeFocused(); + await setContainerWidth(page, 900); + await expect(count).toBeVisible(); + await day.press("F2"); + await expect(count).toBeFocused(); +}); + +test("resizing preserves an active event until blur and skips hidden actions on re-entry", async ({ page }) => { + await mountCalendar(page, { width: 900, suppressMarkers: true }); + const { action, count, day } = targets(page); + await day.focus(); + await day.press("F2"); + await expect(action).toBeFocused(); + await setContainerWidth(page, 320); + await expect(action).toBeFocused(); + await expect(action).toBeVisible(); + await expect(action.locator(".lfc-calendar-event-title")).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(day).toBeFocused(); + await expect(action).toBeHidden(); + await day.press("F2"); + await expect(count).toBeFocused(); + await setContainerWidth(page, 900); + await expect(count).toBeFocused(); + await expect(count).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(count).toBeHidden(); + await day.press("F2"); + await expect(action).toBeFocused(); +}); + +test("container resizing keeps count labels, nodes, source calls and render hooks stable", async ({ page }) => { + await mountCalendar(page, { count: 4, width: 320 }); + const { count } = targets(page); + await expect(count).toHaveAccessibleName(/4 events.+1 more/u); + const label = await count.getAttribute("aria-label"); + const before = await page.evaluate(() => { + const fixture = window.gridCountFixture; + fixture.savedCount = fixture.host.querySelector(".lfc-calendar-grid-more"); + fixture.savedEvent = fixture.host.querySelector("[data-lfc-event-id]"); + return { hooks: fixture.hooks, requests: fixture.requests }; + }); + await setContainerWidth(page, 900); + await expect(count.locator(".lfc-is-wide")).toBeVisible(); + await expect(count.locator(".lfc-is-wide")).toHaveText("1 more"); + await setContainerWidth(page, 320); + await expect(count.locator(".lfc-is-compact")).toBeVisible(); + await expect(count).toHaveAttribute("aria-label", label); + expect(await page.evaluate(() => { + const fixture = window.gridCountFixture; + return { + hooks: fixture.hooks, + requests: fixture.requests, + sameCount: fixture.savedCount === fixture.host.querySelector(".lfc-calendar-grid-more"), + sameEvent: fixture.savedEvent === fixture.host.querySelector("[data-lfc-event-id]") + }; + })).toEqual({ ...before, sameCount: true, sameEvent: true }); +}); + +test("count action cancellation allows an app-owned dialog and restores its initiating action", async ({ page }) => { + await mountCalendar(page, { dialog: true, width: 320 }); + const { count } = targets(page); + await count.click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole("button", { name: "Close chooser" })).toBeFocused(); + await dialog.getByRole("button", { name: "Close chooser" }).click(); + await expect(dialog).toBeHidden(); + await expect(count).toBeFocused(); + expect(await page.evaluate(() => window.gridCountFixture.calendar.getState().selectedDate.day)).toBe(13); +}); + +test("count controls remain usable in a 320px RTL container with enlarged text", async ({ page }) => { + await mountCalendar(page, { display: { compact: "count", wide: "count" }, rtl: true, width: 320 }); + await page.evaluate(() => { document.documentElement.style.fontSize = "32px"; }); + const { count, day } = targets(page); + await expect(count).toBeVisible(); + await expect(count).toHaveAccessibleName(/2 events/u); + const box = await count.boundingBox(); + expect(box).not.toBeNull(); + expect(box.width).toBeGreaterThanOrEqual(24); + expect(box.height).toBeGreaterThanOrEqual(24); + expect(await page.locator("[data-my-calendar]").evaluate((host) => host.scrollWidth - host.clientWidth)).toBeLessThanOrEqual(1); + await day.focus(); + await day.press("F2"); + await expect(count).toBeFocused(); +}); + +async function mountCalendar(page, settings = {}) { + await page.setViewportSize({ width: 1200, height: 1200 }); + await expectLibraryFixtureReady(page); + await page.evaluate(async (settings) => { + const { createCalendar } = await import("/dist/index.js"); + const host = document.querySelector("[data-my-calendar]"); + host.style.inlineSize = `${String(settings.width ?? 320)}px`; + if (settings.rtl) { host.dir = "rtl"; } + const fixture = { errors: [], host, hooks: 0, requests: 0 }; + const options = { + events: () => { + fixture.requests += 1; + return Array.from({ length: settings.count ?? 2 }, (_, index) => ({ + id: `count-${String(index)}`, + start: "2026-07-14T09:00", + title: `Event ${String(index)}` + })); + }, + initialDate: settings.dialog ? "2026-07-13" : "2026-07-14", + locale: "en-US", + maxGridEventsPerDay: 3, + onError: (error) => { fixture.errors.push({ code: error.code, hook: error.hook, message: error.message, cause: String(error.cause) }); }, + now: () => new Date("2026-07-14T12:00:00.000Z"), + renderHooks: [{ + id: "count-browser-fixture", + ...(settings.suppressMarkers ? { renderEventMarker: () => null } : {}), + renderEventOverflow: () => { fixture.hooks += 1; return undefined; } + }] + }; + if (settings.actionable !== false) { options.onEventActivate = () => {}; } + if (settings.display) { options.gridEventDisplay = settings.display; } + if (settings.dialog) { + const dialog = document.createElement("dialog"); + dialog.setAttribute("aria-label", "Choose an event"); + const close = document.createElement("button"); + close.textContent = "Close chooser"; + dialog.append(close); + document.body.append(dialog); + let origin; + close.addEventListener("click", () => { dialog.close(); origin?.focus(); }); + options.onEventOverflowActivate = ({ element, nativeEvent }) => { + nativeEvent.preventDefault(); + origin = element; + dialog.showModal(); + }; + } + fixture.calendar = createCalendar(host, options); + Object.defineProperty(window, "gridCountFixture", { configurable: true, value: fixture }); + fixture.calendar.render(); + }, settings); + await expect(page.getByRole("grid")).toBeVisible(); + await expect.poll(() => page.evaluate(() => ({ + errors: window.gridCountFixture.errors, + phase: window.gridCountFixture.calendar.getState().phase + }))).toEqual({ errors: [], phase: "ready" }); +} + +function targets(page) { + const day = page.locator(`.lfc-calendar-day-button[data-lfc-date="${TARGET_DATE}"]`); + return { + action: day.locator('..').locator(':is(button, a)[data-lfc-event-id="count-0"]'), + count: page.locator(`.lfc-calendar-grid-more[data-lfc-date="${TARGET_DATE}"]`), + day, + grid: page.getByRole("grid") + }; +} + +async function setContainerWidth(page, width) { + await page.locator("[data-my-calendar]").evaluate((host, width) => { host.style.inlineSize = `${String(width)}px`; }, width); +} diff --git a/tests/e2e/responsive-layout.spec.js b/tests/e2e/responsive-layout.spec.js index 71ba23f..68d17f5 100644 --- a/tests/e2e/responsive-layout.spec.js +++ b/tests/e2e/responsive-layout.spec.js @@ -502,6 +502,7 @@ async function mountWeekLayoutFixture( title: "Tall intrinsic layout anchor" }, ...targetEvents], initialDate: fixtureOptions.targetDate, + gridEventDisplay: { compact: "events" }, maxGridEventsPerDay: 2, onDaySelect: () => {}, onEventActivate: () => {}, @@ -861,6 +862,7 @@ async function mountResponsiveMultipleEventFixture( } : events, renderHooks: [renderHook], initialDate: "2026-08-06", + gridEventDisplay: { compact: "events" }, maxGridEventsPerDay: fixtureOptions.maxGridEventsPerDay, onDaySelect: () => { observations.daySelectCalls += 1; diff --git a/tests/public-api.type-test.ts b/tests/public-api.type-test.ts index 9bc77d1..b8b5976 100644 --- a/tests/public-api.type-test.ts +++ b/tests/public-api.type-test.ts @@ -2,6 +2,8 @@ import type { Calendar, CalendarCompactEventOverflowContext, CalendarEventOverflowElements, + CalendarEventOverflowActivation, + CalendarGridEventDisplay, CalendarGridEventPlacement, CalendarOptions, CalendarRenderCleanup, @@ -24,6 +26,32 @@ function consumeTypeAssertions(...values: readonly unknown[]): readonly unknown[ return values; } +/** Count options preserve metadata inference and immutable activation snapshots. */ +export function verifyCountApiTypeContracts(): void { + const modes: readonly CalendarGridEventDisplay[] = ["events", "count", "count-when-multiple"]; + const options: CalendarOptions = { + events: [], + gridEventDisplay: { compact: "count", wide: "count-when-multiple" }, + onEventOverflowActivate: (context) => { + const activation: CalendarEventOverflowActivation = context; + const detail: string | undefined = context.events[0]?.metadata?.detail; + context.nativeEvent.preventDefault(); + //@ts-expect-error The loaded occurrence snapshot cannot be extended by applications. + context.events.length = 0; + //@ts-expect-error Applications cannot change the authoritative count. + context.eventCount = 0; + consumeTypeAssertions(activation, detail); + return Promise.resolve(); + } + }; + const invalid: CalendarOptions = { + events: [], + //@ts-expect-error Only documented count and event presentation modes are supported. + gridEventDisplay: { compact: "auto" } + }; + consumeTypeAssertions(modes, options, invalid); +} + /** Compile-only assertions for public callback and instance contracts. */ export function verifyPublicApiTypeContracts( baseCalendar: Calendar, From 9eb398681e298084232753d5d2a716a4e6ea4683 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:13:37 -0700 Subject: [PATCH 06/22] refactor: audit container-based layout and Baseline fallbacks --- docs/browser-support.md | 6 +- examples/advanced/theme.css | 10 +- examples/example.css | 6 +- examples/index.css | 21 +- scripts/pages-site/deployment-details.css | 3 +- scripts/pages-site/site.css | 19 +- src/internal/dom/palette.ts | 38 +++- src/styles/agenda.css | 29 ++- src/styles/month-grid.css | 4 +- src/styles/pager.css | 8 +- src/styles/responsive.css | 18 +- src/styles/toolbar.css | 2 +- tests/e2e/container-modernization.spec.js | 237 ++++++++++++++++++++++ 13 files changed, 346 insertions(+), 55 deletions(-) create mode 100644 tests/e2e/container-modernization.spec.js diff --git a/docs/browser-support.md b/docs/browser-support.md index 876ab3b..212124e 100644 --- a/docs/browser-support.md +++ b/docs/browser-support.md @@ -36,7 +36,7 @@ The package relies directly on these Baseline platform capabilities: | --- | --- | | JavaScript | ECMAScript modules, promises, `AbortController`, URL parsing, and `Intl` | | DOM and input | Standard DOM, native semantic elements and modal dialogs, Pointer/Touch/Wheel Events, and native scrolling | -| CSS | Custom properties, Grid and subgrid, Scroll Snap, logical and intrinsic sizing, cascade layers, `:where()`, `:has()`, `:dir()`, animations, and inline-size container queries and units | +| CSS | Custom properties, Grid and Flexbox, Scroll Snap, logical and intrinsic sizing, cascade layers, forgiving `:is()` / `:where()` selectors, animations, and inline-size container queries and units | The optional [classic-script recipe](integration-guide.md#classic-script-entry-point) also requires dynamic `import()`. @@ -44,6 +44,8 @@ Capabilities outside the resolved preset require feature detection and a tested The palette uses `light-dark()` when available. Otherwise a small local controller mirrors the effective host `color-scheme` into the same CSS palette, observing system preference and live host/ancestor theme attributes. It does not rerender, refetch, reset picker input, or move focus. The default host scheme remains `light dark`; applications can set `color-scheme: light`, `dark`, or `inherit` on their host. Use `inherit` when an ancestor controls application themes. Removing the override resumes the automatic default. Application token overrides and forced-color/increased-contrast rules remain in the normal cascade. +Agenda columns share tracks with subgrid where supported; explicit per-row tracks provide the supported fallback. Browsers without `:has()` use wrapping agenda rows that retain optional markers, times, titles, and supporting content. Required count actions and compact toolbar layout do not depend on relational selectors. Where `:dir()` is unavailable, the same local controller reflects the effective host direction, including nested ancestor resets and live attribute changes, into package-owned styling state. None of these fallbacks measures layout or replaces controls. Balanced/pretty text wrapping, stable scrollbar gutters, and safe alignment are enhancements over ordinary wrapping, scrolling, and alignment. + Locale-derived week starts use either `Intl.Locale#getWeekInfo()` or `Intl.Locale#weekInfo` and fall back to Sunday only when neither returns a usable `firstDay`. `ResizeObserver` is an optional direct-input enhancement. When available, it clears an in-progress paging gesture after the viewport resizes. Without it, the ordinary calendar and its toolbar, keyboard, picker, and public navigation routes remain available. @@ -52,6 +54,8 @@ Locale-derived week starts use either `Intl.Locale#getWeekInfo()` or `Intl.Local Responsive behavior is CSS-driven from the calendar container. Applications must meet the [minimum supported host width](../DESIGN.md#responsive-model); containers below that floor receive best-effort degradation and are not a supported layout target. There is no JavaScript fallback for missing container-query support. In supported browsers, resizing the host changes layout without rerendering, refetching, or moving focus. +Browser tests cover nested component containers, preserved interaction observers, and forced fallback paths. They disable unsupported selectors and declarations in the loaded CSS as well as JavaScript feature detection, so current-engine parsing cannot silently conceal an untested fallback. + ### Direct-input paging With `swipe` enabled, touch, pen, or horizontal precision scrolling can pull the current grid toward a decorative Previous or Next lane. Vertical scrolling and pinch zoom remain available. The package does not require `scrollend` support, and Previous/Next, Page Up/Down, the month/year picker, and public navigation methods remain available when a device does not produce a qualifying horizontal gesture. diff --git a/examples/advanced/theme.css b/examples/advanced/theme.css index 50bb5fb..ad17c86 100644 --- a/examples/advanced/theme.css +++ b/examples/advanced/theme.css @@ -135,7 +135,7 @@ line-height: 1; } - .my-calendar:dir(rtl) .my-navigation-icon { + .my-calendar:is(:dir(rtl), [dir="rtl"]) .my-navigation-icon { transform: scaleX(-1); } @@ -273,6 +273,7 @@ } .my-dialog { + container: my-dialog / inline-size; box-sizing: border-box; inline-size: min(32rem, calc(100% - 1.5rem)); max-block-size: calc(100dvb - 1.5rem); @@ -299,7 +300,7 @@ max-block-size: inherit; overflow: auto; overscroll-behavior: contain; - padding: clamp(1rem, 4vw, 1.5rem); + padding: clamp(1rem, 4cqi, 1.5rem); scrollbar-gutter: stable; } @@ -314,13 +315,14 @@ .my-dialog h2 { margin: -0.75rem 0 0; - font-size: clamp(1.375rem, 6vw, 1.75rem); + font-size: clamp(1.375rem, 6cqi, 1.75rem); line-height: 1.2; overflow-wrap: anywhere; text-wrap: balance; } .my-event-details { + container: my-event-details / inline-size; display: grid; gap: 0; margin: 0; @@ -365,7 +367,7 @@ min-inline-size: 5rem; } - @media (width < 24rem) { + @container my-event-details (inline-size < 24rem) { .my-event-details > div { grid-template-columns: minmax(0, 1fr); } diff --git a/examples/example.css b/examples/example.css index 551d7b6..957ab9e 100644 --- a/examples/example.css +++ b/examples/example.css @@ -111,6 +111,7 @@ } .my-shell { + container: my-example / inline-size; box-sizing: border-box; display: grid; gap: clamp(1rem, 2vw, 1.5rem); @@ -130,7 +131,7 @@ } .my-intro h1 { - font-size: clamp(1.75rem, 1.4rem + 1.5vw, 2.5rem); + font-size: clamp(1.75rem, 1.4rem + 1.5cqi, 2.5rem); line-height: 1.1; overflow-wrap: anywhere; text-wrap: balance; @@ -143,6 +144,7 @@ } .my-panel { + container: my-example / inline-size; box-sizing: border-box; display: grid; gap: var(--my-gap); @@ -328,7 +330,7 @@ } } - @media (width < 32rem) { + @container my-example (inline-size < 32rem) { .my-control-row, .my-field-row { align-items: stretch; diff --git a/examples/index.css b/examples/index.css index b112453..fcaa2fa 100644 --- a/examples/index.css +++ b/examples/index.css @@ -31,17 +31,19 @@ } .my-landing-header { - display: grid; + display: flex; + flex-wrap: wrap; gap: clamp(1rem, 3vw, 2rem); - grid-template-columns: minmax(0, 1.6fr) minmax(16rem, 1fr); padding-block: clamp(2rem, 8vw, 6rem) clamp(1.5rem, 4vw, 3rem); } .my-landing-header-content { align-content: center; display: grid; + flex: 1.6 1 24rem; gap: 0.75rem; max-inline-size: 66ch; + min-inline-size: 0; } .my-landing-header-content > * { @@ -96,6 +98,7 @@ .my-metadata { align-self: end; display: grid; + flex: 1 1 16rem; gap: 0.75rem; min-inline-size: 0; padding: 1rem; @@ -278,23 +281,13 @@ } @media (hover: hover) { - .my-card:has(a:hover) { + .my-card:hover { border-color: var(--my-primary-color); background: var(--my-subtle-background); translate: 0 -0.125rem; } } - @media (width < 48rem) { - .my-landing-header { - grid-template-columns: minmax(0, 1fr); - } - - .my-metadata { - align-self: auto; - } - } - @media (prefers-contrast: more) { .my-card { border-width: 0.125rem; @@ -302,7 +295,7 @@ } @media (forced-colors: active) { - .my-card:has(a:hover) { + .my-card:hover { border-color: LinkText; } } diff --git a/scripts/pages-site/deployment-details.css b/scripts/pages-site/deployment-details.css index 339e768..8b77da3 100644 --- a/scripts/pages-site/deployment-details.css +++ b/scripts/pages-site/deployment-details.css @@ -1,4 +1,5 @@ .my-developer-footer { + container: my-deployment-details / inline-size; background: Canvas; border: 0.125rem solid currentColor; color: CanvasText; @@ -39,7 +40,7 @@ overflow-wrap: anywhere; } -@media (max-width: 32rem) { +@container my-deployment-details (inline-size <= 32rem) { .my-developer-footer dl { grid-template-columns: 1fr; } diff --git a/scripts/pages-site/site.css b/scripts/pages-site/site.css index 81b3c11..e0d80cc 100644 --- a/scripts/pages-site/site.css +++ b/scripts/pages-site/site.css @@ -97,6 +97,7 @@ main, .my-pages-footer { inline-size: min(100% - 2rem, 72rem); margin-inline: auto; + container: my-pages-section / inline-size; } .my-pages-project-nav { @@ -351,12 +352,17 @@ h3 { .my-pages-footer { align-items: start; border-block-start: 0.0625rem solid var(--my-pages-border); - display: grid; + display: flex; + flex-wrap: wrap; gap: 1.5rem; - grid-template-columns: minmax(12rem, 0.5fr) minmax(0, 1fr); padding-block: 2.5rem; } +.my-pages-footer > div { + flex: 1 1 12rem; + min-inline-size: 0; +} + .my-pages-footer h2 { font-size: 1.25rem; margin: 0; @@ -364,9 +370,11 @@ h3 { .my-pages-footer dl { display: grid; + flex: 2 1 32rem; gap: 1rem; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; + min-inline-size: 0; } .my-pages-footer dt { @@ -396,9 +404,8 @@ h3 { } } -@media (max-width: 48rem) { - .my-pages-deployment-grid, - .my-pages-footer { +@container my-pages-section (inline-size <= 48rem) { + .my-pages-deployment-grid { grid-template-columns: 1fr; } @@ -407,7 +414,7 @@ h3 { } } -@media (max-width: 34rem) { +@container my-pages-section (inline-size <= 34rem) { .my-pages-project-nav { align-items: start; flex-direction: column; diff --git a/src/internal/dom/palette.ts b/src/internal/dom/palette.ts index f640894..2863831 100644 --- a/src/internal/dom/palette.ts +++ b/src/internal/dom/palette.ts @@ -1,15 +1,18 @@ import type { HostWindow } from "./environment.js"; -/** Mirrors the effective host color scheme only when native light-dark() is unavailable. */ +/** Mirrors inherited palette and direction only when their native CSS features are unavailable. */ export class CalendarPalette { + private readonly needsDirectionFallback: boolean; private readonly preference: MediaQueryList | null; private readonly observer: MutationObserver | null; private readonly connectionObserver: MutationObserver | null; private disconnected = false; public constructor(private readonly host: HTMLElement, private readonly window: HostWindow | null) { - const needsFallback = typeof window?.CSS === "object" && !window.CSS.supports("color", "light-dark(white, black)"); - this.preference = needsFallback ? window.matchMedia("(prefers-color-scheme: dark)") : null; + const needsPaletteFallback = typeof window?.CSS === "object" && !window.CSS.supports("color", "light-dark(white, black)"); + this.needsDirectionFallback = typeof window?.CSS === "object" && !window.CSS.supports("selector(:dir(rtl))"); + const needsFallback = window !== null && (needsPaletteFallback || this.needsDirectionFallback); + this.preference = needsPaletteFallback ? window.matchMedia("(prefers-color-scheme: dark)") : null; this.observer = needsFallback ? new window.MutationObserver(this.handleMutation) : null; this.connectionObserver = needsFallback ? new window.MutationObserver(this.handleConnection) : null; if (needsFallback) { @@ -28,6 +31,7 @@ export class CalendarPalette { this.preference?.removeEventListener("change", this.sync); this.host.ownerDocument.removeEventListener("load", this.sync, true); this.host.classList.remove("lfc-palette-light", "lfc-palette-dark"); + this.host.removeAttribute("data-lfc-direction"); } private observeAncestors(): void { @@ -35,7 +39,12 @@ export class CalendarPalette { this.connectionObserver?.disconnect(); for (let ancestor: HTMLElement | null = this.host; ancestor !== null; ancestor = ancestor.parentElement) { //Attribute changes can select application theme rules; child-list changes can reparent the host. - this.observer?.observe(ancestor, { attributes: true, childList: true }); + const observesAutomaticDirection = this.needsDirectionFallback && ancestor.dir === "auto"; + this.observer?.observe(ancestor, { + attributes: true, + childList: true, + ...(observesAutomaticDirection ? { characterData: true, subtree: true } : {}) + }); } if (!this.host.isConnected) { //A document-wide observation lasts only until a detached host is attached again. @@ -61,12 +70,21 @@ export class CalendarPalette { if (this.disconnected || this.window === null) { return; } - const schemes = this.window.getComputedStyle(this.host).colorScheme.split(/\s+/u); - const dark = schemes.includes("dark") && - (!schemes.includes("light") || this.preference?.matches === true); - for (const [name, enabled] of [["lfc-palette-dark", dark], ["lfc-palette-light", !dark]] as const) { - if (this.host.classList.contains(name) !== enabled) { - this.host.classList.toggle(name, enabled); + const style = this.window.getComputedStyle(this.host); + if (this.preference !== null) { + const schemes = style.colorScheme.split(/\s+/u); + const dark = schemes.includes("dark") && + (!schemes.includes("light") || this.preference.matches); + for (const [name, enabled] of [["lfc-palette-dark", dark], ["lfc-palette-light", !dark]] as const) { + if (this.host.classList.contains(name) !== enabled) { + this.host.classList.toggle(name, enabled); + } + } + } + if (this.needsDirectionFallback) { + const direction = style.direction === "rtl" ? "rtl" : "ltr"; + if (this.host.getAttribute("data-lfc-direction") !== direction) { + this.host.setAttribute("data-lfc-direction", direction); } } }; diff --git a/src/styles/agenda.css b/src/styles/agenda.css index ad5cc55..79a85ae 100644 --- a/src/styles/agenda.css +++ b/src/styles/agenda.css @@ -280,7 +280,10 @@ :where(.litefold-calendar) .lfc-calendar-event-marker:empty, :where(.litefold-calendar) .lfc-calendar-time:empty, :where(.litefold-calendar) .lfc-calendar-event-details:empty, - :where(.litefold-calendar) .lfc-calendar-event-trailing:empty, + :where(.litefold-calendar) .lfc-calendar-event-trailing:empty { + display: none; + } + :where(.litefold-calendar) .lfc-calendar-event-leading:has(> .lfc-calendar-event-marker:empty):has( > .lfc-calendar-event-leading-content:empty @@ -288,6 +291,30 @@ display: none; } + @supports not selector(:has(*)) { + /* A wrapping row preserves optional roles without relational-selector state or observers. */ + :where(.litefold-calendar) .lfc-calendar-agenda-list { + grid-template-columns: minmax(0, 1fr); + } + + :where(.litefold-calendar) .lfc-calendar-agenda-event { + display: flex; + flex-wrap: wrap; + } + + :where(.litefold-calendar) + .lfc-calendar-agenda-event + .lfc-calendar-event-leading:not(.lfc-visually-hidden) { + display: contents; + } + + :where(.litefold-calendar) + .lfc-calendar-agenda-event + :is(.lfc-calendar-event-details, .lfc-calendar-event-trailing) { + flex-basis: 100%; + } + } + :where(.litefold-calendar)[aria-busy="true"] { cursor: progress; } diff --git a/src/styles/month-grid.css b/src/styles/month-grid.css index c1a7e8f..4857776 100644 --- a/src/styles/month-grid.css +++ b/src/styles/month-grid.css @@ -367,7 +367,9 @@ overflow-wrap: anywhere; } - :where(.litefold-calendar) .lfc-calendar-grid-more:has(.lfc-calendar-event-overflow.lfc-is-count) { + :where(.litefold-calendar) + .lfc-calendar-day-summaries:is([data-lfc-compact-display="count"], [data-lfc-wide-display="count"]) + .lfc-calendar-grid-more { background: var(--lfc-event-background); border: var(--lfc-internal-hairline) solid var(--lfc-event-border-color); border-radius: var(--lfc-border-radius); diff --git a/src/styles/pager.css b/src/styles/pager.css index af932aa..5848486 100644 --- a/src/styles/pager.css +++ b/src/styles/pager.css @@ -55,17 +55,17 @@ scroll-snap-align: end; } - :where(.litefold-calendar):dir(rtl) .lfc-calendar-swipe-lane-previous { + :where(.litefold-calendar):is(:dir(rtl), [data-lfc-direction="rtl"]) .lfc-calendar-swipe-lane-previous { order: 3; scroll-snap-align: end; } - :where(.litefold-calendar):dir(rtl) .lfc-calendar-swipe-lane-next { + :where(.litefold-calendar):is(:dir(rtl), [data-lfc-direction="rtl"]) .lfc-calendar-swipe-lane-next { order: 1; scroll-snap-align: start; } - :where(.litefold-calendar):dir(rtl) + :where(.litefold-calendar):is(:dir(rtl), [data-lfc-direction="rtl"]) :where(.lfc-calendar-grid, .lfc-calendar-swipe-lane) { direction: rtl; } @@ -96,7 +96,7 @@ line-height: 1; } - :where(.litefold-calendar):dir(rtl) .lfc-calendar-swipe-lane-icon { + :where(.litefold-calendar):is(:dir(rtl), [data-lfc-direction="rtl"]) .lfc-calendar-swipe-lane-icon { transform: scaleX(-1); } diff --git a/src/styles/responsive.css b/src/styles/responsive.css index 17aed84..11f300c 100644 --- a/src/styles/responsive.css +++ b/src/styles/responsive.css @@ -3,22 +3,13 @@ :where(.litefold-calendar) .lfc-calendar-toolbar { display: grid; gap: 0.375rem; - grid-template-areas: - "navigation" - "toolbar-end"; grid-template-columns: minmax(0, 1fr); padding-block: 0.375rem; padding-inline: 0.5rem; } - :where(.litefold-calendar) - .lfc-calendar-toolbar:not(:has(.lfc-calendar-toolbar-end)) { - grid-template-areas: "navigation"; - } - :where(.litefold-calendar) .lfc-calendar-navigation { gap: 0.375rem; - grid-area: navigation; } :where(.litefold-calendar) .lfc-calendar-title { @@ -37,7 +28,6 @@ :where(.litefold-calendar) .lfc-calendar-toolbar-end { gap: 0.375rem; - grid-area: toolbar-end; inline-size: 100%; justify-content: end; margin-inline-start: 0; @@ -379,6 +369,14 @@ grid-column: 1 / -1; } + @supports not selector(:has(*)) { + :where(.litefold-calendar) + .lfc-calendar-agenda-event + .lfc-calendar-event-title { + flex-basis: 100%; + } + } + :where(.litefold-calendar) .lfc-calendar-agenda-event:where( :has(> .lfc-calendar-time:is(:empty, .lfc-visually-hidden)) diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index b463633..5cf4bce 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -228,7 +228,7 @@ inline-size: 1.25em; } - :where(.litefold-calendar):dir(rtl) .lfc-calendar-navigation-icon { + :where(.litefold-calendar):is(:dir(rtl), [data-lfc-direction="rtl"]) .lfc-calendar-navigation-icon { transform: scaleX(-1); } diff --git a/tests/e2e/container-modernization.spec.js b/tests/e2e/container-modernization.spec.js new file mode 100644 index 0000000..fb54d7f --- /dev/null +++ b/tests/e2e/container-modernization.spec.js @@ -0,0 +1,237 @@ +import { readFile } from "node:fs/promises"; + +import { expect, test } from "@playwright/test"; + +import { expectLibraryFixtureReady } from "./helpers.js"; + +const TARGET_DATE = "2026-08-06"; + +test.use({ bypassCSP: true }); + +for (const [name, features] of [ + ["native", []], + ["without relational selectors", ["has"]], + ["without subgrid", ["subgrid"]], + ["combined fallbacks", ["has", "dir", "subgrid"]] +]) { + test(`nested calendar containers preserve counts, focus and role layout (${name})`, async ({ page }) => { + if (features.length > 0) { await disableModernLayoutFeatures(page, features); } + await mountCalendars(page); + const compact = page.locator("#my-audit-compact"); + const wide = page.locator("#my-audit-wide"); + const compactCount = countFor(compact); + await expect(compactCount).toBeVisible(); + await expect(countFor(wide)).toBeHidden(); + await expect(compactCount).toHaveCSS("border-top-style", "solid"); + for (const host of [compact, wide]) { + await expectNoOverflow(host); + await expect(host.locator(".lfc-calendar-agenda-event")).toHaveCount(2); + await expect(host.locator(".lfc-calendar-agenda-event").first()).toHaveCSS("display", features.includes("has") ? "flex" : "grid"); + if (features.includes("subgrid")) { + await expect(host.locator(".lfc-calendar-agenda-item").first()).not.toHaveCSS("grid-template-columns", /subgrid/u); + } + await expect(host.locator(".lfc-calendar-agenda-event .lfc-calendar-event-details:visible")).toHaveCount(0); + for (const title of await host.locator(".lfc-calendar-agenda-event .lfc-calendar-event-title").all()) { + await expect(title).toBeVisible(); + await expectNoOverflow(title); + } + } + await compact.locator(`.lfc-calendar-day-button[data-lfc-date="${TARGET_DATE}"]`).focus(); + await page.keyboard.press("F2"); + await expect(compactCount).toBeFocused(); + await compact.evaluate((host) => { host.style.inlineSize = "900px"; }); + await expect(compactCount).toBeFocused(); + await expect(compactCount).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(compactCount).toBeHidden(); + await compact.evaluate((host) => { host.style.inlineSize = "320px"; }); + await page.evaluate(() => { document.documentElement.style.fontSize = "32px"; }); + await expectNoOverflow(compact); + await expect(compactCount).toBeVisible(); + expect(await page.evaluate(() => window.containerAudit.requests)).toBe(2); + }); +} + +test("direction fallback follows nested ancestor changes without replacing focused controls", async ({ page }) => { + await disableModernLayoutFeatures(page); + await mountCalendars(page); + const host = page.locator("#my-audit-compact"); + const count = countFor(host); + await count.focus(); + await host.evaluate((host) => { + const outer = document.createElement("section"); + outer.dir = "rtl"; + const inner = document.createElement("section"); + inner.dir = "ltr"; + host.before(outer); + outer.append(inner); + inner.append(host); + }); + await count.focus(); + await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "ltr"); + await host.evaluate((host) => { host.parentElement.removeAttribute("dir"); }); + await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "rtl"); + await expect(host.locator(".lfc-calendar-swipe-lane-previous")).toHaveCSS("order", "3"); + await expect(count).toBeFocused(); + await host.evaluate((host) => { host.parentElement.style.direction = "ltr"; }); + await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "ltr"); + await expect(host.locator(".lfc-calendar-swipe-lane-previous")).toHaveCSS("order", "1"); + await expect(count).toBeFocused(); + await host.evaluate((host) => { host.dir = "rtl"; }); + await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "rtl"); + await expect(count).toBeFocused(); + await host.evaluate((host) => { + host.removeAttribute("dir"); + host.parentElement.style.removeProperty("direction"); + host.parentElement.dir = "auto"; + const text = document.createElement("span"); + text.id = "my-audit-direction-text"; + text.textContent = "\u05d0\u05d1\u05d2"; + host.parentElement.prepend(text); + }); + await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "rtl"); + await page.locator("#my-audit-direction-text").evaluate((text) => { text.firstChild.data = "English text"; }); + await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "ltr"); + await expect(count).toBeFocused(); + await page.evaluate(() => { window.containerAudit.calendars[0].destroy(); }); + await expect(host).not.toHaveAttribute("data-lfc-direction"); +}); + +test("example panels use their own named container when nested in a wide shell", async ({ page }) => { + await page.setViewportSize({ width: 1200, height: 1000 }); + await expectLibraryFixtureReady(page); + await addSourceStyles(page, ["examples/example.css"]); + await page.evaluate(() => { + const shell = document.createElement("main"); + shell.className = "my-shell"; + const panel = document.createElement("section"); + panel.className = "my-panel"; + panel.style.inlineSize = "320px"; + const row = document.createElement("div"); + row.className = "my-control-row"; + const label = document.createElement("label"); + label.textContent = "Category"; + const input = document.createElement("input"); + input.className = "my-field"; + input.setAttribute("aria-label", "Category filter"); + const button = document.createElement("button"); + button.className = "my-button"; + button.textContent = "Refresh events"; + row.append(label, input, button); + panel.append(row); + shell.append(panel); + document.body.replaceChildren(shell); + }); + const panel = page.locator(".my-panel"); + const label = panel.locator("label"); + const input = panel.getByRole("textbox"); + await input.focus(); + await expect.poll(async () => { + const [labelBox, inputBox] = await Promise.all([label.boundingBox(), input.boundingBox()]); + return inputBox.y >= labelBox.y + labelBox.height; + }).toBe(true); + await expectNoOverflow(panel); + await panel.evaluate((panel) => { panel.style.inlineSize = "700px"; }); + await expect(input).toBeFocused(); + await expect.poll(async () => { + const [labelBox, inputBox] = await Promise.all([label.boundingBox(), input.boundingBox()]); + return Math.abs(inputBox.y - labelBox.y) < inputBox.height; + }).toBe(true); +}); + +test("published site sections reflow independently and leave the skip link fixed to the viewport", async ({ page }) => { + await page.setViewportSize({ width: 1200, height: 1000 }); + await expectLibraryFixtureReady(page); + const source = await readFile(new URL("../../scripts/pages-site/index.html", import.meta.url), "utf8"); + await page.setContent(source.replaceAll(/]*>[\s\S]*?<\/script>/gu, "") + .replaceAll(/]*>/gu, "")); + await addSourceStyles(page, ["scripts/pages-site/site.css"]); + await page.addStyleTag({ content: ".my-pages-site-header, .my-pages-footer { inline-size: 320px; } body { min-block-size: 2000px; }" }); + const header = page.locator(".my-pages-site-header"); + const footer = page.locator(".my-pages-footer"); + for (const section of [header, footer]) { await expectNoOverflow(section); } + await expect.poll(async () => { + const [wordmark, links] = await Promise.all([ + header.locator(".my-pages-wordmark").boundingBox(), + header.locator(".my-pages-project-nav ul").boundingBox() + ]); + return links.y >= wordmark.y + wordmark.height; + }).toBe(true); + await expect.poll(async () => { + const [heading, metadata] = await Promise.all([footer.locator("h2").boundingBox(), footer.locator("dl").boundingBox()]); + return metadata.y >= heading.y + heading.height; + }).toBe(true); + await page.evaluate(() => { window.scrollTo(0, 250); }); + const skip = page.getByRole("link", { name: "Skip to main content" }); + await skip.focus(); + await expect(skip).toBeFocused(); + const box = await skip.boundingBox(); + expect(box.y).toBeGreaterThanOrEqual(0); + expect(box.y).toBeLessThan(32); + expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(100); +}); + +function countFor(host) { + return host.locator(`.lfc-calendar-grid-more[data-lfc-date="${TARGET_DATE}"]`); +} + +async function expectNoOverflow(locator) { + await expect.poll(() => locator.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1); +} + +async function addSourceStyles(page, files) { + for (const file of files) { + await page.addStyleTag({ content: await readFile(new URL(`../../${file}`, import.meta.url), "utf8") }); + } +} + +async function disableModernLayoutFeatures(page, features = ["dir", "has", "subgrid"]) { + await page.addInitScript((features) => { + const supports = CSS.supports.bind(CSS); + CSS.supports = (...values) => values.some((value) => features.some((feature) => + value.includes(feature === "subgrid" ? "subgrid" : `:${feature}(`))) + ? false : supports(...values); + }, features); + await page.route("**/*.css", async (route) => { + const response = await route.fetch(); + let body = await response.text(); + for (const feature of features) { + body = feature === "subgrid" ? body.replaceAll("subgrid", "lfc-unsupported-subgrid") + : body.replaceAll(`:${feature}(`, `:lfc-unsupported-${feature}(`); + } + await route.fulfill({ body, response }); + }); +} + +async function mountCalendars(page) { + await page.setViewportSize({ width: 1200, height: 1200 }); + await expectLibraryFixtureReady(page); + await page.evaluate(async () => { + const { createCalendar } = await import("/dist/index.js"); + const fixture = { calendars: [], requests: 0 }; + for (const [name, width] of [["compact", 320], ["wide", 900]]) { + const host = document.createElement("div"); + host.id = `my-audit-${name}`; + host.style.inlineSize = `${String(width)}px`; + document.body.append(host); + const calendar = createCalendar(host, { + eventTimeDisplay: name === "compact" ? "none" : "all", + events: () => { + fixture.requests += 1; + return [{ id: "timed", title: "A long event title that wraps safely", start: "2026-08-06T09:00" }, + { id: "all-day", title: "All-day activity", start: "2026-08-06" }]; + }, + initialDate: "2026-08-06", + locale: "en-US", + now: () => new Date("2026-08-06T12:00:00Z"), + onEventActivate: () => {}, + renderHooks: [{ id: "audit-empty-marker", renderEventMarker: ({ event }) => event.id === "all-day" ? null : undefined }] + }); + calendar.render(); + fixture.calendars.push(calendar); + } + window.containerAudit = fixture; + }); + await expect(page.locator("#my-audit-compact")).not.toHaveAttribute("aria-busy", "true"); + await expect(page.locator("#my-audit-wide")).not.toHaveAttribute("aria-busy", "true"); +} From 35aff9ee0a3eb3eb7157de5298bc65c1fcdd5f1f Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:25:05 -0700 Subject: [PATCH 07/22] chore: automate screenshot preparation and visual review --- docs/screenshots/README.md | 38 ++++- .../compact-all-counts-390x844.png | Bin 0 -> 31870 bytes .../compact-default-counts-390x844.png | Bin 0 -> 31366 bytes .../count-keyboard-focus-390x844.png | Bin 0 -> 32408 bytes .../desktop-month-grid-1440x900.png | Bin 43734 -> 44478 bytes .../event-details-dark-1280x800.png | Bin 63329 -> 63705 bytes .../grid-event-keyboard-focus-1440x900.png | Bin 44740 -> 45418 bytes .../mobile-month-agenda-dark-390x844.png | Bin 36324 -> 36904 bytes .../mobile-month-swipe-pull-390x844.png | Bin 34097 -> 34628 bytes docs/screenshots/month-year-jump-1280x800.png | Bin 43499 -> 43491 bytes docs/screenshots/wide-all-counts-1440x900.png | Bin 0 -> 35345 bytes package.json | 7 +- screenshots.manifest.json | 130 ++++++++++++++- scripts/check-screenshots.mjs | 2 +- scripts/lib/screenshot-preparation.mjs | 110 +++++++++++++ scripts/prepare-screenshots.mjs | 34 ++++ scripts/screenshot-contract.mjs | 18 +- scripts/screenshot-scenes.mjs | 59 +++++++ scripts/tests/screenshot-contract.test.mjs | 7 +- scripts/tests/screenshot-preparation.test.mjs | 96 +++++++++++ scripts/update-screenshots.mjs | 155 +++++++++--------- 21 files changed, 554 insertions(+), 102 deletions(-) create mode 100644 docs/screenshots/compact-all-counts-390x844.png create mode 100644 docs/screenshots/compact-default-counts-390x844.png create mode 100644 docs/screenshots/count-keyboard-focus-390x844.png create mode 100644 docs/screenshots/wide-all-counts-1440x900.png create mode 100644 scripts/lib/screenshot-preparation.mjs create mode 100644 scripts/prepare-screenshots.mjs create mode 100644 scripts/tests/screenshot-preparation.test.mjs diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md index dfa415a..45c97b1 100644 --- a/docs/screenshots/README.md +++ b/docs/screenshots/README.md @@ -14,12 +14,16 @@ The manifest covers exactly these PNG files: | `mobile-month-swipe-pull-390x844.png` | Held native touch pull with the abbreviated `Aug 2026` toolbar and decorative `Sep 2026` adjacent-month lane | 390 × 844 | | `event-details-dark-1280x800.png` | Application-owned event details dialog opened from the agenda | 1280 × 800 | | `grid-event-keyboard-focus-1440x900.png` | Visible keyboard focus on a wide-layout grid event action after F2 | 1440 × 900 | +| `compact-default-counts-390x844.png` | Default total counts for multiple events; a single event keeps its marker | 390 × 844 | +| `compact-all-counts-390x844.png` | Total counts on every nonempty compact day | 390 × 844 | +| `wide-all-counts-1440x900.png` | Localized total counts on every nonempty wide day | 1440 × 900 | +| `count-keyboard-focus-390x844.png` | Visible keyboard focus on a compact count button after F2 | 390 × 844 | Only the manifest-listed PNGs belong in this directory. Do not add alternate crops, renamed duplicates, consumer branding, private data, or externally supplied reference images. ## README and package policy -The root README embeds only the desktop month grid and mobile dark-theme agenda. Together they show the responsive and theme range without turning the GitHub and npm package landing pages into the complete six-scene review gallery. +The root README embeds only the desktop month grid and mobile dark-theme agenda. Together they show the responsive and theme range without turning the GitHub and npm package landing pages into the complete review gallery. npm always includes the root `README.md`, while the package `files` allowlist deliberately keeps `docs/screenshots/**` outside the tarball. The previews therefore add no installed package bytes. They use repository-relative Markdown images for same-branch GitHub rendering and npm's GitHub-backed README rendering. An offline renderer opened from an installed package cannot load the excluded PNGs; the hosted GitHub and npm READMEs are the intended preview surfaces. The manifest hashes remain the version-specific visual evidence; the README previews are a current product overview. @@ -51,24 +55,44 @@ Because npm refreshes a package README only when a new version is published, rel ![Keyboard focus on an event action](grid-event-keyboard-focus-1440x900.png) +### Compact default counts + +![Compact calendar with total counts on days containing multiple events](compact-default-counts-390x844.png) + +### Counts on every compact day + +![Compact calendar with total counts on every nonempty day](compact-all-counts-390x844.png) + +### Wide counts + +![Wide calendar with localized total event counts on every nonempty day](wide-all-counts-1440x900.png) + +### Count keyboard focus + +![Keyboard focus on a compact calendar total event count button](count-keyboard-focus-390x844.png) + ## Update captures -Before capture, inspect `git status` so you can distinguish intentional source changes from unrelated work. The updater targets all six PNGs plus the root `screenshots.manifest.json`, even when some regenerated bytes remain unchanged. +Before capture, inspect `git status` so you can distinguish intentional source changes from unrelated work. Preparation targets all ten PNGs plus the root `screenshots.manifest.json`, even when some regenerated bytes remain unchanged. Run from the repository root with Node 24.x and the exact npm version in `package.json#packageManager`: ```sh npm ci --ignore-scripts npx --no-install playwright install chromium -npm run screenshots:update +npm run screenshots:prepare npm run check:screenshots ``` On a fresh Linux machine, add `--with-deps` to the Playwright install command when operating-system browser dependencies are also missing. -`screenshots:update` builds the package and advanced example, starts the repository-owned local server, fixes locale, time zone, color scheme, reduced motion, clock/data fixtures, viewport, and interaction state, and rewrites every canonical capture and manifest record. It does not build unrelated example routes or deployment metadata. Do not hand-edit or recompress the PNGs. +`screenshots:prepare` first verifies the evidence. When it is current, it exits without building or capturing. Otherwise it builds the package and advanced example once, starts the strict-CSP repository server, and captures with fixed locale, time zone, theme, motion, clock/data fixtures, viewport, and interaction state. `screenshots:update` forces the same preparation when investigating browser or font drift. Do not hand-edit or recompress the PNGs. + +All captures go into an ignored staging directory first. The updater validates the complete batch, checks that sources did not change during capture, and creates `.cache/screenshots/review/index.html` with before/after images plus `changes.json`. It replaces canonical images only after successful capture and validation, commits the manifest last, and restores originals if replacement fails. Failed capture batches remain under `.cache/screenshots/staging-*` for diagnosis; canonical evidence stays untouched. The replacement is rollback-capable, not a cross-file filesystem transaction, so interruption during replacement requires inspecting the retained backup before retrying. + +Open the local review gallery and review changed scenes at native dimensions before staging files. CI runs read-only `check:screenshots`; preparation and capture reject attempts to accept stale evidence in CI. A capture lock prevents simultaneous updaters. After a forcibly terminated updater, verify no capture is still running before removing `.cache/screenshots/capture.lock`. -Five scenes capture settled states. The held-pull scene intentionally captures direct manipulation before release so the decorative adjacent-month/year lane can be reviewed; selection timing and post-release browser physics remain browser-test and supported-device concerns. Regenerate and review all six canonical scenes after an intentional source-layout change, even when only the two mobile scenes are expected to gain responsive label differences. +Nine scenes capture settled states. The held-pull scene intentionally captures direct manipulation before release so the decorative adjacent-month/year lane can be reviewed; selection timing and post-release browser physics remain browser-test and supported-device concerns. Review every canonical scene after an intentional source-layout change. The four count scenes mount an application-neutral fixture through the public built-package API, with empty, single-event, and multiple-event days. ## Review checklist @@ -88,7 +112,7 @@ The screenshot manifest is machine-owned and records, for every scene: - Example URL and deterministic setup steps. - Viewport and output dimensions. - SHA-256 hash. -- Source fingerprint covering package source and build configuration, the advanced route and its shared stylesheet, the exact build/server/capture helpers, and pinned compiler/browser inputs. The updater's invoked npm scripts and their automatic `pre`/`post` hooks are included. Other example routes, deployment metadata, Playwright's test configuration, root version metadata, and npm scripts outside that capture lifecycle are excluded so unrelated changes do not require recapture. +- Source fingerprint covering package source, Vite configuration, the advanced route and shared stylesheet, build/server/capture helpers, locked compiler/browser dependencies, and effective JavaScript/CSS browser targets. The preparation scripts and their automatic `pre`/`post` hooks are included. Browser target evidence records its resolution date, but a date change alone does not require recapture. Other example routes, deployment metadata, Playwright's test configuration, root version metadata, and npm scripts outside that capture lifecycle are excluded. - Exact Node patch used for capture; validation accepts that provenance when it belongs to major version 24, while npm, Playwright, and Chromium remain exact-pinned. - Documentation references and exact alt text. @@ -99,6 +123,6 @@ The screenshot manifest is machine-owned and records, for every scene: - If Chromium is missing, rerun `npx --no-install playwright install chromium`; do not substitute a system browser. - If tooling reports the wrong npm version, switch to the exact `packageManager` version before capture. - If hashes or pixels changed unexpectedly, investigate browser/toolchain drift, fixtures, fonts, animation, layout timing, and network requests before updating evidence. -- If only part of the visual contract intentionally changed, still review all six rewritten images; unchanged scenes should remain byte-identical. +- If only part of the visual contract intentionally changed, still review all ten images; unchanged scenes should remain byte-identical. Never accept unexplained diffs. A deliberate visual change must update the implementation, captures, manifest, documentation references, and alt text together. diff --git a/docs/screenshots/compact-all-counts-390x844.png b/docs/screenshots/compact-all-counts-390x844.png new file mode 100644 index 0000000000000000000000000000000000000000..483a74c09b304c55d7ff84078fd6cd936c405d17 GIT binary patch literal 31870 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/compact-default-counts-390x844.png b/docs/screenshots/compact-default-counts-390x844.png new file mode 100644 index 0000000000000000000000000000000000000000..0efe46db5f47a62eff7cca452c5ff9e0eadfa38a GIT binary patch literal 31366 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�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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/count-keyboard-focus-390x844.png b/docs/screenshots/count-keyboard-focus-390x844.png new file mode 100644 index 0000000000000000000000000000000000000000..5da11f812767a622ff5d41fa7de42bd61b988297 GIT binary patch literal 32408 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/desktop-month-grid-1440x900.png b/docs/screenshots/desktop-month-grid-1440x900.png index 71c26cdf90ceb984919c9e091123b00ebdf7a148..f9f86d286bd995d412701d420b1f39143e7b0f6d 100644 GIT binary patch literal 44478 zcmeFZbx@qa*Dg4a009C755WQi*WeC80|d9=2@u@fhXf1m5S-vna2RAD1b6otEV$cX z!|agXe)sOJx>Z}XUv1U?vF{(yHScuy>C@fkoPM6u6RxHrkAw9J3jhG%D7=@^007W( z0e~mL&rp${%yXiI001ul3Nr7syfP0~FeSCW{6?Ji5xpROA@fC3?VaHZn3f>{22KP1 z5E0Ly0z&^sj75k_N(Tp@;ur2T!CRZOf;_YEM7bW;zu)d~lk;c>%Suoja(5A#t({*m z7LM|vssb&rmhDX6W;~3en=H#x&&w@k6s^*z{W;Dzg$#v-CW_jQ3-?hdNNHllhU9Pz z<^?=!>s)JXXF(xB${W3?B^vI}1l1Zm4ka?Pok!ZWhxpkIJ{su%NK2c=M^*w*dEy24 z4-B+tC%bc;PNoIRgl#EfTj0FYqF|KuQm5(VgoYr-rUxvFiXL?X7)(ga5P_MnT1EjU25aL1EQgpVT8q&J zO%E`MCGj6Z$!<@O-;xw!F2;+5-rkj+R5l$i<4YFeJLO_kEVLaS>KdC?;5@cOoR=xn zCU#ngShz8 zXtAU6v4Oy_$;zO+!lWcypgC490a72#qzRfoc+imVB+ay6Ps*h6SMOmR0q_!_YN!`7n9X13QgRL+Nvqf+y)$A-}h5489W` z<>lQ$+Mh4QGC_4GqyTZa?2lu4ohH)a=_z-7BgTAVYNKy)745FHMOqh^@Xku=>6$ov zJf-vINd0_NCgj*?bwza!tDBE$+#vdih1_N}A)kX=pR~cf!KN6+tHM@ahD~_;!$aY5 zN&@AFa273{Uxjk_Z9iRCRM1HD>Vj9)CZm7SY+s0QaR>r2=@Z7~-gvjdHmyGY+CDx1 z7HLhB7fh-8Dv9-U(3az^D@PRPuhMV5W9f{65x;7!HV;0{4Gx^9y?gRu-?=0FRSzCg z)tz+vgAM!JoDB3u1;!r~G;0I((|&fAspqHQdJK*%baf~zF&gam=DC>0WZmEIBgBvI zQfc6~Od0le6D1U^3wW2Vnxi*#jnp3(#%@}l{e1&Hq<8eK5zvYzo_;D5j;^5_O~rRS zJbIh}XWXB4$v~`t24WUn@I7f}LL;ZnU2jW4pQJKO-}Evr zsX_=giWHa(gBp)8)jZ6On%6zdtS&}fLx$Wlu;ye40$5FTuBIII2)nGP%G?Zeq@(Y} zru{Z<8+e_g&O(Pzr^WZ{)}~R9X&ntu*YMS9C2Ad+z^;>3=1)iAtB7c-h2rvJfDIN3 za`SZN@7}fF0Y!jae+se_*sgI;Y^)tEwX4{2BjE8_mA$OY*7Byn1>m}t{2YJK-4y|z zEo{w<5h&8?Gtv5mQ=cj5glg**g$oDGsfNqC4?TkuHUG4_VZ`k75@&`@g|WDG{0#)+yE8~q1PM74l)~P>-tfpHB z!kza>sewkBYs(`>GMR4;wF#j_;4oU#h}3ts_!G3R$2mVL80YnRm!U+9^P*5CBna_) zcg7K(Ai0U|I+J+ z^_K=_>HFKA>4Eej!impoRS;8$0&$zS2aXyQw5>lW!#52794VpLVB^}~VUNJpm(`g` zK~w_X1F}NI(Of#&5qhO#rnE)Fmz-717$+}^z@i+RL(!Lev->NBiGVWdpeUpb71gMM z$KPCq(Tw4dU95lq`TM$UQ^2oatWyASGeE7UN38#r5q7j~f@nhB4Hd>MaabLmyTY=^ zem1g7q%L*MHg5jHW&`b~y|8*04Syt=xmK(HVe`+uqFz~|<;3+#oWs}o+luoz$N1v3 zV9{z|L|zlP`j*}_!s8}wF^1oD(8_zlS0yf^OCL1x;T)ZH2D!YBcUS^hc;J6Yb?hIu zAUQy++*^Dnqr{()~jc^ zw=*q1btJv%zv<|ljxe5!p#cIJ5(MxqhNa3gCf9+}x~&inFL?l76$ zAv_+wImfiBC#1^b#)|nq>|II>);S?fXm7(?>?r(Gqx8X(oVYlbO_siDi9b z_A2d1KWw7PxE-|3NiR_2z@`=jS-epO0RFzi#zp!%`FEy@WpzP*S)@t+Dg}qafLxXr z|E36wqv*Ym3>tFR=a!AjB7NVgdTvaYHK$4E#N@=0sY^Q2KSO1GMo~8yWw$;5E}wf(FO+xb!+e8ZnJ>hm z(Z59iSUP-65*;~ROv6DpC+C;rq}V!0I)s`z^7O=OUStm9>X3GbjGjt}A16LGGYJ2d zF!C!3;oXj0Vsnkq*$=PhKQ5nLd(0KR05^`*JAX^71CA?I-CAy_?A~R+k*^f5Y+btO zY1%Fca`@43f}K940Jd_Q4uG#%beyZo0lABF>XI!oF6~=K(vA{g5q%t+AqF z-g>sg8e#CQhfutfK z0Y{}!_RLL)H2Ff5zr(=M&|~=C5F7Xe@Fu?IZio)l>W}WOc_)8yaXKSGR#UjzOh6pp zvfsVlI~o=Hy<#{p9m7I7e2`#fN9Fge^D*U|m=(9n-fN@o$b$Qt_gO0xktVRSnev_# z%cFD=hsc23XevU+-EHUy)FP3*{E3|MJV9|rCjH&|L$!q`6(toXppn=TM8}!roPmHi z`5z}wUq6Lwkmk{j66sxj=zmIw@JCD9>a@+y-)fkIeY|n-Sx&m|uuu$s%klY^W<`^7 zTP1wZgi}a#*|prEl2{yHleZ;wBQSM&V@`gX8h>IbKlqr-rwahE0ia++CAc&&7Hot9 zd_NCUz)8QSU71sq&{*nL&*Y54UT)c!w_9%QFPVTlv%mY}B|Di@L=KCX!EhH}j4Sg5 z7pG-F6OudD*T|w`;xA@4yWXu$U@rWgT=4B>#J&w5Y`I-&HCs|%NGb5O;<4!IO@_0v z)MFMi@S{;#_Ecd}@O_F8u+W=|Ij5UdcF?{Dr9fP9t+{g(hy2dCb6xRUIWZ*_Q-4d* zEf-S{4v|p81)jF-1{5TAZ_9y3Otzp>i0N$NoBR_NJl!pL;gP_w>K|&&e960kDgWm& z-=C}vk`$$~Ha`XAChTE$&NJDLxzh4sp{ep_7gxznaW;rdH*D7nl)!i%6U4^ z$w@%uVoDwuxCtN4J|;DK!)*%7dKa`GiKQ4ml<}=_EH@^!ywwL3-XC`8`+1+oUS{Xv zaPf$$Wt&#IS2pw+cGFvIQcf~+leMhjMB=-LKLbPO1ZfQ7KMhEt>e-v4tRd~?sXR$y z%oaR4B_5wJeqs*-t`-pb*ia`V?h#{$*M&bkVvc?7{Bhr>M`z`=7lc5wyE5)u%0cM0^r zxMBkDV%w}%-^>-`wIP5Ap$v5o%bLc%%7h4_e5opc``Wa;K!%M8(mw=3+TdE;PUIwv-@KfboI}1 z{+2mH;35k>S1~m^;ui0wJ9eP7DV$_0+(4kz95pc>oyP+C`NghW-!3^GF)|j=_wtzT z!?j+bJ(r3uzw_M1z-ri;H}Vt{@?G-fp>sP-%jU;pkE zw#5^%ztXnpC&;~O_5l&^nYlCjYzx6+K+xxw9s!nIraOh$-e6VSV0kPs@je6ToN%}A zt=mrrO}*YeaBchKU}0u45bMj^FQ32K$VF?>KtAN&KNI{^amzk$c46jpK(it_BG~5Y zYRs`#PW_DQxKZe}V|^q%<5DxFaqkDIK_<4rxl!mVN%L&|w`TV7xdq7v^6-^Rc2|?| z_ga=_11+!W?DnGkWv~8B2swVnZ`qoHPLDtoAowraicb;-plu{X4=awPX+jn{m7w-} z2ndJn!brZ>kw~C*u;#>QA&*F?Rbq6>`}oFyQ8;yetY^wHDnKTtpyAlogL_*;=H#x} zJwzp6eoyWV+#Y^+TS}S9z-A;=^5?5)`pxY=#+&dA;%0Fjc!#U00O#ORQChWXh@22w zlze2MB$$Urbqj|%nL?aH5aI|b!v6wBq5-Gp7QrUmUW(vsR{>AtVc3yvG7UeJ+@0bi zVgANwe)N0#E93g%DKG~v$Lto)P>U1J z!gQ9to5bhI)=`b~Q?h#5z{vs>&D`-m6x}8^ehut#m_l8d-B;<3I4~#7S;e$>hjw9*@pg%LDE%@uhe!FPdbfYK$+XmD^FxsRBHksTDmvpR zVw9=s={$e^ywFnn`hz| zl?139(cIbFJ`v{JaDs@9z`o;?rAM-g8Bv)MKX-mB-&;npO_FxoKe$n=t0?=hV-IEo zXuIWb)tKGPwk9Po4OYr_p5b)x_lRv=jV=m7ZXI9m4$G=LQ1OWC%{DdLr+Z|+x;lC< zTq4Pmj$iaVAA^3Q-QzS$_)E6ApF_3|*<0UP;Rqy3KWK3B9dl@Keo=$Zj`O=LxHPtX-IpBF?fYii zONy}wWesdm^y$Pgsl>YJ6u$f?Id(ri7Vqp3u>IIW4&RW?mHn}6wvYV7eB5r@wUWu@ zpQ4$VLk(o!$B%z@+!0;d*yf;eC4M+Xr*d~wM*-yP0LK%iHNUWLlDVyEsl{v5kpz9q zLnmg#`Ars!pLi~nzIXpUn}tI3?HdGw!^shW)wZ0 z<#%p9Z}HZ%a!cKmhL4d6S8XzbO%faX?+Op!?M}TBTjob8NlF~!5hsuO#8#=G`tbL! zgDSJ~=nA^px2GJXm|e?7R6yFHAI4L|`XteM+5 z?4?h>*TS!#Y2Z|(-dd~JYBsFIB*ArAwGc*;5v6X?Og>}kkg*p*O~OKc z4h3W10NXAVAss<-<#CMeVrsZ;{JaJERet+^4c0#wW2L>Rr;Vsk zH0dNSksCL#&-hHB`h&^Ov{RXLE=G}nxx8$YrL!&JQ=AiP2Ea>^a=GY-{!x;K4RtiO^zwHgub3~&*0OI z@X(v^-}w?RhohE!3tf&#TX-Z;)K#J`k=E!bTlO&(SX`fyk52f_X6Eta{U@aT!s*B> zKoP&!*$JWv>z5_e2SLB_^y?RTIc?kJ+NTCu}Qp=@ttczXxTjWEy*Slo!EFY%nQUNsPE8V3%cWjI>t7j1$W zOHVO>&uFIF)<63wd5$wJj6^ylk6LzqVVcP`?C*Z5HuO2Xxd&D!^PY{GejH`!TylVkpHG=HM}E|98~39V5qEq`+F*{Td3ke_-QiZ_AgH zbBrap(OkN^;>?uU%*wJUA#`Ev-RAq=FT$v`(AuD(LPXeL85qLQc}BHHM?es{RQzqJ z`g>dAFzU1&txaa}mS?LED{r#g*P}3c4b_#IXwg4R#mUw9l#kG%x~sd0(<=PaX@X~q zuw?1>UOdm?Ipk~G5zQX@26C6enx@w}_=KPb3+l%!7xUzv#B zrD|i>>(-I~C>c0d&uR3h^!?SgUw%j#yjEH&{QT>IbTnxH?$wT?FN-LDga>X+A?3{Z zd_)gLawYzgLA|E&n7npR%V5V(HwIYETkc%Uf~s-C&!FD7AWcwzT>pb=ERU zi75+$IYmrZ7H{5*yh|vOPSF;S`Z86O(#kKE7-26%vx|G#mR=)hE@Y`LlcF zDL{zn?N{x2 zjb_j|O6n~gv8joHs*9IG;3I^>_Z^8nAkoS3qQZ?8Us$l+5!`#Mp)j#T7;LC=c562Z z!3MYg`m85A%j#jX1aZa6oa`GTF9XYEAw$jP)w6h>bA16BjFDmTA?c-63GiAsq-BG4 zWe^xq6+gFD`0XF2FX1TnuxsU}=3P}I;^|82OnT;d-7uKdAZ`~;v?cbNk=Fd`R)^TC z<0me}nZevJpgi$Owe^znn=r7Bp=J^Z$LRWp<(4*GzYLyaBfP;7uC>;UuUh=t*cc1Dti$_ zoLpr$EXVBvdY-i-p_R3%jhF^SsJzY3w#Lh{rqs)iI~Wr_EtT=Y7w!^01f+D~->)MH|Hmu?Q|Se){*^f=ZDPBKI}xr%E;1(!E`a$m^u`|4*avLSPes7Ni#ChqWo0R z0(D6nP2;9Nu_5@K-FcBnvc5Basg#!PXxBL_|F}2HXXj`wOlDSMjiVvEI3}tU$(KU~AZhjg zp!CX5&Ts1&5$r5mtF@f`)8)Ez3th%74Q}v|=>LHa=hVqa3qbm=0H~V{4U@4D+)K|_QqEI{o-+qpdW-IJ$k@P9qNMoIL!e1Pu6 zBgs1=z~<=ysfjWf?4D57X+1o>bBqjGd2_tD+!7XZm{Li%0oeR?5Q)<+>?^*zOB47? zT^0Erkgzk?ZrcvJWN54VRDWzjkk$ms{Yf4)zQ{+b1W0&W zYT&u~!Vv`!h(f^k*eT$(*uSCu-xMs=-+hVB;rk~iK&>2^grK*dGEDl|fSQn@uq^$= zBPWA=-akUG|FiHY z-dSZ$*jl7~n89!Td9JOdFa%)p1O>xJe)u#|A(h9DT%gervfQq`ABywpM|%3RmuYy2 z_Nzl(#%KMZ5s5^^jLa-58d{kJC4w7C8NRTsN&_U3WZdC%ICqZ8esFl}w)-aVm3-&d zbdUYftdV%cTvz4HYJjo({-gqOT>|1VTuLXWrw2hb$#sd{IY5X+hSS=Ry@(v%<0${54bS#x38#piu`JzeA3xhDDclOB!qk-S?+Kfv+w&zvA zOnsM(hsfxNCVIr1C2_)q0w&v|m3WpI9?|hmvs`aYgE^g143%t>M}}Eaxj8${o%zzG zmEZnfFMV<%bF-ug8i;-nP2k?+kmHg9TEdmh7`{EuG8Jo4QIT@`M-xR=Qprm?Wc~N; z32UF`4FB~$JTK6o+?w!T6@7%ddk2x3M@YGb=NCV+7C%8P3AjY=n@Mi&56a4q?FIHl zxzI{t<4!+E#hngD&gfjZf$o25FfpPoG&x}A-u`da7Y9q=&kK9(Zvr+9H7+OEx90C5 z!<>E6#(0qv%jfC$rx&AnR^H*|ywvq>M^5tg3j^8@<)cJQ?~YizzyaxN$G8WH6f9VFCF{UI$k#7DuM!$|c?X;x18UDQ_e$%W5n?h=dOomTS2TN+X3uR;|NC1N&$bQvlaYw3)OQ^9! zMQo}7)Z#vJCM6~^EuwktFB3fBiW#-V$G1CSR3Y4#KeWr+o^w{_>bqxCJfz)Tu5ofr z{wb1kzucoDan&+>uNzafbt@=c$f}?^YZ&I{#`Uo{IEBxBpuuS

H+6KxA7xIB`Ywpq= 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 diff --git a/docs/screenshots/event-details-dark-1280x800.png b/docs/screenshots/event-details-dark-1280x800.png index 9960481ffe02fc06f122b1c550ab0ec6bb0e7dcb..760fba40d15706a177843df2e3e90e823181b112 100644 GIT binary patch literal 63705 zcmce7WmuHm_wE3KA|N8t0tyHsDczugpp#&%cXz|kJuq_~ z-uL{j|B36IPv<)G<(cbwcCWS9UhBTs4*n`9iT9ZNF$e_0`z$4{2m)aOe=%M@LI)nF zH|oV8&{NQ7@%PGZsrw7qdRl5}mv?LG6~@vh^oK6Qr30%`^yV?~iuH73SYJpqe;<_z z;T!AqapJ--aG#sxjwR_+=mqM#`)der%9#ERh<@Yu#S0Z1J^tTgd5W~_PUe$14W3vx z-lY@GoE(loczy16vEL~%**01$=_jXd3!YBLB1~Gm>HPCvW{7XCNa|WVceIK3HODyu zW&~oVj#pDo6?u0()l?r&u{htLrgxF}fY>ki4e7Fl$9lk0XlKy{{IZx$im=k+bbuOC zCaE*)d(o0k^jSjm^D7gl^wo;TJ{cX}S#;~7`6CfGdAeroJo(Mn3wt4>Synvj;m@zF zB!-Fkve7`GsF#mgvDsX#wEn2Z8yD@Def>Uz{XFrbM0(0FO*;(@xOWjMe!gB} z27y|?iGk$oC~Wb)VtK^hU*k0^GF!M4#|?Z6^6V_0xoq>5y+v1XX8jY@tUwnzJF0CM zAPhTJef8{*{QH+P5nX1qgcYK97CD~2PGAi!mQ{l{*H~>9E;Dm#k4vNwlde}|rH3ti zD6Q8eb`t~D;YbzSjrrEh*@Q)PmJfs#D+?-GO@P9HmQS>3qIxV!$Se%1iuALd&R(^Z zk~pJG`3FBp30+4uQMVu%Nh@zf^IPXXqjk={%0cWz@B1#_>F2nLizu0j$$Ow$GS)Z6vzF~-PNr(4pyo&5@vFUj|5V%F< z_3B_ID*Msi28ElV2ZPqfJ0-T%t^@vk!flk0fLF6IAM~qW&2Qb|8??PS{0zdgt}$P@ zzC8a8@wY{Mi_ZAb?b7DICP6Jya~!8QmX+s=SPKTLzsAgLi+o{C?b&VVCO>Q6JDgP` zynG*?IQs4xu>slxV#PqWGq7|!{}3mNZmc$cw-Qq3#=oPdV2+tGNkK&fY4CQ;w#9i> z&VQxY6*Z7cq)Zf&6kONfCSD5})pZ_PxXyH+)8#(3AIL_It}cqLgwWi*KV$mv@x

{v^fhh&+`Xh%l`z@csCZC(J4UBud4@wP7JSp|A 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 RNIwJ^&Bf)#N<{R0{|97h$aMe! literal 63329 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

)qWT_}O=kHl^ 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 Mb?<$-^Z4bz0h(rGpa1{> literal 44740 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 S((va9osNdVy)Sp3y!r>5WnYs3 diff --git a/docs/screenshots/mobile-month-agenda-dark-390x844.png b/docs/screenshots/mobile-month-agenda-dark-390x844.png index 2fe695ec39962d5ee99adad88f2026d60d6b8fe2..b0bf647423a06d393cdfb7bf96f13483f78be1e4 100644 GIT binary patch literal 36904 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 F0sxrAv_}8{ literal 36324 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

${escapeHtml(scene.id)} — ${scene.changed ? "changed" : "unchanged"}

`).join("\n"); + await writeFile(join(directory, "index.html"), ` + +Calendar screenshot review

Calendar screenshot review

${changed.length} of ${scenes.length} scenes changed. Compare at native dimensions before committing.

${figures}\n`); + return report; +} + +/** Replaces a validated batch, committing the manifest last and restoring originals on failure. */ +export async function replaceScreenshotBatch({ stagingDirectory, screenshotDirectory, manifestPath, + manifest, renameFile = rename }) { + const backupDirectory = join(stagingDirectory, "backup"); + await mkdir(backupDirectory, { recursive: true }); + const stagedManifest = join(stagingDirectory, "manifest.json"); + await writeFile(stagedManifest, `${JSON.stringify(manifest, null, 2)}\n`); + const files = [...manifest.scenes.map((scene) => ({ + from: join(stagingDirectory, basename(scene.output)), + to: join(screenshotDirectory, basename(scene.output)) + })), { from: stagedManifest, to: manifestPath }]; + const replaced = []; + try { + for (const file of files) { + const backup = join(backupDirectory, basename(file.to)); + let hadOriginal = false; + try { + await renameFile(file.to, backup); + hadOriginal = true; + } catch (error) { + if (error.code !== "ENOENT") { throw error; } + } + const record = { ...file, backup, hadOriginal, installed: false }; + replaced.push(record); + await renameFile(file.from, file.to); + record.installed = true; + } + } catch (error) { + const recoveryErrors = []; + for (const file of replaced.reverse()) { + try { + if (file.installed) { await rm(file.to, { force: true }); } + if (file.hadOriginal) { await rename(file.backup, file.to); } + } catch (recoveryError) { recoveryErrors.push(recoveryError); } + } + if (recoveryErrors.length > 0) { + throw new AggregateError([error, ...recoveryErrors], + `Screenshot replacement failed; recover originals from ${backupDirectory}.`, { cause: error }); + } + throw error; + } +} + +/** Checks captured dimensions and bytes before any canonical file is replaced. */ +export async function validateScreenshotBatch({ directory, manifest, readDimensions, hashFile }) { + for (const scene of manifest.scenes) { + const path = join(directory, basename(scene.output)); + const dimensions = await readDimensions(path); + if (dimensions.width !== scene.viewport.width || dimensions.height !== scene.viewport.height || + await hashFile(path) !== scene.sha256) { + throw new Error(`${scene.id}: staged screenshot dimensions or SHA-256 do not match.`); + } + } +} diff --git a/scripts/prepare-screenshots.mjs b/scripts/prepare-screenshots.mjs new file mode 100644 index 0000000..5b2ade4 --- /dev/null +++ b/scripts/prepare-screenshots.mjs @@ -0,0 +1,34 @@ +import { run, runNpm } from "./lib/process.mjs"; +import { prepareScreenshots } from "./lib/screenshot-preparation.mjs"; +import { assertPinnedNpm, assertSupportedNode, computeSourceFingerprint } from "./screenshot-contract.mjs"; + +assertSupportedNode(); +assertPinnedNpm(); +const arguments_ = process.argv.slice(2); +if (arguments_.some((argument) => argument !== "--force")) { + throw new Error("Usage: npm run screenshots:prepare [-- --force]"); +} + +const verify = () => run(process.execPath, ["scripts/check-screenshots.mjs"]); +const result = await prepareScreenshots({ + force: arguments_.includes("--force"), + checkCurrent: async () => { + try { + await run(process.execPath, ["scripts/check-screenshots.mjs"], { capture: true }); + return true; + } catch { return false; } + }, + build: async () => { + const fingerprint = await computeSourceFingerprint(); + await runNpm(["run", "build:package"]); + await runNpm(["run", "build:examples:advanced"]); + if (await computeSourceFingerprint() !== fingerprint) { + throw new Error("Screenshot sources changed during build. Retry after edits settle."); + } + }, + capture: () => run(process.execPath, ["scripts/update-screenshots.mjs"]), + verify +}); +console.log(result === "current" + ? "Screenshot evidence is current; no build or capture was needed." + : "Screenshot evidence prepared. Review .cache/screenshots/review/index.html before committing."); diff --git a/scripts/screenshot-contract.mjs b/scripts/screenshot-contract.mjs index 4c9ad5e..e9fd5f7 100644 --- a/scripts/screenshot-contract.mjs +++ b/scripts/screenshot-contract.mjs @@ -8,6 +8,7 @@ import { SUPPORTED_NODE_RANGE } from "./lib/node-version.mjs"; import { REPOSITORY_ROOT } from "./lib/process.mjs"; +import { browserTargetsFingerprint } from "./lib/browser-targets.mjs"; export const REQUIRED_NPM_VERSION = "12.0.2"; export const SCREENSHOT_MANIFEST_PATH = resolve(REPOSITORY_ROOT, "screenshots.manifest.json"); @@ -18,7 +19,11 @@ export const EXPECTED_SCENES = Object.freeze([ "mobile-month-agenda-dark", "mobile-month-swipe-pull", "event-details-dark", - "grid-event-keyboard-focus" + "grid-event-keyboard-focus", + "compact-default-counts", + "compact-all-counts", + "wide-all-counts", + "count-keyboard-focus" ]); const SOURCE_EXTENSIONS = new Set([".css", ".html", ".js", ".json", ".mjs", ".ts"]); @@ -36,13 +41,17 @@ const SCREENSHOT_PACKAGE_SCRIPTS = Object.freeze([ "postbuild:examples:advanced", "prescreenshots:update", "screenshots:update", - "postscreenshots:update" + "postscreenshots:update", + "prescreenshots:prepare", + "screenshots:prepare", + "postscreenshots:prepare" ]); export const SCREENSHOT_SOURCE_INPUTS = Object.freeze([ "package.json", "package-lock.json", "tsconfig.base.json", "tsconfig.build.json", + "vite.config.mjs", "src", "examples/example.css", "examples/advanced/index.html", @@ -52,10 +61,13 @@ export const SCREENSHOT_SOURCE_INPUTS = Object.freeze([ "scripts/build-advanced-example.mjs", "scripts/build-package.mjs", "scripts/lib/advanced-example-build.mjs", + "scripts/lib/browser-targets.mjs", "scripts/lib/node-version.mjs", "scripts/lib/package-entries.mjs", "scripts/lib/process.mjs", "scripts/lib/styles.mjs", + "scripts/lib/screenshot-preparation.mjs", + "scripts/prepare-screenshots.mjs", "scripts/screenshot-contract.mjs", "scripts/screenshot-scenes.mjs", "scripts/serve-repository.mjs", @@ -150,6 +162,8 @@ export async function computeSourceFingerprint() { const files = (await Promise.all(SCREENSHOT_SOURCE_INPUTS.map((input) => collectFiles(resolve(REPOSITORY_ROOT, input))))).flat().sort(); const hash = createHash("sha256"); + hash.update(browserTargetsFingerprint()); + hash.update("\0"); for (const file of files) { if (!isRepositoryPath(file)) { throw new Error(`Screenshot source escaped the repository: ${file}`); diff --git a/scripts/screenshot-scenes.mjs b/scripts/screenshot-scenes.mjs index f845a27..4262ea7 100644 --- a/scripts/screenshot-scenes.mjs +++ b/scripts/screenshot-scenes.mjs @@ -5,6 +5,51 @@ const HELD_PULL_MINIMUM_OFFSET = 24; const HELD_PULL_START_RATIO = 0.8; const SWIPE_VIEWPORT_SELECTOR = ".lfc-calendar-swipe-viewport"; +export const ADDITIONAL_SCREENSHOT_SCENES = Object.freeze([ + { id: "compact-default-counts", width: 390, height: 844, + alt: "Compact calendar with total counts on days containing multiple events" }, + { id: "compact-all-counts", width: 390, height: 844, + alt: "Compact calendar with total counts on every nonempty day" }, + { id: "wide-all-counts", width: 1440, height: 900, + alt: "Wide calendar with localized total event counts on every nonempty day" }, + { id: "count-keyboard-focus", width: 390, height: 844, + alt: "Keyboard focus on a compact calendar total event count button" } +].map(({ id, width, height, alt }) => Object.freeze({ + id, route: ADVANCED_ROUTE, output: `docs/screenshots/${id}-${width}x${height}.png`, + viewport: { width, height }, hasTouch: false, + setup: ["wait-ready", "mount-count-fixture", "scroll-calendar", + ...(id === "count-keyboard-focus" ? ["focus-selected-day", "press-f2"] : [])], + alt, references: ["docs/screenshots/README.md"] +}))); + +async function mountCountFixture(page, id) { + await page.evaluate(async (sceneId) => { + const { createCalendar } = await import("/dist/index.js"); + const previous = document.querySelector("[data-my-calendar]"); + const host = previous.cloneNode(false); + previous.replaceWith(host); + const gridEventDisplay = sceneId === "compact-all-counts" + ? { compact: "count" } + : sceneId === "wide-all-counts" ? { wide: "count" } : undefined; + const calendar = createCalendar(host, { + initialDate: "2026-08-06", now: () => new Date("2026-08-06T12:00:00-07:00"), + locale: "en-US", firstDay: 1, gridEventDisplay, maxGridEventsPerDay: 2, + onEventActivate: () => {}, + events: [ + { id: "planning", title: "Planning", start: "2026-08-04T09:00", accentColor: "#805FC0" }, + { id: "workshop", title: "Workshop", start: "2026-08-06T09:00", accentColor: "#3874CB" }, + { id: "review", title: "Project review", start: "2026-08-06T11:00", accentColor: "#805FC0" }, + { id: "office-hours", title: "Office hours", start: "2026-08-10T10:00", accentColor: "#3874CB" }, + { id: "training", title: "Training", start: "2026-08-10T13:00", accentColor: "#805FC0" }, + { id: "demo", title: "Team demo", start: "2026-08-10T15:00", accentColor: "#3874CB" } + ] + }); + calendar.render(); + window.addEventListener("pagehide", () => calendar.destroy(), { once: true }); + }, id); + await page.locator('[role="gridcell"][aria-selected="true"]').waitFor({ state: "visible" }); +} + async function waitForReady(page, route) { const response = await page.goto(route, { waitUntil: "domcontentloaded" }); if (response === null || !response.ok()) { @@ -141,6 +186,20 @@ export async function prepareScreenshotScene(page, scene) { await scrollCalendarIntoView(page); await enterGridActionMode(page); break; + case "compact-default-counts": + case "compact-all-counts": + case "wide-all-counts": + case "count-keyboard-focus": + await mountCountFixture(page, scene.id); + await scrollCalendarIntoView(page); + if (scene.id === "count-keyboard-focus") { + const selectedDay = page.locator('[role="gridcell"][aria-selected="true"] > button'); + await selectedDay.focus(); + await selectedDay.press("F2"); + await page.locator('[role="gridcell"][aria-selected="true"] button:focus') + .filter({ hasText: "2" }).waitFor({ state: "visible" }); + } + break; default: throw new Error(`Unknown screenshot scene: ${scene.id}`); } diff --git a/scripts/tests/screenshot-contract.test.mjs b/scripts/tests/screenshot-contract.test.mjs index 1f84a73..b9d9796 100644 --- a/scripts/tests/screenshot-contract.test.mjs +++ b/scripts/tests/screenshot-contract.test.mjs @@ -30,6 +30,7 @@ void test("screenshot fingerprints select the exact advanced route and build con "package-lock.json", "tsconfig.base.json", "tsconfig.build.json", + "vite.config.mjs", "src", "examples/example.css", "examples/advanced/index.html", @@ -39,10 +40,13 @@ void test("screenshot fingerprints select the exact advanced route and build con "scripts/build-advanced-example.mjs", "scripts/build-package.mjs", "scripts/lib/advanced-example-build.mjs", + "scripts/lib/browser-targets.mjs", "scripts/lib/node-version.mjs", "scripts/lib/package-entries.mjs", "scripts/lib/process.mjs", "scripts/lib/styles.mjs", + "scripts/lib/screenshot-preparation.mjs", + "scripts/prepare-screenshots.mjs", "scripts/screenshot-contract.mjs", "scripts/screenshot-scenes.mjs", "scripts/serve-repository.mjs", @@ -93,8 +97,9 @@ void test("screenshot update builds only the package and advanced example", asyn ); assert.equal( packageManifest.scripts?.["screenshots:update"], - "npm run build:package && npm run build:examples:advanced && node scripts/update-screenshots.mjs" + "node scripts/prepare-screenshots.mjs --force" ); + assert.equal(packageManifest.scripts?.["screenshots:prepare"], "node scripts/prepare-screenshots.mjs"); }); void test("screenshot fingerprints ignore root versions and nonvisual package scripts", () => { diff --git a/scripts/tests/screenshot-preparation.test.mjs b/scripts/tests/screenshot-preparation.test.mjs new file mode 100644 index 0000000..733c258 --- /dev/null +++ b/scripts/tests/screenshot-preparation.test.mjs @@ -0,0 +1,96 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { prepareScreenshots, replaceScreenshotBatch, validateScreenshotBatch, + writeScreenshotReview } from "../lib/screenshot-preparation.mjs"; + +void test("current evidence skips builds and capture; stale evidence builds once before capture", async () => { + const calls = []; + const options = { checkCurrent: async () => true, ci: false, + build: async () => { calls.push("build"); }, capture: async () => { calls.push("capture"); }, + verify: async () => { calls.push("verify"); } }; + assert.equal(await prepareScreenshots(options), "current"); + assert.deepEqual(calls, []); + assert.equal(await prepareScreenshots({ ...options, checkCurrent: async () => false }), "updated"); + assert.deepEqual(calls, ["build", "capture", "verify"]); +}); + +void test("CI cannot accept stale or forced evidence, and failed builds never capture", async () => { + let captures = 0; + const options = { checkCurrent: async () => false, ci: true, + build: async () => { throw new Error("build failed"); }, + capture: async () => { captures++; }, verify: async () => {} }; + await assert.rejects(prepareScreenshots(options), /CI cannot accept/u); + await assert.rejects(prepareScreenshots({ ...options, force: true }), /CI cannot accept/u); + await assert.rejects(prepareScreenshots({ ...options, ci: false }), /build failed/u); + assert.equal(captures, 0); +}); + +async function fixture(t) { + const root = await mkdtemp(join(tmpdir(), "calendar-screenshots-")); + t.after(() => rm(root, { recursive: true, force: true })); + const canonical = join(root, "canonical"); + const staging = join(root, "staging"); + await mkdir(canonical); + await mkdir(staging); + const manifestPath = join(root, "manifest.json"); + await writeFile(manifestPath, "old manifest"); + for (const name of ["one.png", "two.png"]) { + await writeFile(join(canonical, name), `old ${name}`); + await writeFile(join(staging, name), `new ${name}`); + } + const manifest = { scenes: ["one", "two"].map((id) => ({ id, + output: `docs/screenshots/${id}.png`, viewport: { width: 390, height: 844 }, + alt: `Calendar scene ${id}`, sha256: `new-${id}` })) }; + return { root, canonical, staging, manifestPath, manifest }; +} + +void test("failed replacement restores all old images and manifest", async (t) => { + const f = await fixture(t); + await assert.rejects(replaceScreenshotBatch({ stagingDirectory: f.staging, + screenshotDirectory: f.canonical, manifestPath: f.manifestPath, manifest: f.manifest, + renameFile: async (from, to) => { + if (from === join(f.staging, "two.png")) { throw new Error("disk failure"); } + await rename(from, to); + } }), /disk failure/u); + assert.equal(await readFile(f.manifestPath, "utf8"), "old manifest"); + for (const name of ["one.png", "two.png"]) { + assert.equal(await readFile(join(f.canonical, name), "utf8"), `old ${name}`); + } +}); + +void test("successful replacement commits manifest after the complete image batch", async (t) => { + const f = await fixture(t); + await replaceScreenshotBatch({ stagingDirectory: f.staging, screenshotDirectory: f.canonical, + manifestPath: f.manifestPath, manifest: f.manifest, renameFile: async (from, to) => { + if (to === f.manifestPath) { + assert.equal(await readFile(join(f.canonical, "two.png"), "utf8"), "new two.png"); + } + await rename(from, to); + } }); + assert.deepEqual(JSON.parse(await readFile(f.manifestPath, "utf8")), f.manifest); +}); + +void test("batch validation rejects incomplete or mismatched captures before replacement", async (t) => { + const f = await fixture(t); + await assert.rejects(validateScreenshotBatch({ directory: f.staging, manifest: f.manifest, + readDimensions: async () => ({ width: 390, height: 844 }), + hashFile: async () => "wrong-hash" }), /SHA-256/u); + assert.equal(await readFile(f.manifestPath, "utf8"), "old manifest"); +}); + +void test("review gallery records changed scenes and preserves before and after bytes", async (t) => { + const f = await fixture(t); + const review = join(f.root, "review"); + const previousManifest = structuredClone(f.manifest); + previousManifest.scenes[0].sha256 = "old-one"; + const report = await writeScreenshotReview({ directory: review, beforeDirectory: f.canonical, + afterDirectory: f.staging, manifest: f.manifest, previousManifest }); + assert.deepEqual(report.changed, ["one"]); + assert.equal(await readFile(join(review, "before", "one.png"), "utf8"), "old one.png"); + assert.equal(await readFile(join(review, "after", "one.png"), "utf8"), "new one.png"); + assert.match(await readFile(join(review, "index.html"), "utf8"), /Before: Calendar scene one/u); +}); diff --git a/scripts/update-screenshots.mjs b/scripts/update-screenshots.mjs index a66d246..282517b 100644 --- a/scripts/update-screenshots.mjs +++ b/scripts/update-screenshots.mjs @@ -1,116 +1,113 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import { dirname, relative, resolve } from "node:path"; +import { mkdir, mkdtemp, open, rm } from "node:fs/promises"; +import { basename, dirname, join, relative, resolve } from "node:path"; import { chromium } from "@playwright/test"; +import { resolveBrowserTargets } from "./lib/browser-targets.mjs"; import { REPOSITORY_ROOT } from "./lib/process.mjs"; import { - assertSupportedNode, - assertPinnedNpm, - computeSourceFingerprint, - readPngDimensions, - readScreenshotManifest, - SCREENSHOT_DIRECTORY, - SCREENSHOT_MANIFEST_PATH, - sha256File, - validateScreenshotManifest + replaceScreenshotBatch, validateScreenshotBatch, writeScreenshotReview +} from "./lib/screenshot-preparation.mjs"; +import { + assertSupportedNode, assertPinnedNpm, computeSourceFingerprint, readPngDimensions, + readScreenshotManifest, SCREENSHOT_DIRECTORY, SCREENSHOT_MANIFEST_PATH, + sha256File, validateScreenshotManifest } from "./screenshot-contract.mjs"; -import { prepareScreenshotScene } from "./screenshot-scenes.mjs"; +import { ADDITIONAL_SCREENSHOT_SCENES, prepareScreenshotScene } from "./screenshot-scenes.mjs"; import { startRepositoryServer } from "./serve-repository.mjs"; assertSupportedNode(); assertPinnedNpm(); +if (process.env["CI"]) { + throw new Error("CI must verify screenshot evidence; canonical captures can only be prepared locally."); +} -const manifest = await readScreenshotManifest(); +const previousManifest = await readScreenshotManifest(); +const manifest = { + ...previousManifest, + scenes: [...previousManifest.scenes, ...ADDITIONAL_SCREENSHOT_SCENES.filter((scene) => + !previousManifest.scenes.some((previous) => previous.id === scene.id))] +}; const manifestErrors = validateScreenshotManifest(manifest); if (manifestErrors.length > 0) { throw new Error(`Screenshot manifest is invalid:\n- ${manifestErrors.join("\n- ")}`); } -const repositoryServer = await startRepositoryServer({ port: 0 }); -const browser = await chromium.launch({ headless: true }); -const updatedScenes = []; +const cacheDirectory = resolve(REPOSITORY_ROOT, ".cache", "screenshots"); +await mkdir(cacheDirectory, { recursive: true }); +const lockPath = join(cacheDirectory, "capture.lock"); +const lock = await open(lockPath, "wx"); +let repositoryServer; +let browser; try { + const stagingDirectory = await mkdtemp(join(cacheDirectory, "staging-")); + const sourceFingerprint = await computeSourceFingerprint(); + repositoryServer = await startRepositoryServer({ port: 0 }); + browser = await chromium.launch({ headless: true }); + const updatedScenes = []; for (const scene of manifest.scenes) { - const output = resolve(REPOSITORY_ROOT, scene.output); - if (dirname(output) !== SCREENSHOT_DIRECTORY) { + if (dirname(resolve(REPOSITORY_ROOT, scene.output)) !== SCREENSHOT_DIRECTORY) { throw new Error(`${scene.id}: output escaped the canonical screenshot directory.`); } - + const output = join(stagingDirectory, basename(scene.output)); const context = await browser.newContext({ - baseURL: repositoryServer.origin, - colorScheme: "light", - deviceScaleFactor: 1, - hasTouch: scene.hasTouch, - locale: "en-US", - reducedMotion: "reduce", - serviceWorkers: "block", - timezoneId: "America/Los_Angeles", - viewport: scene.viewport + baseURL: repositoryServer.origin, colorScheme: "light", deviceScaleFactor: 1, + hasTouch: scene.hasTouch, locale: "en-US", reducedMotion: "reduce", + serviceWorkers: "block", timezoneId: "America/Los_Angeles", viewport: scene.viewport }); try { const page = await context.newPage(); const runtimeErrors = []; page.on("console", (message) => { - if (message.type() === "error") { - runtimeErrors.push(`console: ${message.text()}`); - } + if (message.type() === "error") { runtimeErrors.push(`console: ${message.text()}`); } }); - page.on("pageerror", (error) => { - runtimeErrors.push(`page: ${error.message}`); - }); - page.on("request", (request) => { - if (new URL(request.url()).origin !== repositoryServer.origin) { - runtimeErrors.push(`external request blocked by screenshot contract: ${request.url()}`); + page.on("pageerror", (error) => { runtimeErrors.push(`page: ${error.message}`); }); + await page.route("**/*", async (route) => { + if (new URL(route.request().url()).origin === repositoryServer.origin) { + await route.continue(); + } else { + runtimeErrors.push(`External request: ${route.request().url()}`); + await route.abort("blockedbyclient"); } }); - await prepareScreenshotScene(page, scene); + await page.screenshot({ animations: "disabled", caret: "hide", fullPage: false, + path: output, scale: "css" }); if (runtimeErrors.length > 0) { throw new Error(`${scene.id} emitted runtime errors:\n- ${runtimeErrors.join("\n- ")}`); } - await mkdir(dirname(output), { recursive: true }); - await page.screenshot({ - animations: "disabled", - caret: "hide", - fullPage: false, - path: output, - scale: "css" - }); - } finally { - await context.close(); - } - - const dimensions = await readPngDimensions(output); - if (dimensions.width !== scene.viewport.width || dimensions.height !== scene.viewport.height) { - throw new Error( - `${scene.id} captured ${String(dimensions.width)}x${String(dimensions.height)}; expected ` + - `${String(scene.viewport.width)}x${String(scene.viewport.height)}.` - ); - } - updatedScenes.push({ - ...scene, - sha256: await sha256File(output) - }); - console.log(`Captured ${relative(REPOSITORY_ROOT, output)}.`); + } finally { await context.close(); } + updatedScenes.push({ ...scene, sha256: await sha256File(output) }); + console.log(`Staged ${scene.output}.`); + } + const updatedManifest = { + ...manifest, state: "final", + toolchain: { ...manifest.toolchain, node: process.versions.node }, + browserTargets: resolveBrowserTargets(), sourceFingerprint, scenes: updatedScenes + }; + const finalErrors = validateScreenshotManifest(updatedManifest, { final: true }); + if (finalErrors.length > 0) { throw new Error(finalErrors.join("\n")); } + await validateScreenshotBatch({ directory: stagingDirectory, manifest: updatedManifest, + readDimensions: readPngDimensions, hashFile: sha256File }); + if (await computeSourceFingerprint() !== sourceFingerprint) { + throw new Error("Screenshot sources changed during capture; canonical evidence was preserved. Retry after edits settle."); } + const review = await writeScreenshotReview({ directory: join(cacheDirectory, "review"), + beforeDirectory: SCREENSHOT_DIRECTORY, afterDirectory: stagingDirectory, + previousManifest, manifest: updatedManifest }); + await replaceScreenshotBatch({ stagingDirectory, screenshotDirectory: SCREENSHOT_DIRECTORY, + manifestPath: SCREENSHOT_MANIFEST_PATH, manifest: updatedManifest }); + await rm(stagingDirectory, { recursive: true, force: true }); + console.log(`Finalized ${updatedScenes.length} screenshots; ${review.changed.length} changed. ` + + `Review ${relative(REPOSITORY_ROOT, join(cacheDirectory, "review", "index.html"))}.`); } finally { - await browser.close(); - await repositoryServer.close(); + try { await browser?.close(); } + finally { + try { await repositoryServer?.close(); } + finally { + await lock.close(); + await rm(lockPath, { force: true }); + } + } } - -const updatedManifest = { - ...manifest, - state: "final", - toolchain: { - ...manifest.toolchain, - node: process.versions.node - }, - sourceFingerprint: await computeSourceFingerprint(), - scenes: updatedScenes -}; -await writeFile(SCREENSHOT_MANIFEST_PATH, `${JSON.stringify(updatedManifest, null, 2)}\n`, "utf8"); -console.log( - `Updated ${String(updatedScenes.length)} deterministic screenshots and finalized their manifest.` -); From b80bbacf76cc201ed02787313cf97a915dd1abd9 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:30:11 -0700 Subject: [PATCH 08/22] docs: reorganize consumer guides and add remote-data walkthrough --- README.md | 7 +- docs/README.md | 27 ++++- docs/api.md | 69 +++++++++--- docs/features.md | 21 ++-- docs/getting-started.md | 30 +++++ docs/integration-guide.md | 127 ++++++++++++++++++---- docs/remote-data.md | 119 ++++++++++++++++++++ examples/README.md | 7 +- examples/index.html | 20 +++- examples/remote-data/README.md | 16 +++ examples/remote-data/events.json | 7 ++ examples/remote-data/index.html | 41 +++++++ examples/remote-data/main.js | 90 +++++++++++++++ examples/remote-data/source.js | 73 +++++++++++++ examples/remote-data/styles.css | 45 ++++++++ examples/remote-data/tsconfig.json | 10 ++ package.json | 4 +- scripts/tests/remote-data-source.test.mjs | 69 ++++++++++++ tests/e2e/developer-pages.spec.js | 9 +- tests/e2e/remote-data.spec.js | 112 +++++++++++++++++++ 20 files changed, 841 insertions(+), 62 deletions(-) create mode 100644 docs/getting-started.md create mode 100644 docs/remote-data.md create mode 100644 examples/remote-data/README.md create mode 100644 examples/remote-data/events.json create mode 100644 examples/remote-data/index.html create mode 100644 examples/remote-data/main.js create mode 100644 examples/remote-data/source.js create mode 100644 examples/remote-data/styles.css create mode 100644 examples/remote-data/tsconfig.json create mode 100644 scripts/tests/remote-data-source.test.mjs create mode 100644 tests/e2e/remote-data.spec.js diff --git a/README.md b/README.md index c549836..f5436e2 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ Litefold Calendar is a mobile-first, responsive month calendar for applications Use Litefold Calendar when your product needs a polished month calendar without adopting a complete scheduling platform or maintaining separate mobile and desktop implementations. -> **Alpha:** Until the first stable release, npm's `alpha` and `latest` dist-tags intentionally select the same public prerelease, so an unqualified install also receives alpha software. Pin an exact version in production-like environments. Public API changes remain possible before `1.0.0` and will be documented in the changelog. +> **Prerelease:** The `nightly` channel contains validated snapshots of `main`. Before the first stable release, `latest` also selects the current nightly; after stable publication, `latest` belongs to stable releases. Pin an exact version for reproducible deployments. Public API changes remain possible before `1.0.0` and are recorded in the changelog. Existing alpha releases remain available by exact version. ## Install ```sh -npm install @tryagaindev/litefold-calendar@alpha +npm install @tryagaindev/litefold-calendar@nightly ``` The package is pure ESM and has no runtime dependencies. The example below assumes an npm-aware build tool that resolves bare module specifiers and CSS imports. For a page without a bundler, use the [classic-script entry-point recipe](docs/integration-guide.md#classic-script-entry-point). @@ -84,6 +84,9 @@ Try the [hosted demo](https://tryagaindev.github.io/litefold-calendar/) or revie ## Next steps +- Follow [Getting started](docs/getting-started.md) for a short adoption path. +- Connect an API and filters with the [remote-data walkthrough](docs/remote-data.md). +- Configure [day event counts](docs/integration-guide.md#choose-event-counts) and an [application-owned chooser](docs/integration-guide.md#own-the-event-chooser). - Run the [basic example](examples/basic/) or browse the [hosted demo](https://tryagaindev.github.io/litefold-calendar/). - Choose a reusable scenario from the [examples guide](examples/README.md). - Use the [documentation hub](docs/README.md) to find integration, contribution, and maintainer guidance. diff --git a/docs/README.md b/docs/README.md index 4f2e1be..4fd0dc5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,17 +16,30 @@ repository internals. | Goal | Canonical guide | | --- | --- | -| Install and render a calendar | [Install](../README.md#install), [first render](../README.md#first-render), and [basic example](../examples/basic/) | -| Decide whether the month-and-agenda model fits | [Features and alpha scope](features.md) | +| Install and render a calendar | [Getting started](getting-started.md), [first render](../README.md#first-render), and [basic example](../examples/basic/) | +| Decide whether the month-and-agenda model fits | [Features and scope](features.md) | | Check browser and platform requirements | [Browser support](browser-support.md) | | Choose a runnable starting point | [Examples guide](../examples/README.md) | | Get help, report a problem, or evaluate an upgrade | [Support policy](../SUPPORT.md) and [changelog](../CHANGELOG.md) | +### Everyday application integration + +Use this lane after the first render, when you need a working data source or +an application interaction. These recipes use plain JavaScript with optional +JSDoc and introduce one task at a time. + +| Goal | Canonical guide | +| --- | --- | +| Fetch events, validate JSON, filter, and refresh after saving | [Remote-data walkthrough](remote-data.md) and [runnable example](../examples/remote-data/) | +| Make busy days recognizable in a sidebar or full-size calendar | [Choose event counts](integration-guide.md#choose-event-counts) | +| Open existing application dialogs from events or total counts | [Own the event chooser](integration-guide.md#own-the-event-chooser) | +| Understand default empty, failure, and Retry behavior | [Remote-data recovery](remote-data.md#4-keep-empty-and-error-states-distinct) | + ### Integration and API -Use this lane when adapting application data, behavior, presentation, or an -existing integration. It favors precise public contracts, ownership, tradeoffs, -and failure behavior. +Use this advanced lane for typed adapters, caching, customization, or exact +public contracts. The exhaustive advanced example also serves repository +coverage; everyday integrations do not need its full setup. | Goal | Canonical guide | | --- | --- | @@ -52,6 +65,7 @@ work, observable verification, and review evidence. Contributor policy lives in | Set up the repository and submit a change | [Contributing](../CONTRIBUTING.md) | | Run focused checks and the complete repository gate | [Contributor commands](../CONTRIBUTOR_COMMANDS.md) | | Commit and push an already-scoped change | [Commit-and-push operation skill](../.agents/skills/commit-and-push/SKILL.md) | +| Carry a change through validation, screenshots, and delivery | [Local-to-production checklist](change-delivery.md) | | Follow source conventions and dependency direction | [Code style](code-style.md) and [internal architecture](architecture.md) | | Understand extension lifecycle implementation | [First-party extensions](first-party-extensions.md) | | Review the `setEvents()` decision or reproduce measurements | [Dynamic event update decision record](dynamic-event-updates-adr.md) | @@ -73,7 +87,8 @@ permissions by itself. | Goal | Canonical guide | | --- | --- | -| Coordinate an authorized npm alpha release | [Release-to-npm operation skill](../.agents/skills/release-to-npm/SKILL.md) and [release operations](release-operations.md) | +| Coordinate nightly publication | [Release-to-npm operation skill](../.agents/skills/release-to-npm/SKILL.md) and [nightly operations](release-operations.md) | +| Deliver and verify a completed change | [Local-to-production checklist](change-delivery.md) | | Review release policy and invariants | [Release policy](releasing.md) | | Configure hosted controls or recover an exceptional release | [Release administration](release-administration.md) | | Verify registry and release evidence | [Package verification](package-verification.md) | diff --git a/docs/api.md b/docs/api.md index e5a5171..5ff277e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,6 +1,6 @@ # Public API reference -This is the exhaustive reference for the package's public alpha API. Import core values and types from `@tryagaindev/litefold-calendar`, CSS from `@tryagaindev/litefold-calendar/styles.css`, and optional first-party components from their documented `/extensions/` subpaths. Internal modules and `dist/` files are not supported entry points. +This is the exhaustive reference for the package's public prerelease API. Import core values and types from `@tryagaindev/litefold-calendar`, CSS from `@tryagaindev/litefold-calendar/styles.css`, and optional first-party components from their documented `/extensions/` subpaths. Internal modules and `dist/` files are not supported entry points. Before `1.0.0`, public names and defaults may still change. Review the [changelog](../CHANGELOG.md) for shipped changes and the @@ -32,9 +32,9 @@ The root module exports exactly the following symbols. Entries in the Types colu | Area | Runtime values | Types | |---|---|---| -| Construction | `createCalendar` | `Calendar`, `CalendarGridEventPlacement`, `CalendarOptions`, `CalendarEventTimeDisplay`, `CalendarWeekRowSizing` | +| Construction | `createCalendar` | `Calendar`, `CalendarGridEventDisplay`, `CalendarGridEventPlacement`, `CalendarOptions`, `CalendarEventTimeDisplay`, `CalendarWeekRowSizing` | | Dates and events | — | `CalendarDate`, `CalendarDateInput`, `CalendarEvent`, `CalendarEventInput`, `CalendarEvents`, `CalendarEventSource`, `CalendarRange`, `CalendarRangeBounds` | -| Actions | — | `CalendarAction`, `CalendarDayContextMenu`, `CalendarDaySelection`, `CalendarEventActionElement`, `CalendarEventActivation`, `CalendarEventContextMenu`, `CalendarEventContextMenuAvailability`, `CalendarEventSurface` | +| Actions | — | `CalendarAction`, `CalendarDayContextMenu`, `CalendarDaySelection`, `CalendarEventActionElement`, `CalendarEventActivation`, `CalendarEventOverflowActivation`, `CalendarEventContextMenu`, `CalendarEventContextMenuAvailability`, `CalendarEventSurface` | | State and announcements | — | `CalendarAnnouncement`, `CalendarIssue`, `CalendarPhase`, `CalendarState` | | Localization and icons | — | `CalendarFirstDay`, `CalendarHeadingLevel`, `CalendarIconFactory`, `CalendarIcons`, `CalendarMessages` | | First-party extensions | — | `CalendarExtension` | @@ -150,11 +150,11 @@ When replacement data commits, the calendar preserves the displayed month, selec ### Built-in month-and-year jump -The configured `h1` through `h6` month heading contains a native button showing the localized displayed month and year. Activating it with pointer, Enter, or Space opens a package-owned `popover="auto"` with dialog semantics and focuses its month ``. The modal fallback makes the rest of the document inert. The required year input follows configured bounds, and changing it disables months that do not intersect those bounds. Both presentations support Jump, Cancel, Escape, and pointer light dismissal. At exactly a `24rem` calendar content width and above, the visible title uses the locale's full month and numeric year. Below `24rem`, package CSS exposes its already-rendered `month: "short", year: "numeric"` form; decorative pull-pager lanes follow the same visual rule. The trigger name, grid name, and live text remain complete, and the compact title and pager lanes are excluded from accessibility. This is package-owned presentation: applications must not measure the viewport or calendar to select a label, or target private title and pager descendants. -Jumping preserves the selected day where possible and otherwise clamps it to the target month and configured range. It never invokes `onDaySelect`. Invalid form input keeps the picker open. Jump, Cancel, and Escape close it and restore focus to the month-title trigger; pointer light-dismiss closes it without moving focus. +Jumping preserves the selected day where possible and otherwise clamps it to the target month and configured range. It never invokes `onDaySelect`. Invalid form input keeps the picker open. Jump, Cancel, and Escape close it and restore focus to the month-title trigger. Popover pointer light dismissal preserves the user's new focus target; modal-dialog backdrop dismissal restores focus to the trigger. The picker, day buttons, Previous, Next, Today, native pull/snap pager, and Page Up/Down keyboard navigation all use the same configured bounds. Dates outside the bounds remain present when required by the fixed 42-day layout but their native day buttons are disabled. Previous, Next, and Today remain in the Tab order and expose `aria-disabled="true"` when they have no permitted destination; their handlers are guarded no-ops in that state. Other package-owned interaction quietly stops or clamps at a boundary. Only an out-of-range application call to `gotoDate()` or `focusDate()` throws. @@ -283,6 +283,8 @@ Metadata is optional and opaque. The package preserves it by reference without i Event IDs must contain a non-whitespace character, remain at most 256 UTF-16 code units, and be unique within one returned snapshot. IDs are preserved without trimming. Titles are trimmed and must then contain 1 through 1,024 UTF-16 code units. +Day occurrences use one canonical order across grid summaries, the agenda, and overflow callbacks: all-day events first, then ascending complete civil start, normalized title, and preserved ID. Title and ID comparisons use UTF-16 code-unit order rather than locale collation. + `accentColor` is the per-event **event marker color** field. It accepts exactly an opaque six-digit hexadecimal color (`#RRGGBB`, case-insensitive input) and normalizes it to uppercase. An invalid value becomes `null` rather than invalidating the event; the built-in marker then uses its CSS-token fallback. This field colors only the built-in SVG marker. It does not tint an event summary or choose its text, background, border, or event leading-rule color. Core rendering does not emit a `style` attribute. See [calendar anatomy and color vocabulary](component-anatomy.md#three-color-roles-that-sound-similar) for the related names. Use `renderEventMarker` when metadata-driven visual treatment needs more than the built-in marker, and keep that application-owned output compatible with the application Content Security Policy. `url` is optional. It must be unchanged by trimming, contain no control character or credentials, and resolve against the host document to a relative or HTTP(S) destination. Both the supplied string and the resolved destination must be no longer than 2,048 UTF-16 code units. Empty, malformed, unsupported-scheme, credential-bearing, control-character, trim-altered, or oversized values reject the complete snapshot. A normalized event exposes the validated relative reference unchanged, a canonical absolute HTTP(S) string, or `null`; the native anchor resolves a relative reference in the host document. @@ -326,6 +328,10 @@ interface CalendarOptions { readonly headingLevel?: CalendarHeadingLevel; readonly weekRowSizing?: CalendarWeekRowSizing; readonly gridEventPlacement?: CalendarGridEventPlacement; + readonly gridEventDisplay?: { + readonly compact?: CalendarGridEventDisplay; + readonly wide?: CalendarGridEventDisplay; + }; readonly now?: (this: void) => Date; readonly sourceEventLimit?: number; readonly maxGridEventsPerDay?: number; @@ -339,6 +345,7 @@ interface CalendarOptions { readonly swipe?: boolean; readonly extensions?: readonly CalendarExtension[]; readonly onEventActivate?: CalendarAction>; + readonly onEventOverflowActivate?: CalendarAction>; readonly isEventContextMenuAvailable?: ( this: void, context: Readonly> @@ -366,9 +373,10 @@ type CalendarHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; type CalendarEventTimeDisplay = "all" | "grid" | "agenda" | "none"; type CalendarWeekRowSizing = "equal" | "content"; type CalendarGridEventPlacement = "top" | "center" | "bottom"; +type CalendarGridEventDisplay = "events" | "count" | "count-when-multiple"; ``` -The options snapshot is immutable for the life of an instance. `setEvents()` is the one narrow replacement API; mutating `options.events` or the original options object has no effect. Recreate the calendar to change locale, time zone, date bounds, callbacks, extensions, render hooks, messages, icons, limits, integration nodes, or other construction-time configuration. External filter or cache state may change independently; call `refetchEvents()` after changing it. Unknown own string keys on the top-level options object, `messages`, `icons`, or a render-hook definition are rejected as `invalid-configuration`, so misspelled configuration does not fail silently; own symbol keys are ignored. Extension factories validate and snapshot their own options before the calendar mutates its host. +The options snapshot is immutable for the life of an instance. `setEvents()` is the one narrow replacement API; mutating `options.events` or the original options object has no effect. Recreate the calendar to change locale, time zone, date bounds, callbacks, extensions, render hooks, messages, icons, limits, integration nodes, or other construction-time configuration. External filter or cache state may change independently; call `refetchEvents()` after changing it. Unknown own string keys on the top-level options object, `gridEventDisplay`, `messages`, `icons`, or a render-hook definition are rejected as `invalid-configuration`, so misspelled configuration does not fail silently; own symbol keys are ignored. Extension factories validate and snapshot their own options before the calendar mutates its host. Every public callback and factory declares `this: void`. Use an arrow function or a function that does not depend on a dynamic receiver. @@ -385,12 +393,13 @@ The [advanced TypeScript example](../examples/advanced/) demonstrates every opti | `locale` | Browser-resolved locale | Controls `Intl` full and abbreviated visual months, short/narrow weekdays, full accessible dates and month names, and time labels. | An empty or invalid BCP 47 language tag is `invalid-configuration`. | | `timeZone` | None; `Date` inputs use device-local fields | Projects supplied `Date` instants into an IANA zone. It never changes event strings. | An empty or invalid zone identifier is `invalid-configuration`. | | `firstDay` | `"locale"` | Uses the locale week convention from either the platform's `Intl.Locale#getWeekInfo()` method or `Intl.Locale#weekInfo` accessor, or accepts explicit `0 = Sunday` through `6 = Saturday`. | Any other value is `invalid-configuration`; locale mode falls back to Sunday when neither platform form yields a valid `firstDay`. | -| `headingLevel` | `2` | Chooses the native `h1` through `h6` month-title level. Its native title button opens the built-in month-and-year popover. The agenda, popover, and status-panel headings use the next level, capped at `h6`. | A non-integer or value outside `1` through `6` is `invalid-configuration`. | +| `headingLevel` | `2` | Chooses the native `h1` through `h6` month-title level. Its native title button opens the built-in month-and-year chooser. The agenda, chooser, and status-panel headings use the next level, capped at `h6`. | A non-integer or value outside `1` through `6` is `invalid-configuration`. | | `weekRowSizing` | `"equal"` | Controls the six week tracks and normal compact slots. `"equal"` gives every week the intrinsic size required by the tallest week and gives package-owned, normally visible compact primary-event, count, and compact-primary overflow roots one common full slot. Focus-only later summaries remain intrinsic. `"content"` sizes each week independently and keeps compact roots intrinsic. Neither mode fixes the calendar height or clips taller content; equal rows can increase the grid's vertical space. | Any other value is `invalid-configuration`; the diagnostic identifies `weekRowSizing`. | | `gridEventPlacement` | `"top"` | Aligns the complete event-summary and overflow stack within the available block space below the top-aligned date at every width. `"center"` and `"bottom"` safely fall back toward the top when the stack cannot fit. It does not affect agenda rows, DOM order, or focus order. | Any other value is `invalid-configuration`; the diagnostic identifies `gridEventPlacement`. | +| `gridEventDisplay` | `{ compact: "count-when-multiple", wide: "events" }` | Independently chooses `"events"`, `"count"`, or `"count-when-multiple"` for calendar-container widths at most `42rem` and above `42rem`. `"count"` shows a total-count button on every nonempty date; `"count-when-multiple"` does so only above one event. Counts use all loaded, normalized occurrences for that date, independent of summary limits, agenda pagination, and render-hook visuals. Empty days show no count action. Container CSS controls transitions without rerender or refetch. | A non-object, unknown own string key, or unsupported compact/wide mode is `invalid-configuration`. Omitted nested properties keep their defaults. | | `now` | `() => new Date()` | Supplies the current instant during construction, every grid render, and Today actions. It may run more than once during one public operation, so keep it synchronous and free of observable side effects. | A construction-time throw/invalid date is `invalid-configuration`; a later failure enters fatal `internal-error`. | | `sourceEventLimit` | `10,000` | Caps one complete source snapshot. Allowed range: `1` through `10,000`. | An invalid setting is `invalid-configuration`; an oversized result is `event-limit-exceeded`. | -| `maxGridEventsPerDay` | `3` | Caps direct event representations in a day cell. Allowed range: `0` through `10`; `0` suppresses individual representations while retaining the accessible day count, agenda events, and native overflow action. Compact presentation follows the [responsive design](../DESIGN.md#responsive-model); the first actionable event remains named and later actions remain available when focused. | An invalid setting is `invalid-configuration`. | +| `maxGridEventsPerDay` | `3` | Caps direct event representations in a day cell. Allowed range: `0` through `10`; `0` suppresses individual representations while retaining the accessible day count, agenda events, and native overflow action. Count presentation ignores this cap. In `"events"` mode, compact presentation follows the [responsive design](../DESIGN.md#responsive-model); the first actionable event remains named and later actions remain available when focused. | An invalid setting is `invalid-configuration`. | | `eventTimeDisplay` | `"all"` | Controls where localized event times are visually exposed: `"all"`, `"grid"`, `"agenda"`, or `"none"`. A time hidden from a surface remains a native `
    ` whose event representations are `
  1. |---|---|---| | Click or tap a day; Enter or Space on a focused day | `onDaySelect` | The selected-day agenda is updated before the action runs; returning or throwing does not cancel selection. | | Click an event action with an activation callback; Enter or Space | `onEventActivate` | `element` is the native anchor/button and `surface` identifies grid or agenda. Direct event activation never selects a day or calls `onDaySelect`. | +| Click a total-count or overflow button; Enter or Space | `onEventOverflowActivate` | Runs before selection or replacement with every occurrence on that date; synchronously prevent default to open an application-owned chooser. | | Right-click a day; Context Menu or Shift+F10 | `onDayContextMenu` | Coordinates are viewport-relative and suitable for positioning an application-owned context surface. | | Right-click an eligible event; Context Menu or Shift+F10 | `onEventContextMenu` | Includes the normalized event, represented occurrence date, surface, action element, native event, and viewport-relative coordinates. | | Click or tap an eligible non-link event with no activation callback; Enter or Space | `onEventContextMenu` | Context-only events remain native buttons. Primary activation invokes their only application action and supplies the resulting click event and coordinates. | @@ -525,7 +545,11 @@ Day selection commits and rerenders before `onDaySelect` runs. Its `element` is Within the grid, exactly one day proxy has `tabindex="0"`; event and overflow actions remain `-1`. The pager viewport is programmatically focusable only, and its decorative lanes are `aria-hidden` and noninteractive, so Tab continues from the toolbar into the managed grid rather than into paging chrome. F2 enters the current cell's first visible action. Up/Down moves without wrapping; Escape or F2 returns to the day. Tab exits forward toward the agenda, while Shift+Tab returns to the day proxy. Focus restoration identifies an occurrence by surface, date, and event ID rather than retaining a replaced node. Stale or detached elements cannot invoke actions. -The native overflow action uses localized `gridMore` visual fallback content and the `gridMoreLabel` date/count accessible name. Custom wide overflow content may replace that visual fallback without replacing the action or its name. Activating the action selects the represented date, resets the agenda expansion, and focuses the agenda heading without invoking `onDaySelect`. +The native overflow action uses localized `gridMore` visual fallback content and the `gridMoreLabel` date/count accessible name. Total-count actions use a localized number in compact presentation, `gridEventCount` text in wide presentation, and `gridEventCountLabel` for the complete date/count accessible name. When one shared action presents a compact total and wide overflow, its accessible name retains the complete total/date and appends the localized wide overflow text so the visible label remains in the name. Custom visuals do not replace the native action or its name. By default, activation selects the represented date, resets the agenda expansion, and focuses the agenda heading without invoking `onDaySelect`. + +`onEventOverflowActivate` receives immutable date and event snapshots before selection or DOM replacement. Its `events` array contains every loaded, normalized occurrence for that date in canonical order; `eventCount` equals its length. Metadata remains application-owned by reference, and DOM/native-event references retain their native behavior. Call `nativeEvent.preventDefault()` synchronously to take ownership of both interaction and focus, including restoration after an application dialog closes. Cancellation after an `await` cannot stop the default action. Destroying the calendar during the callback prevents the default operation. Returned promises use the ordinary action-failure path. + +Count presentation excludes individual summaries from new managed focus entry. A summary already focused when its container crosses the breakpoint remains visible until blur. F2 and arrow navigation skip CSS-hidden actions without measuring the container. The compact default intentionally changes busy days to a total-count action; `gridEventDisplay: { compact: "events" }` restores the earlier presentation. ## Observe state: `CalendarState` @@ -599,6 +623,8 @@ Each icon factory receives the host's `Document` and must return distinct, detac | `gridEventInstructions` | `Use arrow keys to move between dates and Enter or Space to select. Press F2 on a date to move to its visible event actions; use Up and Down Arrow between actions, and Escape or F2 to return.` | — | | `gridMore` | `{count} more` | `{count}` | | `gridMoreLabel` | `View {count} more {eventLabel} for {date}` | `{count}`, `{eventLabel}`, `{date}` | +| `gridEventCount` | `{count} {eventLabel}` | `{count}`, `{eventLabel}` | +| `gridEventCountLabel` | `View {count} {eventLabel} for {date}` | `{count}`, `{eventLabel}`, `{date}` | | `internalErrorMessage` | `The calendar encountered an unexpected error.` | — | | `internalErrorTitle` | `Calendar unavailable` | — | | `jump` | `Jump` | — | @@ -617,7 +643,7 @@ Each icon factory receives the host's `Document` and must return distinct, detac | `today` | `Today` | — | | `year` | `Year` | — | -Every supplied message must be a string containing at least one non-whitespace character. Any complete `{token}` placeholder must be listed for that key; an unsupported placeholder is rejected as `invalid-configuration` rather than rendered literally. Only the placeholders shown above are substituted. In particular, `agendaTitle` and `chooseMonthYear` accept only `{date}`; the singular/plural event noun supplies `{eventLabel}` in `dayLabel` and `gridMoreLabel`. `gridMore` supplies the default wide visual and `context.text`, while `gridMoreLabel` always names the native action with its date and hidden count. Rendered values remain text, not HTML. Override `event` and `events` together, and test long translations; the package does not download locale data or apply language-specific plural rules. +Every supplied message must be a string containing at least one non-whitespace character. Any complete `{token}` placeholder must be listed for that key; an unsupported placeholder is rejected as `invalid-configuration` rather than rendered literally. Only the placeholders shown above are substituted. In particular, `agendaTitle` and `chooseMonthYear` accept only `{date}`; the singular/plural event noun supplies `{eventLabel}` in `dayLabel`, `gridMoreLabel`, `gridEventCount`, and `gridEventCountLabel`. `gridMore` supplies the default wide overflow visual and `context.text`, while `gridMoreLabel` names that action with its date and hidden count. Count-mode wide text uses `gridEventCount`, and `gridEventCountLabel` names the native total-count action. Rendered values remain text, not HTML. Override `event` and `events` together, and test long translations; the package does not download locale data or apply language-specific plural rules. @@ -666,6 +692,7 @@ interface CalendarEventOverflowElements< interface CalendarCompactEventOverflowContext extends CalendarRenderContext<"day"> { + readonly display: "overflow" | "count"; readonly date: CalendarDate; readonly dateString: string; readonly elements: CalendarEventOverflowElements; @@ -678,6 +705,7 @@ interface CalendarCompactEventOverflowContext interface CalendarWideEventOverflowContext extends CalendarRenderContext<"grid-summary"> { + readonly display: "overflow" | "count"; readonly date: CalendarDate; readonly dateString: string; readonly elements: CalendarEventOverflowElements; @@ -778,16 +806,24 @@ Two hooks have singleton ownership because each controls package-owned presentat | Singleton hook | No owner | `undefined` | `null` | `Node` | |---|---|---|---|---| | `renderEventMarker` | Built-in marker | Invalid result | Suppress the marker | Replace the marker | -| `renderEventOverflow` | Social-style compact count and localized wide text | Keep the built-in content for that variant | Suppress a passive compact cue; keep built-in content inside a native action | Replace that variant's visual content | +| `renderEventOverflow` | Localized compact number and wide text for the configured count or overflow presentation | Keep the built-in content for that variant | Suppress a passive compact cue; keep built-in content inside a native action | Replace that variant's visual content | -`renderEventOverflow` receives a discriminated `CalendarEventOverflowContext` for each applicable pre-rendered variant. Branch on `variant`; do not infer the responsive state from viewport measurements: +`renderEventOverflow` receives a discriminated `CalendarEventOverflowContext` for each applicable pre-rendered variant. Branch on `display` for totals versus overflow and `variant` for presentation; do not infer responsive state from viewport measurements. For `display: "overflow"`: - `variant: "compact"` uses `surface: "day"`. When a primary marker is visible, `visibleEventCount` is `1`, `overflowCount` is the additional count, and `text` is signed, such as `+1`, `+999`, `+1K`, or `+1.3K` in `en-US`. Without a visible primary marker, `visibleEventCount` is `0`, `overflowCount` equals `eventCount`, and `text` is the unsigned total, such as `2`. Formatting uses the configured locale and `Intl.NumberFormat` with `notation: "compact"`, `compactDisplay: "short"`, `maximumFractionDigits: 1`, and `useGrouping: false`; the paired-marker formatter also uses `signDisplay: "always"`. The built-in visual is a bare semibold number. [DESIGN.md](../DESIGN.md#responsive-model) owns its exact responsive placement. - `variant: "wide"` uses `surface: "grid-summary"`. `visibleEventCount` is the number of direct grid representations, `overflowCount` is the number omitted from them, and `text` is the localized exact `gridMore` value, such as `51 more`. This variant exists when the native grid-overflow action exists. -`eventCount` is always the authoritative total. `elements.root` is the package-owned variant root and `elements.content` is its visual slot. `elements.action` is `null` for a passive compact cue and is the native `HTMLButtonElement` for the wide variant or when the overflow action itself is the compact primary visual, such as with `maxGridEventsPerDay: 0`. These references are for inspection and placement context: return visual content instead of directly changing, removing, or reparenting package-owned elements. A returned compact node replaces content only and remains within its package-assigned block. In the compact-primary action case, the fallback remains one package-owned action; a returned node stays inside it and `null` keeps the built-in numeric fallback. The native button, accessible name, activation, and focus transfer remain package-owned; `context.text` remains the package-formatted fallback even when a custom node replaces it visually. +For `display: "count"`, both variants report `visibleEventCount: 0` and +`overflowCount: eventCount`. The compact `text` is the localized unsigned total +and wide `text` uses `gridEventCount`. Both have a native button in +`elements.action`; returning `null` keeps its required built-in count visual. +Counts are independent of summary caps, agenda pages, and suppressed markers. +Hooks receive every applicable representation at render time; CSS chooses the +active compact or wide presentation afterward. + +`eventCount` is always the authoritative total. `elements.root` is the package-owned variant root and `elements.content` is its visual slot. For `display: "count"`, `elements.action` is always the native `HTMLButtonElement`, including the default compact total on busy days. For `display: "overflow"`, it is `null` for a passive compact cue and is the native button for the wide variant or a compact primary overflow action, such as with `maxGridEventsPerDay: 0`. These references are for inspection and placement context: return visual content instead of directly changing, removing, or reparenting package-owned elements. A returned compact node replaces content only and remains within its package-assigned block. Inside a native compact action, a returned node stays inside that action and `null` keeps the built-in numeric fallback. The native button, accessible name, activation, and focus transfer remain package-owned; `context.text` remains the package-formatted fallback even when a custom node replaces it visually. -Compact output is presentational, unfocusable, and hidden from the +Returned compact content is presentational, unfocusable, and hidden from the accessibility tree; the day label retains the exact event total. A passive compact cue is pointer-transparent, so activating that area selects the day instead of activating the first event. The package owns responsive placement; @@ -799,6 +835,9 @@ compact roots grow together. Oversized output remains unclipped but is outside the equal-sizing guarantee. Returned wide content replaces only visual content inside the native overflow button and cannot alter its behavior. +Only visual content is hidden from the accessibility tree: a count-mode native +button remains an accessible action with the complete date and total in its name. + Each applicable variant is created during the calendar render. Compact and wide variants coexist, and the container styles select the presentation defined by [the design contract](../DESIGN.md#responsive-model). Resizing therefore does @@ -903,7 +942,7 @@ For a current operational error accepted into state, returning `"default"` or `u ## Avoid common integration mistakes -- Use `onEventActivate`, `onEventContextMenu`, `onDaySelect`, and `onDayContextMenu`; these are the action option names. +- Use `onEventActivate`, `onEventOverflowActivate`, `onEventContextMenu`, `onDaySelect`, and `onDayContextMenu`; these are the action option names. - Pass an in-memory event array directly through `events`. Use a provider only when data depends on the requested range or external state, and add `async` only when it actually awaits work. - Treat provider `end` bounds and event ends as exclusive, but `minDate` and `maxDate` as inclusive selectable dates. Do not subtract a day before querying or add a day to returned events. - Do not put `Z`, offsets, or zone annotations in event strings. Convert application instants to intended civil values before adapting them. diff --git a/docs/features.md b/docs/features.md index 35043b7..f77bff0 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,6 +1,8 @@ -# Features and alpha scope + -This guide is the quickest way to determine whether Litefold Calendar fits a project. It maps common calendar terms to the public API and states what the alpha deliberately does not provide. +# Features and scope + +This guide is the quickest way to determine whether Litefold Calendar fits a project. It maps common calendar terms to the public API and states the package's deliberate boundaries. ## Feature map @@ -11,6 +13,7 @@ This guide is the quickest way to determine whether Litefold Calendar fits a pro | Event model | Date-only, local date-time, point, and multi-day events with exclusive ends; optional validated HTTP(S) or relative links; visual time display can differ by surface | `CalendarEventInput`, `eventTimeDisplay` | | Event data | A static snapshot or abort-aware provider, shape-based synchronous/PromiseLike timing, complete source replacement, current-range refetch, and typed application metadata | `events`, `CalendarEventSource`, `setEvents()`, `refetchEvents()` | | Grid and agenda limits | A configurable grid cap, native overflow action, paged agenda rows, DOM limit, and visible/total progress text | `maxGridEventsPerDay`, `agendaPageSize`, `agendaDomLimit` | +| Day event counts | Independent compact/wide modes for summaries, total counts on every nonempty day, or counts only on days with multiple events; count activation opens the agenda or an application-owned chooser | `gridEventDisplay`, `onEventOverflowActivate` | | Navigation and bounds | Previous, Next, Today, a native month/year jump, public navigation/focus methods, and optional inclusive date limits | `prev()`, `next()`, `today()`, `gotoDate()`, `focusDate()`, `minDate`, `maxDate` | | User actions | Separate day selection, event activation, event context action, and day context action callbacks using native links and buttons | `onDaySelect`, `onEventActivate`, `onEventContextMenu`, `onDayContextMenu` | | Direct input and keyboard | Managed grid keyboard navigation plus RTL-aware native pull/snap paging for touch, pen, and horizontal precision scrolling; toolbar buttons remain the fallback | `swipe`, native interaction model | @@ -36,11 +39,11 @@ The [API reference](api.md) owns exact grid, occupancy, sorting, limit, and agen The typed `events` option accepts a local snapshot or an application-owned, abort-aware provider. The same metadata generic flows through normalized events, actions, render hooks, and complete `setEvents()` replacements. Transport, authorization, aggregation, filtering, and caching remain application responsibilities. -The [event and source contracts](api.md#supply-events-calendarevents-and-calendareventsource) own exact input grammar, URL validation, ranges, cancellation, atomic admission, replacement, reentrancy, retained-data behavior, and normalized output. Use the [integration guide](integration-guide.md#typed-source-adapter) for adapter and caching recipes. +The [event and source contracts](api.md#supply-events-calendarevents-and-calendareventsource) own exact input grammar, URL validation, ranges, cancellation, atomic admission, replacement, reentrancy, retained-data behavior, and normalized output. Start with the [remote-data walkthrough](remote-data.md) for fetching and filters; use the [integration guide](integration-guide.md#typed-source-adapter) for typed adapter and caching recipes. ## Navigation and actions -Users can navigate through native toolbar controls, the month/year popover, optional direct-input paging, keyboard commands, and public methods. Day selection, event activation, event context actions, and day context actions remain separate capabilities. +Users can navigate through native toolbar controls, the month/year chooser, optional direct-input paging, keyboard commands, and public methods. Day selection, event activation, event-count activation, event context actions, and day context actions remain separate capabilities. The [API reference](api.md#control-the-calendar-calendar) owns method and callback contracts, bounds, and failure behavior. The [accessibility interaction model](../ACCESSIBILITY.md#interaction-model) owns keyboard, focus, popover, gesture, and RTL interaction; [DESIGN.md](../DESIGN.md) owns their presentation. @@ -76,9 +79,11 @@ and callback shapes. `fallbackElement` can coordinate application-authored no-JavaScript content. The [API reference owns its exact lifecycle](api.md#application-integration-options); the [progressive-enhancement guide](seo-and-progressive-enhancement.md) owns the server-content, crawlability, metadata, privacy, and verification recipe. -## Deliberate alpha boundaries + + +## Deliberate boundaries -The alpha does not provide: +The package does not provide: - Week, day, time-grid, separate list, timeline, resource, year, or multi-month views. - View switching, configurable grid duration, hidden weekends, week numbers, business hours, background events, or a now indicator. @@ -91,8 +96,8 @@ The alpha does not provide: - Arbitrary HTML strings, per-event class names, or arbitrary background/text/border style inputs. Use validated event URLs, trusted render-hook nodes, and the documented built-in event marker color. - An SSR renderer, automatic JSON-LD, canonical event pages, metadata, sitemap policy, or search/privacy decisions. - Automatic WebMCP registration, a remote MCP server, declarative form or iframe tools, event activation, editing tools, or exposure of event IDs, URLs, metadata, render-hook content, and raw diagnostics. -- Public third-party extension authoring. The alpha supports official extension factories and stable consumer render hooks; any future third-party lifecycle/capability contract will be explicit and lower stability. +- Public third-party extension authoring. The package supports official extension factories and stable consumer render hooks; any future third-party lifecycle/capability contract will be explicit and lower stability. - Framework-specific wrappers, CommonJS output, polyfills, or legacy-browser builds. - Non-Gregorian calendar systems. -These boundaries keep the alpha focused on a responsive, accessible month-and-agenda component with a small integration and security surface. +These boundaries keep the package focused on a responsive, accessible month-and-agenda component with a small integration and security surface. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..9399d8b --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,30 @@ +# Getting started + +Use Litefold Calendar for a month grid and a selected-day event list. Your application supplies events and owns editing, permissions, routing, and dialogs. Review the [feature map](features.md#feature-map) and [browser policy](browser-support.md) before integrating it. + +## Install and render + +Follow the root [installation](../README.md#install) and [first render](../README.md#first-render) example. Import both the JavaScript entry and stylesheet, create the calendar on a client-owned element, then call `render()`. + +Use the [basic JavaScript example](../examples/basic/) to start with a small event array. No TypeScript, metadata interface, framework, remote service, or optional extension is required. + +## Add the next capability + +| When you need… | Continue with… | +| --- | --- | +| Events from an API, filters, or refresh after saving | [Remote data walkthrough](remote-data.md) and [runnable example](../examples/remote-data/) | +| Clear day counts on phones or in a sidebar | [Choose event counts](integration-guide.md#choose-event-counts) | +| An application-owned event picker or details dialog | [Own the event chooser](integration-guide.md#own-the-event-chooser) | +| Useful content before JavaScript starts | [Progressive enhancement](seo-and-progressive-enhancement.md) | +| Exact options, date formats, and callback types | [API reference](api.md) | +| Typed adapters, caching, or render hooks | [Advanced integration](integration-guide.md) | + +## Avoid common integration mistakes + +- Event strings are civil dates or local date-times. Convert API instants into your chosen zone before mapping them; do not append `Z` or an offset. See the [date grammar](api.md#accepted-civil-values). +- Source requests use an inclusive `start` and exclusive `end`, covering the visible six-week grid rather than only the named month. +- A compact day with several events shows a total-count button by default. Activating it opens that date's agenda; provide `onEventOverflowActivate` only when your application owns the alternative interaction. +- Change filters outside the calendar, then call `refetchEvents()`. Use `setEvents()` when replacing the source itself or supplying a complete new local snapshot. +- Call `destroy()` during permanent component/router teardown. The standalone examples preserve instances during a cached `pagehide`. + +The [documentation hub](README.md) separates everyday integration recipes, advanced contracts, contribution, and release operations. diff --git a/docs/integration-guide.md b/docs/integration-guide.md index 6145640..349e5c1 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -2,11 +2,13 @@ This guide shows how an existing web application can adopt Litefold Calendar while keeping application policy outside the package. +For the common API-loading path, start with the [remote-data walkthrough](remote-data.md). This guide is the deeper reference for typed adaptation, caching, styling, and composition. Jump directly to [event counts](#choose-event-counts) or [application-owned dialogs](#own-the-event-chooser) when extending an existing integration. + ## Ownership boundary Litefold Calendar owns: -- Civil-date parsing, interval placement, inclusive instance bounds, the one-current-range month grid, selected-day agenda, and navigation, including the native month/year jump popover. +- Civil-date parsing, interval placement, inclusive instance bounds, the one-current-range month grid, selected-day agenda, and navigation, including the native month/year chooser. - Loading, request cancellation, generation guards, default retry/error UI, managed grid/event focus, progressive fallback coordination, and native pull/snap paging. - Generic event, action, consumer render-hook, opaque first-party extension, message, and documented `--lfc-*` token contracts. @@ -222,8 +224,93 @@ cannot fit; the date remains top-aligned. Equal week rows may make the grid tall because no content is clipped or constrained to a fixed height. Both options are construction-time configuration, so recreate the calendar to change them. +## Choose event counts + +Use `gridEventDisplay` when a single marker plus a small additional count hides +the significance of a busy day. Each width can make its own choice: + +```js +const calendar = createCalendar(host, { + events, + gridEventDisplay: { + compact: "count", + wide: "count-when-multiple" + } +}); +``` + +| Mode | Empty day | One event | Multiple events | +| --- | --- | --- | --- | +| `"events"` | Nothing | Existing event representation | Summaries and overflow | +| `"count"` | Nothing | Total-count button | Total-count button | +| `"count-when-multiple"` | Nothing | Existing event representation | Total-count button | + +The defaults are `compact: "count-when-multiple"` and `wide: "events"`. +Compact means the calendar container is at most `42rem` wide, including a +narrow calendar inside a wide application page. Container CSS chooses the +presentation; resizing neither refetches events nor reruns render hooks. + +Counts include every loaded, normalized occurrence for the date, including +multi-day events. They are independent of `maxGridEventsPerDay`, agenda +pagination, and render-hook visual suppression. The grid cap still controls +individual summaries: `0` suppresses those summaries but does not hide a count. +Compact buttons show localized numbers; wide buttons show localized event +nouns. Accessible names always include the complete count and date. + +**Upgrade note:** Busy compact days now show a total-count action by default. +To retain the earlier marker-and-additional-count presentation, set +`gridEventDisplay: { compact: "events" }`. Set both widths to `"count"` to show +total counts on every nonempty date, or both to `"count-when-multiple"` to keep +single-event actions at all sizes. Exact hook and focus behavior belongs in the +[API contract](api.md#handle-user-actions-calendaraction). + +## Own the event chooser + +Without a callback, activating a count or native overflow button selects that +date, resets agenda pagination, and focuses the agenda heading. It does not +invoke `onDaySelect`. This is sufficient when the selected-day agenda is your +event picker. + +Use the same application dialog controller for individual events and complete +day lists when the application owns scheduling or details: + +```js +const calendar = createCalendar(host, { + events, + gridEventDisplay: { compact: "count-when-multiple", wide: "count-when-multiple" }, + onEventActivate({ dateString, element, event, nativeEvent }) { + nativeEvent.preventDefault(); + return eventDialog.open({ events: [event], dateString, invoker: element }); + }, + onEventOverflowActivate({ dateString, element, events, nativeEvent }) { + nativeEvent.preventDefault(); + return eventDialog.open({ events, dateString, invoker: element }); + } +}); +``` + +`eventDialog` is your application's dialog controller. The +[runnable remote-data example](../examples/remote-data/main.js) implements it +with a native ``, accessible heading, event buttons, and Close action. +Its local choice is a demonstration; your application remains responsible for +authorization, saving, and [refreshing after a save](remote-data.md#3-change-filters-and-refresh). + +Call `preventDefault()` synchronously, before any `await`. The callback runs +before day selection or DOM replacement; cancelling transfers interaction and +focus ownership to your application. Return promises so action failures can be +reported by the library. On dismissal, restore focus to a still-connected +invoker; if a refresh replaced it, use `calendar.focusDate(dateString)` as the +public fallback. Close application dialogs during permanent teardown. + +The callback receives an immutable ordered array of normalized events for the +date. Metadata stays application-owned by reference. Do not infer the list from +visible summaries or a paged agenda, and use `dateString` for multi-day +occurrences instead of assuming `event.start` is the activated date. + ## Application-owned cache and filters +For a provider without caching, use the [basic filtering and refresh recipe](remote-data.md#3-change-filters-and-refresh). Add a cache only when your application needs one; cache identity must include authorization scope, range, and all filters that change the response. + Key cached raw responses by the requested range and every authorization-relevant input. A filter change can reuse an already validated raw response and call `refetchEvents()` once; the source then returns the currently enabled categories. Application cache rules should: @@ -363,29 +450,21 @@ const calendar = createCalendar(host, { }); ``` -The two singleton presentation hooks have distinct behavior: - -| Hook | Hook omitted | `null` / `undefined` return | Returned node | -| --- | --- | --- | --- | -| `renderEventMarker` | Keeps the built-in marker | `null` suppresses it; `undefined` is invalid | Replaces the built-in marker | -| `renderEventOverflow` | Keeps the social-style compact number and localized wide text | `undefined` keeps that variant; `null` suppresses only a passive compact cue and otherwise keeps the native action's default | Replaces that variant's visual content | - -Only one hook set can own each singleton hook. `renderEventOverflow` owns its compact and wide branches together and receives each applicable variant during the calendar render. When a native overflow action has both responsive presentations, both are pre-rendered before CSS chooses which one to expose. The compact branch has `variant: "compact"`, `surface: "day"`, and package-formatted social text such as `+1` when paired with a visible primary marker or an unsigned total when there is no marker. The wide branch has `variant: "wide"`, `surface: "grid-summary"`, and localized exact text such as `2 more`. - -Both contexts expose `eventCount`, `visibleEventCount`, `overflowCount`, `text`, and stable `elements.root` / `elements.content` references. `elements.action` is `null` for a passive compact cue and is the package-owned overflow button for the wide branch or a compact-primary overflow action. Treat those elements as inspection and placement context rather than mutation targets; return the visual node. That button's accessible name, activation, focus transfer, and canonical fallback remain package-owned. Returned nodes must be new, detached, same-document, synchronous, and entirely noninteractive; the package treats them as presentational content. - -The package owns the responsive placement and gives each compact visual an -assigned block. A markerless total uses one centered block, and -`maxGridEventsPerDay: 0` keeps the fallback inside one package-owned overflow -action. The hook replaces only visual content; no public overflow-layout -selector or CSS token overrides placement. Fit returned compact content within -its assigned slot to preserve equal sizing. If it needs more space, increase -`--lfc-control-min-size` and `--lfc-grid-event-min-block-size` on the calendar -root so normal package-owned roots grow together. Oversized output remains -unclipped but opts out of equal visual sizing. [DESIGN.md](../DESIGN.md#responsive-model) -owns the exact geometry and width transitions. - -Container resizing changes CSS visibility only; it does not invoke the hook again or replace either returned node. A failure in either branch quarantines the complete hook set and restores both compact and wide defaults. See the [render-hook API](api.md#customize-rendering-calendarrenderhooks) for every context field and return rule. +Use one owner for each singleton hook, `renderEventMarker` and +`renderEventOverflow`. Return new, detached, noninteractive nodes and style +only your application classes. In the overflow hook, use `context.text` for +the package-localized visual and inspect `display` (`"count"` or `"overflow"`) +and `variant` (`"compact"` or `"wide"`) when the design needs different content. +A total-count action always remains available, including when the hook returns +`null`. + +The package owns the native action, accessible name, focus behavior, and +responsive placement. Fit compact content inside its assigned slot. Increase +`--lfc-control-min-size` and `--lfc-grid-event-min-block-size` together when the +custom content needs more room. Container resizing changes visibility without +rerunning hooks. The [render-hook API](api.md#customize-rendering-calendarrenderhooks) +owns context fields, return semantics, singleton rules, and failure behavior; +[DESIGN.md](../DESIGN.md#responsive-model) owns sizing and width transitions. ### Style hook output diff --git a/docs/remote-data.md b/docs/remote-data.md new file mode 100644 index 0000000..2b59014 --- /dev/null +++ b/docs/remote-data.md @@ -0,0 +1,119 @@ +# Remote data: load, filter, and refresh + +This walkthrough is for an application that already renders a calendar and needs events from an API. It uses JavaScript with optional JSDoc types. Start with the [first render](../README.md#first-render) if you have not created a calendar yet. + +The [runnable remote-data example](../examples/remote-data/) fetches a same-origin JSON file, offers a category filter, and opens an application-owned event chooser. Its source adapter is small enough to copy and adapt. It deliberately leaves caching and advanced render hooks to the [integration guide](integration-guide.md). + +## 1. Supply a function + +The `events` option can be a function. Litefold Calendar calls it for the current range on initial render, month navigation, and explicit refresh. Return the complete event array for that request, or throw on failure. + +```js +import { createCalendar } from "@tryagaindev/litefold-calendar"; +import "@tryagaindev/litefold-calendar/styles.css"; +import { loadEvents } from "./source.js"; + +const host = document.querySelector("[data-my-calendar]"); +const filter = document.querySelector("[data-my-category]"); +if (!(host instanceof HTMLElement) || !(filter instanceof HTMLSelectElement)) { + throw new Error("Calendar controls were not found."); +} + +const calendar = createCalendar(host, { + initialDate: "2026-08-06", + events: (range) => loadEvents(range, filter.value) +}); +calendar.render(); +``` + +To try the fixture, copy [source.js](../examples/remote-data/source.js) and [events.json](../examples/remote-data/events.json) beside your application entry point. Replace `../../dist/index.js` in the source's JSDoc imports with `@tryagaindev/litefold-calendar`. The shown initial date matches the fixture; choose an application date when connecting your service. + +The adapter uses `fetch(url, { signal })`, checks `response.ok`, parses JSON as unknown data, validates every record, and maps only supported fields. Its example schema is date-only `{ id, title, start, end?, category }`; adapt the validator when your service uses timed events or another shape. A `200` response does not prove its body is valid. + +The supplied `signal` aborts superseded requests and pending work during `destroy()`. Forward it to `fetch` and any other cancellable work. Do not convert a transport error or cancellation into `[]`: that represents a successful empty result. The library also prevents superseded results from replacing the current range. + +## 2. Send the range and filter to your service + +The fixture's URL includes `start`, `end`, and `category`, but a static server ignores those parameters. **The runnable example downloads public fixture records and filters them in the browser.** This is not authorization, server filtering, or a production data-storage service. + +For a real endpoint, keep the request and validation steps and let the server apply the range and category: + +```js +import { validateEvents } from "./source.js"; + +async function loadEvents({ start, end, signal }, category) { + const url = new URL("/api/events", window.location.origin); + url.searchParams.set("start", start); + url.searchParams.set("end", end); + url.searchParams.set("category", category); + const response = await fetch(url, { + signal, + headers: { Accept: "application/json" } + }); + if (!response.ok) { + throw new Error(`Event request failed (${response.status}).`); + } + return validateEvents(await response.json()); +} +``` + +| Request field | Meaning | +| --- | --- | +| `start` | Included civil date, such as `2026-07-26` | +| `end` | Excluded civil date, such as `2026-09-06` | +| `category` | Application-owned filter value; validate it on the server | + +Return events that overlap the requested interval, including events that start earlier and continue into it. An event ending exactly at `start` or starting exactly at `end` is outside the range. Apply the documented default duration for missing ends: one day for all-day events, or a point at the start for timed events. An all-day event ending `2026-08-11` occupies dates through August 10. Do not derive requests from the viewport or assume every request begins on the month's first day. + +Keep authentication, authorization, time-zone conversion, and response validation in the application. Filtering authorized results is not a substitute for authorization. The [event-source contract](api.md#supply-events-calendarevents-and-calendareventsource) owns exact range, date, and source behavior. + +## 3. Change filters and refresh + +```js +const lifetime = new AbortController(); +filter.addEventListener("change", () => { + calendar.refetchEvents(); +}, { signal: lifetime.signal }); +``` + +Read the filter once when the provider is invoked, as in `loadEvents(range, filter.value)`. Rapid changes start new requests and cancel older ones. Do not recreate the calendar for ordinary filters; keep the user's month and selected date. + +Use the same method for an explicit Refresh button. After your application confirms a successful write, refresh the current source: + +```js +async function saveAndRefresh(changes) { + await applicationApi.saveEvent(changes); + if (!lifetime.signal.aborted) { + calendar.refetchEvents(); + } +} +``` + +`applicationApi.saveEvent` represents your application's authenticated write operation. Handle its failure in the editing form and leave it open for correction. The lifetime check skips a refresh if permanent teardown occurred while saving; abort that lifetime before destroying the calendar, as shown below. `refetchEvents()` returns `void`; it starts loading and does not await the next snapshot. Use [state callbacks](api.md#observe-state-calendarstate) if other application UI needs to observe loading or completion. + +| Task | Method | +| --- | --- | +| Same provider, changed filter, saved event, or invalidated cache | `refetchEvents()` | +| Replace a local array with a complete updated snapshot | `setEvents(updatedEvents)` | +| Switch to another provider | `setEvents(nextProvider)` | + +Passing an array to `setEvents()` replaces the provider. Subsequent `refetchEvents()` calls use that array; they do not restore your previous remote source. There is no per-event mutation API. + +## 4. Keep empty and error states distinct + +Return `[]` when the server successfully finds no matching events. The selected-day agenda displays its empty message. Throw when transport, HTTP status, parsing, or validation fails; the calendar supplies persistent source-error UI and Retry. A failed refresh can retain the previous events while warning that they may be out of date. + +The runnable example's “Sports (empty)” category demonstrates an empty result. Use the browser's network tooling or the [async-errors example](../examples/async-errors/) to explore failure and recovery. Leave package-owned error presentation enabled until your application has a complete alternative. See [error handling](errors.md) before returning `"handled"` from `onError`. + +## 5. Connect actions and clean up + +Use [event-count settings](integration-guide.md#choose-event-counts) to make busy days recognizable. The [application-owned chooser recipe](integration-guide.md#own-the-event-chooser) receives every loaded occurrence for the date and connects naturally to an existing details or scheduling dialog. The runnable example records a local choice; it does not send a write request. + +For a component or route, run both operations during permanent teardown: + +```js +lifetime.abort(); +calendar.destroy(); +``` + +Close application dialogs and release their listeners too. The [runnable entry point](../examples/remote-data/main.js) demonstrates cleanup on non-cached `pagehide`, native dialog dismissal, and returning focus after a request has replaced the originating event or count button. diff --git a/examples/README.md b/examples/README.md index a082960..887e2e4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,6 +9,7 @@ Use this page to choose, run, and validate the repository examples. Each recipe | Package user — getting started | Render a static event array and handle activation | [Basic JavaScript](basic/) | | Package user — getting started | Preserve useful server-rendered content until the calendar is ready | [Progressive enhancement](progressive-enhancement/) | | Package user — getting started | Load the ESM build from an existing classic-script entry point | [Classic-script loader](classic-script/) | +| Package user — everyday integration | Fetch JSON, filter categories, refresh, and open a count/event dialog | [Remote data JavaScript](remote-data/) | | Package user — integration and API | Integrate a range-aware source, typed metadata, application controls, hooks, state, and WebMCP | [Advanced TypeScript](advanced/) | | Package user — integration and API | Own source, action, and render-hook failure presentation | [Async errors](async-errors/) | | Package user — integration and API | Rewrite a FullCalendar v6 `dayGridMonth` source shape | [FullCalendar v6 migration](fullcalendar-v6-migration/) | @@ -40,9 +41,13 @@ import "@tryagaindev/litefold-calendar/styles.css"; A browser cannot resolve those bare package specifiers without a build tool or import map. The [classic-script recipe](classic-script/) shows the deployed-URL form. +### Everyday integration + +Use [Remote data JavaScript](remote-data/) after the basic example. It demonstrates a real `fetch`, response validation, cancellation, category filtering, total-count actions, native dialogs, and cleanup. The [remote-data walkthrough](../docs/remote-data.md) explains which fixture code changes for a production API and how to refresh after saving. No advanced hook inventory or TypeScript generic setup is required. + ### Integration and API -Use [Advanced TypeScript](advanced/) for reusable integration patterns such as range loading, cancellation, typed metadata, render hooks, external controls, fallback ownership, and optional extensions. Use [Async errors](async-errors/) when deciding whether the package or application owns visible recovery UI. The [FullCalendar v6 migration](fullcalendar-v6-migration/) is a bounded adapter example for that source API shape, not a compatibility layer. +Use [Advanced TypeScript](advanced/) for typed metadata, caching, render hooks, external controls, fallback ownership, and optional extensions. Use [Async errors](async-errors/) when deciding whether the package or application owns visible recovery UI. The [FullCalendar v6 migration](fullcalendar-v6-migration/) is a bounded adapter example for that source API shape, not a compatibility layer. Exact public signatures, defaults, and timing belong in the [API reference](../docs/api.md). Application ownership and composition recipes belong in the [integration guide](../docs/integration-guide.md). diff --git a/examples/index.html b/examples/index.html index 1887ef7..c3e8384 100644 --- a/examples/index.html +++ b/examples/index.html @@ -36,7 +36,7 @@

    Build identity

    Version
    -
    @alpha
    +
    @nightly
    Source commit
    @@ -81,6 +81,24 @@

    >View source for Render static events

  2. +
  3. + +
  4. Complete surface

    diff --git a/examples/remote-data/README.md b/examples/remote-data/README.md new file mode 100644 index 0000000..4ecdd24 --- /dev/null +++ b/examples/remote-data/README.md @@ -0,0 +1,16 @@ +# Remote data and filtering + +A small JavaScript/JSDoc integration between the [basic example](../basic/) and [advanced TypeScript example](../advanced/). + +Run from the repository with `npm run demo`, then open `/examples/remote-data/`. The initial date is August 6, 2026, matching the deterministic fixture. + +- Change Category to filter the selected month; Sports demonstrates an empty result. +- Activate a day count to open the application's native dialog with every occurrence on that date, or activate an individual event to open it with that event alone. Closing the dialog returns focus. +- Choose an event to record a local selection, or use Refresh events to request the JSON again. +- Inspect the network request to see the inclusive `start`, exclusive `end`, category, and forwarded cancellation signal. + +`events.json` is a static public fixture. Its server ignores query parameters; `source.js` validates and filters the downloaded records in the browser. Production endpoints must authorize requests and apply filters on the server. No event is saved by this example. The [remote-data guide](../../docs/remote-data.md) shows the API version and refresh after a real save. + +`main.js` owns controls, count presentation, dialogs, focus return, and teardown. `source.js` owns transport and the example's date-only response schema. All calendar integration uses published API surfaces; installed applications replace relative `dist` imports, including JSDoc imports, with `@tryagaindev/litefold-calendar`. + +Run `node --test scripts/tests/remote-data-source.test.mjs` for source validation and request-boundary checks. Browser coverage lives in `tests/e2e/remote-data.spec.js`; the repository's example typecheck includes `tsconfig.json`. diff --git a/examples/remote-data/events.json b/examples/remote-data/events.json new file mode 100644 index 0000000..7a06653 --- /dev/null +++ b/examples/remote-data/events.json @@ -0,0 +1,7 @@ +[ + { "id": "open-studio", "title": "Open studio", "start": "2026-08-06", "category": "community" }, + { "id": "print-workshop", "title": "Print workshop", "start": "2026-08-06", "category": "workshop" }, + { "id": "garden-meetup", "title": "Garden meetup", "start": "2026-08-06", "category": "community" }, + { "id": "exhibition", "title": "Summer exhibition", "start": "2026-08-08", "end": "2026-08-11", "category": "community" }, + { "id": "bookbinding", "title": "Bookbinding workshop", "start": "2026-08-15", "category": "workshop" } +] diff --git a/examples/remote-data/index.html b/examples/remote-data/index.html new file mode 100644 index 0000000..6f863b3 --- /dev/null +++ b/examples/remote-data/index.html @@ -0,0 +1,41 @@ + + + + + + + Remote data and filtering | litefold-calendar + + + + + +
    +
    +

    Load and filter remote events

    +

    Browse August 2026, filter by category, and activate an event or day count. Each request loads a real JSON file; this demo filters that public fixture in the browser.

    +
    +
    +
    + + + +
    +
    +
    +

    + +

    Choose an event

    +

    This application-owned dialog shows every event for the date. Choosing an event records a local selection only.

    +
      +
      +
      +
      + + + diff --git a/examples/remote-data/main.js b/examples/remote-data/main.js new file mode 100644 index 0000000..4797917 --- /dev/null +++ b/examples/remote-data/main.js @@ -0,0 +1,90 @@ +import { createCalendar } from "../../dist/index.js"; +import { loadEvents } from "./source.js"; + +const host = document.querySelector("[data-my-calendar]"); +const category = document.querySelector("[data-my-category]"); +const refresh = document.querySelector("[data-my-refresh]"); +const result = document.querySelector("[data-my-result]"); +const dialog = document.querySelector("[data-my-dialog]"); +const dialogTitle = document.querySelector("[data-my-dialog-title]"); +const choices = document.querySelector("[data-my-event-choices]"); +if (!(host instanceof HTMLElement) || !(category instanceof HTMLSelectElement) || + !(refresh instanceof HTMLButtonElement) || !(result instanceof HTMLElement) || + !(dialog instanceof HTMLDialogElement) || !(dialogTitle instanceof HTMLElement) || + !(choices instanceof HTMLUListElement)) { + throw new Error("The remote-data example markup is incomplete."); +} + +const lifetime = new AbortController(); +/** @type {{ element: HTMLElement, dateString: string } | null} */ +let returnFocus = null; + +const calendar = createCalendar(host, { + initialDate: "2026-08-06", + //Read the current filter at invocation so each request has one filter snapshot. + events: (range) => loadEvents(range, category.value), + gridEventDisplay: { compact: "count-when-multiple", wide: "count-when-multiple" }, + onEventActivate({ dateString, element, event, nativeEvent }) { + nativeEvent.preventDefault(); + openChooser([event], dateString, element); + }, + onEventOverflowActivate({ dateString, element, events, nativeEvent }) { + //Cancel synchronously, before the library selects or replaces any DOM. + nativeEvent.preventDefault(); + openChooser(events, dateString, element); + } +}); + +/** + * @param {readonly import("../../dist/index.js").CalendarEvent[]} events + * @param {string} dateString + * @param {HTMLElement} element + */ +const openChooser = (events, dateString, element) => { + returnFocus = { element, dateString }; + dialogTitle.textContent = `Events for ${dateString}`; + choices.replaceChildren(); + for (const event of events) { + const item = document.createElement("li"); + const button = document.createElement("button"); + button.type = "button"; + button.className = "my-button"; + button.textContent = `Choose ${event.title}`; + button.addEventListener("click", () => { + result.textContent = `Selected ${event.title} for ${dateString}. No server data was changed.`; + dialog.close(); + }, { once: true }); + item.append(button); + choices.append(item); + } + dialog.showModal(); +}; + +dialog.addEventListener("close", () => { + const target = returnFocus; + returnFocus = null; + choices.replaceChildren(); + if (target === null) { + return; + } + if (target.element.isConnected) { + target.element.focus(); + } + //A refetch may replace the invoker while the dialog is open. + if (document.activeElement !== target.element) { + calendar.focusDate(target.dateString); + } +}, { signal: lifetime.signal }); + +category.addEventListener("change", () => { calendar.refetchEvents(); }, { signal: lifetime.signal }); +refresh.addEventListener("click", () => { calendar.refetchEvents(); }, { signal: lifetime.signal }); +calendar.render(); + +window.addEventListener("pagehide", (event) => { + if (!event.persisted) { + lifetime.abort(); + returnFocus = null; + dialog.close(); + calendar.destroy(); + } +}, { signal: lifetime.signal }); diff --git a/examples/remote-data/source.js b/examples/remote-data/source.js new file mode 100644 index 0000000..d536def --- /dev/null +++ b/examples/remote-data/source.js @@ -0,0 +1,73 @@ +/** @typedef {{ category: string }} EventMetadata */ +/** @typedef {import("../../dist/index.js").CalendarEventInput} EventInput */ +/** @typedef {import("../../dist/index.js").CalendarRange} SourceContext */ + +/** + * Load a real same-origin JSON resource and return a validated calendar snapshot. + * This static fixture filters on the client; a production endpoint must authorize + * and filter records on the server before returning them. + * @param {SourceContext} range + * @param {string} category + * @param {typeof fetch} [request] + * @returns {Promise} + */ +export async function loadEvents({ start, end, signal }, category, request = fetch) { + const url = new URL("./events.json", import.meta.url); + url.searchParams.set("start", start); + url.searchParams.set("end", end); + url.searchParams.set("category", category); + const response = await request(url, { signal, headers: { Accept: "application/json" } }); + if (!response.ok) { + throw new Error(`Event request failed (${response.status}).`); + } + /** @type {unknown} */ + const payload = await response.json(); + signal.throwIfAborted(); + const events = validateEvents(payload); + return events.filter((event) => + (category === "all" || event.metadata?.category === category) && + event.start < end && + (event.end === undefined ? event.start >= start : event.end > start) + ); +} + +/** + * Adapt this example's date-only JSON schema; the library validates its complete + * public event contract again. Timed API records need their own explicit adapter. + * @param {unknown} payload + * @returns {EventInput[]} + */ +export function validateEvents(payload) { + if (!Array.isArray(payload)) { + throw new Error("Expected an event array."); + } + const ids = new Set(); + return payload.map((record) => { + if (record === null || typeof record !== "object" || + typeof record.id !== "string" || record.id.trim() === "" || ids.has(record.id) || + typeof record.title !== "string" || record.title.trim() === "" || + typeof record.category !== "string" || record.category.trim() === "" || + !isCivilDate(record.start) || + (record.end !== undefined && (!isCivilDate(record.end) || record.end <= record.start))) { + throw new Error("The event response contains an invalid record."); + } + ids.add(record.id); + return { + id: record.id, + title: record.title, + start: record.start, + ...(record.end === undefined ? {} : { end: record.end }), + metadata: { category: record.category } + }; + }); +} + +/** @param {unknown} value @returns {value is string} */ +function isCivilDate(value) { + if (typeof value !== "string" || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/u.test(value)) { + return false; + } + const date = new Date(`${value}T00:00:00Z`); + return value.slice(0, 4) !== "0000" && + Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value; +} diff --git a/examples/remote-data/styles.css b/examples/remote-data/styles.css new file mode 100644 index 0000000..fb479d6 --- /dev/null +++ b/examples/remote-data/styles.css @@ -0,0 +1,45 @@ +@layer my.remote-data { + .my-remote-controls { + container-name: my-example my-remote-controls; + } + + .my-event-dialog { + box-sizing: border-box; + inline-size: min(32rem, calc(100% - 2rem)); + max-block-size: calc(100% - 2rem); + padding: 1.25rem; + border: 0.125rem solid var(--my-border-color); + border-radius: var(--my-radius); + background: var(--my-surface-background); + color: var(--my-color); + overflow: auto; + } + + .my-event-dialog::backdrop { + background: rgb(0 0 0 / 0.5); + } + + .my-event-dialog > h2 { + margin-block-start: 0; + overflow-wrap: anywhere; + } + + .my-event-choices { + display: grid; + gap: 0.75rem; + padding: 0; + list-style: none; + } + + .my-event-choices .my-button { + inline-size: 100%; + text-align: start; + overflow-wrap: anywhere; + } + + @container my-remote-controls (inline-size < 28rem) { + .my-control-row > :not(label) { + inline-size: 100%; + } + } +} diff --git a/examples/remote-data/tsconfig.json b/examples/remote-data/tsconfig.json new file mode 100644 index 0000000..b4c337f --- /dev/null +++ b/examples/remote-data/tsconfig.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true + }, + "include": ["main.js", "source.js"] +} diff --git a/package.json b/package.json index 24b6aec..8997d17 100644 --- a/package.json +++ b/package.json @@ -99,11 +99,11 @@ "test:browser:webkit": "npm run build && playwright test --project=webkit", "test:examples": "npm run build && npm run test:examples:built", "test:examples:built": "node scripts/run-advanced-smoke.mjs", - "test:tooling": "node --test scripts/tests/build-pages.test.mjs scripts/tests/calendar-workflow-measurement.test.mjs scripts/tests/check-sbom.test.mjs scripts/tests/dynamic-update-measurement.test.mjs scripts/tests/example-metadata.test.mjs scripts/tests/markdown-diagrams.test.mjs scripts/tests/markdown-heading.test.mjs scripts/tests/node-version.test.mjs scripts/tests/ownership.test.mjs scripts/tests/package-entries.test.mjs scripts/tests/packed-extension-verification.test.mjs scripts/tests/publish-alpha-policy.test.mjs scripts/tests/release-artifacts.test.mjs scripts/tests/release-state.test.mjs scripts/tests/screenshot-contract.test.mjs scripts/tests/semver.test.mjs scripts/tests/serve-repository.test.mjs scripts/tests/styles.test.mjs scripts/tests/workflow-contracts.test.mjs scripts/tests/browser-targets.test.mjs scripts/tests/vite-build.test.mjs scripts/tests/package-bundle-policy.test.mjs scripts/tests/screenshot-preparation.test.mjs", + "test:tooling": "node --test scripts/tests/build-pages.test.mjs scripts/tests/calendar-workflow-measurement.test.mjs scripts/tests/check-sbom.test.mjs scripts/tests/dynamic-update-measurement.test.mjs scripts/tests/example-metadata.test.mjs scripts/tests/markdown-diagrams.test.mjs scripts/tests/markdown-heading.test.mjs scripts/tests/node-version.test.mjs scripts/tests/ownership.test.mjs scripts/tests/package-entries.test.mjs scripts/tests/packed-extension-verification.test.mjs scripts/tests/publish-alpha-policy.test.mjs scripts/tests/release-artifacts.test.mjs scripts/tests/release-state.test.mjs scripts/tests/screenshot-contract.test.mjs scripts/tests/semver.test.mjs scripts/tests/serve-repository.test.mjs scripts/tests/styles.test.mjs scripts/tests/workflow-contracts.test.mjs scripts/tests/browser-targets.test.mjs scripts/tests/vite-build.test.mjs scripts/tests/package-bundle-policy.test.mjs scripts/tests/screenshot-preparation.test.mjs scripts/tests/remote-data-source.test.mjs", "test:unit": "node scripts/run-unit-tests.mjs", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit", "typecheck:examples": "npm run build:package && npm run typecheck:examples:built", - "typecheck:examples:built": "tsc -p examples/advanced/tsconfig.json --noEmit", + "typecheck:examples:built": "tsc -p examples/advanced/tsconfig.json --noEmit && tsc -p examples/remote-data/tsconfig.json --noEmit", "check:license": "node scripts/check-license.mjs", "browsers:report": "node scripts/report-browser-targets.mjs", "dev": "vite --config vite.examples.config.mjs", diff --git a/scripts/tests/remote-data-source.test.mjs b/scripts/tests/remote-data-source.test.mjs new file mode 100644 index 0000000..ee5cc7a --- /dev/null +++ b/scripts/tests/remote-data-source.test.mjs @@ -0,0 +1,69 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { loadEvents, validateEvents } from "../../examples/remote-data/source.js"; + +const RECORDS = [ + { id: "ending-before", title: "Ends at range start", start: "2026-08-01", end: "2026-08-06", category: "community" }, + { id: "overlap", title: "Spans range start", start: "2026-08-05", end: "2026-08-08", category: "community" }, + { id: "included", title: "Starts at range start", start: "2026-08-06", category: "workshop" }, + { id: "other", title: "Last included day", start: "2026-08-09", category: "community" }, + { id: "outside", title: "Starts at exclusive end", start: "2026-08-10", category: "workshop" } +]; + +function range(signal = new AbortController().signal) { + return { start: "2026-08-06", end: "2026-08-10", signal }; +} + +test("remote fixture forwards range, category, and cancellation and filters overlapping dates", async () => { + const requestRange = range(); + const events = await loadEvents(requestRange, "all", async (input, options) => { + assert.equal(input.searchParams.get("start"), requestRange.start); + assert.equal(input.searchParams.get("end"), requestRange.end); + assert.equal(input.searchParams.get("category"), "all"); + assert.equal(options.signal, requestRange.signal); + assert.equal(options.headers.Accept, "application/json"); + return Response.json(RECORDS); + }); + assert.deepEqual(events.map((event) => event.id), ["overlap", "included", "other"]); + assert.deepEqual(events[0].metadata, { category: "community" }); + assert.equal(Object.hasOwn(events[1], "end"), false); +}); + +test("remote fixture applies category filters and preserves a successful empty response", async () => { + const request = async () => Response.json(RECORDS); + assert.deepEqual((await loadEvents(range(), "workshop", request)).map((event) => event.id), ["included"]); + assert.deepEqual(await loadEvents(range(), "sports", request), []); + assert.deepEqual(await loadEvents(range(), "all", async () => Response.json([])), []); +}); + +test("remote fixture propagates HTTP, network, parsing, and cancellation failures", async () => { + await assert.rejects(loadEvents(range(), "all", async () => new Response("Unavailable", { status: 503 })), /503/u); + const failure = new TypeError("Connection lost"); + await assert.rejects(loadEvents(range(), "all", async () => { throw failure; }), (error) => error === failure); + await assert.rejects(loadEvents(range(), "all", async () => new Response("not JSON")), SyntaxError); + const controller = new AbortController(); + await assert.rejects(loadEvents(range(controller.signal), "all", async () => { + controller.abort(); + return Response.json(RECORDS); + }), { name: "AbortError" }); +}); + +test("remote fixture validates the whole response before filtering", async () => { + const invalid = { id: "invalid", title: "Invalid outside range", start: "2020-02-30", category: "sports" }; + await assert.rejects(loadEvents(range(), "workshop", async () => Response.json([...RECORDS, invalid])), /invalid record/u); +}); + +test("remote schema rejects malformed, duplicate, timed, and non-increasing event records", () => { + const valid = RECORDS[2]; + for (const payload of [ + null, {}, [null], ["event"], + [{ ...valid, id: " " }], [{ ...valid, title: " " }], [{ ...valid, category: null }], + [{ ...valid, start: "2026-02-29" }], [{ ...valid, start: "0000-01-01" }], + [{ ...valid, start: "2026-08-06T11:00" }], + [{ ...valid, end: "2026-08-06" }], [{ ...valid, end: "2026-08-05" }], + [valid, valid] + ]) { + assert.throws(() => validateEvents(payload), Error); + } + assert.equal(validateEvents([{ ...valid, start: "2028-02-29" }])[0].start, "2028-02-29"); +}); diff --git a/tests/e2e/developer-pages.spec.js b/tests/e2e/developer-pages.spec.js index 91f6ee2..6309f02 100644 --- a/tests/e2e/developer-pages.spec.js +++ b/tests/e2e/developer-pages.spec.js @@ -139,10 +139,13 @@ for (const [name, route] of EXAMPLE_ROUTES) { }); } -test("examples landing exposes six keyboard-visible task cards", async ({ page }, testInfo) => { +test("examples landing exposes seven keyboard-visible task cards including remote data", async ({ page }, testInfo) => { const response = await page.goto("/examples/", { waitUntil: "domcontentloaded" }); expect(response?.ok()).toBe(true); - await expect(page.locator(".my-card")).toHaveCount(6); + await expect(page.locator(".my-card")).toHaveCount(7); + const remoteData = page.getByRole("link", { exact: true, name: "Load and filter remote events" }); + await expect(remoteData).toBeVisible(); + await expect(remoteData).toHaveAttribute("href", "./remote-data/"); await page.keyboard.press("Tab"); const skipLink = page.getByRole("link", { name: "Skip to examples" }); await expect(skipLink).toBeFocused(); @@ -183,7 +186,7 @@ test.describe("large-text developer-page reflow", () => { await page.setViewportSize({ height: 844, width: 320 }); const response = await page.goto("/examples/", { waitUntil: "domcontentloaded" }); expect(response?.ok()).toBe(true); - await expect(page.locator(".my-card")).toHaveCount(6); + await expect(page.locator(".my-card")).toHaveCount(7); await expect(page.locator("[data-my-metadata-state]")) .toHaveAttribute("data-my-metadata-state", "ready"); await page.addStyleTag({ content: "html { font-size: 150%; }" }); diff --git a/tests/e2e/remote-data.spec.js b/tests/e2e/remote-data.spec.js new file mode 100644 index 0000000..bd86aef --- /dev/null +++ b/tests/e2e/remote-data.spec.js @@ -0,0 +1,112 @@ +import { expect, test } from "@playwright/test"; + +const EXAMPLE = "/examples/remote-data/"; +const FEED = "**/examples/remote-data/events.json?*"; + +async function openExample(page) { + await page.goto(EXAMPLE); + await expect(page.getByRole("grid").getByRole("button", { name: /^View 3 events for /u })).toBeVisible(); +} + +test("remote example fetches the fixture, filters, refreshes, and distinguishes empty results", async ({ page }) => { + const [initialRequest] = await Promise.all([page.waitForRequest(FEED), openExample(page)]); + const url = new URL(initialRequest.url()); + expect(url.searchParams.get("start")).toMatch(/^2026-07-[0-9]{2}$/u); + expect(url.searchParams.get("end")).toMatch(/^2026-09-[0-9]{2}$/u); + expect(url.searchParams.get("category")).toBe("all"); + await page.getByLabel("Category").selectOption("workshop"); + await expect(page.getByRole("grid").getByRole("button", { name: /Print workshop/u })).toBeVisible(); + await expect(page.getByRole("grid").getByRole("button", { name: /^View 3 events for /u })).toHaveCount(0); + await page.getByLabel("Category").selectOption("sports"); + await expect(page.getByText("No events", { exact: true })).toBeVisible(); + const [refresh] = await Promise.all([ + page.waitForRequest(FEED), + page.getByRole("button", { name: "Refresh events", exact: true }).click() + ]); + expect(new URL(refresh.url()).searchParams.get("category")).toBe("sports"); + await expect(page.getByText("No events", { exact: true })).toBeVisible(); +}); + +test("remote example opens a complete day chooser and restores focus", async ({ page }) => { + await openExample(page); + const count = page.getByRole("grid").getByRole("button", { name: /^View 3 events for /u }); + await count.click(); + const dialog = page.getByRole("dialog", { name: "Events for 2026-08-06" }); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole("button", { name: /^Choose /u })).toHaveCount(3); + await dialog.getByRole("button", { name: "Choose Print workshop", exact: true }).click(); + await expect(dialog).not.toBeVisible(); + await expect(page.getByRole("status").filter({ hasText: "Selected Print workshop" })).toContainText("No server data was changed."); + await expect(count).toBeFocused(); + await page.keyboard.press("Enter"); + await expect(dialog).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(dialog).not.toBeVisible(); + await expect(count).toBeFocused(); +}); + +test("remote example exposes Retry after failed response and recovers", async ({ page }) => { + await openExample(page); + await page.route(FEED, (route) => route.fulfill({ status: 503, body: "Unavailable" }), { times: 1 }); + await page.getByRole("button", { name: "Refresh events", exact: true }).click(); + await expect(page.getByRole("button", { name: "Retry", exact: true })).toBeVisible(); + await page.getByRole("button", { name: "Retry", exact: true }).click(); + await expect(page.getByRole("button", { name: "Retry", exact: true })).toHaveCount(0); + await expect(page.getByRole("grid").getByRole("button", { name: /^View 3 events for /u })).toBeVisible(); +}); + +test("remote dialog returns to its date when a refresh replaces the invoker", async ({ page }) => { + await openExample(page); + const count = page.getByRole("grid").getByRole("button", { name: /^View 3 events for /u }); + const invoker = await count.elementHandle(); + await count.click(); + await page.evaluate(() => document.querySelector("[data-my-refresh]").click()); + await expect.poll(() => invoker.evaluate((element) => element.isConnected)).toBe(false); + await expect(page.locator("[data-my-calendar]")).not.toHaveAttribute("aria-busy", "true"); + await page.getByRole("dialog").getByRole("button", { name: "Close", exact: true }).click(); + await expect(page.getByRole("grid").getByRole("button", { name: /^Thursday, August 6, 2026, 3 events$/u })).toBeFocused(); +}); + +test("remote example cancels superseded filters and tears down pending work", async ({ page }) => { + await openExample(page); + let finishOldRequest; + const oldResponse = new Promise((resolve) => { finishOldRequest = resolve; }); + await page.route(FEED, async (route) => { + if (new URL(route.request().url()).searchParams.get("category") === "community") { + await oldResponse; + //The browser may have cancelled the transport before this reply is sent. + await route.fulfill({ json: [{ id: "stale", title: "Stale event", start: "2026-08-06", category: "community" }] }); + } else { + await route.continue(); + } + }); + const [supersededRequest] = await Promise.all([ + page.waitForRequest((request) => new URL(request.url()).searchParams.get("category") === "community"), + page.getByLabel("Category").selectOption("community") + ]); + await Promise.all([ + page.waitForEvent("requestfailed", (request) => request === supersededRequest), + page.getByLabel("Category").selectOption("workshop") + ]); + await expect(page.getByRole("grid").getByRole("button", { name: /Print workshop/u })).toBeVisible(); + finishOldRequest(); + await expect(page.getByText("Stale event", { exact: true })).toHaveCount(0); + let finishRefresh; + const refreshResponse = new Promise((resolve) => { finishRefresh = resolve; }); + await page.route(FEED, async (route) => { + await refreshResponse; + await route.abort(); + }, { times: 1 }); + const [pendingRequest] = await Promise.all([ + page.waitForRequest(FEED), + page.getByRole("button", { name: "Refresh events", exact: true }).click() + ]); + await Promise.all([ + page.waitForEvent("requestfailed", (request) => request === pendingRequest), + page.evaluate(() => window.dispatchEvent(new PageTransitionEvent("pagehide", { persisted: false }))) + ]); + finishRefresh(); + await expect(page.locator("[data-my-calendar]")).toBeEmpty(); + await page.getByLabel("Category").selectOption("all"); + await expect(page.locator("[data-my-calendar]")).toBeEmpty(); +}); From e504060486097b91d0946e15e24632eba4904142 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:31:16 -0700 Subject: [PATCH 09/22] ci: replace alpha releases with unattended nightlies --- .github/workflows/deploy-examples.yml | 45 +- .github/workflows/prepare-alpha.yml | 200 ------- ...{publish-alpha.yml => publish-nightly.yml} | 532 +++++++++++------- docs/example-deployment.md | 23 +- docs/package-verification.md | 62 +- docs/release-administration.md | 271 ++++----- docs/release-operations.md | 292 +++------- docs/releasing.md | 112 ++-- docs/security-model.md | 27 +- package-lock.json | 4 +- package.json | 19 +- scripts/build-pages.mjs | 18 +- scripts/check-package-policy.mjs | 18 +- scripts/check-release-ready.mjs | 87 --- scripts/check-sbom.mjs | 32 +- scripts/lib/nightly-release.mjs | 198 +++++++ scripts/lib/release-artifacts.mjs | 4 + scripts/lib/release-state.mjs | 439 --------------- scripts/lib/semver.mjs | 29 - scripts/nightly-release.mjs | 40 ++ scripts/pack-nightly.mjs | 8 + scripts/pack-release.mjs | 52 +- scripts/pages-site/index.html | 4 +- scripts/pages-site/site.js | 4 +- scripts/prepare-release.mjs | 83 --- scripts/tests/build-pages.test.mjs | 8 +- scripts/tests/check-sbom.test.mjs | 21 + scripts/tests/nightly-release.test.mjs | 170 ++++++ ...st.mjs => publish-nightly-policy.test.mjs} | 121 ++-- scripts/tests/release-artifacts.test.mjs | 8 +- scripts/tests/release-state.test.mjs | 271 --------- scripts/tests/semver.test.mjs | 12 - scripts/tests/workflow-contracts.test.mjs | 222 +++----- 33 files changed, 1342 insertions(+), 2094 deletions(-) delete mode 100644 .github/workflows/prepare-alpha.yml rename .github/workflows/{publish-alpha.yml => publish-nightly.yml} (68%) delete mode 100644 scripts/check-release-ready.mjs create mode 100644 scripts/lib/nightly-release.mjs delete mode 100644 scripts/lib/release-state.mjs create mode 100644 scripts/nightly-release.mjs create mode 100644 scripts/pack-nightly.mjs delete mode 100644 scripts/prepare-release.mjs create mode 100644 scripts/tests/nightly-release.test.mjs rename scripts/tests/{publish-alpha-policy.test.mjs => publish-nightly-policy.test.mjs} (86%) delete mode 100644 scripts/tests/release-state.test.mjs diff --git a/.github/workflows/deploy-examples.yml b/.github/workflows/deploy-examples.yml index bdf180b..2ae6bc5 100644 --- a/.github/workflows/deploy-examples.yml +++ b/.github/workflows/deploy-examples.yml @@ -4,7 +4,7 @@ on: workflow_run: workflows: - CI - - Publish npm alpha + - Publish npm nightly types: - completed branches: @@ -29,7 +29,7 @@ jobs: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.event == 'push' && + (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'schedule' || github.event.workflow_run.event == 'workflow_dispatch') && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.full_name == github.repository }} @@ -38,6 +38,7 @@ jobs: permissions: contents: read + actions: read outputs: channel: ${{ steps.identity.outputs.channel }} @@ -62,6 +63,8 @@ jobs: LFC_UPSTREAM_WORKFLOW: ${{ github.event.workflow_run.name }} LFC_UPSTREAM_WORKFLOW_PATH: ${{ github.event.workflow_run.path }} LFC_WORKFLOW_RUN_SHA: ${{ github.event.workflow_run.head_sha }} + LFC_UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }} + LFC_UPSTREAM_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} run: | set -euo pipefail @@ -77,16 +80,18 @@ jobs: "${upstream_workflow_path}" == ".github/workflows/ci.yml" && "${LFC_UPSTREAM_EVENT}" == "push" ]]; then channel="main" - elif [[ "${LFC_UPSTREAM_WORKFLOW}" == "Publish npm alpha" && - "${upstream_workflow_path}" == ".github/workflows/publish-alpha.yml" && - "${LFC_UPSTREAM_EVENT}" == "push" ]]; then - parent_version="$(git show HEAD^1:package.json | jq --exit-status --raw-output '.version')" - if [[ "${version}" == "${parent_version}" ]]; then - eligible=false - channel="" - else - channel="release" - fi + elif [[ "${LFC_UPSTREAM_WORKFLOW}" == "Publish npm nightly" && + "${upstream_workflow_path}" == ".github/workflows/publish-nightly.yml" && + ( "${LFC_UPSTREAM_EVENT}" == "schedule" || "${LFC_UPSTREAM_EVENT}" == "workflow_dispatch" ) ]]; then + channel="release" + base_version="${version}" + created_at="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${LFC_UPSTREAM_RUN_ID}" --jq '.created_at')" + printf 'LFC_BROWSER_TARGET_DATE=%s\n' "${created_at:0:10}" >> "${GITHUB_ENV}" + stamp="$(date -u --date="${created_at}" +%Y%m%d%H%M%S)" + version="${base_version%-nightly.0}-nightly.${stamp}.${LFC_UPSTREAM_RUN_ID}" + run_jobs="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${LFC_UPSTREAM_RUN_ID}/attempts/${LFC_UPSTREAM_RUN_ATTEMPT}/jobs?per_page=100" --jq '.jobs[] | select(.name == "Publish the verified GitHub prerelease") | .conclusion')" + if [[ "${run_jobs}" == "skipped" ]]; then eligible=false; channel=""; + elif [[ "${run_jobs}" != "success" ]]; then echo 'Publisher finalization is unproven.' >&2; exit 1; fi else echo "Unexpected upstream workflow identity: ${LFC_UPSTREAM_WORKFLOW} (${LFC_UPSTREAM_WORKFLOW_PATH})" >&2 exit 1 @@ -95,8 +100,8 @@ jobs: if [[ "${eligible}" == "true" && "${channel}" == "release" ]]; then release_ref="v${version}" channel="release" - if [[ ! "${version}" =~ ^0\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "A changed release version must be an exact supported alpha." >&2 + if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-nightly\.[0-9]{14}\.[0-9]+$ ]]; then + echo "A changed release version must be an exact generated nightly." >&2 exit 1 fi test "$(git rev-parse --verify "${release_ref}^{commit}")" = "${source_commit}" @@ -105,6 +110,18 @@ jobs: .draft == false and .prerelease == true and .immutable == true and .tag_name == $tag and .target_commitish == $commit ' <<< "${release}" > /dev/null + gh release download "${release_ref}" --repo "${GITHUB_REPOSITORY}" --pattern package-verification.json --dir "${RUNNER_TEMP}/nightly-receipt" + receipt="${RUNNER_TEMP}/nightly-receipt/package-verification.json" + expected_digest="$(jq -er '.assets[] | select(.name == "package-verification.json") | .digest | sub("^sha256:"; "")' <<< "${release}")" + test "$(sha256sum "${receipt}" | cut -d ' ' -f 1)" = "${expected_digest}" + jq --exit-status --arg sha "${source_commit}" --arg version "${version}" --arg base "${base_version}" --arg run "${LFC_UPSTREAM_RUN_ID}" ' + .schemaVersion == 2 and .name == "@tryagaindev/litefold-calendar" and + .sourceCommit == $sha and .version == $version and .sourceVersion == $base and + .sourceTreeDirty == false and .manifestTransform == "version-only" and + .nightly.runId == $run and .nightly.sourceCommit == $sha and .nightly.version == $version and + .nightly.workflow == ".github/workflows/publish-nightly.yml" + ' "${receipt}" > /dev/null + printf 'LFC_NIGHTLY_RECEIPT=%s\n' "${receipt}" >> "${GITHUB_ENV}" fi printf 'eligible=%s\n' "${eligible}" >> "${GITHUB_OUTPUT}" diff --git a/.github/workflows/prepare-alpha.yml b/.github/workflows/prepare-alpha.yml deleted file mode 100644 index 358297f..0000000 --- a/.github/workflows/prepare-alpha.yml +++ /dev/null @@ -1,200 +0,0 @@ -name: Prepare alpha release - -on: - workflow_dispatch: - inputs: - bump: - description: Version change to prepare - required: true - default: Continue alpha - type: choice - options: - - Continue alpha - - Next patch alpha - - Next minor alpha - -permissions: - contents: read - -concurrency: - group: prepare-alpha-${{ github.repository }} - cancel-in-progress: false - -env: - LFC_NODE_VERSION: "24.19.0" - LFC_NPM_VERSION: "12.0.2" - -jobs: - prepare: - name: Prepare release files without write authority - if: ${{ github.ref == 'refs/heads/main' }} - runs-on: ubuntu-latest - timeout-minutes: 10 - - permissions: - contents: read - - outputs: - branch: ${{ steps.release.outputs.branch }} - version: ${{ steps.release.outputs.version }} - - steps: - - name: Check out the trusted default branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up exact Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.LFC_NODE_VERSION }} - package-manager-cache: false - - - name: Set up exact npm - working-directory: ${{ runner.temp }} - run: npm install --global "npm@${LFC_NPM_VERSION}" --ignore-scripts - - - name: Install locked release tooling - run: npm ci --ignore-scripts - - - name: Prepare and validate release state - id: release - env: - LFC_BUMP: ${{ inputs.bump }} - run: | - set -euo pipefail - - git fetch --no-tags origin main - test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" - - case "${LFC_BUMP}" in - "Continue alpha") bump=prerelease ;; - "Next patch alpha") bump=prepatch ;; - "Next minor alpha") bump=preminor ;; - *) echo "Unsupported release bump choice." >&2; exit 1 ;; - esac - - npm run release:prepare -- --bump "${bump}" --json - - version="$(node --input-type=module -e 'import manifest from "./package.json" with { type: "json" }; process.stdout.write(manifest.version);')" - branch="release/v${version}" - - mapfile -t changed < <(git diff --name-only | LC_ALL=C sort) - expected=(CHANGELOG.md package-lock.json package.json) - diff -u \ - <(printf '%s\n' "${expected[@]}") \ - <(printf '%s\n' "${changed[@]}") - - if git ls-remote --exit-code --heads origin "refs/heads/${branch}" > /dev/null 2>&1; then - echo "Release preparation branch ${branch} already exists." >&2 - exit 1 - fi - - npm run release:verify \ - -- --version "${version}" --tag-state absent - - printf 'version=%s\n' "${version}" >> "${GITHUB_OUTPUT}" - printf 'branch=%s\n' "${branch}" >> "${GITHUB_OUTPUT}" - - - name: Upload only the prepared release files - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: alpha-preparation-${{ github.run_id }}-${{ github.run_attempt }} - if-no-files-found: error - overwrite: false - path: | - CHANGELOG.md - package-lock.json - package.json - retention-days: 1 - - create-branch: - name: Create the reviewed release branch - needs: prepare - runs-on: ubuntu-latest - timeout-minutes: 5 - - permissions: - actions: read - contents: write - - steps: - - name: Download the prepared release files - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: alpha-preparation-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/alpha-preparation - - - name: Validate and commit without repository credentials - env: - LFC_BRANCH: ${{ needs.prepare.outputs.branch }} - LFC_PREPARATION: ${{ runner.temp }}/alpha-preparation - LFC_SOURCE_COMMIT: ${{ github.sha }} - LFC_VERSION: ${{ needs.prepare.outputs.version }} - run: | - set -euo pipefail - - expected=(CHANGELOG.md package-lock.json package.json) - mapfile -t actual < <( - find "${LFC_PREPARATION}" -mindepth 1 -maxdepth 1 -type f -printf '%f\n' | - LC_ALL=C sort - ) - diff -u \ - <(printf '%s\n' "${expected[@]}") \ - <(printf '%s\n' "${actual[@]}") - test -z "$(find "${LFC_PREPARATION}" -mindepth 1 ! -type f -print -quit)" - [[ "${LFC_SOURCE_COMMIT}" =~ ^[0-9a-f]{40}$ ]] - [[ "${LFC_VERSION}" =~ ^0\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]] - test "${LFC_BRANCH}" = "release/v${LFC_VERSION}" - test "$(jq --exit-status --raw-output .version "${LFC_PREPARATION}/package.json")" = "${LFC_VERSION}" - test "$(jq --exit-status --raw-output .version "${LFC_PREPARATION}/package-lock.json")" = "${LFC_VERSION}" - test "$(jq --exit-status --raw-output '.packages[""].version' "${LFC_PREPARATION}/package-lock.json")" = "${LFC_VERSION}" - - canonical_origin="https://github.com/${GITHUB_REPOSITORY}.git" - if git -c credential.helper= ls-remote --exit-code --heads \ - "${canonical_origin}" "refs/heads/${LFC_BRANCH}" > /dev/null 2>&1; then - echo "Release preparation branch ${LFC_BRANCH} already exists." >&2 - exit 1 - fi - - repository="${RUNNER_TEMP}/prepared-repository" - mkdir -- "${repository}" - git -C "${repository}" init - git -C "${repository}" remote add origin "${canonical_origin}" - git -C "${repository}" -c credential.helper= fetch --no-tags --depth=1 \ - origin "${LFC_SOURCE_COMMIT}" - git -C "${repository}" switch --detach FETCH_HEAD - test "$(git -C "${repository}" rev-parse --verify HEAD^{commit})" = "${LFC_SOURCE_COMMIT}" - cp -- "${LFC_PREPARATION}/CHANGELOG.md" \ - "${LFC_PREPARATION}/package-lock.json" \ - "${LFC_PREPARATION}/package.json" \ - "${repository}" - mapfile -t changed < <(git -C "${repository}" diff --name-only | LC_ALL=C sort) - diff -u \ - <(printf '%s\n' "${expected[@]}") \ - <(printf '%s\n' "${changed[@]}") - - git -C "${repository}" config user.name "github-actions[bot]" - git -C "${repository}" config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git -C "${repository}" switch --create "${LFC_BRANCH}" - git -C "${repository}" add -- CHANGELOG.md package-lock.json package.json - git -C "${repository}" commit -m "chore: prepare ${LFC_VERSION} release" - - - name: Push only the prepared branch without force - env: - GH_TOKEN: ${{ github.token }} - LFC_BRANCH: ${{ needs.prepare.outputs.branch }} - LFC_VERSION: ${{ needs.prepare.outputs.version }} - working-directory: ${{ runner.temp }}/prepared-repository - run: | - set -euo pipefail - - gh auth setup-git - git push origin "HEAD:refs/heads/${LFC_BRANCH}" - compare_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/main...${LFC_BRANCH}?expand=1" - { - printf '## Alpha %s is ready for review\n\n' "${LFC_VERSION}" - printf '[Open the prefilled release pull request](%s) and merge it after CI succeeds.\n' "${compare_url}" - } >> "${GITHUB_STEP_SUMMARY}" diff --git a/.github/workflows/publish-alpha.yml b/.github/workflows/publish-nightly.yml similarity index 68% rename from .github/workflows/publish-alpha.yml rename to .github/workflows/publish-nightly.yml index 4445f48..4f759df 100644 --- a/.github/workflows/publish-alpha.yml +++ b/.github/workflows/publish-nightly.yml @@ -1,20 +1,20 @@ -name: Publish npm alpha +name: Publish npm nightly on: - push: - branches: - - main - paths: - - .github/workflows/publish-alpha.yml - - CHANGELOG.md - - package-lock.json - - package.json + schedule: + - cron: "0 9 * * *" + workflow_dispatch: + inputs: + expected-source: + description: Optional exact current main commit assertion + required: false + type: string permissions: contents: read concurrency: - group: npm-alpha-${{ github.repository }} + group: npm-nightly-${{ github.repository }} queue: max cancel-in-progress: false @@ -31,62 +31,55 @@ env: jobs: classify: - name: Detect a generated release candidate + name: Select the exact green main snapshot + if: ${{ github.repository == 'tryagaindev/litefold-calendar' && github.ref == 'refs/heads/main' }} runs-on: ubuntu-latest timeout-minutes: 5 - permissions: + actions: read contents: read - outputs: candidate-version: ${{ steps.candidate.outputs.candidate-version }} + base-version: ${{ steps.candidate.outputs.base-version }} + created-at: ${{ steps.candidate.outputs.created-at }} eligible: ${{ steps.candidate.outputs.eligible }} - parent-version: ${{ steps.candidate.outputs.parent-version }} - steps: - - name: Check out the exact pushed commit and its first parent + - name: Check out the exact workflow source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 2 + fetch-depth: 0 persist-credentials: false ref: ${{ github.sha }} - - - name: Classify the exact pushed commit + - name: Classify the exact nightly source id: candidate env: - LFC_EVENT_NAME: ${{ github.event_name }} - LFC_SOURCE_COMMIT: ${{ github.sha }} - LFC_WORKFLOW_COMMIT: ${{ github.workflow_sha }} - LFC_WORKFLOW_REF: ${{ github.ref }} + GH_TOKEN: ${{ github.token }} + LFC_EXPECTED_SOURCE: ${{ inputs.expected-source }} run: | set -euo pipefail - - if [[ "${LFC_EVENT_NAME}" != "push" || - "${LFC_WORKFLOW_REF}" != "refs/heads/main" || - ! "${LFC_SOURCE_COMMIT}" =~ ^[0-9a-f]{40}$ || - ! "${LFC_WORKFLOW_COMMIT}" =~ ^[0-9a-f]{40}$ || - "${LFC_WORKFLOW_COMMIT}" != "${LFC_SOURCE_COMMIT}" ]]; then - echo "The release source must be the exact reviewed workflow commit pushed to main." >&2 - exit 1 - fi - test "$(git rev-parse --verify HEAD^{commit})" = "${LFC_SOURCE_COMMIT}" - parent_commit="$(git rev-parse --verify HEAD^1)" - candidate_manifest="$(&2 - exit 1 - fi - eligible=true + test "${GITHUB_WORKFLOW_SHA}" = "${GITHUB_SHA}" + test "${GITHUB_REF}" = "refs/heads/main" + [[ "${GITHUB_EVENT_NAME}" == "schedule" || "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]] + [[ -z "${LFC_EXPECTED_SOURCE}" || "${LFC_EXPECTED_SOURCE}" == "${GITHUB_SHA}" ]] + gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${GITHUB_SHA}&event=push&per_page=100" > ci.json + jq --exit-status --arg sha "${GITHUB_SHA}" --arg repository "${GITHUB_REPOSITORY}" ' + [.workflow_runs[] | select(.head_sha == $sha and .head_branch == "main" and + .head_repository.full_name == $repository)] | sort_by(.run_number) | last | + .conclusion == "success" + ' ci.json > /dev/null + created_at="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" --jq '.created_at')" + base_version="$(jq --exit-status --raw-output '.version' package.json)" + [[ "${base_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-nightly\.0$ ]] + stamp="$(date -u --date="${created_at}" +%Y%m%d%H%M%S)" + version="${base_version%-nightly.0}-nightly.${stamp}.${GITHUB_RUN_ID}" + current_main="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main" --jq '.object.sha')" + if [[ "${current_main}" != "${GITHUB_SHA}" ]]; then + curl --fail --silent --show-error "https://registry.npmjs.org/@tryagaindev%2Flitefold-calendar/${version}" > "${RUNNER_TEMP}/existing-candidate.json" + jq --exit-status --arg version "${version}" '.name == "@tryagaindev/litefold-calendar" and .version == $version' "${RUNNER_TEMP}/existing-candidate.json" > /dev/null fi - printf 'candidate-version=%s\n' "${candidate_version}" >> "${GITHUB_OUTPUT}" - printf 'eligible=%s\n' "${eligible}" >> "${GITHUB_OUTPUT}" - printf 'parent-version=%s\n' "${parent_version}" >> "${GITHUB_OUTPUT}" + printf 'candidate-version=%s\nbase-version=%s\ncreated-at=%s\neligible=true\n' \ + "${version}" "${base_version}" "${created_at}" >> "${GITHUB_OUTPUT}" + rm -- ci.json verify: name: Verify exact source and create release evidence @@ -94,7 +87,7 @@ jobs: if: >- ${{ needs.classify.outputs.eligible == 'true' && - github.event_name == 'push' && + (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && github.workflow_sha == github.sha }} @@ -103,14 +96,18 @@ jobs: permissions: contents: read + actions: read outputs: + eligible: ${{ steps.registry-state.outputs.eligible }} + stable-exists: ${{ steps.registry-state.outputs.stable-exists }} + base-version: ${{ needs.classify.outputs.base-version }} asset-digests: ${{ steps.evidence.outputs.asset-digests }} bundle-artifact: ${{ steps.identity.outputs.bundle-artifact }} bundle-name: ${{ steps.identity.outputs.bundle-name }} notes-artifact: ${{ steps.identity.outputs.notes-artifact }} notes-sha256: ${{ steps.evidence.outputs.notes-sha256 }} - registry-alpha: ${{ steps.registry-state.outputs.registry-alpha }} + registry-predecessor: ${{ steps.registry-state.outputs.registry-predecessor }} registry-state-sha256: ${{ steps.registry-state.outputs.registry-state-sha256 }} source-commit: ${{ steps.identity.outputs.source-commit }} tag: ${{ steps.identity.outputs.tag }} @@ -137,147 +134,133 @@ jobs: - name: Install locked development tooling run: npm ci --ignore-scripts - - name: Validate the immutable first-parent release diff + - name: Derive and verify immutable nightly artifact identity id: identity env: - LFC_CLASSIFIED_VERSION: ${{ needs.classify.outputs.candidate-version }} - LFC_PARENT_VERSION: ${{ needs.classify.outputs.parent-version }} - LFC_SOURCE_COMMIT: ${{ github.sha }} + LFC_VERSION: ${{ needs.classify.outputs.candidate-version }} + LFC_CREATED_AT: ${{ needs.classify.outputs.created-at }} run: | set -euo pipefail - - source_commit="$(git rev-parse --verify HEAD^{commit})" - test "${source_commit}" = "${LFC_SOURCE_COMMIT}" - git fetch --force --no-tags origin \ - refs/heads/main:refs/remotes/origin/main - test "$(git rev-parse --verify refs/remotes/origin/main^{commit})" = "${source_commit}" - - version="$(node --input-type=module -e 'import manifest from "./package.json" with { type: "json" }; process.stdout.write(manifest.version);')" - test "${version}" = "${LFC_CLASSIFIED_VERSION}" - parent_version="$(git show HEAD^1:package.json | jq -r '.version // empty')" - test "${parent_version}" = "${LFC_PARENT_VERSION}" - test "${parent_version}" != "${version}" - tag="v${version}" - - mapfile -t changed_files < <( - git diff --name-only HEAD^1 HEAD | LC_ALL=C sort -u - ) - expected_files=(CHANGELOG.md package-lock.json package.json) - diff -u \ - <(printf '%s\n' "${expected_files[@]}") \ - <(printf '%s\n' "${changed_files[@]}") - - npm run release:verify -- \ - --version "${version}" \ - --commit "${source_commit}" \ - --tag-state either \ - --require-clean - - printf 'bundle-artifact=npm-release-bundle-%s-%s-%s\n' \ - "${tag}" "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" >> "${GITHUB_OUTPUT}" - printf 'bundle-name=tryagaindev-litefold-calendar-%s\n' "${version}" >> "${GITHUB_OUTPUT}" - printf 'notes-artifact=release-notes-%s-%s-%s\n' \ - "${tag}" "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" >> "${GITHUB_OUTPUT}" - printf 'source-commit=%s\n' "${source_commit}" >> "${GITHUB_OUTPUT}" - printf 'tag=%s\n' "${tag}" >> "${GITHUB_OUTPUT}" - printf 'version=%s\n' "${version}" >> "${GITHUB_OUTPUT}" - - - name: Validate live registry channel before release work + test "$(git rev-parse --verify HEAD^{commit})" = "${GITHUB_SHA}" + npm run nightly:plan -- --run-id "${GITHUB_RUN_ID}" --created-at "${LFC_CREATED_AT}" \ + --event "${GITHUB_EVENT_NAME}" --source-sha "${GITHUB_SHA}" --output "${RUNNER_TEMP}/nightly-plan.json" + test "$(jq -r .version "${RUNNER_TEMP}/nightly-plan.json")" = "${LFC_VERSION}" + printf 'LFC_BROWSER_TARGET_DATE=%s\n' "${LFC_CREATED_AT:0:10}" >> "${GITHUB_ENV}" + tag="v${LFC_VERSION}" + printf 'bundle-artifact=npm-release-bundle-%s-%s-%s\n' "${tag}" "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" >> "${GITHUB_OUTPUT}" + printf 'bundle-name=tryagaindev-litefold-calendar-%s\n' "${LFC_VERSION}" >> "${GITHUB_OUTPUT}" + printf 'notes-artifact=release-notes-%s-%s-%s\n' "${tag}" "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" >> "${GITHUB_OUTPUT}" + printf 'source-commit=%s\ntag=%s\nversion=%s\n' "${GITHUB_SHA}" "${tag}" "${LFC_VERSION}" >> "${GITHUB_OUTPUT}" + + - name: Validate live registry and previous completed nightly id: registry-state env: + GH_TOKEN: ${{ github.token }} LFC_VERSION: ${{ steps.identity.outputs.version }} + LFC_NIGHTLY_TOKEN_EXPIRES_AT: ${{ vars.NPM_NIGHTLY_TAG_TOKEN_EXPIRES_AT }} run: | set -euo pipefail - registry_raw="${RUNNER_TEMP}/registry.raw.json" - registry_json="${RUNNER_TEMP}/registry.json" - registry_error="${RUNNER_TEMP}/registry.err" - set +e - npm view @tryagaindev/litefold-calendar versions dist-tags --json \ - > "${registry_raw}" 2> "${registry_error}" - registry_status=$? - set -e - if [[ "${registry_status}" -eq 0 ]]; then - node --input-type=module --eval "${LFC_NORMALIZE_NPM_VIEW_JSON}" \ - "${registry_raw}" "${registry_json}" - metadata="$(<"${registry_json}")" - registry_state="$( - jq --compact-output --sort-keys ' - { - "dist-tags": ."dist-tags", - versions: (.versions | if type == "string" then [.] else . end | sort) - } - ' "${registry_json}" - )" - registry_alpha="$(jq --raw-output '."dist-tags".alpha // empty' "${registry_json}")" - elif grep -Eq 'E404|404 Not Found' "${registry_error}"; then - echo "The npm package must already exist before the normal alpha release workflow can run." >&2 - exit 1 - else - cat "${registry_error}" >&2 - exit "${registry_status}" - fi - export LFC_REGISTRY_METADATA="${metadata}" - # LFC_PRERELEASE_CHANNEL_POLICY_START + npm view @tryagaindev/litefold-calendar versions dist-tags --json --registry https://registry.npmjs.org/ > "${RUNNER_TEMP}/registry.raw.json" + node --input-type=module --eval "${LFC_NORMALIZE_NPM_VIEW_JSON}" "${RUNNER_TEMP}/registry.raw.json" "${RUNNER_TEMP}/registry.json" node --input-type=module <<'NODE' - import semver from "semver"; - - const candidate = process.env.LFC_VERSION; - const metadata = JSON.parse(process.env.LFC_REGISTRY_METADATA); - const versions = typeof metadata.versions === "string" - ? [metadata.versions] - : metadata.versions; - const tags = metadata["dist-tags"]; - if (!Array.isArray(versions) || tags === null || typeof tags !== "object" || - Array.isArray(tags)) { - throw new Error("npm returned malformed release metadata."); - } - const supportedAlpha = (value) => typeof value === "string" && - /^0\.[0-9]+\.[0-9]+-alpha\.[0-9]+$/u.test(value) && - semver.valid(value) === value; - for (const version of versions) { - if (typeof version !== "string" || semver.valid(version) === null) { - throw new Error("npm returned a malformed published version."); - } - if (version !== candidate && semver.gte(version, candidate)) { - throw new Error(`Published version ${version} is not older than ${candidate}.`); - } - } - if (versions.length === 0) { - throw new Error("The normal alpha release workflow requires an existing npm package."); - } - for (const name of ["alpha", "latest"]) { - const value = tags[name]; - if (!supportedAlpha(value) || !versions.includes(value)) { - throw new Error(`npm ${name} must select a published supported alpha; found ${String(value)}.`); - } - if (value !== candidate && semver.gte(value, candidate)) { - throw new Error(`npm ${name} ${value} cannot move to ${candidate}.`); - } - } - const publicationInProgress = versions.includes(candidate) && - tags.alpha === candidate && tags.latest !== candidate; - if (tags.alpha !== tags.latest && !publicationInProgress) { - throw new Error( - `npm alpha and latest must match before a release; found alpha=${tags.alpha}, latest=${tags.latest}.` - ); + import { readFileSync, appendFileSync } from "node:fs"; + import { readNightlyPlan, validateNightlyRegistryState, tokenExpiryStatus } from "./scripts/lib/nightly-release.mjs"; + const metadata = JSON.parse(readFileSync(`${process.env.RUNNER_TEMP}/registry.json`, "utf8")); + const state = validateNightlyRegistryState(metadata, await readNightlyPlan(`${process.env.RUNNER_TEMP}/nightly-plan.json`)); + if (!state.stableExists) { + const expiry = tokenExpiryStatus(process.env.LFC_NIGHTLY_TOKEN_EXPIRES_AT); + if (expiry.warn) console.log(`::warning::Rotate the nightly tag token before ${expiry.expiresAt}.`); } + appendFileSync(process.env.GITHUB_OUTPUT, `registry-predecessor=${state.predecessor}\nstable-exists=${state.stableExists}\n`); NODE - # LFC_PRERELEASE_CHANNEL_POLICY_END + predecessor="$(jq -r '."dist-tags".nightly // ."dist-tags".alpha' "${RUNNER_TEMP}/registry.json")" + eligible=true + if [[ "${predecessor}" == *-nightly.* && "${predecessor}" != "${LFC_VERSION}" ]]; then + previous="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/v${predecessor}")" + jq --exit-status --arg tag "v${predecessor}" '.draft == false and .prerelease == true and .immutable == true and .tag_name == $tag' <<< "${previous}" > /dev/null + previous_sha="$(jq -er '.target_commitish | select(test("^[0-9a-f]{40}$"))' <<< "${previous}")" + curl --fail --silent --show-error "https://tryagaindev.github.io/litefold-calendar/releases/${predecessor}/examples/metadata.json" > "${RUNNER_TEMP}/previous-pages.json" + jq --exit-status --arg version "${predecessor}" --arg sha "${previous_sha}" '.version == $version and .commit == $sha and .channel == "release"' "${RUNNER_TEMP}/previous-pages.json" > /dev/null + if [[ "${previous_sha}" == "${GITHUB_SHA}" ]]; then eligible=false; fi + fi + registry_state="$(jq --compact-output --sort-keys '{"dist-tags": ."dist-tags", versions: (.versions | if type == "string" then [.] else . end | sort)}' "${RUNNER_TEMP}/registry.json")" + printf 'registry-state-sha256=%s\neligible=%s\n' "$(printf '%s' "${registry_state}" | sha256sum | cut -d ' ' -f 1)" "${eligible}" >> "${GITHUB_OUTPUT}" + if [[ "${eligible}" == "false" ]]; then echo 'No new source since the last completely verified nightly.' >> "${GITHUB_STEP_SUMMARY}"; fi - registry_state_sha256="$( - printf '%s' "${registry_state}" | sha256sum | cut -d ' ' -f 1 - )" - printf 'registry-alpha=%s\n' "${registry_alpha}" >> "${GITHUB_OUTPUT}" - printf 'registry-state-sha256=%s\n' "${registry_state_sha256}" >> "${GITHUB_OUTPUT}" + - name: Restore earlier verified bytes before considering a rebuild + id: recovery + if: ${{ steps.registry-state.outputs.eligible == 'true' }} + env: + GH_TOKEN: ${{ github.token }} + LFC_VERSION: ${{ steps.identity.outputs.version }} + LFC_BUNDLE_DIRECTORY: .artifacts/${{ steps.identity.outputs.bundle-name }} + run: | + set -euo pipefail + gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" > "${RUNNER_TEMP}/prior-artifacts.json" + gh api graphql -f query='query($owner:String!,$name:String!,$ref:String!,$tag:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$ref){id} release(tagName:$tag){id}}}' \ + -F owner="${GITHUB_REPOSITORY_OWNER}" -F name="${GITHUB_REPOSITORY#*/}" \ + -F ref="refs/tags/v${LFC_VERSION}" -F tag="v${LFC_VERSION}" > "${RUNNER_TEMP}/prior-public-state.json" + node --input-type=module <<'NODE' + import { readFileSync, writeFileSync, appendFileSync } from "node:fs"; + import { readNightlyPlan, selectNightlyRecoveryArtifacts } from "./scripts/lib/nightly-release.mjs"; + const root = process.env.RUNNER_TEMP; + const plan = await readNightlyPlan(`${root}/nightly-plan.json`); + const pages = JSON.parse(readFileSync(`${root}/prior-artifacts.json`, "utf8")); + const repository = JSON.parse(readFileSync(`${root}/prior-public-state.json`, "utf8"))?.data?.repository; + if (!Array.isArray(pages) || !repository) throw new Error("Nightly recovery remote evidence is unavailable."); + const registry = JSON.parse(readFileSync(`${root}/registry.json`, "utf8")); + const versions = typeof registry.versions === "string" ? [registry.versions] : registry.versions; + const selected = selectNightlyRecoveryArtifacts({ artifacts: pages.flatMap((page) => page.artifacts), + plan, attempt: Number(process.env.GITHUB_RUN_ATTEMPT), + hasPublicState: repository.ref !== null || repository.release !== null || versions.includes(plan.version) }); + writeFileSync(`${root}/nightly-recovery.json`, JSON.stringify(selected)); + appendFileSync(process.env.GITHUB_OUTPUT, `restored=${selected !== null}\n`); + NODE + if [[ "$(jq -r 'type' "${RUNNER_TEMP}/nightly-recovery.json")" == "null" ]]; then exit 0; fi + for kind in bundle notes; do + id="$(jq -er --arg kind "${kind}" '.[$kind].id' "${RUNNER_TEMP}/nightly-recovery.json")" + digest="$(jq -er --arg kind "${kind}" '.[$kind].digest | sub("^sha256:"; "")' "${RUNNER_TEMP}/nightly-recovery.json")" + gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${id}/zip" > "${RUNNER_TEMP}/nightly-${kind}.zip" + test "$(sha256sum "${RUNNER_TEMP}/nightly-${kind}.zip" | cut -d ' ' -f 1)" = "${digest}" + done + python3 <<'PYTHON' + import os + from pathlib import Path + from zipfile import ZipFile + version = os.environ["LFC_VERSION"] + root = Path(os.environ["RUNNER_TEMP"]) + outputs = {"bundle": (Path(os.environ["LFC_BUNDLE_DIRECTORY"]), + {"LICENSE", "SHA256SUMS", "package-verification.json", "sbom.spdx.json", f"tryagaindev-litefold-calendar-{version}.tgz"}), + "notes": (root / "release-notes", {"release-notes.md"})} + for kind, (destination, expected) in outputs.items(): + with ZipFile(root / f"nightly-{kind}.zip") as archive: + members = archive.infolist() + if len(members) != len(expected) or {entry.filename for entry in members} != expected or sum(entry.file_size for entry in members) > 64 * 1024 * 1024: + raise ValueError("Retained artifact does not contain the exact bounded evidence files.") + destination.mkdir(parents=True, exist_ok=False) + for entry in members: + with (destination / entry.filename).open("xb") as output: + output.write(archive.read(entry)) + PYTHON + node --input-type=module <<'NODE' + import { readFileSync } from "node:fs"; + import { readNightlyPlan, verifyRestoredNightlyBundle } from "./scripts/lib/nightly-release.mjs"; + await verifyRestoredNightlyBundle(process.env.LFC_BUNDLE_DIRECTORY, + await readNightlyPlan(`${process.env.RUNNER_TEMP}/nightly-plan.json`), + JSON.parse(readFileSync("package.json", "utf8"))); + NODE + echo 'Reusing the original digest-verified package bundle and notes; no rebuild.' >> "${GITHUB_STEP_SUMMARY}" - name: Install pinned Playwright browsers + if: ${{ steps.registry-state.outputs.eligible == 'true' && steps.recovery.outputs.restored != 'true' }} run: npx playwright install --with-deps chromium firefox webkit - name: Run the complete release quality gate + if: ${{ steps.registry-state.outputs.eligible == 'true' && steps.recovery.outputs.restored != 'true' }} run: npm run check - name: Upload browser diagnostics on failure - if: failure() + if: failure() && steps.registry-state.outputs.eligible == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-browser-diagnostics-${{ github.run_id }} @@ -288,17 +271,20 @@ jobs: retention-days: 7 - name: Create the verified npm release bundle once - run: npm run package + if: ${{ steps.registry-state.outputs.eligible == 'true' && steps.recovery.outputs.restored != 'true' }} + run: npm run package:nightly -- --plan "${RUNNER_TEMP}/nightly-plan.json" - name: Create normalized release notes and asset digests + if: ${{ steps.registry-state.outputs.eligible == 'true' }} id: evidence env: LFC_BUNDLE_DIRECTORY: .artifacts/${{ steps.identity.outputs.bundle-name }} LFC_NOTES_DIRECTORY: ${{ runner.temp }}/release-notes + LFC_RESTORED: ${{ steps.recovery.outputs.restored }} LFC_VERSION: ${{ steps.identity.outputs.version }} run: | set -euo pipefail - mkdir -- "${LFC_NOTES_DIRECTORY}" + mkdir -p -- "${LFC_NOTES_DIRECTORY}" export LFC_NOTES_PATH="${LFC_NOTES_DIRECTORY}/release-notes.md" node --input-type=module <<'NODE' import { appendFile, readFile, writeFile } from "node:fs/promises"; @@ -307,14 +293,20 @@ jobs: const changelog = await readFile("CHANGELOG.md", "utf8"); const version = process.env.LFC_VERSION; - const heading = `## [${version}]`; + const heading = "## [Unreleased]"; const start = changelog.indexOf(heading); const next = changelog.indexOf("\n## [", start + heading.length); if (start < 0) { throw new Error(`CHANGELOG.md is missing ${heading}.`); } - const notes = changelog.slice(start, next < 0 ? undefined : next).trim() + "\n"; - await writeFile(process.env.LFC_NOTES_PATH, notes, "utf8"); + const notes = `# Nightly ${version}\n\nSource: ${process.env.GITHUB_SHA}\n\n` + changelog.slice(start + heading.length, next < 0 ? undefined : next).trim() + "\n"; + if (process.env.LFC_RESTORED === "true") { + if (await readFile(process.env.LFC_NOTES_PATH, "utf8") !== notes) { + throw new Error("Retained notes differ from the exact source and nightly identity."); + } + } else { + await writeFile(process.env.LFC_NOTES_PATH, notes, "utf8"); + } const directory = process.env.LFC_BUNDLE_DIRECTORY; const expected = [ @@ -342,6 +334,7 @@ jobs: NODE - name: Upload the exact npm release bundle + if: ${{ steps.registry-state.outputs.eligible == 'true' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.identity.outputs.bundle-artifact }} @@ -351,6 +344,7 @@ jobs: retention-days: 30 - name: Upload normalized release notes + if: ${{ steps.registry-state.outputs.eligible == 'true' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.identity.outputs.notes-artifact }} @@ -362,6 +356,7 @@ jobs: stage-release: name: Stage exact tag, draft, and release assets needs: verify + if: ${{ needs.verify.outputs.eligible == 'true' }} runs-on: ubuntu-latest timeout-minutes: 15 @@ -394,6 +389,7 @@ jobs: LFC_NOTES_PATH: ${{ runner.temp }}/release-notes/release-notes.md LFC_NOTES_SHA256: ${{ needs.verify.outputs.notes-sha256 }} LFC_SOURCE_COMMIT: ${{ needs.verify.outputs.source-commit }} + LFC_BASE_VERSION: ${{ needs.verify.outputs.base-version }} LFC_TAG: ${{ needs.verify.outputs.tag }} run: | set -euo pipefail @@ -479,13 +475,13 @@ jobs: printf 'release-id=%s\n' "${release_id}" >> "${GITHUB_OUTPUT}" publish: - name: Publish or resume the verified npm alpha + name: Publish or resume the verified npm nightly needs: - stage-release - verify runs-on: ubuntu-latest timeout-minutes: 12 - environment: npm + environment: npm-nightly permissions: actions: read @@ -510,9 +506,10 @@ jobs: - name: Validate the serialized registry predecessor env: GH_TOKEN: ${{ github.token }} - LFC_REGISTRY_ALPHA: ${{ needs.verify.outputs.registry-alpha }} + LFC_REGISTRY_PREDECESSOR: ${{ needs.verify.outputs.registry-predecessor }} LFC_REGISTRY_STATE_SHA256: ${{ needs.verify.outputs.registry-state-sha256 }} LFC_SOURCE_COMMIT: ${{ needs.verify.outputs.source-commit }} + LFC_BASE_VERSION: ${{ needs.verify.outputs.base-version }} LFC_VERSION: ${{ needs.verify.outputs.version }} run: | set -euo pipefail @@ -551,18 +548,18 @@ jobs: exit 1 fi - registry_alpha="$(jq --raw-output '."dist-tags".alpha // empty' channel.json)" + registry_predecessor="$(jq --raw-output '."dist-tags".nightly // ."dist-tags".alpha // empty' channel.json)" candidate_exists="$( jq --arg candidate "${LFC_VERSION}" --raw-output ' (.versions | if type == "string" then [.] else . end) | contains([$candidate]) ' channel.json )" - test "${registry_alpha}" = "${LFC_REGISTRY_ALPHA}" + test "${registry_predecessor}" = "${LFC_REGISTRY_PREDECESSOR}" if [[ "${candidate_exists}" == "true" ]]; then - if [[ "${registry_alpha}" != "${LFC_VERSION}" ]]; then - echo "The candidate exists on npm but alpha does not select it; refusing ambiguous registry state." >&2 + if [[ "${registry_predecessor}" != "${LFC_VERSION}" ]]; then + echo "The candidate exists on npm but nightly does not select it; refusing ambiguous registry state." >&2 exit 1 fi exit 0 @@ -589,53 +586,53 @@ jobs: "repos/${GITHUB_REPOSITORY}/contents/package.json?ref=${main_commit}" | jq --exit-status --raw-output '.version' )" - if [[ "${main_version}" != "${LFC_VERSION}" ]]; then + if [[ "${main_version}" != "${LFC_BASE_VERSION}" ]]; then echo "main now selects ${main_version}; refusing stale unpublished candidate ${LFC_VERSION}." >&2 exit 1 fi - if [[ ! "${registry_alpha}" =~ ^0\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ || - "${registry_alpha}" == "${LFC_VERSION}" ]]; then - echo "The verified npm alpha predecessor is missing or malformed." >&2 + if [[ ! "${registry_predecessor}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-(alpha\.[0-9]+|nightly\.[0-9]{14}\.[0-9]+)$ || + "${registry_predecessor}" == "${LFC_VERSION}" ]]; then + echo "The verified npm predecessor is missing or malformed." >&2 exit 1 fi - tag_ref="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/v${registry_alpha}")" + tag_ref="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/v${registry_predecessor}")" tag_object_type="$(jq --exit-status --raw-output '.object.type' <<< "${tag_ref}")" tag_object="$( jq --exit-status --raw-output ' if (.object.sha | type == "string" and test("^[0-9a-f]{40}$")) then .object.sha - else error("The registry alpha tag must identify one Git object.") + else error("The registry predecessor tag must identify one Git object.") end ' <<< "${tag_ref}" )" if [[ "${tag_object_type}" == "tag" ]]; then tag="$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${tag_object}")" - registry_alpha_commit="$( + registry_predecessor_commit="$( jq --exit-status --raw-output ' if .object.type == "commit" and (.object.sha | type == "string" and test("^[0-9a-f]{40}$")) then .object.sha - else error("The registry alpha tag must peel to one commit.") + else error("The registry predecessor tag must peel to one commit.") end ' <<< "${tag}" )" elif [[ "${tag_object_type}" == "commit" ]]; then - case "${registry_alpha}:${tag_object}" in + case "${registry_predecessor}:${tag_object}" in "0.1.0-alpha.0:53cf1fbb5f4176929c3105030a62e1d0c235b54f"|"0.2.0-alpha.0:8250ac4da9ada72a2915b8f810be404667ab47da") - registry_alpha_commit="${tag_object}" + registry_predecessor_commit="${tag_object}" ;; *) - echo "The registry alpha tag must be annotated; only the exact historical v0.1.0-alpha.0 and v0.2.0-alpha.0 tags are exempt." >&2 + echo "The registry predecessor tag must be annotated; only the exact historical v0.1.0-alpha.0 and v0.2.0-alpha.0 tags are exempt." >&2 exit 1 ;; esac else - echo "The registry alpha tag must be annotated; only the exact historical v0.1.0-alpha.0 and v0.2.0-alpha.0 tags are exempt." >&2 + echo "The registry predecessor tag must be annotated; only the exact historical v0.1.0-alpha.0 and v0.2.0-alpha.0 tags are exempt." >&2 exit 1 fi comparison="$( - gh api "repos/${GITHUB_REPOSITORY}/compare/${registry_alpha_commit}...${LFC_SOURCE_COMMIT}" + gh api "repos/${GITHUB_REPOSITORY}/compare/${registry_predecessor_commit}...${LFC_SOURCE_COMMIT}" )" jq --exit-status ' .status == "ahead" or .status == "identical" @@ -655,6 +652,7 @@ jobs: LFC_BUNDLE_DIRECTORY: ${{ runner.temp }}/release-bundle LFC_REGISTRY_STATE_SHA256: ${{ needs.verify.outputs.registry-state-sha256 }} LFC_SOURCE_COMMIT: ${{ needs.verify.outputs.source-commit }} + LFC_BASE_VERSION: ${{ needs.verify.outputs.base-version }} LFC_TAG: ${{ needs.verify.outputs.tag }} LFC_VERSION: ${{ needs.verify.outputs.version }} run: | @@ -674,6 +672,9 @@ jobs: jq --exit-status --argjson expected "${LFC_ASSET_DIGESTS}" \ '. == $expected' <<< "${local_digests}" > /dev/null + gh api -H "Accept: application/vnd.github.raw+json" "repos/${GITHUB_REPOSITORY}/contents/package.json?ref=${LFC_SOURCE_COMMIT}" > source-package.json + export LFC_SOURCE_MANIFEST="$( packed-package.json @@ -684,6 +685,7 @@ jobs: const receipt = JSON.parse(process.env.LFC_RECEIPT); const manifest = JSON.parse(process.env.LFC_MANIFEST); + const sourceManifest = JSON.parse(process.env.LFC_SOURCE_MANIFEST); const tarball = await readFile(process.env.LFC_TARBALL); const sha256 = createHash("sha256").update(tarball).digest("hex"); const repository = typeof manifest.repository === "string" @@ -698,16 +700,33 @@ jobs: "bundledDependencies", "bundleDependencies" ]; - if (receipt.schemaVersion !== 1 || + if (receipt.schemaVersion !== 2 || receipt.name !== "@tryagaindev/litefold-calendar" || receipt.version !== process.env.LFC_VERSION || receipt.sourceCommit !== process.env.LFC_SOURCE_COMMIT || receipt.sourceTreeDirty !== false || receipt.sha256 !== sha256 || + receipt.sourceVersion !== process.env.LFC_BASE_VERSION || + receipt.nightly?.sourceCommit !== receipt.sourceCommit || + receipt.nightly?.version !== receipt.version || + receipt.nightly?.runId !== process.env.GITHUB_RUN_ID || + receipt.nightly?.workflow !== ".github/workflows/publish-nightly.yml" || + receipt.manifestTransform !== "version-only" || + receipt.browserTargets?.resolvedAt !== receipt.nightly.createdAt?.slice(0, 10) || + receipt.browserTargets?.query !== "baseline-widely-available" || + receipt.browserTargets?.dataVersions?.vite !== sourceManifest.devDependencies?.vite || + receipt.browserTargets?.dataVersions?.esbuild !== sourceManifest.devDependencies?.esbuild || + receipt.browserTargets?.effectiveQuery !== `Vite ${sourceManifest.devDependencies?.vite}: baseline-widely-available` || + !Array.isArray(receipt.browserTargets?.javascript) || receipt.browserTargets.javascript.length === 0 || + !Array.isArray(receipt.browserTargets?.css) || receipt.browserTargets.css.length === 0 || + receipt.browserTargetsSha256 !== createHash("sha256").update(JSON.stringify(receipt.browserTargets)).digest("hex") || + receipt.sourceManifestSha256 !== createHash("sha256").update(JSON.stringify(sourceManifest)).digest("hex") || + JSON.stringify({ ...sourceManifest, version: receipt.version }) !== JSON.stringify(manifest) || + receipt.publishedManifestSha256 !== createHash("sha256").update(JSON.stringify(manifest)).digest("hex") || manifest.name !== receipt.name || manifest.version !== receipt.version || manifest.private !== false || manifest.license !== "MIT" || repository !== "git+https://github.com/tryagaindev/litefold-calendar.git" || manifest.publishConfig?.access !== "public" || - manifest.publishConfig?.tag !== "alpha" || + manifest.publishConfig?.tag !== "nightly" || manifest.publishConfig?.provenance !== true || forbiddenDependencyFields.some((field) => Object.hasOwn(manifest, field)) || Object.keys(manifest.scripts ?? {}).some((name) => forbiddenLifecycle.test(name))) { @@ -798,7 +817,7 @@ jobs: "repos/${GITHUB_REPOSITORY}/contents/package.json?ref=${main_commit}" | jq --exit-status --raw-output '.version' )" - if [[ "${main_version}" != "${LFC_VERSION}" ]]; then + if [[ "${main_version}" != "${LFC_BASE_VERSION}" ]]; then echo "main now selects ${main_version}; refusing stale unpublished candidate ${LFC_VERSION}." >&2 exit 1 fi @@ -806,7 +825,7 @@ jobs: npm publish "${LFC_TARBALL}" \ --registry https://registry.npmjs.org/ \ --access public \ - --tag alpha \ + --tag nightly \ --provenance \ --ignore-scripts else @@ -816,13 +835,100 @@ jobs: printf 'expected-integrity=%s\n' "${expected_integrity}" >> "${GITHUB_OUTPUT}" printf 'publication=%s\n' "${publication}" >> "${GITHUB_OUTPUT}" - echo "::notice::An authenticated npm package maintainer must ensure latest selects this alpha: npm dist-tag add @tryagaindev/litefold-calendar@${LFC_VERSION} latest --registry https://registry.npmjs.org/" + + + synchronize-latest: + name: Synchronize the temporary nightly latest channel + needs: [publish, verify] + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: npm-nightly-tags + permissions: + contents: read + outputs: + latest-target: ${{ steps.target.outputs.latest-target }} + steps: + - name: Set up exact Node for tag synchronization + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ env.LFC_NODE_VERSION }} + package-manager-cache: false + - name: Install exact npm before exposing the tag credential + working-directory: ${{ runner.temp }} + run: npm install --global "npm@${LFC_NPM_VERSION}" --ignore-scripts + - name: Verify the candidate and stable ownership before tagging + id: target + env: + LFC_VERSION: ${{ needs.verify.outputs.version }} + LFC_EXPECTED_INTEGRITY: ${{ needs.publish.outputs.expected-integrity }} + run: | + set -euo pipefail + ready=false + for attempt in $(seq 1 40); do + if ! npm view @tryagaindev/litefold-calendar versions dist-tags --json --registry https://registry.npmjs.org/ > registry.raw.json || + ! npm view "@tryagaindev/litefold-calendar@${LFC_VERSION}" dist.integrity --json --registry https://registry.npmjs.org/ > integrity.raw.json; then + sleep 10 + continue + fi + node --input-type=module --eval "${LFC_NORMALIZE_NPM_VIEW_JSON}" registry.raw.json registry.json + node --input-type=module --eval "${LFC_NORMALIZE_NPM_VIEW_JSON}" integrity.raw.json integrity.json + # LFC_NIGHTLY_LATEST_POLICY_START + if node --input-type=module <<'NODE' + import { readFileSync, appendFileSync } from "node:fs"; + const data = JSON.parse(readFileSync("registry.json", "utf8")); + const versions = typeof data.versions === "string" ? [data.versions] : data.versions; + const integrity = JSON.parse(readFileSync("integrity.json", "utf8")); + const stable = (version) => typeof version === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/u.test(version); + if (integrity !== null && integrity !== process.env.LFC_EXPECTED_INTEGRITY) { + throw new Error("Nightly registry integrity changed before latest synchronization."); + } + if (!Array.isArray(versions) || !versions.includes(process.env.LFC_VERSION) || + data["dist-tags"]?.nightly !== process.env.LFC_VERSION || integrity === null) { + process.exit(75); + } + const stableExists = versions.some(stable); + const latest = data["dist-tags"].latest; + if (!versions.includes(latest) || (stableExists && !stable(latest))) { + throw new Error("Stable owns latest after its first publication; refusing an inconsistent channel."); + } + const target = stableExists ? latest : process.env.LFC_VERSION; + appendFileSync(process.env.GITHUB_OUTPUT, `latest-target=${target}\nwrite=${latest !== target}\n`); + NODE + # LFC_NIGHTLY_LATEST_POLICY_END + then + ready=true + break + else + status=$? + if [[ "${status}" != "75" ]]; then exit "${status}"; fi + fi + sleep 10 + done + if [[ "${ready}" != "true" ]]; then + echo 'Nightly registry reads did not converge before latest synchronization.' >&2 + exit 1 + fi + - name: Advance only latest to the verified nightly + if: ${{ steps.target.outputs.write == 'true' }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_NIGHTLY_TAG_TOKEN }} + NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/nightly-tag.npmrc + LFC_VERSION: ${{ needs.verify.outputs.version }} + LFC_TOKEN_EXPIRES_AT: ${{ vars.NPM_NIGHTLY_TAG_TOKEN_EXPIRES_AT }} + run: | + set -euo pipefail + test -n "${NODE_AUTH_TOKEN}" + test "$(date -u --date="${LFC_TOKEN_EXPIRES_AT}" +%s)" -gt "$(date -u +%s)" + printf '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\n' > "${NPM_CONFIG_USERCONFIG}" + trap 'rm -f -- "${NPM_CONFIG_USERCONFIG}"' EXIT + npm dist-tag add "@tryagaindev/litefold-calendar@${LFC_VERSION}" latest --registry https://registry.npmjs.org/ verify-registry: name: Verify registry integrity, imports, and provenance needs: - publish - verify + - synchronize-latest runs-on: ubuntu-latest timeout-minutes: 25 @@ -850,6 +956,7 @@ jobs: - name: Verify the published package from a clean consumer env: LFC_EXPECTED_INTEGRITY: ${{ needs.publish.outputs.expected-integrity }} + LFC_LATEST_TARGET: ${{ needs.synchronize-latest.outputs.latest-target }} LFC_SOURCE_COMMIT: ${{ needs.verify.outputs.source-commit }} LFC_TARBALL: ${{ runner.temp }}/release-bundle/${{ needs.verify.outputs.bundle-name }}.tgz LFC_VERSION: ${{ needs.verify.outputs.version }} @@ -876,15 +983,15 @@ jobs: node --input-type=module --eval "${LFC_NORMALIZE_NPM_VIEW_JSON}" \ "${tags_raw}" "${tags_json}" integrity="$(jq -r 'if type == "string" then . else empty end' "${integrity_json}")" - alpha="$(jq -r '.alpha // empty' "${tags_json}")" + nightly="$(jq -r '.nightly // empty' "${tags_json}")" latest="$(jq -r '.latest // empty' "${tags_json}")" if [[ -n "${integrity}" && "${integrity}" != "${LFC_EXPECTED_INTEGRITY}" ]]; then echo "npm integrity conflicts with the verified release bundle." >&2 exit 1 fi if [[ "${integrity}" == "${LFC_EXPECTED_INTEGRITY}" && - "${alpha}" == "${LFC_VERSION}" && - "${latest}" == "${LFC_VERSION}" ]]; then + "${nightly}" == "${LFC_VERSION}" && + "${latest}" == "${LFC_LATEST_TARGET}" ]]; then verified=true break fi @@ -894,7 +1001,7 @@ jobs: done if [[ "${verified}" != "true" ]]; then echo "npm did not expose the exact package integrity and both required dist-tags within the polling window." >&2 - echo "Check for pending or blocked publish-time review before changing registry metadata. Advance latest only after the exact candidate is publicly readable with the expected integrity." >&2 + echo "Check for pending or blocked publish-time review before changing registry metadata. Resume this exact attempt after correcting the reported registry or tag-token failure." >&2 exit 1 fi @@ -919,6 +1026,8 @@ jobs: cd "${consumer}" node --input-type=module --eval ' await import("@tryagaindev/litefold-calendar"); + const { webMcp } = await import("@tryagaindev/litefold-calendar/extensions/webmcp"); + if (typeof webMcp !== "function") throw new Error("Missing published WebMCP extension factory."); const style = import.meta.resolve("@tryagaindev/litefold-calendar/styles.css"); if (!style.endsWith("/dist/styles.css")) throw new Error(`Unexpected style export ${style}`); ' @@ -981,11 +1090,11 @@ jobs: # LFC_GH_VERSION_POLICY_END # The exact workflow identity below binds the signed package to this reviewed source, - # whose sole OIDC publisher declares environment: npm. Protection is platform policy. + # whose sole OIDC publisher declares environment: npm-nightly. Protection is platform policy. gh attestation verify "${LFC_TARBALL}" \ --bundle provenance.sigstore.json \ --repo "${GITHUB_REPOSITORY}" \ - --cert-identity "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/.github/workflows/publish-alpha.yml@refs/heads/main" \ + --cert-identity "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/.github/workflows/publish-nightly.yml@refs/heads/main" \ --cert-oidc-issuer https://token.actions.githubusercontent.com \ --source-ref refs/heads/main \ --source-digest "${LFC_SOURCE_COMMIT}" \ @@ -1033,14 +1142,14 @@ jobs: const sourceCommit = process.env.LFC_SOURCE_COMMIT; const sourceRef = "refs/heads/main"; const eventName = process.env.GITHUB_EVENT_NAME; - const workflowPath = ".github/workflows/publish-alpha.yml"; + const workflowPath = ".github/workflows/publish-nightly.yml"; const workflowIdentity = `${repositoryUrl}/${workflowPath}@${sourceRef}`; const workflowRef = `${repository}/${workflowPath}@${sourceRef}`; requireValue(server === "https://github.com" && repository.toLowerCase() === "tryagaindev/litefold-calendar" && process.env.GITHUB_REPOSITORY_OWNER.toLowerCase() === "tryagaindev", "Current workflow is not the canonical release repository."); - requireValue(eventName === "push" && + requireValue(["schedule", "workflow_dispatch"].includes(eventName) && process.env.GITHUB_REF === sourceRef && process.env.GITHUB_SHA === sourceCommit && process.env.GITHUB_WORKFLOW_REF === workflowRef && @@ -1048,8 +1157,8 @@ jobs: "Current workflow identity does not match the release source."); requireValue(/^[0-9a-f]{40}$/u.test(sourceCommit), "Release provenance source commit must be a full lowercase SHA."); - requireValue(/^0\.[0-9]+\.[0-9]+-alpha\.[0-9]+$/u.test(process.env.LFC_VERSION), - "Release provenance version must be an exact supported alpha."); + requireValue(/^[0-9]+\.[0-9]+\.[0-9]+-nightly\.[0-9]{14}\.[0-9]+$/u.test(process.env.LFC_VERSION), + "Release provenance version must be an exact generated nightly."); const integrityMatch = /^sha512-([A-Za-z0-9+/]+={0,2})$/u.exec( process.env.LFC_EXPECTED_INTEGRITY @@ -1166,6 +1275,7 @@ jobs: LFC_NOTES_SHA256: ${{ needs.verify.outputs.notes-sha256 }} LFC_RELEASE_ID: ${{ needs.stage-release.outputs.release-id }} LFC_SOURCE_COMMIT: ${{ needs.verify.outputs.source-commit }} + LFC_BASE_VERSION: ${{ needs.verify.outputs.base-version }} LFC_TAG: ${{ needs.verify.outputs.tag }} run: | set -euo pipefail diff --git a/docs/example-deployment.md b/docs/example-deployment.md index cd4b740..4b430ac 100644 --- a/docs/example-deployment.md +++ b/docs/example-deployment.md @@ -1,6 +1,6 @@ # Static example deployment -This guide is for maintainers operating the GitHub Pages developer demo. Contributors validate example changes through the [examples contributor lane](../examples/README.md#contributors) and do not deploy them manually: successful CI updates the rolling `main` preview, and successful alpha publication triggers immutable release snapshots. +This guide is for maintainers operating the GitHub Pages developer demo. Contributors validate example changes through the [examples contributor lane](../examples/README.md#contributors) and do not deploy them manually: successful CI updates the rolling `main` preview, and successful nightly publication triggers immutable release snapshots. ## URL and identity contract @@ -9,7 +9,7 @@ This guide is for maintainers operating the GitHub Pages developer demo. Contrib | Rolling main preview | `main/examples/` | Replaced after successful CI for the exact `main` commit, or restored from retained deployment history | | Immutable release | `releases//examples/` | Added by a separate release operation for the exact protected package tag; different bytes can never replace it | -The Pages root selects the greatest SemVer release in `site-manifest.json` for its primary **Run** action. If no valid release exists, it falls back to the rolling preview and an `@alpha` install command. Release pages always show an exact version. +The Pages root selects the greatest SemVer release in `site-manifest.json` for its primary **Run** action. This site selection is independent of npm dist-tags; after a stable publication, a newer nightly may still be the greatest displayed release while npm `latest` remains stable. If no valid release exists, it falls back to the rolling preview and an `@nightly` install command. Release pages always show an exact version. Each landing page and recipe identifies its channel, package version, full source commit, and commit-pinned source links. Static navigation remains useful when JavaScript is disabled or metadata cannot be loaded. Browser enhancement reads only validated same-origin metadata with `no-store`. @@ -20,7 +20,7 @@ All deployed assets are repository-owned and same-origin. The demo has no analyt | Operation | Trigger | Maintainer or operator action | | --- | --- | --- | | Update rolling preview | Successful `CI` run for a push to `main` | None; inspect the resulting **Deploy static examples** run | -| Add release snapshot | Successful **Publish npm alpha** completion after npm and GitHub release verification | None; verify the publisher-linked **Deploy static examples** run | +| Add release snapshot | Successful **Publish npm nightly** completion after npm and GitHub release verification | None; verify the publisher-linked **Deploy static examples** run | | Retry release snapshot | Rerun the original publisher-linked **Deploy static examples** run only after a transient or hosted-state failure | Do not dispatch a release or supply a ref; if no downstream run exists, rerun the original publisher | | Restore rolling preview | Manual **Roll back static examples** dispatch from `main` | Set **Snapshot ref** to an exact retained commit | @@ -32,7 +32,7 @@ Before the first deployment, a maintainer with GitHub `Admin` access to the repository must: 1. Configure Pages to use **GitHub Actions** as its source. -2. Protect the `github-pages` environment independently from `npm` and allow deployments from `main` only. +2. Protect the `github-pages` environment independently from `npm-nightly` and `npm-nightly-tags` and allow deployments from `main` only. 3. Allow the workflow token to create and maintain `pages-content` while restricting direct and force pushes to that branch. 4. Keep `main`, `v*` tags, workflow files, and environment-rule changes under review. @@ -42,13 +42,13 @@ These settings are hosted state and cannot be proved by repository tests. Rechec ## Build and authority boundaries -Pages operations are split by trust boundary. `deploy-examples.yml` is `workflow_run`-only: successful same-repository `CI` and **Publish npm alpha** completions supply an exact `main` head commit for rolling and release channels. `rollback-examples.yml` is `workflow_dispatch`-only and can restore an exact retained `main/` snapshot. Neither workflow accepts a manual release ref. +Pages operations are split by trust boundary. `deploy-examples.yml` is `workflow_run`-only: successful same-repository `CI` and **Publish npm nightly** completions supply the exact verified source commit for rolling and release channels. Nightly triggers are scheduled or manually dispatched from `main`; CI-linked previews still require a push to `main`. `rollback-examples.yml` is `workflow_dispatch`-only and can restore an exact retained `main/` snapshot. Neither workflow accepts a manual release ref. -After npm verification and publication of the immutable GitHub prerelease, successful completion of `publish-alpha.yml` triggers Pages through GitHub's native `workflow_run` event. Pages verifies the canonical workflow name-and-path pair, same-repository push, full head commit, first-parent version change, protected tag, immutable prerelease, and main ancestry before building the release channel. npm/GitHub publication has no Pages authority, and a Pages failure does not make an already-published package version replaceable. +After npm verification and publication of the immutable GitHub prerelease, successful completion of `publish-nightly.yml` triggers Pages through GitHub's native `workflow_run` event. The publisher does not wait for Pages; the operator verifies the downstream run before recording delivery complete, and the next nightly preflight checks the preceding release Pages. Pages verifies the canonical workflow name-and-path pair, same-repository scheduled/manual publisher, full source commit, protected tag, immutable prerelease, and receipt asset digest before building the release channel. It derives the published nightly version from the original publisher run's UTC creation time and run ID, then checks that version and commit against receipt schema 2. The source manifest's development base is expected to differ from the published version; a first-parent version change is not a nightly prerequisite. npm/GitHub publication has no Pages authority, and a Pages failure does not make an already-published package version replaceable. Before an automatic Pages snapshot can be retained, repository tooling: -- Generates `examples/metadata.json` as deployment provenance from the package version, exact source commit, and channel. The file is ignored build output; never edit or commit it. +- Generates `examples/metadata.json` as deployment provenance from the exact source commit and channel. Release builds use the published version established by the publisher receipt; rolling previews use the source development version. The file is ignored build output; never edit or commit it. - Copies only the browser runtime files from `dist/` and `examples/`. - Injects the self-only Content Security Policy and developer provenance navigation into every staged page. - Rejects direct literal remote scripts, stylesheets, media, module imports, workers, sockets, beacons, and fetch targets. @@ -73,9 +73,10 @@ For a rolling preview: For a release: -- Confirm the Pages run was triggered by the successful **Publish npm alpha** run and used that run's exact full head commit. Do not confuse it with the CI-linked rolling-main run that may have the same commit. -- Confirm `releases//examples/metadata.json` reports the exact version, `release` channel, and the full commit from `package-verification.json` and the protected tag. -- Confirm `site-manifest.json` maps that version to the same immutable directory. +- Confirm the Pages run was triggered by the successful **Publish npm nightly** run and used that run's exact full head commit. Do not confuse it with the CI-linked rolling-main run that may have the same commit. +- Confirm the retained receipt's `version`, `sourceCommit`, original publisher run identity, and digest match the immutable release asset. For nightly receipts, also verify `sourceVersion` and the version-only manifest transformation. +- Confirm `releases//examples/metadata.json` reports the published version, `release` channel, and the full commit from `package-verification.json` and the protected tag. A nightly deployment must not accidentally use the source `nightly.0` development base as its directory or displayed version. +- Confirm `site-manifest.json` maps that version to the same immutable directory. Existing alpha and nightly directories remain intact. - Open the release landing page and one recipe deep link; confirm the visible provenance and source links match. A rolling mismatch is expected only during an intentional rollback or while a newer deployment is completing. Any other missing or inconsistent identity requires investigation. @@ -112,7 +113,7 @@ An automatic deployment never moves the retained preview backward or onto diverg If release metadata or a deep link is missing, first compare the upstream publisher run and head SHA, protected tag, GitHub prerelease, package receipt, retained `pages-content` state, and live site. Do not copy files into `pages-content` or edit the site manually. -Rerun the original publisher-linked **Deploy static examples** run only when all existing identities and bytes match and the failure was transient or caused by hosted state. That rerun keeps the original commit SHA, ref, and workflow definition; it cannot consume later changes. If GitHub never created the downstream run, review the current default-branch `deploy-examples.yml`, then select **Re-run all jobs** on the successful original **Publish npm alpha** run. The publisher retains its original identity, while the new downstream run uses the current Pages workflow definition and still pins release source and assembly tooling to the publisher SHA. Validate that new run independently. Never use **Roll back static examples** for a release. If release source, assembly tooling, or published example bytes must change, stop and publish a greater package version. +Rerun the original publisher-linked **Deploy static examples** run only when all existing identities and bytes match and the failure was transient or caused by hosted state. That rerun keeps the original commit SHA, ref, and workflow definition; it cannot consume later changes. If GitHub never created the downstream run, review the current default-branch `deploy-examples.yml`, then select **Re-run all jobs** on the successful original **Publish npm nightly** run. The publisher retains its original identity, while the new downstream run uses the current Pages workflow definition and still pins release source and assembly tooling to the publisher SHA. Validate that new run independently. Never use **Roll back static examples** for a release. If release source, assembly tooling, or published example bytes must change, stop and publish a greater package version. ## Verification diff --git a/docs/package-verification.md b/docs/package-verification.md index b71485b..5ece786 100644 --- a/docs/package-verification.md +++ b/docs/package-verification.md @@ -1,6 +1,6 @@ # Package verification -Use this guide to verify release state, run the same package gate as CI, or inspect a published package. The automated release builds one tarball for the exact eligible `main` push and retains it through publication; the npm-authorized job does not rebuild it. npm creates signatures and provenance during publication, and the workflow verifies both before making the GitHub prerelease public. +Use this guide to verify release state, run the same package gate as CI, or inspect a published package. The automated release builds one tarball for the exact green `main` snapshot and retains it through publication; the npm-authorized job does not rebuild it. npm creates signatures and provenance during publication, and the workflow verifies both before making the GitHub prerelease public. ## Audience routes @@ -38,22 +38,30 @@ Choose the narrowest command that answers your question: | Audience and goal | Command | Scope | | --- | --- | --- | -| Release operator: validate prepared version files | `npm run release:verify` | Local manifests, changelog, repository identity, commit, and local tag state | +| Release operator: validate the snapshot plan | `npm run release:verify -- --plan PATH` | Immutable run identity, source manifest, exact commit, and clean source | | Contributor: run the complete repository gate used by CI | `npm run check` | Static checks, unit and browser tests, screenshots, build, and temporary tarball verification | -| Release operator: retain a local evidence bundle | `npm run package` | The verified tarball and its write-once release bundle under `.artifacts/` | +| Release operator: retain a nightly evidence bundle | `npm run package:nightly -- --plan PATH` | The stamped tarball and its write-once release bundle under `.artifacts/` | | Package user or contributor: preview local npm file selection | `npm pack --dry-run --ignore-scripts` | Local checkout contents only; it is not a substitute for `check:tarball` | npm always includes the root `README.md`, but canonical PNGs remain outside the package's `files` allowlist. A dry run should therefore list `README.md` and no `docs/screenshots/**` entries. This keeps installed package size independent of the screenshot gallery; `check:screenshots` verifies the repository assets separately. Tarball checks cannot prove hosted README rendering, so the [release operations checklist](release-operations.md#6-verify-npm-and-the-github-prerelease) owns the npm package-page check. ## Verify release state -Run the read-only local release-state check after preparing a release or when diagnosing the preparation workflow: +Nightly publication creates a plan from the workflow run's original creation time, +run ID, event, exact source SHA, and `x.y.z-nightly.0` source version. An artifact +version such as `0.6.0-nightly.20260912090000.123456789` is derived from those +immutable inputs. Reruns retain the original identity. ```sh -npm run release:verify +npm run release:verify -- --plan PATH ``` -The check validates that the package is configured as a public alpha, both lockfile version fields match, the changelog is release-ready, the Git origin is canonical, and a local tag (if present) resolves to the current commit. It does not query npm or GitHub. The publication workflow adds the remote collision checks and requires the release commit's first-parent diff to contain only `CHANGELOG.md`, `package-lock.json`, and `package.json`. +Replace `PATH` with the retained nightly plan. This read-only command validates +its schema and derived identity, the manifest's public nightly policy, exact +`HEAD`, and a clean tracked and untracked worktree. It does not query GitHub or +npm. The workflow separately verifies a successful canonical `main` CI run, +fresh registry state, predecessor completion, and current source eligibility. +No release-only source commit or metadata pull request is needed. ## Complete repository gate @@ -81,7 +89,7 @@ For intentional visual changes, follow the [screenshot update procedure](screens ## Retain a local verification bundle ```sh -npm run package +npm run package:nightly -- --plan PATH ``` From a clean worktree, the command creates a write-once directory named `.artifacts/tryagaindev-litefold-calendar-/` containing exactly: @@ -92,7 +100,28 @@ From a clean worktree, the command creates a write-once directory named `.artifa - `SHA256SUMS`. - The packaged `LICENSE`. -The command refuses to overwrite an existing version directory. Do not delete an existing bundle merely to produce different bytes for the same version; increment the prerelease version instead. During an automated release, these files are attached to the draft GitHub prerelease before it becomes immutable. +The command refuses to overwrite an existing version directory. The source +checkout keeps its development version; staging changes only the package +manifest version before packing. The nightly receipt uses schema 2 and retains +`sourceVersion`, the validated `nightly` plan, `manifestTransform: "version-only"`, +and SHA-256 digests of the source and published manifests. `browserTargets` +retains Vite's `baseline-widely-available` preset, versioned `effectiveQuery`, +`resolvedAt`, locked Vite/esbuild `dataVersions`, selected `browsers`, and the +JavaScript/CSS targets returned by Vite's public configuration resolver. +`browserTargetsSha256` hashes its JSON serialization. The receipt records the +original run's UTC creation date, including on reruns; target versions come from +the locked Vite preset. The SBOM and tarball +use the derived published version. `npm run package` retains the ordinary source +version bundle for an explicitly requested local evidence operation. + +Do not delete an existing bundle to produce different bytes for the same +version. Recover an interrupted workflow using the original run and retained +bundle plus notes. The workflow restores its earliest complete artifact pair, +checks archive digests, receipt identity and checksums, and bypasses rebuilding. +Missing or expired evidence with staged or published state stops recovery. +A source correction requires a new reviewed identity. During an automated +release, these files are attached to the draft GitHub prerelease before it +becomes immutable. The SPDX document is canonicalized after npm generates and validates it. Its namespace uses the package version and exact source commit, its timestamp uses the commit's committer time, and its keys have a fixed order. With the pinned toolchain, the same clean commit therefore produces byte-identical `sbom.spdx.json` content. @@ -120,7 +149,18 @@ Tree shaking and package contents answer different questions. The extension file ## Registry and release evidence -The publisher retries npm's eventually consistent reads and verifies the exact version, matching `alpha` and `latest` dist-tags, registry integrity, provenance/signatures, clean installation, root import, documented extension import, and stylesheet import before publishing the GitHub prerelease. Successful publisher completion triggers the separately authorized immutable Pages deployment through a same-repository `workflow_run` event bound to the same exact commit. +The publisher retries npm's eventually consistent reads and verifies the exact +version, `nightly` tag, registry integrity, provenance/signatures, clean +installation, public imports, and stylesheet before publishing the GitHub +prerelease. A separate job advances `latest` to the nightly while no stable +version exists. Once stable exists, verification requires stable ownership of +`latest` and nightlies leave it alone. Historical `alpha` remains frozen. + +Successful publisher completion triggers the separately authorized immutable +Pages deployment through a same-repository `workflow_run`. Pages verifies the +published receipt against the exact source SHA, original run identity, generated +version, and GitHub asset digest before building that release directory. The +publisher does not wait for Pages; operators verify both completed workflows. Inspect an exact published version rather than relying only on movable dist-tags: @@ -133,7 +173,7 @@ Replace `EXACT_VERSION` with the release being checked. Confirm that: - The returned name and version are exact. - `dist.integrity` equals `npmIntegrity` in the GitHub prerelease's `package-verification.json`. -- Both `alpha` and `latest` point to that exact prerelease until the first stable release replaces the temporary channel policy. -- The npm package page shows provenance for the expected repository and `publish-alpha.yml` workflow. +- `nightly` selects that exact version. Before stable exists, `latest` matches it; afterward `latest` selects a stable version. Historical `alpha` has not moved. +- The npm package page shows provenance for the expected repository and `publish-nightly.yml` workflow. The workflow installs the exact version into a clean consumer before running `npm audit signatures`. Running that command in the repository would verify the development dependency tree instead. If a registry read is unavailable, ambiguous, or inconsistent with the retained bundle, stop and use the [recovery matrix](release-administration.md#recovery-matrix). diff --git a/docs/release-administration.md b/docs/release-administration.md index 5a8d7c8..e255a52 100644 --- a/docs/release-administration.md +++ b/docs/release-administration.md @@ -1,205 +1,139 @@ -# Alpha release administration and recovery +# Release administration and recovery -This guide is for maintainers who manage GitHub and npm release controls. It owns -hosted controls, unsupported transitions, exceptional registry actions, reruns, -and the recovery matrix. Release operators use the -[alpha release operations runbook](release-operations.md) for the normal ordered -procedure and exact channel command. +This guide is for maintainers who configure GitHub and npm release controls. The [nightly operations runbook](release-operations.md) owns the normal ordered procedure. This document owns environment setup, credential rotation, immutable-state recovery, and requirements for a future stable publisher. ## One-time hosted prerequisites -Verify these settings before enabling publication and after any owner, -repository, workflow filename, environment, package-ownership, or hosted-policy -change. +Verify hosted settings before enabling the nightly schedule and after changes to ownership, repository, workflow names, environments, package permissions, or policy. Repository tests check configuration contracts; they cannot prove that hosted controls are configured correctly. Record the verifier, UTC time, and sanitized evidence. ### GitHub organization -- Require two-factor authentication for members and outside collaborators. - Grant organization-owner access only where it is needed for policy management - and account recovery. -- Keep base repository permission at **None** and grant repository access - explicitly. -- Permit only approved GitHub-owned Actions and reusable workflows, require full - commit-SHA pinning, keep the default `GITHUB_TOKEN` read-only, and prevent - workflows from creating or approving pull requests. -- Restrict classic personal access tokens, require approval and expiration for - fine-grained tokens, and review OAuth application access. +Require two-factor authentication and grant repository and organization administration only where needed. Keep default repository permission and `GITHUB_TOKEN` access minimal. Allow approved Actions pinned to full commit SHAs, review third-party access, and require expiration for credentials. Keep pull-request review authority separate from workflow write permissions. + +### GitHub repository and release + +- Protect `main` with reviewed pull requests, resolved conversations, linear history, deletion/force-push protection, and required CI. Require **Build, test, and verify package** and both **Qualify Firefox** jobs. Require code-owner and independent review where another eligible maintainer exists. +- Configure `npm-nightly` and `npm-nightly-tags` as separate main-only environments. Neither uses a required reviewer or wait timer: scheduled and manually requested nightlies must complete unattended. Review changes to those environments and their workflow paths. +- Keep `npm-nightly` free of registry tokens. Store the tag credential only in `npm-nightly-tags`. +- Protect `v*` tags against updates and deletion. Permit creation only for the narrowest available actor; GitHub Actions credentials identify an application, not a single workflow. Review every workflow with repository-write authority. +- Enable [GitHub immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases). Stage assets on the draft, then publish. Audit titles and notes against retained evidence because platform immutability protects tags and assets, not every metadata field. +- Keep dependency monitoring, code scanning, secret scanning, push protection, and private vulnerability reporting enabled where available. Resolve or disposition release-blocking findings. ### npm -- Confirm the public repository identity exactly matches - `package.json#repository`. -- Confirm that only the intended accounts can administer the `@tryagaindev` - scope or maintain the package, and require two-factor authentication for those - accounts. -- Bind [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) to - the exact GitHub owner, repository, `publish-alpha.yml` workflow, `npm` - environment, and the **npm publish** allowed action only. Keep the job on a - GitHub-hosted runner with `id-token: write`. -- Disallow long-lived publication tokens. Once trusted publishing is active, - require two-factor authentication and disallow tokens for direct package - publication. +npm supports multiple independent trusted-publisher configurations per package. They are additive: matching any configuration authorizes its permitted operation. Every configuration allows staging by default; direct publication requires a separate opt-in on that configuration. [npm's September 2026 publisher update](https://github.blog/changelog/2026-09-03-multiple-trusted-publishing-configurations-for-npm/) -### GitHub repository and release +Bind [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) to the exact owner `tryagaindev`, repository `litefold-calendar`, workflow filename `publish-nightly.yml`, and environment `npm-nightly`. **Enable direct publication for this nightly binding** so the workflow's `npm publish` completes without staged-release approval. Verify that setting explicitly; adding the binding with its staging-only default does not satisfy unattended delivery. The publisher uses a GitHub-hosted runner and receives `id-token: write`. -- Protect the `npm` environment with required reviewers, block administrator - bypass, and restrict it to `main`. When multiple eligible people can serve as - required reviewers, prevent self-review and require an independent approval. - While the project has one maintainer, do not enable prevent self-review when it - would deadlock publication; record that hosted decision. -- Protect `main` with pull requests, the current **Build, test, and verify - package** check, resolved conversations, CodeQL merge protection, linear - history, and blocks on deletion and force pushes. Require independent approval - and code-owner review whenever another eligible maintainer is available. -- Protect `v*` tags with a repository ruleset that blocks updates and deletion - and grants creation to the narrowest available actor. The workflow creates - and verifies an exact-SHA tag, but its `GITHUB_TOKEN` identifies the installed - GitHub Actions application rather than one specific workflow. Treat the tag - ruleset as defense in depth, keep unrelated workflows from receiving tag-write - authority, and verify the effective bypass actors directly. -- Enable only squash merging, using the pull-request title and description. -- Enable - [GitHub immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases). - Attach artifacts while the prerelease is a draft because published tags and - assets cannot be replaced. Release titles and notes remain editable on the - platform; restrict write access and audit them against retained evidence. -- Enable dependency graph, Dependabot alerts and security updates, CodeQL, - provider-pattern secret scanning, push protection, and private vulnerability - reporting. Verify every repository-level feature directly; an organization - default is not evidence that the repository still has the intended setting. +Adding the nightly binding does not revoke the alpha binding. Confirm no historical alpha publication is in flight, then remove the old alpha binding before enabling the nightly schedule. Preserve historical package versions, tags, releases, and evidence. -### GitHub Pages +Maintain two-factor authentication for package maintainers and verify scope/package ownership. The isolated `latest` update requires a granular write token because the adopted npm workflow does not use OIDC for secondary dist-tag updates. This intentionally replaces the previous package setting that disallowed tokens. Select a publishing access policy that continues to require human 2FA while allowing the configured granular token; verify the effective policy in npm. + +Create or delete tokens and change package access, maintainers, or trusted-publisher settings through a private interactive npm session with a 2FA challenge. A bypass-2FA token cannot perform those management operations. This applies to initial setup and subsequent credential rotations. [npm's July 2026 token-management change](https://github.blog/changelog/2026-07-31-restricting-npm-bypass-2fa-granular-access-tokens/) -- Use **GitHub Actions** as the Pages source. -- Protect the `github-pages` environment independently from npm authority, - block administrator bypass, and restrict deployment to `main`. -- Protect the retained `pages-content` branch from deletion and - non-fast-forward updates. Only the verified retained-state workflow advances - it. -- Keep automatic and rollback Pages workflows in the same non-canceling, - maximum-queue workflow-level concurrency group. +Provision the credential privately through npm's [granular token settings](https://docs.npmjs.com/creating-and-viewing-access-tokens/): + +1. Give it **Read and write (publish and stage)** access to **only** `@tryagaindev/litefold-calendar`. Do not grant the entire scope or organization-management permissions. +2. Enable the 2FA bypass needed for unattended package metadata writes. This grants broader package-write authority than dist-tags alone; keep it isolated even though the workflow uses it only for `npm dist-tag add`. +3. Set expiry to **30 days**. Store its exact UTC expiry as repository variable `NPM_NIGHTLY_TAG_TOKEN_EXPIRES_AT`, formatted `YYYY-MM-DDTHH:mm:ssZ`. +4. Store the value as environment secret `NPM_NIGHTLY_TAG_TOKEN` in `npm-nightly-tags`. Do not paste it into a task, source file, workflow input, or release note. +5. Verify environment branch restrictions and that only the final tag-writing step receives the credential. Tool installation and public registry validation run before that step. + +Do not introduce the token into the OIDC publish job, source checkout, project scripts, package installation, or Pages. The workflow's temporary npm authentication file is removed after the tag command. Keep a private inventory of token owner, selected package, creation/expiry, and rotation verifier without storing the value there. + +npm targets January 2027 for removing direct publication from bypass-2FA granular tokens. Package bytes in this workflow already publish through OIDC; the isolated token remains the secondary-dist-tag workaround tracked in [npm CLI issue 8547](https://github.com/npm/cli/issues/8547). Replace that workaround when supported OIDC tag updates are available. [Announced token publication change](https://github.blog/changelog/2026-07-31-restricting-npm-bypass-2fa-granular-access-tokens/) + +### GitHub Pages -Repository files and tests describe these requirements but cannot prove hosted -settings. Verify them in GitHub and npm, then record the verifier, UTC time, and -sanitized evidence in the private release record. +Use **GitHub Actions** as the Pages source. Keep `github-pages` separate from npm authority and main-only. Protect retained `pages-content` history against deletion and non-fast-forward writes; only the verified retained-state workflow advances it. Automatic deployment and manual rollback share the non-canceling Pages queue. See [example deployment](example-deployment.md). ## Publication authority -The workflow separates source execution from public write authority: +| Stage | Source execution and permissions | +| --- | --- | +| Classify and verify | Exact source checkout, read-only repository access, same-commit green CI, full gate, registry predecessor and prior Pages checks | +| Stage release | No checkout; repository write access to verified tag, draft, notes, and assets | +| Publish npm | No checkout; `npm-nightly` OIDC publishes only checksum-verified retained bytes | +| Synchronize channels | No checkout; `npm-nightly-tags` exposes the granular token only after candidate and stable-ownership checks | +| Verify and finalize | Clean public-package verification, then source-free GitHub finalization | +| Deploy Pages | Publisher-linked receipt verification; separate build, retained-state, and Pages permissions | -| Phase | Source execution | Authority and output | -| --- | --- | --- | -| Classify and verify | Exact pushed commit; complete repository gate | Read-only repository access; retains the package bundle and release notes | -| Stage GitHub release | No checkout or project-code execution | Repository write access; creates or validates the exact tag, draft prerelease, notes, and assets | -| Publish npm | No checkout or candidate-code execution | Protected `npm` environment and npm OIDC; publishes only the checksum-verified tarball under `alpha` | -| Synchronize npm channels | No workflow credential | An authenticated npm package maintainer advances `latest` to the exact verified alpha | -| Verify and finalize | Clean public-package consumer, then a source-free release job | Verifies registry identity, imports, signatures, provenance, tag, release, and retained bytes before the native Pages handoff | +The workflow runs on schedule or manual dispatch from canonical `main`. A new publication requires current main and successful exact-source CI. Recovery of an already published candidate may use the original attempt after main advances only when its retained and public identities still match; this exception cannot authorize a fresh upload from old source. -`publish-alpha.yml` starts only on pushes to `main`. Only a changed alpha -version whose first-parent diff is limited to `package.json`, -`package-lock.json`, and `CHANGELOG.md` enters publication. There is no -manual, arbitrary-ref, or non-current-commit publication path. +The non-canceling `npm-nightly-` concurrency group covers publication, tags, and finalization. Any future stable workflow must use the same group with `queue: max` and `cancel-in-progress: false`. Source-free GitHub mutation commands provide the repository explicitly. Do not add a checkout to infer context. -Release Pages start only through the successful publisher's native -`workflow_run` handoff. Manual rolling-preview recovery remains isolated in -`rollback-examples.yml`. See -[static example deployment](example-deployment.md) for that authority boundary. + -Source-free release jobs name the repository explicitly for every GitHub CLI -mutation. Do not add a checkout merely to infer repository context. +## Nightly channel policy -## Temporary alpha channel policy +Before a stable version exists, completed delivery requires `nightly` and `latest` to select the same nightly. Once any stable version exists, nightly automation must preserve stable ownership of `latest`. A missing or malformed registry response is never interpreted as an empty package or as proof that stable does not exist. -Until a reviewed stable-release procedure replaces it, a completed alpha release -requires npm `alpha` and `latest` to select the same exact prerelease. -Registry reads must be fresh and well-formed; a missing package, failed response, -or malformed response is not an empty tag set. +The historical `alpha` dist-tag, versions, protected tags, immutable releases, and release Pages stay frozen. The first nightly can use the completed alpha as its predecessor. It does not rewrite alpha metadata or republish an alpha. -The npm package maintainer performs authentication and channel mutation privately. Automation -must never receive the browser session, authentication URL, password, token, -one-time code, or recovery code. Follow -[the operations channel step](release-operations.md#5-advance-the-temporary-npm-latest-tag) -for the sole normal command sequence. +## Tag-token rotation -## Unsupported transitions +Before stable exists, verification checks the expiry variable before public writes and emits a warning when seven days or fewer remain. A missing, malformed, or expired timestamp blocks publication. The variable is an operator-recorded fact; it does not independently prove that the token is valid or unrevoked. + +1. In a private interactive npm session, complete the required 2FA challenge and create a replacement with the same package-only access and 30-day expiry. +2. Replace `npm-nightly-tags` secret `NPM_NIGHTLY_TAG_TOKEN` and repository variable `NPM_NIGHTLY_TAG_TOKEN_EXPIRES_AT` together. Verify the timestamp against npm. +3. Resume the original failed attempt if rotation repairs a transient authentication failure; otherwise let the next eligible nightly use it. +4. Verify the exact channel update, then revoke the old token through npm's interactive 2FA flow. Record rotation evidence without credential material. -Stop and design a separately reviewed procedure before any of these transitions: - -- the package's first public publication or trusted-publisher bootstrap; -- the first stable release; -- replacing the temporary `alpha`/`latest` channel policy; -- changing the trusted publisher's allowed action or adopting npm staged - publishing; -- unpublishing a version; -- moving, recreating, deleting, or replacing an immutable tag, release, asset, - or release Pages path; or -- publishing from a ref or commit outside the protected normal path. - -The new procedure must define authorization, exact identities, audit evidence, -credential boundaries, rollback or irreversibility, workflow changes, and -independent verification before execution. Do not consume a candidate already -prepared for the normal automated path. - -Deprecating a defective published version is a supported exceptional metadata -action only when an authorized npm package maintainer has verified the exact version, -replacement guidance, and private release record. Use the dedicated section -below. Rolling `main` Pages rollback is also supported, but only through the -[deployment rollback procedure](example-deployment.md#roll-back-the-rolling-preview). +If a token is compromised, revoke it immediately and investigate package versions and tags before resuming. Do not wait for the normal overlap. Once stable owns `latest`, the nightly tag-writing step no longer needs this token; retire it after confirming the stable workflow's separate authority. ## Rerun procedure -A rerun uses the original run's commit, ref, and workflow definition. It may -consume corrected transient infrastructure, authentication, or hosted -configuration, but it cannot consume source or publisher-workflow changes merged -later. - -1. Open the original **Publish npm alpha** run for the recorded release commit. -2. Confirm its full commit, version, retained bundle, and any npm, tag, draft, - release, notes, or asset state match exactly. -3. Confirm no source or publisher-workflow bytes changed to enable recovery. -4. Select the rerun scope from the recorded job state: - - Use **Re-run failed jobs** when publication did not occur, or when the - `publish` job succeeded and only registry verification, release - finalization, or the native downstream handoff failed. - - Use **Re-run all jobs** when npm accepted the candidate but the `publish` - job itself failed. The `verify` job must rebuild the registry snapshot - before `publish` can safely recognize the existing candidate. - - Also use **Re-run all jobs** when a missing native downstream event must be - emitted again. -5. Independently verify npm, GitHub, and the publisher-linked Pages run after - completion. - -A newly emitted Pages run uses the current default-branch Pages workflow while -pinning release source and assembly tooling to the publisher commit. Review that -workflow and validate the new run independently. If the original run is no longer -rerunnable or any identity cannot be proved, do not recreate the attempt from a -different commit. +A rerun keeps the original run ID, creation time, source commit, workflow definition, and nightly version. `run_attempt` may increase; it does not change the package identity. Later source changes do not become part of that attempt. + +1. Open the original **Publish npm nightly** run. Record failed and successful jobs and the exact version. +2. Compare its receipt, original source, retained tarball, checksum list, any draft/release, and public npm integrity. Classify the state using the matrix below. +3. Use **Re-run failed jobs** when upstream verification and retained evidence remain valid and a failed downstream step can resume them. If registry state changed because npm accepted the candidate, use **Re-run all jobs** to reclassify the existing candidate and validate its retained/public bytes before resuming. +4. Confirm that an existing publication is reused with identical bytes. Do not rebuild and republish the same version. Missing or expired evidence requires investigation, not a guessed reconstruction. +5. Verify package, channels, immutable release, and the exact publisher-linked Pages run after recovery. + +A rerun can consume repaired hosted configuration or credentials. A source or publisher-code repair requires a new reviewed commit and new nightly identity. Do not rerun an old workflow expecting it to load a newer implementation. ## Recovery matrix | Observed state | Required action | | --- | --- | -| Preparation reports an existing release branch or pull request | Record the branch head and pull request, then compare their exact three-file state, base commit, candidate, and review status with the requested preparation. Resume only an exact still-valid match. Otherwise preserve any unrelated work; after explicit authorization, a maintainer with the required GitHub access records the old head, closes the stale pull request, removes only the stale unmerged release branch, and reruns preparation from current `main`. | -| Failure before npm publication | Resolve only a transient infrastructure, authentication, or hosted-configuration problem, then rerun the original exact attempt. Matching staged state may be reused. If source or publisher-workflow bytes must change, prepare a greater alpha. | -| npm accepts the upload but the version remains unavailable after registry polling | Treat the version as pending or blocked by publish-time review. Do not republish, reuse the version, or prepare a replacement while registry state is ambiguous. Check npm status and private npm package maintainer notifications; appeal a block through npm when offered. Once the exact expected integrity is publicly readable, rerun the original attempt. | -| npm accepted identical verified bytes but the run ended afterward | If the `publish` job failed after npm accepted the candidate, use **Re-run all jobs** so `verify` rebuilds the registry snapshot before `publish` recognizes the existing candidate. If `publish` succeeded and only a downstream job failed, use **Re-run failed jobs**. Preserve the original event identity, artifact, and provenance. If exact resumption cannot be proved, stop and prepare a greater alpha; deprecate the incomplete version when appropriate. | -| npm accepted the candidate under `alpha`, but `latest` selects another version | Confirm candidate identity and retained integrity, perform the operations runbook's exact channel step, then rerun the original failed jobs. | -| npm contains the version with different or unverifiable bytes | Stop. Do not reuse or overwrite the version. Investigate, deprecate when appropriate, and prepare a greater alpha. | -| Tag, draft, release, notes, or assets exist with matching identity and bytes | Rerun the original exact attempt; the matching stage is idempotently reusable. | -| Tag, draft, release, notes, or assets conflict or cannot be proved | Stop. Do not move, delete, replace, or reuse them. Investigate and prepare a greater alpha. | -| Published package or immutable prerelease is defective | Preserve immutable state, deprecate the package version when appropriate, and publish a corrected greater alpha. | -| Publisher-linked release Pages run failed | Rerun that exact Pages run only for a transient or hosted-state problem. If release source or assembly tooling must change, publish a corrected greater alpha. | -| No publisher-linked release Pages run exists | Confirm the original publisher succeeded, review the current Pages workflow, rerun all jobs on the original publisher, and validate the newly emitted Pages run against the recorded release commit. | -| Rolling `main` Pages preview must move backward | Use **Roll back static examples** with the exact retained snapshot commit defined by the deployment guide. Never use it to create or replace release Pages. | - -Authentication, network, malformed responses, missing provenance, and ambiguous -or conflicting public state fail closed. +| Source is unchanged and previous nightly completed through Pages | Verify the reported skip. No new version is expected. | +| Candidate was never published and has no partial public state | Repair transient infrastructure and rerun the original attempt only while its source remains current main, or leave it abandoned and start a new eligible current-main run. Each new run has a different version. | +| Candidate has a matching staged tag/draft/assets but no npm version, and its source remains current main | Preserve the stage and resume its exact attempt; do not collide with or overwrite the staged identity. | +| Candidate has a matching staged tag/draft/assets but no npm version, and main has advanced | Confirm no upload was accepted or remains pending. Preserve the abandoned staged identity and retained evidence, then start a new eligible current-main run with a new version. The old attempt cannot publish from stale source; do not move or overwrite its tag, draft, or assets. | +| npm accepted the upload but the exact version is not yet publicly readable | Treat it as pending, blocked, or ambiguous. Check registry status and private package-maintainer notifications. Do not reuse the version or publish another candidate to bypass uncertainty. | +| Exact npm version and integrity exist; channel synchronization or finalization failed | Restore transient credentials/configuration, then resume the original attempt with the retained bytes. Reclassify changed registry state through a full rerun when necessary. | +| Tag token is expired or revoked | Rotate the isolated token and expiry variable, verify package state, then resume the original affected attempt. | +| Stable version exists but `latest` selects a prerelease | Stop. Stable owns `latest`; investigate and use the approved stable recovery procedure. Nightly must not repair this by pointing `latest` to itself. | +| npm version, tag, draft, notes, assets, or receipt conflict | Stop and investigate. Do not overwrite, delete, or reuse the identity. A reviewed correction receives a new greater version. | +| Published package or example bytes are defective | Preserve immutable objects. Publish a corrected greater nightly and consider authorized deprecation of the defective version. | +| Publisher succeeded but its Pages run failed | Rerun the exact publisher-linked Pages run for a transient or hosted-state failure. A source or assembly-code correction needs a new reviewed version. | +| Successful publisher has no downstream Pages run | Verify the current Pages workflow, rerun the original publisher to emit its native completion event, and verify the new downstream run against the original receipt/source. | +| Rolling main preview needs to move backward | Use [rolling-preview rollback](example-deployment.md#roll-back-the-rolling-preview). It does not modify npm or any release snapshot. | +| Historical alpha requires investigation | Read the run's pinned source and retained schema-1 evidence under the historical policy. Preserve frozen public state; migration does not authorize republishing alpha or moving its tags. | + +## Future stable release + +Stable publication is not enabled by the nightly workflow. Before the first stable release, review and merge a stable workflow and validator changes implementing these requirements: + +- Select an explicit stable SemVer through maintainer review. Update source manifest and root lockfile versions consistently, and promote reviewed `[Unreleased]` notes into the dated stable changelog entry. +- Require protected main, exact-source green CI, the full gate, and a separate stable publication environment with the intended human approval policy. Configure its exact npm trusted publisher binding; do not grant the nightly token stable-publication responsibility. +- Use the existing publication queue across stable and nightly jobs. Publish one verified retained bundle with matching SBOM, integrity, source, provenance, protected tag, and immutable release evidence. +- Publish stable under `latest` and verify it before allowing subsequent nightlies. From that first stable onward, nightly automation leaves `latest` under stable ownership even if a later nightly fails. +- Advance the development base beyond the stable series in a reviewed follow-up before the next nightly. Keep `nightly`, `alpha`, and every existing release snapshot intact. +- Define recovery before enabling dispatch: exact-byte resumption for a partial stable attempt, no version reuse or tag movement, corrected greater stable releases for defects, and receipt-bound release Pages verification. + +These are prerequisites for a future stable implementation, not authorization or commands to publish stable now. + +## Unsupported transitions + +Separately review first-package bootstrap, staged publishing, owner/repository transfers, publication outside protected main, unpublishing, or any proposal to move/delete/replace immutable release state. Define exact identities, authority, evidence, credential boundaries, and recovery before changing the normal path. + +Deprecation is a supported exceptional metadata action after authorization for the exact version and replacement message. It does not erase or replace a release. ## Exceptional registry metadata -An authenticated npm package maintainer may deprecate an exact defective version after a -replacement has been selected. Replace every uppercase placeholder and inspect -both versions before mutating registry metadata: +An authorized npm package maintainer may deprecate a defective exact version after inspecting it and the selected replacement. Replace the uppercase placeholders before running: ```sh npm view "@tryagaindev/litefold-calendar@EXACT_VERSION" name version deprecated dist.integrity --json --registry https://registry.npmjs.org/ @@ -208,9 +142,6 @@ npm deprecate "@tryagaindev/litefold-calendar@EXACT_VERSION" "Use REPLACEMENT_VE npm view "@tryagaindev/litefold-calendar@EXACT_VERSION" name version deprecated --json --registry https://registry.npmjs.org/ ``` -The mutation requires explicit authorization immediately before -`npm deprecate`. Record the exact identities, reason, npm package maintainer who -performed the action, UTC time, command result, and sanitized recovery evidence -in the private release record. Public -communication follows the normal changelog, advisory, or release-note decision; -do not expose credentials or private vulnerability details. +Record the exact identities, reason, authorization, operator, UTC time, and readback. Existing authorization for ordinary nightly delivery does not include arbitrary deprecation. Keep private vulnerability details and credentials out of public metadata. + +[Back to the documentation hub](README.md) diff --git a/docs/release-operations.md b/docs/release-operations.md index f894147..20b08d2 100644 --- a/docs/release-operations.md +++ b/docs/release-operations.md @@ -1,270 +1,156 @@ -# Alpha release operations runbook +# Nightly release operations runbook -Follow this runbook to publish `@tryagaindev/litefold-calendar` versions shaped like `0.x.y-alpha.N`. It assumes the operator is new to this repository but can follow exact instructions. The first stable release needs a separate procedure because npm's temporary alpha-as-`latest` rule must end first. +Use this runbook to publish and verify a nightly from reviewed `main`. Daily publication runs at **09:00 UTC**; use manual dispatch when a completed change needs publication immediately. The publisher derives the version, publishes through OIDC, updates the temporary `latest` channel, and hands off release Pages automatically. -The operator runs **Prepare alpha release**, reviews and merges the resulting three-file pull request, reviews the staged draft, coordinates protected npm approval, advances `latest`, and verifies npm, the immutable GitHub prerelease, and automatic Pages deployment. The merge push is the only publication trigger. +There is no release-preparation pull request or manual npm approval for a normal nightly. This guide also explains how to recognize a successful unchanged-source skip. Stable publication is a future procedure described in [release administration](release-administration.md#future-stable-release); these commands do not publish stable. -The five GitHub Actions workflows used here are: +The relevant Actions workflows are: -- **Prepare alpha release** (`prepare-alpha.yml`) -- **CI** (`ci.yml`) -- **Publish npm alpha** (`publish-alpha.yml`) -- **Deploy static examples** (`deploy-examples.yml`) -- **Roll back static examples** (`rollback-examples.yml`; recovery only) - -Read the [release policy](releasing.md) and confirm the [hosted prerequisites](release-administration.md#one-time-hosted-prerequisites) before operating a release. +| Workflow | Purpose | +| --- | --- | +| **CI** (`ci.yml`) | Verify the exact `main` commit, including browser qualification | +| **Publish npm nightly** (`publish-nightly.yml`) | Scheduled or manually requested nightly snapshot | +| **Deploy static examples** (`deploy-examples.yml`) | Automatic rolling preview and publisher-linked release Pages | +| **Roll back static examples** (`rollback-examples.yml`) | Restore retained rolling-preview bytes; recovery only | -Repository automation may use [`$release-to-npm`](../.agents/skills/release-to-npm/SKILL.md) to coordinate this runbook. This runbook remains authoritative, and invoking the skill does not grant repository access, assignment as a required reviewer for the `npm` environment, or npm package maintainer authority. +Read the [release policy](releasing.md) and confirm [hosted prerequisites](release-administration.md#one-time-hosted-prerequisites) before the first run or after an administrative change. The [release operation skill](../.agents/skills/release-to-npm/SKILL.md) coordinates this runbook; the runbook remains authoritative. ## Stop rules -> Stop rather than improvise if a check fails or public state is unclear. Never publish from a local shell, create a tag or GitHub release manually, force-push, move or delete a release tag, replace a release asset, remove npm's `latest` tag, retry from an arbitrary commit, or continue after an unavailable or malformed registry response. Never share an npm OTP, token, CLI authentication URL, or authenticated browser session with automation. +Stop on a failed required check, unknown registry state, mismatched identity, or missing retained evidence. Do not run `npm publish` locally, move or delete a release tag, replace public assets, remove `latest`, or rerun from another source to rescue an existing version. Keep credential material and authentication codes out of chat, logs, and the release record. ## Roles -- The **release operator** coordinates preparation, review, merge, protected - publication approval, and verification. -- An **npm package maintainer** advances the `latest` dist-tag. This can be the - release operator. -- A **required reviewer for the `npm` environment** performs the protected - deployment approval when GitHub policy requires one. - -Do not weaken an environment rule to avoid involving a configured required -reviewer. +The release operator verifies the change and follows the publisher through Pages completion. Repository administrators maintain environments and required checks. An npm package maintainer provisions and rotates the isolated tag credential. Once those controls are configured, a normal run needs no manual channel update or protected-environment approval. ## Start a release record -Create a private working note outside the repository in an approved, non-versioned location, and fill in each value as it becomes known. Do not record credentials or one-time codes. +Keep a private, non-versioned record with these values. Record sanitized evidence, never secrets. -| Evidence | Value | +| Evidence | Value to record | | --- | --- | -| Requested version action | Continue alpha, next patch alpha, or next minor alpha | -| Version | `EXACT_VERSION` | -| Tag | `vEXACT_VERSION` | -| Canonical `main` before preparation | 40-character commit | -| Security review and dispositions | URLs or sanitized private references | -| Hosted-control verification | UTC time and verifier | -| Prepare workflow run | URL | -| Generated release branch | `release/vEXACT_VERSION` | -| Generated branch head | 40-character commit | -| Compare/create-pull-request URL | URL | -| Submitted release pull request | URL | -| Submitted pull-request head | 40-character commit | -| Required review and CI evidence | URLs and approvals | -| Full release commit | 40-character `RELEASE_SHA` | -| Publisher run | URL | -| Protected npm approval | Reviewer and UTC time | -| Retained asset digests | Asset name and SHA-256 for every uploaded asset | -| npm integrity | `sha512-...` | -| npm publication availability | Available; private hold, block, or appeal reference when applicable | -| npm signatures and provenance | Verification result | -| npm `alpha` and `latest` | Exact version | -| GitHub prerelease | URL | -| Release Pages run | URL | -| Release metadata | URL | -| Completion | UTC date and time | - -`EXACT_VERSION` never includes a leading `v`. The tag always does. +| Requested change | Pull request and phase commits | +| Source identity | Full 40-character `SOURCE_SHA`, source development version | +| Validation | Exact-main CI run, Firefox qualification, final gate, visual review | +| Hosted controls | Verifier and UTC verification time | +| Publisher identity | Run URL, run ID, original UTC creation time, attempt number | +| Published identity | `EXACT_VERSION`, protected tag `vEXACT_VERSION` | +| Package evidence | Receipt, npm integrity, every retained asset digest, SBOM, compatibility target report | +| Public verification | Registry versions/tags, signatures, provenance, immutable GitHub prerelease | +| Pages | Publisher-linked run, release metadata URL, matching source and version | +| Completion or skip | UTC time and evidence establishing the outcome | + +`EXACT_VERSION` has no leading `v`; the Git tag does. Copy the generated version from the run or receipt. Do not create a candidate version by hand. ## 1. Complete preflight -Complete every checkbox before running the preparation workflow. +- [ ] All intended changes and `[Unreleased]` notes are merged through required review. +- [ ] The newest **CI** run for the exact current `main` commit succeeded, including **Build, test, and verify package** and both Firefox qualification jobs. +- [ ] The final screenshots and any required manual accessibility evidence were reviewed. +- [ ] Release-blocking security findings are resolved or explicitly dispositioned with recorded evidence. +- [ ] Hosted settings match the [administration guide](release-administration.md#one-time-hosted-prerequisites). +- [ ] Before stable exists, the nightly tag token and its recorded expiry are current. Rotate when the seven-day warning appears. +- [ ] The preceding nightly is complete through Pages, or any partial attempt has been classified for recovery first. -- [ ] Open **Actions** → **CI** and confirm the newest run for `main` succeeded. -- [ ] Confirm all intended source, documentation, tests, screenshots, design decisions, and risk-based accessibility evidence are already merged into `main`. -- [ ] Open `CHANGELOG.md` on `main` and confirm `[Unreleased]` contains meaningful user-visible bullet notes rather than placeholders. -- [ ] Open **Security & quality** and review code-scanning, Dependabot vulnerability and malware, and secret-scanning results. Resolve or explicitly disposition every release-blocking result, and record sanitized evidence in the private release record. -- [ ] Confirm GitHub's private-email setting is enabled for the account that will squash-merge the release pull request. -- [ ] Confirm the `npm` and `github-pages` environments, required CI check, protected `main`, protected `v*` tags, immutable releases, and npm trusted publisher still match the [administration guide](release-administration.md#one-time-hosted-prerequisites). - -Normal release preparation and publication do not require a local commit. If an exceptional repair does require a human-authored local commit, first obtain the exact GitHub display name and protected no-reply email from the authenticated operator's own account settings, then configure and verify those values locally: +Use these read-only checks when working from GitHub CLI and npm: ```sh -git config --local user.name "VERIFIED_GITHUB_NAME" -git config --local user.email "VERIFIED_GITHUB_NOREPLY_EMAIL" -git config --local --get user.name -git config --local --get user.email +gh api repos/tryagaindev/litefold-calendar/git/ref/heads/main --jq .object.sha +gh run list --repo tryagaindev/litefold-calendar --workflow ci.yml --branch main --limit 10 +npm view @tryagaindev/litefold-calendar versions dist-tags --json --registry https://registry.npmjs.org/ ``` -The final two commands must print the exact values that the authenticated operator personally verified. Never copy an identity from documentation, reuse another operator's identity, or configure a public personal address. +Record the returned main commit as `SOURCE_SHA` and inspect the matching CI run. Public npm reads do not require login. During the alpha-to-nightly transition, the historical `alpha` version supplies the predecessor; once a nightly exists, resume incomplete nightly work before starting another candidate. -Run these read-only npm checks in the npm package maintainer's shell: + + -```sh -npm whoami --registry https://registry.npmjs.org/ -npm view @tryagaindev/litefold-calendar dist-tags --json --registry https://registry.npmjs.org/ -``` +## 2. Request the exact nightly -If authentication is absent, the npm package maintainer runs the exact login command below and completes login and two-factor authentication privately in their own shell and browser: +1. Open the repository's **Actions** tab and select **Publish npm nightly**. +2. Select **Run workflow**, with **Use workflow from** set to `main`. +3. Set **Optional exact current main commit assertion** to the recorded `SOURCE_SHA`. +4. Select **Run workflow** and open the new run. Confirm the branch and full source commit match the record. + +Equivalent GitHub CLI dispatch, after replacing `SOURCE_SHA`: ```sh -npm login --registry https://registry.npmjs.org/ +gh workflow run publish-nightly.yml --repo tryagaindev/litefold-calendar --ref main -f expected-source=SOURCE_SHA ``` -After login, rerun `npm whoami`. Do not give automation access to the npm -browser session, authentication URL, OTP, or token. - -Both existing dist-tags must select the same completed prerelease before another alpha release begins. Stop if the registry response is missing, malformed, or the tags differ. - -Executable workflow validation and focused tests own any compatibility allowance -needed for an existing release object. Do not treat an older object's shape as -permission to weaken current protected-tag or immutability requirements. - -## 2. Prepare the release pull request - -1. Open the repository's **Actions** tab. -2. Select **Prepare alpha release**. -3. Select **Run workflow**. -4. Set **Use workflow from** to `main`. -5. Choose one version action: - - | Choice | Result | - | --- | --- | - | **Continue alpha** | Increase only `alpha.N` | - | **Next patch alpha** | Increase the patch and start `alpha.0` | - | **Next minor alpha** | Increase the minor and start `alpha.0` | +Record the actual run URL and ID from Actions. The assertion stops a mismatched request; it does not allow publishing an arbitrary commit. If `main` advances before a new publication, verify its CI and request that reviewed source instead. -6. Select **Run workflow** and wait for every job to succeed. -7. Record the exact version, generated branch, branch-head commit, preparation - run, and compare/create-pull-request URL from the run summary. The workflow - creates the branch but does not create a pull request. -8. After authorization to submit it, open the compare URL, create the release - pull request, and record its actual URL and exact head commit. +The source manifest remains `0.6.0-nightly.0`. For example, a run created at `2026-09-12T09:00:00Z` with ID `123456789` derives `0.6.0-nightly.20260912090000.123456789`. Reruns retain the original timestamp and ID. -Stop if the workflow fails, reports empty notes, reports a branch collision, or produces an unexpected version. Do not create the branch or edit release-state files by hand to bypass the failure. Classify an existing release branch or pull request with the [recovery matrix](release-administration.md#recovery-matrix) before resuming or cleaning it up. + -## 3. Review and merge the release pull request +## 3. Follow verification and publication -1. Open **Files changed** and confirm the pull request changes exactly: +Wait for the exact run to finish. It may queue behind an earlier publication; do not bypass or cancel the shared queue. - - `CHANGELOG.md` - - `package-lock.json` - - `package.json` - -2. Confirm the pull-request head still equals the recorded generated branch - head. -3. Confirm `package.json`, top-level `package-lock.json#version`, and `package-lock.json#packages[""]#version` all equal `EXACT_VERSION`. -4. Confirm the newest dated changelog heading is `[EXACT_VERSION]` and contains the intended notes. -5. Open **Checks** and wait for `CI / Build, test, and verify package` to succeed. -6. Recheck any security, accessibility, design, screenshot, or manual evidence affected by the release. -7. Use **Squash and merge** with the subject `chore: release EXACT_VERSION`. -8. Open the resulting commit on `main`. Record its full 40-character SHA as `RELEASE_SHA` and confirm the human author email is the protected GitHub no-reply address. - -Stop on an extra file, partial version update, unexpected co-author identity, failing check, or ambiguous check result. The publication workflow requires the exact three-file first-parent diff. - -## 4. Review and approve publication - -The merge starts **Publish npm alpha** for `RELEASE_SHA`. - -1. Open that exact workflow run; do not use a run for another commit. In the run header, confirm the branch is `main` and the commit is `RELEASE_SHA`. -2. Confirm **Detect a generated release candidate**, **Verify exact source and create release evidence**, and **Stage exact tag, draft, and release assets** succeed. -3. Inspect the draft GitHub prerelease. Confirm its tag and target are `vEXACT_VERSION` and `RELEASE_SHA`, its notes match the changelog, and it has exactly these five uploaded assets: - - - the `.tgz` package - - `package-verification.json` - - `sbom.spdx.json` - - `SHA256SUMS` - - `LICENSE` - - GitHub's automatic **Source code (zip)** and **Source code (tar.gz)** links are not uploaded release assets and are excluded from this count. +| Job | Expected outcome | +| --- | --- | +| **Select the exact green main snapshot** | Confirms current main, successful exact-source CI, and immutable run identity | +| **Verify exact source and create release evidence** | Checks previous completion and either skips unchanged source or builds one verified candidate | +| **Stage exact tag, draft, and release assets** | Stages the exact source tag and five retained assets | +| **Publish or resume the verified npm nightly** | Publishes or recognizes the identical retained package through `npm-nightly` OIDC | +| **Synchronize the temporary nightly latest channel** | Advances `latest` before stable exists; preserves stable ownership afterward | +| **Verify registry integrity, imports, and provenance** | Verifies the public package from a clean consumer | +| **Publish the verified GitHub prerelease** | Finalizes the verified immutable prerelease and triggers Pages | - Record each uploaded asset's name and SHA-256 digest from the retained - evidence and staged-release verification before approval. +The five uploaded release assets are the package `.tgz`, `package-verification.json`, `sbom.spdx.json`, `SHA256SUMS`, and `LICENSE`. GitHub's automatically generated source archives are excluded from that count. The Actions bundle and notes are retained for 30 days; browser diagnostics for seven days. Complete recovery while evidence remains available. -4. At **Publish or resume the verified npm alpha**, select **Review deployments**. The publication summary is written only after registry verification and finalization, so it is not an approval prerequisite. -5. Select the `npm` environment, then select **Approve and deploy**. When - policy requires a reviewer, the configured required reviewer for the `npm` - environment performs this exact approval. Record the reviewer and UTC time. -6. Wait until the job reports that npm accepted `EXACT_VERSION` under `alpha`. +If the summary reports **No new source since the last completely verified nightly**, downstream publication jobs are skipped. Verify that the previous nightly's receipt and release Pages identify `SOURCE_SHA`; record a successful skip, not a new publication. -The workflow publishes through npm trusted publishing. Never run `npm publish` locally and never add an `NPM_TOKEN` to GitHub. Its GitHub release writer is intentionally source-free and supplies `--repo` explicitly to GitHub CLI commands; do not add a checkout to that job. + -## 5. Advance the temporary npm `latest` tag +## 4. Verify npm and channel ownership -While the publisher is polling, the npm package maintainer runs these commands in their own authenticated shell after replacing `EXACT_VERSION`: +Replace `EXACT_VERSION` with the generated published version and run: ```sh npm view "@tryagaindev/litefold-calendar@EXACT_VERSION" name version dist.integrity --json --registry https://registry.npmjs.org/ -npm dist-tag add @tryagaindev/litefold-calendar@EXACT_VERSION latest --registry https://registry.npmjs.org/ -npm view @tryagaindev/litefold-calendar dist-tags --json --registry https://registry.npmjs.org/ +npm view @tryagaindev/litefold-calendar versions dist-tags --json --registry https://registry.npmjs.org/ ``` -The first result must name the exact candidate. The final result must show both `alpha` and `latest` equal `EXACT_VERSION`. - -Use `npm dist-tag add`; never use `npm dist-tag rm latest`. If npm opens web authentication, the npm package maintainer completes it privately and returns to the same command. Do not grant npm browser access to automation or share the authentication URL or OTP. - -npm's temporary `latest` dist-tag does not make the GitHub release “Latest.” The workflow intentionally leaves the GitHub release marked as a prerelease with `make_latest=false`. - -If the publisher's polling window expires, first query the exact candidate -again. When it is publicly readable with the expected integrity and only -`latest` lags, finish and verify the same dist-tag update, then use **Re-run -failed jobs** on the original publisher run. When the exact candidate remains -unavailable, follow the publish-time-review case in the -[recovery matrix](release-administration.md#recovery-matrix) and do not assume a -dist-tag change is the only missing step. - -## 6. Verify npm and the GitHub prerelease +Check that the exact package is readable and its `dist.integrity` equals the receipt's `npmIntegrity`. `nightly` must select `EXACT_VERSION`. If no stable version exists, `latest` must also select it. If any stable version exists, `latest` must select a stable version and the nightly job must have left it unchanged. Historical `alpha` stays unchanged. -Wait for **Publish npm alpha** to complete successfully, then confirm: +Channel synchronization is automated. If it fails, use [credential rotation and recovery](release-administration.md#tag-token-rotation), then resume the original verified attempt. A successful npm upload is not permission to create a replacement candidate while completion remains uncertain. -- [ ] Both npm dist-tags equal `EXACT_VERSION`. -- [ ] `dist.integrity` equals `npmIntegrity` in `package-verification.json`. -- [ ] The workflow's clean install, root import, extension import, stylesheet import, signatures, and provenance checks passed. -- [ ] The npm package page shows the published README, renders the logo and both representative screenshots, and resolves the hosted demo and full-gallery links. -- [ ] Tag `vEXACT_VERSION` resolves to `RELEASE_SHA`. -- [ ] The GitHub release is public, `draft=false`, `prerelease=true`, and immutable. -- [ ] The release target is `RELEASE_SHA`, the notes are exact, and the same five uploaded assets remain attached; the two automatic source-code archives are excluded from the asset count. + -GitHub locks the tag and uploaded assets, but it still permits edits to the -release title and notes. Project policy forbids those edits after publication; -verify the title and notes against retained evidence rather than treating the -immutability badge as proof of their continued identity. Do not edit or delete -the release. +## 5. Verify the immutable release and installed package -## 7. Verify the automatic release Pages deployment +- [ ] The receipt's published version, source version, full commit, workflow identity, and manifest transformation match the record. +- [ ] The tag resolves to `SOURCE_SHA`; the GitHub release is public, immutable, and marked as a prerelease. +- [ ] All five uploaded assets have the recorded digests; the notes and title match retained evidence. +- [ ] Clean public-package imports, stylesheet use, core/WebMCP integration, registry signatures, and provenance checks passed. +- [ ] For a release introducing calendar behavior, a clean consumer verifies count activation, remote filtering, and focus return after an application dialog. -A successful **Publish npm alpha** completion automatically triggers **Deploy static examples** through GitHub's native `workflow_run` event. Do not manually dispatch a release deployment and do not supply a release ref. +GitHub locks the published tag and assets. Titles, notes, and prerelease metadata remain editable on the platform, so compare those with retained evidence as well. Project policy preserves them after publication. See [GitHub immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases). -Automatic and rollback Pages runs share one non-canceling workflow-level queue so retained-state writes cannot race deployment. A publisher-linked run may legitimately remain queued behind earlier Pages work. Do not cancel it, rerun around it, or weaken the queue; wait for that exact run to start and finish. + -Two **Deploy static examples** runs can exist for the same `RELEASE_SHA`: +## 6. Verify the automatic release Pages deployment -- the run linked to **CI** updates the rolling `main` channel; -- the run linked to **Publish npm alpha** adds the immutable `release` channel. +A successful publisher triggers **Deploy static examples** through `workflow_run`. Select the run linked to **Publish npm nightly**; a separate CI-linked run for the same commit updates only the rolling preview. -Open the publisher-linked run and confirm its upstream head SHA is `RELEASE_SHA`. **Stage a verified Pages channel**, **Preserve deployment snapshot**, **Package retained Pages snapshot**, and **Deploy retained static examples** must succeed. The separately dispatched **Roll back static examples** workflow must not run as part of a release. Then check: +Verify these resources after replacing `EXACT_VERSION`: ```text https://tryagaindev.github.io/litefold-calendar/releases/EXACT_VERSION/examples/metadata.json https://tryagaindev.github.io/litefold-calendar/site-manifest.json ``` -- [ ] Release metadata contains `"channel": "release"`, `"version": "EXACT_VERSION"`, and `"commit": "RELEASE_SHA"`. -- [ ] `site-manifest.json` maps that version and commit to `releases/EXACT_VERSION/examples/`. -- [ ] The release landing page and at least one recipe deep link load and show the same version and source commit. +- [ ] Release metadata contains `channel: release`, the exact published version, and `SOURCE_SHA`. +- [ ] The site manifest maps that version and commit to `releases/EXACT_VERSION/examples/`. +- [ ] The landing page and a recipe deep link load and show the same version and commit-pinned source links. +- [ ] The Pages run used the exact successful publisher and verified its receipt and immutable release assets. -Record the Pages run, metadata URL, and UTC completion time. The release is complete only after every npm, tag, GitHub release, and Pages identity matches. +The publisher hands off Pages without waiting for it. The release record is complete only after these checks pass. For the migration's final manual release, dispatch once more after completion and confirm unchanged-source skipping without a second package or release. ## Recovery routing -Use the canonical [recovery matrix](release-administration.md#recovery-matrix) -before every rerun or exceptional mutation. An existing run retains its -original commit, ref, and workflow definition; it cannot consume a later source -or publisher-workflow fix. - -- Resume the original exact attempt only when the matrix permits it and all - source, workflow, retained-byte, and public identities still match. -- When npm contains the exact verified candidate but `latest` is behind, use - step 5 above and then resume the original publisher as directed by the matrix. -- On conflicting or unprovable public state, changed source or workflow bytes, - or a defective published artifact, stop and follow the greater-version or - exceptional administrative path. -- For a publisher-linked Pages failure or a missing downstream run, use the - Pages cases in the recovery matrix. For a rolling `main` rollback, follow - the [deployment rollback procedure](example-deployment.md#roll-back-the-rolling-preview). - -Record the observed state, exact identities, authorization, action, and -sanitized recovery evidence in the private release record. Never turn the -rolling-preview rollback into a release-deployment shortcut. +Use the [recovery matrix](release-administration.md#recovery-matrix) before rerunning. An original run keeps its source, workflow definition, version, and identity. A transient failure may resume matching retained bytes; later source or workflow corrections require a new reviewed version. Rolling-preview rollback never repairs or replaces a release snapshot. [Back to the documentation hub](README.md) diff --git a/docs/releasing.md b/docs/releasing.md index 4c4b42c..c95b3cd 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,96 +1,58 @@ -# Public alpha release policy +# Nightly release policy -This policy applies to maintainer-authorized public alpha releases of -`@tryagaindev/litefold-calendar`. Release operators follow the -[alpha release operations runbook](release-operations.md); this document owns -the supported release invariants, not commands or UI steps. - -The current procedure supports versions shaped like `0.x.y-alpha.N`. A stable -release, first publication, channel-policy replacement, or other unsupported -transition requires the separately reviewed procedure defined by -[release administration](release-administration.md#unsupported-transitions). +This policy applies to public nightly snapshots of `@tryagaindev/litefold-calendar`. Maintainers use the [nightly operations runbook](release-operations.md) for commands and the [administration guide](release-administration.md) for hosted setup and recovery. ## Supported release state -A completed alpha release must satisfy all of these invariants: - -- `package.json`, both root version fields in `package-lock.json`, and the - promoted `CHANGELOG.md` heading identify the same exact alpha. -- Until a reviewed stable-release procedure replaces the temporary rule, npm - `alpha` and `latest` select that same exact alpha. -- The protected `v` tag resolves to the verified release commit. -- The GitHub release is a public prerelease whose tag and uploaded assets are - platform-immutable. Its title and notes match retained evidence at - publication and remain protected by project policy and access control because - the platform still permits editing them. -- Release Pages metadata identifies the same version and full source commit. -- Existing public versions, tags, release assets, and immutable deployment paths - are never overwritten or repurposed. - -Any compatibility allowance needed to validate an existing release object -belongs in executable validation and focused tests. It does not relax the -requirements for a new release. +The source development version is `0.6.0-nightly.0`. A publication receives a version shaped like `0.6.0-nightly.YYYYMMDDHHmmss.RUN_ID`, using the original workflow run's UTC creation time and numeric run ID. Rerunning that run preserves its version. A new source change requires a new run and version. -## Publication authority +Nightlies snapshot reviewed `main` directly. They do not create daily version commits, release branches, or metadata pull requests. The source version and `[Unreleased]` changelog remain in the checkout; isolated package staging changes only the published manifest's version and stamps the SBOM with the same published identity. -The hosted preparation workflow creates a release-state branch containing only -`CHANGELOG.md`, `package-lock.json`, and `package.json`; it does not publish -or create the pull request. +A completed nightly must satisfy these invariants: -The reviewed pull request must pass required checks and be squash-merged. The -merge push to canonical `main` is the only publication trigger. Publication is -eligible only when the pushed alpha version changes and its first-parent diff -contains exactly those three release-state files. +- The exact source commit passed CI, including required Firefox qualification, and the publication gate. New publication uses current canonical `main`. +- npm `nightly` selects the exact published snapshot. Before any stable version exists, `latest` selects it too. Once a stable version exists, stable owns `latest` permanently. +- The protected `v` tag resolves to the source commit. The GitHub release is a public immutable prerelease with the retained package, receipt, SBOM, license, and checksums. +- Receipt schema 2 binds the source version, published version, source commit, original workflow identity, and version-only manifest transformation. Retained evidence records package integrity, asset digests, toolchain, and resolved compatibility targets. +- Release Pages metadata identifies that same published version and source commit. +- Existing npm versions, release tags, assets, and immutable Pages paths are preserved. The historical `alpha` tag and all alpha releases remain frozen. -Release authority remains separated: +The source development base advances through a reviewed change when the next release series changes. The series must produce generated candidates newer than already published versions. The `nightly.0` development base is never itself a publication candidate. + +## Publication authority -- Repository verification executes against the exact pushed commit and retains - one candidate bundle. -- Source-free GitHub release jobs stage and finalize only the verified identity - and bytes. -- The protected npm environment publishes that retained tarball through trusted - publishing, without a long-lived registry token. -- An authenticated npm package maintainer performs any required channel synchronization in a - private shell. -- A successful exact publisher run is the authority for the automatic release - Pages handoff. +**Publish npm nightly** runs daily at **09:00 UTC** and through manual dispatch from `main`. An optional exact-source assertion protects a requested manual publication against a changed head. Scheduled execution can be delayed by the hosted scheduler; its version uses the actual original run creation time. -A local checkout, locally produced artifact, manual tag, arbitrary ref, or -deployment shortcut is not publication authority. +| Stage | Authority | +| --- | --- | +| Select and verify | Exact source checkout, read-only repository access, successful same-commit CI, fresh registry state, and the complete local gate | +| Stage GitHub release | No source checkout; narrowly scoped write permission for the verified tag, draft, notes, and assets | +| Publish npm | No source checkout; `npm-nightly` environment and OIDC publish only the retained tarball under `nightly` | +| Synchronize `latest` | No source checkout; separate `npm-nightly-tags` environment exposes a package-scoped granular token only to the tag-writing step | +| Verify and finalize | Fresh public-package verification followed by publication of the verified GitHub prerelease | +| Deploy release Pages | Native successful-publisher handoff, verified receipt, exact source, and separate Pages authority | + +Both nightly environments restrict execution to `main` and run without per-release human approval. This is an intentional unattended-delivery policy; code review and required checks protect the source and workflow. The granular token can write package data beyond dist-tags, so it is isolated and expires after 30 days. [Hosted setup](release-administration.md#one-time-hosted-prerequisites) records the required controls. + +One non-canceling publication queue serializes all jobs through channel synchronization and release finalization. A future stable publisher must share that exact queue. Local artifacts and arbitrary refs do not confer publication authority. ## Readiness and completion -Before preparation, all intended implementation, documentation, examples, -tests, design decisions, accessibility evidence, security dispositions, and -meaningful `Unreleased` notes must already be merged to `main`. Release -preparation changes metadata only. +Implementation, documentation, examples, screenshots, accessibility evidence, and meaningful `[Unreleased]` notes must be merged before a nightly is requested. Phase commits receive their focused checks; the exact final source passes the full gate again before publication. + +An unchanged source is skipped only after the previous nightly's registry state, immutable release, and release Pages are verified complete. A failed or partially published attempt is a recovery case, not evidence that nothing changed. The first nightly verifies the existing historical alpha predecessor without moving `alpha`. -A release is complete only when the private release record proves that the same -candidate version, full commit, tarball integrity, npm channels, provenance, -protected tag, immutable GitHub prerelease, retained asset digests, publisher -run, release Pages run, and deployment metadata all agree. +A successful publisher run hands off Pages asynchronously. The operator records completion only after package integrity, provenance, protected tag, GitHub assets, and the publisher-linked Pages metadata agree. A green workflow alone is insufficient. ## Failure policy -Stop when hosted or public state is unavailable, malformed, ambiguous, or -conflicting. Do not work around a failed transition with local publication, -force-pushing, tag movement, asset replacement, version reuse, or a different -source commit. +Unavailable, malformed, ambiguous, or conflicting public state stops publication. Preserve the retained candidate and use the [recovery matrix](release-administration.md#recovery-matrix). Never overwrite a version, move a release tag, replace an asset, or substitute a rebuild for already published bytes. -The original exact attempt may be rerun only when the -[recovery matrix](release-administration.md#recovery-matrix) permits it and -source, workflow, artifact, and public identities remain unchanged. A source or -workflow correction, unverifiable public object, or defective published -artifact requires the documented greater-version or exceptional administrative -path. +A transient failure can resume the original exact run when its source, workflow, version, and retained bytes still match. A source or workflow correction requires a new reviewed commit and new nightly identity. Correct defective published content with a greater version; deprecation is a separately authorized metadata action. ## Operating documents -- [Alpha release operations](release-operations.md) owns the normal ordered - procedure, exact commands, stop rules, and private release record. -- [Release administration](release-administration.md) owns hosted controls, - unsupported transitions, exceptional mutations, reruns, and recovery. -- [Package verification](package-verification.md) owns artifact, installed - package, registry, signature, and provenance evidence. -- [Static example deployment](example-deployment.md) owns Pages verification - and rolling-preview rollback. +- [Nightly release operations](release-operations.md): ordered manual publication, completion checks, and release record. +- [Release administration](release-administration.md): environments, credentials, rotation, recovery, and future stable requirements. +- [Package verification](package-verification.md): artifact and installed-package evidence, integrity, signatures, and provenance. +- [Static example deployment](example-deployment.md): rolling previews, immutable release snapshots, and preview rollback. diff --git a/docs/security-model.md b/docs/security-model.md index 22e5a76..ca75136 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -37,8 +37,9 @@ The model assumes: | Browser agent → WebMCP extension | Tool names, untrusted structured arguments, read/navigation requests | Explicit selection, prefix validation, exact schemas, bounds validation, snapshot-bound opaque cursor validation, existing public navigation paths, no application-action tools | | WebMCP extension → browser agent | Paged visible-range state and presentation-safe source-event fields | Same-origin document registry, untrusted-content annotation, allowed-date filtering, no ID/URL/metadata/cause output, fixed result limits, no superseded-snapshot cache, unregister on teardown | | Contributor → CI | Source and workflow changes | Read-only pull-request permissions, full-SHA actions, no `pull_request_target`, locked script-disabled install, lint/type/policy/tests | -| Exact release commit → retained artifacts | Reviewed source and build tooling | Complete gate, clean-source package build, checksums, integrity, receipt, SBOM, license, attempt-scoped artifacts | -| Verified artifact → npm | Exact tarball and OIDC identity | Protected `npm` environment, source-free publisher, hash/receipt/manifest validation, trusted publishing, provenance, post-publish verification | +| Validated main snapshot → retained artifacts | Reviewed source and build tooling | Complete gate, isolated version overlay, immutable workflow identity, checksums, integrity, receipt, SBOM, license, retained artifacts | +| Verified artifact → npm | Exact tarball and OIDC identity | Main-only `npm-nightly` environment, source-free publisher, hash/receipt/manifest validation, trusted publishing, provenance, post-publish verification | +| Verified nightly → secondary npm tag | Package-scoped granular write token | Separate source-free `npm-nightly-tags` job, 30-day expiry, warning seven days before expiry, registry identity checks, stable ownership of `latest` | | Verified release state → Git tag and GitHub prerelease | Tag and release-write authority | Protected exact-SHA tag, draft-first assets, matching-byte reuse only, immutable release setting, public prerelease last | | Verified Pages input → retained-state writer | Built automatic channel or authenticated retained rollback state | Exact-source or retained-commit identity, credential-stripped unprivileged assembly, exact root CSP, remote-runtime rejection, immutable release preservation | | Exact retained snapshot → GitHub Pages | `pages-content` commit and attempt-scoped Pages artifact | One queued workflow-level lock across writers and deployers, retained-head confirmation, protected Pages authority | @@ -47,7 +48,7 @@ The model assumes: flowchart LR accTitle: Litefold Calendar runtime and publication trust paths accDescr { - At runtime, browser users, application event sources, trusted render hooks, and the optional WebMCP extension interact with the calendar core, DOM, and callbacks. Separately, read-only CI verifies the exact main commit and can update the queued retained-state writer directly. Verified artifacts pass through protected npm and an immutable GitHub prerelease before triggering that writer; an authorized rollback may instead supply an authenticated retained snapshot. The writer hands the exact snapshot to a separately authorized Pages deployment. + At runtime, browser users, application event sources, trusted render hooks, and the optional WebMCP extension interact with the calendar core, DOM, and callbacks. Separately, read-only CI verifies the exact main commit and can update the queued retained-state writer directly. Verified artifacts pass through main-only trusted npm publication, an isolated secondary-tag update, and an immutable GitHub prerelease before triggering that writer; an authorized rollback may instead supply an authenticated retained snapshot. The writer hands the exact snapshot to a separately authorized Pages deployment. } U[Browser user] --> C[Calendar core] S[Application event source] --> C @@ -58,10 +59,11 @@ flowchart LR C --> W E[Trusted consumer render hooks] --> C P[Pull request] --> V[Read-only CI] - V --> R[Exact main release commit] + V --> R[Exact main snapshot] R --> B[Verified artifacts] - B --> N[Protected source-free npm publish] - N --> G[Immutable GitHub prerelease] + B --> N[Main-only source-free npm publish] + N --> L[Isolated secondary-tag update] + L --> G[Immutable GitHub prerelease] G --> Q[Native successful-workflow trigger] V --> X[Queued retained-state writer] Q --> X @@ -103,8 +105,8 @@ The scenarios below describe what each control must prevent. A scenario becomes | TM-006 | Enabling WebMCP discloses more calendar data than intended | Explicit extension selection, same application source authorization, pages of distinct eligible source events from the allowed portion of the loaded visible range, snapshot-bound continuations, no superseded-payload cache, no metadata/URL/ID output, explicit privacy guidance | Titles and raw normalized start/end civil values are intentionally exposed even when visually hidden; applications must opt in only where appropriate | | TM-007 | A model bypasses bounds, forges or replays pagination state, invokes application actions, or causes unbounded fetches | Exact schemas, argument and bounds validation, fixed page size, activation/range/scope/snapshot/offset cursor binding, read-only annotation, one non-read navigation tool, no activation/edit tools, existing source generations | Navigation may request another authorized month; providers must remain safe to repeat | | TM-008 | Duplicate, partial, or stale registrations target the wrong calendar | Stable unique prefix, document-wide collision failure, one shared abort signal, rollback after an observed failure, unregister on destroy, pre-wait and pending-wait lifecycle checks | WebMCP has no atomic batch or timeout, so a never-settling registration cannot be made atomic; application code can separately register conflicting names | -| TM-009 | Pull-request code gains npm, tag, release, or Pages authority | Read-only PR defaults, pinned actions, no `pull_request_target`, separate protected environments/jobs, immutable release checks | Hosted rulesets, approval, and environment configuration remain external controls | -| TM-010 | Release publishes bytes or identity other than the reviewed commit | Exact `push` SHA, deterministic release-state diff, checksums/integrity/receipt/SBOM, source-free OIDC publisher, registry verification, exact tag and immutable release | npm publication is irreversible; defective versions are deprecated and replaced | +| TM-009 | Pull-request code gains npm, tag, release, or Pages authority | Read-only PR defaults, pinned actions, no `pull_request_target`, main-only publication environments, source-free privileged jobs, immutable release checks | Hosted rulesets and environment configuration remain external controls. The secondary-tag token has broader package-write authority; restrict it to this package, exclude organization access, rotate within 30 days, and expose it only to the tag job | +| TM-010 | Release publishes bytes or identity other than the reviewed commit | Exact successful main SHA, immutable workflow creation time/run ID, isolated version overlay, target resolution, checksums/integrity/receipt/SBOM, source-free OIDC publisher, registry verification, exact tag and immutable release | npm publication is irreversible; defective versions are deprecated and replaced. Future stable publication retains a separate review boundary and shares publication concurrency | | TM-011 | Retried publication, rollback, or Pages deployment creates conflicting public state | Matching-byte/identity recovery, no version/tag reuse, retained Pages history, writer-side rollback reconstruction, immutable release directories, one maximum queued workflow-level lock across Pages writers and deployers, public identity verification | Ambiguous external state or a retained-shell policy failure must fail closed and require maintainer review | | TM-012 | A forged, duplicated, or over-privileged extension value gains unintended authority | Package-issued opaque values, synchronous whole-array validation and duplicate-ID rejection, least-privilege capability contexts, per-extension lifecycle guards | Future third-party authoring would create a wider trusted-code boundary and requires a separate lower-stability contract and threat review | @@ -129,12 +131,11 @@ Release-authority paths remain the highest supply-chain priority. Executable ren | `src/internal/dom/` | Semantic structure, managed focus, native interaction, non-executable content | | `src/errors.ts` | Safe user messages and diagnostic separation | | `scripts/check-package-policy.mjs` | Manifest, import, sink, dependency, built-output, and workflow policy | -| Release preparation and verification scripts | Atomic version/changelog transition, exact source state, collision and resume decisions | -| `scripts/pack-release.mjs` | Clean-source tarball, integrity, receipt, SBOM, license, consumer verification | +| Nightly preparation and verification scripts | Immutable candidate identity, exact source state, isolated package-version overlay, unchanged-source classification, collision and retained-byte recovery decisions | +| `scripts/pack-release.mjs` and `scripts/pack-nightly.mjs` | Clean-source tarball, target evidence, integrity, receipt, SBOM, license, consumer verification | | Pages build and assembly scripts | Self-contained assets, exact root CSP, visible identity, retained releases, rolling preview, immutable paths | | `.github/workflows/ci.yml` | Pull-request and main verification without publication authority | -| `.github/workflows/prepare-alpha.yml` | Default-branch preparation and generated release-state branch without publication authority | -| `.github/workflows/publish-alpha.yml` | Exact-`push` release classification, artifact handoff, isolated trusted publishing, tag and prerelease creation | +| `.github/workflows/publish-nightly.yml` | Canonical-main scheduled/manual classification, artifact handoff, isolated trusted publishing and secondary-tag authority, stable ownership, tag and prerelease creation | | `.github/workflows/deploy-examples.yml` | `workflow_run`-only canonical workflow identity, rolling previews, release deployment, retained history, isolated Pages authority, and the shared whole-run deployment queue | | `.github/workflows/rollback-examples.yml` | `workflow_dispatch`-only retained-main rollback reconstructed in the writer from authenticated Git objects, current retained shell, trusted default-branch tooling, and immutable releases; shares the whole-run deployment queue | @@ -159,7 +160,7 @@ recreating, or weakening protection for a current release object. Related operating guidance: -- [Alpha release operations](release-operations.md) defines the step-by-step operator runbook. +- [Nightly release operations](release-operations.md) defines the step-by-step operator runbook. - [Release administration](release-administration.md) defines publication controls and recovery. - [Package verification](package-verification.md) defines artifact evidence. - [First-party extensions](first-party-extensions.md) defines optional component boundaries. diff --git a/package-lock.json b/package-lock.json index 1dbc984..0a04d4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tryagaindev/litefold-calendar", - "version": "0.5.0-alpha.0", + "version": "0.6.0-nightly.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tryagaindev/litefold-calendar", - "version": "0.5.0-alpha.0", + "version": "0.6.0-nightly.0", "license": "MIT", "devDependencies": { "@axe-core/playwright": "4.13.0", diff --git a/package.json b/package.json index 8997d17..13de003 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tryagaindev/litefold-calendar", - "version": "0.5.0-alpha.0", + "version": "0.6.0-nightly.0", "description": "A dependency-free, mobile-first, accessible calendar component.", "private": false, "type": "module", @@ -66,6 +66,7 @@ "build:examples": "node scripts/build-examples.mjs", "build:examples:advanced": "node scripts/build-advanced-example.mjs", "build:package": "node scripts/build-package.mjs", + "browsers:report": "node scripts/report-browser-targets.mjs", "check": "npm run check:static && npm run test:unit && npm run build && npm run test:examples:built && npm run test:browser:built && npm run check:screenshots && npm run check:tarball", "check:fast": "npm run check:static && npm run test:unit && npm run build && npm run test:examples:built && npm run check:screenshots && npm run check:tarball", "check:design": "designmd lint DESIGN.md", @@ -80,14 +81,17 @@ "check:tarball": "node scripts/pack-release.mjs --verify-only", "clean": "node scripts/clean.mjs", "demo": "npm run build && npm run serve:repository", + "dev": "vite --config vite.examples.config.mjs", "lint": "npm run lint:ts && npm run lint:styles", "lint:styles": "stylelint \"src/**/*.css\" \"examples/**/*.css\" \"scripts/pages-site/*.css\"", "lint:ts": "eslint .", "measure:dynamic-events": "npm run build:package && node scripts/measure-dynamic-event-updates.mjs", "measure:workflows": "npm run build:package && node scripts/measure-calendar-workflows.mjs", "package": "node scripts/pack-release.mjs", - "release:prepare": "node scripts/prepare-release.mjs", - "release:verify": "node scripts/check-release-ready.mjs", + "nightly:plan": "node scripts/nightly-release.mjs plan", + "package:nightly": "node scripts/pack-nightly.mjs", + "release:verify": "node scripts/nightly-release.mjs verify", + "screenshots:prepare": "node scripts/prepare-screenshots.mjs", "screenshots:update": "node scripts/prepare-screenshots.mjs --force", "serve:repository": "node scripts/serve-repository.mjs", "test": "npm run test:unit && npm run test:examples", @@ -99,15 +103,12 @@ "test:browser:webkit": "npm run build && playwright test --project=webkit", "test:examples": "npm run build && npm run test:examples:built", "test:examples:built": "node scripts/run-advanced-smoke.mjs", - "test:tooling": "node --test scripts/tests/build-pages.test.mjs scripts/tests/calendar-workflow-measurement.test.mjs scripts/tests/check-sbom.test.mjs scripts/tests/dynamic-update-measurement.test.mjs scripts/tests/example-metadata.test.mjs scripts/tests/markdown-diagrams.test.mjs scripts/tests/markdown-heading.test.mjs scripts/tests/node-version.test.mjs scripts/tests/ownership.test.mjs scripts/tests/package-entries.test.mjs scripts/tests/packed-extension-verification.test.mjs scripts/tests/publish-alpha-policy.test.mjs scripts/tests/release-artifacts.test.mjs scripts/tests/release-state.test.mjs scripts/tests/screenshot-contract.test.mjs scripts/tests/semver.test.mjs scripts/tests/serve-repository.test.mjs scripts/tests/styles.test.mjs scripts/tests/workflow-contracts.test.mjs scripts/tests/browser-targets.test.mjs scripts/tests/vite-build.test.mjs scripts/tests/package-bundle-policy.test.mjs scripts/tests/screenshot-preparation.test.mjs scripts/tests/remote-data-source.test.mjs", + "test:tooling": "node --test scripts/tests/build-pages.test.mjs scripts/tests/calendar-workflow-measurement.test.mjs scripts/tests/check-sbom.test.mjs scripts/tests/dynamic-update-measurement.test.mjs scripts/tests/example-metadata.test.mjs scripts/tests/markdown-diagrams.test.mjs scripts/tests/markdown-heading.test.mjs scripts/tests/node-version.test.mjs scripts/tests/ownership.test.mjs scripts/tests/package-entries.test.mjs scripts/tests/packed-extension-verification.test.mjs scripts/tests/publish-nightly-policy.test.mjs scripts/tests/release-artifacts.test.mjs scripts/tests/screenshot-contract.test.mjs scripts/tests/semver.test.mjs scripts/tests/serve-repository.test.mjs scripts/tests/styles.test.mjs scripts/tests/workflow-contracts.test.mjs scripts/tests/browser-targets.test.mjs scripts/tests/vite-build.test.mjs scripts/tests/package-bundle-policy.test.mjs scripts/tests/nightly-release.test.mjs scripts/tests/screenshot-preparation.test.mjs scripts/tests/remote-data-source.test.mjs", "test:unit": "node scripts/run-unit-tests.mjs", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit", "typecheck:examples": "npm run build:package && npm run typecheck:examples:built", "typecheck:examples:built": "tsc -p examples/advanced/tsconfig.json --noEmit && tsc -p examples/remote-data/tsconfig.json --noEmit", - "check:license": "node scripts/check-license.mjs", - "browsers:report": "node scripts/report-browser-targets.mjs", - "dev": "vite --config vite.examples.config.mjs", - "screenshots:prepare": "node scripts/prepare-screenshots.mjs" + "check:license": "node scripts/check-license.mjs" }, "devDependencies": { "@axe-core/playwright": "4.13.0", @@ -132,6 +133,6 @@ "publishConfig": { "access": "public", "provenance": true, - "tag": "alpha" + "tag": "nightly" } } diff --git a/scripts/build-pages.mjs b/scripts/build-pages.mjs index 027c207..0cec88c 100644 --- a/scripts/build-pages.mjs +++ b/scripts/build-pages.mjs @@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url"; import { parseExampleMetadata, serializeExampleMetadata } from "./lib/example-metadata.mjs"; import { REPOSITORY_ROOT } from "./lib/process.mjs"; import { parseSemVer } from "./lib/semver.mjs"; +import { validateNightlyPlan } from "./lib/nightly-release.mjs"; const SCRIPT_PATH = fileURLToPath(import.meta.url); const DEFAULT_SHELL_DIRECTORY = join(REPOSITORY_ROOT, "scripts", "pages-site"); @@ -82,7 +83,7 @@ function parseArguments(arguments_) { return { outputDirectory: resolve(arguments_[1]) }; } -export function validateDeploymentMetadata(metadata, packageVersion) { +export function validateDeploymentMetadata(metadata, packageVersion, nightlyReceipt = null) { const parsed = parseExampleMetadata(metadata); if (parsed.channel !== "main" && parsed.channel !== "release") { throw new Error("A Pages artifact must use the main or release deployment channel."); @@ -92,7 +93,16 @@ export function validateDeploymentMetadata(metadata, packageVersion) { } catch { throw new Error("Deployment metadata version must match the package version."); } - if (parsed.version !== packageVersion) { + if (nightlyReceipt !== null) { + const plan = validateNightlyPlan(nightlyReceipt.nightly); + if (nightlyReceipt.schemaVersion !== 2 || nightlyReceipt.sourceTreeDirty !== false || + nightlyReceipt.manifestTransform !== "version-only" || nightlyReceipt.sourceVersion !== packageVersion || + plan.baseVersion !== packageVersion || nightlyReceipt.sourceCommit !== parsed.commit || + plan.sourceCommit !== parsed.commit || nightlyReceipt.version !== parsed.version || + plan.version !== parsed.version || parsed.channel !== "release") { + throw new Error("Nightly Pages metadata must match the verified publisher receipt and source."); + } + } else if (parsed.version !== packageVersion) { throw new Error("Deployment metadata version must match the package version."); } @@ -360,7 +370,9 @@ export async function buildPagesArtifact(options) { const metadataPath = join(repositoryRoot, "examples", "metadata.json"); const metadata = validateDeploymentMetadata( JSON.parse(await readFile(metadataPath, "utf8")), - packageManifest.version + packageManifest.version, + process.env.LFC_NIGHTLY_RECEIPT === undefined ? null : + JSON.parse(await readFile(process.env.LFC_NIGHTLY_RECEIPT, "utf8")) ); const serializedMetadata = serializeExampleMetadata(metadata); const repositoryUrl = repositoryWebUrl(packageManifest.repository); diff --git a/scripts/check-package-policy.mjs b/scripts/check-package-policy.mjs index 1019d91..a4142d7 100644 --- a/scripts/check-package-policy.mjs +++ b/scripts/check-package-policy.mjs @@ -58,10 +58,10 @@ const EXPECTED_REPOSITORY = "git+https://github.com/tryagaindev/litefold-calenda const EXPECTED_PUBLISH_CONFIG = Object.freeze({ access: "public", provenance: true, - tag: "alpha" + tag: "nightly" }); const WORKFLOW_NODE_VERSION = "24.19.0"; -const ALPHA_VERSION = /^0\.\d+\.\d+-alpha\.\d+$/u; +const SOURCE_VERSION = /^\d+\.\d+\.\d+-nightly\.0$/u; const PUBLIC_ROOT_CLASS = "litefold-calendar"; const PUBLIC_ROOT_SELECTOR = `:where(.${PUBLIC_ROOT_CLASS})`; const ROOT_ATTRIBUTE_SELECTOR = /\[\s*data-(?:lfc|litefold)-calendar\s*(?=\]|[~|^$*]?=)/iu; @@ -1246,10 +1246,10 @@ if (packageJson.name !== EXPECTED_PACKAGE_NAME) { ); } -if (!ALPHA_VERSION.test(packageJson.version) || +if (!SOURCE_VERSION.test(packageJson.version) || packageJson.private !== false) { addError( - "The manifest must declare a public 0.x.y-alpha.N prerelease." + "The source manifest must declare a public x.y.z-nightly.0 snapshot base." ); } @@ -1438,7 +1438,7 @@ if (!isExactValue( EXPECTED_PUBLISH_CONFIG )) { addError( - "Public alpha packages require exact public access, provenance, and alpha dist-tag policy." + "Public nightly packages require exact public access, provenance, and nightly dist-tag policy." ); } @@ -1581,13 +1581,7 @@ await inspectWorkflowTree({ LFC_NPM_VERSION: String(referenceNpmVersion) }, - ".github/workflows/prepare-alpha.yml": { - LFC_NODE_VERSION: - WORKFLOW_NODE_VERSION, - LFC_NPM_VERSION: - String(referenceNpmVersion) - }, - ".github/workflows/publish-alpha.yml": { + ".github/workflows/publish-nightly.yml": { LFC_NODE_VERSION: WORKFLOW_NODE_VERSION, LFC_NPM_VERSION: diff --git a/scripts/check-release-ready.mjs b/scripts/check-release-ready.mjs deleted file mode 100644 index 5e89c6f..0000000 --- a/scripts/check-release-ready.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -import { verifyRelease } from "./lib/release-state.mjs"; - -const SCRIPT_PATH = fileURLToPath(import.meta.url); -const VALUE_OPTIONS = new Map([ - ["--commit", "expectedCommit"], - ["--tag", "expectedTag"], - ["--tag-state", "tagState"], - ["--version", "expectedVersion"] -]); -const TAG_STATES = new Set(["absent", "either", "matching"]); - -export function parseReleaseVerificationArguments(arguments_) { - const options = { - json: false, - requireClean: false, - tagState: "either" - }; - let legacyTag; - const seenValueOptions = new Set(); - for (let index = 0; index < arguments_.length; index += 1) { - const argument = arguments_[index]; - if (VALUE_OPTIONS.has(argument)) { - const property = VALUE_OPTIONS.get(argument); - const value = arguments_[index + 1]; - if (value === undefined || seenValueOptions.has(argument)) { - throw new Error(`${argument} requires exactly one value.`); - } - options[property] = value; - seenValueOptions.add(argument); - index += 1; - continue; - } - if (argument === "--json" && !options.json) { - options.json = true; - continue; - } - if (argument === "--require-clean" && !options.requireClean) { - options.requireClean = true; - continue; - } - if (!argument.startsWith("-") && legacyTag === undefined) { - legacyTag = argument; - continue; - } - throw new Error( - "Usage: npm run release:verify -- [--version ] [--commit ] [--tag ] [--tag-state absent|either|matching] [--require-clean] [--json]" - ); - } - if (legacyTag !== undefined) { - if (options.expectedTag !== undefined) { - throw new Error("Provide a release tag either positionally or with --tag, not both."); - } - options.expectedTag = legacyTag; - } - if (!TAG_STATES.has(options.tagState)) { - throw new Error("--tag-state must be absent, either, or matching."); - } - return options; -} - -export async function main(arguments_ = process.argv.slice(2)) { - const options = parseReleaseVerificationArguments(arguments_); - const result = await verifyRelease(options); - if (options.json) { - console.log(JSON.stringify(result)); - } else { - console.log(`Release ${result.tag} is ready (commit ${result.git.head}).`); - } - return result; -} - -if (process.argv[1] !== undefined && resolve(process.argv[1]) === SCRIPT_PATH) { - try { - await main(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (process.argv.includes("--json")) { - console.error(JSON.stringify({ error: message, status: "error" })); - } else { - console.error(`Release verification failed: ${message}`); - } - process.exitCode = 1; - } -} diff --git a/scripts/check-sbom.mjs b/scripts/check-sbom.mjs index 66a70d2..b5c662c 100644 --- a/scripts/check-sbom.mjs +++ b/scripts/check-sbom.mjs @@ -1,6 +1,8 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; +import { NIGHTLY_VERSION_PATTERN } from "./lib/nightly-release.mjs"; + import { REPOSITORY_ROOT, runNpm } from "./lib/process.mjs"; const EXPECTED_SPDX_VERSION = "SPDX-2.3"; @@ -114,6 +116,7 @@ function parseArguments(arguments_) { let emitJson = false; let sourceCommit; let sourceDateEpochText; + let version; for (let index = 0; index < arguments_.length; index += 1) { const argument = arguments_[index]; if (argument === "--json") { @@ -124,7 +127,7 @@ function parseArguments(arguments_) { continue; } - if (argument !== "--source-commit" && argument !== "--source-date-epoch") { + if (argument !== "--source-commit" && argument !== "--source-date-epoch" && argument !== "--version") { throw usageError(`Unknown argument ${String(argument)}.`); } const value = arguments_[index + 1]; @@ -132,7 +135,12 @@ function parseArguments(arguments_) { throw usageError(`${argument} requires a value.`); } index += 1; - if (argument === "--source-commit") { + if (argument === "--version") { + if (version !== undefined || !NIGHTLY_VERSION_PATTERN.test(value)) { + throw usageError("--version requires one generated nightly version."); + } + version = value; + } else if (argument === "--source-commit") { if (sourceCommit !== undefined) { throw usageError("--source-commit may be specified only once."); } @@ -160,7 +168,7 @@ function parseArguments(arguments_) { throw usageError("--source-date-epoch must be a representable nonnegative integer."); } - return { emitJson, sourceCommit, sourceDate }; + return { emitJson, sourceCommit, sourceDate, ...(version === undefined ? {} : { version }) }; } function sortObjectKeys(value) { @@ -206,6 +214,24 @@ const sbom = JSON.parse(result.stdout); validateSbom(sbom, packageJson); if (options.emitJson) { + if (options.version !== undefined) { + const previousVersion = packageJson.version; + packageJson.version = options.version; + const runtimePackage = sbom.packages[0]; + const previousId = runtimePackage.SPDXID; + const nextId = previousId.replace(previousVersion, options.version); + runtimePackage.SPDXID = nextId; + runtimePackage.versionInfo = options.version; + sbom.name = sbom.name.replace(previousVersion, options.version); + sbom.documentDescribes = [nextId]; + for (const relationship of sbom.relationships) { + if (relationship.relatedSpdxElement === previousId) {relationship.relatedSpdxElement = nextId;} + if (relationship.spdxElementId === previousId) {relationship.spdxElementId = nextId;} + } + for (const reference of runtimePackage.externalRefs) { + if (reference.referenceType === "purl") {reference.referenceLocator = npmPurl(packageJson.name, options.version);} + } + } const canonicalSbom = canonicalizeSbom( sbom, packageJson, diff --git a/scripts/lib/nightly-release.mjs b/scripts/lib/nightly-release.mjs new file mode 100644 index 0000000..98dd4c1 --- /dev/null +++ b/scripts/lib/nightly-release.mjs @@ -0,0 +1,198 @@ +import { createHash } from "node:crypto"; +import { join } from "node:path"; +import { readFile, readdir, lstat } from "node:fs/promises"; + +import semver from "semver"; + +export const NIGHTLY_PACKAGE = "@tryagaindev/litefold-calendar"; +export const NIGHTLY_WORKFLOW = ".github/workflows/publish-nightly.yml"; +export const NIGHTLY_BASE_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-nightly\.0$/u; +export const NIGHTLY_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-nightly\.(\d{14})\.([1-9]\d*)$/u; +const SHA_PATTERN = /^[0-9a-f]{40}$/u; +const PLAN_KEYS = ["baseVersion", "createdAt", "event", "name", "runId", "schemaVersion", "sourceCommit", "version", "workflow"]; + +/** Derives one immutable nightly identity; reruns use the original run creation time. */ +export function createNightlyPlan({ baseVersion, createdAt, event, runId, sourceCommit }) { + if (!NIGHTLY_BASE_PATTERN.test(baseVersion) || semver.valid(baseVersion) !== baseVersion) { + throw new Error("Nightly source version must be a normalized x.y.z-nightly.0 development base."); + } + if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u.test(createdAt) || + new Date(createdAt).toISOString().replace(".000Z", "Z") !== createdAt) { + throw new Error("Nightly creation time must be the workflow run's exact UTC creation time."); + } + if (typeof runId !== "string" || !/^[1-9]\d*$/u.test(runId) || !Number.isSafeInteger(Number(runId))) { + throw new Error("Nightly run ID must be a positive safe integer string."); + } + if (!SHA_PATTERN.test(sourceCommit) || !["schedule", "workflow_dispatch"].includes(event)) { + throw new Error("Nightly requires a full source SHA and schedule or workflow_dispatch event."); + } + const timestamp = createdAt.replace(/[-:TZ]/gu, ""); + const version = baseVersion.replace(/-nightly\.0$/u, `-nightly.${timestamp}.${runId}`); + return Object.freeze({ baseVersion, createdAt, event, name: NIGHTLY_PACKAGE, runId, + schemaVersion: 1, sourceCommit, version, workflow: NIGHTLY_WORKFLOW }); +} + +/** Rejects modified plans instead of trusting artifact metadata as an instruction. */ +export function validateNightlyPlan(value) { + if (value === null || typeof value !== "object" || Array.isArray(value) || + Object.keys(value).sort().join(",") !== PLAN_KEYS.join(",")) { + throw new Error("Nightly plan has an unexpected schema."); + } + const expected = createNightlyPlan(value); + for (const key of PLAN_KEYS) { + if (value[key] !== expected[key]) { + throw new Error(`Nightly plan ${key} does not match its immutable inputs.`); + } + } + return expected; +} + +export async function readNightlyPlan(path) { + return validateNightlyPlan(JSON.parse(await readFile(path, "utf8"))); +} + +/** Changes only artifact version; the source checkout remains untouched. */ +export function nightlyPackageManifest(manifest, planValue) { + const plan = validateNightlyPlan(planValue); + if (manifest.name !== NIGHTLY_PACKAGE || manifest.version !== plan.baseVersion || + manifest.private !== false || manifest.publishConfig?.tag !== "nightly" || + manifest.publishConfig?.access !== "public" || manifest.publishConfig?.provenance !== true) { + throw new Error("Nightly source manifest does not match the approved package policy and base."); + } + return { ...manifest, version: plan.version }; +} + +/** Validates a fresh registry snapshot, including the first alpha-to-nightly transition. */ +export function validateNightlyRegistryState(metadata, planValue) { + const plan = validateNightlyPlan(planValue); + const versions = typeof metadata?.versions === "string" ? [metadata.versions] : metadata?.versions; + const tags = metadata?.["dist-tags"]; + if (!Array.isArray(versions) || versions.length === 0 || + versions.some((version) => typeof version !== "string" || semver.valid(version) !== version) || + tags === null || typeof tags !== "object" || Array.isArray(tags)) { + throw new Error("Nightly publishing requires valid metadata for the existing npm package."); + } + const stableExists = versions.some((version) => semver.prerelease(version) === null); + if (typeof tags.latest !== "string" || !versions.includes(tags.latest) || + (stableExists && semver.prerelease(tags.latest) !== null)) { + throw new Error("npm latest must select a published version, and belongs to stable after first stable."); + } + const predecessor = tags.nightly ?? tags.alpha; + if (typeof predecessor !== "string" || !versions.includes(predecessor) || + !(NIGHTLY_VERSION_PATTERN.test(predecessor) || /^0\.\d+\.\d+-alpha\.\d+$/u.test(predecessor))) { + throw new Error("Nightly requires a verified published nightly or historical alpha predecessor."); + } + if (tags.nightly !== undefined && !NIGHTLY_VERSION_PATTERN.test(tags.nightly)) { + throw new Error("npm nightly has an unsupported version shape."); + } + if (predecessor !== plan.version && !semver.lt(predecessor, plan.version)) { + throw new Error("Nightly candidate must be newer than its predecessor."); + } + if (versions.some((version) => version !== plan.version && semver.gte(version, plan.version))) { + throw new Error("Nightly base must advance beyond every previously published version."); + } + const existing = versions.includes(plan.version); + if (existing && tags.nightly !== plan.version) { + throw new Error("Existing nightly candidate is not selected by the nightly tag."); + } + if (!stableExists && predecessor !== plan.version && tags.latest !== predecessor) { + throw new Error("A previous nightly has incomplete latest synchronization; resume that attempt first."); + } + return Object.freeze({ existing, latest: tags.latest, predecessor, stableExists }); +} + +/** Checks expiry before public writes; rotation does not expose token material. */ +export function tokenExpiryStatus(expiresAt, now = new Date()) { + const expiry = new Date(expiresAt); + if (typeof expiresAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u.test(expiresAt) || + !Number.isFinite(expiry.getTime()) || !Number.isFinite(now.getTime()) || + expiry.toISOString().replace(".000Z", "Z") !== expiresAt) { + throw new Error("Nightly tag-token expiry must be an explicit UTC timestamp."); + } + const remaining = expiry.getTime() - now.getTime(); + if (remaining <= 0) { + throw new Error("Nightly tag token has expired; rotate it before publishing."); + } + return Object.freeze({ expiresAt, warn: remaining <= 7 * 24 * 60 * 60 * 1_000 }); +} + + +/** Selects retained evidence from this run; incomplete public attempts cannot rebuild. */ +export function selectNightlyRecoveryArtifacts({ artifacts, plan: value, attempt, hasPublicState }) { + const plan = validateNightlyPlan(value); + if (!Array.isArray(artifacts) || !Number.isSafeInteger(attempt) || attempt < 1) { + throw new Error("Nightly recovery requires a complete artifact listing and valid attempt."); + } + const bundlePrefix = `npm-release-bundle-v${plan.version}-${plan.runId}-`; + const notesPrefix = `release-notes-v${plan.version}-${plan.runId}-`; + const candidates = []; + for (let previous = 1; previous < attempt; previous += 1) { + const bundle = artifacts.filter((entry) => entry.name === `${bundlePrefix}${previous}`); + const notes = artifacts.filter((entry) => entry.name === `${notesPrefix}${previous}`); + if (bundle.length > 1 || notes.length > 1) { + throw new Error("Nightly recovery artifact identities are ambiguous."); + } + if (bundle.length === 1 && notes.length === 1) { + candidates.push({ attempt: previous, bundle: bundle[0], notes: notes[0] }); + } + } + const candidate = candidates[0]; + if (candidate === undefined) { + if (hasPublicState) { + throw new Error("Staged or published nightly has no retained complete evidence; refusing to rebuild."); + } + return null; + } + for (const artifact of [candidate.bundle, candidate.notes]) { + if (artifact.expired !== false || !Number.isSafeInteger(artifact.id) || artifact.id < 1 || + !/^sha256:[0-9a-f]{64}$/u.test(artifact.digest)) { + throw new Error("Original nightly evidence is expired or lacks a verifiable artifact digest."); + } + } + return Object.freeze(candidate); +} + +/** Verifies restored bytes against immutable source identity before any release writer. */ +export async function verifyRestoredNightlyBundle(directory, planValue, sourceManifest) { + const plan = validateNightlyPlan(planValue); + const manifest = nightlyPackageManifest(sourceManifest, plan); + const tarball = `tryagaindev-litefold-calendar-${plan.version}.tgz`; + const ordered = [tarball, "package-verification.json", "sbom.spdx.json", "LICENSE"]; + const expected = [...ordered, "SHA256SUMS"].sort(); + const actual = (await readdir(directory)).sort(); + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error("Restored nightly evidence must contain exactly five files."); + } + const files = new Map(); + for (const name of expected) { + const path = join(directory, name); + const metadata = await lstat(path); + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error("Restored nightly evidence must contain regular files only."); + } + files.set(name, await readFile(path)); + } + const digest = (bytes, algorithm = "sha256", encoding = "hex") => createHash(algorithm).update(bytes).digest(encoding); + const checksums = ordered.map((name) => `${digest(files.get(name))} ${name}\n`).join(""); + if (files.get("SHA256SUMS").toString("utf8") !== checksums) { + throw new Error("Restored nightly evidence checksums do not match its retained bytes."); + } + const receipt = JSON.parse(files.get("package-verification.json")); + if (receipt.schemaVersion !== 2 || receipt.name !== NIGHTLY_PACKAGE || receipt.version !== plan.version || + receipt.sourceVersion !== plan.baseVersion || receipt.sourceCommit !== plan.sourceCommit || + receipt.sourceTreeDirty !== false || receipt.manifestTransform !== "version-only" || + JSON.stringify(validateNightlyPlan(receipt.nightly)) !== JSON.stringify(plan) || + receipt.sourceManifestSha256 !== digest(JSON.stringify(sourceManifest)) || + receipt.publishedManifestSha256 !== digest(JSON.stringify(manifest)) || + receipt.sha256 !== digest(files.get(tarball)) || + receipt.npmIntegrity !== `sha512-${digest(files.get(tarball), "sha512", "base64")}` || + receipt.browserTargets?.resolvedAt !== plan.createdAt.slice(0, 10) || + receipt.browserTargets?.query !== "baseline-widely-available" || + receipt.browserTargets?.dataVersions?.vite !== sourceManifest.devDependencies?.vite || + receipt.browserTargets?.dataVersions?.esbuild !== sourceManifest.devDependencies?.esbuild || + receipt.browserTargets?.effectiveQuery !== `Vite ${sourceManifest.devDependencies?.vite}: baseline-widely-available` || + receipt.browserTargetsSha256 !== digest(JSON.stringify(receipt.browserTargets))) { + throw new Error("Restored nightly receipt differs from its exact source, plan, or artifact bytes."); + } + return receipt; +} diff --git a/scripts/lib/release-artifacts.mjs b/scripts/lib/release-artifacts.mjs index 715c778..dba69b6 100644 --- a/scripts/lib/release-artifacts.mjs +++ b/scripts/lib/release-artifacts.mjs @@ -77,6 +77,10 @@ async function assertMissingFinalDirectory(finalDirectory) { } export function parseReleaseArguments(arguments_) { + if (Array.isArray(arguments_) && arguments_.length === 2 && + arguments_[0] === "--nightly-plan" && typeof arguments_[1] === "string" && arguments_[1].length > 0) { + return Object.freeze({ nightlyPlanPath: resolve(arguments_[1]), verifyOnly: false }); + } if (!Array.isArray(arguments_) || arguments_.some((argument) => argument !== "--verify-only")) { throw new Error("Usage: node scripts/pack-release.mjs [--verify-only]"); } diff --git a/scripts/lib/release-state.mjs b/scripts/lib/release-state.mjs deleted file mode 100644 index fbd3291..0000000 --- a/scripts/lib/release-state.mjs +++ /dev/null @@ -1,439 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { readFile, rename, rm, writeFile } from "node:fs/promises"; -import { join, relative, resolve, sep } from "node:path"; - -import { - compareSemVerPrecedence, - isAlphaVersion, - nextAlphaVersion, - parseSemVer -} from "./semver.mjs"; -import { REPOSITORY_ROOT, run } from "./process.mjs"; - -export const EXPECTED_PACKAGE_NAME = "@tryagaindev/litefold-calendar"; -export const EXPECTED_REPOSITORY = "git+https://github.com/tryagaindev/litefold-calendar.git"; -export const EXPECTED_GITHUB_REPOSITORY = "tryagaindev/litefold-calendar"; -export const RELEASE_STATE_FILES = Object.freeze([ - "CHANGELOG.md", - "package-lock.json", - "package.json" -]); - -const EMPTY_UNRELEASED = ""; -const CHANGELOG_HEADING_PATTERN = /^## \[([^\]]+)\](?: - (\d{4}-\d{2}-\d{2}))?[ \t]*$/gmu; -const FULL_COMMIT_PATTERN = /^[0-9a-f]{40}$/u; -const PLACEHOLDER_PATTERN = /^(?:-|\*|\s)*(?:n\/?a|none|nothing|tbd\b.*|todo\b.*|add (?:release notes|changes).*)[.!]?(?:\s*)$/iu; - -function parseJson(text, name) { - try { - return JSON.parse(text); - } catch (error) { - throw new Error(`${name} must contain valid JSON.`, { cause: error }); - } -} - -function serializeJsonLike(original, value) { - const newline = original.includes("\r\n") ? "\r\n" : "\n"; - const indent = /^([ \t]+)"/mu.exec(original)?.[1] ?? "\t"; - return `${JSON.stringify(value, null, indent).replaceAll("\n", newline)}${newline}`; -} - -function repositoryUrl(packageJson) { - return typeof packageJson.repository === "string" - ? packageJson.repository - : packageJson.repository?.url; -} - -function findLineEnd(text, start) { - const newline = text.indexOf("\n", start); - return newline === -1 ? text.length : newline + 1; -} - -function isValidUtcDate(value) { - if (!/^\d{4}-\d{2}-\d{2}$/u.test(value)) { - return false; - } - const date = new Date(`${value}T00:00:00.000Z`); - return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value; -} - -function maskCharacters(value) { - return value.replace(/[^\r\n]/g, " "); -} - -function maskHtmlComments(source) { - const fragments = []; - let cursor = 0; - while (cursor < source.length) { - const commentStart = source.indexOf("", commentStart + 4); - if (commentEnd === -1) { - throw new Error("CHANGELOG.md contains an unterminated HTML comment."); - } - const afterComment = commentEnd + 3; - fragments.push(maskCharacters(source.slice(commentStart, afterComment))); - cursor = afterComment; - } - return fragments.join(""); -} - -function hasMeaningfulNotes(body) { - const source = maskHtmlComments(body).trim(); - if (source.length === 0 || PLACEHOLDER_PATTERN.test(source)) { - return false; - } - return source.split(/\r?\n/u).some((line) => - /^\s*[-*]\s+\S/u.test(line) && !PLACEHOLDER_PATTERN.test(line.trim()) - ); -} - -function isEmptyUnreleased(body) { - return maskHtmlComments(body).trim().length === 0; -} - -function displayPath(path, root) { - return relative(root, path).replaceAll(sep, "/"); -} - -async function captureGit(arguments_, repositoryRoot, allowFailure = false) { - try { - const result = await run("git", arguments_, { capture: true, cwd: repositoryRoot }); - return { exists: true, value: result.stdout.trim() }; - } catch (error) { - if (allowFailure) { - return { exists: false, value: "" }; - } - throw error; - } -} - -function normalizeOriginUrl(value) { - return value - .replace(/^git@github\.com:/iu, "https://github.com/") - .replace(/^git\+https:/iu, "https:") - .replace(/\.git\/?$/iu, "") - .replace(/\/$/u, "") - .toLowerCase(); -} - -/** Parses and validates the release sections in CHANGELOG.md. */ -export function parseChangelog(text) { - const visibleText = maskHtmlComments(text); - for (const line of visibleText.split(/\r?\n/u)) { - if (/^[ \t]{0,3}##[ \t]*\[/u.test(line) && - !/^## \[[^\]]+\](?: - \d{4}-\d{2}-\d{2})?[ \t]*$/u.test(line)) { - throw new Error(`Malformed bracketed changelog heading: ${line}`); - } - } - - const headings = []; - for (const match of visibleText.matchAll(CHANGELOG_HEADING_PATTERN)) { - headings.push({ - bodyStart: findLineEnd(text, match.index + match[0].length), - date: match[2] ?? null, - headingEnd: match.index + match[0].length, - headingStart: match.index, - name: match[1] - }); - } - for (let index = 0; index < headings.length; index += 1) { - headings[index].bodyEnd = headings[index + 1]?.headingStart ?? text.length; - headings[index].body = text.slice(headings[index].bodyStart, headings[index].bodyEnd); - } - - const unreleased = headings.filter((entry) => entry.name === "Unreleased"); - if (unreleased.length !== 1 || unreleased[0].date !== null || headings[0] !== unreleased[0]) { - throw new Error("CHANGELOG.md must begin with exactly one undated [Unreleased] section."); - } - const releases = headings.filter((entry) => entry.name !== "Unreleased"); - const seen = new Set(); - for (const release of releases) { - parseSemVer(release.name); - if (release.date === null || !isValidUtcDate(release.date)) { - throw new Error(`Changelog release ${release.name} must have a valid YYYY-MM-DD date.`); - } - if (seen.has(release.name)) { - throw new Error(`CHANGELOG.md contains duplicate release ${release.name}.`); - } - seen.add(release.name); - } - for (let index = 1; index < releases.length; index += 1) { - if (compareSemVerPrecedence(releases[index - 1].name, releases[index].name) <= 0) { - throw new Error("Dated changelog releases must be in strictly descending SemVer order."); - } - } - return { headings, releases, unreleased: unreleased[0] }; -} - -/** Parses the three versioned release-state files. */ -export function readReleaseStateTexts(texts) { - return { - changelog: parseChangelog(texts.changelog), - packageJson: parseJson(texts.packageJson, "package.json"), - packageLock: parseJson(texts.packageLock, "package-lock.json"), - texts - }; -} - -function validatePackageMetadata(packageJson) { - if (packageJson.name !== EXPECTED_PACKAGE_NAME || packageJson.private !== false) { - throw new Error(`Release publishing requires public package ${EXPECTED_PACKAGE_NAME}.`); - } - if (!isAlphaVersion(packageJson.version)) { - throw new Error("Release publishing requires a 0.x.y-alpha.N version."); - } - if (repositoryUrl(packageJson) !== EXPECTED_REPOSITORY) { - throw new Error(`Release repository must be ${EXPECTED_REPOSITORY}.`); - } - if (packageJson.publishConfig?.access !== "public" || - packageJson.publishConfig?.provenance !== true || - packageJson.publishConfig?.tag !== "alpha") { - throw new Error("Release publishConfig must require public access, provenance, and the alpha dist-tag."); - } -} - -function validateLockfile(packageJson, packageLock) { - if (packageLock.name !== packageJson.name || packageLock.packages?.[""]?.name !== packageJson.name) { - throw new Error("package-lock.json package names must match package.json."); - } - if (packageLock.version !== packageJson.version || - packageLock.packages?.[""]?.version !== packageJson.version) { - throw new Error("package.json and both package-lock.json version fields must match exactly."); - } -} - -/** Validates a release-ready manifest, lockfile, and changelog. */ -export function validatePreparedReleaseState(state, options = {}) { - validatePackageMetadata(state.packageJson); - validateLockfile(state.packageJson, state.packageLock); - const version = state.packageJson.version; - if (options.expectedVersion !== undefined && version !== options.expectedVersion) { - throw new Error(`Release version must be ${options.expectedVersion}; received ${version}.`); - } - const tag = `v${version}`; - if (options.expectedTag !== undefined && tag !== options.expectedTag) { - throw new Error(`Release tag must be ${tag}; received ${options.expectedTag}.`); - } - if (state.changelog.releases[0]?.name !== version || - !hasMeaningfulNotes(state.changelog.releases[0]?.body ?? "")) { - throw new Error(`The newest changelog release must be meaningful [${version}].`); - } - if (options.requireEmptyUnreleased !== false && !isEmptyUnreleased(state.changelog.unreleased.body)) { - throw new Error("The [Unreleased] section must be empty before publishing."); - } - return { tag, version }; -} - -/** Calculates deterministic release-state file content without writing it. */ -export function createPreparedReleaseTexts(texts, options) { - if (!isValidUtcDate(options.date)) { - throw new Error("Release preparation requires a valid UTC date in YYYY-MM-DD form."); - } - const state = readReleaseStateTexts(texts); - validatePackageMetadata(state.packageJson); - validateLockfile(state.packageJson, state.packageLock); - if (state.changelog.releases[0]?.name !== state.packageJson.version) { - throw new Error( - `Current package version ${state.packageJson.version} must match the newest dated changelog release.` - ); - } - if (!hasMeaningfulNotes(state.changelog.unreleased.body)) { - throw new Error("The [Unreleased] section must contain at least one meaningful bullet note."); - } - const version = nextAlphaVersion(state.packageJson.version, options.bump); - if (state.changelog.releases.some((entry) => entry.name === version)) { - throw new Error(`Changelog release ${version} already exists.`); - } - if (state.changelog.releases.some((entry) => compareSemVerPrecedence(version, entry.name) <= 0)) { - throw new Error(`Next release ${version} must be greater than existing changelog releases.`); - } - - const packageJson = structuredClone(state.packageJson); - packageJson.version = version; - const packageLock = structuredClone(state.packageLock); - packageLock.version = version; - packageLock.packages[""].version = version; - const notes = state.changelog.unreleased.body.trim(); - const prefix = texts.changelog.slice(0, state.changelog.unreleased.headingEnd); - const suffix = texts.changelog.slice(state.changelog.unreleased.bodyEnd).trimStart(); - const changelog = `${prefix}\n\n${EMPTY_UNRELEASED}\n\n## [${version}] - ${options.date}\n\n${notes}\n\n${suffix}` - .trimEnd() + "\n"; - const preparedTexts = { - changelog, - packageJson: serializeJsonLike(texts.packageJson, packageJson), - packageLock: serializeJsonLike(texts.packageLock, packageLock) - }; - validatePreparedReleaseState(readReleaseStateTexts(preparedTexts), { expectedVersion: version }); - return { tag: `v${version}`, texts: preparedTexts, version }; -} - -/** Reads all release-state files from a repository root. */ -export async function readReleaseTexts(repositoryRoot = REPOSITORY_ROOT) { - const root = resolve(repositoryRoot); - const [changelog, packageJson, packageLock] = await Promise.all([ - readFile(join(root, "CHANGELOG.md"), "utf8"), - readFile(join(root, "package.json"), "utf8"), - readFile(join(root, "package-lock.json"), "utf8") - ]); - return { changelog, packageJson, packageLock }; -} - -/** Replaces the release-state files and restores originals on replacement failure. */ -export async function writeReleaseFilesAtomically(repositoryRoot, originalTexts, nextTexts, operations = {}) { - const root = resolve(repositoryRoot); - const renameFile = operations.rename ?? rename; - const removeFile = operations.rm ?? rm; - const write = operations.writeFile ?? writeFile; - const transaction = randomUUID(); - const entries = [ - { name: "CHANGELOG.md", original: originalTexts.changelog, value: nextTexts.changelog }, - { name: "package-lock.json", original: originalTexts.packageLock, value: nextTexts.packageLock }, - { name: "package.json", original: originalTexts.packageJson, value: nextTexts.packageJson } - ].map((entry) => ({ - ...entry, - backup: join(root, `${entry.name}.release-${transaction}.bak`), - path: join(root, entry.name), - temporary: join(root, `${entry.name}.release-${transaction}.tmp`) - })); - const backedUp = []; - let completed = false; - try { - for (const entry of entries) { - if (await readFile(entry.path, "utf8") !== entry.original) { - throw new Error(`${displayPath(entry.path, root)} changed during release preparation.`); - } - await write(entry.temporary, entry.value, { encoding: "utf8", flag: "wx" }); - } - for (const entry of entries) { - await renameFile(entry.path, entry.backup); - backedUp.push(entry); - await renameFile(entry.temporary, entry.path); - } - completed = true; - } catch (error) { - const rollbackErrors = []; - for (const entry of [...backedUp].reverse()) { - try { - await removeFile(entry.path, { force: true }); - await renameFile(entry.backup, entry.path); - } catch (rollbackError) { - rollbackErrors.push(rollbackError); - } - } - if (rollbackErrors.length > 0) { - throw new AggregateError( - rollbackErrors, - "Release preparation failed and rollback was incomplete.", - { cause: error } - ); - } - throw error; - } finally { - await Promise.all(entries.map((entry) => removeFile(entry.temporary, { force: true }))); - if (completed) { - await Promise.all(entries.map((entry) => removeFile(entry.backup, { force: true }))); - } - } -} - -/** Inspects the local Git identity used by release preparation and publication. */ -export async function inspectGitReleaseState(repositoryRoot, options = {}) { - const root = resolve(repositoryRoot); - const topLevel = resolve((await captureGit(["rev-parse", "--show-toplevel"], root)).value); - if (topLevel !== root) { - throw new Error(`Release command must run from repository root ${root}.`); - } - const head = (await captureGit(["rev-parse", "HEAD"], root)).value.toLowerCase(); - if (options.expectedCommit !== undefined && - (!FULL_COMMIT_PATTERN.test(options.expectedCommit) || head !== options.expectedCommit)) { - throw new Error(`Release commit must be current HEAD ${head}; received ${options.expectedCommit}.`); - } - const origin = (await captureGit(["config", "--get", "remote.origin.url"], root)).value; - if (normalizeOriginUrl(origin) !== "https://github.com/tryagaindev/litefold-calendar") { - throw new Error("Git origin must be the canonical tryagaindev/litefold-calendar repository."); - } - const status = (await captureGit(["status", "--porcelain=v1", "--untracked-files=all"], root)).value; - if (options.requireClean === true && status.length > 0) { - throw new Error("Release verification requires a clean Git worktree."); - } - - const tagState = options.tagState ?? "either"; - if (!new Set(["absent", "either", "matching"]).has(tagState)) { - throw new Error("Git tag state must be absent, either, or matching."); - } - let tagExists = false; - if (options.tag !== undefined) { - parseSemVer(options.tag.startsWith("v") ? options.tag.slice(1) : ""); - const reference = await captureGit( - ["show-ref", "--verify", "--hash", `refs/tags/${options.tag}`], - root, - true - ); - tagExists = reference.exists; - if (tagExists) { - const tagCommit = await captureGit(["rev-parse", `${options.tag}^{commit}`], root); - if (tagCommit.value.toLowerCase() !== head) { - throw new Error(`Existing tag ${options.tag} does not resolve to current HEAD ${head}.`); - } - } - if (tagState === "absent" && tagExists) { - throw new Error(`Release tag ${options.tag} already exists.`); - } - if (tagState === "matching" && !tagExists) { - throw new Error(`Release tag ${options.tag} does not exist locally.`); - } - } - return { clean: status.length === 0, head, origin, tagExists }; -} - -/** Verifies local release state without querying mutable hosted services. */ -export async function verifyRelease(options = {}) { - const repositoryRoot = resolve(options.repositoryRoot ?? REPOSITORY_ROOT); - if (process.env.GITHUB_REPOSITORY !== undefined && - process.env.GITHUB_REPOSITORY.toLowerCase() !== EXPECTED_GITHUB_REPOSITORY) { - throw new Error(`Release workflow must run from ${EXPECTED_GITHUB_REPOSITORY}.`); - } - const identity = validatePreparedReleaseState( - readReleaseStateTexts(await readReleaseTexts(repositoryRoot)), - { expectedTag: options.expectedTag, expectedVersion: options.expectedVersion } - ); - const git = await inspectGitReleaseState(repositoryRoot, { - expectedCommit: options.expectedCommit, - requireClean: options.requireClean, - tag: identity.tag, - tagState: options.tagState - }); - return { ...identity, git, status: "ready" }; -} - -/** Prepares deterministic release state after validating the current clean repository. */ -export async function prepareRelease(options) { - const repositoryRoot = resolve(options.repositoryRoot ?? REPOSITORY_ROOT); - const originalTexts = await readReleaseTexts(repositoryRoot); - const prepared = createPreparedReleaseTexts(originalTexts, { - bump: options.bump, - date: options.date ?? new Date().toISOString().slice(0, 10) - }); - const git = await inspectGitReleaseState(repositoryRoot, { - requireClean: options.requireClean !== false, - tag: prepared.tag, - tagState: "absent" - }); - if (options.dryRun !== true) { - await writeReleaseFilesAtomically(repositoryRoot, originalTexts, prepared.texts); - } - return { - branch: `release/${prepared.tag}`, - changedFiles: RELEASE_STATE_FILES, - dryRun: options.dryRun === true, - head: git.head, - status: options.dryRun === true ? "planned" : "prepared", - tag: prepared.tag, - version: prepared.version - }; -} diff --git a/scripts/lib/semver.mjs b/scripts/lib/semver.mjs index 3930eae..2a4bed8 100644 --- a/scripts/lib/semver.mjs +++ b/scripts/lib/semver.mjs @@ -1,7 +1,5 @@ import semver from "semver"; -const ALPHA_BUMPS = new Set(["prerelease", "prepatch", "preminor"]); - /** Parses a strict, normalized Semantic Version. */ export function parseSemVer(value) { const parsed = typeof value === "string" ? semver.parse(value) : null; @@ -25,30 +23,3 @@ export function compareSemVer(left, right) { parseSemVer(right); return semver.compareBuild(left, right); } - -/** Calculates the supported next public alpha version. */ -export function nextAlphaVersion(value, bump) { - parseSemVer(value); - if (!ALPHA_BUMPS.has(bump)) { - throw new Error(`Unsupported alpha version bump: ${String(bump)}`); - } - const next = semver.inc(value, bump, "alpha"); - if (next === null || !isAlphaVersion(next)) { - throw new Error(`Unable to calculate the next alpha version from ${value}.`); - } - return next; -} - -/** Identifies the repository's supported 0.x.y-alpha.N release shape. */ -export function isAlphaVersion(value) { - try { - const parsed = parseSemVer(value); - return parsed.major === 0 && - parsed.build.length === 0 && - parsed.prerelease.length === 2 && - parsed.prerelease[0] === "alpha" && - Number.isSafeInteger(parsed.prerelease[1]); - } catch { - return false; - } -} diff --git a/scripts/nightly-release.mjs b/scripts/nightly-release.mjs new file mode 100644 index 0000000..3d8fb68 --- /dev/null +++ b/scripts/nightly-release.mjs @@ -0,0 +1,40 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { createNightlyPlan, readNightlyPlan, nightlyPackageManifest } from "./lib/nightly-release.mjs"; +import { REPOSITORY_ROOT, run } from "./lib/process.mjs"; + +const [command, ...arguments_] = process.argv.slice(2); +const options = new Map(); +for (let index = 0; index < arguments_.length; index += 2) { + const key = arguments_[index]; + const value = arguments_[index + 1]; + if (!key?.startsWith("--") || value === undefined || options.has(key)) { + throw new Error("Nightly options require unique --name value pairs."); + } + options.set(key, value); +} +const manifest = JSON.parse(await readFile(join(REPOSITORY_ROOT, "package.json"), "utf8")); +let plan; +if (command === "plan") { + if ([...options.keys()].some((key) => !["--created-at", "--event", "--output", "--run-id", "--source-sha"].includes(key))) { + throw new Error("Unknown nightly plan option."); + } + plan = createNightlyPlan({ baseVersion: manifest.version, createdAt: options.get("--created-at"), + event: options.get("--event"), runId: options.get("--run-id"), sourceCommit: options.get("--source-sha") }); +} else if (command === "verify" && options.size === 1 && options.has("--plan")) { + plan = await readNightlyPlan(options.get("--plan")); +} else { + throw new Error("Use nightly-release.mjs plan --created-at UTC --event EVENT --run-id ID --source-sha SHA [--output PATH], or verify --plan PATH."); +} +nightlyPackageManifest(manifest, plan); +const head = (await run("git", ["rev-parse", "--verify", "HEAD^{commit}"], { capture: true })).stdout.trim(); +const status = (await run("git", ["status", "--porcelain=v1", "--untracked-files=all"], { capture: true })).stdout.trim(); +if (head !== plan.sourceCommit || status !== "") { + throw new Error("Nightly planning requires the exact committed, clean source checkout."); +} +const serialized = `${JSON.stringify(plan, null, 2)}\n`; +if (options.has("--output")) { + await writeFile(options.get("--output"), serialized, { flag: "wx" }); +} +console.log(serialized.trimEnd()); diff --git a/scripts/pack-nightly.mjs b/scripts/pack-nightly.mjs new file mode 100644 index 0000000..94f0f54 --- /dev/null +++ b/scripts/pack-nightly.mjs @@ -0,0 +1,8 @@ +import { run } from "./lib/process.mjs"; + +const arguments_ = process.argv.slice(2); +if (arguments_.length !== 2 || arguments_[0] !== "--plan" || !arguments_[1]) { + throw new Error("Usage: npm run package:nightly -- --plan "); +} +await run(process.execPath, ["scripts/nightly-release.mjs", "verify", ...arguments_]); +await run(process.execPath, ["scripts/pack-release.mjs", "--nightly-plan", arguments_[1]]); diff --git a/scripts/pack-release.mjs b/scripts/pack-release.mjs index cf52641..78e8c05 100644 --- a/scripts/pack-release.mjs +++ b/scripts/pack-release.mjs @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { copyFile, + cp, mkdtemp, readFile, readdir, @@ -24,6 +25,8 @@ import { } from "./lib/packed-extension-verification.mjs"; import { REPOSITORY_ROOT, run, runNpm, runTsc } from "./lib/process.mjs"; import { verifyPackedBrowserInteraction } from "./lib/packed-browser-verification.mjs"; +import { resolveBrowserTargets } from "./lib/browser-targets.mjs"; +import { readNightlyPlan, nightlyPackageManifest } from "./lib/nightly-release.mjs"; import { cleanupArtifactWorkspace, createArtifactWorkspace, @@ -298,7 +301,10 @@ async function verifyChecksums(artifactDirectory, orderedNames) { } const releaseArguments = parseReleaseArguments(process.argv.slice(2)); -const packageJson = JSON.parse(await readFile(join(REPOSITORY_ROOT, "package.json"), "utf8")); +const sourcePackageJson = JSON.parse(await readFile(join(REPOSITORY_ROOT, "package.json"), "utf8")); +const nightlyPlan = releaseArguments.nightlyPlanPath === undefined + ? null : await readNightlyPlan(releaseArguments.nightlyPlanPath); +const packageJson = nightlyPlan === null ? sourcePackageJson : nightlyPackageManifest(sourcePackageJson, nightlyPlan); const configuredNodeRange = packageJson.devEngines?.runtime?.version; if (configuredNodeRange !== SUPPORTED_NODE_RANGE) { throw new Error(`Release packaging requires package.json to declare Node ${SUPPORTED_NODE_RANGE}.`); @@ -318,12 +324,18 @@ if (requiredNpmVersion !== npmVersion) { const initialRepository = await repositoryState(); async function produceReleaseBundle(artifactDirectory) { - await run(process.execPath, [BUILD_SCRIPT]); + if (nightlyPlan !== null && nightlyPlan.sourceCommit !== initialRepository.commit) { + throw new Error("Nightly plan source differs from the clean package source."); + } + const browserTargetDate = nightlyPlan?.createdAt.slice(0, 10) ?? process.env.LFC_BROWSER_TARGET_DATE; + const browserTargets = resolveBrowserTargets(browserTargetDate === undefined ? {} : { date: browserTargetDate }); + const buildEnvironment = { ...process.env, LFC_BROWSER_TARGET_DATE: browserTargets.resolvedAt }; + await run(process.execPath, [BUILD_SCRIPT], { env: buildEnvironment }); const postBuildRepository = await repositoryState(); if (postBuildRepository.commit !== initialRepository.commit) { throw new Error("Git HEAD changed while rebuilding release output."); } - await run(process.execPath, [POLICY_SCRIPT, "--built", "--pack"]); + await run(process.execPath, [POLICY_SCRIPT, "--built", "--pack"], { env: buildEnvironment }); await copyFile(join(REPOSITORY_ROOT, LICENSE_FILENAME), join(artifactDirectory, LICENSE_FILENAME)); const sbomResult = await run(process.execPath, [ @@ -332,7 +344,8 @@ async function produceReleaseBundle(artifactDirectory) { "--source-commit", initialRepository.commit, "--source-date-epoch", - String(initialRepository.sourceDateEpoch) + String(initialRepository.sourceDateEpoch), + ...(nightlyPlan === null ? [] : ["--version", packageJson.version]) ], { capture: true }); const sbomBytes = `${JSON.stringify(JSON.parse(sbomResult.stdout), null, 2)}\n`; if (sbomResult.stdout !== sbomBytes) { @@ -344,13 +357,29 @@ async function produceReleaseBundle(artifactDirectory) { { encoding: "utf8", flag: "wx" } ); - const pack = await runNpm([ + let packageStaging = null; + let pack; + try { + if (nightlyPlan !== null) { + packageStaging = await mkdtemp(join(tmpdir(), "lfc-nightly-package-")); + await cp(join(REPOSITORY_ROOT, "dist"), join(packageStaging, "dist"), { recursive: true }); + for (const file of ["LICENSE", "README.md"]) { + await copyFile(join(REPOSITORY_ROOT, file), join(packageStaging, file)); + } + await writeFile(join(packageStaging, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`); + } + pack = await runNpm([ "pack", "--ignore-scripts", "--json", "--pack-destination", artifactDirectory - ], { capture: true }); + ], { capture: true, ...(packageStaging === null ? {} : { cwd: packageStaging }) }); + } finally { + if (packageStaging !== null) { + await rm(packageStaging, { recursive: true, force: true }); + } + } const packResult = inspectPackResult(JSON.parse(pack.stdout), packageJson); const tarballName = packResult.filename; const tarballPath = join(artifactDirectory, tarballName); @@ -370,7 +399,7 @@ async function produceReleaseBundle(artifactDirectory) { throw new Error("npm pack SHA-1 compatibility digest does not match the tarball bytes."); } const receipt = { - schemaVersion: 1, + schemaVersion: nightlyPlan === null ? 1 : 2, name: packageJson.name, version: packageJson.version, private: packageJson.private, @@ -378,6 +407,15 @@ async function produceReleaseBundle(artifactDirectory) { sourceRepository: repositoryUrl(packageJson), sourceCommit: initialRepository.commit, sourceTreeDirty: false, + browserTargets, + browserTargetsSha256: sha256(Buffer.from(JSON.stringify(browserTargets))), + ...(nightlyPlan === null ? {} : { + sourceVersion: sourcePackageJson.version, + nightly: nightlyPlan, + manifestTransform: "version-only", + sourceManifestSha256: sha256(Buffer.from(JSON.stringify(sourcePackageJson))), + publishedManifestSha256: sha256(Buffer.from(JSON.stringify(packageJson))) + }), license: packageJson.license, npmIntegrity, sha256: tarballSha256, diff --git a/scripts/pages-site/index.html b/scripts/pages-site/index.html index e56761b..6b794bb 100644 --- a/scripts/pages-site/index.html +++ b/scripts/pages-site/index.html @@ -49,7 +49,7 @@

      Install and render

      Terminal -
      npm install @tryagaindev/litefold-calendar@alpha
      +
      npm install @tryagaindev/litefold-calendar@nightly
      @@ -129,7 +129,7 @@

      Currently selected build

      Version
      -
      alpha dist-tag
      +
      nightly dist-tag
      Channel
      diff --git a/scripts/pages-site/site.js b/scripts/pages-site/site.js index ad4189f..16242ac 100644 --- a/scripts/pages-site/site.js +++ b/scripts/pages-site/site.js @@ -200,10 +200,10 @@ function renderPrimaryDeployment(documentReference, entry) { const isRelease = entry.channel === "release"; setText(documentReference, "my-install-command", - `npm install ${PACKAGE_NAME}@${isRelease ? entry.version : "alpha"}`); + `npm install ${PACKAGE_NAME}@${isRelease ? entry.version : "nightly"}`); setText(documentReference, "my-deployment-summary", isRelease ? `Running immutable release ${entry.version}. Source links are pinned to its commit.` - : `Running main at ${entry.commit}. The install command follows npm's alpha dist-tag.`); + : `Running main at ${entry.commit}. The install command follows npm's nightly dist-tag.`); } function renderUnavailablePrimary(documentReference) { diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs deleted file mode 100644 index d9a5fef..0000000 --- a/scripts/prepare-release.mjs +++ /dev/null @@ -1,83 +0,0 @@ -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -import { prepareRelease } from "./lib/release-state.mjs"; - -const SCRIPT_PATH = fileURLToPath(import.meta.url); -const BUMPS = new Set(["prerelease", "prepatch", "preminor"]); - -export function parsePrepareReleaseArguments(arguments_) { - let bump; - let dryRun = false; - let json = false; - for (let index = 0; index < arguments_.length; index += 1) { - const argument = arguments_[index]; - if (argument === "--bump") { - if (bump !== undefined || arguments_[index + 1] === undefined) { - throw new Error("--bump requires exactly one value."); - } - bump = arguments_[index += 1]; - continue; - } - if (argument === "--dry-run" && !dryRun) { - dryRun = true; - continue; - } - if (argument === "--json" && !json) { - json = true; - continue; - } - throw new Error( - "Usage: npm run release:prepare -- --bump prerelease|prepatch|preminor [--dry-run] [--json]" - ); - } - if (!BUMPS.has(bump)) { - throw new Error( - "Usage: npm run release:prepare -- --bump prerelease|prepatch|preminor [--dry-run] [--json]" - ); - } - return { bump, dryRun, json }; -} - -export function formatPrepareReleaseMessages(result) { - const action = result.dryRun - ? "Would prepare release files for" - : "Prepared release files for"; - const messages = [ - `${action} ${result.tag}; suggested hosted branch: ${result.branch}.`, - `Changed files: ${result.changedFiles.join(", ")}` - ]; - if (!result.dryRun) { - messages.push( - "Review the diff and run npm run release:verify. This local command creates no branch or pull request; use the hosted Prepare alpha release workflow for normal release work." - ); - } - return messages; -} - -export async function main(arguments_ = process.argv.slice(2)) { - const options = parsePrepareReleaseArguments(arguments_); - const result = await prepareRelease(options); - if (options.json) { - console.log(JSON.stringify(result)); - return result; - } - for (const message of formatPrepareReleaseMessages(result)) { - console.log(message); - } - return result; -} - -if (process.argv[1] !== undefined && resolve(process.argv[1]) === SCRIPT_PATH) { - try { - await main(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (process.argv.includes("--json")) { - console.error(JSON.stringify({ error: message, status: "error" })); - } else { - console.error(`Release preparation failed: ${message}`); - } - process.exitCode = 1; - } -} diff --git a/scripts/tests/build-pages.test.mjs b/scripts/tests/build-pages.test.mjs index 8ec52b8..a9ae0e6 100644 --- a/scripts/tests/build-pages.test.mjs +++ b/scripts/tests/build-pages.test.mjs @@ -1055,7 +1055,7 @@ void test("developer shell falls back to main and keeps static links on metadata ); assert.equal( dom.window.document.querySelector("#my-install-command")?.textContent, - "npm install @tryagaindev/litefold-calendar@alpha" + "npm install @tryagaindev/litefold-calendar@nightly" ); assert.match(dom.window.document.querySelector("#my-main-preview")?.textContent ?? "", new RegExp(SECOND_COMMIT, "u")); @@ -1088,7 +1088,7 @@ void test("developer shell falls back to main and keeps static links on metadata ); assert.equal( fallbackDom.window.document.querySelector("#my-install-command")?.textContent, - "npm install @tryagaindev/litefold-calendar@alpha" + "npm install @tryagaindev/litefold-calendar@nightly" ); dom.window.close(); fallbackDom.window.close(); @@ -1124,7 +1124,7 @@ void test("developer shell announces copy success and selects code when clipboar assert.equal(await copyCode(button, documentReference, { clipboard: { writeText: async (value) => { copied = value; } } }), "copied"); - assert.equal(copied, "npm install @tryagaindev/litefold-calendar@alpha"); + assert.equal(copied, "npm install @tryagaindev/litefold-calendar@nightly"); assert.equal(documentReference.querySelector("#my-copy-status")?.textContent, "Install command copied."); assert.equal(await copyCode(button, documentReference, { @@ -1132,7 +1132,7 @@ void test("developer shell announces copy success and selects code when clipboar }), "selected"); assert.equal( documentReference.defaultView?.getSelection()?.toString(), - "npm install @tryagaindev/litefold-calendar@alpha" + "npm install @tryagaindev/litefold-calendar@nightly" ); assert.match(documentReference.querySelector("#my-copy-status")?.textContent ?? "", /selected.*Ctrl\+C/u); dom.window.close(); diff --git a/scripts/tests/check-sbom.test.mjs b/scripts/tests/check-sbom.test.mjs index 23ee8ef..fbee474 100644 --- a/scripts/tests/check-sbom.test.mjs +++ b/scripts/tests/check-sbom.test.mjs @@ -248,3 +248,24 @@ test("check-sbom rejects invalid or incomplete source identity before invoking n }); } }); + + +test("check-sbom nightly overlay changes all SPDX package identities without changing source", async (t) => { + const fixture = await createFixture(t); + const version = "0.6.0-nightly.20260912090000.123456789"; + const { stdout } = await execFileAsync(process.execPath, + [CHECK_SBOM_PATH, ...jsonArguments(), "--version", version], + { cwd: REPOSITORY_ROOT, env: fixture.environment }); + const sbom = JSON.parse(stdout); + const runtime = sbom.packages[0]; + assert.equal(runtime.versionInfo, version); + assert.equal(runtime.externalRefs[0].referenceLocator, npmPurl(packageJson.name, version)); + assert.deepEqual(sbom.documentDescribes, [runtime.SPDXID]); + assert.equal(sbom.relationships[0].relatedSpdxElement, runtime.SPDXID); + assert.equal(sbom.name, `${packageJson.name}@${version}`); + assert.ok(sbom.documentNamespace.includes(version)); + assert.equal(JSON.parse(await readFile(join(REPOSITORY_ROOT, "package.json"), "utf8")).version, packageJson.version); + await assert.rejects(execFileAsync(process.execPath, + [CHECK_SBOM_PATH, ...jsonArguments(), "--version", "1.0.0"], + { cwd: REPOSITORY_ROOT, env: fixture.environment })); +}); diff --git a/scripts/tests/nightly-release.test.mjs b/scripts/tests/nightly-release.test.mjs new file mode 100644 index 0000000..ab00de1 --- /dev/null +++ b/scripts/tests/nightly-release.test.mjs @@ -0,0 +1,170 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createHash } from "node:crypto"; +import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { createNightlyPlan, nightlyPackageManifest, tokenExpiryStatus, validateNightlyPlan, + validateNightlyRegistryState, selectNightlyRecoveryArtifacts, verifyRestoredNightlyBundle } from "../lib/nightly-release.mjs"; +import { validateDeploymentMetadata } from "../build-pages.mjs"; +import { resolveBrowserTargets } from "../lib/browser-targets.mjs"; + +const INPUT = Object.freeze({ baseVersion: "0.6.0-nightly.0", createdAt: "2026-09-12T09:00:00Z", + event: "workflow_dispatch", runId: "123456789", sourceCommit: "a".repeat(40) }); +const PLAN = createNightlyPlan(INPUT); +const ALPHA = "0.5.0-alpha.0"; +const PREVIOUS = "0.6.0-nightly.20260911090000.123456780"; +const initialRegistry = () => ({ versions: [ALPHA], "dist-tags": { alpha: ALPHA, latest: ALPHA } }); + +void test("nightly versions derive from immutable run time and identity without attempt counters", () => { + assert.equal(PLAN.version, "0.6.0-nightly.20260912090000.123456789"); + assert.deepEqual(createNightlyPlan({ ...INPUT }), PLAN); + assert.notEqual(createNightlyPlan({ ...INPUT, runId: "123456790" }).version, PLAN.version); + assert.equal(createNightlyPlan({ ...INPUT, event: "schedule" }).version, PLAN.version); +}); + +void test("nightly plans reject mutable, malformed, stable, and arbitrary-ref inputs", () => { + for (const invalid of [{ baseVersion: "0.6.0" }, { baseVersion: "0.6.0-alpha.0" }, + { createdAt: "2026-02-30T09:00:00Z" }, { createdAt: "2026-09-12T02:00:00-07:00" }, + { runId: 123 }, { runId: "01" }, { runId: "9007199254740992" }, { sourceCommit: "main" }, { event: "push" }]) { + assert.throws(() => createNightlyPlan({ ...INPUT, ...invalid })); + } + assert.throws(() => validateNightlyPlan({ ...PLAN, version: "0.6.0" })); + assert.throws(() => validateNightlyPlan({ ...PLAN, workflow: ".github/workflows/other.yml" })); + assert.throws(() => validateNightlyPlan({ ...PLAN, attempt: 2 })); +}); + +void test("nightly package staging changes only version and never mutates source manifest", () => { + const source = { name: PLAN.name, version: INPUT.baseVersion, private: false, + publishConfig: { access: "public", provenance: true, tag: "nightly" }, files: ["dist"] }; + const before = structuredClone(source); + assert.deepEqual(nightlyPackageManifest(source, PLAN), { ...before, version: PLAN.version }); + assert.deepEqual(source, before); + assert.throws(() => nightlyPackageManifest({ ...source, version: "0.7.0-nightly.0" }, PLAN)); + assert.throws(() => nightlyPackageManifest({ ...source, publishConfig: { tag: "latest" } }, PLAN)); +}); + +void test("first nightly preserves historical alpha and identifies its published predecessor", () => { + assert.deepEqual(validateNightlyRegistryState(initialRegistry(), PLAN), { + existing: false, latest: ALPHA, predecessor: ALPHA, stableExists: false + }); +}); + +void test("ordinary nightlies require previous channel completion and increasing versions", () => { + const metadata = { versions: [ALPHA, PREVIOUS], "dist-tags": { alpha: ALPHA, nightly: PREVIOUS, latest: PREVIOUS } }; + assert.equal(validateNightlyRegistryState(metadata, PLAN).predecessor, PREVIOUS); + assert.throws(() => validateNightlyRegistryState({ ...metadata, + "dist-tags": { ...metadata["dist-tags"], latest: ALPHA } }, PLAN), /incomplete/u); + assert.throws(() => validateNightlyRegistryState({ ...metadata, + versions: [...metadata.versions, "0.7.0-nightly.20260912090000.123456790"] }, PLAN), /advance/u); +}); + +void test("exact candidate can resume between publish and latest synchronization", () => { + const metadata = { versions: [ALPHA, PLAN.version], "dist-tags": { alpha: ALPHA, nightly: PLAN.version, latest: ALPHA } }; + assert.equal(validateNightlyRegistryState(metadata, PLAN).existing, true); + metadata["dist-tags"].latest = PLAN.version; + assert.equal(validateNightlyRegistryState(metadata, PLAN).existing, true); + delete metadata["dist-tags"].nightly; + assert.throws(() => validateNightlyRegistryState(metadata, PLAN), /not selected/u); +}); + +void test("any stable publication permanently reserves latest for stable", () => { + const plan = createNightlyPlan({ ...INPUT, baseVersion: "1.1.0-nightly.0" }); + const metadata = { versions: [ALPHA, "1.0.0"], "dist-tags": { alpha: ALPHA, latest: "1.0.0" } }; + assert.equal(validateNightlyRegistryState(metadata, plan).stableExists, true); + metadata["dist-tags"].latest = ALPHA; + assert.throws(() => validateNightlyRegistryState(metadata, plan), /belongs to stable/u); +}); + +void test("missing and malformed registry evidence cannot become an empty release history", () => { + for (const metadata of [null, {}, { versions: [], "dist-tags": {} }, + { versions: [ALPHA], "dist-tags": { latest: "missing" } }, + { versions: ["bad"], "dist-tags": { alpha: "bad", latest: "bad" } }]) { + assert.throws(() => validateNightlyRegistryState(metadata, PLAN)); + } +}); + +void test("token expiry blocks publication and warns during its last seven days", () => { + const now = new Date("2026-09-12T09:00:00Z"); + assert.equal(tokenExpiryStatus("2026-10-12T09:00:00Z", now).warn, false); + assert.equal(tokenExpiryStatus("2026-09-19T09:00:00Z", now).warn, true); + assert.throws(() => tokenExpiryStatus("2026-09-12T09:00:00Z", now), /expired/u); + assert.throws(() => tokenExpiryStatus(undefined, now)); + assert.throws(() => tokenExpiryStatus("2026-02-30T09:00:00Z", new Date("2026-01-01T00:00:00Z")), /UTC timestamp/u); +}); + +void test("nightly Pages binds a generated package version to the exact source and receipt", () => { + const metadata = { channel: "release", commit: PLAN.sourceCommit, version: PLAN.version }; + const receipt = { schemaVersion: 2, sourceTreeDirty: false, manifestTransform: "version-only", + sourceVersion: PLAN.baseVersion, sourceCommit: PLAN.sourceCommit, version: PLAN.version, nightly: PLAN }; + assert.deepEqual(validateDeploymentMetadata(metadata, PLAN.baseVersion, receipt), metadata); + for (const changed of [{ sourceCommit: "b".repeat(40) }, { sourceVersion: "0.7.0-nightly.0" }, + { sourceTreeDirty: true }, { manifestTransform: "anything" }, { schemaVersion: 1 }]) { + assert.throws(() => validateDeploymentMetadata(metadata, PLAN.baseVersion, { ...receipt, ...changed })); + } + assert.throws(() => validateDeploymentMetadata(metadata, PLAN.baseVersion)); +}); + + +void test("recovery selects the original complete same-run artifacts and never rebuilds public state", () => { + const digest = `sha256:${"a".repeat(64)}`; + const pair = (attempt) => [ + { id: attempt * 2, name: `npm-release-bundle-v${PLAN.version}-${PLAN.runId}-${attempt}`, expired: false, digest }, + { id: attempt * 2 + 1, name: `release-notes-v${PLAN.version}-${PLAN.runId}-${attempt}`, expired: false, digest } + ]; + const first = pair(1); + const options = { artifacts: [...first, ...pair(2)], plan: PLAN, attempt: 3, hasPublicState: true }; + assert.deepEqual(selectNightlyRecoveryArtifacts(options), { attempt: 1, bundle: first[0], notes: first[1] }); + assert.equal(selectNightlyRecoveryArtifacts({ ...options, artifacts: [], hasPublicState: false }), null); + assert.throws(() => selectNightlyRecoveryArtifacts({ ...options, artifacts: [] }), /refusing to rebuild/u); + assert.throws(() => selectNightlyRecoveryArtifacts({ ...options, artifacts: [first[0]] }), /refusing to rebuild/u); + assert.throws(() => selectNightlyRecoveryArtifacts({ ...options, artifacts: [...first, first[0]] }), /ambiguous/u); + assert.throws(() => selectNightlyRecoveryArtifacts({ ...options, artifacts: [{ ...first[0], expired: true }, first[1]] }), /expired/u); + assert.throws(() => selectNightlyRecoveryArtifacts({ ...options, artifacts: [{ ...first[0], digest: null }, first[1]] }), /digest/u); + assert.throws(() => selectNightlyRecoveryArtifacts({ ...options, attempt: 1 }), /refusing to rebuild/u); +}); + +void test("restored bundle validation preserves bytes and rejects rewritten source, targets, and payload", async (context) => { + const directory = await mkdtemp(join(tmpdir(), "lfc-retained-nightly-")); + context.after(() => rm(directory, { recursive: true, force: true })); + const hash = (value, algorithm = "sha256", encoding = "hex") => createHash(algorithm).update(value).digest(encoding); + const targets = resolveBrowserTargets({ date: PLAN.createdAt.slice(0, 10) }); + const source = { name: PLAN.name, version: PLAN.baseVersion, private: false, + devDependencies: { vite: targets.dataVersions.vite, esbuild: targets.dataVersions.esbuild }, + publishConfig: { access: "public", provenance: true, tag: "nightly" } }; + const bytes = Buffer.from("the original already verified tarball"); + const tarball = `tryagaindev-litefold-calendar-${PLAN.version}.tgz`; + const receipt = { schemaVersion: 2, name: PLAN.name, version: PLAN.version, + sourceVersion: PLAN.baseVersion, sourceCommit: PLAN.sourceCommit, sourceTreeDirty: false, + manifestTransform: "version-only", nightly: PLAN, + sourceManifestSha256: hash(JSON.stringify(source)), + publishedManifestSha256: hash(JSON.stringify(nightlyPackageManifest(source, PLAN))), + sha256: hash(bytes), npmIntegrity: `sha512-${hash(bytes, "sha512", "base64")}`, + browserTargets: targets, browserTargetsSha256: hash(JSON.stringify(targets)) }; + async function writeBundle(changes = {}, payload = bytes) { + const files = new Map([[tarball, payload], ["package-verification.json", JSON.stringify({ ...receipt, ...changes })], + ["sbom.spdx.json", "{}"], ["LICENSE", "MIT"]]); + for (const [name, value] of files) { + await writeFile(join(directory, name), value); + } + await writeFile(join(directory, "SHA256SUMS"), [...files].map(([name, value]) => `${hash(value)} ${name}\n`).join("")); + } + await writeBundle(); + assert.deepEqual(await verifyRestoredNightlyBundle(directory, PLAN, source), receipt); + assert.deepEqual(await readFile(join(directory, tarball)), bytes); + await writeFile(join(directory, tarball), "modified without checksum"); + await assert.rejects(verifyRestoredNightlyBundle(directory, PLAN, source), /checksums/u); + for (const changes of [{ sourceCommit: "b".repeat(40) }, { sourceManifestSha256: "b".repeat(64) }, + { nightly: { ...PLAN, runId: "999" } }, { browserTargets: { ...targets, resolvedAt: "2026-09-13" } }, + { browserTargets: { ...targets, query: "baseline widely available" } }, + { browserTargets: { ...targets, dataVersions: { ...targets.dataVersions, vite: "invalid" } } }]) { + await writeBundle(changes); + await assert.rejects(verifyRestoredNightlyBundle(directory, PLAN, source)); + } + await writeBundle({}, Buffer.from("different payload with rewritten checksums")); + await assert.rejects(verifyRestoredNightlyBundle(directory, PLAN, source), /receipt/u); + await writeBundle(); + await writeFile(join(directory, "extra.txt"), "unexpected"); + await assert.rejects(verifyRestoredNightlyBundle(directory, PLAN, source), /exactly five/u); +}); diff --git a/scripts/tests/publish-alpha-policy.test.mjs b/scripts/tests/publish-nightly-policy.test.mjs similarity index 86% rename from scripts/tests/publish-alpha-policy.test.mjs rename to scripts/tests/publish-nightly-policy.test.mjs index 5849bc8..3498244 100644 --- a/scripts/tests/publish-alpha-policy.test.mjs +++ b/scripts/tests/publish-nightly-policy.test.mjs @@ -10,9 +10,9 @@ import { promisify } from "node:util"; import { REPOSITORY_ROOT, runNpm } from "../lib/process.mjs"; const execFileAsync = promisify(execFile); -const WORKFLOW_PATH = join(REPOSITORY_ROOT, ".github", "workflows", "publish-alpha.yml"); +const WORKFLOW_PATH = join(REPOSITORY_ROOT, ".github", "workflows", "publish-nightly.yml"); const FIXTURE_DIRECTORY = join(REPOSITORY_ROOT, "scripts", "tests", "fixtures", "npm-view-json"); -const VERSION = "0.2.0-alpha.0"; +const VERSION = "0.6.0-nightly.20260912090000.123456789"; const SOURCE_COMMIT = "a".repeat(40); const EXPECTED_DIGEST = Buffer.alloc(64, 0xa5); const EXPECTED_INTEGRITY = `sha512-${EXPECTED_DIGEST.toString("base64")}`; @@ -36,7 +36,7 @@ function inlineModule(source, marker) { const lines = source.split(/\r?\n/u); const start = lines.findIndex((line) => line.trim() === `# ${marker}_START`); assert.notEqual(start, -1, `Expected inline module marker ${marker}.`); - assert.match(lines[start + 1].trim(), /^node --input-type=module <<'NODE'$/u); + assert.match(lines[start + 1].trim(), /^(?:if )?node --input-type=module <<'NODE'$/u); const end = lines.findIndex( (line, index) => index > start + 1 && line.trim() === "NODE" ); @@ -92,67 +92,6 @@ void test("the workflow normalizer accepts only npm 12 single-result arrays", as } }); -void test("the prerelease channel policy converges alpha and latest on the candidate", async () => { - const workflow = await readFile(WORKFLOW_PATH, "utf8"); - const policy = inlineModule(workflow, "LFC_PRERELEASE_CHANNEL_POLICY"); - const candidate = "0.3.0-alpha.0"; - const runPolicy = (metadata) => runInlineModule(policy, { - cwd: REPOSITORY_ROOT, - env: { - ...process.env, - LFC_REGISTRY_METADATA: JSON.stringify(metadata), - LFC_VERSION: candidate - } - }); - const versions = ["0.1.0-alpha.0", "0.2.0-alpha.0"]; - - for (const metadata of [ - { - "dist-tags": { alpha: "0.2.0-alpha.0", latest: "0.2.0-alpha.0" }, - versions - }, - { - "dist-tags": { alpha: candidate, latest: "0.2.0-alpha.0" }, - versions: [...versions, candidate] - }, - { - "dist-tags": { alpha: candidate, latest: candidate }, - versions: [...versions, candidate] - } - ]) { - await runPolicy(metadata); - } - - for (const metadata of [ - { "dist-tags": {}, versions: [] }, - { "dist-tags": { alpha: "0.2.0-alpha.0" }, versions }, - { - "dist-tags": { alpha: "0.2.0-alpha.0", latest: "0.1.0-alpha.0" }, - versions - }, - { - "dist-tags": { alpha: "0.2.0-alpha.1", latest: "0.1.0-alpha.0" }, - versions: [...versions, "0.2.0-alpha.1"] - }, - { - "dist-tags": { alpha: "0.2.0-alpha.0", latest: "0.2.0" }, - versions: [...versions, "0.2.0"] - }, - { - "dist-tags": { alpha: "0.2.0-alpha.0", latest: "0.2.1-alpha.0" }, - versions - }, - { - "dist-tags": { alpha: "0.4.0-alpha.0", latest: "0.4.0-alpha.0" }, - versions: [...versions, "0.4.0-alpha.0"] - }, - { "dist-tags": [], versions: [] }, - { "dist-tags": { alpha: "0.2.0-alpha.0", latest: "0.2.0-alpha.0" }, versions: [] } - ]) { - await assert.rejects(runPolicy(metadata)); - } -}); - void test("the pinned npm CLI emits the captured npm 12 view shapes", async (context) => { const workflow = await readFile(WORKFLOW_PATH, "utf8"); const pinnedVersion = /LFC_NPM_VERSION: "([^"]+)"/u.exec(workflow)?.[1]; @@ -277,7 +216,7 @@ void test("the workflow accepts the tested GitHub CLI version and newer releases }); const PROVENANCE_ENVIRONMENT = { - GITHUB_EVENT_NAME: "push", + GITHUB_EVENT_NAME: "workflow_dispatch", GITHUB_REF: "refs/heads/main", GITHUB_REPOSITORY: REPOSITORY, GITHUB_REPOSITORY_ID: "987654321", @@ -288,7 +227,7 @@ const PROVENANCE_ENVIRONMENT = { GITHUB_SERVER_URL: SERVER_URL, GITHUB_SHA: SOURCE_COMMIT, GITHUB_WORKFLOW_REF: - `${REPOSITORY}/.github/workflows/publish-alpha.yml@refs/heads/main`, + `${REPOSITORY}/.github/workflows/publish-nightly.yml@refs/heads/main`, GITHUB_WORKFLOW_SHA: SOURCE_COMMIT, LFC_EXPECTED_INTEGRITY: EXPECTED_INTEGRITY, LFC_SOURCE_COMMIT: SOURCE_COMMIT, @@ -307,14 +246,14 @@ function provenanceStatement(attempt = 3) { buildType: "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1", externalParameters: { workflow: { - path: ".github/workflows/publish-alpha.yml", + path: ".github/workflows/publish-nightly.yml", ref: "refs/heads/main", repository: `${SERVER_URL}/${REPOSITORY}` } }, internalParameters: { github: { - event_name: "push", + event_name: "workflow_dispatch", repository_id: PROVENANCE_ENVIRONMENT.GITHUB_REPOSITORY_ID, repository_owner_id: PROVENANCE_ENVIRONMENT.GITHUB_REPOSITORY_OWNER_ID } @@ -355,13 +294,13 @@ function provenanceBundle(statement = provenanceStatement()) { function provenanceCertificate(attempt = 3) { const workflowIdentity = - `${SERVER_URL}/${REPOSITORY}/.github/workflows/publish-alpha.yml@refs/heads/main`; + `${SERVER_URL}/${REPOSITORY}/.github/workflows/publish-nightly.yml@refs/heads/main`; return { buildConfigDigest: SOURCE_COMMIT, buildConfigURI: workflowIdentity, buildSignerDigest: SOURCE_COMMIT, buildSignerURI: workflowIdentity, - buildTrigger: "push", + buildTrigger: "workflow_dispatch", issuer: "https://token.actions.githubusercontent.com", runInvocationURI: invocation(attempt), runnerEnvironment: "github-hosted", @@ -479,6 +418,11 @@ void test("the workflow provenance policy accepts the current or an earlier atte const policy = inlineModule(workflow, "LFC_PROVENANCE_POLICY"); await runPolicy(context, policy, { attempt: 3 }); await runPolicy(context, policy, { attempt: 1 }); + const statement = provenanceStatement(); + statement.predicate.buildDefinition.internalParameters.github.event_name = "schedule"; + const certificate = provenanceCertificate(); + certificate.buildTrigger = "schedule"; + await runPolicy(context, policy, { statement, certificate, environment: { GITHUB_EVENT_NAME: "schedule" } }); }); void test("the workflow provenance policy rejects conflicting source and builder identities", async (context) => { @@ -490,7 +434,7 @@ void test("the workflow provenance policy rejects conflicting source and builder (options) => { options.statement.predicate.buildDefinition.externalParameters.workflow.ref = "refs/heads/other"; }, (options) => { options.statement.predicate.buildDefinition.externalParameters.workflow.path = ".github/workflows/other.yml"; }, (options) => { options.statement.predicate.buildDefinition.resolvedDependencies[0].digest.gitCommit = "b".repeat(40); }, - (options) => { options.statement.predicate.buildDefinition.internalParameters.github.event_name = "workflow_dispatch"; }, + (options) => { options.statement.predicate.buildDefinition.internalParameters.github.event_name = "push"; }, (options) => { options.statement.predicate.runDetails.builder.id = "https://github.com/actions/runner/self-hosted"; }, (options) => { options.statement.predicate.runDetails.metadata.invocationId = invocation(4); }, (options) => { options.statement.predicate.runDetails.metadata.invocationId = invocation(1, "987654321"); }, @@ -499,13 +443,13 @@ void test("the workflow provenance policy rejects conflicting source and builder (options) => { options.certificate.runnerEnvironment = "self-hosted"; }, (options) => { options.certificate.sourceRepositoryIdentifier = "1"; }, (options) => { options.certificate.runInvocationURI = invocation(2); }, - (options) => { options.certificate.buildTrigger = "workflow_dispatch"; }, + (options) => { options.certificate.buildTrigger = "push"; }, (options) => { options.environment.GITHUB_REPOSITORY = "attacker/repository"; }, (options) => { - options.environment.GITHUB_EVENT_NAME = "workflow_dispatch"; + options.environment.GITHUB_EVENT_NAME = "push"; options.statement.predicate.buildDefinition.internalParameters.github.event_name = - "workflow_dispatch"; - options.certificate.buildTrigger = "workflow_dispatch"; + "push"; + options.certificate.buildTrigger = "push"; }, (options) => { options.environment.GITHUB_EVENT_NAME = "pull_request"; @@ -530,3 +474,30 @@ void test("the workflow provenance policy rejects conflicting source and builder await assert.rejects(runPolicy(context, policy, options)); } }); + + +void test("the source-free latest policy waits for visibility and preserves stable ownership", async (context) => { + const workflow = await readFile(WORKFLOW_PATH, "utf8"); + const policy = inlineModule(workflow, "LFC_NIGHTLY_LATEST_POLICY"); + const directory = await temporaryDirectory(context, "lfc-nightly-latest-"); + const output = join(directory, "output.txt"); + const previous = "0.5.0-alpha.3"; + async function check({ versions = [previous, VERSION], latest = previous, nightly = VERSION, + integrity = EXPECTED_INTEGRITY } = {}) { + await writeFile(join(directory, "registry.json"), JSON.stringify({ versions, "dist-tags": { latest, nightly } })); + await writeFile(join(directory, "integrity.json"), JSON.stringify(integrity)); + await writeFile(output, ""); + await runInlineModule(policy, { cwd: directory, env: { ...process.env, + LFC_VERSION: VERSION, LFC_EXPECTED_INTEGRITY: EXPECTED_INTEGRITY, GITHUB_OUTPUT: output } }); + return readFile(output, "utf8"); + } + assert.equal(await check(), `latest-target=${VERSION}\nwrite=true\n`); + assert.equal(await check({ latest: VERSION }), `latest-target=${VERSION}\nwrite=false\n`); + assert.equal(await check({ versions: [previous, "0.5.0", VERSION], latest: "0.5.0" }), + "latest-target=0.5.0\nwrite=false\n"); + await assert.rejects(check({ versions: [previous] }), { code: 75 }); + await assert.rejects(check({ nightly: previous }), { code: 75 }); + await assert.rejects(check({ integrity: null }), { code: 75 }); + await assert.rejects(check({ integrity: "sha512-conflicting" }), /registry integrity changed/u); + await assert.rejects(check({ versions: [previous, "0.5.0", VERSION] }), /Stable owns latest/u); +}); diff --git a/scripts/tests/release-artifacts.test.mjs b/scripts/tests/release-artifacts.test.mjs index 7af14d5..635aef2 100644 --- a/scripts/tests/release-artifacts.test.mjs +++ b/scripts/tests/release-artifacts.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; import { tmpdir } from "node:os"; import test from "node:test"; @@ -16,8 +16,12 @@ async function createRepository() { return mkdtemp(join(tmpdir(), "lfc-release-artifacts-test-")); } -void test("release arguments accept only one optional verify-only flag", () => { +void test("release arguments distinguish temporary checks from explicit nightly plans", () => { assert.deepEqual(parseReleaseArguments([]), { verifyOnly: false }); + assert.deepEqual(parseReleaseArguments(["--nightly-plan", "plan.json"]), + { nightlyPlanPath: resolve("plan.json"), verifyOnly: false }); + assert.throws(() => parseReleaseArguments(["--nightly-plan"]), /Usage/u); + assert.throws(() => parseReleaseArguments(["--nightly-plan", "plan.json", "--verify-only"]), /Usage/u); assert.deepEqual(parseReleaseArguments(["--verify-only"]), { verifyOnly: true }); assert.throws(() => parseReleaseArguments(["--unknown"]), /Usage/u); assert.throws( diff --git a/scripts/tests/release-state.test.mjs b/scripts/tests/release-state.test.mjs deleted file mode 100644 index ac9b820..0000000 --- a/scripts/tests/release-state.test.mjs +++ /dev/null @@ -1,271 +0,0 @@ -import assert from "node:assert/strict"; -import { execFile } from "node:child_process"; -import { mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import test from "node:test"; -import { promisify } from "node:util"; - -import { parseReleaseVerificationArguments } from "../check-release-ready.mjs"; -import { - createPreparedReleaseTexts, - inspectGitReleaseState, - readReleaseStateTexts, - validatePreparedReleaseState, - writeReleaseFilesAtomically -} from "../lib/release-state.mjs"; -import { - formatPrepareReleaseMessages, - parsePrepareReleaseArguments -} from "../prepare-release.mjs"; - -const execFileAsync = promisify(execFile); - -function fixture(overrides = {}) { - const version = overrides.version ?? "0.2.0-alpha.0"; - const packageJson = { - name: "@tryagaindev/litefold-calendar", - version, - private: false, - repository: { - type: "git", - url: "git+https://github.com/tryagaindev/litefold-calendar.git" - }, - publishConfig: { access: "public", provenance: true, tag: "alpha" } - }; - const packageLock = { - name: packageJson.name, - version, - lockfileVersion: 3, - packages: { "": { name: packageJson.name, version } } - }; - return { - changelog: overrides.changelog ?? [ - "# Changelog", - "", - "## [Unreleased]", - "", - "### Added", - "", - "- Added a useful feature.", - "", - "## [0.2.0-alpha.0] - 2026-08-25", - "", - "- Added the previous feature.", - "" - ].join("\n"), - packageJson: `${JSON.stringify(packageJson, null, 2)}\n`, - packageLock: `${JSON.stringify(packageLock, null, 2)}\n` - }; -} - -void test("release preparation updates the manifests and rotates changelog notes", () => { - for (const [bump, expectedVersion] of [ - ["prerelease", "0.2.0-alpha.1"], - ["prepatch", "0.2.1-alpha.0"], - ["preminor", "0.3.0-alpha.0"] - ]) { - const prepared = createPreparedReleaseTexts(fixture(), { - bump, - date: "2026-08-26" - }); - const state = readReleaseStateTexts(prepared.texts); - assert.equal(prepared.version, expectedVersion); - assert.equal(state.packageJson.version, expectedVersion); - assert.equal(state.packageLock.version, expectedVersion); - assert.equal(state.packageLock.packages[""].version, expectedVersion); - assert.match(state.changelog.releases[0].body, /Added a useful feature/u); - assert.doesNotMatch(state.changelog.unreleased.body, /Added a useful feature/u); - assert.deepEqual(validatePreparedReleaseState(state), { - tag: `v${expectedVersion}`, - version: expectedVersion - }); - } -}); - -void test("release preparation rejects placeholders and inconsistent state", () => { - for (const placeholder of ["- TODO", "- TBD: write the release note", "- "]) { - const texts = fixture({ - changelog: fixture().changelog.replace("- Added a useful feature.", placeholder) - }); - assert.throws( - () => createPreparedReleaseTexts(texts, { bump: "prerelease", date: "2026-08-26" }), - /meaningful bullet/u - ); - } - assert.throws( - () => createPreparedReleaseTexts(fixture({ version: "0.1.0-alpha.0" }), { - bump: "prerelease", - date: "2026-08-26" - }), - /package version.*newest dated changelog release/iu - ); - const prepared = createPreparedReleaseTexts(fixture(), { - bump: "prerelease", - date: "2026-08-26" - }); - prepared.texts.packageLock = prepared.texts.packageLock.replace( - '"version": "0.2.0-alpha.1"', - '"version": "0.2.0-alpha.0"' - ); - assert.throws( - () => validatePreparedReleaseState(readReleaseStateTexts(prepared.texts)), - /version fields must match/u - ); -}); - -void test("changelog HTML comments cannot hide or manufacture release state", () => { - const unterminated = fixture({ - changelog: fixture().changelog.replace( - ["### Added", "", "- Added a useful feature."].join("\n"), - [""].join("\n") - ) - }); - assert.deepEqual( - readReleaseStateTexts(hiddenHeading).changelog.releases.map((entry) => entry.name), - ["0.2.0-alpha.0"] - ); - - const visibleNote = fixture({ - changelog: fixture().changelog.replace( - "- Added a useful feature.", - ["", "- Added a visible feature."].join("\n") - ) - }); - const prepared = createPreparedReleaseTexts(visibleNote, { - bump: "prerelease", - date: "2026-08-26" - }); - assert.match(prepared.texts.changelog, /Added a visible feature/u); - assert.match(prepared.texts.changelog, /Add user-visible changes/u); - assert.deepEqual(validatePreparedReleaseState(readReleaseStateTexts(prepared.texts)), { - tag: "v0.2.0-alpha.1", - version: "0.2.0-alpha.1" - }); - - const astralComment = fixture({ - changelog: fixture().changelog.replace( - ["### Added", "", "- Added a useful feature.", ""].join("\n"), - ["", "", "- Added a visible feature.", ""].join("\n") - ) - }); - assert.throws( - () => validatePreparedReleaseState(readReleaseStateTexts(astralComment)), - /\[Unreleased\] section must be empty/u - ); -}); - -void test("atomic release writes restore originals after a replacement failure", async (context) => { - const repositoryRoot = await mkdtemp(join(tmpdir(), "lfc-release-state-test-")); - context.after(() => rm(repositoryRoot, { force: true, recursive: true })); - const original = fixture(); - const prepared = createPreparedReleaseTexts(original, { - bump: "prerelease", - date: "2026-08-26" - }); - await Promise.all([ - writeFile(join(repositoryRoot, "CHANGELOG.md"), original.changelog, "utf8"), - writeFile(join(repositoryRoot, "package-lock.json"), original.packageLock, "utf8"), - writeFile(join(repositoryRoot, "package.json"), original.packageJson, "utf8") - ]); - let renameCount = 0; - await assert.rejects( - writeReleaseFilesAtomically(repositoryRoot, original, prepared.texts, { - rename: async (...arguments_) => { - renameCount += 1; - if (renameCount === 4) { - throw new Error("simulated rename failure"); - } - await rename(...arguments_); - } - }), - /simulated rename failure/u - ); - assert.deepEqual({ - changelog: await readFile(join(repositoryRoot, "CHANGELOG.md"), "utf8"), - packageJson: await readFile(join(repositoryRoot, "package.json"), "utf8"), - packageLock: await readFile(join(repositoryRoot, "package-lock.json"), "utf8") - }, original); -}); - -void test("release CLI parsers expose preparation and local verification modes", () => { - assert.deepEqual( - parsePrepareReleaseArguments(["--bump", "preminor", "--dry-run", "--json"]), - { bump: "preminor", dryRun: true, json: true } - ); - assert.deepEqual( - parseReleaseVerificationArguments([ - "--version", "0.3.0-alpha.0", - "--commit", "a".repeat(40), - "--tag-state", "absent", - "--require-clean", - "--json" - ]), - { - expectedCommit: "a".repeat(40), - expectedVersion: "0.3.0-alpha.0", - json: true, - requireClean: true, - tagState: "absent" - } - ); - assert.throws(() => parseReleaseVerificationArguments(["--online"]), /Usage/u); -}); - -void test("local release preparation reports only state it creates", () => { - const result = { - branch: "release/v0.3.0-alpha.0", - changedFiles: ["CHANGELOG.md", "package-lock.json", "package.json"], - dryRun: false, - tag: "v0.3.0-alpha.0" - }; - assert.deepEqual(formatPrepareReleaseMessages(result), [ - "Prepared release files for v0.3.0-alpha.0; suggested hosted branch: release/v0.3.0-alpha.0.", - "Changed files: CHANGELOG.md, package-lock.json, package.json", - "Review the diff and run npm run release:verify. This local command creates no branch or pull request; use the hosted Prepare alpha release workflow for normal release work." - ]); - assert.deepEqual(formatPrepareReleaseMessages({ ...result, dryRun: true }), [ - "Would prepare release files for v0.3.0-alpha.0; suggested hosted branch: release/v0.3.0-alpha.0.", - "Changed files: CHANGELOG.md, package-lock.json, package.json" - ]); -}); - -void test("Git verification binds an existing tag to the current commit", async (context) => { - const repositoryRoot = await mkdtemp(join(tmpdir(), "lfc-release-git-test-")); - context.after(() => rm(repositoryRoot, { force: true, recursive: true })); - const git = (...arguments_) => execFileAsync("git", arguments_, { cwd: repositoryRoot }); - await git("init", "--initial-branch=main"); - await git("config", "user.name", "Release Test"); - await git("config", "user.email", "release-test@example.invalid"); - await git("remote", "add", "origin", "https://github.com/tryagaindev/litefold-calendar.git"); - await writeFile(join(repositoryRoot, "fixture.txt"), "fixture\n", "utf8"); - await git("add", "fixture.txt"); - await git("commit", "-m", "test fixture"); - await git("tag", "v0.3.0-alpha.0"); - const state = await inspectGitReleaseState(repositoryRoot, { - tag: "v0.3.0-alpha.0", - tagState: "matching" - }); - assert.equal(state.tagExists, true); - await assert.rejects( - inspectGitReleaseState(repositoryRoot, { - tag: "v0.3.0-alpha.0", - tagState: "absent" - }), - /already exists/u - ); -}); diff --git a/scripts/tests/semver.test.mjs b/scripts/tests/semver.test.mjs index 582bf96..f373038 100644 --- a/scripts/tests/semver.test.mjs +++ b/scripts/tests/semver.test.mjs @@ -4,8 +4,6 @@ import test from "node:test"; import { compareSemVer, compareSemVerPrecedence, - isAlphaVersion, - nextAlphaVersion, parseSemVer } from "../lib/semver.mjs"; @@ -36,13 +34,3 @@ void test("SemVer parser rejects leading zeroes and malformed identifiers", () = assert.throws(() => parseSemVer(value), /Invalid Semantic Version/u); } }); - -void test("alpha bump choices calculate the intended next version", () => { - assert.equal(nextAlphaVersion("0.2.0-alpha.9", "prerelease"), "0.2.0-alpha.10"); - assert.equal(nextAlphaVersion("0.2.0-alpha.9", "prepatch"), "0.2.1-alpha.0"); - assert.equal(nextAlphaVersion("0.2.0-alpha.9", "preminor"), "0.3.0-alpha.0"); - assert.equal(isAlphaVersion("0.2.0-alpha.0"), true); - assert.equal(isAlphaVersion("0.2.0-alpha.0+rebuilt"), false); - assert.equal(isAlphaVersion("1.0.0-alpha.0"), false); - assert.throws(() => nextAlphaVersion("0.2.0-alpha.0", "major"), /Unsupported alpha version bump/u); -}); diff --git a/scripts/tests/workflow-contracts.test.mjs b/scripts/tests/workflow-contracts.test.mjs index 5aa4eb6..36fdca3 100644 --- a/scripts/tests/workflow-contracts.test.mjs +++ b/scripts/tests/workflow-contracts.test.mjs @@ -36,88 +36,56 @@ function occurrences(source, pattern) { return [...source.matchAll(pattern)].length; } -void test("release preparation creates only the reviewed three-file branch", async () => { - const source = await workflow("prepare-alpha.yml"); - const prepare = job(source, "prepare"); - const createBranch = job(source, "create-branch"); - assert.match(trigger(source), /workflow_dispatch:[\s\S]*?Continue alpha[\s\S]*?Next patch alpha[\s\S]*?Next minor alpha/u); - assert.match(prepare, /permissions:[\s\S]*?contents: read/u); - assert.match(prepare, /persist-credentials: false[\s\S]*?ref: \$\{\{ github\.sha \}\}/u); - assert.match(prepare, /npm run release:prepare -- --bump "\$\{bump\}" --json/u); - assert.match(prepare, /expected=\(CHANGELOG\.md package-lock\.json package\.json\)/u); - assert.match(prepare, /npm run release:verify[\s\S]*?--tag-state absent/u); - assert.match(prepare, /branch="release\/v\$\{version\}"/u); - assert.doesNotMatch(prepare, /GH_TOKEN|contents: write|git push/u); - assert.match(createBranch, /permissions:[\s\S]*?actions: read[\s\S]*?contents: write/u); - assert.doesNotMatch(createBranch, /actions\/checkout@|npm ci|npm run |node scripts\//u); - assert.match(createBranch, /git -C "\$\{repository\}" -c credential\.helper= fetch[\s\S]*?"\$\{LFC_SOURCE_COMMIT\}"/u); - assert.match(createBranch, /expected=\(CHANGELOG\.md package-lock\.json package\.json\)/u); - assert.match(createBranch, /GH_TOKEN: \$\{\{ github\.token \}\}[\s\S]*?gh auth setup-git[\s\S]*?git push origin/u); - assert.doesNotMatch(source, /--online|NPM_TOKEN|NODE_AUTH_TOKEN|secrets\.|git push[^\n]*--force/u); -}); - -void test("alpha publication is classified from the exact push to main", async () => { - const source = await workflow("publish-alpha.yml"); +void test("nightly publication uses daily and manual exact-green-main snapshots", async () => { + const source = await workflow("publish-nightly.yml"); const event = trigger(source); const classify = job(source, "classify"); const verify = job(source, "verify"); - assert.match(event, /push:[\s\S]*?branches:[\s\S]*?- main[\s\S]*?paths:[\s\S]*?- \.github\/workflows\/publish-alpha\.yml[\s\S]*?- CHANGELOG\.md[\s\S]*?- package-lock\.json[\s\S]*?- package\.json/u); - assert.doesNotMatch(event, /workflow_run:|workflow_dispatch:|release:/u); - assert.doesNotMatch(source, /LFC_RECOVERY_|current_main_commit|needs\.classify\.outputs\.recovery/u); - assert.doesNotMatch(classify, /source-commit:/u); - assert.match(classify, /fetch-depth: 2[\s\S]*?persist-credentials: false/u); - assert.match(classify, /ref: \$\{\{ github\.sha \}\}/u); - assert.match(classify, /LFC_EVENT_NAME: \$\{\{ github\.event_name \}\}/u); - assert.match(classify, /LFC_SOURCE_COMMIT: \$\{\{ github\.sha \}\}/u); - assert.match(classify, /LFC_WORKFLOW_COMMIT: \$\{\{ github\.workflow_sha \}\}/u); - assert.match(classify, /LFC_WORKFLOW_REF: \$\{\{ github\.ref \}\}/u); - assert.match(classify, /LFC_EVENT_NAME\}" != "push"[\s\S]*?LFC_WORKFLOW_REF\}" != "refs\/heads\/main"/u); - assert.match(classify, /! "\$\{LFC_SOURCE_COMMIT\}" =~ \^\[0-9a-f\]\{40\}\$/u); - assert.match(classify, /! "\$\{LFC_WORKFLOW_COMMIT\}" =~ \^\[0-9a-f\]\{40\}\$/u); - assert.match(classify, /LFC_WORKFLOW_COMMIT\}" != "\$\{LFC_SOURCE_COMMIT\}"/u); - assert.match(classify, /git rev-parse --verify HEAD\^\{commit\}[\s\S]*?LFC_SOURCE_COMMIT/u); - assert.match(classify, /git rev-parse --verify HEAD\^1/u); - assert.match(classify, /git show "\$\{parent_commit\}:package\.json"/u); - assert.match(classify, /candidate_version[\s\S]*?parent_version[\s\S]*?eligible=true/u); - assert.doesNotMatch(classify, /GH_TOKEN|gh api|origin\/main/u); - assert.match( - verify, - /if: >-[\s\S]*?needs\.classify\.outputs\.eligible == 'true'[\s\S]*?github\.event_name == 'push'[\s\S]*?github\.ref == 'refs\/heads\/main'[\s\S]*?github\.workflow_sha == github\.sha/u - ); - assert.match(verify, /ref: \$\{\{ github\.sha \}\}/u); - assert.match(verify, /LFC_SOURCE_COMMIT: \$\{\{ github\.sha \}\}/u); - assert.doesNotMatch(verify, /needs\.classify\.outputs\.source-commit/u); - assert.match(verify, /persist-credentials: false/u); - assert.match( - verify, - /git fetch --force --no-tags origin[\s\S]*?refs\/heads\/main:refs\/remotes\/origin\/main/u - ); - assert.match(verify, /refs\/remotes\/origin\/main\^\{commit\}[\s\S]*?source_commit/u); - assert.match(verify, /git show HEAD\^1:package\.json/u); - assert.match(verify, /git diff --name-only HEAD\^1 HEAD/u); - assert.match(verify, /expected_files=\(CHANGELOG\.md package-lock\.json package\.json\)/u); - assert.match(verify, /npm run release:verify[\s\S]*?--commit "\$\{source_commit\}"[\s\S]*?--require-clean/u); - assert.match(verify, /registry_raw="\$\{RUNNER_TEMP\}\/registry\.raw\.json"/u); - assert.match(verify, /registry_json="\$\{RUNNER_TEMP\}\/registry\.json"/u); - assert.match(verify, /registry_error="\$\{RUNNER_TEMP\}\/registry\.err"/u); - assert.match(verify, /> "\$\{registry_raw\}" 2> "\$\{registry_error\}"/u); - assert.match( - verify, - /LFC_NORMALIZE_NPM_VIEW_JSON\}"[\s\S]*?"\$\{registry_raw\}" "\$\{registry_json\}"/u - ); - assert.equal(occurrences(verify, /registry\.raw\.json/gu), 1); - assert.equal(occurrences(verify, /registry\.json/gu), 1); - assert.equal(occurrences(verify, /registry\.err/gu), 1); + assert.match(event, /schedule:[\s\S]*?cron: "0 9 \* \* \*"[\s\S]*?workflow_dispatch:/u); + assert.doesNotMatch(event, /push:|release:|workflow_run:/u); + assert.match(classify, /github\.repository == 'tryagaindev\/litefold-calendar'[\s\S]*?refs\/heads\/main/u); + assert.match(classify, /GITHUB_WORKFLOW_SHA[\s\S]*?GITHUB_SHA/u); + assert.match(classify, /actions\/workflows\/ci\.yml\/runs[\s\S]*?head_sha[\s\S]*?\.conclusion == "success"/u); + assert.match(classify, /created_at[\s\S]*?GITHUB_RUN_ID/u); + assert.match(verify, /npm run nightly:plan[\s\S]*?nightly-plan\.json/u); + assert.match(verify, /validateNightlyRegistryState[\s\S]*?previous-pages\.json/u); assert.match(verify, /npm run check/u); - assert.match(verify, /npm run package/u); + assert.match(verify, /npm run package:nightly -- --plan/u); + assert.doesNotMatch(source, /git push|git commit|release:prepare|git diff --name-only HEAD\^1 HEAD/u); +}); + +void test("nightly recovery preserves original artifacts and Pages binds the originating attempt", async () => { + const source = await workflow("publish-nightly.yml"); + const verify = job(source, "verify"); + assert.match(verify, /selectNightlyRecoveryArtifacts/u); + assert.match(verify, /actions\/artifacts\/\$\{id\}\/zip[\s\S]*?sha256sum[\s\S]*?\$\{digest\}/u); + assert.match(verify, /verifyRestoredNightlyBundle/u); + assert.equal(occurrences(verify, /steps\.recovery\.outputs\.restored != 'true'/gu), 3); + assert.match(verify, /LFC_BROWSER_TARGET_DATE[\s\S]*?LFC_CREATED_AT/u); + assert.match(verify, /Retained notes differ from the exact source/u); + const pages = await workflow("deploy-examples.yml"); + assert.match(pages, /LFC_UPSTREAM_RUN_ATTEMPT: \$\{\{ github\.event\.workflow_run\.run_attempt \}\}/u); + assert.match(pages, /actions\/runs\/\$\{LFC_UPSTREAM_RUN_ID\}\/attempts\/\$\{LFC_UPSTREAM_RUN_ATTEMPT\}\/jobs/u); +}); + +void test("latest token is isolated from source execution and OIDC publication", async () => { + const source = await workflow("publish-nightly.yml"); + const sync = job(source, "synchronize-latest"); + assert.match(sync, /environment: npm-nightly-tags/u); + assert.doesNotMatch(sync, /actions\/checkout@|npm ci|npm run |node scripts\/|id-token: write|npm publish/u); + assert.match(sync, /versions\.some\(stable\)[\s\S]*?stableExists \? latest : process\.env\.LFC_VERSION/u); + assert.match(sync, /NODE_AUTH_TOKEN: \$\{\{ secrets\.NPM_NIGHTLY_TAG_TOKEN \}\}/u); + assert.match(sync, /npm dist-tag add[\s\S]*?latest --registry https:\/\/registry\.npmjs\.org\//u); + assert.equal(occurrences(source, /secrets\.NPM_NIGHTLY_TAG_TOKEN/gu), 1); + assert.match(sync, /NPM_NIGHTLY_TAG_TOKEN_EXPIRES_AT/u); }); void test("the OIDC publisher consumes only the verified five-file bundle", async () => { - const source = await workflow("publish-alpha.yml"); + const source = await workflow("publish-nightly.yml"); const publish = job(source, "publish"); - assert.match(publish, /environment: npm/u); + assert.match(publish, /environment: npm-nightly/u); assert.match(publish, /id-token: write/u); - assert.equal(occurrences(source, /^\s*environment: npm$/gmu), 1); + assert.equal(occurrences(source, /^\s*environment: npm-nightly$/gmu), 1); assert.equal(occurrences(source, /^\s*id-token: write$/gmu), 1); assert.equal(occurrences(publish, /actions\/download-artifact@/gu), 1); assert.match(publish, /find \.[\s\S]*?wc -l\)" -eq 5/u); @@ -136,20 +104,20 @@ void test("the OIDC publisher consumes only the verified five-file bundle", asyn /forbiddenDependencyFields\.some\(\(field\) => Object\.hasOwn\(manifest, field\)\)/u ); assert.equal(occurrences(publish, /^\s*npm publish\b/gmu), 1); - assert.match(publish, /--registry https:\/\/registry\.npmjs\.org\/[\s\S]*?--access public[\s\S]*?--tag alpha[\s\S]*?--provenance[\s\S]*?--ignore-scripts/u); - assert.match(publish, /manifest\.publishConfig\?\.tag !== "alpha"/u); + assert.match(publish, /--registry https:\/\/registry\.npmjs\.org\/[\s\S]*?--access public[\s\S]*?--tag nightly[\s\S]*?--provenance[\s\S]*?--ignore-scripts/u); + assert.match(publish, /manifest\.publishConfig\?\.tag !== "nightly"/u); assert.doesNotMatch(publish, /actions\/checkout@|npm ci|npm run |node scripts\//u); assert.doesNotMatch(publish, /LFC_PARENT_VERSION|releases\/tags\/v\$\{LFC_PARENT_VERSION\}/u); - assert.doesNotMatch(source, /NPM_TOKEN|NODE_AUTH_TOKEN|registry-release-state\.mjs|release-verification\.mjs|verify-release-state\.mjs/u); + assert.doesNotMatch(publish, /NPM_TOKEN|NODE_AUTH_TOKEN|registry-release-state\.mjs|release-verification\.mjs|verify-release-state\.mjs/u); }); void test("registry-backed candidates share one lock and only current main may publish", async () => { - const source = await workflow("publish-alpha.yml"); + const source = await workflow("publish-nightly.yml"); const verify = job(source, "verify"); const publish = job(source, "publish"); assert.match( source, - /^concurrency:\s*\n\s*group: npm-alpha-\$\{\{ github\.repository \}\}\s*\n\s*queue: max\s*\n\s*cancel-in-progress: false/mu + /^concurrency:\s*\n\s*group: npm-nightly-\$\{\{ github\.repository \}\}\s*\n\s*queue: max\s*\n\s*cancel-in-progress: false/mu ); assert.doesNotMatch(publish, /^\s*concurrency:|group:[^\n]*(?:version|needs\.verify)/mu); assert.equal( @@ -157,7 +125,7 @@ void test("registry-backed candidates share one lock and only current main may p 2 ); assert.equal( - occurrences(publish, /"\$\{main_version\}" != "\$\{LFC_VERSION\}"/gu), + occurrences(publish, /"\$\{main_version\}" != "\$\{LFC_BASE_VERSION\}"/gu), 2 ); assert.equal( @@ -172,32 +140,26 @@ void test("registry-backed candidates share one lock and only current main may p assert.match(publish, /\.object\.type == "commit"[\s\S]*?\.object\.sha/u); assert.match( verify, - /E404\|404 Not Found[\s\S]*?npm package must already exist before the normal alpha release workflow/u + /validateNightlyRegistryState/u ); assert.doesNotMatch(source, /registry-status|LFC_REGISTRY_STATUS|"status":"absent"/u); - const currentMainVersion = "0.2.0-alpha.1"; - const queuedCandidates = ["0.2.0-alpha.0", currentMainVersion]; - assert.deepEqual( - queuedCandidates.map((candidate) => candidate === currentMainVersion), - [false, true] - ); }); void test("npm 12 view results pass through one fail-closed normalizer", async () => { - const source = await workflow("publish-alpha.yml"); + const source = await workflow("publish-nightly.yml"); const verifyRegistry = job(source, "verify-registry"); assert.match( source, /LFC_NORMALIZE_NPM_VIEW_JSON: >-[\s\S]*?!Array\.isArray\(value\) \|\| value\.length !== 1[\s\S]*?JSON\.stringify\(value\[0\]\)/u ); - assert.equal(occurrences(source, /^\s*(?:if )?npm view\b/gmu), 6); + assert.equal(occurrences(source, /^\s*(?:if )?(?:! )?npm view\b/gmu), 8); assert.equal( occurrences( source, /node --input-type=module --eval "\$\{LFC_NORMALIZE_NPM_VIEW_JSON\}"/gu ), - 6 + 8 ); assert.doesNotMatch(source, /jq[^\n]*\.raw\.json|JSON\.parse\([^\n]*\.raw\.json/u); @@ -220,7 +182,7 @@ void test("npm 12 view results pass through one fail-closed normalizer", async ( assert.match(verifyRegistry, /pending or blocked publish-time review/u); }); -void test("greater-alpha recovery follows the published alpha tag rather than the manifest parent", async (context) => { +void test("nightly recovery follows the published predecessor rather than the source manifest parent", async (context) => { const repository = await mkdtemp(join(tmpdir(), "lfc-workflow-lineage-")); context.after(() => rm(repository, { force: true, recursive: true })); const git = (...arguments_) => execFileAsync("git", arguments_, { cwd: repository }); @@ -243,11 +205,11 @@ void test("greater-alpha recovery follows the published alpha tag rather than th assert.notEqual(releaseCommit, parentOutput.trim()); await git("merge-base", "--is-ancestor", releaseCommit, "HEAD"); - const publish = job(await workflow("publish-alpha.yml"), "publish"); + const publish = job(await workflow("publish-nightly.yml"), "publish"); assert.doesNotMatch(publish, /LFC_PARENT_(?:COMMIT|VERSION)|releases\/tags\//u); assert.match( publish, - /registry_alpha="\$\(jq[\s\S]*?git\/ref\/tags\/v\$\{registry_alpha\}[\s\S]*?git\/tags\/\$\{tag_object\}[\s\S]*?compare\/\$\{registry_alpha_commit\}\.\.\.\$\{LFC_SOURCE_COMMIT\}/u + /registry_predecessor="\$\(jq[\s\S]*?git\/ref\/tags\/v\$\{registry_predecessor\}[\s\S]*?git\/tags\/\$\{tag_object\}[\s\S]*?compare\/\$\{registry_predecessor_commit\}\.\.\.\$\{LFC_SOURCE_COMMIT\}/u ); const historicalIdentities = [ ...publish.matchAll(/"([0-9.]+-alpha\.[0-9]+):([0-9a-f]{40})"/gu) @@ -267,11 +229,11 @@ void test("greater-alpha recovery follows the published alpha tag rather than th }); void test("publisher authorization is bound to one verified registry snapshot", async () => { - const source = await workflow("publish-alpha.yml"); + const source = await workflow("publish-nightly.yml"); const verify = job(source, "verify"); const publish = job(source, "publish"); assert.match(verify, /registry-state-sha256: \$\{\{ steps\.registry-state\.outputs\.registry-state-sha256 \}\}/u); - assert.match(verify, /printf 'registry-state-sha256=%s\\n' "\$\{registry_state_sha256\}"/u); + assert.match(verify, /printf 'registry-state-sha256=%s\\neligible=%s\\n'/u); assert.equal( occurrences(publish, /LFC_REGISTRY_STATE_SHA256: \$\{\{ needs\.verify\.outputs\.registry-state-sha256 \}\}/gu), 2 @@ -285,7 +247,7 @@ void test("publisher authorization is bound to one verified registry snapshot", }); void test("release bundle digest lines become explicit jq entries", async () => { - const source = await workflow("publish-alpha.yml"); + const source = await workflow("publish-nightly.yml"); const stage = job(source, "stage-release"); const publish = job(source, "publish"); const corrected = @@ -300,7 +262,7 @@ void test("release bundle digest lines become explicit jq entries", async () => }); void test("draft assets and final release publication are digest-bound and source-free", async () => { - const source = await workflow("publish-alpha.yml"); + const source = await workflow("publish-nightly.yml"); const stage = job(source, "stage-release"); const verifyRegistry = job(source, "verify-registry"); const publishRelease = job(source, "publish-release"); @@ -337,13 +299,13 @@ void test("draft assets and final release publication are digest-bound and sourc )?.[1]; assert.equal(typeof provenancePolicy, "string"); assert.match(provenancePolicy, /const eventName = process\.env\.GITHUB_EVENT_NAME/u); - assert.match(provenancePolicy, /eventName === "push"/u); - assert.doesNotMatch(provenancePolicy, /workflow_dispatch/u); + assert.match(provenancePolicy, /\["schedule", "workflow_dispatch"\]\.includes\(eventName\)/u); + assert.doesNotMatch(provenancePolicy, /eventName === "push"/u); assert.match(provenancePolicy, /process\.env\.GITHUB_SHA === sourceCommit/u); assert.match(provenancePolicy, /process\.env\.GITHUB_WORKFLOW_SHA === sourceCommit/u); assert.match(provenancePolicy, /internalGitHub\?\.event_name === eventName/u); assert.match(provenancePolicy, /certificate\.buildTrigger === eventName/u); - assert.match(verifyRegistry, /"\$\{alpha\}" == "\$\{LFC_VERSION\}"[\s\S]*?"\$\{latest\}" == "\$\{LFC_VERSION\}"/u); + assert.match(verifyRegistry, /"\$\{nightly\}" == "\$\{LFC_VERSION\}"[\s\S]*?"\$\{latest\}" == "\$\{LFC_LATEST_TARGET\}"/u); assert.doesNotMatch(verifyRegistry, /actions\/checkout@|GH_TOKEN|contents: write|id-token: write|npm publish/u); assert.match(publishRelease, /needs:[\s\S]*?- stage-release[\s\S]*?- verify-registry/u); assert.match(publishRelease, /LFC_ASSET_DIGESTS: \$\{\{ needs\.verify\.outputs\.asset-digests \}\}/u); @@ -360,7 +322,7 @@ void test("draft assets and final release publication are digest-bound and sourc }); void test("the publisher relies on the native downstream workflow handoff", async () => { - const source = await workflow("publish-alpha.yml"); + const source = await workflow("publish-nightly.yml"); assert.doesNotMatch(source, /dispatch-release-pages|gh workflow run|actions: write/u); assert.match( source, @@ -376,7 +338,7 @@ void test("automatic Pages deployment is workflow-run-only and exact-source", as const packageSite = job(source, "package-site"); const deploy = job(source, "deploy"); assert.doesNotMatch(source, /LFC_RECOVERY_/u); - assert.match(event, /workflow_run:[\s\S]*?- CI[\s\S]*?- Publish npm alpha[\s\S]*?branches:\s*\n\s+- main/u); + assert.match(event, /workflow_run:[\s\S]*?- CI[\s\S]*?- Publish npm nightly[\s\S]*?branches:\s*\n\s+- main/u); assert.doesNotMatch(event, /workflow_dispatch:|snapshot_ref:/u); assert.doesNotMatch(source, /^ {2}(?:prepare-rollback|rollback-snapshot):/mu); assert.doesNotMatch(source, /^ {2}classify:/mu); @@ -397,7 +359,7 @@ void test("automatic Pages deployment is workflow-run-only and exact-source", as assert.match(build, /upstream_workflow_path="\$\{LFC_UPSTREAM_WORKFLOW_PATH%@\*\}"/u); const ciRouteStart = build.indexOf('if [[ "${LFC_UPSTREAM_WORKFLOW}" == "CI"'); const publisherRouteStart = build.indexOf( - 'elif [[ "${LFC_UPSTREAM_WORKFLOW}" == "Publish npm alpha"' + 'elif [[ "${LFC_UPSTREAM_WORKFLOW}" == "Publish npm nightly"' ); const releaseValidationStart = build.indexOf( 'if [[ "${eligible}" == "true" && "${channel}" == "release" ]]' @@ -409,14 +371,14 @@ void test("automatic Pages deployment is workflow-run-only and exact-source", as assert.match(ciRoute, /upstream_workflow_path\}" == "\.github\/workflows\/ci\.yml"/u); assert.match(ciRoute, /LFC_UPSTREAM_EVENT\}" == "push"/u); assert.doesNotMatch(ciRoute, /workflow_dispatch/u); - assert.match(publisherRoute, /upstream_workflow_path\}" == "\.github\/workflows\/publish-alpha\.yml"/u); + assert.match(publisherRoute, /upstream_workflow_path\}" == "\.github\/workflows\/publish-nightly\.yml"/u); assert.match( publisherRoute, - /LFC_UPSTREAM_EVENT\}" == "push"[\s\S]*?version\}" == "\$\{parent_version\}"[\s\S]*?eligible=false[\s\S]*?channel=""[\s\S]*?channel="release"/u + /LFC_UPSTREAM_EVENT\}" == "schedule"[\s\S]*?workflow_dispatch[\s\S]*?created_at[\s\S]*?LFC_UPSTREAM_RUN_ID[\s\S]*?eligible=false/u ); - assert.doesNotMatch(publisherRoute, /workflow_dispatch/u); + assert.match(publisherRoute, /workflow_dispatch/u); assert.match(build, /Unexpected upstream workflow identity:[\s\S]*?LFC_UPSTREAM_WORKFLOW_PATH/u); - assert.match(build, /eligible.*channel.*release[\s\S]*?release_ref="v\$\{version\}"[\s\S]*?\^0\\\.\[0-9\]\+\\\.\[0-9\]\+-alpha\\\.\[0-9\]\+\$[\s\S]*?release_ref\}\^\{commit\}[\s\S]*?= "\$\{source_commit\}"/u); + assert.match(build, /release_ref="v\$\{version\}"[\s\S]*?nightly[\s\S]*?release_ref\}\^\{commit\}[\s\S]*?source_commit/u); assert.match(build, /Resolve the deployment identity[\s\S]*?Set up exact Node[\s\S]*?Set up exact npm[\s\S]*?npm ci --ignore-scripts[\s\S]*?npm run build/u); assert.equal( occurrences(build, /^\s+if: \$\{\{ steps\.identity\.outputs\.eligible == 'true' \}\}$/gmu), @@ -500,42 +462,18 @@ void test("automatic deployment and rollback triggers remain physically isolated } }); -void test("release documentation keeps publication push-only with one procedural owner", async () => { +void test("release docs distinguish nightly automation from future stable approval", async () => { const [administration, operations, releasing] = await Promise.all([ readFile(join(REPOSITORY_ROOT, "docs", "release-administration.md"), "utf8"), readFile(join(REPOSITORY_ROOT, "docs", "release-operations.md"), "utf8"), readFile(join(REPOSITORY_ROOT, "docs", "releasing.md"), "utf8") ]); assert.match(administration, /## Publication authority/u); - assert.match( - administration, - /`publish-alpha\.yml` starts only on pushes to `main`[\s\S]*?There is no\s+manual, arbitrary-ref, or non-current-commit publication path\./u - ); - assert.match(operations, /The merge push is the only publication trigger\./u); - assert.match(releasing, /merge push to canonical `main`\s+is the only publication trigger\./u); - assert.match(releasing, /\[Alpha release operations\]\(release-operations\.md\) owns the normal ordered/u); - assert.match(releasing, /\[Release administration\]\(release-administration\.md\) owns hosted controls/u); assert.match(administration, /## Recovery matrix/u); - assert.match(administration, /Preparation reports an existing release branch or pull request/u); - assert.match(administration, /npm accepts the upload but the version remains unavailable/u); - assert.match( - administration, - /npm accepted the candidate but the `publish`\s+job itself failed[\s\S]*?Re-run all jobs/u - ); - assert.match( - administration, - /If `publish` succeeded and only a downstream job failed, use \*\*Re-run failed jobs\*\*/u - ); - assert.doesNotMatch(operations, /\| Observed state \| Required action \|/u); - assert.match(operations, /npm dist-tag add @tryagaindev\/litefold-calendar@EXACT_VERSION latest/u); - assert.doesNotMatch(administration, /npm dist-tag add/u); - assert.doesNotMatch(releasing, /npm dist-tag add|## Operator checklist|### 1\./u); for (const guide of [administration, operations, releasing]) { - assert.doesNotMatch( - guide, - /current_main_commit|LFC_RECOVERY_|f35ec0caf6e1557bb7d8d6b80f8a3c207351c51e02832d387109eca80ae77894/u - ); - assert.doesNotMatch(guide, /git push[^\n]*--force/u); + assert.match(guide, /nightly/iu); + assert.match(guide, /stable/iu); + assert.doesNotMatch(guide, /The merge push is the only publication trigger/u); } }); @@ -543,8 +481,7 @@ void test("all third-party workflow actions are pinned to full commits", async ( for (const name of [ "ci.yml", "deploy-examples.yml", - "prepare-alpha.yml", - "publish-alpha.yml", + "publish-nightly.yml", "rollback-examples.yml" ]) { const source = await workflow(name); @@ -569,7 +506,7 @@ void test("CI rejects high-severity dependency regressions on pull requests", as void test("release gates and Playwright retain the complete browser-engine matrix", async () => { const [ciSource, publishSource, packageSource] = await Promise.all([ workflow("ci.yml"), - workflow("publish-alpha.yml"), + workflow("publish-nightly.yml"), readFile(join(REPOSITORY_ROOT, "package.json"), "utf8") ]); const packageManifest = JSON.parse(packageSource); @@ -610,8 +547,7 @@ void test("workflow artifacts use bounded purpose-specific retention", async () const expectations = new Map([ ["ci.yml", [7, 7]], ["deploy-examples.yml", [1, 1]], - ["prepare-alpha.yml", [1]], - ["publish-alpha.yml", [7, 30, 30]], + ["publish-nightly.yml", [7, 30, 30]], ["rollback-examples.yml", [1]] ]); for (const [name, expected] of expectations) { @@ -624,8 +560,7 @@ void test("workflow artifacts use bounded purpose-specific retention", async () void test("artifact downloads use the official Node 24 action", async () => { const workflows = [ "deploy-examples.yml", - "prepare-alpha.yml", - "publish-alpha.yml", + "publish-nightly.yml", "rollback-examples.yml" ]; const expected = @@ -638,15 +573,14 @@ void test("artifact downloads use the official Node 24 action", async () => { assert.equal(match[1], expected, name); } } - assert.equal(downloads, 6); + assert.equal(downloads, 5); }); void test("workflow dependency caches stay disabled", async () => { for (const name of [ "ci.yml", "deploy-examples.yml", - "prepare-alpha.yml", - "publish-alpha.yml", + "publish-nightly.yml", "rollback-examples.yml" ]) { const source = await workflow(name); From bde33d7479f9af179baf8159dd44cdc3c3035a20 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:32:11 -0700 Subject: [PATCH 10/22] docs: add delivery checklist and stable-release guidance --- .agents/skills/release-to-npm/SKILL.md | 46 ++++++---- .../skills/release-to-npm/agents/openai.yaml | 4 +- .../references/litefold-calendar.md | 12 ++- CONTRIBUTOR_COMMANDS.md | 29 +++++- docs/change-delivery.md | 92 +++++++++++++++++++ 5 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 docs/change-delivery.md diff --git a/.agents/skills/release-to-npm/SKILL.md b/.agents/skills/release-to-npm/SKILL.md index 123859f..747d657 100644 --- a/.agents/skills/release-to-npm/SKILL.md +++ b/.agents/skills/release-to-npm/SKILL.md @@ -5,9 +5,11 @@ description: Coordinate a maintainer-authorized npm release through repository-o # Release to npm -Coordinate the repository-owned release process without replacing human -approvals or widening credential boundaries. Read-only planning and verification -do not authorize a public mutation. +Coordinate the repository-owned release process within the user's authorized +scope and the configured hosted controls. Read-only planning and verification +do not authorize a public mutation. An explicit request to implement and publish +through named delivery stages does authorize those stages; preserve that scope +instead of requesting the same permission again. ## Load repository authority @@ -37,10 +39,11 @@ workflow runs, the repository's private release record, and fresh read-only registry responses. A local branch, tag, worktree, artifact directory, cached response, or remembered prior run is not publication authority. -Do not infer a version bump, release role, or permission. Confirm the candidate, -the release operator, any required reviewer for the `npm` environment, and the -authenticated npm package maintainer as required by the runbook. One person may -fill multiple roles only when the hosted policy permits it. +Derive the candidate strategy, environment names, and required roles from the +current runbook and hosted state. An authorized nightly start uses unattended +trusted publication and does not require a manual environment reviewer or local +npm login. Stable publication retains its separately configured review controls. +One person may fill multiple roles only when the hosted policy permits it. For an authorized start or continuation, create or update the private release record defined by the runbook. Keep it outside the repository in an approved, @@ -51,27 +54,32 @@ conversation, command output, or automation. Treat unavailable, stale, malformed, conflicting, or ambiguous identity evidence as a stop condition. -## Pause at mutation boundaries +## Respect mutation boundaries Immediately before every external mutation: 1. State the exact repository, candidate version, source commit, workflow run, and current public identity that are known at that phase. 2. Explain the expected mutation and its irreversible or protected effects. -3. Obtain explicit authorization for that mutation. +3. Confirm that the user's existing authorization covers that mutation; ask only + when it does not. 4. Follow only the corresponding canonical runbook section. 5. Read back the resulting state and update the private record before advancing. -Separate authorization is required for each applicable preparation dispatch, -pull-request submission, merge, protected publication approval, registry -metadata change, workflow rerun, deployment action, and recovery mutation. A -general request to "release" is not standing approval for later irreversible -steps. - -Never publish locally, collect registry credentials, add a long-lived publishing -token, manufacture a release trigger, push or move a version tag, rewrite a -shared branch, create public release state from an arbitrary ref, or weaken a -hosted protection to make progress. +Identify the authorized preparation, pull-request, merge, publication, registry, +deployment, and recovery stages explicitly. Do not widen an ordinary Git request +into a release or a nightly request into a stable release. Required hosted reviews +still apply even when the user authorizes a complete delivery. A scheduled nightly +uses its reviewed unattended workflow; do not insert an interactive approval into +that workflow or bypass the distinct stable publication controls. + +Never publish locally, collect credentials in conversation or release notes, +manufacture a release trigger, push or move a version tag, rewrite a shared branch, +or create public release state from an arbitrary ref. Changes to hosted controls +or credential policy require an explicitly authorized, reviewed migration; a +failed release is not permission to improvise one. Route any scoped, expiring +nightly tag credential through the documented private secret setup, never source +code or retained artifacts. ## Follow the selected phase diff --git a/.agents/skills/release-to-npm/agents/openai.yaml b/.agents/skills/release-to-npm/agents/openai.yaml index 4118426..b792a31 100644 --- a/.agents/skills/release-to-npm/agents/openai.yaml +++ b/.agents/skills/release-to-npm/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Release to npm" - short_description: "Coordinate protected npm releases" - default_prompt: "Use $release-to-npm to classify the requested release phase, load the repository runbook, and pause before every external mutation." + short_description: "Coordinate and verify npm release stages" + default_prompt: "Use $release-to-npm to load the current release runbook, complete the authorized release stages, and verify exact public package and site identities." diff --git a/.agents/skills/release-to-npm/references/litefold-calendar.md b/.agents/skills/release-to-npm/references/litefold-calendar.md index d4ded56..d77f608 100644 --- a/.agents/skills/release-to-npm/references/litefold-calendar.md +++ b/.agents/skills/release-to-npm/references/litefold-calendar.md @@ -17,12 +17,11 @@ Stop before a hosted mutation when any normalized identity differs. | Need | Read | | --- | --- | | Supported release policy and invariants | [Release policy](../../../../docs/releasing.md) | -| Normal operator procedure and private release record | [Alpha release operations](../../../../docs/release-operations.md) | +| Normal operator procedure and private release record | [Nightly release operations](../../../../docs/release-operations.md) | | Hosted controls, unsupported transitions, exceptional actions, and recovery | [Release administration](../../../../docs/release-administration.md) | | Artifact, installed-package, registry, signature, and provenance evidence | [Package verification](../../../../docs/package-verification.md) | | Release-linked example deployment and rollback | [Static example deployment](../../../../docs/example-deployment.md) | -| Executable preparation state machine | [Prepare alpha release workflow](../../../../.github/workflows/prepare-alpha.yml) | -| Executable publication state machine | [Publish alpha release workflow](../../../../.github/workflows/publish-alpha.yml) | +| Executable nightly snapshot and publication state machine | [Publish nightly workflow](../../../../.github/workflows/publish-nightly.yml) | Read the current phase in its canonical owner immediately before acting. The documents and workflows override remembered behavior and this routing file. @@ -39,3 +38,10 @@ Use only the supported transition described by current release administration. If a requested state has no reviewed procedure, stop and require a separately designed and approved one. Describe recovery by the observed state and required identity evidence, without relying on release chronology. + +Nightly publication snapshots reviewed `main` without a daily metadata commit. +Its workflow owns generated versions, exact artifact retention, npm channel +updates, and repeat-run recovery. Preserve historical alpha state. Stable +publication is a separate procedure and must not be inferred from a nightly +request. The [delivery checklist](../../../../docs/change-delivery.md) connects +ordinary source changes with final package and Pages verification. diff --git a/CONTRIBUTOR_COMMANDS.md b/CONTRIBUTOR_COMMANDS.md index 901d628..bc54889 100644 --- a/CONTRIBUTOR_COMMANDS.md +++ b/CONTRIBUTOR_COMMANDS.md @@ -30,6 +30,10 @@ That helper installs the pinned browser binaries without changing package depend ## Explore and build +Use `npm run dev` for Vite's local example development loop. Compatibility, +screenshot, CSP, and release checks use the built package through the repository +server; development-server success does not replace those checks. + Build the package and examples, then start the loopback demo server: ```shell @@ -119,14 +123,15 @@ npm run test:browser:built -- --project=webkit --output=test-results/playwright- Ports `49152` through `49154` are unassigned Dynamic/Private ports rather than registered service ports, but they are not reserved for this project or for testing. Confirm that the block is available locally before starting the processes; each process fails explicitly if another listener owns its port. Do not assign separate ports to projects or workers inside one Playwright invocation. Do not scan through arbitrary ports without browser validation; browsers reject some otherwise valid TCP ports. -## Update screenshots intentionally +## Prepare and review screenshots -When an intentional visual change affects a canonical scene, regenerate the -tracked screenshot set and manifest, review every diff, and follow the -[screenshot contract](docs/screenshots/README.md). +Prepare screenshot evidence after source and build inputs settle. The preparation +command skips current evidence, otherwise builds and validates a staged capture +batch and produces a before/after gallery. Review every changed scene using the +[screenshot contract](docs/screenshots/README.md). CI verification remains read-only. ```shell -npm run screenshots:update +npm run screenshots:prepare ``` ```shell @@ -147,8 +152,22 @@ Before submission, run the complete gate: npm run check ``` +CI also qualifies Firefox on Windows and Linux with 20 zero-retry repetitions of +the affected tests and three complete passes. After building, reproduce that +qualification on the current platform with: + +```shell +node scripts/qualify-firefox.mjs +``` + +This separate qualification is not included in `npm run check`. + ## Deliver a contributor change +For an end-to-end request, follow the [local-to-production checklist](docs/change-delivery.md). +It connects phase commits, clean-tree checks, review, nightly publication, and the +separate release Pages verification. + Automation that supports repository skills can invoke [`$commit-and-push`](.agents/skills/commit-and-push/SKILL.md) to audit Git state, validate and commit only the intended files, and push an ordinary feature branch. The skill is operational guidance, not a shell command or additional repository authority. Package publication and release administration are separate from contributing. diff --git a/docs/change-delivery.md b/docs/change-delivery.md new file mode 100644 index 0000000..0ea5763 --- /dev/null +++ b/docs/change-delivery.md @@ -0,0 +1,92 @@ +# Deliver a change from local development to production + +This checklist is for contributors and maintainers taking a reviewed change through +the nightly channel. Start with the [contributor commands](../CONTRIBUTOR_COMMANDS.md) +for toolchain setup and the [release operations](release-operations.md) for the +current publication procedure. Each document owns its commands; this checklist +connects the stages without duplicating the release runbook. + +## Prepare the change + +- [ ] Fetch the canonical upstream and work on a feature branch based on the intended + source. Preserve unrelated local changes, prototypes, stashes, and existing commits. +- [ ] Define the user-visible outcome and the relevant API, accessibility, browser, + package, and documentation contracts before editing. +- [ ] Make focused changes and run the affected tests. For a phased change, keep each + phase reviewable and record what was checked before committing it. +- [ ] Update the authoritative documentation, executable examples, and `Unreleased` + changelog notes that explain changed behavior. +- [ ] For responsive changes, check the calendar container independently of browser + viewport width. Follow the [browser policy](browser-support.md) and + [accessibility contract](../ACCESSIBILITY.md). + +## Review screenshots and commit + +- [ ] Run screenshot preparation after source and build inputs settle. Review the + generated before/after gallery using the [screenshot procedure](screenshots/README.md). + An unchanged image can still need refreshed source evidence; a changed image needs + visual review. CI never accepts new reference images automatically. +- [ ] Stage only the intended paths or hunks. Review the staged patch, generated + evidence, file list, and whitespace checks. +- [ ] Create a focused Conventional Commit. Inspect the resulting commit and remaining + working-tree state rather than assuming the index was unchanged by hooks. +- [ ] Once the intended changes are committed, run the complete clean-tree + `npm run check` gate. Fix failures in a new focused commit before pushing. + +The [commit-and-push skill](../.agents/skills/commit-and-push/SKILL.md) supports these +Git stages. A commit or push alone does not start publication. + +## Review and merge + +- [ ] Push the feature branch and submit a pull request describing the final behavior, + compatibility changes, tests, and any remaining limitations. +- [ ] Resolve required review and CI results, including Firefox qualification. + A test passing only after a retry is a failure of the gate. +- [ ] Merge through the protected branch workflow. Record the resulting full source SHA. +- [ ] Confirm the required CI results refer to that exact merged source, not a previous + pull-request head or an earlier successful run. + +## Publish and verify nightly + +- [ ] Follow [release operations](release-operations.md) to dispatch the nightly + workflow on canonical `main`, or identify its scheduled run. +- [ ] Record the source SHA, workflow run, generated version, artifact integrity, and + immutable release identity. A started workflow is not a completed publication. +- [ ] Verify the `nightly` npm dist-tag. Before the first stable version, verify + `latest` selects the same nightly; after stable exists, verify nightly automation + has left `latest` under stable ownership. +- [ ] Verify the exact published package, provenance, release assets, and installed + consumer behavior through the [package verification procedure](package-verification.md). +- [ ] Wait for the separate release Pages deployment and verify its version and source + metadata. The publisher cannot wait inside itself for its own completion-triggered + Pages workflow. +- [ ] Confirm an unchanged-source dispatch reports a verified skip rather than creating + another version. Classify partial results using the + [release recovery guidance](release-administration.md) before retrying. + +The [release-to-npm skill](../.agents/skills/release-to-npm/SKILL.md) coordinates this +stage and respects authorization already supplied for the requested release work. + +## Prepare a future stable release + +Stable publication is a separate maintainer operation. Use the +[release policy](releasing.md), [operations runbook](release-operations.md), and +[administration guide](release-administration.md) to establish the supported procedure +before dispatching it. + +- [ ] Choose and review an explicit stable version, API compatibility expectations, + migration guidance, and changelog promotion in a release pull request. +- [ ] Verify the protected stable npm environment, trusted publisher, and reviewer + requirements. Nightly's unattended environment does not authorize stable publication. +- [ ] Serialize stable and nightly publication through the same release concurrency + boundary so a nightly cannot race the transition of `latest`. +- [ ] Publish and verify the stable package and its versioned site before declaring the + transition complete. Keep the nightly channel independently available. +- [ ] Confirm `latest` selects the verified stable version and subsequent nightlies + never move it. Retain immutable historical alpha and nightly artifacts. +- [ ] Follow the documented recovery procedure for any partial result. Never overwrite + a published version or move an immutable version tag to repair a release. + +The first stable release is not implied by completing this checklist for a nightly. + +[Back to the documentation hub](README.md) From 4d063ed1a3487940f13e9b6ade0d102709838515 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 19:16:32 -0700 Subject: [PATCH 11/22] ci: publish nightlies through OIDC without tag tokens --- .agents/skills/release-to-npm/SKILL.md | 12 +- .../references/litefold-calendar.md | 5 +- .github/workflows/publish-nightly.yml | 143 +++++------------- README.md | 2 +- SECURITY.md | 9 +- docs/change-delivery.md | 6 +- docs/example-deployment.md | 2 +- docs/package-verification.md | 16 +- docs/release-administration.md | 48 ++---- docs/release-operations.md | 16 +- docs/releasing.md | 10 +- docs/security-model.md | 12 +- scripts/lib/nightly-release.mjs | 22 +-- scripts/tests/nightly-release.test.mjs | 30 ++-- scripts/tests/publish-nightly-policy.test.mjs | 53 ++++--- scripts/tests/workflow-contracts.test.mjs | 27 ++-- 16 files changed, 155 insertions(+), 258 deletions(-) diff --git a/.agents/skills/release-to-npm/SKILL.md b/.agents/skills/release-to-npm/SKILL.md index 747d657..f79aa0d 100644 --- a/.agents/skills/release-to-npm/SKILL.md +++ b/.agents/skills/release-to-npm/SKILL.md @@ -41,8 +41,10 @@ response, or remembered prior run is not publication authority. Derive the candidate strategy, environment names, and required roles from the current runbook and hosted state. An authorized nightly start uses unattended -trusted publication and does not require a manual environment reviewer or local -npm login. Stable publication retains its separately configured review controls. +OIDC trusted publication and does not require a manual environment reviewer, npm +token, or local npm login. Nightly leaves `latest` unchanged and verifies it +through public registry reads. Stable publication retains its separately +configured review controls. One person may fill multiple roles only when the hosted policy permits it. For an authorized start or continuation, create or update the private release @@ -77,9 +79,9 @@ Never publish locally, collect credentials in conversation or release notes, manufacture a release trigger, push or move a version tag, rewrite a shared branch, or create public release state from an arbitrary ref. Changes to hosted controls or credential policy require an explicitly authorized, reviewed migration; a -failed release is not permission to improvise one. Route any scoped, expiring -nightly tag credential through the documented private secret setup, never source -code or retained artifacts. +failed release is not permission to improvise one. Do not introduce an npm token +or a secondary dist-tag write into the nightly path. The first stable release +owns the future transition of `latest` to stable. ## Follow the selected phase diff --git a/.agents/skills/release-to-npm/references/litefold-calendar.md b/.agents/skills/release-to-npm/references/litefold-calendar.md index d77f608..f425066 100644 --- a/.agents/skills/release-to-npm/references/litefold-calendar.md +++ b/.agents/skills/release-to-npm/references/litefold-calendar.md @@ -40,8 +40,9 @@ designed and approved one. Describe recovery by the observed state and required identity evidence, without relying on release chronology. Nightly publication snapshots reviewed `main` without a daily metadata commit. -Its workflow owns generated versions, exact artifact retention, npm channel -updates, and repeat-run recovery. Preserve historical alpha state. Stable +Its workflow owns generated versions, exact artifact retention, OIDC publication +under `nightly`, and repeat-run recovery. It uses no npm token and verifies that +`latest` remains unchanged. Preserve historical alpha state. Stable publication is a separate procedure and must not be inferred from a nightly request. The [delivery checklist](../../../../docs/change-delivery.md) connects ordinary source changes with final package and Pages verification. diff --git a/.github/workflows/publish-nightly.yml b/.github/workflows/publish-nightly.yml index 4f759df..cc1b85f 100644 --- a/.github/workflows/publish-nightly.yml +++ b/.github/workflows/publish-nightly.yml @@ -108,6 +108,7 @@ jobs: notes-artifact: ${{ steps.identity.outputs.notes-artifact }} notes-sha256: ${{ steps.evidence.outputs.notes-sha256 }} registry-predecessor: ${{ steps.registry-state.outputs.registry-predecessor }} + registry-latest: ${{ steps.registry-state.outputs.registry-latest }} registry-state-sha256: ${{ steps.registry-state.outputs.registry-state-sha256 }} source-commit: ${{ steps.identity.outputs.source-commit }} tag: ${{ steps.identity.outputs.tag }} @@ -157,21 +158,16 @@ jobs: env: GH_TOKEN: ${{ github.token }} LFC_VERSION: ${{ steps.identity.outputs.version }} - LFC_NIGHTLY_TOKEN_EXPIRES_AT: ${{ vars.NPM_NIGHTLY_TAG_TOKEN_EXPIRES_AT }} run: | set -euo pipefail npm view @tryagaindev/litefold-calendar versions dist-tags --json --registry https://registry.npmjs.org/ > "${RUNNER_TEMP}/registry.raw.json" node --input-type=module --eval "${LFC_NORMALIZE_NPM_VIEW_JSON}" "${RUNNER_TEMP}/registry.raw.json" "${RUNNER_TEMP}/registry.json" node --input-type=module <<'NODE' import { readFileSync, appendFileSync } from "node:fs"; - import { readNightlyPlan, validateNightlyRegistryState, tokenExpiryStatus } from "./scripts/lib/nightly-release.mjs"; + import { readNightlyPlan, validateNightlyRegistryState } from "./scripts/lib/nightly-release.mjs"; const metadata = JSON.parse(readFileSync(`${process.env.RUNNER_TEMP}/registry.json`, "utf8")); const state = validateNightlyRegistryState(metadata, await readNightlyPlan(`${process.env.RUNNER_TEMP}/nightly-plan.json`)); - if (!state.stableExists) { - const expiry = tokenExpiryStatus(process.env.LFC_NIGHTLY_TOKEN_EXPIRES_AT); - if (expiry.warn) console.log(`::warning::Rotate the nightly tag token before ${expiry.expiresAt}.`); - } - appendFileSync(process.env.GITHUB_OUTPUT, `registry-predecessor=${state.predecessor}\nstable-exists=${state.stableExists}\n`); + appendFileSync(process.env.GITHUB_OUTPUT, `registry-predecessor=${state.predecessor}\nregistry-latest=${state.latest}\nstable-exists=${state.stableExists}\n`); NODE predecessor="$(jq -r '."dist-tags".nightly // ."dist-tags".alpha' "${RUNNER_TEMP}/registry.json")" eligible=true @@ -837,98 +833,11 @@ jobs: printf 'publication=%s\n' "${publication}" >> "${GITHUB_OUTPUT}" - synchronize-latest: - name: Synchronize the temporary nightly latest channel - needs: [publish, verify] - runs-on: ubuntu-latest - timeout-minutes: 10 - environment: npm-nightly-tags - permissions: - contents: read - outputs: - latest-target: ${{ steps.target.outputs.latest-target }} - steps: - - name: Set up exact Node for tag synchronization - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.LFC_NODE_VERSION }} - package-manager-cache: false - - name: Install exact npm before exposing the tag credential - working-directory: ${{ runner.temp }} - run: npm install --global "npm@${LFC_NPM_VERSION}" --ignore-scripts - - name: Verify the candidate and stable ownership before tagging - id: target - env: - LFC_VERSION: ${{ needs.verify.outputs.version }} - LFC_EXPECTED_INTEGRITY: ${{ needs.publish.outputs.expected-integrity }} - run: | - set -euo pipefail - ready=false - for attempt in $(seq 1 40); do - if ! npm view @tryagaindev/litefold-calendar versions dist-tags --json --registry https://registry.npmjs.org/ > registry.raw.json || - ! npm view "@tryagaindev/litefold-calendar@${LFC_VERSION}" dist.integrity --json --registry https://registry.npmjs.org/ > integrity.raw.json; then - sleep 10 - continue - fi - node --input-type=module --eval "${LFC_NORMALIZE_NPM_VIEW_JSON}" registry.raw.json registry.json - node --input-type=module --eval "${LFC_NORMALIZE_NPM_VIEW_JSON}" integrity.raw.json integrity.json - # LFC_NIGHTLY_LATEST_POLICY_START - if node --input-type=module <<'NODE' - import { readFileSync, appendFileSync } from "node:fs"; - const data = JSON.parse(readFileSync("registry.json", "utf8")); - const versions = typeof data.versions === "string" ? [data.versions] : data.versions; - const integrity = JSON.parse(readFileSync("integrity.json", "utf8")); - const stable = (version) => typeof version === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/u.test(version); - if (integrity !== null && integrity !== process.env.LFC_EXPECTED_INTEGRITY) { - throw new Error("Nightly registry integrity changed before latest synchronization."); - } - if (!Array.isArray(versions) || !versions.includes(process.env.LFC_VERSION) || - data["dist-tags"]?.nightly !== process.env.LFC_VERSION || integrity === null) { - process.exit(75); - } - const stableExists = versions.some(stable); - const latest = data["dist-tags"].latest; - if (!versions.includes(latest) || (stableExists && !stable(latest))) { - throw new Error("Stable owns latest after its first publication; refusing an inconsistent channel."); - } - const target = stableExists ? latest : process.env.LFC_VERSION; - appendFileSync(process.env.GITHUB_OUTPUT, `latest-target=${target}\nwrite=${latest !== target}\n`); - NODE - # LFC_NIGHTLY_LATEST_POLICY_END - then - ready=true - break - else - status=$? - if [[ "${status}" != "75" ]]; then exit "${status}"; fi - fi - sleep 10 - done - if [[ "${ready}" != "true" ]]; then - echo 'Nightly registry reads did not converge before latest synchronization.' >&2 - exit 1 - fi - - name: Advance only latest to the verified nightly - if: ${{ steps.target.outputs.write == 'true' }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_NIGHTLY_TAG_TOKEN }} - NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/nightly-tag.npmrc - LFC_VERSION: ${{ needs.verify.outputs.version }} - LFC_TOKEN_EXPIRES_AT: ${{ vars.NPM_NIGHTLY_TAG_TOKEN_EXPIRES_AT }} - run: | - set -euo pipefail - test -n "${NODE_AUTH_TOKEN}" - test "$(date -u --date="${LFC_TOKEN_EXPIRES_AT}" +%s)" -gt "$(date -u +%s)" - printf '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\n' > "${NPM_CONFIG_USERCONFIG}" - trap 'rm -f -- "${NPM_CONFIG_USERCONFIG}"' EXIT - npm dist-tag add "@tryagaindev/litefold-calendar@${LFC_VERSION}" latest --registry https://registry.npmjs.org/ - verify-registry: name: Verify registry integrity, imports, and provenance needs: - publish - verify - - synchronize-latest runs-on: ubuntu-latest timeout-minutes: 25 @@ -956,7 +865,7 @@ jobs: - name: Verify the published package from a clean consumer env: LFC_EXPECTED_INTEGRITY: ${{ needs.publish.outputs.expected-integrity }} - LFC_LATEST_TARGET: ${{ needs.synchronize-latest.outputs.latest-target }} + LFC_EXPECTED_LATEST: ${{ needs.verify.outputs.registry-latest }} LFC_SOURCE_COMMIT: ${{ needs.verify.outputs.source-commit }} LFC_TARBALL: ${{ runner.temp }}/release-bundle/${{ needs.verify.outputs.bundle-name }}.tgz LFC_VERSION: ${{ needs.verify.outputs.version }} @@ -977,31 +886,49 @@ jobs: > "${integrity_raw}" 2>/dev/null; then node --input-type=module --eval "${LFC_NORMALIZE_NPM_VIEW_JSON}" \ "${integrity_raw}" "${integrity_json}" - if npm view @tryagaindev/litefold-calendar dist-tags \ + if npm view @tryagaindev/litefold-calendar versions dist-tags \ --registry https://registry.npmjs.org/ --json \ > "${tags_raw}" 2>/dev/null; then node --input-type=module --eval "${LFC_NORMALIZE_NPM_VIEW_JSON}" \ "${tags_raw}" "${tags_json}" - integrity="$(jq -r 'if type == "string" then . else empty end' "${integrity_json}")" - nightly="$(jq -r '.nightly // empty' "${tags_json}")" - latest="$(jq -r '.latest // empty' "${tags_json}")" - if [[ -n "${integrity}" && "${integrity}" != "${LFC_EXPECTED_INTEGRITY}" ]]; then - echo "npm integrity conflicts with the verified release bundle." >&2 - exit 1 - fi - if [[ "${integrity}" == "${LFC_EXPECTED_INTEGRITY}" && - "${nightly}" == "${LFC_VERSION}" && - "${latest}" == "${LFC_LATEST_TARGET}" ]]; then + # LFC_NIGHTLY_READBACK_POLICY_START + if node --input-type=module <<'NODE' + import { readFileSync } from "node:fs"; + const root = process.env.RUNNER_TEMP; + const metadata = JSON.parse(readFileSync(`${root}/registry-tags.json`, "utf8")); + const integrity = JSON.parse(readFileSync(`${root}/registry-integrity.json`, "utf8")); + const versions = typeof metadata.versions === "string" ? [metadata.versions] : metadata.versions; + const tags = metadata["dist-tags"]; + if (integrity !== null && integrity !== process.env.LFC_EXPECTED_INTEGRITY) { + throw new Error("npm integrity conflicts with the verified release bundle."); + } + if (!Array.isArray(versions) || !versions.includes(process.env.LFC_VERSION) || + tags?.nightly !== process.env.LFC_VERSION || integrity === null) { + process.exit(75); + } + const stable = (version) => typeof version === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/u.test(version); + if (tags.latest !== process.env.LFC_EXPECTED_LATEST || !versions.includes(tags.latest)) { + throw new Error("npm latest changed from the verified preflight snapshot; nightly cannot modify it."); + } + if (versions.some(stable) ? !stable(tags.latest) : tags.latest !== tags.alpha) { + throw new Error("npm latest must remain on historical alpha until stable takes ownership."); + } + NODE + # LFC_NIGHTLY_READBACK_POLICY_END + then verified=true break + else + status=$? + if [[ "${status}" != "75" ]]; then exit "${status}"; fi fi fi fi [[ "${attempt}" -lt 60 ]] && sleep 10 done if [[ "${verified}" != "true" ]]; then - echo "npm did not expose the exact package integrity and both required dist-tags within the polling window." >&2 - echo "Check for pending or blocked publish-time review before changing registry metadata. Resume this exact attempt after correcting the reported registry or tag-token failure." >&2 + echo "npm did not expose the exact package integrity, nightly tag, and unchanged latest tag within the polling window." >&2 + echo "Check for pending or blocked publish-time review before changing registry metadata. Resume this exact attempt after correcting the reported registry or registry verification failure." >&2 exit 1 fi diff --git a/README.md b/README.md index f5436e2..796efc8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Litefold Calendar is a mobile-first, responsive month calendar for applications Use Litefold Calendar when your product needs a polished month calendar without adopting a complete scheduling platform or maintaining separate mobile and desktop implementations. -> **Prerelease:** The `nightly` channel contains validated snapshots of `main`. Before the first stable release, `latest` also selects the current nightly; after stable publication, `latest` belongs to stable releases. Pin an exact version for reproducible deployments. Public API changes remain possible before `1.0.0` and are recorded in the changelog. Existing alpha releases remain available by exact version. +> **Prerelease:** The `nightly` channel contains validated snapshots of `main`. Nightly publication leaves `latest` on the frozen historical alpha; the first stable release will move `latest` to stable, and stable releases will own it afterward. Install `@nightly` explicitly or pin an exact version for reproducible deployments. Public API changes remain possible before `1.0.0` and are recorded in the changelog. Existing alpha releases remain available by exact version. ## Install diff --git a/SECURITY.md b/SECURITY.md index d331ff8..dff2909 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,15 +4,15 @@ Use this policy to report a suspected vulnerability in the published package, pu ## Supported versions -Litefold Calendar is pre-1.0. Security fixes are released only for the newest public alpha and the current `main` branch. +Litefold Calendar is pre-1.0. Security fixes are released through the newest public nightly and the current `main` branch. | Version | Supported | | --- | --- | -| Latest `alpha` release | Yes | +| Latest `nightly` release | Yes | | Current `main` development state | Yes, before the next release | | Older prereleases, forks, and modified packages | No | -Published npm versions are immutable. A fix receives a new alpha version; affected older versions may be deprecated with upgrade guidance. +Published npm versions are immutable. A fix receives a new nightly version; affected older versions may be deprecated with upgrade guidance. Historical alpha versions remain frozen. ## Report a vulnerability privately @@ -71,8 +71,9 @@ The following properties must hold: ### Build and release -- Public releases originate from the exact eligible `main` push containing an allowlisted deterministic release-state change. The publisher reruns the complete gate for that exact SHA. Retained evidence includes a canonical SPDX 2.3 SBOM bound to the package version, full source SHA, and source-commit time; under the pinned toolchain, its bytes are deterministic for the same clean commit. +- Public nightlies snapshot the exact eligible current `main` commit through scheduled or manual publication. The publisher runs the complete gate for that exact SHA before retaining the candidate bytes. Its generated version binds the original workflow creation time and run ID; isolated staging changes only the package manifest version. Retained evidence includes a canonical SPDX 2.3 SBOM bound to the generated package version, full source SHA, and source-commit time. Recovery reuses verified retained bytes for the same candidate. - Source execution and publication authority never coincide. Verification has read-only repository access and no npm or Pages OIDC. The npm-authorized job checks out no source, installs or imports no candidate package, executes no project code, and publishes only the checksum-verified retained tarball. Registry verification, repository writes, and Pages deployment remain separately scoped. +- Nightly npm publication uses OIDC only under `nightly`, without an npm token or secondary dist-tag write. Public reads verify that `latest` is unchanged from preflight: it remains on the frozen historical alpha before stable and belongs to stable after the first stable release. - Remote release state fails closed. Unavailable, ambiguous, conflicting, or malformed npm, authenticated GraphQL, GitHub, or Pages state never authorizes a transition. Registry completion independently verifies exact integrity, a clean installation and supported imports, signatures, and SLSA provenance bound to the artifact and exact source identity. - Automatic rolling Pages previews move monotonically by source ancestry; only explicit retained-state rollback may move backward. Existing release directories remain immutable. Automatic Pages deployment is `workflow_run`-only, while manual rollback is isolated in a separate `workflow_dispatch`-only workflow that cannot accept a release ref. Rollback reconstructs inside the contents writer from authenticated retained Git objects and the current retained shell; no producer-composed site artifact crosses into write authority. The shared assembler enforces the exact root CSP and rejects remote runtime assets. Both workflows use one non-canceling maximum queue from retained-state validation through deployment, so their writers and deployers cannot interleave. A successful same-repository publisher completion starts release Pages after the registry-verified GitHub prerelease is public, while separately scoped Pages authority performs the deployment. diff --git a/docs/change-delivery.md b/docs/change-delivery.md index 0ea5763..7c1248f 100644 --- a/docs/change-delivery.md +++ b/docs/change-delivery.md @@ -52,9 +52,9 @@ Git stages. A commit or push alone does not start publication. workflow on canonical `main`, or identify its scheduled run. - [ ] Record the source SHA, workflow run, generated version, artifact integrity, and immutable release identity. A started workflow is not a completed publication. -- [ ] Verify the `nightly` npm dist-tag. Before the first stable version, verify - `latest` selects the same nightly; after stable exists, verify nightly automation - has left `latest` under stable ownership. +- [ ] Verify the `nightly` npm dist-tag and confirm `latest` still selects its + recorded preflight version. Once stable exists, that unchanged version must be + stable. Nightly publication uses OIDC only and does not update `latest`. - [ ] Verify the exact published package, provenance, release assets, and installed consumer behavior through the [package verification procedure](package-verification.md). - [ ] Wait for the separate release Pages deployment and verify its version and source diff --git a/docs/example-deployment.md b/docs/example-deployment.md index 4b430ac..ead7716 100644 --- a/docs/example-deployment.md +++ b/docs/example-deployment.md @@ -32,7 +32,7 @@ Before the first deployment, a maintainer with GitHub `Admin` access to the repository must: 1. Configure Pages to use **GitHub Actions** as its source. -2. Protect the `github-pages` environment independently from `npm-nightly` and `npm-nightly-tags` and allow deployments from `main` only. +2. Protect the `github-pages` environment independently from `npm-nightly` and allow deployments from `main` only. 3. Allow the workflow token to create and maintain `pages-content` while restricting direct and force pushes to that branch. 4. Keep `main`, `v*` tags, workflow files, and environment-rule changes under review. diff --git a/docs/package-verification.md b/docs/package-verification.md index 5ece786..8a4a206 100644 --- a/docs/package-verification.md +++ b/docs/package-verification.md @@ -149,12 +149,14 @@ Tree shaking and package contents answer different questions. The extension file ## Registry and release evidence -The publisher retries npm's eventually consistent reads and verifies the exact -version, `nightly` tag, registry integrity, provenance/signatures, clean -installation, public imports, and stylesheet before publishing the GitHub -prerelease. A separate job advances `latest` to the nightly while no stable -version exists. Once stable exists, verification requires stable ownership of -`latest` and nightlies leave it alone. Historical `alpha` remains frozen. +The publisher uses npm trusted publishing through GitHub Actions OIDC, without +an npm token, and publishes under `nightly`. It retries npm's eventually +consistent reads and verifies the exact version, `nightly` tag, registry +integrity, provenance/signatures, clean installation, public imports, and +stylesheet before publishing the GitHub prerelease. The registry verification job +also checks that `latest` remains unchanged from preflight. Before stable exists, +`latest` must select the frozen historical `alpha`; once stable exists, +verification requires stable ownership of `latest`. Successful publisher completion triggers the separately authorized immutable Pages deployment through a same-repository `workflow_run`. Pages verifies the @@ -173,7 +175,7 @@ Replace `EXACT_VERSION` with the release being checked. Confirm that: - The returned name and version are exact. - `dist.integrity` equals `npmIntegrity` in the GitHub prerelease's `package-verification.json`. -- `nightly` selects that exact version. Before stable exists, `latest` matches it; afterward `latest` selects a stable version. Historical `alpha` has not moved. +- `nightly` selects that exact version. `latest` matches its preflight value; once stable exists, it selects a stable version. Historical `alpha` has not moved. - The npm package page shows provenance for the expected repository and `publish-nightly.yml` workflow. The workflow installs the exact version into a clean consumer before running `npm audit signatures`. Running that command in the repository would verify the development dependency tree instead. If a registry read is unavailable, ambiguous, or inconsistent with the retained bundle, stop and use the [recovery matrix](release-administration.md#recovery-matrix). diff --git a/docs/release-administration.md b/docs/release-administration.md index e255a52..b774fbe 100644 --- a/docs/release-administration.md +++ b/docs/release-administration.md @@ -1,6 +1,6 @@ # Release administration and recovery -This guide is for maintainers who configure GitHub and npm release controls. The [nightly operations runbook](release-operations.md) owns the normal ordered procedure. This document owns environment setup, credential rotation, immutable-state recovery, and requirements for a future stable publisher. +This guide is for maintainers who configure GitHub and npm release controls. The [nightly operations runbook](release-operations.md) owns the normal ordered procedure. This document owns environment setup, trusted publishing, immutable-state recovery, and requirements for a future stable publisher. ## One-time hosted prerequisites @@ -13,8 +13,8 @@ Require two-factor authentication and grant repository and organization administ ### GitHub repository and release - Protect `main` with reviewed pull requests, resolved conversations, linear history, deletion/force-push protection, and required CI. Require **Build, test, and verify package** and both **Qualify Firefox** jobs. Require code-owner and independent review where another eligible maintainer exists. -- Configure `npm-nightly` and `npm-nightly-tags` as separate main-only environments. Neither uses a required reviewer or wait timer: scheduled and manually requested nightlies must complete unattended. Review changes to those environments and their workflow paths. -- Keep `npm-nightly` free of registry tokens. Store the tag credential only in `npm-nightly-tags`. +- Configure `npm-nightly` as the main-only npm publication environment, without a required reviewer or wait timer: scheduled and manually requested nightlies must complete unattended. Review changes to the environment and its workflow path. +- Keep `npm-nightly` free of registry tokens. Nightly publication needs no npm secret or additional tag-writing environment. - Protect `v*` tags against updates and deletion. Permit creation only for the narrowest available actor; GitHub Actions credentials identify an application, not a single workflow. Review every workflow with repository-write authority. - Enable [GitHub immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases). Stage assets on the draft, then publish. Audit titles and notes against retained evidence because platform immutability protects tags and assets, not every metadata field. - Keep dependency monitoring, code scanning, secret scanning, push protection, and private vulnerability reporting enabled where available. Resolve or disposition release-blocking findings. @@ -23,25 +23,13 @@ Require two-factor authentication and grant repository and organization administ npm supports multiple independent trusted-publisher configurations per package. They are additive: matching any configuration authorizes its permitted operation. Every configuration allows staging by default; direct publication requires a separate opt-in on that configuration. [npm's September 2026 publisher update](https://github.blog/changelog/2026-09-03-multiple-trusted-publishing-configurations-for-npm/) -Bind [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) to the exact owner `tryagaindev`, repository `litefold-calendar`, workflow filename `publish-nightly.yml`, and environment `npm-nightly`. **Enable direct publication for this nightly binding** so the workflow's `npm publish` completes without staged-release approval. Verify that setting explicitly; adding the binding with its staging-only default does not satisfy unattended delivery. The publisher uses a GitHub-hosted runner and receives `id-token: write`. +Bind [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) to the exact owner `tryagaindev`, repository `litefold-calendar`, workflow filename `publish-nightly.yml`, and environment `npm-nightly`. Enable **Allow npm publish** for direct publication so the workflow's `npm publish` completes without staged-release approval. Save and reload the binding to verify that setting; an unsaved form or the staging-only default does not satisfy unattended delivery. The publisher uses a GitHub-hosted runner and receives `id-token: write`. Adding the nightly binding does not revoke the alpha binding. Confirm no historical alpha publication is in flight, then remove the old alpha binding before enabling the nightly schedule. Preserve historical package versions, tags, releases, and evidence. -Maintain two-factor authentication for package maintainers and verify scope/package ownership. The isolated `latest` update requires a granular write token because the adopted npm workflow does not use OIDC for secondary dist-tag updates. This intentionally replaces the previous package setting that disallowed tokens. Select a publishing access policy that continues to require human 2FA while allowing the configured granular token; verify the effective policy in npm. +Maintain two-factor authentication for package maintainers, verify scope/package ownership, and keep token-based publication disallowed. Configure package access, maintainers, and trusted publishing through a private interactive npm session with the required 2FA challenge. Verify the effective package policy after saving changes. -Create or delete tokens and change package access, maintainers, or trusted-publisher settings through a private interactive npm session with a 2FA challenge. A bypass-2FA token cannot perform those management operations. This applies to initial setup and subsequent credential rotations. [npm's July 2026 token-management change](https://github.blog/changelog/2026-07-31-restricting-npm-bypass-2fa-granular-access-tokens/) - -Provision the credential privately through npm's [granular token settings](https://docs.npmjs.com/creating-and-viewing-access-tokens/): - -1. Give it **Read and write (publish and stage)** access to **only** `@tryagaindev/litefold-calendar`. Do not grant the entire scope or organization-management permissions. -2. Enable the 2FA bypass needed for unattended package metadata writes. This grants broader package-write authority than dist-tags alone; keep it isolated even though the workflow uses it only for `npm dist-tag add`. -3. Set expiry to **30 days**. Store its exact UTC expiry as repository variable `NPM_NIGHTLY_TAG_TOKEN_EXPIRES_AT`, formatted `YYYY-MM-DDTHH:mm:ssZ`. -4. Store the value as environment secret `NPM_NIGHTLY_TAG_TOKEN` in `npm-nightly-tags`. Do not paste it into a task, source file, workflow input, or release note. -5. Verify environment branch restrictions and that only the final tag-writing step receives the credential. Tool installation and public registry validation run before that step. - -Do not introduce the token into the OIDC publish job, source checkout, project scripts, package installation, or Pages. The workflow's temporary npm authentication file is removed after the tag command. Keep a private inventory of token owner, selected package, creation/expiry, and rotation verifier without storing the value there. - -npm targets January 2027 for removing direct publication from bypass-2FA granular tokens. Package bytes in this workflow already publish through OIDC; the isolated token remains the secondary-dist-tag workaround tracked in [npm CLI issue 8547](https://github.com/npm/cli/issues/8547). Replace that workaround when supported OIDC tag updates are available. [Announced token publication change](https://github.blog/changelog/2026-07-31-restricting-npm-bypass-2fa-granular-access-tokens/) +The nightly path publishes only through OIDC under `nightly`. It performs public read-only verification of `latest` and never writes that tag. Do not create an npm token, authentication secret, expiry variable, or token-backed fallback for this workflow. The first stable publisher will own the separate transition of `latest` to stable. ### GitHub Pages @@ -54,8 +42,7 @@ Use **GitHub Actions** as the Pages source. Keep `github-pages` separate from np | Classify and verify | Exact source checkout, read-only repository access, same-commit green CI, full gate, registry predecessor and prior Pages checks | | Stage release | No checkout; repository write access to verified tag, draft, notes, and assets | | Publish npm | No checkout; `npm-nightly` OIDC publishes only checksum-verified retained bytes | -| Synchronize channels | No checkout; `npm-nightly-tags` exposes the granular token only after candidate and stable-ownership checks | -| Verify and finalize | Clean public-package verification, then source-free GitHub finalization | +| Verify and finalize | Clean public-package verification, read-only confirmation that `latest` is unchanged, then source-free GitHub finalization | | Deploy Pages | Publisher-linked receipt verification; separate build, retained-state, and Pages permissions | The workflow runs on schedule or manual dispatch from canonical `main`. A new publication requires current main and successful exact-source CI. Recovery of an already published candidate may use the original attempt after main advances only when its retained and public identities still match; this exception cannot authorize a fresh upload from old source. @@ -66,21 +53,10 @@ The non-canceling `npm-nightly-` concurrency group covers publicatio ## Nightly channel policy -Before a stable version exists, completed delivery requires `nightly` and `latest` to select the same nightly. Once any stable version exists, nightly automation must preserve stable ownership of `latest`. A missing or malformed registry response is never interpreted as an empty package or as proof that stable does not exist. +Completed nightly delivery requires `nightly` to select the exact candidate and `latest` to retain its verified preflight value. Before stable exists, `latest` must select the frozen historical `alpha` version. Nightly never writes `latest`. The first stable release will move `latest` to stable; once any stable version exists, nightly verification also requires it to select a published stable version. A missing or malformed registry response is never interpreted as an empty package or as proof that stable does not exist. The historical `alpha` dist-tag, versions, protected tags, immutable releases, and release Pages stay frozen. The first nightly can use the completed alpha as its predecessor. It does not rewrite alpha metadata or republish an alpha. -## Tag-token rotation - -Before stable exists, verification checks the expiry variable before public writes and emits a warning when seven days or fewer remain. A missing, malformed, or expired timestamp blocks publication. The variable is an operator-recorded fact; it does not independently prove that the token is valid or unrevoked. - -1. In a private interactive npm session, complete the required 2FA challenge and create a replacement with the same package-only access and 30-day expiry. -2. Replace `npm-nightly-tags` secret `NPM_NIGHTLY_TAG_TOKEN` and repository variable `NPM_NIGHTLY_TAG_TOKEN_EXPIRES_AT` together. Verify the timestamp against npm. -3. Resume the original failed attempt if rotation repairs a transient authentication failure; otherwise let the next eligible nightly use it. -4. Verify the exact channel update, then revoke the old token through npm's interactive 2FA flow. Record rotation evidence without credential material. - -If a token is compromised, revoke it immediately and investigate package versions and tags before resuming. Do not wait for the normal overlap. Once stable owns `latest`, the nightly tag-writing step no longer needs this token; retire it after confirming the stable workflow's separate authority. - ## Rerun procedure A rerun keeps the original run ID, creation time, source commit, workflow definition, and nightly version. `run_attempt` may increase; it does not change the package identity. Later source changes do not become part of that attempt. @@ -91,7 +67,7 @@ A rerun keeps the original run ID, creation time, source commit, workflow defini 4. Confirm that an existing publication is reused with identical bytes. Do not rebuild and republish the same version. Missing or expired evidence requires investigation, not a guessed reconstruction. 5. Verify package, channels, immutable release, and the exact publisher-linked Pages run after recovery. -A rerun can consume repaired hosted configuration or credentials. A source or publisher-code repair requires a new reviewed commit and new nightly identity. Do not rerun an old workflow expecting it to load a newer implementation. +A rerun can consume repaired hosted trusted-publisher or environment configuration. A source or publisher-code repair requires a new reviewed commit and new nightly identity. Do not rerun an old workflow expecting it to load a newer implementation. ## Recovery matrix @@ -102,8 +78,8 @@ A rerun can consume repaired hosted configuration or credentials. A source or pu | Candidate has a matching staged tag/draft/assets but no npm version, and its source remains current main | Preserve the stage and resume its exact attempt; do not collide with or overwrite the staged identity. | | Candidate has a matching staged tag/draft/assets but no npm version, and main has advanced | Confirm no upload was accepted or remains pending. Preserve the abandoned staged identity and retained evidence, then start a new eligible current-main run with a new version. The old attempt cannot publish from stale source; do not move or overwrite its tag, draft, or assets. | | npm accepted the upload but the exact version is not yet publicly readable | Treat it as pending, blocked, or ambiguous. Check registry status and private package-maintainer notifications. Do not reuse the version or publish another candidate to bypass uncertainty. | -| Exact npm version and integrity exist; channel synchronization or finalization failed | Restore transient credentials/configuration, then resume the original attempt with the retained bytes. Reclassify changed registry state through a full rerun when necessary. | -| Tag token is expired or revoked | Rotate the isolated token and expiry variable, verify package state, then resume the original affected attempt. | +| Exact npm version and integrity exist; registry verification or finalization failed | Restore transient hosted configuration, then resume the original attempt with the retained bytes. Investigate unexpected tag changes before reclassifying registry state through a full rerun. | +| `latest` differs from its recorded preflight value | Stop and investigate the external change. Nightly has no authority to repair it; do not add a token or move a dist-tag. Resume only after the observed state is explained and valid under the channel policy. | | Stable version exists but `latest` selects a prerelease | Stop. Stable owns `latest`; investigate and use the approved stable recovery procedure. Nightly must not repair this by pointing `latest` to itself. | | npm version, tag, draft, notes, assets, or receipt conflict | Stop and investigate. Do not overwrite, delete, or reuse the identity. A reviewed correction receives a new greater version. | | Published package or example bytes are defective | Preserve immutable objects. Publish a corrected greater nightly and consider authorized deprecation of the defective version. | @@ -117,7 +93,7 @@ A rerun can consume repaired hosted configuration or credentials. A source or pu Stable publication is not enabled by the nightly workflow. Before the first stable release, review and merge a stable workflow and validator changes implementing these requirements: - Select an explicit stable SemVer through maintainer review. Update source manifest and root lockfile versions consistently, and promote reviewed `[Unreleased]` notes into the dated stable changelog entry. -- Require protected main, exact-source green CI, the full gate, and a separate stable publication environment with the intended human approval policy. Configure its exact npm trusted publisher binding; do not grant the nightly token stable-publication responsibility. +- Require protected main, exact-source green CI, the full gate, and a separate stable publication environment with the intended human approval policy. Configure its exact npm trusted publisher binding; nightly publication authority does not authorize stable publication. - Use the existing publication queue across stable and nightly jobs. Publish one verified retained bundle with matching SBOM, integrity, source, provenance, protected tag, and immutable release evidence. - Publish stable under `latest` and verify it before allowing subsequent nightlies. From that first stable onward, nightly automation leaves `latest` under stable ownership even if a later nightly fails. - Advance the development base beyond the stable series in a reviewed follow-up before the next nightly. Keep `nightly`, `alpha`, and every existing release snapshot intact. diff --git a/docs/release-operations.md b/docs/release-operations.md index 20b08d2..6b66d80 100644 --- a/docs/release-operations.md +++ b/docs/release-operations.md @@ -1,6 +1,6 @@ # Nightly release operations runbook -Use this runbook to publish and verify a nightly from reviewed `main`. Daily publication runs at **09:00 UTC**; use manual dispatch when a completed change needs publication immediately. The publisher derives the version, publishes through OIDC, updates the temporary `latest` channel, and hands off release Pages automatically. +Use this runbook to publish and verify a nightly from reviewed `main`. Daily publication runs at **09:00 UTC**; use manual dispatch when a completed change needs publication immediately. The publisher derives the version, publishes under `nightly` through OIDC only, verifies that `latest` is unchanged, and hands off release Pages automatically. There is no release-preparation pull request or manual npm approval for a normal nightly. This guide also explains how to recognize a successful unchanged-source skip. Stable publication is a future procedure described in [release administration](release-administration.md#future-stable-release); these commands do not publish stable. @@ -21,7 +21,7 @@ Stop on a failed required check, unknown registry state, mismatched identity, or ## Roles -The release operator verifies the change and follows the publisher through Pages completion. Repository administrators maintain environments and required checks. An npm package maintainer provisions and rotates the isolated tag credential. Once those controls are configured, a normal run needs no manual channel update or protected-environment approval. +The release operator verifies the change and follows the publisher through Pages completion. Repository administrators maintain environments and required checks. An npm package maintainer configures the trusted publisher. Once those controls are configured, a normal run needs no npm token, manual channel update, or protected-environment approval. ## Start a release record @@ -36,7 +36,7 @@ Keep a private, non-versioned record with these values. Record sanitized evidenc | Publisher identity | Run URL, run ID, original UTC creation time, attempt number | | Published identity | `EXACT_VERSION`, protected tag `vEXACT_VERSION` | | Package evidence | Receipt, npm integrity, every retained asset digest, SBOM, compatibility target report | -| Public verification | Registry versions/tags, signatures, provenance, immutable GitHub prerelease | +| Public verification | Registry versions/tags including preflight and final `latest`, signatures, provenance, immutable GitHub prerelease | | Pages | Publisher-linked run, release metadata URL, matching source and version | | Completion or skip | UTC time and evidence establishing the outcome | @@ -49,7 +49,7 @@ Keep a private, non-versioned record with these values. Record sanitized evidenc - [ ] The final screenshots and any required manual accessibility evidence were reviewed. - [ ] Release-blocking security findings are resolved or explicitly dispositioned with recorded evidence. - [ ] Hosted settings match the [administration guide](release-administration.md#one-time-hosted-prerequisites). -- [ ] Before stable exists, the nightly tag token and its recorded expiry are current. Rotate when the seven-day warning appears. +- [ ] Record the current `latest` version for read-only comparison after publication. Before stable exists, it must select the frozen historical `alpha`; afterward, it must select a stable version. - [ ] The preceding nightly is complete through Pages, or any partial attempt has been classified for recovery first. Use these read-only checks when working from GitHub CLI and npm: @@ -94,8 +94,7 @@ Wait for the exact run to finish. It may queue behind an earlier publication; do | **Verify exact source and create release evidence** | Checks previous completion and either skips unchanged source or builds one verified candidate | | **Stage exact tag, draft, and release assets** | Stages the exact source tag and five retained assets | | **Publish or resume the verified npm nightly** | Publishes or recognizes the identical retained package through `npm-nightly` OIDC | -| **Synchronize the temporary nightly latest channel** | Advances `latest` before stable exists; preserves stable ownership afterward | -| **Verify registry integrity, imports, and provenance** | Verifies the public package from a clean consumer | +| **Verify registry integrity, imports, and provenance** | Verifies the public package from a clean consumer and confirms `latest` still selects its preflight version | | **Publish the verified GitHub prerelease** | Finalizes the verified immutable prerelease and triggers Pages | The five uploaded release assets are the package `.tgz`, `package-verification.json`, `sbom.spdx.json`, `SHA256SUMS`, and `LICENSE`. GitHub's automatically generated source archives are excluded from that count. The Actions bundle and notes are retained for 30 days; browser diagnostics for seven days. Complete recovery while evidence remains available. @@ -113,9 +112,9 @@ npm view "@tryagaindev/litefold-calendar@EXACT_VERSION" name version dist.integr npm view @tryagaindev/litefold-calendar versions dist-tags --json --registry https://registry.npmjs.org/ ``` -Check that the exact package is readable and its `dist.integrity` equals the receipt's `npmIntegrity`. `nightly` must select `EXACT_VERSION`. If no stable version exists, `latest` must also select it. If any stable version exists, `latest` must select a stable version and the nightly job must have left it unchanged. Historical `alpha` stays unchanged. +Check that the exact package is readable and its `dist.integrity` equals the receipt's `npmIntegrity`. `nightly` must select `EXACT_VERSION`. `latest` must still select its recorded preflight version; the nightly workflow never updates it. Before stable exists, `latest` selects the frozen historical `alpha`; once any stable version exists, it selects stable. Historical `alpha` stays unchanged. -Channel synchronization is automated. If it fails, use [credential rotation and recovery](release-administration.md#tag-token-rotation), then resume the original verified attempt. A successful npm upload is not permission to create a replacement candidate while completion remains uncertain. +Channel verification is read-only. If `latest` changed or registry identity is uncertain, stop and use the [recovery matrix](release-administration.md#recovery-matrix). Do not add a token or move a dist-tag to repair a nightly. A successful npm upload is not permission to create a replacement candidate while completion remains uncertain. @@ -124,6 +123,7 @@ Channel synchronization is automated. If it fails, use [credential rotation and - [ ] The receipt's published version, source version, full commit, workflow identity, and manifest transformation match the record. - [ ] The tag resolves to `SOURCE_SHA`; the GitHub release is public, immutable, and marked as a prerelease. - [ ] All five uploaded assets have the recorded digests; the notes and title match retained evidence. +- [ ] The exact nightly version's npm package page renders its README, installation commands, and documentation links correctly. Select the nightly version explicitly; `latest` still identifies historical alpha before stable. - [ ] Clean public-package imports, stylesheet use, core/WebMCP integration, registry signatures, and provenance checks passed. - [ ] For a release introducing calendar behavior, a clean consumer verifies count activation, remote filtering, and focus return after an application dialog. diff --git a/docs/releasing.md b/docs/releasing.md index c95b3cd..2f5e806 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -11,7 +11,7 @@ Nightlies snapshot reviewed `main` directly. They do not create daily version co A completed nightly must satisfy these invariants: - The exact source commit passed CI, including required Firefox qualification, and the publication gate. New publication uses current canonical `main`. -- npm `nightly` selects the exact published snapshot. Before any stable version exists, `latest` selects it too. Once a stable version exists, stable owns `latest` permanently. +- npm `nightly` selects the exact published snapshot. Nightly publication leaves `latest` at its verified preflight value, which remains the frozen historical `alpha` before stable exists. The first stable release will move `latest` to stable, and stable owns it permanently afterward. - The protected `v` tag resolves to the source commit. The GitHub release is a public immutable prerelease with the retained package, receipt, SBOM, license, and checksums. - Receipt schema 2 binds the source version, published version, source commit, original workflow identity, and version-only manifest transformation. Retained evidence records package integrity, asset digests, toolchain, and resolved compatibility targets. - Release Pages metadata identifies that same published version and source commit. @@ -28,13 +28,13 @@ The source development base advances through a reviewed change when the next rel | Select and verify | Exact source checkout, read-only repository access, successful same-commit CI, fresh registry state, and the complete local gate | | Stage GitHub release | No source checkout; narrowly scoped write permission for the verified tag, draft, notes, and assets | | Publish npm | No source checkout; `npm-nightly` environment and OIDC publish only the retained tarball under `nightly` | -| Synchronize `latest` | No source checkout; separate `npm-nightly-tags` environment exposes a package-scoped granular token only to the tag-writing step | +| Verify `latest` | Read-only registry checks confirm its preflight value is unchanged and, once stable exists, still selects stable | | Verify and finalize | Fresh public-package verification followed by publication of the verified GitHub prerelease | | Deploy release Pages | Native successful-publisher handoff, verified receipt, exact source, and separate Pages authority | -Both nightly environments restrict execution to `main` and run without per-release human approval. This is an intentional unattended-delivery policy; code review and required checks protect the source and workflow. The granular token can write package data beyond dist-tags, so it is isolated and expires after 30 days. [Hosted setup](release-administration.md#one-time-hosted-prerequisites) records the required controls. +The `npm-nightly` environment restricts execution to `main` and runs without per-release human approval. Publication uses OIDC only and requires no npm token or tag-writing environment. This is an intentional unattended-delivery policy; code review and required checks protect the source and workflow. [Hosted setup](release-administration.md#one-time-hosted-prerequisites) records the required controls. -One non-canceling publication queue serializes all jobs through channel synchronization and release finalization. A future stable publisher must share that exact queue. Local artifacts and arbitrary refs do not confer publication authority. +One non-canceling publication queue serializes all jobs through registry verification and release finalization. A future stable publisher must share that exact queue. Local artifacts and arbitrary refs do not confer publication authority. ## Readiness and completion @@ -53,6 +53,6 @@ A transient failure can resume the original exact run when its source, workflow, ## Operating documents - [Nightly release operations](release-operations.md): ordered manual publication, completion checks, and release record. -- [Release administration](release-administration.md): environments, credentials, rotation, recovery, and future stable requirements. +- [Release administration](release-administration.md): environments, trusted publishing, recovery, and future stable requirements. - [Package verification](package-verification.md): artifact and installed-package evidence, integrity, signatures, and provenance. - [Static example deployment](example-deployment.md): rolling previews, immutable release snapshots, and preview rollback. diff --git a/docs/security-model.md b/docs/security-model.md index ca75136..fa32944 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -38,8 +38,8 @@ The model assumes: | WebMCP extension → browser agent | Paged visible-range state and presentation-safe source-event fields | Same-origin document registry, untrusted-content annotation, allowed-date filtering, no ID/URL/metadata/cause output, fixed result limits, no superseded-snapshot cache, unregister on teardown | | Contributor → CI | Source and workflow changes | Read-only pull-request permissions, full-SHA actions, no `pull_request_target`, locked script-disabled install, lint/type/policy/tests | | Validated main snapshot → retained artifacts | Reviewed source and build tooling | Complete gate, isolated version overlay, immutable workflow identity, checksums, integrity, receipt, SBOM, license, retained artifacts | -| Verified artifact → npm | Exact tarball and OIDC identity | Main-only `npm-nightly` environment, source-free publisher, hash/receipt/manifest validation, trusted publishing, provenance, post-publish verification | -| Verified nightly → secondary npm tag | Package-scoped granular write token | Separate source-free `npm-nightly-tags` job, 30-day expiry, warning seven days before expiry, registry identity checks, stable ownership of `latest` | +| Verified artifact → npm | Exact tarball and OIDC identity | Main-only `npm-nightly` environment, source-free publisher, hash/receipt/manifest validation, OIDC-only trusted publishing under `nightly`, provenance, no npm token | +| Published npm state → registry verification | Public registry metadata and retained package evidence | Read-only exact-version/integrity verification, unchanged preflight `latest`, frozen historical alpha before stable, stable ownership of `latest` afterward | | Verified release state → Git tag and GitHub prerelease | Tag and release-write authority | Protected exact-SHA tag, draft-first assets, matching-byte reuse only, immutable release setting, public prerelease last | | Verified Pages input → retained-state writer | Built automatic channel or authenticated retained rollback state | Exact-source or retained-commit identity, credential-stripped unprivileged assembly, exact root CSP, remote-runtime rejection, immutable release preservation | | Exact retained snapshot → GitHub Pages | `pages-content` commit and attempt-scoped Pages artifact | One queued workflow-level lock across writers and deployers, retained-head confirmation, protected Pages authority | @@ -48,7 +48,7 @@ The model assumes: flowchart LR accTitle: Litefold Calendar runtime and publication trust paths accDescr { - At runtime, browser users, application event sources, trusted render hooks, and the optional WebMCP extension interact with the calendar core, DOM, and callbacks. Separately, read-only CI verifies the exact main commit and can update the queued retained-state writer directly. Verified artifacts pass through main-only trusted npm publication, an isolated secondary-tag update, and an immutable GitHub prerelease before triggering that writer; an authorized rollback may instead supply an authenticated retained snapshot. The writer hands the exact snapshot to a separately authorized Pages deployment. + At runtime, browser users, application event sources, trusted render hooks, and the optional WebMCP extension interact with the calendar core, DOM, and callbacks. Separately, read-only CI verifies the exact main commit and can update the queued retained-state writer directly. Verified artifacts pass through main-only OIDC npm publication, read-only registry and unchanged-latest verification, and an immutable GitHub prerelease before triggering that writer; an authorized rollback may instead supply an authenticated retained snapshot. The writer hands the exact snapshot to a separately authorized Pages deployment. } U[Browser user] --> C[Calendar core] S[Application event source] --> C @@ -62,7 +62,7 @@ flowchart LR V --> R[Exact main snapshot] R --> B[Verified artifacts] B --> N[Main-only source-free npm publish] - N --> L[Isolated secondary-tag update] + N --> L[Read-only registry verification] L --> G[Immutable GitHub prerelease] G --> Q[Native successful-workflow trigger] V --> X[Queued retained-state writer] @@ -105,7 +105,7 @@ The scenarios below describe what each control must prevent. A scenario becomes | TM-006 | Enabling WebMCP discloses more calendar data than intended | Explicit extension selection, same application source authorization, pages of distinct eligible source events from the allowed portion of the loaded visible range, snapshot-bound continuations, no superseded-payload cache, no metadata/URL/ID output, explicit privacy guidance | Titles and raw normalized start/end civil values are intentionally exposed even when visually hidden; applications must opt in only where appropriate | | TM-007 | A model bypasses bounds, forges or replays pagination state, invokes application actions, or causes unbounded fetches | Exact schemas, argument and bounds validation, fixed page size, activation/range/scope/snapshot/offset cursor binding, read-only annotation, one non-read navigation tool, no activation/edit tools, existing source generations | Navigation may request another authorized month; providers must remain safe to repeat | | TM-008 | Duplicate, partial, or stale registrations target the wrong calendar | Stable unique prefix, document-wide collision failure, one shared abort signal, rollback after an observed failure, unregister on destroy, pre-wait and pending-wait lifecycle checks | WebMCP has no atomic batch or timeout, so a never-settling registration cannot be made atomic; application code can separately register conflicting names | -| TM-009 | Pull-request code gains npm, tag, release, or Pages authority | Read-only PR defaults, pinned actions, no `pull_request_target`, main-only publication environments, source-free privileged jobs, immutable release checks | Hosted rulesets and environment configuration remain external controls. The secondary-tag token has broader package-write authority; restrict it to this package, exclude organization access, rotate within 30 days, and expose it only to the tag job | +| TM-009 | Pull-request code gains npm, tag, release, or Pages authority | Read-only PR defaults, pinned actions, no `pull_request_target`, main-only publication environment, source-free privileged jobs, OIDC-only npm publication, immutable release checks | Hosted rulesets and trusted-publisher configuration remain external controls. Keep npm tokens out of the workflow and verify the exact saved binding and environment restrictions | | TM-010 | Release publishes bytes or identity other than the reviewed commit | Exact successful main SHA, immutable workflow creation time/run ID, isolated version overlay, target resolution, checksums/integrity/receipt/SBOM, source-free OIDC publisher, registry verification, exact tag and immutable release | npm publication is irreversible; defective versions are deprecated and replaced. Future stable publication retains a separate review boundary and shares publication concurrency | | TM-011 | Retried publication, rollback, or Pages deployment creates conflicting public state | Matching-byte/identity recovery, no version/tag reuse, retained Pages history, writer-side rollback reconstruction, immutable release directories, one maximum queued workflow-level lock across Pages writers and deployers, public identity verification | Ambiguous external state or a retained-shell policy failure must fail closed and require maintainer review | | TM-012 | A forged, duplicated, or over-privileged extension value gains unintended authority | Package-issued opaque values, synchronous whole-array validation and duplicate-ID rejection, least-privilege capability contexts, per-extension lifecycle guards | Future third-party authoring would create a wider trusted-code boundary and requires a separate lower-stability contract and threat review | @@ -135,7 +135,7 @@ Release-authority paths remain the highest supply-chain priority. Executable ren | `scripts/pack-release.mjs` and `scripts/pack-nightly.mjs` | Clean-source tarball, target evidence, integrity, receipt, SBOM, license, consumer verification | | Pages build and assembly scripts | Self-contained assets, exact root CSP, visible identity, retained releases, rolling preview, immutable paths | | `.github/workflows/ci.yml` | Pull-request and main verification without publication authority | -| `.github/workflows/publish-nightly.yml` | Canonical-main scheduled/manual classification, artifact handoff, isolated trusted publishing and secondary-tag authority, stable ownership, tag and prerelease creation | +| `.github/workflows/publish-nightly.yml` | Canonical-main scheduled/manual classification, artifact handoff, isolated OIDC-only trusted publishing, read-only latest preservation and stable ownership checks, tag and prerelease creation | | `.github/workflows/deploy-examples.yml` | `workflow_run`-only canonical workflow identity, rolling previews, release deployment, retained history, isolated Pages authority, and the shared whole-run deployment queue | | `.github/workflows/rollback-examples.yml` | `workflow_dispatch`-only retained-main rollback reconstructed in the writer from authenticated Git objects, current retained shell, trusted default-branch tooling, and immutable releases; shares the whole-run deployment queue | diff --git a/scripts/lib/nightly-release.mjs b/scripts/lib/nightly-release.mjs index 98dd4c1..c9f845d 100644 --- a/scripts/lib/nightly-release.mjs +++ b/scripts/lib/nightly-release.mjs @@ -77,6 +77,9 @@ export function validateNightlyRegistryState(metadata, planValue) { (stableExists && semver.prerelease(tags.latest) !== null)) { throw new Error("npm latest must select a published version, and belongs to stable after first stable."); } + if (!stableExists && (tags.latest !== tags.alpha || !/^0\.\d+\.\d+-alpha\.\d+$/u.test(tags.latest))) { + throw new Error("Before first stable, npm latest must remain on the frozen historical alpha."); + } const predecessor = tags.nightly ?? tags.alpha; if (typeof predecessor !== "string" || !versions.includes(predecessor) || !(NIGHTLY_VERSION_PATTERN.test(predecessor) || /^0\.\d+\.\d+-alpha\.\d+$/u.test(predecessor))) { @@ -95,28 +98,9 @@ export function validateNightlyRegistryState(metadata, planValue) { if (existing && tags.nightly !== plan.version) { throw new Error("Existing nightly candidate is not selected by the nightly tag."); } - if (!stableExists && predecessor !== plan.version && tags.latest !== predecessor) { - throw new Error("A previous nightly has incomplete latest synchronization; resume that attempt first."); - } return Object.freeze({ existing, latest: tags.latest, predecessor, stableExists }); } -/** Checks expiry before public writes; rotation does not expose token material. */ -export function tokenExpiryStatus(expiresAt, now = new Date()) { - const expiry = new Date(expiresAt); - if (typeof expiresAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u.test(expiresAt) || - !Number.isFinite(expiry.getTime()) || !Number.isFinite(now.getTime()) || - expiry.toISOString().replace(".000Z", "Z") !== expiresAt) { - throw new Error("Nightly tag-token expiry must be an explicit UTC timestamp."); - } - const remaining = expiry.getTime() - now.getTime(); - if (remaining <= 0) { - throw new Error("Nightly tag token has expired; rotate it before publishing."); - } - return Object.freeze({ expiresAt, warn: remaining <= 7 * 24 * 60 * 60 * 1_000 }); -} - - /** Selects retained evidence from this run; incomplete public attempts cannot rebuild. */ export function selectNightlyRecoveryArtifacts({ artifacts, plan: value, attempt, hasPublicState }) { const plan = validateNightlyPlan(value); diff --git a/scripts/tests/nightly-release.test.mjs b/scripts/tests/nightly-release.test.mjs index ab00de1..17624fe 100644 --- a/scripts/tests/nightly-release.test.mjs +++ b/scripts/tests/nightly-release.test.mjs @@ -5,7 +5,7 @@ import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { createNightlyPlan, nightlyPackageManifest, tokenExpiryStatus, validateNightlyPlan, +import { createNightlyPlan, nightlyPackageManifest, validateNightlyPlan, validateNightlyRegistryState, selectNightlyRecoveryArtifacts, verifyRestoredNightlyBundle } from "../lib/nightly-release.mjs"; import { validateDeploymentMetadata } from "../build-pages.mjs"; import { resolveBrowserTargets } from "../lib/browser-targets.mjs"; @@ -51,20 +51,22 @@ void test("first nightly preserves historical alpha and identifies its published }); }); -void test("ordinary nightlies require previous channel completion and increasing versions", () => { - const metadata = { versions: [ALPHA, PREVIOUS], "dist-tags": { alpha: ALPHA, nightly: PREVIOUS, latest: PREVIOUS } }; +void test("ordinary nightlies advance only nightly while historical alpha keeps latest", () => { + const metadata = { versions: [ALPHA, PREVIOUS], "dist-tags": { alpha: ALPHA, nightly: PREVIOUS, latest: ALPHA } }; assert.equal(validateNightlyRegistryState(metadata, PLAN).predecessor, PREVIOUS); + assert.equal(validateNightlyRegistryState(metadata, PLAN).latest, ALPHA); assert.throws(() => validateNightlyRegistryState({ ...metadata, - "dist-tags": { ...metadata["dist-tags"], latest: ALPHA } }, PLAN), /incomplete/u); + "dist-tags": { ...metadata["dist-tags"], latest: PREVIOUS } }, PLAN), /frozen historical alpha/u); assert.throws(() => validateNightlyRegistryState({ ...metadata, versions: [...metadata.versions, "0.7.0-nightly.20260912090000.123456790"] }, PLAN), /advance/u); }); -void test("exact candidate can resume between publish and latest synchronization", () => { +void test("exact candidate resumes after publication with latest still on historical alpha", () => { const metadata = { versions: [ALPHA, PLAN.version], "dist-tags": { alpha: ALPHA, nightly: PLAN.version, latest: ALPHA } }; assert.equal(validateNightlyRegistryState(metadata, PLAN).existing, true); metadata["dist-tags"].latest = PLAN.version; - assert.equal(validateNightlyRegistryState(metadata, PLAN).existing, true); + assert.throws(() => validateNightlyRegistryState(metadata, PLAN), /frozen historical alpha/u); + metadata["dist-tags"].latest = ALPHA; delete metadata["dist-tags"].nightly; assert.throws(() => validateNightlyRegistryState(metadata, PLAN), /not selected/u); }); @@ -85,13 +87,15 @@ void test("missing and malformed registry evidence cannot become an empty releas } }); -void test("token expiry blocks publication and warns during its last seven days", () => { - const now = new Date("2026-09-12T09:00:00Z"); - assert.equal(tokenExpiryStatus("2026-10-12T09:00:00Z", now).warn, false); - assert.equal(tokenExpiryStatus("2026-09-19T09:00:00Z", now).warn, true); - assert.throws(() => tokenExpiryStatus("2026-09-12T09:00:00Z", now), /expired/u); - assert.throws(() => tokenExpiryStatus(undefined, now)); - assert.throws(() => tokenExpiryStatus("2026-02-30T09:00:00Z", new Date("2026-01-01T00:00:00Z")), /UTC timestamp/u); +void test("nightly cannot adopt another prerelease tag as the frozen pre-stable latest", () => { + const metadata = initialRegistry(); + delete metadata["dist-tags"].alpha; + metadata["dist-tags"].nightly = PREVIOUS; + metadata.versions.push(PREVIOUS); + assert.throws(() => validateNightlyRegistryState(metadata, PLAN), /frozen historical alpha/u); + metadata["dist-tags"].alpha = PREVIOUS; + metadata["dist-tags"].latest = PREVIOUS; + assert.throws(() => validateNightlyRegistryState(metadata, PLAN), /frozen historical alpha/u); }); void test("nightly Pages binds a generated package version to the exact source and receipt", () => { diff --git a/scripts/tests/publish-nightly-policy.test.mjs b/scripts/tests/publish-nightly-policy.test.mjs index 3498244..426759e 100644 --- a/scripts/tests/publish-nightly-policy.test.mjs +++ b/scripts/tests/publish-nightly-policy.test.mjs @@ -66,6 +66,32 @@ async function jsonFixture(name) { return JSON.parse(await readFile(join(FIXTURE_DIRECTORY, name), "utf8")); } +void test("nightly readback verifies candidate bytes and preserves preflight latest without tag writes", async (context) => { + const workflow = await readFile(WORKFLOW_PATH, "utf8"); + const policy = inlineModule(workflow, "LFC_NIGHTLY_READBACK_POLICY"); + const directory = await temporaryDirectory(context, "lfc-nightly-readback-"); + const alpha = "0.5.0-alpha.0"; + async function check({ versions = [alpha, VERSION], latest = alpha, nightly = VERSION, + expectedLatest = alpha, integrity = EXPECTED_INTEGRITY } = {}) { + await writeFile(join(directory, "registry-tags.json"), JSON.stringify({ + versions, "dist-tags": { alpha, latest, nightly } + })); + await writeFile(join(directory, "registry-integrity.json"), JSON.stringify(integrity)); + return runInlineModule(policy, { env: { ...process.env, RUNNER_TEMP: directory, + LFC_VERSION: VERSION, LFC_EXPECTED_INTEGRITY: EXPECTED_INTEGRITY, + LFC_EXPECTED_LATEST: expectedLatest } }); + } + await check(); + await check({ versions: [alpha, "0.5.0", VERSION], latest: "0.5.0", expectedLatest: "0.5.0" }); + await assert.rejects(check({ versions: [alpha] }), { code: 75 }); + await assert.rejects(check({ nightly: alpha }), { code: 75 }); + await assert.rejects(check({ integrity: null }), { code: 75 }); + await assert.rejects(check({ integrity: "sha512-conflict" }), /integrity conflicts/u); + await assert.rejects(check({ latest: VERSION }), /latest changed from the verified preflight/u); + await assert.rejects(check({ versions: [alpha, "0.5.0", VERSION] }), /until stable takes ownership/u); + await assert.rejects(check({ versions: [alpha, "0.5.0", VERSION], latest: "0.5.0" }), /latest changed/u); +}); + void test("the workflow normalizer accepts only npm 12 single-result arrays", async (context) => { const workflow = await readFile(WORKFLOW_PATH, "utf8"); const normalizer = foldedEnvironmentScript(workflow, "LFC_NORMALIZE_NPM_VIEW_JSON"); @@ -474,30 +500,3 @@ void test("the workflow provenance policy rejects conflicting source and builder await assert.rejects(runPolicy(context, policy, options)); } }); - - -void test("the source-free latest policy waits for visibility and preserves stable ownership", async (context) => { - const workflow = await readFile(WORKFLOW_PATH, "utf8"); - const policy = inlineModule(workflow, "LFC_NIGHTLY_LATEST_POLICY"); - const directory = await temporaryDirectory(context, "lfc-nightly-latest-"); - const output = join(directory, "output.txt"); - const previous = "0.5.0-alpha.3"; - async function check({ versions = [previous, VERSION], latest = previous, nightly = VERSION, - integrity = EXPECTED_INTEGRITY } = {}) { - await writeFile(join(directory, "registry.json"), JSON.stringify({ versions, "dist-tags": { latest, nightly } })); - await writeFile(join(directory, "integrity.json"), JSON.stringify(integrity)); - await writeFile(output, ""); - await runInlineModule(policy, { cwd: directory, env: { ...process.env, - LFC_VERSION: VERSION, LFC_EXPECTED_INTEGRITY: EXPECTED_INTEGRITY, GITHUB_OUTPUT: output } }); - return readFile(output, "utf8"); - } - assert.equal(await check(), `latest-target=${VERSION}\nwrite=true\n`); - assert.equal(await check({ latest: VERSION }), `latest-target=${VERSION}\nwrite=false\n`); - assert.equal(await check({ versions: [previous, "0.5.0", VERSION], latest: "0.5.0" }), - "latest-target=0.5.0\nwrite=false\n"); - await assert.rejects(check({ versions: [previous] }), { code: 75 }); - await assert.rejects(check({ nightly: previous }), { code: 75 }); - await assert.rejects(check({ integrity: null }), { code: 75 }); - await assert.rejects(check({ integrity: "sha512-conflicting" }), /registry integrity changed/u); - await assert.rejects(check({ versions: [previous, "0.5.0", VERSION] }), /Stable owns latest/u); -}); diff --git a/scripts/tests/workflow-contracts.test.mjs b/scripts/tests/workflow-contracts.test.mjs index 36fdca3..2a49596 100644 --- a/scripts/tests/workflow-contracts.test.mjs +++ b/scripts/tests/workflow-contracts.test.mjs @@ -68,16 +68,17 @@ void test("nightly recovery preserves original artifacts and Pages binds the ori assert.match(pages, /actions\/runs\/\$\{LFC_UPSTREAM_RUN_ID\}\/attempts\/\$\{LFC_UPSTREAM_RUN_ATTEMPT\}\/jobs/u); }); -void test("latest token is isolated from source execution and OIDC publication", async () => { +void test("nightly publication uses only OIDC and preserves the preflight latest tag", async () => { const source = await workflow("publish-nightly.yml"); - const sync = job(source, "synchronize-latest"); - assert.match(sync, /environment: npm-nightly-tags/u); - assert.doesNotMatch(sync, /actions\/checkout@|npm ci|npm run |node scripts\/|id-token: write|npm publish/u); - assert.match(sync, /versions\.some\(stable\)[\s\S]*?stableExists \? latest : process\.env\.LFC_VERSION/u); - assert.match(sync, /NODE_AUTH_TOKEN: \$\{\{ secrets\.NPM_NIGHTLY_TAG_TOKEN \}\}/u); - assert.match(sync, /npm dist-tag add[\s\S]*?latest --registry https:\/\/registry\.npmjs\.org\//u); - assert.equal(occurrences(source, /secrets\.NPM_NIGHTLY_TAG_TOKEN/gu), 1); - assert.match(sync, /NPM_NIGHTLY_TAG_TOKEN_EXPIRES_AT/u); + const verify = job(source, "verify"); + const verifyRegistry = job(source, "verify-registry"); + assert.doesNotMatch(source, /synchronize-latest|npm-nightly-tags|NPM_TOKEN|NODE_AUTH_TOKEN|NPM_NIGHTLY_TAG_TOKEN|tokenExpiryStatus|npm dist-tag|_authToken/u); + assert.match(verify, /registry-latest: \$\{\{ steps\.registry-state\.outputs\.registry-latest \}\}/u); + assert.match(verify, /validateNightlyRegistryState[\s\S]*?registry-latest=\$\{state\.latest\}/u); + assert.match(verifyRegistry, /needs:\s*\n\s+- publish\s*\n\s+- verify\s*\n/u); + assert.match(verifyRegistry, /LFC_EXPECTED_LATEST: \$\{\{ needs\.verify\.outputs\.registry-latest \}\}/u); + assert.match(verifyRegistry, /tags\.latest !== process\.env\.LFC_EXPECTED_LATEST/u); + assert.doesNotMatch(verifyRegistry, /contents: write|id-token: write|npm publish/u); }); void test("the OIDC publisher consumes only the verified five-file bundle", async () => { @@ -153,13 +154,13 @@ void test("npm 12 view results pass through one fail-closed normalizer", async ( source, /LFC_NORMALIZE_NPM_VIEW_JSON: >-[\s\S]*?!Array\.isArray\(value\) \|\| value\.length !== 1[\s\S]*?JSON\.stringify\(value\[0\]\)/u ); - assert.equal(occurrences(source, /^\s*(?:if )?(?:! )?npm view\b/gmu), 8); + assert.equal(occurrences(source, /^\s*(?:if )?(?:! )?npm view\b/gmu), 6); assert.equal( occurrences( source, /node --input-type=module --eval "\$\{LFC_NORMALIZE_NPM_VIEW_JSON\}"/gu ), - 8 + 6 ); assert.doesNotMatch(source, /jq[^\n]*\.raw\.json|JSON\.parse\([^\n]*\.raw\.json/u); @@ -172,7 +173,7 @@ void test("npm 12 view results pass through one fail-closed normalizer", async ( '"${integrity_raw}" "${integrity_json}"' ); const tagsView = propagationLoop.indexOf( - "npm view @tryagaindev/litefold-calendar dist-tags" + "npm view @tryagaindev/litefold-calendar versions dist-tags" ); const tagsNormalize = propagationLoop.indexOf('"${tags_raw}" "${tags_json}"'); assert.ok(integrityView >= 0 && integrityView < integrityNormalize); @@ -305,7 +306,7 @@ void test("draft assets and final release publication are digest-bound and sourc assert.match(provenancePolicy, /process\.env\.GITHUB_WORKFLOW_SHA === sourceCommit/u); assert.match(provenancePolicy, /internalGitHub\?\.event_name === eventName/u); assert.match(provenancePolicy, /certificate\.buildTrigger === eventName/u); - assert.match(verifyRegistry, /"\$\{nightly\}" == "\$\{LFC_VERSION\}"[\s\S]*?"\$\{latest\}" == "\$\{LFC_LATEST_TARGET\}"/u); + assert.match(verifyRegistry, /tags\?\.nightly !== process\.env\.LFC_VERSION[\s\S]*?tags\.latest !== process\.env\.LFC_EXPECTED_LATEST/u); assert.doesNotMatch(verifyRegistry, /actions\/checkout@|GH_TOKEN|contents: write|id-token: write|npm publish/u); assert.match(publishRelease, /needs:[\s\S]*?- stage-release[\s\S]*?- verify-registry/u); assert.match(publishRelease, /LFC_ASSET_DIGESTS: \$\{\{ needs\.verify\.outputs\.asset-digests \}\}/u); From 2fad1a65da67c3ac9fef26ede3e35d80b44a86f1 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 20:39:01 -0700 Subject: [PATCH 12/22] fix: contain compact controls and reduce rendering work --- .../compact-all-counts-390x844.png | Bin 31870 -> 31618 bytes .../compact-default-counts-390x844.png | Bin 31366 -> 31115 bytes .../count-keyboard-focus-390x844.png | Bin 32408 -> 31291 bytes .../mobile-month-agenda-dark-390x844.png | Bin 36904 -> 36791 bytes .../mobile-month-swipe-pull-390x844.png | Bin 34628 -> 34501 bytes screenshots.manifest.json | 12 +- src/internal/dom/month-title.ts | 8 +- src/internal/dom/palette.ts | 12 +- .../runtime/event-overflow-presentation.ts | 53 +++-- src/styles/month-grid.css | 6 + src/styles/responsive.css | 101 +++++++- src/styles/toolbar.css | 6 +- tests/calendar-boundaries.test.ts | 15 +- tests/e2e/container-modernization.spec.js | 2 +- tests/e2e/grid-event-display.spec.js | 223 +++++++++++++++++- tests/e2e/helpers.js | 21 +- tests/e2e/responsive-layout.spec.js | 71 +++--- tests/e2e/screen-reader-semantics.spec.js | 2 +- tests/e2e/theme-integration.spec.js | 2 + 19 files changed, 449 insertions(+), 85 deletions(-) diff --git a/docs/screenshots/compact-all-counts-390x844.png b/docs/screenshots/compact-all-counts-390x844.png index 483a74c09b304c55d7ff84078fd6cd936c405d17..f3a9beaf9c521755c306876d78a8e13b8013177c 100644 GIT binary patch literal 31618 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 z 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 diff --git a/docs/screenshots/compact-default-counts-390x844.png b/docs/screenshots/compact-default-counts-390x844.png index 0efe46db5f47a62eff7cca452c5ff9e0eadfa38a..309361752d68ab20d3e1d218b07fc1e53a001a81 100644 GIT binary patch literal 31115 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∨9rUw`N@94vD|L{Y^cq=DC;6A}~v_+dK{alL>a4 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 D5)z+L literal 31366 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�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 diff --git a/docs/screenshots/count-keyboard-focus-390x844.png b/docs/screenshots/count-keyboard-focus-390x844.png index 5da11f812767a622ff5d41fa7de42bd61b988297..112a6d0fe29502e2e4243e2009f29444ff2b4894 100644 GIT binary patch literal 31291 zcmd?RWmH>TyYHQPDOR9Bi?(Qsw#B`I1&VvHQrumGRFL8XEp9=A774*A?iSpFOOW6M z3%uO--g}(q+3$V!8RL9CU$4xy#>|{+&FeS+*MF{HWkngnM^ujh001FSR#FuJxD^Nh z-1+qI_Dy7hC>00*JO%(I->ZA1Y|r7p3&&nR*o(~6V~fS8gqtuSsMu_EI1*m*tdd?G zjMzWxHDhnW^W&A64qHR|ULIW}^MA>`l+Et`Ih)+Yq=`-o;o_>DWo<@zJDlu$q`O!g?k!ZQ@V{xq~cs zC4pRvbZV)6`~d)HXQP{cw9oqr*q_G|34@OG46@f-re&_LURky0IY-F=GXC5)S~aJ) z51IAZz4Gzm8hi%Sb(8iHH)gYv1}NVN4Hz_6#9;@-j`Y%4qqW6m`lrfv@LecxEijLl z*v>3Qe2hpDmIn`3BD7Rl(o$u@jtZeLTMvDt|))fhbM^g}?pN4bUr#E<;eC{^<=#sY@q_y9GGxSDx5|cZXpPV+ zOj<^DH1l+k=*O{Z@A<4++?~{NRyz8b1tIAt!Jq)n2sA zKYq-4bKyjE`QamBw;>jQa(MKN={LiG2zpG55#1Yp?;hfUz#^v-M7R>=4&;2sTm|y zJ@28K?q{$SHSyb@Gg%Q`<|&+?T+kjXsg0jjdkkzv12VeHy!Z?0_h-db#|x8lV42OC z%X#_07tw&~KLc1p9M6xza~1vE+DJKIoq_iSKOe0mDS)zMzRPZ`c6b7>K)+D4xMZ#? zGxvw=?SmT;0k~o{XH|MHqzsg0Y>SZxkF10@s&4^g9s{2{vb42~H&2cmA-~C~ zmLZRDp*}2f=>wCF<3S?aahlYIiLQceWBay7MRN})R%P(1-;}zXz;`2QjW`lJzHE1_ zpM51zrk5mrxa2Mc%--lKss5mvlhU=1F|Av8<`uC#YcD^!c0*dmko|Ns|5!Y#-h= z4*ujt{Q8A3HMy=N+nDKg&qyaR9ll`|F6f8Wc&!y_j=xmT^joQmJ0Foh0|RMMWnvy0 zv)|${)9Mt|_OAy@-<6wy~)bOsfu*yQg^f2*|G}VxjrERKUHJf}SDtAr(U@=b!@-b&_j7s7bD|+?2?Dw9@4P!}Z z13NLBS-s<_v<7yrLMt0oE?!ISwb6(07b60%xTcExM1Fh6!&9MdJVuV{=flJS4LL?K z%UNH55-KM-q^~+-CYwHP7d=_FwXvOSnovDV6XIrWvteg-qGk=PNC<;Xl&z3?whRhj zBr()E2&I((-i?vbBoav^d+AFv9(-alEo_>kn0|xyJSX451v_E7gmVnmBVGTMsR?hJ zZEW%hvqXB-eRsm1<&P~){aGqpEpzpiE{j?ZZ2Jp<&~GtmiN|4{uh@U^LGtV1(i-9s zgtM`FibF-uqYYdGf{X|78~C*{`96rv1~lsWs&9|oAMm~2>tKd)42xoEug}&WYc`&p z_0*#X3tuLr>$1oC$_^e&T!rIiS3qkGZc;y?)|t-UU+4)OsE@1Qnx-9P%&3^#(P&fm zgVutQ6xA&a2Jv@q3Z&~aJh{&D^FkDKNJhe0CCwlnkAbDt_4)d*kOs*|jg`CcwAAh} zqeE6Yvs7`aB>~rrthQzGY24Hyxy-#!RAgz=UO&bIxt%Tbv1D6rCn8?nTPF({ zJ9R{K2S)p#a@$@NuV6gBTqU*JZoTJ@)cpxzMVqD-qDd(jKalX|yWRGR64GICBBoW2z*;ibuuo&Ox!o_uU&XlCY4Cpa8(?PTGj?ByBw1Ydj=4}f6&jmIw z?(=goz5kq%m&d!IGtUo7tg&kW`xb>8FP4dTg(u{E@pq)`E$CGD^)Bb#JqYvc?yF_L zK-AS0W<6#O*XrrdPSEnU*x*ChM|k$m)2vr+%Ls&^*kPXQl3WpRQ0=&U)uuL|Rd(T* z4lY!$w}L4`zoS;ws)uiJZ6V;6X8BZrj9@M2m}?>;f46>_Et<(y%{Bdf6t9b?gvzu! z^2>7h6t}Iu4_S5r{bKeQyPI@+sd4T>veKBjTyGrL?;;NF9Iq!b=L#T*oG_O>_cozJ zK1#g2jQn6l(n$lcuUbB&N4;cm+~--$5fEr#In1shN?Kr#F)q}o4)BJ=qmXJPI+0YC zp!L2Fd7r#!RIS8Z9iNSf@k`g@%-cKloMYlPC0mu4%0#J}`h5`7gU_q|e~)u3_%f@a zHwPxY8H!;>7tzLkCFfI1#oQ@Dp&~CnFB9 zG_ZA{K^r_#c9+tV03rboD-+7P_hym;?ou>S@!35QCG+j^znc=WQ9SF+GFk$wo;o-Y z(LnfVAI$~XDASW@R2Uue*2pV(TGyy&0ladz!^^gE)NmUINkGhXJb38o(w7`bZ0-Px zM>xJI+)PESGMjOOY`RsFb*e4uyrwkixl-IiwFl4b@NN+)t}OA#eu*>s@1lY```CY1SNbj8CJ37y@-z%%V3s*!XrmN7+rdY#T`q2Xjy>gbt{I*p6(W|6BB z+!f|ZVmu}`eJc%);*31=Zf|Oz`Elf=cs;o5;cEA%K53vZo;XDR$vV%ri9N+fbETYE ziOoH8q;W@3-XBP`VxI<~Dz(+~l zeLlLEUK2Q|DbhV$BELE@x%5))<<^;H?Ol6wC3ETR;fNe%`$2de)-zea#^9}kp|er( z`s4Ut9KCFOUUfc=+_h!!L$i+d`6lpoo`#5G%TjldFO%)G;29@eQTMHqZzBh*lhe<7 zPV1>U5!2l6f&SD>W!N%HqGK%BrZ4yODJkMNXZv7K%+m|Zv;tWNHLG7XsxjFinqmC7 zbH$XwJ+poN_r_;b*0hXH|Aml!+EUudyIqZLlw7uZ>YhegZH4m&@q>@#?#>JWI&ipE z`0Y$Xnh(dMX_zqVVZZ$S)-{}{4PAq<_V77?kX8t#U1c_zTc^>8?m2-8*fMiRz^~Ea zCH4dH{l~`d`jQ!G<7LgKHmJmO&)z=(*Z2qv`HRI;d2#Cb7?A4z7(=+aT6DP)yRu6> zuH8u*I(4Fz&3N>qNnTvV_N#*tLW`DcVm9w!g;llA!@mEp#MFa)7=K3f^r3*qdsZCn z#{u0Lg4e?t4?I~TSWPR`3kDx$&D`axmG zpr^0x7UcDtJ!?#*{G}=k(#T-<81M{~BP|z2a!Kl+I57l85HzqnfsVS_J%HT(6F@W# zLiDl=7hsnWTkP^<5uDB=z9xbw<=cM(N;;$kl8y?++aQU_=61+f!=mpuJo77Y+RAs_wx29-YB*~Cr}=mMYBc8)me!qZ9L~EefmMG83$-Nn$r}Be?<%smYzJBgF-=C?~H~d}piC2Knv!awxWPZMKeIo6z9w!)dMr@l$+1h)V zX2jnG(7b2*oA3aCNNxte;V$GR2ojX^Ye~#Fa0nAkxh#jHW+`j zB9nd>7suvoS$0`wrYmA%24S6-Ih@pXFxJ!}aa~8e?~jmbVe?`s%6dRdhpGW1ik1|O z9J}c!M(UnY6QU=A)TK2bF4!wvYFX1sc{vScWvHCFHVDv9ZY^zL;j%GW_M;}Td%07o zB-h-V9`4s7J5iOC1ij6M7dk^up3I1?7i()YN7!{SeoeSPlRCd~ReSjOVz=sA-EEx< z_HJbTo;5x0bl#Aqem!?4^Uke5@uPM zg8wT2#Ht68m5Val%&4>-tx26|6i@cW$&S|_<+{WudxguMlt=%TJ-WQO$z$vF-&@2r z6~+aukg{wXKb?7RWe6-<_2 zoJ-r~SE&I5DNkpZ5GeFd@=&g=#5AZhC&ok*3Wx{FdZ1nN+C_9BkPjrOjJ%rKZA};EyPmWTO{A@mmEjPpJr%C8EjVXKN zeR5+Z?6J^UgpZM5tar7yNP_JdymXMI;|ueW|K)uyX z#AO!4cocYCJ@pg8E9}Y?&7vDJ*xBUEBv70U&6+9o8dPf%3Pm3OG?C$o`y*O^`ZOo!-86A4p__jSN zxp?#h%|0K^JR?L%!w<9Sr8L%wy|%g_8MNCKoxf^#JG)Gl8g)r}8_kA6S&x;kR^-0* zuzo$$89ng(&P4zZYu9;k^TC@IeM|Ot9c^JbaYF`C&qPB5MuQcS2OblvHeFdXQeNOG zW#yW_Jo4qyzSh%zk&2?SdEQUi4H^HV?prw+PDGN1%U;O8tfhZ{yb}6mUcP4iWi44U zQ}3QizwO6zWD%`8k^mNPRcWI7*^FJfnbpiZ)A?)b<|MPca;{3To%s^p@s{6>nqP7GMl9<@ zezd-Bq_ETwVLM4_H~VLFDBpwa#O45#$$4LYpqY!6^!sQ}ei7s}d)huaA{jKAkA)ocKn+dE?>nm_WybHfbNCBa~nr$rFDcnOiSy$$V zODmaeVP<`GlvY|;+}2u}8_X(`BYyD#p+5aX2E(0C5ctn+uh%JL8Fg!)BB(TFqboHh zbL|DU@05A5E5&d$75$2fNf{ktws(qlg8n-0eSW)_^*D2A3 zLTt}H5uO8`lgm=m-qr!=OYErsa@tDZBQJ*!Ki5m+dnAiXvuRu zB5Y-y8zrW{fAmRPYOrmQ4TRa%+R8_NlW&Rlc_02wiRcy};)XE3GTule3lACa0D?CV z^4~+qwgfp+K)qMeo@zIX??=3nx?*}R1>amV7t8Y6-^LPJp`(?yBrdc6hui$ao&O<} zmCL;l4_5Xzvp&6=f7?S(3b(Ufcu0!cc|PU#0kH-r)rM%yuk7eW!UWKSyN1gyNyn^g!kMcGKYRi9#w}k3|VX z`?_e)T@qa>pE+}H{yx1Q>ctDEO;`3?t1YVCTS)gx#Pv_iPNBly-pSaH%*7ykVtj3$ z5Bi4ghB_WBJrd7(!D|>}R0mWuryG1*D?rMy=re+kGtifWX2rTtVOH6m9roO;HDbP} z@`8w-&&NXm7ZlKB3u-tkc>Q6%XRY|Xh6E9Xt^cV3w!X{W-r;OZFHRwO#8xszQ`GE zIRvT}v-;KFG01=IhWXS3O>2NX=iCv892a@gPgXd(6+4ShJM!;%5gn^L$|Z#z7%t6! zlySO9Mf7W4`?zBu_@=Evo0oLXf-#jczH~Le160*L{frlwn*qlXXKD&QL7Nix~gL9)XmK+{bx`K?@D89Rd|G1xoEV-ObJ^V z;l=Tn(l2Yl_#entPXeI5S(Ve1hoJ5Q*`X4B-LwAhVKyhFKzp7kpOI#M7Re?yz=7erl5snu1Sth2h@-1m(M8r_sI2yX4Jfn2+c4aqvhj7eu z!&S%SHCJ12`-hn7&!Wn(X*;e9*Il|`$(KWyB8wD>MATg2?cZst04!z)J=E}>mrtrNb2YvE-lVOq zDa$h_={fvFt>=Y}Uyq%-F2z&$X1&0I2S`Z7@~|{nqO~vn~1rB5(2(k)g`vhUobiXCTwxd@A3=w`0>;dg%B%-&okE zLeV9n&A}*UB3Hee^9LmT-c_Wj?sxNozDr+}jJzt${oPRIKpQa;*X?TXPA9qH6;Gz# zyydu8`qkKAoN~`1j{p#S`a6v&4Q#1WxMp*T%e8B2@AclvS^rI!Hbtc`OOgHE2jy5< zLBD#q?}D!>5-ffd)u!ydR46rXr;u$xCZXY8^Q}avX9wuw)ju@P9P|`k#Op9xr!){gRE|=De!C28~x5r;LC<^_6 zEOqV`zf$8Tc3yr+f0tL6P0Nm$i9D!wH?dXLZn>J5#?o*p+LsB*MY^~WD-2Ut@_u=y zm3N&pN)5dC##)E&*(cDG4VA43A0hN@{M)+JWnMC|&ZoNZGHaOh@ofm)>{`Wc8a`vf z1oRPVSS!oT;G^`LUzxp(6P^dqk^k8a=YirSGY{ z-sa~8yyiu>(HGq|A5og6L$qZFEmayMXZOThiE~8i`Gpy*wU}BJ@){tlw`k8 z#!XmbX8m!)MJ;>|ZMp?^xAhc7n;(tWVh1jd(s@F@#kZOf^W`mN8wMDnCnq|NFgeSt zYNc@q$Gudm(gU1?<#JZ5`~4H=*VbU39qhcK0e6lfhVfih`q+iubZ-XJmcve`_|zzh zg4X%lbakyr;kgwurM0lB(dHmt-yK(%$U4BB+Mh9QPXG?hcH%T2XxlbX0LLulM=iW; ze+M2^#?rMtfJ>_aGgp{Y7N^t#H0}@*&YD8hqY?)|S93)iK#qXK4`U}PW(PTa1>aIX zNqZcPSX%FrRZns-*!X?XPFmUAStOIdZj7X<7b4P0^x zYdDd^k6qG(v%P)}Nh0AxH3)X`H4d<8eq3X~T&FnwOT~#_L>Gdm!+syI>2v4wiysbw zS17uI`GFbgXO-fo7bg+}(JO=ix8=M6s&TckQu)KUxEE8E&M|Hng#xPcKYXL9f|=v8 zn|zE;AH}i>WqiuKo&>`puLH-fTqDNNVqZ?qNtjrE7=44ZZa&Rh-aC_fK$?7(Klo$Q zS7jO$+!!K!l0;R0phI?iTi?|}XUOiX zL7G-xQ_M=mk$v&jY3{ z=>ha1rDD3$Kz)2-0;KgmmF_Q)cH#^cLCgC)U57Ge!l3mQF%==?ZurU0biW8fM#64v zsnLyna}>ipS2fxnO~~Ek%Dpq4^U-{u@|EA@IS_vkNark`1_XqEll?9&TjO-R9NC!v z+D2EXTm^^YxUs}MiA&c-fR_x?(qr0k&QRNzEIv42IIGjy=i~=&;;J2Y%nn9RM9c71dKH%=d?CfCQ5b!(-jCk%%*-LV@Asc!8wbPVv2>B1*(%N| zc~z09E%?@{{W|I3?sQEeAnjf;lLvpKBM(wU+;eu~S6c6~FN@_4`#gPQEvYCq176x< zdZB@b=}ULVgOtDXCJMuaU4=3?bur&h>kEkN>)_AyYo zj#Vk`vfg+%JMDZFN~gWGgDS(`l-U`+PVc#pyj#t2^=;mYv5ysdj>PQLc$;p#7F&`W zP8)7*6jmycVBvOtaoinDBmDL#2?@Z4vo<<2Xmr&=4xz5S=7xSu9`5r?FvWu^b#(en zpTM>1dS$V$xYfRvo2FetN$%)M8c0I5Kv$m7QNriCK4WMjlw6WZ*e5079`{lCC=*{n zpUC5K4+ED+GsC?9i|ZF&MTe+<3GK_f*N4F^Y^-s<5m{F#m_41}O&L93>W-yci)#}sHyK~8*Zq4n!`TU%V_m*3>WHf zM?MSD#<8FFaah-fGnhWbAR-|QYB6?Es8*riE&;ulaqI%Xw5S=cKjLZ zP1_$-8^nnxf~L;8qE!f)0I8ZUb4i}38g7Grv8cGcbx=w9=J$t&Otr3M=?kg67pTK9 zHBDpkYJnE$x1-q77`R4qsFmmDXVCvVj{VBlR-Ktpe`DguXqVf@jO4_Z$XtzbIyEz! zZ9)dus_F8#$jPQVJ1bjl$f=R<8>hq0ZTNKa%1%uim)IAOi7qSjUAWz+?|lk965*I_ zKbKq$Zw{EgsZgPe@{vuDg{o%_rvRhO`PG%zscUp)6lSh^c*_vcS(z|^-Ck48tEyQa z0gesW=y35|!zi3axHnxpSPp7^K-( zBbJ2wGz41o6Kx;(@Rg!hiO~ZJul$VZp{J|+x9B;6aFExZ-jIVOAdZ()&-|VT@i03u zOoEC$xzZWsGF>gA?z37Y4vKzib-q>5+O-J*62u=8LdqSH}@ zNux!pQ(=Bs!g~jqqo(&jIc2_Ue^+K5at@X1OQh*l952FZIKS0=(y&M-Cndq0z3Hicb zVf?dKT?wuMF-5j~%nF6)1{UcEB0X2SrX+$qEGRnf`5Q934%5)oTIZP4hgw!hjPH@MzmT+?F=X zb_)22|FzxQI$DQlk0Iwj?@sLPx)oG|C&1d3)vkyuOD;{>w;QN%TxY<%5kd}jYqHg? zLURW%t@5Ys_QI}sa}6O($IVV$#g9^Izsv1+=>%3sGWdQtHT8ig4Wl}OqrWanj$nIY zJz*qoD?QM3NCMVSsq@55&j6H#r0(2GE|lulueu=4r;qD-Jen03 zH*F{2En_oKaVp^=@)~wt&;cDDHY2W3)RxLMr?9~B36MXHI>M8*G|cCw*vWj1q<&U! zZ1OsI89rffxJOg5d{idGA9$=J1P*s^<(9KL>+HuipwxB}5(Mw9GCv>@nEv$$o~*p& zpC+?0NY}<)zeBNkG?hp1#^F1;aA-%U?4*S_+OLl^|Boz}PH4ogOKV-;OQlQOdx|kM zBzZS?llyrmFIsV2 z#ZjJP;1rzs3dt1HwVcK0mi0pAKN^8|8ZYh|+}OkNrOtLbB?3_OzWa+YtO|#;Q5*x9 zxr;4>f$WwPncxQ)ohhp@?8L<8S83rD~^3ON({NHp?IxY(v_`QtXsc3Wx8S!!y*(nxU}8hT)Vyg z1E`O^77(z~8qb#bK(@UQiGT|?nynW|0Z2EhzkON^4^p|VWp1#wnO^hLI(mjn1I8GF z+UWMg+{sQBq&vzZ!B?Kf-Tm7#cc0&w=n~T#;~FNKT(>CF?;dRZ7RVVW{$os$JCM;(6! z+<59-*)-h_3cIRBI zLJJ|@x7hFikh`h~d6Ro)B(*j@1Yssh?sX5z?H(^M03M5aGSnHFFl{0TsCpRD`^LAQ zOOlQbRi)-9`$t4H^ZaxOP*}8-JkAL&n zMT?dvHDN|0_BSe<@ju-3u9XGUI9?v$RyG>k0p%Kz_S@f@!Yt zI7$-gAzap&kLM92d;8nX+%e5|)qiB@>539d&1HFuclRgLV;7|Ts|H8LkE61+5H4`7 zVj$<=lecEyCH{czYN92GI6dkmotW2wuD4P6y{}p{40v~|6nAn2LhLl9cLrXlT+$@= zrzv{X&Fg~hewDrbNAaYbJf#6gR}(kwl8{nqwz$>I^B;)059A_RI|tFML2j|p%HS*O zi4*EzJSm?G*UIf&Q(Rz`V$k?eeMl(1UK1#7e?4_R#%|SQUX8%dT`s8c;|$r?oBvnf zxWAn-pEDLQ){Z3FBvUoRyZ$mE`7iA@ExA8opMZn)JeLJd5hct^lgW}?jSMG zd%EV4oBbEaWqWpW8P1YGx~r#HpMYI&Me0Vf?Orn%OH0sew%U#41}Nrfvdn?7RLCmq zXP5B|x0_{)%$lo={`O;%zqYLZVRE1qczlF^6$=pjs(IsifJ*JBcKAFEm|r5-)#5C6 z9nBhgefz7{pp#z{C`gG|kF;tV-ztm+Q#}SZv1(?BE?j+aMS)kik@!0#4Q4sR@K3MP z|KC*c??#EgeHQ;7@(-1*HFWWfnSZ*B2hkb3pO7BFAD=x^_=vYjOlQVrX}{L9&a_L2yD{H;zm{|3L6nzy7%wp)w72UXb4h$xQ1 znQ^rbL{A_;+3*J?6`dTPQbDw~Hd<0E{O{ol0<&PQYCvrc9f^>1bh48yhigd|a;588 zIpsiei@6d7ET&che9@DJz0*R0Qxkj2$ef6dS7kG99YGO^ zd9E+KD{sNH=e8Ej4u+HC=?d3$<|OK!k5Mn_u_5%t+|I*H8g-S2+0Qb8Q_;qnSWwn{8+R+W>ZB?8y2?4^07ja%u z@dhYxON(V?|85ZVScke1huxVC&|L&~-{B$!DSz?DDRC(t#4IVmTp3l-_-dp8jWSt(#lJif+tXmUE zkafB>p%@?ZMx2T1i+y4kPZvx4FmMN$6Xw(Eep;Tb(>9P~{Hft&_n4t)=E?i40&DWb z>6gWiIB7LlRmKkaEyOmGH_CI~J~oBvQP8>=HIZ7`KZs-TJ~k>sQCjP}58&5HXXS&* z$#W|dW8MUplxW0Kla2_YDkx&Y9+RM)%<~W~lr&Trbi%e_$)Zw&LL}rgygh1vTcLX`;=YQ@9vpjQ5i9GSP; zPCsa#+Q|=lQG(1WPM)TOO{F;URFyO3kKvBQb}xR*1&<*Y9ND;;#6I3wp>2BI&>}p% z?Y?OA!%{A&rYWn0iFL>HwFGLd?SW6Hit~5Pzz3e-T9@KJ!KyZJ#Lv~8+O~UKgD!i9 zHCA$dIK5+TRp(&n(gzi#d0y&(2NX@{D-8xh5Ou?t77kXtMf&@#cic4x;wq_39_4bu zq?$_YzLuMqE*-NKsXLk)CTBDIgThNV^62I%s$K3W$Si8F^qtU9<*C}Y(OyG~ayqn@ zt^Sx~*a;hCJYM9DJ~|GYYzk;^Ke*LC;S~HS>@H2N zVTKy8Xg0W-2uv{~mPH7_$np&T%yszg0fgf{PQ76l4w-vy%v@{qMX9w#nGvoRXlDBSxeHEWp6l`>t+hd)GW7` zEGn5qxz2lRsDWdpdws(;SudtHi|QPyfTNzH=EjImsJb@343kXxee4U8sZqaTdQzC& z66&euwg7vGF3pp?ryC2jD>+pm2S7iBZJb>RrV6BEzErhyqtn+8?9+YPL^``z>}?d( z2|?DEKbB4@5)x|8Gn8qpJk=C*i%sBc-wm1dQ~H+ z_nvc$^9LM>Qq*oi&iAN*P&8>hpPqAb{f2UrHesqb-6F{q}wK2N-I3?D>7K_xp%WR778ibuVnQzQbIsCi$K!4 z{?=3Dd3{!Ipf{f;q?RH(?cT;4PVY`$Rr)+r&+gt$8tYA04xN?i5>xVioYB0VYC1?u z#e98;o+yx%`~OPP{)weSEG1)ku8X?lnqr)n?G(pTLk@zD>MLDA$!?bR)9}DjOYDQ)jM!M>~`y|ubR@+*f~Al0ftgwucNi$7Gq2J z*L6;NTXPy$?~CS&ggNjXPY*)pcTt{_fFYZ|+W6T{&nFdCj6WW^I_ZeFAG`7)2JBs` zlLPn5JXvygq%7!PZK1NW8{f~&6TuGI4FL;M`ApFvp_n|J3cs7T0BLHcKSqly9>vzE z1Q;TKF-5;E(Wtb7RfWFRZ9D;W9E;p|7o+69Gm0!n1Se9!@W578fWJcwuhu*CHDww2 z-n_9+PYss6Z4s~TWNV~2PHTL8qZKAn=mKtkHIrqAsHkqsCJhymj zPBftOc^U)oB~w2uz$4&>$NxJGOEN>C#Ur)#+qeF8WV)g97qd2S9~oIiw*X5mxR1mm zK>fow$!MJkw;CaGC<_N+|BZFEKgNooTd%*Ei3eYj^j!rsTDJT*((Cf{4eT$!rU62A zX2S2zpHO8W6M7R~h8cNrp0~wRD+ZbH!fs7w#<1La#{(@01)!+x^rHOMy+E&mUveJ1 zIqo-P?mi^}@G7y@bD`}Iix75=pBScVg-pjqt#4*TJ^(UWqF>0Zd8B)J7Hq9$9}`k` zqB3O8O5w-%u0lcnuO@IUd-u^X?OLfFaFSJ=BS51j06R&2sMTB(7Bhy9EbBJZA z&dnJ1r^}`%9F0FTp81ygwq(Bh=I!+05MbR>^ql(WWiTyVtWZ}eurrMsP#G+*=D*nznl7 z3nxmat7fv9q8xpy;7#&QUp|+DgP{gQw`32w0olKJHmQp9dj$1=nt=ZuPXB>d|Hshj zPiRlyonBU|8Lo%~;-AD0ud94E%|Dn>1ZTh9ap+PO2_hzoeGuPSit9qDlru$;%^QNt zW80i>G-Z~iH+(I!f+Sn&YD7lk0k(p-~? z1s7G3*T}Lp)BQ|iGwEDN!K#mhee;^0m_YiV))~Q$L1MWx>gcPH=lK>oQ-f0 z{KQ$Qfx4_YhDD~(mrUu@>eC*u{7{=}W!Ewf(m&o>j|(_}B)FAl|CFCk%#>-Hnb_31 z0qusS5T7AG%|Gk%ohK9Be*w>X6B3x7SYiH4=Bh4l(4U^)Usdk#PG%{dzs`&vm8&m} zFVZ(PldBmkTBWNr?OqEjXy(Vf&%}g)+vk-$?OFe>2&iM;gScX713bECoYDRV&=Q zCBmx8Oc*3pH)0z;^AORg;7m~V>lvKqT=i!$;qcgTW~c0oF%|`QkY(ux3Q&}f@R!Q1 z8LVQ^C_kD__-&IBu~L<{_2<>@y|nOPZmOLzou>}}%F?n-$3+Z>IwVcr=7pSzHYq_n zMUBlsNM~rntj48J;mwa~r8sZ{6Jv^45#MB%bSI0?I`6MhnK=lPE~95M!)DrAUwk`d zcb|D~5DP4=%>Ihq1IS+tC}x>jH-KGgEDCKW`{p-d_D6v!pga z3DGcDeW8~8O~bv?r9dK~H|q!2w$_J7!Fd=B$&Nd5(rAmVXu(Z=DGg!jik+Iu3T=6f z2d*;63Ua?D{sL7fi>i;!nl0{)J95QyS6UvkS?xn6pcEbowf_7e)(hQ6?gW9VqT}Qz zCQA<1iHpMdb z{g;ejV>cG-ejUtI>b9`5d+fa+fm5_12)g(F zeuMIRBccb|+Cwy@d@05q`^3EseBEjU_xA{wWY^3ze+{{%uH|oD*`jh2ZOkh`R1Mi{ z1LMxWlle91B-rZSW^G$A73xe1iR60(V_5G-8i^#&2;{Y%c4d2Ms;mP2#_f^LxHn$m zzdT2)HPo-(#cs`Pwz;yS^)7*K-~X8YO4_o$J)(j?SEUev>lx#=pSoKZtDm{WUH%A> zo46+&0c;4c0OaeaZnEn|F8C z&UI|@gP%&QHn$|Mt&jV<`n%)}P#15OE8c8Cs_+xZ+c}9s)CgSEUo+e!&;R&dz#h?o zs#b4pnuZID8LPEq(i540yE3n&r@(==g>Lwz=H%_%et!Bjg+a;TikP#mR23N~SY2cu zK8$P-66?;D2)_&x)+T#UssEvBw}SBMHAl;?VBLi4Po*KhwJ{xOk{`Jgt7)?|E&Ok9 zwjU*hA0*OQTD}R0DL5H$bGnrxH4?`))BB&4n4p&$eCf+HDtR2}EI|bNR3t0b@@(-5 z@m!ZZ;?&vRW70XgQ8%YX<_cf3Gpbdm-jpy)ku^Ubl*lD4z&`jC%Km2|NPCfrMj`a7 zV*I|(o4#M8Au+OVPv%@(5by6uA=|;$Y3|`h({~!QQukT-o?Yr61nUht-?%sd*J^#ODpZOmh z;8~{wsnY!m2#@x04S8!?_)Fffo6j~>%+8mNFW#J|^5?wxPL)m02dj<$#HlUm1Q%_a zXD}icBhwt_;j`7fODph?NcX=pod1slZxsPWu87eqJ~Oh}`s4c_>{DubBPV4C;8AqO zY-+60Y9B)yvVV(qxH0M_&~vJX^#s$f=_uZuYs6#vey%nA7l0jgc~_MYe`P7AnQE^9dk zQS`OhOlxU;SL`o{32LRT7iL#d^L@^=EgH;ZJ#jl-fAwRa=O8@$4v9$QHt2OMe-2kU zNk>keJ|hynylDn|kvf^l12ga`%;$aKv9bLtd0Nx{!s8Q9cc@s2LV?yg87xhfuUmp;*A{Kag-$^0x_%v{r_(T&Y#PgbN^wfrUf zow;+9sI_ByFMLto`&V&uKJ_#kD{DWEZyYNVVUv^$MM3onsO1Exm5cj+n!O=u1G8B$ zmL>$XX$@qHf95CM+HMa^mY&9vkd(b>G`7ILu{SpQG?fW#ji-SWWqs?WzJv8$-OhWV z3%1Ha$-e#D)#EN6725zEUyZ(X}f6Lm%=G~KaPug8GH%pI_PFco}hH&vqZj#!oQ%>Kp7#uSV zPT%t(EY>(RKSh{pLKzOcXTnLQ;LH6@5fQYdi6#AHP9MFFcYbZUw5F&!7ocARFM2?H7Q3Y}3I9Cy6NEFlVy>Q7tK8;^gT z@D(Xfsdd^n)KP+ZWD*M!)J@)J?s=~qEp&b)Dyex=RFYF|f*5BfWdd@$B+_5l%e5WV zq1IP;?Af)g{cfCzAiSM#T#eB+QVjht>0BtV9P?}jXie9-z9;Geb?MV6wRC+ z%}f?n#8~TTT9rX~dQ|x0lS$qPE5(+qUbltV*<@&fDyne;tn*4nqOCK0!OwBobCIdW z)+uP?F*{DfaV@VN#xXYaY=6M5-K@1+i z18E`ML~1uSP)?`I!G1^7fE%^B^g-I38}WAKdN|YbMBxt#MLPygv`u}AC|#Z?GigV6 zr_ji05eiU$wD9*1KOFP){VfThS+7@zZyh^-Dexkptrou~-H=9b4{sOKiOR@`whEq% zk&w4==y2A3m)NUOKcbhXe>L{!e8;k{yodxHMQjABLp+yP#|Cg>5FUNLP+bBY1rLVB==|{1`?fJDDSx z*C^sQPFYE|q50v~)H{(tKoa0Z8mGygTc z5r5#=B%$gfsdsg6VC%!-3L}Uj`Xxb|nnAhvph2UksqJBK$AR`)I7O&FRq`Q+>ayh4ojbbuUP3 zp|DPSso}t|vlFGA&SJt(y7uR4;~~#Tn6&1OF1~TTNSoygv$VAQGDvQn3o9rkSStny z{R)rIn|>v*ocJYV)p{0}Y>GsFd^^}T*C023)?cunIwj?2`IqGSi=E#z<(&i}FC{2k zDR+lg#D3K@hrH|BxRwtzwvR6BgMJJRs}AN#49v9!Er9wrqNM|`5`A~ zQ=cDpX#Lk6mPHxpGLw!}p~|8vlwAU2

      (Xc>CF}x0Y^JK9Puj53h$%*(aEuQQafC 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 SC?_5PKom6;O62}}^WOkS4JuOr literal 32408 zcmdSAWmsInwysN*0KtO?cXy|8CqQtA;10n8jdg-MGz8b+5?mTlJE2$QBQ0Olr(&`sDg-@1!_<%sO)fJKgC!`0WLR# zCO=s`HJ-@{;Y_(_Y71Y`-B&{grA-2)mtAnKyNNYxZzBJKu6xWWOkeRtTKxt`nQg%{K zx^rpC`PduLGhb8I#BvHiR7~6@@rKYUyVi4*?4{ARbT`{1TR+v08oD&q`?Iur=D^8NfZ>jg0d=(zbh-8*`g{kCduuIRh!8ZSCb%6+~aM1?*J_d&W~?3h;#KO z1#*95Eg926R{5#1he@-M%-9TE6h0}-wdhy%Pi;>)K>BJtrG}g%#9`?@DO?W4C%BA> zHQwvJ*M1lM)EBrCS@L(l2rEm@-#GkdD9mkUSE~pp%fPdKjXe6bK*oo2m3LeR5GA7q z;lzA(x)?9wvVsMydD&-QWs_&)clVsMWP;O=+wl8jDdI+%nQpDdVsbcH;qOwuPjxKO zLAVRq7mZbh*qQc=e#oOpiVRWcz6`_{u>Oe=43xe3aCSm^aJJCd;rCHv;VjD#>bv#s zU7~=42OXT1ZGWad_w;LLjZBL%z-o+L%5 zqr3Lth%zO8=cuwXMn(SZDU0N38CtysWLcyjs`fF#Lun8-95244O<7V{e|^QvJ2ee( zLp-&YO8>IU7LP^2WqVx&9WY+Mj;`2{k(EFec`PYR7KZdiiQ`HyM`yH8{w}X*O zb#;_bNf&&0f8|6^orse5(Nu>$^*Xw}*wmqXD_N)TA#dX~%3CQq=vve@Ctr;hSIxs} zg^}!HlvciY27lbOCsuKiIF`7 z@}r~jErfVRLOB4nM{wnw(rxJib_7)Id&w9fuWh`7kbr;JYpc!fWZz?Ptxh9Np3Y$8 z93{-GT%FSK)szRN6{UQR$xn7RyJcnXT(!QY7j~OWMdQ9scTyGMrai9LOG3W7j>XVU zVSSWpEN`4=OUk^ckjtc}d=cj_Z484EL?%`&iF$s3{n(xJ_GTFaZSJ+PteIA3^ol0I zezFvl{5`hUA$F^SRBz=t>}o^A+cWJOo)l+1d&KQjBIie1JoxXduZzyDCP0-l%5pb$ z!mJni6e2FJ46;*Xtjruah{oe)QIFh*lHJ461E$_A$D^DqTwGF_ao?7o&0a3^%L#c< z)Ih*`)kLdks{EZt5v9dmr+r4A!OZ@>&uPail5|*gT5?ClR3nVBC6YjIQ)d`dgYGyTsEm^u~YS!K1()QHP&`GGHYq^i<>BNUzd#?i+}7rXgqb~h-&X3 zi;NbYZmphYQ4~@T4JfZC`hb(-`Yv2G)9G>QPRIDkVeVaL2#>|S*XI&QugZ>|-BADm zd>$UPbQ>^`VYVKKxwFoHxiql!vYB++*8UmltA}Q1hKfE>|L;s`isyDV#ZQ#?O8q~B zhG6$3qsP_j zLkEp;m>NhMM^!!h#l;={V_$Z2B*7GY7s*{dZ$_bD{D!wE2-P#fonzxatN{D9=?I*1 zS)O>`w1>GQF}Q?lkr8!cQ1J(lcZX4WacF9Kt2Cp7K_x?H#RV+K(Pm`Y28L>ep8>x- zgpUp;wJt#+>UOX`?pg}TCupjt%FMS4)i&RF(Q8-7le#-R50xq$Q`5-x$>?U&%AG=a z0v8T59^eit&on~cH%8FmYRtGKX2lKP5pd~t$`v2I<@zLyR_hG|So!y&h=OI$?S?4E zgWV;9SMijMI<0KN)ruB*HDFWN3-8W{XPd)vSsh#YdbiSe+YfSX*i&OdDuQMPg>Zo7 zYt(Pqnwyl?;v#svh|9$YaRWMUcG#9Fn@YYPn3BJf(Bb5Zny(TjsFer?iF!P}ZVfSX zS#IL&tPU-xul*aKbcJ}s@fi*e$MKOfrndKq3C~k!ob#oTAFF4-2@qU#q^)?chDy}A zmsv@pq$=22FUdNkOlJf+|86M1?tkWLaFxHL;%0ood^K8fU?Xs^D52-%(w4Z9^fG-!Jw7bQBkQ^#1}5O(%VFWhI*p1@2jsn{l8h9^U* zXevYq!E?Ik@uL7P7#S;gtlLCRl;QZllRvtbIIpqM#chwxxqTQaDNi#6 z)B-Emd3D(dayW;hGe}sR>@p0}lMdhhZfQBEjP!!}cwd~vM8k7%RIH5tY+LSN(!cIN z#Y!yGdRrC9xY9x9ok%kldm4=}64>c46izH*@(ra2^226GJKgo>hQgMx^pZM=GL5?D z?!vp5F(LTDEdt*&$_x9@0&Wuhm7?Ea_Ep2U1GiY=sil~iO$>pg--_)fZ;43&fl*RS zPXcgHZ!|%Z-{Sl*ftWVyy{CQw$XfKv>sw~3FMoZ)b?)1oJfaQg9`k6SaEVM3WlCi% zC}HvP#$1M~)_$|0CeycKhkC(qxBjUg2u{8Z$zu6H5P#X^8$_4Ov5k#Ot2%)(!P|?| z1)Wt|bN?6VL`^p*O-6R|rZX>dOAVKP=r(xkq?3DdROVMr&c>f?i%=3Mh(QKR>Vp)6 zY?w0iNfOgMYVF&jBi}XtTt3Yr-`{~r9XL+-3WL|U8&Y>#eKWhix zUh-Fg&O$u-)c12UuARpQ0{lVgzpZUo6Pt=M+5~l}x zl*?nXoJdeZOlOp`@(MYBRs*Xn9*T^3Ixp=Z$WKY3#(rMi(_YC>1_@z>C3RDs;%0|! zXBA}Lyl546cXn$-)dFm0<%NIiut}Xokyje|R#Z3})*hZ5mt6PM=xnSK& zo9M9Ud9Cj|qimNlF|%JkEJb<{2|OH^AmDA8#QWF$JY3v-3B!$WY}Sgpb0lyROn?q@ z#Tpy=az%kOU{Yl4ff;p3j@|7Bio}b>(prH^4DLzLDLr(5j>T35sXO~r$=O|RK zkY|IoxoQR!6wY(>Q6f*+zrWyr#GtvvW(0mT1siD_f9@>IYc8xQEZm>8e2zjMo)Hz* z5tq^t7xyhrmoflqy6+&HQAnnwRHo-1t;5W|akSv7-Q0Mhd)V_i&`l1A<-Q8}987T9%qpdjX^|P;nSnx(DBk(XXva+m_ z_NelK*8Uj0(iclnRGu%5vWh9^{kb*sSQJlT(MVJQuhQ1&ig$K)jt1pKetLRRK#*4$ zv_)#KcSNFe;SW-i;;y8`McIA%I;}G~+32JuAJ92e2B2kzlQC2OX58Tps4VC@r zk9cFM+r^FtynGmzRA|d9{nM8Zzm-2CRqCa0dV$(t(T7@6O$glcV5Q&EETF=vD!_#K zu9@~us7HQ|-hsQhI+dsLH7Ud=BZYx~X#s~)9U4YtN80VRa?I>-g@)lqP5`at?MN>$ zw8ylQX-mwtdvtW1Rwf4hynYwU=7UTwMs8SpF?!@5Vi%eF!;SVY#$p9-QW_gC?+8-A z2LDyhP*0nz(bm@JCSE3Nq+Qe{#?+M0MJ%awk>3qX+O7Glec||XMqfStMc$I0N6F{2 zvT$Gnfj2PuLB**4ddpEz@y>oc&d6K|&3ORoRJ9!DOZRl&E9N4^zlX;?bHJYU{g!v0 zCXMQk@9UdD@tDp2o(T7Endh2Te+bTO>M3oM1mm1sI)_T0+(BL%6p(9=rz;A}it9R5 z;`L({N@~EKs+mTmd0`2wae7@sF6-PC2^}mExFk%nEmB}UoZ4F#@icq2VY<8wYea-J7U;Fc8z?72Z#*cK&rbQO=eX}xZ4xOt@E zO#a-{D&~IXP60+=zqjVF7j-2OcWHG#(ak6d*3O{DJHcl3o98$;^=P2>^&xBOu0dQgy0g{;jFR)SP~s?x zE+?F_kpRiea8e|U^00P~se`c&>`anxk@awZrhLy@BCZ!W=f8cWSt?ABBPRqq+B(Q_w`AyO? zXdjC06y+wd4vPk^T>CnDaGomLOSX05UP1)|Gc)u&Q_WXmDZymolngCieFruI4Q;~i zu+EUpv)6n8PIVQ{3#v|~XhTrfhgJj!Il+Z&l~ai=$9u6%`L@aCP+qHs>b-^N+qAl) z=O;@~H*)s_+njyDa|DS#l?1JO2Q7h270W}>KEs?$Hd`jD&PRz5QBz^fl*|6_5%N$9 zMCq^N#z}uz>yJ>+grJ!kd4rK?3Mt3DABn8Xi0!1cNBZ>lA}(1{S6H4&Ta{R+w6C2V z0&U-)Ei78!R~hp%(q6Rto2U;Lx=W46_~*188vQ<-;ALJ1@FSZ0$r7Zo;~cq)rvVEg z5VsS@230IOV@I(tvej0#rr185y=*tvBB_oQV3>d3vB;^Z!a~-GFK#EJ^Z_lF5g?K+ zq6rUwq zRn|ngqGHJtwR5#P^Uh%~*?jG9VP?u&_g7Q&irpe&V>!o%2RgM3vSt?l(bPirlR;{f zUpUA_%k>ic@*e`pGZYylYy7WGi#M7|Na0Rh)Rbjm%$4c47lggLXpnRE8;5ENk?*0c z19%a_9u!CR--P)$k^fE8=EW$R|+jexXHW)n#Fwe5CTPh--gPZa_o^DpE$a=Th=dCARQa-tD z1IR`{KAl{h)o4gQ+OPZgD&l8r&-l2wUaR~X$^?x1ExtW#ucF=8Er88+RH$7<< z_VQ-29M@(5lC1r%&1+>3y-~^%upM_)qGjgqdlyCyX+aYqoU@eYyBA7haic{wR1u7S zGpO?C^!PlP^o7D`HjhKXT+$)N-Sg|^b#>*XwnsC@vx5=&o6*d5dtJ~@&yzTTT4M2x ziFvJF4sq=1MH+Y4$D%{TvMVIJbXApEjPzamu_yTKS8Wxqyq)ff7)isP9R;}c&R{Q| zFMK3RFjSm*uErOE2<(*N?A#|}`aRL@lS@A0hdaKy}(*~p=|PuF+# z;uuoJTs{V|T_3QW;oKl*^h4?s?crvuG7abaHOT3Bm&Z=(Pnl-d*Yj`UD?PKA2E-@5 z`${r1^4qp`k+pE170K2iLPv?ii|Msu(`+qu9J3?PS&+6 z{>iepqbGdSnsXkj;h83{cdu5%0LGP&fR2reqVEkPFMcX&$jf6=r(mQ8+NqZ;Tn^m% ztn|jT0j<7%*P5E>1|@?)UdtzK$HcCG`+whS>9O?1=|1sZSqW&6J;_b1#OxKKUF}on zW((b%C!;+EM`)A8V!(vr1g&;HNj;x8R#YFNx_Ys9beLA)y{B?JzPg}$F(EH*n*)^a6F<{Vw?o21xG7hcVK7Uknm}${;RcG&debbaUqxuTiQ;-jnSvQ8nQw-3E zSTYJ-QBBPa>nA9P4Y6QGRtox11rXFHRee_d9P>98pej&S(4Pk561a2q9;*2Ly>Yl==hsV{fn{nnO{&-iG*g;{k@Eo>^3>})0IQ)7*{+=)BcuT zJfij8&7~WB!fncx@pRU<-v!vz(lZ`@{WNHl%`SLZw)dXW#rL( z2W9Z&tTryN5Vbz{M}^yLPje$wIdBb{>)?KVBCd+w_dDM}yjlz=nvDx>se$b*+4`?u zj!rThep;pZs^A@Xds_jb*WDLlhw*?EH$3R+j;w|P@&)F+P{7&!B$Ee3t`FxO+pDYm z9ar}r`j^aY7EzI93Iq4vS;txj+2gm2Rl_@Q}Szkmk; zm8!nlrY)ZV%z6u!P1)4**U7(^OI@DUF4S$>S{F#KnteYHN_49cDCZoTm;F>8vZ=}) z$rShgc}WGWvs`)2zU)^KCmF4*HsXk}7a*e1BFF!vCjGT4@U!Nkud#IskM5q!+} z7;L&bH&@bQ>?3_~*iVda0L;R}m0ofb=`KbchqwworV_5~jJ?Rfh<=yxf_+9&cOk$R zOD`SZgbT;z{odorKH&kC>$qLbJdldyF&c#mZ7u4;pF90ml-#+u$X2 zZb##^56&&}nrprIz>7Q;p17wPRX$C)OYsR_PXk#)#!|q9WI8`vv?gA3i&rAhf5~NX zU>VEQG`P{Y1WV=iO4I3AF)DBN+jIQ*z6tdC8aK;A_{(N+mMq3xM!Z(wBefjg)QH!l z9loaTc4mYT^tAyQpfwQTf19mLkGisOrMdE?1*sG>065hI*PcDZNT}y_8mamE#nrE= zlCJLk@VotVwqJcv5;r{>5eb$44in2R3gEY29>iQ(=6Y{wTXoJPx~I9{%Qm&)JkR|$ z*^wj(uPs;L2vem%MkZg5_NXKjc*W?H;7FHDaIS_&VAlQ-#-*t~uZ5!|w~*ejwKCon zGob7ypw}3X7;x$PrF&pGY@_{k1?$o{_FI9tw-OoebAGq^`W;?ywmbRa?Xhg%N_on` zMFXUq4wHTx(_QYWDc7s_7oJz;H)V-}S`|Pfd|*D+SxQR92&keN$&^X|3?k`Zp zxFrzpY%{#3fXMsdmx-Nt_e#IY?GjKu08Y{W1oJzmS%W_NVN%f&n=1lK*b@Y{KQQxliL_{K0Mg=~t`J;de2^-Fo{Y_ptEAFjca7iNsR zvrR^8H_^fSh@a^`{2|Nm9O^7mRmsTosJ1Q z3k89m!sD^z2&Y@Lc)EmFm~ASD$s0*ls>D2(HImBmqp%Cun5x8VWl)0?Sq;FwW3(Ny;S1oac)C1gGRL zIL5O^?O3*U2>owQ(Qm)L%mYTy*$q}bA=++tVr`+{bA>ZH+#@rVPtyy~j((f8_&@ZU zpuUkj$5ER2SvM7qD;ikHIG+q zy9>>6O?USS$c$-oD)S!%ueDBVP)qbU&s|+wc5Q_}VN#k&hfN4Bn_MFJda`@Do!XXD z<<<17Np82Y713C_Gu@oOqf+I})W%#qVzF4CrkL^yLX|0oWRJiUKqx-aYEVv;xMf`EuSWuU=pMp-#U z!;2VTG|N+OV zIPT2dH1lIok>c6Gs3SF?!`pQgM4`6neQG*oyl!ip{+5Q>+V+Is6kn;s1aw*JXNe9M z>&`y9$xU}XwI1;mZd&wgxIi0h_B<7ETR$=;fF#xJR@`jqy4$oU?F zHW$D+YL;MCFceL_1+nd*C$$yg_33ToH{-v{CNRmF2IWYA)CwyEL=pwSKU4k=+?DFO zJr>-o*&EUlqJE%It~!1bd+!@q<26D`BwUo3c(7@2XnU>jH*U<6tX$W7$5;$sPLtco z3+DO$fmWc>6fo?}6nRZ1u@y`V?9kw)bw3>uT5oZVuFguT{Z=)7$S-c$AAfC%-+H+p zNNK6r^z1^*ksAAzg zVHq)eXzyM@gJC5b()Yc53=7_D(SH8-G&&INQj;wfJEB>2Q_jum&o{4Wr*{at9z=Xw zRokatX_duw-DJzgO0*^#r9r1%pZVI3Z;o*cPG)xcDeqeq0!?7&WX)HG1|#{`J|(Vu zO6VdyfehqWLhAZlpLV%!T>}>aXb3V4p}U_LKCe?GuT=-t|7ZSP|Wl*f7$9fr^E-H zfjqJ%xFTy8dXofaS)E#-iLq*`5`_gIf2bj(-ah%ZfC2N!a$IsUd(Ky_&wZAiH%VNU zXgE`mv&}mxXnbTqIzIG(|uM=gY3|8w&Rug-is*^>RT>GP3?rwRS6X| zOZYb%Rdb4}&B}e}MBDo}FMlgCSjo!g(3IzBDGUJGUk2Gh;*yYs^uR}e+o}`eR*o7zEv4GK7}lqQ>y)vMt81V@A zWvF_@|HPE&$%cstKf;cQrZXn@9_Uh)GHY#GG9p=igB~#QZfcz-sbvmaUJGyFcYNqm zsT||?pQ))bX%lC!qS=a%40Gr8co1+KbzKsV&0_vgxs>W3_5`~RPMP;)FGDOB^q+vc zBQ9Xds>PW}m7yHwU(>I_mao4(qrQ_#Ic7xP7jVgx*7X@aZ(tTYqf28@!t-Qp!44|2JSv0)nl`oDD3>r~~)L)tg<2 zVVcLJe6FtP_nED6|J&*vR0QB%;msDkn$#=3;5WSAW11ILiOoY&^!{*~hL|9Elkb31 zW!vju8H#b_Fc~&=!0HF%uQryd4L|M&(V~n>(n-}{TzZv^XTU-|_Y%Kf-VbV>k?z9V z=xnFH?TKqO7&J3c>W3-@rT8~k zWV=zy7^3apE;(yXrVRJ8I<)o`O^fYM&olY&K%o5TB9Iqs(saLvFZqw^{Xoq}I2BbF zY=MWB9H31x(0w8&De$qmYMAo6l>$23nExg1ANx z4jpoSw9@VQI~9_jyr2My_)DdS^QbyT0_#Gi%tEuDk&hieSE!0=r}=LUc|74nCy_Ws>{HkBa6MGW67>!;wYIZwEV+?pm{wBFY%`Xy}+gZcqIk(oo5x3kfJG`KF=Hf2sGBmOB*H zdJo&bZ!f=BnN24e!hC&I>@tgNIRbjyWik8i81$Hsh&<+3IFx{#b;;55?!CmO2l}xct(d(e0r`2P}}6nX?(~S zJ3cT;d{?2c@1Q#nfpC2|9?nw0&Y5J)ek>z=Dm03>jS8Jlze$uyK$X?p@!E2ib1p(B z_fgB4yuv-I22}aqZY*Za9wvp)` z#?ZktO0%=#D$t&6Yrm;RM3WRAc{85bWWT=Z>mypX?FT=S-7-^^)eJcZW>U_VwPg|S zz4&m?(>I>V5rt~|?#jYXdYwr&k8DvO;t)THw6&j`aPlQ`@!~>j( zcC2Mg;Rns9=iu`4z)DxjuW=a2m;j=VwI?7E0ybAL#8`bE2%eYCZOyoT-C6?Q|7-cV zuX0-Y|4=RY=ZV@s#7L!l=s=BpU`Tmm8TQFx_Aa}uxSZ5oRBNmpbvzFck0OP)#W ztV(YDjPJwN|6%<>_n=5J3hmc{6f4@IjP>nYuit(OFrEgb`W1L45#q#W)Qt8#-R+U` zCuh;0Y-0JhFK06y40=)DoPpUxEz(%hC8hNeMGb%q-}pw^iyb z)ys&oIcQye+yGj@e$GBz4nwAg5L<+-{UrOlFX!6WlWgc?#gYQE1SDfX$S(SR!Z)#}Bm9W1cl-C5LK40uZzX9sVs@ah`T(5~=%GIhR^O=h6I^if~e~(0# zawIpv91nUG>fVdm)b5FyT4l4T2hEqhh5eM4w<+lViiJs9{u_z)cE2C)QY?;=d}$aP z)ekOL)eo8%b>OH8@vk(!7-&&H!EJ3mXKV}ea|7`b81J9=DCFE`T04op-X*C^tcW0(Z_l;3O5dl_K#p{qra}ITV41=91CZTAF94% zm0s)%xSFis_t)oR!{{|VnD)nxPZuzx?^ql?{T}ay1NQG))OY_bBI4gVon~WXlZtbe zX&wA9_TH;BQ?U84C*8WN3~b*qyM{_kYic7z_ITk3Xgau}Rm`3TE_Od1>|oOa`Z}r) zvz=V!b@?2e5EcoD2xmz-VlPQtK>2ul?#G5b(vjk-GR5gu`!`Fh?GI<4{A7?M<3ExD zMd?-%Np9VkS5dd176ph>UyN5$WvM2-G1_58O1T#_3m5=;?tCyXAKTa;V%*fZ)IO+M zQ80kSCk~!a_&)fSU#$zY-vQMF-$KVZEj8xv=)PaG(YyrDlu;*sg-t_SVFAicJ;Ao) zifhnT)yL2)OTSjN>5ub=YM==6?4oS-K|L{_TR-iaqQ5M+G1~K0JsYl2%cpkvylJFU zs{gl)>U?Q`{L1$kao?-My>kpVor@iI$6jOxzx!8$wmvaDJ}C{jZ{J@_8alJRc9~u7 zFg~BZB%P$APp;zaoDiTalzpT_iJ-&D`*E^o6Wk2PeiCE%c{(zicih7U7ysY@^xs@) z9>-QBNFtMX)^F&M%INCfTLGZU$}Rg;MlZPIeyc>4Mk(7=vr*xxUO;^R%hgxNBJ$D| zvvA;Vvtz?f!}UUB8TloONN(d}AYT7CrZGXsw#pPCQYdq98Qct#r_&mi`C~-)Ge5P9 z#kcIrIeHao`TAdNSO3FB^*^0h|7{Z&t`a|eD&$U;?;nP6JI{Yv^HEX*Q8!8efzLqqAm>3|B zwwaNoKx?Ge)Xk{*a-+a)Pv8Z^WIbi3-+6H#d~LkJ&a$U&RotTU{qJZ`!nrPoX$3jv z)cVz-`kTyV?MvmJmnJmUqt)SedgCy`bHmP={O!$0%zJqgIVOHF- z^5KD>4H>N#SPL818&G*Ihx%z2cEYMl8d1HY0h+pL7z>0ttZeoEUW-jdbE}P!0EXxf5uf(U4w$*p!7oSv^>Gk!xUkcsA9heuNF;%V z>asb(jI9Viu&+4~egj`e6kTUMlO5wJC}xW~#;>u7EP8G(DMx=aYW%loXrJ+jvFSy+8t(icZ#3L=}n36Q%!$H)Njg0feZP(QnSux2GTi2Zt=Vy*zBL zf{fyyotuK6S}*Ivb~lGXU=d}yWWwQ#3)OH9e>R`y9l=4Xo?@hocc$*Fn)=lX_uzr3 z6;CTEi4iF6Ux8>7nzT(-dP@zG+1Pu|Fw_q>bim(N@O&pHN8h_B{42y zM)My$a`2lk4;OJg!B>p$aBj@`5|N73UT1GnQ5SBJ_Zb@Z%sll36|FlW@7G^Lg(q5W zGHAM;9E;%}(Rj*ZWC`_PcEDbwi0C(rXZ4nDN zQA4h*9ta=jq&>5?GcV#GE{wMNAi6A@L;TQWM1KdC8C=**N*xCDyN5!IPA6|<-OB6v zpMA^dNr?PGvX5AKz!gc%UbMn*Y}Ao8ZBY1-NvA7OJ`$lcP+C;ddi3^vT4^&lrddBD zal6U=P@A+_XumotBEF?@AVcqxRZTcaWq^qvI0x5%c<$mHKhAiR1@uWal3YMfor3=W zw^$7@NYH5pZ+6!<{BbqLqkR94lXPztf0G>&bp`}o0~!%7IZfrl%%^@>Sy1&`jw=$S zP3vDFH3?{#g6H`hj3MyaY;r=HPPzPw0q%C6#Bd0^k`PYglLhspyAiJv7UQZc+cv6C zpMO)Y?X8!$BfK>zq?s>&tFcQfdH}luEc?zx`imSBhZ!mUHc?5Q%LW1NomaL2$oJ}cAeual{pK~&rm>3Bax zzM+TLvb#}V@k?R--HVkMN*%IS89!-&?D7~G6Ckyo4q@5Athsl_Cew%7|M;xO`u#kf z?qnimJR9{Hts8;8X!wJVSWL}x*S1&xub=n7%_9GF{r;m@#NqOWu5Ty9U3GcZ$bP$D z{F!@s1^Ye1eaS26XJ)U3kmLvMWz^)f44Mb%MnqE2&pl%m3;<7PQ zu7n1BQrnald+M%_qiMOvk9P+Rot%x@8s=B^x+r-%g+?mAA2r(qR>x~MR2c$J2*IS| z*muyooWZAnFJpi-{2OuLwk5WG$*!bo`MEozea^uT$aZq+^ z3W=2(It6x)kLC;kLb#F!IH!PIRH@PoS{?G0+;^@M}Br_p(GH%baU9=~)Dms*j z1ujA*2bm!C!_LpbEHj9%x$YL^cuD$blxiUvG}MjQ7-t8VZ@th^Jb6X}^u5l`236wm zL(RoSk2b$N2%-#Ncx)YM9AHhxEm4GrEL4om;i2prBX6`QV*2)UQ6YI&2N{m2PsC-O zMvU-DS4Tnnt}VpHZJq{Yq6K;DFO&~~kF-cj905o2%YVBO73mXaX8g2uXK;mosH+lv zMUh0MOIGALoaU9#b6LSnOf|BeLH>w;j#4ed0=9zb(Mzbiq?%6i@yXLY^ZYOsNrx{C zNsLkb*F@3`&PZSGO_#{3<%kDdrG&yd9?4Kl!?kIYv{VMC9G7Gw>4l)vqsn(8lDnPo(o;8r?qEky1>SmVZP@DCHwDMF4$ zC!KNZljcRn&y5l9avl+Sv1ATK?zJ5&UB=we@!_(npQVh| z%Bs+Cjocjh0KY|HhxGJ{$tGf?Wj*LnT&E6_g7-ke^FG%U17);}@fpg$H8Hk+*M%Hu z$=(NbE;j}?_9(G-cOkcj_&*Ghak>9th+Mp(`MRC*fx}Lgr%@~fDu*OmW+d1;tTeq5 zarxhTk^kwy`+spy{?}gSw22P&@s-&wpUASP-h**gKYhCE8lNeBPeay7!YHG#B#ivZ zPP~rCe7gTCuASa3>+yH-`4$p&l{s9mGyhm&&TM9%=JLlu-246owmg!#d_Iq!T92&j z%@a>nWq)VZlYY3w9iQk;wm&TJ%f4peJi;avE8D#MXq1J=-`#8OY4r>oNvMm2S{X_- zQy$c5rEg2sx_Aaix|uay5d*arWpCj@r{jm zotAr8$M)^WslM;@2eodCoQODa=X#~E_DpE znI%m(knQut)Gi-;{AFfGTCo==hgIJQ@t*5?_Zx~Mnm^;!c(2j+W|7Hd5R zcY7K=))PTBT6IhFhWc_lF9JFuE6D1&R-X{qzl$pPaA9-doTTazsTCG+M`vjj_QE}e`^*N3;9>#yOM!|_ zIdeCQ0{pJ-sffq*fdJOm6GqD%HXhmCT>^BzpkvuWrQhaTzdsngpm*^(d*A;NSuee_ ztO{Q=#CP{o`D2j3?RE0`>kDd+L+xm0dFku_&;qh`v(e*+PK>Rj3J0AQ!9D!lGr;a? zSlFnq#EFHI^x#WZXP2Ibi?^5xPal_td1c1&NbJoC(%E9}kNk}Imz|QJPDEJfO0gn( z`$x=M$41WP1SCL8z~N}->Y;U+BoRLY$E9BhST+#3CxZAjYXEgOK+P|&&1jLe9v^1B zJ>GvpwxZhRBf|#y`%9(Ut+b@%4U^d+^*+Xd(pmWkElH77o*hm>QlkX0+Ee2fVyb56 zwS60k6x`I$*+K^R%u{tu1Z_pd$`B7JMk-Ezj@$&LF|7N{{jp_^O>ODLEj)`Ks)Yjs zzWUvmI+uOpTXkp67!S#5@>V4`I46I=slya_Y7E zX?a%0WGJ%NnzkO4+>`L8b`W*9l_4HY?lpxu5{s*-srTlSTyTj@S&~;;1H&K7f6~-x zrQ_iG<(7_p*$Mq*Lu!I@{*0lkEQ-|FF|Qn1iTgUT%YLD6zOhX+i{ljBS5XQ+gyS}> z*=%hwEmaq~+7L`yY#}B}MSCnRwS|xEMiizhOGoD;*0o*J`M`#WxGg?5#_6YpoO8_% zaETW7lM`Dlr|Z7EG@P4{p1J!!a&X%I6ToTHXeBr7%Im#}-6pt7+~K19B!z{aUN$Jk z=OKPHV)zp){illl#63&-=rZ&IS-0(Bp|Xp6JFT_OG8CC5D)wD3WX2ssqN!^*Vu0v? z=ouY(#2Ec8MhO=EnU3GK<89r7zZ;*`nUCI%FtySAQbNOo^W+)pD;D8b+3-#4B7L9z z&xel{<&IPXKoX)U=LgHrZY^v5jkYul|=N#ytd>(gA9E?!q zZu1|FxNoe9`k&rF)@d0-j|#>9NePST{n>h0SfWqosg`&I7+%4EyHr`Y$SaDajXV6x z<28;jD{~$c&vO1pmu2)E(|pIKp*c6egr>6~s>)Ha-4O-Uysf{=A`K%iGE&~w&UzX+ zuTPumm=fAyKz%^sy(o zdRrGanPh95B&C?&!fTV%Oe))m5c#@Lwvp+d4sjfh#LV z#wQsN+*sT7b6;;-uPtm%Z6;m6k*V{Z=nBbLdaq!A?TXRFnJ1dryPp=I>fQ8=7Cx)^ z|Frj&QE|0vmX!cW2*Cpc4@DtCgS&eO5Foe(cXuZQFSH=IyAvchRD!z&cc+lxPEkzd zyK`^%?dhKBnbkk$NB@1#d(Ns=^{!KUKhNI#`Q5}3$VkmFA~~KKoKgEFA*q;_%>=kK zw+OwXPLC3_G9;MFyehhCsPy_W;mfjO%~zynmI$lCt&3_hAp4Q zoYxna%g`?ZrB1Lj`P1#(?`%(|{(@p}_T;ElUwYB7x{RHfS=iu*UjIa|`5>;i#yH|f zf#kT$`Ci{^10A60U#9s*aP_)|?PT*)PkcU0KY}e2Q|Hc`k~?g02DsQ6zXer^L|a>m zKk>;`=jGEL$C*|7R4lE=H!yh(6re_=B;@naXV8jzR6?AvaLaR_)JRp*?ONgP(7yh@ zqbgcoR$+1P18hg*@JK#10$ofiznTLL<$Z3vA}%zI8Jsx_Ljt+kVw3CwH4`D*{W;5^ zH~+2F``>g${-2b3=t$$rw+Q`G-vuD7@!$8S{4>J(k6bMOyerNb^wmJ*c*qf~-mQmfFX7 z_Z|b@sUME2cyaC9>f=b+mb!Jx=>*~vPp`X&63@u8$exN+XI<)0UdQ*V=&kbA;vsFI z@3|CurpVIA<6I`BUTpqtRAt+D4Cde3i>nI;VlF?#brAB~yuSLI7fvVPG`R|4m2DEl zHJTF4mODycZSwx|OK3&wzj`}ytqxD@G}pLcn&EAl($VqB95BvVOA3$NB-0dr5Z$8J z$f9xE)Z9dy^-5ZD(;&>MOkHs6P@3cvDE_X>@1V+K)yf5nKTtgqmcbl)mAmmgT4A6 zoh!U|Z=OwOruXqc-V;hAF58&>W&YVSF;0f8;W0(2e$b#MVCPGxpZ7iK<=YpISUgU* zq(&hlItmB3)lGQ4;>ks}-q#ywhr;_lV3fg5H9l8gG2f3!j?}L*A75KaK1tP+`XfDy zv`J>AnLK~XmI?hKpCOwxS4SOg9deRon!4~!>*%R|qyl})kFAjkjQ*hHy9(?yGx4xm z&n?rPZ<9efT{_KG*@2w{UsCFyyjwippT zuis+ySZ!^`S3(AFmxMU+hkr!%h8`wuqn|V1_WS4<~V22X!)ZE&g`0w3$(p>$U zOoNUQ@&?-SSE8Fz?c_XP4C^tUy18-<7ye=#d0H7cbAhdCBWohz9`MP0LwO3v+U;dh zA|9WG8^Kh1AfvYlS7LjI|#Eq4~;&shrwO;w3L6}4ux39K_wAx~K@t;+vY!pShjo+#3N$b8i# zkVW6za*`^W|2I*btjcFmX_AWE&3KpDTqV;$`D=Embo-b5eZyGH~QEoY!1vxDKB=Lu;b@VtfSb z#Q(BvykDqOeAAaBJ9?%Xu97NNf419pR_d$7^SbM z*$mCFSlO^oY~fmC7}Ziq2O6mC*=K z*aO4=LVSLjB-NVa>{=h5m+(G}!;g`U1uzONHqDHCkhGtW$Sl8E6gX0)gx&CWM~b5< zvZ7hB$|aGYOQ-aUzeS|O-KB8h!}n%^opn=n5KZ{cC*XBY2}1DIktSPqN>o=lRpw^! zXLXhSaL+@I$D?t|)_*|haj&T)OC~Wg6T$X`3&^hFY5A+;5jfXbp$#j-w!K-gcIG;g zTx8i>0XAKB=Nlg ziItX4Wni+H2-)s9!>7{2l*BON?nDn`#Z>BrPMaldGE{p zD*0s>`C8|&^nL*dd)}h1Mt2<2>~V!ye>0EtG(Ib6JFH2TIyxCqn23MrVgi59Pv(#* zOJtidoct=zOwe-aYv&`ZVOu7Y5GTE_@R~cxTTm5HTdLqe=f(Gz%XOQ~5GL(KRxK&w zIB(d`71c4W*x<;%rZ0~-tjX_Vx@?dZ6YsBSN*FW}x{}@03vn!qee6KnI@j&pmaSAo zodUG5oUD{eSZ;yupmzfenP$X-IPLrx{G!=#0hs|XQMBaWT6UIRAm)wz(A*!liFie! zD&A{#nWHujw;BJ+!sS^|cVJdtAh#(~N!~DjhNvr0&_O7~)TOe-3wCKIezin5|HW`2 zqd+p>tKZDX7;2sIRSS*NGLloxkDWUj!OpQOkzYQiCMn$%O&dBYol$;{>_)sb5K>B` zjj$?}9MG?ixF+~Det0R#ZG|U68)7y#qAX!786^LEGN?@QL;F?*Fjq9cJfQ4zqIS98 z+}wR+pDfb!;=K1P#k3*0)amdx55W>$s)Lmt(;^k(STxJ9v2O9%DJ-c?H=^M&(m0VY zdtdk3T9bsF1do}-QiOS^7V0h=>)z?}FPgxGPB@zS(~kOU=JJa)TR&HwPmh(e_I8*%DXzeq#BzLEbBk5*$@|c5hSz_5 zC6bCYzZxlwkl8UdojsCF<#4kWoM2(I=a!Cf{kr?IQ?qvSso&be^Z#B8`0lq+V6bE5 zNRV=UxTvTe;zLg4okCx{;2{-s8m|=nPL3n$-WQKi#4kgcRz@p;4GL6@tp&b%z$1vZN(!z!Z=PX(LHCaCONQcK>YJax zy_$!r*Pi~RN&NmKj$cX@zlOt2?Xf*3m#04U(U&+A%dru1s=;t=gR6kwX+HVBAHC5% zrB(hiwp65y|F{x`u*Q9kGt`Lh04e=lzF3`94s^9%`R2>6#Q{YAB~cV*2bPIRy6M;0 z(i1y=PkRfLV6J$?T=mVMHLQG>AFYcHm6J#9Z}~Gp#Q%-K|LZXLO}EH4exlD=*0Vyw z@b6nxno<=%R*`ufPsw@yOM3iuXFD|C)kdmUJ#%(`y(@dB%;lzae6M|ExEEzs{HCeZ z+w`~;GUrcrd?>M*SGlfk)0NpqfVG0A^cHyfQ6sg6aUHf>KHw$pMZeEKq~B-}OB+}8 z4@`+zla1eA)-qg7yXo+t$IJWV?8WAVR=yF zkf(D7)^i!J5w`dSJCkB{+(79wso#w8YNn^5KdoN#_iqmXAddum26GLYZ~j96WOP=5 zh?OuVEn9xG;@Dy~yq95M6BniAqfCs2*QI|J^-*HnhxySv2!ppbg2SQE5&N)ysU?WU zDH_D4&3{hNQdm{SOt$ti_E6G41Nwf`e5{r7$+$tuToBq`;eB`WcZLC%FQs%tA2g1- z?-yE#;#yi^VewbCMWer~S>bqb4NtEaVOCV-#Y*8#$3ZXBUiVR7zQvP6md2sz(bRkK zv@MKy7Qwx5dQ}JIuGl&^XC?7ev)t`Q$H9{QKV8Ef$5r*d!Ckma)beYe%sg0-**s#K zQStUUt&LHdSW?)rHGjClXWE=q9cWZsxTD>R$!mbGjPh72*R-sKVL$No>LW09sG4LZ zZenaiz-yRV!w&}V3B)^8{nGW(wiRFR5^2-qQC&|$h)1Q()pF(t=>++M=jzH(Lb&c0 z`2$Y{jew8ff_`v54xt4W8QXwSHhO;>VrNQ6!!Gm1ykz^GieE;fSEXS_29B!zKKyD9 zv~`^VCb~ouR=yqeA@!t`N8R+p4d`B1*arss6gHhJfUg$<Z!pxt;viN8$Dw+3DfwzJwCF97pAW87SRH&Nr}T2DK& zQN*l`f3_R&^$r@5b`LHhd=w6Af~@xC_gZ1KXI&58oI_)qoASt+<0kYDPf8TT*1~0! zlwQ467cPj;#b?`%vA}=5EpYT|yzeP88)adz!_z&wM(SaCbNkDwig;wW&-v;*S!A@^ zS3~x(w6UItM57(B?)VqaWIuYGNK!0$$@ZL3)`v@%E)1X7#LdW6w#JjT zw;A&GjOgwtyG;~Kg0=E{7b=Z6r+d^W54h~V&HhdZGvyhWBoo}}=lEF#U&TN380z~; z2)gf@({UdN8~BE9ywxZubg<)<31`|~En3l~ET7phoUUMu3BNf5qIpA71C3>ky80rZd5%5J08 zzwCzbAGd1%xy16HE5!W&x@%Vv0MJ(>ni!d{{hD8xE1oHWY@J%V>~<`iEGT(PL)xR& z_3rVJJ>Z^Sbff34b#ix>E3y+!537$J1t@Na-AJJ5sZ;lh%Iv#s3;o%(tx3XJ)Qr%4U#_$;Ty@mWhSFTp!ht58l~_ zNMqGE#hqw4$|3LL(RMZ6S~^HytDnx-`~FY>;h>9Japt{rv}HzKoIEQU1^NrIpZQ`> zEYFKT5(o%<+vq}%?r+3A(U8~WiNyD;tPm{^*(>Lk5zmEWz2gT+-%Wa5>T+&$!d8?F znjPZ-Khko&XDS;WZue*6i-<-~)oo`OL8O_1vZ0XN^;Ya<5&y0EWoqaJ*BA#I_vFOb zhmRj&^94(+qE7*qYi=_vdYO|`NgC?gn?#F@Of@iT!J{v7l2*tbtVY8w*58DM%@=1P z<;4SU7#La&R6gj6KUDafhXF);a|&~FtMY8K%WSjr{)D^j!si!%sI6JIgKViT8kR0q zGzvtx3Gbf)ehjX5%KaZh+x8(82^yO(36bAAYpa2_ri%9cHQS?r=)9tcn)dp{&(ZZc zm5P+UOZt0ze}%T~uo&Dh@>E&RR5s6ROV~Wwv$7l)i9voY<$IX2qotOk+}zg%cNP0W zo#b<3uGz%DLfXbRQYmA|evMC@lU=bO+wWyX5fQ}qyT(J%8*igiWN>xn1yXw}B?UxN zDWO58WY0J5%RFt#sl=j22egVUyOMA@!B*|v4?Q*K>Kl6NRqkZ)Y$F(ZF4eE-z|4WyqoZw=YTk2M8IzRkcafi z(v~wg0(sy@QTtijtI8^*0NKCBrX~g$n~`bD$VdFVSJ7iu__NbC_~jxi)t}t9uS^U% zaY5>MqI9`M655cCIm)kTvCY4M?6Bib%Q>Vk5JoJb zKo_s&_r1PusGp`5KHbItoLK8OLa@9(UutA2F54kq?RJ|&pnz>tjZolrO4VYC zqRc^w)nk;CoaSQ|e3u+oKM19}e&YD6{kJ=8I_u83LwCYsPI!rwCvOdMr8^Ox&IfEC zU0m{Rb~t>&U5O~t+W5|OM#FJ`Qu2wS(oiQPmFdf0)U zp@3JMUfM9AV`wyjwN)@%AP&kLJa!@+5)P{`O%rFQ)shF**X0AW@%oao$*nGTdnsBM zQe^N1J^*(PmqzT@J%iBC!q_M6rykjS{D#bJ@jo;oLW~VFRTjj1^G9ceP=Gy!=9JqF zw2$qw!Zvsk(xG?)b)=JlTlU*9?lp~ zOhvoR6?b%rjLWHDF0*jwi6_ieQNs_ob$+B=x*R+s-(I?|ctqI_7M}5R2<5?E%jrxS zjWVhqyH(pv(7Z(O9eO{kvMw;Xz9MCn{RbH!8ai7HJBW!MR%q zcDGrZB2vA+!xfV-o3rMDJJ%lK{w!U^+pOA$3ug?P{G{GbOF4ZSl=I6&j=lC8#dp{v zhx&b;pMl?yOU0#>Qy6G|!?w;8it_*xab$(Y3r;7wZ?4rROZ#5*=-E-s4`aP#l(V__ z#t;*2SWV*TOY8g{r0@=)JKKh1l|qV=R%O^gsHb7%@6jPHH$Ime6pUGbYS&MbB-n^! zS$9duUE*l6Vo}o!%NmlWxp${Kg=%N)JDkvcw=~DOoNpu_^~{xz54DP-MN8tYx}^2@ zURb<~f6y)TebA<2gXtW_V3Kxo$iCbyQL_AdEr6{ezE$s7BgS>-YCb4@dWOk6D&Lr| zMHAK-&b=RmMExp_C?4-O>vuUAuXSDsEs0_bq3@1UI;&At4lcozEkaY$NN6qYPv;+W zr=&SUL{MzIp1Y&W-|PsjQhGxDWK}#RnzHY~hi;o#~t4lwFW2Q z1P1oTcxMcRWtX>Su9SFw-K+Y<_a%cgQ2*OzE|;@e+3Rey{SW6Zz&h|xD2Bi52^JNc zBJt$KN)I`Ua_7(crKBs|Aam?%feHGn`l}^L@0#POmk8yG$uW`V`0&pH9#LH3Rerw& z!0)2y9(UJfQpnCHzr)-i8y3E>J$yN!Gkp(IvfwSE<$n=|rvOjx5=i98dWZZK$E{iC z^`d$k&kVf%SheAObKj<%e!teAz#!#?rv&S^Zyw#`a%=QNK~uE=Yx3S#)~pCe3I9Wl z3c(Ym#T5(T=R9LXmvU5M_($KL?L9MT%vSwnW~4yS*=~RKw9)Qa)hIiIN8U9_{q;38 z@SLpuKvhl$n(Yq~9H-Xo>?Cz<4Ejl@dz#4k(unqy$f>cbT}M9XUbt3C0@Yfl?onNI zwWR?9FypbqC7uEG3O*}UKw-O-ExYoutB5$Yu*Z1%*08e(xAM!Ifjrnldt*PB&`{Vd zu-quG)fUE?xAudv)cv-;)zm~EDx)8M5*Zs(Q9sB2bDzETP<~VCgVri>dvy-lPq6Wc z(|a)L_gXVWIsyM7dj$t_7Q;M=kB=-r)31=;o_5qrFWlNynSMORQeHH-@oh-y$Ti7L zAR_k*!W4qIR;ghFmw$xYIe%$hGor8LV$PJw?TA1Au;lPDUNgez8MXOhkS~wdDgkH6 z-SDel*IT$QhFyO8_ayTAp^CGTJL%63W|Q84y>OT}uAq|lxFqil>ddaJ>75U*^G>`r z@X3`cSMbUDg9)MtleA{z67p}_bx6TX6&|RSF|>OD@ks?jn zX4M(9v%)i>MY%Z{(2MEx*Ho@nD&CUG7T|ucFcI8<=6hkyr{mLQkki>UqT>yk>2sEu zuW%_aH*;Xrh7)YvqS0L^eIEX{NKa;c;%L3%`qTD1`l7YcodsPZ`Kr8r!`8kuN81>^ z^^=)hfer#y66P1UtX+M820EW%+c;JP74v$y2r%tc(^4b4J#umL@iW!nBfWWxZmqsb3*24L*d0mLhPPK{)|m zC{HEAsn>I^odZlU(e0(=0`^}c(_(bkA-=wQ^?Qu#e7iAPbVJ19TDrRATwee*IZw*4 zju{%ieY+}IUUJ$YrsGm6G3IU!KW)1{s4HOl@nX&nT3NrKSzO@-Y;659=$f1CBaRd~F5 zNU;A&%nd3(jbX|69I4k9)P1)ZbshM7r~Xf?KFQ7c7wuc$Zj0dln~cT~JPT}0djks# z+~b936D-x?5n_T!ScEpvS~v4TjJleOjxy&nnU&lzttI?~B(5Px%1%M_2MI^7bC-)ep_aMixU@@(jes|A(7DSzT(Yp-Yy~c-pC=c*(ek?)K3Mm z(_=CzfrJook+L#G!fDAHUPT^W3d`)FI32`f^oYvfhML zZZyk4z~ihzesmkDK)25p2LuS!<-GNZ%#nJ~$6YaC9VjE-$XSsJVSbvD*dPx%Qh!Kq zGzlj-eHP&8LZ?^f;xOMG+E9fYY5>s#O}$iKgzptE&D2iSdE)^-=~pd3uib{;?xHlXz;8Jxd<+WgH} zE9s#WCyFl#5sOHF&v-#;Us{$U3RoTPUfiJx?t#-}$^cz9Z+6K z{@q~*buEF5DP(OKD2&H*{`x)~Pi=SnG-ErIwbn8jLJBqNAFoMT)XP3zg{sRy9Mc~7 z=8`#mQS;Et>Kk{V4oP5A;iZ(~g>x1_2ANI;C?==5ito#_BpJ^QZu=J^b!!-zI=6pP(IX5Wy)aqnRc<9Tx_g^DwNQFui>+qq_UHEbs<Gj$WoGl8bsn-O#UC5{ebUJQTbV9gnUm zg9EUL7{6+4v(CHFwzt}tSo1LH%E5)NpL`$I%}6r{nMi9j-7v9RC9%W~CQ=!QJdio@ z3e(kLf~ZF?rhY8{ogQ+q=v?6tK>?ML)=@(-A04+ZLIZf@wU!V)f+^kXFi<-A^cVTX z?<(yT594!&D`#DbG)y&KncEoCRre{}xopB7`DI00t6>rc)8Yk4M%F{-Q4|V}-*k`? zxgBw`+oa3t&Kv-LFGeA8fX6+T0ChD*It)t}s-%)T&hjdQ*(t*ye^wUhdSmac9QW~x zH^)AaZgd)#u=Gc9ruq~Y$fpDNzw=z~{l+*64Vr?1NvFr1 zYK~2yut?JCv%;MYU=R_R<3-`;+M5|pr>NALzGpwFH+$@(K^v{M`X14Rblbc=b;$~L zpmt5V+%0A&$E7&&tgA7}#}?CR3-1Psg7Y`vV)LWR*p)?2!%m)Y#sN~VdZh7& z6C|8L3OE^#8#yH|eHBqLug8kjr_Q6{IkebzwVSaDXFQJpf802Q|M1!To5215yg^5n z4xA7FA}m1{^NOF3UXK3^<;T7uBLtGn4KW=r@ueU)I19CVY{4L2w^rWM)4^r;*P7+8U4YNl#aVZn$F4d zJQx1u(eSpiDjgxlSr~u2;_RlRt>c%{otP^g*B{%7SJ;b2CY@5x$^EkOCm9&)qQm=;NmHR$Vis*zjAlnMw4?Z zbF)WK%SwJx;)Q*CKlyC^3gP2t)5O<_Ed#JndZEu?X=5~SKfs&W=L!z8#X(#=3XCi4 ziAv?h><0=;%&bi2ckjHtHyNbWA1_sQ>~(mTuX%K2I!tUhC-34ajzH57c`oqPrvZQD zn9^Xj*M^e-`OZICuxrZZW1*Lg$pG;-bn8)m6Z2Sp#|z>0@ShinJKU>-`-mUlUf-7| z?)T+QGgEr|ETPZ8zCFl3YkxC0*!_GKb96#Q!g7*Mn6P&jNW((TpfBux>yhXo5@+pT zYvaZM;Oy8WeTVv$dZ2G!st_W9oHcm!XQtpsUKPz=OHRlur4{$Em1W%Zh~Wx>abchd z_?sWd&63vu>4I{uDBUvd?v2#(TRW{$UN^qIv(YvK)!RfBJr*@)s)saw%dVa%jY~&b zr}Q-G3|rUAnTgD}h@~V<-gPQyi!`^s_mG(Q!kJ>w!Oq$85lG9*@&v$%@^9Q-+}3jT zf!hqafU(DQe;?8j76~k|jG*k|yO~j?+@{0@9b<~mQ3Z=BXTvcsf z?61XXgDM~j6E5*Iy1Vz$=5OYrO!AAjN z*C4=uh9La&yT0#C$c`%}bGcR9@9ebmE|`me;KG$l2_4Y)B~XP2Gm3)$cEBE6nex+u zyquIBQ@Sw7&l%)kb{$-p5XsKD-kBMRQguF9+ zkh$6YNE{%#Z1F|3UsP&TwA<|4Wjx(z{3yYJX4abU)ba11ZDBV3+>lHWsN3Lx^Te9HdG?_DSBxM%=l zV#z7iyu74{IESph2;`BktSg0UXWrN#Cuoun2pHdOnh2NvD~hmVv(#4f$IU0D)s!~% zx+2YA81xr?;P;d8KNm&b6uT9$+)$Z-|K~KTnY}F5Sp|b0@#nV}?(wBeNz$6Q=uOnl z(+N!`JOj9h4h$@1O%B4W2~7#)E|gjr`p)iM0LKH(SFqE~erF(lWKUN`LT<~9{PWtT zGOBZh$vZ7;>~Ay321B7S3xr|N%3mZl(|7-~#ZX2W&H2vsR<3)IcB$ohI{A;Z$fi;G z2yiRaAC6$6yNPTO*mKqccYA_6;6rAtdZVLEMI1N)59ewExdv6Rf%6jpY;3laW@)s_ zYnT|xk2=lmQ2-LN5g>AjshRXOsiqS~+M{-Q$BE!2Kkx>IPV+{QUuzb@Q*c+2?&9lL zM>C7mJdO1!BlT;kK}CtYnW`=pj@A^!s|sD}y~91@Zw2nf203EY^IGnkKQ52O(yBhk zMp(Oosu)Zshc?CqwVgit-}F>TABWD^axXWUpz;K7kB&@c#k zY=*>Dni=hNn{vA;6mszhU(I(5J|2^+o_e=uo@Z+`*V0sCuVp<>EX%03(8}VvdK$Gh zn6`}@l{}2}bZ$qL`M9LDoOKuHnJS*$5KBfbC0@gpXFIKxwt6VvO_De0#)`QOZetG# zc&&;%W2+{le-L!I^byFLl!#i;B1;v3^!zHKRg~aF7e$6=GxvMec=E`t;AUsf{?5Q= zlHH$U;saUCaGD-mPUZM3a{Czy&ZOMVP7R3h7ZZ3Ege4^}3*jTEn`mPJ`Cv0DqL zo%~b>%@XkEQaqv}zgY^#AW6p36y}DdOibj)p^7AcpH3I`H8YY-;mlj(FV;Adh+DAT z1vi}+y(dfd*u)V*Wre$?hh05?#82t-DGarz&RRWPesDC`rEidxTG}@^xmA;vmA@`jV6MU0X-~1bIuxtjoTor;W7Mp4G;WwEf|hh&@BMpnUT;Nb zx+4HTnj{3IOSKDR){epHlA#)=D+y)rGC8qyDgQI4T0RNPtq8 zEF@JW2OA54AU0Q6qTdQ8@EP{AKOQT|*_h5=izGcF(=rLl7U_ms{7BO{%t5ovo6P8t zCu6k>TP+=Db@{H(%hp?cmYJQB1YyegAjPOq5;|P~8+DVwN9Px)gMji14!2tF6MekW zTd0mFl+~wS3@_Rq)VsPP94$Vy- zNWTU!R?>PC2jY*pe0=}r;YHR2JbiJP4{|7@YPn%D@%Ru@oU~Zo@9dcLj>{iE`%JVK z^IwnA`sX+O{{{B9cLQx5rU`p?jr9wQ)Q`GH%gTgm$^wWD52+U+NlN-&ttZ6@;m}J3 z&#$aHLr{lJ?kjxqZQmfouTJvocB-MjPU-henUnC-XkCf~j>j2`HA-8@qQ{3UpO0B2-jgWEb{X zKtIxgF4Q48U@)5fYeo)@S+f+f1Q)6>Qq5M@ED@rI!ejF$7-yS5{S3ABkmvSBy7r zABfSglZU5rKtd#Tx6!|7`D>HcI>2De>t%@2R|t*O-fr1M_<8o3-P8ePGdS24URR5!CrgT zPtU?=xe<2R+FIeiWkxDsDY;Jssjq!8JOq2e`Q>uX#B2=UN-X9VCFm%?A<)kW9^n|V zeEA4=%zv#R@3fR6JFl_MFH;-YWP)=ZeqBvNfg?SA-ygmJ>0rsyw8>s7XpMSkf9p%m zN(;d{F-}eDJ=8MH&Pwl8diR#d$S7Q&y4oFs6RPZLhYvZQ<53euu}1c@D{;P6W0=AJcB7J zSy((`Q4zi#nD`S&&PNnJFdG=1whGVw!~i`?bQFx+r+a5N$q9`m^AlrW-KbwZgm()d zmj~HZA0Iz~UE9+;YPzk3#RSiW5m_q@55Hlz$|!~m(;a~Mm=-vr>$<~Y2h&7;uM9~V zW|S!}l}=&EU01xN`04sJ_vyjWdZajBEABrDKK_;@WdJvB?~*&|obdu7z?h046ekzB${$u9J4v{IifUz7< zdLP?<)x3_*=f0o=mdCJ%UGF(4DCzztH7n@}peEk4VLfKEBb?(FPb*FjG#&!ks<~pR zf>+nPDq05c2Ry~Lx;kj(bM!KZT4v{8k~Z-v$zQws;?3%^(z+#W_!AjN@gh&a^16qy zv)2NfxIoSX_JE8+czC$cZ7x90mw)ny1fl>$s)+kKs=f4)D*!nuMafF>Pl5jf4ne9R diff --git a/docs/screenshots/mobile-month-agenda-dark-390x844.png b/docs/screenshots/mobile-month-agenda-dark-390x844.png index b0bf647423a06d393cdfb7bf96f13483f78be1e4..4a23332d8a9d44044198e3e382265bb0fb947161 100644 GIT binary patch literal 36791 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 F0sxrAv_}8{ diff --git a/docs/screenshots/mobile-month-swipe-pull-390x844.png b/docs/screenshots/mobile-month-swipe-pull-390x844.png index 7d8f3476266a6d03abb215ad4af1206092aba821..c7bb4d7bfd2349fe765ae921d8f44562793b3976 100644 GIT binary patch literal 34501 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

      ghfzkgo%B|Af}2@er<`WaK8D* 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 tDX-p%0sw%=$Jd&6+QiP8W7 diff --git a/screenshots.manifest.json b/screenshots.manifest.json index 425f6ad..15a8e2e 100644 --- a/screenshots.manifest.json +++ b/screenshots.manifest.json @@ -7,7 +7,7 @@ "playwright": "1.62.1", "browser": "chromium" }, - "sourceFingerprint": "fb28e2b9348573cd34c8da7dbd3f69533f7f36934420140c60125f757e0eaa5b", + "sourceFingerprint": "cc6e5a6a445fc20bcef7f8117364f5003c223436b9df49448b3140371c71cdb9", "scenes": [ { "id": "desktop-month-grid", @@ -68,7 +68,7 @@ "README.md", "docs/screenshots/README.md" ], - "sha256": "f38dfc110721bdfcf159e0670e9f51b99af846dd0bc0b2d747168bea8388679d" + "sha256": "f6125a23bac8d0e82558980dadd3cdfef642b1aa2ab3cb3a48a4d23666913c64" }, { "id": "mobile-month-swipe-pull", @@ -88,7 +88,7 @@ "references": [ "docs/screenshots/README.md" ], - "sha256": "baa9ec985ae5505cb3081b1ef16bcb3f6bc43439e92989ba6e6624b6852b3b74" + "sha256": "601d7c09a370600a81700e3615c2d1ed4d1547b30d1dc8d865f756be68759625" }, { "id": "event-details-dark", @@ -149,7 +149,7 @@ "references": [ "docs/screenshots/README.md" ], - "sha256": "e05eb374b815306c635fc367848b01f48bd25f1c0d5a7ae51e30f1097a546774" + "sha256": "5415fe5b86e34abaaad608ceebc013d6640fd6c541c572b3b99079d647e14422" }, { "id": "compact-all-counts", @@ -169,7 +169,7 @@ "references": [ "docs/screenshots/README.md" ], - "sha256": "dd43d435efd2e7ce95484be7a43443977a3c2c13e5c2ef3e6aadab6e9778927b" + "sha256": "73651a8dc586f24b481484c81ff64ae00c97cb4d60a104ff1fa145a9a6c9280e" }, { "id": "wide-all-counts", @@ -211,7 +211,7 @@ "references": [ "docs/screenshots/README.md" ], - "sha256": "3d54f1da2819de8345daa7fa952ac8f67bf0ac292baeb78b108fd85162ab3cce" + "sha256": "bfffb7ca4cf635f00d15217feadc26aecab0a34ed9e429d520974fc89df9f849" } ], "browserTargets": { diff --git a/src/internal/dom/month-title.ts b/src/internal/dom/month-title.ts index d45c6e6..1d39038 100644 --- a/src/internal/dom/month-title.ts +++ b/src/internal/dom/month-title.ts @@ -55,7 +55,7 @@ export class CalendarMonthTitleRenderer { const fullTitle = this.fullFormatter.format(date); const compactTitle = this.compactFormatter.format(date); setTextContent(elements.titleLabelFull, fullTitle); - setCompactTitle(elements.titleLabelCompact, compactTitle); + setTextContent(elements.titleLabelCompact, compactTitle); setAccessibleLabel( elements.titleButton, formatCalendarMessage(this.triggerLabelTemplate, { date: fullTitle }) @@ -69,12 +69,6 @@ function setAccessibleLabel(element: HTMLButtonElement, value: string): void { } } -function setCompactTitle(element: HTMLSpanElement, value: string): void { - if (element.getAttribute("data-lfc-compact-title") !== value) { - element.setAttribute("data-lfc-compact-title", value); - } -} - function setTextContent(element: Node, value: string): void { if (element.textContent !== value) { element.textContent = value; diff --git a/src/internal/dom/palette.ts b/src/internal/dom/palette.ts index 2863831..623257f 100644 --- a/src/internal/dom/palette.ts +++ b/src/internal/dom/palette.ts @@ -52,17 +52,21 @@ export class CalendarPalette { } } - private readonly handleMutation = (): void => { + private readonly handleMutation = (records: readonly MutationRecord[]): void => { if (this.disconnected) { return; } - this.observeAncestors(); + if (records.some((record) => record.type === "childList" || + (this.needsDirectionFallback && record.attributeName === "dir"))) { + this.observeAncestors(); + } this.sync(); }; private readonly handleConnection = (): void => { - if (this.host.isConnected) { - this.handleMutation(); + if (!this.disconnected && this.host.isConnected) { + this.observeAncestors(); + this.sync(); } }; diff --git a/src/internal/runtime/event-overflow-presentation.ts b/src/internal/runtime/event-overflow-presentation.ts index 48f43cd..9dd9ace 100644 --- a/src/internal/runtime/event-overflow-presentation.ts +++ b/src/internal/runtime/event-overflow-presentation.ts @@ -176,13 +176,8 @@ export class CalendarEventOverflowPresenter { button.classList.toggle("lfc-is-wide-count-only", wideCount && !compactCount && !hasOverflow); const overflowCount = wideCount ? options.eventCount : Math.max(0, options.eventCount - this.options.gridEventLimit); - const formattedCount = this.options.numberFormatter.format(overflowCount); - const text = formatCalendarMessage(wideCount ? this.options.messages.gridEventCount : this.options.messages.gridMore, { - count: formattedCount, - eventLabel: overflowCount === 1 ? this.options.messages.event : this.options.messages.events - }); - const wide = createEventOverflowElements(this.options.document, "wide", text); - wide.root.classList.toggle("lfc-is-count", wideCount); + const wide = wideCount || hasOverflow + ? this.createWideOverflow(options, button, wideCount, overflowCount) : null; const accessibleCount = compactCount || wideCount ? options.eventCount : overflowCount; const accessibleLabel = formatCalendarMessage( compactCount || wideCount ? this.options.messages.gridEventCountLabel : this.options.messages.gridMoreLabel, { @@ -192,23 +187,35 @@ export class CalendarEventOverflowPresenter { ? this.options.messages.event : this.options.messages.events }); - button.setAttribute("aria-label", compactCount && !wideCount && hasOverflow - ? `${accessibleLabel}, ${text}` : accessibleLabel); + button.setAttribute("aria-label", compactCount && !wideCount && wide !== null + ? `${accessibleLabel}, ${wide.text}` : accessibleLabel); + return Object.freeze({ button, wide }); + } + + private createWideOverflow( + options: Readonly, + button: HTMLButtonElement, + wideCount: boolean, + overflowCount: number + ): Readonly { + const text = formatCalendarMessage(wideCount ? this.options.messages.gridEventCount : this.options.messages.gridMore, { + count: this.options.numberFormatter.format(overflowCount), + eventLabel: overflowCount === 1 ? this.options.messages.event : this.options.messages.events + }); + const wide = createEventOverflowElements(this.options.document, "wide", text); + wide.root.classList.toggle("lfc-is-count", wideCount); return Object.freeze({ - button, - wide: !wideCount && !hasOverflow ? null : Object.freeze({ - action: button, - content: wide.content, - date: options.date, - dateString: options.dateString, - display: wideCount ? "count" : "overflow", - eventCount: options.eventCount, - overflowCount, - root: wide.root, - text, - variant: "wide", - visibleEventCount: options.eventCount - overflowCount - }) + action: button, + content: wide.content, + date: options.date, + dateString: options.dateString, + display: wideCount ? "count" : "overflow", + eventCount: options.eventCount, + overflowCount, + root: wide.root, + text, + variant: "wide", + visibleEventCount: options.eventCount - overflowCount }); } diff --git a/src/styles/month-grid.css b/src/styles/month-grid.css index 4857776..add151f 100644 --- a/src/styles/month-grid.css +++ b/src/styles/month-grid.css @@ -257,6 +257,12 @@ line-height: 1; } + :where(.litefold-calendar) + .lfc-calendar-event-overflow.lfc-is-compact.lfc-is-count { + font-size: 0.875rem; + font-weight: 700; + } + :where(.litefold-calendar) .lfc-calendar-event-overflow-content { align-items: center; align-items: safe center; diff --git a/src/styles/responsive.css b/src/styles/responsive.css index 11f300c..7683653 100644 --- a/src/styles/responsive.css +++ b/src/styles/responsive.css @@ -9,12 +9,20 @@ } :where(.litefold-calendar) .lfc-calendar-navigation { + display: flex; + flex-wrap: wrap; gap: 0.375rem; } + :where(.litefold-calendar) .lfc-calendar-month-stepper { + flex: none; + } + :where(.litefold-calendar) .lfc-calendar-title { + flex: 1 1 max-content; font-size: 1.125rem; font-weight: var(--lfc-internal-font-weight-medium); + max-inline-size: 100%; place-self: center stretch; text-align: center; } @@ -26,6 +34,10 @@ padding-inline: 0; } + :where(.litefold-calendar) .lfc-calendar-today-button { + margin-inline-start: auto; + } + :where(.litefold-calendar) .lfc-calendar-toolbar-end { gap: 0.375rem; inline-size: 100%; @@ -52,6 +64,7 @@ :where(.litefold-calendar) .lfc-calendar-day-button { gap: 0.125rem; + grid-template-columns: minmax(0, 1fr); justify-items: center; min-block-size: var(--lfc-compact-day-min-block-size); padding: var(--lfc-internal-day-padding); @@ -61,8 +74,16 @@ grid-template-rows: minmax(var(--lfc-compact-day-min-block-size), auto); } + /* A definite preferred size avoids the repeated grid layout seen with a percentage min() width. */ :where(.litefold-calendar) .lfc-calendar-day-number { + aspect-ratio: 1; + border-width: var(--lfc-internal-hairline); + inline-size: 2rem; justify-self: center; + max-inline-size: calc(100% + 2 * var(--lfc-internal-day-padding)); + min-block-size: min-content; + min-inline-size: 0; + padding: 0; } :where(.litefold-calendar) .lfc-calendar-day-badge { @@ -298,6 +319,65 @@ .lfc-calendar-grid-more { grid-row: auto; max-inline-size: 100%; + white-space: normal; + } + + :where(.litefold-calendar) + .lfc-calendar-day-summaries[data-lfc-compact-display="count"] + .lfc-calendar-grid-more.lfc-is-compact-primary { + aspect-ratio: auto; + display: grid; + grid-template-columns: minmax(0, 1fr); + padding: 0; + } + + /* The decorative square sets a floor; wrapped count content can make the row taller. */ + :where(.litefold-calendar) + .lfc-calendar-weeks[data-lfc-week-row-sizing="equal"] + .lfc-calendar-day-summaries[data-lfc-compact-display="count"] + .lfc-calendar-grid-more.lfc-is-compact-primary::before { + aspect-ratio: 1; + content: ""; + grid-area: 1 / 1; + inline-size: 100%; + pointer-events: none; + } + + :where(.litefold-calendar) + .lfc-calendar-day-summaries[data-lfc-compact-display="count"] + .lfc-calendar-grid-more:focus-visible { + outline-offset: calc( + -1 * min(var(--lfc-internal-focus-size), calc(3 * var(--lfc-internal-hairline))) + ); + outline-width: min(var(--lfc-internal-focus-size), calc(3 * var(--lfc-internal-hairline))); + } + + :where(.litefold-calendar) + .lfc-calendar-grid-more + .lfc-calendar-event-overflow.lfc-is-compact.lfc-is-count, + :where(.litefold-calendar) + .lfc-calendar-grid-more + .lfc-is-compact.lfc-is-count + .lfc-calendar-event-overflow-content, + :where(.litefold-calendar) + .lfc-calendar-grid-more + .lfc-is-compact.lfc-is-count + .lfc-event-overflow-default-content { + inline-size: 100%; + max-inline-size: 100%; + min-inline-size: 0; + overflow-wrap: anywhere; + white-space: normal; + } + + :where(.litefold-calendar) + .lfc-calendar-day-summaries[data-lfc-compact-display="count"] + .lfc-calendar-grid-more + > .lfc-calendar-event-overflow.lfc-is-compact.lfc-is-count { + align-self: stretch; + grid-area: 1 / 1; + padding-block: var(--lfc-internal-day-padding); + padding-inline: calc(2 * var(--lfc-internal-hairline)); } :where(.litefold-calendar) @@ -401,14 +481,27 @@ } :where(.litefold-calendar) .lfc-calendar-navigation { - grid-template-areas: - "stepper title" - "today today"; - grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); + row-gap: 0; + } + + /* Keep Today on its own row while the steppers and title wrap at their intrinsic widths. */ + :where(.litefold-calendar) .lfc-calendar-navigation::after { + content: ""; + flex-basis: 100%; + order: 1; } :where(.litefold-calendar) .lfc-calendar-month-stepper { justify-self: start; + margin-block-end: 0.375rem; + } + + :where(.litefold-calendar) .lfc-calendar-title { + margin-block-end: 0.375rem; + } + + :where(.litefold-calendar) .lfc-calendar-today-button { + order: 2; } :where(.litefold-calendar) .lfc-calendar-month-picker-fields { diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index 5cf4bce..617b5a4 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -18,7 +18,7 @@ flex: 1 1 32rem; gap: 0.375rem; grid-template-areas: "stepper title today"; - grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(max-content, 1fr); + grid-template-columns: minmax(max-content, 1fr) minmax(0, 2fr) minmax(min-content, 1fr); min-inline-size: 0; } @@ -92,10 +92,6 @@ min-inline-size: 0; } - :where(.litefold-calendar) .lfc-calendar-title-label-compact::before { - content: attr(data-lfc-compact-title); - } - :where(.litefold-calendar) .lfc-calendar-month-picker { background: var(--lfc-background); border: var(--lfc-internal-hairline) solid var(--lfc-border-color); diff --git a/tests/calendar-boundaries.test.ts b/tests/calendar-boundaries.test.ts index ce7a397..c14690f 100644 --- a/tests/calendar-boundaries.test.ts +++ b/tests/calendar-boundaries.test.ts @@ -460,7 +460,9 @@ void test("the month title opens a localized native popover and jumps with day c assert.ok(gridLabelId !== null && gridLabelId.length > 0); assert.equal(picker.heading.contains(dom.window.document.getElementById(gridLabelId)), true); assert.equal(picker.trigger.type, "button"); - assert.equal(picker.trigger.textContent, "July 2026"); + assert.equal(picker.trigger.querySelector(".lfc-calendar-title-label-full")?.textContent, "July 2026"); + assert.equal(picker.trigger.querySelector(".lfc-calendar-title-label-compact")?.textContent, "Jul 2026"); + assert.equal(picker.trigger.querySelector(".lfc-calendar-title-label-compact")?.getAttribute("aria-hidden"), "true"); assert.equal(picker.trigger.getAttribute("aria-label"), "Choose reporting month, currently July 2026"); assert.equal(picker.popover.getAttribute("popover"), "auto"); assert.equal(picker.popover.getAttribute("role"), "dialog"); @@ -547,7 +549,8 @@ void test("the month title opens a localized native popover and jumps with day c assert.equal(isPopoverOpen(picker.popover), false); assert.equal(picker.trigger.getAttribute("aria-expanded"), "false"); assert.equal(dom.window.document.activeElement, picker.trigger); - assert.equal(picker.trigger.textContent, "February 2027"); + assert.equal(picker.trigger.querySelector(".lfc-calendar-title-label-full")?.textContent, "February 2027"); + assert.equal(picker.trigger.querySelector(".lfc-calendar-title-label-compact")?.textContent, "Feb 2027"); assert.equal(picker.trigger.getAttribute("aria-label"), "Choose reporting month, currently February 2027"); }); @@ -904,7 +907,13 @@ function getMonthYearPicker( function getAccessibleName(element: HTMLElement): string { const labelledBy = element.getAttribute("aria-labelledby")?.trim().split(/\s+/u) ?? []; const referenced = labelledBy - .map((id) => element.ownerDocument.getElementById(id)?.textContent?.trim() ?? "") + .map((id) => { + const referencedElement = element.ownerDocument.getElementById(id); + if (referencedElement === null) { return ""; } + const copy = referencedElement.cloneNode(true) as HTMLElement; + for (const hidden of copy.querySelectorAll('[aria-hidden="true"]')) { hidden.remove(); } + return copy.textContent?.trim() ?? ""; + }) .filter((value) => value.length > 0) .join(" "); if (referenced.length > 0) { diff --git a/tests/e2e/container-modernization.spec.js b/tests/e2e/container-modernization.spec.js index fb54d7f..09f3ccc 100644 --- a/tests/e2e/container-modernization.spec.js +++ b/tests/e2e/container-modernization.spec.js @@ -83,13 +83,13 @@ test("direction fallback follows nested ancestor changes without replacing focus await host.evaluate((host) => { host.removeAttribute("dir"); host.parentElement.style.removeProperty("direction"); - host.parentElement.dir = "auto"; const text = document.createElement("span"); text.id = "my-audit-direction-text"; text.textContent = "\u05d0\u05d1\u05d2"; host.parentElement.prepend(text); }); await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "rtl"); + await host.evaluate((host) => { host.parentElement.dir = "auto"; }); await page.locator("#my-audit-direction-text").evaluate((text) => { text.firstChild.data = "English text"; }); await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "ltr"); await expect(count).toBeFocused(); diff --git a/tests/e2e/grid-event-display.spec.js b/tests/e2e/grid-event-display.spec.js index 47ee9a1..d6c5348 100644 --- a/tests/e2e/grid-event-display.spec.js +++ b/tests/e2e/grid-event-display.spec.js @@ -71,6 +71,56 @@ test("resizing preserves an active event until blur and skips hidden actions on await expect(action).toBeFocused(); }); +for (const update of ["setEvents", "refetchEvents"]) { + test(`${update} restores focus to the visible count after resizing a focused event`, async ({ page }) => { + await mountCalendar(page, { width: 900, suppressMarkers: true }); + const { action, count, day, grid } = targets(page); + await day.focus(); + await day.press("F2"); + await expect(action).toBeFocused(); + await setContainerWidth(page, 320); + await expect(action).toBeFocused(); + await expect(action).toBeVisible(); + + await page.evaluate((update) => { + const fixture = window.gridCountFixture; + fixture.savedEvent = document.activeElement; + if (update === "refetchEvents") { + fixture.titlePrefix = "Updated event"; + fixture.calendar.refetchEvents(); + } else { + fixture.calendar.setEvents([0, 1].map((index) => ({ + id: `count-${String(index)}`, + start: "2026-07-14T09:00", + title: `Updated event ${String(index)}` + }))); + } + }, update); + + await expect(count).toBeFocused(); + await expect(count).toBeVisible(); + await expect(action).toBeHidden(); + await expectOnlyOneGridTabStop(grid); + expect(await page.evaluate(() => { + const fixture = window.gridCountFixture; + return { + oldActionConnected: fixture.savedEvent.isConnected, + requests: fixture.requests, + sameAction: fixture.savedEvent === fixture.host.querySelector('[data-lfc-event-id="count-0"]') + }; + })).toEqual({ oldActionConnected: false, requests: update === "refetchEvents" ? 2 : 1, sameAction: false }); + + await page.keyboard.press("ArrowUp"); + await expect(count).toBeFocused(); + await page.keyboard.press("Escape"); + await expect(day).toBeFocused(); + await day.press("F2"); + await expect(count).toBeFocused(); + await page.keyboard.press("Tab"); + await expect(page.locator(".lfc-calendar-agenda-title")).toBeFocused(); + }); +} + test("container resizing keeps count labels, nodes, source calls and render hooks stable", async ({ page }) => { await mountCalendar(page, { count: 4, width: 320 }); const { count } = targets(page); @@ -128,6 +178,175 @@ test("count controls remain usable in a 320px RTL container with enlarged text", await expect(count).toBeFocused(); }); +for (const total of [12, 123]) { + for (const enlarged of [false, true]) { + for (const rtl of [false, true]) { + test(`${String(total)} compact events fit at ${enlarged ? "200%" : "normal"} text in ${rtl ? "RTL" : "LTR"}`, async ({ page }) => { + await mountCalendar(page, { count: total, rtl, width: 320 }); + const { count, day, grid } = targets(page); + const normalFontSize = await count.locator(".lfc-is-compact").evaluate((label) => + Number.parseFloat(getComputedStyle(label).fontSize)); + if (enlarged) { + await page.evaluate(() => { + const root = document.documentElement; + root.style.fontSize = `${String(Number.parseFloat(getComputedStyle(root).fontSize) * 2)}px`; + }); + } + await expect(count.locator(".lfc-is-compact")).toHaveText(String(total)); + await day.focus(); + await day.press("F2"); + await expect(count).toBeFocused(); + await expectOnlyOneGridTabStop(grid); + const geometry = await count.evaluate((button) => { + const rect = (element) => { + const { left, top, right, bottom, width, height } = element.getBoundingClientRect(); + return { left, top, right, bottom, width, height }; + }; + const box = rect(button); + const style = getComputedStyle(button); + const outlineWidth = Number.parseFloat(style.outlineWidth); + const outlineOffset = Number.parseFloat(style.outlineOffset); + const outerExtent = Math.max(0, outlineWidth + outlineOffset); + const innerInset = Math.max(0, -outlineOffset); + const label = button.querySelector(".lfc-is-compact"); + const walker = document.createTreeWalker(label, NodeFilter.SHOW_TEXT); + const lines = []; + while (walker.nextNode()) { + if (walker.currentNode.textContent.trim().length === 0) { continue; } + const range = document.createRange(); + range.selectNodeContents(walker.currentNode); + for (const { left, top, right, bottom, width, height } of range.getClientRects()) { + if (width > 0 && height > 0) { lines.push({ left, top, right, bottom }); } + } + } + return { + box, + cell: rect(button.closest(".lfc-calendar-day")), + fontSize: Number.parseFloat(getComputedStyle(label).fontSize), + focusVisible: button.matches(":focus-visible"), + lines, + outlineStyle: style.outlineStyle, + outlineWidth, + ring: { left: box.left - outerExtent, top: box.top - outerExtent, right: box.right + outerExtent, bottom: box.bottom + outerExtent }, + ringInterior: { left: box.left + innerInset, top: box.top + innerInset, right: box.right - innerInset, bottom: box.bottom - innerInset } + }; + }); + expect(geometry.box.width).toBeGreaterThanOrEqual(24); + expect(geometry.box.height).toBeGreaterThanOrEqual(24); + expect(geometry.fontSize).toBeCloseTo(normalFontSize * (enlarged ? 2 : 1), 1); + expect(geometry.focusVisible).toBe(true); + expect(geometry.outlineStyle).not.toBe("none"); + expect(geometry.outlineWidth).toBeGreaterThan(0); + expectRectInside(geometry.box, geometry.cell, "count button inside its day"); + expectRectInside(geometry.ring, geometry.box, "focus ring inside its button"); + expectRectInside(geometry.ring, geometry.cell, "focus ring inside its day"); + expect(geometry.lines.length).toBeGreaterThan(0); + for (const line of geometry.lines) { + expectRectInside(line, geometry.ringInterior, "count text inside its focus ring"); + } + await expectDayNumberContained(page, "2026-07-06"); + await expectDayNumberContained(page, "2026-07-10"); + expect(await page.locator("[data-my-calendar]").evaluate((host) => host.scrollWidth - host.clientWidth)).toBeLessThanOrEqual(1); + await page.keyboard.press("Enter"); + await expect(page.locator(".lfc-calendar-agenda-title")).toBeFocused(); + await expect(page.locator(".lfc-calendar-agenda-event").first()).toBeVisible(); + }); + } + } +} + +for (const width of [320, 358]) { + for (const enlarged of [false, true]) { + for (const rtl of [false, true]) { + test(`navigation touch areas fit a ${String(width)}px ${rtl ? "RTL" : "LTR"} host at ${enlarged ? "200%" : "normal"} text`, async ({ page }) => { + await mountCalendar(page, { count: 12, rtl, width }); + if (enlarged) { + await page.evaluate(() => { + const root = document.documentElement; + root.style.fontSize = `${String(Number.parseFloat(getComputedStyle(root).fontSize) * 2)}px`; + }); + } + const navigation = page.getByRole("group", { name: "Calendar navigation", exact: true }); + const title = navigation.locator(".lfc-calendar-title-button"); + await expect(title).toHaveAccessibleName("Choose month and year, currently July 2026"); + await expect(title.locator(".lfc-calendar-title-label-compact")).toBeVisible(); + await expect(navigation.getByRole("button")).toHaveCount(4); + const geometry = await navigation.evaluate((container) => { + const rect = (element) => { + const { left, top, right, bottom, width, height } = element.getBoundingClientRect(); + return { left, top, right, bottom, width, height }; + }; + const titleButton = container.querySelector(".lfc-calendar-title-button"); + const visibleTitle = titleButton.querySelector(".lfc-calendar-title-label-compact"); + return { + controls: [...container.querySelectorAll("button")].map((button) => ({ + ...rect(button), + name: button.getAttribute("aria-label") ?? button.textContent.trim() + })), + host: rect(container.closest(".litefold-calendar")), + navigation: rect(container), + title: rect(titleButton), + visibleTitle: rect(visibleTitle), + visibleTitleWidth: visibleTitle.clientWidth, + visibleTitleScrollWidth: visibleTitle.scrollWidth + }; + }); + expectRectInside(geometry.navigation, geometry.host, "navigation inside its host"); + for (const control of geometry.controls) { + expect(control.width, `${control.name} target width`).toBeGreaterThanOrEqual(24); + expect(control.height, `${control.name} target height`).toBeGreaterThanOrEqual(24); + expectRectInside(control, geometry.navigation, `${control.name} inside navigation`); + } + for (let index = 0; index < geometry.controls.length; index += 1) { + const left = geometry.controls[index]; + for (const right of geometry.controls.slice(index + 1)) { + const overlapWidth = Math.min(left.right, right.right) - Math.max(left.left, right.left); + const overlapHeight = Math.min(left.bottom, right.bottom) - Math.max(left.top, right.top); + expect(overlapWidth <= 1 || overlapHeight <= 1, `${left.name} and ${right.name} touch areas do not overlap`).toBe(true); + } + } + expectRectInside(geometry.visibleTitle, geometry.title, "visible month title inside its button"); + expect(geometry.visibleTitleWidth).toBeGreaterThan(0); + expect(geometry.visibleTitleScrollWidth, "visible month title is not clipped").toBeLessThanOrEqual(geometry.visibleTitleWidth + 1); + }); + } + } +} + +function expectRectInside(inner, outer, description) { + const tolerance = 1; + expect(inner.left, `${description}: left`).toBeGreaterThanOrEqual(outer.left - tolerance); + expect(inner.top, `${description}: top`).toBeGreaterThanOrEqual(outer.top - tolerance); + expect(inner.right, `${description}: right`).toBeLessThanOrEqual(outer.right + tolerance); + expect(inner.bottom, `${description}: bottom`).toBeLessThanOrEqual(outer.bottom + tolerance); +} + +async function expectDayNumberContained(page, date) { + const number = page.locator(`.lfc-calendar-day-button[data-lfc-date="${date}"] .lfc-calendar-day-number`); + await expect(number).toHaveText(String(Number(date.slice(-2)))); + const geometry = await number.evaluate((badge) => { + const rect = (element) => { + const { left, top, right, bottom } = element.getBoundingClientRect(); + return { left, top, right, bottom }; + }; + const range = document.createRange(); + range.selectNodeContents(badge); + return { + badge: rect(badge), + cell: rect(badge.closest(".lfc-calendar-day")), + lines: [...range.getClientRects()].map(({ left, top, right, bottom }) => ({ left, top, right, bottom })) + }; + }); + expectRectInside(geometry.badge, geometry.cell, `${date} badge inside its day`); + expect(geometry.lines.length).toBeGreaterThan(0); + for (const line of geometry.lines) { + //Text ranges include vertical font metrics beyond a line-height: 1 badge; the day owns clipping. + expectRectInside(line, geometry.cell, `${date} text inside its day`); + expect(line.left, `${date} text inside badge left`).toBeGreaterThanOrEqual(geometry.badge.left - 1); + expect(line.right, `${date} text inside badge right`).toBeLessThanOrEqual(geometry.badge.right + 1); + } +} + async function mountCalendar(page, settings = {}) { await page.setViewportSize({ width: 1200, height: 1200 }); await expectLibraryFixtureReady(page); @@ -136,14 +355,14 @@ async function mountCalendar(page, settings = {}) { const host = document.querySelector("[data-my-calendar]"); host.style.inlineSize = `${String(settings.width ?? 320)}px`; if (settings.rtl) { host.dir = "rtl"; } - const fixture = { errors: [], host, hooks: 0, requests: 0 }; + const fixture = { errors: [], host, hooks: 0, requests: 0, titlePrefix: "Event" }; const options = { events: () => { fixture.requests += 1; return Array.from({ length: settings.count ?? 2 }, (_, index) => ({ id: `count-${String(index)}`, start: "2026-07-14T09:00", - title: `Event ${String(index)}` + title: `${fixture.titlePrefix} ${String(index)}` })); }, initialDate: settings.dialog ? "2026-07-13" : "2026-07-14", diff --git a/tests/e2e/helpers.js b/tests/e2e/helpers.js index 0cfebbf..0dbf905 100644 --- a/tests/e2e/helpers.js +++ b/tests/e2e/helpers.js @@ -27,8 +27,27 @@ export async function expectLibraryFixtureReady(page, { calendar = false } = {}) const response = await route.fetch(); await route.fulfill({ body: LIBRARY_FIXTURE_DOCUMENT, response }); }, { times: 1 }); - const response = await page.goto(LIBRARY_FIXTURE_ROUTE, { waitUntil: "load" }); + let stylesheetResponse = null; + const captureStylesheetResponse = (response) => { + if (response.request().resourceType() === "stylesheet" && + new URL(response.url()).pathname === "/dist/styles.css") { + stylesheetResponse = response; + } + }; + page.on("response", captureStylesheetResponse); + let response; + try { + response = await page.goto(LIBRARY_FIXTURE_ROUTE, { waitUntil: "load" }); + } finally { + page.off("response", captureStylesheetResponse); + } expect(response?.ok(), "Expected the isolated library document to load.").toBe(true); + expect(stylesheetResponse?.ok(), + `Expected /dist/styles.css to load successfully; received ${stylesheetResponse === null ? "no response" : `HTTP ${String(stylesheetResponse.status())}`}. Finish the package build before browser tests and keep dist unchanged during the run.` + ).toBe(true); + expect(await page.locator('link[rel="stylesheet"][href="/dist/styles.css"]').evaluate((link) => + link.sheet !== null && !link.sheet.disabled && link.sheet.cssRules.length > 0 + ), "Expected the library stylesheet to be parsed and enabled before running browser assertions.").toBe(true); await expect(page.locator(READY_SELECTOR)).toHaveCount(1); if (calendar) { await page.evaluate(async () => { diff --git a/tests/e2e/responsive-layout.spec.js b/tests/e2e/responsive-layout.spec.js index 68d17f5..dac075d 100644 --- a/tests/e2e/responsive-layout.spec.js +++ b/tests/e2e/responsive-layout.spec.js @@ -229,10 +229,7 @@ async function expectResponsiveMonthLabels( }); await expect(titleFull).toHaveText(expectedTitleFull); await expect(titleCompact).toHaveAttribute("aria-hidden", "true"); - await expect(titleCompact).toHaveAttribute( - "data-lfc-compact-title", - expectedTitleCompact - ); + await expect(titleCompact).toHaveText(expectedTitleCompact); const titleButton = host.locator(".lfc-calendar-title-button"); const titleButtonLabel = await titleButton.getAttribute("aria-label"); expect(titleButtonLabel).not.toBeNull(); @@ -288,7 +285,7 @@ async function showSeptember(page) { await expect(page.locator("[data-my-calendar]")).not.toHaveAttribute("aria-busy", "true"); await expect(page.locator(".lfc-calendar-title-label-full")).toHaveText(SEPTEMBER_TITLE); await expect(page.locator(".lfc-calendar-title-label-compact")) - .toHaveAttribute("data-lfc-compact-title", compactTitle); + .toHaveText(compactTitle); } async function expectActiveMonthTitleNotToOverflow(host) { @@ -300,15 +297,19 @@ async function expectActiveMonthTitleNotToOverflow(host) { } const active = getComputedStyle(compact).display === "none" ? full : compact; const style = getComputedStyle(active); + const textRange = document.createRange(); + textRange.selectNodeContents(active); return { clientWidth: active.clientWidth, height: active.getBoundingClientRect().height, lineHeight: Number.parseFloat(style.lineHeight), scrollWidth: active.scrollWidth, + textWidth: textRange.getBoundingClientRect().width, whiteSpace: style.whiteSpace }; }); expect(metrics.clientWidth).toBeGreaterThan(0); + expect(metrics.textWidth).toBeGreaterThan(0); expect(metrics.scrollWidth - metrics.clientWidth).toBeLessThanOrEqual(1); expect(metrics.height).toBeLessThanOrEqual(metrics.lineHeight + 1); expect(metrics.whiteSpace).toBe("nowrap"); @@ -327,12 +328,8 @@ async function expectSingleLineMonthTitle( const titleLabelFull = page.locator(".lfc-calendar-title-label-full"); const titleLabelCompact = page.locator(".lfc-calendar-title-label-compact"); await expect(titleLabelFull).toHaveText(title); - await expect(titleLabelCompact).toHaveText(""); + await expect(titleLabelCompact).toHaveText(expectedCompactTitle); await expect(titleLabelCompact).toHaveAttribute("aria-hidden", "true"); - await expect(titleLabelCompact).toHaveAttribute( - "data-lfc-compact-title", - expectedCompactTitle - ); await expect(page.locator(".lfc-calendar-title-button")).toHaveAttribute( "aria-label", @@ -354,36 +351,29 @@ async function expectSingleLineMonthTitle( const activeTitle = compactVisible ? compactTitleLabel : fullTitleLabel; const style = getComputedStyle(activeTitle); const rect = activeTitle.getBoundingClientRect(); + const textRange = document.createRange(); + textRange.selectNodeContents(activeTitle); return { - activeText: compactVisible - ? compactTitleLabel.getAttribute("data-lfc-compact-title") ?? "" - : fullTitleLabel.textContent ?? "", + activeText: activeTitle.textContent ?? "", clientWidth: activeTitle.clientWidth, compactVisible, height: rect.height, lineHeight: Number.parseFloat(style.lineHeight), overflowX: style.overflowX, - pseudoContent: compactVisible - ? getComputedStyle(compactTitleLabel, "::before").content - : "none", scrollWidth: activeTitle.scrollWidth, textOverflow: style.textOverflow, + textWidth: textRange.getBoundingClientRect().width, textWrap: style.textWrap, whiteSpace: style.whiteSpace }; }); expect(metrics.activeText).toBe(metrics.compactVisible ? expectedCompactTitle : title); expect(metrics.clientWidth).toBeGreaterThan(0); + expect(metrics.textWidth).toBeGreaterThan(0); expect(metrics.lineHeight).toBeGreaterThan(0); expect(metrics.height).toBeLessThanOrEqual(metrics.lineHeight + 1); expect(metrics.textWrap).toBe("nowrap"); expect(metrics.whiteSpace).toBe("nowrap"); - if (metrics.compactVisible) { - expect( - metrics.pseudoContent.includes(expectedCompactTitle) || - metrics.pseudoContent === "attr(data-lfc-compact-title)" - ).toBe(true); - } if (overflowExpected) { expect(metrics.scrollWidth - metrics.clientWidth).toBeGreaterThan(1); expect(metrics.overflowX).toBe("hidden"); @@ -401,7 +391,7 @@ async function setLongMonthTitle(page) { throw new Error("Expected both month title presentations."); } fullTitle.textContent = titles.full; - compactTitle.setAttribute("data-lfc-compact-title", titles.compact); + compactTitle.textContent = titles.compact; const titleButton = titleLabel.closest(".lfc-calendar-title-button"); if (!(titleButton instanceof HTMLButtonElement)) { throw new Error("Expected the month title button."); @@ -416,6 +406,7 @@ async function setLongMonthTitle(page) { async function expectCompactToolbarVisualLayout(page, layout) { const positions = await page.locator(".lfc-calendar-toolbar").evaluate((toolbar) => { const selectors = { + navigation: ".lfc-calendar-navigation", next: ".lfc-calendar-nav-button-next", previous: ".lfc-calendar-nav-button-previous", title: ".lfc-calendar-title-button", @@ -431,10 +422,28 @@ async function expectCompactToolbarVisualLayout(page, layout) { return [name, { bottom: box.bottom, center: box.top + (box.height / 2), + left: box.left, + right: box.right, top: box.top }]; })); }); + const controls = [positions.previous, positions.next, positions.title, positions.today]; + for (const [index, control] of controls.entries()) { + expect(control.left).toBeGreaterThanOrEqual(positions.navigation.left - 1); + expect(control.right).toBeLessThanOrEqual(positions.navigation.right + 1); + expect(control.top).toBeGreaterThanOrEqual(positions.navigation.top - 1); + expect(control.bottom).toBeLessThanOrEqual(positions.navigation.bottom + 1); + for (const other of controls.slice(index + 1)) { + const inlineOverlap = Math.min(control.right, other.right) - + Math.max(control.left, other.left); + const blockOverlap = Math.min(control.bottom, other.bottom) - + Math.max(control.top, other.top); + expect(inlineOverlap <= 1 || blockOverlap <= 1, + "Expected separate navigation button hit areas.").toBe(true); + } + } + expect(Math.abs(positions.next.center - positions.previous.center)).toBeLessThanOrEqual(1); if (layout === "two-row") { for (const control of [positions.title, positions.previous, positions.next]) { @@ -450,10 +459,12 @@ async function expectCompactToolbarVisualLayout(page, layout) { } expect(layout).toBe("three-row"); - for (const control of [positions.title, positions.previous, positions.next]) { - expect(Math.abs(control.center - positions.title.center)).toBeLessThanOrEqual(1); + const titleSharesStepperRow = Math.abs(positions.title.center - positions.previous.center) <= 1; + if (!titleSharesStepperRow) { + expect(positions.title.top).toBeGreaterThanOrEqual( + Math.max(positions.previous.bottom, positions.next.bottom) + ); } - expect(Math.abs(positions.next.center - positions.previous.center)).toBeLessThanOrEqual(1); expect(positions.today.top).toBeGreaterThanOrEqual( Math.max(positions.previous.bottom, positions.next.bottom, positions.title.bottom) ); @@ -1657,8 +1668,8 @@ test("equal compact slots honor the existing public control and event size token expect(singleton.inlineSize).toBeGreaterThanOrEqual(48); }); -test("focused later compact actions retain a keyboard-operable target for minimal content", async ({ page }) => { - await page.setViewportSize({ height: COMPACT_VIEWPORT_HEIGHT, width: 390 }); +test("a focused later event retains its minimal-content target when compact counts appear", async ({ page }) => { + await page.setViewportSize({ height: COMPACT_VIEWPORT_HEIGHT, width: 1_100 }); await expectExampleReady(page, "/examples/advanced/"); const selectedCell = page.getByRole("grid").locator( @@ -1693,6 +1704,10 @@ test("focused later compact actions retain a keyboard-operable target for minima await expect(actions.first()).toBeFocused(); await page.keyboard.press("ArrowDown"); await expect(laterAction).toBeFocused(); + await page.setViewportSize({ height: COMPACT_VIEWPORT_HEIGHT, width: 390 }); + await expect(laterAction).toBeFocused(); + await expect(laterAction).toBeVisible(); + await expect(actions.first()).toBeHidden(); await expect(laterAction).toHaveAccessibleName(/.+/u); const box = await laterAction.boundingBox(); diff --git a/tests/e2e/screen-reader-semantics.spec.js b/tests/e2e/screen-reader-semantics.spec.js index 6348ce6..0735128 100644 --- a/tests/e2e/screen-reader-semantics.spec.js +++ b/tests/e2e/screen-reader-semantics.spec.js @@ -93,7 +93,7 @@ test.describe("screen-reader-facing browser semantics", () => { await expect(selectedDay).toMatchAriaSnapshot(` - gridcell [selected]: - button "Thursday, August 6, 2026, 53 items" - - button "View 51 more items for Thursday, August 6, 2026" + - button "View 53 items for Thursday, August 6, 2026, 51 additional" `); await expect(selectedDay.locator(":scope > .lfc-calendar-day-button")) .toHaveAttribute("aria-current", "date"); diff --git a/tests/e2e/theme-integration.spec.js b/tests/e2e/theme-integration.spec.js index d380463..8a857e5 100644 --- a/tests/e2e/theme-integration.spec.js +++ b/tests/e2e/theme-integration.spec.js @@ -108,6 +108,8 @@ for (const fallback of [false, true]) { parent.append(window.__lfcTheme.host); }); await expectPalette(host, "light"); + await host.evaluate((element) => { element.parentElement.className = "my-dark"; }); + await expectPalette(host, "dark"); await page.evaluate(() => window.__lfcTheme.calendar.destroy()); await expect(host).not.toHaveClass(/lfc-palette/u); }); From ab72543b2fb9c235482d5fcd0afc46d9cbfd77ae Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 20:40:48 -0700 Subject: [PATCH 13/22] docs: reconcile consumer guidance and design contracts --- ACCESSIBILITY.md | 13 +++-- CHANGELOG.md | 19 ++++++- CONTRIBUTING.md | 2 +- DESIGN.md | 31 ++++++---- MAINTAINERS.md | 4 +- docs/assets/calendar-anatomy.svg | 18 +++--- docs/component-anatomy.md | 60 ++++++++++++++------ docs/fullcalendar-v6-migration.md | 6 +- docs/integration-guide.md | 48 ++++++++++++++++ docs/remote-data.md | 3 + docs/screenshots/README.md | 2 +- examples/fullcalendar-v6-migration/README.md | 2 +- 12 files changed, 155 insertions(+), 53 deletions(-) diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md index 873e75d..cee2bcc 100644 --- a/ACCESSIBILITY.md +++ b/ACCESSIBILITY.md @@ -41,7 +41,7 @@ Activation invokes `onEventOverflowActivate` with an immutable occurrence snapsh At the supported year limits, a spillover occurrence may belong to a month whose complete six-week grid cannot be rendered. Its application callback still runs, but default navigation leaves selection unchanged, matching the day proxy's boundary behavior. -Replacing event input keeps package-owned focus on the same day or event occurrence when it still exists. If a focused event disappears, focus returns to its owning day; focus outside the calendar is not moved. +Replacing or refetching event input keeps package-owned focus on the same day or event occurrence when its replacement remains available for focus. An event preserved visually while focused across a container-size change may become hidden after a data rerender; focus then moves to the day's available action, normally its total-count button in count presentation, or falls back to the day proxy. If a focused event disappears, focus returns to its owning day. Focus outside the calendar is not moved. `eventTimeDisplay` controls visual time exposure without removing information from assistive technology. For `"grid"`, `"agenda"`, and `"none"`, a suppressed surface still contains its native `

      ` ### Month grid and day states -The grid is always seven equal logical columns and six week rows. Equal week-row sizing is the default; content sizing preserves six rows but lets their intrinsic heights differ. Outside-month days use the surface tone and muted ink. Out-of-range days remain visible at `0.55` opacity but disabled and expose no event action. Day numbers align to the logical end on wide layouts and center in compact layouts while remaining at the block start in either presentation. +The grid is always seven equal logical columns and six week rows. Equal week-row sizing is the default; content sizing preserves six rows but lets their intrinsic heights differ. Outside-month days use the surface tone and muted ink. Out-of-range days remain visible at `0.55` opacity but disabled and expose no event action. Day numbers align to the logical end on wide layouts and center in compact layouts while remaining at the block start in either presentation. Compact date-number decoration stays within the cell width, including with enlarged text; the full-cell day button retains its independent interaction target. -Today is a primary-filled circular number with its own border. Selection colors the whole cell and adds an inset primary outline. Keyboard focus uses the violet ring and takes visual precedence over the selected outline. Hover and press preview the selected palette only on eligible, unselected, non-Today day-number circles; selection and agenda behavior follow the [interaction model](ACCESSIBILITY.md#interaction-model). +Today is a primary-filled date number with its own border and the contained rounded geometry defined above. Selection colors the whole cell and adds an inset primary outline. Keyboard focus uses the violet ring and takes visual precedence over the selected outline. Hover and press preview the selected palette only on eligible, unselected, non-Today date-number decoration; selection and agenda behavior follow the [interaction model](ACCESSIBILITY.md#interaction-model). ### Events and agenda -Grid summaries use the event palette, logical-leading accent rule, marker slot, and ellipsized text. Total-count buttons use the same event palette with a visible border and no leading accent. Compact count buttons retain the full compact control target; wide counts display localized text such as “3 events.” The [responsive model](#responsive-model) defines when count and individual-event presentations apply. +Grid summaries use the event palette, logical-leading accent rule, marker slot, and ellipsized text. Total-count buttons use the same event palette with a visible rounded border and no leading accent. Compact count buttons retain the compact control target and compact-count typography while allowing intrinsic growth for wrapped text. Their keyboard focus ring is inset within the button, stays inside the owning day cell, and preserves the target size. Its thickness is capped at 3 CSS pixels so text enlargement does not crowd the count. Wide counts display localized text such as “3 events.” The [responsive model](#responsive-model) defines when count and individual-event presentations apply. The legacy passive compact overflow cue remains a bare semibold number. The package owns placement according to `gridEventPlacement`; render hooks replace only noninteractive visual content within the assigned block. `renderEventOverflow` exposes `display: "count" | "overflow"` in addition to the compact/wide variant. Count contexts set `visibleEventCount: 0` and `overflowCount: eventCount`. Mandatory actions keep their fallback for `null` or invalid output. The [interaction model](ACCESSIBILITY.md#interaction-model) defines application override and focus ownership. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 3ae0a12..f7b56c9 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -43,7 +43,7 @@ Maintainers are expected to: - Triage issues and pull requests, enforce the documented public, accessibility, security, and package contracts, and explain material tradeoffs. - Require repository checks and relevant manual evidence before merge; maintainer-authored changes follow the same gate as community changes. -- Follow the [alpha release operations runbook](docs/release-operations.md), [release policy](docs/releasing.md), and [release administration guide](docs/release-administration.md) for publication work. +- Follow the [nightly release operations runbook](docs/release-operations.md), [release policy](docs/releasing.md), and [release administration guide](docs/release-administration.md) for publication work. - Handle suspected vulnerabilities through [SECURITY.md](SECURITY.md) and keep sensitive reports out of public issues. - Keep [CODEOWNERS](.github/CODEOWNERS), team membership, and repository or registry access aligned with current responsibilities. @@ -51,7 +51,7 @@ Maintainers are expected to: Community input and evidence are welcome. Eligible maintainers make final technical, security, governance, and release decisions. Material decisions -should preserve documented alpha guarantees and explain important tradeoffs in +should preserve documented public guarantees and explain important tradeoffs in issues or pull requests. Code of Conduct reports follow the separate assignment and recusal rules below. diff --git a/docs/assets/calendar-anatomy.svg b/docs/assets/calendar-anatomy.svg index 41fd887..c62549a 100644 --- a/docs/assets/calendar-anatomy.svg +++ b/docs/assets/calendar-anatomy.svg @@ -9,7 +9,7 @@ synchronized; do not turn private lfc-* selectors into consumer API. --> Litefold Calendar and event anatomy - An annotated schematic identifies the month title, day button and date, selected day, grid event summary or action, compact social-style overflow count, wide overflow action, selected-day agenda, event leading rule, marker slot, leading content, time, title, details, and trailing content. A color key distinguishes the primary interface color, event leading-rule color, and per-event marker color. + An annotated schematic shows wide event summaries beside the default compact total-count button. It identifies the month title, day button and date, selected day, grid event summary or action, compact total-count action, wide overflow action, selected-day agenda, event leading rule, marker slot, leading content, time, title, details, and trailing content. A color key distinguishes the primary interface color, event leading-rule color, and per-event marker color. @@ -156,11 +156,9 @@ 2 more - - - - - +3 + + + 4 @@ -187,12 +185,12 @@ Grid event summary/action - + - Compact overflow cue - Social-style count (+N) - + Compact total-count action + Default for multiple events + Wide overflow action diff --git a/docs/component-anatomy.md b/docs/component-anatomy.md index 48aebef..804253b 100644 --- a/docs/component-anatomy.md +++ b/docs/component-anatomy.md @@ -6,8 +6,9 @@ contexts; they are not names for private CSS selectors. [![Annotated wide and compact calendar cells showing day, event, and overflow anatomy](assets/calendar-anatomy.svg)](assets/calendar-anatomy.svg) -The diagram is a schematic. Responsive CSS can reflow or visually hide parts, -but it does not change their public names or ownership. +The diagram is a schematic showing wide event summaries beside the default +compact total-count presentation. Responsive CSS can reflow or visually hide +parts, but it does not change their public names or ownership. ## Calendar surface @@ -92,23 +93,34 @@ text, a shape, or another non-color cue. ## Event overflow **Event overflow** is the umbrella term for the day-level presentation used -when the cell cannot show every event summary. It is separate from an individual -event representation and from the day badge. +to show a total count or access occurrences beyond the visible summaries. It is +separate from an individual event representation and from the day badge. + +`gridEventDisplay` chooses the presentation independently for compact and wide +containers. By default, compact days with multiple occurrences show a total-count +button; wide days show capped individual summaries and an overflow action when +needed. The [API reference](api.md#configure-behavior-calendaroptions) owns +all modes, defaults, and count rules. | Variant | What users see | Action ownership | | --- | --- | --- | -| Compact overflow cue | A social-style count such as `+3`, paired with the compact primary event marker/action, or an unsigned total such as `4` when no marker is visible. | Usually passive with `elements.action === null`. When the overflow button itself is the compact primary control, such as with `maxGridEventsPerDay: 0`, `elements.action` is that native button. | -| Wide overflow action | Localized text such as `3 more` after the visible grid summaries. | Always presented inside the native overflow button exposed as `elements.action`. | - -Both applicable variants are created during a render. Container CSS chooses -which one is visible; resizing does not rerun `renderEventOverflow`. In compact -layout with equal week rows, package-owned, normally visible primary-event, -count, and compact-primary overflow roots use one common full slot. Paired roots -may share a row or stack while retaining that size. Content-sized weeks keep -compact roots intrinsic, and later event summaries exposed only on focus remain -intrinsic in either mode. A markerless total or action-backed fallback uses one -centered block. The configured `gridEventPlacement` moves the complete summaries -area, not either compact block independently. +| Compact total-count action | A bordered button containing a localized total such as `4`. | `display: "count"`; `elements.action` is the native button. Visual content is decorative; the action announces the exact total and date. | +| Wide total-count action | A bordered button containing localized text such as `4 events`. | `display: "count"`; the native button is shared with the applicable compact presentation. | +| Compact overflow cue in `"events"` mode | A signed additional count such as `+3`, paired with the compact primary event marker/action, or an unsigned total such as `4` when no marker is visible. | `display: "overflow"`; usually passive with `elements.action === null`. When the overflow button itself is the compact primary control, such as with `maxGridEventsPerDay: 0`, `elements.action` is that native button. | +| Wide overflow action in `"events"` mode | Localized text such as `3 more` after the visible grid summaries. | `display: "overflow"`; always presented inside the native overflow button exposed as `elements.action`. | + +All applicable variants are created during a render. Container CSS chooses +which summaries and variant are visible; resizing does not rerun +`renderEventOverflow`. An already focused action stays visible until blur. +Normally visible compact primary events and overflow cues can share a row or +stack, while total counts use one centered button. Equal week rows give count +buttons a square minimum with rounded borders; wrapped or enlarged count text +can make them taller. Content-sized weeks keep these buttons intrinsically +sized without that square minimum. Both modes preserve target size and allow +text to wrap without shrinking the count typography. Later event summaries +exposed only on focus remain intrinsic in either mode. The configured +`gridEventPlacement` moves the complete summaries area. See the +[responsive design](../DESIGN.md#responsive-model) for the geometry contract. Every overflow context exposes: @@ -117,9 +129,15 @@ Every overflow context exposes: - `elements.action`: the native overflow button, or `null` for a passive compact cue. - `variant`: `"compact"` or `"wide"`; use this instead of measuring the viewport. +- `display`: `"count"` for a total or `"overflow"` for remaining occurrences. - `eventCount`, `visibleEventCount`, and `overflowCount`: authoritative counts. - `text`: the package-formatted visual fallback for that variant. +Count contexts set `visibleEventCount: 0` and `overflowCount: eventCount`. +`null` retains built-in content inside a native action; it can suppress only a +passive compact cue. Count actions therefore remain available after visual +customization. + Return a detached, noninteractive node from `renderEventOverflow` to customize the content. The package retains placement, target geometry, accessible naming, activation, and focus behavior. Fit compact output within its assigned slot, or @@ -127,6 +145,12 @@ increase `--lfc-control-min-size` and `--lfc-grid-event-min-block-size` so norma package-owned roots grow together. Oversized output remains unclipped but opts out of equal visual sizing. +Activating a count or overflow action invokes `onEventOverflowActivate` before +the default selection and agenda focus transfer. Synchronous cancellation lets +the application own its event chooser and focus restoration. See the +[accessibility interaction model](../ACCESSIBILITY.md#interaction-model) for +the exact callback and focus responsibilities. + ## Three color roles that sound similar The public names retain *accent* for compatibility, but the following terms are @@ -152,7 +176,9 @@ a CSS-token override. Prefer the full terms above instead of saying only | Add content immediately after the title | `renderEventDetails` | | Add content after all other event content | `renderEventTrailing` | | Add a decorative day badge | `renderDayBadge` | -| Replace compact and wide overflow visuals | `renderEventOverflow`, branching on `context.variant` | +| Choose total counts or individual summaries | `gridEventDisplay` | +| Replace compact and wide count/overflow visuals | `renderEventOverflow`, branching on `context.variant` and `context.display` | +| Open an application-owned event chooser | `onEventOverflowActivate` with synchronous cancellation | | Observe or carefully decorate a live day or event representation | `dayDidMount` or `eventDidMount`, with synchronous cleanup | `renderEventMarker` and `renderEventOverflow` are singleton presentation hooks: diff --git a/docs/fullcalendar-v6-migration.md b/docs/fullcalendar-v6-migration.md index d6c8f95..993a7fe 100644 --- a/docs/fullcalendar-v6-migration.md +++ b/docs/fullcalendar-v6-migration.md @@ -9,11 +9,11 @@ Litefold Calendar is not a FullCalendar compatibility layer. Check the [feature Add Litefold Calendar to the application: ```sh -npm install @tryagaindev/litefold-calendar@alpha +npm install @tryagaindev/litefold-calendar@nightly ``` -Use the movable `alpha` tag only to evaluate the current candidate. For a -repeatable migration or production-like environment, replace `alpha` with the +Use the movable `nightly` tag only to evaluate the current snapshot. For a +repeatable migration or production-like environment, replace `nightly` with the exact prerelease reported by the registry and save that exact version. Remove the FullCalendar packages and plugins after the rewrite no longer imports them. Unlike FullCalendar v6, Litefold Calendar requires an explicit stylesheet import. diff --git a/docs/integration-guide.md b/docs/integration-guide.md index 349e5c1..2a01b8d 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -140,6 +140,54 @@ requests from viewport or gesture state. The [event-source contract](api.md#supply-events-calendarevents-and-calendareventsource) owns invocation, range, cancellation, replacement, and commit behavior. +### Yield during expensive response adaptation + +If profiling shows that adapting a large validated response blocks interaction, +yield between records inside the already asynchronous event source. This can +improve responsiveness while adding elapsed time. Keep the simple mapping above +for small responses such as the five-record remote-data fixture. The **8 ms** +budget below is a starting point to profile and tune on your target devices. + +```ts +async function adaptLargeResponse( + records: readonly ApplicationRecord[], + signal: AbortSignal +): Promise[]> { + signal.throwIfAborted(); + const WORK_BUDGET_MS = 8; + const events: CalendarEventInput[] = []; + let deadline = performance.now() + WORK_BUDGET_MS; + + for (const record of records) { + events.push(toCalendarInput(record)); + if (events.length < records.length && performance.now() >= deadline) { + if (typeof globalThis.scheduler?.yield === "function") { + await globalThis.scheduler.yield(); + } else { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + signal.throwIfAborted(); + deadline = performance.now() + WORK_BUDGET_MS; + } + } + + return events; +} +``` + +After the provider fetches and validates its response, replace +`return records.map(toCalendarInput)` with `return adaptLargeResponse(records, signal)`. +The helper returns the whole array only after adaptation succeeds; an adapter +failure or cancellation rejects the request without publishing a partial result. +The provider's signal is checked before work and after every yield. + +Feature detection uses [`scheduler.yield()`](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield) +when available and a timer fallback otherwise. A yield cannot preempt one adapter +call or split JSON parsing, sorting, or DOM layout into smaller operations; profile +those separately. See [Optimize long tasks](https://web.dev/articles/optimize-long-tasks) +for the scheduling tradeoffs. This application-side helper preserves the core's +synchronous local-array behavior. + ## Bound one calendar instance Use independently optional, inclusive civil-date bounds when product policy limits navigation or selection: diff --git a/docs/remote-data.md b/docs/remote-data.md index 2b59014..81f39df 100644 --- a/docs/remote-data.md +++ b/docs/remote-data.md @@ -117,3 +117,6 @@ calendar.destroy(); ``` Close application dialogs and release their listeners too. The [runnable entry point](../examples/remote-data/main.js) demonstrates cleanup on non-cached `pagehide`, native dialog dismissal, and returning focus after a request has replaced the originating event or count button. + +If profiling later finds expensive adaptation of large responses, see the optional +[yielding adapter](integration-guide.md#yield-during-expensive-response-adaptation). diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md index 45c97b1..e2e879d 100644 --- a/docs/screenshots/README.md +++ b/docs/screenshots/README.md @@ -10,7 +10,7 @@ The manifest covers exactly these PNG files: |---|---|---| | `desktop-month-grid-1440x900.png` | Wide advanced month grid with direct event actions and overflow | 1440 × 900 | | `month-year-jump-1280x800.png` | Open native month-and-year chooser at a bounded month | 1280 × 800 | -| `mobile-month-agenda-dark-390x844.png` | Compact dark navigation with the abbreviated `Aug 2026` title, month-grid event summaries, and selected-day agenda | 390 × 844 | +| `mobile-month-agenda-dark-390x844.png` | Compact dark navigation with the abbreviated `Aug 2026` title, a single-event marker, total-count buttons, and selected-day agenda | 390 × 844 | | `mobile-month-swipe-pull-390x844.png` | Held native touch pull with the abbreviated `Aug 2026` toolbar and decorative `Sep 2026` adjacent-month lane | 390 × 844 | | `event-details-dark-1280x800.png` | Application-owned event details dialog opened from the agenda | 1280 × 800 | | `grid-event-keyboard-focus-1440x900.png` | Visible keyboard focus on a wide-layout grid event action after F2 | 1440 × 900 | diff --git a/examples/fullcalendar-v6-migration/README.md b/examples/fullcalendar-v6-migration/README.md index 152ec46..f193a5b 100644 --- a/examples/fullcalendar-v6-migration/README.md +++ b/examples/fullcalendar-v6-migration/README.md @@ -10,7 +10,7 @@ Follow the shared [local run instructions](../README.md#run-locally), then choos ## Choose a package version -Use `npm install @tryagaindev/litefold-calendar@alpha` only for evaluation against the moving prerelease channel. After selecting a version for migration or production-like validation, install that exact prerelease version and commit the resulting lockfile so later channel movement cannot change the tested package. +Use `npm install @tryagaindev/litefold-calendar@nightly` only for evaluation against the moving prerelease channel. After selecting a version for migration or production-like validation, install that exact prerelease version and commit the resulting lockfile so later channel movement cannot change the tested package. ## Follow the migration flow From d461815e95a178354c9754e498d671481ca67948 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 20:44:47 -0700 Subject: [PATCH 14/22] test: extract month-picker fixture helpers --- tests/calendar-boundaries.test.ts | 54 +------------------------------ tests/helpers/month-picker.ts | 54 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 53 deletions(-) create mode 100644 tests/helpers/month-picker.ts diff --git a/tests/calendar-boundaries.test.ts b/tests/calendar-boundaries.test.ts index c14690f..bf669b3 100644 --- a/tests/calendar-boundaries.test.ts +++ b/tests/calendar-boundaries.test.ts @@ -14,6 +14,7 @@ import { isPopoverOpen, waitFor } from "./helpers/dom.js"; +import { getMonthYearPicker } from "./helpers/month-picker.js"; void test("spillover day clicks stop quietly at supported month boundaries", async (context) => { const { dom, host } = setupDom(context); @@ -839,23 +840,6 @@ interface TestDom { readonly host: HTMLElement; } -interface MonthYearPicker { - readonly cancel: HTMLButtonElement; - readonly heading: HTMLHeadingElement; - readonly jump: HTMLButtonElement; - readonly month: HTMLSelectElement; - readonly popover: HTMLElement; - readonly trigger: HTMLButtonElement; - readonly year: HTMLInputElement; -} - -interface MonthYearPickerLabels { - readonly cancel: string; - readonly jump: string; - readonly month: string; - readonly year: string; -} - function setupDom( context: TestContext, markup = '
      ' @@ -875,35 +859,6 @@ async function waitForPhase( await waitFor(() => calendar.getState().phase === phase, `${phase} calendar state`); } -function getMonthYearPicker( - host: HTMLElement, - labels: MonthYearPickerLabels = { - cancel: "Cancel", - jump: "Jump", - month: "Month", - year: "Year" - } -): MonthYearPicker { - const heading = host.querySelector(".lfc-calendar-title"); - const trigger = heading?.querySelector("button"); - const popover = host.querySelector('[popover="auto"][role="dialog"]'); - const month = popover?.querySelector("select"); - const year = popover?.querySelector('input[type="number"]'); - assert.ok(heading, "Expected the calendar month heading to exist."); - assert.ok(trigger, "Expected the month heading to contain a native button."); - assert.ok(popover, "Expected the automatic month-and-year popover to exist."); - assert.ok(month, "Expected the month chooser to use a native select."); - assert.ok(year, "Expected the year chooser to use a numeric input."); - assert.equal(getControlLabel(popover, month), labels.month); - assert.equal(getControlLabel(popover, year), labels.year); - const buttons = [...popover.querySelectorAll("button")]; - const jump = buttons.find((button) => button.textContent?.trim() === labels.jump); - const cancel = buttons.find((button) => button.textContent?.trim() === labels.cancel); - assert.ok(jump, `Expected a ${labels.jump} button in the month-and-year popover.`); - assert.ok(cancel, `Expected a ${labels.cancel} button in the month-and-year popover.`); - return { cancel, heading, jump, month, popover, trigger, year }; -} - function getAccessibleName(element: HTMLElement): string { const labelledBy = element.getAttribute("aria-labelledby")?.trim().split(/\s+/u) ?? []; const referenced = labelledBy @@ -922,13 +877,6 @@ function getAccessibleName(element: HTMLElement): string { return element.getAttribute("aria-label")?.trim() ?? ""; } -function getControlLabel(root: HTMLElement, control: HTMLElement): string { - const label = [...root.querySelectorAll("label")] - .find((candidate) => candidate.htmlFor === control.id || candidate.contains(control)); - assert.ok(label, `Expected a label for ${control.localName}.`); - return label.querySelector(":scope > span")?.textContent?.trim() ?? label.textContent?.trim() ?? ""; -} - function getGrid(host: HTMLElement): HTMLElement { const grid = host.querySelector("[role='grid']"); assert.ok(grid); diff --git a/tests/helpers/month-picker.ts b/tests/helpers/month-picker.ts new file mode 100644 index 0000000..06d8025 --- /dev/null +++ b/tests/helpers/month-picker.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; + +interface MonthYearPicker { + readonly cancel: HTMLButtonElement; + readonly heading: HTMLHeadingElement; + readonly jump: HTMLButtonElement; + readonly month: HTMLSelectElement; + readonly popover: HTMLElement; + readonly trigger: HTMLButtonElement; + readonly year: HTMLInputElement; +} + +interface MonthYearPickerLabels { + readonly cancel: string; + readonly jump: string; + readonly month: string; + readonly year: string; +} + +export function getMonthYearPicker( + host: HTMLElement, + labels: MonthYearPickerLabels = { + cancel: "Cancel", + jump: "Jump", + month: "Month", + year: "Year" + } +): MonthYearPicker { + const heading = host.querySelector(".lfc-calendar-title"); + const trigger = heading?.querySelector("button"); + const popover = host.querySelector('[popover="auto"][role="dialog"]'); + const month = popover?.querySelector("select"); + const year = popover?.querySelector('input[type="number"]'); + assert.ok(heading, "Expected the calendar month heading to exist."); + assert.ok(trigger, "Expected the month heading to contain a native button."); + assert.ok(popover, "Expected the automatic month-and-year popover to exist."); + assert.ok(month, "Expected the month chooser to use a native select."); + assert.ok(year, "Expected the year chooser to use a numeric input."); + assert.equal(getControlLabel(popover, month), labels.month); + assert.equal(getControlLabel(popover, year), labels.year); + const buttons = [...popover.querySelectorAll("button")]; + const jump = buttons.find((button) => button.textContent?.trim() === labels.jump); + const cancel = buttons.find((button) => button.textContent?.trim() === labels.cancel); + assert.ok(jump, `Expected a ${labels.jump} button in the month-and-year popover.`); + assert.ok(cancel, `Expected a ${labels.cancel} button in the month-and-year popover.`); + return { cancel, heading, jump, month, popover, trigger, year }; +} + +function getControlLabel(root: HTMLElement, control: HTMLElement): string { + const label = [...root.querySelectorAll("label")] + .find((candidate) => candidate.htmlFor === control.id || candidate.contains(control)); + assert.ok(label, `Expected a label for ${control.localName}.`); + return label.querySelector(":scope > span")?.textContent?.trim() ?? label.textContent?.trim() ?? ""; +} From 6745e4fd79aa78c951ceb999fd2f819983084b12 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 21:02:45 -0700 Subject: [PATCH 15/22] test: verify rendered styles and current overflow contracts --- scripts/advanced-smoke/contracts.mjs | 44 +----- scripts/advanced-smoke/landing.mjs | 3 +- scripts/advanced-smoke/ordered-scenarios.mjs | 139 +---------------- scripts/advanced-smoke/overflow.mjs | 148 +++++++++++++++++++ scripts/run-advanced-smoke.mjs | 4 +- tests/e2e/container-modernization.spec.js | 113 ++++++++++++++ 6 files changed, 274 insertions(+), 177 deletions(-) create mode 100644 scripts/advanced-smoke/overflow.mjs diff --git a/scripts/advanced-smoke/contracts.mjs b/scripts/advanced-smoke/contracts.mjs index 6629616..fe51fd2 100644 --- a/scripts/advanced-smoke/contracts.mjs +++ b/scripts/advanced-smoke/contracts.mjs @@ -7,51 +7,9 @@ import { composeStyles } from "../lib/styles.mjs"; const EXAMPLE_DIRECTORY = join(REPOSITORY_ROOT, "examples", "advanced"); -export async function verifyAdvancedStyleContracts() { +export async function verifyAdvancedThemeTokenCoverage() { const packageStyles = await composeStyles(); const exampleStyles = await readFile(join(EXAMPLE_DIRECTORY, "theme.css"), "utf8"); - const narrowAgendaStart = packageStyles.indexOf("@container lfc-calendar (inline-size < 24rem)"); - const narrowToolbarStart = packageStyles.indexOf("@container lfc-calendar (inline-size <= 20rem)"); - assert.ok( - narrowAgendaStart >= 0 && narrowToolbarStart > narrowAgendaStart, - "Expected ordered narrow-container rules." - ); - const narrowAgendaStyles = packageStyles.slice(narrowAgendaStart, narrowToolbarStart); - assert.match( - narrowAgendaStyles, - /\.lfc-calendar-event-title\s*\{[^}]*\bgrid-column:\s*1\s*\/\s*-1;[^}]*\bgrid-row:\s*2;/u, - "Expected narrow agenda titles to wrap across the full row." - ); - assert.equal( - [...narrowAgendaStyles.matchAll(/\.lfc-calendar-event-(?:details|trailing)\s*\{[^}]*\bgrid-column:\s*1\s*\/\s*-1;/gu)].length, - 2, - "Expected narrow agenda details and trailing content to wrap across the full row." - ); - assert.match( - packageStyles, - /\.lfc-calendar-more\s*\{[^}]*\bpadding-inline:\s*0\.25rem;/u, - "Expected the grid overflow row to preserve an inline text inset." - ); - assert.match( - packageStyles, - /\.lfc-calendar-event-summary\s*\{[^}]*\bborder-radius:\s*var\(--lfc-border-radius\);/u, - "Expected grid summaries to use the public component radius." - ); - assert.match( - packageStyles, - /\.lfc-calendar-agenda-event\s*\{[^}]*\bborder-radius:\s*var\(--lfc-border-radius\);/u, - "Expected agenda rows to use the public component radius." - ); - assert.match( - packageStyles, - /:dir\(rtl\)\s+\.lfc-calendar-navigation-icon\s*\{[^}]*\btransform:\s*scaleX\(-1\);/u, - "Expected built-in navigation icons to mirror with semantic RTL direction." - ); - assert.match( - exampleStyles, - /\.my-calendar:dir\(rtl\)\s+\.my-navigation-icon\s*\{[^}]*\btransform:\s*scaleX\(-1\);/u, - "Expected custom navigation icons to mirror with semantic RTL direction." - ); const publicTokens = new Set( [...packageStyles.matchAll(/(--lfc-(?!internal-)[a-z0-9-]+)\s*:/gu)] .map((match) => match[1]) diff --git a/scripts/advanced-smoke/landing.mjs b/scripts/advanced-smoke/landing.mjs index fbe3e74..7269247 100644 --- a/scripts/advanced-smoke/landing.mjs +++ b/scripts/advanced-smoke/landing.mjs @@ -15,7 +15,8 @@ const EXPECTED_EXAMPLE_PATHS = Object.freeze([ "basic/", "classic-script/", "fullcalendar-v6-migration/", - "progressive-enhancement/" + "progressive-enhancement/", + "remote-data/" ]); const LANDING_MODULE = join(EXAMPLES_DIRECTORY, "index.js"); diff --git a/scripts/advanced-smoke/ordered-scenarios.mjs b/scripts/advanced-smoke/ordered-scenarios.mjs index a8c07af..8599cf6 100644 --- a/scripts/advanced-smoke/ordered-scenarios.mjs +++ b/scripts/advanced-smoke/ordered-scenarios.mjs @@ -10,6 +10,7 @@ import { requireSelectedDay, waitFor } from "./helpers.mjs"; +import { verifyAdvancedOverflowPresentation } from "./overflow.mjs"; const APPOINTMENT_ID = "appointment:41"; const APPOINTMENT_TITLE = "Design review"; @@ -169,7 +170,8 @@ export async function runAdvancedSmokeScenarios(environment) { ".lfc-calendar-title-button", dom.window.HTMLButtonElement ); - assert.equal(titleButton.textContent, "August 2026"); + assert.equal(titleButton.querySelector(".lfc-calendar-title-label-full")?.textContent, "August 2026"); + assert.equal(titleButton.querySelector(".lfc-calendar-title-label-compact")?.textContent, "Aug 2026"); assert.equal( titleButton.getAttribute("aria-label"), "Choose schedule month and year, currently August 2026" @@ -360,7 +362,7 @@ export async function runAdvancedSmokeScenarios(environment) { const selectedGridActions = [...selectedCell.querySelectorAll( '[data-test-event-surface="grid-summary"], .lfc-calendar-grid-more' )]; - assert.ok(selectedGridActions.length > 0, "Expected visible grid actions for the selected day."); + assert.ok(selectedGridActions.length > 0, "Expected prepared grid actions for the selected day."); assert.equal( selectedGridActions.every((action) => action.getAttribute("tabindex") === "-1"), true, @@ -397,132 +399,7 @@ export async function runAdvancedSmokeScenarios(environment) { 2, "Expected maxGridEventsPerDay to cap visual summaries." ); - const compactOverflow = requireElement( - selectedCell, - ":scope .lfc-calendar-event-overflow-cluster " + - "> .lfc-calendar-event-overflow.lfc-is-compact", - dom.window.HTMLSpanElement - ); - assert.equal( - compactOverflow.getAttribute("aria-hidden"), - "true", - "The compact event-overflow number must remain outside the accessibility tree." - ); - assert.equal( - compactOverflow.classList.contains("lfc-has-custom-event-overflow"), - true, - "Expected the unified hook to expose its custom compact state." - ); - const compactOverflowContent = requireElement( - compactOverflow, - ":scope > .lfc-calendar-event-overflow-content " + - "> .my-event-overflow-compact", - dom.window.HTMLSpanElement - ); - assert.equal(compactOverflowContent.textContent, "+52"); - assert.deepEqual( - { - actionBacked: compactOverflowContent.dataset["testActionBacked"], - date: compactOverflowContent.dataset["testDate"], - eventCount: compactOverflowContent.dataset["testEventCount"], - overflowCount: compactOverflowContent.dataset["testOverflowCount"], - surface: compactOverflowContent.dataset["testSurface"], - variant: compactOverflowContent.dataset["testVariant"], - visibleEventCount: compactOverflowContent.dataset["testVisibleEventCount"] - }, - { - actionBacked: "false", - date: "2026-08-06", - eventCount: "53", - overflowCount: "52", - surface: "day", - variant: "compact", - visibleEventCount: "1" - }, - "Expected the compact branch to receive the authoritative adaptive count context." - ); - assert.ok( - selectedCell.querySelector( - '[data-test-event-surface="grid-summary"] .my-event-marker' - ) instanceof dom.window.HTMLSpanElement, - "The compact event-overflow number must coexist with a custom event marker." - ); - const selectedOverflowAction = requireElement( - selectedCell, - ":scope .lfc-calendar-grid-more", - dom.window.HTMLButtonElement - ); - const wideOverflow = requireElement( - selectedOverflowAction, - ":scope > .lfc-calendar-event-overflow.lfc-is-wide", - dom.window.HTMLSpanElement - ); - const wideOverflowContent = requireElement( - wideOverflow, - ":scope > .lfc-calendar-event-overflow-content", - dom.window.HTMLSpanElement - ); - const customOverflowContent = requireElement( - wideOverflowContent, - ":scope > .my-event-overflow-wide", - dom.window.HTMLSpanElement - ); - assert.equal( - wideOverflowContent.querySelector(".lfc-event-overflow-default-content"), - null, - "Expected custom wide DOM to replace only the variant's package visual." - ); - assert.equal(wideOverflow.getAttribute("aria-hidden"), "true"); - assert.equal(customOverflowContent.ownerDocument, document); - assert.equal(customOverflowContent.textContent, "51 additionalin agenda"); - assert.equal( - customOverflowContent.querySelector( - ":scope > .my-event-overflow-wide-count" - )?.textContent, - "51 additional" - ); - assert.equal( - customOverflowContent.querySelector( - ":scope > .my-event-overflow-wide-destination" - )?.textContent, - "in agenda" - ); - assert.deepEqual( - { - actionBacked: customOverflowContent.dataset["testActionBacked"], - date: customOverflowContent.dataset["testDate"], - eventCount: customOverflowContent.dataset["testEventCount"], - overflowCount: customOverflowContent.dataset["testOverflowCount"], - surface: customOverflowContent.dataset["testSurface"], - variant: customOverflowContent.dataset["testVariant"], - visibleEventCount: customOverflowContent.dataset["testVisibleEventCount"] - }, - { - actionBacked: "true", - date: "2026-08-06", - eventCount: "53", - overflowCount: "51", - surface: "grid-summary", - variant: "wide", - visibleEventCount: "2" - }, - "Expected the wide branch to receive the authoritative adaptive count context." - ); - assert.equal( - customOverflowContent.querySelector("a, button, input, select, textarea, [tabindex]"), - null, - "Custom overflow content must remain noninteractive." - ); - assert.equal( - wideOverflow.classList.contains("lfc-has-custom-event-overflow"), - true, - "Expected the wide variant root to expose its custom-content state." - ); - assert.match( - selectedOverflowAction.getAttribute("aria-label") ?? "", - /^View 51 more items for /u, - "Custom visual content must not replace the native overflow action's localized name." - ); + verifyAdvancedOverflowPresentation({ document, dom, selectedCell }); selectedDay.dispatchEvent(new dom.window.KeyboardEvent("keydown", { bubbles: true, @@ -556,7 +433,7 @@ export async function runAdvancedSmokeScenarios(environment) { ); previousDate.click(); await waitFor(() => selectedDate.textContent === "2026-08-05", "the overflow test setup selection"); - const actionBeforeOverflow = actionResult.textContent; + assert.equal(actionResult.textContent, "Selected 2026-08-05 with click on button."); const representedDay = requireElement( host, '[role="gridcell"] > button[data-lfc-date="2026-08-06"]', @@ -573,8 +450,8 @@ export async function runAdvancedSmokeScenarios(environment) { await waitFor(() => selectedDate.textContent === "2026-08-06", "the native grid overflow action"); assert.equal( actionResult.textContent, - actionBeforeOverflow, - "Custom overflow content must not change the native action or call onDaySelect." + "Viewing 53 items on 2026-08-06 with click on button.", + "The overflow callback must report the full count; default selection must not invoke onDaySelect." ); const agendaHeadingId = agenda.getAttribute("aria-labelledby"); assert.notEqual(agendaHeadingId, null); diff --git a/scripts/advanced-smoke/overflow.mjs b/scripts/advanced-smoke/overflow.mjs new file mode 100644 index 0000000..0ec62a5 --- /dev/null +++ b/scripts/advanced-smoke/overflow.mjs @@ -0,0 +1,148 @@ +import assert from "node:assert/strict"; + +import { requireElement } from "./helpers.mjs"; + +export function verifyAdvancedOverflowPresentation({ document, dom, selectedCell }) { + const selectedOverflowAction = requireElement( + selectedCell, + ":scope .lfc-calendar-grid-more", + dom.window.HTMLButtonElement + ); + assert.equal(selectedCell.querySelectorAll(".lfc-calendar-grid-more").length, 1, + "Compact counts and wide overflow must share one native action."); + const compactOverflow = requireElement( + selectedOverflowAction, + ":scope > .lfc-calendar-event-overflow.lfc-is-compact", + dom.window.HTMLSpanElement + ); + assert.equal( + compactOverflow.getAttribute("aria-hidden"), + "true", + "The compact total visual must remain outside the accessibility tree of its named action." + ); + assert.equal( + compactOverflow.classList.contains("lfc-has-custom-event-overflow"), + true, + "Expected the unified hook to expose its custom compact state." + ); + const compactOverflowContent = requireElement( + compactOverflow, + ":scope > .lfc-calendar-event-overflow-content " + + "> .my-event-overflow-compact", + dom.window.HTMLSpanElement + ); + assert.equal(compactOverflowContent.textContent, "53"); + assert.equal(compactOverflowContent.ownerDocument, document); + assert.equal( + compactOverflow.querySelector(".lfc-event-overflow-default-content"), + null, + "Expected custom compact DOM to replace only the variant's package visual." + ); + assert.equal( + compactOverflowContent.querySelector("a, button, input, select, textarea, [tabindex]"), + null, + "Custom count content must remain noninteractive inside the package-owned action." + ); + assert.deepEqual( + { + actionBacked: compactOverflowContent.dataset["testActionBacked"], + date: compactOverflowContent.dataset["testDate"], + display: compactOverflowContent.dataset["testDisplay"], + eventCount: compactOverflowContent.dataset["testEventCount"], + overflowCount: compactOverflowContent.dataset["testOverflowCount"], + surface: compactOverflowContent.dataset["testSurface"], + variant: compactOverflowContent.dataset["testVariant"], + visibleEventCount: compactOverflowContent.dataset["testVisibleEventCount"] + }, + { + actionBacked: "true", + date: "2026-08-06", + display: "count", + eventCount: "53", + overflowCount: "53", + surface: "day", + variant: "compact", + visibleEventCount: "0" + }, + "Expected the compact branch to receive the authoritative adaptive count context." + ); + assert.ok( + selectedCell.querySelector( + '[data-test-event-surface="grid-summary"] .my-event-marker' + ) instanceof dom.window.HTMLSpanElement, + "Pre-rendered wide event summaries must retain their custom markers." + ); + const wideOverflow = requireElement( + selectedOverflowAction, + ":scope > .lfc-calendar-event-overflow.lfc-is-wide", + dom.window.HTMLSpanElement + ); + const wideOverflowContent = requireElement( + wideOverflow, + ":scope > .lfc-calendar-event-overflow-content", + dom.window.HTMLSpanElement + ); + const customOverflowContent = requireElement( + wideOverflowContent, + ":scope > .my-event-overflow-wide", + dom.window.HTMLSpanElement + ); + assert.equal( + wideOverflowContent.querySelector(".lfc-event-overflow-default-content"), + null, + "Expected custom wide DOM to replace only the variant's package visual." + ); + assert.equal(wideOverflow.getAttribute("aria-hidden"), "true"); + assert.equal(customOverflowContent.ownerDocument, document); + assert.equal(customOverflowContent.textContent, "51 additionalin agenda"); + assert.equal( + customOverflowContent.querySelector( + ":scope > .my-event-overflow-wide-count" + )?.textContent, + "51 additional" + ); + assert.equal( + customOverflowContent.querySelector( + ":scope > .my-event-overflow-wide-destination" + )?.textContent, + "in agenda" + ); + assert.deepEqual( + { + actionBacked: customOverflowContent.dataset["testActionBacked"], + date: customOverflowContent.dataset["testDate"], + display: customOverflowContent.dataset["testDisplay"], + eventCount: customOverflowContent.dataset["testEventCount"], + overflowCount: customOverflowContent.dataset["testOverflowCount"], + surface: customOverflowContent.dataset["testSurface"], + variant: customOverflowContent.dataset["testVariant"], + visibleEventCount: customOverflowContent.dataset["testVisibleEventCount"] + }, + { + actionBacked: "true", + date: "2026-08-06", + display: "overflow", + eventCount: "53", + overflowCount: "51", + surface: "grid-summary", + variant: "wide", + visibleEventCount: "2" + }, + "Expected the wide branch to receive the authoritative adaptive count context." + ); + assert.equal( + customOverflowContent.querySelector("a, button, input, select, textarea, [tabindex]"), + null, + "Custom overflow content must remain noninteractive." + ); + assert.equal( + wideOverflow.classList.contains("lfc-has-custom-event-overflow"), + true, + "Expected the wide variant root to expose its custom-content state." + ); + assert.equal( + selectedOverflowAction.getAttribute("aria-label") ?? "", + "View 53 items for Thursday, August 6, 2026, 51 additional", + "The shared action must retain the full count and date plus the wide visible label." + ); +} diff --git a/scripts/run-advanced-smoke.mjs b/scripts/run-advanced-smoke.mjs index 7e0e3ff..0ad0455 100644 --- a/scripts/run-advanced-smoke.mjs +++ b/scripts/run-advanced-smoke.mjs @@ -1,10 +1,10 @@ -import { verifyAdvancedStyleContracts } from "./advanced-smoke/contracts.mjs"; +import { verifyAdvancedThemeTokenCoverage } from "./advanced-smoke/contracts.mjs"; import { createAdvancedSmokeEnvironment } from "./advanced-smoke/environment.mjs"; import { verifyExamplesLandingPage } from "./advanced-smoke/landing.mjs"; import { runAdvancedSmokeScenarios } from "./advanced-smoke/ordered-scenarios.mjs"; import { verifyRecipeExamples } from "./advanced-smoke/recipes.mjs"; -await verifyAdvancedStyleContracts(); +await verifyAdvancedThemeTokenCoverage(); await verifyExamplesLandingPage(); const environment = await createAdvancedSmokeEnvironment(); diff --git a/tests/e2e/container-modernization.spec.js b/tests/e2e/container-modernization.spec.js index 09f3ccc..c1f0c4f 100644 --- a/tests/e2e/container-modernization.spec.js +++ b/tests/e2e/container-modernization.spec.js @@ -52,6 +52,109 @@ for (const [name, features] of [ }); } +for (const fallback of [false, true]) { + test(`built-in and custom navigation icons follow direction (${fallback ? "direction fallback" : "native"})`, async ({ page }) => { + if (fallback) { await disableModernLayoutFeatures(page, ["dir"]); } + await expectLibraryFixtureReady(page); + await page.addStyleTag({ url: "/examples/advanced/theme.css" }); + await page.evaluate(async () => { + const { createCalendar } = await import("/dist/index.js"); + for (const custom of [false, true]) { + const host = document.createElement("div"); + host.id = custom ? "my-custom-navigation" : "my-native-navigation"; + host.className = "my-calendar"; + document.body.append(host); + const createIcon = (ownerDocument, text) => { + const icon = ownerDocument.createElement("span"); + icon.className = "my-navigation-icon"; + icon.dir = "ltr"; + icon.setAttribute("aria-hidden", "true"); + icon.textContent = text; + return icon; + }; + createCalendar(host, { + events: [], initialDate: "2026-08-06", + ...(custom ? { icons: { + next: (ownerDocument) => createIcon(ownerDocument, "\u2192"), + previous: (ownerDocument) => createIcon(ownerDocument, "\u2190") + } } : {}) + }).render(); + } + }); + for (const id of ["my-native-navigation", "my-custom-navigation"]) { + const host = page.locator(`#${id}`); + const icons = host.locator(".lfc-calendar-navigation-icon, .my-navigation-icon"); + await expect(icons).toHaveCount(2); + const originalIcons = await host.evaluateHandle((element) => + [...element.querySelectorAll(".lfc-calendar-navigation-icon, .my-navigation-icon")]); + const previous = host.getByRole("button", { name: "Previous month", exact: true }); + await previous.focus(); + for (const direction of ["ltr", "rtl", "ltr"]) { + await host.evaluate((element, value) => { element.dir = value; }, direction); + if (fallback) { await expect(host).toHaveAttribute("data-lfc-direction", direction); } + await expectNavigationMirroring(icons, direction); + await expect(previous).toBeFocused(); + expect(await host.evaluate((element, originals) => + [...element.querySelectorAll(".lfc-calendar-navigation-icon, .my-navigation-icon")] + .every((icon, index) => icon === originals[index]), originalIcons)).toBe(true); + } + await originalIcons.dispose(); + } + }); +} + +test("event rows honor live radius tokens, overflow insets and narrow agenda spans", async ({ page }) => { + await expectLibraryFixtureReady(page); + await page.evaluate(async () => { + const { createCalendar } = await import("/dist/index.js"); + const host = document.querySelector("[data-my-calendar]"); + host.style.inlineSize = "900px"; + createCalendar(host, { + events: [ + { id: "first", title: "Calendar review", start: "2026-08-06T09:00" }, + { id: "second", title: "Follow-up meeting", start: "2026-08-06T10:00" } + ], + initialDate: "2026-08-06", + gridEventDisplay: { compact: "events", wide: "events" }, + maxGridEventsPerDay: 1, + renderHooks: [{ + id: "style-contract-content", + renderEventDetails: ({ document }) => document.createTextNode("Confirmed"), + renderEventTrailing: ({ document }) => document.createTextNode("View details") + }] + }).render(); + }); + const host = page.locator("[data-my-calendar]"); + const summary = host.locator(".lfc-calendar-event-summary").first(); + const agendaEvent = host.locator(".lfc-calendar-agenda-event").first(); + for (const radius of ["16px", "24px"]) { + await host.evaluate((element, value) => { element.style.setProperty("--lfc-border-radius", value); }, radius); + await expect(summary).toHaveCSS("border-radius", radius); + await expect(agendaEvent).toHaveCSS("border-radius", radius); + } + const overflow = host.locator(".lfc-calendar-more").first(); + await expect(overflow).toBeVisible(); + await expect.poll(() => overflow.evaluate((element) => { + const style = getComputedStyle(element); + return Number.parseFloat(style.paddingInlineStart) > 0 && style.paddingInlineStart === style.paddingInlineEnd; + })).toBe(true); + await host.evaluate((element) => { element.style.inlineSize = "320px"; }); + for (const direction of ["ltr", "rtl"]) { + await host.evaluate((element, value) => { element.dir = value; }, direction); + await expect.poll(() => agendaEvent.evaluate((element) => { + const box = (selector) => element.querySelector(selector).getBoundingClientRect(); + const leading = box(".lfc-calendar-event-leading"); + const time = box(".lfc-calendar-time"); + const rows = ["title", "details", "trailing"].map((slot) => box(`.lfc-calendar-event-${slot}`)); + return rows.every((row, index) => row.width > 0 && row.height > 0 && + row.left <= Math.min(leading.left, time.left) + 1 && + row.right >= Math.max(leading.right, time.right) - 1 && + row.top >= (index === 0 ? Math.max(leading.bottom, time.bottom) : rows[index - 1].bottom) - 1); + })).toBe(true); + await expectNoOverflow(agendaEvent); + } +}); + test("direction fallback follows nested ancestor changes without replacing focused controls", async ({ page }) => { await disableModernLayoutFeatures(page); await mountCalendars(page); @@ -69,12 +172,15 @@ test("direction fallback follows nested ancestor changes without replacing focus }); await count.focus(); await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "ltr"); + await expectNavigationMirroring(host.locator(".lfc-calendar-navigation-icon"), "ltr"); await host.evaluate((host) => { host.parentElement.removeAttribute("dir"); }); await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "rtl"); + await expectNavigationMirroring(host.locator(".lfc-calendar-navigation-icon"), "rtl"); await expect(host.locator(".lfc-calendar-swipe-lane-previous")).toHaveCSS("order", "3"); await expect(count).toBeFocused(); await host.evaluate((host) => { host.parentElement.style.direction = "ltr"; }); await expect(host.locator(".lfc-calendar-grid")).toHaveCSS("direction", "ltr"); + await expectNavigationMirroring(host.locator(".lfc-calendar-navigation-icon"), "ltr"); await expect(host.locator(".lfc-calendar-swipe-lane-previous")).toHaveCSS("order", "1"); await expect(count).toBeFocused(); await host.evaluate((host) => { host.dir = "rtl"; }); @@ -175,6 +281,13 @@ function countFor(host) { return host.locator(`.lfc-calendar-grid-more[data-lfc-date="${TARGET_DATE}"]`); } +async function expectNavigationMirroring(icons, direction) { + await expect.poll(() => icons.evaluateAll((elements) => elements.map((element) => { + const transform = getComputedStyle(element).transform; + return transform === "none" ? 1 : new DOMMatrixReadOnly(transform).a; + }))).toEqual(direction === "rtl" ? [-1, -1] : [1, 1]); +} + async function expectNoOverflow(locator) { await expect.poll(() => locator.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1); } From 805c173004a137aa15da10386de4c11cea06d78f Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 21:31:55 -0700 Subject: [PATCH 16/22] test: share recipe coverage across validation tools --- scripts/advanced-smoke/landing.mjs | 11 ++-------- scripts/lib/example-routes.mjs | 10 +++++++++ scripts/tests/serve-repository.test.mjs | 11 ++-------- tests/e2e/developer-pages.spec.js | 29 ++++++++++++++----------- tests/e2e/remote-data.spec.js | 5 ++++- 5 files changed, 34 insertions(+), 32 deletions(-) create mode 100644 scripts/lib/example-routes.mjs diff --git a/scripts/advanced-smoke/landing.mjs b/scripts/advanced-smoke/landing.mjs index 7269247..b30dd53 100644 --- a/scripts/advanced-smoke/landing.mjs +++ b/scripts/advanced-smoke/landing.mjs @@ -6,18 +6,11 @@ import { pathToFileURL } from "node:url"; import { JSDOM } from "jsdom"; import { parseExampleMetadata } from "../lib/example-metadata.mjs"; +import { EXAMPLE_ROUTES } from "../lib/example-routes.mjs"; import { REPOSITORY_ROOT } from "../lib/process.mjs"; const EXAMPLES_DIRECTORY = join(REPOSITORY_ROOT, "examples"); -const EXPECTED_EXAMPLE_PATHS = Object.freeze([ - "advanced/", - "async-errors/", - "basic/", - "classic-script/", - "fullcalendar-v6-migration/", - "progressive-enhancement/", - "remote-data/" -]); +const EXPECTED_EXAMPLE_PATHS = EXAMPLE_ROUTES.map(({ route }) => route.slice("/examples/".length)).sort(); const LANDING_MODULE = join(EXAMPLES_DIRECTORY, "index.js"); function installGlobal(name, value, descriptors) { diff --git a/scripts/lib/example-routes.mjs b/scripts/lib/example-routes.mjs new file mode 100644 index 0000000..b0cb084 --- /dev/null +++ b/scripts/lib/example-routes.mjs @@ -0,0 +1,10 @@ +/** Independent expected recipe routes shared by validation tooling, never generated from the landing HTML. */ +export const EXAMPLE_ROUTES = Object.freeze([ + { name: "advanced", route: "/examples/advanced/" }, + { name: "asynchronous errors", route: "/examples/async-errors/" }, + { name: "basic", route: "/examples/basic/" }, + { name: "classic script", route: "/examples/classic-script/" }, + { name: "migration", route: "/examples/fullcalendar-v6-migration/" }, + { name: "progressive enhancement", route: "/examples/progressive-enhancement/" }, + { name: "remote data", route: "/examples/remote-data/" } +].map((recipe) => Object.freeze(recipe))); diff --git a/scripts/tests/serve-repository.test.mjs b/scripts/tests/serve-repository.test.mjs index ada7c73..b5f6360 100644 --- a/scripts/tests/serve-repository.test.mjs +++ b/scripts/tests/serve-repository.test.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { request } from "node:http"; import test from "node:test"; +import { EXAMPLE_ROUTES } from "../lib/example-routes.mjs"; import { startRepositoryServer } from "../serve-repository.mjs"; const EXPECTED_SECURITY_HEADERS = Object.freeze({ @@ -11,14 +12,6 @@ const EXPECTED_SECURITY_HEADERS = Object.freeze({ "referrer-policy": "no-referrer", "x-content-type-options": "nosniff" }); -const EXAMPLE_ROUTES = Object.freeze([ - "/examples/advanced/", - "/examples/async-errors/", - "/examples/basic/", - "/examples/classic-script/", - "/examples/fullcalendar-v6-migration/", - "/examples/progressive-enhancement/" -]); function requestWithHost(origin, host) { const url = new URL(origin); @@ -65,7 +58,7 @@ void test("repository server routes the landing page without weakening its bound assert.match(await landing.text(), /Examples \| Litefold Calendar<\/title>/u); assertSecurityHeaders(landing); - for (const route of EXAMPLE_ROUTES) { + for (const { route } of EXAMPLE_ROUTES) { const example = await fetch(`${server.origin}${route}`); assert.equal(example.status, 200, `Expected a runnable example route: ${route}`); assert.match(example.headers.get("content-type") ?? "", /^text\/html/u); diff --git a/tests/e2e/developer-pages.spec.js b/tests/e2e/developer-pages.spec.js index 6309f02..0f20f3d 100644 --- a/tests/e2e/developer-pages.spec.js +++ b/tests/e2e/developer-pages.spec.js @@ -2,18 +2,11 @@ import { readFile } from "node:fs/promises"; import { expect, test } from "@playwright/test"; +import { EXAMPLE_ROUTES } from "../../scripts/lib/example-routes.mjs"; import { expectNoAutomatedAccessibilityViolations } from "./helpers.js"; const COMMIT = "0123456789abcdef0123456789abcdef01234567"; const RELEASE_VERSION = "0.2.0-alpha.0"; -const EXAMPLE_ROUTES = Object.freeze([ - ["basic", "/examples/basic/"], - ["advanced", "/examples/advanced/"], - ["asynchronous errors", "/examples/async-errors/"], - ["classic script", "/examples/classic-script/"], - ["migration", "/examples/fullcalendar-v6-migration/"], - ["progressive enhancement", "/examples/progressive-enhancement/"] -]); const PAGES_INDEX = new URL("../../scripts/pages-site/index.html", import.meta.url); const PAGES_MARK = new URL("../../docs/assets/litefold-calendar-mark.svg", import.meta.url); const PAGES_SCRIPT = new URL("../../scripts/pages-site/site.js", import.meta.url); @@ -128,21 +121,31 @@ function contrastRatio(first, second) { (Math.min(firstLuminance, secondLuminance) + 0.05); } -for (const [name, route] of EXAMPLE_ROUTES) { +for (const { name, route } of EXAMPLE_ROUTES) { test(`${name} recipe loads and passes automated accessibility checks`, async ({ page }, testInfo) => { - const response = await page.goto(route, { waitUntil: "domcontentloaded" }); + const feed = route === "/examples/remote-data/" + ? page.waitForResponse((response) => new URL(response.url()).pathname === `${route}events.json`) + : Promise.resolve(null); + const [response, feedResponse] = await Promise.all([ + page.goto(route, { waitUntil: "domcontentloaded" }), feed + ]); expect(response?.ok(), `Expected ${route} to return a successful response.`).toBe(true); const host = page.locator(".litefold-calendar").first(); await expect(host).toBeVisible(); + if (feedResponse !== null) { + expect(feedResponse.ok(), "Expected the remote recipe's event feed to load successfully.").toBe(true); + await feedResponse.finished(); + await expect(host.getByRole("grid").getByRole("button", { name: /^View 3 events for /u })).toBeVisible(); + } await expect(host).not.toHaveAttribute("aria-busy", "true"); await expectNoAutomatedAccessibilityViolations(page, testInfo); }); } -test("examples landing exposes seven keyboard-visible task cards including remote data", async ({ page }, testInfo) => { +test("examples landing exposes the expected keyboard-visible task cards including remote data", async ({ page }, testInfo) => { const response = await page.goto("/examples/", { waitUntil: "domcontentloaded" }); expect(response?.ok()).toBe(true); - await expect(page.locator(".my-card")).toHaveCount(7); + await expect(page.locator(".my-card")).toHaveCount(EXAMPLE_ROUTES.length); const remoteData = page.getByRole("link", { exact: true, name: "Load and filter remote events" }); await expect(remoteData).toBeVisible(); await expect(remoteData).toHaveAttribute("href", "./remote-data/"); @@ -186,7 +189,7 @@ test.describe("large-text developer-page reflow", () => { await page.setViewportSize({ height: 844, width: 320 }); const response = await page.goto("/examples/", { waitUntil: "domcontentloaded" }); expect(response?.ok()).toBe(true); - await expect(page.locator(".my-card")).toHaveCount(7); + await expect(page.locator(".my-card")).toHaveCount(EXAMPLE_ROUTES.length); await expect(page.locator("[data-my-metadata-state]")) .toHaveAttribute("data-my-metadata-state", "ready"); await page.addStyleTag({ content: "html { font-size: 150%; }" }); diff --git a/tests/e2e/remote-data.spec.js b/tests/e2e/remote-data.spec.js index bd86aef..7554613 100644 --- a/tests/e2e/remote-data.spec.js +++ b/tests/e2e/remote-data.spec.js @@ -1,5 +1,7 @@ import { expect, test } from "@playwright/test"; +import { expectNoAutomatedAccessibilityViolations } from "./helpers.js"; + const EXAMPLE = "/examples/remote-data/"; const FEED = "**/examples/remote-data/events.json?*"; @@ -27,13 +29,14 @@ test("remote example fetches the fixture, filters, refreshes, and distinguishes await expect(page.getByText("No events", { exact: true })).toBeVisible(); }); -test("remote example opens a complete day chooser and restores focus", async ({ page }) => { +test("remote example opens a complete day chooser and restores focus", async ({ page }, testInfo) => { await openExample(page); const count = page.getByRole("grid").getByRole("button", { name: /^View 3 events for /u }); await count.click(); const dialog = page.getByRole("dialog", { name: "Events for 2026-08-06" }); await expect(dialog).toBeVisible(); await expect(dialog.getByRole("button", { name: /^Choose /u })).toHaveCount(3); + await expectNoAutomatedAccessibilityViolations(page, testInfo); await dialog.getByRole("button", { name: "Choose Print workshop", exact: true }).click(); await expect(dialog).not.toBeVisible(); await expect(page.getByRole("status").filter({ hasText: "Selected Print workshop" })).toContainText("No server data was changed."); From 0633725dc9262d52b8d0d3fb5ba4a63b72b6c55f Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:09:49 -0700 Subject: [PATCH 17/22] test: isolate native pager browser fixtures --- tests/e2e/swipe-gestures.spec.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/e2e/swipe-gestures.spec.js b/tests/e2e/swipe-gestures.spec.js index 02f0f45..4384746 100644 --- a/tests/e2e/swipe-gestures.spec.js +++ b/tests/e2e/swipe-gestures.spec.js @@ -213,9 +213,8 @@ function pagingLaneLabels(host, direction) { test.describe("native month pager", () => { test.use({ reducedMotion: "no-preference" }); - test("renders hidden semantic lanes around one live grid and settles the current snap", async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); - const host = page.locator(HOST_SELECTOR); + test("renders hidden semantic lanes around one live grid and settles the current snap", { tag: "@firefox-regression" }, async ({ page }) => { + const host = await mountCalendarFixture(page); const viewport = host.locator(VIEWPORT_SELECTOR); await expect(host).toHaveAttribute("data-lfc-swipe-enabled", "true"); const semantics = await host.evaluate((element) => { @@ -319,7 +318,9 @@ test.describe("native month pager", () => { }); expect((await readPagerMetrics(host)).maximum).toBeGreaterThan(0); await setPagerPosition(host, "next", true, 0.2); - await expect(page.locator(MONTH_SELECTOR)).toHaveText("2026-08-01"); + await expect.poll(() => page.evaluate(() => + window.__lfcSwipeFixture.calendar.getState().displayedMonth + )).toEqual({ day: 1, month: 8, year: 2026 }); await expectPagerClean(host); await expect(viewport).not.toHaveAttribute("aria-hidden"); }); @@ -550,15 +551,16 @@ test.describe("native month pager", () => { await expect(actionResult).toHaveText(initialActionResult ?? ""); }); - test("trusted wheel pages horizontally and leaves vertical wheel unhandled", async ({ page }) => { - await expectExampleReady(page, "/examples/advanced/"); - const host = page.locator(HOST_SELECTOR); + test("trusted wheel pages horizontally and leaves vertical wheel unhandled", { tag: "@firefox-regression" }, async ({ page }) => { + const host = await mountCalendarFixture(page); let point = await gesturePoint(page.locator( '.lfc-calendar-day-button[data-lfc-date="2026-08-13"]' ), 16); await page.mouse.move(point.x, point.y); await page.mouse.wheel(200, 0); - await expect(page.locator(MONTH_SELECTOR)).toHaveText("2026-09-01"); + await expect.poll(() => page.evaluate(() => + window.__lfcSwipeFixture.calendar.getState().displayedMonth + )).toEqual({ day: 1, month: 9, year: 2026 }); await expectPagerClean(host); const viewport = host.locator(VIEWPORT_SELECTOR); @@ -586,7 +588,9 @@ test.describe("native month pager", () => { defaultPrevented: false, isTrusted: true }); - await expect(page.locator(MONTH_SELECTOR)).toHaveText("2026-09-01"); + await expect.poll(() => page.evaluate(() => + window.__lfcSwipeFixture.calendar.getState().displayedMonth + )).toEqual({ day: 1, month: 9, year: 2026 }); await expectPagerClean(host); }); From b5d31aa233bc3b3f83f0fe6c10dc936f878ef084 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:26:44 -0700 Subject: [PATCH 18/22] fix: resolve Vite example styles directly from source --- scripts/tests/vite-build.test.mjs | 32 +++++++++++++++++++++++++------ vite.examples.config.mjs | 26 ++++++++++++++----------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/scripts/tests/vite-build.test.mjs b/scripts/tests/vite-build.test.mjs index 42bdc64..25bc4b8 100644 --- a/scripts/tests/vite-build.test.mjs +++ b/scripts/tests/vite-build.test.mjs @@ -1,13 +1,15 @@ import assert from "node:assert/strict"; -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; +import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; import test from "node:test"; -import { createServer } from "vite"; +import { createServer, normalizePath } from "vite"; import { createLibraryConfig } from "../../vite.config.mjs"; import examplesConfig, { sourceExamplesPlugin } from "../../vite.examples.config.mjs"; import { REPOSITORY_ROOT } from "../lib/process.mjs"; +import { composeStyles } from "../lib/styles.mjs"; void test("Vite preserves public ESM entries, shared chunks, and standalone CSS", async () => { const config = await createLibraryConfig(); @@ -24,11 +26,17 @@ void test("Vite preserves public ESM entries, shared chunks, and standalone CSS" assert.equal(tsconfig.compilerOptions.declarationMap, true); }); -void test("Vite examples resolve package imports and style requests to source", async () => { +void test("Vite examples resolve source modules and refresh styles without prebuilt output", async () => { + const root = await mkdtemp(join(tmpdir(), "lfc-vite-examples-")); + assert.deepEqual(await readdir(root), [], "The served root must start without dist or other generated files."); + const plugin = sourceExamplesPlugin(); const server = await createServer({ ...examplesConfig, configFile: false, - server: { middlewareMode: true, watch: null }, + root, + plugins: [plugin], + cacheDir: join(root, ".vite"), + server: { middlewareMode: true, watch: null, fs: { allow: [root, REPOSITORY_ROOT] } }, optimizeDeps: { noDiscovery: true } }); try { @@ -37,11 +45,23 @@ void test("Vite examples resolve package imports and style requests to source", const extension = await server.transformRequest("/dist/extensions/webmcp/index.js"); assert.match(extension.code, /webMcp/u); const styles = await server.transformRequest("/dist/styles.css?direct"); - assert.match(styles.code, /@layer lfc/u); + assert.equal(styles.code, await composeStyles(), "Direct CSS must come from the current source composition."); + const importedStyles = await server.transformRequest("/dist/styles.css"); + assert.match(importedStyles.code, /__vite__updateStyle/u); + const directModule = await server.moduleGraph.getModuleByUrl("/dist/styles.css?direct"); + const importedModule = await server.moduleGraph.getModuleByUrl("/dist/styles.css"); + assert.ok(directModule.transformResult); + assert.ok(importedModule.transformResult); + plugin.handleHotUpdate({ file: normalizePath(join(REPOSITORY_ROOT, "src", "styles", "tokens.css")), server }); + assert.equal(directModule.transformResult, null, "Source edits must invalidate direct stylesheet requests."); + assert.equal(importedModule.transformResult, null, "Source edits must invalidate CSS module requests."); + assert.equal((await server.transformRequest("/dist/styles.css?direct")).code, await composeStyles()); const advanced = await server.transformIndexHtml("/examples/advanced/index.html", '<script type="module" src="./main.js"></script>'); assert.match(advanced, /src="\.\/main\.ts"/u); } finally { await server.close(); + assert.equal(dirname(root), resolve(tmpdir()), "Remove only the temporary root created by this test."); + await rm(root, { recursive: true, force: true }); } }); diff --git a/vite.examples.config.mjs b/vite.examples.config.mjs index ca00988..31721fc 100644 --- a/vite.examples.config.mjs +++ b/vite.examples.config.mjs @@ -1,6 +1,6 @@ import { dirname, join, resolve } from "node:path"; -import { defineConfig } from "vite"; +import { defineConfig, normalizePath } from "vite"; import { REPOSITORY_ROOT } from "./scripts/lib/process.mjs"; import { composeStyles } from "./scripts/lib/styles.mjs"; @@ -27,23 +27,26 @@ export function sourceExamplesPlugin() { }); }, resolveId(source, importer) { - const path = source.startsWith("/") - ? join(REPOSITORY_ROOT, source) - : importer === undefined ? null : resolve(dirname(importer), source); + //Preserve Vite flags that distinguish direct CSS from imported style modules. + const pathname = source.replace(/[?#].*$/u, ""); + const suffix = source.slice(pathname.length); + const path = pathname.startsWith("/") + ? join(REPOSITORY_ROOT, pathname) + : importer === undefined ? null : resolve(dirname(importer), pathname); if (path === join(REPOSITORY_ROOT, "dist", "styles.css")) { - return VIRTUAL_STYLES; + return `${VIRTUAL_STYLES}${suffix}`; } if (path === join(REPOSITORY_ROOT, "dist", "index.js")) { - return join(REPOSITORY_ROOT, "src", "index.ts"); + return `${join(REPOSITORY_ROOT, "src", "index.ts")}${suffix}`; } const extension = path?.replaceAll("\\", "/") .match(/\/dist\/extensions\/([a-z][a-z0-9-]*)\/index\.js$/u)?.[1]; if (extension !== undefined && path.startsWith(join(REPOSITORY_ROOT, "dist"))) { - return join(REPOSITORY_ROOT, "src", "extensions", extension, "index.ts"); + return `${join(REPOSITORY_ROOT, "src", "extensions", extension, "index.ts")}${suffix}`; } }, load(id) { - if (id === VIRTUAL_STYLES) { return composeStyles(); } + if (id.replace(/[?#].*$/u, "") === VIRTUAL_STYLES) { return composeStyles(); } }, transformIndexHtml: { order: "pre", @@ -54,9 +57,10 @@ export function sourceExamplesPlugin() { } }, handleHotUpdate(context) { - if (context.file.startsWith(join(REPOSITORY_ROOT, "src", "styles"))) { - const module = context.server.moduleGraph.getModuleById(VIRTUAL_STYLES); - if (module !== undefined) { context.server.moduleGraph.invalidateModule(module); } + if (normalizePath(context.file).startsWith(`${normalizePath(join(REPOSITORY_ROOT, "src", "styles"))}/`)) { + for (const module of context.server.moduleGraph.getModulesByFile(VIRTUAL_STYLES) ?? []) { + context.server.moduleGraph.invalidateModule(module); + } context.server.ws.send({ type: "full-reload" }); return []; } From 513245e63b7eb6ab7256b06f7336bf28e2b5ba4d Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:40:13 -0700 Subject: [PATCH 19/22] fix: contain enlarged compact date labels --- ACCESSIBILITY.md | 2 +- DESIGN.md | 2 +- docs/component-anatomy.md | 2 +- src/styles/responsive.css | 1 - tests/e2e/grid-event-display.spec.js | 50 ++++++++++++++++++++++++++-- 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md index cee2bcc..5e2c0a5 100644 --- a/ACCESSIBILITY.md +++ b/ACCESSIBILITY.md @@ -169,7 +169,7 @@ Manual test procedure (run the affected groups for a focused change and all grou - Run 280 CSS pixels separately as a best-effort robustness stress check. - Immediately below, at, and above a `24rem` calendar content width, confirm only the below-boundary toolbar and exposed pager text use the locale's abbreviated month with a numeric year. Confirm the full visual is used at the boundary and above, and that the full trigger, grid, and live names remain unchanged while pager lanes stay absent from the accessibility tree. - At representative supported phone widths and default text sizing, confirm the abbreviated month/year title stays on one line without horizontal overflow. For longer translations or enlarged text, confirm any visual ellipsis retains the complete accessible text. Test a long localized Today label: it may wrap or move to a following row, while the complete navigation targets remain separate, within the host, and in their documented focus order. - - At 320 CSS pixels and 200% text size, check multi-digit totals such as 12 and 123 in equal and content-sized weeks, in LTR and RTL. Confirm count text grows with the text setting, remains fully visible through wrapping and taller buttons when needed, and retains its exact accessible total. Confirm Today and other date-number decoration stay within their cells while day and count actions retain their target sizes. + - At 320 CSS pixels and 200% text size, check multi-digit totals such as 12 and 123 in equal and content-sized weeks, in LTR and RTL. Confirm count text grows with the text setting, remains fully visible through wrapping and taller buttons when needed, and retains its exact accessible total. Confirm Today and other date-number decoration stay within their cells; wrapped two-digit dates remain readable within the reserved date slot without overlapping count controls. Day and count actions retain their target sizes. - Follow the [responsive visual checks](DESIGN.md#dos-and-donts) for custom marker and count geometry. Confirm count buttons remain actionable while their visual hook content and passive `"events"` cues remain decorative. Focus a compact count button and confirm its inset ring remains visible inside the button and its owning day cell without reducing the target size. Across the compact/wide boundary, retain an already focused action until blur, then confirm F2 and arrow navigation skip CSS-hidden summaries. Complete accessible names and managed action order must remain unchanged, pointer activation must reach the documented target, and reflow must introduce no persistent two-dimensional scrolling. 7. **Application integration** - Repeat affected tasks with application toolbar, progressive fallback, and render-hook content enabled. diff --git a/DESIGN.md b/DESIGN.md index b691e9e..0468ff5 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -380,7 +380,7 @@ The default shape is a modest `0.5rem` radius for the outer shell, grid, agenda, - Day cells remain rectangular so seven columns read as a single ruled ledger. - Grid and agenda event slips use the same public radius as their containing panels; the tighter scale comes from their smaller height and padding rather than a one-off corner value. -- Day numbers prefer circular geometry within the available cell width; containment and readable enlarged text take precedence over a perfect circle. Previous/Next remain quiet rectangular controls with the system radius rather than introducing a second pill treatment. +- Day numbers prefer circular geometry within the available cell width; containment and readable enlarged text take precedence over a perfect circle. At narrow widths with enlarged text, two-digit dates may wrap inside a taller rounded shape within the reserved date slot. Previous/Next remain quiet rectangular controls with the system radius rather than introducing a second pill treatment. - Focus and selection use outlines, not shape changes. - Borders are generally one-CSS-pixel hairlines. Increased-contrast mode thickens the boundaries that carry state. - Built-in navigation icons are simple directional glyphs; the default event marker is a small filled SVG circle. No icon font, remote image, mascot, ornament, or decorative illustration belongs in core. diff --git a/docs/component-anatomy.md b/docs/component-anatomy.md index 804253b..ef6da7a 100644 --- a/docs/component-anatomy.md +++ b/docs/component-anatomy.md @@ -50,7 +50,7 @@ tracks share the tallest intrinsic height or size independently; it does not change this DOM structure. The phrase **day badge** means the `renderDayBadge` slot. Use **Today -indicator** for the circular treatment applied to today's day number, and use +indicator** for the rounded treatment applied to today's day number, and use **event marker** for the visual attached to an event. These are three different parts. diff --git a/src/styles/responsive.css b/src/styles/responsive.css index 7683653..3029edf 100644 --- a/src/styles/responsive.css +++ b/src/styles/responsive.css @@ -81,7 +81,6 @@ inline-size: 2rem; justify-self: center; max-inline-size: calc(100% + 2 * var(--lfc-internal-day-padding)); - min-block-size: min-content; min-inline-size: 0; padding: 0; } diff --git a/tests/e2e/grid-event-display.spec.js b/tests/e2e/grid-event-display.spec.js index d6c5348..8be39dd 100644 --- a/tests/e2e/grid-event-display.spec.js +++ b/tests/e2e/grid-event-display.spec.js @@ -178,6 +178,33 @@ test("count controls remain usable in a 320px RTL container with enlarged text", await expect(count).toBeFocused(); }); +test("wrapped date digits stay inside their day and above its count at 200% text", { tag: "@firefox-regression" }, async ({ page }) => { + await mountCalendar(page, { count: 12, width: 320 }); + await page.locator("[data-my-calendar]").evaluate(async (host) => { + //These local monospace fonts make two 32px digits wider than the compact badge's content box. + host.style.setProperty("--lfc-font-family", '"Courier New", "Liberation Mono", monospace'); + host.style.setProperty("--lfc-font-size", "1rem"); + document.documentElement.style.fontSize = "32px"; + await document.fonts.ready; + }); + const { count, day, grid } = targets(page); + await expect(day).toHaveAttribute("aria-current", "date"); + await day.focus(); + await day.press("F2"); + await expect(count).toBeFocused(); + await expectOnlyOneGridTabStop(grid); + const ordinary = await expectDayNumberContained(page, "2026-07-10", { wrapped: true }); + const today = await expectDayNumberContained(page, TARGET_DATE, { wrapped: true }); + expect(ordinary.fontSize).toBe(32); + expect(today.fontSize).toBe(32); + const countBox = await count.boundingBox(); + expect(countBox).not.toBeNull(); + expect(today.badge.bottom, "today badge stays above its count").toBeLessThanOrEqual(countBox.y + 1); + for (const line of today.lines) { + expect(line.bottom, "today text stays above its count").toBeLessThanOrEqual(countBox.y + 1); + } +}); + for (const total of [12, 123]) { for (const enlarged of [false, true]) { for (const rtl of [false, true]) { @@ -321,7 +348,7 @@ function expectRectInside(inner, outer, description) { expect(inner.bottom, `${description}: bottom`).toBeLessThanOrEqual(outer.bottom + tolerance); } -async function expectDayNumberContained(page, date) { +async function expectDayNumberContained(page, date, { wrapped = false } = {}) { const number = page.locator(`.lfc-calendar-day-button[data-lfc-date="${date}"] .lfc-calendar-day-number`); await expect(number).toHaveText(String(Number(date.slice(-2)))); const geometry = await number.evaluate((badge) => { @@ -331,12 +358,30 @@ async function expectDayNumberContained(page, date) { }; const range = document.createRange(); range.selectNodeContents(badge); + const lines = [...range.getClientRects()].map(({ left, top, right, bottom }) => ({ left, top, right, bottom })); + const characters = []; + const walker = document.createTreeWalker(badge, NodeFilter.SHOW_TEXT); + while (walker.nextNode()) { + for (let offset = 0; offset < walker.currentNode.textContent.length; offset += 1) { + range.setStart(walker.currentNode, offset); + range.setEnd(walker.currentNode, offset + 1); + characters.push(rect(range)); + } + } return { badge: rect(badge), cell: rect(badge.closest(".lfc-calendar-day")), - lines: [...range.getClientRects()].map(({ left, top, right, bottom }) => ({ left, top, right, bottom })) + characters, + fontSize: Number.parseFloat(getComputedStyle(badge).fontSize), + lines }; }); + if (wrapped) { + expect(geometry.characters, `${date} has two measured digits`).toHaveLength(2); + expect(geometry.characters[1].top, `${date} digits occupy separate lines`).toBeGreaterThan( + geometry.characters[0].top + geometry.fontSize / 2 + ); + } expectRectInside(geometry.badge, geometry.cell, `${date} badge inside its day`); expect(geometry.lines.length).toBeGreaterThan(0); for (const line of geometry.lines) { @@ -345,6 +390,7 @@ async function expectDayNumberContained(page, date) { expect(line.left, `${date} text inside badge left`).toBeGreaterThanOrEqual(geometry.badge.left - 1); expect(line.right, `${date} text inside badge right`).toBeLessThanOrEqual(geometry.badge.right + 1); } + return geometry; } async function mountCalendar(page, settings = {}) { From fe7c8c09a8e4febd050bef7cf943cd5c85ed63f7 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:43:12 -0700 Subject: [PATCH 20/22] test: use valid static layout and marker fixtures --- tests/e2e/container-modernization.spec.js | 40 ++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/e2e/container-modernization.spec.js b/tests/e2e/container-modernization.spec.js index c1f0c4f..81e3eaa 100644 --- a/tests/e2e/container-modernization.spec.js +++ b/tests/e2e/container-modernization.spec.js @@ -249,8 +249,19 @@ test("published site sections reflow independently and leave the skip link fixed await page.setViewportSize({ width: 1200, height: 1000 }); await expectLibraryFixtureReady(page); const source = await readFile(new URL("../../scripts/pages-site/index.html", import.meta.url), "utf8"); - await page.setContent(source.replaceAll(/<script\b[^>]*>[\s\S]*?<\/script>/gu, "") - .replaceAll(/<link\b[^>]*>/gu, "")); + await page.evaluate((source) => { + //Import the site's owned layout sections from an inert document, preserving the minimal fixture head. + const parsed = new DOMParser().parseFromString(source, "text/html"); + const sections = [".my-pages-skip-link", ".my-pages-site-header", "#my-pages-content", ".my-pages-footer"] + .map((selector) => { + const section = parsed.querySelector(selector); + if (section === null || section.querySelector("script, link") !== null) { + throw new Error(`Expected one static site layout section without executable or stylesheet elements: ${selector}`); + } + return document.importNode(section, true); + }); + document.body.replaceChildren(...sections); + }, source); await addSourceStyles(page, ["scripts/pages-site/site.css"]); await page.addStyleTag({ content: ".my-pages-site-header, .my-pages-footer { inline-size: 320px; } body { min-block-size: 2000px; }" }); const header = page.locator(".my-pages-site-header"); @@ -321,7 +332,7 @@ async function mountCalendars(page) { await expectLibraryFixtureReady(page); await page.evaluate(async () => { const { createCalendar } = await import("/dist/index.js"); - const fixture = { calendars: [], requests: 0 }; + const fixture = { calendars: [], errors: [], requests: 0 }; for (const [name, width] of [["compact", 320], ["wide", 900]]) { const host = document.createElement("div"); host.id = `my-audit-${name}`; @@ -337,8 +348,18 @@ async function mountCalendars(page) { initialDate: "2026-08-06", locale: "en-US", now: () => new Date("2026-08-06T12:00:00Z"), + onError: (error) => { fixture.errors.push({ code: error.code, hook: error.hook, message: error.message }); }, onEventActivate: () => {}, - renderHooks: [{ id: "audit-empty-marker", renderEventMarker: ({ event }) => event.id === "all-day" ? null : undefined }] + renderHooks: [{ + id: "audit-empty-marker", + renderEventMarker: ({ document: ownerDocument, event }) => { + if (event.id === "all-day") { return null; } + const marker = ownerDocument.createElement("span"); + marker.className = "my-audit-event-marker"; + marker.textContent = "\u2022"; + return marker; + } + }] }); calendar.render(); fixture.calendars.push(calendar); @@ -347,4 +368,15 @@ async function mountCalendars(page) { }); await expect(page.locator("#my-audit-compact")).not.toHaveAttribute("aria-busy", "true"); await expect(page.locator("#my-audit-wide")).not.toHaveAttribute("aria-busy", "true"); + expect(await page.evaluate(() => ({ + errors: window.containerAudit.errors, + phases: window.containerAudit.calendars.map((calendar) => calendar.getState().phase) + }))).toEqual({ errors: [], phases: ["ready", "ready"] }); + for (const id of ["my-audit-compact", "my-audit-wide"]) { + const host = page.locator(`#${id}`); + const emptyMarkers = host.locator('[data-lfc-event-id="all-day"] .lfc-calendar-event-marker'); + await expect(emptyMarkers).toHaveCount(2); + for (const marker of await emptyMarkers.all()) { await expect(marker).toBeEmpty(); } + await expect(host.locator('[data-lfc-event-id="timed"] .my-audit-event-marker')).toHaveText(["\u2022", "\u2022"]); + } } From 69377e41e890cfcdff3e04e5fb7b432dcf6718e8 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:43:12 -0700 Subject: [PATCH 21/22] chore: refresh reviewed screenshot evidence --- .../grid-event-keyboard-focus-1440x900.png | Bin 45418 -> 45498 bytes screenshots.manifest.json | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/screenshots/grid-event-keyboard-focus-1440x900.png b/docs/screenshots/grid-event-keyboard-focus-1440x900.png index 73cbfeeb898c1d46b2c9bc775e0666fc80cd113c..0e4230d9775604d2853f5a284d0b8b89b4b84f77 100644 GIT binary patch literal 45498 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�)nc1zTRs0gA6NEQ*0 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 C??Yn% literal 45418 zcmeFZWl)?=w>AnSKpqGr1Ph*o;6W1H2~Kc#2oT)eEd&b|f(3WC;4m-@3GVLhFnExG z8JPKQ^1gdl?X#;+olol2`Elk)Dl^@8->cW^UcJ_}t|n4NNd^yx90v^z4Np#1QVk6a zGY1Xr!IwuEz%O&`=%Hw6PtoKg-)nef94`MOrfI$bJL@HSY9248ORVu!u}hdB<@b9D z)*1!X{EUn21dDv@+=Rkv`53iSg`(J49{SQfos9z3w`y$7Co0V3d50Un^J;&z?=Hm* z;@2+cuPXEM!|Y&AH26#Bf};~#@S{bH63tXzM<b;ViXY@5Y0RQ7*JKX>QK)@MEQa+3 z=lB;F1s9uaW*5dmQn=_-Wzr8ROALMSsTD`t*2fza5j>TZZ-x{VqAC6X9zu(2#TMV% zC0!aCY5(DM=_j6-Z#ZZPzj!8g^m0wCvLEcqVAp$Wq6|Bj8_~S)FLo2qn)tNkWQ~P3 zoR`y{IcPzwBY44?g#~f`^AyEvUmICdbKfs$V2hM!2+<2ABQ$EP=O7e|5q=p&J_vT* zHAc;!jSpEDP~3MblLh(d{24Kx{o)8oPhkimr(F|c)oF6?ZLJ228~>||6ez;WbAko6 z0Ix)|i4J<e-LSi+iVD6{u;#-ChRA`ZrKaH@&%$;)dofiwf!W3nQ8ay@<OF#_U);ZA zE%(A}V$<fsK#aaFMhQ9w{#4)g7EzA^I?$#fL49OE_+9Ox%7gm{3+gWz7Mlp~pHg{1 zwb-DjH272ukBdMI?{#5O(@!qaf3@(i>KQ015{ltHb;^;hDk!Ajc6f8&l7khU#?>y~ z5u64nFJ^0Nh2(F4vv~u99E-h>dn?5ymYWb#w)tI96VAMj>P_BRekAf5ZB13C<6az% zd8Bv4chVBqaCDPW3^Wyat2|(p1<AytYNwt$sxDR;DG@cSZ=`yuvg={n%i=vlJLSv4 zy4M1=JJg2M=sa)GD-C2(U-dT1BXsK&9a^0B4^8;^x^`#;{*^bgB(Z|z$eY&fC<p%| z`tHTS65e=YXTO3rc=tC|VyZACzD&3bsiU2m-DugY8vjKYJoNWYscl4}U0&YJivPty zob0OhekxiH-_#oL%Ans{#6-ujD==TRKS@*d6&Xu-@}!A1-xJ@Lh(BC^d)L><ge#P{ zlO2b%3K?sH6p@7KpAs4K<NqL2tyi479?y)k!?mmQ72x{7=`C{6c^4c^Op}~Qr4YmX zw%SWE=K4lrw<SAO&1TRAU*ZFASeVfib;tF$JnjNnajN(|{FBmg@sqyI-S9+J2}vBm ztci5?ZWhPpo*}SM?p)Q^bPnHkw6e~LrJ?Te_R6|R*AOgn-gO_s%da$i_bQ!fpx|XU zl>49{2mbz@-W(n8op^9zsxF0$@BaG6-dKA~U8N?jD(71UrNEovK;u8C$!|#i!sSK| z-tynl_!1A63T=@Qnc-S6&l?eOP@iXD_w4&aW4c}(7mQa@4@zyN<I(~HV6gt+1aAEn z_UnMoPYjoS1fKlUysvj6QkZEO^Y7%LX)2l^RX^2(Z$C5?IdYZWKHE&7qIZmP)}|wJ z__YYblna0B=%*Y0mdgj${r#M~6-Qg@89IxZ7Hsxdm-tsn)&Q4pvP4WCt%m>RLZyat z^hMavr5gL?qMJpoV<~Ud;prpB)yfQmeNUx@h9RbN5jbR=7U2PVCg%V0-hy6ScRx{U zE%GTb(P^^%pHX7jJos11ngbX%^OYHt{6=zef+UE8U+R_utqn9w)p1@rzHwQ6{>Q%; zx=*y*Z6Y84_WYqQyVw1ttGqvwqVPwD*_;Z4Gq>J!ZqLnFahv$yLTFTL!bNq6uf8>v z^I3F#t;9`rikG+&%@U~sJBeRng^enSB80jSU&0X4$@(#;%qWaLKb|3haC3`2AUKV$ zvoJh7g0C7<%G}O`cqG-OK?7cRmFGT|81$6MJCgi_4x4LX?4g&e+RohXPH=fRqV5h6 z7SOOChX}klnKwkc{2T;qx;51VUxd<L4{KE{{v<;*Azo!@i#P5QSN12Kmy<CBtw`i3 zI|>e++bJr_OEXo<5V>5$wAMd+nt|26LBKp49pTYZqKL<Jr)5<pyXXDG#C)1b;<jgR zs=us^aNK0wq21J|fX8Os(NV2PEj;%)a?`YHrTo1M9=`h>?lf#;LN|?_i`BucUy3*> zmP0E$Qnzy0^i|Oa?wfLE?9-=35CM^k@?k}C-m~KwDKz&uC27DFP&4s5n1d0{d-4)k zU?I~I4x#FYk7@Q{Lluxf4;H~zJ&GMtq5xoJmd88v-38eQjQE*BRAz%z+?5FEzO0IC zV!hNhdI~KgoIW(Ii&m1JMrgw*m``l{B+qD>Se8{h-}%&Jbe>YJGko1SSSXfldvLeS zPM|)0Fsi{@pQ<l6jC<1Z>)1!%+J|%6U8}6rdSiTE5v{=jUcNbFcXzcy6Zk0Sn7|2G zL_z{Cj;pDye%oaz7;l_M)o&Gdn^lj?-!O~7KR21J-kPg6+86C9SyZoWavDxJh6`I! z9QJsaC)1Y6m>RYWP>#GqDGD`hII*zf!NdZSQ!WG^iTHdH1^#w&vG2n#&L2J=xE}ve zahP-+VwV6}*?IX^DoR6MU0y?+Y4=f-y(Sh!QFHEz3ai=e9kSIj8x3ldf6zpcQ_Upw zu4o7mQB{nF#z9F_WrUje_C74bVei)G43_D8HIjO}%Up%(2gOLhVV8fr49ZEZ^qcp5 zo?s?!T(n%Hx+$~x-<C{pGYn-xY}S1-ymT&p0fsq^xHdt(*OtMSDxmnzu)IJOb;ruQ z^D|pH2~G+Ao;K4nZX{&%;V})1TZ}Clrn9q4bB0<&kFnVBm?Y-?Qgk}%8Fo!!qp*Nb zY<s(JozCH=RIRpYCMDImxaf0rF>+va1?n;{2Z{$5aB2$#VyeJO{=Sy6*^REhPu82} zD2>b7Z{MaiMiV#qomY7;FFKkACO6>I(5NvZnM&DW2W;D3P`SQxpG8M&2>#dlccPPu zH`I6vn9BxNQR8n{7n%(G0bITXzIkIgCE&!HdX5g9BOUoK1YwPZ7>G4H6NjuM`I8W| zAU!eQY{DU|v+66c2SuSHto0$Oi>gBOf8Mc6R^S>EkOiKBpi~@9$4j<z6>D-5nF<3s z{N5)rG!>6UUFllId@N>aqhNw{tNzo2ZCf_~gtzN8ICKYJ+wx<U`8Vz#Q1J0yhh~v% zQ%d|^QBHAuEK1_ZDq|J<3w5VR<JI&Gkk(4}UiAYmrZNS{%8~lhK3wtsKhKo>8vGiK zA>Z}}W}*#w8;^2CN3Y=p^vmD2mLj6?)A`({q6i}Cv|{7DS`WtfZR1KO@#Kyh>Qw7O zvS_G_AUl^QA=$PR9IgSMGJuC*-rH*<aoGo=Jl9rAr4oISMqG{d9T~*q3vLqrk1d~_ z1J*+$zgH6D0FsI#%GlF*v2n9mrECx8zksEX4h}FjP}(M*s?LO{jeMuT7Jc;3=v2Bh z{Z`8pe9M#?i(ir7nnZ=PFD`6;$JBZ<RoOi@(!7lsj>M<t4bb=Q1qKholD=2;cZivO zqQyk#vZ8Q2A#s2Pqvm_=SzZTDi*blDO^lo{VS<m_xr?#w(dPtPTK9Yo1C-4^zCCDN zg!M3digR3s1aGX7Pfowa#xV;iVh>l)Z@I~^$^OSzOW}O%$f8#p5j8Ua4>-G)wI|;Y zaPjYY@{i?I`mL|`x2Q-o>L-X@ey`#3`gUR<+P!z4iL@xEFIT@eQgh_|xxsQ<4lzP6 zsUOgTKesQ|(Q)NSu()S@+<5l<_G`L_^%9x=V!~EQgC%zYt8sv5)zW^L+(FP<i(!4a zv<%ghQsW<ECjod}%bnpHO@y7zqS>*V=rT)Ia|Ny<Tg9X0;?#>D?VgsWV~snCp*{)t z!RT%s=u)zVoD03b$}d7XFo3=HVhs(gyKk|MMU9jH#aQ6lSU?)XO}y$`I<2IWTAM1P zm9g?LCvH3O)N?vAna))&?+BmxgFWx{e{OtOlywp+^qBblf;`TF=*ql319{uFC1Ly7 z7zTw!TUbL#?~1CUwHoLm@soF><EAPpxQ)O{r!Q6Uv)`)Ad)!)^&ZzO0QVG2}8H$Wb z^R_@r2saGc!|`qMGh<`M>(!i}^IJRh+x`BnpiPxu*ivHw9g1yWf-)gyMIX7520i0f zSSc#IbECINfILMGPED3tlE8taf*SK7C0Y*AEN!DT3&Az~CjHSW!2$@2znffL!&qr3 zuf~ueR61kY?Mr;1h>di|)|1hxl7pd&yK@je>v*80o`IXrHNUF<JN(0$>F(2`;z+8h zqCF6SyHg#L-u!ZYh{wAln<B>1vk35l<6?VpxCP=mpMi^W82ms;4Ptx&l3{oM#Cg?S z=*#)=et}cpFL247mFie)dIUc7WmDYV(QdWj^O{O8$g}DAhTY9t_(*hIM1@><TDXOB z%m9fyBfH7w^%EtVthwk69$EFXSEVKFYNVdUwN1m#0pOt<UEiIU)uJP-{MmxphRqX_ zqe*&C`>~S9#SN~Ix-A3#>&uL){LW3x9II}<>co<j!YW5i3o{b1)I#N!kI{mFv5ngz zI$8<)LrM+}#4d3;nqyGGsMGdrA-0WSPuB8677qyfN`3kb$39_Bxh^%B<<H9l{erOS zuM-Y7%e-EpWw@hzU@5SRNa9u(-%-K&AZq5Kw<YkrDAIpQQQS|W{&I`^mZ#KqFx*Sq z9Hxtqz<O9Q(f-A~BacZ~iknE2ndv(X<npN4Vo#h5hDq~yw$Vr~MtQ8tou7HbuT*g~ zT#4&+Ra;C7AH2?O6QFM&m2^_vtH^3&OE2|&ldy<p{0&sd(MDt3qknzfXpvjL@*?U- z*#P(7=mPQ<v=<$*LPmERK8}Nhrt;(w-^XtSt-`sPyqNrtBzB0JRfgPNY>ElD{PX}{ zxwOx680%jqMnX_%M>vRA_(B@L<Go+0d3GODv@w;0m*FZC!h#CR@47oD52$GO5Bftw zI`au#zJx}dM30hOk#f7<Az-~886aA@^C_R9f#GsQy0$Ip2+vG+mSt~M>N}e?)1$uH zo=j3nyB|x9hxV*~yVR=YV#O(l{dYN01isZcV)0dBm0wSP1W2Z^3Sc_mc+B3)GK7yt zxf%Zy`q|(y-ku}{KK-DqeSlPh(U82@Q0`OB>D<~6@UiU5b+UdkSx#gCBsLO?4?kKJ zxJk6XC%R13wrV+$rRi?87ImDayBX2^)LlO0_H?k!Yn05(Yd6oMz^XXTe{TAqbD~^Q z`f!+ntGTqKD3NdZ=^KA+_nFnD;Xy-55xYfPeMpmZg+R0V@>2eq>8HKxJ{6N=5sUHf zg&~w>{@(=VYiN|x+Qd`(=B_Ij8l4L4&)lQk)n_^171A=9@h0#1Y=SJ17Ylng@gN=+ z!_V#dnq|C`uL8;?!pj40e$<n5d6qg(5xEZ|&Yej^OTvvmh*h?F(XRkuD^l6fcGU!% znJwu*umFdbWi<O=nxm*Y!vvBb&MwzMxw#7t{!&5kC?XF9!%KgcwNKWacdfKSVZ)Ul zp8&@%H3t6kEF`YFHct51$R(kPhC-DTjoPsqKk~)07&sy(`a_vp;T{!1YRCh7smo8h zQ09Oi&(kCI%r;uR&OME2M)7054vvouMy?8&X&9Vdm4sJhCpAm6FI(+HUNC=h+CS4> z763(MLJjCI^NczO0+F9Rh+)*A1+w|@6~+&|(Y^`>HP&Ip=KY2O&Aze(VKx)f%Pm&z zk%hpqpkL;Tc8uE|7a#JtC38^W=-G)FD)l^1odKI)@+2DPNBd7|WLlu>)mS(RF&a8F zKoRBs)BOjcIK}Aj_v$f7Dst-8ib4()$Yw)cn^R71_E7lIP@c>3swkU67Lv6>r<KBJ z81qktn$JP1Qj%I0O8wnR#|{hqA-x+HBwJmSB_;4Mw^?o8%oKHtgXia2W+c*75hA2D zru%z5QoVIHo6ct(rYR6%<8%kNto9pO-TknF25n3G!*7kWYNR#>qRldkswLjLS!sxV zenF8RWVzm>6%Ue8p4w#lVbE<K&shb-72ls$50ekH-G!Ob(x*!XpXIDKd$*)mnYnZO zz@*|v(pDeudIna{n(a*wf>s&Af742J4B#aUmSwIB`h&i~m?}3(btbi}HPyNYMI-Ce z>TAWQ@+EBu9O+fu?_Jj&=`rWE2;Wt_LrwtYGFUT3{@PX$V@kLYYu~4puUS*@ll9^V zXl}!F4s^n~jrQtbXn^cM9+S>~S<sf@ylL2%^_41h3?0g6=#MogexkrL2GZq$S1!xD zyIadUbSG)gYOA->`d8ZtD3Nz9Qx$%MgN?#QcON^82o{QH`CH6ms$5?ADaV$GAy<^$ zu}@k*B3`GsEVg*v#Ef6{#n&Sxrr+-v$2__@t*|IM--}P9mK(LdRS~QYIqxzr$4**| zmtjGP*@wS&t}C{SPJ*1=&Q(UaBe>3B<y!2;LF-mjTy$%Gd+H#z!tb5E;a%7Y+M0QX zqUOOex3^^L+Lt8OO|JXvFE`i@;IA^tav|!)!PODVtKmnnCDq4LEa2ONojn`#g|@FO zv$WIK+;OIXMwdpf;3ow__Iv)*+TrhSy*MeK5q+j-M*FyD`pP{&BO-{P(6k{oAUM{9 zWlGpBW0@-T=68AH#I)3I`_4w5z}seOZf6yZn^stWf4%z`3%e<bc$#Qi#QU2A9M4sw zSK^#{_O~)-exl5t!=Ib`#v=M^eJ1v2Wm3ND{7C;NzC2(*f^2kvR2I3d5iRgkbBadk z;x^XkT)s-B-XyN6sM&GKAS_8hmICrEl8S4!BNz+{6O}ta9sX?JVq5;)Wcyt!A-Ily zL8go_`MewpW~AxYjXqcmdm5zXT`f|l;5F<vmSG0cVh}%m>$d3e)+ez@`%T=wj22v& zTeH>b<|0NyhQEZ|1*^h<<SD<pV>SRfbg93L#BJw|5JYbr9`;oBt6q!q-E21sI2{Nb z;8k(QCi(f!!X2z3>it-=MRKc1ncxqHKCV22xppVL5M8Kqoc~+R_2I;4*{bLAPtdhS zC~z6cfy|+!g9>h=U*+Kw6nHYh;`ZpYX;!JXGDc$+bmqs<?AuRle@oJKXyk*)`ZZYO zotS3J>GG><f@SgH$aCE?)-c59{eZl$cJMb(X7C`)K1vx=EjEjWigHAnGXhrK^nnMp z$<2_afU4>%3R!_bDUH<K=+R^ti*0sKW^pK3^g9-_3X$d$b@+HlqVpa!G|9^Sl{w{t z)FOsH%=kxH>nj(WrQ&59_dxF_bk&?J$2s-XQVKtoYg<ASLL1U6{fV`d4GGD&ch;$E z>ZqBuN>)||Sia1-COu@*I`uY#ln72U5qR>(m7KoAr@urzy7NfX^kZOo8A^|z-<ibC ztVO&slvk%ioe3^sX57c3eh-9WAIJV&`RQw%EzXWYDvVf#vAqv04i+YwaCAIA-Smy{ zKm#O(1rQK~Vb||kb{++RA-u(Q<o2<{PMOy09%xdCzof3cblc~+FN!cv&6AmC^Xj(; zFuNZH0yH(3S=`QImNJIbg*_y(nGZgvP(|LpQ3<@(EbcfmIl*!(?H*B_9`cuHq|p^A zsxJ2!-I@8r+Ys2PZ%+d<R!Qm5q`R6gIvI2gT&VM?(!Rth#iV}lr`s~}moE7LXk0zZ zhLv{#v9b1qLiw=LFJuc$F^XU{5E){1nNIr@tfxKvEMDy~EMiP8w-c#(9%>vE?lq)| z=%w(Q8VAzT$9j|R<<&<u8oz6wZgc)B+)1maCZgmnxxUT8D}8T%^sCEc?~pxgWG37F zt~?)vFKDoI`JF(%x`W*li)7KjUA^fV-%xr)N5CblB(ZQ7w;U2+;%;8vz;@C%Nav5> zY$wwgC(sa5nCLtr3{n7H3!OCeqroS9?n*_$3!B!~rl@UKPLBZHB*iTet-IoD0dMw; zCz^4X`MNLSB3ltjCn*tR3kygeMJU&U@|d|wa;Q;>9(->$RF${kX(36!JvI+kiC(Am zmFRYt%5YkmmJ6uHOJWP_Jv0@XviRDf47*Nym<VL?NNV`Nt(Qa>TDWb&XF3VX+_lDr zPuE|naF51kD_=%33aclse*0-dohK=voIRn-d;0S@eSvJ7<Y$G9WTHobE7-I4HJEqR z%xC<CKRkqT@Ekj!+WUdTHjl29ZL+$@c9Qm`QN1)RmPl8+?&p=!urbF&^%Xz7OV3>X zfV(Spx`4xAEHONMG%?+gipk3yCZEevQ;BD2al$R`A)QlQn5(=BM=({xHq_0=r%N}- z-76MjFT=FvH21siYLTi1e0=}s&#I4)`aHj^c7N0C_SKBe7Ill&sj6CG_Y_pC2vphM zA2ERrpkzLG-{ul>5sMX+zpqYp{<X+`grNl*qR}&Y@YEXB`hdG!u3AK9C1i=n3W4$D z2CkcC?Dh{8Kd0XphhbHGqkJU=_V(Pb^xjN9`~Ub%tL62KK27JsKBRuDIUQO+sl$$| zVmMP&mQE$t%T8Ky)k-q0zWFTPYcy$PR0Z55!0%^Nu=@K&3bS1~d52eFW3cq%STPr? z3O@KPSACrY@(R;sTD*lS{6eI!B?R<7xupJZfVmn9wz{0^NiSvK7g~~sRo+M`OyAlW z;H7hjyuYL`*-2SYK(c+g$SPUM+gnpTUu2s^&S@7)27#o2<T(z!<!GGVuExC)3AGMT zy3Ohahj{Gu2%J6nV@rqjTtJ?)PM@DYFSxYzyrjp_VE;!UEosAg>Q!b_boSdCSkCAJ z`uX&xi0e87)*}a#U+2bbE#5GXB#)Xv&o5*6_KSf!L5%Z=kd?mXmGVMcNZ+#5KM`&w z6_pEP0#^MPan~{9F4bh?xw)iwDa4qBcccz~NVnZj34W?kJT49Usd|!YhaBXBO>c9{ z3r#ze;9&6VGaDo>6(c@xr;$JW^HCsgsEX~2Y(%S$M%<{cyL*}Alz3#xlTptf=_~sF z+&)c@vec3u9@)&5Jz5sbC-0b36#nDvVw&M}k{kJ)l0H1qFd=WEmZh%E<~nb?w34?j zLVXRI=cov`juVZ4p*+yp(d2Gjd6yiey{a;;z~rN>wn_LUJ%xVOimLt_l)}mh@O?p_ zqsC1Ld!wOqi@onbmkWsbU`~&NFw%IDT6W5du{$la5>b1~qw0c!e+pZ^XUz1JGW!Q= zu|a-vFrccq&ca}8=O<x}c5I#~#BQZ=SD|*pY~Nm{YMzyq{z=Doxi8014Jqnxf^Utq zUy6GwGP*iBvKTVbmsO+5KMvX!{H^@@b+WrcH_Vs{6g6bJW%tUaEd}b?ddv7WQMzU) zeEU25@^+?>BzrM8I{{vCM<`PDoD17&uk-2ISH~oBvHn?TJSUagNl=w{L|Z3NLSUiJ zZvs@(+GcZ=w_Vo2tDbEjCFP$XMJqfQ8SQsV_~G(>0-s|&01Gy#y8I9_Gz^X)_+-NP z2*A^}ZRQ=1SJE1uVyQS1kvqLEDgg1#C^=GLR~uYUriDUV=ZB-WK6&(y3bscI8%!T< z!|}yY6B@^1V*7?MK30q5Ry2sRW!|qok7La<w-4r^-IUy{1i}-VO)9WFFtDOfiDk1y zPLtdmOd?9(mhdK1k8q!9bW<4&((X)kN6l=cmy%Gu31x3JW?H_M6&CwmCagsERwmL; z$zhVn^);VrQa*ue&N=|Xc9<zUR`foL<17zr#YG!<(fv?O=SAX<i%VI#dZoLJ&pW#H z)amo;<>bXV|3Ksxwy0exywL@gN4JT#Usk>3@v7`{kOBv$;#C0Offyv^ONJ1&WXaY* zsojh0EfqH-^j`3a2s}Hiq9`x_YNPh5n-7!Cjsm`poZ`-n#4XZpNR3X{?#8YdaQMK* zJ)sYGGS{vnt(2_<r@otx+Z*_!pU~gmeYIhqW<cVw*e<pQB4T>w`H(I9&33&D29)4m zxMPk#Me%Q6*q4bV#AcR0g{7)*z_9F}8`ERt;BJq-o>At}0Oi7ruS1PI958DGtRw-5 zGKq{uU5ctaB-~CF42`LwI4CJ%nyXO?nth{7c{gIikQ5si^6Rhn5vWv3{`fD=mfquu zPKho<Hi0!>8;@!DPdc^lCyyWzWBe40TKVv>CGXywu`{U#C46vh6>mHxQd(D2Rj(Bk z0i=BH)z>QK+RLPs$qPQ5@`<%j1}&>%mg|@C6J&SMwC;*dUU+(aXbo{DruXnYW}}u? z=JJAL^xWl#4yW9w6zzA_DKgkB^75+XY^!ZMh*1<nnH2THjQ{)6EUK-j>2G6CVAxg3 zzWPqaKWop0*l8{?og&9|LQ%D$meTxQrP<f^0{KS)6HHF4Fw+hd?{C^id3e)Qrqb;b z)B=+)Yp0^L#M>!}^JV%sv!Jmp%=(J3bWu%_kKPd~FJEa$^bAmYe3}eril2Zh>pm5L zc11uxNzkqOc~h!iY8XTWMDD?e1#H^16L$ptn`_IDqqNkI+EUe+*t6SJeZQfzY^w&> zN9ApKiWOF5^X`Y+%>PrJs_cDabb9wJ{%ILvpV>@IH)n+#6oAMTSjN-<fOo6T2~41P z`8*7QIv<P=7<IEj{mj&y$#1@vU%hJ!ygDE(Yes&VKOaO)jl13mlD;eVacm2z>^vjO zj(~o^r+MM2qvDYiFl<%va=I+#jIliKnYWz~xuXT<u4Cl17OS5!+LI_C8vtsmv5<BF z9=A@5+=qj_c3DWMjw;0ta-VJ=RK;xjp<Ipyck1nj0+KTe3{4qA0N4zYk#SX@gh0+` zjCcI`?xvlUK@EghOl-*Si4J}OmeT483uK}2Znl*PiH?sduNoqF2}=ZQ{?<CwD&7o< zA{Hxv`awX0Sqg^`VAm5*(G%2dh$rjN5CEYygp*UEp@re`MEnO9fEJf#QKY!5fOORu zTHi*kOcltPk;-82kN5Pqhqknocs9AXRJ|^yD=IKjQUb$zh1uiq35~pr7-NO<JU)8k z`b(H5@`|&g3Ax;RB{XWq01<zX=bvUbZe1MGfN1{Lg^zVxTJlzO(^BQ;zb5|l@;tX& zEox{e1eggq`1hbcnozA>g`R9&ahm`xFo65tLI&_NQQUob0QfN8!+yZ`j)|p1Eu{w+ zFEMh$94vP0IV`7MPhn%=-**X$%J(<LF-t-lpv`PyQ^Ln6@db_xpiquBJEOEe`wkPW zP_Dmurm>+84f^llK7KjrI9(R~2kk?hJ4_ip1PkZiH6t-}piq7T6eA@d3vbXEdo58& zALw;Tme*%o<tgld5n$My$NsIpmxoKvH(2lR#mJITzUtXkM#|paI5|dK1VD!%aiGKp zmfNH!T-E1WKq<#o>b3Cka|k^6r~-%%c<Uky1KicS$*D7TAmtYMTdNY2Yio7ipWp5` zL@gp9KRQ#?)#{!Ll5o6M+hL@OJ6hnYMpzk{=@sP!{Hwc3G&wIdf0s5OPPil<@Y4TO z6)<n1zj^_hy#DF|;6|MNH4q@$#@nwrHa5QxZ|*rF0I)9wPy!Zyx&{CJmbN~_WE1gs z<^Gpv?qB%==#k_0-^cqa4$#(3y9WFVe7^pRF!0}P_+JE5d^jfNU}4TsXjx!1s4(28 zFgP>>b_J#b^ddjE7(Ucrm}|1zADbv@02|`sx}tME#K7l@{*h3e@a~<xe!bJrx^t`5 zD(^aro*rootewrksz4Y!A!cIOXLUv#rCdh7(7ZLGub(*;Ffl`$PL}#dz?sf>J&q~Q z^aST%M?g9AGB)VJ<&l!n%1j?hcz?yiDKNVy#`^VrF@-VRt$bo`ZlWx-Fsg2oCU6Pz z&FQ9`s#W^QB`5b=0x{u@KhG|qS`Nnxr;geCi>+petG}d?4q9pWx7Gg2<&Ji+wj)yc z>PQfyM8?1*54qGoxOlNjBQ1L3Xm8DQKTDy!f+x4Zv|;wqbD1f|FM{9zIRjpI(@GQe zc+YmGBK5w{sceGCl{^iy$&ZxjZwH@eu7B!fd}p;9dG77kVb)PqSgEg{B2hdFEIxqq zA+j_y(4LkyUgPfz_<FRpn(^_<<?Vtg{rQ%^yQ1WY+&&r44D0hp_XH5Hisuv*i*q`L zipr#7%K*W}UlyFaZ{^4Uw4n)EQ0grd84eQtyJfhqzOnx4eG4^b<BZnCb`Vg{Bt)MA z*AFR}nVJ24-CJ72M=Bu?E<*s-zJ!e|)5lN%+ZynLWc%VTc1Nhtr-ai=?tl0%>xGb5 z%orXU`Z8TJM28>iVIYqr@1Mc`G2Tv74Rw(O(aaUi#4O`udXJFgVT$8|EKeWBwY2l@ zot&A{)h)HDLRYuD3guW*mQqvK5BB<P&8pZF9$>JQ^4Fi_uwMw;qiwQ&QMN2HXZ1&M zp6x%D^s{pLSv1X6-NJ0FMv6==^FEB@t6F{_4i76k9JqEbW}{aex55Egs$6#+L%R!i zbBr`@>L*wfamsU(7^#-o6^MsVG{LXRPucUu{25#O%C2h)mWa_3#qscS?3lu1=?=0o zM99^B@b;^f&YlD^5xfk>^`uMDVf7ykRKi6wuk&<Ef-EUa85kGR2)n6sJBvvBZxm1= zq>Q%K)xib(J3CT!7_-?#Xg2cjtkgnOPcC|5tXq_vi0eBk;iP~+R%t=#B?W!VNzVwJ zHPA|)0q(z0mw#bdAzqZ^v8UQHx`@whn@5oc@%~i=+#=!_nIvGYyn5ar2mbI0`qp;l zdT&+leI`_Z{}%V=pmX(O!B<5v^mvl<%}#T323w|~#{!>3QEEa~1?b8CR!Ng0Kew%0 z15dqYLEOp0+X}%;K4|oEpLcd_BEwhB;UIBM7d>laPya40gqq^VPLaH7Wla9T>>jJg z4UHxpUv}26S?ucVwgOz%w^i?YFd_{Qy=>!_;Xql<Mobe}D~uuKbxCg6I&hsF9}hJU z{F!j8Xl2vqtE-4!L69hqAp>Mm9jR9V4gdLP-?|W4_Zls+TUBJao*@?cr@y8+Jl{jf z#V4#S*0UOFpe{$c@?PZ6m*Y2BDzs3}8p){$EcbxPxU^MAFava1Qq@>~ksmxYDUo=q z@HtFVDPD76q*Dgu>>>0fk^Gk!F>oT8JMGUKdiob4E7m6KQjj=Ia%FlK!oCM6e`qC~ zBe!L}VsZNCZ10(+obCi{$j?<P=T=7|X3S@8i!R#%N+@Hf58XEb8#cI3?WUC*M(U|T zG14xt;<mR*m;I-L&D6e%Jzy3{x4@jK2;+h3@fRnRHO!{hX$WsV%0{zkA`7ZQ-_(E* zY=_kp2H=$iM3srJSAn-PQnZ-Ea(=%m$0$yrf8wGs{b<MR9~CAH{G2x);H<_;zc(uW zS|tt$lqrIo>;*Q>TLLaatbx4!Fj`uER$+d*G^_1T)v*k2t~M?h7$wA7n<6=r<_}&r z;p>xpEDTU<L$PaSsx#4F)lTlTX?)^*NKB+VJ@<r$J;g$m>M7cG?i2hR1z<~6^2o^2 z;Ci%B&rVH^q10b%mx%_bzb<?obqre6g)Y*YPpu{CR}aCruCtV1RXltg1g&ZZNe98< zEnb-D@U;?0r~{t^eHOn5zo9RvRH??;Lnh+l-AE~Ov_n2Jkv+3aqr_3gJUUM;W5rR` z-Ja@eMXtsawhU+qy>~~-He*kD;3FTNx2T`2D#{qeFKoajqG<fY1)M&-z`YGEk+{yE zIUQhom-Im-x#S}2`If8D8+y|t`W_{8X36`#3AH(ABJkBXQU|59XjJ)E9vEz1q$z8V zYM9TgC86HP`N~6>#&V{Xg(Eg)xRZpGWlFGQ>Rr}1#Y5Cm2Dc?#W_h5Kimn*cqS<Ok z_<Uc=5>zik>wCSV$2m2WJSw-?LGW|yR-V#;&}5`eTv&1GN991Zu=R_yJRW?g2cupZ z1uNayhr(Y;#-FBdbSC`apPn#*Kjh+LtGooZ@b=G>%lhT4;q4e@Yth@xpahrntxc2Q z-p;Q(u*b~1J+&8>`&J6N`D6Ja{CH%?gf9)@S*WdHT`5;ze0H9TkR{s=>dV)gL$go# zM<V9(KcF>aLp6@HH0~@*4)jO~>nxHRUx}#}U(h+|;oQV>d8Xt2@EfMA<p+C}&g1z+ zN<R(jbJ*H?4|%hY;3D*<-qMr2jEk=&M&b+d@g+8~;xIROfDihzu->dRXFXB<lO{qZ zFTa%F4Vp#}Ii;BQmKO7$*$v@HXrCsY=yMIzy@9XNrxW;fpRuO>;iT{3yvom6M_*Wh z*1svW9)r2Fp<LiEWuUF6(+rq27o;E84y8wMsfz=Lr4BeGiuN&r9PshP`n7!1GI(!# z4WVhaKAz@W*`L&2y<%(#(@=^p)i^PW1W9uQ;cx+AeIpw6Orkf9y$&d6Vp`;p;pMS0 zJr3h=FUmmq7ZU+yu?kU!xT+J5)GsC?;EUQX)-;jAiD~-(B+vY%3<4+8|Fgj5e=`DV zlb&^4CF$r)dv}r#kP<aO3)s2k_x0_d-s!x3b!J0LAT}!}X=exupO!?s+*pPv5UHfa zp<1#&tH1isxaI?MbD4p|uQA!CVMac>jQ|EH_FwYMeSr7B7?AyM&g=g!<outKsEr;Y zsFtXR=304fM}legSkBT(Jtd2F)w{uy#X4Z2s)_VSj;R4Ox}{v~No?hWIUDDo135?; z<PQ~pp4#}wd;7A~u(AQSPWea&QEla(n$~&~>*M(;g#-gDcjI#u;;r3umAu-oB)u<K zT23atpIK!HC(m)D5-gOcijmt5D?C#2oB*-1zsEJw^F+?lU^ZxWg>1|8&Bv$AY<#*V ze}adNs|1#8;G_w@xX){Eqw@ueu*2yU7uwnT-ix8T#5&%2dF-t;XJEV>%J65XG$NXt z6h|8Xf&(#=@A<RHO_kW~1b>ET=;HK_(Z)J!9dC95DVObJYuxUnRNPgpOVE6Kd20~5 z?CM>2vT^NjD?T-ki4!6@lnhdF<938OT-!V>k3y<{c)!@LZbU&m)_A(1j0(^<EVA+< zCR$onKS*JVZq`^k<Y&vNZY^zNytpt#?{qr)b0avj+za8cF`%)Hr$qFx&Qh{Yy#e`K zZR^{bs=pCX^0NOCCpEHvgwpwW5*B1qlCvgxHJ?N?jF(n(NE7Zzg7dt7uF~=9cz4lV zZEtiIlgJF!JIX5Z+9$@~6nXI$9G#FSk)2+{B7ue@aX;BK(VL0gb}D_c{>e@(?<zmb zBlh07o|uEV`Mkkv?N*{nPm@jCaH+vxo!Qp(AJG(AWALbFekr-9Tq}2GEnVyFmHyb= zH98qn1$~K)8zua`c}d3J?@8-RMh9ux*10YGG(Iku12|KsS(u$b7K1<x+%8So^`-Qc z=Z)7&@{b5%c6TbJUt}1?FY81p^3tH&n`{>BLVhCjMi&8UK3UIe4!&#r3`~c8u-u<d zC~8_=4uJOxr(m~jF^`=Nu=Bk<Hjrz#y}MxX?~Yr{pTK-P-*A4Gd{-3imHC{eR#}W` zA@;@rC9aqh^EN=(I3VDvW_did^MO5P&I`bw5EX^i(J!mKwb%0a;BDsFRVtWw*SyY# zf%WURrfYn~#87?7<50sUhT+1fsvnc{6AAjo%1CC(K?dO#S)Rw+qy}m(c~4`h7I<gh zK<}bk6SXoC-1;43^*_HUy>*qpEWWCr%jYdH$k(^LthHG77m$OzBiv-->r}5;dwta@ zYQS~iesaZ%`8e|gTU0vqAsm$Uh%q9e*eHaE6W0Zgkq{`YVYd3iO|$6b{{ssMH+GS% z69EgR=s20oHV(R6#so-XgzX-W(<oa+(ksIbO9GDJ6GNU=?027rKR=(Iv@m)fW&A4j zome;SNY8>=L?UjA!;>0ybIM3(xjzC;C$gmxLBAN=+}C>pk04ALSa=><!?Mas@!y$t zwHAmK^!uil82KdB%hu4QioZ~?<c02S?8*<J7Xtj>MCeR4#j3F@GPbl=klp=F`_^$V zjq)-<Y(BPgyj@VOvc1t&|5=-ba0tEI*w6>0^E89yAH6F|o{>SII9-L;+bIIC(d>H) zb$XtthPf;Bv0(BS9>hKnu{r19A>G85EvU$gA4ccj`}^B)S=i&@Hfev6@nTK>g9f+8 zXT6w@r8RN!#i;t_uRU(N#`xCTKc3-c1gTI0smlj9-hkc-YvT-%#j^d%?|!iSMQuD) z9fJ3z0N%x>LqTqftMjA`4bneQX%Jam{bSB>vOf28FD&Bmx`5qTm+0lewC`LOM9GOb zl7IkvPh!+5QP=6W!(24era<ayiytzjbGNFco$Hr;n)O14k>le~sgsl*<Ym(P0HauG z%SOm$(^&T8fYHg!B_;X;wdv0fR7l^oXTvl)da<Rlw+Sfdn}~xpfUsI6qg7^>J^A=` z(;>gdO4mwZ!sVdvl_Ak@OEE`jVXilxN?{|oQ7yK|oHx~zm)L~hEN4Yf_~CI873mCI z&ePTTrl;XRd=8hZ?GKL9#E|_~J&ITD{Po$YYr%x8GlLEnQIV4JmB+KZY}n~6A&)Y0 zYc6chbu{{*#6jQf0ItZCr&j3!Te$R9>u>l;!0~vSh%yL7O2cFFQMOGAvcJk|9;Cxa zjE<ed(eI0VSxCW#&N*oP`&$4s+Kso?znTH_u^jw(dqT>k+?i>K82W$9E(l(;2Db zKct#H&YQDdr4RMVCIi2|XN0VMRrie8e9qCK*&(*vQeH-~eK<dSvBnPRF(RQd+-_ko z3n2VGT}NS=kvjoMA&}e@c;>1GT8A9UMl=<oC4LQhpptx%kKo+m<X!JgGTyh)yNRZp zSEQ=9%oa^!3)DFo3U?3r?r~WBX}_l+0(CZ+1WF|V_rx7e{I(d|%X*tlxk}Y`yK{|< zWkPsL8ITP7;Clmfj<B51p9wdnb_!c@7d;O?ruiLiu|t;MxRKX;MPvrO=WF>BBHZNb zaqi3mCJBV5NTk$$(d>A7DYiiMAglpyj`)R1LmW#z!7O45vmT#f|M(FXT|(vkFZ2?9 zY|rD7Wbw735`fFPZvlS@rjf;CX2b!s`GzdgW!4t<Y_yA>WVqgiG7pdMI~!p2+DcrS z8N9)mXrleqR<d4vOQPX=TjCeS4Cz-{e7;$(b7h9u+THSyq~T?(IDg={+`+)g$@@$= z9D~D7Bj5%9SvCca%S5U<3Yc*|pQ$48Y?3+=@lZ~4n3!moHD+j^Ql=mmz=I!b_!@^D z3pB3Fn|xXhSh~oGr+WBULJAW*hpn)Oq0jhx?9oaYFYhYqP7Iy#pL=-?ivrK~FL07Y zZ}hH7F|dliyVUFneS&^{3H<-}XV<{}J8t%Wp1)2pIcXQISSl<!&Tz&m+xV<rN5OwG z8ISQ{EYJj+8j3Nn#^J3$^a-7jI{&}p3qbkp{_}su6#okz_5YRd=l@t7_5Tm_*XaN6 zMD_p2l>I-M9^^4Y#sRkCr?3A?4hlZ&2iQY`CWfr$vpJO_uQf~Wb0232;XNP++zfno z*!38Fdv4jR1<*}cu*f}Vf1fg_-d_L<IUnb>fB1OOm)!^eu{*v2hsfXNe<Se!>${~o z5*WveXn6!oScf{@Pk{7fLlWJ%Z+2qzCx%G0k|*-P`ir7?WY<I4Z~!elExD&s1Eq=8 z4Yu3O$v|p;x4etIi+bCM&09xOPZW${(xNE8M+gQ_{KsNv7{F^)`0nxU)4k?Q-`#U_ z+eZ6H6;ZI`{6%nG;ZN(UjtLDe&hM7XOm=QfKgS!Eq)X>LIUU09eD%9{<JD0D&O_Jn z0GP^1c3)`llKf=UDsp+bCQllWQgqT<?={x_UWZGQ-KIdle@W6u$jbaGr|vGK6;nAw zaj?b9e}7{PJpNWbdAOJvczi&LA^-1HitRts+R_l<TzfJUjXZS?9ILKmTV_<IRo}!` zZn)WOQG&6iQwuq9SzQ=^uPOq_);S9I9!Tu0wzhWFPneIE_Pjlzn+BOF$x>%yXE%O< ztfM*>)HMQs7c!rCW37EJ@Y8#NRockZk;&gP=z&*dIjr_I*OU}VE0T+Pfie@F3tktt z-|ZLx?vE(qegQBpf~F}(GvJp2xRZ!G11Y?vlU<k9+WmdP8VY2CE}-@M=nD-E$72&- z>2BMeOi%sT$$qL}!JYS_|B?^h`hgIoN0{u+0z{2yQvd4O2tU8P@xFmA+lP9qtGXr^ z!Itfgw^nX;zXk%v%whaHt<vL_;l=h#$nTP~yDI#=<RzHP)0pf>tMwB99*Bt5UFP(_ z=*|zgfJh0C@n?OLOg{ZJq2|)P<@)oJe;D7p{e-$r3QLC@6I;oK<5MOe!K(|k@y-bY zGyYet-X=}RD(~0rNIUNXZW|jnNK7GD&B<;E243;jeve!+rT0IrD*)rZtS{@#%iG9H zyglgI7Trl^6;ty|aB(<|r6d!hN?|q@+Vk43z5Jebb&_j}-auj!60&zU{5Af@DwWGU zl9_`mg6v#MVs8OzLP6l75a;i@Bp+#gQo`V{`V&(A5U;UXZ@Poa^z>^326<J@CwA#k zU(zQ=u!SbIbAsy+dF`TWR?ZU%_2UZpVFxRE9Y4*F$!Sj4-*57mtpu(bQ^FZP^+pFB zn2wSLH)3G1^53@P<~sM-XQlG>^KwEbGjm53pZPXnf^fy;fMSl+IeyHQmf~Y-fj*P# zoH%94Lohi}U$|%}a6N?bnTehKD+eb>c{w4YN4i707L05et&D*+CC#uHO9&!CU|Dwl zqG_!(59f_ylt(Pq2H9zVRnYPLWwlX2K>2eH&kxw>4?K^5CJKTSb;$*Kq#}YH_!+Rm zvZ6&8Fc3i6yQew9N{X;H(cke%%)*2>W^!<FT}<8umoxD(;wWka$%IzM_;qhvEvt!< z)m{bON*4_K(m0h?yqIM#dh|SzV%G5kN`!;E%&IU2Ib}S;;&6hhvH_T}rGD6ZJu&(i zSQ~pFZ_Cj0E;-s#F7xX)EFs#i?mqCDnpbxAb24wK2y~<oMi?tMvhAX}SXpM+74|;B zL0nS07e`DLxRfOH><=Ntxc;5EP8^x=PabIkv1R#3G~C=}?x#CVLiqN?DqoilDPN9+ zi)<4!e@U%7DVUDJ@YQ?7m2>ZAATdMCAZVs(VHwyNHju=C@;=~qH%tCd<|1Up%s|iU zI944W+>D(5Lv^FEs)7^sE1h=Y7DEf~5d7hC=?)xGNPDAcy;q^n{~>VA)NJG*W6hp; zcQ&o6&g2A^zq^LW0VNb(s#?Ybv(}#-IqCzm()Ed4^Z=;_K(hR7Ao*@NiaDa;B;rN( z`>c(jl@plc9VKg%vPf-=03Tj|ed_+QpeM}|B40J3XS;H;TY=c-hX?tp67Q(<ZCi^a zX+U#Xn>~3yO33R}tmA10F6y>|gN#b{63_e_s;Ba6-99Jlbtv^K4|~};8Nyv}={Fa2 zn;cg3$oz0!$rB?2E}ogyI?eYsZd-Ebj86erDfHlZeZbB4Lc(z2&9r9Ip*nC%&(1&3 zo4dFZETkVn6c3s?v@-C6ql()Ftk%joS6183A?1FB>4xjZnS@qiGd!$Y?4Bo@Cd)u! z6W8XfDF<>FpdVndVBrP%GdrB%X=Q86`Vq~v@ksXBRdw9)%bbU22NJKU(!tOE%rE6W z2*=NaEB47I9H{;}V1M+yvFL7fwaM?3WkP+n=kbb9kKIDvTL-m669@W#DR-o?Q{`xT z=1;(@{R2&cR17S86zFC*6rkL;Lu~pwb8}sY$NEwyE+`O~iJfhhj-D?ks$6=w87U}O z_E)97Ly0R~d9;(PmcX?KLAUPH!vhpqTp*T{v9O@dr&}k6t*qK!58D@1#R$HuO?=rq zE<bSV99-1S@|NgWYal10bYI(@;(S{gFK^7Plq8aUeP#BPnDO!bo=|WgdEoM#Z&Acd zdN0QB2O$jsRrbl8zXCfpP1u<F_bw`eGx27a70XzVfSa43D^%Ngv&u^M*j}1x{JPbu zs;agI8q_F{AMd^`*3xS4CNUx@;Ne1O<s_5;D)gh94qZH|D>GVzHtb4ESlE?OkCPDR zd$Y%#h{-X`MwC44y$RqoFk8v5c5G#qd#_<unS>MsErJ_A66=+5+@5h{cB_E?0}HUC z3J+(aHoD;Azm;^`ld9GhCk4Q+=DR3E30EV$(}S1zfOB-`kkYwUUXQlb5xL8Tk0xg- z8-U$c^KV}kAkQIp%Iy=Y_1>u6-wwjXsVSET6f?Q%cA3For{zbQ;4lv%__wDP?IC+c z8gKm=6i?bX-8K_zZhooXb?3&3du-vtFcYpREKk2f{EtEsDzWjL2A4>w3EVe8?i~Z3 z{{6f;Tn{OOKBqQ=BHUYdWy+=3IZh}~k>3l=gjg|yKqdzp$t}+IeInj^BAZ37J>1PJ zD=v`7G4EWYW(-ZN^+xnWg_=&olN`fi91Fbz*1g8mQA~lwy57nL3ARob6k;9YYie=X z{lun0ap6Ix^?0|(zTxw~qMF#U>NKD0yPfH`dpW#FT7H}znJUX)&-402$p?Guz1C`< zR9rFZdp+Aui}EoG=!lJHV8Al}AyWAgTP-OCdG%2?jhmC|Y-22Q)o*prZXpCoY$&dH z_r|KM<hyinLSdjWxuAH?I&dM2gi{y5&;;-v-@$uT(4}gqZq2toqBBxvMgvDbCStH& zR30Vbyq#<inZM((<o}9kajt9cvXl^jl2wTA!=apyS67vvkwSWDRz$8hzrdoBc(Uod z0zY0TM*D8l^+aHzi8%w3q6O97nJ?HnP%Vf$bDl&#NRa}%iN+_bp2@-b)r|p8%HIhA z-MDQn;N1&_9v+R#uPu2OvR{-bSGArE(pkG=%d`^Pia0FPmjb8x2Z{T1i)gg5a2U#- z!Y?o{>byNe=*uS32hom+sXNcQUjLlvQ17r=Z?nvxH4`2404?a%JwhBL^PXaMhN7_o zI#%PKUr)JdxZB-5^nusgsVQeXo+So?IEt)j%L2DLJTjhN=)R9u%uRpj+~0fs*=Swk zuqleX>4r4f$2JFU*(4Zq;!qBq2<md#IJ|kISV&Awv_j||$OY1$V?6iBPdt<+Ynf1B zVHqr)M4fcM9R>d4M9!!CP-Sg=yu!P#DWn*AgECs7yvlZLIsy@7-$?pdV6s3?Cr<m- z8GfYl0)K8nZ2ew+Dve7Oq8s?j{GPWCT0M7yEwGcVTj%Q2;GpMAt=k{RYnQr}x!N5q zte5zXB9F3HkEaga?}`kk=}));V_@EsCd5jgZTvx!W?lN)wToRYqc$TCDPXmZ5f_rq zNS&3~Y2F1-djYQsbBZT~{~5_`%gMi)yFAfPQQG}yihBPpk($cq2Y%+*@2ZA)Krvs% z0V4$c?xF_O+236L{+#E*XfMcnxi??0C`0Hv$neIFoPum}D$gYp6ING^icfGy4LIKH zDsRzsSx@BQG8})HG_zdrfZ78jrXZJlqE60hU(Zi60q*j!`Z&mZHU{hP8wy;F1fmD? ze6mm07sfxW;gZbrqOOg-X2xl@oi%aJY3yzh4ZHMuH$s+dz$oH>M|qBo)K7GnYk3a2 zt%|A4U;6$EHFCE)x`e#zT28kPZJCGR{6Rh~iFTgs*-2KcIE-2Ja9kKEZwb}hBwhJ= zjB4vH`ewVYjax|Et{-^*o_5@kkL*6~zP|_@^sXy=Wbv|)etGY3*>1?qmNtRG!4l=X zfv8|{_NEflg&mns4pgNUCdo|}Y3!947_HFg@}6@}oZJSyJrHeGTXcJSwC*@RQQDvQ z6mdHGE>O@#ThDS9l#b6PeI%3)MCl(-|Jpn#x;)fGzN)lX*oWJne@-h+me1AF_&NrC zln>0+TO*esjWPYQTTGEEZJwX&U}E@c1?++Sq`=NNZOO4-6X7syoI3f=Zok3!$sk#8 z4_&Hy%7Xl_aKLV1F-m>FnhL5M<itJSHj+9ri0&P&C4NF}03SW^<uwkpueC%KXUg*0 z$of3M7CL$jPDOM&QZ6T5t_45JFv+Kur33VNWc=wrW#w=?D?V3yhBH0+{uhrlz4B6o z5PKt)_W`T2Z@2|UgQO2<@yK4l*<9u-{*J8UpJv@g=#BOW@h$aWL>oYn6M*Vt0JmCV zZ@-mionG<QIdcpEbH2&nD;7!r<gnOwG2k3Np+;Cc`K(YT_F`(KdugTncW$QCuE-Te zlC_DskWD?1b2txIrZv00I}+|v5Uv_nKXKiQ1hSC__e7zf_bfw@!1XUnWM3k!X(7(Z z6J3|5oK^V<UEh{u9Zu9S_|%+%%79g2IHoNe^kF=VOZ_W<!B1L#k%O+Rf*Q_NiqtWb z72N_z_ljcgIfcJE4j)#WtusKX550LZLN%Iz^Skd0z~M!E&UWt=A1{OwdWT}VoKR!| zn=qA^tu0{pYW|{lr(MsqVb!c!eqVZR==ey;{$jVafy4QLnYW?Yjh=Ih-Pb5<d9op) zK`b<jYze;Ez28)KoT|&Sf4HnL!iPg`z?U@J@&xZQaG~E|3E<at<aASm%CnsItzO-w z-<{JTg0mFU()X%q3n{{B3I7LsZygrZ+qMg1peO<=EhQkG(v5(Cba$yp$52DhpduhG z-HgN#L+8*)H%NDP_t3Bx{+{P~^LzKZkMH}QeH{DP>mUD+Su^WicbwOGo!5EYl+P5r ztx74VjoF^^!U;LsIaoFVVW_tcl@^(4CCn;K6C6z1<|Y%n?BxTK;sM|+1BB<%i9ZUG z&6k;RHo~qJX)f+4Z4~^pjpNu!BG(w7JazH#tUYr5&_Hn)|6t8L%1(DjyELkVcJ7@F zJ)qh7_4&4l0=P%a#T(%dwlpp<dpeP(#%UEVbZ{4*ipfCd0Iis#yMLSHKy(oZ;+%3? zgDvKV3_`q5;K?VjFzUMFR~fZpt30#)5IBB_*#8Wr?hcEB4Q7t!fzkHG*-21$pzq;# z!AR3xOBJ!M$bqS<2}6N|v&pa{MWyleo@-%i&r6#19Pg_HnDcN@sQcFMuO>PJT3toL zW<iDc44E<Y5z)h=kZZkduFcH?=C@m`qSm5WlPUJ~N}gAwfzDK1f-VBwo2Orf7TVIM zl~#J!*vieBWX%oVf%+ubGDUQ|R1#{0!P17FT2U2GX}>{#g_U3JVzP=NEj0;y097UJ z$>Uu)+!YqC>EfAMg)(6Gee&Wqf|hlt-wUM+3cU8rr2WLp$;+s#A4wZoa|#kY9Div8 z$5P{@wrE<o<7XSns|d#s)gTKS^VUh7D6RCp-ZxJR7z0X$3Xd<2bVO~UQ~VXxhZuZU z_?FpepkJO=1xbLdEyCKW9z^LJr!!Wz{^0kEIbx_5mu651dNEy1r!~%Qk&)e?5~~DM zTmU}@a=X#Kx9}fFu@caj>Ix8H+ViqV20U`$@F{{zbmWohv#7EGCHsO8jpbCHdN8XU zFP*489IkeEjbm_VOWeM&6o1_JY}xE7LuMLwXBiLInUAqneVsU|e2^?d`3T&^@1rCZ zKaS^&%<XZDef#u}qoZSucRw0B<mY4yWVQ=AQ+G9#I%O!^{+hSkUVm8(m5Ig;F(!`r zRtV<C=YGFQ4}w~Z<8-;0YSv_UX1S|;Z!Rf-)6438)93w^%SFe8C^pbo+3Qq_0H&Mk z((RtV*KQvs5uDq+RoZ-)VXLs96>t_x=Fvo|s+1`eZH;KDt2KOafoiiGx@Q4Wn**q0 zuKvSUR$cZMrem*FsB3GakE5Ky<!{Tdbu1?c7c~kl&UTk=uNT8bgbmjQjXRVMa2f4y zStCHD0^pABnHC|I*KK(|GW+9-)8`om0<lEPFDF7BpT`W!T1UC_Pc{svc9Tt@ip!_M z2|Tu(;-}uHM`gB;u_EBE6r=;#(x<%?DeFcOw(qG<o*e86NsmjD$qN<=XTrQI(r48y z@Gl64?c6V_@qvXPq<iat@%ig`wRSp<X$<$Q_V=&3>c$>z9Oa0Yb5BDQ5t@v=9D%$< z*_3_AWHK792P(%;R0;UCpDheDq@EKZ7+<v}*9K$~gcx|YzQANye1|{G^Qm_pcb1ic zN`b};HN^#-_~b0Nz=VivF(Y^g7agG@l+14FSwEL-n=K(2D9G;vN2<C&DZg37-D#Un z6mN50zE9eyIV=l*DW~ncNn=UxxJqf`_`I@f;2=agV_k#Ga4<-6a5XE?DErt(x}ua2 zr(idn-+Q?r%BcweE@EnGZlA|4g?i=~s(|eb&DZ}{4@y+J5*>_ru=JMtWQ1bGSs}<e zes#8;y9|_KI8hmT<u5D~)BCCXM0MC{ZCThW!red5S=<%jYclLF3TEM4MwJveG4J9J z=cyMjp%|jVJnEE@USZO34TW}y9jUTH8SE^#z9Pp1VOguHa)Ad7R+)?c^~Z4>%pnib zNyqE&93)AlnDKEbWLU^8>g>Jt3ueRqn@o!fNgoG?$MJDp6cG*}%Y|2tOd{NOfFik5 zH7wv{;RNkB_c*zUli9^Oo5zvCQZe~n8JjudoOLBv-jM6JklCIcEqNH7N5xOGnn#q2 z1sIPKCsbKx1U`>gI6NL5b=%bGL&|oC3(nC;RDnVe-FS$tw6WylP((L%c5GknvIOHJ zKGkrCyshAL|HOooVaIOf?32g9H!mu21N*kZaDp%Mnd+A#P)RcOx?kUB!TV1Gr_1BQ zyb#YCMvn{k^?j{Xl<m|~(K*_jkaHK(Ps!86Rfb|Zw@$8(r7q+ut3EG&7`}E^Icr<G z!|XC0@5Epv+}1Gs4tOv3ZY?=Ji#cwNs0%-HS<62vs6<-n4y`v%30;Bp1$8c-fG+00 z!B~@fnL(oanfWa4%Wc<pw)F*cAF0#xX6_yB%a8W<z}Jm%W;Obpc1>@Q-kmgm0608Q za51>@ur_RsYSgsJlNa7*^|_kb$MU)w)`yj18w%2Uj(5nJaVqo#0sGO=VtTq^Z#21! zBPM4AsQv^#Ypm(ANV$Esj!=7ui#bV4EaMa7H9RzDzB+ts?{cp-6PV0T;$;S<SjiNI z25&IyX|6U;QNNl&*fV@l${!V|7h8d&sBHZ`nC<2|j3PhxS}*A62W@($jZiLd2uC=j zF+OKCp%9Km9r0Cq4N}yPIGIe8m-s)r&HTI{`?Dfb4vB4}T@~}C?ND>mqS=i0e7!I1 zF1-tCM|;>oCYm3Yfip=4=Zl}iq3N~OCe*_{H?LA7VyFwfp1dwCdDf-k4?!L*$Pdi? zD%Pv`O-B1~Y<<k#^`NWE2-*VC!%+PC4EW5mESiCY%7c<YgJhoFp;3AFh^80=tv=A| z_|RZ=-0k8CzCF6pF+3+nhV-v<lH4op0yi3~ujIBL1eNI`&$q5&YWFR-e)TbIY9>M4 zYrwG8WlC2#FF$p*a=f|iese6+28ig`UAa{5puM@HTWU>;XI+i*eqwQsOmpF7t)!JC z->&8cjkkW<T?wicr30f%Emlp(|BiBl(HSFRPNYYAE`yE)AH@{+C0CkmS|5BY^!5Qq zG+tgLi1Y|~$#W|9<T&c-YVSkuqWwezHXA$aFQ~tB0ci9Rlf4Qsuj!3?E0FA9Jy~V7 zyYu<!dlEhAwAC5wD9?G@1E0%Si4L7ObbcKl^s^ImMq}%PZ*g!HOua=-o8Slarkg`z z<asN)2ekzf`6l`->IIx1i#4oVZo`?#&$lDgvL-lm*>G=vx_&8$dRw-k*diNp+4CT{ zD3Y~zcy!g({2!r(BiJ!H!F+k!2nh+CsXc5PkKk3Ls7(%wkBQRw0Xy+~V<I2VW82?Y z5#?<$YB-WSou`nXQvU$$r`YXs2|s<p30nSibletq=CWk@!jIY!E-k1Ofh{aQ>BRLK z>Dk!Y@b2g1gI$sKS}+^k=5>_g8KL6R6?sCY-cac{?&542cymI1C~hma5q6z$xf!QN zo|Id4T$pB~GQ2)Pa0L}%;-FgZ$k+6SITjZ#wUML!iA)CG5zc_TcAlOM=c(0CY4)87 z^BB2f4Ed7R_4wT{%Nm68epH&iq}_6;&m@~o9?bvGB8Z26*y*fBc!CZ!fm%3LchFpG zZlmU(LIcYuvFkh{O+79UH_m<D)ZmGJluX1tZXW3*abVTqTth!g(JUE*>o%5>$3x%b zJO&2xjhoUmJwF1Y{1Amoht+YkRjDOOv^}D~g3-sjQ=IrdVz{IvygdBijg@07bR=lD ztA~M^+4H3`F?E`h>55cOPyE=}3zyvw;9bLpqNZ$@5=@`0rq9&-)q}|Ltt8a=Cr7=3 zU|niSNi|a^y?TLipR%HpUz&hXP6gE*5b}N@7pf=~{%~WT<_~gG(YjB>)vWl_w92VW zr~Z5^c*&#IS7$_0^0SqnmuQ)t^ll$*+$0ZME!U2+uBEuMZt78cSn@=5>}0hzUuntO zRGZ;X4wl>?Bcf^O)kXyY4=tIX-eW)=?|(bDmMfpR8CnY-kC=I-P`zOZPfg9{th%u} z9$(Q^5?WWgSj^O)!dv91f|bQs`JbJ+?E0_?uFqM1u(Vwg9?}TlNsm+Qrt~4}*w>*$ zy$VhmpKkwlEzQ(F(lcQ8bFnT<9l*k_)_-0Fd!=*%try;vIzRp$WnAC~Ne4UYAqDa) zHE?4T%lHp$4NZ5B)wE!7oaN1z|4Fc6$)un!5J7wpBh1#fvp(Uk$i=kZJvRbl7KsMu z{3@0-a8AFA#;&e}0laql(WbtNXpLuRWvO`yCiP!Jiu%V>HQ`%l!_c2*5i7LkoN%W_ zyH5+VBNtKcc#q0XijyvO8(z;E)Lm|3eR&)g&x1ND=J)u@`xuQ}?G|6q%gi4OBKtC` zYZsW5&43_BfF)(lc|=&nBfL)3ig!-tihm=TVR2?%C90>besXaW-iC<@gzCzEikfuT zxVNQ#0k=qjUIx_{Bdc})&2~7`U8l0MtD$XHrBPiFw$7>=^Ay?fYL;Hccg6_KXr4#$ zgK|Qw;pIV)%@WL(RWOD?jfpz?WT}1G80r`unBdHRM)lRWsNsR@xYExMH>c7G5=S_} zy%5oZUE(<2Vd3EaJl<FjjXm@jJ3W;f(0X{mE2;sWpyYQpZzyJ7&siQz=9C%>h#c&i zmFe2p6uHTBjz2!uBA`MYQj}8+ZU3&ArK^L6ND4lSPZCUu2VcIi+&3QBp>s4lBGh}# z#mUW<j$BQRQKQIP&Qh33uA*kE%aPT`z!evrdpCJ~f*u3Y<IV|Osm!*BXl%Ry=DYTv z`7X25Y!;b?0l12YY;)Rp?dvFlypSKQB}nCj%@-}hP~hY&;S<ao*T#x9M=fBAb2_G_ zwn$otcLd&`IAK@=niK5YoZ&po<aTQ)*G2V!JvDsktp;Wln+bi18`n}5$*tZ&@v(Er zc`l5n>-Sqa#tl3P$3%zN=k;$e3|dnJ%1hgg4ZJ&Z-BqLXjoTBO<jx}vQqm%6rGj^+ zJzfIYR+`(8E^-N46oAH%6Af}NXl}_h86D(&fPA(fI~1S)u-M1E)&~01Fe+3ng5WJA zEw21@t*1lHT2g(CE{)A1qiY-?-vJXo5#I^`5nf>21EUTJM{W=vBVHf-A=k3Ngsaqw z!M`)`yrkBzDmvC;{e5DTI}qKt1|-UVuwGaGp;6QN%{!+BIPoKI1Dvj*uVcn>P=~d> zi)`-=1N`rPQE?7h=SYZ=!n=9?RIAFIjKmND`q7EsON1c?SLYcvqOnBWh$NWdDcE8> zMVr4O`_bx`K0ye|q8L~&q)r?FFbl^kz?kDRX%*yMt#_<~`5ZOZ&bnHhXE09x$QN{J zLA2eetm4;1mI+M%XvB2ONl2TBp7K~xV$%q><*U7#{sYiXt6#bdAHtl;34C*hfAop! z9p@dza=!X-6{*yx39hcn$N2FtAr<_GV?ehtZ!xL7+^E%$yCFkeCRn=*e~YYoBQrux z^q)75lt~#+O$c-*80nwB;V3m7O6hImx_TUHF}vp&wg_Vq;riMd&w&Khw|OftM^ zX!y62YEC7R_{1dI4m7J9Kv~BRmK8jV_xv(DBLS4?VJE9zz7SwMaH@IA7V<c^CIY8T zqz|mDpsy(VY+){u@chU%rEhHeVSO^KQnyMbx!>$=N<5orhK)?WHA0V{#>C@?v(Jg~ z+v#GD550Dq-;TIGwCgV^Q!~95WGQ;{Sa2(+2=0c=3<eNAAgE`qKE@wbx0_3%AP9;f z_gi|C8*`Fi91Jhe(F>8|YzpyzvA!&fC@9S-QDJ}+z%wAP4Uel}H-<YaqO2-cKbX&C zpbM{nt>fWsL>l^&Df9V)Pm9&{J;R8dBqIR|c(T=`s%@%0L#O4Ll1Zr}AHIlg)$!~M z$Ux)8^K{Z{#c7hSIgTn4-!MOLI-QIy?}<Ys5z&|9j*`TFvQB%BAND8cL=~8dN>MbK zM8DTkP^>E!{`_!L!H$OJhUToURLCqx{*oG-nP7!(WTnKU;Rs@aB2%dYOG;Ox{`Gf& zfk;lOmvJ|L5k3~O7nK~MicaHj4_AdcIc*Ex1cZidGbyKcfsKYY+fb{%FT|l^x(?W) zJ8u!kuQcglk(T0ZJH58~WCHg*^{PCNlANj1SDQ|w=rC=cq=Cs-7q$1a;B0Kpo0{{< z%TV5>$@O^a#`W!bRKJlB;1PDGD_rE^O^R-c#BU_s5`ULp`y`YwNxgK#=hG<ud7eQ4 zTON3Q)>g=3dlD{CMzs-M8z`y!(OKs(*oVGdmb;uS=VavZUF`X_bW6Acb&0EY*oy;C zxg`5`Ved#8{M_$y4+A%hvJgLrJWz~=g6&0CzqO6++4Gk8m^EI13fYl<7tjGf(B+r6 zGcGL65|*m?T3y(8qfE7k%=7IIB&|#c!eFV|4{-|&J@4SGA15c*XweF53VfcHbwrWB zZpX#2U%avDQ%I=JqHKCeFB&MCkXwJIe%v`R!rBr&5*<V_>+aEsIbhuJdX}Feq!<@# z`B7Ty3FgjvvOsA`MTDBRJ2!ZhP&_u8*lwza!O2=n@fW4FMxOfP;%_EM!&F}m7xDof zy0B_ymR;9R4h5;=q(P%x#{Eb98w*;NkjZDP+bvC-)t`FbpIOm}zB+kK5gX4#0O;+3 zA9%|f^gF}%Ll8y&2xFDXC|P7`GPlq@P=H;|wpL#!wX=D_+iG-Mxr<Ln@SL<J#px%E z1?18`5>3u_;C^|UwrKvu%&8C36e=)Xtg}lH8(*OB=uoTBjZ}U%1oL>X7qX?aB~#@@ z3*<}@84_%}X9+`c)@8VjYTV2@ikI0|aSTWp1a9ocMyZuC79On;g<^2%Biz_|x1?gB zf$Km)`cPeM+e~DWZD%7Hp<AHNg%+@?*Edo9)~jgC4}C*8WSigXZQQ;3^8Xkdha2O^ zL`JB+xBUP(K>uxje{1mU4d`XmfTB&^tE)-(#FdXllNBh{UzAAI{gNAtf$ZK*^^=h_ z#SSdhD!h8OUBac4)I{I63IW^Y`RBEKYxW0QFQHXG5z9p;>i$BnRIZlAk7goO95kQf zUdJ(F+3!-F2-w?&=ubC6Mpi$t$yL6+<r8J(9$Gy}W7#joo@Vm=U8>1+SKGO?FGQ&y zoirR^aQ!Y#`T!a<e}TN*)BT{9P0r<J;$PzN(#1*w8rqiC&4Pl%k&<LkRP3^#*Ejlz zDc-j4j6MGk5xE{e7yj)VzAcJSeomdRa>nbxq)Uyu<Q=Gw9w95`ysXNeNY2vDP82Te zuxy|kFg~Jj(;io4kV1mfhOjh7auNNp$-Txm6Gqnvo77ZyPxrzxfr%X<u9KyUdIq`| zelFQ0C6_0KQ{v*2QQ4;K6VH_>!iCD^evit~WNJuMt!Z|4FqoSfCY*e+_CVG;S7?_J zP)D=^xUO=2aZzLZ5`a%eNt4y~5p5e035R1W@vH_;CFiVX<);FuwCi$9&6w0x!rtkv zrD1V^0g}6|QF$XHFIdW~^6bq(E9gLBIuFwKD}m-CW-Y(;N{oupFSfO$gw)F(ijcnC zrrF8|>%kGKs(wG0`=qU;4c6-~o0*4Q$soQs&qS~2r(nC(qZdD_S1}@Ttb>#d7NT8s zWBwvC-fkD|j?PaZhRH#vBysq|Y<An9SjfNQ&|Tv3l~n7EawLG;S=4|Rr7Fw&4=!P- z$g1yw)1MLl!>A7AW=fpTA3EPl#yggO%&geW2ug7>y4nj@btIGaNg|@@Ts=4lXkyc< zVLBtf)>fGr>~wP5oMeTsqI5hLCOI+!DYpM`9)QV4s9SaPh+!Vv-#Cb~d?nq71!Q=2 zTTAegxC~P%?0kX^dsToIl?$z|^oi?0&Rz`czRs!M|B>odwK3AsSY={EK#KN8?9})X z003e=sxu^$Si5_0vp6gHeKJGMs+?_EEu*yd_Lyc#YwQ_Qnmzyf48#Ca8P8i*Ad~us zbqMFOu`=1P(5j+Bv+PfX<qLq>iwg6WLuFRBb)AzqFPk44o#^m3)kNv|!;_MWBgpUK zEU)xj25&-1I8&0MxFxdCek}%)0K_7F(BkAh+%5#hXWf*1jJ+d1J(Y+0QmE2|gQ-;} zsDfNAwX$3~uj8;;Cgh6D{j3xZnCwJ=s2@!^Hp3>5)C`O7x?yz2MB_fYr-{qh2O`^< z?i<T#kWy$FHxiN4_pY|h`SrTpV%KodJ<p^I(M5V?&WBLNGZUqOSW)ERmK0qs5UBkL z2i>BFy@74=uRa2~Fs}ns=%82bDR=<yuLKe$%W-d&M$sPv1ZitbT|hpPXAlpX{!w_d zx}=yqvOUGDH_lyB%3(24TJ*-`;NZHwDh?I~rJGlEo{E!i8|j(7{x#U?8zOn<mU%#< zSG$Orc!ZqX)PzAMklgrpEO!8QM0B?jKr9L6hR4r{WC)kM;>Py}DswSci$lp|>Y*%b z7n)G$HRMZYY^>0S{>J7E6SYy4X~A_%9z<FW2>StLG{Ex@*XbQb+sLDAd&e#uRn3%0 z81JeFLM1<!nB|WPB-9XXz5ifPKoOiC=$PLJxO)C!UFl<;#IC0j^1|7s6zYCZIMVYo zmAs_ODf}FOrt$tiupAAy3>VGqxZzcLtcI4*ff7aBg{isOm~vi<?&1KykIQB%)yJOc z;b4Aku5h(p0*}?_3EY7e{?S$0X5y2lPv(Q}p%KvnSo|}yV9s9V4egElO0+axj;-*P z`MNT=DjK^U))x@;&b>Hicy2Y3nK}5rvPf@*Un`IWrN;pt^G$Pd)tSQBGE_;hPB_yb zf;RX*LZXV^X5)8vZ;DQgZTZQLFrU*)@LB3u*rGv=yLRAtc$ChMWM#XnjoR+?%b=Vy zq~p#b^^8hM-0Bepo1~z;NuqHicX?rUJ=3S&ig@;KoY-jZV}U&A+&=1Gkqc5JdFTTz zmN%JX(*mWZ4q)rfD8ar5e7uN@oYBFXE=5tVz6u#!X|q7qdkKO932}OMywwilIzHJ9 zl?*aSwUAX>(q4PlV*i)|pDe3H`!a-BFCW_H1_0=OZqr?G$pqPq3vQQmtd#3p4Zm1# z1a}76Zfdoa7xJLU$7F}%sw+>ZwaiNGZTBZ`z<61-m~zyZa!jF$nj4vd*Nzr##ZEW8 z)(ImfFziHoaLUZ>y}tl2>T}y@SyMesw<7JtETY>cA;$293q1=rr67xn`_en5ZG-M@ z17^|*9j6^*Hsv@1%~rM`+j=NiACn{^Bx&;#>F=q86JE9|f%%(c>&;faw6gb0a@%Yz zW;U*DJ5lI1@j4=u8*f_7ra~O)6f+Hk6Oui7sAb$I;8Bi*f|>a!8!6{4R^>Q%zD0m5 zytTX_HzMS1sR-IJ+g{fF2)&;6nD0^)sL+j#Cvn38N2Ov}WZ*fz`ylP$IRa4b0GG|n zk;2<7`>xWv?@b#-B^zvC3FbYy&hXJ9WZq^{_L!*FV|}t8N9eiNM^VOOp?N_p?PjL0 z2sQl8YgIi_R?cB_EwyWwZY~ke&_b#@W-qjSpq2TtQL1*PyIuRIIM}`Q4jSIAQ#ZMq zTU_O+k^#iDckKD0S7<FOoa75<HS(~uAGSNv*qoP>$7R1RZ`y8zsu8Nrw4$pz%4z2G z*7GVp)gH;a+eJ|32|i!k=oYQNPJct@dVG_7IS-4Gu9;RHLLBr~7_q`)27(I-jl3p? zCA{jA#v`EHOH?xvdkK1Li8jU=M^=lh$|+TXz%T_U({Z!kLGxuZ=_^3p6Hegb?T0b^ z&$6nSF+JnBWtUP(A)OZ6)<AkvU}2<TprLom+9^Lv!z9^WwW{cRtWV1fs!BEen>WiQ zGtwpcYH{xdW#mtorA(FtWSHOp0{+~lK5$~T{QxiL?mu#`o|o0ab%yRWg>yJx9)s6D zfmcoyR6L3~FgX@5VO~W^Z<2`5R4<l{4oeC~SIn#7eSF}q$R4&UIAeleDAf39M8gFW z^FHINB>TV9D1XNLKhP!5Lc`KojE&X?(@u@IPytAbMN9kT6H*nA0o=!c8HhUOVAHNu z{r-uuEE|iv*Vnq}>JA~k%&$>$TYH1vslDmeI0H4Er7R2@)9va0DpKMA<M8LV{4M=^ z7eBnWB9f_0{{kRO@os%sLbLxTG|RtB>mmfUe~=26zSoI98g{VmBc`^N>|9^oO*c6P z!)`PMd(O|9FrQtV;Y<eRy98fA7B*M4OL0@iD+jZ6+HledL}|C794=B`MUox`dWYif z9<1*C$Njn?6HD9Kf(HxLh-M#lN~v{WrT<8?q@t#3&i&F#+=_Hw!%%VM^{2X5F|b(p z=$&jl;hN)dpMk^8p?L#Q%M9p*n`@C0j6>6Z{3gQG@+Lo42LkyvZ|7hMrCu*zqp_6p z=iHf`RdgzI%>QX3YvE-%YN6B!x)Ju;a0|7C6OhA7mu6=%=69~H!^}!o7mvSZj!5Bi zTNND%3e)@yeyRmco;_ApA$xeG5Wr%2v6}Ej_;|IRZ{ZNEv@x<?YSRliV1779Xavr7 z6<@C%qc`;tOqK))c?J9?m&u06;<mWtq$CsLUyrPPOQjx#CJe}h2E-VZp2}rwI_7Qz zJMYhzz$v*GTV-_GhTAIXg$`&h65>ekSy*LpWeqQpuruo)C`8{-_q&Ng*Iy}<i@|CP zjY{A)0o42GEFPQu6fdEPEGp+!3rg!N5y*&Af{A%5E<QLQg<p|h-rwluH`YaEGCkd; zqI@wE!)i^MQ6i4jy>JotY%$4rBJL`e-7i_fqu~dFIJpP!YE3>-9B1_HQ7+K&F9Ipi zf(a*T6OZ3OxIK`#s~Hsacr^3oeM%>vz<mXqEGO!#Nej7<v)(g1^?lgKhh`<19#$oA z31GP8&o!9TN;pO?!0_ST*`-Id%x~HzO3$)Ob4>1&){|dL9dlnJCA5-N%<kxJT(wsR zZ@k#aoitPVYG=Nmt|@8p)xD09^bDy{K@WMNTVH$PST)PpGE>IW$6Rq1#G}n1OOwt& z!TlUt+CO<i`_m}Mq^34ri;a@WvZ3Hk%j%>;+`$>1jjSs3NcLX?e4A@&2Y4!A=a=m+ zbz&!2u+<H4GKi002x1|VwOyXG40GqN4w~jC(hIods1S7ZFoCR@;ifM6N~WvHx&qqt zJyI3ugD?kyji35f5)npx&c~Zfw^YM|4K6E-ARd!tyW8BMV(JXssK?QX)%inW_>Z5- zL^TVOO>{#zVD8{_mK*adCy5ak|MU0iI=;W*B<JVMniPA{Dj}&~#%c=}O2XHDoBKPM zh1c`5IL!tls>h=C4{^bovb?#|EIt)sPVgaBw(1d84%J@GBtvT^lRI?I25g(h$y<l1 z^u{o#fk4N@h%i?8^Ie*Di|@43g^y0(P3SfDYMAUO&|LrS1Y`j#wiYZcR0&=h!j;gq zF6QGz#xv8^PI@2@MRO~HDlTm%^&mV9cQjBK=9gS4BGJLlfOGmd+{4zgQhFG6ELWUp zN7_~K>RVn;cGev|X}3eUE~s`LXUPKhvczzp_^V_HJW$eLf@-OX(wJpAp)qP3!QrO` zjc4AN-cIybA&@d*u|IHPv2ynN3DP<u#FssD*DVT>Tp=CN(;nLOtuhN<I69|cD37A= zr}4XdF1apsVAj_$9g`k8Ea;oR8WMixvQ|KvmN246xia{CI!(Q@DVRfB`^9u`evhJ* z9A84%5O54<kGJu5F*JP-&Fu<dtl3KJOQ(w}(SU&+bnO?rO4dzklB)vz${Z6GN;EpY zuMKqWbdKYyuGfsgv+8_}&^;nXj!=$-yfc9UtcZLy9EMM4E4elP8_T)X>I}1m*X`92 zRgK>P`cWKMwVw|v+f$@8KRWAv{DSAKUTN^Hxn^>(bwiU8fntcBMojNZ{5O;kJcuKF zyMLpEcs@*X^jzcS8J4nt8YyDvr?xhbOqSDNl#Z0OOb01PXuex-eQv``Om#h22n)#5 zstM~2{Sd`7mK~Jn-+fv%mJ7>>3Q-IfP0&=wUCOlBz?PNM(5ifj5|82tF^1I%yHtB_ zCjwd;H4$MZ;iQAFm$6J6Fh*ZHq+>pxKD{Foe}b~c;19j*H8zD)<Zw80$&{^Svc|LI zki`P3k`TFJiN8kyva(o>faaa^%q&hI^s=)iGPO}eT8M?9^Q_$HZZ=Kc^sXTrn;e4= z_kldrExFmgWzaqNTn+~{Kcp~Sw^(dGiwt5q8|oSd;ysg{-{mo72vpm;nuB+kW^!s( z&;{gR>r!E$Nv}3<8^dUQonAv(_3#1hKNz~Yx78$nF?1(+h@d9M6B^1Fx(nma@nzD} z(_S?Bv<uva<XB8{m=(JBhF;)watrcU;s#%L!QnKPUz@*K;bN^rPEmU<Vmr(AUl+TW zjx?3?j2PWy&fNV?{Wcc+^KKSfpU;L&5oMZ#E?ipH$kH!QFSoG~#Du9Qnk58n*T)Yp zyvjEw`!svj4UrPT&KAp_JZYZ{o`4|%rxJm~({_<TjNkZ@pk+A*n;b}0$$Ji>X*4;| zca^P)eaq7OGhFHV62dnh2C$-d`htE~eY$jbr(TxgU!~a&O7PMwQ(9k|P8oVUk*3QI zclBz6LN^{$%hWQX1H|2JnGP_T%)pM;6%S&5E2fDkg6IsRF=Ft2ON&56bq04YU`biF zAF_a3Q#OdxnGx?L^#!!mZ^Sixn#W4C_6d0Y$WsEnEoj<4WCu_>rsu3JOMDZMeTt<p zIqY-I?RDVNFQYLd25Zqv)UwodUfZyG^(tdcl1}2hYqx5O&it9dEJdpK?wj?trlLMa zvbPF&wTU?;my4REO~9ZbUWcTWSQX_Zwb?$BAL0t=A7+vXsbxmMZ<>)qWT_}O=kHl^ z23v`}8H7q1L$&YQCBD7WI^Fg9q$9-?RkAJv{8!gl=fG|$V;<kOjX^Mi!hOPNeLmkt zG&N%fYpIOYc^Ne1IQ?CbifN<vzKRAyfY!__Vlq>`VpY|82wy67#^XD7Ab@wUZc};k z!XJiCp<*h8_prB_cb4#?4Hc%(e4b{QJRo&(d`vbOg=>eQr@<8UmHESXUaPT<>5fpk zjUttcj_%BUJ6CN$t5tlF`8g?gpQ)^F8zTm_Fn?^2n!lwi!%O-%>ai@dQZfQaAQ#67 zt2h7L_zY;6#OIf=`DPIIBym*Zl_zkd?oje`Gkx9*9^Rc5K<2lDE}<=u-TAxMAfG^m z15sgr;*9e!W<amf<v`OVKc$yJR_iJ&F+)qmCIVLhmPVbSF63|&ktTvLj0#9zoCr8& zu7q^%Sq90lsA&;ptLMQ0IE(%P&`!I`bC<pDs}HlXoE5(+b)>k!^$2H8>agC0Bi4U+ zd0f*TKiAv-wx^EOg~0XN&@G<9Yxp}CVA{EDX0)z7BiA0qP@Zn00fz)k9ZJSq%92NE za#Nl(*uUW(hOU%cFXZoJBaQ=H&@YZWH+4K>*4U!23g6VW6$!6~(mVc+au(y{EKQq_ zhP=WjcZ0Pso!_XBzs%+5M_(b}ej%1|zj`DC2oVXQHBDILLxv6KtINRz8sViU^&3G_ z!cJC2ioE(&eEt*~a%^<)?;;_uFOD6bww2p(pY{55`40?6HqB~q8~tF?OxG7-)(`Aq zIdhN{baL<2I(Np_Ds1x><VJt(+JX#(Ah^DQT2wg{wh|KpCe#*!FxSQ)P1&)+kLs~3 zCuM}CIa?9YMsusYfxGi<a`yY9gQAxa*CutQg~P0QrDc@XHAW>t?n?RR&)6l^LyrP7 zy^?3at9r9rk+3R{dauJcStgw^1>(h%<%uj;6%7ZbsBg1F{_^X1D<rjAqN)eO9{D8p zrzFQjRm(ZY|AO<1x6EOHaH)DKJSb^g7?7c^TzlBzpA;?+M(je?wJe>he=1BoU<ffV zv)(|&vC7H^^o<%Bt>&2t&oaL5a=FG(p{)dkJkyA0P#>2r65gcHh?7=*4b$oJQ-5xk z{%X10A**NelOA1KWcnm|lG7dOqIcqH62H|cRJeOt^Hs$uhaizqP0zTrf`W@o#EuZ# zx7y#-xl0)if%xF1A}^^>#vo}XE%tt=x95YUgwTzrN87~<T}Duoy-dU|ytxa@O_JQa zym5xhY5seSnF^*VXOM3B>t|(|N5q6Y8(`#beuiD<oCWf)<pm1y$1?kF&T1K+BJe5x zJJKunBWINSU%?pzk5*&!+cJi7)P+_%vUy>-jkX%H8A9%p(MXNIrg91(TN29ni)bAa zw*3du${2icdOWaL(7Qcd(P4b-@|K&v!ykmhww{(S?9zd387-nXMrxE);R)B<?U>RP z5Tv5Ck*{SHd$Zr8pXk7}J}gC2O_cL&*mwsl>xHB>9#vtJ)qGUkKg#J_z&)Srb*m14 zz%f9=m)UqJKao=97(wOWmFf<1bt5$z@i?Xw1C8eLK9y084+BIaK#KD`$jbCnjZJst zFPog4;<MPi9I93YPTTIhY}ui6fWT3HdM$tVmvLvEk)LCZrbo>@=#!=s@>X^1`cfou zPYqBe+ZuFPk5D4z#}C$J7^VSvF^zJ{RAQrATfQ*KZ;c`Hi}Kcbir&ElO24~-Of#)! zBlEZ=okseeIBcPe<-81we6JB}79FFEMcYW@<7gST-`XnkR*zJyR~a2(*>Z1*Wl3c^ z4EPDXDpk%0;YN~51v;>7LI1EOhUuxAhvleks}ia=ac27UJUckw>`p(Kjs+InWE*61 z@Luk#>i<TSsaEwP%M#xBY%Bmo7Ci<)NC{<hPnMOUEFlbAt0AD%%{~{%fPm}vPe9hq zmej+=&FNrQrW_g(?#{30KGnn0Enjyv69Wy^nGW{DyBgF$DiVya0D*$HPjHw7UE22> z{l~7^w2<^vs#)9Gp{l?lx4?<ZSoIm=(fZ(v-NBgqkP>GgU>nKksom1`$ZnP4Pl6s> zyAK3cBNq!Y>3Yt;Rgq|@5Ssm+5e{W5n49lk)G9kb74D2^dew#5FaG4Og$jlz$NzvV z`7x>HQ}(FSkpbefWULK(WpqGA6-q9DF*#cuI57hLA=z9UB)6}#J)Oh*6&*o!J(X8# z#ZUcJ*MCBDhS<#VQ;qOm&rPx@m~nd}eo+)A6VukbZ8-{bJ1J4xM74O}OIB{|+zu7` z?NHUF>vbBeJ`vq-7=?`drBt)X%-l-%`z2B7#_6?KHEZY&Pl^I3Jz3k^BH;n7WUK5f zjJ`~i&l}m_5H3*tp_&fKr*A)?0<$-!9U8u!R`(f+?)@_EG5Mra&Cmy_TNo`J68t0m z*Mc~^|AKA7HfB|af-`zv)N}NmdAr~k*8HGRmRsnXKA%g9eH5r496vJr@KtX`)lZQ{ z&s)CvZ?^?(%YH59pNz56lG%Dz3OIi*aJ(@?h@3v?be>y{Gy>EzXO4n>w&2M}R-8Gl z1y{AlI5})>$*)OdyhyoG+aq%8qqa(qEsTds!?NT6$}i^+EjNG5pJM!1JTi}8ho}1T z@>Lj^Q<VBju@Syi!5J5lmr#H8B{NC^;^b&Pu;u@ZQ?FHO*07#uB;6iYdUXZ6*EB@Y zlptE_Zd6Avqr6P7f<mW)VpWn$L7_rSM&HBn0QUxBEnF;Dfbx1mddsBJ8jxR_n6}BS z7PBUHi8#NyIxnl)(mOl*qj39p@J$jEHz8WVU^1choDhj3+pJzxQkq?{sgdV+{@9jg zI(XKDT6o>o79|YuzB*h*jyz>n#uHU8xYej0OPB6QS^HMu(g&gdrS%{XV0@I{R;kZf ziup5J$G^krmy74ZRmKKbN;J!7e@ed4{*7ZMkXgRH{udoBxSs))0KaNZ7Vv?Of8O}3 zJu8qi5V~~`e*eb#M4J`3sWK!uf3}Y0vmdv~j5J9!YnD?Pj<^<3AO&O*b0h~d)vI~| zZ)B~%T#jFmX3=S!1wW9WrcuL7eEu&sYkzsJwOlC<;hfLie)gZ_ulE0$6T^RgBa=?0 zOK89L!veSd7BjQ0NBf&6LEbT;67@v|HVWOi?Ci^U`ZR&R=Q<gaYL;pbX+ywKio9n* z9+30o&AYxC-CqSu_FJAwEU6y4w=ORK*yTijqDWJZqjdiyA^_AF|LIPXs%Zm_ACB@( z@{q{P7~6a8PT@s|5Wk+CX(a<Y3B##)Ml|oyw`j`I-4YFht2ROjIqm~uUNR#mCDpw% zbo$b)21`)Svo<}#P!iC=Ea<NF{U>5)=vw_$Ton#-bjLZwjw35hII%=))Yy4glSpvu z{fNR{@tXTj#LqH^%Q!s>%E&66d_888^I$tuuV24(8BF%6qXoRv9@pb6j{4YkKVxUc z$I}<Tl%!;?Iz2;jYxeCG;j`o6C)xX3878ye<eHCLjeYHhG{->*d(hvX0$c&SLn)}s z-V4(IuE_R!qU#*<wx=Flx3ujn@fLM+PHi{GdEbqjhO6f<b(cH$lx-U{Dlo9}rv_f5 z&fa@`jge4uJNMDL@TpLoDB`B#Y3@?{^)%8;w+vgin(ZAow|QY^#}AdsRA*9ywMnC2 zFV)X4*eN|_*T9%0VG2<U-l{4M`0s8m6m^I?bxc2ZvQ-4-1+K!lv9;?U$1gPsUpp24 z5uqx-$oyp>cn@CPX4M$brzAogN7elOi^xVM!M%HsmcD*_Iu0K}r0f$hH*~H2Kcm^! zs#ag@TN&r`bo}mvG|Nm4IcY1(Zry#k0Xub7JAbRM5^U#eI*cpb-|^UA0-j#2;9~lF znzjuF+SMY8uJSO!@0KLWM8N7G$A1L$?XufkONH2eWVn&(?CB&#+t5}#I-ef>HcsS) z0MWmLw)XFhf6)3SJ(oj<9{G|^;I!u~4r){<S{U{D<I-w-4+T}mfI(R5D_BspZ9*A$ zhyj<_@bD0);b2o}#Dc^l48G6O(R$Jo8ED5Z>D>D_7pXzor)-IndT$=7js``G6kIqb z;k_93Xb3C(HFEdyQwkm%&vc>^^ZWvz&tGFJ&TGkQ-d_!@7hJf2;&6d!^KJd2@+1|_ zfl|`9S8=V*?yS1tgrk~(?0hPHFXt{hU=;m}nu+>LZ~hm7XtIm>uD;M&=N7l40BXva z=(_>$w@GT=LMlz3^WQMsAe)GTn&wQlVJG_UJXhz~#4iRZavMuGZaN!(Hc0qAV1077 z&^_|f%-(pjyf)|{<H^;`G!ojH<87#20X;l`iYLxq3D>iH0BZAYjRzodl=-x4me3WW zo^Jfc<nrY=1|1?tKz~Afv+XwKo85jK_aC<Oe*PZ2U*fy+3SYAnP$buiX#V;M_w|!9 z!F)!@p(xzD=303#1aR|*kV-kq>8J>x))=l592Vn0r$nSSN0aYNh}x_OyO%xt{QP$Z z{a)7E+BS&1eE-*s_B?~jy&tu_Lb~4#??Ea?(TDtpZp-BLU5uU<eQ?mbBiYs0htF=_ zx^vys>I&dack7b|yprC!dx9R7ds>QO#i*_5lI(KUQcO-K#c2pxOF(<p7%`c8lAM8P z#cIIQX%_z3+bYD*(o%mqJw16ua_rDBCBxbl<8@h52sLT7bN_g6<L$vr@G9IxcVVuo z#l9-e=4wUpU?rW?+Are*)O~V-xyMoM&Skk+4jFx$U?Dbh_d!pq%fLmQdtaJ2{BzR> z`MryXr29|wHsWJugPaaX)_3i=KekP*oq7UhXphYj=Qr86A@v0rzF6d8e63wVqRYz7 zIv-xU%f3AR`qz7q-u#I_>)!&(oe=QwWw$3Qe7SYiVR-jV(!1~V^j8VE6qTHH<{5q$ zf)S^4wsW~20){v2TZ`ZCT1<3Tj>aAbW;|J@BF<uJ&6|ADCh1ESX=U4PHYMK6=To>< zZ#U~uvQM!!Q&sXN7P*gOSS<8CtnpU3{#pD_<#gqe{9&?~T<C*GM1>rf`2&*Xc5vCu zn~Q9-8A5g^xKa*wLH_Ik;`5iBrIQrUJ9*3RpXV72?06&yLBv(szY3!ut+$o@-1OQ& zA?>Z93vm4_2lZQ3>h1Nv=y-mc-3G4zQ*NZd{zTyKT)=<rIRAgg>(7h-FJ1Zn8Dmt? ze>{mq-z)rE2aKqEl-L^MEs&hEFEc-nlpT#@_qay*>^_?N!2Rq^ZB{3Ui|7SIT<Top z_kZiDuSUP)k_!FPH5&Nz|7(A!f7j~a7LeYGk%8+!>tFEy%Shodib9pzKO2gGwnls3 zVN=X4FDh@Sl-_+J23<G_Y!z_r31sErrE@<^sJ+Mz$ekS{jG^d!ev9^|_6x@+zW#;p z9#FHn?bi4k8`O5PCItyoLa&m6DC{p_h=)&@n7bA63@i8R*PI&pemh3S3QiIu(${Z~ zKO7K;#z@rl;eDn8>Lmjlg@NOlyw>w!@0fkj8&J7_SM0N<Bl3GwLGa3Zm$d*=TS=LV z@#8xp=e3`VdM+2j_JS9B)!|w2Q=^Me{;*TH0=l9W_!0fd+e-8pimU62RdLj^#}mD$ zq(?ytQzOK<Gh;C7_fJ*p<6oQE1O(qSyTmAx&PB7uDDDyW6zA@mxg-@)_nM9kEboRC zRX(qDojS$HuXyrS5?!kSq{3DD_;G$ChvdDSET{aWagV(1c6b*dGtoS2?@lZcJDY$J znlE7Z^^+Za{}F$ZbRR^+I`N=v#!pCZ7fIkjC@lP9p-PQUDUoYH{MXARy4K~2`a1J; zZC<Z!lPmP>w!W#Sji7ZzwuBE6fBO<*H1Rv9y_S!e6#xqj8)$Y<?N+hotKS=~y_ch; z4c+rJUQ7J`d)o2hp{m`3Ut_D@+m8&f6_(ct4Yvsw1T5M{zK=a+Q=<GY;O3VHT4D_v zdwO_S`M`TAl<)t=$e1)DW_5y|J@SHUZ15A%SzJXhg!O2f4X9TKd~NMXX$xi#dbZ|; zfo9@HtLIu>E=z%T95bDsvOiV)Qtb>aV*fQ*`Kgls^r(bz;cYYd*^1Xla+Wz|;stn> zYFs%|%kGiAy?Dhjs#IOHjELyB()U9T<E&<@r`AH;6yqA*Jpth}OA%oV;-}b0Dz|^J z#`IQp>e1ugmyt$xsb6PP&FujE?PC(~Ky!G&V%?nO8-z*k4v`2D!-b}%ZLTC~Xw1&3 z{e14)J66bGGIdJaCzX|~d&(9VFUDcAXT7cW*nZ#KTJwiD<6?!Kc>YL2f+UuqcLZ)N z^o08ZDxiKN#9n+#k|AzmE!DvCqXjEHpSOC=+)pBWJW><O_D-gxTgzipKZ9rFgGSr* zXiY#7zmWTZ%eU=z!lc>93sgG93r!ipp*8bD3)4Q{*7@5y{2r^_DuxMZU-j=l(P<;h zaQoI1nk@Q$LBvRUF^E$|G!ug;IT3J~kz0+4B<bBYK)tXfn{H9Ob5G5!q2Z;5vR!00 zB~ytL08`1iZ)${0=IiI$QyYd@e5ime$aU${jv)!lw_PUVESW5fGY`?n{)7fI(`BbV zDc%q2s!jvp25<G-JISV$c$}~!M)P#$CTTCUhenb-3XTmchK%AZOl8U#n!^4)NL|j2 z3j4wMUKycj$Ikb+hi4(Pnw_KBj8XKOuCJC^y3D+s#a1HtJfHU9;a-+M&ZMjO_s{+J z5+2iwnQll@CO$kihr8Jhx+s&_^RQ5>VbI<2Hl63^ck24o6M;z~d&kdsmHd9-1FDPP zBzHKe^O3|Qmw~^<cq9)sYy$IX^<$_QhYv4v;wnp7cwa8#lRnW$N0*L!Q5+!K)?9n? z#)Z-s8%v+4bc)G*bL+V13&rmm4BwC!0Pd#cK7SK0B*1(u8YNM+3@crI>$@_3?Cn8x zN5!oe^({}~nXMQb`*Xh!KnHV~^Y)#(mmlRiLbu3juX-e}ztqO3#Fm`+N@Mn0{lw7# z0f8a<LvKy729*roTz7Z`490}!4XlK@{%|*{10jkL{1%Hb{hQ!LYEe`|c$HnL$5i=m zd@~=Po5(i^5K&Dmyud4Y5MnNx5EwR_9~>?CUXNnw<$H|q6Wd5AZhWBkCPCY}Y58%2 zU`KV-+WrXMNigMyG(*-{!H(tAgHSJmk%Dfb8QEWG>gsmv@Bexcz#5epyy9KCxIlM} zGZ21JW<ObHmD*GK5_E?MI6LP09d?$)D0ST{4=cs^ftHm#UaMuN!5Sabu;2Af93`Dz z$8egb=AzNk^4kkGL1(fV%VJ`_^%A{%Xzfcyd7XL$QuJKvJxz@x-`SN97fnK9>+UE0 z=&i^rF~h(ey@S~}1KXkj{J@#{i1Hsl4&mVbgdY_GR%+X%@P&}7yzJp{+~GF8N@b9! zJibc<sD>gD;NQMOKUEzs>S69HkULGww`QeF;ha4l4_Jg>yn6qulfELEvpg}5^8I(l zGlh0yvn<3BU2Zq9*p-1NrjUE0BMBdzA3oMI#Q%Ko`{Bbsr{h2WnEdm#{}h}2e>wvA zzmN9Mf?HNjS16l1Lb(iwB0%FQF?bH-X_f5l@Vn~&>C6EBm(C0d_m$ZY2P9a&QnDF9 zpMyUQoz(tScc+V_EQ(vT&h7Qzn3lh8%ytcbrXzMd?|=-XsP58IVYEAzw$L{aa?l4@ z+|N{@CJKHMAkgsUYwVqH4B|JWPf5kF3Z+^}-nG;GXwVw@oYo$U@r=S%O#P`RB^78X z<qqE4o=+S78V1u&yjWr7<({*a@f&KHO2a@(qzYAlx~BM^3{bmhX#YF@p*rmFMQ`%B zGCmw3<dXL++nx5BK^=^^yD48ZKju}Kc-oE|#XaTg$12O#=XrHn`6hX6_8HQu<EUY^ zOo?|2KI`GC_EKqU`e-Q@+M+PHGN9<fIu>qN*EhfNvx_N;+`f7xw6E0S!s~M8l+fk= zf#fol)`5V|`~^WBDU3M*Rm;StQ2PX$xqNI4o6_8vU^#4cw!BF?dUoRNH|qKP<=wm3 z-@7tqu})i-g293A(>xiYl9^iu`+J`g9<VS~4|LAT$CfkwP-3*H7-pJ%l+ue7H1Jq9 zSubeBy?d5&KyBNTzFF_lGImpsTsEO@4I;9KRI6D&JZP?xhW2lUDJV>wkaFg(y1al? zErc2AJFOlp;d95E;FJyl-$R<D?XIG-`G!1e>${$VS?Fd)O)(PO_pmjzqU=~2GxM_4 zrb^-LKx4qpV-;u9zGciTq9DTC9hzx-fS?dv6_%|VtFmqwA((CrqIkW~`E_ri+t9$T zZ&uH9IE3yYjQ{;gKvhgIvDai7c(j*zc(F>Sk%7ui9qpsT5jyhb19;QdNXf8nqZMVG zFe`L8uhZ35ZmUt;x#4%NIA)k>JGS1ZOrBjcFQRbCv?b`!=<`KLO{dAm&2@;ZfNUCk zNpY>zre4?&cYFyNGIyLzUDe~x4y*B8EZcriRu7&?9G9~Nfp;DOf=YxzhkIZfj~6r7 z4%iVX@I^?u;~CPTqZmhgjcetLUe&kXdB(xuJg?VQ;{o6ljfh_{`3t_2?2bpq6yF-3 z^(n4->SI9it&bO3>tg|Y_#3m@s>1Vk`s$LIM+Pf+YYNc{*Af*32PglX3*c~BSqSq1 zIyHc1Md!zrt0yVQNu0zMG)finSO{nKGar)cjvq&VMRV)H!2T{bt=R&4scJbou_}BP zFXK&HqsC>v<o3l=6?ocl*1%4^D6Xl=h|F9J77+PFe_^nv`c3JA6E%Fqei=D{nhl0k z^6GPy?q+|f0iJ7g?<)#R+|_NQX36hHaHS`SLVOWrw)tw+FM!1MH~^nA(ZMihaX@rr z{dXAz1H|I`LGR`4kd>1s2G&Mq+Yeh;lit>5Gd42TcSc&75+{vMdGcL#n!--*lRhml zILz#xd0}awH-z}q@e}G|y?HeGfo$X9f#$gHWc6lR^N9{J7M8dw%RA+(wL&*@S;psn zmc$FsNb<@A-}psuJy}X2>r=RyZdFdp8mu(Q*S<`cFM#`khA@_3#cWY|59F`<QrWG3 ziM}#qR3c&L#k+FYLF-T_XPma9<H{mz6by_pdz?{o<>0W<DJ_V4aL_Ts5=ikv2x4PC ze$_gtim6%PN*5-dG{RgW9;_~(JO3SUR-h`gx=yMsNH`QrN)40X^yb!7tUs>epPvx) zBY0tzRNrY)@uElYn4IflXY6%tU~fK>iC?tx#&Je$DZ$|d>-alI+9!q(RQo>i*jCMI z^YW3_)aCE0H_-9@sWC-l_p}aj5Oe`kJCCpB%<P*Y={=QEN?H<9@1L!}d12%>8M*Hg zT3Ni_%P(5%ejX2eEOTD5dSfJ-vb%mU)v)OThh%uMQ{)8nWGO-+q7K%+hS^u+));Uo z#T;)qKa`@T$jR%&hr#z!TreiI?z(zZxVU`(WY>tR+`xToHDuM-=(Y1Qu{W(^#c#8) zGenKAT$a~77PW`-&H((Py$Cl(c&fzybZ%anAWI`zn$f%>9fcGYH2D=wR`7z~r1f;E zVsl(c0IWhjUiQgzchEMjvd#@D+WkU@=WLqC<KXh?V^D>r!mOv-&NHM-gMqP@FK8Ck zoRw4Mv}L!L<!@X^rWte<8aFU^sd6o6(j-zJ$CX;R-W#(bayj-jZ#9@y4KKW;wML_Z zu#FJAO7QBLTrj^&Da(sfWpuBW@?7V5_JyDA^v$NLfucL-gF2H5hJw(AZy{h#IbLC| z4?Dd-T#=(`^J7~GrrkA%1~QflHutv-1wzWurtQU2$6l<#QPAakD-2y0HuWhx|C7~Y zU4l<D&7&fj3WIx|FF{-JqFK@#_in0TL>j%hwNZnawWQ*syDq2hFJbCV%{G%qiWtFy z$q^QvRjV%6FAOJZO!P45(Ca*-E`xIh5q;ktUf|vAq1N91>R$_TydQX$D%Idd_E`KK zv~$4Gb2AwSC%L%hVmoiZF>oYrCi#1oYR<L+ayu>jG1-qcV9mrRMA$0Ue4!_LZDk1~ zsO$=5rLWqWh^7%O@UTEN2AB2HZ`nz0nu^eN+{5SA*8vu6a>QwYB<H#mG^=7>QJ}g| zrNXOsYWbd`f&$lUo!!jD(_U23r)2Gz_+Go#pTv$HJWRXi3Wu=SL?OXk_wF-zaZG1( z(Q$804039UIm$jgl<$AEcb!2^HtiZk;e9~@3WAD)B7%T`(n1Ijv4DUOdPk%qp!7}> z8v-IA2+|X20V$$%=~X%ip%{9PA&?MiC?V(ZJ#)^PIe*TN^W)5!`DQY+Gf#G(y?6K8 zoonyCpKI-xH}8!we&}H8Zl%lo7MDz_+9B(Vo@CFfkSI_=jBm1k3^(a&^n&s!%fVhN zT#{M`$JB|=KF@bIySY}}rqOOY=b>KQDdj%+<agg1n}>i&a@qW$TIBQWxr$eBwyA?A zs(`60qSJhjW<2^eGirc_fZdiC_at`TygZGVIJu$b$Puto+^R$;e<Ws<Sou69jfa36 z6iF3&gabtFfv<$Ld$9LtH<xQzJ8r)*#rjv##B+W&97p-u62v2@42=?p%<I;;y9}0- zmM3waDcHCfX)|i)>jQ2LEBzVr(T}B9-H*gu+3kZF2Vcr+%S#UW(;2DCC}34UX$`!2 zQMYc#$a)eIrR4Y2+HGq7J5C_T-_0f1ZD0%f-Um5U^?3Xj(bR@P{F#;}c^)-@X`zBo zRa<d{Y+#-<XO#voK_e-(CjCL~j$JJuPr@3_QHqVmqn;YzCth1`Avit}jc3EIlgs0A z0Xu8+k|F5)tq3T&C2k<5#BXY?Oc1$>)2n?l_^GyI*ZpiKyPJt{jpbbS*-hsp->N&! z=Mf&)K`&=Q9c};Gw%`jaL)(Z$=Uw??GN^$heHij_@C<<)Ca=;s_H_ny-I_aGT02)d zQQmdcg8FL<rt;CXf<!^z{PgO|+n@!S5C2m^6*}4+^9E>`7=&^=b*qj+eXlq#AfSAC z*abNlL2TVG2fb$iz)WNhe`=vnBrd@lgRsT@y4{dpG`He^e*1Bf{)^D^#;aeD9cn&M z5ZhvgKljJ_o0;*X!b1zKig9@Duxb?{xm|@hC~xQuG@$8K56g*-q0eC5p!FYYC4OzE zuQ~t2Mzcg`0n*3xujF4{@LwnhXxoW}fjqzNgBsmdl20F4ukHNJc>?ghd-^#u8V;-p z*yaG`o8Awezw$GB?y8??Dz~8{lv1;%QGfi~cY>**oYgxytDOJ*o#Eh{{V97Ni17VC z*<Jik)lTali|7{Z!CHge$6Iz)+2(#L%`~_lzE-o=y<Y!gS{@#AX_5q7zu<%KY`Lv^ zusYzu@M=j2LXj0+CUZqugueQ;*RkgHKdNvVAVk7GZ8wZ$qDp$$U0F7!4+a=33|$1G zC&r<6M0IlmO_x?#`gQx^kus%0kZx<QSVg#7-w#@>Z#P8xQ{vMx@KsvRToQ&SqA72J zkFjqmYx8Tp%lS&^TCxJHv8u!Tn4Qq<^G<i`%ExX|&+J)vLS-m(2LfsMLXrjp;^AN$ zJ?uB}=;APnRz9YxMfY^Q99s{jG=w-mo!k%F@Xo$1{83eLgR}4gf>b1sH-@iX40E1N z2D{s>*-VYj@xM^6qUMfhWcclkI15yGkq^VPomFn$cyKnmG=#c{=Vu-aM3I);cE|G= zh_YbfNx#&$JlX5ip&3wmd1#0J_Q2{SbzVtO3Sn&;M++Ri<B+<Qe&{|hoEwv2Ia0l~ zqOsg>rChq+G8|v;X0j^&8{u5Fo(2_Z<m+^{97E}CZpFYnzAaCi7asqn_-$bwMe<S4 z7+#7YfE%5+hIe_Tr}3FS8Ml$pTE<kn;6atgW?v>l<{%^K(YQBpX`5O9R~fB2MLMix z>=b4d)Ha8^7mAt>g4GS?M_1QD{KhvfiK1c4Hea3`RV)syL0KYzYn6peN(QM{h6^wm zS>6V_q5>p=7OW<YIfz`xuYpOTc@T4M-@B0Rt=`4iF{~`Yg;Da0zxr$?6jz;Q7Fy>! zj=BNCx?xPBL{%haBih0(14Lp<Z(FG%o=nSzw44O<<neQf9b>)cO8M9>%L=#Qx7cZ< zmNOYu^=&#ep1!kL*1svDDuj6&h#cnz?6&zyUCm~`_3N-LLepb}w2vd{hWdG3<_+<u zXojZk;*gsO5^p<6Jtkp#jA|A5q~F2IT)|L*m;#hn$#hkEQ0Vh~fPyNei69s&&}pql z1%`5VgBclVCq7vqYL18~ewc?&{>;J~?NcWrT8A{JL~|d3@6x4Uf`@R)Q;m_$WK)qz zs7IgXB~%bm({{7)h08_M{tU*(GZ;9o-z)5yw%Y$uXS>iU9iIFpo}#LaoY+`=XF6^* zUEyJ-N{XyPr})k|)8A~vrr1Spd8L{~^i#uLk<l_(6eX+7t@nHO)4`^2g%h8~-GjV_ ziWv$A+dsF$t2oJ{m$|}uFERF58G(ib6K6E7`NJ-#*6&9ehGk>&P0B)EBbUWd=<^sa z-&DT_YHH7TdVqPA;HmRRC5XmEm)>@GF=yK+|GZ<Z&uX_`Qmn?EC!Z^Hgux#LQZtzJ zZ@pyBGR&T>1D(9$OB_Nt3{Z<hRFwm}H^^00Y)zR}bFubFB{%K9moeG<=e#Z@D&>`X zaNjBIs35=>!d2EMxd*I}sViTDo)Wob<EeDZ-RYgO_%Lm3fQ=0OrRwfBOqMIso=unT z6u8iOY2vQS#ZKqX-Td3^rG3M+gyp&1+__CA{nMVew|2<%>rCO(c_j>gGodM9>eK7{ z{kvN`5l49E7j-7P<xvdPyx}>tWK_~qJR%eq#lFHb%+Y4wDl3gV*8)z1Nc&7f>$kgX zy9)R|uI}m94yPxKczijPxqNFmj|;dHrfsSH=ss?T*KcLKX0)c=Cr@Q#b@b=fD0=O< z=Wj`b*B+T`Wi8Zig`(AcjK}v-%~?=;xr$?xT)Vri&Q}G=ohHjm-w0aD-EA3b7@^t$ z+oQ4SR!Ok`^*ygmZsw%Ei7BCP>#MiXmirUvvK^~Qa?#86QNw|Ty?)lr<?l%W3YpTB zOH)|4SlE&Gk8=IHNoen(TOlS=<`q!6{kbJAJoi6F{wD$IA}&;1sQT{Toq?!L&NwwP zM=wtMz<6r?{V*jx7n5=R#Kj=;FP81boeopyI;z3^##qefndKG!-~i>ma~BnjR*+?b zWH6!MLcWR?L0XB3YSWxz+2Sz7!fM?2pmX`Dq80lD!EJXBalvoE09fhPdv(78lbti( zAOwOk;g`=bO%eXX7r^4Y6Xard`Hbizg~f)R<J7$$MDV+MC|4CrQfyja&}Fvh+UWXP z%ggHWaH2n{(N#HX`34{%#&mn@pJ0i9CI4@GA_mYAW%6{nc=}W7xn?!LeCskN8p#H5 zg?c7K<|B*(m_RDWSVuC|>T58_ogg4DNEWqsh`KNgz0#B^I{QgUF5lFH6fVdD@S~fX zb1^V*ku>o1FU5Qi2WbQmvONxzx#(n4Y(4YyFHk|)^H(cBbDAUhtK8{5j6?XJ7xHTy z$_pzU*SUZ`H~W*@Q}0rPe%?-(4!^WTr84k;c(kbr5UW^6^r~<@sR#hh=!3sA;$S<G z@(jPD_U8r6f@|Y@{i%LWEvwk{05u3B4C%|N%-T16$PSqUh@>?gw_$7~J&9F8(gUQ~ z^{oHouy-B)jiVQ^Iw|kZ#bf}UX_~l_&}%qs&td3Q><($L{>Pxrzh^BR?+^&lj&*ZV zrT4~<%>aT?O{P8i8UlMewUqwgeK;fZZmfm88%w^r#>xCgHYNHCtTPAXC~ccGDvjYb zzTV~(%|D_-uD&22qq-4s6%Y;KN?c5%U5%j~;s*Rz$19#KU@OvQq+P~o%Z0Tp`TiR2 z4B7${YawO578`T^G^iUBfGj7|cflT0l}~yq+jgyM1Av32%Rrd&sJkBJ^_y+Diy7s$ z2nSsGw-0twjuykJ4*J{k>$}-m?agSViZeDdQi^~;DpS*p`XBjfrpv+qgl+t<6OEI! z_|R&5LQNmVW3tq5b961O>M0{|0B|ufwJrZj?T}N<eAXH)Dpz1rqWNlnCf`uBDM_uV zIcZ@pDXF0r*&3%IV9t15<BU4k+V>-gJmLj#$CVZvK$$M3zml9#HQV2Tmef&A)XGE% zm=UGh6#zP>{3&~jyRk6y5;E0Vx?UKlD-K%e)W;iHA`*>*snRD~Y+y>8`xJi&Ry~?& z<N*h$U?dz0y8zza<tvBeEu35fhG0F>rz?z51tk=Ppk&tuYE?mc(SKku6{4C#I|xT^ zzyZMmcP=3Mx+EVPePM^<*V+hAqjpW#A1jCgFf2=N#MBk!8v?b1n5T~B;)x6u(c6Bf zJ@s_ZsGuiw?ijE?&=?SRpRTL#Avng8w&Hf|AM~#`_h(SP%zPpPGv=a+l6Bxwx4avp z&d2omU3in4pIIZ3el>Q>0k~|VRX6ME*CV>*F^~#Ex1jZhq$LTX2WZMMg{J_$^(+wO z_U}c0mHUOzYdQc(%-;M!%kjaDK0OAi&pv)JnBt**;-Web1=nOtXycBVsnY>!-~wit z>U0?a7@t3znwviEeW`+!Y3~1Nlk%$M&3)zx-pYre){Ni#>nF`%3*nN6{1>a&-xt}S zd!@=HZ=`;M7==sI)S}k^z|7`Sf7TrVt*sE&0jY`{&yB3POD(2jKQy9hbgV8l=&@^5 zuEH8y+@P^RZoY~|!gunue-vxkG`P1MdJ)2D2}pGLIYoQjSQcYig$(O(*IlQheXRO- z33qfbBDU7V<SSkVp7Rn3XB?&eaP-I;<`p*sx&S13Rk2exa@q**yHMysm-QK{AHCOc zP<(&0aMW$-xjS;7YK#|jf;C)U{4o62CqdmQGrSV}8{SIgWS$$RRT3bdOr}O81M>BS zjyk(U?wF~3(dastbuY(2M8pz8jjz1Qw<x74P?lTyr-6W6O_1YY(?bvOsS!A$@0<Zw z!}zo{njC@&H7Q#UgP>@toR8-g@26<^`A&G6;c1iaCIx7QKD@6(;~q$r-qjY5e{ee( zmRK_!^bl)KVC>K9$%}HPq&(UEJB)b0j3SNZEr0Cdbr`2K{}v-&*uf#8Y00*)lJUL0 zD+3p`JY%eTJbrVvhR^~SGost1ri!oLiQWo(iG)d{sQsM9&Y*G$d7i%=e{}MI&IfwC zHXkySXs%)-9)k$t3E?>TMPb8eeq+ZR`22cT%6o4lX1?5QiA2Fx9@-Emb7&E6p15M- z35St)&IYH|vjKP6Lj~f`5}T;uu#uiIY|@S7WC5f2PCakXkUB>%{20@x{l67EwIxKS zO>G%Io5Q52C@*Y-`bYY8Rcf5~$q}Wp+}k}8?&byyvR)&#x!~^}9D@Z$XN$L%wWcQK zISGb7tln7h#mU^#kW>Vhy3_+*uIm&p#mh@7E(bHD`83_b?yN1k*wIvaaUy<qvcq8} zTnfUTJ!#!Psn?SkwPPLxlZ6_{Y#OrZHZIO*`SR5d-$-1&)c*`Mb1fGgh!00#tiJ@Z z=b|}@Hbq6-J~z@0l{@E08|%NekDKDInzQLLYW2>DRaVcp%u9ly^$%a`cc+fwd>kyw zT~qgAA7XvUhBf&B$U^@<adGj3`q(Kk0cJs`UFj0e^i?hsr3utho3{4~Pm#db)v_=L zO8M};=aUA<yZV-g#mpez0J)b_tMFW~>Di@Wp_G~0biKz&ik5h!iGCQTg>kU}xH?Dw z%x>LFgVT=7cD-a9CsuF?vZU8T*dMa}DZ|FsJXo!3IG&kk;k=wDTuY6XTGiKcGBno5 z>a`z(ts19<8^mo>a?9R)7+`~R4XR}XJJH{15<BV?2Rl#j8kYsF(24b`-#gkrH($kv z<`{Qsq6*GH0OdnIVBJ9yZ`K&KM;rCV$!1-_`ShFo4h)_pI##}KSRu}*{M9ZvT!OiE zV+7jAd-dFhm79D?XF5-n!O<`w!uTuGy}rbu->Hymxg0|z1~5nQ7Q#Tuz}q{Tns1|I z4t2@`yAeM@_o(!ATz2!zj-JWQ^LJD8c-=#{zRYK`wYOg=1t$#xXmw^UaVWf>Lx?Y@ zo2PufhF9Y-U2RN$;Dpqwv7DZbmjDHR)KcK<R32xw;KMqIJn&M9N})HL6?dhVHl~XW zmacp`f^_K%eN<|Cca@+l6Lwcz++iZ0Sk>JqL{JvP>xq4cP0*^2zpL*6MEL$QhH1Xi zV6feTtt5l`{f8KZ!LDBPLYY^ro0i|IY&-OVvy)Jc!@#FF^hv$sOBdrVgxd`59r}5d zNKa&_DkjcYopsnLZ*S61A(K^A4(&uaiM!w=c1eaBlnc^&{pgGFJ`%g50)b{X>tui6 zmhkT9F-&ilBM;`)T^5t&7M8s?AHAzUn6C1r8C=XxC>Sw_b3Yc9rF|g`0X9B~XOT>| zvO0d%vM}pyD%#j;&u;!2(N;0~%lQB<Lbn)qNm2Tyud&bT$t(90(Y_E%*1X}C^f<#V z?o*V51PN{nN3B4BpUa=|e2*EAk&uti8vYgDd&rP(3321JOGl|rS>bqr6XwfR_b=?8 zGnT2D``qlv=4>Qx?UVCT>;ayr-Szoedy0|fQiPU+&o`s$_jNG*dAX4QsYDj=6i%97 zAQ46{uuJK^&hO;NnpqU=r(>+}9!j(8J;u4CnZy76YMwVP`eAbnFY^IUxJk--eu23F z(Cc2n%qeU15^its?srHz+NZ0TJnikhkdIC4;WnNi&8LW@r0{hmXNza-#cyyCBk#x_ zB%aqDntQwTDYBxF536zBL0rv9T-Fh5b#}l2@^zla_}3j*1ugQ=9B(In<4<0jzDT2X z#q6S!ccXC8i_Dfz>-VwMnwkP}-zQSO!|*}2UFYQ%y5A*r^*Wz+W@38J`DZG(Ik)F_ zvpI<Hy<RU~=Fn2IhZx`FJ5{VD{Viwp4#gmE?g8Uj*Vc=9!zuu@;khI=r{H9~CP#|7 zB&&~)v0iE3hf;ZYS<d@&#s`=LYP(`ny;LQzaMQy;*=2rm@wkUtmHsF%Zs~It1)VM; zDq>QcaT!AfSu~!!6ayY(B_E9))g(}wOh8fKSZSE3jJ=XphY*K_9NeZeM!1_-v+LIY zFE7Upq@8HEh+`I}qEK7s=4ZvmPGvzE&Z!s5=Sy?pT27Cbx|R1ZsR^zX?&k;RB?&pY z+1(-K6OTq9yJOnjc%xhefa-^Si(z|j8o&9M$VY24E7#1m>c7Lv3@`QI>FlzIg_f@D z-P%E;@8Mpb$y<i6EI)1y;TqG5@;puPeIGxr+V;;7#yH8#1wyrXn;}(_BXRw_U-Ln% z)nn(oR`>%FE#fb@4qiVE?}dY|U9XD7ao<8)R8XV{4@5Kq982@u`IHQyWpzVmuK8KM zRZ3`d^WlB>Y~sp@J|<(bW5c8VE+7zx*7Bd?p0nuj%2YnU-+_c=eEvHgPu;>%7O%+e z7R#mvsU}Q6Snm4j^0=T=JL-k(@+!Zr18aOjJu@*}0utb)?)^^vvq_I?U#Y!+H#rb{ zMk9RCUMfV~i<HZ+lBWgG5+i<qAc*1(1D7A@9t`xaDxw1MhUq-%M**4Q-Y|&tN6Cxt zC*8@fBl_)kAk7*RUJwc4;CML2nf&n2(tSXF;(F@N_6|R%Z%NMwWse6n9q9C$_|0VD z;Ei1WuU@e>NmgmJVgxq-wSk|eHj6Y*KugcPb7FW~o`X}@J5Q-}_8N%!?V}MM^@H+0 z-bunH$$FKYzmb)C7T%Q)md&kQJ0uL2#ZsZR4)%`R{xW&V#b(~*htKXV>fXoiw$&eK zDUWDp7-G{SZvyexk}<!Is2#^ZCNpyZI+tYa8VjdX=Uz-8hb5Y3t{bk)IG5j{(!%a> znPd>*I|+p&p|xkHKitbX6BNeBG%K-+w$b@shMJ*&!3-tM0~{hv^PZZKKW-K(a1J8e z)GYIoIYJ%VcDLUpY@W&<UL&Vj8KK_Y5|?BYqM7hEhxMxw@wLn!E{$bjeb0Cs{v;<+ zJTN8Q)=MX!Whns!3F_>1y%A~f!r|6f!KgnV?^hyojFs!dhV~Pa#)pYIz(xcPd>?a| zbYomeDm1cT3ksO98h+u==NJFoqi_V=0>ogb$=Lyvm5Jq#naoTo#McTh@4JwICrq06 Mb?<$-^Z4bz0h(rGpa1{> diff --git a/screenshots.manifest.json b/screenshots.manifest.json index 15a8e2e..07af5b3 100644 --- a/screenshots.manifest.json +++ b/screenshots.manifest.json @@ -7,7 +7,7 @@ "playwright": "1.62.1", "browser": "chromium" }, - "sourceFingerprint": "cc6e5a6a445fc20bcef7f8117364f5003c223436b9df49448b3140371c71cdb9", + "sourceFingerprint": "d40a3c61db5ed6c7b75500f967077517da3b764aff4adcc131041f6cc29ce3e4", "scenes": [ { "id": "desktop-month-grid", @@ -129,7 +129,7 @@ "references": [ "docs/screenshots/README.md" ], - "sha256": "9540be650dc76258b1fdff3352a96a32cdef07802e000763ba62261c1bae6d0b" + "sha256": "b844e61bb81953fb0592d207198945c589378bcecf68b3ba13488a4cb362f056" }, { "id": "compact-default-counts", From 6ddebb0bd84c852230246de58de51aec33a9cad5 Mon Sep 17 00:00:00 2001 From: Basi Angulo <6586019+redbasi@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:27:43 -0700 Subject: [PATCH 22/22] ci: keep Firefox validation local --- .../references/litefold-calendar.md | 10 ++-- .github/workflows/ci.yml | 53 +------------------ .github/workflows/publish-nightly.yml | 2 +- ACCESSIBILITY.md | 4 +- CHANGELOG.md | 3 +- CONTRIBUTING.md | 4 +- CONTRIBUTOR_COMMANDS.md | 14 ++--- SECURITY.md | 2 +- docs/browser-support.md | 6 +-- docs/change-delivery.md | 4 +- docs/example-deployment.md | 2 +- docs/package-verification.md | 11 ++-- docs/release-administration.md | 2 +- docs/release-operations.md | 6 +-- docs/releasing.md | 4 +- docs/security-model.md | 2 +- playwright.config.mjs | 2 +- scripts/tests/workflow-contracts.test.mjs | 53 +++++++++++-------- 18 files changed, 74 insertions(+), 110 deletions(-) diff --git a/.agents/skills/commit-and-push/references/litefold-calendar.md b/.agents/skills/commit-and-push/references/litefold-calendar.md index 7e9de1b..49376bd 100644 --- a/.agents/skills/commit-and-push/references/litefold-calendar.md +++ b/.agents/skills/commit-and-push/references/litefold-calendar.md @@ -33,10 +33,12 @@ Resolve the current Node and package-manager selection from repository manifests dependency state from the lockfile, script composition from `package.json`, and hosted behavior from workflows. Do not freeze those values in this skill. -The contributor command index owns local command sequences. Its complete -repository gate is the gate run by hosted CI, but hosted CI may add platform -controls such as dependency review and its exact execution environment. Report -local and hosted results separately. +The contributor command index owns local command sequences and the local versus +hosted browser matrix. The shared gate command checks Chromium, Firefox, and +WebKit locally; hosted CI and publication select Chromium and WebKit. Hosted CI +also adds controls such as dependency review and its exact execution environment. +Report local and hosted results separately, and re-read the command index before +choosing validation. ## Repository-specific safeguards diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf13eb8..f5e71ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,9 +50,9 @@ jobs: run: npm ci --ignore-scripts - name: Install pinned Playwright browsers - run: npx playwright install --with-deps chromium firefox webkit + run: npx playwright install --with-deps chromium webkit - - name: Run the complete local quality gate + - name: Run the CI quality gate run: npm run check - name: Upload browser results and failure diagnostics @@ -65,52 +65,3 @@ jobs: playwright-report/ test-results/ retention-days: 7 - - firefox-qualification: - name: Qualify Firefox (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - timeout-minutes: 90 - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest] - - steps: - - name: Check out source without persisted credentials - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up exact Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ env.LFC_NODE_VERSION }} - package-manager-cache: false - - - name: Set up exact npm - shell: bash - working-directory: ${{ runner.temp }} - run: npm install --global "npm@${LFC_NPM_VERSION}" --ignore-scripts - - - name: Install locked development tooling - run: npm ci --ignore-scripts - - - name: Install pinned Firefox - run: npx playwright install --with-deps firefox - - - name: Build package and examples - run: npm run build - - - name: Repeat regressions and qualify three complete Firefox runs - env: - DEBUG: pw:browser - run: node scripts/qualify-firefox.mjs - - - name: Upload Firefox qualification evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: firefox-qualification-${{ matrix.os }}-${{ github.run_id }} - if-no-files-found: ignore - path: test-results/firefox-qualification/ - retention-days: 7 diff --git a/.github/workflows/publish-nightly.yml b/.github/workflows/publish-nightly.yml index cc1b85f..997431b 100644 --- a/.github/workflows/publish-nightly.yml +++ b/.github/workflows/publish-nightly.yml @@ -249,7 +249,7 @@ jobs: - name: Install pinned Playwright browsers if: ${{ steps.registry-state.outputs.eligible == 'true' && steps.recovery.outputs.restored != 'true' }} - run: npx playwright install --with-deps chromium firefox webkit + run: npx playwright install --with-deps chromium webkit - name: Run the complete release quality gate if: ${{ steps.registry-state.outputs.eligible == 'true' && steps.recovery.outputs.restored != 'true' }} diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md index 5e2c0a5..824fd15 100644 --- a/ACCESSIBILITY.md +++ b/ACCESSIBILITY.md @@ -125,11 +125,11 @@ The [DESIGN.md motion treatment](DESIGN.md#pager-direction-and-motion) is presen ## Testing -Automated unit/DOM and Playwright checks in Chromium, Firefox, and WebKit cover semantics, native links/buttons, F2/Escape/Tab behavior, focus retention, compact and wide event-overflow customization, compact layout, RTL, reflow, normal and reduced selection feedback, native pager semantics and fallbacks, optional WebMCP navigation without focus or callback side effects, forced colors, error announcements, hostile content, and automated accessibility rules. These checks gate every release. +Automated unit/DOM and Playwright checks cover semantics, native links/buttons, F2/Escape/Tab behavior, focus retention, compact and wide event-overflow customization, compact layout, RTL, reflow, normal and reduced selection feedback, native pager semantics and fallbacks, optional WebMCP navigation without focus or callback side effects, forced colors, error announcements, hostile content, and automated accessibility rules. Chromium and WebKit run in hosted CI and gate publication. Firefox runs locally for now and remains within the [browser support policy](docs/browser-support.md); the complete local gate includes all three engines. ### Automated screen-reader-facing coverage -`tests/e2e/screen-reader-semantics.spec.js` runs in all three Playwright engine projects and verifies the screen-reader-facing browser contract: +`tests/e2e/screen-reader-semantics.spec.js` runs in Chromium and WebKit in hosted CI and in all three Playwright engine projects locally. It verifies the screen-reader-facing browser contract: - Playwright-computed roles, accessible names, heading levels, selected/current states, and agenda relationships for navigation, the grid, and the selected-day region. - Keyboard movement and activation keep the current date, focused date, and selected date distinguishable, preserve the single roving tab stop, and publish the updated agenda. diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a2821..6c3670b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,10 +18,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Browser support now uses Vite's `baseline-widely-available` preset, maintained through reviewed Vite updates. Build, screenshot, and release evidence records the effective targets and tooling versions. - Adopted Vite library mode and example development while retaining explicit CSS loading, ESM imports, TypeScript declarations, and optional WebMCP isolation. Component layout in the examples and published site now responds to container width. - Replaced alpha delivery with daily and manually dispatched main-branch nightly snapshots in the next minor series. OIDC publication updates only `nightly`, leaving `latest` on historical alpha until stable publication. Existing alpha packages and release snapshots remain frozen; nightly publication retains exact source, package, and Pages identity without daily version commits or npm tokens. +- Hosted CI and publication now run Chromium and WebKit. Firefox checks and repeated qualification remain available locally while hosted Firefox automation is paused; Firefox remains supported within the Vite Baseline window. ### Fixed -- Isolated library browser fixtures from advanced-example setup, removed unsupported protocol tests before setup on Firefox/WebKit, and replaced a timing-dependent wheel assertion with deterministic transaction coverage. CI rejects flaky results and qualifies Firefox separately on Windows and Linux. +- Isolated library browser fixtures from advanced-example setup, removed unsupported protocol tests before setup on Firefox/WebKit, and replaced a timing-dependent wheel assertion with deterministic transaction coverage. CI rejects flaky results. - Added native dialog and palette fallbacks for supported Baseline browsers without Popover or `light-dark()` support. - Kept compact count labels, focus rings, and date decoration within their day cells at narrow widths and enlarged text. Month navigation now reflows without overlapping controls when space is limited. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbf34db..2b90bdd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ Small fixes may go directly to a pull request. Discuss large public-API, date-mo - Use a current Node.js 24 release. [`.nvmrc`](.nvmrc) and `package.json#devEngines` define the supported runtime line. - Use the exact npm release selected by `package.json#packageManager` for the final repository gate. The broader `devEngines.packageManager` range is a bootstrap compatibility warning, not the version used to validate a contribution. -- Install the exact development dependencies from `package-lock.json` and use the repository-pinned Chromium, Firefox, and WebKit binaries. The [contributor command reference](CONTRIBUTOR_COMMANDS.md#set-up-the-repository) owns the copyable setup commands. +- Install the exact development dependencies from `package-lock.json` and use the repository-pinned Chromium, Firefox, and WebKit binaries for local checks. Hosted CI runs Chromium and WebKit; Firefox is exercised locally for now. The [contributor command reference](CONTRIBUTOR_COMMANDS.md#set-up-the-repository) owns the copyable setup commands. - Keep text files UTF-8 with LF line endings. Git attributes and EditorConfig enforce this across supported editors and operating systems. - Do not add runtime, peer, optional, or bundled dependencies; install hooks; remote assets; CDNs; fonts; or icons. Exact-pin and justify any new development dependency or browser tooling in the pull request. - Target [Vite's Baseline Widely available preset](docs/browser-support.md) for the library, examples, and published site. Resolve `baseline-widely-available` through Vite's public API; the support window is fixed per major, not a daily rolling query. Review weekly Dependabot Vite updates, including majors, through CI. Any capability outside the effective targets needs feature detection and a tested supported fallback. @@ -61,7 +61,7 @@ geometry, recovery matrices, and compatibility claims. Add focused regression tests for changed behavior and public contracts. Include relevant failure paths, stale asynchronous results, teardown, hostile input, and accessibility semantics. If a behavior or public contract does not need a regression test, explain why in the pull request. -Browser-neutral end-to-end scenarios must run in the Chromium, Firefox, and WebKit projects. Scope a scenario to one engine only when the required Playwright capability is engine-specific, state that reason in the skip annotation, and keep the portable public-behavior path covered across all three engines. +Browser-neutral end-to-end scenarios must remain runnable in the Chromium, Firefox, and WebKit projects. Chromium and WebKit gate hosted CI and publication; run Firefox locally for affected browser behavior. Scope a scenario to one engine only when the required Playwright capability is engine-specific, state that reason in the skip annotation, and keep the portable public-behavior path covered across all three engines. This automation policy does not reduce the [supported browser window](docs/browser-support.md). Automated accessibility coverage includes Playwright-computed ARIA snapshots and live-region/focus contracts in each browser engine. It supplements rather than replaces keyboard review and testing with actual assistive technologies. diff --git a/CONTRIBUTOR_COMMANDS.md b/CONTRIBUTOR_COMMANDS.md index bc54889..41425a1 100644 --- a/CONTRIBUTOR_COMMANDS.md +++ b/CONTRIBUTOR_COMMANDS.md @@ -26,7 +26,7 @@ npm ci --ignore-scripts npm run test:browser:install ``` -That helper installs the pinned browser binaries without changing package dependencies. On a fresh Linux host that lacks browser system dependencies, use `npx --no-install playwright install --with-deps chromium firefox webkit` instead. +That helper installs all three pinned browser binaries for local checks without changing package dependencies. On a fresh Linux host that lacks browser system dependencies, use `npx --no-install playwright install --with-deps chromium firefox webkit` instead. Hosted CI and publication install only Chromium and WebKit; Firefox checks remain local for now. ## Explore and build @@ -102,7 +102,7 @@ npm run test:browser:webkit ### Run browser checks concurrently -One Playwright invocation starts one repository server on port `4173` by default and shares that origin across every configured browser project and worker. Keep that shared origin for the normal browser matrix so Chromium, Firefox, and WebKit exercise the same server contract. +One Playwright invocation starts one repository server on port `4173` by default and shares that origin across every configured browser project and worker. Keep that shared origin for the local browser matrix so Chromium, Firefox, and WebKit exercise the same server contract. When separate Playwright CLI processes must run concurrently in the same checkout, build once, then give each process both a distinct `LFC_PLAYWRIGHT_PORT` value from IANA's [Dynamic/Private range](https://www.rfc-editor.org/rfc/rfc6335.html#section-6) (`49152` through `65535`) and a distinct `--output` directory. There is no dedicated standardized Playwright test port; the examples use the first three ports in that general-purpose range. Launch these commands in separate PowerShell sessions after `npm run build` completes: @@ -140,7 +140,7 @@ npm run check:screenshots ## Run the final gate -Both commands include tarball verification and therefore require the intended changes to be committed and the worktree to be clean. `npm run check` is the complete repository gate invoked by CI; it does not reproduce hosted controls such as dependency review or the workflow's exact environment. `check:fast` omits real-browser scenarios and is only a local fallback when the required Playwright browser binaries are unavailable. +Both commands include tarball verification and therefore require the intended changes to be committed and the worktree to be clean. `npm run check` is the complete repository gate invoked locally and by CI. Locally, its browser suite includes Chromium, Firefox, and WebKit; when `CI` is set, the Playwright configuration selects Chromium and WebKit. The same selection applies to `test:browser` and `test:browser:built`. Local checks do not reproduce hosted controls such as dependency review or the workflow's exact environment. `check:fast` omits real-browser scenarios and is only a local fallback when the required Playwright browser binaries are unavailable. ```shell npm run check:fast @@ -152,15 +152,15 @@ Before submission, run the complete gate: npm run check ``` -CI also qualifies Firefox on Windows and Linux with 20 zero-retry repetitions of -the affected tests and three complete passes. After building, reproduce that -qualification on the current platform with: +For additional local Firefox diagnosis, repeat the affected tests 20 times with +zero retries and run three complete passes. After building, run this optional +qualification on the current platform with `CI` unset: ```shell node scripts/qualify-firefox.mjs ``` -This separate qualification is not included in `npm run check`. +This separate local qualification is not included in `npm run check` and is not a hosted required check. Firefox remains within the [browser support policy](docs/browser-support.md). ## Deliver a contributor change diff --git a/SECURITY.md b/SECURITY.md index dff2909..e2e0a52 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -71,7 +71,7 @@ The following properties must hold: ### Build and release -- Public nightlies snapshot the exact eligible current `main` commit through scheduled or manual publication. The publisher runs the complete gate for that exact SHA before retaining the candidate bytes. Its generated version binds the original workflow creation time and run ID; isolated staging changes only the package manifest version. Retained evidence includes a canonical SPDX 2.3 SBOM bound to the generated package version, full source SHA, and source-commit time. Recovery reuses verified retained bytes for the same candidate. +- Public nightlies snapshot the exact eligible current `main` commit through scheduled or manual publication. The publisher runs the hosted publication gate, including Chromium and WebKit, for that exact SHA before retaining the candidate bytes. Its generated version binds the original workflow creation time and run ID; isolated staging changes only the package manifest version. Retained evidence includes a canonical SPDX 2.3 SBOM bound to the generated package version, full source SHA, and source-commit time. Recovery reuses verified retained bytes for the same candidate. - Source execution and publication authority never coincide. Verification has read-only repository access and no npm or Pages OIDC. The npm-authorized job checks out no source, installs or imports no candidate package, executes no project code, and publishes only the checksum-verified retained tarball. Registry verification, repository writes, and Pages deployment remain separately scoped. - Nightly npm publication uses OIDC only under `nightly`, without an npm token or secondary dist-tag write. Public reads verify that `latest` is unchanged from preflight: it remains on the frozen historical alpha before stable and belongs to stable after the first stable release. - Remote release state fails closed. Unavailable, ambiguous, conflicting, or malformed npm, authenticated GraphQL, GitHub, or Pages state never authorizes a transition. Registry completion independently verifies exact integrity, a clean installation and supported imports, signatures, and SLSA provenance bound to the artifact and exact source identity. diff --git a/docs/browser-support.md b/docs/browser-support.md index 212124e..053edeb 100644 --- a/docs/browser-support.md +++ b/docs/browser-support.md @@ -18,15 +18,15 @@ Run `npm run browsers:report` to see the preset identifier, resolution date, res Install available security and patch updates. Beta, Dev, Canary, Nightly, and Safari Technology Preview releases are useful for early testing but do not define the support target. Baseline is a feature-interoperability policy; it does not certify every webview, device, or assistive-technology combination. -This policy defines where the project accepts compatibility defects. Automated coverage gates every release; dated browser, device, and assistive-technology results are tracked in the [accessibility test record](../ACCESSIBILITY.md#assistive-technology-record) and repeated when relevant behavior changes. +This policy defines where the project accepts compatibility defects. Chromium and WebKit automation gates every release; Firefox is exercised locally for now. This temporary automation scope does not remove Firefox support. Dated browser, device, and assistive-technology results are tracked in the [accessibility test record](../ACCESSIBILITY.md#assistive-technology-record) and repeated when relevant behavior changes. ## Automated engine matrix -The Playwright release gate runs the end-to-end suite in Chromium (`Desktop Chrome`), Firefox (`Desktop Firefox`), and WebKit (`Desktop Safari`) projects. Browser-neutral interaction, layout, accessibility-rule, and Playwright-computed ARIA scenarios are expected to pass in all three engines. The project descriptors provide deterministic engine coverage; the WebKit project does not replace testing branded Safari on macOS, iOS, or iPadOS, and none of the projects substitutes for testing with an actual screen reader or inspecting a native platform accessibility tree. +The hosted Playwright gate runs the end-to-end suite in Chromium (`Desktop Chrome`) and WebKit (`Desktop Safari`). The Firefox (`Desktop Firefox`) project is retained for local checks because its hosted automation has been unstable. Browser-neutral interaction, layout, accessibility-rule, and Playwright-computed ARIA scenarios remain expected to pass in all three engines. The WebKit project does not replace testing branded Safari on macOS, iOS, or iPadOS, and none of the projects substitutes for testing with an actual screen reader or inspecting a native platform accessibility tree. A small capability-specific subset remains Chromium-only because it requires Playwright's Chrome DevTools Protocol session: trusted synthetic touch/pen injection and programmatic 400% browser zoom. Portable wheel, keyboard, disabled-pager, responsive, and accessibility outcomes continue to run in every engine. This protocol boundary is a test-tool limitation, not an exclusion from the support policy. -Use `npm run test:browser` for the complete matrix or `npm run test:browser:chromium`, `npm run test:browser:firefox`, and `npm run test:browser:webkit` while diagnosing one engine. When running separate engine processes concurrently, follow the [concurrent browser-check guidance](../CONTRIBUTOR_COMMANDS.md#run-browser-checks-concurrently) so each process has its own port and output directory. The supported-browser, device, and assistive-technology matrix remains a required manual release activity according to the [accessibility testing policy](../ACCESSIBILITY.md#testing). +Use `npm run test:browser` for the complete local matrix or `npm run test:browser:chromium`, `npm run test:browser:firefox`, and `npm run test:browser:webkit` while diagnosing one engine. When `CI` is set, the Playwright configuration excludes Firefox; run local Firefox checks with `CI` unset. Optional repeated local qualification is described in the [contributor command reference](../CONTRIBUTOR_COMMANDS.md#run-the-final-gate). When running separate engine processes concurrently, follow the [concurrent browser-check guidance](../CONTRIBUTOR_COMMANDS.md#run-browser-checks-concurrently) so each process has its own port and output directory. The supported-browser, device, and assistive-technology matrix remains a required manual release activity according to the [accessibility testing policy](../ACCESSIBILITY.md#testing). ## Required platform diff --git a/docs/change-delivery.md b/docs/change-delivery.md index 7c1248f..409df0f 100644 --- a/docs/change-delivery.md +++ b/docs/change-delivery.md @@ -40,7 +40,9 @@ Git stages. A commit or push alone does not start publication. - [ ] Push the feature branch and submit a pull request describing the final behavior, compatibility changes, tests, and any remaining limitations. -- [ ] Resolve required review and CI results, including Firefox qualification. +- [ ] Resolve required review and the **Build, test, and verify package** CI result, + including its Chromium and WebKit checks. Record affected Firefox checks locally; + Firefox has no required hosted status. A test passing only after a retry is a failure of the gate. - [ ] Merge through the protected branch workflow. Record the resulting full source SHA. - [ ] Confirm the required CI results refer to that exact merged source, not a previous diff --git a/docs/example-deployment.md b/docs/example-deployment.md index ead7716..5983dd4 100644 --- a/docs/example-deployment.md +++ b/docs/example-deployment.md @@ -117,6 +117,6 @@ Rerun the original publisher-linked **Deploy static examples** run only when all ## Verification -`npm run check` is the complete repository gate. Tooling covers metadata and manifest validation, release ordering, static fallbacks, commit-pinned links, asset filtering, remote-resource rejection, exact CSP enforcement, retained-path immutability, idempotent retries, monotonic preview ancestry, workflow permissions, shared workflow-level queuing, authenticated writer-side rollback reconstruction, and rollback selection. Browser tests run in Chromium, Firefox, and WebKit and cover every published example route plus keyboard/focus behavior, narrow full-SHA reflow, dark mode, forced colors, reduced motion, automated accessibility rules, Playwright-computed ARIA structure, and live-region/focus behavior. +`npm run check` is the complete repository gate. Tooling covers metadata and manifest validation, release ordering, static fallbacks, commit-pinned links, asset filtering, remote-resource rejection, exact CSP enforcement, retained-path immutability, idempotent retries, monotonic preview ancestry, workflow permissions, shared workflow-level queuing, authenticated writer-side rollback reconstruction, and rollback selection. Browser tests run in Chromium and WebKit in hosted CI, with Firefox retained in the local matrix. They cover every published example route plus keyboard/focus behavior, narrow full-SHA reflow, dark mode, forced colors, reduced motion, automated accessibility rules, Playwright-computed ARIA structure, and live-region/focus behavior. [Back to the documentation hub](README.md) diff --git a/docs/package-verification.md b/docs/package-verification.md index 8a4a206..a1f2afd 100644 --- a/docs/package-verification.md +++ b/docs/package-verification.md @@ -27,7 +27,7 @@ Run repository commands from the repository root with: - A Node version allowed by `package.json#devEngines.runtime.version` (currently Node 24.x). - The exact npm version in `package.json#packageManager`. - Dependencies installed with `npm ci --ignore-scripts`. -- Playwright Chromium, Firefox, and WebKit installed before the complete browser gate. +- Playwright Chromium, Firefox, and WebKit installed before the complete local browser gate; hosted CI and publication install and run Chromium and WebKit only. `npm run check` ends with temporary release-tarball verification, and `npm run package` creates retained release evidence. Both require a clean @@ -39,7 +39,7 @@ Choose the narrowest command that answers your question: | Audience and goal | Command | Scope | | --- | --- | --- | | Release operator: validate the snapshot plan | `npm run release:verify -- --plan PATH` | Immutable run identity, source manifest, exact commit, and clean source | -| Contributor: run the complete repository gate used by CI | `npm run check` | Static checks, unit and browser tests, screenshots, build, and temporary tarball verification | +| Contributor: run the complete local repository gate | `npm run check` | Static checks, unit and browser tests, screenshots, build, and temporary tarball verification | | Release operator: retain a nightly evidence bundle | `npm run package:nightly -- --plan PATH` | The stamped tarball and its write-once release bundle under `.artifacts/` | | Package user or contributor: preview local npm file selection | `npm pack --dry-run --ignore-scripts` | Local checkout contents only; it is not a substitute for `check:tarball` | @@ -78,9 +78,10 @@ The final `check:tarball` stage creates its tarball in a temporary directory. It - Packed-byte DOM interaction in an installed JSDOM consumer fixture, including replacement, refetch, activation, and teardown. -Hosted CI runs this complete repository gate in its selected environment and -adds platform-owned controls such as pull-request dependency review. A local -`npm run check` result is not evidence that those hosted controls ran. +Hosted CI runs the same gate command with Chromium and WebKit; the local browser +matrix also includes Firefox. Hosted CI adds platform-owned controls such as +pull-request dependency review. A local `npm run check` result is not evidence +that those hosted controls ran. Package policy also verifies the optional-extension boundary. The root module graph must not reach `dist/extensions/**` or WebMCP, while the documented WebMCP subpath must contain its JavaScript, declarations, and source maps. The clean consumer passes `webMcp` through `CalendarOptions.extensions` using only public imports. Optional entries must also evaluate under Node without reading DOM globals. diff --git a/docs/release-administration.md b/docs/release-administration.md index b774fbe..c120020 100644 --- a/docs/release-administration.md +++ b/docs/release-administration.md @@ -12,7 +12,7 @@ Require two-factor authentication and grant repository and organization administ ### GitHub repository and release -- Protect `main` with reviewed pull requests, resolved conversations, linear history, deletion/force-push protection, and required CI. Require **Build, test, and verify package** and both **Qualify Firefox** jobs. Require code-owner and independent review where another eligible maintainer exists. +- Protect `main` with reviewed pull requests, resolved conversations, linear history, deletion/force-push protection, and required CI. The required status is **Build, test, and verify package**, which runs Chromium and WebKit. Remove any former **Qualify Firefox** status requirements when adopting this workflow; Firefox checks now run locally. Require code-owner and independent review where another eligible maintainer exists. - Configure `npm-nightly` as the main-only npm publication environment, without a required reviewer or wait timer: scheduled and manually requested nightlies must complete unattended. Review changes to the environment and its workflow path. - Keep `npm-nightly` free of registry tokens. Nightly publication needs no npm secret or additional tag-writing environment. - Protect `v*` tags against updates and deletion. Permit creation only for the narrowest available actor; GitHub Actions credentials identify an application, not a single workflow. Review every workflow with repository-write authority. diff --git a/docs/release-operations.md b/docs/release-operations.md index 6b66d80..7b55cd2 100644 --- a/docs/release-operations.md +++ b/docs/release-operations.md @@ -8,7 +8,7 @@ The relevant Actions workflows are: | Workflow | Purpose | | --- | --- | -| **CI** (`ci.yml`) | Verify the exact `main` commit, including browser qualification | +| **CI** (`ci.yml`) | Verify the exact `main` commit, including Chromium and WebKit checks | | **Publish npm nightly** (`publish-nightly.yml`) | Scheduled or manually requested nightly snapshot | | **Deploy static examples** (`deploy-examples.yml`) | Automatic rolling preview and publisher-linked release Pages | | **Roll back static examples** (`rollback-examples.yml`) | Restore retained rolling-preview bytes; recovery only | @@ -31,7 +31,7 @@ Keep a private, non-versioned record with these values. Record sanitized evidenc | --- | --- | | Requested change | Pull request and phase commits | | Source identity | Full 40-character `SOURCE_SHA`, source development version | -| Validation | Exact-main CI run, Firefox qualification, final gate, visual review | +| Validation | Exact-main CI run, final gate, local Firefox results when applicable, visual review | | Hosted controls | Verifier and UTC verification time | | Publisher identity | Run URL, run ID, original UTC creation time, attempt number | | Published identity | `EXACT_VERSION`, protected tag `vEXACT_VERSION` | @@ -45,7 +45,7 @@ Keep a private, non-versioned record with these values. Record sanitized evidenc ## 1. Complete preflight - [ ] All intended changes and `[Unreleased]` notes are merged through required review. -- [ ] The newest **CI** run for the exact current `main` commit succeeded, including **Build, test, and verify package** and both Firefox qualification jobs. +- [ ] The newest **CI** run for the exact current `main` commit succeeded with **Build, test, and verify package**. Hosted browser checks cover Chromium and WebKit; Firefox is checked locally and has no required hosted status. - [ ] The final screenshots and any required manual accessibility evidence were reviewed. - [ ] Release-blocking security findings are resolved or explicitly dispositioned with recorded evidence. - [ ] Hosted settings match the [administration guide](release-administration.md#one-time-hosted-prerequisites). diff --git a/docs/releasing.md b/docs/releasing.md index 2f5e806..4e52160 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -10,7 +10,7 @@ Nightlies snapshot reviewed `main` directly. They do not create daily version co A completed nightly must satisfy these invariants: -- The exact source commit passed CI, including required Firefox qualification, and the publication gate. New publication uses current canonical `main`. +- The exact source commit passed CI and the publication gate, whose hosted browser checks run Chromium and WebKit. Firefox remains supported and is checked locally. New publication uses current canonical `main`. - npm `nightly` selects the exact published snapshot. Nightly publication leaves `latest` at its verified preflight value, which remains the frozen historical `alpha` before stable exists. The first stable release will move `latest` to stable, and stable owns it permanently afterward. - The protected `v<version>` tag resolves to the source commit. The GitHub release is a public immutable prerelease with the retained package, receipt, SBOM, license, and checksums. - Receipt schema 2 binds the source version, published version, source commit, original workflow identity, and version-only manifest transformation. Retained evidence records package integrity, asset digests, toolchain, and resolved compatibility targets. @@ -25,7 +25,7 @@ The source development base advances through a reviewed change when the next rel | Stage | Authority | | --- | --- | -| Select and verify | Exact source checkout, read-only repository access, successful same-commit CI, fresh registry state, and the complete local gate | +| Select and verify | Exact source checkout, read-only repository access, successful same-commit CI, fresh registry state, and the hosted publication gate | | Stage GitHub release | No source checkout; narrowly scoped write permission for the verified tag, draft, notes, and assets | | Publish npm | No source checkout; `npm-nightly` environment and OIDC publish only the retained tarball under `nightly` | | Verify `latest` | Read-only registry checks confirm its preflight value is unchanged and, once stable exists, still selects stable | diff --git a/docs/security-model.md b/docs/security-model.md index fa32944..c622882 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -37,7 +37,7 @@ The model assumes: | Browser agent → WebMCP extension | Tool names, untrusted structured arguments, read/navigation requests | Explicit selection, prefix validation, exact schemas, bounds validation, snapshot-bound opaque cursor validation, existing public navigation paths, no application-action tools | | WebMCP extension → browser agent | Paged visible-range state and presentation-safe source-event fields | Same-origin document registry, untrusted-content annotation, allowed-date filtering, no ID/URL/metadata/cause output, fixed result limits, no superseded-snapshot cache, unregister on teardown | | Contributor → CI | Source and workflow changes | Read-only pull-request permissions, full-SHA actions, no `pull_request_target`, locked script-disabled install, lint/type/policy/tests | -| Validated main snapshot → retained artifacts | Reviewed source and build tooling | Complete gate, isolated version overlay, immutable workflow identity, checksums, integrity, receipt, SBOM, license, retained artifacts | +| Validated main snapshot → retained artifacts | Reviewed source and build tooling | Hosted publication gate (Chromium and WebKit), isolated version overlay, immutable workflow identity, checksums, integrity, receipt, SBOM, license, retained artifacts | | Verified artifact → npm | Exact tarball and OIDC identity | Main-only `npm-nightly` environment, source-free publisher, hash/receipt/manifest validation, OIDC-only trusted publishing under `nightly`, provenance, no npm token | | Published npm state → registry verification | Public registry metadata and retained package evidence | Read-only exact-version/integrity verification, unchanged preflight `latest`, frozen historical alpha before stable, stable ownership of `latest` afterward | | Verified release state → Git tag and GitHub prerelease | Tag and release-write authority | Protected exact-SHA tag, draft-first assets, matching-byte reuse only, immutable release setting, public prerelease last | diff --git a/playwright.config.mjs b/playwright.config.mjs index 52d942a..048bbac 100644 --- a/playwright.config.mjs +++ b/playwright.config.mjs @@ -47,7 +47,7 @@ export default defineConfig({ ...devices["Desktop Safari"] } } - ], + ].filter(({ name }) => !process.env["CI"] || name !== "firefox"), reporter: process.env["CI"] ? [ ["github"], diff --git a/scripts/tests/workflow-contracts.test.mjs b/scripts/tests/workflow-contracts.test.mjs index 2a49596..17f3a9e 100644 --- a/scripts/tests/workflow-contracts.test.mjs +++ b/scripts/tests/workflow-contracts.test.mjs @@ -504,7 +504,7 @@ void test("CI rejects high-severity dependency regressions on pull requests", as assert.doesNotMatch(source, /pull-requests: write|issues: write/u); }); -void test("release gates and Playwright retain the complete browser-engine matrix", async () => { +void test("hosted gates run Chromium and WebKit while local Playwright retains Firefox", async () => { const [ciSource, publishSource, packageSource] = await Promise.all([ workflow("ci.yml"), workflow("publish-nightly.yml"), @@ -517,20 +517,39 @@ void test("release gates and Playwright retain the complete browser-engine matri for (const verify of [ciVerify, publishVerify]) { assert.match( verify, - /name: Install pinned Playwright browsers[\s\S]*?npx playwright install --with-deps chromium firefox webkit/u + /name: Install pinned Playwright browsers[\s\S]*?npx playwright install --with-deps chromium webkit/u ); + assert.match(verify, /run: npm run check/u); } + for (const source of [ciSource, publishSource]) { + assert.doesNotMatch(source, /firefox/iu); + } + const [localProjects, ciProjects] = await Promise.all([false, true].map(async (ci) => { + const { stdout } = await execFileAsync(process.execPath, ["--input-type=module", "--eval", ` + import configuration from "./playwright.config.mjs"; + process.stdout.write(JSON.stringify(configuration.projects.map(({ grepInvert, name, use }) => ({ + browserType: use.defaultBrowserType, + grepInvert: grepInvert?.source ?? null, + name + })))); + `], { + cwd: REPOSITORY_ROOT, + env: { ...process.env, CI: ci ? "true" : "" } + }); + return JSON.parse(stdout); + })); assert.deepEqual( - playwrightConfiguration.projects?.map(({ name, use }) => ({ - browserType: use?.defaultBrowserType, - name - })), + localProjects, [ - { browserType: "chromium", name: "chromium" }, - { browserType: "firefox", name: "firefox" }, - { browserType: "webkit", name: "webkit" } + { browserType: "chromium", grepInvert: null, name: "chromium" }, + { browserType: "firefox", grepInvert: "@chromium-input", name: "firefox" }, + { browserType: "webkit", grepInvert: "@chromium-input", name: "webkit" } ] ); + assert.deepEqual(ciProjects, [ + { browserType: "chromium", grepInvert: null, name: "chromium" }, + { browserType: "webkit", grepInvert: "@chromium-input", name: "webkit" } + ]); assert.equal( packageManifest.scripts["test:browser:install"], "playwright install chromium firefox webkit" @@ -546,7 +565,7 @@ void test("release gates and Playwright retain the complete browser-engine matri void test("workflow artifacts use bounded purpose-specific retention", async () => { const expectations = new Map([ - ["ci.yml", [7, 7]], + ["ci.yml", [7]], ["deploy-examples.yml", [1, 1]], ["publish-nightly.yml", [7, 30, 30]], ["rollback-examples.yml", [1]] @@ -594,23 +613,11 @@ void test("workflow dependency caches stay disabled", async () => { } }); -void test("browser gates reject flakes and retain reproducible Firefox qualification evidence", async () => { - const source = await workflow("ci.yml"); - const qualification = job(source, "firefox-qualification"); +void test("browser gates reject flakes and local Firefox qualification retains reproducible evidence", async () => { assert.equal(playwrightConfiguration.failOnFlakyTests, true); - assert.match(qualification, /os: \[ubuntu-latest, windows-latest\]/u); - assert.match(qualification, /node scripts\/qualify-firefox\.mjs/u); - assert.match(qualification, /if: always\(\)[\s\S]*?path: test-results\/firefox-qualification\//u); const config = await readFile(join(REPOSITORY_ROOT, "playwright.config.mjs"), "utf8"); assert.match(config, /workers: process\.env\["CI"\] \? 1 : 2/u); assert.match(config, /retries: process\.env\["CI"\] \? 1 : 0/u); - for (const project of playwrightConfiguration.projects) { - if (project.name === "chromium") { - assert.equal(project.grepInvert, undefined); - } else { - assert.equal(project.grepInvert?.test("@chromium-input"), true); - } - } const qualifier = await readFile(join(REPOSITORY_ROOT, "scripts", "qualify-firefox.mjs"), "utf8"); assert.match(qualifier, /--repeat-each=20/u); assert.match(qualifier, /length: 3/u);

    ghfzkgo%B|Af}2@er<`WaK8D* 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 tDX-p%0sw%=$Jd&6+QiP8W7 literal 34097 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index f100a6a..24b6aec 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "package": "node scripts/pack-release.mjs", "release:prepare": "node scripts/prepare-release.mjs", "release:verify": "node scripts/check-release-ready.mjs", - "screenshots:update": "npm run build:package && npm run build:examples:advanced && node scripts/update-screenshots.mjs", + "screenshots:update": "node scripts/prepare-screenshots.mjs --force", "serve:repository": "node scripts/serve-repository.mjs", "test": "npm run test:unit && npm run test:examples", "test:browser": "npm run build && npm run test:browser:built", @@ -99,14 +99,15 @@ "test:browser:webkit": "npm run build && playwright test --project=webkit", "test:examples": "npm run build && npm run test:examples:built", "test:examples:built": "node scripts/run-advanced-smoke.mjs", - "test:tooling": "node --test scripts/tests/build-pages.test.mjs scripts/tests/calendar-workflow-measurement.test.mjs scripts/tests/check-sbom.test.mjs scripts/tests/dynamic-update-measurement.test.mjs scripts/tests/example-metadata.test.mjs scripts/tests/markdown-diagrams.test.mjs scripts/tests/markdown-heading.test.mjs scripts/tests/node-version.test.mjs scripts/tests/ownership.test.mjs scripts/tests/package-entries.test.mjs scripts/tests/packed-extension-verification.test.mjs scripts/tests/publish-alpha-policy.test.mjs scripts/tests/release-artifacts.test.mjs scripts/tests/release-state.test.mjs scripts/tests/screenshot-contract.test.mjs scripts/tests/semver.test.mjs scripts/tests/serve-repository.test.mjs scripts/tests/styles.test.mjs scripts/tests/workflow-contracts.test.mjs scripts/tests/browser-targets.test.mjs scripts/tests/vite-build.test.mjs scripts/tests/package-bundle-policy.test.mjs", + "test:tooling": "node --test scripts/tests/build-pages.test.mjs scripts/tests/calendar-workflow-measurement.test.mjs scripts/tests/check-sbom.test.mjs scripts/tests/dynamic-update-measurement.test.mjs scripts/tests/example-metadata.test.mjs scripts/tests/markdown-diagrams.test.mjs scripts/tests/markdown-heading.test.mjs scripts/tests/node-version.test.mjs scripts/tests/ownership.test.mjs scripts/tests/package-entries.test.mjs scripts/tests/packed-extension-verification.test.mjs scripts/tests/publish-alpha-policy.test.mjs scripts/tests/release-artifacts.test.mjs scripts/tests/release-state.test.mjs scripts/tests/screenshot-contract.test.mjs scripts/tests/semver.test.mjs scripts/tests/serve-repository.test.mjs scripts/tests/styles.test.mjs scripts/tests/workflow-contracts.test.mjs scripts/tests/browser-targets.test.mjs scripts/tests/vite-build.test.mjs scripts/tests/package-bundle-policy.test.mjs scripts/tests/screenshot-preparation.test.mjs", "test:unit": "node scripts/run-unit-tests.mjs", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit", "typecheck:examples": "npm run build:package && npm run typecheck:examples:built", "typecheck:examples:built": "tsc -p examples/advanced/tsconfig.json --noEmit", "check:license": "node scripts/check-license.mjs", "browsers:report": "node scripts/report-browser-targets.mjs", - "dev": "vite --config vite.examples.config.mjs" + "dev": "vite --config vite.examples.config.mjs", + "screenshots:prepare": "node scripts/prepare-screenshots.mjs" }, "devDependencies": { "@axe-core/playwright": "4.13.0", diff --git a/screenshots.manifest.json b/screenshots.manifest.json index f5b27c2..425f6ad 100644 --- a/screenshots.manifest.json +++ b/screenshots.manifest.json @@ -2,12 +2,12 @@ "version": 1, "state": "final", "toolchain": { - "node": "24.19.0", + "node": "24.20.0", "npm": "12.0.2", "playwright": "1.62.1", "browser": "chromium" }, - "sourceFingerprint": "6fbe2fdf6051785e75c204b4ac5ba80351ed7c059d6f5efecabd5a03913621de", + "sourceFingerprint": "fb28e2b9348573cd34c8da7dbd3f69533f7f36934420140c60125f757e0eaa5b", "scenes": [ { "id": "desktop-month-grid", @@ -27,7 +27,7 @@ "README.md", "docs/screenshots/README.md" ], - "sha256": "cab5462e7035251e61388c52973c6185d393e74a7acbaf9f60a2b61c16ee96b3" + "sha256": "ad46e85303e93587df067d5dbc7f5cb53a520019739d1d660ddbc150013228b3" }, { "id": "month-year-jump", @@ -47,7 +47,7 @@ "references": [ "docs/screenshots/README.md" ], - "sha256": "9de8f1ddb1505a6bd889bc597f93400f8446f7fd08416658c3565303f26acf88" + "sha256": "aacdf4aa1ee35ff8b2fb7fd6a349d3b05e47c792cae4c85b5420da7d4305a87b" }, { "id": "mobile-month-agenda-dark", @@ -68,7 +68,7 @@ "README.md", "docs/screenshots/README.md" ], - "sha256": "19d4a21c9caa193c470f2b15733faf665c291115e22f591371663452325a1bd6" + "sha256": "f38dfc110721bdfcf159e0670e9f51b99af846dd0bc0b2d747168bea8388679d" }, { "id": "mobile-month-swipe-pull", @@ -88,7 +88,7 @@ "references": [ "docs/screenshots/README.md" ], - "sha256": "7843bb9598864fb96d9d4574c219fc9e3c49a3a2e5c7dcb742a68f457079e833" + "sha256": "baa9ec985ae5505cb3081b1ef16bcb3f6bc43439e92989ba6e6624b6852b3b74" }, { "id": "event-details-dark", @@ -108,7 +108,7 @@ "references": [ "docs/screenshots/README.md" ], - "sha256": "4905e43d8985d88ebc22a12c7181c3c0f34e9082b3d517eda984dd1797b0375a" + "sha256": "2b89afe1a643301884a53ccf59a7dcd932d22fca8b76b79c06fd9d9fe1a9a856" }, { "id": "grid-event-keyboard-focus", @@ -129,7 +129,119 @@ "references": [ "docs/screenshots/README.md" ], - "sha256": "6b82afcda9f73bade132a65923120b2569bec46b8ea2de74aa38e17a02d79e4e" + "sha256": "9540be650dc76258b1fdff3352a96a32cdef07802e000763ba62261c1bae6d0b" + }, + { + "id": "compact-default-counts", + "route": "/examples/advanced/", + "output": "docs/screenshots/compact-default-counts-390x844.png", + "viewport": { + "width": 390, + "height": 844 + }, + "hasTouch": false, + "setup": [ + "wait-ready", + "mount-count-fixture", + "scroll-calendar" + ], + "alt": "Compact calendar with total counts on days containing multiple events", + "references": [ + "docs/screenshots/README.md" + ], + "sha256": "e05eb374b815306c635fc367848b01f48bd25f1c0d5a7ae51e30f1097a546774" + }, + { + "id": "compact-all-counts", + "route": "/examples/advanced/", + "output": "docs/screenshots/compact-all-counts-390x844.png", + "viewport": { + "width": 390, + "height": 844 + }, + "hasTouch": false, + "setup": [ + "wait-ready", + "mount-count-fixture", + "scroll-calendar" + ], + "alt": "Compact calendar with total counts on every nonempty day", + "references": [ + "docs/screenshots/README.md" + ], + "sha256": "dd43d435efd2e7ce95484be7a43443977a3c2c13e5c2ef3e6aadab6e9778927b" + }, + { + "id": "wide-all-counts", + "route": "/examples/advanced/", + "output": "docs/screenshots/wide-all-counts-1440x900.png", + "viewport": { + "width": 1440, + "height": 900 + }, + "hasTouch": false, + "setup": [ + "wait-ready", + "mount-count-fixture", + "scroll-calendar" + ], + "alt": "Wide calendar with localized total event counts on every nonempty day", + "references": [ + "docs/screenshots/README.md" + ], + "sha256": "3110b2b7c269419f1dc63ae8fd8bb642305c1b335a69d1770f4be5e9c14b9852" + }, + { + "id": "count-keyboard-focus", + "route": "/examples/advanced/", + "output": "docs/screenshots/count-keyboard-focus-390x844.png", + "viewport": { + "width": 390, + "height": 844 + }, + "hasTouch": false, + "setup": [ + "wait-ready", + "mount-count-fixture", + "scroll-calendar", + "focus-selected-day", + "press-f2" + ], + "alt": "Keyboard focus on a compact calendar total event count button", + "references": [ + "docs/screenshots/README.md" + ], + "sha256": "3d54f1da2819de8345daa7fa952ac8f67bf0ac292baeb78b108fd85162ab3cce" } - ] + ], + "browserTargets": { + "query": "baseline-widely-available", + "effectiveQuery": "Vite 8.3.0: baseline-widely-available", + "resolvedAt": "2026-09-13", + "dataVersions": { + "vite": "8.3.0", + "esbuild": "0.28.2" + }, + "browsers": [ + "chrome111", + "edge111", + "firefox114", + "safari16.4", + "ios16.4" + ], + "javascript": [ + "chrome111", + "edge111", + "firefox114", + "safari16.4", + "ios16.4" + ], + "css": [ + "chrome111", + "edge111", + "firefox114", + "safari16.4", + "ios16.4" + ] + } } diff --git a/scripts/check-screenshots.mjs b/scripts/check-screenshots.mjs index c323a78..d02eb07 100644 --- a/scripts/check-screenshots.mjs +++ b/scripts/check-screenshots.mjs @@ -114,7 +114,7 @@ if (Array.isArray(manifest.scenes)) { try { const currentFingerprint = await computeSourceFingerprint(); if (currentFingerprint !== manifest.sourceFingerprint) { - errors.push("Screenshot source fingerprint is stale; run npm run screenshots:update."); + errors.push("Screenshot source fingerprint is stale; run npm run screenshots:prepare."); } } catch (error) { errors.push(`Unable to compute screenshot source fingerprint (${error.message}).`); diff --git a/scripts/lib/screenshot-preparation.mjs b/scripts/lib/screenshot-preparation.mjs new file mode 100644 index 0000000..5e60a2e --- /dev/null +++ b/scripts/lib/screenshot-preparation.mjs @@ -0,0 +1,110 @@ +import { copyFile, mkdir, rename, rm, writeFile } from "node:fs/promises"; +import { basename, join } from "node:path"; + +/** Builds once only when evidence is stale. CI can verify evidence but never accept it. */ +export async function prepareScreenshots({ checkCurrent, build, capture, verify, force = false, + ci = Boolean(process.env["CI"]) }) { + if (!force && await checkCurrent()) { + return "current"; + } + if (ci) { + throw new Error("Screenshot evidence is stale. Prepare and review captures locally; CI cannot accept them."); + } + await build(); + await capture(); + await verify(); + return "updated"; +} + +function escapeHtml(value) { + return String(value).replaceAll("&", "&").replaceAll("<", "<") + .replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); +} + +/** Creates a self-contained comparison gallery outside canonical documentation assets. */ +export async function writeScreenshotReview({ directory, beforeDirectory, afterDirectory, + previousManifest, manifest }) { + await mkdir(join(directory, "before"), { recursive: true }); + await mkdir(join(directory, "after"), { recursive: true }); + const scenes = []; + for (const scene of manifest.scenes) { + const filename = basename(scene.output); + const previous = previousManifest.scenes.find((candidate) => candidate.id === scene.id); + let before = null; + try { + await copyFile(join(beforeDirectory, filename), join(directory, "before", filename)); + before = `before/${filename}`; + } catch (error) { + if (error.code !== "ENOENT") { throw error; } + } + await copyFile(join(afterDirectory, filename), join(directory, "after", filename)); + scenes.push({ id: scene.id, alt: scene.alt, before, after: `after/${filename}`, + changed: previous?.sha256 !== scene.sha256, + previousSha256: previous?.sha256 ?? null, sha256: scene.sha256 }); + } + const changed = scenes.filter((scene) => scene.changed).map((scene) => scene.id); + const report = { sourceFingerprint: manifest.sourceFingerprint, changed, scenes }; + await writeFile(join(directory, "changes.json"), `${JSON.stringify(report, null, 2)}\n`); + const figures = scenes.map((scene) => `