diff --git a/README.md b/README.md index 3bbbdfba..0aa0e457 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ The rules that hold across every component — worth two minutes before your fir flat. Parts are styleable via `data-slot` and state attributes such as `data-open`. - There is **no polymorphic `as` prop**. -Forms are TanStack Form plus any Standard Schema validator; Table is headless and assembled -from hooks. Both, with toasts, icons and dates, are covered in +Forms are built in and take any Standard Schema validator; DataGrid derives filtering, +sorting, pagination and selection itself. Both, with toasts, icons and dates, are covered in **[docs/ui-usage.md](https://github.com/pathscale/ui/blob/master/docs/ui-usage.md)**. ## Requirements @@ -137,8 +137,7 @@ line and must not be used. The complete compiler contract, hard-error behavior, commands, and porting analyzer are documented in [docs/layouts.md](docs/layouts.md). -Peers include `@solid-primitives/*`, `@tanstack/solid-form` and `@tanstack/solid-table`. -Two are optional and only needed for the features they back: `popmotion` (JS animation +Two peers are optional and only needed for the features they back: `popmotion` (JS animation driver) and `@standard-schema/spec` (schema validation). ## Subpath exports diff --git a/bun.lock b/bun.lock index 53b1c804..f2d1281a 100644 --- a/bun.lock +++ b/bun.lock @@ -5,9 +5,7 @@ "": { "name": "@pathscale/ui", "dependencies": { - "@iconify/tailwind4": "^1.2.3", - "@pathscale/rsbuild-plugin-iconify": "^1.0.4", - "@tanstack/solid-virtual": "^3.13.27", + "clsx": "^2.1.1", "tailwind-merge": "^3.6.0", }, "devDependencies": { @@ -19,23 +17,10 @@ "@rsbuild/core": "^2.0.9", "@rsbuild/plugin-solid": "^1.2.1", "@rslib/core": "^0.22.0", - "@solid-primitives/event-listener": "^2.4.5", - "@solid-primitives/intersection-observer": "^2.2.4", - "@solid-primitives/keyboard": "^1.3.5", - "@solid-primitives/media": "^2.3.5", - "@solid-primitives/props": "^3.2.3", - "@solid-primitives/resize-observer": "^2.1.5", - "@solid-primitives/scheduled": "^1.5.3", - "@solid-primitives/scroll": "^2.1.5", - "@solid-primitives/storage": "^2.1.4", - "@solid-primitives/utils": "^6.4.0", "@standard-schema/spec": "^1.1.0", - "@tanstack/solid-form": "^1.33.0", - "@tanstack/solid-table": "^8.21.3", "@types/bun": "^1.3.14", "babel-preset-solid": "^1.9.12", "cally": "^0.8.0", - "clsx": "^2.1.1", "cssnano": "^7.1.9", "postcss": "^8.5.15", "postcss-cli": "^11.0.1", @@ -48,19 +33,7 @@ "typescript": "^6.0.3", }, "peerDependencies": { - "@solid-primitives/event-listener": "^2.3.0", - "@solid-primitives/intersection-observer": "^2.1.3", - "@solid-primitives/keyboard": "^1.2.5", - "@solid-primitives/media": "^2.2.5", - "@solid-primitives/props": "^3.1.8", - "@solid-primitives/resize-observer": "^2.0.22", - "@solid-primitives/scheduled": "^1.4.1", - "@solid-primitives/scroll": "^2.0.20", - "@solid-primitives/storage": "^2.1.1", - "@solid-primitives/utils": "^6.2.1", "@standard-schema/spec": "^1.0.0", - "@tanstack/solid-form": "^1.29.0", - "@tanstack/solid-table": "^8.0.0", "popmotion": "^11.0.5", "solid-js": "^1.9", "solid-layouts": "^0.1.3", @@ -178,8 +151,6 @@ "@colordx/core": ["@colordx/core@5.4.3", "", {}, "sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ=="], - "@cyberalien/svg-utils": ["@cyberalien/svg-utils@1.2.15", "", { "dependencies": { "@iconify/types": "^2.0.0" } }, "sha512-ZbKU6npzW5PNocdoLVJYfKzaP+c/RpT6JUkoaKrW1DOcw6lyXub8XtcNpI3xok6FnyNjS6ZbsrrtjTnS9yeZAQ=="], - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -188,10 +159,6 @@ "@iconify/json": ["@iconify/json@2.2.481", "", { "dependencies": { "@iconify/types": "*", "pathe": "^2.0.3" } }, "sha512-H/bCPJN+JawFnEgSFtBGQMz2nAc3KNsLZ6fKG6KfJX7X9H0blv0Acbt6umCBVRxZ0xBM/9FcUYZst4/Zv6SB/A=="], - "@iconify/tailwind4": ["@iconify/tailwind4@1.2.3", "", { "dependencies": { "@iconify/tools": "^5.0.5", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0" }, "peerDependencies": { "tailwindcss": ">= 4.0.0" } }, "sha512-z8SKiMHRASJKF/IY//87MF88lcB7ulxh8vlhQXXLWsBkNtOh6ese9R41MyGpQeqXdRvQVt+/fX2glQtHFjQ+MA=="], - - "@iconify/tools": ["@iconify/tools@5.0.12", "", { "dependencies": { "@cyberalien/svg-utils": "^1.2.15", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.3", "fflate": "^0.8.3", "modern-tar": "^0.7.6", "pathe": "^2.0.3", "svgo": "^4.0.1" } }, "sha512-aFPwSFmFphUPVjNLUkgxgUPSPVgTEEjv0mE7NgvfoQBuE5TtsMrKa4HTY65cM5oXZ2a1xKQcW/RKhiOKEiAarw=="], - "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], "@iconify/utils": ["@iconify/utils@2.3.0", "", { "dependencies": { "@antfu/install-pkg": "^1.0.0", "@antfu/utils": "^8.1.0", "@iconify/types": "^2.0.0", "debug": "^4.4.0", "globals": "^15.14.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "mlly": "^1.7.4" } }, "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA=="], @@ -220,8 +187,6 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@pathscale/rsbuild-plugin-iconify": ["@pathscale/rsbuild-plugin-iconify@1.0.4", "", { "dependencies": { "@iconify/json": "^2.2.481", "@iconify/utils": "^2.3.0", "@rspack/core": "^1.7.11", "cssnano": "^7.1.9", "postcss": "^8.5.15", "postcss-cli": "^11.0.1", "svgo": "^3.3.3" }, "peerDependencies": { "@rsbuild/core": "^1.3.22", "@rslib/core": "^0.9.1" } }, "sha512-KrCuzAa9AzbXojh7ne+yfldfipWZ39d/BFfwDhLNlDvQxpena5I/0h4eoa4x4RlaCux6h+Ay81KmHGrdR0tpyw=="], - "@pathscale/rsbuild-plugin-ui-css-purge": ["@pathscale/rsbuild-plugin-ui-css-purge@0.9.11", "", { "dependencies": { "@swc/core": "^1.15.40", "lightningcss": "^1.32.0", "postcss": "^8.5.15" }, "bin": { "rsbuild-plugin-ui-css-purge": "dist/postbuild-purge.js", "generate-manifest": "src/generate-manifest.ts" } }, "sha512-iIyScjlJ1/yfbtD+vPWQP+S5WSQp6tBtppDAMGImBPCxzoSnpQPwhvsyFGpNyVwGUZj/8QxhbpY34ev0FXKpng=="], "@rsbuild/core": ["@rsbuild/core@2.0.9", "", { "dependencies": { "@rspack/core": "~2.0.5", "@swc/helpers": "^0.5.23" }, "peerDependencies": { "core-js": ">= 3.0.0" }, "optionalPeers": ["core-js"], "bin": { "rsbuild": "./bin/rsbuild.js" } }, "sha512-lK2bMNuwh3TuLXLskS7nG3fnQk+6eaLeeZiquJWcna4JZx9iaI59JSd+iLcg5TeZLjEVygkwn/HcE+yuYDQRAw=="], @@ -256,32 +221,6 @@ "@rspack/core": ["@rspack/core@2.0.5", "", { "dependencies": { "@rspack/binding": "2.0.5" }, "peerDependencies": { "@module-federation/runtime-tools": "^0.24.1 || ^2.0.0", "@swc/helpers": "^0.5.23" }, "optionalPeers": ["@module-federation/runtime-tools", "@swc/helpers"] }, "sha512-9tv2HAnSiTote5WPH2tmz1hLZ1zKbzkiZc1eYp7LP/8jcsiJBuf40ihiWidAgbbuYtJo3kWET6q+qOm5UhNiGA=="], - "@rspack/lite-tapable": ["@rspack/lite-tapable@1.1.0", "", {}, "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw=="], - - "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.5", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA=="], - - "@solid-primitives/intersection-observer": ["@solid-primitives/intersection-observer@2.2.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-3+QaAT/TUdEaeKkqiTf+sk8g1/vE+QM9PucG2/JI8hLI8ALpBYBnlhtvcwIdpjaKMSpPN/1L4hneLvwqnx4YRA=="], - - "@solid-primitives/keyboard": ["@solid-primitives/keyboard@1.3.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-sav+l+PL+74z3yaftVs7qd8c2SXkqzuxPOVibUe5wYMt+U5Hxp3V3XCPgBPN2I6cANjvoFtz0NiU8uHVLdi9FQ=="], - - "@solid-primitives/media": ["@solid-primitives/media@2.3.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-LX9fB5WDaK87FMDtUB1qokBOfT2et9Uobv/zZaKLH9caFSz4+P70MBKEIBHcZQy+9MV5M2XvGYLTbLskjkzMjA=="], - - "@solid-primitives/props": ["@solid-primitives/props@3.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA=="], - - "@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw=="], - - "@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], - - "@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-oNwLE6E6lxJAWrc8QXuwM0k2oU1BnANnkChwMw82aK1j3+mWGJkG1IFe5gCwbV+afYmjI76t9JJV3md/8tLw+g=="], - - "@solid-primitives/scroll": ["@solid-primitives/scroll@2.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-G7t4Kt1E+f4d7iZ2dGDWsra2k1WQ5VOtXrTcg7MC+DrR5QMq8YCj9wB/C9yxHJ1lGi7Sv+mzjNLtVCFO5ECzUg=="], - - "@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ=="], - - "@solid-primitives/storage": ["@solid-primitives/storage@2.1.4", "", { "dependencies": { "@solid-primitives/utils": "^6.2.3" }, "peerDependencies": { "solid-js": "^1.6.12", "solid-start": ">=0.2.26" }, "optionalPeers": ["solid-start"] }, "sha512-ziGRMSPi2PDnY6L6YPGWYnzmUEPIwO0PaDT9N1WNW9P2tUMMhgqKLXBrYfjytpjwY3UM6kIrbONMRADjyGtzAw=="], - - "@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@swc/core": ["@swc/core@1.15.40", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.26" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.40", "@swc/core-darwin-x64": "1.15.40", "@swc/core-linux-arm-gnueabihf": "1.15.40", "@swc/core-linux-arm64-gnu": "1.15.40", "@swc/core-linux-arm64-musl": "1.15.40", "@swc/core-linux-ppc64-gnu": "1.15.40", "@swc/core-linux-s390x-gnu": "1.15.40", "@swc/core-linux-x64-gnu": "1.15.40", "@swc/core-linux-x64-musl": "1.15.40", "@swc/core-win32-arm64-msvc": "1.15.40", "@swc/core-win32-ia32-msvc": "1.15.40", "@swc/core-win32-x64-msvc": "1.15.40" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg=="], @@ -316,26 +255,6 @@ "@swc/types": ["@swc/types@0.1.26", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw=="], - "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="], - - "@tanstack/form-core": ["@tanstack/form-core@1.33.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.11.0" } }, "sha512-AV4Pw9Dk4orFsuPBcDssfWMJFs+yMYBae7zZ4oTqrCf4ftNGQKxvrQRZeqKHG6A4TkiLeSvf2kzIjcVkrW7E6w=="], - - "@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="], - - "@tanstack/solid-form": ["@tanstack/solid-form@1.33.0", "", { "dependencies": { "@tanstack/form-core": "1.33.0", "@tanstack/solid-store": "^0.11.0" }, "peerDependencies": { "solid-js": ">=1.9.9" } }, "sha512-+KXp+T/fD+nO5JarDj1W5GctEm4aFuz13DkHVJrdIHGo0evMXlFNWJT57n+TOyvoLoKehknKjM1o41rrKClSTQ=="], - - "@tanstack/solid-store": ["@tanstack/solid-store@0.11.0", "", { "dependencies": { "@tanstack/store": "0.11.0" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-2isL0ZnnyI1iN0V+QPrxE3OcPndohBgVlBcHZYoAOIAiU1WoWjVy0q5gb0suPu1Id0h5cKC23JnwzQTxWDZD0w=="], - - "@tanstack/solid-table": ["@tanstack/solid-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "solid-js": ">=1.3" } }, "sha512-PmhfSLBxVKiFs01LtYOYrCRhCyTUjxmb4KlxRQiqcALtip8+DOJeeezQM4RSX/GUS0SMVHyH/dNboCpcO++k2A=="], - - "@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.27", "", { "dependencies": { "@tanstack/virtual-core": "3.16.1" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-pQZJa2IOR1rr76A1l4wV4iY1mrku6B8otNGw2hg1ywKCrbl74y50AbpPYR06ziMOjGjrGZBBAdTk+VY9IJON6w=="], - - "@tanstack/store": ["@tanstack/store@0.11.0", "", {}, "sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw=="], - - "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], - - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.16.1", "", {}, "sha512-br4gmJ3eI2IXth0fAAVWNdi/Iqb5ZDIUG9409Q17qnXegeHHc9vPEeMsSwpGf29GqayfmqFPtv9S4rxcZOub3Q=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -444,8 +363,6 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], @@ -464,8 +381,6 @@ "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], - "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -524,8 +439,6 @@ "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], - "modern-tar": ["modern-tar@0.7.6", "", {}, "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], @@ -662,8 +575,6 @@ "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], - "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], - "thenby": ["thenby@1.4.1", "", {}, "sha512-D5a/bO0KdalOE3q8MlrRmSxjbKZHT3MQmXkJP+r97Vw8MMwOZKOwUSEyTtK7eSMj2y0kyAjpYMRMZmmLw1FtNQ=="], "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], @@ -698,14 +609,6 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "@iconify/tailwind4/@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], - - "@iconify/tools/@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], - - "@iconify/tools/svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core": ["@rspack/core@1.7.11", "", { "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.11", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew=="], - "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], @@ -720,12 +623,6 @@ "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@iconify/tools/svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - - "@iconify/tools/svgo/css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding": ["@rspack/binding@1.7.11", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.11", "@rspack/binding-darwin-x64": "1.7.11", "@rspack/binding-linux-arm64-gnu": "1.7.11", "@rspack/binding-linux-arm64-musl": "1.7.11", "@rspack/binding-linux-x64-gnu": "1.7.11", "@rspack/binding-linux-x64-musl": "1.7.11", "@rspack/binding-wasm32-wasi": "1.7.11", "@rspack/binding-win32-arm64-msvc": "1.7.11", "@rspack/binding-win32-ia32-msvc": "1.7.11", "@rspack/binding-win32-x64-msvc": "1.7.11" } }, "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q=="], - "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], @@ -734,30 +631,6 @@ "postcss-svgo/svgo/css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], - "@iconify/tools/svgo/css-tree/mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.7.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.7.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.7.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.7.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.7.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.7.11", "", { "os": "win32", "cpu": "x64" }, "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q=="], - "postcss-svgo/svgo/css-tree/mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], } } diff --git a/docs/ui-usage.md b/docs/ui-usage.md index 71295963..eadca7b8 100644 --- a/docs/ui-usage.md +++ b/docs/ui-usage.md @@ -146,7 +146,7 @@ the same normalized hex that it displays. Surface strength, softness and accent remain consumer theme concerns; use the literal selected hex as their input rather than making the wheel silently transform it. -## Forms (TanStack Form + Standard Schema) +## Forms (built in, plus Standard Schema) ```tsx import { createForm, Form, FormField, FormSubmitButton } from "@pathscale/ui"; @@ -165,8 +165,8 @@ const form = createForm({ ``` - `Form` without a `form` prop = plain styled `
` (`FormRoot`). With `form` = context provider + wired submit. -- Inside a ``: `useField(name)` → `{value, error, touched, invalid, handleChange, handleBlur}`. **Errors are touch-gated** — `error()` is `undefined` until the field blurs. `FormSubmitButton` disables on `!canSubmit` (not touch-gated), so the button can be disabled with no visible error. -- Escape hatch: `form._tsForm` is the raw TanStack form API (typed `any` on purpose). +- Inside a ``: `useField(name)` → `{value, error, touched, invalid, handleChange, handleBlur}`. **Errors are touch-gated** — `error()` is `undefined` until the field blurs. `FormSubmitButton` disables on `!form.isValid()` (not touch-gated), so the button can be disabled with no visible error. A failed `submit()` touches every field, so the errors it refused on all become visible at once. +- The form API is the library's own: `values()`, `getFieldValue`, `getFieldMeta`, `setFieldValue`, `validateField`, `submit()`, `isSubmitting()`, `isValid()`. There is no longer a `_tsForm` escape hatch, because there is no longer a wrapped library to escape to. - Schema validation runs on change+blur+submit; blur errors clear immediately on change once valid. ## DataGrid (assembled) @@ -250,7 +250,7 @@ const table = useTableModel({ - State-slice hooks (all controlled-or-uncontrolled): `useTableSorting`, `useTableSelection`, `useTableFiltering` (per-column popovers + `getColumnFilterProps`), `useTablePagination` (⚠️ `nextPage(max)`/`lastPage(max)` need caller-supplied max page index), `useTableExpansion`. - Parts: TableRoot/ScrollContainer/Content/Header/Column/Body/Row/Cell/ExpandedRow/Footer/PageSize/ResizableContainer/ColumnResizer/LoadMore(+Content), plus SortIcon, ExpandToggle, InlineConfirm, MobileListView (responsive card fallback), VirtualSpacerRow. -- **Virtualization is not built in**: combine `useVirtualRows` (wraps @tanstack/solid-virtual) + `VirtualSpacerRow` yourself. See the Table section above. +- **Virtualization is not built in.** `useVirtualRows` was a thin wrapper over `@tanstack/solid-virtual` that nothing in the library used; it was removed with the rest of TanStack. `VirtualSpacerRow` is still here for a caller that brings its own windowing. ## Motion diff --git a/package.json b/package.json index 9461f423..8ea74e12 100644 --- a/package.json +++ b/package.json @@ -73,19 +73,7 @@ "@rsbuild/core": "^2.0.9", "@rsbuild/plugin-solid": "^1.2.1", "@rslib/core": "^0.22.0", - "@solid-primitives/event-listener": "^2.4.5", - "@solid-primitives/intersection-observer": "^2.2.4", - "@solid-primitives/keyboard": "^1.3.5", - "@solid-primitives/media": "^2.3.5", - "@solid-primitives/props": "^3.2.3", - "@solid-primitives/resize-observer": "^2.1.5", - "@solid-primitives/scheduled": "^1.5.3", - "@solid-primitives/scroll": "^2.1.5", - "@solid-primitives/storage": "^2.1.4", - "@solid-primitives/utils": "^6.4.0", "@standard-schema/spec": "^1.1.0", - "@tanstack/solid-form": "^1.33.0", - "@tanstack/solid-table": "^8.21.3", "@types/bun": "^1.3.14", "babel-preset-solid": "^1.9.12", "cally": "^0.8.0", @@ -102,23 +90,10 @@ }, "dependencies": { "clsx": "^2.1.1", - "@tanstack/solid-virtual": "^3.13.27", "tailwind-merge": "^3.6.0" }, "peerDependencies": { - "@solid-primitives/event-listener": "^2.3.0", - "@solid-primitives/intersection-observer": "^2.1.3", - "@solid-primitives/keyboard": "^1.2.5", - "@solid-primitives/media": "^2.2.5", - "@solid-primitives/props": "^3.1.8", - "@solid-primitives/resize-observer": "^2.0.22", - "@solid-primitives/scheduled": "^1.4.1", - "@solid-primitives/scroll": "^2.0.20", - "@solid-primitives/storage": "^2.1.1", - "@solid-primitives/utils": "^6.2.1", "@standard-schema/spec": "^1.0.0", - "@tanstack/solid-form": "^1.29.0", - "@tanstack/solid-table": "^8.0.0", "popmotion": "^11.0.5", "solid-js": "^1.9", "solid-layouts": "^0.1.3" diff --git a/src/components/form/Form.layout.tsx b/src/components/form/Form.layout.tsx index 19759b91..a7668f6b 100644 --- a/src/components/form/Form.layout.tsx +++ b/src/components/form/Form.layout.tsx @@ -57,7 +57,7 @@ const FormWithContext: Layout = () const handleSubmit: JSX.EventHandlerUnion = (e) => { e.preventDefault(); e.stopPropagation(); - local.form._tsForm.handleSubmit(); + void local.form.submit(); if (typeof local.onSubmit === "function") { local.onSubmit(e); } diff --git a/src/components/form/FormField.layout.tsx b/src/components/form/FormField.layout.tsx index 19158f38..653033cb 100644 --- a/src/components/form/FormField.layout.tsx +++ b/src/components/form/FormField.layout.tsx @@ -1,6 +1,5 @@ import { Show, splitProps, type Component, type JSX } from "solid-js"; import { twMerge } from "tailwind-merge"; -import type { FieldApi } from "@tanstack/solid-form"; import Input from "../input"; import type { InputFieldProps } from "../input"; @@ -75,49 +74,37 @@ const FormField: Layout = () => { }; const form = resolveForm(); - const tsForm = form._tsForm; - return ( - FieldApi) => { - const errorMessage = () => - field().state.meta.isTouched - ? getFirstFieldError((field().state.meta.errors ?? []) as unknown[]) - : undefined; + // Read straight off the form rather than through a render-prop child. The + // engine is ours now, so field state is just three accessors, and a wrapper + // component that exists only to hand them down would be a layer with no job. + const meta = () => form.getFieldMeta(local.name); + const errorMessage = () => + meta().isTouched ? getFirstFieldError(meta().errors) : undefined; - return ( -
- - - + return ( +
+ + + - { - field().handleChange(e.currentTarget.value as never); - }} - onBlur={() => field().handleBlur()} - aria-invalid={Boolean(errorMessage()) ? true : undefined} - issues={errorMessage() ? [{ code: "invalid", message: String(errorMessage()) }] : undefined} - /> + { + form.setFieldValue(local.name, e.currentTarget.value); + }} + onBlur={() => form.validateField(local.name, "blur")} + aria-invalid={errorMessage() ? true : undefined} + issues={errorMessage() ? [{ code: "invalid", message: String(errorMessage()) }] : undefined} + /> - -
- ); - }} - /> + +
); }; diff --git a/src/components/form/FormSubmitButton.layout.tsx b/src/components/form/FormSubmitButton.layout.tsx index ec17ad13..6e7efa95 100644 --- a/src/components/form/FormSubmitButton.layout.tsx +++ b/src/components/form/FormSubmitButton.layout.tsx @@ -45,24 +45,18 @@ const FormSubmitButton: Layout = }; const form = resolveForm(); - const tsForm = form._tsForm; + // `` was a render prop whose only job was to deliver two + // booleans. Both are accessors on the form now, so the component reads them + // directly and Solid tracks them the same way. return ( - ({ - canSubmit: s.canSubmit, - isSubmitting: s.isSubmitting, - })} - children={(state: () => { canSubmit: boolean; isSubmitting: boolean }) => ( - - )} - /> + ); }; diff --git a/src/components/table/index.ts b/src/components/table/index.ts index 3e271e53..5dea56b2 100644 --- a/src/components/table/index.ts +++ b/src/components/table/index.ts @@ -1,33 +1,7 @@ export type { ExpandToggleProps } from "./ExpandToggle.generated"; export { default as ExpandToggle } from "./ExpandToggle.generated"; -export type { - HookSortDescriptor, - HookSortDirection, - TableSelectionState, - UseAnchoredOverlayPositionOptions, - UseTableExpansionOptions, - UseTableExpansionResult, - UseTableFilteringOptions, - UseTableFilteringResult, - UseTableModelOptions, - UseTablePaginationOptions, - UseTablePaginationResult, - UseTableSelectionOptions, - UseTableSelectionResult, - UseTableSortingOptions, - UseTableSortingResult, -} from "./hooks"; -export { - toSortDescriptor, - toSortingState, - useAnchoredOverlayPosition, - useTableExpansion, - useTableFiltering, - useTableModel, - useTablePagination, - useTableSelection, - useTableSorting, -} from "./hooks"; +export type { UseAnchoredOverlayPositionOptions } from "./hooks"; +export { useAnchoredOverlayPosition } from "./hooks"; export type { InlineConfirmProps, InlineConfirmVariant, diff --git a/src/hooks/form/FormContext.ts b/src/hooks/form/FormContext.ts index b8d057cc..832aa74b 100644 --- a/src/hooks/form/FormContext.ts +++ b/src/hooks/form/FormContext.ts @@ -5,9 +5,8 @@ import type { FormApi } from "./createForm"; // Context // --------------------------------------------------------------------------- -// We use `any` here because the form's generic parameters are deeply nested -// TanStack types — the context just needs to hold the opaque FormApi wrapper. -// eslint-disable-next-line @typescript-eslint/no-explicit-any +// The context holds a form whose value type the provider knows and the +// consumer does not, so the stored type erases it. export type AnyFormApi = FormApi; export const FormContext = createContext(null); @@ -26,7 +25,7 @@ export const FormContext = createContext(null); * @example * // Inside a component rendered as a child of * const form = useFormContext(); - * const tsForm = form._tsForm; + * const value = form.getFieldValue("email"); */ export const useFormContext = (): AnyFormApi => { const ctx = useContext(FormContext); diff --git a/src/hooks/form/createForm.ts b/src/hooks/form/createForm.ts index cdb1ace1..3d4bd874 100644 --- a/src/hooks/form/createForm.ts +++ b/src/hooks/form/createForm.ts @@ -1,11 +1,9 @@ -import { createForm as createTSForm } from "@tanstack/solid-form"; +import { createStore, produce } from "solid-js/store"; import type { StandardSchemaV1 } from "@standard-schema/spec"; +import { getFirstFieldError } from "./getFirstFieldError"; -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -type AnyValues = Record; +// biome-ignore lint/suspicious/noExplicitAny: a form's values are the caller's shape +type AnyValues = Record; export type AsyncValidatorFn = (context: { value: TValues; @@ -20,8 +18,7 @@ export type CreateFormOptions = { /** * Any Standard Schema-compatible schema (Zod, Valibot, Arktype, ...). - * Applied as `validators.onChange`, `validators.onBlur` and - * `validators.onSubmit`. + * Runs on change, on blur and on submit. */ schema?: StandardSchemaV1; @@ -40,189 +37,197 @@ export type CreateFormOptions = { onSubmit?: (value: TValues) => void | Promise; }; +/** What a field tracks besides its value. */ +type FieldMeta = { + isTouched: boolean; + errors: string[]; +}; + /** * The form API returned by `createForm`. * - * Consumers should use `` to wire it into the component tree. - * For advanced use, access the raw TanStack Form instance via `api._tsForm`. + * Consumers should use `` to wire it into the component tree, + * and read individual fields with `useField(name)`. */ export type FormApi = { - /** - * Raw TanStack Form instance. Intended as an escape hatch for cases not - * covered by the library's abstractions (custom field rendering, ``, etc.). - * Prefixed with `_` to signal that it's an internal detail. - * - * TanStack Form's type has 12 generic parameters — we intentionally erase - * them here to keep the public API simple. Use `form._tsForm` to access the - * full typed instance in advanced scenarios. - */ - // biome-ignore lint/suspicious/noExplicitAny: TanStack Form has 12 generic params; erased for public API simplicity - _tsForm: any; + /** Every field's current value. */ + values: () => TValues; + /** One field's current value. */ + getFieldValue: (name: string) => unknown; + /** One field's touched flag and errors. */ + getFieldMeta: (name: string) => FieldMeta; + /** Write a value and re-run the synchronous schema. */ + setFieldValue: (name: string, value: unknown) => void; + /** Mark touched and validate, which is what a blur means. */ + validateField: (name: string, cause: "change" | "blur") => void | Promise; + /** Validate everything, run async validators, then `onSubmit` if clean. */ + submit: () => Promise; + /** True while `submit()` is awaiting. */ + isSubmitting: () => boolean; + /** True when no field holds an error. */ + isValid: () => boolean; /** Phantom field: preserves TValues for type inference in consuming hooks. */ _values?: TValues; }; -// --------------------------------------------------------------------------- -// Internal validation logic -// --------------------------------------------------------------------------- - -type ValidationCause = "change" | "blur" | "submit" | "mount" | "server"; - -type ValidationLogicProps = { - // biome-ignore lint/suspicious/noExplicitAny: TanStack Form validator map shape is internal - validators?: any; - // biome-ignore lint/suspicious/noExplicitAny: TanStack Form API type is intentionally erased in wrapper - form: any; - event: { - type: ValidationCause; - async: boolean; - }; - runValidation: (props: { - validators: Array< - | { - // biome-ignore lint/suspicious/noExplicitAny: validator function types are internal - fn: any; - cause: ValidationCause; - } - | undefined - >; - // biome-ignore lint/suspicious/noExplicitAny: TanStack Form API type is intentionally erased in wrapper - form: any; - }) => void; -}; - -const createValidationLogic = (props: ValidationLogicProps) => { - if (!props.validators) { - return props.runValidation({ - validators: [], - form: props.form, - }); - } - - const isAsync = props.event.async; - - const onMountValidator = isAsync - ? undefined - : ({ fn: props.validators.onMount, cause: "mount" as const }); - - const onChangeValidator = { - fn: isAsync ? props.validators.onChangeAsync : props.validators.onChange, - cause: "change" as const, - }; - - const onBlurValidator = { - fn: isAsync ? props.validators.onBlurAsync : props.validators.onBlur, - cause: "blur" as const, - }; - - const onSubmitValidator = { - fn: isAsync ? props.validators.onSubmitAsync : props.validators.onSubmit, - cause: "submit" as const, - }; - - const onServerValidator = isAsync - ? undefined - : ({ - fn: () => undefined, - cause: "server" as const, - }); - - switch (props.event.type) { - case "mount": { - return props.runValidation({ - validators: [onMountValidator], - form: props.form, - }); - } - case "submit": { - return props.runValidation({ - validators: [ - onChangeValidator, - onBlurValidator, - onSubmitValidator, - onServerValidator, - ], - form: props.form, - }); - } - case "server": { - return props.runValidation({ - validators: [], - form: props.form, - }); - } - case "blur": { - return props.runValidation({ - validators: [onBlurValidator, onServerValidator], - form: props.form, - }); - } - case "change": { - // Re-run blur validators on each change so blur-origin errors are cleared - // immediately when the value becomes valid again. - return props.runValidation({ - validators: [onChangeValidator, onBlurValidator, onServerValidator], - form: props.form, - }); - } - default: { - throw new Error(`Unknown validation event type: ${props.event.type}`); - } +/** + * Read a Standard Schema's issues into a per-field error map. + * + * The spec puts the field on `issue.path`, which is a list of keys or of + * `{ key }` segments depending on the library. Only the first segment is used: + * this form is flat, and a nested path would key an error to a field name that + * does not exist rather than to its parent. + */ +const issuesToErrors = (issues: readonly StandardSchemaV1.Issue[]): Record => { + const errors: Record = {}; + for (const issue of issues) { + const head = issue.path?.[0]; + const name = + typeof head === "object" && head !== null && "key" in head ? String(head.key) : String(head ?? ""); + if (!name) continue; + (errors[name] ??= []).push(issue.message); } + return errors; }; -// --------------------------------------------------------------------------- -// Factory -// --------------------------------------------------------------------------- - /** - * Creates a new form instance backed by TanStack Form. + * A form, without TanStack. + * + * The engine is deliberately small because the field components already carry + * the hard parts: `Input`, `Select`, `Checkbox` and the rest own their own + * presentation, `state="invalid"` and error slot. What was missing is + * bookkeeping — values, touched, errors, and when to run the schema — and that + * is what this is. * - * ```tsx - * const form = createForm({ - * defaultValues: { email: "", password: "" }, - * schema: loginSchema, // Zod or Valibot — both work - * onSubmit: async (values) => { await login(values); }, - * }); + * `@tanstack/solid-form` used to supply it, through a `_tsForm` escape hatch + * that leaked its twelve erased generics into this library's public type. It + * also peer-pinned Solid 1, which made it one of three packages standing + * between this library and Solid 2. * - * return ( - * - * - * - * Log in - * - * ); - * ``` + * Validation runs on change and on blur, and errors are only *shown* once a + * field is touched — the display gate lives in `useField`, so a submit can + * surface every error at once by touching every field. */ export const createForm = ( options: CreateFormOptions, ): FormApi => { - const hasSchema = Boolean(options.schema); - const hasAsyncValidators = Boolean(options.asyncValidators); - - // biome-ignore lint/suspicious/noExplicitAny: TanStack Form generics are erased at our wrapper boundary - const tsForm: any = createTSForm(() => ({ - defaultValues: options.defaultValues as Record, - - validators: hasSchema || hasAsyncValidators - ? { - // biome-ignore lint/suspicious/noExplicitAny: Standard Schema bridges Zod/Valibot/etc. - onChange: options.schema as any, - // biome-ignore lint/suspicious/noExplicitAny: Standard Schema bridges Zod/Valibot/etc. - onBlur: options.schema as any, - // biome-ignore lint/suspicious/noExplicitAny: Standard Schema bridges Zod/Valibot/etc. - onSubmit: options.schema as any, - onBlurAsync: options.asyncValidators?.onBlur as any, - onSubmitAsync: options.asyncValidators?.onSubmit as any, + const [values, setValues] = createStore({ ...options.defaultValues }); + const [meta, setMeta] = createStore>({}); + const [status, setStatus] = createStore({ isSubmitting: false }); + + const metaFor = (name: string): FieldMeta => meta[name] ?? { isTouched: false, errors: [] }; + + /** Run the schema and replace every field's errors with its verdict. */ + const runSchema = (): boolean => { + const schema = options.schema; + if (!schema) return true; + const result = schema["~standard"].validate(values as TValues); + // A Standard Schema may validate asynchronously. The synchronous callers + // here (change, blur) cannot wait for one, and treating a pending result as + // "no errors" would clear real ones, so it is left to `submit()`. + if (result instanceof Promise) return true; + const errors = result.issues ? issuesToErrors(result.issues) : {}; + setMeta( + produce((draft) => { + const names = new Set([...Object.keys(draft), ...Object.keys(errors)]); + for (const name of names) { + draft[name] = { isTouched: draft[name]?.isTouched ?? false, errors: errors[name] ?? [] }; } - : undefined, + }), + ); + return !result.issues?.length; + }; + + const setFieldValue = (name: string, value: unknown): void => { + // Cast at the boundary: the store is typed by the caller's values, and a + // field name is a string the caller chose, so this is the one place the two + // cannot be related without making `name` a keyof and losing dotted paths. + (setValues as unknown as (fn: (draft: AnyValues) => void) => void)( + produce((draft: AnyValues) => { + draft[name] = value; + }), + ); + runSchema(); + }; - validationLogic: createValidationLogic, + const validateField = (name: string, cause: "change" | "blur"): void => { + if (cause === "blur") { + setMeta( + produce((draft) => { + draft[name] = { isTouched: true, errors: draft[name]?.errors ?? [] }; + }), + ); + } + runSchema(); + }; - onSubmit: async ({ value }: { value: unknown }) => { - await options.onSubmit?.(value as TValues); - }, - })); + const submit = async (): Promise => { + // Touch everything first: an untouched field hides its error, and a submit + // that silently does nothing because of an error you cannot see is the + // worst version of this. + setMeta( + produce((draft) => { + for (const name of Object.keys(values as AnyValues)) { + draft[name] = { isTouched: true, errors: draft[name]?.errors ?? [] }; + } + }), + ); + + let ok = runSchema(); + + // An async schema is resolved here, where there is somewhere to await it. + const schema = options.schema; + if (schema) { + const result = schema["~standard"].validate(values as TValues); + if (result instanceof Promise) { + const resolved = await result; + const errors = resolved.issues ? issuesToErrors(resolved.issues) : {}; + setMeta( + produce((draft) => { + for (const name of new Set([...Object.keys(draft), ...Object.keys(errors)])) { + draft[name] = { isTouched: true, errors: errors[name] ?? [] }; + } + }), + ); + ok = !resolved.issues?.length; + } + } - return { _tsForm: tsForm }; + if (!ok) return; + + setStatus("isSubmitting", true); + try { + const async = options.asyncValidators?.onSubmit; + if (async) { + const serverErrors = await async({ value: values as TValues }); + if (serverErrors && Object.keys(serverErrors).length > 0) { + setMeta( + produce((draft) => { + for (const [name, message] of Object.entries(serverErrors)) { + if (!message) continue; + draft[name] = { isTouched: true, errors: [String(message)] }; + } + }), + ); + return; + } + } + await options.onSubmit?.(values as TValues); + } finally { + setStatus("isSubmitting", false); + } + }; + + return { + values: () => values as TValues, + getFieldValue: (name) => (values as AnyValues)[name], + getFieldMeta: metaFor, + setFieldValue, + validateField, + submit, + isSubmitting: () => status.isSubmitting, + isValid: () => Object.values(meta).every((m) => (m?.errors.length ?? 0) === 0), + }; }; + +export { getFirstFieldError }; diff --git a/src/hooks/form/useFieldNew.ts b/src/hooks/form/useFieldNew.ts index 27b613c1..1fde17fe 100644 --- a/src/hooks/form/useFieldNew.ts +++ b/src/hooks/form/useFieldNew.ts @@ -29,8 +29,8 @@ export type UseFieldResult = { * Reads live field state for `name` from the nearest `
` context. * * Must be called **inside** a `` descendant component. - * For fields that need fine-grained rendering, prefer using `form._tsForm.Field` - * render prop directly. + * Field components own their own presentation; this supplies the state they + * need to show it. * * ```tsx * // Inside a child of @@ -46,29 +46,22 @@ export type UseFieldResult = { */ export const useField = (name: string): UseFieldResult => { const form = useFormContext(); - const tsForm = form._tsForm; - const value = createMemo(() => tsForm.getFieldValue(name as never)); + const value = createMemo(() => form.getFieldValue(name)); + const meta = createMemo(() => form.getFieldMeta(name)); + const touched = createMemo(() => meta().isTouched); - const meta = createMemo(() => tsForm.getFieldMeta(name as never)); - - const touched = createMemo(() => Boolean(meta()?.isTouched)); - - const error = createMemo((): string | undefined => { - const m = meta(); - if (!m?.isTouched) return undefined; - return getFirstFieldError((m.errors ?? []) as unknown[]); - }); + // Gated on `isTouched` so a form does not open covered in errors for fields + // nobody has reached yet. `submit()` touches everything, which is what makes + // a failed submit show all of them at once. + const error = createMemo((): string | undefined => + meta().isTouched ? getFirstFieldError(meta().errors) : undefined, + ); const invalid = createMemo(() => Boolean(error())); - const handleChange = (value: unknown) => { - tsForm.setFieldValue(name as never, value as never); - }; - - const handleBlur = () => { - tsForm.validateField(name as never, "blur"); - }; + const handleChange = (next: unknown) => form.setFieldValue(name, next); + const handleBlur = () => form.validateField(name, "blur"); return { value, error, touched, invalid, handleChange, handleBlur }; }; diff --git a/src/hooks/table/helpers.ts b/src/hooks/table/helpers.ts deleted file mode 100644 index 74ef3b36..00000000 --- a/src/hooks/table/helpers.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Accessor } from "solid-js"; -import type { SortingState, Updater } from "@tanstack/solid-table"; -import type { HookSortDescriptor } from "./useTableSorting"; - -export const resolveUpdater = (previous: T, updater: Updater): T => { - if (typeof updater === "function") { - return (updater as (prev: T) => T)(previous); - } - return updater; -}; - -export const asAccessor = (value: Accessor | T): Accessor => { - if (typeof value === "function") { - return value as Accessor; - } - return () => value; -}; - -export const toSortDescriptor = ( - sorting: SortingState, -): HookSortDescriptor | undefined => { - const activeSort = sorting[0]; - if (!activeSort) return undefined; - - return { - column: activeSort.id, - direction: activeSort.desc ? "descending" : "ascending", - }; -}; - -export const toSortingState = ( - descriptor: HookSortDescriptor | undefined, -): SortingState => { - if (!descriptor) return []; - - return [ - { - id: descriptor.column, - desc: descriptor.direction === "descending", - }, - ]; -}; diff --git a/src/hooks/table/index.ts b/src/hooks/table/index.ts index 61e6c5b5..07e48e26 100644 --- a/src/hooks/table/index.ts +++ b/src/hooks/table/index.ts @@ -1,35 +1,14 @@ -export { - useTableSorting, - type UseTableSortingOptions, - type UseTableSortingResult, - type HookSortDirection, - type HookSortDescriptor, -} from "./useTableSorting"; -export { - useTablePagination, - type UseTablePaginationOptions, - type UseTablePaginationResult, -} from "./useTablePagination"; -export { - useTableFiltering, - type UseTableFilteringOptions, - type UseTableFilteringResult, -} from "./useTableFiltering"; -export { - useTableSelection, - type UseTableSelectionOptions, - type UseTableSelectionResult, - type TableSelectionState, -} from "./useTableSelection"; -export { - useTableExpansion, - type UseTableExpansionOptions, - type UseTableExpansionResult, -} from "./useTableExpansion"; -export { - useTableModel, - type UseTableModelOptions, -} from "./useTableModel"; -export { toSortDescriptor, toSortingState } from "./helpers"; +/* + * What is left of `hooks/table` after TanStack. + * + * The sorting, filtering, pagination, selection, expansion and model hooks were + * a thin public shim over `@tanstack/solid-table`. Nothing inside this library + * used one, and `createDataGrid` answers the same questions natively — its rows + * derive `filteredRows -> sortedRows -> pageRows`, with selection alongside — so + * the shim was a second table engine kept alive only by its own exports. + * + * `useAnchoredOverlayPosition` stays because it never had anything to do with + * TanStack: it positions an overlay against a cell, which a grid still needs. + */ export { useAnchoredOverlayPosition } from "./useAnchoredOverlayPosition"; export type { UseAnchoredOverlayPositionOptions } from "./useAnchoredOverlayPosition"; diff --git a/src/hooks/table/useTableExpansion.ts b/src/hooks/table/useTableExpansion.ts deleted file mode 100644 index 71d055aa..00000000 --- a/src/hooks/table/useTableExpansion.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { createSignal, type Accessor } from "solid-js"; -import type { ExpandedState, OnChangeFn } from "@tanstack/solid-table"; -import { resolveUpdater } from "./helpers"; - -export interface UseTableExpansionOptions { - expanded?: Accessor; - setExpanded?: OnChangeFn; - initialExpanded?: ExpandedState; -} - -export interface UseTableExpansionResult { - expanded: Accessor; - setExpanded: OnChangeFn; - collapseAll: () => void; -} - -export const useTableExpansion = ( - options: UseTableExpansionOptions = {}, -): UseTableExpansionResult => { - const [internalExpanded, setInternalExpanded] = createSignal( - options.initialExpanded ?? {}, - ); - - const expanded = () => options.expanded?.() ?? internalExpanded(); - - const setExpanded: OnChangeFn = (updater) => { - if (options.setExpanded) { - options.setExpanded(updater); - return; - } - setInternalExpanded((prev) => resolveUpdater(prev, updater)); - }; - - const collapseAll = () => setExpanded({}); - - return { - expanded, - setExpanded, - collapseAll, - }; -}; diff --git a/src/hooks/table/useTableFiltering.ts b/src/hooks/table/useTableFiltering.ts deleted file mode 100644 index b9c66888..00000000 --- a/src/hooks/table/useTableFiltering.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { createMemo, createSignal, type Accessor, type JSX } from "solid-js"; -import type { - ColumnFiltersState, - OnChangeFn, -} from "@tanstack/solid-table"; -import { resolveUpdater } from "./helpers"; - -export interface UseTableFilteringOptions { - columnFilters?: Accessor; - setColumnFilters?: OnChangeFn; - initialColumnFilters?: ColumnFiltersState; - globalFilter?: Accessor; - setGlobalFilter?: OnChangeFn; - initialGlobalFilter?: string; -} - -export interface UseTableFilteringResult { - columnFilters: Accessor; - setColumnFilters: OnChangeFn; - globalFilter: Accessor; - setGlobalFilter: OnChangeFn; - getColumnFilterValue: (columnId: string) => string; - setColumnFilterValue: (columnId: string, value: string) => void; - getColumnFilterProps: ( - columnId: string, - options?: { - afterChange?: (value: string) => void; - }, - ) => { - readonly value: string; - onInput: JSX.EventHandler; - onChange: JSX.EventHandler< - HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, - Event - >; - }; - openFilterFor: Accessor; - closeFilter: () => void; - toggleFilter: (columnId: string) => void; - anyFilterActive: Accessor; -} - -export const useTableFiltering = ( - options: UseTableFilteringOptions = {}, -): UseTableFilteringResult => { - const [internalColumnFilters, setInternalColumnFilters] = - createSignal(options.initialColumnFilters ?? []); - const [internalGlobalFilter, setInternalGlobalFilter] = createSignal( - options.initialGlobalFilter ?? "", - ); - const [openFilterFor, setOpenFilterFor] = createSignal(null); - - const columnFilters = () => - options.columnFilters?.() ?? internalColumnFilters(); - const globalFilter = () => options.globalFilter?.() ?? internalGlobalFilter(); - - const setColumnFilters: OnChangeFn = (updater) => { - if (options.setColumnFilters) { - options.setColumnFilters(updater); - return; - } - setInternalColumnFilters((prev) => resolveUpdater(prev, updater)); - }; - - const setGlobalFilter: OnChangeFn = (updater) => { - if (options.setGlobalFilter) { - options.setGlobalFilter(updater); - return; - } - setInternalGlobalFilter((prev) => resolveUpdater(prev, updater)); - }; - - const closeFilter = () => setOpenFilterFor(null); - const toggleFilter = (columnId: string) => { - setOpenFilterFor((current) => (current === columnId ? null : columnId)); - }; - - const getColumnFilterValue = (columnId: string) => { - const filter = columnFilters().find((entry) => entry.id === columnId); - return typeof filter?.value === "string" ? filter.value : ""; - }; - - const setColumnFilterValue = (columnId: string, value: string) => { - setColumnFilters((previous) => { - const next = previous.filter((entry) => entry.id !== columnId); - if (value.trim().length > 0) { - next.push({ id: columnId, value }); - } - return next; - }); - }; - - const getColumnFilterProps = ( - columnId: string, - bindingOptions?: { - afterChange?: (value: string) => void; - }, - ) => { - const applyValue = (value: string) => { - setColumnFilterValue(columnId, value); - bindingOptions?.afterChange?.(value); - }; - - return { - get value() { - return getColumnFilterValue(columnId); - }, - onInput: (event: InputEvent & { - currentTarget: HTMLInputElement | HTMLTextAreaElement; - target: Element; - }) => { - applyValue(event.currentTarget.value); - }, - onChange: (event: Event & { - currentTarget: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; - target: Element; - }) => { - applyValue(event.currentTarget.value); - }, - }; - }; - - const anyFilterActive = createMemo(() => columnFilters().length > 0); - - return { - columnFilters, - setColumnFilters, - globalFilter, - setGlobalFilter, - getColumnFilterValue, - setColumnFilterValue, - getColumnFilterProps, - openFilterFor, - closeFilter, - toggleFilter, - anyFilterActive, - }; -}; diff --git a/src/hooks/table/useTableModel.ts b/src/hooks/table/useTableModel.ts deleted file mode 100644 index 6e67742c..00000000 --- a/src/hooks/table/useTableModel.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Accessor } from "solid-js"; -import { - createSolidTable, - getCoreRowModel, - getExpandedRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - type ColumnDef, - type ColumnFiltersState, - type ExpandedState, - type OnChangeFn, - type PaginationState, - type Row, - type SortingState, - type Table, -} from "@tanstack/solid-table"; -import { asAccessor } from "./helpers"; -import type { TableSelectionState } from "./useTableSelection"; - -export interface UseTableModelOptions { - data: Accessor | TData[]; - columns: Accessor[]> | ColumnDef[]; - getRowId?: (originalRow: TData, index: number, parent?: Row) => string; - sorting?: Accessor; - setSorting?: OnChangeFn; - columnFilters?: Accessor; - setColumnFilters?: OnChangeFn; - pagination?: Accessor; - setPagination?: OnChangeFn; - globalFilter?: Accessor; - setGlobalFilter?: OnChangeFn; - rowSelection?: Accessor; - setRowSelection?: OnChangeFn; - expanded?: Accessor; - setExpanded?: OnChangeFn; - enableSorting?: boolean; - enableFilters?: boolean; - enablePagination?: boolean; - enableRowSelection?: boolean; - enableExpanding?: boolean; - getRowCanExpand?: (row: Row) => boolean; -} - -export const useTableModel = ( - options: UseTableModelOptions, -): Table => { - const dataAccessor = asAccessor(options.data); - const columnsAccessor = asAccessor(options.columns); - - return createSolidTable({ - get data() { - return dataAccessor(); - }, - get columns() { - return columnsAccessor(); - }, - getRowId: options.getRowId, - state: { - get sorting() { - return options.sorting?.() ?? []; - }, - get columnFilters() { - return options.columnFilters?.() ?? []; - }, - get pagination() { - return options.pagination?.() ?? { pageIndex: 0, pageSize: 10 }; - }, - get globalFilter() { - return options.globalFilter?.() ?? ""; - }, - get rowSelection() { - return options.rowSelection?.() ?? {}; - }, - get expanded() { - return options.expanded?.() ?? {}; - }, - }, - onSortingChange: options.setSorting, - onColumnFiltersChange: options.setColumnFilters, - onPaginationChange: options.setPagination, - onGlobalFilterChange: options.setGlobalFilter, - onRowSelectionChange: options.setRowSelection, - onExpandedChange: options.setExpanded, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getExpandedRowModel: getExpandedRowModel(), - enableSorting: options.enableSorting, - enableFilters: options.enableFilters, - manualPagination: - options.enablePagination === undefined ? false : !options.enablePagination, - enableRowSelection: options.enableRowSelection, - enableExpanding: options.enableExpanding, - getRowCanExpand: options.getRowCanExpand, - }); -}; diff --git a/src/hooks/table/useTablePagination.ts b/src/hooks/table/useTablePagination.ts deleted file mode 100644 index 30e1d55b..00000000 --- a/src/hooks/table/useTablePagination.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { createSignal, type Accessor } from "solid-js"; -import type { OnChangeFn, PaginationState } from "@tanstack/solid-table"; -import { resolveUpdater } from "./helpers"; - -const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100] as const; - -export interface UseTablePaginationOptions { - pagination?: Accessor; - setPagination?: OnChangeFn; - initialPagination?: PaginationState; - pageSizeOptions?: readonly number[]; -} - -export interface UseTablePaginationResult { - pagination: Accessor; - setPagination: OnChangeFn; - pageSizeOptions: Accessor; - setPageIndex: (index: number) => void; - setPageSize: (size: number) => void; - firstPage: () => void; - previousPage: () => void; - nextPage: (maxPageIndex: number) => void; - lastPage: (maxPageIndex: number) => void; -} - -export const useTablePagination = ( - options: UseTablePaginationOptions = {}, -): UseTablePaginationResult => { - const [internalPagination, setInternalPagination] = createSignal( - options.initialPagination ?? { pageIndex: 0, pageSize: 10 }, - ); - - const pagination = () => options.pagination?.() ?? internalPagination(); - const pageSizeOptions = () => options.pageSizeOptions ?? DEFAULT_PAGE_SIZE_OPTIONS; - - const setPagination: OnChangeFn = (updater) => { - if (options.setPagination) { - options.setPagination(updater); - return; - } - setInternalPagination((prev) => resolveUpdater(prev, updater)); - }; - - const setPageIndex = (index: number) => { - setPagination((prev) => ({ ...prev, pageIndex: Math.max(0, index) })); - }; - - const setPageSize = (size: number) => { - setPagination((prev) => ({ ...prev, pageSize: size })); - }; - - const firstPage = () => setPageIndex(0); - const previousPage = () => setPageIndex(pagination().pageIndex - 1); - const nextPage = (maxPageIndex: number) => - setPageIndex(Math.min(maxPageIndex, pagination().pageIndex + 1)); - const lastPage = (maxPageIndex: number) => setPageIndex(maxPageIndex); - - return { - pagination, - setPagination, - pageSizeOptions, - setPageIndex, - setPageSize, - firstPage, - previousPage, - nextPage, - lastPage, - }; -}; diff --git a/src/hooks/table/useTableSelection.ts b/src/hooks/table/useTableSelection.ts deleted file mode 100644 index 175a38e5..00000000 --- a/src/hooks/table/useTableSelection.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createSignal, type Accessor } from "solid-js"; -import type { OnChangeFn } from "@tanstack/solid-table"; -import { resolveUpdater } from "./helpers"; - -export type TableSelectionState = Record; - -export interface UseTableSelectionOptions { - rowSelection?: Accessor; - setRowSelection?: OnChangeFn; - initialRowSelection?: TableSelectionState; -} - -export interface UseTableSelectionResult { - rowSelection: Accessor; - setRowSelection: OnChangeFn; - clearSelection: () => void; -} - -export const useTableSelection = ( - options: UseTableSelectionOptions = {}, -): UseTableSelectionResult => { - const [internalRowSelection, setInternalRowSelection] = - createSignal(options.initialRowSelection ?? {}); - - const rowSelection = () => options.rowSelection?.() ?? internalRowSelection(); - - const setRowSelection: OnChangeFn = (updater) => { - if (options.setRowSelection) { - options.setRowSelection(updater); - return; - } - setInternalRowSelection((prev) => resolveUpdater(prev, updater)); - }; - - const clearSelection = () => setRowSelection({}); - - return { - rowSelection, - setRowSelection, - clearSelection, - }; -}; diff --git a/src/hooks/table/useTableSorting.ts b/src/hooks/table/useTableSorting.ts deleted file mode 100644 index 86a27eb3..00000000 --- a/src/hooks/table/useTableSorting.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createMemo, createSignal, type Accessor } from "solid-js"; -import type { OnChangeFn, SortingState } from "@tanstack/solid-table"; -import { resolveUpdater, toSortDescriptor, toSortingState } from "./helpers"; - -export type HookSortDirection = "ascending" | "descending"; - -export type HookSortDescriptor = { - column: string; - direction: HookSortDirection; -}; - -export interface UseTableSortingOptions { - sorting?: Accessor; - setSorting?: OnChangeFn; - initialSorting?: SortingState; -} - -export interface UseTableSortingResult { - sorting: Accessor; - setSorting: OnChangeFn; - sortDescriptor: Accessor; - setSortDescriptor: (descriptor: HookSortDescriptor) => void; -} - -export const useTableSorting = ( - options: UseTableSortingOptions = {}, -): UseTableSortingResult => { - const [internalSorting, setInternalSorting] = createSignal( - options.initialSorting ?? [], - ); - - const sorting = () => options.sorting?.() ?? internalSorting(); - - const setSorting: OnChangeFn = (updater) => { - if (options.setSorting) { - options.setSorting(updater); - return; - } - setInternalSorting((prev) => resolveUpdater(prev, updater)); - }; - - const sortDescriptor = createMemo(() => { - return toSortDescriptor(sorting()); - }); - - const setSortDescriptor = (descriptor: HookSortDescriptor) => { - setSorting(toSortingState(descriptor)); - }; - - return { - sorting, - setSorting, - sortDescriptor, - setSortDescriptor, - }; -}; diff --git a/src/index.ts b/src/index.ts index 974451a6..1da2bc4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -152,7 +152,7 @@ export type { FieldsetActionsProps, } from "./components/fieldset"; // --------------------------------------------------------------------------- -// Form components and hooks (TanStack-based API) +// Form components and hooks // --------------------------------------------------------------------------- export { default as Form, @@ -389,34 +389,8 @@ export type { InlineConfirmProps as TableInlineConfirmProps, InlineConfirmVariant as TableInlineConfirmVariant, } from "./components/table"; -export { - useTableModel, - useTableSorting, - useTablePagination, - useTableFiltering, - useTableSelection, - useTableExpansion, - toSortDescriptor, - toSortingState, - useAnchoredOverlayPosition, -} from "./hooks/table"; -export type { - UseTableModelOptions, - UseTableSortingOptions, - UseTableSortingResult, - HookSortDirection, - HookSortDescriptor, - UseTablePaginationOptions, - UseTablePaginationResult, - UseTableFilteringOptions, - UseTableFilteringResult, - UseTableSelectionOptions, - UseTableSelectionResult, - TableSelectionState, - UseTableExpansionOptions, - UseTableExpansionResult, - UseAnchoredOverlayPositionOptions, -} from "./hooks/table"; +export { useAnchoredOverlayPosition } from "./hooks/table"; +export type { UseAnchoredOverlayPositionOptions } from "./hooks/table"; export { default as Tabs } from "./components/tabs"; export type { @@ -513,12 +487,6 @@ export type { // Motion export * from "./motion"; -export { useVirtualRows } from "./primitives/virtualizer"; -export type { - UseVirtualRowsOptions, - UseVirtualRowsResult, - VirtualRowsRange, -} from "./primitives/virtualizer"; export { useStreamingBuffer, useStreamingSubscription, diff --git a/src/primitives/virtualizer/index.ts b/src/primitives/virtualizer/index.ts deleted file mode 100644 index 385935ce..00000000 --- a/src/primitives/virtualizer/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { useVirtualRows } from "./useVirtualRows"; -export type { - UseVirtualRowsOptions, - UseVirtualRowsResult, - VirtualRowsRange, -} from "./useVirtualRows"; diff --git a/src/primitives/virtualizer/useVirtualRows.ts b/src/primitives/virtualizer/useVirtualRows.ts deleted file mode 100644 index 9579e1c3..00000000 --- a/src/primitives/virtualizer/useVirtualRows.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { createEffect, createMemo, type Accessor } from "solid-js"; -import { - createVirtualizer, - type PartialKeys, - type VirtualItem, - type Virtualizer, - type VirtualizerOptions, -} from "@tanstack/solid-virtual"; - -type OmittedVirtualizerOptions = - | "observeElementRect" - | "observeElementOffset" - | "scrollToFn"; - -export type UseVirtualRowsOptions< - TScrollElement extends Element, - TItemElement extends Element, -> = PartialKeys< - VirtualizerOptions, - OmittedVirtualizerOptions ->; - -export type VirtualRowsRange = { - startIndex: number; - endIndex: number; -}; - -export type UseVirtualRowsResult< - TScrollElement extends Element, - TItemElement extends Element, -> = { - virtualizer: Virtualizer; - virtualItems: Accessor; - totalSize: Accessor; - range: Accessor; - startIndex: Accessor; - endIndex: Accessor; -}; - -export const useVirtualRows = < - TScrollElement extends Element, - TItemElement extends Element, ->( - options: UseVirtualRowsOptions, -): UseVirtualRowsResult => { - const virtualizer = createVirtualizer(options); - - createEffect(() => { - const scrollElement = options.getScrollElement(); - if (!scrollElement) return; - virtualizer.measure(); - }); - - const virtualItems = () => virtualizer.getVirtualItems(); - const totalSize = () => virtualizer.getTotalSize(); - - const range = createMemo(() => { - const items = virtualItems(); - if (items.length === 0) { - return undefined; - } - - return { - startIndex: items[0].index, - endIndex: items[items.length - 1].index, - }; - }); - - const startIndex = createMemo(() => range()?.startIndex ?? -1); - const endIndex = createMemo(() => range()?.endIndex ?? -1); - - return { - virtualizer, - virtualItems, - totalSize, - range, - startIndex, - endIndex, - }; -}; diff --git a/tests/hooks/form/createForm.test.ts b/tests/hooks/form/createForm.test.ts index c06ae993..ca3b598a 100644 --- a/tests/hooks/form/createForm.test.ts +++ b/tests/hooks/form/createForm.test.ts @@ -50,19 +50,19 @@ const createPasswordForm = (onSubmit?: (value: PasswordValues) => void | Promise describe("createForm", () => { it("clears blur-origin errors after valid change", async () => { const { form, dispose } = createPasswordForm(); - const tsForm = form._tsForm; try { - tsForm.setFieldValue("password", "abc"); - await tsForm.validateField("password", "blur"); + form.setFieldValue("password", "abc"); + await form.validateField("password", "blur"); - expect(tsForm.state.canSubmit).toBeFalse(); - expect(tsForm.getFieldMeta("password")?.errorMap?.onBlur).toBeDefined(); + expect(form.isValid()).toBeFalse(); + expect(form.getFieldMeta("password").errors.length).toBeGreaterThan(0); + expect(form.getFieldMeta("password").isTouched).toBeTrue(); - tsForm.setFieldValue("password", "abcd"); + form.setFieldValue("password", "abcd"); - expect(tsForm.state.canSubmit).toBeTrue(); - expect(tsForm.getFieldMeta("password")?.errorMap?.onBlur).toBeUndefined(); + expect(form.isValid()).toBeTrue(); + expect(form.getFieldMeta("password").errors).toEqual([]); } finally { dispose(); } @@ -71,24 +71,42 @@ describe("createForm", () => { it("allows submit after correcting a blur-invalid value", async () => { const onSubmit = mock(async (_value: PasswordValues) => {}); const { form, dispose } = createPasswordForm(onSubmit); - const tsForm = form._tsForm; try { - tsForm.setFieldValue("password", "abc"); - await tsForm.validateField("password", "blur"); - await tsForm.handleSubmit(); + form.setFieldValue("password", "abc"); + await form.validateField("password", "blur"); + await form.submit(); - expect(tsForm.state.canSubmit).toBeFalse(); + expect(form.isValid()).toBeFalse(); expect(onSubmit).toHaveBeenCalledTimes(0); - tsForm.setFieldValue("password", "abcd"); - await tsForm.handleSubmit(); + form.setFieldValue("password", "abcd"); + await form.submit(); - expect(tsForm.state.canSubmit).toBeTrue(); + expect(form.isValid()).toBeTrue(); expect(onSubmit).toHaveBeenCalledTimes(1); expect(onSubmit.mock.calls[0]?.[0]).toEqual({ password: "abcd" }); } finally { dispose(); } }); + + it("shows every error on a failed submit, not only the touched ones", async () => { + const onSubmit = mock(async (_value: PasswordValues) => {}); + const { form, dispose } = createPasswordForm(onSubmit); + + try { + // Nothing blurred, so nothing is touched and nothing displays yet. + expect(form.getFieldMeta("password").isTouched).toBeFalse(); + + form.setFieldValue("password", "abc"); + await form.submit(); + + // A submit that refuses has to say why, so it touches everything. + expect(form.getFieldMeta("password").isTouched).toBeTrue(); + expect(onSubmit).toHaveBeenCalledTimes(0); + } finally { + dispose(); + } + }); });