diff --git a/.changeset/config.json b/.changeset/config.json index e13042f0c2..7c3d4c9697 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -24,6 +24,7 @@ "preact-ts", "svelte-ts", "vanilla-ts", + "lit-ts", "website" ] } diff --git a/e2e/_utils.ts b/e2e/_utils.ts index 3ea3d7470d..2592d93aaf 100644 --- a/e2e/_utils.ts +++ b/e2e/_utils.ts @@ -1,15 +1,65 @@ import AxeBuilder from "@axe-core/playwright" import { expect, type Locator, type Page } from "@playwright/test" -export async function a11y(page: Page, selector = "[data-part=root]") { +// Types and Framework Context Detection +export type DomMode = "shadow-dom" | "light-dom" + +// The context is determined by environment variables at test runtime +// VITE_DOM_MODE is the primary control (matches client-side behavior) +// Examples: +// - `VITE_DOM_MODE=shadow-dom FRAMEWORK=react npx playwright test` (force Shadow DOM in React) +// - `VITE_DOM_MODE=light-dom FRAMEWORK=lit npx playwright test` (force Light DOM in Lit) +// - `FRAMEWORK=lit npx playwright test` (default Lit behavior: Shadow DOM) +export const DOM_MODE: DomMode = + process.env.VITE_DOM_MODE === "light-dom" ? "light-dom" : process.env.FRAMEWORK === "lit" ? "shadow-dom" : "light-dom" + +console.log(`Running E2E tests in '${DOM_MODE}' context for '${process.env.FRAMEWORK}'.`) + +/** + * Context-aware utility to locate a component part defined by a 'part' or 'data-part' attribute. + * This function abstracts the structural differences between Shadow DOM and Light DOM implementations. + * + * @param parent The root Playwright Page or a parent Locator to search within + * @param hostSelector A unique selector for the component's host/root element (e.g., 'accordion-page', '[data-testid="accordion-root"]') + * @param partName The name of the part to locate (e.g., 'trigger', 'content') + * @returns A Playwright Locator for the requested part + */ +export function getPart(parent: Page | Locator, hostSelector: string, partName: string): Locator { + // Use [part="..."] for CSS Shadow Parts standard, fallback to [data-part="..."] + const partSelector = `[part="${partName}"], [data-part="${partName}"]` + + if (DOM_MODE === "shadow-dom") { + // For Shadow DOM, use a descendant combinator. Playwright's engine will + // pierce the shadow root of the element matching hostSelector. + return parent.locator(`${hostSelector} ${partSelector}`) + } else { + // For Light DOM, the structure might be a direct child or a deeper descendant. + // In many simple cases, the selector is identical to the shadow DOM version. + return parent.locator(`${hostSelector} ${partSelector}`) + } +} + +/** + * Performs an accessibility scan, optionally scoped to a specific selector. + * @param page The Playwright Page object + * @param selector Optional selector to scope the scan (defaults to "[data-part=root]") + * @param hostSelector A CSS selector for a shadow host element + */ +export async function a11y(page: Page, selector = "[data-part=root]", hostSelector?: string) { await page.waitForSelector(selector) - const results = await new AxeBuilder({ page: page as any }) - .disableRules(["color-contrast"]) - .include(selector) - .analyze() + let selection = new AxeBuilder({ page: page as any }).disableRules(["color-contrast"]) + + if (hostSelector && DOM_MODE === "shadow-dom") { + selection = selection.include(hostSelector) + } + if (selector) { + selection = selection.include(selector) + } + + const accessibilityScanResults = await selection.analyze() - expect(results.violations).toEqual([]) + expect(accessibilityScanResults.violations).toEqual([]) } export const testid = (part: string) => `[data-testid=${esc(part)}]` diff --git a/e2e/checkbox.e2e.ts b/e2e/checkbox.e2e.ts index bcd7fdf71b..219b0c7527 100644 --- a/e2e/checkbox.e2e.ts +++ b/e2e/checkbox.e2e.ts @@ -1,64 +1,66 @@ -import { expect, type Page, test } from "@playwright/test" -import { a11y, controls, part, testid } from "./_utils" +import { expect, test } from "@playwright/test" +import { CheckboxModel } from "./models/checkbox.model" -const root = part("root") -const label = part("label") -const control = part("control") -const input = testid("hidden-input") - -const expectToBeChecked = async (page: Page) => { - await expect(page.locator(root)).toHaveAttribute("data-state", "checked") - await expect(page.locator(label)).toHaveAttribute("data-state", "checked") - await expect(page.locator(control)).toHaveAttribute("data-state", "checked") -} +let I: CheckboxModel test.beforeEach(async ({ page }) => { - await page.goto("/checkbox") + I = new CheckboxModel(page) + await I.goto() }) -test("should have no accessibility violation", async ({ page }) => { - await a11y(page) +test("should have no accessibility violation", async () => { + await I.checkAccessibility() }) -test("should be checked when clicked", async ({ page }) => { - await page.click(root) - await expectToBeChecked(page) +test("should be checked when clicked", async () => { + await I.root.click() + await I.expectToBeChecked() }) test("should be focused when page is tabbed", async ({ page }) => { await page.click("main") await page.keyboard.press("Tab") - await expect(page.locator(input)).toBeFocused() - await expect(page.locator(control)).toHaveAttribute("data-focus", "") + await expect(I.input).toBeFocused() + await expect(I.control).toHaveAttribute("data-focus", "") }) test("should be checked when spacebar is pressed while focused", async ({ page }) => { await page.click("main") await page.keyboard.press("Tab") await page.keyboard.press(" ") - await expectToBeChecked(page) + await I.expectToBeChecked() }) -test("should have disabled attributes when disabled", async ({ page }) => { - await controls(page).bool("disabled") - await expect(page.locator(input)).toBeDisabled() +test("should have disabled attributes when disabled", async () => { + await I.controls.bool("disabled") + await I.expectToBeDisabled() }) test("should not be focusable when disabled", async ({ page }) => { - await controls(page).bool("disabled") + await I.controls.bool("disabled") await page.click("main") await page.keyboard.press("Tab") - await expect(page.locator(input)).not.toBeFocused() + await expect(I.input).not.toBeFocused() }) test("input is not blurred on label click", async ({ page }) => { let blurCount = 0 await page.exposeFunction("trackBlur", () => blurCount++) - await page.locator(input).evaluate((input) => { + await I.input.evaluate((input) => { input.addEventListener("blur", (window as any).trackBlur) }) - await page.click(label) - await page.click(label) - await page.click(label) + await I.label.click() + await I.label.click() + await I.label.click() expect(blurCount).toBe(0) }) + +test("reset form should restore initial state", async () => { + await expect(I.input).not.toBeChecked() + + await I.label.click() + await expect(I.input).toBeChecked() + + await I.resetButton.click() + await expect(I.input).not.toBeChecked() +}) diff --git a/e2e/collapsible.e2e.ts b/e2e/collapsible.e2e.ts index 5ef3218caf..1de285e6eb 100644 --- a/e2e/collapsible.e2e.ts +++ b/e2e/collapsible.e2e.ts @@ -1,30 +1,34 @@ import { expect, test } from "@playwright/test" -import { a11y, part } from "./_utils" +import { CollapsibleModel } from "./models/collapsible.model" -const trigger = part("trigger") -const content = part("content") +let I: CollapsibleModel test.describe("collapsible", () => { test.beforeEach(async ({ page }) => { - await page.goto("/collapsible") + I = new CollapsibleModel(page) + await I.goto() }) - test("should have no accessibility violation", async ({ page }) => { - await a11y(page) + test("should have no accessibility violation", async () => { + await I.checkAccessibility() }) - test("[toggle] should be open when clicked", async ({ page }) => { - await page.click(trigger) - await expect(page.locator(content)).toBeVisible() + test("[toggle] should be open when clicked", async () => { + await I.clickTrigger() + await I.seeContent() - await page.click(trigger) - await expect(page.locator(content)).not.toBeVisible() + await I.clickTrigger() + await I.dontSeeContent() }) - test.skip("[closed] content should not be reachable via tab key", async ({ page }) => { - await page.click(trigger) - await page.click(trigger) - await page.keyboard.press("Tab") - await expect(page.getByRole("button", { name: "Open" })).toBeFocused() + test("[closed] content should not be reachable via tab key", async () => { + await I.clickTrigger() + await I.seeContent() + + await I.clickTrigger() + await I.dontSeeContent() + + await I.pressKey("Tab") + await expect(I.host.getByTestId("open-button")).toBeFocused() }) }) diff --git a/e2e/menu-nested.e2e.ts b/e2e/menu-nested.e2e.ts index 9c0031cb0a..5560daa95a 100644 --- a/e2e/menu-nested.e2e.ts +++ b/e2e/menu-nested.e2e.ts @@ -1,50 +1,8 @@ -import { expect, type Page, test } from "@playwright/test" -import { clickOutside, rect, testid } from "./_utils" - -const menu_1 = { - trigger: testid("trigger"), - menu: testid("menu"), - sub_trigger: testid("more-tools"), -} - -const menu_2 = { - trigger: testid("more-tools"), - menu: testid("more-tools-submenu"), - sub_trigger: testid("open-nested"), -} - -const menu_3 = { - trigger: testid("open-nested"), - menu: testid("open-nested-submenu"), -} - -const expectToBeFocused = async (page: Page, id: string) => { - return await expect(page.locator(id).first()).toHaveAttribute("data-highlighted", "") -} - -const navigateToSubmenuTrigger = async (page: Page) => { - await page.click(menu_1.trigger) - await page.keyboard.press("ArrowDown") - await page.keyboard.press("ArrowDown") - await page.keyboard.press("ArrowDown") - await page.keyboard.press("ArrowDown") -} - -async function expectSubmenuToBeFocused(page: Page) { - await expect(page.locator(menu_2.menu)).toBeVisible() - await expect(page.locator(menu_2.menu)).toBeFocused() - await expectToBeFocused(page, menu_2.trigger) -} - -async function expectAllMenusToBeClosed(page: Page) { - // close all - await expect(page.locator(menu_1.menu)).toBeHidden() - await expect(page.locator(menu_2.menu)).toBeHidden() - await expect(page.locator(menu_3.menu)).toBeHidden() - - // focus trigger - await expect(page.locator(menu_1.trigger)).toBeFocused() -} +import { expect, test } from "@playwright/test" +import { MenuNestedModel } from "./models/menu-nested.model" +import { clickOutside, rect } from "./_utils" + +let I: MenuNestedModel test.describe("nested menu / pointer", async () => { test.beforeEach(async ({ page }) => { @@ -54,166 +12,179 @@ test.describe("nested menu / pointer", async () => { test.describe("nested menu / keyboard navigation", async () => { test.beforeEach(async ({ page }) => { - await page.goto("/menu-nested") + I = new MenuNestedModel(page) + await I.goto() }) - test("open submenu when moving focus to trigger", async ({ page }) => { - await navigateToSubmenuTrigger(page) - await expect(page.locator(menu_2.menu)).toBeHidden() + test("open submenu when moving focus to trigger", async () => { + await I.navigateToSubmenuTrigger() + await expect(I.menu2.menu).toBeHidden() }) - test("open submenu with space", async ({ page }) => { - await navigateToSubmenuTrigger(page) - await page.keyboard.press("Space") - await page.waitForTimeout(1) - await expectSubmenuToBeFocused(page) + test("open submenu with space", async () => { + await I.navigateToSubmenuTrigger() + await I.page.keyboard.press("Space") + await I.page.waitForTimeout(1) + await I.expectSubmenuToBeFocused() }) - test("open submenu with enter", async ({ page }) => { - await navigateToSubmenuTrigger(page) - await page.keyboard.press("Enter") - await expectSubmenuToBeFocused(page) + test("open submenu with enter", async () => { + await I.navigateToSubmenuTrigger() + await I.page.keyboard.press("Enter") + await I.expectSubmenuToBeFocused() }) - test("open submenu with arrow right", async ({ page }) => { - await navigateToSubmenuTrigger(page) - await page.keyboard.press("ArrowRight") - await expectSubmenuToBeFocused(page) + test("open submenu with arrow right", async () => { + await I.navigateToSubmenuTrigger() + await I.page.keyboard.press("ArrowRight") + await I.expectSubmenuToBeFocused() }) - test("close submenu with arrow left", async ({ page }) => { - await navigateToSubmenuTrigger(page) - await page.keyboard.press("Enter", { delay: 20 }) - await page.keyboard.press("ArrowLeft") - await expect(page.locator(menu_2.menu)).toBeHidden() + test("close submenu with arrow left", async () => { + await I.navigateToSubmenuTrigger() + await I.page.keyboard.press("Enter", { delay: 20 }) + await I.page.keyboard.press("ArrowLeft") + await expect(I.menu2.menu).toBeHidden() }) }) test.describe("nested menu / keyboard typeahead", async () => { test.beforeEach(async ({ page }) => { - await page.goto("/menu-nested") + I = new MenuNestedModel(page) + await I.goto() }) - test("parent menu", async ({ page }) => { - await page.click(menu_1.trigger) + test("parent menu", async () => { + await I.menu1.trigger.click() + await expect(I.menu1.menu).toBeFocused() - await page.keyboard.type("n") - await expectToBeFocused(page, testid("new-file")) + await I.page.keyboard.type("n") + await I.expectToBeHighlighted(I.getTestId("new-file")) - await page.keyboard.type("n") - await expectToBeFocused(page, testid("new-tab")) + await I.page.keyboard.type("n") + await I.expectToBeHighlighted(I.getTestId("new-tab")) - await page.keyboard.type("new w") - await expectToBeFocused(page, testid("new-win")) + await I.page.keyboard.type("new w") + await I.expectToBeHighlighted(I.getTestId("new-win")) }) - test.skip("nested menu", async ({ page }) => { - await page.click(menu_1.trigger) + test("nested menu", async () => { + await I.menu1.trigger.click() + await expect(I.menu1.menu).toBeFocused() - await page.keyboard.type("m") - await expectToBeFocused(page, testid("more-tools")) + await I.page.keyboard.type("m") + await I.expectToBeHighlighted(I.getTestId("more-tools")) // open submenu - await page.keyboard.press("Enter") - await expect(page.locator(menu_2.menu)).toBeVisible() + await I.page.keyboard.press("Enter") + await I.expectSubmenuToBeFocused() - await page.keyboard.type("s") - await expectToBeFocused(page, testid("switch-win")) + await I.page.keyboard.type("s") + await I.expectToBeHighlighted(I.getTestId("switch-win")) }) }) -test.describe("nested menu / select item", async () => { +test.describe("nested menu / select item", () => { test.beforeEach(async ({ page }) => { - await page.goto("/menu-nested") + I = new MenuNestedModel(page) + await I.goto() }) - test("using keyboard", async ({ page }) => { - await navigateToSubmenuTrigger(page) - await page.keyboard.press("Enter", { delay: 10 }) + test("using keyboard", async () => { + await I.navigateToSubmenuTrigger() + await I.page.keyboard.press("Enter") + await I.expectSubmenuToBeFocused() // open menu 3 - await page.keyboard.press("ArrowDown") - await page.keyboard.press("ArrowDown") - await page.keyboard.press("ArrowDown") - await page.keyboard.press("Enter", { delay: 10 }) + await I.page.keyboard.press("ArrowDown") + await I.page.keyboard.press("ArrowDown") + await I.page.keyboard.press("ArrowDown") + await I.page.keyboard.press("Enter") + await expect(I.menu3.menu).toBeFocused() // select first item in menu 3 - await page.keyboard.press("Enter", { delay: 10 }) - await expectAllMenusToBeClosed(page) + await I.page.keyboard.press("Enter") + await I.expectAllMenusToBeClosed() }) - test("using pointer click", async ({ page }) => { - await page.click(menu_1.trigger) - await page.hover(menu_1.sub_trigger) - await page.hover(menu_2.sub_trigger) + test("using pointer click", async () => { + await I.menu1.trigger.click() + await expect(I.menu1.menu).toBeFocused() + await I.menu1.subTrigger.hover() + await I.menu2.subTrigger.hover() - await page.hover(testid("playground")) - await page.click(testid("playground")) - await expectAllMenusToBeClosed(page) + await I.getTestId("playground").hover() + await I.getTestId("playground").click() + await I.expectAllMenusToBeClosed() }) - test("clicking outside or blur", async ({ page }) => { - await page.click(menu_1.trigger) - await page.hover(menu_2.trigger) - await page.hover("body", { position: { x: 10, y: 10 } }) - await clickOutside(page) - await expectAllMenusToBeClosed(page) + test("clicking outside or blur", async () => { + await I.menu1.trigger.click() + await I.menu2.trigger.hover() + await I.page.hover("body", { position: { x: 10, y: 10 } }) + await clickOutside(I.page) + await I.expectAllMenusToBeClosed() }) }) test.describe("nested menu / pointer movement", async () => { test.beforeEach(async ({ page }) => { - await page.goto("/menu-nested") + I = new MenuNestedModel(page) + await I.goto() }) - test("should open submenu and not focus first item", async ({ page }) => { - await page.click(menu_1.trigger) - await page.hover(menu_1.sub_trigger) + test("should open submenu and not focus first item", async () => { + await I.menu1.trigger.click() + // await expect(I.menu1.menu).toBeFocused() + await I.menu1.subTrigger.hover() - await expect(page.locator(menu_2.menu)).toBeVisible() - await expect(page.locator(menu_2.menu)).toBeFocused() - await expectToBeFocused(page, menu_2.trigger) + await expect(I.menu2.menu).toBeVisible() + await expect(I.menu2.menu).toBeFocused() + await I.expectToBeHighlighted(I.menu2.trigger) - const focusedItemCount = await page.locator(menu_2.menu).locator("[data-focus]").count() + const focusedItemCount = await I.menu2.menu.locator("[data-focus]").count() expect(focusedItemCount).toBe(0) }) - test("should not close when moving pointer to submenu and back to parent trigger", async ({ page }) => { - await page.click(menu_1.trigger) + test("should not close when moving pointer to submenu and back to parent trigger", async () => { + await I.menu1.trigger.click() + + await I.menu1.subTrigger.hover() - await page.hover(menu_1.sub_trigger) - await page.hover(testid("save-page")) - await page.hover(menu_1.sub_trigger) + await I.getTestId("save-page").hover() + await I.menu1.subTrigger.hover() - await expect(page.locator(menu_1.menu)).toBeVisible() - await expect(page.locator(menu_2.menu)).toBeVisible() + await expect(I.menu1.menu).toBeVisible() + await expect(I.menu2.menu).toBeVisible() }) - test("should close submenu when moving pointer away", async ({ page }) => { - await page.click(menu_1.trigger) - await page.hover(menu_2.trigger) + test("should close submenu when moving pointer away", async () => { + await I.menu1.trigger.click() + await I.menu2.trigger.hover() - const menu_el = page.locator(menu_2.trigger) - const bbox = await rect(menu_el) + const bbox = await rect(I.menu2.trigger) - await page.hover("body", { position: { x: bbox.x - 20, y: bbox.height / 2 + bbox.y } }) - await expect(page.locator(menu_2.menu)).toBeHidden() - await expect(page.locator(menu_1.menu)).toBeFocused() + await I.page.hover("body", { position: { x: bbox.x - 20, y: bbox.height / 2 + bbox.y } }) + await expect(I.menu2.menu).toBeHidden() + await expect(I.menu1.menu).toBeFocused() }) - test("should close open submenu when moving pointer to parent menu item", async ({ page }) => { - await page.click(menu_1.trigger) - await page.hover(menu_2.trigger) + test("should close open submenu when moving pointer to parent menu item", async () => { + await I.menu1.trigger.click() + await expect(I.menu1.menu).toBeVisible() + await I.menu2.trigger.hover() + await expect(I.menu2.menu).toBeVisible() - const menuitem = testid("new-tab") + const menuitem = I.getTestId("new-tab") - await page.hover(menuitem) + // FIXME: .hover() causes "subtree intercepts pointer events" error in shadow-dom + await menuitem.hover() // dispatch extra mouse movement to trigger hover - await page.locator(menuitem).dispatchEvent("pointermove") + await menuitem.dispatchEvent("pointermove") - await expect(page.locator(menu_2.menu)).toBeHidden() - await expect(page.locator(menu_1.menu)).toBeVisible() - await expect(page.locator(menu_1.menu)).toBeFocused() - await expectToBeFocused(page, testid("new-tab")) + await expect(I.menu2.menu).toBeHidden() + await expect(I.menu1.menu).toBeVisible() + await expect(I.menu1.menu).toBeFocused() + await I.expectToBeHighlighted(I.getTestId("new-tab")) }) }) diff --git a/e2e/menu-option.e2e.ts b/e2e/menu-option.e2e.ts index fdda2353f7..9282fd8587 100644 --- a/e2e/menu-option.e2e.ts +++ b/e2e/menu-option.e2e.ts @@ -5,7 +5,7 @@ let I: MenuModel test.describe("menu option", () => { test.beforeEach(async ({ page }) => { - I = new MenuModel(page) + I = new MenuModel(page, "menu-options-page") await I.goto("/menu-options") }) @@ -65,6 +65,7 @@ test.describe("menu option", () => { // open the menu await I.focusTrigger() await I.pressKey("Enter") + await I.seeDropdownIsFocused() // // navigate the 'Phone' item await I.pressKey("ArrowDown", 4) diff --git a/e2e/models/accordion.model.ts b/e2e/models/accordion.model.ts index 4ed95274a2..4977fd824b 100644 --- a/e2e/models/accordion.model.ts +++ b/e2e/models/accordion.model.ts @@ -1,26 +1,24 @@ import { expect, type Page } from "@playwright/test" -import { a11y, testid } from "../_utils" +import { testid } from "../_utils" import { Model } from "./model" +const shadowHost = "accordion-page" + export class AccordionModel extends Model { constructor(public page: Page) { - super(page) + super(page, shadowHost) } goto() { return this.page.goto("/accordion") } - checkAccessibility(selector?: string): Promise { - return a11y(this.page, selector) - } - getTrigger(id: string) { - return this.page.locator(testid(`${id}:trigger`)) + return this.host.locator(testid(`${id}:trigger`)) } getContent(id: string) { - return this.page.locator(testid(`${id}:content`)) + return this.host.locator(testid(`${id}:content`)) } async focusTrigger(id: string) { diff --git a/e2e/models/checkbox.model.ts b/e2e/models/checkbox.model.ts new file mode 100644 index 0000000000..05c6ff4d4c --- /dev/null +++ b/e2e/models/checkbox.model.ts @@ -0,0 +1,47 @@ +import { expect, type Page } from "@playwright/test" +import { part, testid } from "../_utils" +import { Model } from "./model" + +const shadowHost = "checkbox-page" + +export class CheckboxModel extends Model { + constructor(public page: Page) { + super(page, shadowHost) + } + + goto() { + return this.page.goto("/checkbox") + } + + get root() { + return this.host.locator(part("root")) + } + + get label() { + return this.host.locator(part("label")) + } + + get control() { + return this.host.locator(part("control")) + } + + get input() { + return this.host.locator(testid("hidden-input")) + } + + get resetButton() { + return this.host.getByRole("button", { name: "Reset Form" }) + } + + async expectToBeChecked() { + await expect(this.root).toHaveAttribute("data-state", "checked") + await expect(this.label).toHaveAttribute("data-state", "checked") + await expect(this.control).toHaveAttribute("data-state", "checked") + } + + async expectToBeDisabled() { + await expect(this.root).toHaveAttribute("data-disabled", "") + await expect(this.control).toHaveAttribute("data-disabled", "") + await expect(this.input).toBeDisabled() + } +} diff --git a/e2e/models/collapsible.model.ts b/e2e/models/collapsible.model.ts new file mode 100644 index 0000000000..e3ad4f16c6 --- /dev/null +++ b/e2e/models/collapsible.model.ts @@ -0,0 +1,32 @@ +import { expect, type Page } from "@playwright/test" +import { Model } from "./model" + +export class CollapsibleModel extends Model { + constructor(public page: Page) { + super(page, "collapsible-page") + } + + goto(url = "/collapsible") { + return this.page.goto(url) + } + + get trigger() { + return this.host.locator("[data-scope='collapsible'][data-part='trigger']") + } + + get content() { + return this.host.locator("[data-scope='collapsible'][data-part='content']") + } + + async clickTrigger() { + return this.trigger.click() + } + + async seeContent() { + await expect(this.content).toBeVisible() + } + + async dontSeeContent() { + await expect(this.content).not.toBeVisible() + } +} diff --git a/e2e/models/dialog.model.ts b/e2e/models/dialog.model.ts index e2b369350f..44b36222ee 100644 --- a/e2e/models/dialog.model.ts +++ b/e2e/models/dialog.model.ts @@ -1,17 +1,20 @@ import { expect, type Page } from "@playwright/test" -import { a11y } from "../_utils" +import { testid } from "../_utils" import { Model } from "./model" +const shadowHost = "dialog-nested-page" + export class DialogModel extends Model { constructor( public page: Page, private id: string, ) { - super(page) + super(page, shadowHost) } - checkAccessibility() { - return a11y(this.page, "[role=dialog]") + checkAccessibility(): Promise { + // return a11y(this.page, "[role=dialog]", shadowHost) + return super.checkAccessibility("[role=dialog]") } goto(url = "/dialog-nested") { @@ -19,15 +22,15 @@ export class DialogModel extends Model { } private get trigger() { - return this.page.locator(`[data-testid='trigger-${this.id}']`) + return this.host.locator(testid(`trigger-${this.id}`)) } private get content() { - return this.page.locator(`[data-testid='positioner-${this.id}']`) + return this.host.locator(testid(`positioner-${this.id}`)) } private get closeTrigger() { - return this.page.locator(`[data-testid='close-${this.id}']`) + return this.host.locator(testid(`close-${this.id}`)) } clickTrigger(opts: { delay?: number } = {}) { diff --git a/e2e/models/menu-nested.model.ts b/e2e/models/menu-nested.model.ts new file mode 100644 index 0000000000..1ecc6f1dcf --- /dev/null +++ b/e2e/models/menu-nested.model.ts @@ -0,0 +1,79 @@ +import { expect, type Locator, type Page } from "@playwright/test" +import { Model } from "./model" +import { testid } from "../_utils" + +export class MenuNestedModel extends Model { + constructor( + public page: Page, + shadowHost = "menu-nested-page", + ) { + super(page, shadowHost) + } + + goto(url = "/menu-nested") { + return this.page.goto(url) + } + + getTestId(id: string) { + return this.host.locator(testid(id)) + } + + get menu1() { + return { + trigger: this.getTestId("trigger"), + menu: this.getTestId("menu"), + subTrigger: this.getTestId("more-tools"), + } + } + + get menu2() { + return { + trigger: this.getTestId("more-tools"), + menu: this.getTestId("more-tools-submenu"), + subTrigger: this.getTestId("open-nested"), + } + } + + get menu3() { + return { + trigger: this.getTestId("open-nested"), + menu: this.getTestId("open-nested-submenu"), + } + } + + async expectToBeHighlighted(locator: Locator) { + await expect(locator).toHaveAttribute("data-highlighted", "") + } + + async expectItemToBeHighlighted(id: string) { + return this.expectToBeHighlighted(this.getTestId(id)) + } + + async navigateToSubmenuTrigger() { + await this.menu1.trigger.click() + await expect(this.menu1.menu).toBeFocused() + await this.page.keyboard.press("ArrowDown") + await this.page.keyboard.press("ArrowDown") + await this.page.keyboard.press("ArrowDown") + await this.page.keyboard.press("ArrowDown") + await this.expectToBeHighlighted(this.menu2.trigger) + // await this.page.keyboard.type("m") + // await this.expectItemToBeFocused("more-tools") + } + + async expectSubmenuToBeFocused() { + await expect(this.menu2.menu).toBeVisible() + await expect(this.menu2.menu).toBeFocused() + await this.expectToBeHighlighted(this.menu2.trigger) + } + + async expectAllMenusToBeClosed() { + // close all + await expect(this.menu1.menu).toBeHidden() + await expect(this.menu2.menu).toBeHidden() + await expect(this.menu3.menu).toBeHidden() + + // focus trigger + await expect(this.menu1.trigger).toBeFocused() + } +} diff --git a/e2e/models/menu.model.ts b/e2e/models/menu.model.ts index 4632d96c9a..5028e837a7 100644 --- a/e2e/models/menu.model.ts +++ b/e2e/models/menu.model.ts @@ -1,14 +1,18 @@ import { expect, type Page } from "@playwright/test" -import { a11y, isInViewport } from "../_utils" +import { isInViewport } from "../_utils" import { Model } from "./model" export class MenuModel extends Model { - constructor(public page: Page) { - super(page) + constructor( + public page: Page, + shadowHost = "menu-page", + ) { + super(page, shadowHost) } - checkAccessibility() { - return a11y(this.page, "main") + checkAccessibility(): Promise { + // return a11y(this.page, "main", shadowHost) + return super.checkAccessibility("main") } goto(url = "/menu") { @@ -16,23 +20,23 @@ export class MenuModel extends Model { } private get trigger() { - return this.page.locator("[data-scope=menu][data-part=trigger]") + return this.host.locator("[data-scope=menu][data-part=trigger]") } private get contextTrigger() { - return this.page.locator("[data-scope=menu][data-part=context-trigger]") + return this.host.locator("[data-scope=menu][data-part=context-trigger]") } private get content() { - return this.page.locator("[data-scope=menu][data-part=content]") + return this.host.locator("[data-scope=menu][data-part=content]") } getItem = (text: string) => { - return this.page.locator(`[data-part=item]`, { hasText: text }) + return this.host.locator(`[data-part=item]`, { hasText: text }) } get highlightedItem() { - return this.page.locator("[data-part=item][data-highlighted]") + return this.host.locator("[data-part=item][data-highlighted]") } type(input: string) { @@ -100,7 +104,7 @@ export class MenuModel extends Model { } seeMenuIsPositioned = async () => { - const positioner = this.page.locator("[data-scope=menu][data-part=positioner]") + const positioner = this.host.locator("[data-scope=menu][data-part=positioner]") await expect(positioner).toHaveCSS("--x", /\d+px/) await expect(positioner).toHaveCSS("--y", /\d+px/) } diff --git a/e2e/models/model.ts b/e2e/models/model.ts index 4a808c46f5..b338a3e205 100644 --- a/e2e/models/model.ts +++ b/e2e/models/model.ts @@ -1,8 +1,15 @@ import { expect, type Page } from "@playwright/test" -import { a11y, clickOutside, clickViz, controls, repeat } from "../_utils" +import { a11y, clickOutside, clickViz, controls, DOM_MODE, repeat } from "../_utils" export class Model { - constructor(public page: Page) {} + constructor( + public page: Page, + private shadowHost?: string, + ) {} + + get host() { + return DOM_MODE === "shadow-dom" && this.shadowHost ? this.page.locator(this.shadowHost) : this.page + } get controls() { return controls(this.page) @@ -17,7 +24,7 @@ export class Model { } checkAccessibility(selector?: string) { - return a11y(this.page, selector) + return a11y(this.page, selector, this.shadowHost) } pressKey(key: string, times = 1) { diff --git a/e2e/models/popover.model.ts b/e2e/models/popover.model.ts index 4ad475cb77..c1e199bc20 100644 --- a/e2e/models/popover.model.ts +++ b/e2e/models/popover.model.ts @@ -1,14 +1,12 @@ import { expect, type Page } from "@playwright/test" -import { a11y, testid } from "../_utils" +import { testid } from "../_utils" import { Model } from "./model" +const shadowHost = "popover-page" + export class PopoverModel extends Model { constructor(public page: Page) { - super(page) - } - - checkAccessibility() { - return a11y(this.page) + super(page, shadowHost) } goto() { @@ -16,31 +14,31 @@ export class PopoverModel extends Model { } get trigger() { - return this.page.locator("[data-scope=popover][data-part=trigger]") + return this.host.locator("[data-scope=popover][data-part=trigger]") } get content() { - return this.page.locator("[data-scope=popover][data-part=content]") + return this.host.locator("[data-scope=popover][data-part=content]") } get closeTrigger() { - return this.page.locator("[data-scope=popover][data-part=close-trigger]") + return this.content.locator("[data-scope=popover][data-part=close-trigger]") } get buttonBefore() { - return this.page.locator(testid("button-before")) + return this.host.locator(testid("button-before")) } get buttonAfter() { - return this.page.locator(testid("button-after")) + return this.host.locator(testid("button-after")) } get link() { - return this.page.locator(testid("focusable-link")) + return this.content.locator(testid("focusable-link")) } get plainText() { - return this.page.locator(testid("plain-text")) + return this.host.locator(testid("plain-text")) } clickClose() { diff --git a/e2e/models/radio-group.model.ts b/e2e/models/radio-group.model.ts index 0cb5e45436..cc72bfea87 100644 --- a/e2e/models/radio-group.model.ts +++ b/e2e/models/radio-group.model.ts @@ -3,7 +3,7 @@ import { Model } from "./model" export class RadioGroupModel extends Model { constructor(public page: Page) { - super(page) + super(page, "radio-group-page") } goto(url = "/radio-group") { @@ -11,19 +11,19 @@ export class RadioGroupModel extends Model { } get root() { - return this.page.locator("[data-scope='radio-group'][data-part='root']") + return this.host.locator("[data-scope='radio-group'][data-part='root']") } get label() { - return this.page.locator("[data-scope='radio-group'][data-part='label']") + return this.host.locator("[data-scope='radio-group'][data-part='label']") } getRadio(value: string) { return { - radio: this.page.getByTestId(`radio-${value}`), - label: this.page.getByTestId(`label-${value}`), - input: this.page.getByTestId(`input-${value}`), - control: this.page.getByTestId(`control-${value}`), + radio: this.host.getByTestId(`radio-${value}`), + label: this.host.getByTestId(`label-${value}`), + input: this.host.getByTestId(`input-${value}`), + control: this.host.getByTestId(`control-${value}`), } } diff --git a/e2e/models/slider.model.ts b/e2e/models/slider.model.ts index e71556b350..ddc5d440cc 100644 --- a/e2e/models/slider.model.ts +++ b/e2e/models/slider.model.ts @@ -1,14 +1,13 @@ import { expect, type Page } from "@playwright/test" -import { a11y, rect } from "../_utils" +import { rect } from "../_utils" import { Model } from "./model" export class SliderModel extends Model { - constructor(public page: Page) { - super(page) - } - - checkAccessibility() { - return a11y(this.page) + constructor( + public page: Page, + shadowHost = "slider-page", + ) { + super(page, shadowHost) } goto(url = "/slider") { @@ -16,15 +15,15 @@ export class SliderModel extends Model { } getThumb(index = 0) { - return this.page.locator(`[data-scope='slider'][data-part='thumb'][data-index='${index}']`) + return this.host.locator(`[data-scope='slider'][data-part='thumb'][data-index='${index}']`) } get track() { - return this.page.locator("[data-scope='slider'][data-part='track']") + return this.host.locator("[data-scope='slider'][data-part='track']") } get output() { - return this.page.locator("[data-scope='slider'][data-part='value-text']") + return this.host.locator("[data-scope='slider'][data-part='value-text']") } focusThumb(index?: number) { diff --git a/e2e/models/switch.model.ts b/e2e/models/switch.model.ts index be9ba41e51..938ad0ddf2 100644 --- a/e2e/models/switch.model.ts +++ b/e2e/models/switch.model.ts @@ -1,14 +1,12 @@ import { expect, type Page } from "@playwright/test" -import { a11y, repeat } from "../_utils" +import { repeat } from "../_utils" import { Model } from "./model" +const shadowHost = "switch-page" + export class SwitchModel extends Model { constructor(public page: Page) { - super(page) - } - - checkAccessibility() { - return a11y(this.page) + super(page, shadowHost) } goto(url = "/switch") { @@ -16,19 +14,19 @@ export class SwitchModel extends Model { } get root() { - return this.page.locator("[data-scope='switch'][data-part='root']") + return this.host.locator("[data-scope='switch'][data-part='root']") } get label() { - return this.page.locator("[data-scope='switch'][data-part='label']") + return this.host.locator("[data-scope='switch'][data-part='label']") } get control() { - return this.page.locator("[data-scope='switch'][data-part='control']") + return this.host.locator("[data-scope='switch'][data-part='control']") } get input() { - return this.page.locator("[data-scope='switch'][data-part='root'] input") + return this.host.locator("[data-scope='switch'][data-part='root'] input") } async clickCheckbox() { diff --git a/e2e/models/tabs.model.ts b/e2e/models/tabs.model.ts index dfae098881..bbf485cba4 100644 --- a/e2e/models/tabs.model.ts +++ b/e2e/models/tabs.model.ts @@ -1,14 +1,12 @@ import { expect, type Page } from "@playwright/test" -import { a11y, testid } from "../_utils" +import { testid } from "../_utils" import { Model } from "./model" +const shadowHost = "tabs-page" + export class TabsModel extends Model { constructor(public page: Page) { - super(page) - } - - checkAccessibility() { - return a11y(this.page) + super(page, shadowHost) } goto() { @@ -16,11 +14,11 @@ export class TabsModel extends Model { } private getTabTrigger = (id: string) => { - return this.page.locator(testid(`${id}-tab`)) + return this.host.locator(testid(`${id}-tab`)) } private getTabContent = (id: string) => { - return this.page.locator(testid(`${id}-tab-panel`)) + return this.host.locator(testid(`${id}-tab-panel`)) } clickTab = async (id: string) => { diff --git a/e2e/models/toggle-group.model.ts b/e2e/models/toggle-group.model.ts index 18e79bd122..dd75be3ce0 100644 --- a/e2e/models/toggle-group.model.ts +++ b/e2e/models/toggle-group.model.ts @@ -2,15 +2,17 @@ import { type Page, expect } from "@playwright/test" import { Model } from "./model" import { part } from "../_utils" +const shadowHost = "toggle-group-page" + type Item = "bold" | "italic" | "underline" export class ToggleGroupModel extends Model { constructor(page: Page) { - super(page) + super(page, shadowHost) } private __item(item: Item) { - return this.page.locator(part("item")).nth(["bold", "italic", "underline"].indexOf(item)) + return this.host.locator(part("item")).nth(["bold", "italic", "underline"].indexOf(item)) } clickItem(item: Item) { diff --git a/e2e/popover.e2e.ts b/e2e/popover.e2e.ts index bb136ad027..c54598d056 100644 --- a/e2e/popover.e2e.ts +++ b/e2e/popover.e2e.ts @@ -92,4 +92,13 @@ test.describe("popover", () => { await I.seeButtonBeforeIsFocused() await I.dontSeeContent() }) + + test("[focus] should close popover when focus moves to button-after element", async () => { + await I.clickTrigger() + await I.seeContent() + await I.seeLinkIsFocused() + await I.pressKey("Tab", 3) + await I.seeButtonAfterIsFocused() + await I.dontSeeContent() + }) }) diff --git a/examples/lit-ts/.gitignore b/examples/lit-ts/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/examples/lit-ts/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/lit-ts/index.html b/examples/lit-ts/index.html new file mode 100644 index 0000000000..c240aa0398 --- /dev/null +++ b/examples/lit-ts/index.html @@ -0,0 +1,13 @@ + + + + + + + Zag.js + Lit + + + + + + diff --git a/examples/lit-ts/package.json b/examples/lit-ts/package.json new file mode 100644 index 0000000000..6cad639913 --- /dev/null +++ b/examples/lit-ts/package.json @@ -0,0 +1,98 @@ +{ + "name": "lit-ts", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3004", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@types/form-serialize": "0.7.4", + "typescript": "5.9.2", + "vite": "7.1.3" + }, + "dependencies": { + "@internationalized/date": "3.9.0", + "@open-wc/lit-helpers": "0.7.0", + "@zag-js/accordion": "workspace:*", + "@zag-js/anatomy": "workspace:*", + "@zag-js/anatomy-icons": "workspace:*", + "@zag-js/angle-slider": "workspace:*", + "@zag-js/aria-hidden": "workspace:*", + "@zag-js/async-list": "workspace:*", + "@zag-js/auto-resize": "workspace:*", + "@zag-js/avatar": "workspace:*", + "@zag-js/carousel": "workspace:*", + "@zag-js/checkbox": "workspace:*", + "@zag-js/clipboard": "workspace:*", + "@zag-js/collapsible": "workspace:*", + "@zag-js/collection": "workspace:*", + "@zag-js/color-picker": "workspace:*", + "@zag-js/color-utils": "workspace:*", + "@zag-js/combobox": "workspace:*", + "@zag-js/core": "workspace:*", + "@zag-js/date-picker": "workspace:*", + "@zag-js/date-utils": "workspace:*", + "@zag-js/dialog": "workspace:*", + "@zag-js/dismissable": "workspace:*", + "@zag-js/docs": "workspace:*", + "@zag-js/dom-query": "workspace:*", + "@zag-js/editable": "workspace:*", + "@zag-js/file-upload": "workspace:*", + "@zag-js/file-utils": "workspace:*", + "@zag-js/floating-panel": "workspace:*", + "@zag-js/focus-trap": "workspace:*", + "@zag-js/focus-visible": "workspace:*", + "@zag-js/highlight-word": "workspace:*", + "@zag-js/hover-card": "workspace:*", + "@zag-js/i18n-utils": "workspace:*", + "@zag-js/interact-outside": "workspace:*", + "@zag-js/json-tree-utils": "workspace:*", + "@zag-js/listbox": "workspace:*", + "@zag-js/lit": "workspace:*", + "@zag-js/live-region": "workspace:*", + "@zag-js/menu": "workspace:*", + "@zag-js/navigation-menu": "workspace:*", + "@zag-js/number-input": "workspace:*", + "@zag-js/pagination": "workspace:*", + "@zag-js/password-input": "workspace:*", + "@zag-js/pin-input": "workspace:*", + "@zag-js/popover": "workspace:*", + "@zag-js/popper": "workspace:*", + "@zag-js/presence": "workspace:*", + "@zag-js/progress": "workspace:*", + "@zag-js/qr-code": "workspace:*", + "@zag-js/radio-group": "workspace:*", + "@zag-js/rating-group": "workspace:*", + "@zag-js/rect-utils": "workspace:*", + "@zag-js/remove-scroll": "workspace:*", + "@zag-js/scroll-snap": "workspace:*", + "@zag-js/select": "workspace:*", + "@zag-js/shared": "workspace:*", + "@zag-js/signature-pad": "workspace:*", + "@zag-js/slider": "workspace:*", + "@zag-js/splitter": "workspace:*", + "@zag-js/steps": "workspace:*", + "@zag-js/store": "workspace:*", + "@zag-js/stringify-state": "workspace:*", + "@zag-js/switch": "workspace:*", + "@zag-js/tabs": "workspace:*", + "@zag-js/tags-input": "workspace:*", + "@zag-js/timer": "workspace:*", + "@zag-js/toast": "workspace:*", + "@zag-js/toggle": "workspace:*", + "@zag-js/toggle-group": "workspace:*", + "@zag-js/tooltip": "workspace:*", + "@zag-js/tour": "workspace:*", + "@zag-js/tree-view": "workspace:*", + "@zag-js/types": "workspace:*", + "@zag-js/utils": "workspace:*", + "form-serialize": "0.7.2", + "lit": "3.3.1", + "lucide": "0.542.0", + "match-sorter": "8.1.0", + "nanoid": "^5.1.5" + } +} diff --git a/examples/lit-ts/public/vite.svg b/examples/lit-ts/public/vite.svg new file mode 100644 index 0000000000..e7b8dfb1b2 --- /dev/null +++ b/examples/lit-ts/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/lit-ts/src/components/state-visualizer.ts b/examples/lit-ts/src/components/state-visualizer.ts new file mode 100644 index 0000000000..b7cbf9e958 --- /dev/null +++ b/examples/lit-ts/src/components/state-visualizer.ts @@ -0,0 +1,53 @@ +import { LitElement, html, unsafeCSS } from "lit" +import { customElement, property } from "lit/decorators.js" +import { unsafeHTML } from "lit/directives/unsafe-html.js" +import type { MachineSchema, Service } from "@zag-js/core" +import { highlightState } from "@zag-js/stringify-state" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" + +@customElement("state-visualizer") +export class StateVisualizer extends LitElement { + static styles = unsafeCSS(styleLayout) + + @property({ attribute: false }) + state?: Service + + @property({ type: String }) + label?: string + + @property({ attribute: false }) + omit?: string[] + + @property({ attribute: false }) + context?: Array + + render() { + const service = this.state + + if (!service) { + return html`
No state available
` + } + + const obj = { + state: service.state.get(), + event: service.event.current(), + previousEvent: service.event.previous(), + context: this.context + ? Object.fromEntries(this.context.map((key) => [key, service.context.get(key)])) + : undefined, + } + + const highlighted = highlightState(obj, this.omit) + + return html` +
+
+          
+ ${this.label || "Visualizer"} +
${unsafeHTML(highlighted)}
+
+
+
+ ` + } +} diff --git a/examples/lit-ts/src/components/toolbar.ts b/examples/lit-ts/src/components/toolbar.ts new file mode 100644 index 0000000000..0bf4d47bd0 --- /dev/null +++ b/examples/lit-ts/src/components/toolbar.ts @@ -0,0 +1,140 @@ +import { LitElement, html, unsafeCSS } from "lit" +import { customElement, property, state } from "lit/decorators.js" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import type { ControlsController } from "../lib/controls-controller" + +@customElement("zag-toolbar") +export class Toolbar extends LitElement { + static styles = unsafeCSS(styleLayout) + + @property({ attribute: false }) + controls?: ControlsController + + @state() + private activeTab = 0 + + render() { + const hasControls = !!this.controls + + return html` +
+ + +
+ ${hasControls + ? html`
${this.renderControls()}
` + : ""} +
+ +
+
+
+ ` + } + + private renderControls() { + const { controls } = this + if (!controls) return "" + + return html` +
+ ${controls.getControlKeys().map((key) => { + const config = controls.getControlConfig(key) + const value = controls.getValue(key) + const { type, label = key, options, placeholder, min, max } = config + + switch (type) { + case "boolean": + return html` +
+ { + const target = e.target as HTMLInputElement + controls.setState(key, target.checked) + }} + /> + +
+ ` + case "string": + return html` +
+ + { + if (e.key === "Enter") { + const target = e.target as HTMLInputElement + controls.setState(key, target.value) + } + }} + /> +
+ ` + case "select": + return html` +
+ + +
+ ` + case "number": + return html` +
+ + { + if (e.key === "Enter") { + const target = e.target as HTMLInputElement + const val = parseFloat(target.value) + controls.setState(key, isNaN(val) ? 0 : val) + } + }} + /> +
+ ` + default: + return "" + } + })} +
+ ` + } +} diff --git a/examples/lit-ts/src/lib/controls-controller.ts b/examples/lit-ts/src/lib/controls-controller.ts new file mode 100644 index 0000000000..a612456439 --- /dev/null +++ b/examples/lit-ts/src/lib/controls-controller.ts @@ -0,0 +1,46 @@ +import { type ControlRecord, deepGet, deepSet, getControlDefaults } from "@zag-js/shared" +import type { ReactiveController, ReactiveControllerHost } from "lit" + +export class ControlsController implements ReactiveController { + private host: ReactiveControllerHost + private state: any + private config: T + + constructor(host: ReactiveControllerHost, config: T) { + this.host = host + this.config = config + this.state = getControlDefaults(config) + host.addController(this) + } + + hostConnected() { + // Nothing needed here for now + } + + hostDisconnected() { + // Nothing needed here for now + } + + get context() { + return this.state + } + + setState(key: string, value: any) { + const newState = structuredClone(this.state) + deepSet(newState, key, value) + this.state = newState + this.host.requestUpdate() + } + + getValue(key: string) { + return deepGet(this.state, key) + } + + getControlKeys() { + return Object.keys(this.config) + } + + getControlConfig(key: string) { + return this.config[key] as any + } +} diff --git a/examples/lit-ts/src/lib/page-element.ts b/examples/lit-ts/src/lib/page-element.ts new file mode 100644 index 0000000000..fd54e4a99f --- /dev/null +++ b/examples/lit-ts/src/lib/page-element.ts @@ -0,0 +1,27 @@ +import { LitElement } from "lit" + +// Detect DOM mode - use Vite's import.meta.env for client-side access +// These are injected at build time by Vite +const DOM_MODE = import.meta.env?.VITE_DOM_MODE === "light-dom" ? "light-dom" : "shadow-dom" + +/** + * Base class for page components that automatically switches between Shadow DOM and Light DOM + * based on the testing context. + * + * Environment Variables: + * - DOM_MODE=shadow-dom: Force Shadow DOM regardless of framework + * - DOM_MODE=light-dom: Force Light DOM regardless of framework + * - FRAMEWORK=lit: Default to Shadow DOM (can be overridden by DOM_MODE) + * - Default: Light DOM for CSS compatibility + */ +export class PageElement extends LitElement { + constructor() { + super() + + // Use Light DOM by default for CSS compatibility, unless explicitly testing Shadow DOM + if (DOM_MODE === "light-dom") { + this.createRenderRoot = () => this + } + // When DOM_MODE === 'shadow-dom', use default Lit behavior (Shadow DOM) + } +} diff --git a/examples/lit-ts/src/main.css b/examples/lit-ts/src/main.css new file mode 100644 index 0000000000..acfaffcd06 --- /dev/null +++ b/examples/lit-ts/src/main.css @@ -0,0 +1,4 @@ +.component-page { + flex: auto; + display: flex; +} diff --git a/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts new file mode 100644 index 0000000000..49d2998e6d --- /dev/null +++ b/examples/lit-ts/src/main.ts @@ -0,0 +1,167 @@ +import { LitElement, html } from "lit" +import { customElement, property } from "lit/decorators.js" +import { routesData } from "@zag-js/shared" + +import "@zag-js/shared/src/style.css" +import "./main.css" + +// Import toolbar components +import "./components/toolbar" +import "./components/state-visualizer" + +// Import all page components +import "./pages/accordion" +import "./pages/checkbox" +import "./pages/collapsible" +import "./pages/dialog" +import "./pages/dialog-nested" +import "./pages/menu" +import "./pages/menu-nested" +import "./pages/menu-options" +import "./pages/popover" +import "./pages/radio-group" +import "./pages/range-slider" +import "./pages/slider" +import "./pages/switch" +import "./pages/tabs" +import "./pages/toggle" +import "./pages/toggle-group" + +// Sort alphabetically in place (Why not in shared/?) +routesData.sort((a, b) => a.label.localeCompare(b.label)) + +@customElement("zag-app") +export class ZagApp extends LitElement { + // Light dom (no shadow root) due to css + protected createRenderRoot() { + return this + } + + @property({ type: String }) + currentPath = window.location.pathname + + connectedCallback() { + super.connectedCallback() + this.updatePath() + window.addEventListener("popstate", this.updatePath) + } + + disconnectedCallback() { + super.disconnectedCallback() + window.removeEventListener("popstate", this.updatePath) + } + + private updatePath = () => { + this.currentPath = window.location.pathname + } + + private navigate(path: string) { + window.history.pushState({}, "", path) + this.currentPath = path + } + + private renderContent() { + switch (this.currentPath) { + case "/accordion": + return html`` + case "/checkbox": + return html`` + case "/collapsible": + return html`` + case "/dialog": + return html`` + case "/dialog-nested": + return html`` + case "/menu": + return html`` + case "/menu-nested": + return html`` + case "/menu-options": + return html`` + case "/popover": + return html`` + case "/radio-group": + return html`` + case "/range-slider": + return html`` + case "/slider": + return html`` + case "/switch": + return html`` + case "/tabs": + return html`` + case "/toggle": + return html`` + case "/toggle-group": + return html`` + default: + return this.renderHome() + } + } + + private renderHome() { + return html` +
+

Zag.js + Lit

+

Select a component from the sidebar to see it in action.

+
+ ` + } + + render() { + const routes = routesData.filter((route) => + // Only show routes we have implemented + [ + "/accordion", + "/checkbox", + "/collapsible", + "/dialog", + "/dialog-nested", + "/menu", + "/menu-nested", + "/menu-options", + "/popover", + "/radio-group", + "/range-slider", + "/slider", + "/switch", + "/tabs", + "/toggle", + "/toggle-group", + ].includes(route.path), + ) + + return html` + + ` + } +} diff --git a/examples/lit-ts/src/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts new file mode 100644 index 0000000000..0d05379578 --- /dev/null +++ b/examples/lit-ts/src/pages/accordion.ts @@ -0,0 +1,58 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as accordion from "@zag-js/accordion" +import { accordionControls, accordionData } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/accordion.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { ArrowRight, createElement } from "lucide" +import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("accordion-page") +export class AccordionPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, accordionControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, accordion.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + ...this.controls.context, + })) + + render() { + const api = accordion.connect(this.machine.service, normalizeProps) + + return html` +
+
+ ${accordionData.map( + (item) => html` +
+

+ +

+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. +
+
+ `, + )} +
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/checkbox.ts b/examples/lit-ts/src/pages/checkbox.ts new file mode 100644 index 0000000000..07274ccea4 --- /dev/null +++ b/examples/lit-ts/src/pages/checkbox.ts @@ -0,0 +1,61 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as checkbox from "@zag-js/checkbox" +import { checkboxControls } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/checkbox.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("checkbox-page") +export class CheckboxPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, checkboxControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, checkbox.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + name: "checkbox", + ...this.controls.context, + })) + + private handleFormChange = (e: Event) => { + const form = e.currentTarget as HTMLFormElement + const formData = new FormData(form) + const result = Object.fromEntries(formData.entries()) + console.log(result) + } + + render() { + const api = checkbox.connect(this.machine.service, normalizeProps) + + return html` +
+
+
+ + + + + +
+
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/collapsible.ts b/examples/lit-ts/src/pages/collapsible.ts new file mode 100644 index 0000000000..0fef118441 --- /dev/null +++ b/examples/lit-ts/src/pages/collapsible.ts @@ -0,0 +1,62 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as collapsible from "@zag-js/collapsible" +import { collapsibleControls } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/collapsible.css?inline" +import styleKeyframes from "@zag-js/shared/src/css/keyframes.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" +import { ChevronDown, createElement } from "lucide" + +@customElement("collapsible-page") +export class CollapsiblePage extends PageElement { + static styles = unsafeCSS(styleComponent + styleKeyframes + styleLayout + stylePage) + + private controls = new ControlsController(this, collapsibleControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, collapsible.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + ...this.controls.context, + })) + + render() { + const api = collapsible.connect(this.machine.service, normalizeProps) + + return html` +
+
+ +
+

+ Lorem dfd dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna sfsd. Ut enim ad minimdfd v eniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt + mollit anim id est laborum. Some Link +

+
+
+ +
+
Toggle Controls
+ + +
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/dialog-nested.ts b/examples/lit-ts/src/pages/dialog-nested.ts new file mode 100644 index 0000000000..871ec408f7 --- /dev/null +++ b/examples/lit-ts/src/pages/dialog-nested.ts @@ -0,0 +1,84 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as dialog from "@zag-js/dialog" +import styleComponent from "@zag-js/shared/src/css/dialog.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { PageElement } from "../lib/page-element" + +@customElement("dialog-nested-page") +export class DialogNestedPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private machine1Id = nanoid(5) + private machine2Id = nanoid(5) + + // Dialog 1 + private machine1 = new MachineController(this, dialog.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machine1Id, + })) + + // Dialog 2 + private machine2 = new MachineController(this, dialog.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machine2Id, + })) + + render() { + const parentDialog = dialog.connect(this.machine1.service, normalizeProps) + const childDialog = dialog.connect(this.machine2.service, normalizeProps) + + return html` +
+
+ + +
+ + ${parentDialog.open + ? html` +
+
+
+

Edit profile

+

+ Make changes to your profile here. Click save when you are done. +

+ + + + + + + ${childDialog.open + ? html` +
+
+
+

Nested

+ + +
+
+ ` + : ""} +
+
+ ` + : ""} +
+
+ + + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/dialog.ts b/examples/lit-ts/src/pages/dialog.ts new file mode 100644 index 0000000000..8cc84fcced --- /dev/null +++ b/examples/lit-ts/src/pages/dialog.ts @@ -0,0 +1,55 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as dialog from "@zag-js/dialog" +import styleComponent from "@zag-js/shared/src/css/dialog.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { PageElement } from "../lib/page-element" + +@customElement("dialog-page") +export class DialogPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private machineId = nanoid(5) + + private machine = new MachineController(this, dialog.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + })) + + render() { + const api = dialog.connect(this.machine.service, normalizeProps) + + return html` +
+ + + ${api.open + ? html` +
+
+
+

Edit profile

+

+ Make changes to your profile here. Click save when you are done. +

+
+ + +
+ +
+
+ ` + : ""} +
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/menu-nested.ts b/examples/lit-ts/src/pages/menu-nested.ts new file mode 100644 index 0000000000..6bf5417a31 --- /dev/null +++ b/examples/lit-ts/src/pages/menu-nested.ts @@ -0,0 +1,106 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as menu from "@zag-js/menu" +import { menuData } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/menu.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { PageElement } from "../lib/page-element" + +@customElement("menu-nested-page") +export class MenuNestedPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private rootId = nanoid(5) + private subId = nanoid(5) + private sub2Id = nanoid(5) + + private rootMachine = new MachineController(this, menu.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.rootId, + })) + + private subMachine = new MachineController(this, menu.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.subId, + })) + + private sub2Machine = new MachineController(this, menu.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.sub2Id, + })) + + protected firstUpdated() { + const root = menu.connect(this.rootMachine.service, normalizeProps) + const sub = menu.connect(this.subMachine.service, normalizeProps) + const sub2 = menu.connect(this.sub2Machine.service, normalizeProps) + + // Set up parent-child relationships + root.setChild(this.subMachine.service) + sub.setParent(this.rootMachine.service) + + sub.setChild(this.sub2Machine.service) + sub2.setParent(this.subMachine.service) + } + + render() { + const root = menu.connect(this.rootMachine.service, normalizeProps) + const sub = menu.connect(this.subMachine.service, normalizeProps) + const sub2 = menu.connect(this.sub2Machine.service, normalizeProps) + + const triggerItemProps = root.getTriggerItemProps(sub) + const triggerItem2Props = sub.getTriggerItemProps(sub2) + + const [level1, level2, level3] = menuData + + return html` +
+
+ + + +
+
    + ${level1.map((item) => { + const props = item.trigger ? triggerItemProps : root.getItemProps({ value: item.value }) + return html`
  • ${item.label}
  • ` + })} +
+
+ + +
+
    + ${level2.map((item) => { + const props = item.trigger ? triggerItem2Props : sub.getItemProps({ value: item.value }) + return html`
  • ${item.label}
  • ` + })} +
+
+ + +
+
    + ${level3.map( + (item) => html` +
  • + ${item.label} +
  • + `, + )} +
+
+
+
+ + + + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/menu-options.ts b/examples/lit-ts/src/pages/menu-options.ts new file mode 100644 index 0000000000..1161fb6e1b --- /dev/null +++ b/examples/lit-ts/src/pages/menu-options.ts @@ -0,0 +1,102 @@ +import { html, unsafeCSS } from "lit" +import { customElement, state } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as menu from "@zag-js/menu" +import { menuOptionData, menuControls } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/menu.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("menu-options-page") +export class MenuOptionsPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, menuControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, menu.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + ...this.controls.context, + })) + + @state() + private order = "" + + @state() + private type: string[] = [] + + private setOrder = (value: string, checked: boolean) => { + this.order = checked ? value : "" + } + + private setType = (value: string, checked: boolean) => { + if (checked) { + this.type = [...this.type, value] + } else { + this.type = this.type.filter((x) => x !== value) + } + } + + render() { + const api = menu.connect(this.machine.service, normalizeProps) + + const radios = menuOptionData.order.map((item) => ({ + type: "radio" as const, + name: "order", + value: item.value, + label: item.label, + checked: this.order === item.value, + onCheckedChange: (checked: boolean) => this.setOrder(item.value, checked), + })) + + const checkboxes = menuOptionData.type.map((item) => ({ + type: "checkbox" as const, + name: "type", + value: item.value, + label: item.label, + checked: this.type.includes(item.value), + onCheckedChange: (checked: boolean) => this.setType(item.value, checked), + })) + + return html` +
+
+ + +
+
+ ${radios.map( + (item) => html` +
+ ✅ + ${item.label} +
+ `, + )} +
+ ${checkboxes.map( + (item) => html` +
+ ✅ + ${item.label} +
+ `, + )} +
+
+
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/menu.ts b/examples/lit-ts/src/pages/menu.ts new file mode 100644 index 0000000000..b95cbfe59d --- /dev/null +++ b/examples/lit-ts/src/pages/menu.ts @@ -0,0 +1,57 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as menu from "@zag-js/menu" +import { menuControls } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/menu.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("menu-page") +export class MenuPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, menuControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, menu.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + onSelect: console.log, + ...this.controls.context, + })) + + render() { + const api = menu.connect(this.machine.service, normalizeProps) + + return html` +
+
+ + ${api.open + ? html` +
+
    +
  • Edit
  • +
  • + Duplicate +
  • +
  • Delete
  • +
  • Export...
  • +
+
+ ` + : ""} +
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/page.css b/examples/lit-ts/src/pages/page.css new file mode 100644 index 0000000000..45d77b862f --- /dev/null +++ b/examples/lit-ts/src/pages/page.css @@ -0,0 +1,3 @@ +:host { + outline: none !important; +} diff --git a/examples/lit-ts/src/pages/popover.ts b/examples/lit-ts/src/pages/popover.ts new file mode 100644 index 0000000000..23f42e1ac4 --- /dev/null +++ b/examples/lit-ts/src/pages/popover.ts @@ -0,0 +1,70 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as popover from "@zag-js/popover" +import { popoverControls } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/popover.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("popover-page") +export class PopoverPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, popoverControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, popover.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + ...this.controls.context, + })) + + render() { + const api = popover.connect(this.machine.service, normalizeProps) + + return html` +
+
+ + + + +
anchor
+ + ${api.open + ? html` +
+
+
+
+
+
Popover Title
+ +
+
+ ` + : ""} + I am just text + +
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/radio-group.ts b/examples/lit-ts/src/pages/radio-group.ts new file mode 100644 index 0000000000..2947727515 --- /dev/null +++ b/examples/lit-ts/src/pages/radio-group.ts @@ -0,0 +1,71 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as radio from "@zag-js/radio-group" +import { radioControls, radioData } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/radio-group.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("radio-group-page") +export class RadioGroupPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, radioControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, radio.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + name: "fruit", + ...this.controls.context, + })) + + private handleFormChange = (e: Event) => { + const form = e.currentTarget as HTMLFormElement + const formData = new FormData(form) + const result = Object.fromEntries(formData.entries()) + console.log(result) + } + + render() { + const api = radio.connect(this.machine.service, normalizeProps) + + return html` +
+
+
+
+

Fruits

+
+ ${radioData.map( + (opt) => html` + + `, + )} +
+ + + + + +
+
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/range-slider.ts b/examples/lit-ts/src/pages/range-slider.ts new file mode 100644 index 0000000000..56e045dd9c --- /dev/null +++ b/examples/lit-ts/src/pages/range-slider.ts @@ -0,0 +1,76 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as slider from "@zag-js/slider" +import { sliderControls } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/slider.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import serialize from "form-serialize" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("range-slider-page") +export class RangeSliderPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, sliderControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, slider.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + name: "quantity", + defaultValue: [10, 60], + ...this.controls.context, + })) + + private handleFormChange = (e: Event) => { + const form = e.currentTarget as HTMLFormElement + const formData = serialize(form, { hash: true }) + console.log(formData) + } + + render() { + const api = slider.connect(this.machine.service, normalizeProps) + + return html` +
+
+
+
+ + ${api.value.join(" - ")} +
+
+
+
+
+
+ ${api.value.map( + (_, index) => html` +
+ +
+ `, + )} +
+
+ * + * + * + * +
+
+
+
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/slider.ts b/examples/lit-ts/src/pages/slider.ts new file mode 100644 index 0000000000..2067f82885 --- /dev/null +++ b/examples/lit-ts/src/pages/slider.ts @@ -0,0 +1,76 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as slider from "@zag-js/slider" +import { sliderControls } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/slider.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import serialize from "form-serialize" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("slider-page") +export class SliderPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, sliderControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, slider.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + name: "quantity", + defaultValue: [0], + ...this.controls.context, + })) + + private handleFormChange = (e: Event) => { + const form = e.currentTarget as HTMLFormElement + const formData = serialize(form, { hash: true }) + console.log(formData) + } + + render() { + const api = slider.connect(this.machine.service, normalizeProps) + + return html` +
+
+
+
+ + ${api.value} +
+
+
+
+
+
+ ${api.getThumbValue(0)} + ${api.value.map( + (_, index) => html` +
+ +
+ `, + )} +
+
+ * + * + * +
+
+
+
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/switch.ts b/examples/lit-ts/src/pages/switch.ts new file mode 100644 index 0000000000..f02734f644 --- /dev/null +++ b/examples/lit-ts/src/pages/switch.ts @@ -0,0 +1,47 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as zagSwitch from "@zag-js/switch" +import { switchControls } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/switch.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("switch-page") +export class SwitchPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, switchControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, zagSwitch.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + name: "switch", + ...this.controls.context, + })) + + render() { + const api = zagSwitch.connect(this.machine.service, normalizeProps) + + return html` +
+ +
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/tabs.ts b/examples/lit-ts/src/pages/tabs.ts new file mode 100644 index 0000000000..d1c80d6be2 --- /dev/null +++ b/examples/lit-ts/src/pages/tabs.ts @@ -0,0 +1,60 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as tabs from "@zag-js/tabs" +import { tabsControls, tabsData } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/tabs.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("tabs-page") +export class TabsPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, tabsControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, tabs.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + defaultValue: "nils", + ...this.controls.context, + })) + + render() { + const api = tabs.connect(this.machine.service, normalizeProps) + + return html` +
+
+
+
+ ${tabsData.map( + (data) => html` + + `, + )} +
+ ${tabsData.map( + (data) => html` +
+

${data.content}

+ ${data.id === "agnes" ? html`` : null} +
+ `, + )} +
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/toggle-group.ts b/examples/lit-ts/src/pages/toggle-group.ts new file mode 100644 index 0000000000..b68f78b5c4 --- /dev/null +++ b/examples/lit-ts/src/pages/toggle-group.ts @@ -0,0 +1,45 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as toggleGroup from "@zag-js/toggle-group" +import { toggleGroupControls, toggleGroupData } from "@zag-js/shared" +import styleComponent from "@zag-js/shared/src/css/toggle-group.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import { PageElement } from "../lib/page-element" + +@customElement("toggle-group-page") +export class ToggleGroupPage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private controls = new ControlsController(this, toggleGroupControls) + private machineId = nanoid(5) + + private machine = new MachineController(this, toggleGroup.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: this.machineId, + ...this.controls.context, + })) + + render() { + const api = toggleGroup.connect(this.machine.service, normalizeProps) + + return html` +
+ +
+ ${toggleGroupData.map( + (item) => html``, + )} +
+
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/pages/toggle.ts b/examples/lit-ts/src/pages/toggle.ts new file mode 100644 index 0000000000..0afce7eca4 --- /dev/null +++ b/examples/lit-ts/src/pages/toggle.ts @@ -0,0 +1,33 @@ +import { html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" +import * as toggle from "@zag-js/toggle" +import styleComponent from "@zag-js/shared/src/css/toggle.css?inline" +import styleLayout from "@zag-js/shared/src/css/layout.css?inline" +import stylePage from "./page.css?inline" +import { MachineController, normalizeProps } from "@zag-js/lit" +import { Bold, createElement } from "lucide" +import { PageElement } from "../lib/page-element" + +@customElement("toggle-page") +export class TogglePage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private machine = new MachineController(this, toggle.machine) + + render() { + const api = toggle.connect(this.machine.service, normalizeProps) + + return html` +
+ +
+ + + + + ` + } +} diff --git a/examples/lit-ts/src/vite-env.d.ts b/examples/lit-ts/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/lit-ts/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/lit-ts/tsconfig.json b/examples/lit-ts/tsconfig.json new file mode 100644 index 0000000000..833d304523 --- /dev/null +++ b/examples/lit-ts/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts"] +} diff --git a/examples/next-ts/pages/collapsible.tsx b/examples/next-ts/pages/collapsible.tsx index 18de760cd3..d1c052e841 100644 --- a/examples/next-ts/pages/collapsible.tsx +++ b/examples/next-ts/pages/collapsible.tsx @@ -40,7 +40,9 @@ export default function Page() {
Toggle Controls
- +
diff --git a/examples/nuxt-ts/app/pages/collapsible.vue b/examples/nuxt-ts/app/pages/collapsible.vue index 6b8fcbcfe5..8de743bbca 100644 --- a/examples/nuxt-ts/app/pages/collapsible.vue +++ b/examples/nuxt-ts/app/pages/collapsible.vue @@ -35,6 +35,12 @@ const api = computed(() => collapsible.connect(service, normalizeProps))

+ +
+
Toggle Controls
+ + +
diff --git a/examples/solid-ts/src/routes/collapsible.tsx b/examples/solid-ts/src/routes/collapsible.tsx index dfa27a5e75..fe896187f3 100644 --- a/examples/solid-ts/src/routes/collapsible.tsx +++ b/examples/solid-ts/src/routes/collapsible.tsx @@ -37,7 +37,9 @@ export default function Page() {
Toggle Controls
- +
diff --git a/examples/svelte-ts/src/routes/collapsible.svelte b/examples/svelte-ts/src/routes/collapsible.svelte index ed1ba4e47f..d3cbf1ed3c 100644 --- a/examples/svelte-ts/src/routes/collapsible.svelte +++ b/examples/svelte-ts/src/routes/collapsible.svelte @@ -41,7 +41,7 @@
Toggle Controls
- +
diff --git a/package.json b/package.json index 26afdea8b1..0a1d732820 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "e2e-react": "cross-env FRAMEWORK=react playwright test", "e2e-vue": "cross-env FRAMEWORK=vue playwright test", "e2e-solid": "cross-env FRAMEWORK=solid playwright test", + "e2e-lit": "cross-env FRAMEWORK=lit playwright test", "generate-machine": "plop machine && pnpm sync-pkgs", "generate-util": "plop utility && pnpm sync-pkgs", "typecheck": "pnpm packages -- typecheck", @@ -45,7 +46,8 @@ "test-svelte": "cd packages/frameworks/svelte && pnpm exec vitest --passWithNoTests", "test-solid": "cd packages/frameworks/solid && pnpm exec vitest --passWithNoTests", "test-vue": "cd packages/frameworks/vue && pnpm exec vitest --passWithNoTests", - "test": "pnpm test-js --run && pnpm test-react --run && pnpm test-svelte --run && pnpm test-solid --run && pnpm test-vue --run", + "test-lit": "cd packages/frameworks/lit && pnpm exec vitest --passWithNoTests", + "test": "pnpm test-js --run && pnpm test-react --run && pnpm test-svelte --run && pnpm test-solid --run && pnpm test-vue --run && pnpm test-lit --run", "slack": "tsx scripts/slack.ts", "play": "tsx scripts/play.ts", "document-types": "tsx scripts/typedocs.ts", diff --git a/packages/core/src/merge-props.ts b/packages/core/src/merge-props.ts index d1e0e8ab63..bf12553319 100644 --- a/packages/core/src/merge-props.ts +++ b/packages/core/src/merge-props.ts @@ -38,36 +38,46 @@ type TupleTypes = T[number] type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never -export function mergeProps(...args: T[]): UnionToIntersection> { - let result: Props = {} - - for (let props of args) { - for (let key in result) { - if (key.startsWith("on") && typeof result[key] === "function" && typeof props[key] === "function") { - result[key] = callAll(props[key], result[key]) - continue - } +interface MergePropsOptions { + eventPrefix?: string +} - if (key === "className" || key === "class") { - result[key] = clsx(result[key], props[key]) - continue - } +export type MergePropsFunction = (...args: T[]) => UnionToIntersection> - if (key === "style") { - result[key] = css(result[key], props[key]) - continue - } +export function createMergeProps({ eventPrefix = "on" }: MergePropsOptions = {}): MergePropsFunction { + return function mergeProps(...args: T[]): UnionToIntersection> { + let result: Props = {} - result[key] = props[key] !== undefined ? props[key] : result[key] - } + for (let props of args) { + for (let key in result) { + if (key.startsWith(eventPrefix) && typeof result[key] === "function" && typeof props[key] === "function") { + result[key] = callAll(props[key], result[key]) + continue + } + + if (key === "className" || key === "class") { + result[key] = clsx(result[key], props[key]) + continue + } - // Add props from b that are not in a - for (let key in props) { - if (result[key] === undefined) { - result[key] = props[key] + if (key === "style") { + result[key] = css(result[key], props[key]) + continue + } + + result[key] = props[key] !== undefined ? props[key] : result[key] + } + + // Add props from b that are not in a + for (let key in props) { + if (result[key] === undefined) { + result[key] = props[key] + } } } - } - return result as any + return result as any + } } + +export const mergeProps = createMergeProps() diff --git a/packages/frameworks/lit/CHANGELOG.md b/packages/frameworks/lit/CHANGELOG.md new file mode 100644 index 0000000000..1423fc6bf4 --- /dev/null +++ b/packages/frameworks/lit/CHANGELOG.md @@ -0,0 +1,2302 @@ +# @zag-js/solid + +## 1.22.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.22.1 + - @zag-js/store@1.22.1 + - @zag-js/types@1.22.1 + - @zag-js/utils@1.22.1 + +## 1.22.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.22.0 + - @zag-js/store@1.22.0 + - @zag-js/types@1.22.0 + - @zag-js/utils@1.22.0 + +## 1.21.9 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.21.9 + - @zag-js/store@1.21.9 + - @zag-js/types@1.21.9 + - @zag-js/utils@1.21.9 + +## 1.21.8 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.21.8 + - @zag-js/store@1.21.8 + - @zag-js/types@1.21.8 + - @zag-js/utils@1.21.8 + +## 1.21.7 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.21.7 + - @zag-js/store@1.21.7 + - @zag-js/types@1.21.7 + - @zag-js/utils@1.21.7 + +## 1.21.6 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.21.6 + - @zag-js/store@1.21.6 + - @zag-js/types@1.21.6 + - @zag-js/utils@1.21.6 + +## 1.21.5 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.21.5 + - @zag-js/store@1.21.5 + - @zag-js/types@1.21.5 + - @zag-js/utils@1.21.5 + +## 1.21.4 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.21.4 + - @zag-js/store@1.21.4 + - @zag-js/types@1.21.4 + - @zag-js/utils@1.21.4 + +## 1.21.3 + +### Patch Changes + +- [`7ff4117`](https://github.com/chakra-ui/zag/commit/7ff41177cfde7aeb92605f796a112de9079353a9) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Improve runtime performance of components by removing refs/events + from stateful to non-stateful objects. + +- Updated dependencies []: + - @zag-js/core@1.21.3 + - @zag-js/store@1.21.3 + - @zag-js/types@1.21.3 + - @zag-js/utils@1.21.3 + +## 1.21.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.21.2 + - @zag-js/store@1.21.2 + - @zag-js/types@1.21.2 + - @zag-js/utils@1.21.2 + +## 1.21.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.21.1 + - @zag-js/store@1.21.1 + - @zag-js/types@1.21.1 + - @zag-js/utils@1.21.1 + +## 1.21.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.21.0 + - @zag-js/store@1.21.0 + - @zag-js/types@1.21.0 + - @zag-js/utils@1.21.0 + +## 1.20.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.20.1 + - @zag-js/store@1.20.1 + - @zag-js/types@1.20.1 + - @zag-js/utils@1.20.1 + +## 1.20.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.20.0 + - @zag-js/store@1.20.0 + - @zag-js/types@1.20.0 + - @zag-js/utils@1.20.0 + +## 1.19.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.19.0 + - @zag-js/store@1.19.0 + - @zag-js/types@1.19.0 + - @zag-js/utils@1.19.0 + +## 1.18.5 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.18.5 + - @zag-js/store@1.18.5 + - @zag-js/types@1.18.5 + - @zag-js/utils@1.18.5 + +## 1.18.4 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.18.4 + - @zag-js/store@1.18.4 + - @zag-js/types@1.18.4 + - @zag-js/utils@1.18.4 + +## 1.18.3 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.18.3 + - @zag-js/store@1.18.3 + - @zag-js/types@1.18.3 + - @zag-js/utils@1.18.3 + +## 1.18.2 + +### Patch Changes + +- Updated dependencies [[`11843e6`](https://github.com/chakra-ui/zag/commit/11843e6adf62b906006890c8003b38da2850c8ee)]: + - @zag-js/utils@1.18.2 + - @zag-js/core@1.18.2 + - @zag-js/store@1.18.2 + - @zag-js/types@1.18.2 + +## 1.18.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.18.1 + - @zag-js/store@1.18.1 + - @zag-js/types@1.18.1 + - @zag-js/utils@1.18.1 + +## 1.18.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.18.0 + - @zag-js/store@1.18.0 + - @zag-js/types@1.18.0 + - @zag-js/utils@1.18.0 + +## 1.17.4 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.17.4 + - @zag-js/store@1.17.4 + - @zag-js/types@1.17.4 + - @zag-js/utils@1.17.4 + +## 1.17.3 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.17.3 + - @zag-js/store@1.17.3 + - @zag-js/types@1.17.3 + - @zag-js/utils@1.17.3 + +## 1.17.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.17.2 + - @zag-js/store@1.17.2 + - @zag-js/types@1.17.2 + - @zag-js/utils@1.17.2 + +## 1.17.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.17.1 + - @zag-js/store@1.17.1 + - @zag-js/types@1.17.1 + - @zag-js/utils@1.17.1 + +## 1.17.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.17.0 + - @zag-js/store@1.17.0 + - @zag-js/types@1.17.0 + - @zag-js/utils@1.17.0 + +## 1.16.0 + +### Patch Changes + +- Updated dependencies [[`6f6c8f3`](https://github.com/chakra-ui/zag/commit/6f6c8f329d9eb9d9889eff4317c84a4f41d4bfb2)]: + - @zag-js/types@1.16.0 + - @zag-js/core@1.16.0 + - @zag-js/store@1.16.0 + - @zag-js/utils@1.16.0 + +## 1.15.7 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.15.7 + - @zag-js/store@1.15.7 + - @zag-js/types@1.15.7 + - @zag-js/utils@1.15.7 + +## 1.15.6 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.15.6 + - @zag-js/store@1.15.6 + - @zag-js/types@1.15.6 + - @zag-js/utils@1.15.6 + +## 1.15.5 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.15.5 + - @zag-js/store@1.15.5 + - @zag-js/types@1.15.5 + - @zag-js/utils@1.15.5 + +## 1.15.4 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.15.4 + - @zag-js/store@1.15.4 + - @zag-js/types@1.15.4 + - @zag-js/utils@1.15.4 + +## 1.15.3 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.15.3 + - @zag-js/store@1.15.3 + - @zag-js/types@1.15.3 + - @zag-js/utils@1.15.3 + +## 1.15.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.15.2 + - @zag-js/store@1.15.2 + - @zag-js/types@1.15.2 + - @zag-js/utils@1.15.2 + +## 1.15.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.15.1 + - @zag-js/store@1.15.1 + - @zag-js/types@1.15.1 + - @zag-js/utils@1.15.1 + +## 1.15.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.15.0 + - @zag-js/store@1.15.0 + - @zag-js/types@1.15.0 + - @zag-js/utils@1.15.0 + +## 1.14.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.14.0 + - @zag-js/store@1.14.0 + - @zag-js/types@1.14.0 + - @zag-js/utils@1.14.0 + +## 1.13.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.13.1 + - @zag-js/store@1.13.1 + - @zag-js/types@1.13.1 + - @zag-js/utils@1.13.1 + +## 1.13.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.13.0 + - @zag-js/store@1.13.0 + - @zag-js/types@1.13.0 + - @zag-js/utils@1.13.0 + +## 1.12.4 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.12.4 + - @zag-js/store@1.12.4 + - @zag-js/types@1.12.4 + - @zag-js/utils@1.12.4 + +## 1.12.3 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.12.3 + - @zag-js/store@1.12.3 + - @zag-js/types@1.12.3 + - @zag-js/utils@1.12.3 + +## 1.12.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.12.2 + - @zag-js/store@1.12.2 + - @zag-js/types@1.12.2 + - @zag-js/utils@1.12.2 + +## 1.12.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.12.1 + - @zag-js/store@1.12.1 + - @zag-js/types@1.12.1 + - @zag-js/utils@1.12.1 + +## 1.12.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.12.0 + - @zag-js/store@1.12.0 + - @zag-js/types@1.12.0 + - @zag-js/utils@1.12.0 + +## 1.11.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.11.0 + - @zag-js/store@1.11.0 + - @zag-js/types@1.11.0 + - @zag-js/utils@1.11.0 + +## 1.10.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.10.0 + - @zag-js/store@1.10.0 + - @zag-js/types@1.10.0 + - @zag-js/utils@1.10.0 + +## 1.9.3 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.9.3 + - @zag-js/store@1.9.3 + - @zag-js/types@1.9.3 + - @zag-js/utils@1.9.3 + +## 1.9.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.9.2 + - @zag-js/store@1.9.2 + - @zag-js/types@1.9.2 + - @zag-js/utils@1.9.2 + +## 1.9.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.9.1 + - @zag-js/store@1.9.1 + - @zag-js/types@1.9.1 + - @zag-js/utils@1.9.1 + +## 1.9.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.9.0 + - @zag-js/store@1.9.0 + - @zag-js/types@1.9.0 + - @zag-js/utils@1.9.0 + +## 1.8.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.8.2 + - @zag-js/store@1.8.2 + - @zag-js/types@1.8.2 + - @zag-js/utils@1.8.2 + +## 1.8.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.8.1 + - @zag-js/store@1.8.1 + - @zag-js/types@1.8.1 + - @zag-js/utils@1.8.1 + +## 1.8.0 + +### Patch Changes + +- [`66f7828`](https://github.com/chakra-ui/zag/commit/66f7828541102fcf4f0fba05bb241e20a5ed45cb) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - [Internal] Support `ref` and `cleanup` on bindable function to + help create using state compositions + +- Updated dependencies [[`66f7828`](https://github.com/chakra-ui/zag/commit/66f7828541102fcf4f0fba05bb241e20a5ed45cb)]: + - @zag-js/core@1.8.0 + - @zag-js/store@1.8.0 + - @zag-js/types@1.8.0 + - @zag-js/utils@1.8.0 + +## 1.7.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.7.0 + - @zag-js/store@1.7.0 + - @zag-js/types@1.7.0 + - @zag-js/utils@1.7.0 + +## 1.6.2 + +### Patch Changes + +- [#2377](https://github.com/chakra-ui/zag/pull/2377) + [`e5ba28a`](https://github.com/chakra-ui/zag/commit/e5ba28a01ccab8afa2f11a82b67031a82e2675f5) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Ensure machine has started before processing events. + +- Updated dependencies []: + - @zag-js/core@1.6.2 + - @zag-js/store@1.6.2 + - @zag-js/types@1.6.2 + - @zag-js/utils@1.6.2 + +## 1.6.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.6.1 + - @zag-js/store@1.6.1 + - @zag-js/types@1.6.1 + - @zag-js/utils@1.6.1 + +## 1.6.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.6.0 + - @zag-js/store@1.6.0 + - @zag-js/types@1.6.0 + - @zag-js/utils@1.6.0 + +## 1.5.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.5.0 + - @zag-js/store@1.5.0 + - @zag-js/types@1.5.0 + - @zag-js/utils@1.5.0 + +## 1.4.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.4.2 + - @zag-js/store@1.4.2 + - @zag-js/types@1.4.2 + - @zag-js/utils@1.4.2 + +## 1.4.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.4.1 + - @zag-js/store@1.4.1 + - @zag-js/types@1.4.1 + - @zag-js/utils@1.4.1 + +## 1.4.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.4.0 + - @zag-js/store@1.4.0 + - @zag-js/types@1.4.0 + - @zag-js/utils@1.4.0 + +## 1.3.3 + +### Patch Changes + +- Updated dependencies [[`66ba41b`](https://github.com/chakra-ui/zag/commit/66ba41bb10b232ff08e3cfbfc6cbf2a1c7449e21)]: + - @zag-js/utils@1.3.3 + - @zag-js/core@1.3.3 + - @zag-js/store@1.3.3 + - @zag-js/types@1.3.3 + +## 1.3.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.3.2 + - @zag-js/store@1.3.2 + - @zag-js/types@1.3.2 + - @zag-js/utils@1.3.2 + +## 1.3.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.3.1 + - @zag-js/store@1.3.1 + - @zag-js/types@1.3.1 + - @zag-js/utils@1.3.1 + +## 1.3.0 + +### Minor Changes + +- [`c92f847`](https://github.com/chakra-ui/zag/commit/c92f84728ae473ac4c437009cbd79125747e5dd0) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Support `reenter:true` in machine transitions + +### Patch Changes + +- [`01566a1`](https://github.com/chakra-ui/zag/commit/01566a171ef426410b29b881fe1014bd26c2f86f) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Fix issue where machines that hold complex objects + +- Updated dependencies []: + - @zag-js/core@1.3.0 + - @zag-js/store@1.3.0 + - @zag-js/types@1.3.0 + - @zag-js/utils@1.3.0 + +## 1.2.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.2.1 + - @zag-js/store@1.2.1 + - @zag-js/types@1.2.1 + - @zag-js/utils@1.2.1 + +## 1.2.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.2.0 + - @zag-js/store@1.2.0 + - @zag-js/types@1.2.0 + - @zag-js/utils@1.2.0 + +## 1.1.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.1.0 + - @zag-js/store@1.1.0 + - @zag-js/types@1.1.0 + - @zag-js/utils@1.1.0 + +## 1.0.2 + +### Patch Changes + +- [`08d4b92`](https://github.com/chakra-ui/zag/commit/08d4b926a136d0098f04d631c8be0f66579bfb20) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Fix issue where `undefined` values were not filtered out before + resolving props + +- Updated dependencies []: + - @zag-js/core@1.0.2 + - @zag-js/store@1.0.2 + - @zag-js/types@1.0.2 + - @zag-js/utils@1.0.2 + +## 1.0.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@1.0.1 + - @zag-js/store@1.0.1 + - @zag-js/types@1.0.1 + - @zag-js/utils@1.0.1 + +## 1.0.0 + +### Patch Changes + +- Updated dependencies [[`b1caa44`](https://github.com/chakra-ui/zag/commit/b1caa44085e7f1da0ad24fc7b25178081811646c)]: + - @zag-js/core@1.0.0 + - @zag-js/store@1.0.0 + - @zag-js/types@1.0.0 + - @zag-js/utils@1.0.0 + +## 0.82.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.82.2 + - @zag-js/store@0.82.2 + - @zag-js/types@0.82.2 + +## 0.82.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.82.1 + - @zag-js/store@0.82.1 + - @zag-js/types@0.82.1 + +## 0.82.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.82.0 + - @zag-js/store@0.82.0 + - @zag-js/types@0.82.0 + +## 0.81.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.81.2 + - @zag-js/store@0.81.2 + - @zag-js/types@0.81.2 + +## 0.81.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.81.1 + - @zag-js/store@0.81.1 + - @zag-js/types@0.81.1 + +## 0.81.0 + +### Patch Changes + +- Updated dependencies [[`552e55d`](https://github.com/chakra-ui/zag/commit/552e55db4ec8c0fa86c5b7e5ce3ad08eb350ca68)]: + - @zag-js/types@0.81.0 + - @zag-js/core@0.81.0 + - @zag-js/store@0.81.0 + +## 0.80.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.80.0 + - @zag-js/store@0.80.0 + - @zag-js/types@0.80.0 + +## 0.79.3 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.79.3 + - @zag-js/store@0.79.3 + - @zag-js/types@0.79.3 + +## 0.79.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.79.2 + - @zag-js/store@0.79.2 + - @zag-js/types@0.79.2 + +## 0.79.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.79.1 + - @zag-js/store@0.79.1 + - @zag-js/types@0.79.1 + +## 0.79.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.79.0 + - @zag-js/store@0.79.0 + - @zag-js/types@0.79.0 + +## 0.78.3 + +### Patch Changes + +- Updated dependencies [[`5584a83`](https://github.com/chakra-ui/zag/commit/5584a833151ee9f2c2ef9c07b6d699addfbca18e)]: + - @zag-js/store@0.78.3 + - @zag-js/core@0.78.3 + - @zag-js/types@0.78.3 + +## 0.78.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.78.2 + - @zag-js/store@0.78.2 + - @zag-js/types@0.78.2 + +## 0.78.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.78.1 + - @zag-js/store@0.78.1 + - @zag-js/types@0.78.1 + +## 0.78.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.78.0 + - @zag-js/store@0.78.0 + - @zag-js/types@0.78.0 + +## 0.77.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.77.1 + - @zag-js/store@0.77.1 + - @zag-js/types@0.77.1 + +## 0.77.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.77.0 + - @zag-js/store@0.77.0 + - @zag-js/types@0.77.0 + +## 0.76.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.76.0 + - @zag-js/store@0.76.0 + - @zag-js/types@0.76.0 + +## 0.75.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.75.0 + - @zag-js/store@0.75.0 + - @zag-js/types@0.75.0 + +## 0.74.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.74.2 + - @zag-js/store@0.74.2 + - @zag-js/types@0.74.2 + +## 0.74.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.74.1 + - @zag-js/store@0.74.1 + - @zag-js/types@0.74.1 + +## 0.74.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.74.0 + - @zag-js/store@0.74.0 + - @zag-js/types@0.74.0 + +## 0.73.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.73.1 + - @zag-js/store@0.73.1 + - @zag-js/types@0.73.1 + +## 0.73.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.73.0 + - @zag-js/store@0.73.0 + - @zag-js/types@0.73.0 + +## 0.72.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.72.0 + - @zag-js/store@0.72.0 + - @zag-js/types@0.72.0 + +## 0.71.0 + +### Minor Changes + +- [`b3a251e`](https://github.com/chakra-ui/zag/commit/b3a251e5e10b9b27af353e0f41117329846b14e9) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - We no longer ship `src` files in the packages. + +### Patch Changes + +- Updated dependencies [[`b3a251e`](https://github.com/chakra-ui/zag/commit/b3a251e5e10b9b27af353e0f41117329846b14e9)]: + - @zag-js/core@0.71.0 + - @zag-js/store@0.71.0 + - @zag-js/types@0.71.0 + +## 0.70.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.70.0 + - @zag-js/store@0.70.0 + - @zag-js/types@0.70.0 + +## 0.69.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.69.0 + - @zag-js/store@0.69.0 + - @zag-js/types@0.69.0 + +## 0.68.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.68.1 + - @zag-js/store@0.68.1 + - @zag-js/types@0.68.1 + +## 0.68.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.68.0 + - @zag-js/store@0.68.0 + - @zag-js/types@0.68.0 + +## 0.67.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.67.0 + - @zag-js/store@0.67.0 + - @zag-js/types@0.67.0 + +## 0.66.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.66.1 + - @zag-js/store@0.66.1 + - @zag-js/types@0.66.1 + +## 0.66.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.66.0 + - @zag-js/store@0.66.0 + - @zag-js/types@0.66.0 + +## 0.65.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.65.1 + - @zag-js/store@0.65.1 + - @zag-js/types@0.65.1 + +## 0.65.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.65.0 + - @zag-js/store@0.65.0 + - @zag-js/types@0.65.0 + +## 0.64.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.64.0 + - @zag-js/store@0.64.0 + - @zag-js/types@0.64.0 + +## 0.63.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.63.0 + - @zag-js/store@0.63.0 + - @zag-js/types@0.63.0 + +## 0.62.1 + +### Patch Changes + +- Updated dependencies [[`5644790`](https://github.com/chakra-ui/zag/commit/564479081d37cd06bc38043fccf9c229379a1531)]: + - @zag-js/core@0.62.1 + - @zag-js/store@0.62.1 + - @zag-js/types@0.62.1 + +## 0.62.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.62.0 + - @zag-js/store@0.62.0 + - @zag-js/types@0.62.0 + +## 0.61.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.61.1 + - @zag-js/store@0.61.1 + - @zag-js/types@0.61.1 + +## 0.61.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.61.0 + - @zag-js/store@0.61.0 + - @zag-js/types@0.61.0 + +## 0.60.0 + +### Patch Changes + +- Updated dependencies [[`49bf73b`](https://github.com/chakra-ui/zag/commit/49bf73b7119bdd5dfd40d33119c3543626e201f0)]: + - @zag-js/store@0.60.0 + - @zag-js/core@0.60.0 + - @zag-js/types@0.60.0 + +## 0.59.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.59.0 + - @zag-js/store@0.59.0 + - @zag-js/types@0.59.0 + +## 0.58.3 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.58.3 + - @zag-js/store@0.58.3 + - @zag-js/types@0.58.3 + +## 0.58.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.58.2 + - @zag-js/store@0.58.2 + - @zag-js/types@0.58.2 + +## 0.58.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.58.1 + - @zag-js/store@0.58.1 + - @zag-js/types@0.58.1 + +## 0.58.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.58.0 + - @zag-js/store@0.58.0 + - @zag-js/types@0.58.0 + +## 0.57.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.57.0 + - @zag-js/store@0.57.0 + - @zag-js/types@0.57.0 + +## 0.56.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.56.1 + - @zag-js/store@0.56.1 + - @zag-js/types@0.56.1 + +## 0.56.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.56.0 + - @zag-js/store@0.56.0 + - @zag-js/types@0.56.0 + +## 0.55.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.55.0 + - @zag-js/store@0.55.0 + - @zag-js/types@0.55.0 + +## 0.54.0 + +### Patch Changes + +- Updated dependencies [[`590c177`](https://github.com/chakra-ui/zag/commit/590c1779f5208fb99114c872175e779508f2f96d)]: + - @zag-js/core@0.54.0 + - @zag-js/store@0.54.0 + - @zag-js/types@0.54.0 + +## 0.53.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.53.0 + - @zag-js/store@0.53.0 + - @zag-js/types@0.53.0 + +## 0.52.0 + +### Patch Changes + +- [`cec00aa`](https://github.com/chakra-ui/zag/commit/cec00aa477b827daeb5fed773364ffa73d107f26) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Fix SSR issue in Solid.js where spreading `readOnly: false` adds + the `readonly` attribute on editable elements, making them uneditable. +- Updated dependencies []: + - @zag-js/core@0.52.0 + - @zag-js/store@0.52.0 + - @zag-js/types@0.52.0 + +## 0.51.2 + +### Patch Changes + +- Updated dependencies [[`62eb21b`](https://github.com/chakra-ui/zag/commit/62eb21b60355dd0645936baf4692315134e7488c)]: + - @zag-js/core@0.51.2 + - @zag-js/store@0.51.2 + - @zag-js/types@0.51.2 + +## 0.51.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.51.1 + - @zag-js/store@0.51.1 + - @zag-js/types@0.51.1 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.51.0 + - @zag-js/store@0.51.0 + - @zag-js/types@0.51.0 + +## 0.50.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.50.0 + - @zag-js/store@0.50.0 + - @zag-js/types@0.50.0 + +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`c8aeca4`](https://github.com/chakra-ui/zag/commit/c8aeca475b078806c2765659668d843037746ba6)]: + - @zag-js/store@0.49.0 + - @zag-js/core@0.49.0 + - @zag-js/types@0.49.0 + +## 0.48.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.48.0 + - @zag-js/store@0.48.0 + - @zag-js/types@0.48.0 + +## 0.47.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.47.0 + - @zag-js/store@0.47.0 + - @zag-js/types@0.47.0 + +## 0.46.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.46.0 + - @zag-js/store@0.46.0 + - @zag-js/types@0.46.0 + +## 0.45.0 + +### Minor Changes + +- [`ccb34b5`](https://github.com/chakra-ui/zag/commit/ccb34b5268e5e93083ad2ad7edbffa0c64ac2657) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Rewrite `mergeProps` to prevent issues with children that read + from context, and ensure props are always up-to-date. + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.45.0 + - @zag-js/store@0.45.0 + - @zag-js/types@0.45.0 + +## 0.44.0 + +### Minor Changes + +- [`198f525`](https://github.com/chakra-ui/zag/commit/198f5253a09eac721c5bae9e468588f9d93ea7bb) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - [Breaking] Refactor `mergeProps` from solid-js to ensure + consistent merging of props with other frameworks. The previous implementation was returning a Proxy object which was + causing issues. + + Now it returns a signal that can be called to get the merged props. Under the hood, it uses the `createMemo` function + from `solid-js`. + + **Before** + + ```js + const props = mergeProps({ a: 1 }, { a: 2 }) + props // Proxy { a: 2 } + ``` + + **After** + + ```js + const props = mergeProps({ a: 1 }, { a: 2 }) + props() // { a: 2 } + ``` + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.44.0 + - @zag-js/store@0.44.0 + - @zag-js/types@0.44.0 + +## 0.43.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.43.0 + - @zag-js/store@0.43.0 + - @zag-js/types@0.43.0 + +## 0.42.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.42.0 + - @zag-js/store@0.42.0 + - @zag-js/types@0.42.0 + +## 0.41.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.41.0 + - @zag-js/store@0.41.0 + - @zag-js/types@0.41.0 + +## 0.40.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.40.0 + - @zag-js/store@0.40.0 + - @zag-js/types@0.40.0 + +## 0.39.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.39.0 + - @zag-js/store@0.39.0 + - @zag-js/types@0.39.0 + +## 0.38.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.38.1 + - @zag-js/store@0.38.1 + - @zag-js/types@0.38.1 + +## 0.38.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.38.0 + - @zag-js/store@0.38.0 + - @zag-js/types@0.38.0 + +## 0.37.3 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.37.3 + - @zag-js/store@0.37.3 + - @zag-js/types@0.37.3 + +## 0.37.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.37.2 + - @zag-js/store@0.37.2 + - @zag-js/types@0.37.2 + +## 0.37.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.37.1 + - @zag-js/store@0.37.1 + - @zag-js/types@0.37.1 + +## 0.37.0 + +### Patch Changes + +- Updated dependencies [[`2a024fb`](https://github.com/chakra-ui/zag/commit/2a024fbd2e98343218d4d658e91f1d8c751e1a4d)]: + - @zag-js/types@0.37.0 + - @zag-js/core@0.37.0 + - @zag-js/store@0.37.0 + +## 0.36.3 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.36.3 + - @zag-js/store@0.36.3 + - @zag-js/types@0.36.3 + +## 0.36.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.36.2 + - @zag-js/store@0.36.2 + - @zag-js/types@0.36.2 + +## 0.36.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.36.1 + - @zag-js/store@0.36.1 + - @zag-js/types@0.36.1 + +## 0.36.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.36.0 + - @zag-js/store@0.36.0 + - @zag-js/types@0.36.0 + +## 0.35.0 + +### Patch Changes + +- Updated dependencies [[`0216161`](https://github.com/chakra-ui/zag/commit/0216161fd3d429409abc96941d33a0c333ef8d36)]: + - @zag-js/store@0.35.0 + - @zag-js/core@0.35.0 + - @zag-js/types@0.35.0 + +## 0.34.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.34.0 + - @zag-js/store@0.34.0 + - @zag-js/types@0.34.0 + +## 0.33.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.33.2 + - @zag-js/store@0.33.2 + - @zag-js/types@0.33.2 + +## 0.33.1 + +### Patch Changes + +- Updated dependencies [[`80af758`](https://github.com/chakra-ui/zag/commit/80af758900606b43afc5b1e23edbf043a5e085ae)]: + - @zag-js/store@0.33.1 + - @zag-js/core@0.33.1 + - @zag-js/types@0.33.1 + +## 0.33.0 + +### Patch Changes + +- Updated dependencies [[`7872cdf`](https://github.com/chakra-ui/zag/commit/7872cdf8aeb28b9a30cd4a016bd12e5366054511)]: + - @zag-js/core@0.33.0 + - @zag-js/store@0.33.0 + - @zag-js/types@0.33.0 + +## 0.32.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.32.1 + - @zag-js/store@0.32.1 + - @zag-js/types@0.32.1 + +## 0.32.0 + +### Patch Changes + +- [#1095](https://github.com/chakra-ui/zag/pull/1095) + [`651346b`](https://github.com/chakra-ui/zag/commit/651346b1cd280b3882253425e9054caf985f83a7) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Refactor useSnapshot, useService and useMachine to track context + changes + +- Updated dependencies []: + - @zag-js/core@0.32.0 + - @zag-js/store@0.32.0 + - @zag-js/types@0.32.0 + +## 0.31.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.31.1 + - @zag-js/store@0.31.1 + - @zag-js/types@0.31.1 + +## 0.31.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.31.0 + - @zag-js/store@0.31.0 + - @zag-js/types@0.31.0 + +## 0.30.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.30.0 + - @zag-js/store@0.30.0 + - @zag-js/types@0.30.0 + +## 0.29.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.29.0 + - @zag-js/store@0.29.0 + - @zag-js/types@0.29.0 + +## 0.28.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.28.1 + - @zag-js/store@0.28.1 + - @zag-js/types@0.28.1 + +## 0.28.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.28.0 + - @zag-js/store@0.28.0 + - @zag-js/types@0.28.0 + +## 0.27.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.27.1 + - @zag-js/store@0.27.1 + - @zag-js/types@0.27.1 + +## 0.27.0 + +### Patch Changes + +- Updated dependencies [[`152b0a78`](https://github.com/chakra-ui/zag/commit/152b0a78b6ba18442f38164ce90789bc243f6e00)]: + - @zag-js/core@0.27.0 + - @zag-js/store@0.27.0 + - @zag-js/types@0.27.0 + +## 0.26.0 + +### Minor Changes + +- [`56ca6a97`](https://github.com/chakra-ui/zag/commit/56ca6a977695fb3993558664f8e185de6ab7eb77) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Remove support for pressable machine in favor of using native + button. We no longer want to maintain this machine due to the internal complexity across devices and browsers. + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.26.0 + - @zag-js/store@0.26.0 + - @zag-js/types@0.26.0 + +## 0.25.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.25.0 + - @zag-js/store@0.25.0 + - @zag-js/types@0.25.0 + +## 0.24.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.24.0 + - @zag-js/store@0.24.0 + - @zag-js/types@0.24.0 + +## 0.23.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.23.0 + - @zag-js/store@0.23.0 + - @zag-js/types@0.23.0 + +## 0.22.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.22.0 + - @zag-js/store@0.22.0 + - @zag-js/types@0.22.0 + +## 0.21.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.21.0 + - @zag-js/store@0.21.0 + - @zag-js/types@0.21.0 + +## 0.20.0 + +### Patch Changes + +- [`942db6ca`](https://github.com/chakra-ui/zag/commit/942db6caf9f699d6af56929c835b10ae80cfbc85) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Remove toggle machine + +- Updated dependencies [[`9a3a82f0`](https://github.com/chakra-ui/zag/commit/9a3a82f0b3738beda59c313fafd51360e6b0322f), + [`942db6ca`](https://github.com/chakra-ui/zag/commit/942db6caf9f699d6af56929c835b10ae80cfbc85)]: + - @zag-js/types@0.20.0 + - @zag-js/core@0.20.0 + - @zag-js/store@0.20.0 + +## 0.19.1 + +### Patch Changes + +- Updated dependencies [[`3f0b6a19`](https://github.com/chakra-ui/zag/commit/3f0b6a19dcf9779846efb2bc093235299301bbdb)]: + - @zag-js/core@0.19.1 + - @zag-js/store@0.19.1 + - @zag-js/types@0.19.1 + +## 0.19.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.19.0 + - @zag-js/store@0.19.0 + - @zag-js/types@0.19.0 + +## 0.18.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.18.0 + - @zag-js/store@0.18.0 + - @zag-js/types@0.18.0 + +## 0.17.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.17.0 + - @zag-js/store@0.17.0 + - @zag-js/types@0.17.0 + +## 0.16.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.16.0 + - @zag-js/store@0.16.0 + - @zag-js/types@0.16.0 + +## 0.15.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.15.0 + - @zag-js/store@0.15.0 + - @zag-js/types@0.15.0 + +## 0.14.0 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.14.0 + - @zag-js/store@0.14.0 + - @zag-js/types@0.14.0 + +## 0.13.0 + +### Patch Changes + +- Updated dependencies [[`4a2d8b77`](https://github.com/chakra-ui/zag/commit/4a2d8b77d1e71ad6b6c10134bc4186db6e6c0414)]: + - @zag-js/core@0.13.0 + - @zag-js/store@0.13.0 + - @zag-js/types@0.13.0 + +## 0.12.0 + +### Patch Changes + +- [`1da42934`](https://github.com/chakra-ui/zag/commit/1da429345166a0c00602f305cfd8ac11c5b14c10) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Export `PropType` utility for usage in the `PublicApi` + +- Updated dependencies []: + - @zag-js/core@0.12.0 + - @zag-js/store@0.12.0 + - @zag-js/types@0.12.0 + +## 0.11.2 + +### Patch Changes + +- [`0a2af673`](https://github.com/chakra-ui/zag/commit/0a2af67370de5ad4a4fd501f51c78aa76e6f3bf2) Thanks + [@cschroeter](https://github.com/cschroeter)! - Export missing types + +- Updated dependencies []: + - @zag-js/core@0.11.2 + - @zag-js/store@0.11.2 + - @zag-js/types@0.11.2 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.11.1 + - @zag-js/store@0.11.1 + - @zag-js/types@0.11.1 + +## 0.11.0 + +### Patch Changes + +- [`4f371874`](https://github.com/chakra-ui/zag/commit/4f3718742dc88a2cd8726bdd889c9bbde94f5bce) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Rebuild all packages using tsup + +- Updated dependencies [[`4f371874`](https://github.com/chakra-ui/zag/commit/4f3718742dc88a2cd8726bdd889c9bbde94f5bce)]: + - @zag-js/store@0.11.0 + - @zag-js/types@0.11.0 + - @zag-js/core@0.11.0 + +## 0.10.5 + +### Patch Changes + +- [`622eea18`](https://github.com/chakra-ui/zag/commit/622eea1834d575d2ddd225e96121561b334597eb) Thanks + [@cschroeter](https://github.com/cschroeter)! - Fix an issue with type declarations + +- Updated dependencies []: + - @zag-js/core@0.10.5 + - @zag-js/store@0.10.5 + - @zag-js/types@0.10.5 + +## 0.10.4 + +### Patch Changes + +- [`2e2524e9`](https://github.com/chakra-ui/zag/commit/2e2524e9cfdb829037da8073d7fc5ad895556672) Thanks + [@cschroeter](https://github.com/cschroeter)! - Declare solid-js, react and vue as external dependencies + +- Updated dependencies []: + - @zag-js/core@0.10.4 + - @zag-js/store@0.10.4 + - @zag-js/types@0.10.4 + +## 0.10.3 + +### Patch Changes + +- [`c59a8dec`](https://github.com/chakra-ui/zag/commit/c59a8dec15ab57d218823bfe7af6d723972be6c7) Thanks + [@cschroeter](https://github.com/cschroeter)! - Use vite to build packages + +- Updated dependencies [[`c59a8dec`](https://github.com/chakra-ui/zag/commit/c59a8dec15ab57d218823bfe7af6d723972be6c7)]: + - @zag-js/core@0.10.3 + - @zag-js/store@0.10.3 + - @zag-js/types@0.10.3 + +## 0.10.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.10.2 + - @zag-js/store@0.10.2 + - @zag-js/types@0.10.2 + +## 0.10.1 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.10.1 + - @zag-js/store@0.10.1 + - @zag-js/types@0.10.1 + +## 0.10.0 + +### Patch Changes + +- Updated dependencies [[`2a1fb4a0`](https://github.com/chakra-ui/zag/commit/2a1fb4a0740e6ad8e2902265e14597f087007675)]: + - @zag-js/types@0.10.0 + - @zag-js/core@0.10.0 + - @zag-js/store@0.10.0 + +## 0.9.2 + +### Patch Changes + +- Updated dependencies []: + - @zag-js/core@0.9.2 + - @zag-js/store@0.9.2 + - @zag-js/types@0.9.2 + +## 0.9.1 + +### Patch Changes + +- [`8469daa1`](https://github.com/chakra-ui/zag/commit/8469daa15fd7f2c0a80869a8715b0342bd3c355f) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Force release every package to fix regression + +- Updated dependencies [[`8469daa1`](https://github.com/chakra-ui/zag/commit/8469daa15fd7f2c0a80869a8715b0342bd3c355f)]: + - @zag-js/core@0.9.1 + - @zag-js/store@0.9.1 + - @zag-js/types@0.9.1 + +## 0.8.0 + +### Minor Changes + +- [`bb037fb9`](https://github.com/chakra-ui/zag/commit/bb037fb985c54fe508d274c7e8fa0b4c7b20909d) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Expose propTraps from solid package + +### Patch Changes + +- [`f17f0363`](https://github.com/chakra-ui/zag/commit/f17f036309bda6c11199a49ce09e5f35fd880a71) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Update solid-js version + +## 0.7.0 + +### Patch Changes + +- [`413cdf18`](https://github.com/chakra-ui/zag/commit/413cdf180f718469c9c8b879a43aa4501d1ae59c) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Improve reactivity of `mergeProps` + +- Updated dependencies [[`413cdf18`](https://github.com/chakra-ui/zag/commit/413cdf180f718469c9c8b879a43aa4501d1ae59c)]: + - @zag-js/core@0.7.0 + +## 0.5.0 + +### Patch Changes + +- Updated dependencies [[`ec07ff35`](https://github.com/chakra-ui/zag/commit/ec07ff3590916ebcb4450b64207370ee2af9d3d1), + [`54377b1c`](https://github.com/chakra-ui/zag/commit/54377b1c4ed85deb06453a00648b7c2c1f0c72df)]: + - @zag-js/core@0.5.0 + - @zag-js/types@0.5.0 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [[`30dbeb28`](https://github.com/chakra-ui/zag/commit/30dbeb282f7901c33518097a0e1dd9a857f7efb0)]: + - @zag-js/core@0.2.12 + +## 0.3.0 + +### Minor Changes + +- [`51ca61aa`](https://github.com/chakra-ui/zag/commit/51ca61aab3b2c8f188fa87f4a4f06ece673cf240) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Add support for passing an accessor or signal as transient + context. + + ```jsx + function Component(props) { + const [state, send] = useMachine(machine({ id: createUniqueId() }), { + context: createMemo(() => ({ + max: props.max, + min: props.min, + })), + }) + } + ``` + +### Patch Changes + +- Updated dependencies [[`1e10b1f4`](https://github.com/chakra-ui/zag/commit/1e10b1f40016f5c9bdf0924a3470b9383c0dbce2)]: + - @zag-js/core@0.2.11 + +## 0.2.10 + +### Patch Changes + +- [`5277f653`](https://github.com/chakra-ui/zag/commit/5277f65311c46e5792f605021d58b3b7e7dc3eaa) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Update dependencies to latest versions + +- Updated dependencies [[`5277f653`](https://github.com/chakra-ui/zag/commit/5277f65311c46e5792f605021d58b3b7e7dc3eaa)]: + - @zag-js/store@0.2.8 + - @zag-js/core@0.2.10 + +## 0.2.9 + +### Patch Changes + +- Updated dependencies [[`df27f257`](https://github.com/chakra-ui/zag/commit/df27f257f53d194013b528342d3d9aef994d0d5c)]: + - @zag-js/core@0.2.9 + - @zag-js/store@0.2.7 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies [[`28dd7680`](https://github.com/chakra-ui/zag/commit/28dd768067f153e1f142154c8a8ce9bbde3746e2)]: + - @zag-js/store@0.2.6 + - @zag-js/core@0.2.8 + +## 0.2.7 + +### Patch Changes + +- [`6957678d`](https://github.com/chakra-ui/zag/commit/6957678d2f00f4d219e791dffed91446e64211e7) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Switch to `es2020` to support `import.meta.env` + +- [`fef822b9`](https://github.com/chakra-ui/zag/commit/fef822b91a4a9dbfc3c1e8f88a89727a3231326a) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Add `style` to prop types in `normalizeProps` + +- Updated dependencies [[`f7bb988a`](https://github.com/chakra-ui/zag/commit/f7bb988aaeda6c6caebe95823f4cd44baa0d5e78), + [`6957678d`](https://github.com/chakra-ui/zag/commit/6957678d2f00f4d219e791dffed91446e64211e7), + [`fef822b9`](https://github.com/chakra-ui/zag/commit/fef822b91a4a9dbfc3c1e8f88a89727a3231326a)]: + - @zag-js/core@0.2.7 + - @zag-js/store@0.2.5 + - @zag-js/types@0.3.4 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`80de0b7c`](https://github.com/chakra-ui/zag/commit/80de0b7c7f888a254a3e1fec2da5338e235bc699), + [`88ccbbed`](https://github.com/chakra-ui/zag/commit/88ccbbed937acf10d4338e2c6d7f1e6b9eb538c8)]: + - @zag-js/core@0.2.6 + - @zag-js/store@0.2.4 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`c1f609df`](https://github.com/chakra-ui/zag/commit/c1f609dfabbc31c296ebdc1e89480313130f832b), + [`6e6f0f4d`](https://github.com/chakra-ui/zag/commit/6e6f0f4d757b63b045af15639e7ae101c25514da), + [`c7e85e20`](https://github.com/chakra-ui/zag/commit/c7e85e20d4d08b56852768becf2fc5f7f4275dcc)]: + - @zag-js/types@0.3.3 + - @zag-js/store@0.2.3 + - @zag-js/core@0.2.5 + +## 0.2.4 + +### Patch Changes + +- [#462](https://github.com/chakra-ui/zag/pull/462) + [`f8c47a2b`](https://github.com/chakra-ui/zag/commit/f8c47a2b4442bfadc4d98315a8c1ac4aa4020822) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Update packages to use explicit `exports` field in `package.json` + +- Updated dependencies [[`4c98f016`](https://github.com/chakra-ui/zag/commit/4c98f016ae3d48b1b74f4dc8c302ef9a1c664260), + [`f8c47a2b`](https://github.com/chakra-ui/zag/commit/f8c47a2b4442bfadc4d98315a8c1ac4aa4020822)]: + - @zag-js/core@0.2.4 + - @zag-js/store@0.2.2 + - @zag-js/types@0.3.2 + +## 0.2.3 + +### Patch Changes + +- [`9332f1ca`](https://github.com/chakra-ui/zag/commit/9332f1caf5122c16a3edb48e20664b04714d226c) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Bump dependencies + +- Updated dependencies [[`9d936614`](https://github.com/chakra-ui/zag/commit/9d93661439f10a550c154e9f290905d32e8f509b)]: + - @zag-js/core@0.2.3 + - @zag-js/store@0.2.1 + +## 0.2.2 + +### Patch Changes + +- [`44feef0b`](https://github.com/chakra-ui/zag/commit/44feef0bdf312e27d6faf1aa8ab0ecff0281108c) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Provide deep merge utility for nested context values + +- Updated dependencies [[`44feef0b`](https://github.com/chakra-ui/zag/commit/44feef0bdf312e27d6faf1aa8ab0ecff0281108c), + [`810e7d85`](https://github.com/chakra-ui/zag/commit/810e7d85274a26e0fe76dbdb2829fd7ab7f982a6), + [`e328b306`](https://github.com/chakra-ui/zag/commit/e328b306bf06d151fff4907a7e8e1160f07af855), + [`65976dd5`](https://github.com/chakra-ui/zag/commit/65976dd51902b1c4a4460cd196467156a705a999)]: + - @zag-js/core@0.2.2 + - @zag-js/types@0.3.1 + +## 0.2.1 + +### Patch Changes + +- [#384](https://github.com/chakra-ui/zag/pull/384) + [`4aa6955f`](https://github.com/chakra-ui/zag/commit/4aa6955fab7ff6fee8545dcf491576640c69c64e) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Improve support for updating the internal machine options. + + Fix react controlled context. + +- [#381](https://github.com/chakra-ui/zag/pull/381) + [`21775db5`](https://github.com/chakra-ui/zag/commit/21775db5ac318b095f603e7030ec7645e104f663) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Omit undefined values passed in machine's context + +- Updated dependencies [[`4aa6955f`](https://github.com/chakra-ui/zag/commit/4aa6955fab7ff6fee8545dcf491576640c69c64e)]: + - @zag-js/core@0.2.1 + +## 0.2.0 + +### Minor Changes + +- [#375](https://github.com/chakra-ui/zag/pull/375) + [`9cb4e9de`](https://github.com/chakra-ui/zag/commit/9cb4e9de28a3c6666860bc068c86be67a3b1a2ca) Thanks + [@darrylblake](https://github.com/darrylblake)! - Ensures code is transpiled with `es2019` target for environments + that don't support `es2020` and up, i.e. Cypress. + +### Patch Changes + +- Updated dependencies [[`9cb4e9de`](https://github.com/chakra-ui/zag/commit/9cb4e9de28a3c6666860bc068c86be67a3b1a2ca)]: + - @zag-js/core@0.2.0 + - @zag-js/store@0.2.0 + - @zag-js/types@0.3.0 + +## 0.1.17 + +### Patch Changes + +- [`52552156`](https://github.com/chakra-ui/zag/commit/52552156ded1b00f873576f52b11d0414f5dfee7) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Force new release + +- Updated dependencies [[`52552156`](https://github.com/chakra-ui/zag/commit/52552156ded1b00f873576f52b11d0414f5dfee7)]: + - @zag-js/core@0.1.12 + - @zag-js/store@0.1.4 + - @zag-js/types@0.2.7 + +## 0.1.16 + +### Patch Changes + +- [#325](https://github.com/chakra-ui/zag/pull/325) + [`c0cc303e`](https://github.com/chakra-ui/zag/commit/c0cc303e9824ea395c06d9faa699d23e19ef6538) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Switch packages to use ESM and `type=module` + +- Updated dependencies [[`61c11646`](https://github.com/chakra-ui/zag/commit/61c116467c1758bdda7efe1f27d4ed26e7d44624), + [`c0cc303e`](https://github.com/chakra-ui/zag/commit/c0cc303e9824ea395c06d9faa699d23e19ef6538)]: + - @zag-js/core@0.1.11 + - @zag-js/store@0.1.3 + - @zag-js/types@0.2.6 + +## 0.1.15 + +### Patch Changes + +- [`55e6a55c`](https://github.com/chakra-ui/zag/commit/55e6a55c37a60eea5caa446270cd1f6012d7363d) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Bump all packages + +- Updated dependencies [[`ce97956c`](https://github.com/chakra-ui/zag/commit/ce97956c0586ce842f7b082dd71cc6d68909ad58), + [`55e6a55c`](https://github.com/chakra-ui/zag/commit/55e6a55c37a60eea5caa446270cd1f6012d7363d)]: + - @zag-js/core@0.1.10 + - @zag-js/store@0.1.2 + - @zag-js/types@0.2.5 + +## 0.1.14 + +### Patch Changes + +- Updated dependencies [[`1d30333e`](https://github.com/chakra-ui/zag/commit/1d30333e0d3011707950adab26878cde9ed1c242)]: + - @zag-js/types@0.2.4 + +## 0.1.13 + +### Patch Changes + +- [#224](https://github.com/chakra-ui/zag/pull/224) + [`b7eb3f20`](https://github.com/chakra-ui/zag/commit/b7eb3f204cda6ac913b66787c27942294abfb0ee) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Update framework dependencies + +## 0.1.12 + +### Patch Changes + +- [`2a2566b8`](https://github.com/chakra-ui/zag/commit/2a2566b8be1441ae98215bec594e4c996f3b8aaf) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Trigger new version due to changes in build chain + +- Updated dependencies [[`2a2566b8`](https://github.com/chakra-ui/zag/commit/2a2566b8be1441ae98215bec594e4c996f3b8aaf)]: + - @zag-js/core@0.1.9 + - @zag-js/store@0.1.1 + - @zag-js/types@0.2.3 + +## 0.1.11 + +### Patch Changes + +- [#195](https://github.com/chakra-ui/zag/pull/195) + [`90f2e443`](https://github.com/chakra-ui/zag/commit/90f2e44376f012d14e3703d6959392e4f3bdddd0) Thanks + [@anubra266](https://github.com/anubra266)! - Improve SSR by omitting `useSetup` step. + +* [#197](https://github.com/chakra-ui/zag/pull/197) + [`4ea550d9`](https://github.com/chakra-ui/zag/commit/4ea550d9983e0d20af123481f256cc5cf03d2358) Thanks + [@anubra266](https://github.com/anubra266)! - Remove `useSetup` hook + +* Updated dependencies [[`c5872be2`](https://github.com/chakra-ui/zag/commit/c5872be2fe057675fb8c7c64ed2c10b99daf697e)]: + - @zag-js/core@0.1.8 + +## 0.1.10 + +### Patch Changes + +- [#178](https://github.com/chakra-ui/zag/pull/178) + [`1abed11b`](https://github.com/chakra-ui/zag/commit/1abed11bda7fc56fd3f77c3b842e89a934ee3253) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - BREAKING 💥: Refactor connect function in favor of uniform APIs + across frameworks + + Due to the fact that we tried to make "React" the baseline, there was a lot of inherent complexity in how we managed + types in the codebase. + + We've now removed the `PropTypes` export in favor of passing `normalizeProps` in the `api.connect` function. This is + now required for React as well. + + You can remove the `` generic and Zag will auto-infer the types from `normalizeProps`. + + **For Vue and Solid** + + ```diff + -api.connect(state, send, normalizeProps) + +api.connect(state, send, normalizeProps) + ``` + + **For React** + + ```diff + -api.connect(state, send) + +api.connect(state, send, normalizeProps) + ``` + +* [`3a53a1e9`](https://github.com/chakra-ui/zag/commit/3a53a1e97306a9fedf1706b95f8e38b03750c2f3) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Refactor to use local `@zag-js/store` package + +- [`664e61f9`](https://github.com/chakra-ui/zag/commit/664e61f94844f0405b7e646e4a30b8f0f737f21c) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Pin dependency versions + +* [`a630876a`](https://github.com/chakra-ui/zag/commit/a630876ac2c0544aed2f3694a50f175799d3464d) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Normalize the default checked and default value props + +* Updated dependencies [[`1abed11b`](https://github.com/chakra-ui/zag/commit/1abed11bda7fc56fd3f77c3b842e89a934ee3253), + [`3a53a1e9`](https://github.com/chakra-ui/zag/commit/3a53a1e97306a9fedf1706b95f8e38b03750c2f3), + [`3a53a1e9`](https://github.com/chakra-ui/zag/commit/3a53a1e97306a9fedf1706b95f8e38b03750c2f3)]: + - @zag-js/core@0.1.7 + - @zag-js/store@0.1.0 + +## 0.1.9 + +### Patch Changes + +- [#143](https://github.com/chakra-ui/zag/pull/143) + [`ea8c878f`](https://github.com/chakra-ui/zag/commit/ea8c878f8e6f8b09aed30d0284ada66aa5700761) Thanks + [@renovate](https://github.com/apps/renovate)! - chore(deps): update dependency solid-js to v1.4.4 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies [[`5982d826`](https://github.com/chakra-ui/zag/commit/5982d826126a7b83252fcd0b0479079fccb62189)]: + - @zag-js/core@0.1.6 + +## 0.1.7 + +### Patch Changes + +- [`3e920136`](https://github.com/chakra-ui/zag/commit/3e920136c537445a36cf0d04045de1d8ff037ecf) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Expose type utilities to frameworks + +* [`9ebe6b45`](https://github.com/chakra-ui/zag/commit/9ebe6b455bfc1b7bf1ad8f770d70ea7656b6c1fe) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Remove unneeded `Promise.resolve(...)` + +* Updated dependencies [[`0d3065e9`](https://github.com/chakra-ui/zag/commit/0d3065e94d707d3161d901576421beae66c32aba), + [`587cbec9`](https://github.com/chakra-ui/zag/commit/587cbec9b32ee9e8faef5ceeefb779231b152018)]: + - @zag-js/core@0.1.5 + +## 0.1.6 + +### Patch Changes + +- [#89](https://github.com/chakra-ui/zag/pull/89) + [`a71d5d2a`](https://github.com/chakra-ui/zag/commit/a71d5d2a984e4293ebeb55944e27df20492ad1c0) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Add incremental support for shadow root in machines + +- Updated dependencies [[`bcf247f1`](https://github.com/chakra-ui/zag/commit/bcf247f18afa5413a7b008f5ab5cbd3665350cb9), + [`a71d5d2a`](https://github.com/chakra-ui/zag/commit/a71d5d2a984e4293ebeb55944e27df20492ad1c0)]: + - @zag-js/core@0.1.4 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies [[`46ef565`](https://github.com/chakra-ui/zag/commit/46ef5659a855a382af1e5b0e24d35d03466cfb22)]: + - @zag-js/core@0.1.3 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [[`3f715bd`](https://github.com/chakra-ui/zag/commit/3f715bdc4f52cdbf71ce9a22a3fc20d31c5fea89)]: + - @zag-js/core@0.1.2 + +## 0.1.3 + +### Patch Changes + +- [#62](https://github.com/chakra-ui/zag/pull/62) + [`e4441c6`](https://github.com/chakra-ui/zag/commit/e4441c6f1fae0f7d8391f0f1403138c70bbc6b1a) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Widen type for `element` type in `PropTypes` + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`8ef855e`](https://github.com/chakra-ui/zag/commit/8ef855efdf8aaca4355c816cc446bc745e34ec54)]: + - @zag-js/core@0.1.1 + +## 0.1.1 + +### Patch Changes + +- [`3e145c1`](https://github.com/chakra-ui/zag/commit/3e145c185d598766aae420f724c7759390cb0404) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Export `mergeProps` utility from framework packages + +## 0.1.0 + +### Minor Changes + +- [`157aadc`](https://github.com/chakra-ui/zag/commit/157aadc3ac572d2289432efe32ae3f15a2be4ad1) Thanks + [@segunadebayo](https://github.com/segunadebayo)! - Initial release + +### Patch Changes + +- Updated dependencies [[`157aadc`](https://github.com/chakra-ui/zag/commit/157aadc3ac572d2289432efe32ae3f15a2be4ad1)]: + - @zag-js/core@0.1.0 diff --git a/packages/frameworks/lit/package.json b/packages/frameworks/lit/package.json new file mode 100644 index 0000000000..6de9d34f38 --- /dev/null +++ b/packages/frameworks/lit/package.json @@ -0,0 +1,57 @@ +{ + "name": "@zag-js/lit", + "version": "1.22.1", + "description": "The lit wrapper for zag", + "keywords": [ + "ui-machines", + "state-machines", + "zag", + "lit", + "controller", + "web-components" + ], + "author": "Segun Adebayo ", + "homepage": "https://github.com/chakra-ui/zag#readme", + "license": "MIT", + "repository": "https://github.com/chakra-ui/zag/tree/main/packages/frameworks/lit", + "sideEffects": false, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "bugs": { + "url": "https://github.com/chakra-ui/zag/issues" + }, + "dependencies": { + "@solid-primitives/keyed": "^1.5.2", + "@zag-js/utils": "workspace:*", + "@zag-js/core": "workspace:*", + "@zag-js/store": "workspace:*", + "@zag-js/types": "workspace:*" + }, + "devDependencies": { + "@types/jsdom": "^21.1.7", + "solid-js": "1.9.9", + "lit": "^3.0.0", + "clean-package": "2.2.0", + "@solidjs/testing-library": "^0.8.10", + "@testing-library/jest-dom": "^6.8.0", + "jsdom": "^26.1.0" + }, + "peerDependencies": { + "solid-js": ">=1.1.3", + "lit": ">=3.0.0" + }, + "scripts": { + "build": "tsup", + "lint": "eslint src", + "typecheck": "tsc --noEmit", + "test": "vitest", + "prepack": "clean-package", + "postpack": "clean-package restore" + }, + "clean-package": "../../../clean-package.config.json", + "main": "src/index.ts" +} diff --git a/packages/frameworks/lit/src/bindable.ts b/packages/frameworks/lit/src/bindable.ts new file mode 100644 index 0000000000..34347acefc --- /dev/null +++ b/packages/frameworks/lit/src/bindable.ts @@ -0,0 +1,58 @@ +import type { Bindable, BindableParams } from "@zag-js/core" +import { proxy } from "@zag-js/store" +import { isFunction } from "@zag-js/utils" + +export function bindable(props: () => BindableParams): Bindable { + const initial = props().value ?? props().defaultValue + + if (props().debug) { + console.log(`[bindable > ${props().debug}] initial`, initial) + } + + const eq = props().isEqual ?? Object.is + + const store = proxy({ value: initial as T }) + + const controlled = () => props().value !== undefined + + return { + initial, + ref: store, + get() { + return controlled() ? (props().value as T) : store.value + }, + set(nextValue: T | ((prev: T) => T)) { + const prev = store.value + const next = isFunction(nextValue) ? nextValue(prev as T) : nextValue + + if (props().debug) { + console.log(`[bindable > ${props().debug}] setValue`, { next, prev }) + } + + if (!controlled()) store.value = next + if (!eq(next, prev)) { + props().onChange?.(next, prev) + } + }, + invoke(nextValue: T, prevValue: T) { + props().onChange?.(nextValue, prevValue) + }, + hash(value: T) { + return props().hash?.(value) ?? String(value) + }, + } +} + +bindable.cleanup = (_fn: VoidFunction) => { + // No-op in vanilla implementation +} + +bindable.ref = (defaultValue: T) => { + let value = defaultValue + return { + get: () => value, + set: (next: T) => { + value = next + }, + } +} diff --git a/packages/frameworks/lit/src/index.ts b/packages/frameworks/lit/src/index.ts new file mode 100644 index 0000000000..79e6c13681 --- /dev/null +++ b/packages/frameworks/lit/src/index.ts @@ -0,0 +1,4 @@ +export * from "./machine" +export { MachineController } from "./machine-controller" +export { mergeProps } from "./merge-props" +export * from "./normalize-props" diff --git a/packages/frameworks/lit/src/machine-controller.ts b/packages/frameworks/lit/src/machine-controller.ts new file mode 100644 index 0000000000..7d2db30351 --- /dev/null +++ b/packages/frameworks/lit/src/machine-controller.ts @@ -0,0 +1,37 @@ +import type { Machine, MachineSchema } from "@zag-js/core" +import type { ReactiveController, ReactiveControllerHost } from "lit" +import { LitMachine } from "./machine" + +export class MachineController implements ReactiveController { + private machine: LitMachine + + constructor( + private host: ReactiveControllerHost, + machineConfig: Machine, + getProps?: () => Partial, + ) { + this.machine = new LitMachine(machineConfig, getProps) + + // Register for lifecycle updates + host.addController(this) + } + + hostConnected() { + this.machine.subscribe(() => { + this.host.requestUpdate() + }) + } + + hostUpdated(): void { + // Start the machine after the initial html has been rendered + this.machine.start() + } + + hostDisconnected() { + this.machine.stop() + } + + get service() { + return this.machine.service + } +} diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts new file mode 100644 index 0000000000..904905ab80 --- /dev/null +++ b/packages/frameworks/lit/src/machine.ts @@ -0,0 +1,340 @@ +import type { + ActionsOrFn, + Bindable, + BindableContext, + BindableRefs, + ChooseFn, + ComputedFn, + EffectsOrFn, + GuardFn, + Machine, + MachineSchema, + Params, + PropFn, + Scope, + Service, +} from "@zag-js/core" +import { createScope, INIT_STATE, MachineStatus } from "@zag-js/core" +import { subscribe } from "@zag-js/store" +import { compact, identity, isEqual, isFunction, isString, runIfFn, toArray, warn } from "@zag-js/utils" +import { bindable } from "./bindable" +import { createRefs } from "./refs" + +export class LitMachine { + private readonly scope: Scope + private readonly ctx: BindableContext + private readonly prop: PropFn + private readonly state: Bindable + private readonly refs: BindableRefs + private readonly computed: ComputedFn + + private event: any = { type: "" } + private previousEvent: any + + private readonly effects = new Map() + private transition: any = null + + private cleanups: VoidFunction[] = [] + private subscriptions: Array<(service: Service) => void> = [] + private trackers: { deps: any[]; fn: any }[] = [] + private status = MachineStatus.NotStarted + + private readonly getEvent = () => ({ + ...this.event, + current: () => this.event, + previous: () => this.previousEvent, + }) + + private readonly getState = () => ({ + ...this.state, + matches: (...values: T["state"][]) => values.includes(this.state.get()), + hasTag: (tag: T["tag"]) => !!this.machine.states[this.state.get() as T["state"]]?.tags?.includes(tag), + }) + + private readonly debug = (...args: any[]) => { + if (this.machine.debug) console.log(...args) + } + + constructor( + private readonly machine: Machine, + userProps: Partial | (() => Partial) = {}, + ) { + const self = this + + // create scope + const { id, ids, getRootNode } = runIfFn(userProps) as any + this.scope = createScope({ id, ids, getRootNode }) + + // create prop + this.prop = (key) => { + const __props = runIfFn(userProps) + const props: any = machine.props?.({ props: compact(__props), scope: self.scope }) ?? __props + return props[key] as any + } + + // create context + const context: any = machine.context?.({ + prop: self.prop, + bindable, + scope: self.scope, + flush(fn: VoidFunction) { + queueMicrotask(fn) // requestUpdate? + }, + getContext: () => self.ctx as any, + getComputed: () => self.computed as any, + getRefs: () => self.refs as any, + getEvent: self.getEvent, + }) + + // subscribe to context changes + if (context) { + Object.values(context).forEach((item: any) => { + const unsub = subscribe(item.ref, () => this.publish()) + this.cleanups.push(unsub) + }) + } + + // context function + this.ctx = { + get(key) { + return context?.[key].get() + }, + set(key, value) { + context?.[key].set(value) + }, + initial(key) { + return context?.[key].initial + }, + hash(key) { + const current = context?.[key].get() + return context?.[key].hash(current) + }, + } + + this.computed = (key) => + machine.computed?.[key]({ + context: self.ctx as any, + event: self.getEvent(), + prop: self.prop, + refs: self.refs, + scope: self.scope, + computed: self.computed as any, + }) ?? ({} as any) + + this.refs = createRefs(self.machine.refs?.({ prop: self.prop, context: self.ctx }) ?? {}) + + // state + this.state = bindable(() => ({ + defaultValue: self.machine.initialState({ prop: self.prop }), + onChange: (nextState, prevState) => { + // compute effects: exit -> transition -> enter + + // exit effects + if (prevState) { + const exitEffects = self.effects.get(prevState) + exitEffects?.() + self.effects.delete(prevState) + } + + // exit actions + if (prevState) { + // @ts-ignore + self.action(self.machine.states[prevState]?.exit) + } + + // transition actions + self.action(self.transition?.actions) + + // enter effect + // @ts-ignore + const cleanup = self.effect(self.machine.states[nextState]?.effects) + if (cleanup) self.effects.set(nextState as string, cleanup) + + // root entry actions + if (prevState === INIT_STATE) { + self.action(self.machine.entry) + const cleanup = self.effect(self.machine.effects) + if (cleanup) self.effects.set(INIT_STATE, cleanup) + } + + // enter actions + // @ts-ignore + self.action(self.machine.states[nextState]?.entry) + }, + })) + + this.cleanups.push(subscribe(this.state.ref, () => self.publish())) + } + + private readonly send = (event: any) => { + queueMicrotask(() => { + // check status inside microtask to prevent race condition + if (this.status !== MachineStatus.Started) return + + this.previousEvent = this.event + this.event = event + + this.debug("send", event) + + let currentState = this.state.get() + + const transitions = + // @ts-ignore + this.machine.states[currentState].on?.[event.type] ?? + // @ts-ignore + this.machine.on?.[event.type] + + const transition = this.choose(transitions) + if (!transition) return + + // save current transition + this.transition = transition + const target = transition.target ?? currentState + + this.debug("transition", transition) + + const changed = target !== currentState + if (changed) { + // state change is high priority + this.state.set(target) + } else { + // call transition actions + this.action(transition.actions) + } + }) + } + + private readonly action = (keys: ActionsOrFn | undefined) => { + const strs = isFunction(keys) ? keys(this.getParams()) : keys + if (!strs) return + const fns = strs.map((s) => { + const fn = this.machine.implementations?.actions?.[s] + if (!fn) warn(`[zag-js] No implementation found for action "${JSON.stringify(s)}"`) + return fn + }) + for (const fn of fns) { + fn?.(this.getParams()) + } + } + + private readonly guard = (str: T["guard"] | GuardFn) => { + if (isFunction(str)) return str(this.getParams()) + return this.machine.implementations?.guards?.[str](this.getParams()) + } + + private readonly effect = (keys: EffectsOrFn | undefined) => { + const strs = isFunction(keys) ? keys(this.getParams()) : keys + if (!strs) return + const fns = strs.map((s) => { + const fn = this.machine.implementations?.effects?.[s] + if (!fn) warn(`[zag-js] No implementation found for effect "${JSON.stringify(s)}"`) + return fn + }) + const cleanups: VoidFunction[] = [] + for (const fn of fns) { + const cleanup = fn?.(this.getParams()) + if (cleanup) cleanups.push(cleanup) + } + return () => cleanups.forEach((fn) => fn?.()) + } + + private readonly choose: ChooseFn = (transitions) => { + return toArray(transitions).find((t: any) => { + let result = !t.guard + if (isString(t.guard)) result = !!this.guard(t.guard) + else if (isFunction(t.guard)) result = t.guard(this.getParams()) + return result + }) + } + + start() { + if (this.status === MachineStatus.Started) return + + this.status = MachineStatus.Started + this.debug("initializing...") + this.state.invoke(this.state.initial!, INIT_STATE) + this.setupTrackers() + } + + stop() { + if (this.status === MachineStatus.Stopped) return + + // run exit effects + this.effects.forEach((fn) => fn?.()) + this.effects.clear() + this.transition = null + this.action(this.machine.exit) + + // unsubscribe from all subscriptions + this.cleanups.forEach((unsub) => unsub()) + this.cleanups = [] + this.trackers = [] + this.subscriptions = [] + + this.status = MachineStatus.Stopped + this.debug("unmounting...") + } + + readonly subscribe = (fn: (service: Service) => void) => { + this.subscriptions.push(fn) + // return unsubscribe function + return () => { + const index = this.subscriptions.indexOf(fn) + if (index > -1) { + this.subscriptions.splice(index, 1) + } + } + } + + get service(): Service { + return { + state: this.getState(), + send: this.send, + context: this.ctx, + prop: this.prop, + scope: this.scope, + refs: this.refs, + computed: this.computed, + event: this.getEvent(), + getStatus: () => this.status, + } + } + + private readonly publish = () => { + this.callTrackers() + this.subscriptions.forEach((fn) => fn(this.service)) + } + + private readonly setupTrackers = () => { + this.machine.watch?.(this.getParams()) + } + + private readonly callTrackers = () => { + this.trackers.forEach(({ deps, fn }) => { + const next = deps.map((dep) => dep()) + if (!isEqual(fn.prev, next)) { + fn() + fn.prev = next + } + }) + } + + private readonly getParams = (): Params => ({ + state: this.getState(), + context: this.ctx, + event: this.getEvent(), + prop: this.prop, + send: this.send, + action: this.action, + guard: this.guard, + track: (deps: any[], fn: any) => { + fn.prev = deps.map((dep) => dep()) + this.trackers.push({ deps, fn }) + }, + refs: this.refs, + computed: this.computed, + flush: identity, // requestUpdate? + scope: this.scope, + choose: this.choose, + }) +} diff --git a/packages/frameworks/lit/src/merge-props.ts b/packages/frameworks/lit/src/merge-props.ts new file mode 100644 index 0000000000..a761aad5fe --- /dev/null +++ b/packages/frameworks/lit/src/merge-props.ts @@ -0,0 +1,4 @@ +import { createMergeProps } from "@zag-js/core" + +// Create mergeProps function for Lit's event syntax (@event instead of onEvent) +export const mergeProps = createMergeProps({ eventPrefix: "@" }) diff --git a/packages/frameworks/lit/src/normalize-props.ts b/packages/frameworks/lit/src/normalize-props.ts new file mode 100644 index 0000000000..026b323236 --- /dev/null +++ b/packages/frameworks/lit/src/normalize-props.ts @@ -0,0 +1,149 @@ +import { createNormalizer } from "@zag-js/types" +import { isObject } from "@zag-js/utils" + +// Lit uses built-in template binding syntax for prop normalization: +// - 'attribute-name': value -> attribute +// - '?boolean-attr': true -> boolean attribute +// - '@event-name': handler -> event listener +// - '.property': value -> property assignment + +// TODO: Improve types +type LitElementProps = { + [key: string]: any + style?: string | Record +} + +type RequiredElements = + | "button" + | "label" + | "input" + | "textarea" + | "img" + | "output" + | "select" + | "rect" + | "circle" + | "svg" + | "path" + +export type PropTypes = Record & { + element: LitElementProps + style: Record +} + +const propMap: Record = { + onFocus: "onfocusin", + onBlur: "onfocusout", + onDoubleClick: "ondblclick", + onChange: "oninput", + defaultChecked: "checked", + defaultValue: "value", + defaultSelected: "selected", + htmlFor: "for", + className: "class", +} + +// Properties that should be set as properties, not attributes +const propertyNames = new Set([ + "value", + "checked", + "selected", + "disabled", + "readonly", + "multiple", + "hidden", + "contenteditable", + "draggable", + "spellcheck", + "autocomplete", +]) + +const svgAttributes = new Set( + "viewBox,preserveAspectRatio,fillRule,clipPath,clipRule,strokeWidth,strokeLinecap,strokeLinejoin,strokeDasharray,strokeDashoffset,strokeMiterlimit".split( + ",", + ), +) + +const shouldPreserveCase = (key: string): boolean => { + return ( + key.startsWith("data-") || + key.startsWith("aria-") || + key.includes(":") || // XML namespaced + key.startsWith("xml") || // XML attributes + svgAttributes.has(key) + ) +} + +export function toStyleString(style: Record): string { + let string = "" + + for (let key in style) { + /** + * Ignore null and undefined values. + */ + const value = style[key] + if (value === null || value === undefined) continue + + /** + * Convert camelCase to kebab-case except for CSS custom properties. + */ + if (!key.startsWith("--")) { + key = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`) + } + + string += `${key}:${value};` + } + + return string +} + +type Dict = Record + +export const normalizeProps = createNormalizer((props: Dict) => { + const normalized: Dict = {} + + for (let key in props) { + const value = props[key] + + // Handle style objects + if (key === "style" && isObject(value)) { + normalized["style"] = toStyleString(value) + continue + } + + // Map React-style prop names + if (key in propMap) { + key = propMap[key] + } + + // Convert to Lit event listener syntax: @eventname + if (key.startsWith("on") && typeof value === "function") { + normalized[`@${key.toLowerCase().slice(2)}`] = value + continue + } + + // Handle boolean attributes with ? prefix + if (typeof value === "boolean") { + // ARIA booleans must be string values "true" or "false" + if (key.startsWith("aria-")) { + normalized[key] = value.toString() + continue + } + + normalized[`?${key.toLowerCase()}`] = value + continue + } + + // Handle properties that should be set as properties with . prefix + if (propertyNames.has(key.toLowerCase())) { + normalized[`.${key.toLowerCase()}`] = value + continue + } + + // Everything else as attribute + const attrKey = shouldPreserveCase(key) ? key : key.toLowerCase() + normalized[attrKey] = value + } + + return normalized +}) diff --git a/packages/frameworks/lit/src/refs.ts b/packages/frameworks/lit/src/refs.ts new file mode 100644 index 0000000000..81057207f3 --- /dev/null +++ b/packages/frameworks/lit/src/refs.ts @@ -0,0 +1,11 @@ +export function createRefs(refs: T) { + const ref = { current: refs } + return { + get(key: K): T[K] { + return ref.current[key] + }, + set(key: K, value: T[K]) { + ref.current[key] = value + }, + } +} diff --git a/packages/frameworks/lit/src/track.ts b/packages/frameworks/lit/src/track.ts new file mode 100644 index 0000000000..c532a581c2 --- /dev/null +++ b/packages/frameworks/lit/src/track.ts @@ -0,0 +1,30 @@ +import { isEqual, isFunction } from "@zag-js/utils" +import { createEffect } from "solid-js" + +function access(v: T | (() => T)): T { + if (isFunction(v)) return v() + return v +} + +export const createTrack = (deps: any[], effect: VoidFunction) => { + let prevDeps: any[] = [] + let isFirstRun = true + createEffect(() => { + if (isFirstRun) { + prevDeps = deps.map((d) => access(d)) + isFirstRun = false + return + } + let changed = false + for (let i = 0; i < deps.length; i++) { + if (!isEqual(prevDeps[i], access(deps[i]))) { + changed = true + break + } + } + if (changed) { + prevDeps = deps.map((d) => access(d)) + effect() + } + }) +} diff --git a/packages/frameworks/lit/test-toggle.html b/packages/frameworks/lit/test-toggle.html new file mode 100644 index 0000000000..cd48f48e3b --- /dev/null +++ b/packages/frameworks/lit/test-toggle.html @@ -0,0 +1,55 @@ + + + + + + Lit Toggle Test + + + + + + diff --git a/packages/frameworks/lit/tests/machine.test.ts b/packages/frameworks/lit/tests/machine.test.ts new file mode 100644 index 0000000000..51c59a5674 --- /dev/null +++ b/packages/frameworks/lit/tests/machine.test.ts @@ -0,0 +1,428 @@ +// TODO: These tests were AI generated and need review +// - Fix TypeScript issues +// - Verify test correctness and expectations +// - Ensure tests match real Lit component lifecycle + +import { createMachine } from "@zag-js/core" +import { MachineController } from "../src" + +// Mock LitElement for testing +class MockLitElement { + updateRequested = 0 + controllers: any[] = [] + + requestUpdate() { + this.updateRequested++ + } + + addController(controller: any) { + this.controllers.push(controller) + } + + removeController(controller: any) { + const index = this.controllers.indexOf(controller) + if (index >= 0) { + this.controllers.splice(index, 1) + } + } +} + +function renderMachine(machine: any, props: any = {}) { + const host = new MockLitElement() + const controller = new MachineController(host as any, machine, () => props) + + // Simulate hostConnected + controller.hostConnected() + + const send = async (event: any) => { + controller.service.send(event) + await Promise.resolve() + } + + const { service } = controller + + return { controller, host, send, service } +} + +describe("LitMachine", () => { + test("initial state", () => { + const machine = createMachine({ + initialState() { + return "foo" + }, + states: { + foo: { + on: { + NEXT: { target: "bar" }, + }, + }, + bar: {}, + }, + }) + + const { service } = renderMachine(machine) + + expect(service.state.get()).toBe("foo") + }) + + test("initial entry action", async () => { + const fooEntry = vi.fn() + const rootEntry = vi.fn() + + const machine = createMachine({ + initialState() { + return "foo" + }, + entry: ["rootEntry"], + states: { + foo: { + entry: ["fooEntry"], + }, + }, + implementations: { + actions: { + fooEntry, + rootEntry, + }, + }, + }) + + renderMachine(machine) + await Promise.resolve() + + expect(fooEntry).toHaveBeenCalledOnce() + expect(rootEntry).toHaveBeenCalledOnce() + }) + + test("current state and context", () => { + const machine = createMachine({ + initialState() { + return "test" + }, + context({ bindable }) { + return { foo: bindable(() => ({ defaultValue: "bar" })) } + }, + states: { + test: {}, + }, + }) + + const { service } = renderMachine(machine) + + expect(service.state.get()).toEqual("test") + expect(service.context.get("foo")).toEqual("bar") + }) + + test("send event", async () => { + let done = vi.fn() + const machine = createMachine({ + initialState() { + return "test" + }, + context({ bindable }) { + return { foo: bindable(() => ({ defaultValue: "bar" })) } + }, + states: { + test: { + on: { + CHANGE: { target: "success" }, + }, + }, + success: { + entry: ["done"], + }, + }, + implementations: { + actions: { + done, + }, + }, + }) + + const { send } = renderMachine(machine) + await Promise.resolve() + + await send({ type: "CHANGE" }) + expect(done).toHaveBeenCalledOnce() + }) + + test("state tags", async () => { + const machine = createMachine({ + initialState() { + return "green" + }, + states: { + green: { + tags: ["go"], + on: { + TIMER: { + target: "yellow", + }, + }, + }, + yellow: { + tags: ["go"], + on: { + TIMER: { + target: "red", + }, + }, + }, + red: { + tags: ["stop"], + }, + }, + }) + + const { service, send } = renderMachine(machine) + await Promise.resolve() + + expect(service.state.hasTag("go")).toBeTruthy() + + await send({ type: "TIMER" }) + expect(service.state.get()).toBe("yellow") + expect(service.state.hasTag("go")).toBeTruthy() + + await send({ type: "TIMER" }) + expect(service.state.get()).toBe("red") + expect(service.state.hasTag("go")).toBeFalsy() + }) + + test("computed", async () => { + const machine = createMachine({ + initialState() { + return "test" + }, + states: { + test: { + on: { + UPDATE: { + actions: ["setValue"], + }, + }, + }, + }, + context({ bindable }) { + return { value: bindable(() => ({ defaultValue: "bar" })) } + }, + computed: { + length: ({ context }) => context.get("value").length, + }, + implementations: { + actions: { + setValue: ({ context }) => context.set("value", "hello"), + }, + }, + }) + + const { service, send } = renderMachine(machine) + await Promise.resolve() + + expect(service.computed("length")).toEqual(3) + + await send({ type: "UPDATE" }) + expect(service.computed("length")).toEqual(5) + }) + + test("watch", async () => { + const notify = vi.fn() + const machine = createMachine({ + initialState() { + return "test" + }, + states: { + test: { + on: { + UPDATE: { + actions: ["setValue"], + }, + }, + }, + }, + context({ bindable }) { + return { value: bindable(() => ({ defaultValue: "bar" })) } + }, + watch({ track, context, action }) { + track([() => context.get("value")], () => { + action(["notify"]) + }) + }, + implementations: { + actions: { + setValue: ({ context }) => context.set("value", "hello"), + notify, + }, + }, + }) + + const { send } = renderMachine(machine) + + // send update twice and expect notify to be called once (since the value is the same) + await send({ type: "UPDATE" }) + await send({ type: "UPDATE" }) + expect(notify).toHaveBeenCalledOnce() + }) + + test("guard: basic", async () => { + const machine = createMachine({ + props() { + return { max: 1 } + }, + initialState() { + return "test" + }, + + context({ bindable }) { + return { count: bindable(() => ({ defaultValue: 0 })) } + }, + + states: { + test: { + on: { + INCREMENT: { + guard: "isBelowMax", + actions: ["increment"], + }, + }, + }, + }, + + implementations: { + guards: { + isBelowMax: ({ prop, context }) => prop("max") > context.get("count"), + }, + actions: { + increment: ({ context }) => context.set("count", context.get("count") + 1), + }, + }, + }) + + const { service, send } = renderMachine(machine, { max: 1 }) + await Promise.resolve() + + await send({ type: "INCREMENT" }) + expect(service.context.get("count")).toEqual(1) + + await send({ type: "INCREMENT" }) + expect(service.context.get("count")).toEqual(1) + }) + + test("context: controlled", async () => { + const machine = createMachine({ + props() { + return { value: "foo", defaultValue: "" } + }, + initialState() { + return "test" + }, + + context({ bindable, prop }) { + return { + value: bindable(() => ({ + defaultValue: prop("defaultValue"), + value: prop("value"), + })), + } + }, + + states: { + test: { + on: { + "VALUE.SET": { + actions: ["setValue"], + }, + }, + }, + }, + + implementations: { + actions: { + setValue: ({ context, event }) => context.set("value", event.value), + }, + }, + }) + + const { service, send } = renderMachine(machine, { value: "foo", defaultValue: "" }) + + await send({ type: "VALUE.SET", value: "next" }) + + // since value is controlled, it should not change + expect(service.context.get("value")).toEqual("foo") + }) +}) + +describe("MachineController", () => { + test("triggers host.requestUpdate on state changes", async () => { + const machine = createMachine({ + initialState() { + return "idle" + }, + states: { + idle: { + on: { + START: { target: "active" }, + }, + }, + active: {}, + }, + }) + + const { host, send } = renderMachine(machine) + + // Initial update from machine start + expect(host.updateRequested).toBeGreaterThan(0) + const initialUpdates = host.updateRequested + + await send({ type: "START" }) + + // Should have triggered additional update + expect(host.updateRequested).toBeGreaterThan(initialUpdates) + }) + + test("provides service API", () => { + const machine = createMachine({ + initialState() { + return "test" + }, + context({ bindable }) { + return { value: bindable(() => ({ defaultValue: "initial" })) } + }, + states: { + test: {}, + }, + }) + + const { service } = renderMachine(machine) + + // Check all service API properties are available + expect(service.state).toBeDefined() + expect(service.send).toBeDefined() + expect(service.context).toBeDefined() + expect(service.prop).toBeDefined() + expect(service.scope).toBeDefined() + expect(service.refs).toBeDefined() + expect(service.computed).toBeDefined() + expect(service.event).toBeDefined() + }) + + test("cleanup on hostDisconnected", () => { + const machine = createMachine({ + initialState() { + return "test" + }, + states: { + test: {}, + }, + }) + + const host = new MockLitElement() + const controller = new MachineController(host as any, machine, () => ({})) + + controller.hostConnected() + expect(controller.service).toBeDefined() + + controller.hostDisconnected() + // Machine should be stopped and cleaned up + expect(controller.service.getStatus()).toBe("Stopped") + }) +}) diff --git a/packages/frameworks/lit/tests/merge-props.test.ts b/packages/frameworks/lit/tests/merge-props.test.ts new file mode 100644 index 0000000000..53b8818cd9 --- /dev/null +++ b/packages/frameworks/lit/tests/merge-props.test.ts @@ -0,0 +1,94 @@ +import { mergeProps } from "../src" + +describe("mergeProps", () => { + it("handles one argument", () => { + const onClick = () => {} + const className = "primary" + const id = "test_id" + + const props = mergeProps({ onClick, className, id }) + + expect(props.onClick).toBe(onClick) + expect(props.className).toBe(className) + expect(props.id).toBe(id) + }) + + it("combines handlers with @ prefix", () => { + let count = 0 + + const mockFn = vi.fn(() => { + count++ + }) + + const props = mergeProps({ "@click": mockFn }, { "@click": mockFn }, { "@click": mockFn }) + + props["@click"]() + expect(mockFn).toBeCalledTimes(3) + expect(count).toBe(3) + }) + + it("combines css classes", () => { + const className1 = "primary" + const className2 = "hover" + const className3 = "focus" + + const props = mergeProps({ class: className1 }, { class: className2 }, { class: className3 }) + expect(props.class).toBe("primary hover focus") + + const props2 = mergeProps({ className: className1 }, { className: className2 }, { className: className3 }) + expect(props2.className).toBe("primary hover focus") + }) + + it("combines styles", () => { + const stringStyles = ` + margin: 24px; + padding: 2; + background-image: url("http://example.com/image.png"); + border: 1px solid #123456; + --x: 123; + ` + + const objStyles = { + margin: "10px", + "font-size": "2rem", + } + + const props = mergeProps({ style: stringStyles }, { style: objStyles }) + + expect(props.style).toMatchInlineSnapshot(` + { + "--x": "123", + "background-image": "url("http://example.com/image.png")", + "border": "1px solid #123456", + "font-size": "2rem", + "margin": "10px", + "padding": "2", + } + `) + }) + + it("handles Lit @ event prefix", () => { + const mockFn1 = vi.fn() + const mockFn2 = vi.fn() + + const props = mergeProps({ "@click": mockFn1 }, { "@click": mockFn2 }) + + props["@click"]("test") + expect(mockFn1).toBeCalledWith("test") + expect(mockFn2).toBeCalledWith("test") + }) + + it("last value overwrites the event-listeners", () => { + const mockFn = vi.fn() + const message1 = "click1" + const message2 = "click2" + + const props = mergeProps( + { "@event": () => mockFn(message1) }, + { "@event": () => mockFn(message2) }, + { "@event": "overwrites" }, + ) + + expect(props["@event"]).toBe("overwrites") + }) +}) diff --git a/packages/frameworks/lit/tsconfig.json b/packages/frameworks/lit/tsconfig.json new file mode 100644 index 0000000000..a56c4a3eee --- /dev/null +++ b/packages/frameworks/lit/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["src", "tests"], + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/.tsbuildinfo" + } +} diff --git a/packages/frameworks/lit/vite.config.ts b/packages/frameworks/lit/vite.config.ts new file mode 100644 index 0000000000..f9a6eb55f8 --- /dev/null +++ b/packages/frameworks/lit/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + retry: 2, + globals: true, + environment: "jsdom", + css: false, + setupFiles: "./vitest.setup.ts", + }, +}) diff --git a/packages/frameworks/lit/vitest.setup.ts b/packages/frameworks/lit/vitest.setup.ts new file mode 100644 index 0000000000..d96f01d6f5 --- /dev/null +++ b/packages/frameworks/lit/vitest.setup.ts @@ -0,0 +1,9 @@ +import "@testing-library/jest-dom/vitest" +import { JSDOM } from "jsdom" + +const { window } = new JSDOM() + +// @ts-ignore +window.requestAnimationFrame = (cb: VoidFunction) => setTimeout(cb, 1000 / 60) + +Object.assign(global, { window, document: window.document }) diff --git a/playwright.config.ts b/playwright.config.ts index e68b5c87ef..c53e6d4bae 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -34,6 +34,12 @@ export function getWebServer(): WebServer { url: "http://localhost:3003", reuseExistingServer: !CI, }, + lit: { + cwd: "./examples/lit-ts", + command: "pnpm vite --port 3004", + url: "http://localhost:3004", + reuseExistingServer: !CI, + }, } return frameworks[framework] diff --git a/playwright.lit.config.ts b/playwright.lit.config.ts new file mode 100644 index 0000000000..4d29eedc19 --- /dev/null +++ b/playwright.lit.config.ts @@ -0,0 +1,53 @@ +import { defineConfig } from "@playwright/test" +import baseConfig, { getWebServer } from "./playwright.config" + +// Set default Lit framework +process.env.FRAMEWORK = process.env.FRAMEWORK || "lit" +const webServer = getWebServer() + +export default defineConfig({ + ...baseConfig, + + webServer, + use: { + baseURL: webServer.url, + }, + + // Only test implemented Lit components + testMatch: [ + "**/accordion.e2e.ts", + "**/checkbox.e2e.ts", + "**/collapsible.e2e.ts", + "**/dialog.e2e.ts", + "**/menu.e2e.ts", + "**/menu-nested.e2e.ts", + "**/menu-option.e2e.ts", + "**/popover.e2e.ts", + "**/radio-group.e2e.ts", + "**/slider.e2e.ts", + "**/switch.e2e.ts", + "**/tabs.e2e.ts", + "**/toggle-group.e2e.ts", + "**/toggle.e2e.ts", + ], + + // // Custom projects for Lit testing with different DOM modes + // projects: [ + // { + // name: "lit-light-dom", + // use: { + // ...baseConfig.use, + // }, + // // Environment variables are set via process.env before test execution + // // VITE_DOM_MODE=light-dom will be set when running this project + // }, + // { + // name: "lit-shadow-dom", + // use: { + // ...baseConfig.use, + // }, + // // Environment variables are set via process.env before test execution + // // VITE_DOM_MODE=shadow-dom will be set when running this project + // }, + // ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72dfcf8cbb..af8a489d8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,7 +70,7 @@ importers: version: 10.1.8(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-import: specifier: 2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)) + version: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-prettier: specifier: 5.5.4 version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1))(prettier@3.6.2) @@ -129,6 +129,259 @@ importers: specifier: 3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + examples/lit-ts: + dependencies: + '@internationalized/date': + specifier: 3.9.0 + version: 3.9.0 + '@open-wc/lit-helpers': + specifier: 0.7.0 + version: 0.7.0(lit@3.3.1) + '@zag-js/accordion': + specifier: workspace:* + version: link:../../packages/machines/accordion + '@zag-js/anatomy': + specifier: workspace:* + version: link:../../packages/anatomy + '@zag-js/anatomy-icons': + specifier: workspace:* + version: link:../../packages/anatomy-icons + '@zag-js/angle-slider': + specifier: workspace:* + version: link:../../packages/machines/angle-slider + '@zag-js/aria-hidden': + specifier: workspace:* + version: link:../../packages/utilities/aria-hidden + '@zag-js/async-list': + specifier: workspace:* + version: link:../../packages/machines/async-list + '@zag-js/auto-resize': + specifier: workspace:* + version: link:../../packages/utilities/auto-resize + '@zag-js/avatar': + specifier: workspace:* + version: link:../../packages/machines/avatar + '@zag-js/carousel': + specifier: workspace:* + version: link:../../packages/machines/carousel + '@zag-js/checkbox': + specifier: workspace:* + version: link:../../packages/machines/checkbox + '@zag-js/clipboard': + specifier: workspace:* + version: link:../../packages/machines/clipboard + '@zag-js/collapsible': + specifier: workspace:* + version: link:../../packages/machines/collapsible + '@zag-js/collection': + specifier: workspace:* + version: link:../../packages/utilities/collection + '@zag-js/color-picker': + specifier: workspace:* + version: link:../../packages/machines/color-picker + '@zag-js/color-utils': + specifier: workspace:* + version: link:../../packages/utilities/color-utils + '@zag-js/combobox': + specifier: workspace:* + version: link:../../packages/machines/combobox + '@zag-js/core': + specifier: workspace:* + version: link:../../packages/core + '@zag-js/date-picker': + specifier: workspace:* + version: link:../../packages/machines/date-picker + '@zag-js/date-utils': + specifier: workspace:* + version: link:../../packages/utilities/date-utils + '@zag-js/dialog': + specifier: workspace:* + version: link:../../packages/machines/dialog + '@zag-js/dismissable': + specifier: workspace:* + version: link:../../packages/utilities/dismissable + '@zag-js/docs': + specifier: workspace:* + version: link:../../packages/docs + '@zag-js/dom-query': + specifier: workspace:* + version: link:../../packages/utilities/dom-query + '@zag-js/editable': + specifier: workspace:* + version: link:../../packages/machines/editable + '@zag-js/file-upload': + specifier: workspace:* + version: link:../../packages/machines/file-upload + '@zag-js/file-utils': + specifier: workspace:* + version: link:../../packages/utilities/file-utils + '@zag-js/floating-panel': + specifier: workspace:* + version: link:../../packages/machines/floating-panel + '@zag-js/focus-trap': + specifier: workspace:* + version: link:../../packages/utilities/focus-trap + '@zag-js/focus-visible': + specifier: workspace:* + version: link:../../packages/utilities/focus-visible + '@zag-js/highlight-word': + specifier: workspace:* + version: link:../../packages/utilities/highlight-word + '@zag-js/hover-card': + specifier: workspace:* + version: link:../../packages/machines/hover-card + '@zag-js/i18n-utils': + specifier: workspace:* + version: link:../../packages/utilities/i18n-utils + '@zag-js/interact-outside': + specifier: workspace:* + version: link:../../packages/utilities/interact-outside + '@zag-js/json-tree-utils': + specifier: workspace:* + version: link:../../packages/utilities/json-tree-utils + '@zag-js/listbox': + specifier: workspace:* + version: link:../../packages/machines/listbox + '@zag-js/lit': + specifier: workspace:* + version: link:../../packages/frameworks/lit + '@zag-js/live-region': + specifier: workspace:* + version: link:../../packages/utilities/live-region + '@zag-js/menu': + specifier: workspace:* + version: link:../../packages/machines/menu + '@zag-js/navigation-menu': + specifier: workspace:* + version: link:../../packages/machines/navigation-menu + '@zag-js/number-input': + specifier: workspace:* + version: link:../../packages/machines/number-input + '@zag-js/pagination': + specifier: workspace:* + version: link:../../packages/machines/pagination + '@zag-js/password-input': + specifier: workspace:* + version: link:../../packages/machines/password-input + '@zag-js/pin-input': + specifier: workspace:* + version: link:../../packages/machines/pin-input + '@zag-js/popover': + specifier: workspace:* + version: link:../../packages/machines/popover + '@zag-js/popper': + specifier: workspace:* + version: link:../../packages/utilities/popper + '@zag-js/presence': + specifier: workspace:* + version: link:../../packages/machines/presence + '@zag-js/progress': + specifier: workspace:* + version: link:../../packages/machines/progress + '@zag-js/qr-code': + specifier: workspace:* + version: link:../../packages/machines/qr-code + '@zag-js/radio-group': + specifier: workspace:* + version: link:../../packages/machines/radio-group + '@zag-js/rating-group': + specifier: workspace:* + version: link:../../packages/machines/rating-group + '@zag-js/rect-utils': + specifier: workspace:* + version: link:../../packages/utilities/rect + '@zag-js/remove-scroll': + specifier: workspace:* + version: link:../../packages/utilities/remove-scroll + '@zag-js/scroll-snap': + specifier: workspace:* + version: link:../../packages/utilities/scroll-snap + '@zag-js/select': + specifier: workspace:* + version: link:../../packages/machines/select + '@zag-js/shared': + specifier: workspace:* + version: link:../../shared + '@zag-js/signature-pad': + specifier: workspace:* + version: link:../../packages/machines/signature-pad + '@zag-js/slider': + specifier: workspace:* + version: link:../../packages/machines/slider + '@zag-js/splitter': + specifier: workspace:* + version: link:../../packages/machines/splitter + '@zag-js/steps': + specifier: workspace:* + version: link:../../packages/machines/steps + '@zag-js/store': + specifier: workspace:* + version: link:../../packages/store + '@zag-js/stringify-state': + specifier: workspace:* + version: link:../../packages/utilities/stringify-state + '@zag-js/switch': + specifier: workspace:* + version: link:../../packages/machines/switch + '@zag-js/tabs': + specifier: workspace:* + version: link:../../packages/machines/tabs + '@zag-js/tags-input': + specifier: workspace:* + version: link:../../packages/machines/tags-input + '@zag-js/timer': + specifier: workspace:* + version: link:../../packages/machines/timer + '@zag-js/toast': + specifier: workspace:* + version: link:../../packages/machines/toast + '@zag-js/toggle': + specifier: workspace:* + version: link:../../packages/machines/toggle + '@zag-js/toggle-group': + specifier: workspace:* + version: link:../../packages/machines/toggle-group + '@zag-js/tooltip': + specifier: workspace:* + version: link:../../packages/machines/tooltip + '@zag-js/tour': + specifier: workspace:* + version: link:../../packages/machines/tour + '@zag-js/tree-view': + specifier: workspace:* + version: link:../../packages/machines/tree-view + '@zag-js/types': + specifier: workspace:* + version: link:../../packages/types + '@zag-js/utils': + specifier: workspace:* + version: link:../../packages/utilities/core + form-serialize: + specifier: 0.7.2 + version: 0.7.2 + lit: + specifier: 3.3.1 + version: 3.3.1 + lucide: + specifier: 0.542.0 + version: 0.542.0 + match-sorter: + specifier: 8.1.0 + version: 8.1.0 + nanoid: + specifier: ^5.1.5 + version: 5.1.5 + devDependencies: + '@types/form-serialize': + specifier: 0.7.4 + version: 0.7.4 + typescript: + specifier: 5.9.2 + version: 5.9.2 + vite: + specifier: 7.1.3 + version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + examples/next-ts: dependencies: '@internationalized/date': @@ -1750,6 +2003,46 @@ importers: specifier: 2.2.0 version: 2.2.0 + packages/frameworks/lit: + dependencies: + '@solid-primitives/keyed': + specifier: ^1.5.2 + version: 1.5.2(solid-js@1.9.9) + '@zag-js/core': + specifier: workspace:* + version: link:../../core + '@zag-js/store': + specifier: workspace:* + version: link:../../store + '@zag-js/types': + specifier: workspace:* + version: link:../../types + '@zag-js/utils': + specifier: workspace:* + version: link:../../utilities/core + devDependencies: + '@solidjs/testing-library': + specifier: ^0.8.10 + version: 0.8.10(@solidjs/router@0.15.3(solid-js@1.9.9))(solid-js@1.9.9) + '@testing-library/jest-dom': + specifier: ^6.8.0 + version: 6.8.0 + '@types/jsdom': + specifier: ^21.1.7 + version: 21.1.7 + clean-package: + specifier: 2.2.0 + version: 2.2.0 + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + lit: + specifier: ^3.0.0 + version: 3.3.1 + solid-js: + specifier: 1.9.9 + version: 1.9.9 + packages/frameworks/preact: dependencies: '@zag-js/core': @@ -4379,7 +4672,7 @@ packages: '@emotion/use-insertion-effect-with-fallbacks@1.2.0': resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} peerDependencies: - react: '>=16.8.0' + react: ^18.3.1 '@emotion/utils@1.4.2': resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} @@ -5115,6 +5408,12 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@lit-labs/ssr-dom-shim@1.4.0': + resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} + + '@lit/reactive-element@2.1.1': + resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -5363,6 +5662,11 @@ packages: '@octokit/types@14.1.0': resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@open-wc/lit-helpers@0.7.0': + resolution: {integrity: sha512-4NBlx5ve0EvZplCRJbESm0MdMbRCw16alP2y76KAAAwzmFFXXrUj5hFwhw55+sSg5qaRRx6sY+s7usKgnNo3TQ==} + peerDependencies: + lit: ^2.0.0 || ^3.0.0 + '@opentelemetry/api-logs@0.39.1': resolution: {integrity: sha512-9BJ8lMcOzEN0lu+Qji801y707oFO4xT3db6cosPvl+k7ItUHKN5ofWqtSbM9gbt1H4JJ/4/2TVrqI9Rq7hNv6Q==} engines: {node: '>=14'} @@ -6622,6 +6926,9 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -9867,6 +10174,15 @@ packages: resolution: {integrity: sha512-VVd7cS6W+vLJu2wmq4QmfVj14Iep7cz4r/OWNk36Aq5ZOY7G8/BfCrQFexcwB1OIxB3yERiePfE/REBjEFulag==} engines: {node: '>=20.0.0'} + lit-element@4.2.1: + resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} + + lit-html@3.3.1: + resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} + + lit@3.3.1: + resolution: {integrity: sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==} + load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} @@ -10031,6 +10347,9 @@ packages: peerDependencies: vue: '>=3.0.1' + lucide@0.542.0: + resolution: {integrity: sha512-+EtDSHjqg/nONgCfnjHCNd84OzbDjxR8ShnOf+oImlU+A8gqlptZ6pGrMCnhEDw8pVNQv3zu/L0eDvMzcc7nWA==} + luxon@3.7.1: resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==} engines: {node: '>=12'} @@ -14736,6 +15055,12 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@lit-labs/ssr-dom-shim@1.4.0': {} + + '@lit/reactive-element@2.1.1': + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.3 @@ -15274,6 +15599,10 @@ snapshots: dependencies: '@octokit/openapi-types': 25.1.0 + '@open-wc/lit-helpers@0.7.0(lit@3.3.1)': + dependencies: + lit: 3.3.1 + '@opentelemetry/api-logs@0.39.1': dependencies: '@opentelemetry/api': 1.9.0 @@ -16417,6 +16746,8 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@types/trusted-types@2.0.7': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -16428,10 +16759,10 @@ snapshots: '@types/node': 24.3.0 optional: true - '@typescript-eslint/eslint-plugin@8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/eslint-plugin@8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) '@typescript-eslint/scope-manager': 8.41.0 '@typescript-eslint/type-utils': 8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) '@typescript-eslint/utils': 8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) @@ -18563,12 +18894,12 @@ snapshots: dependencies: '@next/eslint-plugin-next': 15.5.2 '@rushstack/eslint-patch': 1.12.0 - '@typescript-eslint/eslint-plugin': 8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/eslint-plugin': 8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) '@typescript-eslint/parser': 8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.33.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.33.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.33.0(jiti@2.5.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.33.0(jiti@2.5.1)) @@ -18626,44 +18957,44 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 - eslint: 9.33.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.5.1) get-tsconfig: 4.10.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.14 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.33.0(jiti@2.5.1) get-tsconfig: 4.10.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.14 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.33.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color @@ -18678,16 +19009,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.34.0(jiti@2.5.1)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - eslint-plugin-compat@6.0.2(eslint@9.34.0(jiti@2.5.1)): dependencies: '@mdn/browser-compat-data': 5.7.6 @@ -18700,7 +19021,7 @@ snapshots: lodash.memoize: 4.1.2 semver: 7.7.2 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -18711,7 +19032,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.33.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -18723,7 +19044,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -18758,35 +19079,6 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.34.0(jiti@2.5.1) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.34.0(jiti@2.5.1)) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.33.0(jiti@2.5.1)): dependencies: aria-query: 5.3.2 @@ -20486,6 +20778,22 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.0 + lit-element@4.2.1: + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@lit/reactive-element': 2.1.1 + lit-html: 3.3.1 + + lit-html@3.3.1: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.1: + dependencies: + '@lit/reactive-element': 2.1.1 + lit-element: 4.2.1 + lit-html: 3.3.1 + load-json-file@4.0.0: dependencies: graceful-fs: 4.2.11 @@ -20640,6 +20948,8 @@ snapshots: dependencies: vue: 3.5.20(typescript@5.9.2) + lucide@0.542.0: {} + luxon@3.7.1: {} lz-string@1.5.0: {} diff --git a/shared/src/css/layout.css b/shared/src/css/layout.css index df9e5dee71..5c2d521f30 100644 --- a/shared/src/css/layout.css +++ b/shared/src/css/layout.css @@ -86,7 +86,8 @@ body { overflow: hidden; } -.page main { +.page main, +:host main { flex: auto; display: flex; gap: 10px; @@ -192,7 +193,7 @@ a[aria-current="page"] { max-height: 100%; } -.toolbar .viz { +.viz { font-size: 13px; --viz-font: SF Mono, Menlo, monospace; padding-left: 14px; @@ -201,13 +202,13 @@ a[aria-current="page"] { border-bottom: solid 1px #d7e0da; } -.toolbar .viz summary { +.viz summary { margin-bottom: 24; font-family: var(--viz-font); font-weight: bold; } -.toolbar .viz summary + div > * { +.viz summary + div > * { font-family: var(--viz-font); }