From 3ce01147ae0f0357ef9ede03a12bd97ddc2cb9c5 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Tue, 2 Sep 2025 21:00:01 +0300 Subject: [PATCH 01/56] feat: copy solid package to lit --- packages/frameworks/lit/CHANGELOG.md | 2302 +++++++++++++++++ packages/frameworks/lit/package.json | 54 + packages/frameworks/lit/src/bindable.ts | 67 + packages/frameworks/lit/src/index.ts | 4 + packages/frameworks/lit/src/machine.ts | 306 +++ packages/frameworks/lit/src/merge-props.ts | 53 + .../frameworks/lit/src/normalize-props.ts | 82 + packages/frameworks/lit/src/refs.ts | 11 + packages/frameworks/lit/src/track.ts | 30 + .../frameworks/lit/tests/merge-props.test.ts | 151 ++ packages/frameworks/lit/tsconfig.json | 7 + packages/frameworks/lit/vite.config.ts | 11 + packages/frameworks/lit/vitest.setup.ts | 9 + 13 files changed, 3087 insertions(+) create mode 100644 packages/frameworks/lit/CHANGELOG.md create mode 100644 packages/frameworks/lit/package.json create mode 100644 packages/frameworks/lit/src/bindable.ts create mode 100644 packages/frameworks/lit/src/index.ts create mode 100644 packages/frameworks/lit/src/machine.ts create mode 100644 packages/frameworks/lit/src/merge-props.ts create mode 100644 packages/frameworks/lit/src/normalize-props.ts create mode 100644 packages/frameworks/lit/src/refs.ts create mode 100644 packages/frameworks/lit/src/track.ts create mode 100644 packages/frameworks/lit/tests/merge-props.test.ts create mode 100644 packages/frameworks/lit/tsconfig.json create mode 100644 packages/frameworks/lit/vite.config.ts create mode 100644 packages/frameworks/lit/vitest.setup.ts 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..e081af01b5 --- /dev/null +++ b/packages/frameworks/lit/package.json @@ -0,0 +1,54 @@ +{ + "name": "@zag-js/solid", + "version": "1.22.1", + "description": "The solid.js wrapper for zag", + "keywords": [ + "ui-machines", + "state-machines", + "zag", + "solid", + "use-machine", + "hook" + ], + "author": "Segun Adebayo ", + "homepage": "https://github.com/chakra-ui/zag#readme", + "license": "MIT", + "repository": "https://github.com/chakra-ui/zag/tree/main/packages/frameworks/solid", + "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", + "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" + }, + "scripts": { + "build": "tsup", + "lint": "eslint src", + "typecheck": "tsc --noEmit", + "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..9792d455eb --- /dev/null +++ b/packages/frameworks/lit/src/bindable.ts @@ -0,0 +1,67 @@ +import type { Bindable, BindableParams } from "@zag-js/core" +import { isFunction } from "@zag-js/utils" +import { createEffect, createMemo, createSignal, type Accessor, onCleanup } from "solid-js" + +export function createBindable(props: Accessor>): Bindable { + const initial = props().value ?? props().defaultValue + + const eq = props().isEqual ?? Object.is + + const [value, setValue] = createSignal(initial as T) + const controlled = createMemo(() => props().value != undefined) + + const valueRef = { current: value() } + const prevValue: Record<"current", T | undefined> = { current: undefined } + + createEffect(() => { + const v = controlled() ? props().value : value() + prevValue.current = v + valueRef.current = v as T + }) + + const set = (v: T | ((prev: T) => T)) => { + const prev = prevValue.current + const next = isFunction(v) ? v(valueRef.current as T) : v + + if (props().debug) { + console.log(`[bindable > ${props().debug}] setValue`, { next, prev }) + } + + if (!controlled()) setValue(next as any) + if (!eq(next, prev)) { + props().onChange?.(next, prev) + } + } + + function get(): T { + const v = (controlled() ? props().value : value) as T + return isFunction(v) ? v() : v + } + + return { + initial, + ref: valueRef, + get, + set, + invoke(nextValue: T, prevValue: T) { + props().onChange?.(nextValue, prevValue) + }, + hash(value: T) { + return props().hash?.(value) ?? String(value) + }, + } +} + +createBindable.cleanup = (fn: VoidFunction) => { + onCleanup(() => fn()) +} + +createBindable.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..9dba37aef8 --- /dev/null +++ b/packages/frameworks/lit/src/index.ts @@ -0,0 +1,4 @@ +export { Key } from "@solid-primitives/keyed" +export * from "./machine" +export { mergeProps } from "./merge-props" +export * from "./normalize-props" diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts new file mode 100644 index 0000000000..10bacf6533 --- /dev/null +++ b/packages/frameworks/lit/src/machine.ts @@ -0,0 +1,306 @@ +import type { + ActionsOrFn, + GuardFn, + Machine, + MachineSchema, + Service, + ChooseFn, + ComputedFn, + EffectsOrFn, + BindableContext, + Params, +} from "@zag-js/core" +import { createScope, INIT_STATE, MachineStatus } from "@zag-js/core" +import { compact, isFunction, isString, toArray, warn, ensure } from "@zag-js/utils" +import { type Accessor, createMemo, mergeProps, onCleanup, onMount } from "solid-js" +import { createBindable } from "./bindable" +import { createRefs } from "./refs" +import { createTrack } from "./track" + +export function useMachine( + machine: Machine, + userProps: Partial | Accessor> = {}, +): Service { + const scope = createMemo(() => { + const { id, ids, getRootNode } = access(userProps) as any + return createScope({ id, ids, getRootNode }) + }) + + const debug = (...args: any[]) => { + if (machine.debug) console.log(...args) + } + + const props = createMemo( + () => + machine.props?.({ + props: compact(access(userProps)), + scope: scope(), + }) ?? access(userProps), + ) + + const prop: any = createProp(props) + + const context: any = machine.context?.({ + prop, + bindable: createBindable, + get scope() { + return scope() + }, + flush, + getContext() { + return ctx as any + }, + getComputed() { + return computed as any + }, + getRefs() { + return refs as any + }, + getEvent() { + return getEvent() + }, + }) + + const ctx: BindableContext = { + 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) + }, + } + + const effects = { current: new Map() } + const transitionRef: { current: any } = { current: null } + + const previousEventRef: { current: any } = { current: null } + const eventRef: { current: any } = { current: { type: "" } } + + const getEvent = (): any => + mergeProps(eventRef.current, { + current() { + return eventRef.current + }, + previous() { + return previousEventRef.current + }, + }) + + const getState = () => + mergeProps(state, { + matches(...values: T["state"][]) { + const current = state.get() + return values.includes(current) + }, + hasTag(tag: T["tag"]) { + const current = state.get() + return !!machine.states[current as T["state"]]?.tags?.includes(tag) + }, + }) + + const refs = createRefs(machine.refs?.({ prop, context: ctx }) ?? {}) + + const getParams = (): Params => ({ + state: getState(), + context: ctx, + event: getEvent(), + prop, + send, + action, + guard, + track: createTrack, + refs, + computed, + flush, + get scope() { + return scope() + }, + choose, + }) + + const action = (keys: ActionsOrFn | undefined) => { + const strs = isFunction(keys) ? keys(getParams()) : keys + if (!strs) return + const fns = strs.map((s) => { + const fn = 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?.(getParams()) + } + } + + const guard = (str: T["guard"] | GuardFn) => { + if (isFunction(str)) return str(getParams()) + return machine.implementations?.guards?.[str](getParams()) + } + + const effect = (keys: EffectsOrFn | undefined) => { + const strs = isFunction(keys) ? keys(getParams()) : keys + if (!strs) return + const fns = strs.map((s) => { + const fn = 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?.(getParams()) + if (cleanup) cleanups.push(cleanup) + } + return () => cleanups.forEach((fn) => fn?.()) + } + + const choose: ChooseFn = (transitions) => { + return toArray(transitions).find((t) => { + let result = !t.guard + if (isString(t.guard)) result = !!guard(t.guard) + else if (isFunction(t.guard)) result = t.guard(getParams()) + return result + }) + } + + const computed: ComputedFn = (key) => { + ensure(machine.computed, () => `[zag-js] No computed object found on machine`) + const fn = machine.computed[key] + return fn({ + context: ctx, + event: eventRef.current, + prop, + refs, + scope: scope(), + computed: computed, + }) + } + + const state = createBindable(() => ({ + defaultValue: machine.initialState({ prop }), + onChange(nextState, prevState) { + // compute effects: exit -> transition -> enter + + // exit effects + if (prevState) { + const exitEffects = effects.current.get(prevState) + exitEffects?.() + effects.current.delete(prevState) + } + + // exit actions + if (prevState) { + action(machine.states[prevState]?.exit) + } + + // transition actions + action(transitionRef.current?.actions) + + // enter effect + const cleanup = effect(machine.states[nextState]?.effects) + if (cleanup) effects.current.set(nextState as string, cleanup) + + // root entry actions + if (prevState === INIT_STATE) { + action(machine.entry) + const cleanup = effect(machine.effects) + if (cleanup) effects.current.set(INIT_STATE, cleanup) + } + + // enter actions + action(machine.states[nextState]?.entry) + }, + })) + + let status = MachineStatus.NotStarted + + onMount(() => { + const started = status === MachineStatus.Started + status = MachineStatus.Started + debug(started ? "rehydrating..." : "initializing...") + state.invoke(state.initial!, INIT_STATE) + }) + + onCleanup(() => { + debug("unmounting...") + status = MachineStatus.Stopped + + const fns = effects.current + fns.forEach((fn) => fn?.()) + effects.current = new Map() + transitionRef.current = null + + action(machine.exit) + }) + + const send = (event: any) => { + if (status !== MachineStatus.Started) return + + previousEventRef.current = eventRef.current + eventRef.current = event + + let currentState = state.get() + + const transitions = + // @ts-ignore + machine.states[currentState].on?.[event.type] ?? + // @ts-ignore + machine.on?.[event.type] + + const transition = choose(transitions) + if (!transition) return + + // save current transition + transitionRef.current = transition + const target = transition.target ?? currentState + + debug("transition", event.type, transition.target || currentState, `(${transition.actions})`) + + const changed = target !== currentState + if (changed) { + // state change is high priority + state.set(target) + } else if (transition.reenter && !changed) { + // reenter will re-invoke the current state + state.invoke(currentState, currentState) + } else { + // call transition actions + action(transition.actions) + } + } + + machine.watch?.(getParams()) + + return { + state: getState(), + send, + context: ctx, + prop, + get scope() { + return scope() + }, + refs, + computed, + event: getEvent(), + getStatus: () => status, + } as unknown as Service +} + +function flush(fn: VoidFunction) { + fn() +} + +function access(value: T | Accessor) { + return isFunction(value) ? value() : value +} + +function createProp(value: Accessor) { + return function get(key: K): T[K] { + return value()[key] + } +} diff --git a/packages/frameworks/lit/src/merge-props.ts b/packages/frameworks/lit/src/merge-props.ts new file mode 100644 index 0000000000..5c4f4696d0 --- /dev/null +++ b/packages/frameworks/lit/src/merge-props.ts @@ -0,0 +1,53 @@ +import { mergeProps as zagMergeProps } from "@zag-js/core" + +export type MaybeAccessor = T | (() => T) + +export function mergeProps(source: MaybeAccessor): T +export function mergeProps(source: MaybeAccessor, source1: MaybeAccessor): T & U +export function mergeProps( + source: MaybeAccessor, + source1: MaybeAccessor, + source2: MaybeAccessor, +): T & U & V +export function mergeProps( + source: MaybeAccessor, + source1: MaybeAccessor, + source2: MaybeAccessor, + source3: MaybeAccessor, +): T & U & V & W +export function mergeProps(...sources: any[]) { + const target = {} + for (let i = 0; i < sources.length; i++) { + let source = sources[i] + if (typeof source === "function") source = source() + if (source) { + const descriptors = Object.getOwnPropertyDescriptors(source) + for (const key in descriptors) { + if (key in target) continue + Object.defineProperty(target, key, { + enumerable: true, + get() { + let e = {} + if (key === "style" || key === "class" || key === "className" || key.startsWith("on")) { + for (let i = 0; i < sources.length; i++) { + let s = sources[i] + if (typeof s === "function") s = s() + e = zagMergeProps(e, { [key]: (s || {})[key] }) + } + + return (e as any)[key] + } + for (let i = sources.length - 1; i >= 0; i--) { + let v, + s = sources[i] + if (typeof s === "function") s = s() + v = (s || {})[key] + if (v !== undefined) return v + } + }, + }) + } + } + } + return target +} diff --git a/packages/frameworks/lit/src/normalize-props.ts b/packages/frameworks/lit/src/normalize-props.ts new file mode 100644 index 0000000000..0863723b04 --- /dev/null +++ b/packages/frameworks/lit/src/normalize-props.ts @@ -0,0 +1,82 @@ +import { createNormalizer } from "@zag-js/types" +import { isNumber, isObject, isString } from "@zag-js/utils" +import type { JSX } from "solid-js" + +export type PropTypes = JSX.IntrinsicElements & { + element: JSX.HTMLAttributes + style: JSX.CSSProperties +} + +const eventMap: Record = { + onFocus: "onFocusIn", + onBlur: "onFocusOut", + onDoubleClick: "onDblClick", + onChange: "onInput", + defaultChecked: "checked", + defaultValue: "value", + htmlFor: "for", + className: "class", +} + +const format = (v: string) => (v.startsWith("--") ? v : hyphenateStyleName(v)) + +type StyleObject = Record + +function toSolidProp(prop: string) { + return prop in eventMap ? eventMap[prop] : prop +} + +type Dict = Record + +export const normalizeProps = createNormalizer((props: Dict) => { + const normalized: Dict = {} + + for (const key in props) { + const value = props[key] + + if (key === "readOnly" && value === false) { + continue + } + + if (key === "style" && isObject(value)) { + normalized["style"] = cssify(value) + continue + } + + if (key === "children") { + if (isString(value)) { + normalized["textContent"] = value + } + continue + } + + normalized[toSolidProp(key)] = value + } + return normalized +}) + +function cssify(style: StyleObject): StyleObject { + let css = {} as StyleObject + for (const property in style) { + const value = style[property] + if (!isString(value) && !isNumber(value)) continue + css[format(property)] = value + } + + return css +} + +const uppercasePattern = /[A-Z]/g +const msPattern = /^ms-/ + +function toHyphenLower(match: string) { + return "-" + match.toLowerCase() +} + +const cache: Record = {} + +function hyphenateStyleName(name: string) { + if (cache.hasOwnProperty(name)) return cache[name] + var hName = name.replace(uppercasePattern, toHyphenLower) + return (cache[name] = msPattern.test(hName) ? "-" + hName : hName) +} 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/tests/merge-props.test.ts b/packages/frameworks/lit/tests/merge-props.test.ts new file mode 100644 index 0000000000..c19f5de93d --- /dev/null +++ b/packages/frameworks/lit/tests/merge-props.test.ts @@ -0,0 +1,151 @@ +import { createComputed, createRoot, createSignal, mergeProps as _mergeProps } from "solid-js" +import { mergeProps } from "../src" + +describe("mergeProps", () => { + it("handles one argument", () => + createRoot((dispose) => { + 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) + + dispose() + })) + + it("combines handlers", async () => { + createRoot(async (dispose) => { + let count = 0 + + const mockFn = vi.fn(() => { + count++ + }) + + const props = mergeProps({ onClick: mockFn }, { onClick: mockFn }, { onClick: mockFn }) + + props.onClick() + expect(mockFn).toBeCalledTimes(3) + expect(count).toBe(3) + + dispose() + }) + }) + + it("combines css classes", async () => { + createRoot(async (dispose) => { + 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") + + dispose() + }) + }) + + it("combines styles", () => + createRoot((dispose) => { + 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", + } + `) + + dispose() + })) + + it("accepts function sources", () => { + createRoot(() => { + const [signal, setSignal] = createSignal({ + class: "primary", + style: { + margin: "10px", + }, + }) + + const props = mergeProps( + signal, + { class: "secondary" }, + { + style: { padding: "10px" }, + }, + ) + + let i = 0 + + createComputed(() => { + if (i === 0) { + expect(props.class).toBe("primary secondary") + expect(props.style).toEqual({ margin: "10px", padding: "10px" }) + i++ + } else { + expect(props.class).toBe("tertiary secondary") + expect(props.style).toEqual({ padding: "10px" }) + expect(props.foo).toEqual("bar") + } + }) + + setSignal({ class: "tertiary", foo: "bar" }) + }) + }) + + it("last value overwrites the event-listeners", async () => { + createRoot(async (dispose) => { + const mockFn = vi.fn() + const message1 = "click1" + const message2 = "click2" + + const props = mergeProps( + { onEvent: () => mockFn(message1) }, + { onEvent: () => mockFn(message2) }, + { onEvent: "overwrites" }, + ) + + expect(props.onEvent).toBe("overwrites") + + dispose() + }) + }) + + it("works with mergeProps", () => { + const cb1 = vi.fn() + const cb2 = vi.fn() + const combined = mergeProps({ onClick: cb1 }, { onClick: cb2 }) + const merged = _mergeProps(combined) + + merged.onClick("foo") + + expect(cb1).toHaveBeenCalledTimes(1) + expect(cb1).toBeCalledWith("foo") + expect(cb2).toHaveBeenCalledTimes(1) + expect(cb2).toBeCalledWith("foo") + }) +}) 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 }) From ed2a9e15403541dfee31cadc14192d735f9f1dd0 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Tue, 2 Sep 2025 23:38:32 +0300 Subject: [PATCH 02/56] feat: initial claude created lit package --- package.json | 3 +- packages/core/src/merge-props.ts | 60 +- packages/frameworks/lit/package.json | 17 +- packages/frameworks/lit/src/bindable.ts | 65 +-- packages/frameworks/lit/src/index.ts | 1 - packages/frameworks/lit/src/machine.ts | 548 ++++++++++-------- packages/frameworks/lit/src/merge-props.ts | 55 +- .../frameworks/lit/src/normalize-props.ts | 107 ++-- packages/frameworks/lit/test-toggle.html | 57 ++ packages/frameworks/lit/tests/machine.test.ts | 421 ++++++++++++++ .../frameworks/lit/tests/merge-props.test.ts | 191 +++--- pnpm-lock.yaml | 82 +++ 12 files changed, 1074 insertions(+), 533 deletions(-) create mode 100644 packages/frameworks/lit/test-toggle.html create mode 100644 packages/frameworks/lit/tests/machine.test.ts diff --git a/package.json b/package.json index 26afdea8b1..128dbd60b2 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,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/package.json b/packages/frameworks/lit/package.json index e081af01b5..6de9d34f38 100644 --- a/packages/frameworks/lit/package.json +++ b/packages/frameworks/lit/package.json @@ -1,19 +1,19 @@ { - "name": "@zag-js/solid", + "name": "@zag-js/lit", "version": "1.22.1", - "description": "The solid.js wrapper for zag", + "description": "The lit wrapper for zag", "keywords": [ "ui-machines", "state-machines", "zag", - "solid", - "use-machine", - "hook" + "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/solid", + "repository": "https://github.com/chakra-ui/zag/tree/main/packages/frameworks/lit", "sideEffects": false, "files": [ "dist" @@ -34,18 +34,21 @@ "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" + "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" }, diff --git a/packages/frameworks/lit/src/bindable.ts b/packages/frameworks/lit/src/bindable.ts index 9792d455eb..34347acefc 100644 --- a/packages/frameworks/lit/src/bindable.ts +++ b/packages/frameworks/lit/src/bindable.ts @@ -1,48 +1,39 @@ import type { Bindable, BindableParams } from "@zag-js/core" +import { proxy } from "@zag-js/store" import { isFunction } from "@zag-js/utils" -import { createEffect, createMemo, createSignal, type Accessor, onCleanup } from "solid-js" -export function createBindable(props: Accessor>): Bindable { +export function bindable(props: () => BindableParams): Bindable { const initial = props().value ?? props().defaultValue - const eq = props().isEqual ?? Object.is - - const [value, setValue] = createSignal(initial as T) - const controlled = createMemo(() => props().value != undefined) - - const valueRef = { current: value() } - const prevValue: Record<"current", T | undefined> = { current: undefined } - - createEffect(() => { - const v = controlled() ? props().value : value() - prevValue.current = v - valueRef.current = v as T - }) - - const set = (v: T | ((prev: T) => T)) => { - const prev = prevValue.current - const next = isFunction(v) ? v(valueRef.current as T) : v + if (props().debug) { + console.log(`[bindable > ${props().debug}] initial`, initial) + } - if (props().debug) { - console.log(`[bindable > ${props().debug}] setValue`, { next, prev }) - } + const eq = props().isEqual ?? Object.is - if (!controlled()) setValue(next as any) - if (!eq(next, prev)) { - props().onChange?.(next, prev) - } - } + const store = proxy({ value: initial as T }) - function get(): T { - const v = (controlled() ? props().value : value) as T - return isFunction(v) ? v() : v - } + const controlled = () => props().value !== undefined return { initial, - ref: valueRef, - get, - set, + 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) }, @@ -52,11 +43,11 @@ export function createBindable(props: Accessor>): Bindable< } } -createBindable.cleanup = (fn: VoidFunction) => { - onCleanup(() => fn()) +bindable.cleanup = (_fn: VoidFunction) => { + // No-op in vanilla implementation } -createBindable.ref = (defaultValue: T) => { +bindable.ref = (defaultValue: T) => { let value = defaultValue return { get: () => value, diff --git a/packages/frameworks/lit/src/index.ts b/packages/frameworks/lit/src/index.ts index 9dba37aef8..64d783f0c3 100644 --- a/packages/frameworks/lit/src/index.ts +++ b/packages/frameworks/lit/src/index.ts @@ -1,4 +1,3 @@ -export { Key } from "@solid-primitives/keyed" export * from "./machine" export { mergeProps } from "./merge-props" export * from "./normalize-props" diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts index 10bacf6533..f8687bc600 100644 --- a/packages/frameworks/lit/src/machine.ts +++ b/packages/frameworks/lit/src/machine.ts @@ -1,306 +1,390 @@ import type { ActionsOrFn, - GuardFn, - Machine, - MachineSchema, - Service, + Bindable, + BindableContext, + BindableRefs, ChooseFn, ComputedFn, EffectsOrFn, - BindableContext, + GuardFn, + Machine, + MachineSchema, Params, + PropFn, + Scope, + Service, } from "@zag-js/core" import { createScope, INIT_STATE, MachineStatus } from "@zag-js/core" -import { compact, isFunction, isString, toArray, warn, ensure } from "@zag-js/utils" -import { type Accessor, createMemo, mergeProps, onCleanup, onMount } from "solid-js" -import { createBindable } from "./bindable" +import { subscribe } from "@zag-js/store" +import { compact, identity, isEqual, isFunction, isString, runIfFn, toArray, warn } from "@zag-js/utils" +import type { ReactiveController, ReactiveControllerHost } from "lit" +import { bindable } from "./bindable" import { createRefs } from "./refs" -import { createTrack } from "./track" - -export function useMachine( - machine: Machine, - userProps: Partial | Accessor> = {}, -): Service { - const scope = createMemo(() => { - const { id, ids, getRootNode } = access(userProps) as any - return createScope({ id, ids, getRootNode }) - }) - const debug = (...args: any[]) => { - if (machine.debug) console.log(...args) - } +export class LitMachine { + scope: Scope + ctx: BindableContext + prop: PropFn + state: Bindable + refs: BindableRefs + computed: ComputedFn - const props = createMemo( - () => - machine.props?.({ - props: compact(access(userProps)), - scope: scope(), - }) ?? access(userProps), - ) - - const prop: any = createProp(props) - - const context: any = machine.context?.({ - prop, - bindable: createBindable, - get scope() { - return scope() - }, - flush, - getContext() { - return ctx as any - }, - getComputed() { - return computed as any - }, - getRefs() { - return refs as any - }, - getEvent() { - return getEvent() - }, + private event: any = { type: "" } + private previousEvent: any + + private effects = new Map() + private transition: any = null + + private cleanups: VoidFunction[] = [] + private subscriptions: Array<(service: Service) => void> = [] + + private getEvent = () => ({ + ...this.event, + current: () => this.event, + previous: () => this.previousEvent, }) - const ctx: BindableContext = { - 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) - }, + private 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), + }) + + debug = (...args: any[]) => { + if (this.machine.debug) console.log(...args) } - const effects = { current: new Map() } - const transitionRef: { current: any } = { current: null } + notify = () => { + this.publish() + } - const previousEventRef: { current: any } = { current: null } - const eventRef: { current: any } = { current: { type: "" } } + constructor( + private machine: Machine, + userProps: Partial | (() => Partial) = {}, + ) { + // create scope + const { id, ids, getRootNode } = runIfFn(userProps) as any + this.scope = createScope({ id, ids, getRootNode }) + + // create prop + const prop: PropFn = (key) => { + const __props = runIfFn(userProps) + const props: any = machine.props?.({ props: compact(__props), scope: this.scope }) ?? __props + return props[key] as any + } + this.prop = prop - const getEvent = (): any => - mergeProps(eventRef.current, { - current() { - return eventRef.current + // create context + const context: any = machine.context?.({ + prop, + bindable, + scope: this.scope, + flush(fn: VoidFunction) { + queueMicrotask(fn) }, - previous() { - return previousEventRef.current + getContext() { + return ctx as any }, + getComputed() { + return computed as any + }, + getRefs() { + return refs as any + }, + getEvent: this.getEvent.bind(this), }) - const getState = () => - mergeProps(state, { - matches(...values: T["state"][]) { - const current = state.get() - return values.includes(current) + // subscribe to context changes + if (context) { + Object.values(context).forEach((item: any) => { + const unsub = subscribe(item.ref, () => this.notify()) + this.cleanups.push(unsub) + }) + } + + // context function + const ctx: BindableContext = { + get(key) { + return context?.[key].get() }, - hasTag(tag: T["tag"]) { - const current = state.get() - return !!machine.states[current as T["state"]]?.tags?.includes(tag) + 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.ctx = ctx + + const computed: ComputedFn = (key) => { + return ( + machine.computed?.[key]({ + context: ctx as any, + event: this.getEvent(), + prop, + refs: this.refs, + scope: this.scope, + computed: computed as any, + }) ?? ({} as any) + ) + } + this.computed = computed + + const refs: BindableRefs = createRefs(machine.refs?.({ prop, context: ctx }) ?? {}) + this.refs = refs + + // state + const state = bindable(() => ({ + defaultValue: machine.initialState({ prop }), + onChange: (nextState, prevState) => { + // compute effects: exit -> transition -> enter + + // exit effects + if (prevState) { + const exitEffects = this.effects.get(prevState) + exitEffects?.() + this.effects.delete(prevState) + } + + // exit actions + if (prevState) { + // @ts-ignore + this.action(machine.states[prevState]?.exit) + } + + // transition actions + this.action(this.transition?.actions) + + // enter effect + // @ts-ignore + const cleanup = this.effect(machine.states[nextState]?.effects) + if (cleanup) this.effects.set(nextState as string, cleanup) + + // root entry actions + if (prevState === INIT_STATE) { + this.action(machine.entry) + const cleanup = this.effect(machine.effects) + if (cleanup) this.effects.set(INIT_STATE, cleanup) + } + + // enter actions + // @ts-ignore + this.action(machine.states[nextState]?.entry) + }, + })) + this.state = state + this.cleanups.push(subscribe(this.state.ref, () => this.notify())) + } - const refs = createRefs(machine.refs?.({ prop, context: ctx }) ?? {}) - - const getParams = (): Params => ({ - state: getState(), - context: ctx, - event: getEvent(), - prop, - send, - action, - guard, - track: createTrack, - refs, - computed, - flush, - get scope() { - return scope() - }, - choose, - }) + send = (event: any) => { + if (this.status !== MachineStatus.Started) return + + queueMicrotask(() => { + this.previousEvent = this.event + this.event = event + + this.debug("send", event) + + let currentState = this.state.get() - const action = (keys: ActionsOrFn | undefined) => { - const strs = isFunction(keys) ? keys(getParams()) : keys + 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 action = (keys: ActionsOrFn | undefined) => { + const strs = isFunction(keys) ? keys(this.getParams()) : keys if (!strs) return const fns = strs.map((s) => { - const fn = machine.implementations?.actions?.[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?.(getParams()) + fn?.(this.getParams()) } } - const guard = (str: T["guard"] | GuardFn) => { - if (isFunction(str)) return str(getParams()) - return machine.implementations?.guards?.[str](getParams()) + private guard = (str: T["guard"] | GuardFn) => { + if (isFunction(str)) return str(this.getParams()) + return this.machine.implementations?.guards?.[str](this.getParams()) } - const effect = (keys: EffectsOrFn | undefined) => { - const strs = isFunction(keys) ? keys(getParams()) : keys + private effect = (keys: EffectsOrFn | undefined) => { + const strs = isFunction(keys) ? keys(this.getParams()) : keys if (!strs) return const fns = strs.map((s) => { - const fn = machine.implementations?.effects?.[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?.(getParams()) + const cleanup = fn?.(this.getParams()) if (cleanup) cleanups.push(cleanup) } return () => cleanups.forEach((fn) => fn?.()) } - const choose: ChooseFn = (transitions) => { - return toArray(transitions).find((t) => { + private choose: ChooseFn = (transitions) => { + return toArray(transitions).find((t: any) => { let result = !t.guard - if (isString(t.guard)) result = !!guard(t.guard) - else if (isFunction(t.guard)) result = t.guard(getParams()) + if (isString(t.guard)) result = !!this.guard(t.guard) + else if (isFunction(t.guard)) result = t.guard(this.getParams()) return result }) } - const computed: ComputedFn = (key) => { - ensure(machine.computed, () => `[zag-js] No computed object found on machine`) - const fn = machine.computed[key] - return fn({ - context: ctx, - event: eventRef.current, - prop, - refs, - scope: scope(), - computed: computed, - }) + start() { + this.status = MachineStatus.Started + this.debug("initializing...") + this.state.invoke(this.state.initial!, INIT_STATE) + this.setupTrackers() } - const state = createBindable(() => ({ - defaultValue: machine.initialState({ prop }), - onChange(nextState, prevState) { - // compute effects: exit -> transition -> enter + stop() { + // run exit effects + this.effects.forEach((fn) => fn?.()) + this.effects.clear() + this.transition = null + this.action(this.machine.exit) - // exit effects - if (prevState) { - const exitEffects = effects.current.get(prevState) - exitEffects?.() - effects.current.delete(prevState) - } + // unsubscribe from all subscriptions + this.cleanups.forEach((unsub) => unsub()) + this.cleanups = [] - // exit actions - if (prevState) { - action(machine.states[prevState]?.exit) - } - - // transition actions - action(transitionRef.current?.actions) - - // enter effect - const cleanup = effect(machine.states[nextState]?.effects) - if (cleanup) effects.current.set(nextState as string, cleanup) + this.status = MachineStatus.Stopped + this.debug("unmounting...") + } - // root entry actions - if (prevState === INIT_STATE) { - action(machine.entry) - const cleanup = effect(machine.effects) - if (cleanup) effects.current.set(INIT_STATE, cleanup) - } + subscribe = (fn: (service: Service) => void) => { + this.subscriptions.push(fn) + } - // enter actions - action(machine.states[nextState]?.entry) - }, - })) + private status = MachineStatus.NotStarted + + 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, + } + } - let status = MachineStatus.NotStarted + private publish = () => { + this.callTrackers() + this.subscriptions.forEach((fn) => fn(this.service)) + } - onMount(() => { - const started = status === MachineStatus.Started - status = MachineStatus.Started - debug(started ? "rehydrating..." : "initializing...") - state.invoke(state.initial!, INIT_STATE) - }) + private trackers: { deps: any[]; fn: any }[] = [] - onCleanup(() => { - debug("unmounting...") - status = MachineStatus.Stopped + private setupTrackers = () => { + this.machine.watch?.(this.getParams()) + } - const fns = effects.current - fns.forEach((fn) => fn?.()) - effects.current = new Map() - transitionRef.current = null + private callTrackers = () => { + this.trackers.forEach(({ deps, fn }) => { + const next = deps.map((dep) => dep()) + if (!isEqual(fn.prev, next)) { + fn() + fn.prev = next + } + }) + } - action(machine.exit) + 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, + scope: this.scope, + choose: this.choose, }) +} - const send = (event: any) => { - if (status !== MachineStatus.Started) return - - previousEventRef.current = eventRef.current - eventRef.current = event - - let currentState = state.get() - - const transitions = - // @ts-ignore - machine.states[currentState].on?.[event.type] ?? - // @ts-ignore - machine.on?.[event.type] - - const transition = choose(transitions) - if (!transition) return - - // save current transition - transitionRef.current = transition - const target = transition.target ?? currentState - - debug("transition", event.type, transition.target || currentState, `(${transition.actions})`) - - const changed = target !== currentState - if (changed) { - // state change is high priority - state.set(target) - } else if (transition.reenter && !changed) { - // reenter will re-invoke the current state - state.invoke(currentState, currentState) - } else { - // call transition actions - action(transition.actions) - } +export class ZagController implements ReactiveController { + private machine: LitMachine + public api: any // Will be set in constructor and updated on changes + + constructor( + private host: ReactiveControllerHost, + machineConfig: Machine, + props: Partial = {}, + ) { + this.machine = new LitMachine(machineConfig, props) + host.addController(this) } - machine.watch?.(getParams()) + hostConnected() { + this.machine.subscribe(() => { + // Update API when machine state changes + this.updateApi() + // Request Lit component update + this.host.requestUpdate() + }) + this.machine.start() + this.updateApi() + // Trigger initial update to sync with host element + this.host.requestUpdate() + } - return { - state: getState(), - send, - context: ctx, - prop, - get scope() { - return scope() - }, - refs, - computed, - event: getEvent(), - getStatus: () => status, - } as unknown as Service -} + hostDisconnected() { + this.machine.stop() + } -function flush(fn: VoidFunction) { - fn() -} + private updateApi() { + // This will be implemented once we have the connect function + // For now, just expose the service + this.api = this.machine.service + } -function access(value: T | Accessor) { - return isFunction(value) ? value() : value -} + // Expose machine methods for advanced usage + get service() { + return this.machine.service + } -function createProp(value: Accessor) { - return function get(key: K): T[K] { - return value()[key] + send = (event: any) => { + this.machine.send(event) } } diff --git a/packages/frameworks/lit/src/merge-props.ts b/packages/frameworks/lit/src/merge-props.ts index 5c4f4696d0..a761aad5fe 100644 --- a/packages/frameworks/lit/src/merge-props.ts +++ b/packages/frameworks/lit/src/merge-props.ts @@ -1,53 +1,4 @@ -import { mergeProps as zagMergeProps } from "@zag-js/core" +import { createMergeProps } from "@zag-js/core" -export type MaybeAccessor = T | (() => T) - -export function mergeProps(source: MaybeAccessor): T -export function mergeProps(source: MaybeAccessor, source1: MaybeAccessor): T & U -export function mergeProps( - source: MaybeAccessor, - source1: MaybeAccessor, - source2: MaybeAccessor, -): T & U & V -export function mergeProps( - source: MaybeAccessor, - source1: MaybeAccessor, - source2: MaybeAccessor, - source3: MaybeAccessor, -): T & U & V & W -export function mergeProps(...sources: any[]) { - const target = {} - for (let i = 0; i < sources.length; i++) { - let source = sources[i] - if (typeof source === "function") source = source() - if (source) { - const descriptors = Object.getOwnPropertyDescriptors(source) - for (const key in descriptors) { - if (key in target) continue - Object.defineProperty(target, key, { - enumerable: true, - get() { - let e = {} - if (key === "style" || key === "class" || key === "className" || key.startsWith("on")) { - for (let i = 0; i < sources.length; i++) { - let s = sources[i] - if (typeof s === "function") s = s() - e = zagMergeProps(e, { [key]: (s || {})[key] }) - } - - return (e as any)[key] - } - for (let i = sources.length - 1; i >= 0; i--) { - let v, - s = sources[i] - if (typeof s === "function") s = s() - v = (s || {})[key] - if (v !== undefined) return v - } - }, - }) - } - } - } - return target -} +// 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 index 0863723b04..bb80ce9cb0 100644 --- a/packages/frameworks/lit/src/normalize-props.ts +++ b/packages/frameworks/lit/src/normalize-props.ts @@ -1,29 +1,32 @@ import { createNormalizer } from "@zag-js/types" -import { isNumber, isObject, isString } from "@zag-js/utils" -import type { JSX } from "solid-js" -export type PropTypes = JSX.IntrinsicElements & { - element: JSX.HTMLAttributes - style: JSX.CSSProperties -} +// 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 -const eventMap: Record = { - onFocus: "onFocusIn", - onBlur: "onFocusOut", - onDoubleClick: "onDblClick", - onChange: "onInput", - defaultChecked: "checked", - defaultValue: "value", - htmlFor: "for", - className: "class", +type LitElementProps = { + [key: string]: any + style?: string | Record } -const format = (v: string) => (v.startsWith("--") ? v : hyphenateStyleName(v)) - -type StyleObject = Record - -function toSolidProp(prop: string) { - return prop in eventMap ? eventMap[prop] : prop +type RequiredElements = + | "button" + | "label" + | "input" + | "textarea" + | "img" + | "output" + | "select" + | "rect" + | "circle" + | "svg" + | "path" + +export type PropTypes = Record & { + element: LitElementProps + style: Record } type Dict = Record @@ -34,49 +37,45 @@ export const normalizeProps = createNormalizer((props: Dict) => { for (const key in props) { const value = props[key] - if (key === "readOnly" && value === false) { + // Skip undefined values + if (value === undefined) { continue } - if (key === "style" && isObject(value)) { - normalized["style"] = cssify(value) + // Handle event handlers - prefix with @ + if (key.startsWith("on") && typeof value === "function") { + const eventName = key.slice(2).toLowerCase() + normalized[`@${eventName}`] = value continue } - if (key === "children") { - if (isString(value)) { - normalized["textContent"] = value - } + // Handle boolean attributes - prefix with ? + if (typeof value === "boolean") { + normalized[`?${key}`] = value continue } - normalized[toSolidProp(key)] = value - } - return normalized -}) - -function cssify(style: StyleObject): StyleObject { - let css = {} as StyleObject - for (const property in style) { - const value = style[property] - if (!isString(value) && !isNumber(value)) continue - css[format(property)] = value - } - - return css -} + // Handle properties that should be set as properties not attributes + if (key === "value" || key === "checked" || key === "selected") { + normalized[`.${key}`] = value + continue + } -const uppercasePattern = /[A-Z]/g -const msPattern = /^ms-/ + // Handle className -> class mapping + if (key === "className") { + normalized["class"] = value + continue + } -function toHyphenLower(match: string) { - return "-" + match.toLowerCase() -} + // Handle htmlFor -> for mapping + if (key === "htmlFor") { + normalized["for"] = value + continue + } -const cache: Record = {} + // Everything else as attribute + normalized[key] = value + } -function hyphenateStyleName(name: string) { - if (cache.hasOwnProperty(name)) return cache[name] - var hName = name.replace(uppercasePattern, toHyphenLower) - return (cache[name] = msPattern.test(hName) ? "-" + hName : hName) -} + return normalized +}) diff --git a/packages/frameworks/lit/test-toggle.html b/packages/frameworks/lit/test-toggle.html new file mode 100644 index 0000000000..1c3c9ab702 --- /dev/null +++ b/packages/frameworks/lit/test-toggle.html @@ -0,0 +1,57 @@ + + + + + + Lit Toggle Test + + + + + + \ No newline at end of file diff --git a/packages/frameworks/lit/tests/machine.test.ts b/packages/frameworks/lit/tests/machine.test.ts new file mode 100644 index 0000000000..d7171267dd --- /dev/null +++ b/packages/frameworks/lit/tests/machine.test.ts @@ -0,0 +1,421 @@ +import { createMachine } from "@zag-js/core" +import { ZagController } 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) { + const host = new MockLitElement() + const controller = new ZagController(host as any, machine) + + // Simulate hostConnected + controller.hostConnected() + + const send = async (event: any) => { + controller.send(event) + await Promise.resolve() + } + + return { controller, host, send, api: controller.api } +} + +describe("LitMachine", () => { + test("initial state", () => { + const machine = createMachine({ + initialState() { + return "foo" + }, + states: { + foo: { + on: { + NEXT: { target: "bar" }, + }, + }, + bar: {}, + }, + }) + + const { controller } = renderMachine(machine) + + expect(controller.api.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 { controller } = renderMachine(machine) + + expect(controller.api.state.get()).toEqual("test") + expect(controller.api.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 { controller, send } = renderMachine(machine) + await Promise.resolve() + + expect(controller.api.state.hasTag("go")).toBeTruthy() + + await send({ type: "TIMER" }) + expect(controller.api.state.get()).toBe("yellow") + expect(controller.api.state.hasTag("go")).toBeTruthy() + + await send({ type: "TIMER" }) + expect(controller.api.state.get()).toBe("red") + expect(controller.api.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 { controller, send } = renderMachine(machine) + await Promise.resolve() + + expect(controller.api.computed("length")).toEqual(3) + + await send({ type: "UPDATE" }) + expect(controller.api.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 { controller, send } = renderMachine(machine) + await Promise.resolve() + + await send({ type: "INCREMENT" }) + expect(controller.api.context.get("count")).toEqual(1) + + await send({ type: "INCREMENT" }) + expect(controller.api.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 { controller, send } = renderMachine(machine) + + await send({ type: "VALUE.SET", value: "next" }) + + // since value is controlled, it should not change + expect(controller.api.context.get("value")).toEqual("foo") + }) +}) + +describe("ZagController", () => { + 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 { controller } = renderMachine(machine) + + // Check all service API properties are available + expect(controller.api.state).toBeDefined() + expect(controller.api.send).toBeDefined() + expect(controller.api.context).toBeDefined() + expect(controller.api.prop).toBeDefined() + expect(controller.api.scope).toBeDefined() + expect(controller.api.refs).toBeDefined() + expect(controller.api.computed).toBeDefined() + expect(controller.api.event).toBeDefined() + }) + + test("cleanup on hostDisconnected", () => { + const machine = createMachine({ + initialState() { + return "test" + }, + states: { + test: {}, + }, + }) + + const host = new MockLitElement() + const controller = new ZagController(host as any, machine) + + controller.hostConnected() + expect(controller.api).toBeDefined() + + controller.hostDisconnected() + // Machine should be stopped and cleaned up + expect(controller.api.getStatus()).toBe("Stopped") + }) +}) diff --git a/packages/frameworks/lit/tests/merge-props.test.ts b/packages/frameworks/lit/tests/merge-props.test.ts index c19f5de93d..53b8818cd9 100644 --- a/packages/frameworks/lit/tests/merge-props.test.ts +++ b/packages/frameworks/lit/tests/merge-props.test.ts @@ -1,151 +1,94 @@ -import { createComputed, createRoot, createSignal, mergeProps as _mergeProps } from "solid-js" import { mergeProps } from "../src" describe("mergeProps", () => { - it("handles one argument", () => - createRoot((dispose) => { - const onClick = () => {} - const className = "primary" - const id = "test_id" + it("handles one argument", () => { + const onClick = () => {} + const className = "primary" + const id = "test_id" - const props = mergeProps({ onClick, className, id }) + const props = mergeProps({ onClick, className, id }) - expect(props.onClick).toBe(onClick) - expect(props.className).toBe(className) - expect(props.id).toBe(id) - - dispose() - })) - - it("combines handlers", async () => { - createRoot(async (dispose) => { - let count = 0 + expect(props.onClick).toBe(onClick) + expect(props.className).toBe(className) + expect(props.id).toBe(id) + }) - const mockFn = vi.fn(() => { - count++ - }) + it("combines handlers with @ prefix", () => { + let count = 0 - const props = mergeProps({ onClick: mockFn }, { onClick: mockFn }, { onClick: mockFn }) + const mockFn = vi.fn(() => { + count++ + }) - props.onClick() - expect(mockFn).toBeCalledTimes(3) - expect(count).toBe(3) + const props = mergeProps({ "@click": mockFn }, { "@click": mockFn }, { "@click": mockFn }) - dispose() - }) + props["@click"]() + expect(mockFn).toBeCalledTimes(3) + expect(count).toBe(3) }) - it("combines css classes", async () => { - createRoot(async (dispose) => { - 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") + it("combines css classes", () => { + const className1 = "primary" + const className2 = "hover" + const className3 = "focus" - const props2 = mergeProps({ className: className1 }, { className: className2 }, { className: className3 }) - expect(props2.className).toBe("primary hover focus") + const props = mergeProps({ class: className1 }, { class: className2 }, { class: className3 }) + expect(props.class).toBe("primary hover focus") - dispose() - }) + const props2 = mergeProps({ className: className1 }, { className: className2 }, { className: className3 }) + expect(props2.className).toBe("primary hover focus") }) - it("combines styles", () => - createRoot((dispose) => { - const stringStyles = ` - margin: 24px; - padding: 2; - background-image: url("http://example.com/image.png"); - border: 1px solid #123456; - --x: 123; - ` - - const objStyles = { - margin: "10px", + 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", } - - 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", - } - `) - - dispose() - })) - - it("accepts function sources", () => { - createRoot(() => { - const [signal, setSignal] = createSignal({ - class: "primary", - style: { - margin: "10px", - }, - }) - - const props = mergeProps( - signal, - { class: "secondary" }, - { - style: { padding: "10px" }, - }, - ) - - let i = 0 - - createComputed(() => { - if (i === 0) { - expect(props.class).toBe("primary secondary") - expect(props.style).toEqual({ margin: "10px", padding: "10px" }) - i++ - } else { - expect(props.class).toBe("tertiary secondary") - expect(props.style).toEqual({ padding: "10px" }) - expect(props.foo).toEqual("bar") - } - }) - - setSignal({ class: "tertiary", foo: "bar" }) - }) + `) }) - it("last value overwrites the event-listeners", async () => { - createRoot(async (dispose) => { - const mockFn = vi.fn() - const message1 = "click1" - const message2 = "click2" + it("handles Lit @ event prefix", () => { + const mockFn1 = vi.fn() + const mockFn2 = vi.fn() - const props = mergeProps( - { onEvent: () => mockFn(message1) }, - { onEvent: () => mockFn(message2) }, - { onEvent: "overwrites" }, - ) + const props = mergeProps({ "@click": mockFn1 }, { "@click": mockFn2 }) - expect(props.onEvent).toBe("overwrites") - - dispose() - }) + props["@click"]("test") + expect(mockFn1).toBeCalledWith("test") + expect(mockFn2).toBeCalledWith("test") }) - it("works with mergeProps", () => { - const cb1 = vi.fn() - const cb2 = vi.fn() - const combined = mergeProps({ onClick: cb1 }, { onClick: cb2 }) - const merged = _mergeProps(combined) + it("last value overwrites the event-listeners", () => { + const mockFn = vi.fn() + const message1 = "click1" + const message2 = "click2" - merged.onClick("foo") + const props = mergeProps( + { "@event": () => mockFn(message1) }, + { "@event": () => mockFn(message2) }, + { "@event": "overwrites" }, + ) - expect(cb1).toHaveBeenCalledTimes(1) - expect(cb1).toBeCalledWith("foo") - expect(cb2).toHaveBeenCalledTimes(1) - expect(cb2).toBeCalledWith("foo") + expect(props["@event"]).toBe("overwrites") }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72dfcf8cbb..d793d25066 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1750,6 +1750,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': @@ -5115,6 +5155,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==} @@ -6622,6 +6668,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 +9916,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'} @@ -14736,6 +14794,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 @@ -16417,6 +16481,8 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@types/trusted-types@2.0.7': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -20486,6 +20552,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 From 681a5bc5a6659278c6935b8bdd326c93b9fdfcb5 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 10:42:44 +0300 Subject: [PATCH 03/56] test: initial testing setup --- examples/lit-ts/index.html | 76 ++++ examples/lit-ts/package.json | 34 ++ .../lit-ts/src/components/accordion-demo.ts | 98 +++++ examples/lit-ts/src/components/toggle-demo.ts | 52 +++ examples/lit-ts/src/main.ts | 109 +++++ examples/lit-ts/tsconfig.json | 21 + examples/lit-ts/tsconfig.node.json | 10 + examples/lit-ts/vite.config.ts | 10 + package.json | 1 + playwright.config.ts | 6 + pnpm-lock.yaml | 403 +++++++++++++++--- 11 files changed, 766 insertions(+), 54 deletions(-) create mode 100644 examples/lit-ts/index.html create mode 100644 examples/lit-ts/package.json create mode 100644 examples/lit-ts/src/components/accordion-demo.ts create mode 100644 examples/lit-ts/src/components/toggle-demo.ts create mode 100644 examples/lit-ts/src/main.ts create mode 100644 examples/lit-ts/tsconfig.json create mode 100644 examples/lit-ts/tsconfig.node.json create mode 100644 examples/lit-ts/vite.config.ts diff --git a/examples/lit-ts/index.html b/examples/lit-ts/index.html new file mode 100644 index 0000000000..581aa9d774 --- /dev/null +++ b/examples/lit-ts/index.html @@ -0,0 +1,76 @@ + + + + + + + Zag.js + Lit + + + + + + + diff --git a/examples/lit-ts/package.json b/examples/lit-ts/package.json new file mode 100644 index 0000000000..4be2ef0eea --- /dev/null +++ b/examples/lit-ts/package.json @@ -0,0 +1,34 @@ +{ + "name": "lit-ts", + "type": "module", + "private": "true", + "scripts": { + "dev": "vite --port 3004", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@zag-js/accordion": "workspace:*", + "@zag-js/avatar": "workspace:*", + "@zag-js/checkbox": "workspace:*", + "@zag-js/collapsible": "workspace:*", + "@zag-js/dialog": "workspace:*", + "@zag-js/editable": "workspace:*", + "@zag-js/lit": "workspace:*", + "@zag-js/popover": "workspace:*", + "@zag-js/radio-group": "workspace:*", + "@zag-js/slider": "workspace:*", + "@zag-js/switch": "workspace:*", + "@zag-js/tabs": "workspace:*", + "@zag-js/toggle": "workspace:*", + "@zag-js/tooltip": "workspace:*", + "lit": "^3.0.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "vite": "^5.0.0" + }, + "engines": { + "node": ">=18" + } +} diff --git a/examples/lit-ts/src/components/accordion-demo.ts b/examples/lit-ts/src/components/accordion-demo.ts new file mode 100644 index 0000000000..26a5537c72 --- /dev/null +++ b/examples/lit-ts/src/components/accordion-demo.ts @@ -0,0 +1,98 @@ +import { LitElement, html, css } from "lit" +import { customElement } from "lit/decorators.js" +import * as accordion from "@zag-js/accordion" +import { ZagController } from "@zag-js/lit" + +// Sample data matching other framework examples +const accordionData = [ + { id: "home", label: "Home" }, + { id: "about", label: "About" }, + { id: "contact", label: "Contact" }, +] + +@customElement("accordion-demo") +export class AccordionDemo extends LitElement { + static styles = css` + :host { + display: block; + } + + .accordion-item { + border-bottom: 1px solid #e5e7eb; + } + + .accordion-trigger { + width: 100%; + padding: 16px; + text-align: left; + background: none; + border: none; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 16px; + font-weight: 500; + } + + .accordion-trigger:hover { + background: #f9fafb; + } + + .accordion-indicator { + transition: transform 0.2s; + } + + .accordion-item[data-state="open"] .accordion-indicator { + transform: rotate(90deg); + } + + .accordion-content { + padding: 0 16px; + overflow: hidden; + } + + .accordion-content[data-state="open"] { + padding: 16px; + } + + .accordion-content[data-state="closed"] { + display: none; + } + ` + + private zagController = new ZagController(this, accordion.machine, accordion.connect, { id: "accordion-demo" }) + + render() { + const { api } = this.zagController + + 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/components/toggle-demo.ts b/examples/lit-ts/src/components/toggle-demo.ts new file mode 100644 index 0000000000..98d74a6687 --- /dev/null +++ b/examples/lit-ts/src/components/toggle-demo.ts @@ -0,0 +1,52 @@ +import { LitElement, html, css } from "lit" +import { customElement } from "lit/decorators.js" +import * as toggle from "@zag-js/toggle" +import { ZagController } from "@zag-js/lit" + +@customElement("toggle-demo") +export class ToggleDemo extends LitElement { + static styles = css` + :host { + display: block; + } + + .toggle-button { + padding: 12px 16px; + border: 2px solid #ccc; + background: white; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + font-size: 14px; + } + + .toggle-button[data-state="on"] { + background: #007acc; + color: white; + border-color: #007acc; + } + + .toggle-button:hover { + opacity: 0.8; + } + + .toggle-button:focus { + outline: 2px solid #007acc; + outline-offset: 2px; + } + ` + + private zagController = new ZagController(this, toggle.machine, toggle.connect, { id: "toggle-demo" }) + + render() { + const { api } = this.zagController + + return html` +
+

Toggle Demo

+ +

Current state: ${api.pressed ? "Pressed" : "Not Pressed"}

+
+ ` + } +} diff --git a/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts new file mode 100644 index 0000000000..7cf439c2bf --- /dev/null +++ b/examples/lit-ts/src/main.ts @@ -0,0 +1,109 @@ +import { LitElement, html, css } from "lit" +import { customElement, state } from "lit/decorators.js" +import "./components/accordion-demo.js" +import "./components/toggle-demo.js" + +interface RouteData { + path: string + label: string + component: string +} + +const routes: RouteData[] = [ + { path: "/accordion", label: "Accordion", component: "accordion-demo" }, + { path: "/toggle", label: "Toggle", component: "toggle-demo" }, +] + +@customElement("zag-app") +export class ZagApp extends LitElement { + static styles = css` + :host { + display: block; + } + ` + + static properties = { + currentPath: { state: true }, + } + + declare currentPath: string + + connectedCallback() { + super.connectedCallback() + this.currentPath = window.location.pathname + window.addEventListener("popstate", this._handlePopState) + this._handleNavigation() + } + + disconnectedCallback() { + super.disconnectedCallback() + window.removeEventListener("popstate", this._handlePopState) + } + + private _handlePopState = () => { + this.currentPath = window.location.pathname + } + + private _handleNavigation() { + // Simple client-side routing + document.addEventListener("click", (e) => { + const target = e.target as HTMLElement + if (target.tagName === "A" && target.getAttribute("href")?.startsWith("/")) { + e.preventDefault() + const path = target.getAttribute("href")! + history.pushState(null, "", path) + this.currentPath = path + } + }) + } + + render() { + if (this.currentPath === "/" || this.currentPath === "") { + return this._renderIndex() + } + + const route = routes.find((r) => r.path === this.currentPath) + if (route) { + return html` +
+

${route.label}

+
${this._renderComponent(route.component)}
+ ← Back to home +
+ ` + } + + return html`
+

404 - Not Found

+ ← Back to home +
` + } + + private _renderIndex() { + return html` +
+

Zag.js + Lit

+ +
+ ` + } + + private _renderComponent(componentName: string) { + switch (componentName) { + case "accordion-demo": + return html`` + case "toggle-demo": + return html`` + default: + return html`
Component not found
` + } + } +} diff --git a/examples/lit-ts/tsconfig.json b/examples/lit-ts/tsconfig.json new file mode 100644 index 0000000000..ff48e50c45 --- /dev/null +++ b/examples/lit-ts/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "experimentalDecorators": true + }, + "include": ["src/**/*"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/lit-ts/tsconfig.node.json b/examples/lit-ts/tsconfig.node.json new file mode 100644 index 0000000000..42872c59f5 --- /dev/null +++ b/examples/lit-ts/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/lit-ts/vite.config.ts b/examples/lit-ts/vite.config.ts new file mode 100644 index 0000000000..cedc61b812 --- /dev/null +++ b/examples/lit-ts/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite" + +export default defineConfig({ + server: { + port: 3004, + }, + build: { + target: "esnext", + }, +}) diff --git a/package.json b/package.json index 128dbd60b2..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", diff --git a/playwright.config.ts b/playwright.config.ts index e68b5c87ef..b863dcb04b 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 dev", + url: "http://localhost:3004", + reuseExistingServer: !CI, + }, } return frameworks[framework] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d793d25066..7397823f93 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,61 @@ 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: + '@zag-js/accordion': + specifier: workspace:* + version: link:../../packages/machines/accordion + '@zag-js/avatar': + specifier: workspace:* + version: link:../../packages/machines/avatar + '@zag-js/checkbox': + specifier: workspace:* + version: link:../../packages/machines/checkbox + '@zag-js/collapsible': + specifier: workspace:* + version: link:../../packages/machines/collapsible + '@zag-js/dialog': + specifier: workspace:* + version: link:../../packages/machines/dialog + '@zag-js/editable': + specifier: workspace:* + version: link:../../packages/machines/editable + '@zag-js/lit': + specifier: workspace:* + version: link:../../packages/frameworks/lit + '@zag-js/popover': + specifier: workspace:* + version: link:../../packages/machines/popover + '@zag-js/radio-group': + specifier: workspace:* + version: link:../../packages/machines/radio-group + '@zag-js/slider': + specifier: workspace:* + version: link:../../packages/machines/slider + '@zag-js/switch': + specifier: workspace:* + version: link:../../packages/machines/switch + '@zag-js/tabs': + specifier: workspace:* + version: link:../../packages/machines/tabs + '@zag-js/toggle': + specifier: workspace:* + version: link:../../packages/machines/toggle + '@zag-js/tooltip': + specifier: workspace:* + version: link:../../packages/machines/tooltip + lit: + specifier: ^3.0.0 + version: 3.3.1 + devDependencies: + typescript: + specifier: ^5.0.0 + version: 5.9.2 + vite: + specifier: ^5.0.0 + version: 5.4.19(@types/node@24.3.0)(terser@5.43.1) + examples/next-ts: dependencies: '@internationalized/date': @@ -4419,7 +4474,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==} @@ -4435,6 +4490,12 @@ packages: peerDependencies: esbuild: '*' + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.5': resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} engines: {node: '>=18'} @@ -4453,6 +4514,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.5': resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} engines: {node: '>=18'} @@ -4471,6 +4538,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.5': resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} engines: {node: '>=18'} @@ -4489,6 +4562,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.5': resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} engines: {node: '>=18'} @@ -4507,6 +4586,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.5': resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} engines: {node: '>=18'} @@ -4525,6 +4610,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.5': resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} engines: {node: '>=18'} @@ -4543,6 +4634,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.5': resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} engines: {node: '>=18'} @@ -4561,6 +4658,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.5': resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} engines: {node: '>=18'} @@ -4579,6 +4682,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.5': resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} engines: {node: '>=18'} @@ -4597,6 +4706,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.5': resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} engines: {node: '>=18'} @@ -4615,6 +4730,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.5': resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} engines: {node: '>=18'} @@ -4633,6 +4754,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.5': resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} engines: {node: '>=18'} @@ -4651,6 +4778,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.5': resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} engines: {node: '>=18'} @@ -4669,6 +4802,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.5': resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} engines: {node: '>=18'} @@ -4687,6 +4826,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.5': resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} engines: {node: '>=18'} @@ -4705,6 +4850,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.5': resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} engines: {node: '>=18'} @@ -4723,6 +4874,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.5': resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} engines: {node: '>=18'} @@ -4753,6 +4910,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.5': resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} engines: {node: '>=18'} @@ -4783,6 +4946,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.5': resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} engines: {node: '>=18'} @@ -4807,6 +4976,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.5': resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} engines: {node: '>=18'} @@ -4825,6 +5000,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.5': resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} engines: {node: '>=18'} @@ -4843,6 +5024,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.5': resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} engines: {node: '>=18'} @@ -4861,6 +5048,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.5': resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} engines: {node: '>=18'} @@ -8388,6 +8581,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.5: resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} @@ -12935,6 +13133,37 @@ packages: vite: optional: true + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@6.3.5: resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -14319,6 +14548,9 @@ snapshots: transitivePeerDependencies: - supports-color + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.25.5': optional: true @@ -14328,6 +14560,9 @@ snapshots: '@esbuild/android-arm64@0.17.19': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.25.5': optional: true @@ -14337,6 +14572,9 @@ snapshots: '@esbuild/android-arm@0.17.19': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.25.5': optional: true @@ -14346,6 +14584,9 @@ snapshots: '@esbuild/android-x64@0.17.19': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.25.5': optional: true @@ -14355,6 +14596,9 @@ snapshots: '@esbuild/darwin-arm64@0.17.19': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.25.5': optional: true @@ -14364,6 +14608,9 @@ snapshots: '@esbuild/darwin-x64@0.17.19': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.25.5': optional: true @@ -14373,6 +14620,9 @@ snapshots: '@esbuild/freebsd-arm64@0.17.19': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.25.5': optional: true @@ -14382,6 +14632,9 @@ snapshots: '@esbuild/freebsd-x64@0.17.19': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.25.5': optional: true @@ -14391,6 +14644,9 @@ snapshots: '@esbuild/linux-arm64@0.17.19': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.25.5': optional: true @@ -14400,6 +14656,9 @@ snapshots: '@esbuild/linux-arm@0.17.19': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.25.5': optional: true @@ -14409,6 +14668,9 @@ snapshots: '@esbuild/linux-ia32@0.17.19': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.25.5': optional: true @@ -14418,6 +14680,9 @@ snapshots: '@esbuild/linux-loong64@0.17.19': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.25.5': optional: true @@ -14427,6 +14692,9 @@ snapshots: '@esbuild/linux-mips64el@0.17.19': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.25.5': optional: true @@ -14436,6 +14704,9 @@ snapshots: '@esbuild/linux-ppc64@0.17.19': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.25.5': optional: true @@ -14445,6 +14716,9 @@ snapshots: '@esbuild/linux-riscv64@0.17.19': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.25.5': optional: true @@ -14454,6 +14728,9 @@ snapshots: '@esbuild/linux-s390x@0.17.19': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.25.5': optional: true @@ -14463,6 +14740,9 @@ snapshots: '@esbuild/linux-x64@0.17.19': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.25.5': optional: true @@ -14478,6 +14758,9 @@ snapshots: '@esbuild/netbsd-x64@0.17.19': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.25.5': optional: true @@ -14493,6 +14776,9 @@ snapshots: '@esbuild/openbsd-x64@0.17.19': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.25.5': optional: true @@ -14505,6 +14791,9 @@ snapshots: '@esbuild/sunos-x64@0.17.19': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.25.5': optional: true @@ -14514,6 +14803,9 @@ snapshots: '@esbuild/win32-arm64@0.17.19': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.25.5': optional: true @@ -14523,6 +14815,9 @@ snapshots: '@esbuild/win32-ia32@0.17.19': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.25.5': optional: true @@ -14532,6 +14827,9 @@ snapshots: '@esbuild/win32-x64@0.17.19': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.25.5': optional: true @@ -16494,10 +16792,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) @@ -18550,6 +18848,32 @@ snapshots: '@esbuild/win32-ia32': 0.17.19 '@esbuild/win32-x64': 0.17.19 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.5: optionalDependencies: '@esbuild/aix-ppc64': 0.25.5 @@ -18629,12 +18953,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(@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@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)) @@ -18692,7 +19016,7 @@ 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.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -18703,7 +19027,7 @@ snapshots: 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.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 @@ -18722,14 +19046,14 @@ 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-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-plugin-import@2.32.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)))(eslint@9.33.0(jiti@2.5.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(@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@9.33.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color @@ -18744,16 +19068,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 @@ -18766,7 +19080,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 @@ -18777,7 +19091,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-plugin-import@2.32.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)))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -18789,7 +19103,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 @@ -18824,35 +19138,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 @@ -24478,6 +24763,16 @@ snapshots: - supports-color - typescript + vite@5.4.19(@types/node@24.3.0)(terser@5.43.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.48.0 + optionalDependencies: + '@types/node': 24.3.0 + fsevents: 2.3.3 + terser: 5.43.1 + vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 From 9d8391831eec4163445fcc00b6d625dbde310b12 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 10:44:33 +0300 Subject: [PATCH 04/56] refactor: split out ZagController --- packages/frameworks/lit/src/machine.ts | 47 ------------------ packages/frameworks/lit/src/zag-controller.ts | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+), 47 deletions(-) create mode 100644 packages/frameworks/lit/src/zag-controller.ts diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts index f8687bc600..0b998e6a29 100644 --- a/packages/frameworks/lit/src/machine.ts +++ b/packages/frameworks/lit/src/machine.ts @@ -17,7 +17,6 @@ import type { 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 type { ReactiveController, ReactiveControllerHost } from "lit" import { bindable } from "./bindable" import { createRefs } from "./refs" @@ -342,49 +341,3 @@ export class LitMachine { choose: this.choose, }) } - -export class ZagController implements ReactiveController { - private machine: LitMachine - public api: any // Will be set in constructor and updated on changes - - constructor( - private host: ReactiveControllerHost, - machineConfig: Machine, - props: Partial = {}, - ) { - this.machine = new LitMachine(machineConfig, props) - host.addController(this) - } - - hostConnected() { - this.machine.subscribe(() => { - // Update API when machine state changes - this.updateApi() - // Request Lit component update - this.host.requestUpdate() - }) - this.machine.start() - this.updateApi() - // Trigger initial update to sync with host element - this.host.requestUpdate() - } - - hostDisconnected() { - this.machine.stop() - } - - private updateApi() { - // This will be implemented once we have the connect function - // For now, just expose the service - this.api = this.machine.service - } - - // Expose machine methods for advanced usage - get service() { - return this.machine.service - } - - send = (event: any) => { - this.machine.send(event) - } -} diff --git a/packages/frameworks/lit/src/zag-controller.ts b/packages/frameworks/lit/src/zag-controller.ts new file mode 100644 index 0000000000..18b45b5f38 --- /dev/null +++ b/packages/frameworks/lit/src/zag-controller.ts @@ -0,0 +1,49 @@ +import type { Machine, MachineSchema } from "@zag-js/core" +import type { ReactiveController, ReactiveControllerHost } from "lit" +import { LitMachine } from "./machine" + +export class ZagController implements ReactiveController { + private machine: LitMachine + public api: any // Will be set in constructor and updated on changes + + constructor( + private host: ReactiveControllerHost, + machineConfig: Machine, + props: Partial = {}, + ) { + this.machine = new LitMachine(machineConfig, props) + host.addController(this) + } + + hostConnected() { + this.machine.subscribe(() => { + // Update API when machine state changes + this.updateApi() + // Request Lit component update + this.host.requestUpdate() + }) + this.machine.start() + this.updateApi() + // Trigger initial update to sync with host element + this.host.requestUpdate() + } + + hostDisconnected() { + this.machine.stop() + } + + private updateApi() { + // This will be implemented once we have the connect function + // For now, just expose the service + this.api = this.machine.service + } + + // Expose machine methods for advanced usage + get service() { + return this.machine.service + } + + send = (event: any) => { + this.machine.send(event) + } +} From 0d28d3bb99331f75b6c3c3eed61db5c94919213f Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 16:54:08 +0300 Subject: [PATCH 05/56] fix: refined normalize props? --- .../frameworks/lit/src/normalize-props.ts | 89 ++++++++++++++----- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/packages/frameworks/lit/src/normalize-props.ts b/packages/frameworks/lit/src/normalize-props.ts index bb80ce9cb0..3ad474be78 100644 --- a/packages/frameworks/lit/src/normalize-props.ts +++ b/packages/frameworks/lit/src/normalize-props.ts @@ -29,51 +29,100 @@ export type PropTypes = Record & { style: Record } +// Map React-style event names to DOM event names +const eventMap: Record = { + onFocus: "onfocusin", + onBlur: "onfocusout", + onChange: "oninput", + onDoubleClick: "ondblclick", +} + +// Map React-style prop names to HTML attribute names +const propMap: Record = { + className: "class", + htmlFor: "for", +} + +// Properties that should be set as properties, not attributes +const propertyNames = new Set([ + "value", + "checked", + "selected", + "defaultValue", + "defaultChecked", + "defaultSelected", + "disabled", + "readOnly", + "multiple", + "hidden", + "contentEditable", + "draggable", + "spellcheck", + "autocomplete", +]) + +const toStyleString = (style: Record): string => { + let styleString = "" + for (const [key, value] of Object.entries(style)) { + if (value == null) continue + + // Convert camelCase to kebab-case for CSS properties (except CSS custom properties) + const cssKey = key.startsWith("--") ? key : key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`) + + styleString += `${cssKey}:${value};` + } + return styleString +} + type Dict = Record export const normalizeProps = createNormalizer((props: Dict) => { const normalized: Dict = {} - for (const key in props) { - const value = props[key] - + for (let [key, value] of Object.entries(props)) { // Skip undefined values if (value === undefined) { continue } - // Handle event handlers - prefix with @ + // Handle style objects + if (key === "style" && typeof value === "object" && value !== null) { + normalized.style = toStyleString(value) + continue + } + + // Map React-style prop names + if (key in propMap) { + key = propMap[key] + } + + // Handle event handlers if (key.startsWith("on") && typeof value === "function") { + // Map React-style event names to DOM events + const mappedEvent = eventMap[key] + if (mappedEvent) { + key = mappedEvent + } + + // Convert to Lit event listener syntax: @eventname const eventName = key.slice(2).toLowerCase() normalized[`@${eventName}`] = value continue } - // Handle boolean attributes - prefix with ? + // Handle boolean attributes with ? prefix if (typeof value === "boolean") { normalized[`?${key}`] = value continue } - // Handle properties that should be set as properties not attributes - if (key === "value" || key === "checked" || key === "selected") { + // Handle properties that should be set as properties with . prefix + if (propertyNames.has(key)) { normalized[`.${key}`] = value continue } - // Handle className -> class mapping - if (key === "className") { - normalized["class"] = value - continue - } - - // Handle htmlFor -> for mapping - if (key === "htmlFor") { - normalized["for"] = value - continue - } - - // Everything else as attribute + // Everything else as attribute (preserve original case for data-* and aria-* attributes) normalized[key] = value } From d5f009c975f33c7640dc17d42ac67eeec7333541 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 17:33:15 +0300 Subject: [PATCH 06/56] fix: refine normalizeProps for Lit --- .../frameworks/lit/src/normalize-props.ts | 95 +++++++++++-------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/packages/frameworks/lit/src/normalize-props.ts b/packages/frameworks/lit/src/normalize-props.ts index 3ad474be78..0c386c5034 100644 --- a/packages/frameworks/lit/src/normalize-props.ts +++ b/packages/frameworks/lit/src/normalize-props.ts @@ -1,4 +1,5 @@ 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 @@ -29,18 +30,16 @@ export type PropTypes = Record & { style: Record } -// Map React-style event names to DOM event names -const eventMap: Record = { +const propMap: Record = { onFocus: "onfocusin", onBlur: "onfocusout", - onChange: "oninput", onDoubleClick: "ondblclick", -} - -// Map React-style prop names to HTML attribute names -const propMap: Record = { - className: "class", + onChange: "oninput", + defaultChecked: "checked", + defaultValue: "value", + defaultSelected: "selected", htmlFor: "for", + className: "class", } // Properties that should be set as properties, not attributes @@ -48,30 +47,53 @@ const propertyNames = new Set([ "value", "checked", "selected", - "defaultValue", - "defaultChecked", - "defaultSelected", "disabled", - "readOnly", + "readonly", "multiple", "hidden", - "contentEditable", + "contenteditable", "draggable", "spellcheck", "autocomplete", ]) -const toStyleString = (style: Record): string => { - let styleString = "" - for (const [key, value] of Object.entries(style)) { - if (value == null) continue +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) + ) +} - // Convert camelCase to kebab-case for CSS properties (except CSS custom properties) - const cssKey = key.startsWith("--") ? key : key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`) +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()}`) + } - styleString += `${cssKey}:${value};` + string += `${key}:${value};` } - return styleString + + return string } type Dict = Record @@ -79,15 +101,17 @@ type Dict = Record export const normalizeProps = createNormalizer((props: Dict) => { const normalized: Dict = {} - for (let [key, value] of Object.entries(props)) { + for (let key in props) { + const value = props[key] + // Skip undefined values if (value === undefined) { continue } // Handle style objects - if (key === "style" && typeof value === "object" && value !== null) { - normalized.style = toStyleString(value) + if (key === "style" && isObject(value)) { + normalized["style"] = toStyleString(value) continue } @@ -96,34 +120,27 @@ export const normalizeProps = createNormalizer((props: Dict) => { key = propMap[key] } - // Handle event handlers + // Convert to Lit event listener syntax: @eventname if (key.startsWith("on") && typeof value === "function") { - // Map React-style event names to DOM events - const mappedEvent = eventMap[key] - if (mappedEvent) { - key = mappedEvent - } - - // Convert to Lit event listener syntax: @eventname - const eventName = key.slice(2).toLowerCase() - normalized[`@${eventName}`] = value + normalized[`@${key.toLowerCase().slice(2)}`] = value continue } // Handle boolean attributes with ? prefix if (typeof value === "boolean") { - normalized[`?${key}`] = value + normalized[`?${key.toLowerCase()}`] = value continue } // Handle properties that should be set as properties with . prefix - if (propertyNames.has(key)) { - normalized[`.${key}`] = value + if (propertyNames.has(key.toLowerCase())) { + normalized[`.${key.toLowerCase()}`] = value continue } - // Everything else as attribute (preserve original case for data-* and aria-* attributes) - normalized[key] = value + // Everything else as attribute + const attrKey = shouldPreserveCase(key) ? key : key.toLowerCase() + normalized[attrKey] = value } return normalized From 839395ca250677c790bd5e31a9a50f6e889bc20f Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 17:51:01 +0300 Subject: [PATCH 07/56] fix: refine ZagController --- packages/frameworks/lit/src/zag-controller.ts | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/packages/frameworks/lit/src/zag-controller.ts b/packages/frameworks/lit/src/zag-controller.ts index 18b45b5f38..e36b53c317 100644 --- a/packages/frameworks/lit/src/zag-controller.ts +++ b/packages/frameworks/lit/src/zag-controller.ts @@ -2,48 +2,35 @@ import type { Machine, MachineSchema } from "@zag-js/core" import type { ReactiveController, ReactiveControllerHost } from "lit" import { LitMachine } from "./machine" -export class ZagController implements ReactiveController { - private machine: LitMachine - public api: any // Will be set in constructor and updated on changes +export class ZagController implements ReactiveController { + private machine: LitMachine constructor( private host: ReactiveControllerHost, - machineConfig: Machine, - props: Partial = {}, + machineConfig: Machine, + getProps?: () => Partial, ) { - this.machine = new LitMachine(machineConfig, props) + this.machine = new LitMachine(machineConfig, getProps) + + // Register for lifecycle updates host.addController(this) } hostConnected() { + // Start the machine when the host is connected this.machine.subscribe(() => { - // Update API when machine state changes - this.updateApi() // Request Lit component update this.host.requestUpdate() }) this.machine.start() - this.updateApi() - // Trigger initial update to sync with host element - this.host.requestUpdate() } hostDisconnected() { this.machine.stop() } - private updateApi() { - // This will be implemented once we have the connect function - // For now, just expose the service - this.api = this.machine.service - } - // Expose machine methods for advanced usage get service() { return this.machine.service } - - send = (event: any) => { - this.machine.send(event) - } } From 49b1431eed27c3e20f509850d217f7f06b74f0c0 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 18:02:22 +0300 Subject: [PATCH 08/56] fix: export ZagController --- packages/frameworks/lit/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frameworks/lit/src/index.ts b/packages/frameworks/lit/src/index.ts index 64d783f0c3..713a4d060b 100644 --- a/packages/frameworks/lit/src/index.ts +++ b/packages/frameworks/lit/src/index.ts @@ -1,3 +1,4 @@ export * from "./machine" export { mergeProps } from "./merge-props" export * from "./normalize-props" +export { ZagController } from "./zag-controller" From 680c83dee101440a74e41d4c4b8afd157356961e Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 18:17:33 +0300 Subject: [PATCH 09/56] fix: refine lit example --- examples/lit-ts/package.json | 1 + .../lit-ts/src/components/accordion-demo.ts | 17 +++++----- examples/lit-ts/src/components/toggle-demo.ts | 11 ++++--- pnpm-lock.yaml | 32 +++++++++++++------ 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/examples/lit-ts/package.json b/examples/lit-ts/package.json index 4be2ef0eea..02cfdea19a 100644 --- a/examples/lit-ts/package.json +++ b/examples/lit-ts/package.json @@ -8,6 +8,7 @@ "preview": "vite preview" }, "dependencies": { + "@open-wc/lit-helpers": "^0.7.0", "@zag-js/accordion": "workspace:*", "@zag-js/avatar": "workspace:*", "@zag-js/checkbox": "workspace:*", diff --git a/examples/lit-ts/src/components/accordion-demo.ts b/examples/lit-ts/src/components/accordion-demo.ts index 26a5537c72..e2f7853614 100644 --- a/examples/lit-ts/src/components/accordion-demo.ts +++ b/examples/lit-ts/src/components/accordion-demo.ts @@ -1,7 +1,8 @@ import { LitElement, html, css } from "lit" import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" import * as accordion from "@zag-js/accordion" -import { ZagController } from "@zag-js/lit" +import { ZagController, normalizeProps } from "@zag-js/lit" // Sample data matching other framework examples const accordionData = [ @@ -61,30 +62,30 @@ export class AccordionDemo extends LitElement { } ` - private zagController = new ZagController(this, accordion.machine, accordion.connect, { id: "accordion-demo" }) + private zagController = new ZagController(this, accordion.machine, () => ({ id: "accordion-demo" })) render() { - const { api } = this.zagController + const api = accordion.connect(this.zagController.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/components/toggle-demo.ts b/examples/lit-ts/src/components/toggle-demo.ts index 98d74a6687..cc1c3f8c9e 100644 --- a/examples/lit-ts/src/components/toggle-demo.ts +++ b/examples/lit-ts/src/components/toggle-demo.ts @@ -1,7 +1,8 @@ import { LitElement, html, css } from "lit" import { customElement } from "lit/decorators.js" +import { spread } from "@open-wc/lit-helpers" import * as toggle from "@zag-js/toggle" -import { ZagController } from "@zag-js/lit" +import { ZagController, normalizeProps } from "@zag-js/lit" @customElement("toggle-demo") export class ToggleDemo extends LitElement { @@ -36,15 +37,17 @@ export class ToggleDemo extends LitElement { } ` - private zagController = new ZagController(this, toggle.machine, toggle.connect, { id: "toggle-demo" }) + private zagController = new ZagController(this, toggle.machine, () => ({})) render() { - const { api } = this.zagController + const api = toggle.connect(this.zagController.service, normalizeProps) return html`

Toggle Demo

- +

Current state: ${api.pressed ? "Pressed" : "Not Pressed"}

` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7397823f93..1aaaae9b24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: examples/lit-ts: dependencies: + '@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 @@ -5602,6 +5605,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'} @@ -15636,6 +15644,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 @@ -18957,7 +18969,7 @@ snapshots: '@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.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.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)) 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)) @@ -19016,44 +19028,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.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.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.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-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.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-plugin-import@2.32.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)))(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.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.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.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 @@ -19091,7 +19103,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.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-plugin-import@2.32.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)))(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 From 98aa9e9d2fdfa47d7208caac75957cfcb6379d65 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 18:17:50 +0300 Subject: [PATCH 10/56] test: minor lit machine.test tweaks --- packages/frameworks/lit/tests/machine.test.ts | 77 ++++++++++--------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/packages/frameworks/lit/tests/machine.test.ts b/packages/frameworks/lit/tests/machine.test.ts index d7171267dd..95af716eef 100644 --- a/packages/frameworks/lit/tests/machine.test.ts +++ b/packages/frameworks/lit/tests/machine.test.ts @@ -1,3 +1,8 @@ +// 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 { ZagController } from "../src" @@ -22,19 +27,21 @@ class MockLitElement { } } -function renderMachine(machine: any) { +function renderMachine(machine: any, props: any = {}) { const host = new MockLitElement() - const controller = new ZagController(host as any, machine) + const controller = new ZagController(host as any, machine, () => props) // Simulate hostConnected controller.hostConnected() const send = async (event: any) => { - controller.send(event) + controller.service.send(event) await Promise.resolve() } - return { controller, host, send, api: controller.api } + const { service } = controller + + return { controller, host, send, service } } describe("LitMachine", () => { @@ -53,9 +60,9 @@ describe("LitMachine", () => { }, }) - const { controller } = renderMachine(machine) + const { service } = renderMachine(machine) - expect(controller.api.state.get()).toBe("foo") + expect(service.state.get()).toBe("foo") }) test("initial entry action", async () => { @@ -100,10 +107,10 @@ describe("LitMachine", () => { }, }) - const { controller } = renderMachine(machine) + const { service } = renderMachine(machine) - expect(controller.api.state.get()).toEqual("test") - expect(controller.api.context.get("foo")).toEqual("bar") + expect(service.state.get()).toEqual("test") + expect(service.context.get("foo")).toEqual("bar") }) test("send event", async () => { @@ -167,18 +174,18 @@ describe("LitMachine", () => { }, }) - const { controller, send } = renderMachine(machine) + const { service, send } = renderMachine(machine) await Promise.resolve() - expect(controller.api.state.hasTag("go")).toBeTruthy() + expect(service.state.hasTag("go")).toBeTruthy() await send({ type: "TIMER" }) - expect(controller.api.state.get()).toBe("yellow") - expect(controller.api.state.hasTag("go")).toBeTruthy() + expect(service.state.get()).toBe("yellow") + expect(service.state.hasTag("go")).toBeTruthy() await send({ type: "TIMER" }) - expect(controller.api.state.get()).toBe("red") - expect(controller.api.state.hasTag("go")).toBeFalsy() + expect(service.state.get()).toBe("red") + expect(service.state.hasTag("go")).toBeFalsy() }) test("computed", async () => { @@ -208,13 +215,13 @@ describe("LitMachine", () => { }, }) - const { controller, send } = renderMachine(machine) + const { service, send } = renderMachine(machine) await Promise.resolve() - expect(controller.api.computed("length")).toEqual(3) + expect(service.computed("length")).toEqual(3) await send({ type: "UPDATE" }) - expect(controller.api.computed("length")).toEqual(5) + expect(service.computed("length")).toEqual(5) }) test("watch", async () => { @@ -290,14 +297,14 @@ describe("LitMachine", () => { }, }) - const { controller, send } = renderMachine(machine) + const { service, send } = renderMachine(machine, { max: 1 }) await Promise.resolve() await send({ type: "INCREMENT" }) - expect(controller.api.context.get("count")).toEqual(1) + expect(service.context.get("count")).toEqual(1) await send({ type: "INCREMENT" }) - expect(controller.api.context.get("count")).toEqual(1) + expect(service.context.get("count")).toEqual(1) }) test("context: controlled", async () => { @@ -335,12 +342,12 @@ describe("LitMachine", () => { }, }) - const { controller, send } = renderMachine(machine) + const { service, send } = renderMachine(machine, { value: "foo", defaultValue: "" }) await send({ type: "VALUE.SET", value: "next" }) // since value is controlled, it should not change - expect(controller.api.context.get("value")).toEqual("foo") + expect(service.context.get("value")).toEqual("foo") }) }) @@ -385,17 +392,17 @@ describe("ZagController", () => { }, }) - const { controller } = renderMachine(machine) + const { service } = renderMachine(machine) // Check all service API properties are available - expect(controller.api.state).toBeDefined() - expect(controller.api.send).toBeDefined() - expect(controller.api.context).toBeDefined() - expect(controller.api.prop).toBeDefined() - expect(controller.api.scope).toBeDefined() - expect(controller.api.refs).toBeDefined() - expect(controller.api.computed).toBeDefined() - expect(controller.api.event).toBeDefined() + 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", () => { @@ -409,13 +416,13 @@ describe("ZagController", () => { }) const host = new MockLitElement() - const controller = new ZagController(host as any, machine) + const controller = new ZagController(host as any, machine, () => ({})) controller.hostConnected() - expect(controller.api).toBeDefined() + expect(controller.service).toBeDefined() controller.hostDisconnected() // Machine should be stopped and cleaned up - expect(controller.api.getStatus()).toBe("Stopped") + expect(controller.service.getStatus()).toBe("Stopped") }) }) From 9c4b57e083d871a5529a7c61c05568b47c91bfcc Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 18:31:39 +0300 Subject: [PATCH 11/56] fix: refine lit example --- examples/lit-ts/package.json | 2 +- examples/lit-ts/src/main.ts | 2 +- examples/lit-ts/tsconfig.json | 8 ++++++-- examples/lit-ts/vite.config.ts | 3 --- playwright.config.ts | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/lit-ts/package.json b/examples/lit-ts/package.json index 02cfdea19a..a056f7ff7d 100644 --- a/examples/lit-ts/package.json +++ b/examples/lit-ts/package.json @@ -3,7 +3,7 @@ "type": "module", "private": "true", "scripts": { - "dev": "vite --port 3004", + "dev": "vite", "build": "vite build", "preview": "vite preview" }, diff --git a/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts index 7cf439c2bf..db63c695ad 100644 --- a/examples/lit-ts/src/main.ts +++ b/examples/lit-ts/src/main.ts @@ -1,5 +1,5 @@ import { LitElement, html, css } from "lit" -import { customElement, state } from "lit/decorators.js" +import { customElement } from "lit/decorators.js" import "./components/accordion-demo.js" import "./components/toggle-demo.js" diff --git a/examples/lit-ts/tsconfig.json b/examples/lit-ts/tsconfig.json index ff48e50c45..c772514f00 100644 --- a/examples/lit-ts/tsconfig.json +++ b/examples/lit-ts/tsconfig.json @@ -2,20 +2,24 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, + + /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, + + /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "experimentalDecorators": true }, - "include": ["src/**/*"], + "include": ["src/**/*.ts"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/examples/lit-ts/vite.config.ts b/examples/lit-ts/vite.config.ts index cedc61b812..d3e482fab4 100644 --- a/examples/lit-ts/vite.config.ts +++ b/examples/lit-ts/vite.config.ts @@ -1,9 +1,6 @@ import { defineConfig } from "vite" export default defineConfig({ - server: { - port: 3004, - }, build: { target: "esnext", }, diff --git a/playwright.config.ts b/playwright.config.ts index b863dcb04b..c53e6d4bae 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -36,7 +36,7 @@ export function getWebServer(): WebServer { }, lit: { cwd: "./examples/lit-ts", - command: "pnpm dev", + command: "pnpm vite --port 3004", url: "http://localhost:3004", reuseExistingServer: !CI, }, From 18ab8bb4811e5d30d6caef3d182f8b77e3e25844 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 19:21:57 +0300 Subject: [PATCH 12/56] fix: example adapatations --- .changeset/config.json | 1 + examples/lit-ts/.gitignore | 24 + examples/lit-ts/index.html | 77 +-- examples/lit-ts/package.json | 81 ++- examples/lit-ts/pages/accordion.html | 17 + examples/lit-ts/public/vite.svg | 1 + examples/lit-ts/src/accordion.ts | 40 ++ .../lit-ts/src/components/accordion-demo.ts | 99 ---- examples/lit-ts/src/components/toggle-demo.ts | 55 -- examples/lit-ts/src/main.ts | 109 ---- examples/lit-ts/src/vite-env.d.ts | 1 + examples/lit-ts/tsconfig.json | 17 +- examples/lit-ts/tsconfig.node.json | 10 - examples/lit-ts/vite.config.ts | 7 - pnpm-lock.yaml | 555 ++++++++---------- 15 files changed, 426 insertions(+), 668 deletions(-) create mode 100644 examples/lit-ts/.gitignore create mode 100644 examples/lit-ts/pages/accordion.html create mode 100644 examples/lit-ts/public/vite.svg create mode 100644 examples/lit-ts/src/accordion.ts delete mode 100644 examples/lit-ts/src/components/accordion-demo.ts delete mode 100644 examples/lit-ts/src/components/toggle-demo.ts delete mode 100644 examples/lit-ts/src/main.ts create mode 100644 examples/lit-ts/src/vite-env.d.ts delete mode 100644 examples/lit-ts/tsconfig.node.json delete mode 100644 examples/lit-ts/vite.config.ts 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/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 index 581aa9d774..6b59e2ad17 100644 --- a/examples/lit-ts/index.html +++ b/examples/lit-ts/index.html @@ -4,73 +4,16 @@ - Zag.js + Lit - + Vite + Lit - - - + +

Lit + Zag

+ + Accordion + Avatar + Avatar with Controller + Popover + Checkbox + Combobox diff --git a/examples/lit-ts/package.json b/examples/lit-ts/package.json index a056f7ff7d..77c9201fb0 100644 --- a/examples/lit-ts/package.json +++ b/examples/lit-ts/package.json @@ -1,35 +1,96 @@ { "name": "lit-ts", + "private": true, + "version": "0.0.0", "type": "module", - "private": "true", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, + "devDependencies": { + "typescript": "5.9.2", + "vite": "7.1.3" + }, "dependencies": { - "@open-wc/lit-helpers": "^0.7.0", + "@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:*", - "lit": "^3.0.0" - }, - "devDependencies": { - "typescript": "^5.0.0", - "vite": "^5.0.0" - }, - "engines": { - "node": ">=18" + "@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", + "match-sorter": "8.1.0", + "nanoid": "^5.1.5" } } diff --git a/examples/lit-ts/pages/accordion.html b/examples/lit-ts/pages/accordion.html new file mode 100644 index 0000000000..313d365ebd --- /dev/null +++ b/examples/lit-ts/pages/accordion.html @@ -0,0 +1,17 @@ + + + + + + + + Vite + TS + + +

Accordion

+ + + + + + 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/accordion.ts b/examples/lit-ts/src/accordion.ts new file mode 100644 index 0000000000..1425a3da3d --- /dev/null +++ b/examples/lit-ts/src/accordion.ts @@ -0,0 +1,40 @@ +import { spread } from "@open-wc/lit-helpers" +import * as accordion from "@zag-js/accordion" +import { accordionData } from "@zag-js/shared" +import style from "@zag-js/shared/src/css/accordion.css?inline" +import { ZagController, normalizeProps } from "@zag-js/lit" +import { LitElement, html, unsafeCSS } from "lit" +import { customElement } from "lit/decorators.js" +import { nanoid } from "nanoid" + +@customElement("accordion-element") +export class Accordion extends LitElement { + private zag = new ZagController(this, accordion.machine, () => ({ id: nanoid() })) + + static styles = unsafeCSS(style) + + render() { + const api = accordion.connect(this.zag.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/components/accordion-demo.ts b/examples/lit-ts/src/components/accordion-demo.ts deleted file mode 100644 index e2f7853614..0000000000 --- a/examples/lit-ts/src/components/accordion-demo.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { LitElement, html, css } from "lit" -import { customElement } from "lit/decorators.js" -import { spread } from "@open-wc/lit-helpers" -import * as accordion from "@zag-js/accordion" -import { ZagController, normalizeProps } from "@zag-js/lit" - -// Sample data matching other framework examples -const accordionData = [ - { id: "home", label: "Home" }, - { id: "about", label: "About" }, - { id: "contact", label: "Contact" }, -] - -@customElement("accordion-demo") -export class AccordionDemo extends LitElement { - static styles = css` - :host { - display: block; - } - - .accordion-item { - border-bottom: 1px solid #e5e7eb; - } - - .accordion-trigger { - width: 100%; - padding: 16px; - text-align: left; - background: none; - border: none; - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 16px; - font-weight: 500; - } - - .accordion-trigger:hover { - background: #f9fafb; - } - - .accordion-indicator { - transition: transform 0.2s; - } - - .accordion-item[data-state="open"] .accordion-indicator { - transform: rotate(90deg); - } - - .accordion-content { - padding: 0 16px; - overflow: hidden; - } - - .accordion-content[data-state="open"] { - padding: 16px; - } - - .accordion-content[data-state="closed"] { - display: none; - } - ` - - private zagController = new ZagController(this, accordion.machine, () => ({ id: "accordion-demo" })) - - render() { - const api = accordion.connect(this.zagController.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/components/toggle-demo.ts b/examples/lit-ts/src/components/toggle-demo.ts deleted file mode 100644 index cc1c3f8c9e..0000000000 --- a/examples/lit-ts/src/components/toggle-demo.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { LitElement, html, css } from "lit" -import { customElement } from "lit/decorators.js" -import { spread } from "@open-wc/lit-helpers" -import * as toggle from "@zag-js/toggle" -import { ZagController, normalizeProps } from "@zag-js/lit" - -@customElement("toggle-demo") -export class ToggleDemo extends LitElement { - static styles = css` - :host { - display: block; - } - - .toggle-button { - padding: 12px 16px; - border: 2px solid #ccc; - background: white; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s; - font-size: 14px; - } - - .toggle-button[data-state="on"] { - background: #007acc; - color: white; - border-color: #007acc; - } - - .toggle-button:hover { - opacity: 0.8; - } - - .toggle-button:focus { - outline: 2px solid #007acc; - outline-offset: 2px; - } - ` - - private zagController = new ZagController(this, toggle.machine, () => ({})) - - render() { - const api = toggle.connect(this.zagController.service, normalizeProps) - - return html` -
-

Toggle Demo

- -

Current state: ${api.pressed ? "Pressed" : "Not Pressed"}

-
- ` - } -} diff --git a/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts deleted file mode 100644 index db63c695ad..0000000000 --- a/examples/lit-ts/src/main.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { LitElement, html, css } from "lit" -import { customElement } from "lit/decorators.js" -import "./components/accordion-demo.js" -import "./components/toggle-demo.js" - -interface RouteData { - path: string - label: string - component: string -} - -const routes: RouteData[] = [ - { path: "/accordion", label: "Accordion", component: "accordion-demo" }, - { path: "/toggle", label: "Toggle", component: "toggle-demo" }, -] - -@customElement("zag-app") -export class ZagApp extends LitElement { - static styles = css` - :host { - display: block; - } - ` - - static properties = { - currentPath: { state: true }, - } - - declare currentPath: string - - connectedCallback() { - super.connectedCallback() - this.currentPath = window.location.pathname - window.addEventListener("popstate", this._handlePopState) - this._handleNavigation() - } - - disconnectedCallback() { - super.disconnectedCallback() - window.removeEventListener("popstate", this._handlePopState) - } - - private _handlePopState = () => { - this.currentPath = window.location.pathname - } - - private _handleNavigation() { - // Simple client-side routing - document.addEventListener("click", (e) => { - const target = e.target as HTMLElement - if (target.tagName === "A" && target.getAttribute("href")?.startsWith("/")) { - e.preventDefault() - const path = target.getAttribute("href")! - history.pushState(null, "", path) - this.currentPath = path - } - }) - } - - render() { - if (this.currentPath === "/" || this.currentPath === "") { - return this._renderIndex() - } - - const route = routes.find((r) => r.path === this.currentPath) - if (route) { - return html` -
-

${route.label}

-
${this._renderComponent(route.component)}
- ← Back to home -
- ` - } - - return html`
-

404 - Not Found

- ← Back to home -
` - } - - private _renderIndex() { - return html` -
-

Zag.js + Lit

- -
- ` - } - - private _renderComponent(componentName: string) { - switch (componentName) { - case "accordion-demo": - return html`` - case "toggle-demo": - return html`` - default: - return html`
Component not found
` - } - } -} 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 index c772514f00..833d304523 100644 --- a/examples/lit-ts/tsconfig.json +++ b/examples/lit-ts/tsconfig.json @@ -1,25 +1,26 @@ { "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, + "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, - "experimentalDecorators": true + "noUncheckedSideEffectImports": true }, - "include": ["src/**/*.ts"], - "references": [{ "path": "./tsconfig.node.json" }] + "include": ["src/**/*.ts"] } diff --git a/examples/lit-ts/tsconfig.node.json b/examples/lit-ts/tsconfig.node.json deleted file mode 100644 index 42872c59f5..0000000000 --- a/examples/lit-ts/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/examples/lit-ts/vite.config.ts b/examples/lit-ts/vite.config.ts deleted file mode 100644 index d3e482fab4..0000000000 --- a/examples/lit-ts/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from "vite" - -export default defineConfig({ - build: { - target: "esnext", - }, -}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1aaaae9b24..733b799875 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-import-resolver-typescript@3.10.1)(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@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) @@ -131,61 +131,250 @@ importers: examples/lit-ts: dependencies: + '@internationalized/date': + specifier: 3.9.0 + version: 3.9.0 '@open-wc/lit-helpers': - specifier: ^0.7.0 + 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.0.0 + specifier: 3.3.1 version: 3.3.1 + match-sorter: + specifier: 8.1.0 + version: 8.1.0 + nanoid: + specifier: ^5.1.5 + version: 5.1.5 devDependencies: typescript: - specifier: ^5.0.0 + specifier: 5.9.2 version: 5.9.2 vite: - specifier: ^5.0.0 - version: 5.4.19(@types/node@24.3.0)(terser@5.43.1) + 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: @@ -4477,7 +4666,7 @@ packages: '@emotion/use-insertion-effect-with-fallbacks@1.2.0': resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} peerDependencies: - react: ^18.3.1 + react: '>=16.8.0' '@emotion/utils@1.4.2': resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} @@ -4493,12 +4682,6 @@ packages: peerDependencies: esbuild: '*' - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.5': resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} engines: {node: '>=18'} @@ -4517,12 +4700,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.5': resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} engines: {node: '>=18'} @@ -4541,12 +4718,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.5': resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} engines: {node: '>=18'} @@ -4565,12 +4736,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.5': resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} engines: {node: '>=18'} @@ -4589,12 +4754,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.5': resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} engines: {node: '>=18'} @@ -4613,12 +4772,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.5': resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} engines: {node: '>=18'} @@ -4637,12 +4790,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.5': resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} engines: {node: '>=18'} @@ -4661,12 +4808,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.5': resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} engines: {node: '>=18'} @@ -4685,12 +4826,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.5': resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} engines: {node: '>=18'} @@ -4709,12 +4844,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.5': resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} engines: {node: '>=18'} @@ -4733,12 +4862,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.5': resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} engines: {node: '>=18'} @@ -4757,12 +4880,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.5': resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} engines: {node: '>=18'} @@ -4781,12 +4898,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.5': resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} engines: {node: '>=18'} @@ -4805,12 +4916,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.5': resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} engines: {node: '>=18'} @@ -4829,12 +4934,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.5': resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} engines: {node: '>=18'} @@ -4853,12 +4952,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.5': resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} engines: {node: '>=18'} @@ -4877,12 +4970,6 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.5': resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} engines: {node: '>=18'} @@ -4913,12 +5000,6 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.5': resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} engines: {node: '>=18'} @@ -4949,12 +5030,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.5': resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} engines: {node: '>=18'} @@ -4979,12 +5054,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.5': resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} engines: {node: '>=18'} @@ -5003,12 +5072,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.5': resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} engines: {node: '>=18'} @@ -5027,12 +5090,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.5': resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} engines: {node: '>=18'} @@ -5051,12 +5108,6 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.5': resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} engines: {node: '>=18'} @@ -8589,11 +8640,6 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.25.5: resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} @@ -13141,37 +13187,6 @@ packages: vite: optional: true - vite@5.4.19: - resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - vite@6.3.5: resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -14556,9 +14571,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@esbuild/aix-ppc64@0.21.5': - optional: true - '@esbuild/aix-ppc64@0.25.5': optional: true @@ -14568,9 +14580,6 @@ snapshots: '@esbuild/android-arm64@0.17.19': optional: true - '@esbuild/android-arm64@0.21.5': - optional: true - '@esbuild/android-arm64@0.25.5': optional: true @@ -14580,9 +14589,6 @@ snapshots: '@esbuild/android-arm@0.17.19': optional: true - '@esbuild/android-arm@0.21.5': - optional: true - '@esbuild/android-arm@0.25.5': optional: true @@ -14592,9 +14598,6 @@ snapshots: '@esbuild/android-x64@0.17.19': optional: true - '@esbuild/android-x64@0.21.5': - optional: true - '@esbuild/android-x64@0.25.5': optional: true @@ -14604,9 +14607,6 @@ snapshots: '@esbuild/darwin-arm64@0.17.19': optional: true - '@esbuild/darwin-arm64@0.21.5': - optional: true - '@esbuild/darwin-arm64@0.25.5': optional: true @@ -14616,9 +14616,6 @@ snapshots: '@esbuild/darwin-x64@0.17.19': optional: true - '@esbuild/darwin-x64@0.21.5': - optional: true - '@esbuild/darwin-x64@0.25.5': optional: true @@ -14628,9 +14625,6 @@ snapshots: '@esbuild/freebsd-arm64@0.17.19': optional: true - '@esbuild/freebsd-arm64@0.21.5': - optional: true - '@esbuild/freebsd-arm64@0.25.5': optional: true @@ -14640,9 +14634,6 @@ snapshots: '@esbuild/freebsd-x64@0.17.19': optional: true - '@esbuild/freebsd-x64@0.21.5': - optional: true - '@esbuild/freebsd-x64@0.25.5': optional: true @@ -14652,9 +14643,6 @@ snapshots: '@esbuild/linux-arm64@0.17.19': optional: true - '@esbuild/linux-arm64@0.21.5': - optional: true - '@esbuild/linux-arm64@0.25.5': optional: true @@ -14664,9 +14652,6 @@ snapshots: '@esbuild/linux-arm@0.17.19': optional: true - '@esbuild/linux-arm@0.21.5': - optional: true - '@esbuild/linux-arm@0.25.5': optional: true @@ -14676,9 +14661,6 @@ snapshots: '@esbuild/linux-ia32@0.17.19': optional: true - '@esbuild/linux-ia32@0.21.5': - optional: true - '@esbuild/linux-ia32@0.25.5': optional: true @@ -14688,9 +14670,6 @@ snapshots: '@esbuild/linux-loong64@0.17.19': optional: true - '@esbuild/linux-loong64@0.21.5': - optional: true - '@esbuild/linux-loong64@0.25.5': optional: true @@ -14700,9 +14679,6 @@ snapshots: '@esbuild/linux-mips64el@0.17.19': optional: true - '@esbuild/linux-mips64el@0.21.5': - optional: true - '@esbuild/linux-mips64el@0.25.5': optional: true @@ -14712,9 +14688,6 @@ snapshots: '@esbuild/linux-ppc64@0.17.19': optional: true - '@esbuild/linux-ppc64@0.21.5': - optional: true - '@esbuild/linux-ppc64@0.25.5': optional: true @@ -14724,9 +14697,6 @@ snapshots: '@esbuild/linux-riscv64@0.17.19': optional: true - '@esbuild/linux-riscv64@0.21.5': - optional: true - '@esbuild/linux-riscv64@0.25.5': optional: true @@ -14736,9 +14706,6 @@ snapshots: '@esbuild/linux-s390x@0.17.19': optional: true - '@esbuild/linux-s390x@0.21.5': - optional: true - '@esbuild/linux-s390x@0.25.5': optional: true @@ -14748,9 +14715,6 @@ snapshots: '@esbuild/linux-x64@0.17.19': optional: true - '@esbuild/linux-x64@0.21.5': - optional: true - '@esbuild/linux-x64@0.25.5': optional: true @@ -14766,9 +14730,6 @@ snapshots: '@esbuild/netbsd-x64@0.17.19': optional: true - '@esbuild/netbsd-x64@0.21.5': - optional: true - '@esbuild/netbsd-x64@0.25.5': optional: true @@ -14784,9 +14745,6 @@ snapshots: '@esbuild/openbsd-x64@0.17.19': optional: true - '@esbuild/openbsd-x64@0.21.5': - optional: true - '@esbuild/openbsd-x64@0.25.5': optional: true @@ -14799,9 +14757,6 @@ snapshots: '@esbuild/sunos-x64@0.17.19': optional: true - '@esbuild/sunos-x64@0.21.5': - optional: true - '@esbuild/sunos-x64@0.25.5': optional: true @@ -14811,9 +14766,6 @@ snapshots: '@esbuild/win32-arm64@0.17.19': optional: true - '@esbuild/win32-arm64@0.21.5': - optional: true - '@esbuild/win32-arm64@0.25.5': optional: true @@ -14823,9 +14775,6 @@ snapshots: '@esbuild/win32-ia32@0.17.19': optional: true - '@esbuild/win32-ia32@0.21.5': - optional: true - '@esbuild/win32-ia32@0.25.5': optional: true @@ -14835,9 +14784,6 @@ snapshots: '@esbuild/win32-x64@0.17.19': optional: true - '@esbuild/win32-x64@0.21.5': - optional: true - '@esbuild/win32-x64@0.25.5': optional: true @@ -16804,10 +16750,10 @@ snapshots: '@types/node': 24.3.0 optional: true - '@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/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)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.41.0(eslint@9.34.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) @@ -18860,32 +18806,6 @@ snapshots: '@esbuild/win32-ia32': 0.17.19 '@esbuild/win32-x64': 0.17.19 - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - esbuild@0.25.5: optionalDependencies: '@esbuild/aix-ppc64': 0.25.5 @@ -18965,12 +18885,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.33.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.34.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)(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-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-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)) @@ -19028,44 +18948,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.34.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.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.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(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.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-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-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)): + 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)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@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) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(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.33.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color @@ -19080,6 +19000,16 @@ 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 @@ -19092,7 +19022,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.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-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)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -19103,7 +19033,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.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)) + 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)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -19115,7 +19045,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.41.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) + '@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 @@ -19150,6 +19080,35 @@ 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 @@ -24775,16 +24734,6 @@ snapshots: - supports-color - typescript - vite@5.4.19(@types/node@24.3.0)(terser@5.43.1): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.6 - rollup: 4.48.0 - optionalDependencies: - '@types/node': 24.3.0 - fsevents: 2.3.3 - terser: 5.43.1 - vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 From 41e89eb803edb4a292284bcbc9417ac4a7d38a24 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 20:50:43 +0300 Subject: [PATCH 13/56] fix: progress on example --- examples/lit-ts/index.html | 14 +--- examples/lit-ts/package.json | 3 +- examples/lit-ts/pages/accordion.html | 17 ---- examples/lit-ts/src/accordion.ts | 40 ---------- examples/lit-ts/src/main.css | 4 + examples/lit-ts/src/main.ts | 105 +++++++++++++++++++++++++ examples/lit-ts/src/pages/accordion.ts | 65 +++++++++++++++ pnpm-lock.yaml | 8 ++ 8 files changed, 188 insertions(+), 68 deletions(-) delete mode 100644 examples/lit-ts/pages/accordion.html delete mode 100644 examples/lit-ts/src/accordion.ts create mode 100644 examples/lit-ts/src/main.css create mode 100644 examples/lit-ts/src/main.ts create mode 100644 examples/lit-ts/src/pages/accordion.ts diff --git a/examples/lit-ts/index.html b/examples/lit-ts/index.html index 6b59e2ad17..c240aa0398 100644 --- a/examples/lit-ts/index.html +++ b/examples/lit-ts/index.html @@ -4,16 +4,10 @@ - Vite + Lit + Zag.js + Lit - -

Lit + Zag

- - Accordion - Avatar - Avatar with Controller - Popover - Checkbox - Combobox + + + diff --git a/examples/lit-ts/package.json b/examples/lit-ts/package.json index 77c9201fb0..e23f3b900e 100644 --- a/examples/lit-ts/package.json +++ b/examples/lit-ts/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --port 3004", "build": "vite build", "preview": "vite preview" }, @@ -90,6 +90,7 @@ "@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/pages/accordion.html b/examples/lit-ts/pages/accordion.html deleted file mode 100644 index 313d365ebd..0000000000 --- a/examples/lit-ts/pages/accordion.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - Vite + TS - - -

Accordion

- - - - - - diff --git a/examples/lit-ts/src/accordion.ts b/examples/lit-ts/src/accordion.ts deleted file mode 100644 index 1425a3da3d..0000000000 --- a/examples/lit-ts/src/accordion.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { spread } from "@open-wc/lit-helpers" -import * as accordion from "@zag-js/accordion" -import { accordionData } from "@zag-js/shared" -import style from "@zag-js/shared/src/css/accordion.css?inline" -import { ZagController, normalizeProps } from "@zag-js/lit" -import { LitElement, html, unsafeCSS } from "lit" -import { customElement } from "lit/decorators.js" -import { nanoid } from "nanoid" - -@customElement("accordion-element") -export class Accordion extends LitElement { - private zag = new ZagController(this, accordion.machine, () => ({ id: nanoid() })) - - static styles = unsafeCSS(style) - - render() { - const api = accordion.connect(this.zag.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/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..499598b053 --- /dev/null +++ b/examples/lit-ts/src/main.ts @@ -0,0 +1,105 @@ +import { LitElement, css, html } from "lit" +import { customElement, property } from "lit/decorators.js" +import { routesData } from "@zag-js/shared" +// import style from "@zag-js/shared/src/css/layout.css?inline" +import "@zag-js/shared/src/style.css" +import "./main.css" + +// Import all page components +import "./pages/accordion" + +@customElement("zag-app") +export class ZagApp extends LitElement { + // Light dom (no shadow root) due to css + protected createRenderRoot() { + return this + } + + // static style = css` + // .component-page { + // flex: auto; + // display: flex; + // } + // ` + + @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`` + 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"].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..cdcc01a94b --- /dev/null +++ b/examples/lit-ts/src/pages/accordion.ts @@ -0,0 +1,65 @@ +import { LitElement, 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 { ZagController, normalizeProps } from "@zag-js/lit" +import { ArrowRight, createElement } from "lucide" +import { nanoid } from "nanoid" + +@customElement("accordion-page") +export class AccordionPage extends LitElement { + private zagController = new ZagController(this, accordion.machine, () => ({ + id: nanoid(), + // Add some basic controls for testing + collapsible: false, + multiple: false, + })) + + static styles = unsafeCSS(styleLayout + "\n" + styleComponent) + + // Light dom (no shadow root) + protected createRenderRoot() { + return this + } + + render() { + const api = accordion.connect(this.zagController.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. +
+
+ `, + )} +
+
+ +
+
+

Controls

+

Dynamic controls coming soon...

+
+
+

State

+
${JSON.stringify(this.zagController.service.state.get(), null, 2)}
+
+
+ ` + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 733b799875..be3c20e347 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -362,6 +362,9 @@ importers: 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 @@ -10341,6 +10344,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'} @@ -20978,6 +20984,8 @@ snapshots: dependencies: vue: 3.5.20(typescript@5.9.2) + lucide@0.542.0: {} + luxon@3.7.1: {} lz-string@1.5.0: {} From 2a8b8d95704621526bad585cbda5da7fcfd9a1d3 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 21:36:10 +0300 Subject: [PATCH 14/56] fix: example page styling --- examples/lit-ts/src/pages/accordion.ts | 11 +++-------- examples/lit-ts/src/pages/page.css | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 examples/lit-ts/src/pages/page.css diff --git a/examples/lit-ts/src/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts index cdcc01a94b..099c1e8414 100644 --- a/examples/lit-ts/src/pages/accordion.ts +++ b/examples/lit-ts/src/pages/accordion.ts @@ -5,26 +5,21 @@ 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 { ZagController, normalizeProps } from "@zag-js/lit" import { ArrowRight, createElement } from "lucide" import { nanoid } from "nanoid" @customElement("accordion-page") export class AccordionPage extends LitElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + private zagController = new ZagController(this, accordion.machine, () => ({ id: nanoid(), - // Add some basic controls for testing collapsible: false, multiple: false, })) - static styles = unsafeCSS(styleLayout + "\n" + styleComponent) - - // Light dom (no shadow root) - protected createRenderRoot() { - return this - } - render() { const api = accordion.connect(this.zagController.service, normalizeProps) diff --git a/examples/lit-ts/src/pages/page.css b/examples/lit-ts/src/pages/page.css new file mode 100644 index 0000000000..822b667ced --- /dev/null +++ b/examples/lit-ts/src/pages/page.css @@ -0,0 +1,15 @@ +:host { + outline: none !important; +} + +/* Copied from shared/src/css/layout.css (.page main) */ +:host main { + flex: auto; + display: flex; + gap: 10px; + position: relative; + flex-direction: column; + align-items: flex-start; + padding: 40px; + overflow-y: auto; +} From 0ade4287d31eee45beec6b9b2ebd134fa204bd66 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 22:11:55 +0300 Subject: [PATCH 15/56] fix: initial working controls --- examples/lit-ts/src/components/toolbar.ts | 156 ++++++++++++++++++ .../lit-ts/src/lib/controls-controller.ts | 46 ++++++ examples/lit-ts/src/pages/accordion.ts | 18 +- 3 files changed, 208 insertions(+), 12 deletions(-) create mode 100644 examples/lit-ts/src/components/toolbar.ts create mode 100644 examples/lit-ts/src/lib/controls-controller.ts diff --git a/examples/lit-ts/src/components/toolbar.ts b/examples/lit-ts/src/components/toolbar.ts new file mode 100644 index 0000000000..e2ca4be6db --- /dev/null +++ b/examples/lit-ts/src/components/toolbar.ts @@ -0,0 +1,156 @@ +import { LitElement, html, unsafeCSS } from "lit" +import { customElement, property, state } from "lit/decorators.js" +import type { Service } from "@zag-js/core" +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 + + @property({ attribute: false }) + service?: Service + + @state() + private activeTab = 0 + + render() { + const hasControls = !!this.controls + + return html` +
+ + +
+ ${hasControls + ? html`
${this.renderControls()}
` + : ""} +
+ ${this.renderStateVisualizer()} +
+
+
+ ` + } + + private renderControls() { + if (!this.controls) return "" + + return html` +
+ ${this.controls.getControlKeys().map((key) => { + const config = this.controls!.getControlConfig(key) + const value = this.controls!.getValue(key) + const { type, label = key, options, placeholder, min, max } = config + + switch (type) { + case "boolean": + return html` +
+ { + const target = e.target as HTMLInputElement + this.controls!.setState(key, target.checked) + }} + /> + +
+ ` + case "string": + return html` +
+ + { + if (e.key === "Enter") { + const target = e.target as HTMLInputElement + this.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) + this.controls!.setState(key, isNaN(val) ? 0 : val) + } + }} + /> +
+ ` + default: + return "" + } + })} +
+ ` + } + + private renderStateVisualizer() { + if (!this.service) { + return html`
No service available
` + } + + return html` +
+

State

+
${JSON.stringify(this.service.state.get(), null, 2)}
+
+ ` + } +} 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/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts index 099c1e8414..cdd505f8b7 100644 --- a/examples/lit-ts/src/pages/accordion.ts +++ b/examples/lit-ts/src/pages/accordion.ts @@ -9,15 +9,18 @@ import stylePage from "./page.css?inline" import { ZagController, normalizeProps } from "@zag-js/lit" import { ArrowRight, createElement } from "lucide" import { nanoid } from "nanoid" +import { ControlsController } from "../lib/controls-controller" +import "../components/toolbar" @customElement("accordion-page") export class AccordionPage extends LitElement { static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + private controls = new ControlsController(this, accordionControls) + private zagController = new ZagController(this, accordion.machine, () => ({ id: nanoid(), - collapsible: false, - multiple: false, + ...this.controls.context, })) render() { @@ -45,16 +48,7 @@ export class AccordionPage extends LitElement {
-
-
-

Controls

-

Dynamic controls coming soon...

-
-
-

State

-
${JSON.stringify(this.zagController.service.state.get(), null, 2)}
-
-
+ ` } } From 52f697253255b98a34d16807e3946033fa0dd657 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 23:16:49 +0300 Subject: [PATCH 16/56] feat: initial working visualizer --- .../lit-ts/src/components/state-visualizer.ts | 52 +++++++++++++++++++ examples/lit-ts/src/components/toolbar.ts | 12 +---- 2 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 examples/lit-ts/src/components/state-visualizer.ts 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..3cdc7e84b0 --- /dev/null +++ b/examples/lit-ts/src/components/state-visualizer.ts @@ -0,0 +1,52 @@ +import { LitElement, html } 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" + +@customElement("state-visualizer") +export class StateVisualizer extends LitElement { + protected createRenderRoot() { + return this + } + + @property({ attribute: false }) + state!: Service + + @property({ type: String }) + label?: string + + @property({ attribute: false }) + omit?: string[] + + @property({ attribute: false }) + context?: Array + + render() { + if (!this.state) { + return html`
No state available
` + } + + const obj = { + state: this.state.state.get(), + event: this.state.event.current(), + previousEvent: this.state.event.previous(), + context: this.context + ? Object.fromEntries(this.context.map((key) => [key, this.state.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 index e2ca4be6db..ba54d0bcbf 100644 --- a/examples/lit-ts/src/components/toolbar.ts +++ b/examples/lit-ts/src/components/toolbar.ts @@ -3,6 +3,7 @@ import { customElement, property, state } from "lit/decorators.js" import type { Service } from "@zag-js/core" import styleLayout from "@zag-js/shared/src/css/layout.css?inline" import type { ControlsController } from "../lib/controls-controller" +import "./state-visualizer" @customElement("zag-toolbar") export class Toolbar extends LitElement { @@ -142,15 +143,6 @@ export class Toolbar extends LitElement { } private renderStateVisualizer() { - if (!this.service) { - return html`
No service available
` - } - - return html` -
-

State

-
${JSON.stringify(this.service.state.get(), null, 2)}
-
- ` + return html`` } } From 99dcb51eebb633dee989abfad1ce0c6b216a0085 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 23:30:26 +0300 Subject: [PATCH 17/56] fix: typescript tweaks --- .../lit-ts/src/components/state-visualizer.ts | 14 ++++++++------ examples/lit-ts/src/components/toolbar.ts | 17 +++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/examples/lit-ts/src/components/state-visualizer.ts b/examples/lit-ts/src/components/state-visualizer.ts index 3cdc7e84b0..50568262ce 100644 --- a/examples/lit-ts/src/components/state-visualizer.ts +++ b/examples/lit-ts/src/components/state-visualizer.ts @@ -11,7 +11,7 @@ export class StateVisualizer extends LitElement { } @property({ attribute: false }) - state!: Service + state?: Service @property({ type: String }) label?: string @@ -23,16 +23,18 @@ export class StateVisualizer extends LitElement { context?: Array render() { - if (!this.state) { + const service = this.state + + if (!service) { return html`
No state available
` } const obj = { - state: this.state.state.get(), - event: this.state.event.current(), - previousEvent: this.state.event.previous(), + state: service.state.get(), + event: service.event.current(), + previousEvent: service.event.previous(), context: this.context - ? Object.fromEntries(this.context.map((key) => [key, this.state.context.get(key)])) + ? Object.fromEntries(this.context.map((key) => [key, service.context.get(key)])) : undefined, } diff --git a/examples/lit-ts/src/components/toolbar.ts b/examples/lit-ts/src/components/toolbar.ts index ba54d0bcbf..6aa78272ef 100644 --- a/examples/lit-ts/src/components/toolbar.ts +++ b/examples/lit-ts/src/components/toolbar.ts @@ -50,13 +50,14 @@ export class Toolbar extends LitElement { } private renderControls() { - if (!this.controls) return "" + const { controls } = this + if (!controls) return "" return html`
- ${this.controls.getControlKeys().map((key) => { - const config = this.controls!.getControlConfig(key) - const value = this.controls!.getValue(key) + ${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) { @@ -70,7 +71,7 @@ export class Toolbar extends LitElement { .checked=${value} @change=${(e: Event) => { const target = e.target as HTMLInputElement - this.controls!.setState(key, target.checked) + controls.setState(key, target.checked) }} /> @@ -89,7 +90,7 @@ export class Toolbar extends LitElement { @keydown=${(e: KeyboardEvent) => { if (e.key === "Enter") { const target = e.target as HTMLInputElement - this.controls!.setState(key, target.value) + controls.setState(key, target.value) } }} /> @@ -105,7 +106,7 @@ export class Toolbar extends LitElement { .value=${value} @change=${(e: Event) => { const target = e.target as HTMLSelectElement - this.controls!.setState(key, target.value) + controls.setState(key, target.value) }} > @@ -128,7 +129,7 @@ export class Toolbar extends LitElement { if (e.key === "Enter") { const target = e.target as HTMLInputElement const val = parseFloat(target.value) - this.controls!.setState(key, isNaN(val) ? 0 : val) + controls.setState(key, isNaN(val) ? 0 : val) } }} /> From df2388444072e575b58b1ab910fc923180fb0747 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 23:57:41 +0300 Subject: [PATCH 18/56] fix: use slot for state-visualizer --- examples/lit-ts/src/components/toolbar.ts | 11 +---------- examples/lit-ts/src/main.ts | 15 ++++++--------- examples/lit-ts/src/pages/accordion.ts | 5 +++-- shared/src/css/layout.css | 6 +++--- 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/examples/lit-ts/src/components/toolbar.ts b/examples/lit-ts/src/components/toolbar.ts index 6aa78272ef..0bf4d47bd0 100644 --- a/examples/lit-ts/src/components/toolbar.ts +++ b/examples/lit-ts/src/components/toolbar.ts @@ -1,9 +1,7 @@ import { LitElement, html, unsafeCSS } from "lit" import { customElement, property, state } from "lit/decorators.js" -import type { Service } from "@zag-js/core" import styleLayout from "@zag-js/shared/src/css/layout.css?inline" import type { ControlsController } from "../lib/controls-controller" -import "./state-visualizer" @customElement("zag-toolbar") export class Toolbar extends LitElement { @@ -12,9 +10,6 @@ export class Toolbar extends LitElement { @property({ attribute: false }) controls?: ControlsController - @property({ attribute: false }) - service?: Service - @state() private activeTab = 0 @@ -42,7 +37,7 @@ export class Toolbar extends LitElement { ? html`
${this.renderControls()}
` : ""}
- ${this.renderStateVisualizer()} +
@@ -142,8 +137,4 @@ export class Toolbar extends LitElement {
` } - - private renderStateVisualizer() { - return html`` - } } diff --git a/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts index 499598b053..7a8ad8cdeb 100644 --- a/examples/lit-ts/src/main.ts +++ b/examples/lit-ts/src/main.ts @@ -1,10 +1,14 @@ -import { LitElement, css, html } from "lit" +import { LitElement, html } from "lit" import { customElement, property } from "lit/decorators.js" import { routesData } from "@zag-js/shared" -// import style from "@zag-js/shared/src/css/layout.css?inline" + 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" @@ -15,13 +19,6 @@ export class ZagApp extends LitElement { return this } - // static style = css` - // .component-page { - // flex: auto; - // display: flex; - // } - // ` - @property({ type: String }) currentPath = window.location.pathname diff --git a/examples/lit-ts/src/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts index cdd505f8b7..4b61807bbe 100644 --- a/examples/lit-ts/src/pages/accordion.ts +++ b/examples/lit-ts/src/pages/accordion.ts @@ -10,7 +10,6 @@ import { ZagController, normalizeProps } from "@zag-js/lit" import { ArrowRight, createElement } from "lucide" import { nanoid } from "nanoid" import { ControlsController } from "../lib/controls-controller" -import "../components/toolbar" @customElement("accordion-page") export class AccordionPage extends LitElement { @@ -48,7 +47,9 @@ export class AccordionPage extends LitElement {
- + + + ` } } diff --git a/shared/src/css/layout.css b/shared/src/css/layout.css index df9e5dee71..9327115bea 100644 --- a/shared/src/css/layout.css +++ b/shared/src/css/layout.css @@ -192,7 +192,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 +201,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); } From 0e4071dd6590b06b6a779d29424fd55f88bd22ae Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Wed, 3 Sep 2025 23:58:20 +0300 Subject: [PATCH 19/56] refactor: add :host main rule to layout.css --- examples/lit-ts/src/pages/page.css | 12 ------------ shared/src/css/layout.css | 3 ++- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/examples/lit-ts/src/pages/page.css b/examples/lit-ts/src/pages/page.css index 822b667ced..45d77b862f 100644 --- a/examples/lit-ts/src/pages/page.css +++ b/examples/lit-ts/src/pages/page.css @@ -1,15 +1,3 @@ :host { outline: none !important; } - -/* Copied from shared/src/css/layout.css (.page main) */ -:host main { - flex: auto; - display: flex; - gap: 10px; - position: relative; - flex-direction: column; - align-items: flex-start; - padding: 40px; - overflow-y: auto; -} diff --git a/shared/src/css/layout.css b/shared/src/css/layout.css index 9327115bea..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; From c046e4cdb6960a6c09cc77dcbb7810e0ea586255 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 00:01:35 +0300 Subject: [PATCH 20/56] fix: use shadow dom for StateVisualizer --- examples/lit-ts/src/components/state-visualizer.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/lit-ts/src/components/state-visualizer.ts b/examples/lit-ts/src/components/state-visualizer.ts index 50568262ce..b7cbf9e958 100644 --- a/examples/lit-ts/src/components/state-visualizer.ts +++ b/examples/lit-ts/src/components/state-visualizer.ts @@ -1,14 +1,13 @@ -import { LitElement, html } from "lit" +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 { - protected createRenderRoot() { - return this - } + static styles = unsafeCSS(styleLayout) @property({ attribute: false }) state?: Service From f973d8a10d36b7fdd0ab4d7650bf45e62b2c478a Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 10:44:11 +0300 Subject: [PATCH 21/56] test: wip a11y testing --- e2e/_utils.ts | 117 ++++++++++++++++++++++++- e2e/models/accordion.model.ts | 14 ++- examples/lit-ts/src/pages/accordion.ts | 20 +++-- 3 files changed, 140 insertions(+), 11 deletions(-) diff --git a/e2e/_utils.ts b/e2e/_utils.ts index 3ea3d7470d..5ed3bd5d86 100644 --- a/e2e/_utils.ts +++ b/e2e/_utils.ts @@ -1,19 +1,132 @@ import AxeBuilder from "@axe-core/playwright" import { expect, type Locator, type Page } from "@playwright/test" +import { AxeResults, type RunOptions, type ElementContext } from "axe-core" +import { run } from "axe-core" +// Types and Framework Context Detection +export type FrameworkContext = "shadow-dom" | "light-dom" + +// The context is determined by an environment variable at test runtime +// Example: `TEST_FRAMEWORK=shadow-dom npx playwright test` or `FRAMEWORK=lit npx playwright test` +export const FRAMEWORK_CONTEXT: FrameworkContext = + (process.env.TEST_FRAMEWORK as FrameworkContext) || (process.env.FRAMEWORK === "lit" ? "shadow-dom" : "light-dom") + +console.log(`Running E2E tests in '${FRAMEWORK_CONTEXT}' context.`) + +// Augment the window object for type safety in page.evaluate +declare global { + interface Window { + axe: { + run: typeof run + } + // axe: { + // run: (context?: ElementContext, options?: RunOptions) => Promise + // } + } +} + +/** + * 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 (FRAMEWORK_CONTEXT === "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]") + */ export async function a11y(page: Page, selector = "[data-part=root]") { + // if (FRAMEWORK_CONTEXT === "shadow-dom") { + // // For Shadow DOM, do a full-page scan since AxeBuilder.include() + // // has issues finding elements across shadow boundaries + // const accessibilityScanResults = await new AxeBuilder({ page: page as any }) + // .disableRules(["color-contrast"]) + // .analyze() + + // expect(accessibilityScanResults.violations).toEqual([]) + // } else { + // // For Light DOM, use the original scoped approach + // } await page.waitForSelector(selector) - const results = await new AxeBuilder({ page: page as any }) + const accessibilityScanResults = await new AxeBuilder({ page: page as any }) .disableRules(["color-contrast"]) .include(selector) .analyze() - expect(results.violations).toEqual([]) + expect(accessibilityScanResults.violations).toEqual([]) +} + +/** + * Performs a targeted accessibility scan on the shadow root of a specific component. + * @param page The Playwright Page object + * @param hostSelector A CSS selector for the shadow host element + */ +export async function a11yInShadow(page: Page, hostSelector: string | undefined, selector = "[data-part=root]") { + // // Inject axe-core library into the page + // await page.addScriptTag({ + // path: require.resolve("axe-core/axe.min.js"), + // }) + + // const results = await page.evaluate((selector) => { + // const host = document.querySelector(selector) + // if (!host || !host.shadowRoot) { + // throw new Error(`Host element '${selector}' not found or has no shadowRoot.`) + // } + + // // Run axe directly on the shadowRoot with minimal configuration + // return window.axe.run(host.shadowRoot) + // }, hostSelector) + + // expect(results.violations).toEqual([]) + + await page.waitForSelector(selector) + + let selection = new AxeBuilder({ page: page as any }).disableRules(["color-contrast"]) + + if (hostSelector) { + selection = selection.include(hostSelector) + } + if (selector) { + selection = selection.include(selector) + } + + const accessibilityScanResults = await selection.analyze() + + expect(accessibilityScanResults.violations).toEqual([]) } export const testid = (part: string) => `[data-testid=${esc(part)}]` +/** + * Combines host selector and target selector for framework-aware locators. + * @param host Optional host selector (e.g., 'accordion-page'). If undefined, returns just the target. + * @param target The target selector (e.g., '[data-testid="about:trigger"]') + * @returns Combined selector string + */ +export function withHost(componentHost: string | undefined, target: string): string { + return FRAMEWORK_CONTEXT === "shadow-dom" && componentHost ? `${componentHost} ${target}` : target +} + export const controls = (page: Page) => { return { num: async (id: string, value: string) => { diff --git a/e2e/models/accordion.model.ts b/e2e/models/accordion.model.ts index 4ed95274a2..2145f5e016 100644 --- a/e2e/models/accordion.model.ts +++ b/e2e/models/accordion.model.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test" -import { a11y, testid } from "../_utils" +import { a11y, a11yInShadow, testid, withHost, FRAMEWORK_CONTEXT } from "../_utils" import { Model } from "./model" export class AccordionModel extends Model { @@ -12,15 +12,21 @@ export class AccordionModel extends Model { } checkAccessibility(selector?: string): Promise { - return a11y(this.page, selector) + if (FRAMEWORK_CONTEXT === "shadow-dom") { + // For Shadow DOM, use the specialized shadow root accessibility scan + return a11yInShadow(this.page, "accordion-page") + } else { + // For Light DOM, use the original scoped approach + return a11y(this.page, selector) + } } getTrigger(id: string) { - return this.page.locator(testid(`${id}:trigger`)) + return this.page.locator(withHost("accordion-page", testid(`${id}:trigger`))) } getContent(id: string) { - return this.page.locator(testid(`${id}:content`)) + return this.page.locator(withHost("accordion-page", testid(`${id}:content`))) } async focusTrigger(id: string) { diff --git a/examples/lit-ts/src/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts index 4b61807bbe..94c3decf53 100644 --- a/examples/lit-ts/src/pages/accordion.ts +++ b/examples/lit-ts/src/pages/accordion.ts @@ -27,17 +27,27 @@ export class AccordionPage extends LitElement { 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.
From 251629aa8e48cfd4e97d8be45d0922216cffa4cb Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 11:54:01 +0300 Subject: [PATCH 22/56] test: working shadow-dom a11y testing --- e2e/_utils.ts | 63 ++--------------------------------- e2e/models/accordion.model.ts | 16 ++++----- 2 files changed, 9 insertions(+), 70 deletions(-) diff --git a/e2e/_utils.ts b/e2e/_utils.ts index 5ed3bd5d86..9163fbf6c3 100644 --- a/e2e/_utils.ts +++ b/e2e/_utils.ts @@ -1,7 +1,5 @@ import AxeBuilder from "@axe-core/playwright" import { expect, type Locator, type Page } from "@playwright/test" -import { AxeResults, type RunOptions, type ElementContext } from "axe-core" -import { run } from "axe-core" // Types and Framework Context Detection export type FrameworkContext = "shadow-dom" | "light-dom" @@ -13,18 +11,6 @@ export const FRAMEWORK_CONTEXT: FrameworkContext = console.log(`Running E2E tests in '${FRAMEWORK_CONTEXT}' context.`) -// Augment the window object for type safety in page.evaluate -declare global { - interface Window { - axe: { - run: typeof run - } - // axe: { - // run: (context?: ElementContext, options?: RunOptions) => Promise - // } - } -} - /** * 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. @@ -53,57 +39,14 @@ export function getPart(parent: Page | Locator, hostSelector: string, partName: * 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]") { - // if (FRAMEWORK_CONTEXT === "shadow-dom") { - // // For Shadow DOM, do a full-page scan since AxeBuilder.include() - // // has issues finding elements across shadow boundaries - // const accessibilityScanResults = await new AxeBuilder({ page: page as any }) - // .disableRules(["color-contrast"]) - // .analyze() - - // expect(accessibilityScanResults.violations).toEqual([]) - // } else { - // // For Light DOM, use the original scoped approach - // } - await page.waitForSelector(selector) - - const accessibilityScanResults = await new AxeBuilder({ page: page as any }) - .disableRules(["color-contrast"]) - .include(selector) - .analyze() - - expect(accessibilityScanResults.violations).toEqual([]) -} - -/** - * Performs a targeted accessibility scan on the shadow root of a specific component. - * @param page The Playwright Page object - * @param hostSelector A CSS selector for the shadow host element - */ -export async function a11yInShadow(page: Page, hostSelector: string | undefined, selector = "[data-part=root]") { - // // Inject axe-core library into the page - // await page.addScriptTag({ - // path: require.resolve("axe-core/axe.min.js"), - // }) - - // const results = await page.evaluate((selector) => { - // const host = document.querySelector(selector) - // if (!host || !host.shadowRoot) { - // throw new Error(`Host element '${selector}' not found or has no shadowRoot.`) - // } - - // // Run axe directly on the shadowRoot with minimal configuration - // return window.axe.run(host.shadowRoot) - // }, hostSelector) - - // expect(results.violations).toEqual([]) - +export async function a11y(page: Page, selector = "[data-part=root]", hostSelector?: string) { await page.waitForSelector(selector) let selection = new AxeBuilder({ page: page as any }).disableRules(["color-contrast"]) - if (hostSelector) { + if (hostSelector && FRAMEWORK_CONTEXT === "shadow-dom") { selection = selection.include(hostSelector) } if (selector) { diff --git a/e2e/models/accordion.model.ts b/e2e/models/accordion.model.ts index 2145f5e016..56db0f127a 100644 --- a/e2e/models/accordion.model.ts +++ b/e2e/models/accordion.model.ts @@ -1,7 +1,9 @@ import { expect, type Page } from "@playwright/test" -import { a11y, a11yInShadow, testid, withHost, FRAMEWORK_CONTEXT } from "../_utils" +import { a11y, testid, withHost } from "../_utils" import { Model } from "./model" +const shadowHost = "accordion-page" + export class AccordionModel extends Model { constructor(public page: Page) { super(page) @@ -12,21 +14,15 @@ export class AccordionModel extends Model { } checkAccessibility(selector?: string): Promise { - if (FRAMEWORK_CONTEXT === "shadow-dom") { - // For Shadow DOM, use the specialized shadow root accessibility scan - return a11yInShadow(this.page, "accordion-page") - } else { - // For Light DOM, use the original scoped approach - return a11y(this.page, selector) - } + return a11y(this.page, selector, shadowHost) } getTrigger(id: string) { - return this.page.locator(withHost("accordion-page", testid(`${id}:trigger`))) + return this.page.locator(withHost(shadowHost, testid(`${id}:trigger`))) } getContent(id: string) { - return this.page.locator(withHost("accordion-page", testid(`${id}:content`))) + return this.page.locator(withHost(shadowHost, testid(`${id}:content`))) } async focusTrigger(id: string) { From f1d1c20e257671ced1bc30e4a491d99097454568 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 12:20:01 +0300 Subject: [PATCH 23/56] fix: implement dom mode switch for lit-ts --- e2e/_utils.ts | 22 +++++++++++--------- examples/lit-ts/src/lib/page-element.ts | 27 +++++++++++++++++++++++++ examples/lit-ts/src/pages/accordion.ts | 5 +++-- 3 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 examples/lit-ts/src/lib/page-element.ts diff --git a/e2e/_utils.ts b/e2e/_utils.ts index 9163fbf6c3..59aeda1f92 100644 --- a/e2e/_utils.ts +++ b/e2e/_utils.ts @@ -2,14 +2,18 @@ import AxeBuilder from "@axe-core/playwright" import { expect, type Locator, type Page } from "@playwright/test" // Types and Framework Context Detection -export type FrameworkContext = "shadow-dom" | "light-dom" +export type DomMode = "shadow-dom" | "light-dom" -// The context is determined by an environment variable at test runtime -// Example: `TEST_FRAMEWORK=shadow-dom npx playwright test` or `FRAMEWORK=lit npx playwright test` -export const FRAMEWORK_CONTEXT: FrameworkContext = - (process.env.TEST_FRAMEWORK as FrameworkContext) || (process.env.FRAMEWORK === "lit" ? "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 '${FRAMEWORK_CONTEXT}' context.`) +console.log(`Running E2E tests in '${DOM_MODE}' context.`) /** * Context-aware utility to locate a component part defined by a 'part' or 'data-part' attribute. @@ -24,7 +28,7 @@ export function getPart(parent: Page | Locator, hostSelector: string, partName: // Use [part="..."] for CSS Shadow Parts standard, fallback to [data-part="..."] const partSelector = `[part="${partName}"], [data-part="${partName}"]` - if (FRAMEWORK_CONTEXT === "shadow-dom") { + 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}`) @@ -46,7 +50,7 @@ export async function a11y(page: Page, selector = "[data-part=root]", hostSelect let selection = new AxeBuilder({ page: page as any }).disableRules(["color-contrast"]) - if (hostSelector && FRAMEWORK_CONTEXT === "shadow-dom") { + if (hostSelector && DOM_MODE === "shadow-dom") { selection = selection.include(hostSelector) } if (selector) { @@ -67,7 +71,7 @@ export const testid = (part: string) => `[data-testid=${esc(part)}]` * @returns Combined selector string */ export function withHost(componentHost: string | undefined, target: string): string { - return FRAMEWORK_CONTEXT === "shadow-dom" && componentHost ? `${componentHost} ${target}` : target + return DOM_MODE === "shadow-dom" && componentHost ? `${componentHost} ${target}` : target } export const controls = (page: Page) => { 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/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts index 94c3decf53..26d0c3535c 100644 --- a/examples/lit-ts/src/pages/accordion.ts +++ b/examples/lit-ts/src/pages/accordion.ts @@ -1,4 +1,4 @@ -import { LitElement, html, unsafeCSS } from "lit" +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" @@ -10,9 +10,10 @@ import { ZagController, 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 LitElement { +export class AccordionPage extends PageElement { static styles = unsafeCSS(styleComponent + styleLayout + stylePage) private controls = new ControlsController(this, accordionControls) From 760e8e0b00e0dcf10314c1edd515137707efd98c Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 12:46:10 +0300 Subject: [PATCH 24/56] fix: handle shadow dom root node --- examples/lit-ts/src/pages/accordion.ts | 1 + packages/frameworks/lit/src/zag-controller.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/lit-ts/src/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts index 26d0c3535c..e112268820 100644 --- a/examples/lit-ts/src/pages/accordion.ts +++ b/examples/lit-ts/src/pages/accordion.ts @@ -19,6 +19,7 @@ export class AccordionPage extends PageElement { private controls = new ControlsController(this, accordionControls) private zagController = new ZagController(this, accordion.machine, () => ({ + getRootNode: () => this.shadowRoot, id: nanoid(), ...this.controls.context, })) diff --git a/packages/frameworks/lit/src/zag-controller.ts b/packages/frameworks/lit/src/zag-controller.ts index e36b53c317..da7e9bc178 100644 --- a/packages/frameworks/lit/src/zag-controller.ts +++ b/packages/frameworks/lit/src/zag-controller.ts @@ -8,7 +8,7 @@ export class ZagController implements ReactiveCon constructor( private host: ReactiveControllerHost, machineConfig: Machine, - getProps?: () => Partial, + getProps?: () => Partial & { getRootNode?: () => ShadowRoot | Document | Node }, ) { this.machine = new LitMachine(machineConfig, getProps) From ee791cba68feefa08b68be1d966c4fccbf75d876 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 12:53:10 +0300 Subject: [PATCH 25/56] fix: remove unused (css) parts from accordion --- examples/lit-ts/src/pages/accordion.ts | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/examples/lit-ts/src/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts index e112268820..cab133fa0b 100644 --- a/examples/lit-ts/src/pages/accordion.ts +++ b/examples/lit-ts/src/pages/accordion.ts @@ -29,27 +29,17 @@ export class AccordionPage extends PageElement { 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.
From 2db59771d880c70d94bf5b81cde4f7f74038ce03 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 15:34:40 +0300 Subject: [PATCH 26/56] fix(@zag-js/lit): include undefined prop values --- packages/frameworks/lit/src/normalize-props.ts | 5 ----- packages/frameworks/lit/src/zag-controller.ts | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/frameworks/lit/src/normalize-props.ts b/packages/frameworks/lit/src/normalize-props.ts index 0c386c5034..8222a1f2f8 100644 --- a/packages/frameworks/lit/src/normalize-props.ts +++ b/packages/frameworks/lit/src/normalize-props.ts @@ -104,11 +104,6 @@ export const normalizeProps = createNormalizer((props: Dict) => { for (let key in props) { const value = props[key] - // Skip undefined values - if (value === undefined) { - continue - } - // Handle style objects if (key === "style" && isObject(value)) { normalized["style"] = toStyleString(value) diff --git a/packages/frameworks/lit/src/zag-controller.ts b/packages/frameworks/lit/src/zag-controller.ts index da7e9bc178..d97133e3b1 100644 --- a/packages/frameworks/lit/src/zag-controller.ts +++ b/packages/frameworks/lit/src/zag-controller.ts @@ -8,7 +8,7 @@ export class ZagController implements ReactiveCon constructor( private host: ReactiveControllerHost, machineConfig: Machine, - getProps?: () => Partial & { getRootNode?: () => ShadowRoot | Document | Node }, + getProps?: () => Partial & { getRootNode?: () => ShadowRoot | Document | Node | null }, ) { this.machine = new LitMachine(machineConfig, getProps) From caddf80a576cc4bf0eebcc692579f9216674b04f Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 16:26:19 +0300 Subject: [PATCH 27/56] feat(lit-ts): wip dialog, menu, toggle-group, toggle --- e2e/models/dialog.model.ts | 14 ++-- e2e/models/menu.model.ts | 20 +++--- e2e/models/toggle-group.model.ts | 10 ++- examples/lit-ts/src/main.ts | 17 ++++- examples/lit-ts/src/pages/dialog-nested.ts | 81 ++++++++++++++++++++++ examples/lit-ts/src/pages/dialog.ts | 53 ++++++++++++++ examples/lit-ts/src/pages/menu.ts | 56 +++++++++++++++ examples/lit-ts/src/pages/toggle-group.ts | 48 +++++++++++++ examples/lit-ts/src/pages/toggle.ts | 37 ++++++++++ 9 files changed, 318 insertions(+), 18 deletions(-) create mode 100644 examples/lit-ts/src/pages/dialog-nested.ts create mode 100644 examples/lit-ts/src/pages/dialog.ts create mode 100644 examples/lit-ts/src/pages/menu.ts create mode 100644 examples/lit-ts/src/pages/toggle-group.ts create mode 100644 examples/lit-ts/src/pages/toggle.ts diff --git a/e2e/models/dialog.model.ts b/e2e/models/dialog.model.ts index e2b369350f..76f32d17c4 100644 --- a/e2e/models/dialog.model.ts +++ b/e2e/models/dialog.model.ts @@ -1,7 +1,9 @@ import { expect, type Page } from "@playwright/test" -import { a11y } from "../_utils" +import { a11y, testid, withHost } from "../_utils" import { Model } from "./model" +const shadowHost = "dialog-nested-page" + export class DialogModel extends Model { constructor( public page: Page, @@ -10,8 +12,8 @@ export class DialogModel extends Model { super(page) } - checkAccessibility() { - return a11y(this.page, "[role=dialog]") + checkAccessibility(selector?: string): Promise { + return a11y(this.page, "[role=dialog]", shadowHost) } goto(url = "/dialog-nested") { @@ -19,15 +21,15 @@ export class DialogModel extends Model { } private get trigger() { - return this.page.locator(`[data-testid='trigger-${this.id}']`) + return this.page.locator(withHost(shadowHost, testid(`trigger-${this.id}`))) } private get content() { - return this.page.locator(`[data-testid='positioner-${this.id}']`) + return this.page.locator(withHost(shadowHost, testid(`positioner-${this.id}`))) } private get closeTrigger() { - return this.page.locator(`[data-testid='close-${this.id}']`) + return this.page.locator(withHost(shadowHost, testid(`close-${this.id}`))) } clickTrigger(opts: { delay?: number } = {}) { diff --git a/e2e/models/menu.model.ts b/e2e/models/menu.model.ts index 4632d96c9a..68bf7034c8 100644 --- a/e2e/models/menu.model.ts +++ b/e2e/models/menu.model.ts @@ -1,14 +1,16 @@ import { expect, type Page } from "@playwright/test" -import { a11y, isInViewport } from "../_utils" +import { a11y, isInViewport, withHost } from "../_utils" import { Model } from "./model" +const shadowHost = "menu-page" + export class MenuModel extends Model { constructor(public page: Page) { super(page) } - checkAccessibility() { - return a11y(this.page, "main") + checkAccessibility(selector?: string): Promise { + return a11y(this.page, "main", shadowHost) } goto(url = "/menu") { @@ -16,23 +18,23 @@ export class MenuModel extends Model { } private get trigger() { - return this.page.locator("[data-scope=menu][data-part=trigger]") + return this.page.locator(withHost(shadowHost, "[data-scope=menu][data-part=trigger]")) } private get contextTrigger() { - return this.page.locator("[data-scope=menu][data-part=context-trigger]") + return this.page.locator(withHost(shadowHost, "[data-scope=menu][data-part=context-trigger]")) } private get content() { - return this.page.locator("[data-scope=menu][data-part=content]") + return this.page.locator(withHost(shadowHost, "[data-scope=menu][data-part=content]")) } getItem = (text: string) => { - return this.page.locator(`[data-part=item]`, { hasText: text }) + return this.page.locator(withHost(shadowHost, `[data-part=item]`), { hasText: text }) } get highlightedItem() { - return this.page.locator("[data-part=item][data-highlighted]") + return this.page.locator(withHost(shadowHost, "[data-part=item][data-highlighted]")) } type(input: string) { @@ -100,7 +102,7 @@ export class MenuModel extends Model { } seeMenuIsPositioned = async () => { - const positioner = this.page.locator("[data-scope=menu][data-part=positioner]") + const positioner = this.page.locator(withHost(shadowHost, "[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/toggle-group.model.ts b/e2e/models/toggle-group.model.ts index 18e79bd122..6c2b33bef9 100644 --- a/e2e/models/toggle-group.model.ts +++ b/e2e/models/toggle-group.model.ts @@ -1,6 +1,8 @@ import { type Page, expect } from "@playwright/test" import { Model } from "./model" -import { part } from "../_utils" +import { a11y, testid, withHost } from "../_utils" + +const shadowHost = "toggle-group-page" type Item = "bold" | "italic" | "underline" @@ -9,8 +11,12 @@ export class ToggleGroupModel extends Model { super(page) } + checkAccessibility(selector?: string): Promise { + return a11y(this.page, selector, shadowHost) + } + private __item(item: Item) { - return this.page.locator(part("item")).nth(["bold", "italic", "underline"].indexOf(item)) + return this.page.locator(withHost(shadowHost, testid(item))) } clickItem(item: Item) { diff --git a/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts index 7a8ad8cdeb..fd9f796330 100644 --- a/examples/lit-ts/src/main.ts +++ b/examples/lit-ts/src/main.ts @@ -11,6 +11,11 @@ import "./components/state-visualizer" // Import all page components import "./pages/accordion" +import "./pages/dialog" +import "./pages/dialog-nested" +import "./pages/toggle" +import "./pages/toggle-group" +import "./pages/menu" @customElement("zag-app") export class ZagApp extends LitElement { @@ -46,6 +51,16 @@ export class ZagApp extends LitElement { switch (this.currentPath) { case "/accordion": return html`` + case "/dialog": + return html`` + case "/dialog-nested": + return html`` + case "/toggle": + return html`` + case "/toggle-group": + return html`` + case "/menu": + return html`` default: return this.renderHome() } @@ -63,7 +78,7 @@ export class ZagApp extends LitElement { render() { const routes = routesData.filter((route) => // Only show routes we have implemented - ["/accordion"].includes(route.path), + ["/accordion", "/dialog", "/dialog-nested", "/toggle", "/toggle-group", "/menu"].includes(route.path), ) return html` 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..35672c5e9f --- /dev/null +++ b/examples/lit-ts/src/pages/dialog-nested.ts @@ -0,0 +1,81 @@ +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 { ZagController, 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) + + // Dialog 1 + private zagController1 = new ZagController(this, dialog.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + })) + + // Dialog 2 + private zagController2 = new ZagController(this, dialog.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + })) + + render() { + const parentDialog = dialog.connect(this.zagController1.service, normalizeProps) + const childDialog = dialog.connect(this.zagController2.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..0a41b3cd0c --- /dev/null +++ b/examples/lit-ts/src/pages/dialog.ts @@ -0,0 +1,53 @@ +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 { ZagController, 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 zagController = new ZagController(this, dialog.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + })) + + render() { + const api = dialog.connect(this.zagController.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.ts b/examples/lit-ts/src/pages/menu.ts new file mode 100644 index 0000000000..4a198c0c74 --- /dev/null +++ b/examples/lit-ts/src/pages/menu.ts @@ -0,0 +1,56 @@ +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 { ZagController, 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 zagController = new ZagController(this, menu.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + onSelect: console.log, + ...this.controls.context, + })) + + render() { + const api = menu.connect(this.zagController.service, normalizeProps) + + return html` +
+
+ + ${api.open + ? html` +
+
    +
  • Edit
  • +
  • + Duplicate +
  • +
  • Delete
  • +
  • Export...
  • +
+
+ ` + : ""} +
+
+ + + + + ` + } +} 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..38170afd69 --- /dev/null +++ b/examples/lit-ts/src/pages/toggle-group.ts @@ -0,0 +1,48 @@ +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 { ZagController, 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 zagController = new ZagController(this, toggleGroup.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + ...this.controls.context, + })) + + render() { + const api = toggleGroup.connect(this.zagController.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..2e467e24e9 --- /dev/null +++ b/examples/lit-ts/src/pages/toggle.ts @@ -0,0 +1,37 @@ +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 { ZagController, normalizeProps } from "@zag-js/lit" +import { Bold, createElement } from "lucide" +import { nanoid } from "nanoid" +import { PageElement } from "../lib/page-element" + +@customElement("toggle-page") +export class TogglePage extends PageElement { + static styles = unsafeCSS(styleComponent + styleLayout + stylePage) + + private zagController = new ZagController(this, toggle.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + })) + + render() { + const api = toggle.connect(this.zagController.service, normalizeProps) + + return html` +
+ +
+ + + + + ` + } +} From cb14f361d679d2c64ce06c4149b80989bfcd8c2e Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 17:03:42 +0300 Subject: [PATCH 28/56] fix(@zag-js/lit): aria booleans must be strings --- packages/frameworks/lit/src/normalize-props.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/frameworks/lit/src/normalize-props.ts b/packages/frameworks/lit/src/normalize-props.ts index 8222a1f2f8..acb42f21bf 100644 --- a/packages/frameworks/lit/src/normalize-props.ts +++ b/packages/frameworks/lit/src/normalize-props.ts @@ -123,6 +123,12 @@ export const normalizeProps = createNormalizer((props: Dict) => { // 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 } From 896bec9491d17b74893e3279a5620ec875f64c41 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 17:07:42 +0300 Subject: [PATCH 29/56] fix(e2e): pass shadowHost to Model --- e2e/models/model.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/e2e/models/model.ts b/e2e/models/model.ts index 4a808c46f5..5841969cf5 100644 --- a/e2e/models/model.ts +++ b/e2e/models/model.ts @@ -2,7 +2,10 @@ import { expect, type Page } from "@playwright/test" import { a11y, clickOutside, clickViz, controls, repeat } from "../_utils" export class Model { - constructor(public page: Page) {} + constructor( + public page: Page, + private shadowHost?: string, + ) {} get controls() { return controls(this.page) @@ -17,7 +20,7 @@ export class Model { } checkAccessibility(selector?: string) { - return a11y(this.page, selector) + return a11y(this.page, selector, this.shadowHost) } pressKey(key: string, times = 1) { From d98a2f4e515fa606ad95247c329f922c76d3c637 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Thu, 4 Sep 2025 19:45:46 +0300 Subject: [PATCH 30/56] test(e2e): fix issues --- e2e/models/accordion.model.ts | 8 ++------ e2e/models/dialog.model.ts | 9 +++++---- e2e/models/menu.model.ts | 9 +++++---- e2e/models/toggle-group.model.ts | 10 +++------- examples/lit-ts/src/pages/toggle-group.ts | 6 +----- 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/e2e/models/accordion.model.ts b/e2e/models/accordion.model.ts index 56db0f127a..f8ea83fd7a 100644 --- a/e2e/models/accordion.model.ts +++ b/e2e/models/accordion.model.ts @@ -1,22 +1,18 @@ import { expect, type Page } from "@playwright/test" -import { a11y, testid, withHost } from "../_utils" +import { testid, withHost } 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, shadowHost) - } - getTrigger(id: string) { return this.page.locator(withHost(shadowHost, testid(`${id}:trigger`))) } diff --git a/e2e/models/dialog.model.ts b/e2e/models/dialog.model.ts index 76f32d17c4..3fe079bb1b 100644 --- a/e2e/models/dialog.model.ts +++ b/e2e/models/dialog.model.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test" -import { a11y, testid, withHost } from "../_utils" +import { testid, withHost } from "../_utils" import { Model } from "./model" const shadowHost = "dialog-nested-page" @@ -9,11 +9,12 @@ export class DialogModel extends Model { public page: Page, private id: string, ) { - super(page) + super(page, shadowHost) } - checkAccessibility(selector?: string): Promise { - return a11y(this.page, "[role=dialog]", shadowHost) + checkAccessibility(): Promise { + // return a11y(this.page, "[role=dialog]", shadowHost) + return super.checkAccessibility("[role=dialog]") } goto(url = "/dialog-nested") { diff --git a/e2e/models/menu.model.ts b/e2e/models/menu.model.ts index 68bf7034c8..f4b237b4cb 100644 --- a/e2e/models/menu.model.ts +++ b/e2e/models/menu.model.ts @@ -1,16 +1,17 @@ import { expect, type Page } from "@playwright/test" -import { a11y, isInViewport, withHost } from "../_utils" +import { isInViewport, withHost } from "../_utils" import { Model } from "./model" const shadowHost = "menu-page" export class MenuModel extends Model { constructor(public page: Page) { - super(page) + super(page, shadowHost) } - checkAccessibility(selector?: string): Promise { - return a11y(this.page, "main", shadowHost) + checkAccessibility(): Promise { + // return a11y(this.page, "main", shadowHost) + return super.checkAccessibility("main") } goto(url = "/menu") { diff --git a/e2e/models/toggle-group.model.ts b/e2e/models/toggle-group.model.ts index 6c2b33bef9..4bbb65c12c 100644 --- a/e2e/models/toggle-group.model.ts +++ b/e2e/models/toggle-group.model.ts @@ -1,6 +1,6 @@ import { type Page, expect } from "@playwright/test" import { Model } from "./model" -import { a11y, testid, withHost } from "../_utils" +import { part, withHost } from "../_utils" const shadowHost = "toggle-group-page" @@ -8,15 +8,11 @@ type Item = "bold" | "italic" | "underline" export class ToggleGroupModel extends Model { constructor(page: Page) { - super(page) - } - - checkAccessibility(selector?: string): Promise { - return a11y(this.page, selector, shadowHost) + super(page, shadowHost) } private __item(item: Item) { - return this.page.locator(withHost(shadowHost, testid(item))) + return this.page.locator(withHost(shadowHost, part("item"))).nth(["bold", "italic", "underline"].indexOf(item)) } clickItem(item: Item) { diff --git a/examples/lit-ts/src/pages/toggle-group.ts b/examples/lit-ts/src/pages/toggle-group.ts index 38170afd69..364fa23b55 100644 --- a/examples/lit-ts/src/pages/toggle-group.ts +++ b/examples/lit-ts/src/pages/toggle-group.ts @@ -31,11 +31,7 @@ export class ToggleGroupPage extends PageElement {
${toggleGroupData.map( - (item) => html` - - `, + (item) => html``, )}
From cc3a89dae7336fd2eea26bda09dabf724b8459d4 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sat, 6 Sep 2025 18:15:13 +0300 Subject: [PATCH 31/56] test(e2e): create playwright.lit.config for subset testing --- e2e/_utils.ts | 2 +- playwright.lit.config.ts | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 playwright.lit.config.ts diff --git a/e2e/_utils.ts b/e2e/_utils.ts index 59aeda1f92..39b7255048 100644 --- a/e2e/_utils.ts +++ b/e2e/_utils.ts @@ -13,7 +13,7 @@ export type DomMode = "shadow-dom" | "light-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.`) +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. diff --git a/playwright.lit.config.ts b/playwright.lit.config.ts new file mode 100644 index 0000000000..39fce025bb --- /dev/null +++ b/playwright.lit.config.ts @@ -0,0 +1,44 @@ +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", + "**/dialog.e2e.ts", + "**/menu.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 + // }, + // ], +}) From a6b517c6a4c3be80d2c51ae673a5068c60e90227 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sat, 6 Sep 2025 18:43:37 +0300 Subject: [PATCH 32/56] refactor(lit): rename ZagController to MachineController --- examples/lit-ts/src/pages/accordion.ts | 8 +- examples/lit-ts/src/pages/dialog-nested.ts | 14 +-- examples/lit-ts/src/pages/dialog.ts | 8 +- examples/lit-ts/src/pages/menu.ts | 8 +- examples/lit-ts/src/pages/toggle-group.ts | 8 +- examples/lit-ts/src/pages/toggle.ts | 8 +- packages/frameworks/lit/src/index.ts | 2 +- ...ag-controller.ts => machine-controller.ts} | 2 +- packages/frameworks/lit/test-toggle.html | 102 +++++++++--------- packages/frameworks/lit/tests/machine.test.ts | 8 +- 10 files changed, 83 insertions(+), 85 deletions(-) rename packages/frameworks/lit/src/{zag-controller.ts => machine-controller.ts} (90%) diff --git a/examples/lit-ts/src/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts index cab133fa0b..2286502611 100644 --- a/examples/lit-ts/src/pages/accordion.ts +++ b/examples/lit-ts/src/pages/accordion.ts @@ -6,7 +6,7 @@ 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 { ZagController, normalizeProps } from "@zag-js/lit" +import { MachineController, normalizeProps } from "@zag-js/lit" import { ArrowRight, createElement } from "lucide" import { nanoid } from "nanoid" import { ControlsController } from "../lib/controls-controller" @@ -18,14 +18,14 @@ export class AccordionPage extends PageElement { private controls = new ControlsController(this, accordionControls) - private zagController = new ZagController(this, accordion.machine, () => ({ + private machine = new MachineController(this, accordion.machine, () => ({ getRootNode: () => this.shadowRoot, id: nanoid(), ...this.controls.context, })) render() { - const api = accordion.connect(this.zagController.service, normalizeProps) + const api = accordion.connect(this.machine.service, normalizeProps) return html`
@@ -50,7 +50,7 @@ export class AccordionPage extends PageElement {
- + ` } diff --git a/examples/lit-ts/src/pages/dialog-nested.ts b/examples/lit-ts/src/pages/dialog-nested.ts index 35672c5e9f..aa9883a8f2 100644 --- a/examples/lit-ts/src/pages/dialog-nested.ts +++ b/examples/lit-ts/src/pages/dialog-nested.ts @@ -5,7 +5,7 @@ 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 { ZagController, normalizeProps } from "@zag-js/lit" +import { MachineController, normalizeProps } from "@zag-js/lit" import { nanoid } from "nanoid" import { PageElement } from "../lib/page-element" @@ -14,20 +14,20 @@ export class DialogNestedPage extends PageElement { static styles = unsafeCSS(styleComponent + styleLayout + stylePage) // Dialog 1 - private zagController1 = new ZagController(this, dialog.machine, () => ({ + private machine1 = new MachineController(this, dialog.machine, () => ({ getRootNode: () => this.shadowRoot, id: nanoid(), })) // Dialog 2 - private zagController2 = new ZagController(this, dialog.machine, () => ({ + private machine2 = new MachineController(this, dialog.machine, () => ({ getRootNode: () => this.shadowRoot, id: nanoid(), })) render() { - const parentDialog = dialog.connect(this.zagController1.service, normalizeProps) - const childDialog = dialog.connect(this.zagController2.service, normalizeProps) + const parentDialog = dialog.connect(this.machine1.service, normalizeProps) + const childDialog = dialog.connect(this.machine2.service, normalizeProps) return html`
@@ -73,8 +73,8 @@ export class DialogNestedPage extends PageElement {
- - + + ` } diff --git a/examples/lit-ts/src/pages/dialog.ts b/examples/lit-ts/src/pages/dialog.ts index 0a41b3cd0c..5205673ada 100644 --- a/examples/lit-ts/src/pages/dialog.ts +++ b/examples/lit-ts/src/pages/dialog.ts @@ -5,7 +5,7 @@ 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 { ZagController, normalizeProps } from "@zag-js/lit" +import { MachineController, normalizeProps } from "@zag-js/lit" import { nanoid } from "nanoid" import { PageElement } from "../lib/page-element" @@ -13,13 +13,13 @@ import { PageElement } from "../lib/page-element" export class DialogPage extends PageElement { static styles = unsafeCSS(styleComponent + styleLayout + stylePage) - private zagController = new ZagController(this, dialog.machine, () => ({ + private machine = new MachineController(this, dialog.machine, () => ({ getRootNode: () => this.shadowRoot, id: nanoid(), })) render() { - const api = dialog.connect(this.zagController.service, normalizeProps) + const api = dialog.connect(this.machine.service, normalizeProps) return html`
@@ -46,7 +46,7 @@ export class DialogPage extends PageElement {
- + ` } diff --git a/examples/lit-ts/src/pages/menu.ts b/examples/lit-ts/src/pages/menu.ts index 4a198c0c74..1ab1db5a62 100644 --- a/examples/lit-ts/src/pages/menu.ts +++ b/examples/lit-ts/src/pages/menu.ts @@ -6,7 +6,7 @@ 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 { ZagController, normalizeProps } from "@zag-js/lit" +import { MachineController, normalizeProps } from "@zag-js/lit" import { nanoid } from "nanoid" import { ControlsController } from "../lib/controls-controller" import { PageElement } from "../lib/page-element" @@ -17,7 +17,7 @@ export class MenuPage extends PageElement { private controls = new ControlsController(this, menuControls) - private zagController = new ZagController(this, menu.machine, () => ({ + private machine = new MachineController(this, menu.machine, () => ({ getRootNode: () => this.shadowRoot, id: nanoid(), onSelect: console.log, @@ -25,7 +25,7 @@ export class MenuPage extends PageElement { })) render() { - const api = menu.connect(this.zagController.service, normalizeProps) + const api = menu.connect(this.machine.service, normalizeProps) return html`
@@ -49,7 +49,7 @@ export class MenuPage extends PageElement {
- + ` } diff --git a/examples/lit-ts/src/pages/toggle-group.ts b/examples/lit-ts/src/pages/toggle-group.ts index 364fa23b55..050ae5fbe4 100644 --- a/examples/lit-ts/src/pages/toggle-group.ts +++ b/examples/lit-ts/src/pages/toggle-group.ts @@ -6,7 +6,7 @@ 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 { ZagController, normalizeProps } from "@zag-js/lit" +import { MachineController, normalizeProps } from "@zag-js/lit" import { nanoid } from "nanoid" import { ControlsController } from "../lib/controls-controller" import { PageElement } from "../lib/page-element" @@ -17,14 +17,14 @@ export class ToggleGroupPage extends PageElement { private controls = new ControlsController(this, toggleGroupControls) - private zagController = new ZagController(this, toggleGroup.machine, () => ({ + private machine = new MachineController(this, toggleGroup.machine, () => ({ getRootNode: () => this.shadowRoot, id: nanoid(), ...this.controls.context, })) render() { - const api = toggleGroup.connect(this.zagController.service, normalizeProps) + const api = toggleGroup.connect(this.machine.service, normalizeProps) return html`
@@ -37,7 +37,7 @@ export class ToggleGroupPage extends PageElement {
- + ` } diff --git a/examples/lit-ts/src/pages/toggle.ts b/examples/lit-ts/src/pages/toggle.ts index 2e467e24e9..31575896f2 100644 --- a/examples/lit-ts/src/pages/toggle.ts +++ b/examples/lit-ts/src/pages/toggle.ts @@ -5,7 +5,7 @@ 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 { ZagController, normalizeProps } from "@zag-js/lit" +import { MachineController, normalizeProps } from "@zag-js/lit" import { Bold, createElement } from "lucide" import { nanoid } from "nanoid" import { PageElement } from "../lib/page-element" @@ -14,13 +14,13 @@ import { PageElement } from "../lib/page-element" export class TogglePage extends PageElement { static styles = unsafeCSS(styleComponent + styleLayout + stylePage) - private zagController = new ZagController(this, toggle.machine, () => ({ + private machine = new MachineController(this, toggle.machine, () => ({ getRootNode: () => this.shadowRoot, id: nanoid(), })) render() { - const api = toggle.connect(this.zagController.service, normalizeProps) + const api = toggle.connect(this.machine.service, normalizeProps) return html`
@@ -30,7 +30,7 @@ export class TogglePage extends PageElement {
- + ` } diff --git a/packages/frameworks/lit/src/index.ts b/packages/frameworks/lit/src/index.ts index 713a4d060b..79e6c13681 100644 --- a/packages/frameworks/lit/src/index.ts +++ b/packages/frameworks/lit/src/index.ts @@ -1,4 +1,4 @@ export * from "./machine" +export { MachineController } from "./machine-controller" export { mergeProps } from "./merge-props" export * from "./normalize-props" -export { ZagController } from "./zag-controller" diff --git a/packages/frameworks/lit/src/zag-controller.ts b/packages/frameworks/lit/src/machine-controller.ts similarity index 90% rename from packages/frameworks/lit/src/zag-controller.ts rename to packages/frameworks/lit/src/machine-controller.ts index d97133e3b1..385f3021ce 100644 --- a/packages/frameworks/lit/src/zag-controller.ts +++ b/packages/frameworks/lit/src/machine-controller.ts @@ -2,7 +2,7 @@ import type { Machine, MachineSchema } from "@zag-js/core" import type { ReactiveController, ReactiveControllerHost } from "lit" import { LitMachine } from "./machine" -export class ZagController implements ReactiveController { +export class MachineController implements ReactiveController { private machine: LitMachine constructor( diff --git a/packages/frameworks/lit/test-toggle.html b/packages/frameworks/lit/test-toggle.html index 1c3c9ab702..cd48f48e3b 100644 --- a/packages/frameworks/lit/test-toggle.html +++ b/packages/frameworks/lit/test-toggle.html @@ -1,57 +1,55 @@ - + - - - - Lit Toggle Test - - - - - - \ No newline at end of file + + + + + + diff --git a/packages/frameworks/lit/tests/machine.test.ts b/packages/frameworks/lit/tests/machine.test.ts index 95af716eef..51c59a5674 100644 --- a/packages/frameworks/lit/tests/machine.test.ts +++ b/packages/frameworks/lit/tests/machine.test.ts @@ -4,7 +4,7 @@ // - Ensure tests match real Lit component lifecycle import { createMachine } from "@zag-js/core" -import { ZagController } from "../src" +import { MachineController } from "../src" // Mock LitElement for testing class MockLitElement { @@ -29,7 +29,7 @@ class MockLitElement { function renderMachine(machine: any, props: any = {}) { const host = new MockLitElement() - const controller = new ZagController(host as any, machine, () => props) + const controller = new MachineController(host as any, machine, () => props) // Simulate hostConnected controller.hostConnected() @@ -351,7 +351,7 @@ describe("LitMachine", () => { }) }) -describe("ZagController", () => { +describe("MachineController", () => { test("triggers host.requestUpdate on state changes", async () => { const machine = createMachine({ initialState() { @@ -416,7 +416,7 @@ describe("ZagController", () => { }) const host = new MockLitElement() - const controller = new ZagController(host as any, machine, () => ({})) + const controller = new MachineController(host as any, machine, () => ({})) controller.hostConnected() expect(controller.service).toBeDefined() From 00db01bfe14c5f1e20fceaa5d66279617a50c855 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 12:17:22 +0300 Subject: [PATCH 33/56] feat(lit-ts): add checkbox, popover. switch, tabs --- examples/lit-ts/src/main.ts | 31 ++++++++++-- examples/lit-ts/src/pages/checkbox.ts | 60 +++++++++++++++++++++++ examples/lit-ts/src/pages/popover.ts | 69 +++++++++++++++++++++++++++ examples/lit-ts/src/pages/switch.ts | 46 ++++++++++++++++++ examples/lit-ts/src/pages/tabs.ts | 59 +++++++++++++++++++++++ 5 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 examples/lit-ts/src/pages/checkbox.ts create mode 100644 examples/lit-ts/src/pages/popover.ts create mode 100644 examples/lit-ts/src/pages/switch.ts create mode 100644 examples/lit-ts/src/pages/tabs.ts diff --git a/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts index fd9f796330..d017eaca3e 100644 --- a/examples/lit-ts/src/main.ts +++ b/examples/lit-ts/src/main.ts @@ -11,11 +11,15 @@ import "./components/state-visualizer" // Import all page components import "./pages/accordion" +import "./pages/checkbox" import "./pages/dialog" import "./pages/dialog-nested" +import "./pages/menu" +import "./pages/popover" +import "./pages/switch" +import "./pages/tabs" import "./pages/toggle" import "./pages/toggle-group" -import "./pages/menu" @customElement("zag-app") export class ZagApp extends LitElement { @@ -51,16 +55,24 @@ export class ZagApp extends LitElement { switch (this.currentPath) { case "/accordion": return html`` + case "/checkbox": + return html`` case "/dialog": return html`` case "/dialog-nested": return html`` + case "/menu": + return html`` + case "/popover": + return html`` + case "/switch": + return html`` + case "/tabs": + return html`` case "/toggle": return html`` case "/toggle-group": return html`` - case "/menu": - return html`` default: return this.renderHome() } @@ -78,7 +90,18 @@ export class ZagApp extends LitElement { render() { const routes = routesData.filter((route) => // Only show routes we have implemented - ["/accordion", "/dialog", "/dialog-nested", "/toggle", "/toggle-group", "/menu"].includes(route.path), + [ + "/accordion", + "/checkbox", + "/dialog", + "/dialog-nested", + "/menu", + "/popover", + "/switch", + "/tabs", + "/toggle", + "/toggle-group", + ].includes(route.path), ) return html` diff --git a/examples/lit-ts/src/pages/checkbox.ts b/examples/lit-ts/src/pages/checkbox.ts new file mode 100644 index 0000000000..c943b185e5 --- /dev/null +++ b/examples/lit-ts/src/pages/checkbox.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 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 machine = new MachineController(this, checkbox.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + 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/popover.ts b/examples/lit-ts/src/pages/popover.ts new file mode 100644 index 0000000000..2b352e0802 --- /dev/null +++ b/examples/lit-ts/src/pages/popover.ts @@ -0,0 +1,69 @@ +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 machine = new MachineController(this, popover.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + ...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/switch.ts b/examples/lit-ts/src/pages/switch.ts new file mode 100644 index 0000000000..313b617bf6 --- /dev/null +++ b/examples/lit-ts/src/pages/switch.ts @@ -0,0 +1,46 @@ +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 machine = new MachineController(this, zagSwitch.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + 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..0b951c2097 --- /dev/null +++ b/examples/lit-ts/src/pages/tabs.ts @@ -0,0 +1,59 @@ +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 machine = new MachineController(this, tabs.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + 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} +
+ `, + )} +
+
+ + + + + ` + } +} From c25af38dade231b11d0cdff003adf7e612311e81 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 12:21:11 +0300 Subject: [PATCH 34/56] fix(e2e): handle shadow-dom in checkbox, popover. switch, tabs --- e2e/models/popover.model.ts | 24 +++++++++++------------- e2e/models/switch.model.ts | 18 ++++++++---------- e2e/models/tabs.model.ts | 14 ++++++-------- playwright.lit.config.ts | 4 ++++ 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/e2e/models/popover.model.ts b/e2e/models/popover.model.ts index 4ad475cb77..fe33aa0aa1 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, withHost } 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.page.locator(withHost(shadowHost, "[data-scope=popover][data-part=trigger]")) } get content() { - return this.page.locator("[data-scope=popover][data-part=content]") + return this.page.locator(withHost(shadowHost, "[data-scope=popover][data-part=content]")) } get closeTrigger() { - return this.page.locator("[data-scope=popover][data-part=close-trigger]") + return this.page.locator(withHost(shadowHost, "[data-scope=popover][data-part=close-trigger]")) } get buttonBefore() { - return this.page.locator(testid("button-before")) + return this.page.locator(withHost(shadowHost, testid("button-before"))) } get buttonAfter() { - return this.page.locator(testid("button-after")) + return this.page.locator(withHost(shadowHost, testid("button-after"))) } get link() { - return this.page.locator(testid("focusable-link")) + return this.page.locator(withHost(shadowHost, testid("focusable-link"))) } get plainText() { - return this.page.locator(testid("plain-text")) + return this.page.locator(withHost(shadowHost, testid("plain-text"))) } clickClose() { diff --git a/e2e/models/switch.model.ts b/e2e/models/switch.model.ts index be9ba41e51..84b149a107 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, withHost } 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.page.locator(withHost(shadowHost, "[data-scope='switch'][data-part='root']")) } get label() { - return this.page.locator("[data-scope='switch'][data-part='label']") + return this.page.locator(withHost(shadowHost, "[data-scope='switch'][data-part='label']")) } get control() { - return this.page.locator("[data-scope='switch'][data-part='control']") + return this.page.locator(withHost(shadowHost, "[data-scope='switch'][data-part='control']")) } get input() { - return this.page.locator("[data-scope='switch'][data-part='root'] input") + return this.page.locator(withHost(shadowHost, "[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..1b7a7c53b4 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, withHost } 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.page.locator(withHost(shadowHost, testid(`${id}-tab`))) } private getTabContent = (id: string) => { - return this.page.locator(testid(`${id}-tab-panel`)) + return this.page.locator(withHost(shadowHost, testid(`${id}-tab-panel`))) } clickTab = async (id: string) => { diff --git a/playwright.lit.config.ts b/playwright.lit.config.ts index 39fce025bb..65e5fe6d4e 100644 --- a/playwright.lit.config.ts +++ b/playwright.lit.config.ts @@ -16,8 +16,12 @@ export default defineConfig({ // Only test implemented Lit components testMatch: [ "**/accordion.e2e.ts", + "**/checkbox.e2e.ts", "**/dialog.e2e.ts", "**/menu.e2e.ts", + "**/popover.e2e.ts", + "**/switch.e2e.ts", + "**/tabs.e2e.ts", "**/toggle-group.e2e.ts", "**/toggle.e2e.ts", ], From 0a877ef5f5c48d59f976ece51f70cbd11aa624c0 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 12:29:05 +0300 Subject: [PATCH 35/56] refactor(e2e): create CheckboxModel --- e2e/checkbox.e2e.ts | 52 +++++++++++++++--------------------- e2e/models/checkbox.model.ts | 43 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 30 deletions(-) create mode 100644 e2e/models/checkbox.model.ts diff --git a/e2e/checkbox.e2e.ts b/e2e/checkbox.e2e.ts index bcd7fdf71b..c32ede2bd7 100644 --- a/e2e/checkbox.e2e.ts +++ b/e2e/checkbox.e2e.ts @@ -1,64 +1,56 @@ -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) }) diff --git a/e2e/models/checkbox.model.ts b/e2e/models/checkbox.model.ts new file mode 100644 index 0000000000..79e882482c --- /dev/null +++ b/e2e/models/checkbox.model.ts @@ -0,0 +1,43 @@ +import { expect, type Page } from "@playwright/test" +import { part, testid, withHost } 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.page.locator(withHost(shadowHost, part("root"))) + } + + get label() { + return this.page.locator(withHost(shadowHost, part("label"))) + } + + get control() { + return this.page.locator(withHost(shadowHost, part("control"))) + } + + get input() { + return this.page.locator(withHost(shadowHost, testid("hidden-input"))) + } + + 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() + } +} From 4607e9421d3e6961fd0a4158e2cf8ee5b76295c5 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 12:30:33 +0300 Subject: [PATCH 36/56] feat(e2e): add popover blur close test --- e2e/popover.e2e.ts | 9 +++++++++ 1 file changed, 9 insertions(+) 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() + }) }) From d031894fc8413179a6afbc4cb0bb7ec4e40d811a Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 13:40:25 +0300 Subject: [PATCH 37/56] refactor(e2e): switch to this.host pattern --- e2e/_utils.ts | 10 ---------- e2e/models/accordion.model.ts | 6 +++--- e2e/models/checkbox.model.ts | 10 +++++----- e2e/models/dialog.model.ts | 8 ++++---- e2e/models/menu.model.ts | 14 +++++++------- e2e/models/model.ts | 6 +++++- e2e/models/popover.model.ts | 16 ++++++++-------- e2e/models/switch.model.ts | 10 +++++----- e2e/models/tabs.model.ts | 6 +++--- e2e/models/toggle-group.model.ts | 4 ++-- 10 files changed, 42 insertions(+), 48 deletions(-) diff --git a/e2e/_utils.ts b/e2e/_utils.ts index 39b7255048..2592d93aaf 100644 --- a/e2e/_utils.ts +++ b/e2e/_utils.ts @@ -64,16 +64,6 @@ export async function a11y(page: Page, selector = "[data-part=root]", hostSelect export const testid = (part: string) => `[data-testid=${esc(part)}]` -/** - * Combines host selector and target selector for framework-aware locators. - * @param host Optional host selector (e.g., 'accordion-page'). If undefined, returns just the target. - * @param target The target selector (e.g., '[data-testid="about:trigger"]') - * @returns Combined selector string - */ -export function withHost(componentHost: string | undefined, target: string): string { - return DOM_MODE === "shadow-dom" && componentHost ? `${componentHost} ${target}` : target -} - export const controls = (page: Page) => { return { num: async (id: string, value: string) => { diff --git a/e2e/models/accordion.model.ts b/e2e/models/accordion.model.ts index f8ea83fd7a..4977fd824b 100644 --- a/e2e/models/accordion.model.ts +++ b/e2e/models/accordion.model.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test" -import { testid, withHost } from "../_utils" +import { testid } from "../_utils" import { Model } from "./model" const shadowHost = "accordion-page" @@ -14,11 +14,11 @@ export class AccordionModel extends Model { } getTrigger(id: string) { - return this.page.locator(withHost(shadowHost, testid(`${id}:trigger`))) + return this.host.locator(testid(`${id}:trigger`)) } getContent(id: string) { - return this.page.locator(withHost(shadowHost, 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 index 79e882482c..5527569796 100644 --- a/e2e/models/checkbox.model.ts +++ b/e2e/models/checkbox.model.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test" -import { part, testid, withHost } from "../_utils" +import { part, testid } from "../_utils" import { Model } from "./model" const shadowHost = "checkbox-page" @@ -14,19 +14,19 @@ export class CheckboxModel extends Model { } get root() { - return this.page.locator(withHost(shadowHost, part("root"))) + return this.host.locator(part("root")) } get label() { - return this.page.locator(withHost(shadowHost, part("label"))) + return this.host.locator(part("label")) } get control() { - return this.page.locator(withHost(shadowHost, part("control"))) + return this.host.locator(part("control")) } get input() { - return this.page.locator(withHost(shadowHost, testid("hidden-input"))) + return this.host.locator(testid("hidden-input")) } async expectToBeChecked() { diff --git a/e2e/models/dialog.model.ts b/e2e/models/dialog.model.ts index 3fe079bb1b..44b36222ee 100644 --- a/e2e/models/dialog.model.ts +++ b/e2e/models/dialog.model.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test" -import { testid, withHost } from "../_utils" +import { testid } from "../_utils" import { Model } from "./model" const shadowHost = "dialog-nested-page" @@ -22,15 +22,15 @@ export class DialogModel extends Model { } private get trigger() { - return this.page.locator(withHost(shadowHost, testid(`trigger-${this.id}`))) + return this.host.locator(testid(`trigger-${this.id}`)) } private get content() { - return this.page.locator(withHost(shadowHost, testid(`positioner-${this.id}`))) + return this.host.locator(testid(`positioner-${this.id}`)) } private get closeTrigger() { - return this.page.locator(withHost(shadowHost, testid(`close-${this.id}`))) + return this.host.locator(testid(`close-${this.id}`)) } clickTrigger(opts: { delay?: number } = {}) { diff --git a/e2e/models/menu.model.ts b/e2e/models/menu.model.ts index f4b237b4cb..c9228526d7 100644 --- a/e2e/models/menu.model.ts +++ b/e2e/models/menu.model.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test" -import { isInViewport, withHost } from "../_utils" +import { isInViewport } from "../_utils" import { Model } from "./model" const shadowHost = "menu-page" @@ -19,23 +19,23 @@ export class MenuModel extends Model { } private get trigger() { - return this.page.locator(withHost(shadowHost, "[data-scope=menu][data-part=trigger]")) + return this.host.locator("[data-scope=menu][data-part=trigger]") } private get contextTrigger() { - return this.page.locator(withHost(shadowHost, "[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(withHost(shadowHost, "[data-scope=menu][data-part=content]")) + return this.host.locator("[data-scope=menu][data-part=content]") } getItem = (text: string) => { - return this.page.locator(withHost(shadowHost, `[data-part=item]`), { hasText: text }) + return this.host.locator(`[data-part=item]`, { hasText: text }) } get highlightedItem() { - return this.page.locator(withHost(shadowHost, "[data-part=item][data-highlighted]")) + return this.host.locator("[data-part=item][data-highlighted]") } type(input: string) { @@ -103,7 +103,7 @@ export class MenuModel extends Model { } seeMenuIsPositioned = async () => { - const positioner = this.page.locator(withHost(shadowHost, "[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 5841969cf5..b338a3e205 100644 --- a/e2e/models/model.ts +++ b/e2e/models/model.ts @@ -1,5 +1,5 @@ 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( @@ -7,6 +7,10 @@ export class Model { 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) } diff --git a/e2e/models/popover.model.ts b/e2e/models/popover.model.ts index fe33aa0aa1..c1e199bc20 100644 --- a/e2e/models/popover.model.ts +++ b/e2e/models/popover.model.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test" -import { testid, withHost } from "../_utils" +import { testid } from "../_utils" import { Model } from "./model" const shadowHost = "popover-page" @@ -14,31 +14,31 @@ export class PopoverModel extends Model { } get trigger() { - return this.page.locator(withHost(shadowHost, "[data-scope=popover][data-part=trigger]")) + return this.host.locator("[data-scope=popover][data-part=trigger]") } get content() { - return this.page.locator(withHost(shadowHost, "[data-scope=popover][data-part=content]")) + return this.host.locator("[data-scope=popover][data-part=content]") } get closeTrigger() { - return this.page.locator(withHost(shadowHost, "[data-scope=popover][data-part=close-trigger]")) + return this.content.locator("[data-scope=popover][data-part=close-trigger]") } get buttonBefore() { - return this.page.locator(withHost(shadowHost, testid("button-before"))) + return this.host.locator(testid("button-before")) } get buttonAfter() { - return this.page.locator(withHost(shadowHost, testid("button-after"))) + return this.host.locator(testid("button-after")) } get link() { - return this.page.locator(withHost(shadowHost, testid("focusable-link"))) + return this.content.locator(testid("focusable-link")) } get plainText() { - return this.page.locator(withHost(shadowHost, testid("plain-text"))) + return this.host.locator(testid("plain-text")) } clickClose() { diff --git a/e2e/models/switch.model.ts b/e2e/models/switch.model.ts index 84b149a107..938ad0ddf2 100644 --- a/e2e/models/switch.model.ts +++ b/e2e/models/switch.model.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test" -import { repeat, withHost } from "../_utils" +import { repeat } from "../_utils" import { Model } from "./model" const shadowHost = "switch-page" @@ -14,19 +14,19 @@ export class SwitchModel extends Model { } get root() { - return this.page.locator(withHost(shadowHost, "[data-scope='switch'][data-part='root']")) + return this.host.locator("[data-scope='switch'][data-part='root']") } get label() { - return this.page.locator(withHost(shadowHost, "[data-scope='switch'][data-part='label']")) + return this.host.locator("[data-scope='switch'][data-part='label']") } get control() { - return this.page.locator(withHost(shadowHost, "[data-scope='switch'][data-part='control']")) + return this.host.locator("[data-scope='switch'][data-part='control']") } get input() { - return this.page.locator(withHost(shadowHost, "[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 1b7a7c53b4..bbf485cba4 100644 --- a/e2e/models/tabs.model.ts +++ b/e2e/models/tabs.model.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test" -import { testid, withHost } from "../_utils" +import { testid } from "../_utils" import { Model } from "./model" const shadowHost = "tabs-page" @@ -14,11 +14,11 @@ export class TabsModel extends Model { } private getTabTrigger = (id: string) => { - return this.page.locator(withHost(shadowHost, testid(`${id}-tab`))) + return this.host.locator(testid(`${id}-tab`)) } private getTabContent = (id: string) => { - return this.page.locator(withHost(shadowHost, 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 4bbb65c12c..dd75be3ce0 100644 --- a/e2e/models/toggle-group.model.ts +++ b/e2e/models/toggle-group.model.ts @@ -1,6 +1,6 @@ import { type Page, expect } from "@playwright/test" import { Model } from "./model" -import { part, withHost } from "../_utils" +import { part } from "../_utils" const shadowHost = "toggle-group-page" @@ -12,7 +12,7 @@ export class ToggleGroupModel extends Model { } private __item(item: Item) { - return this.page.locator(withHost(shadowHost, part("item"))).nth(["bold", "italic", "underline"].indexOf(item)) + return this.host.locator(part("item")).nth(["bold", "italic", "underline"].indexOf(item)) } clickItem(item: Item) { From 03357dac44ea1d86fcf2968ce73ab4c5d5ffd3a8 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 15:22:45 +0300 Subject: [PATCH 38/56] feat(e2e): add reset form test --- e2e/checkbox.e2e.ts | 10 ++++++++++ e2e/models/checkbox.model.ts | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/e2e/checkbox.e2e.ts b/e2e/checkbox.e2e.ts index c32ede2bd7..219b0c7527 100644 --- a/e2e/checkbox.e2e.ts +++ b/e2e/checkbox.e2e.ts @@ -54,3 +54,13 @@ test("input is not blurred on label click", async ({ page }) => { 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/models/checkbox.model.ts b/e2e/models/checkbox.model.ts index 5527569796..05c6ff4d4c 100644 --- a/e2e/models/checkbox.model.ts +++ b/e2e/models/checkbox.model.ts @@ -29,6 +29,10 @@ export class CheckboxModel extends Model { 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") From 43c530279ed1a85c66e28c8febf0545dc9ed5b9e Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 17:12:09 +0300 Subject: [PATCH 39/56] fix(lit): add machine.started and start after initial render --- packages/frameworks/lit/src/machine-controller.ts | 11 +++++++---- packages/frameworks/lit/src/machine.ts | 4 ++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/frameworks/lit/src/machine-controller.ts b/packages/frameworks/lit/src/machine-controller.ts index 385f3021ce..913d00c459 100644 --- a/packages/frameworks/lit/src/machine-controller.ts +++ b/packages/frameworks/lit/src/machine-controller.ts @@ -17,19 +17,22 @@ export class MachineController implements Reactiv } hostConnected() { - // Start the machine when the host is connected this.machine.subscribe(() => { - // Request Lit component update this.host.requestUpdate() }) - this.machine.start() + } + + hostUpdated(): void { + // Start the machine after the initial html has been rendered + if (!this.machine.started) { + this.machine.start() + } } hostDisconnected() { this.machine.stop() } - // Expose machine methods for advanced usage get service() { return this.machine.service } diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts index 0b998e6a29..3a160d8e0f 100644 --- a/packages/frameworks/lit/src/machine.ts +++ b/packages/frameworks/lit/src/machine.ts @@ -287,6 +287,10 @@ export class LitMachine { private status = MachineStatus.NotStarted + get started() { + return this.status === MachineStatus.Started + } + get service(): Service { return { state: this.getState(), From 7609226d14d16f7b217e34895c74f54af58f1afe Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 17:33:18 +0300 Subject: [PATCH 40/56] feat(lit-ts): add radio-group --- e2e/models/radio-group.model.ts | 14 ++--- examples/lit-ts/src/main.ts | 4 ++ examples/lit-ts/src/pages/radio-group.ts | 70 ++++++++++++++++++++++++ playwright.lit.config.ts | 1 + 4 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 examples/lit-ts/src/pages/radio-group.ts 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/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts index d017eaca3e..5ac3238390 100644 --- a/examples/lit-ts/src/main.ts +++ b/examples/lit-ts/src/main.ts @@ -16,6 +16,7 @@ import "./pages/dialog" import "./pages/dialog-nested" import "./pages/menu" import "./pages/popover" +import "./pages/radio-group" import "./pages/switch" import "./pages/tabs" import "./pages/toggle" @@ -65,6 +66,8 @@ export class ZagApp extends LitElement { return html`` case "/popover": return html`` + case "/radio-group": + return html`` case "/switch": return html`` case "/tabs": @@ -97,6 +100,7 @@ export class ZagApp extends LitElement { "/dialog-nested", "/menu", "/popover", + "/radio-group", "/switch", "/tabs", "/toggle", 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..49156f9ade --- /dev/null +++ b/examples/lit-ts/src/pages/radio-group.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 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 machine = new MachineController(this, radio.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + 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/playwright.lit.config.ts b/playwright.lit.config.ts index 65e5fe6d4e..06782e0333 100644 --- a/playwright.lit.config.ts +++ b/playwright.lit.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ "**/dialog.e2e.ts", "**/menu.e2e.ts", "**/popover.e2e.ts", + "**/radio-group.e2e.ts", "**/switch.e2e.ts", "**/tabs.e2e.ts", "**/toggle-group.e2e.ts", From 6f57ba8702288cbbb7923f54636f6de24291f2c1 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 17:57:12 +0300 Subject: [PATCH 41/56] feat(lit-ts): add collapsible --- e2e/collapsible.e2e.ts | 32 ++++++------- e2e/models/collapsible.model.ts | 32 +++++++++++++ examples/lit-ts/src/main.ts | 4 ++ examples/lit-ts/src/pages/collapsible.ts | 61 ++++++++++++++++++++++++ playwright.lit.config.ts | 1 + 5 files changed, 114 insertions(+), 16 deletions(-) create mode 100644 e2e/models/collapsible.model.ts create mode 100644 examples/lit-ts/src/pages/collapsible.ts diff --git a/e2e/collapsible.e2e.ts b/e2e/collapsible.e2e.ts index 5ef3218caf..267b6bd3aa 100644 --- a/e2e/collapsible.e2e.ts +++ b/e2e/collapsible.e2e.ts @@ -1,30 +1,30 @@ 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.skip("[closed] content should not be reachable via tab key", async () => { + await I.clickTrigger() + await I.clickTrigger() + await I.pressKey("Tab") + await expect(I.host.getByRole("button", { name: "Open" })).toBeFocused() }) }) 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/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts index 5ac3238390..28a64fb8aa 100644 --- a/examples/lit-ts/src/main.ts +++ b/examples/lit-ts/src/main.ts @@ -12,6 +12,7 @@ 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" @@ -58,6 +59,8 @@ export class ZagApp extends LitElement { return html`` case "/checkbox": return html`` + case "/collapsible": + return html`` case "/dialog": return html`` case "/dialog-nested": @@ -96,6 +99,7 @@ export class ZagApp extends LitElement { [ "/accordion", "/checkbox", + "/collapsible", "/dialog", "/dialog-nested", "/menu", diff --git a/examples/lit-ts/src/pages/collapsible.ts b/examples/lit-ts/src/pages/collapsible.ts new file mode 100644 index 0000000000..c222b3e7a3 --- /dev/null +++ b/examples/lit-ts/src/pages/collapsible.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 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 machine = new MachineController(this, collapsible.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + ...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/playwright.lit.config.ts b/playwright.lit.config.ts index 06782e0333..dcdb8879c6 100644 --- a/playwright.lit.config.ts +++ b/playwright.lit.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ testMatch: [ "**/accordion.e2e.ts", "**/checkbox.e2e.ts", + "**/collapsible.e2e.ts", "**/dialog.e2e.ts", "**/menu.e2e.ts", "**/popover.e2e.ts", From 7e72aae918f32046948697bd8a6dd1df20e19b61 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 18:05:07 +0300 Subject: [PATCH 42/56] fix(e2e): re-enable collapsible tab key test --- e2e/collapsible.e2e.ts | 8 ++++++-- examples/lit-ts/src/pages/collapsible.ts | 2 +- examples/next-ts/pages/collapsible.tsx | 4 +++- examples/nuxt-ts/app/pages/collapsible.vue | 6 ++++++ examples/solid-ts/src/routes/collapsible.tsx | 4 +++- examples/svelte-ts/src/routes/collapsible.svelte | 2 +- 6 files changed, 20 insertions(+), 6 deletions(-) diff --git a/e2e/collapsible.e2e.ts b/e2e/collapsible.e2e.ts index 267b6bd3aa..1de285e6eb 100644 --- a/e2e/collapsible.e2e.ts +++ b/e2e/collapsible.e2e.ts @@ -21,10 +21,14 @@ test.describe("collapsible", () => { await I.dontSeeContent() }) - test.skip("[closed] content should not be reachable via tab key", async () => { + 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.getByRole("button", { name: "Open" })).toBeFocused() + await expect(I.host.getByTestId("open-button")).toBeFocused() }) }) diff --git a/examples/lit-ts/src/pages/collapsible.ts b/examples/lit-ts/src/pages/collapsible.ts index c222b3e7a3..3260dac917 100644 --- a/examples/lit-ts/src/pages/collapsible.ts +++ b/examples/lit-ts/src/pages/collapsible.ts @@ -48,7 +48,7 @@ export class CollapsiblePage extends PageElement {
Toggle Controls
- +
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
- +
From 253928f0f51d13fd540fe5da11eeecec4c5a1e08 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 18:31:37 +0300 Subject: [PATCH 43/56] feat(lit-ts): add slider and range-slider --- e2e/models/slider.model.ts | 19 +++--- examples/lit-ts/package.json | 1 + examples/lit-ts/src/main.ts | 8 +++ examples/lit-ts/src/pages/range-slider.ts | 75 +++++++++++++++++++++ examples/lit-ts/src/pages/slider.ts | 75 +++++++++++++++++++++ playwright.lit.config.ts | 1 + pnpm-lock.yaml | 80 +++++++---------------- 7 files changed, 191 insertions(+), 68 deletions(-) create mode 100644 examples/lit-ts/src/pages/range-slider.ts create mode 100644 examples/lit-ts/src/pages/slider.ts 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/examples/lit-ts/package.json b/examples/lit-ts/package.json index e23f3b900e..6cad639913 100644 --- a/examples/lit-ts/package.json +++ b/examples/lit-ts/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "devDependencies": { + "@types/form-serialize": "0.7.4", "typescript": "5.9.2", "vite": "7.1.3" }, diff --git a/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts index 28a64fb8aa..f7f0387217 100644 --- a/examples/lit-ts/src/main.ts +++ b/examples/lit-ts/src/main.ts @@ -18,6 +18,8 @@ import "./pages/dialog-nested" import "./pages/menu" import "./pages/popover" import "./pages/radio-group" +import "./pages/range-slider" +import "./pages/slider" import "./pages/switch" import "./pages/tabs" import "./pages/toggle" @@ -71,6 +73,10 @@ export class ZagApp extends LitElement { return html`` case "/radio-group": return html`` + case "/range-slider": + return html`` + case "/slider": + return html`` case "/switch": return html`` case "/tabs": @@ -105,6 +111,8 @@ export class ZagApp extends LitElement { "/menu", "/popover", "/radio-group", + "/range-slider", + "/slider", "/switch", "/tabs", "/toggle", 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..a41bb1e2a7 --- /dev/null +++ b/examples/lit-ts/src/pages/range-slider.ts @@ -0,0 +1,75 @@ +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 machine = new MachineController(this, slider.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + 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..1a18a518ce --- /dev/null +++ b/examples/lit-ts/src/pages/slider.ts @@ -0,0 +1,75 @@ +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 machine = new MachineController(this, slider.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + 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/playwright.lit.config.ts b/playwright.lit.config.ts index dcdb8879c6..80798bb46c 100644 --- a/playwright.lit.config.ts +++ b/playwright.lit.config.ts @@ -22,6 +22,7 @@ export default defineConfig({ "**/menu.e2e.ts", "**/popover.e2e.ts", "**/radio-group.e2e.ts", + "**/slider.e2e.ts", "**/switch.e2e.ts", "**/tabs.e2e.ts", "**/toggle-group.e2e.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be3c20e347..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) @@ -372,6 +372,9 @@ importers: 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 @@ -4669,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==} @@ -16756,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) @@ -18891,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)) @@ -18954,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 @@ -19006,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 @@ -19028,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 @@ -19039,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 @@ -19051,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 @@ -19086,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 From 2b81fbed28af0d407ee92da48a30a63a9b975c4a Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 21:47:27 +0300 Subject: [PATCH 44/56] feat(lit-ts): add menu-options --- e2e/menu-option.e2e.ts | 3 +- e2e/models/menu.model.ts | 7 +- examples/lit-ts/src/main.ts | 4 + examples/lit-ts/src/pages/menu-options.ts | 101 ++++++++++++++++++++++ playwright.lit.config.ts | 1 + 5 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 examples/lit-ts/src/pages/menu-options.ts 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/menu.model.ts b/e2e/models/menu.model.ts index c9228526d7..5028e837a7 100644 --- a/e2e/models/menu.model.ts +++ b/e2e/models/menu.model.ts @@ -2,10 +2,11 @@ import { expect, type Page } from "@playwright/test" import { isInViewport } from "../_utils" import { Model } from "./model" -const shadowHost = "menu-page" - export class MenuModel extends Model { - constructor(public page: Page) { + constructor( + public page: Page, + shadowHost = "menu-page", + ) { super(page, shadowHost) } diff --git a/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts index f7f0387217..b2cdc40dc8 100644 --- a/examples/lit-ts/src/main.ts +++ b/examples/lit-ts/src/main.ts @@ -16,6 +16,7 @@ import "./pages/collapsible" import "./pages/dialog" import "./pages/dialog-nested" import "./pages/menu" +import "./pages/menu-options" import "./pages/popover" import "./pages/radio-group" import "./pages/range-slider" @@ -69,6 +70,8 @@ export class ZagApp extends LitElement { return html`` case "/menu": return html`` + case "/menu-options": + return html`` case "/popover": return html`` case "/radio-group": @@ -109,6 +112,7 @@ export class ZagApp extends LitElement { "/dialog", "/dialog-nested", "/menu", + "/menu-options", "/popover", "/radio-group", "/range-slider", 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..dbc1fe7e1a --- /dev/null +++ b/examples/lit-ts/src/pages/menu-options.ts @@ -0,0 +1,101 @@ +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 machine = new MachineController(this, menu.machine, () => ({ + getRootNode: () => this.shadowRoot, + id: nanoid(), + ...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/playwright.lit.config.ts b/playwright.lit.config.ts index 80798bb46c..e707725982 100644 --- a/playwright.lit.config.ts +++ b/playwright.lit.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ "**/collapsible.e2e.ts", "**/dialog.e2e.ts", "**/menu.e2e.ts", + "**/menu-option.e2e.ts", "**/popover.e2e.ts", "**/radio-group.e2e.ts", "**/slider.e2e.ts", From d55b5f5706b1fadf4439d29b0f1a314698143f77 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 23:07:18 +0300 Subject: [PATCH 45/56] feat(lit-ts): create nested-menu with issues --- e2e/menu-nested.e2e.ts | 256 ++++++++++------------- e2e/models/menu-nested.model.ts | 79 +++++++ examples/lit-ts/src/main.ts | 4 + examples/lit-ts/src/pages/menu-nested.ts | 101 +++++++++ playwright.lit.config.ts | 1 + 5 files changed, 298 insertions(+), 143 deletions(-) create mode 100644 e2e/models/menu-nested.model.ts create mode 100644 examples/lit-ts/src/pages/menu-nested.ts diff --git a/e2e/menu-nested.e2e.ts b/e2e/menu-nested.e2e.ts index 9c0031cb0a..fb67abd13d 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,178 @@ 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) + 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/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/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts index b2cdc40dc8..a2571166f7 100644 --- a/examples/lit-ts/src/main.ts +++ b/examples/lit-ts/src/main.ts @@ -16,6 +16,7 @@ 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" @@ -70,6 +71,8 @@ export class ZagApp extends LitElement { return html`` case "/menu": return html`` + case "/menu-nested": + return html`` case "/menu-options": return html`` case "/popover": @@ -112,6 +115,7 @@ export class ZagApp extends LitElement { "/dialog", "/dialog-nested", "/menu", + "/menu-nested", "/menu-options", "/popover", "/radio-group", 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..351b19490f --- /dev/null +++ b/examples/lit-ts/src/pages/menu-nested.ts @@ -0,0 +1,101 @@ +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 rootMachine = new MachineController(this, menu.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: nanoid(), + })) + + private subMachine = new MachineController(this, menu.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: nanoid(), + })) + + private sub2Machine = new MachineController(this, menu.machine, () => ({ + getRootNode: () => this.shadowRoot || this.ownerDocument, + id: nanoid(), + })) + + 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) + + 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/playwright.lit.config.ts b/playwright.lit.config.ts index e707725982..4d29eedc19 100644 --- a/playwright.lit.config.ts +++ b/playwright.lit.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ "**/collapsible.e2e.ts", "**/dialog.e2e.ts", "**/menu.e2e.ts", + "**/menu-nested.e2e.ts", "**/menu-option.e2e.ts", "**/popover.e2e.ts", "**/radio-group.e2e.ts", From bf78848781387f723e26b958b4a42fe96f54fd14 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Sun, 7 Sep 2025 23:08:38 +0300 Subject: [PATCH 46/56] fix(lit): simplify MachineController getProps type --- packages/frameworks/lit/src/machine-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frameworks/lit/src/machine-controller.ts b/packages/frameworks/lit/src/machine-controller.ts index 913d00c459..651f0279fd 100644 --- a/packages/frameworks/lit/src/machine-controller.ts +++ b/packages/frameworks/lit/src/machine-controller.ts @@ -8,7 +8,7 @@ export class MachineController implements Reactiv constructor( private host: ReactiveControllerHost, machineConfig: Machine, - getProps?: () => Partial & { getRootNode?: () => ShadowRoot | Document | Node | null }, + getProps?: () => Partial, ) { this.machine = new LitMachine(machineConfig, getProps) From 55f2729994d076418d6fc6494c62bfd67cbd22c6 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Mon, 8 Sep 2025 09:42:02 +0300 Subject: [PATCH 47/56] fix(lit-ts): stabilize manu ids --- e2e/menu-nested.e2e.ts | 1 + examples/lit-ts/src/pages/menu-nested.ts | 17 +++++++++++------ packages/frameworks/lit/src/normalize-props.ts | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/e2e/menu-nested.e2e.ts b/e2e/menu-nested.e2e.ts index fb67abd13d..5560daa95a 100644 --- a/e2e/menu-nested.e2e.ts +++ b/e2e/menu-nested.e2e.ts @@ -177,6 +177,7 @@ test.describe("nested menu / pointer movement", async () => { const menuitem = I.getTestId("new-tab") + // FIXME: .hover() causes "subtree intercepts pointer events" error in shadow-dom await menuitem.hover() // dispatch extra mouse movement to trigger hover await menuitem.dispatchEvent("pointermove") diff --git a/examples/lit-ts/src/pages/menu-nested.ts b/examples/lit-ts/src/pages/menu-nested.ts index 351b19490f..d2452ad9d0 100644 --- a/examples/lit-ts/src/pages/menu-nested.ts +++ b/examples/lit-ts/src/pages/menu-nested.ts @@ -14,19 +14,23 @@ import { PageElement } from "../lib/page-element" 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: nanoid(), + getRootNode: () => this.getRootNode(), + id: this.rootId, })) private subMachine = new MachineController(this, menu.machine, () => ({ - getRootNode: () => this.shadowRoot || this.ownerDocument, - id: nanoid(), + getRootNode: () => this.getRootNode(), + id: this.subId, })) private sub2Machine = new MachineController(this, menu.machine, () => ({ - getRootNode: () => this.shadowRoot || this.ownerDocument, - id: nanoid(), + getRootNode: () => this.getRootNode(), + id: this.sub2Id, })) protected firstUpdated() { @@ -34,6 +38,7 @@ export class MenuNestedPage extends PageElement { 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) diff --git a/packages/frameworks/lit/src/normalize-props.ts b/packages/frameworks/lit/src/normalize-props.ts index acb42f21bf..026b323236 100644 --- a/packages/frameworks/lit/src/normalize-props.ts +++ b/packages/frameworks/lit/src/normalize-props.ts @@ -7,6 +7,7 @@ import { isObject } from "@zag-js/utils" // - '@event-name': handler -> event listener // - '.property': value -> property assignment +// TODO: Improve types type LitElementProps = { [key: string]: any style?: string | Record From 95117bb4218d6d4bc53fc6caf93b2093714f42ed Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Mon, 8 Sep 2025 09:51:50 +0300 Subject: [PATCH 48/56] fix(lit-ts): getRootNode type issue --- examples/lit-ts/src/pages/accordion.ts | 2 +- examples/lit-ts/src/pages/checkbox.ts | 2 +- examples/lit-ts/src/pages/collapsible.ts | 2 +- examples/lit-ts/src/pages/dialog-nested.ts | 4 ++-- examples/lit-ts/src/pages/dialog.ts | 2 +- examples/lit-ts/src/pages/menu-nested.ts | 6 +++--- examples/lit-ts/src/pages/menu-options.ts | 2 +- examples/lit-ts/src/pages/menu.ts | 2 +- examples/lit-ts/src/pages/popover.ts | 2 +- examples/lit-ts/src/pages/radio-group.ts | 2 +- examples/lit-ts/src/pages/range-slider.ts | 2 +- examples/lit-ts/src/pages/slider.ts | 2 +- examples/lit-ts/src/pages/switch.ts | 2 +- examples/lit-ts/src/pages/tabs.ts | 2 +- examples/lit-ts/src/pages/toggle-group.ts | 2 +- examples/lit-ts/src/pages/toggle.ts | 6 +----- 16 files changed, 19 insertions(+), 23 deletions(-) diff --git a/examples/lit-ts/src/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts index 2286502611..812fabc9aa 100644 --- a/examples/lit-ts/src/pages/accordion.ts +++ b/examples/lit-ts/src/pages/accordion.ts @@ -19,7 +19,7 @@ export class AccordionPage extends PageElement { private controls = new ControlsController(this, accordionControls) private machine = new MachineController(this, accordion.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/checkbox.ts b/examples/lit-ts/src/pages/checkbox.ts index c943b185e5..e0165ca89c 100644 --- a/examples/lit-ts/src/pages/checkbox.ts +++ b/examples/lit-ts/src/pages/checkbox.ts @@ -18,7 +18,7 @@ export class CheckboxPage extends PageElement { private controls = new ControlsController(this, checkboxControls) private machine = new MachineController(this, checkbox.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), name: "checkbox", ...this.controls.context, diff --git a/examples/lit-ts/src/pages/collapsible.ts b/examples/lit-ts/src/pages/collapsible.ts index 3260dac917..2d22b580f7 100644 --- a/examples/lit-ts/src/pages/collapsible.ts +++ b/examples/lit-ts/src/pages/collapsible.ts @@ -20,7 +20,7 @@ export class CollapsiblePage extends PageElement { private controls = new ControlsController(this, collapsibleControls) private machine = new MachineController(this, collapsible.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/dialog-nested.ts b/examples/lit-ts/src/pages/dialog-nested.ts index aa9883a8f2..15ccc7b307 100644 --- a/examples/lit-ts/src/pages/dialog-nested.ts +++ b/examples/lit-ts/src/pages/dialog-nested.ts @@ -15,13 +15,13 @@ export class DialogNestedPage extends PageElement { // Dialog 1 private machine1 = new MachineController(this, dialog.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), })) // Dialog 2 private machine2 = new MachineController(this, dialog.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), })) diff --git a/examples/lit-ts/src/pages/dialog.ts b/examples/lit-ts/src/pages/dialog.ts index 5205673ada..ed59deff40 100644 --- a/examples/lit-ts/src/pages/dialog.ts +++ b/examples/lit-ts/src/pages/dialog.ts @@ -14,7 +14,7 @@ export class DialogPage extends PageElement { static styles = unsafeCSS(styleComponent + styleLayout + stylePage) private machine = new MachineController(this, dialog.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), })) diff --git a/examples/lit-ts/src/pages/menu-nested.ts b/examples/lit-ts/src/pages/menu-nested.ts index d2452ad9d0..6bf5417a31 100644 --- a/examples/lit-ts/src/pages/menu-nested.ts +++ b/examples/lit-ts/src/pages/menu-nested.ts @@ -19,17 +19,17 @@ export class MenuNestedPage extends PageElement { private sub2Id = nanoid(5) private rootMachine = new MachineController(this, menu.machine, () => ({ - getRootNode: () => this.getRootNode(), + getRootNode: () => this.shadowRoot || this.ownerDocument, id: this.rootId, })) private subMachine = new MachineController(this, menu.machine, () => ({ - getRootNode: () => this.getRootNode(), + getRootNode: () => this.shadowRoot || this.ownerDocument, id: this.subId, })) private sub2Machine = new MachineController(this, menu.machine, () => ({ - getRootNode: () => this.getRootNode(), + getRootNode: () => this.shadowRoot || this.ownerDocument, id: this.sub2Id, })) diff --git a/examples/lit-ts/src/pages/menu-options.ts b/examples/lit-ts/src/pages/menu-options.ts index dbc1fe7e1a..b3df9d8009 100644 --- a/examples/lit-ts/src/pages/menu-options.ts +++ b/examples/lit-ts/src/pages/menu-options.ts @@ -18,7 +18,7 @@ export class MenuOptionsPage extends PageElement { private controls = new ControlsController(this, menuControls) private machine = new MachineController(this, menu.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/menu.ts b/examples/lit-ts/src/pages/menu.ts index 1ab1db5a62..7cf57fd830 100644 --- a/examples/lit-ts/src/pages/menu.ts +++ b/examples/lit-ts/src/pages/menu.ts @@ -18,7 +18,7 @@ export class MenuPage extends PageElement { private controls = new ControlsController(this, menuControls) private machine = new MachineController(this, menu.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), onSelect: console.log, ...this.controls.context, diff --git a/examples/lit-ts/src/pages/popover.ts b/examples/lit-ts/src/pages/popover.ts index 2b352e0802..71a8f957d7 100644 --- a/examples/lit-ts/src/pages/popover.ts +++ b/examples/lit-ts/src/pages/popover.ts @@ -18,7 +18,7 @@ export class PopoverPage extends PageElement { private controls = new ControlsController(this, popoverControls) private machine = new MachineController(this, popover.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/radio-group.ts b/examples/lit-ts/src/pages/radio-group.ts index 49156f9ade..497ee329a7 100644 --- a/examples/lit-ts/src/pages/radio-group.ts +++ b/examples/lit-ts/src/pages/radio-group.ts @@ -18,7 +18,7 @@ export class RadioGroupPage extends PageElement { private controls = new ControlsController(this, radioControls) private machine = new MachineController(this, radio.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), name: "fruit", ...this.controls.context, diff --git a/examples/lit-ts/src/pages/range-slider.ts b/examples/lit-ts/src/pages/range-slider.ts index a41bb1e2a7..8d83fc19c1 100644 --- a/examples/lit-ts/src/pages/range-slider.ts +++ b/examples/lit-ts/src/pages/range-slider.ts @@ -19,7 +19,7 @@ export class RangeSliderPage extends PageElement { private controls = new ControlsController(this, sliderControls) private machine = new MachineController(this, slider.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), name: "quantity", defaultValue: [10, 60], diff --git a/examples/lit-ts/src/pages/slider.ts b/examples/lit-ts/src/pages/slider.ts index 1a18a518ce..ff60965c2f 100644 --- a/examples/lit-ts/src/pages/slider.ts +++ b/examples/lit-ts/src/pages/slider.ts @@ -19,7 +19,7 @@ export class SliderPage extends PageElement { private controls = new ControlsController(this, sliderControls) private machine = new MachineController(this, slider.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), name: "quantity", defaultValue: [0], diff --git a/examples/lit-ts/src/pages/switch.ts b/examples/lit-ts/src/pages/switch.ts index 313b617bf6..a45823f202 100644 --- a/examples/lit-ts/src/pages/switch.ts +++ b/examples/lit-ts/src/pages/switch.ts @@ -18,7 +18,7 @@ export class SwitchPage extends PageElement { private controls = new ControlsController(this, switchControls) private machine = new MachineController(this, zagSwitch.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), name: "switch", ...this.controls.context, diff --git a/examples/lit-ts/src/pages/tabs.ts b/examples/lit-ts/src/pages/tabs.ts index 0b951c2097..8186f57482 100644 --- a/examples/lit-ts/src/pages/tabs.ts +++ b/examples/lit-ts/src/pages/tabs.ts @@ -18,7 +18,7 @@ export class TabsPage extends PageElement { private controls = new ControlsController(this, tabsControls) private machine = new MachineController(this, tabs.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), defaultValue: "nils", ...this.controls.context, diff --git a/examples/lit-ts/src/pages/toggle-group.ts b/examples/lit-ts/src/pages/toggle-group.ts index 050ae5fbe4..7337d228fb 100644 --- a/examples/lit-ts/src/pages/toggle-group.ts +++ b/examples/lit-ts/src/pages/toggle-group.ts @@ -18,7 +18,7 @@ export class ToggleGroupPage extends PageElement { private controls = new ControlsController(this, toggleGroupControls) private machine = new MachineController(this, toggleGroup.machine, () => ({ - getRootNode: () => this.shadowRoot, + getRootNode: () => this.shadowRoot || this.ownerDocument, id: nanoid(), ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/toggle.ts b/examples/lit-ts/src/pages/toggle.ts index 31575896f2..0afce7eca4 100644 --- a/examples/lit-ts/src/pages/toggle.ts +++ b/examples/lit-ts/src/pages/toggle.ts @@ -7,17 +7,13 @@ 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 { nanoid } from "nanoid" 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, () => ({ - getRootNode: () => this.shadowRoot, - id: nanoid(), - })) + private machine = new MachineController(this, toggle.machine) render() { const api = toggle.connect(this.machine.service, normalizeProps) From 8803203166c21f18e9c265649608a2ebb05b001a Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Mon, 8 Sep 2025 10:16:55 +0300 Subject: [PATCH 49/56] fix(lit-ts): stabilize ids --- examples/lit-ts/src/pages/accordion.ts | 3 ++- examples/lit-ts/src/pages/checkbox.ts | 3 ++- examples/lit-ts/src/pages/collapsible.ts | 3 ++- examples/lit-ts/src/pages/dialog-nested.ts | 7 +++++-- examples/lit-ts/src/pages/dialog.ts | 4 +++- examples/lit-ts/src/pages/menu-options.ts | 3 ++- examples/lit-ts/src/pages/menu.ts | 3 ++- examples/lit-ts/src/pages/popover.ts | 3 ++- examples/lit-ts/src/pages/radio-group.ts | 3 ++- examples/lit-ts/src/pages/range-slider.ts | 3 ++- examples/lit-ts/src/pages/slider.ts | 3 ++- examples/lit-ts/src/pages/switch.ts | 3 ++- examples/lit-ts/src/pages/tabs.ts | 3 ++- examples/lit-ts/src/pages/toggle-group.ts | 3 ++- 14 files changed, 32 insertions(+), 15 deletions(-) diff --git a/examples/lit-ts/src/pages/accordion.ts b/examples/lit-ts/src/pages/accordion.ts index 812fabc9aa..0d05379578 100644 --- a/examples/lit-ts/src/pages/accordion.ts +++ b/examples/lit-ts/src/pages/accordion.ts @@ -17,10 +17,11 @@ 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: nanoid(), + id: this.machineId, ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/checkbox.ts b/examples/lit-ts/src/pages/checkbox.ts index e0165ca89c..07274ccea4 100644 --- a/examples/lit-ts/src/pages/checkbox.ts +++ b/examples/lit-ts/src/pages/checkbox.ts @@ -16,10 +16,11 @@ 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: nanoid(), + id: this.machineId, name: "checkbox", ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/collapsible.ts b/examples/lit-ts/src/pages/collapsible.ts index 2d22b580f7..0fef118441 100644 --- a/examples/lit-ts/src/pages/collapsible.ts +++ b/examples/lit-ts/src/pages/collapsible.ts @@ -18,10 +18,11 @@ 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: nanoid(), + id: this.machineId, ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/dialog-nested.ts b/examples/lit-ts/src/pages/dialog-nested.ts index 15ccc7b307..871ec408f7 100644 --- a/examples/lit-ts/src/pages/dialog-nested.ts +++ b/examples/lit-ts/src/pages/dialog-nested.ts @@ -13,16 +13,19 @@ import { PageElement } from "../lib/page-element" 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: nanoid(), + id: this.machine1Id, })) // Dialog 2 private machine2 = new MachineController(this, dialog.machine, () => ({ getRootNode: () => this.shadowRoot || this.ownerDocument, - id: nanoid(), + id: this.machine2Id, })) render() { diff --git a/examples/lit-ts/src/pages/dialog.ts b/examples/lit-ts/src/pages/dialog.ts index ed59deff40..8cc84fcced 100644 --- a/examples/lit-ts/src/pages/dialog.ts +++ b/examples/lit-ts/src/pages/dialog.ts @@ -13,9 +13,11 @@ import { PageElement } from "../lib/page-element" 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: nanoid(), + id: this.machineId, })) render() { diff --git a/examples/lit-ts/src/pages/menu-options.ts b/examples/lit-ts/src/pages/menu-options.ts index b3df9d8009..1161fb6e1b 100644 --- a/examples/lit-ts/src/pages/menu-options.ts +++ b/examples/lit-ts/src/pages/menu-options.ts @@ -16,10 +16,11 @@ 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: nanoid(), + id: this.machineId, ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/menu.ts b/examples/lit-ts/src/pages/menu.ts index 7cf57fd830..b95cbfe59d 100644 --- a/examples/lit-ts/src/pages/menu.ts +++ b/examples/lit-ts/src/pages/menu.ts @@ -16,10 +16,11 @@ 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: nanoid(), + id: this.machineId, onSelect: console.log, ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/popover.ts b/examples/lit-ts/src/pages/popover.ts index 71a8f957d7..23f42e1ac4 100644 --- a/examples/lit-ts/src/pages/popover.ts +++ b/examples/lit-ts/src/pages/popover.ts @@ -16,10 +16,11 @@ 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: nanoid(), + id: this.machineId, ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/radio-group.ts b/examples/lit-ts/src/pages/radio-group.ts index 497ee329a7..2947727515 100644 --- a/examples/lit-ts/src/pages/radio-group.ts +++ b/examples/lit-ts/src/pages/radio-group.ts @@ -16,10 +16,11 @@ 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: nanoid(), + id: this.machineId, name: "fruit", ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/range-slider.ts b/examples/lit-ts/src/pages/range-slider.ts index 8d83fc19c1..56e045dd9c 100644 --- a/examples/lit-ts/src/pages/range-slider.ts +++ b/examples/lit-ts/src/pages/range-slider.ts @@ -17,10 +17,11 @@ 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: nanoid(), + id: this.machineId, name: "quantity", defaultValue: [10, 60], ...this.controls.context, diff --git a/examples/lit-ts/src/pages/slider.ts b/examples/lit-ts/src/pages/slider.ts index ff60965c2f..2067f82885 100644 --- a/examples/lit-ts/src/pages/slider.ts +++ b/examples/lit-ts/src/pages/slider.ts @@ -17,10 +17,11 @@ 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: nanoid(), + id: this.machineId, name: "quantity", defaultValue: [0], ...this.controls.context, diff --git a/examples/lit-ts/src/pages/switch.ts b/examples/lit-ts/src/pages/switch.ts index a45823f202..f02734f644 100644 --- a/examples/lit-ts/src/pages/switch.ts +++ b/examples/lit-ts/src/pages/switch.ts @@ -16,10 +16,11 @@ 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: nanoid(), + id: this.machineId, name: "switch", ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/tabs.ts b/examples/lit-ts/src/pages/tabs.ts index 8186f57482..d1c80d6be2 100644 --- a/examples/lit-ts/src/pages/tabs.ts +++ b/examples/lit-ts/src/pages/tabs.ts @@ -16,10 +16,11 @@ 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: nanoid(), + id: this.machineId, defaultValue: "nils", ...this.controls.context, })) diff --git a/examples/lit-ts/src/pages/toggle-group.ts b/examples/lit-ts/src/pages/toggle-group.ts index 7337d228fb..b68f78b5c4 100644 --- a/examples/lit-ts/src/pages/toggle-group.ts +++ b/examples/lit-ts/src/pages/toggle-group.ts @@ -16,10 +16,11 @@ 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: nanoid(), + id: this.machineId, ...this.controls.context, })) From 3d7368c291430091b351448e92501d2279148894 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Mon, 8 Sep 2025 10:50:43 +0300 Subject: [PATCH 50/56] fix(lit-ts): sort menu alphabetically --- examples/lit-ts/src/main.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/lit-ts/src/main.ts b/examples/lit-ts/src/main.ts index a2571166f7..49d2998e6d 100644 --- a/examples/lit-ts/src/main.ts +++ b/examples/lit-ts/src/main.ts @@ -27,6 +27,9 @@ 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 From a953f273489736c51ff4eccfb20129a049155b9e Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Tue, 9 Sep 2025 00:57:29 +0300 Subject: [PATCH 51/56] fix(lit): mark internal machine properties private + readonly --- packages/frameworks/lit/src/machine.ts | 50 +++++++++++++------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts index 3a160d8e0f..88c7a8effb 100644 --- a/packages/frameworks/lit/src/machine.ts +++ b/packages/frameworks/lit/src/machine.ts @@ -21,44 +21,46 @@ import { bindable } from "./bindable" import { createRefs } from "./refs" export class LitMachine { - scope: Scope - ctx: BindableContext - prop: PropFn - state: Bindable - refs: BindableRefs - computed: ComputedFn + 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 effects = new Map() + private readonly effects = new Map() private transition: any = null private cleanups: VoidFunction[] = [] - private subscriptions: Array<(service: Service) => void> = [] + private readonly subscriptions: Array<(service: Service) => void> = [] + private trackers: { deps: any[]; fn: any }[] = [] + private status = MachineStatus.NotStarted - private getEvent = () => ({ + private readonly getEvent = () => ({ ...this.event, current: () => this.event, previous: () => this.previousEvent, }) - private getState = () => ({ + 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), }) - debug = (...args: any[]) => { + private readonly debug = (...args: any[]) => { if (this.machine.debug) console.log(...args) } - notify = () => { + private readonly notify = () => { this.publish() } constructor( - private machine: Machine, + private readonly machine: Machine, userProps: Partial | (() => Partial) = {}, ) { // create scope @@ -179,7 +181,7 @@ export class LitMachine { this.cleanups.push(subscribe(this.state.ref, () => this.notify())) } - send = (event: any) => { + private readonly send = (event: any) => { if (this.status !== MachineStatus.Started) return queueMicrotask(() => { @@ -216,7 +218,7 @@ export class LitMachine { }) } - private action = (keys: ActionsOrFn | undefined) => { + private readonly action = (keys: ActionsOrFn | undefined) => { const strs = isFunction(keys) ? keys(this.getParams()) : keys if (!strs) return const fns = strs.map((s) => { @@ -229,12 +231,12 @@ export class LitMachine { } } - private guard = (str: T["guard"] | GuardFn) => { + private readonly guard = (str: T["guard"] | GuardFn) => { if (isFunction(str)) return str(this.getParams()) return this.machine.implementations?.guards?.[str](this.getParams()) } - private effect = (keys: EffectsOrFn | undefined) => { + private readonly effect = (keys: EffectsOrFn | undefined) => { const strs = isFunction(keys) ? keys(this.getParams()) : keys if (!strs) return const fns = strs.map((s) => { @@ -250,7 +252,7 @@ export class LitMachine { return () => cleanups.forEach((fn) => fn?.()) } - private choose: ChooseFn = (transitions) => { + private readonly choose: ChooseFn = (transitions) => { return toArray(transitions).find((t: any) => { let result = !t.guard if (isString(t.guard)) result = !!this.guard(t.guard) @@ -285,8 +287,6 @@ export class LitMachine { this.subscriptions.push(fn) } - private status = MachineStatus.NotStarted - get started() { return this.status === MachineStatus.Started } @@ -305,18 +305,16 @@ export class LitMachine { } } - private publish = () => { + private readonly publish = () => { this.callTrackers() this.subscriptions.forEach((fn) => fn(this.service)) } - private trackers: { deps: any[]; fn: any }[] = [] - - private setupTrackers = () => { + private readonly setupTrackers = () => { this.machine.watch?.(this.getParams()) } - private callTrackers = () => { + private readonly callTrackers = () => { this.trackers.forEach(({ deps, fn }) => { const next = deps.map((dep) => dep()) if (!isEqual(fn.prev, next)) { @@ -326,7 +324,7 @@ export class LitMachine { }) } - getParams = (): Params => ({ + private readonly getParams = (): Params => ({ state: this.getState(), context: this.ctx, event: this.getEvent(), From 6b79d4806fcdf08b8a66405b7c0c40e39ea4573a Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Tue, 9 Sep 2025 01:01:17 +0300 Subject: [PATCH 52/56] refactor(lit): remove unused machine notify method --- packages/frameworks/lit/src/machine.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts index 88c7a8effb..2fe6c8f79b 100644 --- a/packages/frameworks/lit/src/machine.ts +++ b/packages/frameworks/lit/src/machine.ts @@ -55,10 +55,6 @@ export class LitMachine { if (this.machine.debug) console.log(...args) } - private readonly notify = () => { - this.publish() - } - constructor( private readonly machine: Machine, userProps: Partial | (() => Partial) = {}, @@ -98,7 +94,7 @@ export class LitMachine { // subscribe to context changes if (context) { Object.values(context).forEach((item: any) => { - const unsub = subscribe(item.ref, () => this.notify()) + const unsub = subscribe(item.ref, () => this.publish()) this.cleanups.push(unsub) }) } @@ -178,7 +174,7 @@ export class LitMachine { }, })) this.state = state - this.cleanups.push(subscribe(this.state.ref, () => this.notify())) + this.cleanups.push(subscribe(this.state.ref, () => this.publish())) } private readonly send = (event: any) => { From 6b8c21b7c6082dd58165d331671c46f959a10444 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Tue, 9 Sep 2025 01:03:30 +0300 Subject: [PATCH 53/56] fix(lit): account for some machine edge cases --- packages/frameworks/lit/src/machine.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts index 2fe6c8f79b..15fea09cf4 100644 --- a/packages/frameworks/lit/src/machine.ts +++ b/packages/frameworks/lit/src/machine.ts @@ -178,9 +178,10 @@ export class LitMachine { } private readonly send = (event: any) => { - if (this.status !== MachineStatus.Started) return - queueMicrotask(() => { + // check status inside microtask to prevent race condition + if (this.status !== MachineStatus.Started) return + this.previousEvent = this.event this.event = event @@ -258,6 +259,8 @@ export class LitMachine { } start() { + if (this.status === MachineStatus.Started) return + this.status = MachineStatus.Started this.debug("initializing...") this.state.invoke(this.state.initial!, INIT_STATE) @@ -265,6 +268,8 @@ export class LitMachine { } stop() { + if (this.status === MachineStatus.Stopped) return + // run exit effects this.effects.forEach((fn) => fn?.()) this.effects.clear() @@ -275,12 +280,24 @@ export class LitMachine { this.cleanups.forEach((unsub) => unsub()) this.cleanups = [] + // clear trackers to prevent memory leak + this.trackers = [] + + // Should we clear this.subscriptions too? + this.status = MachineStatus.Stopped this.debug("unmounting...") } - subscribe = (fn: (service: Service) => void) => { + 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 started() { @@ -334,7 +351,7 @@ export class LitMachine { }, refs: this.refs, computed: this.computed, - flush: identity, + flush: identity, // requestUpdate? scope: this.scope, choose: this.choose, }) From eaf04c815fe1241cd9b8d0764e8db4798f04664d Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Tue, 9 Sep 2025 01:08:00 +0300 Subject: [PATCH 54/56] refactor(lit): use 'self' in machine constructor --- packages/frameworks/lit/src/machine.ts | 85 +++++++++++--------------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts index 15fea09cf4..f75f4baf76 100644 --- a/packages/frameworks/lit/src/machine.ts +++ b/packages/frameworks/lit/src/machine.ts @@ -59,36 +59,31 @@ export class LitMachine { 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 - const prop: PropFn = (key) => { + this.prop = (key) => { const __props = runIfFn(userProps) - const props: any = machine.props?.({ props: compact(__props), scope: this.scope }) ?? __props + const props: any = machine.props?.({ props: compact(__props), scope: self.scope }) ?? __props return props[key] as any } - this.prop = prop // create context const context: any = machine.context?.({ - prop, + prop: self.prop, bindable, - scope: this.scope, + scope: self.scope, flush(fn: VoidFunction) { - queueMicrotask(fn) - }, - getContext() { - return ctx as any - }, - getComputed() { - return computed as any + queueMicrotask(fn) // requestUpdate? }, - getRefs() { - return refs as any - }, - getEvent: this.getEvent.bind(this), + getContext: () => self.ctx as any, + getComputed: () => self.computed as any, + getRefs: () => self.refs as any, + getEvent: self.getEvent, }) // subscribe to context changes @@ -100,7 +95,7 @@ export class LitMachine { } // context function - const ctx: BindableContext = { + this.ctx = { get(key) { return context?.[key].get() }, @@ -115,66 +110,60 @@ export class LitMachine { return context?.[key].hash(current) }, } - this.ctx = ctx - - const computed: ComputedFn = (key) => { - return ( - machine.computed?.[key]({ - context: ctx as any, - event: this.getEvent(), - prop, - refs: this.refs, - scope: this.scope, - computed: computed as any, - }) ?? ({} as any) - ) - } - this.computed = computed - const refs: BindableRefs = createRefs(machine.refs?.({ prop, context: ctx }) ?? {}) - this.refs = refs + 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 - const state = bindable(() => ({ - defaultValue: machine.initialState({ prop }), + this.state = bindable(() => ({ + defaultValue: self.machine.initialState({ prop: self.prop }), onChange: (nextState, prevState) => { // compute effects: exit -> transition -> enter // exit effects if (prevState) { - const exitEffects = this.effects.get(prevState) + const exitEffects = self.effects.get(prevState) exitEffects?.() - this.effects.delete(prevState) + self.effects.delete(prevState) } // exit actions if (prevState) { // @ts-ignore - this.action(machine.states[prevState]?.exit) + self.action(self.machine.states[prevState]?.exit) } // transition actions - this.action(this.transition?.actions) + self.action(self.transition?.actions) // enter effect // @ts-ignore - const cleanup = this.effect(machine.states[nextState]?.effects) - if (cleanup) this.effects.set(nextState as string, cleanup) + 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) { - this.action(machine.entry) - const cleanup = this.effect(machine.effects) - if (cleanup) this.effects.set(INIT_STATE, cleanup) + self.action(self.machine.entry) + const cleanup = self.effect(self.machine.effects) + if (cleanup) self.effects.set(INIT_STATE, cleanup) } // enter actions // @ts-ignore - this.action(machine.states[nextState]?.entry) + self.action(self.machine.states[nextState]?.entry) }, })) - this.state = state - this.cleanups.push(subscribe(this.state.ref, () => this.publish())) + + this.cleanups.push(subscribe(this.state.ref, () => self.publish())) } private readonly send = (event: any) => { From c1923b25975dbc5082d0c69d08b50fe9b5f7871b Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Tue, 9 Sep 2025 09:21:33 +0300 Subject: [PATCH 55/56] fix(lit): clean up subscriptions --- packages/frameworks/lit/src/machine.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts index f75f4baf76..1be0df835d 100644 --- a/packages/frameworks/lit/src/machine.ts +++ b/packages/frameworks/lit/src/machine.ts @@ -35,7 +35,7 @@ export class LitMachine { private transition: any = null private cleanups: VoidFunction[] = [] - private readonly subscriptions: Array<(service: Service) => void> = [] + private subscriptions: Array<(service: Service) => void> = [] private trackers: { deps: any[]; fn: any }[] = [] private status = MachineStatus.NotStarted @@ -268,11 +268,8 @@ export class LitMachine { // unsubscribe from all subscriptions this.cleanups.forEach((unsub) => unsub()) this.cleanups = [] - - // clear trackers to prevent memory leak this.trackers = [] - - // Should we clear this.subscriptions too? + this.subscriptions = [] this.status = MachineStatus.Stopped this.debug("unmounting...") From 5b51fc985292ed579743fd974824fa379691da45 Mon Sep 17 00:00:00 2001 From: Nik Paro Date: Tue, 9 Sep 2025 09:27:09 +0300 Subject: [PATCH 56/56] fix(lit): remove unneeded started fn --- packages/frameworks/lit/src/machine-controller.ts | 4 +--- packages/frameworks/lit/src/machine.ts | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/frameworks/lit/src/machine-controller.ts b/packages/frameworks/lit/src/machine-controller.ts index 651f0279fd..7d2db30351 100644 --- a/packages/frameworks/lit/src/machine-controller.ts +++ b/packages/frameworks/lit/src/machine-controller.ts @@ -24,9 +24,7 @@ export class MachineController implements Reactiv hostUpdated(): void { // Start the machine after the initial html has been rendered - if (!this.machine.started) { - this.machine.start() - } + this.machine.start() } hostDisconnected() { diff --git a/packages/frameworks/lit/src/machine.ts b/packages/frameworks/lit/src/machine.ts index 1be0df835d..904905ab80 100644 --- a/packages/frameworks/lit/src/machine.ts +++ b/packages/frameworks/lit/src/machine.ts @@ -286,10 +286,6 @@ export class LitMachine { } } - get started() { - return this.status === MachineStatus.Started - } - get service(): Service { return { state: this.getState(),