commit 253d0f95ee64e98e3dd8ad6ddb9f40956daba359 Author: seb Date: Sat Aug 8 16:58:48 2026 +0200 Genesis diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd6e803 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..033a7d2 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# dashgui + +Reusable custom **GUI class components** (React + MUI), shared across the +`dash*` apps. This is the "outsourced" home for custom GUI primitives that +previously lived inside `dashfront/src/components`. + +This package is source-only: consumers import directly from `src/` via the +`exports` map, so there is no build step for the library itself. + +## Contents + +| Export | Source | Description | +| --- | --- | --- | +| `dashgui/panels` | `src/panels/` | `PanelTile`, `PanelTileGrid`, `EditablePanelTile`, `injectMuiTheme`, layout helpers | +| `dashgui/lists` | `src/lists/` | `RecordList` — generic presentational data list (loading/error/empty states + table with edit/delete) | +| `dashgui/tiles` | `src/tiles/` | `Tile`, `TILE_SIZE`, `tileBaseSx` | +| `dashgui/ElevatedPill` | `src/ElevatedPill.jsx` | `ElevatedPill`, `ToggleElevatedPill`, `ActionElevatedPill` | +| `dashgui/SingleLineTextEdit` | `src/SingleLineTextEdit.jsx` | Centralized single-line text field | + +## Design rules + +- **No app coupling.** No Redux, router, notistack, storage manager, or backend + error imports. Error surfaces are injected via props (e.g. `EditablePanelTile`'s + `onError`, `RecordList`'s `error`). Data and actions are always passed in. +- **Class components** for the base primitives (matches the existing codebase). +- Only `react`, MUI, and MUI icons as (peer) dependencies. + +## Consuming from dashfront + +`dashfront` declares `"dashgui": "file:../dashgui"` (npm workspace / symlink). +Source imports keep changes instantly visible in the app. + +## Development harness + +Run `npm run dev` to render the components on **mock pages** (`dev/`) without a +backend. This is where test cases with mock pages will live. + +```sh +npm install +npm run dev # http://localhost:5199 +npm run build # bundle the dev harness (dist/) +npm run lint +``` + +## Future + +- Test cases (unit + component tests) with mock pages will be added under + `dev/` / `src/__tests__/` as they are written. diff --git a/index.html b/index.html new file mode 100644 index 0000000..9efc0fc --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + dashgui dev harness + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6a69e69 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1960 @@ +{ + "name": "dashgui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dashgui", + "version": "0.1.0", + "devDependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^9.2.0", + "@mui/material": "^9.2.0", + "@vitejs/plugin-react": "6.0.2", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "vite": "^8.2.0" + }, + "peerDependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^9.0.0", + "@mui/material": "^9.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.3.1.tgz", + "integrity": "sha512-IAyAFNQbT7hysJ9HXphiOmWJF7G1OglzHanqCgvQgH9LA2ydxtmaTBDbcBqw6euZesyShiwvpvbnYO1GY1AyXQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.3.1.tgz", + "integrity": "sha512-rZj5ccG7vkpV38o/l4ys+chfE9GFypmvZr9dSNBoYVCNBD9yC6KfKq1TYpEMshWbjic7GKQ8MA1LQlcpGgcq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^9.3.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.3.1.tgz", + "integrity": "sha512-NahAEGIXqS1K0bA4th1jeFxBguS59NOcLbMA0vU+fSaPWKjtwGBGGHeTwlc9PSmzMjOZKceeFWESB8fVHr31hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/core-downloads-tracker": "^9.3.1", + "@mui/system": "^9.3.0", + "@mui/types": "^9.3.0", + "@mui/utils": "^9.3.0", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.8", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^9.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.3.0.tgz", + "integrity": "sha512-ERvqk5pejf9aRnQcDILSWGtFmsEMiVxlQ4+xsVCjsEvmK0fV9BiVP/cQwAF5dwyDFbve4lTrlcgeFEecVzTNiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/utils": "^9.3.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.3.0.tgz", + "integrity": "sha512-x9+KYxhjoHYZ4nioxdKnvQWdw0RScbhoZfQ4tv3Db742683U3wlYSp6zV6Us78dMFnKnyTrPTkQKyutp14gnKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.3.0.tgz", + "integrity": "sha512-0l4LqHJxZj65xSrioniGsxm7VNoGXonPo203oZjhBUvIDPeBqRTb7Mqc45Qxs6sO6WGR7WE/9cJ6lb4lkvHkjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/private-theming": "^9.3.0", + "@mui/styled-engine": "^9.3.0", + "@mui/types": "^9.3.0", + "@mui/utils": "^9.3.0", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.3.0.tgz", + "integrity": "sha512-2JSxyfpEFWNUB2vKs/T1BvkfyNisMHWph8bLMj8T0uHwmLl/0qfAwQkfwMT6kxLXN9uIum9AEbECXU8er3amIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.3.0.tgz", + "integrity": "sha512-2HZdHwWJ6eB+7lVGSOHsByGw8jeRulT4g0NZ608Wb8Q57DE2jbNqrWPFuJsvkQQiBiTmlpvQL3i+/62zsiPrkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/types": "^9.3.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a967a46 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "dashgui", + "version": "0.1.0", + "description": "Reusable custom GUI class components shared across dash* apps. Outsource target for dashfront's GUI primitives.", + "type": "module", + "main": "src/index.js", + "module": "src/index.js", + "exports": { + ".": "./src/index.js", + "./panels": "./src/panels/index.js", + "./panels/panelTileLayout": "./src/panels/panelTileLayout.js", + "./lists": "./src/lists/index.js", + "./tiles": "./src/tiles/index.js", + "./ElevatedPill": "./src/ElevatedPill.jsx", + "./SingleLineTextEdit": "./src/SingleLineTextEdit.jsx", + "./package.json": "./package.json" + }, + "files": [ + "src" + ], + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint ." + }, + "peerDependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^9.0.0", + "@mui/material": "^9.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^9.2.0", + "@mui/material": "^9.2.0", + "@vitejs/plugin-react": "6.0.2", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "vite": "^8.2.0" + } +} diff --git a/src/ElevatedPill.jsx b/src/ElevatedPill.jsx new file mode 100644 index 0000000..e1609df --- /dev/null +++ b/src/ElevatedPill.jsx @@ -0,0 +1,125 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import Switch from '@mui/material/Switch'; +import Typography from '@mui/material/Typography'; +import { alpha } from '@mui/material/styles'; + +/** + * Base pill with elevation, shared styling, and an overridable trailing element. + * Root class for ToggleElevatedPill and ActionElevatedPill. + * + * @component ElevatedPill + * @import dashgui/ElevatedPill + * @defaultExport + * + * @prop {ReactNode} [label] - Text shown inside the pill. + * @prop {boolean} [active=false] - When true, uses a stronger primary gradient background. + * @prop {() => void} [onClick] - Called when the pill is clicked. + * + * @behaviour Rendered as a pill-shaped (borderRadius 999) inline-flex Box with a subtle primary-tinted gradient and shadow 1. + * @behaviour `renderTrailing()` may be overridden by subclasses to append an element (switch / icon). + * @behaviour Clicking toggles via onClick — the pill itself is not a form control. + */ +class ElevatedPill extends Component { + renderTrailing() { + return null; + } + + render() { + const { label, active = false, onClick } = this.props; + return ( + ({ + display: 'inline-flex', + alignItems: 'center', + gap: 0.5, + px: 1.5, + py: 0.25, + borderRadius: 999, + background: active + ? `linear-gradient(180deg, ${alpha(theme.palette.primary.main, 0.28)}, ${alpha( + theme.palette.primary.main, + 0.22, + )})` + : `linear-gradient(180deg, ${alpha(theme.palette.primary.main, 0.16)}, ${alpha( + theme.palette.primary.main, + 0.10, + )})`, + color: 'text.primary', + cursor: 'pointer', + userSelect: 'none', + boxShadow: theme.shadows[1], + transition: 'background 150ms ease, box-shadow 150ms ease', + })} + > + + {label} + + {this.renderTrailing()} + + ); + } +} + +/** + * ElevatedPill with a trailing MUI Switch bound to `checked` / `onToggle` — + * a labelled on/off toggle. + * + * @component ToggleElevatedPill + * @import dashgui/ElevatedPill + * + * @prop {ReactNode} [label] - Text shown next to the switch. + * @prop {boolean} checked - Controlled switch state. + * @prop {(next: boolean) => void} onToggle - Called with the new checked value when the switch is toggled. + * + * @behaviour The switch is styled with a success color when on, grey when off. + * @behaviour Controlled: the parent owns `checked` and updates it via `onToggle`. + */ +export class ToggleElevatedPill extends ElevatedPill { + renderTrailing() { + const { checked, onToggle } = this.props; + return ( + onToggle(event.target.checked)} + sx={(theme) => ({ + '& .MuiSwitch-switchBase': { + color: checked ? 'success.main' : 'grey.300', + }, + '& .Mui-checked': { + color: 'success.main', + }, + '& .Mui-checked + .MuiSwitch-track': { + backgroundColor: alpha(theme.palette.success.main, 0.35), + }, + })} + /> + ); + } +} + +/** + * ElevatedPill whose trailing slot is a custom icon — used for clickable pill + * buttons. + * + * @component ActionElevatedPill + * @import dashgui/ElevatedPill + * + * @prop {ReactNode} [label] - Text shown next to the icon. + * @prop {ReactNode} [icon] - Icon node rendered as the trailing element. + * @prop {boolean} [active=false] - When true, uses the stronger primary gradient background. + * @prop {() => void} [onClick] - Called when the pill is clicked. + * + * @behaviour Clicking the whole pill fires `onClick` — the icon is decorative, not a separate control. + * @behaviour `active` gives a pressed/selected look via the stronger gradient. + */ +export class ActionElevatedPill extends ElevatedPill { + renderTrailing() { + return this.props.icon; + } +} + +export default ElevatedPill; diff --git a/src/SingleLineTextEdit.jsx b/src/SingleLineTextEdit.jsx new file mode 100644 index 0000000..1c177f3 --- /dev/null +++ b/src/SingleLineTextEdit.jsx @@ -0,0 +1,86 @@ +import { Component } from 'react'; +import TextField from '@mui/material/TextField'; + +/** + * Centralized single-line text field with the same outlined look everywhere. + * Wraps MUI TextField; `value`/`onChange` follow the `(nextValue)` signature + * used across the app. + * + * @component SingleLineTextEdit + * @import dashgui/SingleLineTextEdit + * @defaultExport + * + * @prop {string} value - Controlled field value. + * @prop {(nextValue: string) => void} onChange - Called with the new string value (not an event). Not called while `readOnly`. + * @prop {React.Ref} [inputRef] - Ref forwarded to the inner input (used by EditablePanelTile to focus on edit). + * @prop {ReactNode} [label=Name] - Outlined field label. + * @prop {boolean} [readOnly=false] - When true, the input is read-only: non-editable, not tabbable, spellcheck off, and onChange is suppressed. + * @prop {boolean} [disabled=false] - Passed to TextField — disables the field. + * @prop {boolean} [required=false] - Passed to TextField — marks the field required. + * @prop {boolean} [fullWidth=true] - TextField fullWidth prop. + * @prop {boolean} [autoFocus=false] - TextField autoFocus prop. + * @prop {string} [autoComplete=off] - TextField autoComplete prop. + * @prop {'small' | 'medium'} [size] - TextField size prop. + * @prop {object} [slotProps] - MUI slotProps, merged (input/htmlInput are augmented for readOnly/spellCheck/tabIndex). + * @prop {object} [sx] - Extra styles passed to TextField. + * @prop {any} [otherProps] - Any remaining TextField props (helperText, etc.) are spread onto TextField. + * + * @behaviour Always renders `variant="outlined"` for a consistent look. + * @behaviour readOnly inputs get `spellCheck=false` and `tabIndex=-1`, so they are visually editable but not interactive. + * @behaviour Because it wraps TextField, styling is theme-consistent across the app. + */ +class SingleLineTextEdit extends Component { + render() { + const { + value, + onChange, + inputRef, + label = 'Name', + disabled = false, + readOnly = false, + autoFocus = false, + autoComplete = 'off', + size, + fullWidth = true, + required = false, + sx, + slotProps, + ...rest + } = this.props; + + return ( + { + if (!readOnly) onChange(event.target.value); + }} + autoFocus={autoFocus} + autoComplete={autoComplete} + slotProps={{ + ...slotProps, + input: { + readOnly, + ...slotProps?.input, + }, + htmlInput: { + spellCheck: false, + readOnly, + tabIndex: readOnly ? -1 : undefined, + ...slotProps?.htmlInput, + }, + }} + sx={sx} + {...rest} + /> + ); + } +} + +export default SingleLineTextEdit; diff --git a/src/dev/ComponentDocs.jsx b/src/dev/ComponentDocs.jsx new file mode 100644 index 0000000..ffb86a6 --- /dev/null +++ b/src/dev/ComponentDocs.jsx @@ -0,0 +1,178 @@ +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; +import Paper from '@mui/material/Paper'; +import Stack from '@mui/material/Stack'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Typography from '@mui/material/Typography'; +import { componentDocs } from 'virtual:component-docs'; + +/** + * Look up a component's docs from the registry extracted by the Vite plugin. + */ +export function getComponentDoc(name) { + return componentDocs[name]; +} + +/** + * Props documentation table. + * `props`: [{ name, type, required?, default?, description }] + */ +export function PropsTable({ props }) { + return ( + + + + + Prop + Type + Default + Description + + + + {props.map((p) => ( + + + + + {p.name} + + {p.required ? ( + + ) : null} + + + + + {p.type} + + + + {p.default != null ? ( + + {p.default} + + ) : ( + '—' + )} + + + {p.description} + + + ))} + +
+
+ ); +} + +/** Behaviour notes rendered as a bulleted list. */ +export function BehaviourList({ items }) { + return ( + + {items.map((item, i) => ( + + {item} + + ))} + + ); +} + +/** + * Full documentation block for one component. Reads the component's docs from + * the registry extracted by the Vite plugin (from JSDoc in the source files). + * + * name — component name as tagged with `@component` in the source JSDoc + * + * Optionally accepts overrides for any field (e.g. `props`/`behaviour`) when a + * mock page wants to add context on top of the source docs. + */ +export function ComponentDocs({ name, ...overrides }) { + const doc = { ...getComponentDoc(name), ...overrides }; + const { importPath, defaultExport, description, props = [], behaviour = [] } = doc; + + const importLine = importPath + ? defaultExport + ? `import ${name} from '${importPath}'` + : `import { ${name} } from '${importPath}'` + : null; + + return ( + + + {name} + + {importLine ? ( + + {importLine} + + ) : null} + {description ? ( + + {description} + + ) : null} + {props.length > 0 ? ( + <> + + Props + + + + ) : null} + {behaviour.length > 0 ? ( + <> + + Behaviour + + + + ) : null} + + ); +} + +export default ComponentDocs; diff --git a/src/dev/DevApp.jsx b/src/dev/DevApp.jsx new file mode 100644 index 0000000..8275b51 --- /dev/null +++ b/src/dev/DevApp.jsx @@ -0,0 +1,55 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Typography from '@mui/material/Typography'; +import PanelsMockPage from './mock-pages/PanelsMockPage'; +import TilesMockPage from './mock-pages/TilesMockPage'; +import PillsMockPage from './mock-pages/PillsMockPage'; +import TextFieldsMockPage from './mock-pages/TextFieldsMockPage'; +import ListsMockPage from './mock-pages/ListsMockPage'; + +const PAGES = [ + { id: 'panels', label: 'Panels', node: }, + { id: 'lists', label: 'Listen', node: }, + { id: 'tiles', label: 'Tiles', node: }, + { id: 'pills', label: 'Pills', node: }, + { id: 'textfields', label: 'Text fields', node: }, +]; + +/** + * Dev harness shell. Each tab renders a "mock page" exercising the library's + * components with fake data — no backend, no redux. Future test cases will be + * written against the same mock pages. + */ +class DevApp extends Component { + constructor(props) { + super(props); + this.state = { tab: 'panels' }; + } + + render() { + const { tab } = this.state; + const page = PAGES.find((p) => p.id === tab); + + return ( + + + dashgui — component gallery + + this.setState({ tab: next })} + sx={{ mb: 3 }} + > + {PAGES.map((p) => ( + + ))} + + {page?.node} + + ); + } +} + +export { DevApp }; diff --git a/src/dev/main.jsx b/src/dev/main.jsx new file mode 100644 index 0000000..c4328ea --- /dev/null +++ b/src/dev/main.jsx @@ -0,0 +1,17 @@ +import { createRoot } from 'react-dom/client'; +import CssBaseline from '@mui/material/CssBaseline'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import { DevApp } from './DevApp'; + +// Standalone harness — mocks the consuming app's theme/bootstrap so the GUI +// primitives can be inspected without a backend. +const theme = createTheme({ + shape: { borderRadius: 4 }, +}); + +createRoot(document.getElementById('root')).render( + + + + , +); diff --git a/src/dev/mock-pages/ListsMockPage.jsx b/src/dev/mock-pages/ListsMockPage.jsx new file mode 100644 index 0000000..ab9fec1 --- /dev/null +++ b/src/dev/mock-pages/ListsMockPage.jsx @@ -0,0 +1,154 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import RecordList from '../../lists/RecordList'; +import { ComponentDocs } from '../ComponentDocs'; + +const MOCK_ROWS = [ + { + id: 1, + number: 'L-1001', + date: '2026-08-01', + party: 'Müller GmbH', + status: 'fertig', + }, + { + id: 2, + number: 'L-1002', + date: '2026-08-03', + party: 'Schmidt & Co', + status: 'offen', + }, + { + id: 3, + number: 'L-1003', + date: '2026-08-05', + party: 'Weber OHG', + status: 'offen', + }, +]; + +const COLUMNS = [ + { key: 'number', label: 'Nummer' }, + { key: 'date', label: 'Lieferdatum' }, + { key: 'party', label: 'Lieferant' }, + { + key: 'status', + label: 'Status', + render: (row) => ( + + ), + }, +]; + +/** + * Mock page exercising RecordList with fake in-memory rows — no storage + * manager, no router, no toaster. Mirrors the shape the four dashfront list + * pages share (Lieferungen / Angebote / Aufträge / Kunden). + */ +class ListsMockPage extends Component { + constructor(props) { + super(props); + this.state = { + status: 'ready', + rows: MOCK_ROWS, + log: [], + }; + } + + simulateLoading = () => { + this.setState({ status: 'loading' }); + setTimeout(() => this.setState({ status: 'ready' }), 800); + }; + + simulateFailure = () => { + this.setState({ status: 'failed' }); + }; + + handleCreate = () => { + this.log('create'); + }; + + handleEdit = (row) => { + this.log(`edit ${row.number}`); + }; + + handleDelete = (row) => { + this.log(`delete ${row.number}`); + this.setState((s) => ({ rows: s.rows.filter((r) => r.id !== row.id) })); + }; + + log(msg) { + this.setState((s) => ({ log: [msg, ...s.log].slice(0, 6) })); + } + + render() { + const { status, rows, log } = this.state; + const failedNode = status === 'failed' ? ( + + Mock-Fehlerzustand (hier würde BackendError stehen). + + ) : null; + + return ( + + + + + + + + `„${r.number}“ bearbeiten`} + getDeleteLabel={(r) => `„${r.number}“ löschen`} + /> + + {log.length > 0 ? ( + + + Aktionen: + + {log.map((entry, i) => ( + + • {entry} + + ))} + + ) : null} + + + + ); + } +} + +export default ListsMockPage; diff --git a/src/dev/mock-pages/PanelsMockPage.jsx b/src/dev/mock-pages/PanelsMockPage.jsx new file mode 100644 index 0000000..c8c3ab1 --- /dev/null +++ b/src/dev/mock-pages/PanelsMockPage.jsx @@ -0,0 +1,128 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { EditablePanelTile, PanelTile, PanelTileGrid } from '../../panels'; +import SingleLineTextEdit from '../../SingleLineTextEdit'; +import { ComponentDocs } from '../ComponentDocs'; + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Mock page exercising PanelTile / EditablePanelTile with fake in-memory data. + * `onError` is passed to simulate the consuming app's error snackbar. + */ +class PanelsMockPage extends Component { + constructor(props) { + super(props); + this.state = { + companyName: 'ACME GmbH', + productName: 'Erdnussbutter 350g', + lastSaved: null, + errors: [], + }; + } + + handleSave = async (next) => { + // Simulated latency so the save spinner is visible. + await sleep(600); + this.setState({ companyName: next, lastSaved: next }); + }; + + handleProductSave = async (next) => { + await sleep(600); + this.setState({ productName: next, lastSaved: next }); + }; + + // Mirrors the consuming app's notifyError: returns a normalized { message }. + handleError = (err) => { + const message = String(err?.message ?? err ?? 'Unbekannter Fehler'); + this.setState((prev) => ({ errors: [message, ...prev.errors].slice(0, 5) })); + return { message }; + }; + + render() { + const { companyName, productName, lastSaved, errors } = this.state; + + return ( + + + {companyName || '—'} + } + edit={({ draft, onChange, inputRef }) => ( + + )} + /> + + {productName || '—'} + } + edit={({ draft, onChange, inputRef }) => ( + + )} + /> + + + + PanelTile ohne action — read-only. + + + + + + + {lastSaved != null + ? `Zuletzt gespeichert: ${lastSaved}` + : 'Noch nichts gespeichert.'} + + + + {errors.length > 0 ? ( + + + Mock-Fehler (würde als Snackbar erscheinen): + + {errors.map((e, i) => ( + + • {e} + + ))} + + ) : null} + + + + + + + + ); + } +} + +export default PanelsMockPage; diff --git a/src/dev/mock-pages/PillsMockPage.jsx b/src/dev/mock-pages/PillsMockPage.jsx new file mode 100644 index 0000000..190f429 --- /dev/null +++ b/src/dev/mock-pages/PillsMockPage.jsx @@ -0,0 +1,55 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import ToggleOnIcon from '@mui/icons-material/ToggleOn'; +import { + ActionElevatedPill, + ToggleElevatedPill, +} from '../../ElevatedPill'; +import ElevatedPill from '../../ElevatedPill'; +import { ComponentDocs } from '../ComponentDocs'; + +class PillsMockPage extends Component { + constructor(props) { + super(props); + this.state = { enabled: true, active: true }; + } + + render() { + const { enabled, active } = this.state; + return ( + + + + Toggle: + + this.setState({ enabled: next })} + /> + + + + Action: + + this.setState((s) => ({ active: !s.active }))} + icon={} + /> + + + + + + + + + + ); + } +} + +export default PillsMockPage; diff --git a/src/dev/mock-pages/TextFieldsMockPage.jsx b/src/dev/mock-pages/TextFieldsMockPage.jsx new file mode 100644 index 0000000..9c64f32 --- /dev/null +++ b/src/dev/mock-pages/TextFieldsMockPage.jsx @@ -0,0 +1,48 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import SingleLineTextEdit from '../../SingleLineTextEdit'; +import { ComponentDocs } from '../ComponentDocs'; + +class TextFieldsMockPage extends Component { + constructor(props) { + super(props); + this.state = { name: '', note: 'Schreibgeschützt' }; + } + + render() { + const { name, note } = this.state; + return ( + + + this.setState({ name: next })} + helperText="Gemeinsames Einzeilen-Feld" + /> + {}} + /> + + Aktueller Wert: {JSON.stringify(name)} + + + + + + ); + } +} + +export default TextFieldsMockPage; diff --git a/src/dev/mock-pages/TilesMockPage.jsx b/src/dev/mock-pages/TilesMockPage.jsx new file mode 100644 index 0000000..eb7e2c1 --- /dev/null +++ b/src/dev/mock-pages/TilesMockPage.jsx @@ -0,0 +1,76 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { Tile, tileBaseSx, TILE_SIZE } from '../../tiles'; +import FolderIcon from '@mui/icons-material/FolderOutlined'; +import AddIcon from '@mui/icons-material/Add'; +import { ComponentDocs } from '../ComponentDocs'; + +class MockFolderTile extends Tile { + renderIcon() { + return ; + } +} + +class MockAddTile extends Tile { + renderIcon() { + return ; + } +} + +/** + * Mock page exercising the base Tile primitive with subclasses, mirroring how + * dashfront's ActionTile/CategoryTile/etc. extend it. + */ +class TilesMockPage extends Component { + constructor(props) { + super(props); + this.state = { clicks: 0 }; + } + + render() { + const { clicks } = this.state; + return ( + + + Klicks: {clicks} + + + this.setState((s) => ({ clicks: s.clicks + 1 }))} + /> + this.setState((s) => ({ clicks: s.clicks + 1 }))} + /> + + + + tileBaseSx direkt + + + + + + + + + ); + } +} + +export default TilesMockPage; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..1f01774 --- /dev/null +++ b/src/index.js @@ -0,0 +1,16 @@ +/** + * dashgui — reusable custom GUI class components. + * + * Source-only package; consumers import via the `exports` map in package.json. + * Sub-path exports keep the component groups addressable: + * + * import { PanelTile, EditablePanelTile } from 'dashgui/panels'; + * import { Tile } from 'dashgui/tiles'; + * import ElevatedPill from 'dashgui/ElevatedPill'; + * import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; + */ +export * from './panels'; +export * from './lists'; +export * from './tiles'; +export { default as ElevatedPill } from './ElevatedPill'; +export { default as SingleLineTextEdit } from './SingleLineTextEdit'; diff --git a/src/lists/RecordList.jsx b/src/lists/RecordList.jsx new file mode 100644 index 0000000..ba1a2a9 --- /dev/null +++ b/src/lists/RecordList.jsx @@ -0,0 +1,196 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import IconButton from '@mui/material/IconButton'; +import Stack from '@mui/material/Stack'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Typography from '@mui/material/Typography'; +import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; +import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; +import { PanelTile, PanelTileGrid } from '../panels'; + +/** + * Generic data-list GUI component — pure presentation, no data layer. + * + * Renders a PanelTileGrid > PanelTile with an optional create button, an empty + * state, and a table of rows with per-row edit/delete actions. Everything data + * related is injected via props, so this component never touches a storage + * manager, router, toaster, or backend error formatter. + * + * @component RecordList + * @import dashgui/lists + * + * @prop {'loading' | 'ready' | 'failed'} status - Controls which state renders: a centered spinner, the table, or the `error` node. + * @prop {object[]} rows - Row objects to render. Each row needs the `rowKey` field for the React key. + * @prop {Array<{key: string, label: ReactNode, render?: (row) => ReactNode, align?: string, padding?: string}>} columns - Column definitions. `key` is the row field (or used with `render`); `render(row)` customizes the cell; `align`/`padding` map to TableCell. + * @prop {ReactNode} [error] - Node rendered when `status === "failed"` (e.g. a formatted backend error). + * @prop {string} [rowKey=id] - Field used as the row identity / React key. + * @prop {string} [createLabel] - Label of the top-right create button. Omit to hide the button entirely. + * @prop {() => void} [onCreate] - Called when the create button is clicked. + * @prop {(row) => void} [onEdit] - Called when a row is clicked or its edit button is pressed. Omit to hide the edit button and row-click affordance. + * @prop {(row) => void} [onDelete] - Called when a row's delete button is pressed. Omit to hide the delete button. + * @prop {boolean} [deleteDisabled=false] - Disables the delete buttons — e.g. `busyId != null` while a delete is in flight. + * @prop {ReactNode} [emptyMessage] - Shown instead of the table when `rows` is empty. Omit to render nothing. + * @prop {string} [color] - Background color of the wrapping PanelTile. + * @prop {number | string} [minHeight=240] - Minimum height of the wrapping PanelTile. + * @prop {(row) => string} [getEditLabel] - aria-label for each edit button (defaults to `„“ bearbeiten`). + * @prop {(row) => string} [getDeleteLabel] - aria-label for each delete button (defaults to `„“ löschen`). + * @prop {object} [sx] - Extra styles passed to the wrapping PanelTile. + * + * @behaviour `status === "loading"` renders only a centered CircularProgress (no panel chrome). + * @behaviour `status === "failed"` renders only the `error` node — or nothing if `error` is omitted. + * @behaviour Clicking a row calls `onEdit(row)` (only when `onEdit` is provided); the action buttons stop propagation so they do not double-trigger the row click. + * @behaviour The actions column appears only if `onEdit` or `onDelete` is provided. + * @behaviour No data layer: this component never touches storage, routing, toasters, or backend error formatting. + */ +class RecordList extends Component { + handleRowClick(row) { + this.props.onEdit?.(row); + } + + renderLoading() { + return ( + + + + ); + } + + renderCreateButton() { + const { createLabel, onCreate } = this.props; + if (!createLabel) return null; + return ( + + + + ); + } + + renderEmpty() { + const { emptyMessage } = this.props; + if (!emptyMessage) return null; + return ( + + {emptyMessage} + + ); + } + + renderTable() { + const { + rows, + columns, + rowKey = 'id', + onEdit, + onDelete, + deleteDisabled, + getEditLabel, + getDeleteLabel, + } = this.props; + + const hasActions = Boolean(onEdit || onDelete); + + return ( + + + + + {columns.map((col) => ( + + {col.label} + + ))} + {hasActions ? : null} + + + + {rows.map((row) => { + const id = row[rowKey]; + return ( + this.handleRowClick(row)} + > + {columns.map((col) => ( + + {col.render ? col.render(row) : row[col.key]} + + ))} + {hasActions ? ( + e.stopPropagation()} + > + {onEdit ? ( + onEdit(row)} + > + + + ) : null} + {onDelete ? ( + onDelete(row)} + > + + + ) : null} + + ) : null} + + ); + })} + +
+
+ ); + } + + render() { + const { status, error, rows, color, minHeight = 240, sx } = this.props; + + if (status === 'loading') return this.renderLoading(); + if (status === 'failed') return error ?? null; + + return ( + + + + {this.renderCreateButton()} + {rows.length === 0 ? this.renderEmpty() : this.renderTable()} + + + + ); + } +} + +export default RecordList; diff --git a/src/lists/index.js b/src/lists/index.js new file mode 100644 index 0000000..022d2c9 --- /dev/null +++ b/src/lists/index.js @@ -0,0 +1 @@ +export { default as RecordList } from './RecordList'; diff --git a/src/panels/EditablePanelTile.jsx b/src/panels/EditablePanelTile.jsx new file mode 100644 index 0000000..4f7dbf1 --- /dev/null +++ b/src/panels/EditablePanelTile.jsx @@ -0,0 +1,300 @@ +import { Component, createRef } from 'react'; +import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined'; +import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; +import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; +import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import IconButton from '@mui/material/IconButton'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import { injectMuiTheme } from './injectMuiTheme'; +import { panelTilePaperSx, TILE_ACTION_PADDING, tileActionIconSx, tileActionSx } from './panelTileLayout'; + +function valuesEqual(a, b) { + return JSON.stringify(a) === JSON.stringify(b); +} + +/** + * Panel tile with centralized inline-edit chrome: edit/save/cancel actions, + * dirty detection, keyboard handling, and an injected error surface. Pass + * `onSave` to enable editing; omit it for a read-only tile. + * + * @component EditablePanelTile + * @import dashgui/panels + * + * @prop {any} [value] - The saved value shown in read mode and used as the baseline for dirty detection. + * @prop {ReactNode} [title] - Overline label shown above the body. + * @prop {ReactNode} [display] - Node rendered while not editing. + * @prop {ReactNode | function} [edit] - Node rendered while editing, or a render function receiving `{ draft, onChange, inputRef }`. + * @prop {(draft) => Promise} [onSave] - Async save handler. The tile exits edit mode only after this resolves; if it rejects, the error is surfaced via `onError` and shown inline. + * @prop {(err) => void} [onError] - Injected error surface (e.g. snackbar). Called when saving rejects. Omit to swallow save errors silently. + * @prop {(draft, saved) => boolean} [isDirty] - Optional custom dirty check. Defaults to JSON deep equality between draft and saved value. + * @prop {(draft, saved) => boolean} [canSave] - Optional extra validation gate on top of the default rules (dirty, non-null draft, not already saving). + * @prop {number} [wide=1] - Grid column span (1–3), same semantics as PanelTile. + * @prop {string} [color] - Background color for the paper. + * @prop {number | string} [minHeight=120] - Minimum height of the paper. + * @prop {string} [contentAlignItems=center] - CSS `align-items` for the body content row (e.g. "flex-start"). + * @prop {function} [overlay] - Optional `({ editing, draft, onChange }) => ReactNode` rendered above the title, e.g. a full-tile overlay while editing. + * @prop {object} [sx] - Extra styles merged into the paper. + * + * @behaviour Clicking the edit icon clones the saved value into a draft and focuses the first input (via `inputRef`). + * @behaviour Enter (without Shift) saves when allowed; Escape cancels. Keys are handled on the body row while editing. + * @behaviour A dirty tile shows a rose inset ring; an editing-but-clean tile shows a slate ring. + * @behaviour While saving, the save button becomes a spinner and both buttons are disabled. + * @behaviour On save failure the tile stays in edit mode, shows an inline error Alert, and forwards the error to `onError`. + * @behaviour Read-only tiles (no `onSave`) render no action buttons at all. + */ +class EditablePanelTileBase extends Component { + constructor(props) { + super(props); + this.inputRef = createRef(); + this.state = { + editing: false, + draft: null, + saving: false, + error: null, + }; + } + + componentDidUpdate(_prevProps, prevState) { + if (!prevState.editing && this.state.editing) { + this.inputRef.current?.focus(); + } + } + + getSavedValue() { + return this.props.value; + } + + isDirty() { + const { draft, editing } = this.state; + if (!editing || draft == null) return false; + if (this.props.isDirty) { + return this.props.isDirty(draft, this.getSavedValue()); + } + return !valuesEqual(draft, this.getSavedValue()); + } + + canSave() { + const { draft, saving } = this.state; + if (saving || !this.isDirty() || draft == null) return false; + if (this.props.canSave) { + return this.props.canSave(draft, this.getSavedValue()); + } + return true; + } + + handleStartEdit = (event) => { + event.stopPropagation(); + this.setState({ + editing: true, + draft: structuredClone(this.getSavedValue()), + error: null, + }); + }; + + handleCancel = (event) => { + event?.stopPropagation(); + this.setState({ + editing: false, + draft: null, + error: null, + }); + }; + + handleDraftChange = (nextDraft) => { + this.setState({ draft: nextDraft, error: null }); + }; + + handleEditKeyDown = (event) => { + if (event.key === 'Escape') { + event.preventDefault(); + event.stopPropagation(); + this.handleCancel(); + return; + } + + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + event.stopPropagation(); + if (this.canSave()) { + void this.handleSave(event); + } + } + }; + + handleSave = async (event) => { + event?.stopPropagation(); + if (!this.canSave()) return; + + const { onSave, onError } = this.props; + const { draft } = this.state; + + try { + await onSave(draft); + this.setState({ editing: false, draft: null, saving: false, error: null }); + } catch (err) { + const message = String(err?.message ?? err ?? 'Unbekannter Fehler'); + onError?.(err); + this.setState({ saving: false, error: message }); + } + }; + + renderTitle() { + const { title, onSave } = this.props; + if (!title) return null; + return ( + + {title} + + ); + } + + renderActions() { + const { onSave } = this.props; + if (!onSave) return null; + + const { editing, saving } = this.state; + const iconSx = tileActionIconSx; + + if (!editing) { + return ( + <> + + + + + + + + ); + } + + return ( + <> + + {saving ? ( + + ) : ( + + )} + + + + + + ); + } + + renderBody() { + const { display, edit } = this.props; + const { editing, draft } = this.state; + + if (editing && draft != null) { + if (typeof edit === 'function') { + return edit({ + draft, + onChange: this.handleDraftChange, + inputRef: this.inputRef, + }); + } + return edit; + } + return display; + } + + renderOverlayNode() { + const { overlay } = this.props; + const { editing, draft } = this.state; + if (typeof overlay !== 'function') return null; + return overlay({ + editing, + draft, + onChange: this.handleDraftChange, + }); + } + + render() { + const { + onSave, + theme, + wide = 1, + sx, + minHeight = 120, + color, + contentAlignItems = 'center', + } = this.props; + const { editing, error } = this.state; + const pending = this.isDirty(); + const actionPadding = onSave ? TILE_ACTION_PADDING : 0; + + return ( + + {this.renderActions()} + {this.renderOverlayNode()} + {this.renderTitle()} + + {this.renderBody()} + + {error ? ( + + {error} + + ) : null} + + ); + } +} + +export const EditablePanelTile = injectMuiTheme(EditablePanelTileBase); +export default EditablePanelTile; diff --git a/src/panels/PanelTile.jsx b/src/panels/PanelTile.jsx new file mode 100644 index 0000000..4c58dfd --- /dev/null +++ b/src/panels/PanelTile.jsx @@ -0,0 +1,156 @@ +import { Component } from 'react'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import IconButton from '@mui/material/IconButton'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; +import { injectMuiTheme } from './injectMuiTheme'; +import { + GRID_COLUMNS, + TILE_ACTION_PADDING, + panelTilePaperSx, + tileActionIconSx, + tileActionSx, +} from './panelTileLayout'; + +/** + * Read-only content panel with a title and an optional corner action (icon + * button with busy spinner). It is a styled MUI Paper; children fill the body. + * + * @component PanelTile + * @import dashgui/panels + * + * @prop {ReactNode} [title] - Overline label shown above the body. Omit to render no header. + * @prop {ReactNode} children - Body content filling the tile between title and edges. + * @prop {number} [wide=1] - Grid column span (1–3). On sm the span is capped at 2; on xs tiles always span the full row. + * @prop {string} [color] - Background color for the paper (e.g. a hex like "#fef3c7"). + * @prop {number | string} [minHeight=120] - Minimum height of the paper in px. + * @prop {number} [elevation=4] - MUI Paper elevation (shadow depth). + * @prop {object} [sx] - Extra styles merged into the paper (last, so they win). + * @prop {object} [action] - Optional corner action `{ label, icon, onClick, busy? }`. While `busy` is true the icon is replaced by a spinner and clicks are disabled. + * + * @behaviour The action button sits absolutely in the top-right corner. + * @behaviour A hidden placeholder button reserves space so the title never shifts when an action appears. + * @behaviour Title, action, and children stack vertically; children flex to fill remaining height. + */ +class PanelTileBase extends Component { + renderTitle() { + const { title, action } = this.props; + if (!title) return null; + return ( + + {title} + + ); + } + + renderAction() { + const { action } = this.props; + if (!action) return null; + + const { label, icon: Icon, onClick, busy } = action; + + return ( + <> + + + + + {busy ? ( + + ) : ( + + )} + + + ); + } + + render() { + const { + theme, + wide = 1, + children, + sx, + minHeight = 120, + color, + action, + elevation = 4, + } = this.props; + const actionPadding = action ? TILE_ACTION_PADDING : 0; + + return ( + + {this.renderAction()} + {this.renderTitle()} + {children} + + ); + } +} + +export const PanelTile = injectMuiTheme(PanelTileBase); + +/** + * Responsive grid container for panel tiles. One column on xs, two on sm, + * GRID_COLUMNS (4) on md and up. + * + * @component PanelTileGrid + * @import dashgui/panels + * + * @prop {ReactNode} children - The tiles to lay out (typically PanelTile / EditablePanelTile). + * @prop {object} [sx] - Extra styles merged into the grid container. + * + * @behaviour Uses a CSS grid with `gap: 2` (theme spacing). + * @behaviour Children position themselves via the `wide` prop of each tile. + */ +export function PanelTileGrid({ children, sx }) { + return ( + + {children} + + ); +} + +export { GRID_COLUMNS }; diff --git a/src/panels/index.js b/src/panels/index.js new file mode 100644 index 0000000..f24644f --- /dev/null +++ b/src/panels/index.js @@ -0,0 +1,4 @@ +export { PanelTile, PanelTileGrid, GRID_COLUMNS } from './PanelTile'; +export { EditablePanelTile } from './EditablePanelTile'; +export { injectMuiTheme } from './injectMuiTheme'; +export { PENDING_EDIT_BORDER } from './panelTileLayout'; diff --git a/src/panels/injectMuiTheme.jsx b/src/panels/injectMuiTheme.jsx new file mode 100644 index 0000000..6ad40fa --- /dev/null +++ b/src/panels/injectMuiTheme.jsx @@ -0,0 +1,13 @@ +import { useTheme } from '@mui/material/styles'; + +/** Injects MUI theme into class components (replaces removed withTheme HOC). */ +export function injectMuiTheme(Wrapped) { + function ComponentWithTheme(props) { + const theme = useTheme(); + return ; + } + + const name = Wrapped.displayName || Wrapped.name || 'Component'; + ComponentWithTheme.displayName = `injectMuiTheme(${name})`; + return ComponentWithTheme; +} diff --git a/src/panels/panelTileLayout.js b/src/panels/panelTileLayout.js new file mode 100644 index 0000000..980e5e2 --- /dev/null +++ b/src/panels/panelTileLayout.js @@ -0,0 +1,74 @@ +const GRID_COLUMNS = 4; + +export const PENDING_EDIT_BORDER = '#e11d48'; +export const EDITING_TILE_BORDER = '#94a3b8'; + +function clampWide(wide) { + return Math.min(Math.max(wide, 1), 3); +} + +export function responsiveColumnSpan(wide) { + const span = clampWide(wide); + return { + xs: '1 / -1', + sm: span >= 2 ? 'span 2' : 'span 1', + md: `span ${span}`, + }; +} + +export const PENDING_EDIT_RING_PX = 4; +export const TILE_ACTION_PADDING = 12; + +export const tileActionSx = { + position: 'absolute', + top: 4, + color: 'text.secondary', +}; + +export const tileActionIconSx = { + fontSize: 28, +}; + +export const PANEL_TILE_BORDER_RADIUS = 4; + +function tileRingBoxShadow(ringColor, elevationShadow) { + const inset = `inset 0 0 0 ${PENDING_EDIT_RING_PX}px ${ringColor}`; + return elevationShadow ? `${inset}, ${elevationShadow}` : inset; +} + +export function panelTilePaperSx({ + wide, + sx, + minHeight = 120, + editing = false, + pending = false, + color, + theme, +}) { + const elevationShadow = theme?.shadows?.[4]; + const ringColor = pending + ? PENDING_EDIT_BORDER + : editing + ? EDITING_TILE_BORDER + : null; + + return { + gridColumn: responsiveColumnSpan(wide), + position: 'relative', + p: 2, + minHeight, + display: 'flex', + flexDirection: 'column', + border: 'none', + borderRadius: PANEL_TILE_BORDER_RADIUS, + ...(color ? { bgcolor: color } : {}), + ...(ringColor + ? { + boxShadow: tileRingBoxShadow(ringColor, elevationShadow), + } + : {}), + ...sx, + }; +} + +export { GRID_COLUMNS }; diff --git a/src/tiles/Tile.jsx b/src/tiles/Tile.jsx new file mode 100644 index 0000000..295d7c0 --- /dev/null +++ b/src/tiles/Tile.jsx @@ -0,0 +1,128 @@ +import { Component } from 'react'; +import ButtonBase from '@mui/material/ButtonBase'; +import Typography from '@mui/material/Typography'; + +/** + * Square side length (px) used as the basis for the tile grid in consuming + * apps. + * + * @component TILE_SIZE + * @import dashgui/tiles + * + * @behaviour Constant (140) exported for building grids: `repeat(auto-fill, minmax(TILE_SIZEpx, 1fr))`. + */ +export const TILE_SIZE = 140; + +/** Matches panel tile rounding (theme.shape.borderRadius × 4 → typically 16px). */ +export const TILE_BORDER_RADIUS = 4; + +/** + * Shared tile styling, exported for reuse — lets consumers render a tile-like + * box without extending the Tile class (see the blue box in the gallery). + * + * @component tileBaseSx + * @import dashgui/tiles + * + * @behaviour Sets a square aspect ratio, centered flex column layout, rounded corners, background.paper fill, shadow 3, and hover states (background hover + shadow 6). + * @behaviour Not tied to a click handler — add your own onClick when used directly. Spread additional styles after it to override (e.g. bgcolor, color). + */ +export const tileBaseSx = { + aspectRatio: '1 / 1', + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: 1, + p: 1.5, + cursor: 'pointer', + userSelect: 'none', + textAlign: 'center', + border: 'none', + borderRadius: TILE_BORDER_RADIUS, + overflow: 'hidden', + bgcolor: 'background.paper', + boxShadow: 3, + transition: 'box-shadow 0.15s, background-color 0.15s', + '&:hover': { + bgcolor: 'action.hover', + boxShadow: 6, + }, +}; + +/** + * Base square tile (ButtonBase) meant to be subclassed — subclasses override + * the render hooks (e.g. `renderIcon`) and pass label/secondary text via props. + * Renders the shared `tileBaseSx` styling. + * + * @component Tile + * @import dashgui/tiles + * + * @prop {ReactNode} [label] - Primary text shown under the icon. + * @prop {ReactNode} [secondary] - Small secondary caption shown under the label. + * @prop {() => void} [onClick] - Called when the tile is clicked (disabled tiles are inert). + * @prop {boolean} [disabled=false] - Fades the tile to 50% opacity and disables pointer events. + * @prop {object} [sx] - Extra styles merged on top of `tileBaseSx` (win over it). + * + * @behaviour Renders inside a `ButtonBase` with `type="button"` and the shared `tileBaseSx` square look (aspect-ratio 1:1, rounded, shadow, hover feedback). + * @behaviour Subclasses override `renderIcon()`, `renderLabel()`, `renderSecondary()`, or `renderExtra()` to compose the tile body. + * @behaviour `TILE_SIZE` (140) is the basis for sizing the containing grid in the consuming app. + */ +export class Tile extends Component { + handleClick = () => { + this.props.onClick?.(); + }; + + renderIcon() { + return null; + } + + renderLabel() { + const { label } = this.props; + if (!label) return null; + return ( + + {label} + + ); + } + + renderSecondary() { + const { secondary } = this.props; + if (!secondary) return null; + return ( + + {secondary} + + ); + } + + renderExtra() { + return null; + } + + render() { + const { sx, disabled } = this.props; + return ( + + {this.renderIcon()} + {this.renderLabel()} + {this.renderSecondary()} + {this.renderExtra()} + + ); + } +} diff --git a/src/tiles/index.js b/src/tiles/index.js new file mode 100644 index 0000000..c091caa --- /dev/null +++ b/src/tiles/index.js @@ -0,0 +1 @@ +export { TILE_SIZE, Tile, tileBaseSx } from './Tile'; diff --git a/vite-plugin-component-docs.js b/vite-plugin-component-docs.js new file mode 100644 index 0000000..18ee22b --- /dev/null +++ b/vite-plugin-component-docs.js @@ -0,0 +1,185 @@ +import { parse } from '@babel/parser'; +import fs from 'node:fs'; +import path from 'node:path'; + +/** + * Vite plugin — "component docs" virtual module. + * + * Reads JSDoc blocks tagged `@component` from the source files listed in + * SOURCES, parses them into a registry, and exposes it to the dev gallery as + * `import { componentDocs } from 'virtual:component-docs'`. + * + * The JSDoc format (kept next to the implementation): + * + * Block comment: "/**" ... "*​/" (see parseDoc for tag handling) + * + * @component PanelTile ← component name used in the gallery + * @import dashgui/panels ← sub-path export for the import snippet + * @defaultExport ← optional: `import X from '…'` (default export) + * + * @prop {type} [name=default] - Description ← optional props use [name=default] + * @prop {type} requiredProp - Description ← props without brackets are required + * @prop {type} [optionalProp] - Description ← optional without a default + * + * @behaviour A behaviour note. + * @behaviour Another behaviour note. + */ +const VIRTUAL_ID = 'virtual:component-docs'; +const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_ID; + +const SOURCES = [ + { file: 'src/panels/PanelTile.jsx', importPath: 'dashgui/panels' }, + { file: 'src/panels/EditablePanelTile.jsx', importPath: 'dashgui/panels' }, + { file: 'src/ElevatedPill.jsx', importPath: 'dashgui/ElevatedPill' }, + { file: 'src/SingleLineTextEdit.jsx', importPath: 'dashgui/SingleLineTextEdit' }, + { file: 'src/tiles/Tile.jsx', importPath: 'dashgui/tiles' }, + { file: 'src/lists/RecordList.jsx', importPath: 'dashgui/lists' }, +]; + +/** Parse a single `@prop {type} [name=default] - description` line. */ +function parseProp(text) { + const typeMatch = text.match(/^\{(.*?)\}\s+(.*)$/s); + const type = typeMatch ? typeMatch[1].trim() : ''; + const rest = typeMatch ? typeMatch[2].trim() : text.trim(); + + let name; + let defaultValue; + let required = true; + let description = ''; + + const optionalMatch = rest.match(/^\[([^\]=]*)(?:=([^\]]*))?\]\s*(?:-\s*)?(.*)$/s); + if (optionalMatch) { + required = false; + name = optionalMatch[1].trim(); + defaultValue = optionalMatch[2] !== undefined ? optionalMatch[2].trim() : undefined; + description = optionalMatch[3].trim(); + } else { + const reqMatch = rest.match(/^([^\s-]+)(?:\s*-\s*)?(.*)$/s); + if (reqMatch) { + name = reqMatch[1].trim(); + description = reqMatch[2].trim(); + } + } + + return { name, type, required, default: defaultValue, description }; +} + +/** Parse a `/** … *​/` comment value (as provided by @babel/parser). */ +function parseDoc(commentValue, fallback) { + const lines = commentValue + .split('\n') + .map((l) => l.replace(/^\s*\*\s?/, '').trim()) + .filter((l, idx, arr) => !(l === '' && (idx === 0 || idx === arr.length - 1 || arr[idx - 1] === ''))); + + const descLines = []; + const props = []; + const behaviour = []; + let name = fallback.name || ''; + let importPath = fallback.importPath || ''; + let defaultExport = false; + + let i = 0; + while (i < lines.length && !lines[i].startsWith('@')) { + if (lines[i] !== '') descLines.push(lines[i]); + i++; + } + + for (; i < lines.length; i++) { + const m = lines[i].match(/^@(\w+)\s*(.*)$/); + if (!m) continue; + const [, tag, rest] = m; + switch (tag) { + case 'component': + name = rest.trim(); + break; + case 'import': + importPath = rest.trim(); + break; + case 'defaultExport': + defaultExport = true; + break; + case 'prop': + props.push(parseProp(rest.trim())); + break; + case 'behaviour': + behaviour.push(rest.trim()); + break; + default: + break; + } + } + + return { name, importPath, defaultExport, description: descLines.join(' '), props, behaviour }; +} + +/** Walk a source file and collect every `@component`-tagged JSDoc block. */ +function extractFromFile(root, source) { + const fullPath = path.join(root, source.file); + let code; + try { + code = fs.readFileSync(fullPath, 'utf8'); + } catch (err) { + console.warn(`[component-docs] cannot read ${source.file}: ${err.message}`); + return {}; + } + + let ast; + try { + ast = parse(code, { + sourceType: 'module', + plugins: ['jsx', 'classProperties'], + errorRecovery: true, + }); + } catch (err) { + console.warn(`[component-docs] cannot parse ${source.file}: ${err.message}`); + return {}; + } + + const found = {}; + for (const stmt of ast.program.body) { + const comments = stmt.leadingComments || []; + const doc = comments[comments.length - 1]; + if (!doc || doc.type !== 'CommentBlock' || !doc.value.includes('@component')) continue; + + const parsed = parseDoc(doc.value, { name: '', importPath: source.importPath }); + if (!parsed.name) continue; + found[parsed.name] = parsed; + } + return found; +} + +function buildRegistry(root) { + const registry = {}; + for (const source of SOURCES) { + Object.assign(registry, extractFromFile(root, source)); + } + return registry; +} + +export function componentDocsPlugin() { + const documentedFiles = SOURCES.map((s) => s.file); + + return { + name: 'component-docs', + enforce: 'pre', + + resolveId(id) { + if (id === VIRTUAL_ID) return RESOLVED_VIRTUAL_ID; + return null; + }, + + load(id) { + if (id !== RESOLVED_VIRTUAL_ID) return null; + const registry = buildRegistry(process.cwd()); + return `export const componentDocs = ${JSON.stringify(registry)};`; + }, + + handleHotUpdate(ctx) { + const { file, server } = ctx; + if (!documentedFiles.some((f) => file.endsWith(f))) return; + const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ID); + if (mod) server.moduleGraph.invalidateModule(mod); + server.ws.send({ type: 'full-reload' }); + }, + }; +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..c13a5f6 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { componentDocsPlugin } from './vite-plugin-component-docs.js'; + +// Library dev server — renders the mock pages under /dev so components can be +// inspected in isolation, and doubles as a Vite config for consumers that want +// to preview this package standalone. +export default defineConfig({ + plugins: [react(), componentDocsPlugin()], + server: { + port: 5199, + }, + build: { + // This build is only used for the standalone dev/preview harness. + // Consumers (dashfront) import source via the package `exports` map. + outDir: 'dist', + }, +});