diff --git a/.storybook/manager.ts b/.storybook/manager.ts index 5ae11ce7..338a7927 100644 --- a/.storybook/manager.ts +++ b/.storybook/manager.ts @@ -5,15 +5,15 @@ addons.setConfig({ theme: create({ base: "light", brandTitle: - '

frappe-ui-react

(v1.0.0-beta.3)', // update version as per package.json + '

frappe-ui-react

(v1.0.2)', // update version as per package.json brandUrl: undefined, // disables link on the title brandImage: undefined, - // Sidebar/Toolbar active state color - barSelectedColor: 'rgb(153, 153, 153)', - + // Sidebar/Toolbar active state color + barSelectedColor: 'rgb(153, 153, 153)', + // Primary accent color for buttons, links, focus states - colorPrimary: 'rgb(153, 153, 153)', + colorPrimary: 'rgb(153, 153, 153)', colorSecondary: 'rgb(153, 153, 153)', fontBase: '"Inter", sans-serif', }), diff --git a/package-lock.json b/package-lock.json index 3ef9a031..8d3cb015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "frappe-ui-react", - "version": "1.0.0-beta.3", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frappe-ui-react", - "version": "1.0.0-beta.3", + "version": "1.0.1", "workspaces": [ "packages/*" ], @@ -51,7 +51,7 @@ "playwright": "^1.54.1", "prettier": "^2.8.8", "rimraf": "^5.0.5", - "storybook": "^9.1.3", + "storybook": "^9.1.17", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", "vite": "^7.1.11", @@ -132,6 +132,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1939,6 +1940,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1962,6 +1964,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -5298,6 +5301,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5577,6 +5581,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5587,6 +5592,7 @@ "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5650,6 +5656,7 @@ "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.0", @@ -5690,6 +5697,7 @@ "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -5920,6 +5928,34 @@ "dev": true, "license": "ISC" }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@unrs/resolver-binding-darwin-arm64": { "version": "1.11.1", "cpu": [ @@ -5932,6 +5968,233 @@ "darwin" ] }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -5959,6 +6222,7 @@ "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", @@ -6096,6 +6360,7 @@ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", @@ -6154,6 +6419,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6686,6 +6952,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -7810,6 +8077,7 @@ "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7851,6 +8119,7 @@ "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.3.4" }, @@ -7887,6 +8156,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9654,6 +9924,7 @@ "version": "30.2.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10583,6 +10854,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -12421,6 +12693,7 @@ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -12539,6 +12812,7 @@ "node_modules/quill": { "version": "2.0.3", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "eventemitter3": "^5.0.1", "lodash-es": "^4.17.21", @@ -12552,6 +12826,7 @@ "node_modules/quill-delta": { "version": "5.1.0", "license": "MIT", + "peer": true, "dependencies": { "fast-diff": "^1.3.0", "lodash.clonedeep": "^4.5.0", @@ -12630,6 +12905,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12670,6 +12946,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13097,6 +13374,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -13628,9 +13906,12 @@ } }, "node_modules/storybook": { - "version": "9.1.10", + "version": "9.1.17", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.17.tgz", + "integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -14438,6 +14719,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14724,6 +15006,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -14865,6 +15148,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -15410,7 +15694,7 @@ }, "packages/frappe-ui-react": { "name": "@rtcamp/frappe-ui-react", - "version": "1.0.0-beta.3", + "version": "1.0.1", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.27.13", diff --git a/package.json b/package.json index e7d54d17..a1c1af2e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frappe-ui-react", "private": true, - "version": "1.0.0-beta.3", + "version": "1.0.2", "type": "module", "workspaces": [ "packages/*" @@ -64,7 +64,7 @@ "playwright": "^1.54.1", "prettier": "^2.8.8", "rimraf": "^5.0.5", - "storybook": "^9.1.3", + "storybook": "^9.1.17", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", "vite": "^7.1.11", diff --git a/packages/frappe-ui-react/README.md b/packages/frappe-ui-react/README.md index 6082ddb2..fdc5a4de 100644 --- a/packages/frappe-ui-react/README.md +++ b/packages/frappe-ui-react/README.md @@ -1,49 +1,93 @@ -# Frappe UI React +Frappe UI React is a modern component library designed for building frontend applications in React, specifically tailored for the [Frappe Framework](https://frappe.io/framework). It is inspired by the original [frappe-ui](https://github.com/frappe/frappe-ui), which is created in Vue, offering a similar components and aesthetic in React. However, we are not limited to a one-to-one port and also provide a growing collection of custom-built components to give developers more power and flexibility. -Frappe UI React is a component library designed for rapid UI development using React 19 and Tailwind 4. It is inspired by the original [frappe-ui](https://github.com/frappe/frappe-ui), offering a similar aesthetic for a consistent user experience. However, we are not limited to a one-to-one port and also provide a growing collection of custom-built components to give developers more power and flexibility. +## Prerequisites -## Under the Hood +- Node.js v20 +- TailwindCSS v4 -- [TailwindCSS](https://github.com/tailwindlabs/tailwindcss): Utility first CSS Framework to build design system based UI. -- [Headless UI](https://github.com/tailwindlabs/headlessui): Unstyled and accessible UI components. -- [Radix UI](https://github.com/radix-ui/themes): Unstyled and accessible UI components. -- [TipTap](https://github.com/ueberdosis/tiptap): ProseMirror based rich-text editor with a Vue API. -- [dayjs](https://github.com/iamkun/dayjs): Minimal javascript library for working with dates. +## Usage -## Links +You can set up `frappe-ui-react` in your existing Frappe app with the following simple steps. You can also spin up a new project instantly using the [frappe-ui-react-starter](https://github.com/rtCamp/frappe-ui-react-starter) template. -- [Documentation](https://frappeui.com) -- [Community](https://github.com/rtCamp/frappe-ui-react/discussions) +### Step 1: Installation -## Prerequisites +Install the package using npm. -- Node.js v20 -- TailwindCSS v4 +```bash +npm install @rtcamp/frappe-ui-react +``` -## Usage +### Step 2: Configuration + +Import the theme CSS directly into your project (e.g., in `index.css`) and provide the source of the frappe-ui-react package so that it picks the styles automatically. -```sh -npm install @frappe-ui-react/frappe-ui-react -# or -yarn add @frappe-ui-react/frappe-ui-react +```css +@import '@rtcamp/frappe-ui-react/theme'; +@source "../../node_modules/@rtcamp/frappe-ui-react/dist"; ``` -Now, import the required components in your React app: +[Tailwind @source directive](https://tailwindcss.com/docs/functions-and-directives#source-directive) explicitly specifies source files that aren't picked up by Tailwind's automatic content detection. + +**(Tailwind v3 usage):** + +If you are using a Tailwind v3 configuration, you should take two steps: + +- Import the `theme-v3` CSS into your index.css file, and ensure that `index.css` is then imported in your `index.tsx`. + +- Import the Tailwind configuration from frappe-ui-react and either use it as a preset or extend your existing configuration. + +```css +/* index.css */ +@import '@rtcamp/frappe-ui-react/theme-v3'; +``` ```js -import { Button } from "@frappe-ui-react/frappe-ui-react"; +// tailwind.config.js in your project +module.exports = { + presets: [ + require('@rtcamp/frappe-ui-react/tailwind/preset') + ], + content: [ + path.resolve(__dirname, "../../node_modules/@rtcamp/frappe-ui-react/dist") + ] + // Additional configuration... +} +``` + +### Step 3: Import and Use Components + +You can now import components and use them in your project. + +```jsx +import './index.css'; +import { Button } from "@rtcamp/frappe-ui-react"; function App() { return (
); } export default App; ``` + +## Under the Hood + +This library is built on top of several excellent open-source projects: + +- **[TailwindCSS](https://github.com/tailwindlabs/tailwindcss)**: Utility-first CSS framework for building design system-based UIs. +- **[Headless UI](https://github.com/tailwindlabs/headlessui)**: Unstyled and accessible UI components. +- **[Radix UI](https://github.com/radix-ui/themes)**: Low-level, unstyled, and accessible UI primitives. +- **[React Quill](https://github.com/zenoamaro/react-quill)**: Rich text editor component for React. +- **[dayjs](https://github.com/iamkun/dayjs)**: Lightweight JavaScript library for working with dates. + +## Inspiration & Credits + +This project, **Frappe UI React**, is heavily inspired by the original **[Frappe UI](https://github.com/frappe/frappe-ui)** project. Frappe UI is a fantastic Vue.js component library, and our goal with Frappe UI React is to bring a similar aesthetic and component experience to the React ecosystem. \ No newline at end of file diff --git a/packages/frappe-ui-react/package.json b/packages/frappe-ui-react/package.json index 857ab1ce..fd279ba5 100644 --- a/packages/frappe-ui-react/package.json +++ b/packages/frappe-ui-react/package.json @@ -1,6 +1,6 @@ { "name": "@rtcamp/frappe-ui-react", - "version": "1.0.0-beta.3", + "version": "1.0.2", "main": "dist/index.js", "module": "dist/index.js", "types": "dist-types/index.d.ts", diff --git a/packages/frappe-ui-react/src/components/autoComplete/autoComplete.tsx b/packages/frappe-ui-react/src/components/autoComplete/autoComplete.tsx index 3f889719..6416a890 100644 --- a/packages/frappe-ui-react/src/components/autoComplete/autoComplete.tsx +++ b/packages/frappe-ui-react/src/components/autoComplete/autoComplete.tsx @@ -272,9 +272,9 @@ const Autocomplete: React.FC = ({ const clearAll = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - if(multiple){ + if (multiple) { handleComboboxChange([]); - }else{ + } else { setQuery(""); } }, @@ -295,213 +295,223 @@ const Autocomplete: React.FC = ({ ); return ( - - {({ open: isComboboxOpen }) => ( - ( -
- {label && ( - - )} - -
- )} - body={({ isOpen: isPopoverOpen }) => - isPopoverOpen && ( -
- {!hideSearch && ( -
-
-
- query} - onChange={( - event: React.ChangeEvent - ) => { - cancelRef.current?.removeAttribute("inert"); - cancelRef.current?.removeAttribute("aria-hidden"); - setQuery(event.target.value); - }} - autoComplete="off" - onBlur={() => { - cancelRef.current?.removeAttribute("inert"); - cancelRef.current?.removeAttribute("aria-hidden"); - }} - placeholder="Search" - /> -
{ - cancelRef.current?.removeAttribute("inert"); - cancelRef.current?.removeAttribute("aria-hidden"); - }} - > - {loading ? ( - + )} + +
+ )} + body={({ isOpen: isPopoverOpen }) => + isPopoverOpen && ( +
+ {!hideSearch && ( +
+
+
+ query} + onChange={( + event: React.ChangeEvent + ) => { + cancelRef.current?.removeAttribute("inert"); + cancelRef.current?.removeAttribute("aria-hidden"); + setQuery(event.target.value); + }} + autoComplete="off" + onBlur={() => { + cancelRef.current?.removeAttribute("inert"); + cancelRef.current?.removeAttribute("aria-hidden"); + }} + placeholder="Search" /> - ) : ( - - )} + {loading ? ( + + ) : ( + + )} +
+
-
-
- )} + )} - - {groups.length === 0 ? ( -
  • - No results found -
  • - ) : ( - groups.map((group) => ( -
    - {group.group && !group.hideLabel && ( -
    - {group.group} -
    - )} - {group.items.slice(0, maxOptions).map((option, idx) => ( - - `flex cursor-pointer items-center justify-between rounded px-2.5 py-1.5 text-base ${ - focus ? "bg-surface-gray-3" : "" - } ${ - (option as Option).disabled ? "opacity-50" : "" - }` - } - > - <> -
    - {(itemPrefix || multiple) && ( -
    - {itemPrefix ? ( - itemPrefix(option as AutocompleteOption) - ) : isOptionSelected(option as Option) ? ( - - ) : ( -
    + + {groups.length === 0 ? ( +
  • + No results found +
  • + ) : ( + groups.map((group) => ( +
    + {group.group && !group.hideLabel && ( +
    + {group.group} +
    + )} + {group.items + .slice(0, maxOptions) + .map((option, idx) => ( + + `flex cursor-pointer items-center justify-between rounded px-2.5 py-1.5 text-base ${ + focus ? "bg-surface-gray-3" : "" + } ${ + (option as Option).disabled + ? "opacity-50" + : "" + }` + } + > + <> +
    + {(itemPrefix || multiple) && ( +
    + {itemPrefix ? ( + itemPrefix( + option as AutocompleteOption + ) + ) : isOptionSelected( + option as Option + ) ? ( + + ) : ( +
    + )} +
    )} + + {getLabel(option)} +
    - )} - - {getLabel(option)} - -
    - - {itemSuffix && ( -
    - {itemSuffix(option as Option)} - {(option as Option)?.description && ( -
    - {(option as Option).description} + + {itemSuffix && ( +
    + {itemSuffix(option as Option)} + {(option as Option)?.description && ( +
    + {(option as Option).description} +
    + )}
    )} -
    - )} - - - ))} -
    - )) - )} - - - {showFooter && multiple && ( -
    - {multiple ? ( -
    - {!areAllOptionsSelected && ( -
    - ) : ( -
    -
    + + + ))} +
    + )) )} -
    - )} -
    - ) - } - /> - )} - + + + {showFooter && multiple && ( +
    + {multiple ? ( +
    + {!areAllOptionsSelected && ( +
    + ) : ( +
    +
    + )} +
    + )} +
    + ) + } + /> + )} + +
    ); }; diff --git a/packages/frappe-ui-react/src/components/calendar/calendar.stories.tsx b/packages/frappe-ui-react/src/components/calendar/calendar.stories.tsx index 854a6433..8912aa46 100644 --- a/packages/frappe-ui-react/src/components/calendar/calendar.stories.tsx +++ b/packages/frappe-ui-react/src/components/calendar/calendar.stories.tsx @@ -2,7 +2,12 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import type { CalendarConfig, CalendarEvent } from "./types"; import { Calendar } from "./calendar"; -import TabButtons from "../tabButtons"; +import { Button } from "../button"; +import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; +import { Select } from "../select"; +import { DatePicker } from "../datePicker"; +import { dayjs } from "../../utils/dayjs"; +import { action } from "storybook/actions"; const meta: Meta = { title: "Components/Calendar", @@ -122,9 +127,12 @@ export const Default: Story = { args: { config: { ...config, - createNewEvent: (event: CalendarEvent) => console.log("Create Event", event), - updateEventState: (event: CalendarEvent) => console.log("Update Event", event), - deleteEvent: (eventId: string|number) => console.log("Delete Event", eventId), + createNewEvent: (event: CalendarEvent) => + console.log("Create Event", event), + updateEventState: (event: CalendarEvent) => + console.log("Update Event", event), + deleteEvent: (eventId: string | number) => + console.log("Delete Event", eventId), }, events, }, @@ -143,39 +151,54 @@ export const CustomHeader: Story = { decrement, increment, enabledModes, - activeView, updateActiveView, + setCalendarDate, + formatter, }) => ( -
    -
    -

    - {currentMonthYear} -

    -
    - - -
    - ({ - value: mode.id, - label: mode.label, - }) - )} - value={activeView} - onChange={updateActiveView} + {displayValue} + + )} + +
    + +
    + setHour(Number(e.target.value))} - /> -
    -
    - setMinute(Number(e.target.value))} - /> -
    -
    - setSecond(Number(e.target.value))} + + {/* Time Picker Section */} +
    e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + ( + ( + + )} /> + )} + body={() => ( +
    e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + {timeOptions.map((time) => { + const isSelected = timeValue === time; + return ( + + ); + })} +
    + )} + /> +
    + + {/* Footer Actions */} + {clearable && ( +
    +
    + +
    + {(dateValue || timeValue) && ( + + )}
    -
    - {/* Actions */} -
    - -
    + )}
    )} /> diff --git a/packages/frappe-ui-react/src/components/datePicker/types.ts b/packages/frappe-ui-react/src/components/datePicker/types.ts index 8563385f..8b0d9435 100644 --- a/packages/frappe-ui-react/src/components/datePicker/types.ts +++ b/packages/frappe-ui-react/src/components/datePicker/types.ts @@ -1,10 +1,17 @@ +export interface DatePickerChildrenProps { + togglePopover: () => void; + isOpen: boolean; + displayValue: string; +} + export interface DatePickerProps { value?: string | string[]; modelValue?: string | string[]; placeholder?: string; formatter?: (date: string) => string; readonly?: boolean; - inputClass?: string | string[] | Record; + inputClass?: string; + variant?: "subtle" | "outline" | "ghost"; placement?: | "top-start" | "top" @@ -19,10 +26,59 @@ export interface DatePickerProps { | "right" | "right-end"; label?: string; + clearable?: boolean; onChange?: (value: string | string[]) => void; + children?: (props: DatePickerChildrenProps) => React.ReactNode; +} + +export interface DateTimePickerProps { + value?: string; + placeholder?: string; + formatter?: (date: string) => string; + placement?: + | "top-start" + | "top" + | "top-end" + | "bottom-start" + | "bottom" + | "bottom-end" + | "left-start" + | "left" + | "left-end" + | "right-start" + | "right" + | "right-end"; + label?: string; + clearable?: boolean; + onChange?: (value: string) => void; + children?: (props: DatePickerChildrenProps) => React.ReactNode; +} + +export interface DateRangePickerProps { + value?: string[]; + placeholder?: string; + formatter?: (from: string, to: string) => string; + placement?: + | "top-start" + | "top" + | "top-end" + | "bottom-start" + | "bottom" + | "bottom-end" + | "left-start" + | "left" + | "left-end" + | "right-start" + | "right" + | "right-end"; + label?: string; + onChange?: (value: string[]) => void; + children?: (props: DatePickerChildrenProps) => React.ReactNode; } export type DatePickerEmits = { (event: "update:modelValue", value: string): void; (event: "change", value: string): void; }; + +export type DatePickerViewMode = "date" | "month" | "year"; diff --git a/packages/frappe-ui-react/src/components/datePicker/useDatePicker.ts b/packages/frappe-ui-react/src/components/datePicker/useDatePicker.ts index bccc7bd0..139262cb 100644 --- a/packages/frappe-ui-react/src/components/datePicker/useDatePicker.ts +++ b/packages/frappe-ui-react/src/components/datePicker/useDatePicker.ts @@ -1,26 +1,99 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { getDate, getDatesAfter, getDaysInMonth, getDateValue } from "./utils"; +import { + getDate, + getDatesAfter, + getDaysInMonth, + getDateValue, + getDateTimeValue, + formatDateTime12h, +} from "./utils"; +import { DatePickerViewMode } from "./types"; + +const MONTHS = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +function formatTime12h(hours: number, minutes: number): string { + const period = hours >= 12 ? "pm" : "am"; + const h = hours % 12 || 12; + return `${h}:${minutes.toString().padStart(2, "0")} ${period}`; +} + +function parseTimeValue(timeStr: string): { hours: number; minutes: number } { + if (!timeStr) return { hours: 0, minutes: 0 }; + const [time, period] = timeStr.split(" "); + const [h, m] = time.split(":").map(Number); + let hours = h; + const periodLower = period?.toLowerCase(); + if (periodLower === "pm" && h !== 12) hours += 12; + if (periodLower === "am" && h === 12) hours = 0; + return { hours, minutes: m }; +} + +function generateTimeOptions(): string[] { + const options: string[] = []; + for (let h = 0; h < 24; h++) { + for (let m = 0; m < 60; m += 15) { + options.push(formatTime12h(h, m)); + } + } + return options; +} export function useDatePicker({ value, onChange, + withTime = false, }: { value?: string | string[]; onChange?: (value: string | string[]) => void; + withTime?: boolean; } = {}) { const today = useMemo(() => getDate(), []); const [open, setOpen] = useState(false); - const [dateValue, setDateValue] = useState( - typeof value === "string" ? value : "" - ); + const [view, setView] = useState("date"); + const [dateValue, setDateValue] = useState(""); + const [timeValue, setTimeValue] = useState(withTime ? "12:00 am" : ""); const [currentYear, setCurrentYear] = useState(today.getFullYear()); const [currentMonth, setCurrentMonth] = useState( today.getMonth() + 1 ); useEffect(() => { - if (typeof value === "string") setDateValue(value); - }, [value]); + if (typeof value === "string" && value) { + if (withTime) { + const parts = value.split(" "); + const datePart = parts[0] || ""; + setDateValue(datePart); + + if (parts.length >= 2) { + const timePart = parts.slice(1).join(" "); + const timeMatch = timePart.match(/^(\d{1,2}):(\d{2})/); + if (timeMatch) { + const hours = parseInt(timeMatch[1], 10); + const minutes = parseInt(timeMatch[2], 10); + setTimeValue(formatTime12h(hours, minutes)); + } + } + } else { + setDateValue(value); + } + } else { + setDateValue(""); + setTimeValue(""); + } + }, [value, withTime]); const dates = useMemo(() => { if (!(currentYear && currentMonth)) return []; @@ -36,20 +109,13 @@ export function useDatePicker({ const daysInMonth = getDaysInMonth(monthIndex, year); const datesInMonth = getDatesAfter(firstDayOfMonth, daysInMonth - 1); - let allDates = [ + const allDates = [ ...leftPadding, firstDayOfMonth, ...datesInMonth, ...rightPadding, ]; - if (allDates.length < 42) { - const lastDate = allDates.at(-1); - if (lastDate) { - const finalPadding = getDatesAfter(lastDate, 42 - allDates.length); - allDates = allDates.concat(finalPadding); - } - } return allDates; }, [currentYear, currentMonth]); @@ -65,8 +131,8 @@ export function useDatePicker({ const formattedMonth = useMemo(() => { if (!(currentYear && currentMonth)) return ""; const date = getDate(currentYear, currentMonth - 1, 1); - const month = date.toLocaleString("en-US", { month: "long" }); - return `${month}, ${date.getFullYear()}`; + const month = date.toLocaleString("en-US", { month: "short" }); + return `${month} ${date.getFullYear()}`; }, [currentYear, currentMonth]); const prevMonth = useCallback(() => { @@ -93,16 +159,160 @@ export function useDatePicker({ (d: Date, close = false) => { const v = getDateValue(d); setDateValue(v); - onChange?.(v); + if (withTime) { + const { hours, minutes } = parseTimeValue(timeValue); + const newDate = getDate(v); + newDate.setHours(hours, minutes, 0, 0); + onChange?.(getDateTimeValue(newDate)); + } else { + onChange?.(v); + } if (close) setOpen(false); }, - [onChange] + [onChange, withTime, timeValue] ); const selectToday = useCallback(() => { selectDate(getDate(), true); }, [selectDate]); + const timeOptions = useMemo(() => generateTimeOptions(), []); + + const selectTime = useCallback( + (time: string) => { + setTimeValue(time); + if (dateValue) { + const { hours, minutes } = parseTimeValue(time); + const d = getDate(dateValue); + d.setHours(hours, minutes, 0, 0); + onChange?.(getDateTimeValue(d)); + } + }, + [dateValue, onChange] + ); + + const selectNow = useCallback(() => { + const now = getDate(); + const v = getDateValue(now); + setDateValue(v); + if (withTime) { + const time = formatTime12h(now.getHours(), now.getMinutes()); + setTimeValue(time); + onChange?.(getDateTimeValue(now)); + } else { + onChange?.(v); + } + setOpen(false); + }, [onChange, withTime]); + + const selectTomorrow = useCallback(() => { + const tomorrow = getDate(); + tomorrow.setDate(tomorrow.getDate() + 1); + const v = getDateValue(tomorrow); + setDateValue(v); + + if (withTime) { + const time = + timeValue || formatTime12h(getDate().getHours(), getDate().getMinutes()); + if (!timeValue) setTimeValue(time); + const { hours, minutes } = parseTimeValue(time); + tomorrow.setHours(hours, minutes, 0, 0); + onChange?.(getDateTimeValue(tomorrow)); + } else { + tomorrow.setHours(0, 0, 0, 0); + onChange?.(v); + } + }, [onChange, withTime, timeValue]); + + const clearValue = useCallback(() => { + setDateValue(""); + setTimeValue(""); + onChange?.(""); + setOpen(false); + }, [onChange]); + + const displayValue = useMemo(() => { + if (!dateValue) return ""; + if (withTime) { + const { hours, minutes } = parseTimeValue(timeValue); + const d = getDate(dateValue); + d.setHours(hours, minutes, 0, 0); + return formatDateTime12h(d); + } + return dateValue; + }, [dateValue, timeValue, withTime]); + + const cycleView = useCallback(() => { + setView((prev) => { + if (prev === "date") return "month"; + if (prev === "month") return "year"; + return "date"; + }); + }, []); + + const selectMonth = useCallback((monthIndex: number) => { + setCurrentMonth(monthIndex + 1); + setView("date"); + }, []); + + const selectYear = useCallback((year: number) => { + setCurrentYear(year); + setView("month"); + }, []); + + const yearRangeStart = useMemo( + () => currentYear - (currentYear % 12), + [currentYear] + ); + + const yearRange = useMemo( + () => Array.from({ length: 12 }, (_, i) => yearRangeStart + i), + [yearRangeStart] + ); + + const prev = useCallback(() => { + if (view === "date") { + setCurrentMonth((prev) => { + if (prev === 1) { + setCurrentYear((y) => y - 1); + return 12; + } + return prev - 1; + }); + } else if (view === "month") { + setCurrentYear((y) => y - 1); + } else { + setCurrentYear((y) => y - 12); + } + }, [view]); + + const next = useCallback(() => { + if (view === "date") { + setCurrentMonth((prev) => { + if (prev === 12) { + setCurrentYear((y) => y + 1); + return 1; + } + return prev + 1; + }); + } else if (view === "month") { + setCurrentYear((y) => y + 1); + } else { + setCurrentYear((y) => y + 12); + } + }, [view]); + + const resetView = useCallback(() => { + setView("date"); + if (dateValue) { + const selectedDate = getDate(dateValue); + if (!isNaN(selectedDate.getTime())) { + setCurrentYear(selectedDate.getFullYear()); + setCurrentMonth(selectedDate.getMonth() + 1); + } + } + }, [dateValue]); + return { open, setOpen, @@ -120,5 +330,24 @@ export function useDatePicker({ nextMonth, selectDate, selectToday, + view, + setView, + cycleView, + selectMonth, + selectYear, + yearRangeStart, + yearRange, + prev, + next, + resetView, + months: MONTHS, + timeValue, + setTimeValue, + timeOptions, + selectTime, + selectNow, + selectTomorrow, + clearValue, + displayValue, }; } diff --git a/packages/frappe-ui-react/src/components/datePicker/utils.ts b/packages/frappe-ui-react/src/components/datePicker/utils.ts index 61326bd0..ce12b1c4 100644 --- a/packages/frappe-ui-react/src/components/datePicker/utils.ts +++ b/packages/frappe-ui-react/src/components/datePicker/utils.ts @@ -27,6 +27,11 @@ export function getDateTimeValue(date: Date | string) { return dayjs(date).format("YYYY-MM-DD HH:mm:ss"); } +export function formatDateTime12h(date: Date | string) { + if (!date || date.toString() === "Invalid Date") return ""; + return dayjs(date).format("YYYY-MM-DD h:mm a"); +} + export function getDatesAfter(date: Date, count: number) { let incrementer = 1; if (count < 0) { diff --git a/packages/frappe-ui-react/src/components/dropdown/dropdown.stories.tsx b/packages/frappe-ui-react/src/components/dropdown/dropdown.stories.tsx index c0d0913d..0f3bfa12 100644 --- a/packages/frappe-ui-react/src/components/dropdown/dropdown.stories.tsx +++ b/packages/frappe-ui-react/src/components/dropdown/dropdown.stories.tsx @@ -1,9 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { action } from "storybook/actions"; +import { useState } from "react"; import Dropdown from "./dropdown"; import { Button } from "../button"; -import { DropdownOptions } from "./types"; +import type { DropdownOptions } from "./types"; export default { title: "Components/Dropdown", @@ -73,18 +74,6 @@ const groupedActions: DropdownOptions = [ { label: "Export", icon: "download", - submenu: [ - { - label: "Export as PDF", - icon: "file-text", - onClick: () => action("Export as PDF clicked")(), - }, - { - label: "Export as CSV", - icon: "file", - onClick: () => action("Export as CSV clicked")(), - }, - ], }, { label: "Share", @@ -244,11 +233,77 @@ export const CenterAligned: StoryObj = { }; export const WithSubmenus: StoryObj = { - ...DropdownTemplate, args: { options: submenuActions, button: { label: "With Submenus" }, }, + render: function Render(args) { + const [collaborateValue, setCollaborateValue] = useState(false); + + const options: DropdownOptions = [ + ...submenuActions, + { + label: "Collaborate", + switch: true, + icon: "file-text", + switchValue: collaborateValue, + onClick: (val) => { + setCollaborateValue(val as boolean); + action("Collaborate switch value:")(val); + }, + }, + ]; + + return ( +
    + +
    + ); + }, +}; + +export const WithSwitches: StoryObj = { + args: { + button: { label: "With Switches" }, + }, + render: function Render(args) { + const [lockValue, setLockValue] = useState(true); + const [collaborateValue, setCollaborateValue] = useState(false); + + const options: DropdownOptions = [ + { + label: "Rename", + icon: "edit", + onClick: () => action("Rename clicked")(), + }, + { + label: "Lock", + icon: "lock", + switch: true, + switchValue: lockValue, + onClick: (val) => { + setLockValue(val as boolean); + action("Lock switch value:")(val); + }, + }, + { + label: "Collaborate", + switch: true, + icon: "users", + switchValue: collaborateValue, + onClick: (val) => { + setCollaborateValue(val as boolean); + action("Collaborate switch value:")(val); + }, + }, + ]; + + return ( +
    + +
    + ); + }, }; export const WithNestedSubmenus: StoryObj = { diff --git a/packages/frappe-ui-react/src/components/dropdown/dropdown.tsx b/packages/frappe-ui-react/src/components/dropdown/dropdown.tsx index f63950ad..7b8d753f 100644 --- a/packages/frappe-ui-react/src/components/dropdown/dropdown.tsx +++ b/packages/frappe-ui-react/src/components/dropdown/dropdown.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useCallback } from "react"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { Button, ButtonProps } from "../button"; +import { Switch } from "../switch"; import type { DropdownProps, DropdownOption, @@ -14,7 +15,7 @@ const cssClasses = { dropdownContent: "min-w-40 divide-y divide-outline-gray-modals rounded-lg bg-surface-modal shadow-2xl ring-black focus:outline-none dropdown-content border border-outline-gray-1 z-100", groupContainer: "p-1.5", - groupLabel: "flex h-7 items-center px-2 text-sm font-medium text-ink-gray-5", + groupLabel: "flex h-7 items-center px-2 text-sm font-medium text-ink-gray-7", itemLabel: "whitespace-nowrap", itemIcon: "mr-2 h-4 w-4 flex-shrink-0", chevronIcon: "ml-auto h-4 w-4 flex-shrink-0", @@ -61,10 +62,13 @@ const Dropdown: React.FC = ({ label: option.label, icon: option.icon, component: option.component, - onClick: () => handleItemClick(option), + onClick: option.switch ? option.onClick : () => handleItemClick(option), submenu: option.submenu, condition: option.condition, theme: option.theme, + disabled: option.disabled, + switch: option.switch, + switchValue: option.switchValue, }; }, [handleItemClick] @@ -151,6 +155,29 @@ const Dropdown: React.FC = ({ if (item.component) { const CustomComponent = item.component; return ; + } else if (item.switch) { + return ( +
    e.preventDefault()} + > + {item.icon && + (typeof item.icon === "string" ? ( + + ) : React.isValidElement(item.icon) ? ( + item.icon + ) : null)} + {item.label} + item.onClick?.(checked)} + /> +
    + ); } else if (item.submenu) { return ( @@ -201,7 +228,7 @@ const Dropdown: React.FC = ({ subItem.onClick?.()} > {renderDropdownItem(subItem)} @@ -268,7 +295,7 @@ const Dropdown: React.FC = ({ )} {group.items.map((item) => (
    - + !item.switch && item.onClick?.()}> {renderDropdownItem(item)}
    diff --git a/packages/frappe-ui-react/src/components/dropdown/types.ts b/packages/frappe-ui-react/src/components/dropdown/types.ts index e673f458..d05eea6e 100644 --- a/packages/frappe-ui-react/src/components/dropdown/types.ts +++ b/packages/frappe-ui-react/src/components/dropdown/types.ts @@ -4,7 +4,7 @@ import { ButtonTheme } from "../button"; export interface DropdownOption { label: string; - onClick?: () => void; + onClick?: (val?: boolean) => void; link?: string; icon?: string | ReactNode; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -12,6 +12,9 @@ export interface DropdownOption { theme?: ButtonTheme; submenu?: DropdownOptions; condition?: () => boolean; + disabled?: boolean; + switch?: boolean; + switchValue?: boolean; } export interface DropdownGroupOption { diff --git a/packages/frappe-ui-react/src/components/formControl/formControl.stories.tsx b/packages/frappe-ui-react/src/components/formControl/formControl.stories.tsx index e967fefa..5a108fe4 100644 --- a/packages/frappe-ui-react/src/components/formControl/formControl.stories.tsx +++ b/packages/frappe-ui-react/src/components/formControl/formControl.stories.tsx @@ -3,6 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import FormControl from "./formControl"; import FeatherIcon from "../featherIcon"; import { useState } from "react"; +import { Avatar } from "../avatar"; const meta: Meta = { title: "Components/FormControl", @@ -156,11 +157,13 @@ export const Autocomplete: Story = { render: (args) => { const [value, setValue] = useState(""); return ( - setValue(_value)} - /> +
    + setValue(_value)} + /> +
    ); }, }; @@ -182,10 +185,33 @@ export const Checkbox: Story = { }, }; -export const WithPrefixIcon: Story = { +export const PrefixSlotIcon: Story = { args: { type: "text", placeholder: "", prefix: () => , }, }; + +export const SuffixSlotIcon: Story = { + args: { + type: "text", + placeholder: "", + suffix: () => , + }, +}; + +export const PrefixSlotAvatar: Story = { + args: { + type: "text", + placeholder: "", + prefix: () => ( + + ), + }, +}; diff --git a/packages/frappe-ui-react/src/components/index.ts b/packages/frappe-ui-react/src/components/index.ts index dc7053e8..fd9c7673 100644 --- a/packages/frappe-ui-react/src/components/index.ts +++ b/packages/frappe-ui-react/src/components/index.ts @@ -36,6 +36,11 @@ export { default as TextEditor } from "./textEditor"; export * from "./toast"; export * from "./tooltip"; export * from "./tree"; +export * from "./signaturePad"; +export * from "./kanbanBoard"; +export * from "./stepper"; +export * from "./dataTable"; +export * from "./timeline"; export { default as Card } from "./card"; export { default as featherIcon } from "./featherIcon"; diff --git a/packages/frappe-ui-react/src/components/kanbanBoard/index.ts b/packages/frappe-ui-react/src/components/kanbanBoard/index.ts new file mode 100644 index 00000000..56d4d703 --- /dev/null +++ b/packages/frappe-ui-react/src/components/kanbanBoard/index.ts @@ -0,0 +1,4 @@ +export { KanbanBoard } from "./kanbanBoard"; +export type { KanbanBoardProps, KanbanCard, KanbanColumn } from "./types"; + + diff --git a/packages/frappe-ui-react/src/components/kanbanBoard/kanbanBoard.stories.tsx b/packages/frappe-ui-react/src/components/kanbanBoard/kanbanBoard.stories.tsx new file mode 100644 index 00000000..6140347e --- /dev/null +++ b/packages/frappe-ui-react/src/components/kanbanBoard/kanbanBoard.stories.tsx @@ -0,0 +1,131 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { KanbanBoard } from "./index"; +import type { KanbanColumn } from "./types"; + +const meta: Meta = { + title: "Components/KanbanBoard", + tags: ["autodocs"], + component: KanbanBoard, + parameters: { docs: { source: { type: "dynamic" } }, layout: "padded" }, +}; + +export default meta; +type Story = StoryObj; + +const initialColumns: KanbanColumn[] = [ + { + id: "todo", + title: "To Do", + cards: [ + { id: "1", title: "Design new feature", description: "Create mockups for the new dashboard" }, + { id: "2", title: "Review PR #123", description: "Check the code changes" }, + ], + }, + { + id: "in-progress", + title: "In Progress", + cards: [ + { id: "3", title: "Implement API", description: "Create endpoints for user management" }, + ], + }, + { + id: "review", + title: "Review", + cards: [ + { id: "4", title: "Write tests", description: "Add unit tests for components" }, + ], + }, + { + id: "done", + title: "Done", + cards: [ + { id: "5", title: "Fix bug #456", description: "Resolved the authentication issue" }, + { id: "6", title: "Update documentation", description: "Documented the new API" }, + ], + }, +]; + +export const Default: Story = { + args: { + columns: initialColumns, + enableDragDrop: true, + showAddCard: false, + }, + render: (args) => ( +
    + +
    + ), + argTypes: { + columns: { + control: "object", + description: "Columns configuration", + }, + enableDragDrop: { + control: "boolean", + description: "Whether drag and drop is enabled", + }, + showAddCard: { + control: "boolean", + description: "Whether to show add card button", + }, + onCardMove: { + control: false, + description: "Callback when a card is moved", + }, + onCardClick: { + control: false, + description: "Callback when a card is clicked", + }, + onCardAdd: { + control: false, + description: "Callback when a new card is added", + }, + renderCard: { + control: false, + description: "Custom render function for cards", + }, + renderColumnHeader: { + control: false, + description: "Custom render function for column headers", + }, + className: { + control: "text", + description: "Custom class name", + }, + }, +}; + +export const WithAddCard: Story = { + args: { + ...Default.args, + showAddCard: true, + }, + render: (args) => ( +
    + +
    + ), +}; + +export const WithCallbacks: Story = { + args: { + ...Default.args, + onCardMove: (cardId, fromColumnId, toColumnId) => { + console.log(`Card ${cardId} moved from ${fromColumnId} to ${toColumnId}`); + }, + onCardClick: (card, columnId) => { + alert(`Clicked card: ${card.title} in column: ${columnId}`); + }, + onCardAdd: (columnId, card) => { + console.log(`Card added to ${columnId}:`, card); + }, + }, + render: (args) => ( +
    + +
    + ), +}; + + diff --git a/packages/frappe-ui-react/src/components/kanbanBoard/kanbanBoard.tsx b/packages/frappe-ui-react/src/components/kanbanBoard/kanbanBoard.tsx new file mode 100644 index 00000000..ad6e3701 --- /dev/null +++ b/packages/frappe-ui-react/src/components/kanbanBoard/kanbanBoard.tsx @@ -0,0 +1,177 @@ +import React, { useState, useCallback } from "react"; +import FeatherIcon from "../featherIcon"; +import { Button } from "../button"; +import type { KanbanBoardProps, KanbanCard, KanbanColumn } from "./types"; + +export const KanbanBoard: React.FC = ({ + columns: initialColumns, + onCardMove, + onCardClick, + onCardAdd, + renderCard, + renderColumnHeader, + enableDragDrop = true, + showAddCard = false, + className = "", +}) => { + const [columns, setColumns] = useState(initialColumns); + const [draggedCard, setDraggedCard] = useState<{ card: KanbanCard; columnId: string } | null>(null); + const [dragOverColumn, setDragOverColumn] = useState(null); + + const handleDragStart = useCallback( + (e: React.DragEvent, card: KanbanCard, columnId: string) => { + if (!enableDragDrop) return; + setDraggedCard({ card, columnId }); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", ""); + }, + [enableDragDrop] + ); + + const handleDragOver = useCallback( + (e: React.DragEvent, columnId: string) => { + if (!enableDragDrop || !draggedCard) return; + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + setDragOverColumn(columnId); + }, + [enableDragDrop, draggedCard] + ); + + const handleDragLeave = useCallback(() => { + setDragOverColumn(null); + }, []); + + const handleDrop = useCallback( + (e: React.DragEvent, targetColumnId: string) => { + if (!enableDragDrop || !draggedCard) return; + e.preventDefault(); + + const { card, columnId: sourceColumnId } = draggedCard; + + if (sourceColumnId === targetColumnId) { + setDraggedCard(null); + setDragOverColumn(null); + return; + } + + const newColumns = columns.map((col) => { + if (col.id === sourceColumnId) { + return { + ...col, + cards: col.cards.filter((c) => c.id !== card.id), + }; + } + if (col.id === targetColumnId) { + return { + ...col, + cards: [...col.cards, card], + }; + } + return col; + }); + + setColumns(newColumns); + setDraggedCard(null); + setDragOverColumn(null); + + if (onCardMove) { + const targetColumn = newColumns.find((col) => col.id === targetColumnId); + const newIndex = targetColumn?.cards.length ? targetColumn.cards.length - 1 : 0; + onCardMove(card.id, sourceColumnId, targetColumnId, newIndex); + } + }, + [enableDragDrop, draggedCard, columns, onCardMove] + ); + + const handleCardClick = useCallback( + (card: KanbanCard, columnId: string) => { + onCardClick?.(card, columnId); + }, + [onCardClick] + ); + + const handleAddCard = useCallback( + (columnId: string) => { + const newCard: KanbanCard = { + id: `card-${Date.now()}`, + title: "New Card", + description: "Click to edit", + }; + const newColumns = columns.map((col) => + col.id === columnId ? { ...col, cards: [...col.cards, newCard] } : col + ); + setColumns(newColumns); + onCardAdd?.(columnId, newCard); + }, + [columns, onCardAdd] + ); + + const defaultRenderCard = (card: KanbanCard, columnId: string) => ( +
    handleCardClick(card, columnId)} + > +
    {card.title}
    + {card.description && ( +
    {card.description}
    + )} +
    + ); + + const defaultRenderColumnHeader = (column: KanbanColumn) => ( +
    +
    + {column.title} + + {column.cards.length} + +
    +
    + ); + + return ( +
    + {columns.map((column) => ( +
    handleDragOver(e, column.id)} + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, column.id)} + > +
    + {renderColumnHeader ? renderColumnHeader(column) : defaultRenderColumnHeader(column)} +
    + {column.cards.map((card) => ( +
    handleDragStart(e, card, column.id)} + className={enableDragDrop ? "cursor-move" : ""} + > + {renderCard ? renderCard(card, column.id) : defaultRenderCard(card, column.id)} +
    + ))} + {showAddCard && !column.disabled && ( +
    +
    +
    + ))} +
    + ); +}; + + diff --git a/packages/frappe-ui-react/src/components/kanbanBoard/types.ts b/packages/frappe-ui-react/src/components/kanbanBoard/types.ts new file mode 100644 index 00000000..83f52934 --- /dev/null +++ b/packages/frappe-ui-react/src/components/kanbanBoard/types.ts @@ -0,0 +1,84 @@ +export interface KanbanCard { + /** + * Unique identifier for the card + */ + id: string; + /** + * Title of the card + */ + title: string; + /** + * Description or content of the card + */ + description?: string; + /** + * Additional metadata + */ + [key: string]: unknown; +} + +export interface KanbanColumn { + /** + * Unique identifier for the column + */ + id: string; + /** + * Title of the column + */ + title: string; + /** + * Cards in this column + */ + cards: KanbanCard[]; + /** + * Maximum number of cards allowed (optional) + */ + maxCards?: number; + /** + * Whether the column is disabled + */ + disabled?: boolean; +} + +export interface KanbanBoardProps { + /** + * Columns configuration + */ + columns: KanbanColumn[]; + /** + * Callback when a card is moved + */ + onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void; + /** + * Callback when a card is clicked + */ + onCardClick?: (card: KanbanCard, columnId: string) => void; + /** + * Callback when a new card is added + */ + onCardAdd?: (columnId: string, card: KanbanCard) => void; + /** + * Custom render function for cards + */ + renderCard?: (card: KanbanCard, columnId: string) => React.ReactNode; + /** + * Custom render function for column headers + */ + renderColumnHeader?: (column: KanbanColumn) => React.ReactNode; + /** + * Whether drag and drop is enabled + * @default true + */ + enableDragDrop?: boolean; + /** + * Whether to show add card button + * @default false + */ + showAddCard?: boolean; + /** + * Custom class name + */ + className?: string; +} + + diff --git a/packages/frappe-ui-react/src/components/listview/listHeader.tsx b/packages/frappe-ui-react/src/components/listview/listHeader.tsx index 605f3ce3..c1f03207 100644 --- a/packages/frappe-ui-react/src/components/listview/listHeader.tsx +++ b/packages/frappe-ui-react/src/components/listview/listHeader.tsx @@ -35,7 +35,7 @@ const ListHeader: React.FC = ({ children }) => { item={column} lastItem={index === list.columns.length - 1} onColumnWidthUpdated={(width: number) => { - list.updateColumnWidth(index, width); + list.options.updateColumnWidth(index, width); }} /> ))} diff --git a/packages/frappe-ui-react/src/components/listview/listprovider.tsx b/packages/frappe-ui-react/src/components/listview/listprovider.tsx index 2c7bc539..5244b598 100644 --- a/packages/frappe-ui-react/src/components/listview/listprovider.tsx +++ b/packages/frappe-ui-react/src/components/listview/listprovider.tsx @@ -58,8 +58,9 @@ export const ListProvider: React.FC = ({ title: "No Data", description: "No data available", }, + updateColumnWidth, }; - }, [options]); + }, [options, updateColumnWidth]); const showGroupedRows = useMemo( () => rows.every((row) => row.group && row.rows && Array.isArray(row.rows)), @@ -135,7 +136,6 @@ export const ListProvider: React.FC = ({ toggleAllRows, emptyState: options.emptyState, setColumns: () => {}, - updateColumnWidth, }, }), [ @@ -150,7 +150,6 @@ export const ListProvider: React.FC = ({ allRowsSelected, toggleRow, toggleAllRows, - updateColumnWidth, ] ); return ( diff --git a/packages/frappe-ui-react/src/components/popover/popover.tsx b/packages/frappe-ui-react/src/components/popover/popover.tsx index 18050dfb..93b9a5c3 100644 --- a/packages/frappe-ui-react/src/components/popover/popover.tsx +++ b/packages/frappe-ui-react/src/components/popover/popover.tsx @@ -16,6 +16,7 @@ function getOrCreatePopoverRoot(): HTMLElement { if (!root) { root = document.createElement("div"); root.id = popoverRootId; + root.style.position = "absolute"; document.body.appendChild(root); } return root; @@ -363,7 +364,7 @@ const Popover: React.FC = ({ {createPortal(
    ; const Template: StoryObj = { + args: { + value: "", + options: [ + { label: "John Doe", value: "john-doe" }, + { label: "Jane Doe", value: "jane-doe" }, + { label: "John Smith", value: "john-smith" }, + { label: "Jane Smith", value: "jane-smith", disabled: true }, + { label: "John Wayne", value: "john-wayne" }, + { label: "Jane Wayne", value: "jane-wayne" }, + ], + }, render: (args) => { const [value, setValue] = useState(args.value || ""); @@ -58,16 +70,7 @@ const Template: StoryObj = {