diff --git a/CHANGELOG.md b/CHANGELOG.md index a64014b7..d59ddc4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.0] - 2026-02-20 + +## Added +- Search for `Select` and `MultiSelect` +- Type ahead support for `Select` and `MultiSelect`npm +- `Combobox` component +- `FilterList` component for dynamically choosing and setting filters +- `useSelectState`, `useMultiSelectState`, `useCombobox`, `useSingleSelection`, `useMultiSelection` +- `useTypeAheadSearch` for getting the value of a timed type ahead search + +## Changed +- `useSearch` to require less parameter and only do a simple search and caching the result + +## Fixed +- imports in `TimePicker` and `DateTimeInput` + +## Security +- update packages + ## [0.8.12] - 2026-02-15 ### Fixed diff --git a/locales/de-DE.arb b/locales/de-DE.arb index 7041e18f..35aede0c 100644 --- a/locales/de-DE.arb +++ b/locales/de-DE.arb @@ -1,5 +1,6 @@ { "add": "Hinzufügen", + "addFilter": "Filter hinzufügen", "all": "Alle", "apply": "Anwenden", "back": "Zurück", @@ -30,10 +31,12 @@ "discardChanges": "Änderungen Verwerfen", "done": "Fertig", "edit": "Bearbeiten", + "editFilter": "Filter bearbeiten", "enterText": "Text hier eingeben", "error": "Fehler", "errorOccurred": "Ein Fehler ist aufgetreten", "exit": "Beenden", + "filterOptions": "Optionen filtern", "fieldRequiredError": "Dieses Feld ist erforderlich.", "first": "Erste", "previous": "Vorherige", @@ -51,16 +54,98 @@ "no": "Nein", "none": "Nichts", "nothingFound": "Nichts gefunden", + "nResultsFound": "{count, plural, =1{# Ergebnis gefunden} other{# Ergebnisse gefunden}}", + "@nResultsFound": { + "placeholders": { + "count": { "type": "number" } + } + }, "of": "von", "optional": "Optional", "pleaseWait": "Bitte warten...", "previous": "Vorherige", "remove": "Entfernen", + "removeFilter": "Filter entfernen", "required": "Erforderlich", "reset": "Zurücksetzen", + "rBetween": "Zwischen {value1} und {value2}", + "@rBetween": { + "placeholders": { + "value1": {}, + "value2": {} + } + }, + "rContains": "Enthält {value}", + "@rContains": { + "placeholders": { + "value": {} + } + }, + "rEndsWith": "Endet mit {value}", + "@rEndsWith": { + "placeholders": { + "value": {} + } + }, + "rEquals": "Gleich {value}", + "@rEquals": { + "placeholders": { + "value": {} + } + }, + "rGreaterThan": "Größer als {value}", + "@rGreaterThan": { + "placeholders": { + "value": {} + } + }, + "rGreaterThanOrEqual": "Größer oder gleich {value}", + "@rGreaterThanOrEqual": { + "placeholders": { + "value": {} + } + }, + "rLessThan": "Kleiner als {value}", + "@rLessThan": { + "placeholders": { + "value": {} + } + }, + "rLessThanOrEqual": "Kleiner oder gleich {value}", + "@rLessThanOrEqual": { + "placeholders": { + "value": {} + } + }, + "rNotBetween": "Nicht zwischen {value1} und {value2}", + "@rNotBetween": { + "placeholders": { + "value1": {}, + "value2": {} + } + }, + "rNotContains": "Enthält nicht {value}", + "@rNotContains": { + "placeholders": { + "value": {} + } + }, + "rNotEquals": "Nicht gleich {value}", + "@rNotEquals": { + "placeholders": { + "value": {} + } + }, + "rStartsWith": "Beginnt mit {value}", + "@rStartsWith": { + "placeholders": { + "value": {} + } + }, "save": "Speichern", "saved": "Gespeichert", "search": "Suche", + "searchResults": "Suchergebnisse", "select": "Select", "selection": "Auswahl", "selectOption": "Option auswählen", @@ -137,6 +222,8 @@ "invalidEmail": "Die E-Mail ist ungültig.", "isFalse": "Ist falsch", "isTrue": "Ist wahr", + "isUndefined": "Ist undefiniert", + "isNotUndefined": "Ist definiert", "lessThan": "Kleiner als", "lessThanOrEqual": "Kleiner oder gleich", "after": "Nach", @@ -218,6 +305,7 @@ "showColumn": "Spalte einblenden", "pinned": "Angeheftet", "unpin": "Loslösen", + "unknown": "Unbekannt", "pinLeft": "Links anheften", "pinRight": "Rechts anheften", "changeVisibility": "Sichtbarkeit ändern", diff --git a/locales/en-US.arb b/locales/en-US.arb index bcfbc0af..83d344e4 100644 --- a/locales/en-US.arb +++ b/locales/en-US.arb @@ -1,5 +1,6 @@ { "add": "Add", + "addFilter": "Add filter", "all": "All", "apply": "Apply", "back": "Back", @@ -31,10 +32,12 @@ "discardChanges": "Discard Changes", "done": "Done", "edit": "Edit", + "editFilter": "Edit filter", "enterText": "Enter text here", "error": "Error", "errorOccurred": "An error occurred", "exit": "Exit", + "filterOptions": "Filter options", "fieldRequiredError": "This field is required.", "first": "First", "previous": "Previous", @@ -52,16 +55,98 @@ "no": "No", "none": "None", "nothingFound": "Nothing found", + "nResultsFound": "{count, plural, =1{# result found} other{# results found}}", + "@nResultsFound": { + "placeholders": { + "count": { "type": "number" } + } + }, "of": "of", "optional": "Optional", "pleaseWait": "Please wait...", "previous": "Previous", "remove": "Remove", + "removeFilter": "Remove filter", "required": "Required", "reset": "Reset", + "rBetween": "Between {value1} and {value2}", + "@rBetween": { + "placeholders": { + "value1": {}, + "value2": {} + } + }, + "rContains": "Contains {value}", + "@rContains": { + "placeholders": { + "value": {} + } + }, + "rEndsWith": "Ends with {value}", + "@rEndsWith": { + "placeholders": { + "value": {} + } + }, + "rEquals": "Equals {value}", + "@rEquals": { + "placeholders": { + "value": {} + } + }, + "rGreaterThan": "Greater than {value}", + "@rGreaterThan": { + "placeholders": { + "value": {} + } + }, + "rGreaterThanOrEqual": "Greater than or equal {value}", + "@rGreaterThanOrEqual": { + "placeholders": { + "value": {} + } + }, + "rLessThan": "Less than {value}", + "@rLessThan": { + "placeholders": { + "value": {} + } + }, + "rLessThanOrEqual": "Less than or equal {value}", + "@rLessThanOrEqual": { + "placeholders": { + "value": {} + } + }, + "rNotBetween": "Not between {value1} and {value2}", + "@rNotBetween": { + "placeholders": { + "value1": {}, + "value2": {} + } + }, + "rNotContains": "Not contains {value}", + "@rNotContains": { + "placeholders": { + "value": {} + } + }, + "rNotEquals": "Not equals {value}", + "@rNotEquals": { + "placeholders": { + "value": {} + } + }, + "rStartsWith": "Starts with {value}", + "@rStartsWith": { + "placeholders": { + "value": {} + } + }, "save": "Save", "saved": "Saved", "search": "Search", + "searchResults": "Search results", "select": "Select", "selection": "Selection", "selectOption": "Select an option", @@ -138,6 +223,8 @@ "invalidEmail": "The email is not valid.", "isFalse": "Is false", "isTrue": "Is true", + "isUndefined": "Is undefined", + "isNotUndefined": "Is defined", "lessThan": "Less than", "lessThanOrEqual": "Less than or equal", "after": "After", @@ -219,6 +306,7 @@ "showColumn": "Show column", "pinned": "Pinned", "unpin": "Unpin", + "unknown": "Unknown", "pinLeft": "Pin to left", "pinRight": "Pin to right", "changeVisibility": "Change visibility", diff --git a/package-lock.json b/package-lock.json index dc540d4c..d5143c3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "@helpwave/hightide", - "version": "0.8.8", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@helpwave/hightide", - "version": "0.8.8", + "version": "0.9.0", "license": "MPL-2.0", "dependencies": { "@helpwave/internationalization": "0.4.0", + "@radix-ui/react-slot": "1.2.4", "@tailwindcss/cli": "4.1.18", "@tanstack/react-table": "8.21.3", "clsx": "2.1.1", @@ -28,9 +29,9 @@ "@babel/preset-typescript": "7.26.0", "@faker-js/faker": "10.1.0", "@helpwave/eslint-config": "0.0.11", - "@storybook/addon-docs": "10.2.8", - "@storybook/addon-links": "10.2.8", - "@storybook/nextjs": "^10.2.8", + "@storybook/addon-docs": "10.2.10", + "@storybook/addon-links": "10.2.10", + "@storybook/nextjs": "10.2.10", "@tailwindcss/postcss": "4.1.18", "@types/jest": "30.0.0", "@types/node": "20.17.10", @@ -40,12 +41,12 @@ "@vitest/mocker": "4.0.16", "autoprefixer": "10.4.23", "eslint": "9.31.0", - "eslint-plugin-storybook": "10.2.8", + "eslint-plugin-storybook": "10.2.10", "jest": "30.2.0", "postcss": "8.5.6", - "storybook": "10.2.8", - "ts-jest": "29.4.5", - "tsup": "8.5.0", + "storybook": "10.2.10", + "ts-jest": "29.4.6", + "tsup": "8.5.1", "typescript": "5.7.2", "webpack": "5.105.2" } @@ -115,7 +116,6 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -2532,44 +2532,20 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/config-helpers": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", @@ -2617,34 +2593,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -2712,6 +2664,189 @@ "typescript-eslint": "^8.32.1" } }, + "node_modules/@helpwave/eslint-config/node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@helpwave/eslint-config/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@helpwave/eslint-config/node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@helpwave/eslint-config/node_modules/eslint": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/@helpwave/eslint-config/node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/@helpwave/eslint-config/node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/@helpwave/eslint-config/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@helpwave/eslint-config/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.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/@helpwave/internationalization": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@helpwave/internationalization/-/internationalization-0.4.0.tgz", @@ -2780,6 +2915,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -2797,6 +2933,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2820,6 +2957,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2843,6 +2981,7 @@ "os": [ "darwin" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2860,6 +2999,7 @@ "os": [ "darwin" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2877,6 +3017,7 @@ "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2894,6 +3035,7 @@ "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2911,6 +3053,7 @@ "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2928,6 +3071,7 @@ "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2945,6 +3089,7 @@ "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2962,6 +3107,7 @@ "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2979,6 +3125,7 @@ "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2996,6 +3143,7 @@ "os": [ "linux" ], + "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -3013,6 +3161,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3036,6 +3185,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3059,6 +3209,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3082,6 +3233,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3105,6 +3257,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3128,6 +3281,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3151,6 +3305,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3174,6 +3329,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3194,6 +3350,7 @@ "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/runtime": "^1.7.0" }, @@ -3217,6 +3374,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3237,6 +3395,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3257,6 +3416,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3282,36 +3442,121 @@ "node": ">=12" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", @@ -3465,6 +3710,137 @@ } } }, + "node_modules/@jest/core/node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@jest/core/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/core/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/core/node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jest/diff-sequences": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", @@ -3619,6 +3995,54 @@ } } }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jest/schemas": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", @@ -3723,21 +4147,21 @@ } }, "node_modules/@jest/transform/node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -3863,12 +4287,13 @@ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@next/eslint-plugin-next": { - "version": "15.5.11", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.11.tgz", - "integrity": "sha512-tS/HYQOjIoX9ZNDQitba/baS8sTvo3ekY6Vgdx5lmhN4jov082bdApIChXr94qhMZHvEciz9DZglFFnhguQp/A==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.12.tgz", + "integrity": "sha512-+ZRSDFTv4aC96aMb5E41rMjysx8ApkryevnvEYZvPZO52KvkqP5rNExLUXJFr9P4s0f3oqNQR6vopCZsPWKDcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3888,6 +4313,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10" } @@ -3905,6 +4331,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10" } @@ -3922,6 +4349,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -3939,6 +4367,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -3956,6 +4385,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -3973,6 +4403,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -3990,6 +4421,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4007,6 +4439,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10" } @@ -4442,10 +4875,43 @@ "node": ">= 12" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -4457,9 +4923,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -4471,9 +4937,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -4485,9 +4951,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -4499,9 +4965,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -4513,9 +4979,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -4527,9 +4993,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -4541,9 +5007,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -4555,9 +5021,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -4569,9 +5035,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -4583,9 +5049,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -4597,9 +5063,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -4611,9 +5077,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -4625,9 +5091,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -4639,9 +5105,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -4653,9 +5119,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -4667,9 +5133,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -4681,9 +5147,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -4695,9 +5161,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -4709,9 +5175,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -4723,9 +5189,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -4737,9 +5203,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -4751,9 +5217,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -4765,9 +5231,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -4779,9 +5245,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -4820,16 +5286,16 @@ } }, "node_modules/@storybook/addon-docs": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.8.tgz", - "integrity": "sha512-cEoWqQrLzrxOwZFee5zrD4cYrdEWKV80POb7jUZO0r5vfl2DuslIr3n/+RfLT52runCV4aZcFEfOfP/IWHNPxg==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.10.tgz", + "integrity": "sha512-2wIYtdvZIzPbQ5194M5Igpy8faNbQ135nuO5ZaZ2VuttqGr+IJcGnDP42zYwbAsGs28G8ohpkbSgIzVyJWUhPQ==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.2.8", + "@storybook/csf-plugin": "10.2.10", "@storybook/icons": "^2.0.1", - "@storybook/react-dom-shim": "10.2.8", + "@storybook/react-dom-shim": "10.2.10", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" @@ -4839,13 +5305,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.8" + "storybook": "^10.2.10" } }, "node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.2.8.tgz", - "integrity": "sha512-kKkLYhRXb33YtIPdavD2DU25sb14sqPYdcQFpyqu4TaD9truPPqW8P5PLTUgERydt/eRvRlnhauPHavU1kjsnA==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.2.10.tgz", + "integrity": "sha512-aFvgaNDAnKMjuyhPK5ialT22pPqMN0XfPBNPeeNVPYztngkdKBa8WFqF/umDd47HxAjebq+vn6uId1xHyOHH3g==", "dev": true, "license": "MIT", "dependencies": { @@ -4858,7 +5324,7 @@ "peerDependencies": { "esbuild": "*", "rollup": "*", - "storybook": "^10.2.8", + "storybook": "^10.2.10", "vite": "*", "webpack": "*" }, @@ -4878,9 +5344,9 @@ } }, "node_modules/@storybook/addon-links": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.2.8.tgz", - "integrity": "sha512-5yy8+6z1OLxrQGpLzwIChO53hCMGVMMrRSG98IslMzhExEbK4+prf6gKMA0t4SdWAjkKgzbRz2YNnv9N6rEO5Q==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.2.10.tgz", + "integrity": "sha512-oo9Xx4/2OVJtptXKpqH4ySri7ZuBdiSOXlZVGejEfLa0Jeajlh/KIlREpGvzPPOqUVT7dSddWzBjJmJUyQC3ew==", "dev": true, "license": "MIT", "dependencies": { @@ -4892,7 +5358,7 @@ }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.2.8" + "storybook": "^10.2.10" }, "peerDependenciesMeta": { "react": { @@ -4901,13 +5367,13 @@ } }, "node_modules/@storybook/builder-webpack5": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.2.8.tgz", - "integrity": "sha512-77i/is0a4HIRwkcxs3wQnQCnIahLONKxSp0cURjBU38kj/M0ukOOlOPIIJOm4HgI202yLjvGNiaMcLWFxHfl8w==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.2.10.tgz", + "integrity": "sha512-bIHAXiX9NwZlB5dJ2W+rZcwo1Dkmg0JOwL/F/rB9O4IlkjTsoOe/+BcLchfRdqRk7ENCVFNwaq8aXxnKmiIOMQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/core-webpack": "10.2.8", + "@storybook/core-webpack": "10.2.10", "case-sensitive-paths-webpack-plugin": "^2.4.0", "cjs-module-lexer": "^1.2.3", "css-loader": "^7.1.2", @@ -4928,7 +5394,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.8" + "storybook": "^10.2.10" }, "peerDependenciesMeta": { "typescript": { @@ -4937,9 +5403,9 @@ } }, "node_modules/@storybook/builder-webpack5/node_modules/css-loader": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.3.tgz", - "integrity": "sha512-frbERmjT0UC5lMheWpJmMilnt9GEhbZJN/heUb7/zaJYeIzj5St9HvDcfshzzOqbsS+rYpMk++2SD3vGETDSyA==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", "dev": true, "license": "MIT", "dependencies": { @@ -4960,7 +5426,7 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "@rspack/core": "0.x || 1.x", + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "webpack": "^5.27.0" }, "peerDependenciesMeta": { @@ -5003,9 +5469,9 @@ } }, "node_modules/@storybook/core-webpack": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.2.8.tgz", - "integrity": "sha512-TmKUbFVxDEoCybFC9Ps6gfcbZnKCc4DIclmIxEnkzKUuP0I6gh5w5Xd4Uf1hXroWIzZPNtm0SWsNOKycP+FQqQ==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.2.10.tgz", + "integrity": "sha512-bhz20jQWn0UB6GfYeO3oou8w8jXSVs+dgPglsxPr+tOusUuyT5FO270PHixZovVtrHgFAKHLXUEHUNuOvUsMig==", "dev": true, "license": "MIT", "dependencies": { @@ -5016,7 +5482,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.8" + "storybook": "^10.2.10" } }, "node_modules/@storybook/global": { @@ -5038,9 +5504,9 @@ } }, "node_modules/@storybook/nextjs": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/nextjs/-/nextjs-10.2.8.tgz", - "integrity": "sha512-CZqHsNqMYbw9tK2pUfSzpc7VVBaeAticpw4lnxUANxW3nCTpX82wrQGj4bRWqZL3WfUMw8WfdAC4htJo5kLVBA==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@storybook/nextjs/-/nextjs-10.2.10.tgz", + "integrity": "sha512-OTyghwvsvXpAtcZcY7XNUKGC1hJQKmz7x/y56h7kIedVrw+v8UZA1wu+obmml+QCkPXOAMS5GQzXIkNJGxyYTw==", "dev": true, "license": "MIT", "dependencies": { @@ -5058,9 +5524,9 @@ "@babel/preset-typescript": "^7.28.5", "@babel/runtime": "^7.28.4", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", - "@storybook/builder-webpack5": "10.2.8", - "@storybook/preset-react-webpack": "10.2.8", - "@storybook/react": "10.2.8", + "@storybook/builder-webpack5": "10.2.10", + "@storybook/preset-react-webpack": "10.2.10", + "@storybook/react": "10.2.10", "@types/semver": "^7.7.1", "babel-loader": "^9.1.3", "css-loader": "^6.7.3", @@ -5086,7 +5552,7 @@ "next": "^14.1.0 || ^15.0.0 || ^16.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.2.8", + "storybook": "^10.2.10", "webpack": "^5.0.0" }, "peerDependenciesMeta": { @@ -5104,7 +5570,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -5304,13 +5769,13 @@ } }, "node_modules/@storybook/preset-react-webpack": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.2.8.tgz", - "integrity": "sha512-R+w1aT+NQ2eXHkPRpVnt/aBk5V5/L7+1EhFTnyQaEcviIanPlRURKhbOQi02gSGW/alekMLKtSvPTzow/VyvRA==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.2.10.tgz", + "integrity": "sha512-DaV7uKpNF/2iBjcGL81HA7Kx8ZZb9D4MfG1VxpdtmDOKS20YIDNdCFeUbcAkUlG3lhshUGcGL8YiRp3o4b1X6Q==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/core-webpack": "10.2.8", + "@storybook/core-webpack": "10.2.10", "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", "@types/semver": "^7.7.1", "magic-string": "^0.30.5", @@ -5327,7 +5792,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.2.8" + "storybook": "^10.2.10" }, "peerDependenciesMeta": { "typescript": { @@ -5349,14 +5814,14 @@ } }, "node_modules/@storybook/react": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.2.8.tgz", - "integrity": "sha512-nMFqQFUXq6Zg2O5SeuomyWnrIx61QfpNQMrfor8eCEzHrWNnXrrvVsz2RnHIgXN8RVyaWGDPh1srAECu/kDHXw==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.2.10.tgz", + "integrity": "sha512-PcsChzPI8lhllB9exV7nFb96093i6sTwIl0jpPjaTFPQCRoueR9E/YeP3qSKQL9xt4cmii0cW7F0RUx25rW93Q==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.2.8", + "@storybook/react-dom-shim": "10.2.10", "react-docgen": "^8.0.2" }, "funding": { @@ -5366,7 +5831,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.2.8", + "storybook": "^10.2.10", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -5396,9 +5861,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.2.8.tgz", - "integrity": "sha512-Xde9X3VszFV1pTXfc2ZFM89XOCGRxJD8MUIzDwkcT9xaki5a+8srs/fsXj75fMY6gMYfcL5lNRZvCqg37HOmcQ==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.2.10.tgz", + "integrity": "sha512-TmBrhyLHn8B8rvDHKk5uW5BqzO1M1T+fqFNWg88NIAJOoyX4Uc90FIJjDuN1OJmWKGwB5vLmPwaKBYsTe1yS+w==", "dev": true, "license": "MIT", "funding": { @@ -5408,7 +5873,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.2.8" + "storybook": "^10.2.10" } }, "node_modules/@storybook/react/node_modules/@babel/core": { @@ -5510,12 +5975,26 @@ "eslint": ">=8.40.0" } }, + "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -5834,6 +6313,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5848,22 +6328,13 @@ "node": ">=18" } }, - "node_modules/@testing-library/dom/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/dom/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -5877,6 +6348,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -5891,7 +6363,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", @@ -5949,7 +6422,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -6123,9 +6597,8 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.3.tgz", "integrity": "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg==", - "dev": true, + "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -6186,17 +6659,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -6209,8 +6682,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -6225,17 +6698,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "engines": { @@ -6246,19 +6718,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", "debug": "^4.4.3" }, "engines": { @@ -6273,14 +6745,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6291,9 +6763,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", "dev": true, "license": "MIT", "engines": { @@ -6308,15 +6780,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", - "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -6328,14 +6800,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", - "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", "engines": { @@ -6347,16 +6819,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", - "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.54.0", - "@typescript-eslint/tsconfig-utils": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", @@ -6374,10 +6846,36 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -6388,16 +6886,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", - "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0" + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6407,19 +6905,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", - "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6989,12 +7487,11 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7055,12 +7552,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7091,9 +7587,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -7167,16 +7663,13 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, "node_modules/ansi-styles": { @@ -7397,9 +7890,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -7832,9 +8325,9 @@ } }, "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "dev": true, "license": "MIT" }, @@ -7846,13 +8339,14 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -8028,7 +8522,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8419,69 +8912,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -8676,9 +9106,9 @@ } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -8856,7 +9286,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -9097,9 +9527,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -9121,7 +9551,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-converter": { "version": "0.2.0", @@ -9262,9 +9693,9 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -9282,9 +9713,9 @@ } }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, @@ -9554,7 +9985,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -9619,7 +10049,6 @@ "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -9675,126 +10104,64 @@ } } }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "node_modules/eslint-plugin-storybook": { + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.2.10.tgz", + "integrity": "sha512-aWkoh2rhTaEsMA4yB1iVIcISM5wb0uffp09ZqhwpoD4GAngCs131uq6un+QdnOMc7vXyAnBBfsuhtOj8WwCUgw==", "dev": true, "license": "MIT", "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" + "@typescript-eslint/utils": "^8.48.0" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + "eslint": ">=8", + "storybook": "^10.2.10" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "Apache-2.0", "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-storybook": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.2.8.tgz", - "integrity": "sha512-BtysXrg1RoYT3DIrCc+svZ0+L3mbWsu7suxTLGrihBY5HfWHkJge+qjlBBR1Nm2ZMslfuFS5K0NUWbWCJRu6kg==", + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^8.48.0" - }, - "peerDependencies": { - "eslint": ">=8", - "storybook": "^10.2.8" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" } }, - "node_modules/eslint-visitor-keys": { + "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", @@ -9807,43 +10174,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -9862,6 +10192,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -9987,13 +10330,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/exit-x": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", @@ -10248,9 +10584,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -10287,6 +10623,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", @@ -10315,30 +10664,6 @@ "webpack": "^5.11.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -10581,21 +10906,22 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -11654,9 +11980,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -11698,9 +12024,9 @@ } }, "node_modules/istanbul-lib-report/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -11779,7 +12105,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -11896,7 +12221,70 @@ } } }, - "node_modules/jest-config": { + "node_modules/jest-cli/node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/jest-cli/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-cli/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-cli/node_modules/jest-config": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", @@ -11948,35 +12336,20 @@ } } }, - "node_modules/jest-config/node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "node_modules/jest-cli/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16 || 14 >=14.17" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/jest-diff": { @@ -12231,10 +12604,21 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-runtime": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", - "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", "dev": true, "license": "MIT", "dependencies": { @@ -12265,6 +12649,16 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/jest-runtime/node_modules/cjs-module-lexer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", @@ -12272,6 +12666,44 @@ "dev": true, "license": "MIT" }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-snapshot": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", @@ -12306,21 +12738,21 @@ } }, "node_modules/jest-snapshot/node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -12347,9 +12779,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -12964,13 +13396,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "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", @@ -13026,6 +13451,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -13166,9 +13592,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -13230,19 +13656,16 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { @@ -13256,11 +13679,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -13361,6 +13784,7 @@ "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", @@ -13429,6 +13853,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -13444,6 +13869,7 @@ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "client-only": "0.0.1" }, @@ -13486,6 +13912,25 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -14188,7 +14633,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14498,9 +14942,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -14613,7 +15057,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14681,7 +15124,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14702,7 +15144,6 @@ "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14902,29 +15343,6 @@ "strip-ansi": "^6.0.1" } }, - "node_modules/renderkid/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/renderkid/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -15066,52 +15484,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/ripemd160": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", @@ -15190,12 +15562,11 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -15207,31 +15578,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -15416,12 +15787,11 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15463,16 +15833,6 @@ "semver": "bin/semver.js" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -15558,6 +15918,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "optional": true, + "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -15603,6 +15964,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -15710,17 +16072,11 @@ } }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "license": "ISC" }, "node_modules/slash": { "version": "3.0.0", @@ -15752,9 +16108,9 @@ } }, "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", "dependencies": { @@ -15814,12 +16170,11 @@ } }, "node_modules/storybook": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.8.tgz", - "integrity": "sha512-885uSIn8NQw2ZG7vy84K45lHCOSyz1DVsDV8pHiHQj3J0riCuWLNeO50lK9z98zE8kjhgTtxAAkMTy5nkmNRKQ==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.10.tgz", + "integrity": "sha512-N4U42qKgzMHS7DjqLz5bY4P7rnvJtYkWFCyKspZr3FhPUuy6CWOae3aYC2BjXkHrdug0Jyta6VxFTuB1tYUKhg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", @@ -15954,45 +16309,19 @@ "node": ">=10" } }, - "node_modules/string-length/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/string-width-cjs": { @@ -16011,40 +16340,10 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "license": "MIT", "dependencies": { @@ -16140,19 +16439,16 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/strip-ansi-cjs": { @@ -16169,16 +16465,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -16380,16 +16666,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -16452,17 +16737,6 @@ "dev": true, "license": "MIT" }, - "node_modules/terser/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -16478,52 +16752,6 @@ "node": ">=8" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -16646,16 +16874,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -16697,9 +16915,9 @@ "license": "Apache-2.0" }, "node_modules/ts-jest": { - "version": "29.4.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", - "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", "dev": true, "license": "MIT", "dependencies": { @@ -16777,587 +16995,103 @@ }, "node_modules/tsconfig-paths": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", - "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.7.0", - "tapable": "^2.2.1", - "tsconfig-paths": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true, - "license": "0BSD" - }, - "node_modules/tsup": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.0.tgz", - "integrity": "sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.25.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "0.8.0-beta.0", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/tsup/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsup/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, "engines": { - "node": ">=18" + "node": ">=6" } }, - "node_modules/tsup/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, "engines": { - "node": ">=18" + "node": ">=10.13.0" } }, - "node_modules/tsup/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=4" } }, - "node_modules/tsup/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, + "license": "0BSD" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", "dev": true, - "hasInstallScript": true, "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, "bin": { - "esbuild": "bin/esbuild" + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" }, "engines": { "node": ">=18" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } } }, "node_modules/tsup/node_modules/resolve-from": { @@ -17371,17 +17105,13 @@ } }, "node_modules/tsup/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "deprecated": "The work that was done in this beta branch won't be included in future versions", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "whatwg-url": "^7.0.0" - }, "engines": { - "node": ">= 8" + "node": ">= 12" } }, "node_modules/tty-browserify": { @@ -17420,7 +17150,6 @@ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -17512,7 +17241,6 @@ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17522,16 +17250,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", - "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.54.0", - "@typescript-eslint/parser": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0" + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -17541,7 +17269,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -17843,20 +17571,12 @@ "node": ">=10.13.0" } }, - "node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/webpack": { "version": "5.105.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz", "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -17935,36 +17655,12 @@ "integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-html-community": "0.0.8", "html-entities": "^2.1.0", "strip-ansi": "^6.0.0" } }, - "node_modules/webpack-hot-middleware/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-hot-middleware/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/webpack-sources": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", @@ -18013,18 +17709,6 @@ "node": ">=4.0" } }, - "node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -18148,18 +17832,18 @@ "license": "MIT" }, "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -18184,64 +17868,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -18263,6 +17889,19 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -18357,51 +17996,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 6d796fa9..dcaff71c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "git+https://github.com/helpwave/hightide.git" }, "license": "MPL-2.0", - "version": "0.8.12", + "version": "0.9.0", "files": [ "dist" ], @@ -39,6 +39,7 @@ }, "dependencies": { "@helpwave/internationalization": "0.4.0", + "@radix-ui/react-slot": "1.2.4", "@tailwindcss/cli": "4.1.18", "@tanstack/react-table": "8.21.3", "clsx": "2.1.1", @@ -54,9 +55,9 @@ "@babel/preset-typescript": "7.26.0", "@faker-js/faker": "10.1.0", "@helpwave/eslint-config": "0.0.11", - "@storybook/addon-docs": "10.2.8", - "@storybook/addon-links": "10.2.8", - "@storybook/nextjs": "^10.2.8", + "@storybook/addon-docs": "10.2.10", + "@storybook/addon-links": "10.2.10", + "@storybook/nextjs": "10.2.10", "@tailwindcss/postcss": "4.1.18", "@types/jest": "30.0.0", "@types/node": "20.17.10", @@ -66,12 +67,12 @@ "@vitest/mocker": "4.0.16", "autoprefixer": "10.4.23", "eslint": "9.31.0", - "eslint-plugin-storybook": "10.2.8", + "eslint-plugin-storybook": "10.2.10", "jest": "30.2.0", "postcss": "8.5.6", - "storybook": "10.2.8", - "ts-jest": "29.4.5", - "tsup": "8.5.0", + "storybook": "10.2.10", + "ts-jest": "29.4.6", + "tsup": "8.5.1", "typescript": "5.7.2", "webpack": "5.105.2" }, diff --git a/src/components/display-and-visualization/Chip.tsx b/src/components/display-and-visualization/Chip.tsx index 25b37b79..76ed5fa0 100644 --- a/src/components/display-and-visualization/Chip.tsx +++ b/src/components/display-and-visualization/Chip.tsx @@ -3,7 +3,7 @@ import { ButtonUtil } from '@/src/components/user-interaction/Button' type ChipSize = 'xs' | 'sm' | 'md' | 'lg' | null -type ChipColoringStyle = 'solid' | 'tonal' | null +type ChipColoringStyle = 'solid' | 'tonal' | 'outline' | 'tonal-outline' | null const chipColors = ButtonUtil.colors export type ChipColor = typeof chipColors[number] diff --git a/src/components/layout/ListBox.tsx b/src/components/layout/ListBox.tsx deleted file mode 100644 index f159ed38..00000000 --- a/src/components/layout/ListBox.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import type { HTMLAttributes, RefObject } from 'react' -import React, { createContext, forwardRef, useCallback, useContext, useEffect, useRef, useState } from 'react' -import { clsx } from 'clsx' -import { match } from '@/src/utils/match' -import { useControlledState } from '@/src/hooks/useControlledState' - -// -// Context -// -type RegisteredItem = { - id: string, - value: string, - disabled: boolean, - ref: React.RefObject, -} - -type ListBoxContextType = { - registerItem: (item: RegisteredItem) => void, - unregisterItem: (id: string) => void, - - highlightedId?: string, - setHighlightedId: (id: string) => void, - - onItemClick: (id: string) => void, - isSelected: (value: string) => boolean, -} - -const ListBoxContext = createContext(null) - -function useListBoxContext() { - const ctx = useContext(ListBoxContext) - if (!ctx) { - throw new Error('ListBoxItem must be used within a ListBoxPrimitive') - } - return ctx -} - - -/* - * ListBoxItem - */ -export type ListBoxItemProps = HTMLAttributes & { - value: string, - disabled?: boolean, -} - -export const ListBoxItem = forwardRef( - function ListBoxItem({ value, disabled = false, children, className, ...rest }, ref) { - const { - registerItem, - unregisterItem, - highlightedId, - setHighlightedId, - onItemClick, - isSelected, - } = useListBoxContext() - - const itemRef = useRef(null) - const id = React.useId() - - // Register with parent - useEffect(() => { - registerItem({ id, value, disabled, ref: itemRef }) - return () => unregisterItem(id) - }, [id, value, disabled, registerItem, unregisterItem]) - - const isHighlighted = highlightedId === id - const selected = isSelected(value) - - return ( -
  • { - itemRef.current = node - if (typeof ref === 'function') ref(node) - else if (ref) (ref as RefObject).current = node - }} - id={id} - role="option" - aria-disabled={disabled} - aria-selected={selected} - data-highlighted={isHighlighted ? '' : undefined} - data-selected={selected ? '' : undefined} - data-disabled={disabled ? '' : undefined} - className={clsx( - 'flex-row-1 items-center px-2 py-1 rounded-md', - 'data-highlighted:bg-primary/20', - 'data-disabled:text-disabled data-disabled:cursor-not-allowed', - 'not-data-disabled:cursor-pointer', - className - )} - onClick={() => { - if (!disabled) onItemClick(id) - }} - onMouseEnter={() => { - if (!disabled) { - setHighlightedId(id) - } - }} - {...rest} - > - {children ?? value} -
  • - ) - } -) - -type ListBoxOrientation = 'vertical' | 'horizontal' - -// -// ListBoxPrimitive -// -export type ListBoxPrimitiveProps = HTMLAttributes & { - value?: string[], - initialValue?: string[], - onItemClicked?: (value: string) => void, - onSelectionChanged?: (value: string[]) => void, - isSelection?: boolean, - isMultiple?: boolean, - orientation?: ListBoxOrientation, -} - - -export const ListBoxPrimitive = forwardRef( - function ListBoxPrimitive({ - value: controlledValue, - initialValue, - onSelectionChanged, - onItemClicked, - isSelection = false, - isMultiple = false, - orientation = 'vertical', - ...props - }, ref) { - const [value, setValue] = useControlledState({ - value: controlledValue, - onValueChange: onSelectionChanged, - defaultValue: initialValue, - }) - const itemsRef = useRef([]) - const [highlightedIndex, setHighlightedIndex] = useState(undefined) - - const registerItem = useCallback((item: RegisteredItem) => { - itemsRef.current.push(item) - itemsRef.current.sort((a, b) => { - const aEl = a.ref.current - const bEl = b.ref.current - if (!aEl || !bEl) return 0 - return aEl.compareDocumentPosition(bEl) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1 - }) - }, []) - - const unregisterItem = useCallback((id: string) => { - itemsRef.current = itemsRef.current.filter(i => i.id !== id) - }, []) - - const isSelected = useCallback( - (val: string) => (value ?? []).includes(val), - [value] - ) - - const onItemClickedHandler = useCallback( - (id: string) => { - const index = itemsRef.current.findIndex(i => i.id === id) - if (index === -1) { - console.error('ListBoxItem provided an invalid id') - return - } - const item = itemsRef.current[index] - const val = item.value - onItemClicked?.(val) - setHighlightedIndex(index) - if (!isSelection) return - if (!isMultiple) { - setValue([val]) - } else { - if (isSelected(val)) { - setValue((value ?? []).filter(v => v !== val)) - } else { - setValue([...(value ?? []), val]) - } - } - }, - [onItemClicked, isSelection, isMultiple, setValue, isSelected, value] - ) - - const setHighlightedId = useCallback((id: string) => { - const index = itemsRef.current.findIndex(i => i.id === id) - if (index !== -1) { - setHighlightedIndex(index) - } - }, []) - - // Scroll highlighted item into view - useEffect(() => { - if (highlightedIndex !== undefined) { - itemsRef.current[highlightedIndex]?.ref.current?.scrollIntoView({ block: 'nearest', behavior: 'auto' }) - } - }, [highlightedIndex]) - - const highlightedItem: RegisteredItem | undefined = itemsRef.current[highlightedIndex] - const ctxValue: ListBoxContextType = { - registerItem, - unregisterItem, - highlightedId: highlightedItem?.id, - setHighlightedId, - onItemClick: onItemClickedHandler, - isSelected, - } - - const moveHighlight = (delta: number) => { - if (itemsRef.current.length === 0) return - let nextIndex = highlightedIndex ?? -1 - for (let i = 0; i < itemsRef.current.length; i++) { - nextIndex = (nextIndex + delta + itemsRef.current.length) % itemsRef.current.length - if (!itemsRef.current[nextIndex].disabled) break - } - setHighlightedIndex(nextIndex) - } - - return ( - -
      { - if (highlightedIndex === undefined) { - const firstEnabled = itemsRef.current.findIndex(i => !i.disabled) - setHighlightedIndex(firstEnabled !== -1 ? firstEnabled : undefined) - } - props.onFocus?.(event) - }} - onBlur={event => { - setHighlightedIndex(undefined) - props.onBlur?.(event) - }} - onKeyDown={(event) => { - switch (event.key) { - case match(orientation, { - vertical: 'ArrowDown', - horizontal: 'ArrowUp' - }): - moveHighlight(1) - event.preventDefault() - break - case match(orientation, { - vertical: 'ArrowUp', - horizontal: 'ArrowDown' - }): - moveHighlight(-1) - event.preventDefault() - break - case 'Home': - setHighlightedIndex(itemsRef.current.findIndex(i => !i.disabled)) - event.preventDefault() - break - case 'End': - for (let i = itemsRef.current.length - 1; i >= 0; i--) { - if (!itemsRef.current[i].disabled) { - setHighlightedIndex(i) - break - } - } - event.preventDefault() - break - case 'Enter': - case ' ': - if (highlightedIndex !== undefined) { - event.preventDefault() - onItemClickedHandler(itemsRef.current[highlightedIndex].id) - } - break - } - props.onKeyDown?.(event) - }} - role="listbox" - aria-multiselectable={isSelection ? isMultiple : undefined} - aria-orientation={orientation} - tabIndex={0} - > - {props.children} -
    -
    - ) - } -) - -/* - * ListBoxMultiple - */ -export type ListBoxMultipleProps = Omit -export const ListBoxMultiple = ({ ...props }: ListBoxMultipleProps) => { - return ( - - ) -} - -export type ListBoxProps = Omit & { - value?: string, - onSelectionChanged?: (value: string) => void, -} -export const ListBox = forwardRef(function ListBox({ - value, - onSelectionChanged, - ...props -}, ref) { - return ( - { - onSelectionChanged(newValue[0] ?? value) - }} - isMultiple={false} - {...props} - /> - ) -}) diff --git a/src/components/layout/dialog/premade/LanguageDialog.tsx b/src/components/layout/dialog/premade/LanguageDialog.tsx index 45d7f674..7cd5f1f0 100644 --- a/src/components/layout/dialog/premade/LanguageDialog.tsx +++ b/src/components/layout/dialog/premade/LanguageDialog.tsx @@ -6,12 +6,12 @@ import { useLocale } from '@/src/global-contexts/LocaleContext' import { Button } from '@/src/components/user-interaction/Button' import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' import type { HightideTranslationLocales } from '@/src/i18n/translations' -import type { SelectProps } from '@/src/components/user-interaction/select/Select' -import { Select } from '@/src/components/user-interaction/select/Select' -import { SelectOption } from '@/src/components/user-interaction/select/SelectComponents' +import type { SelectProps } from '@/src/components/user-interaction/Select/Select' +import { Select } from '@/src/components/user-interaction/Select/Select' +import { SelectOption } from '@/src/components/user-interaction/Select/SelectOption' import clsx from 'clsx' -type LanguageSelectProps = Omit +type LanguageSelectProps = Omit export const LanguageSelect = ({ ...props }: LanguageSelectProps) => { const { locale, setLocale } = useLocale() @@ -30,7 +30,9 @@ export const LanguageSelect = ({ ...props }: LanguageSelectProps) => { }} > {LocalizationUtil.locals.map((local) => ( - {LocalizationUtil.languagesLocalNames[local]} + + {LocalizationUtil.languagesLocalNames[local]} + ))} ) diff --git a/src/components/layout/dialog/premade/ThemeDialog.tsx b/src/components/layout/dialog/premade/ThemeDialog.tsx index 688e297e..f77d592a 100644 --- a/src/components/layout/dialog/premade/ThemeDialog.tsx +++ b/src/components/layout/dialog/premade/ThemeDialog.tsx @@ -8,9 +8,9 @@ import type { ThemeType } from '@/src/global-contexts/ThemeContext' import { ThemeUtil, useTheme } from '@/src/global-contexts/ThemeContext' import { Button } from '@/src/components/user-interaction/Button' import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' -import type { SelectProps } from '@/src/components/user-interaction/select/Select' -import { Select } from '@/src/components/user-interaction/select/Select' -import { SelectOption } from '@/src/components/user-interaction/select/SelectComponents' +import type { SelectProps } from '@/src/components/user-interaction/Select/Select' +import { Select } from '@/src/components/user-interaction/Select/Select' +import { SelectOption } from '@/src/components/user-interaction/Select/SelectOption' export interface ThemeIconProps extends HTMLAttributes { theme?: ThemeType, @@ -30,18 +30,19 @@ export const ThemeIcon = ({ theme: themeOverride, ...props }: ThemeIconProps) => } } -export type ThemeSelectProps = Omit +export type ThemeSelectProps = Omit, 'value' | 'children'> export const ThemeSelect = ({ ...props }: ThemeSelectProps) => { const translation = useHightideTranslation() const { theme, setTheme } = useTheme() return ( - { - onFilterValueChange({ - operator: newOperator as TableTextFilter, - parameter: needsParameterInput ? parameter : {}, - }) - }} - buttonProps={{ className: 'min-w-64' }} - > - {availableOperators.map((op) => ( - - - - ))} - - {translation('parameter')} - - { - onFilterValueChange({ - operator, - parameter: { ...parameter, searchText }, - }) - }} - className="min-w-64" - /> -
    - { - onFilterValueChange({ - operator, - parameter: { ...parameter, isCaseSensitive }, - }) - }} - /> - -
    -
    - - - {translation('noParameterRequired')} - - - - ) -} - -export type NumberFilterProps = TableFilterBaseProps - -export const NumberFilter = ({ filterValue, onFilterValueChange }: NumberFilterProps) => { - const translation = useHightideTranslation() - const operator = filterValue?.operator ?? 'numberBetween' - const parameter = filterValue?.parameter ?? {} - - const availableOperators = useMemo(() => [ - ...TableFilterOperator.number, - ...TableFilterOperator.generic, - ], []) - - const needsRangeInput = operator === 'numberBetween' || operator === 'numberNotBetween' - const needsParameterInput = operator !== 'undefined' && operator !== 'notUndefined' - - return ( -
    - - {translation('parameter')} - -
    - - {({ ariaAttributes, interactionStates, id }) => ( - { - const num = Number(text) - onFilterValueChange({ - operator, - parameter: { ...parameter, min: isNaN(num) ? undefined : num }, - }) - }} - className="input-indicator-hidden min-w-64" - /> - )} - - - {({ ariaAttributes, interactionStates, id }) => ( - { - const num = Number(text) - onFilterValueChange({ - operator, - parameter: { ...parameter, max: isNaN(num) ? undefined : num }, - }) - }} - className="input-indicator-hidden min-w-64" - /> - )} - -
    -
    - - { - const num = Number(text) - onFilterValueChange({ - operator, - parameter: { compareValue: isNaN(num) ? undefined : num }, - }) - }} - className="min-w-64" - /> - - - - {translation('noParameterRequired')} - - -
    - ) -} - -export type DateFilterProps = TableFilterBaseProps - -export const DateFilter = ({ filterValue, onFilterValueChange }: DateFilterProps) => { - const translation = useHightideTranslation() - const id = useId() - const ids = { - startDate: `date-filter-start-date-${id}`, - endDate: `date-filter-end-date-${id}`, - compareDate: `date-filter-compare-date-${id}`, - } - const operator = filterValue?.operator ?? 'dateBetween' - const parameter = filterValue?.parameter ?? {} - const [temporaryMinDateValue, setTemporaryMinDateValue] = useState(null) - const [temporaryMaxDateValue, setTemporaryMaxDateValue] = useState(null) - - const availableOperators = useMemo(() => [ - ...TableFilterOperator.date, - ...TableFilterOperator.generic, - ], []) - - const needsRangeInput = operator === 'dateBetween' || operator === 'dateNotBetween' - const needsParameterInput = operator !== 'undefined' && operator !== 'notUndefined' - - return ( -
    - - {translation('parameter')} - -
    - - setTemporaryMinDateValue(value)} - onEditComplete={value => { - if (value && parameter.max && value > parameter.max) { - if (!parameter.min) { - onFilterValueChange({ - operator, - parameter: { min: parameter.max, max: value }, - }) - } else { - const diff = parameter.max.getTime() - parameter.min.getTime() - onFilterValueChange({ - operator, - parameter: { min: value, max: new Date(value.getTime() + diff) }, - }) - } - } else { - onFilterValueChange({ - operator, - parameter: { ...parameter, min: value }, - }) - } - setTemporaryMinDateValue(null) - }} - allowRemove={true} - outsideClickCloses={false} - className="min-w-64" - /> - - setTemporaryMaxDateValue(value)} - onEditComplete={value => { - if (value && parameter.min && value < parameter.min) { - if (!parameter.max) { - onFilterValueChange({ - operator, - parameter: { min: value, max: parameter.min }, - }) - } else { - const diff = parameter.max.getTime() - parameter.min.getTime() - onFilterValueChange({ - operator, - parameter: { min: new Date(value.getTime() - diff), max: value }, - }) - } - } else { - onFilterValueChange({ - operator, - parameter: { ...parameter, max: value }, - }) - } - }} - allowRemove={true} - outsideClickCloses={false} - className="min-w-64" - /> -
    -
    - - - { - onFilterValueChange({ - operator, - parameter: { compareDate }, - }) - }} - allowRemove={true} - outsideClickCloses={false} - className="min-w-64" - /> - - - - {translation('noParameterRequired')} - - -
    - ) -} - -export type DatetimeFilterProps = TableFilterBaseProps - -export const DatetimeFilter = ({ filterValue, onFilterValueChange }: DatetimeFilterProps) => { - const translation = useHightideTranslation() - const id = useId() - const ids = { - startDate: `datetime-filter-start-date-${id}`, - endDate: `datetime-filter-end-date-${id}`, - compareDate: `datetime-filter-compare-date-${id}`, - } - const operator = filterValue?.operator ?? 'dateTimeBetween' - const parameter = filterValue?.parameter ?? {} - const [temporaryMinDateValue, setTemporaryMinDateValue] = useState(null) - const [temporaryMaxDateValue, setTemporaryMaxDateValue] = useState(null) - - const availableOperators = useMemo(() => [ - ...TableFilterOperator.dateTime, - ...TableFilterOperator.generic, - ], []) - - const needsRangeInput = operator === 'dateTimeBetween' || operator === 'dateTimeNotBetween' - const needsParameterInput = operator !== 'undefined' && operator !== 'notUndefined' - - return ( -
    - - {translation('parameter')} - -
    - - setTemporaryMinDateValue(value)} - onEditComplete={value => { - if (value && parameter.max && value > parameter.max) { - if (!parameter.min) { - onFilterValueChange({ - operator, - parameter: { min: parameter.max, max: value }, - }) - } else { - const diff = parameter.max.getTime() - parameter.min.getTime() - onFilterValueChange({ - operator, - parameter: { min: value, max: new Date(value.getTime() + diff) }, - }) - } - } else { - onFilterValueChange({ - operator, - parameter: { ...parameter, min: value }, - }) - } - setTemporaryMinDateValue(null) - }} - allowRemove={true} - outsideClickCloses={false} - className="min-w-64" - /> - - setTemporaryMaxDateValue(value)} - onEditComplete={value => { - if (value && parameter.min && value < parameter.min) { - if (!parameter.max) { - onFilterValueChange({ - operator, - parameter: { min: value, max: parameter.min }, - }) - } else { - const diff = parameter.max.getTime() - parameter.min.getTime() - onFilterValueChange({ - operator, - parameter: { min: new Date(value.getTime() - diff), max: value }, - }) - } - } else { - onFilterValueChange({ - operator, - parameter: { ...parameter, max: value }, - }) - } - }} - allowRemove={true} - outsideClickCloses={false} - className="min-w-64" - /> -
    -
    - - - { - onFilterValueChange({ - operator, - parameter: { compareDatetime }, - }) - }} - allowRemove={true} - outsideClickCloses={false} - className="min-w-64" - /> - - - - {translation('noParameterRequired')} - - -
    - ) -} -export type BooleanFilterProps = TableFilterBaseProps - -export const BooleanFilter = ({ filterValue, onFilterValueChange }: BooleanFilterProps) => { - const operator = filterValue?.operator ?? 'booleanIsTrue' - - const availableOperators = useMemo(() => [ - ...TableFilterOperator.boolean, - ...TableFilterOperator.generic, - ], []) - - - return ( -
    - -
    - ) -} - -export type TagsFilterProps = TableFilterBaseProps - -export const TagsFilter = ({ columnId, filterValue, onFilterValueChange }: TagsFilterProps) => { - const translation = useHightideTranslation() - const { table } = useTableStateWithoutSizingContext() - const operator = filterValue?.operator ?? 'tagsContains' - const parameter = filterValue?.parameter ?? {} - - const availableOperators = useMemo(() => [ - ...TableFilterOperator.multiTags, - ...TableFilterOperator.generic, - ], []) - - const availableTags = useMemo(() => { - const column = table.getColumn(columnId) - if (!column) return [] - return column.columnDef.meta?.filterData?.tags ?? [] - }, [columnId, table]) - - if (availableTags.length === 0) { - return null - } - - const needsParameterInput = operator !== 'undefined' && operator !== 'notUndefined' - - return ( -
    - - {translation('parameter')} - - String(tag)) : []} - onValueChange={(selectedTags: string[]) => { - onFilterValueChange({ - operator, - parameter: { searchTags: selectedTags.length > 0 ? selectedTags : undefined }, - }) - }} - buttonProps={{ className: 'min-w-64' }} - > - {availableTags.map(({ tag, label }) => ( - - {label} - - ))} - - - - - {translation('noParameterRequired')} - - -
    - ) -} - -export type TagsSingleFilterProps = TableFilterBaseProps -export const TagsSingleFilter = ({ columnId, filterValue, onFilterValueChange }: TagsSingleFilterProps) => { - const translation = useHightideTranslation() - const { table } = useTableStateWithoutSizingContext() - const operator = filterValue?.operator ?? 'tagsSingleContains' - const parameter = filterValue?.parameter ?? {} - - const availableOperators = useMemo(() => [ - ...TableFilterOperator.singleTag, - ...TableFilterOperator.generic, - ], []) - - const availableTags = useMemo(() => { - const column = table.getColumn(columnId) - if (!column) return [] - return column.columnDef.meta?.filterData?.tags ?? [] - }, [columnId, table]) - - if (availableTags.length === 0) { - return null - } - - const needsParameterInput = operator !== 'undefined' && operator !== 'notUndefined' - const needsMultiSelect = operator === 'tagsSingleContains' || operator === 'tagsSingleNotContains' - - return ( -
    - - {translation('parameter')} - - String(tag)) : []} - onValueChange={(selectedTags: string[]) => { - onFilterValueChange({ - operator, - parameter: { searchTagsContains: selectedTags.length > 0 ? selectedTags : undefined }, - }) - }} - buttonProps={{ className: 'min-w-64' }} - > - {availableTags.map(({ tag, label }) => ( - - {label} - - ))} - - - - - - - - {translation('noParameterRequired')} - - -
    - ) -} - -export type GenericFilterProps = TableFilterBaseProps - -export const GenericFilter = ({ filterValue, onFilterValueChange }: GenericFilterProps) => { - const operator = filterValue?.operator ?? 'notUndefined' - - const availableOperators = useMemo(() => [ - ...TableFilterOperator.generic, - ], []) - - return ( -
    - -
    - ) -} - -export interface TableFilterContentProps extends TableFilterBaseProps { - filterType: TableFilterCategory, -} - -export const TableFilterContent = ({ filterType, ...props }: TableFilterContentProps) => { - switch (filterType) { - case 'text': - return } /> - case 'number': - return } /> - case 'date': - return } /> - case 'dateTime': - return } /> - case 'boolean': - return } /> - case 'multiTags': - return } /> - case 'singleTag': - return } /> - case 'generic': - return } /> - default: - return null - } -} diff --git a/src/components/layout/table/TableHeader.tsx b/src/components/layout/table/TableHeader.tsx index 61618f2d..c36230a2 100644 --- a/src/components/layout/table/TableHeader.tsx +++ b/src/components/layout/table/TableHeader.tsx @@ -5,9 +5,8 @@ import { Visibility } from '../Visibility' import { TableSortButton } from './TableSortButton' import { TableFilterButton } from './TableFilterButton' import { useCallback, useEffect } from 'react' -import type { TableFilterCategory } from './TableFilter' -import { isTableFilterCategory } from './TableFilter' import { TableStateContext, useTableStateWithoutSizingContext } from './TableContext' +import { DataTypeUtils, type DataType } from '../../user-interaction/data/data-types' export type TableHeaderProps = { isSticky?: boolean, @@ -125,10 +124,10 @@ export const TableHeader = ({ isSticky = false }: TableHeaderProps) => { }} /> - + {flexRender( diff --git a/src/components/layout/table/TablePagination.tsx b/src/components/layout/table/TablePagination.tsx index 3369b1db..403c11bb 100644 --- a/src/components/layout/table/TablePagination.tsx +++ b/src/components/layout/table/TablePagination.tsx @@ -1,7 +1,7 @@ import { Pagination, type PaginationProps } from '@/src/components/layout/navigation/Pagination' import type { HTMLAttributes } from 'react' -import { Select, type SelectProps } from '@/src/components/user-interaction/select/Select' -import { SelectOption } from '@/src/components/user-interaction/select/SelectComponents' +import { Select, type SelectProps } from '@/src/components/user-interaction/Select/Select' +import { SelectOption } from '@/src/components/user-interaction/Select/SelectOption' import { Visibility } from '../Visibility' import clsx from 'clsx' import { useTableStateWithoutSizingContext } from './TableContext' @@ -27,7 +27,7 @@ export const TablePaginationMenu = ({ ...props }: TablePaginationMenuProps) => { const defaultPageSizeOptions: number[] = [10, 25, 50, 100, 500, 1000] as const -export interface TablePageSizeSelectProps extends SelectProps { +export interface TablePageSizeSelectProps extends Omit { pageSizeOptions?: number[], } @@ -45,9 +45,7 @@ export const TablePageSizeSelect = ({ onValueChange={(value) => table.setPageSize(Number(value))} > {pageSizeOptions.map(size => ( - - {size} - + ))} ) diff --git a/src/components/layout/table/TableProvider.tsx b/src/components/layout/table/TableProvider.tsx index 239fc31e..62e4f315 100644 --- a/src/components/layout/table/TableProvider.tsx +++ b/src/components/layout/table/TableProvider.tsx @@ -13,16 +13,16 @@ import { useWindowResizeObserver } from '@/src/hooks/useResizeCallbackWrapper' import { AutoColumnOrderFeature } from './AutoColumnOrderFeature' export type TableProviderProps = { - data: T[], - columns?: ColumnDef[], - children?: ReactNode, - isUsingFillerRows?: boolean, - fillerRowCell?: (columnId: string, table: ReactTable) => ReactNode, - initialState?: InitialTableState, - onRowClick?: (row: Row, table: ReactTable) => void, - onFillerRowClick?: (index: number, table: ReactTable) => void, - state?: Partial, - } & Partial> + data: T[], + columns?: ColumnDef[], + children?: ReactNode, + isUsingFillerRows?: boolean, + fillerRowCell?: (columnId: string, table: ReactTable) => ReactNode, + initialState?: InitialTableState, + onRowClick?: (row: Row, table: ReactTable) => void, + onFillerRowClick?: (index: number, table: ReactTable) => void, + state?: Partial, +} & Partial> export const TableProvider = ({ data, @@ -133,7 +133,7 @@ export const TableProvider = ({ boolean: TableFilter.boolean, multiTags: TableFilter.multiTags, singleTag: TableFilter.singleTag, - generic: TableFilter.generic, + unknownType: TableFilter.unknownType, }, _features: [ ...(tableOptions._features ?? []), @@ -254,4 +254,4 @@ export const TableProvider = ({ ) -} \ No newline at end of file +} diff --git a/src/components/layout/table/types.ts b/src/components/layout/table/types.ts index 9061d917..89d24ea8 100644 --- a/src/components/layout/table/types.ts +++ b/src/components/layout/table/types.ts @@ -6,7 +6,7 @@ declare module '@tanstack/react-table' { interface ColumnMeta { className?: string, filterData?: { - tags?: { tag: string, label: ReactNode }[], + tags?: { tag: string, label: string, display?: ReactNode }[], }, columnLabel?: string, } @@ -24,7 +24,7 @@ declare module '@tanstack/react-table' { boolean: FilterFn, multiTags: FilterFn, singleTag: FilterFn, - generic: FilterFn, + unknownType: FilterFn, } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/components/user-interaction/Button.tsx b/src/components/user-interaction/Button.tsx index 0b4c7f0d..0c221302 100644 --- a/src/components/user-interaction/Button.tsx +++ b/src/components/user-interaction/Button.tsx @@ -6,7 +6,7 @@ import { forwardRef } from 'react' */ type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | null -type ButtonColoringStyle = 'outline' | 'solid' | 'text' | 'tonal' | null +type ButtonColoringStyle = 'outline' | 'solid' | 'text' | 'tonal' | 'tonal-outline' | null const buttonColorsList = ['primary', 'secondary', 'positive', 'warning', 'negative', 'neutral'] as const diff --git a/src/components/user-interaction/Combobox/Combobox.tsx b/src/components/user-interaction/Combobox/Combobox.tsx new file mode 100644 index 00000000..4884015f --- /dev/null +++ b/src/components/user-interaction/Combobox/Combobox.tsx @@ -0,0 +1,40 @@ +import type { JSX } from 'react' +import { forwardRef, type ReactNode } from 'react' +import { ComboboxRoot } from './ComboboxRoot' +import { ComboboxInput } from './ComboboxInput' +import { ComboboxList } from './ComboboxList' +import type { ComboboxInputProps } from './ComboboxInput' +import type { ComboboxListProps } from './ComboboxList' + +export interface ComboboxProps { + children: ReactNode, + onItemClick?: (value: T) => void, + id?: string, + searchQuery?: string, + onSearchQueryChange?: (value: string) => void, + initialSearchQuery?: string, + inputProps?: ComboboxInputProps, + listProps?: ComboboxListProps, +} + +export const Combobox = forwardRef>(function Combobox ({ + children, + onItemClick, + searchQuery, + onSearchQueryChange, + initialSearchQuery, + inputProps, + listProps, +}, ref) { + return ( + + onItemClick={onItemClick} + searchQuery={searchQuery} + onSearchQueryChange={onSearchQueryChange} + initialSearchQuery={initialSearchQuery} + > + + {children} + + ) +}) as (props: ComboboxProps & React.RefAttributes) => JSX.Element diff --git a/src/components/user-interaction/Combobox/ComboboxContext.tsx b/src/components/user-interaction/Combobox/ComboboxContext.tsx new file mode 100644 index 00000000..064cc0ee --- /dev/null +++ b/src/components/user-interaction/Combobox/ComboboxContext.tsx @@ -0,0 +1,67 @@ +import type { Dispatch, ReactNode, RefObject, SetStateAction } from 'react' +import { createContext, useContext } from 'react' + +export interface ComboboxOptionType { + id: string, + value: T, + label?: string, + display?: ReactNode, + disabled?: boolean, + ref: RefObject, +} + +export interface ComboboxContextIds { + trigger: string, + listbox: string, +} + +export interface ComboboxContextInternalState { + highlightedId: string | null, +} + +export interface ComboboxContextComputedState { + options: ReadonlyArray>, + visibleOptionIds: ReadonlyArray, + idToOptionMap: Record>, +} + +export interface ComboboxContextActions { + registerOption(option: ComboboxOptionType): () => void, + selectOption(id: string): void, + highlightFirst(): void, + highlightLast(): void, + highlightNext(): void, + highlightPrevious(): void, + highlightItem(id: string): void, +} + +export interface ComboboxContextLayout { + listRef: RefObject, + registerList(ref: RefObject): () => void, +} + +export interface ComboboxContextSearch { + searchQuery: string, + setSearchQuery(query: string): void, +} + +export interface ComboboxContextConfig { + ids: ComboboxContextIds, + setIds: Dispatch>, +} + +export interface ComboboxContextType extends ComboboxContextInternalState, ComboboxContextComputedState, ComboboxContextActions { + config: ComboboxContextConfig, + layout: ComboboxContextLayout, + search: ComboboxContextSearch, +} + +export const ComboboxContext = createContext | null>(null) + +export function useComboboxContext(): ComboboxContextType { + const ctx = useContext(ComboboxContext) + if (ctx == null) { + throw new Error('useComboboxContext must be used within ComboboxRoot') + } + return ctx as ComboboxContextType +} diff --git a/src/components/user-interaction/Combobox/ComboboxInput.tsx b/src/components/user-interaction/Combobox/ComboboxInput.tsx new file mode 100644 index 00000000..ca3b9f61 --- /dev/null +++ b/src/components/user-interaction/Combobox/ComboboxInput.tsx @@ -0,0 +1,63 @@ +import { type ComponentProps, forwardRef, useCallback } from 'react' +import { Input } from '@/src/components/user-interaction/input/Input' +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import { useComboboxContext } from './ComboboxContext' + +export type ComboboxInputProps = Omit, 'value'> + +export const ComboboxInput = forwardRef( + function ComboboxInput(props, ref) { + const translation = useHightideTranslation() + const context = useComboboxContext() + const { highlightNext, highlightPrevious, highlightFirst, highlightLast, highlightedId, selectOption } = context + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + props.onKeyDown?.(event) + switch (event.key) { + case 'ArrowDown': + highlightNext() + event.preventDefault() + break + case 'ArrowUp': + highlightPrevious() + event.preventDefault() + break + case 'Home': + highlightFirst() + event.preventDefault() + break + case 'End': + highlightLast() + event.preventDefault() + break + case 'Enter': + if (highlightedId) { + selectOption(highlightedId) + event.preventDefault() + } + break + default: + break + } + }, + [props, highlightedId, selectOption, highlightNext, highlightPrevious, highlightFirst, highlightLast] + ) + + return ( + 0} + aria-controls={context.config.ids.listbox} + aria-activedescendant={context.highlightedId ?? undefined} + aria-autocomplete="list" + /> + ) + } +) diff --git a/src/components/user-interaction/Combobox/ComboboxList.tsx b/src/components/user-interaction/Combobox/ComboboxList.tsx new file mode 100644 index 00000000..52c3c212 --- /dev/null +++ b/src/components/user-interaction/Combobox/ComboboxList.tsx @@ -0,0 +1,54 @@ +import type { HTMLAttributes, RefObject } from 'react' +import { forwardRef, useEffect, useRef } from 'react' +import clsx from 'clsx' +import { useComboboxContext } from './ComboboxContext' +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' + +export type ComboboxListProps = HTMLAttributes + +export const ComboboxList = forwardRef( + function ComboboxList({ children, ...props }, ref) { + const translation = useHightideTranslation() + const context = useComboboxContext() + const { layout } = context + const { registerList } = layout + const innerRef = useRef(null) + + useEffect(() => { + return registerList(innerRef as RefObject) + }, [registerList]) + + const setRefs = (node: HTMLUListElement | null) => { + (innerRef as RefObject).current = node + if (typeof ref === 'function') ref(node) + else if (ref) (ref as RefObject).current = node + } + + const count = context.visibleOptionIds.length + + return ( +
      + {children} +
    • 0 })} + > + {translation('nResultsFound', { count })} +
    • +
    + ) + } +) diff --git a/src/components/user-interaction/Combobox/ComboboxOption.tsx b/src/components/user-interaction/Combobox/ComboboxOption.tsx new file mode 100644 index 00000000..fa5bb8d2 --- /dev/null +++ b/src/components/user-interaction/Combobox/ComboboxOption.tsx @@ -0,0 +1,88 @@ +import type { HTMLAttributes, ReactNode, RefObject } from 'react' +import { forwardRef, useEffect, useId, useRef } from 'react' +import clsx from 'clsx' +import { useComboboxContext } from './ComboboxContext' + +export interface ComboboxOptionProps extends HTMLAttributes { + value: T, + label: string, + disabled?: boolean, +} + +export const ComboboxOption = forwardRef>(function ComboboxOption({ + children, + value, + label, + disabled = false, + id: idProp, + className, + ...restProps +}, ref) { + const context = useComboboxContext() + const { registerOption } = context + const itemRef = useRef(null) + const generatedId = useId() + const optionId = idProp ?? `combobox-option-${generatedId}` + + const resolvedDisplay: ReactNode = children ?? label + + useEffect(() => { + return registerOption({ + id: optionId, + value, + label, + display: resolvedDisplay, + disabled, + ref: itemRef as React.RefObject, + }) + }, [optionId, value, label, resolvedDisplay, disabled, registerOption]) + + useEffect(() => { + if (context.highlightedId === optionId) { + itemRef.current?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' }) + } + }, [context.highlightedId, optionId]) + + const isVisible = context.visibleOptionIds.includes(optionId) + const isHighlighted = context.highlightedId === optionId + + return ( +
  • { + itemRef.current = node + if (typeof ref === 'function') ref(node) + else if (ref) (ref as RefObject).current = node + }} + id={optionId} + hidden={!isVisible} + + role="option" + aria-selected={isHighlighted} + aria-disabled={disabled} + aria-hidden={!isVisible} + + data-name="combobox-option" + data-highlighted={isHighlighted ? '' : undefined} + data-visible={isVisible ? '' : undefined} + data-disabled={disabled ? '' : undefined} + className={clsx(!isVisible && 'hidden', className)} + onClick={(event) => { + if (!disabled) { + context.selectOption(optionId) + restProps.onClick?.(event) + } + }} + onMouseEnter={(event) => { + if (!disabled) { + context.highlightItem(optionId) + restProps.onMouseEnter?.(event) + } + }} + > + {resolvedDisplay} +
  • + ) +}) + +ComboboxOption.displayName = 'ComboboxOption' diff --git a/src/components/user-interaction/Combobox/ComboboxRoot.tsx b/src/components/user-interaction/Combobox/ComboboxRoot.tsx new file mode 100644 index 00000000..fa5eb468 --- /dev/null +++ b/src/components/user-interaction/Combobox/ComboboxRoot.tsx @@ -0,0 +1,135 @@ +import type { ReactNode, RefObject } from 'react' +import { useCallback, useId, useMemo, useState } from 'react' +import { ComboboxContext } from './ComboboxContext' +import type { ComboboxContextConfig, ComboboxContextIds, ComboboxContextLayout, ComboboxContextType, ComboboxOptionType } from './ComboboxContext' +import type { UseComboboxOptions } from './useCombobox' +import { useCombobox } from './useCombobox' +import { DOMUtils } from '@/src/utils/dom' + +export interface ComboboxRootProps extends Omit { + children: ReactNode, + onItemClick?: (value: T) => void, +} + +export function ComboboxRoot({ + children, + onItemClick, + ...hookProps +}: ComboboxRootProps) { + const [options, setOptions] = useState[]>([]) + const [listRef, setListRef] = useState | null>(null) + const generatedId = useId() + const [ids, setIds] = useState({ + trigger: `combobox-${generatedId}`, + listbox: `combobox-${generatedId}-listbox`, + }) + + const registerOption = useCallback( + (option: ComboboxOptionType) => { + setOptions((prev) => { + const next = prev.filter((o) => o.id !== option.id) + next.push(option) + next.sort((a, b) => + DOMUtils.compareDocumentPosition(a.ref.current, b.ref.current)) + return next + }) + return () => + setOptions((prev) => prev.filter((o) => o.id !== option.id)) + }, + [] + ) + + const registerList = useCallback((ref: RefObject) => { + setListRef(() => ref) + return () => setListRef(null) + }, []) + + const hookOptions = useMemo( + () => + options.map((o) => ({ + id: o.id, + label: o.label, + disabled: o.disabled, + })), + [options] + ) + + const state = useCombobox({ ...hookProps, options: hookOptions }) + + const idToOptionMap = useMemo(() => { + return options.reduce((acc, o) => { + acc[o.id] = o + return acc + }, {} as Record>) + }, [options]) + + const selectOption = useCallback( + (id: string) => { + const option = idToOptionMap[id] + if (option) onItemClick?.(option.value as T) + }, + [idToOptionMap, onItemClick] + ) + + const config: ComboboxContextConfig = useMemo( + () => ({ ids, setIds }), + [ids, setIds] + ) + + const layout: ComboboxContextLayout = useMemo( + () => ({ + listRef: listRef ?? { current: null }, + registerList, + }), + [listRef, registerList] + ) + + const search = useMemo( + () => ({ + searchQuery: state.searchQuery, + setSearchQuery: state.setSearchQuery, + }), + [state.searchQuery, state.setSearchQuery] + ) + + const contextValue = useMemo( + () => ({ + highlightedId: state.highlightedId, + options, + visibleOptionIds: state.visibleOptionIds, + idToOptionMap, + registerOption, + selectOption, + highlightFirst: state.highlightFirst, + highlightLast: state.highlightLast, + highlightNext: state.highlightNext, + highlightPrevious: state.highlightPrevious, + highlightItem: state.highlightItem, + config, + layout, + search, + }), + [ + state.highlightedId, + state.visibleOptionIds, + state.highlightFirst, + state.highlightLast, + state.highlightNext, + state.highlightPrevious, + state.highlightItem, + options, + idToOptionMap, + registerOption, + selectOption, + config, + layout, + search, + ] + ) + + return ( + }> + {children} + + ) +} diff --git a/src/components/user-interaction/Combobox/useCombobox.ts b/src/components/user-interaction/Combobox/useCombobox.ts new file mode 100644 index 00000000..f60b588c --- /dev/null +++ b/src/components/user-interaction/Combobox/useCombobox.ts @@ -0,0 +1,110 @@ +import { useCallback, useMemo } from 'react' +import { useListNavigation } from '@/src/hooks/useListNavigation' +import { useControlledState } from '@/src/hooks/useControlledState' +import { useSearch } from '@/src/hooks' + +export interface UseComboboxOption { + id: string, + label?: string, + disabled?: boolean, +} + +export interface UseComboboxOptions { + options: ReadonlyArray, + searchQuery?: string, + onSearchQueryChange?: (query: string) => void, + initialSearchQuery?: string, +} + +export interface UseComboboxState { + searchQuery: string, + highlightedId: string | null, +} + +export interface UseComboboxComputedState { + visibleOptionIds: ReadonlyArray, +} + +export interface UseComboboxActions { + setSearchQuery: (query: string) => void, + highlightFirst: () => void, + highlightLast: () => void, + highlightNext: () => void, + highlightPrevious: () => void, + highlightItem: (id: string) => void, +} + +export interface UseComboboxReturn extends UseComboboxState, UseComboboxComputedState, UseComboboxActions {} + +export function useCombobox({ + options, + searchQuery: controlledSearchQuery, + onSearchQueryChange, + initialSearchQuery = '', +}: UseComboboxOptions): UseComboboxReturn { + const [searchQuery, setSearchQuery] = useControlledState({ + value: controlledSearchQuery, + onValueChange: onSearchQueryChange, + defaultValue: initialSearchQuery, + }) + + const { searchResult: visibleOptions } = useSearch({ + items: options, + searchQuery: searchQuery ?? '', + toTags: useCallback((o: UseComboboxOption) => [o.label], []), + }) + + const visibleOptionIds = useMemo( + () => visibleOptions.map((o) => o.id), + [visibleOptions] + ) + + const enabledOptionIds = useMemo( + () => visibleOptions.filter((o) => !o.disabled).map((o) => o.id), + [visibleOptions] + ) + + const listNav = useListNavigation({ options: enabledOptionIds }) + + const highlightItem = useCallback( + (id: string) => { + if (!enabledOptionIds.includes(id)) return + listNav.highlight(id) + }, + [enabledOptionIds, listNav] + ) + + const state: UseComboboxState = useMemo( + () => ({ + searchQuery: searchQuery ?? '', + highlightedId: listNav.highlightedId, + }), + [searchQuery, listNav.highlightedId] + ) + + const computedState: UseComboboxComputedState = useMemo( + () => ({ visibleOptionIds }), + [visibleOptionIds] + ) + + const actions: UseComboboxActions = useMemo( + () => ({ + setSearchQuery, + highlightFirst: listNav.first, + highlightLast: listNav.last, + highlightNext: listNav.next, + highlightPrevious: listNav.previous, + highlightItem, + }), + [setSearchQuery, listNav.first, listNav.last, listNav.next, listNav.previous, highlightItem] + ) + + return useMemo( + (): UseComboboxReturn => ({ + ...state, + ...computedState, + ...actions, + }), + [state, computedState, actions] + ) +} diff --git a/src/components/user-interaction/IconButton.tsx b/src/components/user-interaction/IconButton.tsx index d7630d8d..edb7398d 100644 --- a/src/components/user-interaction/IconButton.tsx +++ b/src/components/user-interaction/IconButton.tsx @@ -10,7 +10,7 @@ import { useLogOnce } from '@/src/hooks/useLogOnce' */ type IconButtonSize = 'xs' | 'sm' | 'md' | 'lg' | null -type IconButtonColoringStyle = 'outline' | 'solid' | 'text' | 'tonal' | null +type IconButtonColoringStyle = 'outline' | 'solid' | 'text' | 'tonal' | 'tonal-outline' | null @@ -23,7 +23,6 @@ export interface IconButtonBaseProps extends ButtonHTMLAttributes(function IconButtonBase({ @@ -31,7 +30,6 @@ export const IconButtonBase = forwardRef size = 'md', color = 'primary', coloringStyle = 'solid', - allowClickEventPropagation = false, disabled, ...props }, ref) { @@ -44,9 +42,6 @@ export const IconButtonBase = forwardRef onClick={event => { - if(!allowClickEventPropagation) { - event.stopPropagation() - } props.onClick?.(event) }} diff --git a/src/components/user-interaction/MultiSelect/MultiSelect.tsx b/src/components/user-interaction/MultiSelect/MultiSelect.tsx new file mode 100644 index 00000000..807fe597 --- /dev/null +++ b/src/components/user-interaction/MultiSelect/MultiSelect.tsx @@ -0,0 +1,23 @@ +import { forwardRef } from 'react' +import type { MultiSelectRootProps } from './MultiSelectRoot' +import { MultiSelectRoot } from './MultiSelectRoot' +import type { MultiSelectButtonProps } from './MultiSelectButton' +import { MultiSelectButton } from './MultiSelectButton' +import type { MultiSelectContentProps } from './MultiSelectContent' +import { MultiSelectContent } from './MultiSelectContent' + +export interface MultiSelectProps extends MultiSelectRootProps { + contentPanelProps?: MultiSelectContentProps, + buttonProps?: MultiSelectButtonProps, +} + +export const MultiSelect = forwardRef>( + function MultiSelect({ children, contentPanelProps, buttonProps, ...props }: MultiSelectProps, ref) { + return ( + {...props}> + + {children} + + ) + } +) as (props: MultiSelectProps & { ref?: React.Ref }) => React.ReactElement diff --git a/src/components/user-interaction/MultiSelect/MultiSelectButton.tsx b/src/components/user-interaction/MultiSelect/MultiSelectButton.tsx new file mode 100644 index 00000000..55b7cc24 --- /dev/null +++ b/src/components/user-interaction/MultiSelect/MultiSelectButton.tsx @@ -0,0 +1,115 @@ +import type { ComponentPropsWithoutRef, ReactNode } from 'react' +import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' +import { useMultiSelectContext } from './MultiSelectContext' +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import { ExpansionIcon } from '@/src/components/display-and-visualization/ExpansionIcon' +import { MultiSelectOptionDisplayContext } from './MultiSelectOption' + +export interface MultiSelectButtonProps + extends ComponentPropsWithoutRef<'div'> { + placeholder?: ReactNode, + disabled?: boolean, + selectedDisplay?: (values: T[]) => ReactNode, + hideExpansionIcon?: boolean, +} + +export const MultiSelectButton = forwardRef< + HTMLDivElement, + MultiSelectButtonProps +>(function MultiSelectButton( + { + id, + placeholder, + disabled: disabledOverride, + selectedDisplay, + hideExpansionIcon = false, + ...props + }: MultiSelectButtonProps, + ref +) { + const translation = useHightideTranslation() + const context = useMultiSelectContext() + const { config, layout } = context + const { setIds } = config + const { registerTrigger } = layout + + useEffect(() => { + if (id) setIds((prev) => ({ ...prev, trigger: id })) + }, [id, setIds]) + + const innerRef = useRef(null) + useImperativeHandle(ref, () => innerRef.current!) + + useEffect(() => { + const unregister = registerTrigger(innerRef) + return () => unregister() + }, [registerTrigger]) + + const disabled = !!disabledOverride || !!context.disabled + const invalid = context.invalid + const hasValue = context.value.length > 0 + const selectedOptions = context.selectedIds + .map((id) => context.idToOptionMap[id]) + .filter(Boolean) + + return ( +
    { + props.onClick?.(event) + context.toggleIsOpen() + }} + onKeyDown={(event) => { + props.onKeyDown?.(event) + if (disabled) return + switch (event.key) { + case 'Enter': + case ' ': + context.toggleIsOpen() + event.preventDefault() + event.stopPropagation() + break + case 'ArrowDown': + context.setIsOpen(true, 'first') + event.preventDefault() + event.stopPropagation() + break + case 'ArrowUp': + context.setIsOpen(true, 'last') + event.preventDefault() + event.stopPropagation() + break + } + }} + data-name={props['data-name'] ?? 'multi-select-button'} + data-value={hasValue ? '' : undefined} + data-disabled={disabled ? '' : undefined} + data-invalid={invalid ? '' : undefined} + tabIndex={disabled ? -1 : 0} + role="button" + aria-invalid={invalid} + aria-disabled={disabled} + aria-haspopup="dialog" + aria-expanded={context.isOpen} + aria-controls={context.isOpen ? context.config.ids.content : undefined} + > + + {hasValue + ? selectedDisplay?.(context.value) ?? ( +
    + {selectedOptions.map((opt, index) => ( + + {opt.display} + {index < selectedOptions.length - 1 && ,} + + ))} +
    + ) + : placeholder ?? translation('clickToSelect')} +
    + {!hideExpansionIcon && } +
    + ) +}) diff --git a/src/components/user-interaction/MultiSelect/MultiSelectChipDisplay.tsx b/src/components/user-interaction/MultiSelect/MultiSelectChipDisplay.tsx new file mode 100644 index 00000000..f2ebf791 --- /dev/null +++ b/src/components/user-interaction/MultiSelect/MultiSelectChipDisplay.tsx @@ -0,0 +1,134 @@ +import type { HTMLAttributes, ReactNode } from 'react' +import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' +import { useMultiSelectContext } from './MultiSelectContext' +import type { MultiSelectRootProps } from './MultiSelectRoot' +import { MultiSelectRoot } from './MultiSelectRoot' +import type { MultiSelectContentProps } from './MultiSelectContent' +import { MultiSelectContent } from './MultiSelectContent' +import { IconButton } from '@/src/components/user-interaction/IconButton' +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import { XIcon, Plus } from 'lucide-react' + +export type MultiSelectChipDisplayButtonProps = HTMLAttributes & { + disabled?: boolean, + placeholder?: ReactNode, +}; + +export const MultiSelectChipDisplayButton = forwardRef< + HTMLDivElement, + MultiSelectChipDisplayButtonProps +>(function MultiSelectChipDisplayButton({ id, ...props }, ref) { + const translation = useHightideTranslation() + const context = useMultiSelectContext() + const { config, layout } = context + const { setIds } = config + const { registerTrigger } = layout + + useEffect(() => { + if (id) setIds((prev) => ({ ...prev, trigger: id })) + }, [id, setIds]) + + const innerRef = useRef(null) + useImperativeHandle(ref, () => innerRef.current!) + + useEffect(() => { + const unregister = registerTrigger(innerRef) + return () => unregister() + }, [registerTrigger]) + + const disabled = !!props?.disabled || !!context.disabled + const invalid = context.invalid + const selectedOptions = context.selectedIds + .map((oid) => context.idToOptionMap[oid]) + .filter(Boolean) + + return ( +
    { + props.onClick?.(event) + if(event.defaultPrevented) return + context.toggleIsOpen() + }} + data-name={props['data-name'] ?? 'multi-select-chip-display-button'} + data-value={context.value.length > 0 ? '' : undefined} + data-disabled={disabled ? '' : undefined} + data-invalid={invalid ? '' : undefined} + aria-invalid={invalid} + aria-disabled={disabled} + > + {selectedOptions.map((opt) => ( +
    + {opt.display} + { + context.toggleSelection(opt.id, false) + e.preventDefault() + }} + size="sm" + color="negative" + coloringStyle="text" + className="flex-row-0 items-center size-7 p-1" + > + + +
    + ))} + { + event.stopPropagation() + context.toggleIsOpen() + }} + onKeyDown={(event) => { + switch (event.key) { + case 'ArrowDown': + context.setIsOpen(true, 'first') + break + case 'ArrowUp': + context.setIsOpen(true, 'last') + } + }} + tooltip={translation('changeSelection')} + size="md" + color="neutral" + aria-invalid={invalid} + aria-disabled={disabled} + aria-haspopup="dialog" + aria-expanded={context.isOpen} + aria-controls={ + context.isOpen ? context.config.ids.content : undefined + } + className="size-9" + > + + +
    + ) +}) + +export type MultiSelectChipDisplayProps = MultiSelectRootProps & { + contentPanelProps?: MultiSelectContentProps, + chipDisplayProps?: MultiSelectChipDisplayButtonProps, +}; + +export const MultiSelectChipDisplay = forwardRef( + function MultiSelectChipDisplay( + { + children, + contentPanelProps, + chipDisplayProps, + ...props + }: MultiSelectChipDisplayProps, + ref: React.ForwardedRef + ) { + return ( + {...props}> + + {children} + + ) + } +) diff --git a/src/components/user-interaction/MultiSelect/MultiSelectContent.tsx b/src/components/user-interaction/MultiSelect/MultiSelectContent.tsx new file mode 100644 index 00000000..9cd16ccc --- /dev/null +++ b/src/components/user-interaction/MultiSelect/MultiSelectContent.tsx @@ -0,0 +1,150 @@ +import type { ComponentProps } from 'react' +import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react' +import { useMultiSelectContext } from './MultiSelectContext' +import clsx from 'clsx' +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import { PopUp, type PopUpProps } from '@/src/components/layout/popup/PopUp' +import { Input } from '@/src/components/user-interaction/input/Input' +import { Visibility } from '@/src/components/layout/Visibility' + +export interface MultiSelectContentProps extends PopUpProps { + showSearch?: boolean, + searchInputProps?: Omit, 'value' | 'onValueChange'>, +} + +export const MultiSelectContent = forwardRef< + HTMLUListElement, + MultiSelectContentProps +>(function MultiSelectContent( + { id, options, showSearch: showSearchOverride, searchInputProps, ...props }, + ref +) { + const translation = useHightideTranslation() + const innerRef = useRef(null) + const searchInputRef = useRef(null) + useImperativeHandle(ref, () => innerRef.current!) + + const context = useMultiSelectContext() + const { config, highlightNext, highlightPrevious, highlightFirst, highlightLast, highlightedId, handleTypeaheadKey, toggleSelection } = context + const { setIds } = config + + useEffect(() => { + if (id) setIds((prev) => ({ ...prev, content: id })) + }, [id, setIds]) + + const showSearch = showSearchOverride ?? context.search.hasSearch + const listboxAriaLabel = showSearch ? translation('searchResults') : undefined + + const keyHandler = useCallback( + (event: React.KeyboardEvent) => { + switch (event.key) { + case 'ArrowDown': + highlightNext() + event.preventDefault() + break + case 'ArrowUp': + highlightPrevious() + event.preventDefault() + break + case 'Home': + event.preventDefault() + highlightFirst() + break + case 'End': + event.preventDefault() + highlightLast() + break + case 'Enter': + case ' ': + if (showSearch && event.key === ' ') return + if (highlightedId) { + toggleSelection(highlightedId) + event.preventDefault() + } + break + default: + if ( + !showSearch && + !event.ctrlKey && + !event.metaKey && + !event.altKey && + event.key.length === 1 + ) { + handleTypeaheadKey(event.key) + event.preventDefault() + } + break + } + }, + [showSearch, handleTypeaheadKey, toggleSelection, highlightedId, highlightNext, highlightPrevious, highlightFirst, highlightLast] + ) + + return ( + { + context.setIsOpen(false) + props.onClose?.() + }} + aria-labelledby={context.config.ids.trigger} + className={clsx('gap-y-1', props.className)} + > + {showSearch && ( + + )} +
      + {props.children} + +
    • 0, + })} + > + {translation('nResultsFound', { + count: context.visibleOptionIds.length, + })} +
    • +
      +
    +
    + ) +}) diff --git a/src/components/user-interaction/MultiSelect/MultiSelectContext.tsx b/src/components/user-interaction/MultiSelect/MultiSelectContext.tsx new file mode 100644 index 00000000..542ffed1 --- /dev/null +++ b/src/components/user-interaction/MultiSelect/MultiSelectContext.tsx @@ -0,0 +1,81 @@ +import type { Dispatch, ReactNode, RefObject, SetStateAction } from 'react' +import { createContext, useContext } from 'react' +import type { FormFieldInteractionStates } from '@/src/components/form/FieldLayout' +import type { UseMultiSelectFirstHighlightBehavior } from './useMultiSelect' + +export interface MultiSelectOptionType { + id: string, + value: T, + label?: string, + display?: ReactNode, + disabled?: boolean, + ref: RefObject, +} + +export interface MultiSelectContextIds { + trigger: string, + content: string, + listbox: string, + searchInput: string, +} + +export interface MultiSelectContextState extends FormFieldInteractionStates { + value: T[], + options: ReadonlyArray>, + selectedIds: string[], + highlightedId: string | null, + isOpen: boolean, +} + +export interface MultiSelectContextComputedState { + visibleOptionIds: ReadonlyArray, + idToOptionMap: Record>, +} + +export interface MultiSelectContextActions { + registerOption(option: MultiSelectOptionType): () => void, + toggleSelection(id: string, isSelected?: boolean): void, + highlightFirst(): void, + highlightLast(): void, + highlightNext(): void, + highlightPrevious(): void, + highlightItem(id: string): void, + handleTypeaheadKey(key: string): void, + setIsOpen(open: boolean, behavior?: UseMultiSelectFirstHighlightBehavior): void, + toggleIsOpen(behavior?: UseMultiSelectFirstHighlightBehavior): void, +} + +export interface MultiSelectContextLayout { + triggerRef: RefObject, + registerTrigger(element: RefObject): () => void, +} + +export interface MultiSelectContextSearch { + hasSearch: boolean, + searchQuery?: string, + setSearchQuery(query: string): void, +} + +export type MultiSelectIconAppearance = 'left' | 'right' | 'none'; + +export interface MultiSelectContextConfig { + iconAppearance: MultiSelectIconAppearance, + ids: MultiSelectContextIds, + setIds: Dispatch>, +} + +export interface MultiSelectContextType extends MultiSelectContextActions, MultiSelectContextState, MultiSelectContextComputedState { + config: MultiSelectContextConfig, + layout: MultiSelectContextLayout, + search: MultiSelectContextSearch, +} + +const MultiSelectContext = createContext | null>(null) + +export function useMultiSelectContext(): MultiSelectContextType { + const ctx = useContext(MultiSelectContext) + if (!ctx) throw new Error('useMultiSelectContext must be used within MultiSelectRoot') + return ctx as MultiSelectContextType +} + +export { MultiSelectContext } diff --git a/src/components/user-interaction/MultiSelect/MultiSelectOption.tsx b/src/components/user-interaction/MultiSelect/MultiSelectOption.tsx new file mode 100644 index 00000000..1c43dba1 --- /dev/null +++ b/src/components/user-interaction/MultiSelect/MultiSelectOption.tsx @@ -0,0 +1,120 @@ +import clsx from 'clsx' +import { CheckIcon } from 'lucide-react' +import type { HTMLAttributes, ReactNode, RefObject } from 'react' +import { createContext, forwardRef, useContext, useEffect, useId, useRef } from 'react' +import type { MultiSelectIconAppearance } from './MultiSelectContext' +import { useMultiSelectContext } from './MultiSelectContext' + +export type MultiSelectOptionDisplayLocation = 'trigger' | 'list'; + +export const MultiSelectOptionDisplayContext = + createContext(null) + +export function useMultiSelectOptionDisplayLocation(): MultiSelectOptionDisplayLocation { + const context = useContext(MultiSelectOptionDisplayContext) + if (!context) { + throw new Error( + 'useMultiSelectOptionDisplayLocation must be used within a MultiSelectOptionDisplayContext' + ) + } + return context +} + +export interface MultiSelectOptionProps extends HTMLAttributes { + value: T, + label: string, + disabled?: boolean, + iconAppearance?: MultiSelectIconAppearance, +} + +export const MultiSelectOption = forwardRef< + HTMLLIElement, + MultiSelectOptionProps +>(function MultiSelectOption( + { + children, + label, + value, + disabled = false, + iconAppearance, + ...props + }: MultiSelectOptionProps, + ref +) { + const context = useMultiSelectContext() + const { registerOption } = context + const itemRef = useRef(null) + + const display: ReactNode = children ?? label + const iconAppearanceResolved = iconAppearance ?? context.config.iconAppearance + + const generatedId = useId() + const optionId = props?.id ?? 'multi-select-option-' + generatedId + + useEffect(() => { + return registerOption({ + id: optionId, + value, + label, + display, + disabled: Boolean(disabled), + ref: itemRef as React.RefObject, + }) + }, [optionId, value, label, disabled, registerOption, display]) + + const isHighlighted = context.highlightedId === optionId + const isSelected = context.selectedIds.includes(optionId) + const isVisible = context.visibleOptionIds.includes(optionId) + + return ( +
  • { + itemRef.current = node + if (typeof ref === 'function') ref(node) + else if (ref) (ref as RefObject).current = node + }} + id={optionId} + hidden={!isVisible} + role="option" + aria-disabled={disabled} + aria-selected={isSelected} + aria-hidden={!isVisible} + + data-name="multi-select-list-option" + data-highlighted={isHighlighted ? '' : undefined} + data-selected={isSelected ? '' : undefined} + data-disabled={disabled ? '' : undefined} + data-visible={isVisible ? '' : undefined} + + onClick={(event) => { + if (!disabled) { + context.toggleSelection(optionId) + props.onClick?.(event) + } + }} + onMouseEnter={(event) => { + if (!disabled) { + context.highlightItem(optionId) + props.onMouseEnter?.(event) + } + }} + > + {iconAppearanceResolved === 'left' && ( + + )} + + {display} + + {iconAppearanceResolved === 'right' && ( + + )} +
  • + ) +}) diff --git a/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx b/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx new file mode 100644 index 00000000..a265a478 --- /dev/null +++ b/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx @@ -0,0 +1,208 @@ +import type { ReactNode, RefObject } from 'react' +import { useCallback, useEffect, useId, useMemo, useState } from 'react' +import { MultiSelectContext } from './MultiSelectContext' +import type { MultiSelectContextType, MultiSelectIconAppearance, MultiSelectOptionType } from './MultiSelectContext' +import { useMultiSelect } from './useMultiSelect' +import { DOMUtils } from '@/src/utils/dom' +import type { FormFieldDataHandling } from '@/src/components/form/FormField' +import type { FormFieldInteractionStates } from '@/src/components/form/FieldLayout' +import { PopUpContext } from '@/src/components/layout/popup/PopUpContext' + +export interface MultiSelectIds { + trigger: string, + content: string, + listbox: string, + searchInput: string, +} + +export interface MultiSelectRootProps extends Partial>, Partial { + initialValue?: T[], + compareFunction?: (a: T, b: T) => boolean, + initialIsOpen?: boolean, + onClose?: () => void, + showSearch?: boolean, + iconAppearance?: MultiSelectIconAppearance, + children: ReactNode, +} + +export function MultiSelectRoot({ + children, + value, + onValueChange, + onEditComplete, + initialValue, + compareFunction, + initialIsOpen = false, + onClose, + showSearch = true, + iconAppearance = 'right', + invalid = false, + disabled = false, + readOnly = false, + required = false, +}: MultiSelectRootProps) { + const [triggerRef, setTriggerRef] = useState | null>(null) + const [options, setOptions] = useState[]>([]) + const generatedId = useId() + const [ids, setIds] = useState({ + trigger: 'multi-select-' + generatedId, + content: 'multi-select-content-' + generatedId, + listbox: 'multi-select-listbox-' + generatedId, + searchInput: 'multi-select-search-' + generatedId, + }) + + const registerOption = useCallback((item: MultiSelectOptionType) => { + setOptions((prev) => { + const next = prev.filter((o) => o.id !== item.id) + next.push(item) + next.sort((a, b) => + DOMUtils.compareDocumentPosition(a.ref.current, b.ref.current)) + return next + }) + return () => setOptions((prev) => prev.filter((o) => o.id !== item.id)) + }, []) + + const registerTrigger = useCallback((ref: RefObject) => { + setTriggerRef(ref) + return () => setTriggerRef(null) + }, []) + + const compare = useMemo(() => compareFunction ?? Object.is, [compareFunction]) + + const idToOptionMap = useMemo( + () => + options.reduce( + (acc, o) => { + acc[o.id] = o + return acc + }, + {} as Record> + ), + [options] + ) + + const mappedValueIds = useMemo(() => { + if (value == null) return undefined + return value + .map((v) => options.find((o) => compare(o.value, v))?.id) + .filter((id) => id !== undefined) + }, [options, value, compare]) + + const mappedInitialValueIds = useMemo(() => { + if (initialValue == null) return [] + return initialValue + .map((v) => options.find((o) => compare(o.value, v))?.id) + .filter((id) => id !== undefined) + }, [options, initialValue, compare]) + + const onValueChangeStable = useCallback( + (ids: string[]) => { + const values = ids + .map((id) => idToOptionMap[id]?.value) + .filter((v): v is T => v != null) + onValueChange?.(values) + }, + [idToOptionMap, onValueChange] + ) + + const onEditCompleteStable = useCallback( + (ids: string[]) => { + const values = ids + .map((id) => idToOptionMap[id]?.value) + .filter((v): v is T => v != null) + onEditComplete?.(values) + }, + [idToOptionMap, onEditComplete] + ) + + const state = useMultiSelect({ + options: options.map((o) => ({ id: o.id, label: o.label, disabled: o.disabled })), + value: mappedValueIds, + onValueChange: onValueChangeStable, + onEditComplete: onEditCompleteStable, + initialValue: mappedInitialValueIds, + initialIsOpen, + onClose, + }) + const { setSearchQuery } = state + + useEffect(() => { + if (showSearch === false) { + setSearchQuery('') + } + }, [showSearch, setSearchQuery]) + + const contextValue = useMemo((): MultiSelectContextType => { + const valueT = state.value + .map((id) => idToOptionMap[id]?.value) + .filter((v): v is T => v != null) + return { + invalid, + disabled, + readOnly, + required, + selectedIds: state.value, + highlightedId: state.highlightedId, + isOpen: state.isOpen, + options, + visibleOptionIds: state.visibleOptionIds, + idToOptionMap, + value: valueT, + registerOption, + toggleSelection: state.toggleSelection, + highlightFirst: state.highlightFirst, + highlightLast: state.highlightLast, + highlightNext: state.highlightNext, + highlightPrevious: state.highlightPrevious, + highlightItem: state.highlightItem, + handleTypeaheadKey: state.handleTypeaheadKey, + setIsOpen: state.setIsOpen, + toggleIsOpen: state.toggleOpen, + config: { + iconAppearance, + ids, + setIds, + }, + layout: { + triggerRef, + registerTrigger, + }, + search: { + hasSearch: showSearch, + searchQuery: state.searchQuery, + setSearchQuery: state.setSearchQuery, + }, + } + }, [ + invalid, + disabled, + readOnly, + required, + state, + options, + idToOptionMap, + registerOption, + iconAppearance, + ids, + triggerRef, + registerTrigger, + showSearch, + ]) + + return ( + }> + + {children} + + + ) +} diff --git a/src/components/user-interaction/MultiSelect/useMultiSelect.ts b/src/components/user-interaction/MultiSelect/useMultiSelect.ts new file mode 100644 index 00000000..4aead8e8 --- /dev/null +++ b/src/components/user-interaction/MultiSelect/useMultiSelect.ts @@ -0,0 +1,243 @@ +import { + useCallback, + useEffect, + useMemo, + useState +} from 'react' +import { useMultiSelection } from '@/src/hooks/useMultiSelection' +import { useListNavigation } from '@/src/hooks/useListNavigation' +import { useEventCallbackStabilizer } from '@/src/hooks/useEventCallbackStabelizer' +import { useSearch, useTypeAheadSearch } from '@/src/hooks' + +export interface UseMultiSelectOption { + id: string, + label?: string, + disabled?: boolean, +} + +export interface UseMultiSelectOptions { + options: ReadonlyArray, + value?: ReadonlyArray, + onValueChange?: (value: string[]) => void, + onEditComplete?: (value: string[]) => void, + initialValue?: string[], + initialIsOpen?: boolean, + onClose?: () => void, + typeAheadResetMs?: number, +} + +export type UseMultiSelectFirstHighlightBehavior = 'first' | 'last'; + +export interface UseMultiSelectState { + value: string[], + highlightedId: string | null, + isOpen: boolean, + searchQuery: string, + options: ReadonlyArray, +} + +export interface UseMultiSelectComputedState { + visibleOptionIds: ReadonlyArray, +} + +export interface UseMultiSelectActions { + setIsOpen: (isOpen: boolean, behavior?: UseMultiSelectFirstHighlightBehavior) => void, + toggleOpen: (behavior?: UseMultiSelectFirstHighlightBehavior) => void, + setSearchQuery: (query: string) => void, + highlightFirst: () => void, + highlightLast: () => void, + highlightNext: () => void, + highlightPrevious: () => void, + highlightItem: (id: string) => void, + toggleSelection: (id: string, isSelected?: boolean) => void, + setSelection: (ids: string[]) => void, + isSelected: (id: string) => boolean, + handleTypeaheadKey: (key: string) => void, +} + +export interface UseMultiSelectReturn extends UseMultiSelectState, UseMultiSelectComputedState, UseMultiSelectActions {} + +export function useMultiSelect({ + options, + value: controlledValue, + onValueChange, + onEditComplete, + initialValue = [], + onClose, + initialIsOpen = false, + typeAheadResetMs = 500, +}: UseMultiSelectOptions): UseMultiSelectReturn { + const [isOpen, setIsOpen] = useState(initialIsOpen) + const [searchQuery, setSearchQuery] = useState('') + + const selectionOptions = useMemo( + () => options.map((o) => ({ id: o.id, disabled: o.disabled })), + [options] + ) + + const { selection, toggleSelection, setSelection, isSelected } = useMultiSelection({ + options: selectionOptions, + value: controlledValue, + onSelectionChange: (ids) => onValueChange?.(Array.from(ids)), + initialSelection: initialValue ?? [], + isControlled: controlledValue !== undefined, + }) + + const editCompleteStable = useEventCallbackStabilizer(onEditComplete) + const onCloseStable = useEventCallbackStabilizer(onClose) + + const { searchResult: visibleOptions } = useSearch({ + items: options, + searchQuery, + toTags: useCallback((o: UseMultiSelectOption) => [o.label ?? ''], []), + }) + + const visibleOptionIds = useMemo( + () => visibleOptions.map((o) => o.id), + [visibleOptions] + ) + + const enabledOptions = useMemo( + () => visibleOptions.filter((o) => !o.disabled), + [visibleOptions] + ) + + const listNav = useListNavigation({ + options: enabledOptions.map((o) => o.id), + initialValue: selection[0] ?? null, + }) + const { highlight: listNavHighlight } = listNav + + const typeAhead = useTypeAheadSearch({ + options: enabledOptions, + resetTimer: typeAheadResetMs, + toString: (o) => o.label ?? '', + onResultChange: useCallback( + (option: UseMultiSelectOption | null) => { + if (option) listNav.highlight(option.id) + }, + [listNav] + ), + }) + const { reset: typeAheadReset, addToTypeAhead } = typeAhead + + useEffect(() => { + if (!isOpen) typeAheadReset() + }, [isOpen, typeAheadReset]) + + const highlightItem = useCallback((id: string) => { + if (!enabledOptions.some((o) => o.id === id)) return + listNavHighlight(id) + }, [enabledOptions, listNavHighlight]) + + const toggleSelectionValue = useCallback((id: string, newIsSelected?: boolean) => { + const next = newIsSelected ?? !isSelected(id) + if (next) { + toggleSelection(id) + } else { + setSelection(selection.filter((s) => s !== id)) + } + highlightItem(id) + }, [toggleSelection, setSelection, highlightItem, isSelected, selection]) + + const setIsOpenWrapper = useCallback( + (open: boolean, behavior?: UseMultiSelectFirstHighlightBehavior) => { + setIsOpen(open) + behavior = behavior ?? 'first' + if (open) { + if (enabledOptions.length > 0) { + let selected: UseMultiSelectOption | undefined + if (behavior === 'first') { + selected = enabledOptions.find((o) => isSelected(o.id)) + selected ??= enabledOptions[0] + } else if (behavior === 'last') { + selected = [...enabledOptions] + .reverse() + .find((o) => isSelected(o.id)) + selected ??= enabledOptions[enabledOptions.length - 1] + } + if (selected) highlightItem(selected.id) + } + } else { + setSearchQuery('') + onCloseStable?.() + editCompleteStable?.(Array.from(selection)) + } + }, + [ + selection, isSelected, + highlightItem, + onCloseStable, + editCompleteStable, + enabledOptions, + ] + ) + + const toggleOpenWrapper = useCallback( + (behavior?: UseMultiSelectFirstHighlightBehavior) => { + setIsOpenWrapper(!isOpen, behavior) + }, + [isOpen, setIsOpenWrapper] + ) + + const state: UseMultiSelectState = useMemo( + () => ({ + value: [...selection], + highlightedId: listNav.highlightedId, + isOpen, + searchQuery, + options, + }), + [ + selection, + listNav.highlightedId, + isOpen, + searchQuery, + options, + ] + ) + + const computedState: UseMultiSelectComputedState = useMemo( + () => ({ visibleOptionIds }), + [visibleOptionIds] + ) + + const actions: UseMultiSelectActions = useMemo( + () => ({ + setIsOpen: setIsOpenWrapper, + toggleOpen: toggleOpenWrapper, + setSearchQuery, + highlightFirst: listNav.first, + highlightLast: listNav.last, + highlightNext: listNav.next, + highlightPrevious: listNav.previous, + highlightItem, + toggleSelection: toggleSelectionValue, + setSelection: setSelection, + isSelected, + handleTypeaheadKey: addToTypeAhead, + }), + [ + setIsOpenWrapper, + toggleOpenWrapper, + listNav.first, + listNav.last, + listNav.next, + listNav.previous, + highlightItem, + toggleSelectionValue, + setSelection, + isSelected, + addToTypeAhead, + ] + ) + + return useMemo( + (): UseMultiSelectReturn => ({ + ...state, + ...computedState, + ...actions, + }), + [state, computedState, actions] + ) +} diff --git a/src/components/user-interaction/Select/Select.tsx b/src/components/user-interaction/Select/Select.tsx new file mode 100644 index 00000000..f9d030be --- /dev/null +++ b/src/components/user-interaction/Select/Select.tsx @@ -0,0 +1,36 @@ +import type { ReactNode, JSX } from 'react' +import { forwardRef } from 'react' +import type { SelectRootProps } from './SelectRoot' +import { SelectRoot } from './SelectRoot' +import type { SelectButtonProps } from './SelectButton' +import { SelectButton } from './SelectButton' +import type { SelectContentProps } from './SelectContent' +import { SelectContent } from './SelectContent' +import type { SelectOptionType } from './SelectContext' + +export type SelectProps = SelectRootProps & { + contentPanelProps?: SelectContentProps, + buttonProps?: Omit, 'selectedDisplay'> & { + selectedDisplay?: (value: SelectOptionType | null) => ReactNode, + } & { [key: string]: unknown }, +}; + +export const Select = forwardRef>(function Select( + { children, contentPanelProps, buttonProps, ...props }: SelectProps, + ref +) { + + return ( + {...props}> + | null) => { + if (!buttonProps?.selectedDisplay) return undefined + return buttonProps.selectedDisplay(value as SelectOptionType) + }} + /> + {children} + + ) +}) as (props: SelectProps & React.RefAttributes) => JSX.Element diff --git a/src/components/user-interaction/Select/SelectButton.tsx b/src/components/user-interaction/Select/SelectButton.tsx new file mode 100644 index 00000000..3751a790 --- /dev/null +++ b/src/components/user-interaction/Select/SelectButton.tsx @@ -0,0 +1,103 @@ +import type { ComponentPropsWithoutRef, ReactNode } from 'react' +import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' +import type { SelectOptionType } from './SelectContext' +import { useSelectContext } from './SelectContext' +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import { ExpansionIcon } from '@/src/components/display-and-visualization/ExpansionIcon' +import { SelectOptionDisplayContext } from './SelectOption' + +export interface SelectButtonProps extends ComponentPropsWithoutRef<'div'> { + placeholder?: ReactNode, + disabled?: boolean, + selectedDisplay?: (value: SelectOptionType | null) => ReactNode, + hideExpansionIcon?: boolean, +} + +export const SelectButton = forwardRef>( + function SelectButton( + { + id, + placeholder, + disabled: disabledOverride, + selectedDisplay, + hideExpansionIcon = false, + ...props + }: SelectButtonProps, + ref + ) { + const translation = useHightideTranslation() + const context = useSelectContext() + const { config, layout } = context + const { setIds } = config + const { registerTrigger } = layout + + useEffect(() => { + if (id) setIds((prev) => ({ ...prev, trigger: id })) + }, [id, setIds]) + + const innerRef = useRef(null) + useImperativeHandle(ref, () => innerRef.current!) + + useEffect(() => { + const unregister = registerTrigger(innerRef) + return () => unregister() + }, [registerTrigger]) + + const disabled = !!disabledOverride || !!context.disabled + const invalid = context.invalid + const hasValue = context.selectedId !== null + const selectedOption = context.idToOptionMap[context.selectedId] ?? null + + return ( +
    { + props.onClick?.(event) + context.toggleIsOpen() + }} + onKeyDown={(event) => { + props.onKeyDown?.(event) + if (disabled) return + switch (event.key) { + case 'Enter': + case ' ': + context.toggleIsOpen() + event.preventDefault() + event.stopPropagation() + break + case 'ArrowDown': + context.setIsOpen(true, 'first') + event.preventDefault() + event.stopPropagation() + break + case 'ArrowUp': + context.setIsOpen(true, 'last') + event.preventDefault() + event.stopPropagation() + break + } + }} + data-name={props['data-name'] ?? 'select-button'} + data-value={hasValue ? '' : undefined} + data-disabled={disabled ? '' : undefined} + data-invalid={invalid ? '' : undefined} + tabIndex={disabled ? -1 : 0} + role="button" + aria-invalid={invalid} + aria-disabled={disabled} + aria-haspopup="dialog" + aria-expanded={context.isOpen} + aria-controls={context.isOpen ? context.config.ids.content : undefined} + > + + {hasValue + ? selectedDisplay?.(selectedOption) ?? (selectedOption.display) + : placeholder ?? translation('clickToSelect')} + + {!hideExpansionIcon && } +
    + ) + } +) diff --git a/src/components/user-interaction/Select/SelectContent.tsx b/src/components/user-interaction/Select/SelectContent.tsx new file mode 100644 index 00000000..23dfcf11 --- /dev/null +++ b/src/components/user-interaction/Select/SelectContent.tsx @@ -0,0 +1,135 @@ +import type { ComponentProps } from 'react' +import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react' +import { useSelectContext } from './SelectContext' +import clsx from 'clsx' +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import { PopUp, type PopUpProps } from '@/src/components/layout/popup/PopUp' +import { Input } from '@/src/components/user-interaction/input/Input' +import { Visibility } from '@/src/components/layout/Visibility' + +export interface SelectContentProps extends PopUpProps { + showSearch?: boolean, + searchInputProps?: Omit, 'value' | 'onValueChange'>, +} + +export const SelectContent = forwardRef(function SelectContent({ + id, options, showSearch: showSearchOverride, searchInputProps, ...props +}, ref) { + const translation = useHightideTranslation() + const innerRef = useRef(null) + const searchInputRef = useRef(null) + useImperativeHandle(ref, () => innerRef.current!) + + const context = useSelectContext() + const { config, handleTypeaheadKey, toggleSelection, highlightNext, highlightPrevious, highlightFirst, highlightLast, highlightedId } = context + const { setIds } = config + + useEffect(() => { + if (id) setIds((prev) => ({ ...prev, content: id })) + }, [id, setIds]) + + const showSearch = showSearchOverride ?? context.search.hasSearch + const listboxAriaLabel = showSearch ? translation('searchResults') : undefined + + const keyHandler = useCallback( + (event: React.KeyboardEvent) => { + switch (event.key) { + case 'ArrowDown': + highlightNext() + event.preventDefault() + break + case 'ArrowUp': + highlightPrevious() + event.preventDefault() + break + case 'Home': + event.preventDefault() + highlightFirst() + break + case 'End': + event.preventDefault() + highlightLast() + break + case 'Enter': + case ' ': + if (showSearch && event.key === ' ') return + if (highlightedId) { + toggleSelection(highlightedId) + event.preventDefault() + } + break + default: + if (!showSearch && !event.ctrlKey && !event.metaKey && !event.altKey && event.key.length === 1) { + handleTypeaheadKey(event.key) + event.preventDefault() + } + break + } + }, + [showSearch, handleTypeaheadKey, toggleSelection, highlightedId, highlightNext, highlightPrevious, highlightFirst, highlightLast] + ) + + return ( + { + context.setIsOpen(false) + props.onClose?.() + }} + aria-labelledby={context.config.ids.trigger} + className={clsx('gap-y-1', props.className)} + > + {showSearch && ( + + )} +
      + {props.children} + +
    • 0 })} + > + {translation('nResultsFound', { count: context.visibleOptionIds.length })} +
    • +
      +
    +
    + ) +}) diff --git a/src/components/user-interaction/Select/SelectContext.tsx b/src/components/user-interaction/Select/SelectContext.tsx new file mode 100644 index 00000000..00d64a64 --- /dev/null +++ b/src/components/user-interaction/Select/SelectContext.tsx @@ -0,0 +1,78 @@ +import type { Dispatch, ReactNode, RefObject, SetStateAction } from 'react' +import { createContext, useContext } from 'react' +import type { UseSelectFirstHighlightBehavior } from './useSelect' +import type { FormFieldInteractionStates } from '../../form/FieldLayout' + +export interface SelectOptionType { + id: string, + value: T, + label?: string, + display?: ReactNode, + disabled?: boolean, + ref: RefObject, +} + +export interface SelectContextIds { + trigger: string, + content: string, + listbox: string, + searchInput: string, +} + +export interface SelectContextState extends FormFieldInteractionStates { + selectedId: string | null, + options: ReadonlyArray>, + highlightedId: string | null, + isOpen: boolean, +} + +export interface SelectContextComputedState { + visibleOptionIds: ReadonlyArray, + idToOptionMap: Record>, +} + +export interface SelectContextActions { + registerOption(option: SelectOptionType): () => void, + toggleSelection(id: string): void, + highlightFirst(): void, + highlightLast(): void, + highlightNext(): void, + highlightPrevious(): void, + highlightItem(id: string): void, + handleTypeaheadKey(key: string): void, + setIsOpen(open: boolean, behavior?: UseSelectFirstHighlightBehavior): void, + toggleIsOpen(behavior?: UseSelectFirstHighlightBehavior): void, +} + +export interface SelectContextLayout { + triggerRef: RefObject, + registerTrigger(element: RefObject): () => void, +} + +export interface SelectContextSearch { + hasSearch: boolean, + searchQuery?: string, + setSearchQuery(query: string): void, +} + +export type SelectIconAppearance = 'left' | 'right' | 'none'; + +export interface SelectContextConfig { + iconAppearance: SelectIconAppearance, + ids: SelectContextIds, + setIds: Dispatch>, +} + +export interface SelectContextType extends SelectContextActions, SelectContextState, SelectContextComputedState { + config: SelectContextConfig, + layout: SelectContextLayout, + search: SelectContextSearch, +} + +export const SelectContext = createContext | null>(null) + +export function useSelectContext(): SelectContextType { + const ctx = useContext(SelectContext) + if (!ctx) throw new Error('useSelectContext must be used within SelectRoot') + return ctx as SelectContextType +} diff --git a/src/components/user-interaction/Select/SelectOption.tsx b/src/components/user-interaction/Select/SelectOption.tsx new file mode 100644 index 00000000..66d770d3 --- /dev/null +++ b/src/components/user-interaction/Select/SelectOption.tsx @@ -0,0 +1,109 @@ +import clsx from 'clsx' +import { CheckIcon } from 'lucide-react' +import type { HTMLAttributes, ReactNode, RefObject } from 'react' +import { createContext, forwardRef, useContext, useEffect, useId, useRef } from 'react' +import type { SelectIconAppearance } from './SelectContext' +import { useSelectContext } from './SelectContext' + +export type SelectOptionDisplayLocation = 'trigger' | 'list'; + +export const SelectOptionDisplayContext = createContext(null) + +export function useSelectOptionDisplayLocation(): SelectOptionDisplayLocation { + const context = useContext(SelectOptionDisplayContext) + if (!context) { + throw new Error('useSelectOptionDisplayLocation must be used within a SelectOptionDisplayContext') + } + return context +} + +export interface SelectOptionProps extends HTMLAttributes { + value: T, + label: string, + disabled?: boolean, + iconAppearance?: SelectIconAppearance, +} + +export const SelectOption = forwardRef>(function SelectOption({ + children, + label, + value, + disabled = false, + iconAppearance, + ...props +}: SelectOptionProps, ref) { + const context= useSelectContext() + const { registerOption } = context + const itemRef = useRef(null) + + const display: ReactNode = children ?? label + const iconAppearanceResolved = iconAppearance ?? context.config.iconAppearance + + const generatedId = useId() + const optionId = props?.id ?? 'select-option-' + generatedId + + useEffect(() => { + return registerOption({ + id: optionId, + value, + label, + display, + disabled: disabled, + ref: itemRef as React.RefObject, + }) + }, [value, label, disabled, registerOption, display, optionId]) + + const isHighlighted = context.highlightedId === optionId + const isSelected = context.selectedId === optionId + const isVisible = context.visibleOptionIds.includes(optionId) + + return ( +
  • { + itemRef.current = node + if (typeof ref === 'function') ref(node) + else if (ref) (ref as RefObject).current = node + }} + id={optionId} + hidden={!isVisible} + role="option" + aria-disabled={disabled} + aria-selected={isSelected} + aria-hidden={!isVisible} + + data-name="select-list-option" + data-highlighted={isHighlighted ? '' : undefined} + data-selected={isSelected ? '' : undefined} + data-disabled={disabled ? '' : undefined} + data-visible={isVisible ? '' : undefined} + + onClick={(event) => { + if (!disabled) { + context.toggleSelection(optionId) + props.onClick?.(event) + } + }} + onMouseEnter={(event) => { + if (!disabled) { + context.highlightItem(optionId) + props.onMouseEnter?.(event) + } + }} + > + {iconAppearanceResolved === 'left' && context.selectedId !== null && ( + + )} + {display} + {iconAppearanceResolved === 'right' && context.selectedId !== null && ( + + )} +
  • + ) +}) diff --git a/src/components/user-interaction/Select/SelectRoot.tsx b/src/components/user-interaction/Select/SelectRoot.tsx new file mode 100644 index 00000000..e56802b0 --- /dev/null +++ b/src/components/user-interaction/Select/SelectRoot.tsx @@ -0,0 +1,198 @@ +import type { ReactNode, RefObject } from 'react' +import { useCallback, useEffect, useId, useMemo, useState } from 'react' +import { SelectContext } from './SelectContext' +import type { SelectContextConfig, SelectContextLayout, SelectOptionType } from './SelectContext' +import { useSelect } from '@/src/components/user-interaction/Select/useSelect' +import { DOMUtils } from '@/src/utils/dom' +import type { FormFieldDataHandling } from '@/src/components/form/FormField' +import { useEventCallbackStabilizer } from '@/src/hooks/useEventCallbackStabelizer' +import type { FormFieldInteractionStates } from '@/src/components/form/FieldLayout' +import { PopUpContext } from '@/src/components/layout/popup/PopUpContext' + +export interface SelectIds { + trigger: string, + content: string, + listbox: string, + searchInput: string, +} + +export interface SelectRootProps extends Partial>, Partial { + value?: T | null, + initialValue?: T | null, + compareFunction?: (a: T | null, b: T | null) => boolean, + initialIsOpen?: boolean, + onClose?: () => void, + onIsOpenChange?: (isOpen: boolean) => void, + showSearch?: boolean, + iconAppearance?: 'left' | 'right' | 'none', + children: ReactNode, +} + +export function SelectRoot({ + children, + value, + onValueChange, + onEditComplete, + initialValue, + compareFunction, + initialIsOpen = false, + onClose, + onIsOpenChange, + showSearch = true, + iconAppearance = 'right', + invalid = false, + disabled = false, + readOnly = false, + required = false, +}: SelectRootProps) { + const [triggerRef, setTriggerRef] = useState | null>(null) + const [options, setOptions] = useState[]>([]) + const generatedId = useId() + const [ids, setIds] = useState({ + trigger: 'select-' + generatedId, + content: 'select-content-' + generatedId, + listbox: 'select-listbox-' + generatedId, + searchInput: 'select-search-' + generatedId, + }) + + + const registerOption = useCallback( + (item: SelectOptionType) => { + setOptions((prev) => { + const next = prev.filter((o) => o.value !== item.value) + next.push(item) + next.sort((a, b) => + DOMUtils.compareDocumentPosition(a.ref.current, b.ref.current)) + return next + }) + return () => + setOptions((prev) => prev.filter((o) => o.value !== item.value)) + }, + [] + ) + + const registerTrigger = useCallback((ref: RefObject) => { + setTriggerRef(ref) + return () => { + setTriggerRef(null) + } + }, []) + + const compare = useMemo(() => compareFunction ?? Object.is, [compareFunction]) + + const idToOptionMap = useMemo(() => { + return options.reduce((acc, o) => { + acc[o.id] = o + return acc + }, {} as Record>) + }, [options]) + + const mappedValueId = useMemo(() => { + if(value === undefined) return undefined + return options.find((o) => compare(o.value, value))?.id ?? null + }, [options, value, compare]) + + const mappedInitialValueId = useMemo(() => { + if(initialValue === undefined) return undefined + return options.find((o) => compare(o.value, initialValue))?.id ?? null + }, [options, initialValue, compare]) + + const onValueChangeStable = useEventCallbackStabilizer(onValueChange) + const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete) + const onIsOpenChangeStable = useEventCallbackStabilizer(onIsOpenChange) + + const onValueChangeWrapper = useCallback((value: string) => { + const option = idToOptionMap[value] + if(option === undefined) { + console.warn(`Attempted to select an option ${value} that is not valid`) + return + } + onValueChangeStable(option.value) + }, [onValueChangeStable, idToOptionMap]) + + const onEditCompleteWrapper = useCallback((value: string) => { + const option = idToOptionMap[value] + if(option === undefined) { + console.warn(`Attempted to edit complete an option ${value} that is not valid`) + return + } + onEditCompleteStable(option.value) + }, [onEditCompleteStable, idToOptionMap]) + + + const state = useSelect({ + value: mappedValueId, + initialValue: mappedInitialValueId, + onValueChange: onValueChangeWrapper, + onEditComplete: onEditCompleteWrapper, + options, + initialIsOpen, + onClose, + onIsOpenChange: onIsOpenChangeStable, + }) + const { setSearchQuery } = state + + useEffect(() => { + if(showSearch === false) { + setSearchQuery('') + } + }, [showSearch, setSearchQuery]) + + const config: SelectContextConfig = useMemo(() => ({ + iconAppearance, + ids, + setIds, + }), [iconAppearance, ids, setIds]) + + const layout: SelectContextLayout = useMemo(() => ({ + triggerRef, + registerTrigger, + }), [triggerRef, registerTrigger]) + + return ( + + + {children} + + + ) +} diff --git a/src/components/user-interaction/Select/useSelect.ts b/src/components/user-interaction/Select/useSelect.ts new file mode 100644 index 00000000..b5c0a0e8 --- /dev/null +++ b/src/components/user-interaction/Select/useSelect.ts @@ -0,0 +1,187 @@ +import { + useCallback, + useEffect, + useMemo, + useState +} from 'react' +import { useSingleSelection } from '@/src/hooks/useSingleSelection' +import { useListNavigation } from '@/src/hooks/useListNavigation' +import { useEventCallbackStabilizer } from '@/src/hooks/useEventCallbackStabelizer' +import { useSearch, useTypeAheadSearch } from '@/src/hooks' + +export interface UseSelectOption { + id: string, + label?: string, + disabled?: boolean, +} + +export interface UseSelectOptions { + options: ReadonlyArray, + value?: string | null, + initialValue?: string | null, + initialIsOpen?: boolean, + onValueChange?: (value: string) => void, + onEditComplete?: (value: string) => void, + onClose?: () => void, + onIsOpenChange?: (isOpen: boolean) => void, + typeAheadResetMs?: number, +} + +export type UseSelectFirstHighlightBehavior = 'first' | 'last'; + +export interface UseSelectState { + value: string | null, + highlightedValue: string | undefined, + isOpen: boolean, + searchQuery: string, + options: ReadonlyArray, +} + +export interface UseSelectComputedState { + visibleOptionIds: ReadonlyArray, +} + +export interface UseSelectActions { + setIsOpen: (isOpen: boolean, behavior?: UseSelectFirstHighlightBehavior) => void, + toggleOpen: (behavior?: UseSelectFirstHighlightBehavior) => void, + setSearchQuery: (query: string) => void, + highlightFirst: () => void, + highlightLast: () => void, + highlightNext: () => void, + highlightPrevious: () => void, + highlightItem: (value: string) => void, + selectValue: (value: string) => void, + handleTypeaheadKey: (key: string) => void, +} + +export interface UseSelectReturn extends UseSelectState, UseSelectComputedState, UseSelectActions {} + +export function useSelect({ + options, + value: controlledValue, + onValueChange, + onEditComplete, + initialValue = null, + onClose, + onIsOpenChange, + initialIsOpen = false, + typeAheadResetMs = 500, +}: UseSelectOptions): UseSelectReturn { + const [isOpen, setIsOpen] = useState(initialIsOpen) + const [searchQuery, setSearchQuery] = useState('') + + const onValueChangeStable = useEventCallbackStabilizer(onValueChange) + const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete) + const onCloseStable = useEventCallbackStabilizer(onClose) + const onIsOpenChangeStable = useEventCallbackStabilizer(onIsOpenChange) + + const onSelectionChangeWrapper = useCallback((id: string | null) => { + if(id === null) return + onValueChangeStable(id) + onEditCompleteStable(id) + setIsOpen(false) + }, [onValueChangeStable, onEditCompleteStable, setIsOpen]) + + const { selection, selectValue } = useSingleSelection({ + options: options, + selection: controlledValue, + onSelectionChange: onSelectionChangeWrapper, + initialSelection: initialValue, + }) + + const { searchResult: visibleOptions } = useSearch({ + items: options, + searchQuery, + toTags: useCallback((o: UseSelectOption) => [o.label], []), + }) + + const visibleOptionIds = useMemo(() => visibleOptions.map((o) => o.id), [visibleOptions]) + + const enabledOptions = useMemo(() => visibleOptions.filter((o) => !o.disabled), [visibleOptions]) + + const { + highlightedId, + highlight: listNavHighlight, + first: listNavFirst, + last: listNavLast, + next: listNavNext, + previous: listNavPrevious, + } = useListNavigation({ + options: enabledOptions.map((o) => o.id), + initialValue: selection, + }) + + const { addToTypeAhead, reset: typeAheadReset } = useTypeAheadSearch({ + options: enabledOptions, + resetTimer: typeAheadResetMs, + toString: (o) => o.label ?? '', + onResultChange: useCallback((option: UseSelectOption | null) => { + if (option) listNavHighlight(option.id) + }, [listNavHighlight]), + }) + + useEffect(() => { + if (!isOpen) typeAheadReset() + }, [isOpen, typeAheadReset]) + + const state: UseSelectState = useMemo(() => ({ + value: selection, + highlightedValue: highlightedId, + isOpen, + searchQuery, + options, + }), [selection, highlightedId, isOpen, searchQuery, options]) + + const computedState: UseSelectComputedState = useMemo(() => ({ + visibleOptionIds, + }), [visibleOptionIds]) + + const highlightItem = useCallback((value: string) => { + if (!enabledOptions.some((o) => o.id === value)) return + listNavHighlight(value) + }, [enabledOptions, listNavHighlight]) + + const setIsOpenWrapper = useCallback((isOpen: boolean, behavior?: UseSelectFirstHighlightBehavior) => { + behavior = behavior ?? 'first' + if(isOpen) { + if(selection == null) { + if(behavior === 'first') { + listNavFirst() + } else if (behavior === 'last') { + listNavLast() + } + } else { + highlightItem(selection) + } + } else { + setSearchQuery('') + onCloseStable?.() + } + setIsOpen(isOpen) + onIsOpenChangeStable(isOpen) + }, [setIsOpen, highlightItem, onCloseStable, setSearchQuery, onIsOpenChangeStable, selection, listNavFirst, listNavLast]) + + const toggleOpenWrapper = useCallback((behavior?: UseSelectFirstHighlightBehavior) => { + const next = !isOpen + setIsOpenWrapper(next, behavior) + }, [isOpen, setIsOpenWrapper]) + + const actions: UseSelectActions = useMemo(() => ({ + selectValue: (id: string) => selectValue(id), + setIsOpen: setIsOpenWrapper, + toggleOpen: toggleOpenWrapper, + setSearchQuery, + highlightFirst: listNavFirst, + highlightLast: listNavLast, + highlightNext: listNavNext, + highlightPrevious: listNavPrevious, + highlightItem, + handleTypeaheadKey: addToTypeAhead, + }), [selectValue, setIsOpenWrapper, listNavFirst, listNavLast, listNavNext, listNavPrevious, highlightItem, addToTypeAhead, toggleOpenWrapper]) + + return useMemo(() => ({ + ...state, + ...computedState, + ...actions, + }), [state, computedState, actions]) +} diff --git a/src/components/user-interaction/data/FilterList.tsx b/src/components/user-interaction/data/FilterList.tsx new file mode 100644 index 00000000..ee5b9381 --- /dev/null +++ b/src/components/user-interaction/data/FilterList.tsx @@ -0,0 +1,178 @@ +import { useMemo, useState, type ReactNode } from 'react' +import type { FilterValue } from './filter-function' +import { FilterValueUtils, useFilterValueTranslation } from './filter-function' +import { DataTypeUtils, type DataType } from './data-types' +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import { PlusIcon } from 'lucide-react' +import { PopUpRoot } from '../../layout/popup/PopUpRoot' +import { PopUp } from '../../layout/popup/PopUp' +import { PopUpOpener } from '../../layout/popup/PopUpOpener' +import { Button } from '../Button' +import { FilterPopUp } from './FilterPopUp' +import { Combobox } from '@/src/components/user-interaction/Combobox/Combobox' +import { ComboboxOption } from '@/src/components/user-interaction/Combobox/ComboboxOption' +import { PopUpContext } from '../../layout/popup/PopUpContext' +import { ExpansionIcon } from '../../display-and-visualization/ExpansionIcon' +import { FilterOperatorUtils } from './FilterOperator' + +export interface IdentifierFilterValue extends FilterValue { + id: string, +} + +export interface FilterListPopUpBuilderProps { + value: FilterValue, + onValueChange: (value: FilterValue) => void, + onRemove: () => void, + dataType: DataType, + tags: ReadonlyArray<{ tag: string, label: string, display?: ReactNode }>, + name: string, + isOpen: boolean, + close: () => void, +} + +export interface FilterListItem { + id: string, + label: string, + dataType: DataType, + tags: ReadonlyArray<{ tag: string, label: string, display?: ReactNode }>, + popUpBuilder?: (props: FilterListPopUpBuilderProps) => ReactNode, +} + +export interface FilterListProps { + value: IdentifierFilterValue[], + onValueChange: (value: IdentifierFilterValue[]) => void, + availableItems: FilterListItem[], +} + +export const FilterList = ({ value, onValueChange, availableItems }: FilterListProps) => { + const translation = useHightideTranslation() + const filterValueToLabel = useFilterValueTranslation() + const activeIds = useMemo(() => value.map((item) => item.id), [value]) + const inactiveItems = useMemo(() => availableItems.filter((item) => !activeIds.includes(item.id)).sort((a, b) => a.label.localeCompare(b.label)), [availableItems, activeIds]) + const itemRecord = useMemo(() => availableItems.reduce((acc, item) => { + acc[item.id] = item + return acc + }, {} as Record), [availableItems]) + const [editState, setEditState] = useState(undefined) + + const valueWithEditState = useMemo(() => { + let foundEditValue = false + for(const item of value) { + if(item.id === editState?.id) { + foundEditValue = true + break + } + } + if(!foundEditValue && editState) { + return [...value, editState] + } + return value + }, [value, editState]) + + return ( +
    + + + {({ toggleOpen, props }) => ( + + )} + + + + {({ setIsOpen }) => ( + { + const item = itemRecord[id] + if(!item) return + const newValue: IdentifierFilterValue = { + id: item.id, + dataType: item.dataType, + operator: FilterOperatorUtils.getDefaultOperator(item.dataType), + parameter: {} + } + setEditState(newValue) + setIsOpen(false) + }} + > + {inactiveItems.map(item => ( + + {DataTypeUtils.toIcon(item.dataType)} + {item.label} + + ))} + + )} + + + + {valueWithEditState.map(filterValue => { + const item = itemRecord[filterValue.id] + if(!item) return null + return ( + { + if (!isOpen) { + const isEditStateValid = editState ? FilterValueUtils.isValid(editState) : false + if(isEditStateValid) { + onValueChange(valueWithEditState.map(prevItem => prevItem.id === filterValue.id ? { ...prevItem, ...editState } : prevItem)) + } + setEditState(undefined) + } else { + const valueItem = value.find(prevItem => prevItem.id === filterValue.id) + if(!valueItem) return + setEditState({ ...valueItem }) + } + }} + > + + {({ toggleOpen, props, isOpen }) => ( + + )} + + {item.popUpBuilder ? ( + + {({ isOpen, setIsOpen }) => ( + item.popUpBuilder({ + value: editState?.id === filterValue.id ? editState : filterValue, + onValueChange: value => setEditState({ ...filterValue, ...value }), + onRemove: () => { + onValueChange(value.filter(prevItem => prevItem.id !== filterValue.id)) + setEditState(undefined) + }, + dataType: item.dataType, + tags: item.tags, + name: item.label, + isOpen, + close: () => setIsOpen(false), + }) + )} + + ) : ( + { + setEditState({ ...filterValue, ...value }) + }} + onRemove={() => { + onValueChange(value.filter(prevItem => prevItem.id !== filterValue.id)) + setEditState(undefined) + }} + /> + )} + + ) + })} +
    + ) +} \ No newline at end of file diff --git a/src/components/user-interaction/data/FilterOperator.tsx b/src/components/user-interaction/data/FilterOperator.tsx new file mode 100644 index 00000000..f4d5eb70 --- /dev/null +++ b/src/components/user-interaction/data/FilterOperator.tsx @@ -0,0 +1,183 @@ +import { + ChevronRight, + ChevronLeft, + CheckCircle2, + XCircle, + Equal, + EqualNot, + SearchCheck, + SearchX, + CircleDashed, + CircleDot +} from 'lucide-react' +import type { DataType } from '@/src/components/user-interaction/data/data-types' +import type { ReactNode } from 'react' + +const filterOperators = [ + 'equals', 'notEquals', 'contains', 'notContains', 'startsWith', 'endsWith', + 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'between', 'notBetween', + 'isTrue', 'isFalse', + 'isUndefined', 'isNotUndefined' +] as const + +export type FilterOperator = (typeof filterOperators)[number] + +const filterOperatorsByCategory: Record = { + text: ['equals', 'notEquals', 'contains', 'notContains', 'startsWith', 'endsWith', 'isUndefined', 'isNotUndefined'], + number: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'between', 'notBetween', 'isUndefined', 'isNotUndefined'], + date: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'between', 'notBetween', 'isUndefined', 'isNotUndefined'], + dateTime: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'between', 'notBetween', 'isUndefined', 'isNotUndefined'], + boolean: ['isTrue', 'isFalse', 'isUndefined', 'isNotUndefined'], + multiTags: ['equals', 'notEquals', 'contains', 'notContains', 'isUndefined', 'isNotUndefined'], + singleTag: ['equals', 'notEquals', 'contains', 'notContains', 'isUndefined', 'isNotUndefined'], + unknownType: ['isUndefined', 'isNotUndefined'], +} as const + +export type FilterOperatorUnknownType = (typeof filterOperatorsByCategory.unknownType)[number] +export type FilterOperatorText = (typeof filterOperatorsByCategory.text)[number] +export type FilterOperatorNumber = (typeof filterOperatorsByCategory.number)[number] +export type FilterOperatorDate = (typeof filterOperatorsByCategory.date)[number] +export type FilterOperatorDatetime = (typeof filterOperatorsByCategory.dateTime)[number] +export type FilterOperatorBoolean = (typeof filterOperatorsByCategory.boolean)[number] +export type FilterOperatorTags = (typeof filterOperatorsByCategory.multiTags)[number] +export type FilterOperatorTagsSingle = (typeof filterOperatorsByCategory.singleTag)[number] + +function isFilterOperatorText(value: unknown): value is FilterOperatorText { + return typeof value === 'string' && filterOperatorsByCategory.text.some(o => o === value) +} + +function isFilterOperatorNumber(value: unknown): value is FilterOperatorNumber { + return typeof value === 'string' && filterOperatorsByCategory.number.some(o => o === value) +} + +function isFilterOperatorDate(value: unknown): value is FilterOperatorDate { + return typeof value === 'string' && filterOperatorsByCategory.date.some(o => o === value) +} + +function isFilterOperatorDatetime(value: unknown): value is FilterOperatorDatetime { + return typeof value === 'string' && filterOperatorsByCategory.dateTime.some(o => o === value) +} + +function isFilterOperatorBoolean(value: unknown): value is FilterOperatorBoolean { + return typeof value === 'string' && filterOperatorsByCategory.boolean.some(o => o === value) +} + +function isFilterOperatorTags(value: unknown): value is FilterOperatorTags { + return typeof value === 'string' && filterOperatorsByCategory.multiTags.some(o => o === value) +} + +function isFilterOperatorTagsSingle(value: unknown): value is FilterOperatorTagsSingle { + return typeof value === 'string' && filterOperatorsByCategory.singleTag.some(o => o === value) +} + +function isFilterOperatorUnknownType(value: unknown): value is FilterOperatorUnknownType { + return typeof value === 'string' && filterOperatorsByCategory.unknownType.some(o => o === value) +} + +function isFilterOperator(value: unknown): value is FilterOperator { + return typeof value === 'string' && filterOperators.some(o => o === value) +} + +type OperatorInfoResult = { + icon: ReactNode, + translationKey: string, + replacementTranslationKey: string, +} +const getOperatorInfo = (operator: FilterOperator) : OperatorInfoResult => { + switch (operator) { + case 'equals': return { icon: , translationKey: 'equals', replacementTranslationKey: 'rEquals' } + case 'notEquals': return { icon: , translationKey: 'notEquals', replacementTranslationKey: 'rNotEquals' } + case 'contains': return { icon: , translationKey: 'contains', replacementTranslationKey: 'rContains' } + case 'notContains': return { icon: , translationKey: 'notContains', replacementTranslationKey: 'rNotContains' } + case 'startsWith': return { icon: , translationKey: 'startsWith', replacementTranslationKey: 'rStartsWith' } + case 'endsWith': return { icon: , translationKey: 'endsWith', replacementTranslationKey: 'rEndsWith' } + case 'greaterThan': return { + icon: (
    + + +
    + ), + translationKey: 'greaterThanOrEqual', + replacementTranslationKey: 'rGreaterThanOrEqual' + } + case 'greaterThanOrEqual': return { + icon: (
    + + +
    + ), + translationKey: 'greaterThanOrEqual', + replacementTranslationKey: 'rGreaterThanOrEqual' + } + case 'lessThan': return { + icon: , + translationKey: 'lessThan', + replacementTranslationKey: 'rLessThan' + } + case 'lessThanOrEqual': return { + icon: (
    + + +
    + ), + translationKey: 'lessThanOrEqual', + replacementTranslationKey: 'rLessThanOrEqual' + } + case 'between': return { + icon: (
    + + +
    + ), + translationKey: 'between', + replacementTranslationKey: 'rBetween' + } + case 'notBetween': return { + icon: (
    + + +
    + ), + translationKey: 'notBetween', + replacementTranslationKey: 'rNotBetween' + } + case 'isTrue': return { icon: , translationKey: 'isTrue', replacementTranslationKey: 'isTrue' } + case 'isFalse': return { icon: , translationKey: 'isFalse', replacementTranslationKey: 'isFalse' } + case 'isUndefined': return { icon: , translationKey: 'isUndefined', replacementTranslationKey: 'isUndefined' } + case 'isNotUndefined': return { icon: , translationKey: 'isNotUndefined', replacementTranslationKey: 'isNotUndefined' } + default: return { icon: null, translationKey: 'unknown translation key', replacementTranslationKey: 'unknown' } + } +} + +function getDefaultOperator(dataType: DataType): FilterOperator { + switch (dataType) { + case 'text': return 'contains' + case 'number': return 'between' + case 'date': return 'between' + case 'dateTime': return 'between' + case 'boolean': return 'isTrue' + case 'multiTags': return 'contains' + case 'singleTag': return 'contains' + case 'unknownType': return 'isNotUndefined' + } +} + + + +export const FilterOperatorUtils = { + operators: filterOperators, + operatorsByCategory: filterOperatorsByCategory, + getInfo: getOperatorInfo, + getDefaultOperator, + typeCheck: { + all: isFilterOperator, + text: isFilterOperatorText, + number: isFilterOperatorNumber, + date: isFilterOperatorDate, + datetime: isFilterOperatorDatetime, + boolean: isFilterOperatorBoolean, + tags: isFilterOperatorTags, + tagsSingle: isFilterOperatorTagsSingle, + unknownType: isFilterOperatorUnknownType, + }, +} diff --git a/src/components/user-interaction/data/FilterOperatorLabel.tsx b/src/components/user-interaction/data/FilterOperatorLabel.tsx new file mode 100644 index 00000000..ec6f0ec5 --- /dev/null +++ b/src/components/user-interaction/data/FilterOperatorLabel.tsx @@ -0,0 +1,20 @@ +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import type { FilterOperator } from '@/src/components/user-interaction/data/FilterOperator' +import { FilterOperatorUtils } from '@/src/components/user-interaction/data/FilterOperator' + +export type FilterOperatorLabelProps = { + operator: FilterOperator, + } + +export const FilterOperatorLabel = ({ operator }: FilterOperatorLabelProps) => { + const translation = useHightideTranslation() + const { icon, translationKey } = FilterOperatorUtils.getInfo(operator) + const label = typeof translationKey === 'string' ? translation(translationKey) : translationKey + + return ( +
    + {icon} + {label} +
    + ) +} \ No newline at end of file diff --git a/src/components/user-interaction/data/FilterPopUp.tsx b/src/components/user-interaction/data/FilterPopUp.tsx new file mode 100644 index 00000000..0bf4e3ca --- /dev/null +++ b/src/components/user-interaction/data/FilterPopUp.tsx @@ -0,0 +1,764 @@ +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import { Visibility } from '@/src/components/layout/Visibility' +import { IconButton } from '@/src/components/user-interaction/IconButton' +import { TrashIcon, XIcon } from 'lucide-react' +import { PopUp, type PopUpProps } from '@/src/components/layout/popup/PopUp' +import type { FilterValue } from './filter-function' +import type { FilterOperator } from './FilterOperator' +import { FilterOperatorUtils } from './FilterOperator' +import type { ReactNode } from 'react' +import { forwardRef, useId, useMemo, useState } from 'react' +import { Select } from '../Select/Select' +import { SelectOption } from '../Select/SelectOption' +import { Input } from '../input/Input' +import { Checkbox } from '../Checkbox' +import { DateTimeInput } from '../input/DateTimeInput' +import { MultiSelect } from '../MultiSelect/MultiSelect' +import { MultiSelectOption } from '../MultiSelect/MultiSelectOption' +import type { DataType } from './data-types' +import clsx from 'clsx' +import { FilterOperatorLabel } from './FilterOperatorLabel' + +export interface FilterPopUpProps extends PopUpProps { + name?: ReactNode, + value?: FilterValue, + onValueChange: (value: FilterValue) => void, + onRemove: () => void, +} + +export interface FilterPopUpBaseProps extends PopUpProps { + /** + * The name of the object/column the filter is applied to + */ + name?: ReactNode, + operator: FilterOperator, + onOperatorChange: (operator: FilterOperator) => void, + onRemove: () => void, + allowedOperators: FilterOperator[], + hasValue: boolean, + noParameterRequired?: boolean, +} + +export const FilterBasePopUp = forwardRef(function FilterBasePopUp ({ + children, + name, + operator, + onOperatorChange, + onRemove, + allowedOperators, + hasValue, + noParameterRequired = false, + ...props +}: FilterPopUpBaseProps, ref) { + const translation = useHightideTranslation() + + return ( + +
    +
    + {name ?? translation('filter')} + +
    + + + + + + + + + + +
    + {children} + +
    + {translation('noParameterRequired')} +
    +
    +
    + ) +}) + + +export const TextFilterPopUp = forwardRef(function TextFilterPopUp ({ + name, value, onValueChange, onRemove, ...props +}: FilterPopUpProps, ref) { + const translation = useHightideTranslation() + const id = useId() + const ids = { + search: `text-filter-search-${id}`, + caseSensitive: `text-filter-case-sensitive-${id}`, + } + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'contains' + if(!FilterOperatorUtils.typeCheck.text(suggestion)) { + return 'contains' + } + return suggestion + }, [value]) + const parameter = value?.parameter ?? {} + + const needsParameterInput = operator !== 'isUndefined' && operator !== 'isNotUndefined' + return ( + onValueChange({ dataType: 'text', parameter, operator: newOperator })} + onRemove={onRemove} + allowedOperators={FilterOperatorUtils.operatorsByCategory.text} + hasValue={!!value} + noParameterRequired={!needsParameterInput} + > + +
    + + { + onValueChange({ + dataType: 'text', + operator, + parameter: { ...parameter, searchText }, + }) + }} + className="min-w-64" + /> +
    +
    + { + onValueChange({ + dataType: 'text', + operator, + parameter: { ...parameter, isCaseSensitive }, + }) + }} + /> + +
    +
    +
    + ) +}) + +export const NumberFilterPopUp = forwardRef(function NumberFilterPopUp ({ + name, value, onValueChange, onRemove, ...props +}: FilterPopUpProps, ref) { + const translation = useHightideTranslation() + const id = useId() + const ids = { + min: `number-filter-min-${id}`, + max: `number-filter-max-${id}`, + compareValue: `number-filter-compare-value-${id}`, + } + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'between' + if (!FilterOperatorUtils.typeCheck.number(suggestion)) { + return 'between' + } + return suggestion + }, [value]) + const parameter = value?.parameter ?? {} + + const needsRangeInput = operator === 'between' || operator === 'notBetween' + const needsParameterInput = operator !== 'isUndefined' && operator !== 'isNotUndefined' + + return ( + { + onValueChange({ dataType: 'number', parameter, operator: newOperator }) + }} + onRemove={onRemove} + allowedOperators={FilterOperatorUtils.operatorsByCategory.number} + hasValue={!!value} + noParameterRequired={!needsParameterInput} + > + +
    + + { + const num = Number(text) + onValueChange({ + dataType: 'number', + operator, + parameter: { ...parameter, minNumber: isNaN(num) ? undefined : num }, + }) + }} + className="min-w-64" + /> +
    +
    + + { + const num = Number(text) + onValueChange({ + dataType: 'number', + operator, + parameter: { ...parameter, maxNumber: isNaN(num) ? undefined : num }, + }) + }} + className="min-w-64" + /> +
    +
    + + { + const num = Number(text) + onValueChange({ + dataType: 'number', + operator, + parameter: { ...parameter, compareValue: isNaN(num) ? undefined : num }, + }) + }} + className="min-w-64" + /> + +
    + ) +}) + +export const DateFilterPopUp = forwardRef(function DateFilterPopUp ({ + name, value, onValueChange, onRemove, ...props +}: FilterPopUpProps, ref) { + const translation = useHightideTranslation() + const id = useId() + const ids = { + startDate: `date-filter-start-date-${id}`, + endDate: `date-filter-end-date-${id}`, + compareDate: `date-filter-compare-date-${id}`, + } + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'between' + if (!FilterOperatorUtils.typeCheck.date(suggestion)) { + return 'between' + } + return suggestion + }, [value]) + const parameter = value?.parameter ?? {} + const [temporaryMinDateValue, setTemporaryMinDateValue] = useState(null) + const [temporaryMaxDateValue, setTemporaryMaxDateValue] = useState(null) + + const needsRangeInput = operator === 'between' || operator === 'notBetween' + const needsParameterInput = operator !== 'isUndefined' && operator !== 'isNotUndefined' + + return ( + onValueChange({ dataType: 'date', parameter, operator: newOperator })} + onRemove={onRemove} + allowedOperators={FilterOperatorUtils.operatorsByCategory.date} + hasValue={!!value} + noParameterRequired={!needsParameterInput} + > + +
    + + { + if (dateValue && parameter.maxDate && dateValue > parameter.maxDate) { + if (!parameter.minDate) { + onValueChange({ + dataType: 'date', + operator, + parameter: { ...parameter, minDate: parameter.maxDate, maxDate: dateValue }, + }) + } else { + const diff = parameter.maxDate.getTime() - parameter.minDate.getTime() + onValueChange({ + dataType: 'date', + operator, + parameter: { ...parameter, minDate: dateValue, maxDate: new Date(dateValue.getTime() + diff) }, + }) + } + } else { + onValueChange({ + dataType: 'date', + operator, + parameter: { ...parameter, minDate: dateValue }, + }) + } + setTemporaryMinDateValue(null) + }} + allowRemove={true} + outsideClickCloses={false} + className="min-w-64" + /> +
    +
    + + { + if (dateValue && parameter.minDate && dateValue < parameter.minDate) { + if (!parameter.maxDate) { + onValueChange({ + dataType: 'date', + operator, + parameter: { ...parameter, minDate: dateValue, maxDate: parameter.minDate }, + }) + } else { + const diff = parameter.maxDate.getTime() - parameter.minDate.getTime() + onValueChange({ + dataType: 'date', + operator, + parameter: { ...parameter, minDate: new Date(dateValue.getTime() - diff), maxDate: dateValue }, + }) + } + } else { + onValueChange({ + dataType: 'date', + operator, + parameter: { ...parameter, maxDate: dateValue }, + }) + } + setTemporaryMaxDateValue(null) + }} + allowRemove={true} + outsideClickCloses={false} + className="min-w-64" + /> +
    +
    + + + { + onValueChange({ + ...value, + parameter: { ...parameter, compareDate }, + }) + }} + allowRemove={true} + outsideClickCloses={false} + className="min-w-64" + /> + + + + {translation('noParameterRequired')} + + +
    + ) +}) + +export const DatetimeFilterPopUp = forwardRef(function DatetimeFilterPopUp ({ + name, value, onValueChange, onRemove, ...props +}: FilterPopUpProps, ref) { + const translation = useHightideTranslation() + const id = useId() + const ids = { + startDate: `datetime-filter-start-date-${id}`, + endDate: `datetime-filter-end-date-${id}`, + compareDate: `datetime-filter-compare-date-${id}`, + } + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'between' + if (!FilterOperatorUtils.typeCheck.datetime(suggestion)) { + return 'between' + } + return suggestion + }, [value]) + const parameter = value?.parameter ?? {} + const [temporaryMinDateValue, setTemporaryMinDateValue] = useState(null) + const [temporaryMaxDateValue, setTemporaryMaxDateValue] = useState(null) + + const needsRangeInput = operator === 'between' || operator === 'notBetween' + const needsParameterInput = operator !== 'isUndefined' && operator !== 'isNotUndefined' + + return ( + onValueChange({ dataType: 'dateTime', parameter, operator: newOperator })} + onRemove={onRemove} + allowedOperators={FilterOperatorUtils.operatorsByCategory.dateTime} + hasValue={!!value} + > + {translation('parameter')} + +
    + + { + if (dateValue && parameter.maxDate && dateValue > parameter.maxDate) { + if (!parameter.minDate) { + onValueChange({ + dataType: 'dateTime', + operator, + parameter: { ...parameter, minDate: parameter.maxDate, maxDate: dateValue }, + }) + } else { + const diff = parameter.maxDate.getTime() - parameter.minDate.getTime() + onValueChange({ + dataType: 'dateTime', + operator, + parameter: { ...parameter, minDate: dateValue, maxDate: new Date(dateValue.getTime() + diff) }, + }) + } + } else { + onValueChange({ + dataType: 'dateTime', + operator, + parameter: { ...parameter, minDate: dateValue }, + }) + } + setTemporaryMinDateValue(null) + }} + allowRemove={true} + outsideClickCloses={false} + className="min-w-64" + /> + + { + if (dateValue && parameter.minDate && dateValue < parameter.minDate) { + if (!parameter.maxDate) { + onValueChange({ + dataType: 'dateTime', + operator, + parameter: { ...parameter, minDate: dateValue, maxDate: parameter.minDate }, + }) + } else { + const diff = parameter.maxDate.getTime() - parameter.minDate.getTime() + onValueChange({ + dataType: 'dateTime', + operator, + parameter: { ...parameter, minDate: new Date(dateValue.getTime() - diff), maxDate: dateValue }, + }) + } + } else { + onValueChange({ + dataType: 'dateTime', + operator, + parameter: { ...parameter, maxDate: dateValue }, + }) + } + setTemporaryMaxDateValue(null) + }} + allowRemove={true} + outsideClickCloses={false} + className="min-w-64" + /> +
    +
    + + + { + onValueChange({ + dataType: 'dateTime', + operator, + parameter: { ...parameter, compareDate }, + }) + }} + allowRemove={true} + outsideClickCloses={false} + className="min-w-64" + /> + + + + {translation('noParameterRequired')} + + +
    + ) +}) + +export const BooleanFilterPopUp = forwardRef(function BooleanFilterPopUp ({ + name, value, onValueChange, onRemove, ...props +}: FilterPopUpProps, ref) { + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'isTrue' + if (!FilterOperatorUtils.typeCheck.boolean(suggestion)) { + return 'isTrue' + } + return suggestion + }, [value]) + const parameter = value?.parameter ?? {} + + return ( + onValueChange({ dataType: 'boolean', parameter, operator: newOperator })} + onRemove={onRemove} + allowedOperators={FilterOperatorUtils.operatorsByCategory.boolean} + hasValue={!!value} + /> + ) +}) + +export interface TagsFilterPopUpProps extends FilterPopUpProps { + tags: ReadonlyArray<{ tag: string, label: string, display?: ReactNode }>, +} + +export const TagsFilterPopUp = forwardRef(function TagsFilterPopUp ({ + name, value, onValueChange, onRemove, tags: availableTags, ...props +}: TagsFilterPopUpProps, ref) { + const translation = useHightideTranslation() + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'contains' + if (!FilterOperatorUtils.typeCheck.tags(suggestion)) { + return 'contains' + } + return suggestion + }, [value]) + const parameter = value?.parameter ?? {} + const selectedTags = (Array.isArray(parameter.multiOptionSearch) ? parameter.multiOptionSearch : []) as string[] + + const needsParameterInput = operator !== 'isUndefined' && operator !== 'isNotUndefined' + + if (availableTags.length === 0) { + return null + } + + return ( + onValueChange({ dataType: 'multiTags', parameter, operator: newOperator })} + onRemove={onRemove} + allowedOperators={FilterOperatorUtils.operatorsByCategory.multiTags} + hasValue={!!value} + > + {translation('parameter')} + + { + onValueChange({ + dataType: 'multiTags', + operator, + parameter: { ...parameter, multiOptionSearch: selected.length > 0 ? selected : undefined }, + }) + }} + buttonProps={{ className: 'min-w-64' }} + > + {availableTags.map(({ tag, label }) => ( + + {label} + + ))} + + + + + {translation('noParameterRequired')} + + + + ) +}) + +export interface TagsSingleFilterPopUpProps extends FilterPopUpProps { + tags: ReadonlyArray<{ tag: string, label: string, display?: ReactNode }>, +} + +export const TagsSingleFilterPopUp = forwardRef(function TagsSingleFilterPopUp ({ + name, value, onValueChange, onRemove, tags: availableTags, ...props +}: TagsSingleFilterPopUpProps, ref) { + const translation = useHightideTranslation() + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'contains' + if (!FilterOperatorUtils.typeCheck.tagsSingle(suggestion)) { + return 'contains' + } + return suggestion + }, [value]) + const parameter = value?.parameter ?? {} + const selectedTagsMulti = (Array.isArray(parameter.multiOptionSearch) ? parameter.multiOptionSearch : []) as string[] + const selectedTagSingle = parameter.singleOptionSearch != null ? String(parameter.singleOptionSearch) : undefined + + const needsParameterInput = operator !== 'isUndefined' && operator !== 'isNotUndefined' + const needsMultiSelect = operator === 'contains' || operator === 'notContains' + + if (availableTags.length === 0) { + return null + } + + return ( + onValueChange({ dataType: 'singleTag', parameter, operator: newOperator })} + onRemove={onRemove} + allowedOperators={FilterOperatorUtils.operatorsByCategory.singleTag} + hasValue={!!value} + > + {translation('parameter')} + + { + onValueChange({ + dataType: 'singleTag', + operator, + parameter: { ...parameter, multiOptionSearch: selected.length > 0 ? selected : undefined }, + }) + }} + buttonProps={{ className: 'min-w-64' }} + > + {availableTags.map(({ tag, label }) => ( + + ))} + + + + + + + + {translation('noParameterRequired')} + + + + ) +}) + +export const GenericFilterPopUp = forwardRef(function GenericFilterPopUp ({ name, value, onValueChange, ...props }: FilterPopUpProps, ref) { + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'isNotUndefined' + if (!FilterOperatorUtils.typeCheck.unknownType(suggestion)) { + return 'isNotUndefined' + } + return suggestion + }, [value]) + + return ( + onValueChange({ ...value, operator: newOperator })} + onRemove={() => onValueChange({ ...value, operator: undefined })} + allowedOperators={FilterOperatorUtils.operatorsByCategory.unknownType} + hasValue={!!value} + /> + ) +}) + +export interface DataTypeFilterPopUpProps extends FilterPopUpProps { + dataType: DataType, + tags: ReadonlyArray<{ tag: string, label: string, display?: ReactNode }>, +} +export const FilterPopUp = forwardRef(function FilterPopUp ({ + name, + value, + onValueChange, + dataType, + tags, + ...props +}: DataTypeFilterPopUpProps, ref) { + switch (dataType) { + case 'text': + return + case 'number': + return + case 'date': + return + case 'dateTime': + return + case 'boolean': + return + case 'multiTags': + return + case 'singleTag': + return + case 'unknownType': + return + } +}) \ No newline at end of file diff --git a/src/components/user-interaction/data/data-types.tsx b/src/components/user-interaction/data/data-types.tsx new file mode 100644 index 00000000..aa7206c0 --- /dev/null +++ b/src/components/user-interaction/data/data-types.tsx @@ -0,0 +1,70 @@ +import { Binary, Calendar, CalendarClock, Check, Database, Tag, Tags, TextIcon } from 'lucide-react' +import type { ReactNode } from 'react' + +const dataTypes = [ + 'text', + 'number', + 'date', + 'dateTime', + 'boolean', + 'singleTag', + 'multiTags', + 'unknownType', +] as const +export type DataType = (typeof dataTypes)[number] + +export interface DataValue { + textValue?: string, + numberValue?: number, + booleanValue?: boolean, + dateValue?: Date, + singleSelectValue?: string, + multiSelectValue?: string[], +} + +const getDefaultValue = (type: DataType, selectOptions?: string[]): DataValue => { + switch (type) { + case 'text': + return { textValue: '' } + case 'number': + return { numberValue: 0 } + case 'boolean': + return { booleanValue: false } + case 'date': + case 'dateTime': + return { dateValue: new Date() } + case 'singleTag': + return { singleSelectValue: selectOptions?.[0] } + case 'multiTags': + return { multiSelectValue: [] } + default: + return {} + } +} + +function toIcon(type: DataType): ReactNode { + switch (type) { + case 'text': + return + case 'number': + return + case 'boolean': + return + case 'date': + return + case 'dateTime': + return + case 'singleTag': + return + case 'multiTags': + return + case 'unknownType': + return + } +} + +export const DataTypeUtils = { + types: dataTypes, + getDefaultValue, + toIcon, +} \ No newline at end of file diff --git a/src/components/user-interaction/data/filter-function.ts b/src/components/user-interaction/data/filter-function.ts new file mode 100644 index 00000000..ce591f41 --- /dev/null +++ b/src/components/user-interaction/data/filter-function.ts @@ -0,0 +1,559 @@ +import { useCallback } from 'react' +import { DateUtils } from '@/src/utils/date' +import { useLocale } from '@/src/global-contexts/LocaleContext' +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import { type FilterOperator } from './FilterOperator' +import type { DataType } from './data-types' + +export type FilterParameter = { + searchText?: string, + isCaseSensitive?: boolean, + compareValue?: number, + minNumber?: number, + maxNumber?: number, + compareDate?: Date, + minDate?: Date, + maxDate?: Date, + multiOptionSearch?: unknown[], + singleOptionSearch?: unknown, +} + +const allowedOperatorsByDataType: Record = { + text: ['equals', 'notEquals', 'contains', 'notContains', 'startsWith', 'endsWith', 'isUndefined', 'isNotUndefined'], + number: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'between', 'notBetween', 'isUndefined', 'isNotUndefined'], + date: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'between', 'notBetween', 'isUndefined', 'isNotUndefined'], + dateTime: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'between', 'notBetween', 'isUndefined', 'isNotUndefined'], + boolean: ['isTrue', 'isFalse', 'isUndefined', 'isNotUndefined'], + multiTags: ['equals', 'notEquals', 'contains', 'notContains', 'isUndefined', 'isNotUndefined'], + singleTag: ['equals', 'notEquals', 'contains', 'notContains', 'isUndefined', 'isNotUndefined'], + unknownType: ['isUndefined', 'isNotUndefined'], +} + +export type FilterValue = { + dataType: DataType, + operator: FilterOperator, + parameter: FilterParameter, +} + +const OPERATORS_WITHOUT_PARAMETERS: FilterOperator[] = [ + 'isUndefined', + 'isNotUndefined', + 'isTrue', + 'isFalse', +] + +function isParameterValidForOperator( + dataType: DataType, + operator: FilterOperator, + parameter: FilterParameter +): boolean { + if (OPERATORS_WITHOUT_PARAMETERS.includes(operator)) { + return true + } + + switch (dataType) { + case 'text': { + return typeof parameter.searchText === 'string' + } + case 'number': { + if (operator === 'between' || operator === 'notBetween') { + const min = parameter.minNumber + const max = parameter.maxNumber + return ( + typeof min === 'number' && + !Number.isNaN(min) && + typeof max === 'number' && + !Number.isNaN(max) && + min <= max + ) + } + const v = parameter.compareValue + return typeof v === 'number' && !Number.isNaN(v) + } + case 'date': + case 'dateTime': { + if (operator === 'between' || operator === 'notBetween') { + const minDate = DateUtils.tryParseDate(parameter.minDate) + const maxDate = DateUtils.tryParseDate(parameter.maxDate) + if (!minDate || !maxDate) return false + const minNorm = dataType === 'date' + ? DateUtils.toOnlyDate(minDate).getTime() + : DateUtils.toDateTimeOnly(minDate).getTime() + const maxNorm = dataType === 'date' + ? DateUtils.toOnlyDate(maxDate).getTime() + : DateUtils.toDateTimeOnly(maxDate).getTime() + return minNorm <= maxNorm + } + return DateUtils.tryParseDate(parameter.compareDate) != null + } + case 'boolean': + return true + case 'multiTags': { + return Array.isArray(parameter.multiOptionSearch) + } + case 'singleTag': { + if (operator === 'contains' || operator === 'notContains') { + return Array.isArray(parameter.multiOptionSearch) + } + if(operator === 'equals' || operator === 'notEquals') { + return typeof parameter.singleOptionSearch === 'string' + } + return true + } + case 'unknownType': + return true + default: + return false + } +} + +function isFilterValueValid(value: FilterValue): boolean { + const allowed = allowedOperatorsByDataType[value.dataType] + if (!allowed?.includes(value.operator)) { + return false + } + return isParameterValidForOperator(value.dataType, value.operator, value.parameter) +} + +export const FilterValueUtils = { + allowedOperatorsByDataType, + isValid: isFilterValueValid, +} + +/** + * Filters a text value based on the provided filter value. + */ +function filterText(value: unknown, operator: FilterOperator, parameter: FilterParameter): boolean { + const isCaseSensitive = parameter.isCaseSensitive ?? false + + const searchText = isCaseSensitive ? parameter.searchText ?? '' : (parameter.searchText ?? '').toLowerCase() + const cellText = isCaseSensitive ? value?.toString() ?? '' : value?.toString().toLowerCase() ?? '' + + switch (operator) { + case 'equals': + return cellText === searchText + case 'notEquals': + return cellText !== searchText + case 'contains': + return cellText.includes(searchText) + case 'notContains': + return !cellText.includes(searchText) + case 'startsWith': + return cellText.startsWith(searchText) + case 'endsWith': + return cellText.endsWith(searchText) + case 'isUndefined': + return value === undefined || value === null + case 'isNotUndefined': + return value !== undefined && value !== null + default: + return false + } +} + +/** + * Filters a number value based on the provided filter value. + */ +function filterNumber(value: unknown, operator: FilterOperator, parameter: FilterParameter): boolean { + if (typeof value !== 'number') { + if (operator === 'isUndefined') { + return value === undefined || value === null + } + if (operator === 'isNotUndefined') { + return value !== undefined && value !== null + } + return false + } + + switch (operator) { + case 'equals': + return value === parameter.compareValue + case 'notEquals': + return value !== parameter.compareValue + case 'greaterThan': + return value > (parameter.compareValue ?? 0) + case 'greaterThanOrEqual': + return value >= (parameter.compareValue ?? 0) + case 'lessThan': + return value < (parameter.compareValue ?? 0) + case 'lessThanOrEqual': + return value <= (parameter.compareValue ?? 0) + case 'between': + return value >= (parameter.minNumber ?? -Infinity) && value <= (parameter.maxNumber ?? Infinity) + case 'notBetween': + return value < (parameter.minNumber ?? -Infinity) || value > (parameter.maxNumber ?? Infinity) + case 'isUndefined': + return value === undefined || value === null + case 'isNotUndefined': + return value !== undefined && value !== null + default: + return false + } +} + +/** + * Filters a date value based on the provided filter value. + * Only compares dates, ignoring time components. + */ +function filterDate(value: unknown, operator: FilterOperator, parameter: FilterParameter): boolean { + const date = DateUtils.tryParseDate(value as Date | string | number | undefined | null) + if (!date) { + if (operator === 'isUndefined') { + return value === undefined || value === null + } + if (operator === 'isNotUndefined') { + return value !== undefined && value !== null + } + return false + } + + const normalizedDate = DateUtils.toOnlyDate(date) + + switch (operator) { + case 'equals': { + const filterDate = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDate) return false + return normalizedDate.getTime() === DateUtils.toOnlyDate(filterDate).getTime() + } + case 'notEquals': { + const filterDate = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDate) return false + return normalizedDate.getTime() !== DateUtils.toOnlyDate(filterDate).getTime() + } + case 'greaterThan': { + const filterDate = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDate) return false + return normalizedDate > DateUtils.toOnlyDate(filterDate) + } + case 'greaterThanOrEqual': { + const filterDate = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDate) return false + return normalizedDate >= DateUtils.toOnlyDate(filterDate) + } + case 'lessThan': { + const filterDate = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDate) return false + return normalizedDate < DateUtils.toOnlyDate(filterDate) + } + case 'lessThanOrEqual': { + const filterDate = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDate) return false + return normalizedDate <= DateUtils.toOnlyDate(filterDate) + } + case 'between': { + const minDate = DateUtils.tryParseDate(parameter.minDate) + const maxDate = DateUtils.tryParseDate(parameter.maxDate) + if (!minDate || !maxDate) return false + return normalizedDate >= DateUtils.toOnlyDate(minDate) && normalizedDate <= DateUtils.toOnlyDate(maxDate) + } + case 'notBetween': { + const minDate = DateUtils.tryParseDate(parameter.minDate) + const maxDate = DateUtils.tryParseDate(parameter.maxDate) + if (!minDate || !maxDate) return false + return normalizedDate < DateUtils.toOnlyDate(minDate) || normalizedDate > DateUtils.toOnlyDate(maxDate) + } + default: + return false + } +} + +/** + * Filters a dateTime value based on the provided filter value. + */ +function filterDateTime(value: unknown, operator: FilterOperator, parameter: FilterParameter): boolean { + const dateTime = DateUtils.tryParseDate(value as Date | string | number | undefined | null) + if (!dateTime) { + if (operator === 'isUndefined') { + return value === undefined || value === null + } + if (operator === 'isNotUndefined') { + return value !== undefined && value !== null + } + return false + } + + const normalizedDatetime = DateUtils.toDateTimeOnly(dateTime) + + switch (operator) { + case 'equals': { + const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDatetime) return false + return normalizedDatetime.getTime() === DateUtils.toDateTimeOnly(filterDatetime).getTime() + } + case 'notEquals': { + const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDatetime) return false + return normalizedDatetime.getTime() !== DateUtils.toDateTimeOnly(filterDatetime).getTime() + } + case 'greaterThan': { + const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDatetime) return false + return normalizedDatetime > DateUtils.toDateTimeOnly(filterDatetime) + } + case 'greaterThanOrEqual': { + const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDatetime) return false + return normalizedDatetime >= DateUtils.toDateTimeOnly(filterDatetime) + } + case 'lessThan': { + const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDatetime) return false + return normalizedDatetime < DateUtils.toDateTimeOnly(filterDatetime) + } + case 'lessThanOrEqual': { + const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + if (!filterDatetime) return false + return normalizedDatetime <= DateUtils.toDateTimeOnly(filterDatetime) + } + case 'between': { + const minDatetime = DateUtils.tryParseDate(parameter.minDate) + const maxDatetime = DateUtils.tryParseDate(parameter.maxDate) + if (!minDatetime || !maxDatetime) return false + return normalizedDatetime >= DateUtils.toDateTimeOnly(minDatetime) && normalizedDatetime <= DateUtils.toDateTimeOnly(maxDatetime) + } + case 'notBetween': { + const minDatetime = DateUtils.tryParseDate(parameter.minDate) + const maxDatetime = DateUtils.tryParseDate(parameter.maxDate) + if (!minDatetime || !maxDatetime) return false + return normalizedDatetime < DateUtils.toDateTimeOnly(minDatetime) || normalizedDatetime > DateUtils.toDateTimeOnly(maxDatetime) + } + default: + return false + } +} + +/** + * Filters a boolean value based on the provided filter value. + */ +function filterBoolean(value: unknown, operator: FilterOperator): boolean { + switch (operator) { + case 'isTrue': + return value === true + case 'isFalse': + return value === false + case 'isUndefined': + return value === undefined || value === null + case 'isNotUndefined': + return value !== undefined && value !== null + default: + return false + } +} + +/** + * Filters a tags array value based on the provided filter value. + */ +function filterMultiTags(value: unknown, operator: FilterOperator, parameter: FilterParameter): boolean { + switch (operator) { + case 'equals': { + if (!Array.isArray(value) || !Array.isArray(parameter.multiOptionSearch)) return false + if (value.length !== parameter.multiOptionSearch.length) return false + const valueSet = new Set(value) + const searchTagsSet = new Set(parameter.multiOptionSearch) + if (valueSet.size !== searchTagsSet.size) return false + return Array.from(valueSet).every(tag => searchTagsSet.has(tag)) + } + case 'notEquals': { + if (!Array.isArray(value) || !Array.isArray(parameter.multiOptionSearch)) return true + if (value.length !== parameter.multiOptionSearch.length) return true + const valueSet = new Set(value) + const searchTagsSet = new Set(parameter.multiOptionSearch) + if (valueSet.size !== searchTagsSet.size) return true + return !Array.from(valueSet).every(tag => searchTagsSet.has(tag)) + } + case 'contains': { + if (!Array.isArray(value) || !Array.isArray(parameter.multiOptionSearch)) return false + return parameter.multiOptionSearch.every(tag => value.includes(tag)) + } + case 'notContains': { + if (!Array.isArray(value) || !Array.isArray(parameter.multiOptionSearch)) return true + return !parameter.multiOptionSearch.every(tag => value.includes(tag)) + } + case 'isUndefined': + return value === undefined || value === null + case 'isNotUndefined': + return value !== undefined && value !== null + default: + return false + } +} + +/** + * Filters a single tag value based on the provided filter value. + */ +function filterSingleTag(value: unknown, operator: FilterOperator, parameter: FilterParameter): boolean { + switch (operator) { + case 'equals': + return value === parameter.singleOptionSearch + case 'notEquals': + return value !== parameter.singleOptionSearch + case 'contains': + return parameter.multiOptionSearch?.includes(value) ?? false + case 'notContains': + return !(parameter.multiOptionSearch?.includes(value) ?? false) + case 'isUndefined': + return value === undefined || value === null + case 'isNotUndefined': + return value !== undefined && value !== null + default: + return false + } +} + +/** + * Filters a generic value based on the provided filter value. + */ +function filterUnknownType(value: unknown, operator: FilterOperator): boolean { + switch (operator) { + case 'isUndefined': + return value === undefined || value === null + case 'isNotUndefined': + return value !== undefined && value !== null + default: + return false + } +} + +export const FilterFunctions: Record boolean> = { + text: filterText, + number: filterNumber, + date: filterDate, + dateTime: filterDateTime, + boolean: filterBoolean, + singleTag: filterSingleTag, + multiTags: filterMultiTags, + unknownType: filterUnknownType, +} + +export type FilterValueTranslationOptions = { + tags?: ReadonlyArray<{ tag: string, label: string }>, +} + +function formatDateParam( + dateParam: Date | string | number | undefined | null, + locale: string, + format: 'date' | 'dateTime' +): string { + const d = DateUtils.tryParseDate(dateParam) + return d ? DateUtils.formatAbsolute(d, locale, format) : '' +} + +function tagToLabel(tags: ReadonlyArray<{ tag: string, label: string }> | undefined, value: unknown): string { + if (!tags) return String(value ?? '') + const entry = tags.find(t => t.tag === value || t.tag === String(value)) + return entry?.label ?? String(value ?? '') +} + +export function useFilterValueTranslation(): (value: FilterValue, options?: FilterValueTranslationOptions) => string { + const translation = useHightideTranslation() + const { locale } = useLocale() + + return useCallback((value: FilterValue, options?: FilterValueTranslationOptions): string => { + const p = value.parameter + const tags = options?.tags + const dateFormat = value.dataType === 'dateTime' ? 'dateTime' as const : 'date' as const + + switch (value.operator) { + case 'equals': + if (value.dataType === 'date' || value.dataType === 'dateTime') { + return translation('rEquals', { value: formatDateParam(p.compareDate, locale, dateFormat) ?? '-' }) + } + if (value.dataType === 'singleTag') { + return translation('rEquals', { value: tagToLabel(tags, p.singleOptionSearch) }) + } + if (value.dataType === 'multiTags') { + const valueStr = (p.multiOptionSearch ?? []).map(v => tagToLabel(tags, v)).join(', ') + return translation('rEquals', { value: valueStr }) + } + return translation('rEquals', { value: String(p.searchText ?? p.compareValue ?? '') }) + case 'notEquals': + if (value.dataType === 'date' || value.dataType === 'dateTime') { + return translation('rNotEquals', { value: formatDateParam(p.compareDate, locale, dateFormat) }) + } + if (value.dataType === 'singleTag') { + return translation('rNotEquals', { value: tagToLabel(tags, p.singleOptionSearch) }) + } + if (value.dataType === 'multiTags') { + const valueStr = (p.multiOptionSearch ?? []).map(v => tagToLabel(tags, v)).join(', ') + return translation('rNotEquals', { value: valueStr }) + } + return translation('rNotEquals', { value: String(p.searchText ?? p.compareValue ?? '') }) + case 'contains': + if (value.dataType === 'multiTags' || value.dataType === 'singleTag') { + const valueStr = value.dataType === 'singleTag' + ? tagToLabel(tags, p.singleOptionSearch) + : (p.multiOptionSearch ?? []).map(v => tagToLabel(tags, v)).join(', ') + return translation('rContains', { value: valueStr }) + } + return translation('rContains', { value: String(p.searchText ?? '') }) + case 'notContains': + if (value.dataType === 'multiTags' || value.dataType === 'singleTag') { + const valueStr = value.dataType === 'singleTag' + ? tagToLabel(tags, p.singleOptionSearch) + : (p.multiOptionSearch ?? []).map(v => tagToLabel(tags, v)).join(', ') + return translation('rNotContains', { value: valueStr }) + } + return translation('rNotContains', { value: `"${String(p.searchText ?? '')}"` }) + case 'startsWith': + return translation('rStartsWith', { value: `"${String(p.searchText ?? '')}"` }) + case 'endsWith': + return translation('rEndsWith', { value: `"${String(p.searchText ?? '')}"` }) + case 'greaterThan': + return translation('rGreaterThan', { + value: value.dataType === 'date' || value.dataType === 'dateTime' + ? formatDateParam(p.compareDate, locale, dateFormat) ?? '-' + : String(p.compareValue ?? '-'), + }) + case 'greaterThanOrEqual': + return translation('rGreaterThanOrEqual', { + value: value.dataType === 'date' || value.dataType === 'dateTime' + ? formatDateParam(p.compareDate, locale, dateFormat) ?? '-' + : String(p.compareValue ?? '-'), + }) + case 'lessThan': + return translation('rLessThan', { + value: value.dataType === 'date' || value.dataType === 'dateTime' + ? formatDateParam(p.compareDate, locale, dateFormat) ?? '-' + : String(p.compareValue ?? '-'), + }) + case 'lessThanOrEqual': + return translation('rLessThanOrEqual', { + value: value.dataType === 'date' || value.dataType === 'dateTime' + ? formatDateParam(p.compareDate, locale, dateFormat) ?? '-' + : String(p.compareValue ?? '-'), + }) + case 'between': + if (value.dataType === 'date' || value.dataType === 'dateTime') { + return translation('rBetween', { + value1: formatDateParam(p.minDate, locale, dateFormat) ?? '-', + value2: formatDateParam(p.maxDate, locale, dateFormat) ?? '-', + }) + } + return translation('rBetween', { + value1: String(p.minNumber ?? '-'), + value2: String(p.maxNumber ?? '-'), + }) + case 'notBetween': + if (value.dataType === 'date' || value.dataType === 'dateTime') { + return translation('rNotBetween', { + value1: formatDateParam(p.minDate, locale, dateFormat) ?? '-', + value2: formatDateParam(p.maxDate, locale, dateFormat) ?? '-', + }) + } + return translation('rNotBetween', { + value1: String(p.minNumber ?? '-'), + value2: String(p.maxNumber ?? '-'), + }) + case 'isTrue': + return translation('isTrue') + case 'isFalse': + return translation('isFalse') + case 'isUndefined': + return translation('isUndefined') + case 'isNotUndefined': + return translation('isNotUndefined') + default: + return '' + } + }, [translation, locale]) +} \ No newline at end of file diff --git a/src/components/user-interaction/date/TimePicker.tsx b/src/components/user-interaction/date/TimePicker.tsx index da87e418..2253f5c7 100644 --- a/src/components/user-interaction/date/TimePicker.tsx +++ b/src/components/user-interaction/date/TimePicker.tsx @@ -1,10 +1,10 @@ import { useEffect, useMemo, useRef } from 'react' import { closestMatch, range } from '@/src/utils/array' import { Button } from '@/src/components/user-interaction/Button' -import type { FormFieldDataHandling } from '../../form/FormField' +import type { FormFieldDataHandling } from '@/src/components/form/FormField' import { useControlledState } from '@/src/hooks/useControlledState' -import type { DateTimePrecision } from '@/src/utils' -import { Visibility } from '../../layout' +import type { DateTimePrecision } from '@/src/utils/date' +import { Visibility } from '@/src/components/layout/Visibility' export type TimePickerMinuteIncrement = '1min' | '5min' | '10min' | '15min' | '30min' diff --git a/src/components/user-interaction/input/DateTimeInput.tsx b/src/components/user-interaction/input/DateTimeInput.tsx index 4d74a924..22804674 100644 --- a/src/components/user-interaction/input/DateTimeInput.tsx +++ b/src/components/user-interaction/input/DateTimeInput.tsx @@ -13,7 +13,7 @@ import type { FormFieldInteractionStates } from '@/src/components/form/FieldLayo import { PopUp } from '../../layout/popup/PopUp' import { IconButton } from '../IconButton' import { DateUtils, type DateTimeFormat } from '@/src/utils/date' -import { useDelay } from '@/src/hooks' +import { useDelay } from '@/src/hooks/useDelay' export interface DateTimeInputProps extends Partial, diff --git a/src/components/user-interaction/properties/MultiSelectProperty.tsx b/src/components/user-interaction/properties/MultiSelectProperty.tsx index 3cac36e9..704906a3 100644 --- a/src/components/user-interaction/properties/MultiSelectProperty.tsx +++ b/src/components/user-interaction/properties/MultiSelectProperty.tsx @@ -2,9 +2,9 @@ import { List } from 'lucide-react' import { PropertyBase, type PropertyField } from '@/src/components/user-interaction/properties/PropertyBase' import type { PropsWithChildren } from 'react' import { PropsUtil } from '@/src/utils/propsUtil' -import { MultiSelectChipDisplay } from '@/src/components/user-interaction/select/MultiSelectChipDisplay' +import { MultiSelectChipDisplay } from '@/src/components/user-interaction/MultiSelect/MultiSelectChipDisplay' -export type MultiSelectPropertyProps = PropertyField & PropsWithChildren +export interface MultiSelectPropertyProps extends PropertyField, PropsWithChildren {} /** * An Input for MultiSelect properties */ @@ -30,9 +30,10 @@ export const MultiSelectProperty = ({ > { - onValueChange?.(value) - onEditComplete?.(value) + onValueChange={(val) => { + const arr = val as string[] + onValueChange?.(arr) + onEditComplete?.(arr) }} disabled={props.readOnly} contentPanelProps={{ diff --git a/src/components/user-interaction/properties/SelectProperty.tsx b/src/components/user-interaction/properties/SelectProperty.tsx index 95596265..a395d566 100644 --- a/src/components/user-interaction/properties/SelectProperty.tsx +++ b/src/components/user-interaction/properties/SelectProperty.tsx @@ -3,10 +3,11 @@ import type { PropsWithChildren } from 'react' import type { PropertyField } from '@/src/components/user-interaction/properties/PropertyBase' import { PropertyBase } from '@/src/components/user-interaction/properties/PropertyBase' import { PropsUtil } from '@/src/utils/propsUtil' -import { SelectRoot } from '../select/SelectContext' -import { SelectButton, SelectContent } from '../select/SelectComponents' +import { SelectRoot } from '@/src/components/user-interaction/Select/SelectRoot' +import { SelectButton } from '@/src/components/user-interaction/Select/SelectButton' +import { SelectContent } from '@/src/components/user-interaction/Select/SelectContent' -export type SingleSelectPropertyProps = PropertyField & PropsWithChildren +export interface SingleSelectPropertyProps extends PropertyField, PropsWithChildren {} /** * An Input for SingleSelect properties diff --git a/src/components/user-interaction/select/MultiSelect.tsx b/src/components/user-interaction/select/MultiSelect.tsx deleted file mode 100644 index 2082d952..00000000 --- a/src/components/user-interaction/select/MultiSelect.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { MultiSelectRootProps } from './SelectContext' -import { MultiSelectRoot } from './SelectContext' -import type { MultiSelectContentProps, MultiSelectButtonProps } from './SelectComponents' -import { MultiSelectButton, MultiSelectContent } from './SelectComponents' -import { forwardRef } from 'react' - -// -// MultiSelect -// -export type MultiSelectProps = MultiSelectRootProps & { - contentPanelProps?: MultiSelectContentProps, - buttonProps?: MultiSelectButtonProps, - } - -export const MultiSelect = forwardRef(function MultiSelect({ - children, - contentPanelProps, - buttonProps, - ...props -}, ref) { - return ( - - - {children} - - ) -}) diff --git a/src/components/user-interaction/select/MultiSelectChipDisplay.tsx b/src/components/user-interaction/select/MultiSelectChipDisplay.tsx deleted file mode 100644 index c3a43dd2..00000000 --- a/src/components/user-interaction/select/MultiSelectChipDisplay.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import type { MultiSelectRootProps } from './SelectContext' -import { MultiSelectRoot, useSelectContext } from './SelectContext' -import type { HTMLAttributes, ReactNode } from 'react' -import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' -import { XIcon, Plus } from 'lucide-react' -import { MultiSelectContent, type MultiSelectContentProps } from './SelectComponents' -import { IconButton } from '../IconButton' -import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' - -/// -/// MultiSelectChipDisplay -/// -type MultiSelectChipDisplayButtonProps = HTMLAttributes & { - disabled?: boolean, - placeholder?: ReactNode, - } - -export const MultiSelectChipDisplayButton = forwardRef(function MultiSelectChipDisplayButton({ - id, - ...props -}, ref) { - const translation = useHightideTranslation() - const { state, trigger, item, ids, setIds } = useSelectContext() - const { register, unregister, toggleOpen } = trigger - - useEffect(() => { - if(id) { - setIds(prev => ({ - ...prev, - trigger: id, - })) - } - }, [id, setIds]) - const innerRef = useRef(null) - useImperativeHandle(ref, () => innerRef.current) - - useEffect(() => { - register(innerRef) - return () => unregister() - }, [register, unregister]) - - const disabled = !!props?.disabled || !!state.disabled - const invalid = state.invalid - - return ( -
    { - toggleOpen() - props.onClick?.(event) - }} - - data-name={props['data-name'] ?? 'select-chip-display'} - data-value={state.value.length > 0 ? '' : undefined} - data-disabled={disabled ? '' : undefined} - data-invalid={invalid ? '' : undefined} - - aria-invalid={invalid} - aria-disabled={disabled} - > - {state.selectedOptions.map(({ value, label }) => ( -
    - {label} - { - item.toggleSelection(value, false) - }} - size="sm" - color="negative" - coloringStyle="text" - className="flex-row-0 items-center size-7 p-1" - > - - -
    - ))} - { - event.stopPropagation() - toggleOpen() - }} - onKeyDown={event => { - switch (event.key) { - case 'ArrowDown': - toggleOpen(true, { highlightStartPositionBehavior: 'first' }) - break - case 'ArrowUp': - toggleOpen(true, { highlightStartPositionBehavior: 'last' }) - } - }} - tooltip={translation('changeSelection')} - size="md" - color="neutral" - - aria-invalid={invalid} - aria-disabled={disabled} - aria-haspopup="listbox" - aria-expanded={state.isOpen} - aria-controls={state.isOpen ? ids.content : undefined} - - className="size-9" - > - - -
    - ) -}) - - -// -// MultiSelectChipDisplay -// -export type MultiSelectChipDisplayProps = MultiSelectRootProps & { - contentPanelProps?: MultiSelectContentProps, - chipDisplayProps?: MultiSelectChipDisplayButtonProps, - } - -export const MultiSelectChipDisplay = forwardRef(function MultiSelectChipDisplay({ - children, - contentPanelProps, - chipDisplayProps, - ...props -}, ref) { - return ( - - - {children} - - ) -}) diff --git a/src/components/user-interaction/select/Select.tsx b/src/components/user-interaction/select/Select.tsx deleted file mode 100644 index 137b415b..00000000 --- a/src/components/user-interaction/select/Select.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { ReactNode } from 'react' -import { - forwardRef -} from 'react' -import type { SelectRootProps } from './SelectContext' -import { SelectRoot } from './SelectContext' -import type { SelectButtonProps, SelectContentProps } from './SelectComponents' -import { SelectButton } from './SelectComponents' -import { SelectContent } from './SelectComponents' - -// -// Select -// -export type SelectProps = SelectRootProps & { - contentPanelProps?: SelectContentProps, - buttonProps?: Omit & { selectedDisplay?: (value: string) => ReactNode }, -} - -export const Select = forwardRef(function Select({ - children, - contentPanelProps, - buttonProps, - ...props -}, ref) { - return ( - - { - const value = values[0] - if (!buttonProps?.selectedDisplay) return undefined - return buttonProps.selectedDisplay(value) - }} - /> - {children} - - ) -}) diff --git a/src/components/user-interaction/select/SelectComponents.tsx b/src/components/user-interaction/select/SelectComponents.tsx deleted file mode 100644 index 56c7d0b2..00000000 --- a/src/components/user-interaction/select/SelectComponents.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode, RefObject } from 'react' -import type { SelectIconAppearance } from './SelectContext' -import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' -import { useSelectContext } from './SelectContext' -import clsx from 'clsx' -import { CheckIcon } from 'lucide-react' -import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' -import { ExpansionIcon } from '@/src/components/display-and-visualization/ExpansionIcon' -import { PopUp, type PopUpProps } from '../../layout/popup/PopUp' - -// -// SelectOption -// -export type SelectOptionProps = Omit, 'children'> & { - value: string, - disabled?: boolean, - iconAppearance?: SelectIconAppearance, - children?: ReactNode, - } - -export const SelectOption = forwardRef( - function SelectOption({ children, value, disabled = false, iconAppearance, className, ...restProps }, ref) { - const { state, config, item, trigger } = useSelectContext() - const { register, unregister, toggleSelection, highlightItem } = item - const itemRef = useRef(null) - - iconAppearance ??= config.iconAppearance - - const label = children ?? value - - // Register with parent - useEffect(() => { - register({ - value, - label, - disabled, - ref: itemRef, - }) - return () => unregister(value) - }, [value, disabled, register, unregister, children, label]) - - const isHighlighted = state.highlightedValue === value - const isSelected = state.value.includes(value) - - return ( -
  • { - itemRef.current = node - if (typeof ref === 'function') ref(node) - else if (ref) (ref as RefObject).current = node - }} - id={value} - role="option" - aria-disabled={disabled} - aria-selected={isSelected} - data-highlighted={isHighlighted ? '' : undefined} - data-selected={isSelected ? '' : undefined} - data-disabled={disabled ? '' : undefined} - className={clsx( - 'flex-row-1 items-center px-2 py-1 rounded-md', - 'data-highlighted:bg-primary/20', - 'data-disabled:text-disabled data-disabled:cursor-not-allowed', - 'not-data-disabled:cursor-pointer', - className - )} - onClick={(event) => { - if (!disabled) { - toggleSelection(value) - if (!config.isMultiSelect) { - trigger.toggleOpen(false) - } - restProps.onClick?.(event) - } - }} - onMouseEnter={(event) => { - if (!disabled) { - highlightItem(value) - restProps.onMouseEnter?.(event) - } - }} - > - {iconAppearance === 'left' && (state.value.length > 0 || config.isMultiSelect) && ( - - )} - {label} - {iconAppearance === 'right' && (state.value.length > 0 || config.isMultiSelect) && ( - - )} -
  • - ) - } -) - -/// -/// SelectButton -/// -export type SelectButtonProps = ButtonHTMLAttributes & { - placeholder?: ReactNode, - selectedDisplay?: (value: string[]) => ReactNode, - hideExpansionIcon?: boolean, -} - -export const SelectButton = forwardRef(function SelectButton({ - id, - placeholder, - selectedDisplay, - hideExpansionIcon = false, - ...props -}, ref) { - const translation = useHightideTranslation() - const { state, trigger, setIds, ids } = useSelectContext() - const { register, unregister, toggleOpen } = trigger - - useEffect(() => { - if(id) { - setIds(prev => ({ - ...prev, - trigger: id, - })) - } - }, [id, setIds]) - const innerRef = useRef(null) - useImperativeHandle(ref, () => innerRef.current) - - useEffect(() => { - register(innerRef) - return () => unregister() - }, [register, unregister]) - - const disabled = !!props?.disabled || !!state.disabled - const invalid = state.invalid - const hasValue = state.value.length > 0 - - return ( - - ) -}) - -/// -/// SelectContent -/// -export type SelectContentProps = PopUpProps - -export const SelectContent = forwardRef(function SelectContent({ - id, - options, - ...props -}, ref) { - const innerRef = useRef(null) - useImperativeHandle(ref, () => innerRef.current) - - const { trigger, state, config, item, ids, setIds } = useSelectContext() - - useEffect(() => { - if(id) { - setIds(prev => ({ - ...prev, - content: id, - })) - } - }, [id, setIds]) - - return ( - { - trigger.toggleOpen(false) - props.onClose?.() - }} - - aria-labelledby={ids.trigger} - > -
      { - switch (event.key) { - case 'ArrowDown': - item.moveHighlightedIndex(1) - event.preventDefault() - break - case 'ArrowUp': - item.moveHighlightedIndex(-1) - event.preventDefault() - break - case 'Home': - // TODO support later by selecting the first not disabled entry - event.preventDefault() - break - case 'End': - // TODO support later by selecting the last not disabled entry - event.preventDefault() - break - case 'Enter': // Fall through - case ' ': - if (state.highlightedValue) { - item.toggleSelection(state.highlightedValue) - if (!config.isMultiSelect) { - trigger.toggleOpen(false) - } - event.preventDefault() - } - break - } - }} - - className={clsx('flex-col-0 p-2 overflow-auto')} - - role="listbox" - aria-multiselectable={config.isMultiSelect} - aria-orientation="vertical" - tabIndex={0} - > - {props.children} -
    -
    - ) -}) - -/// -/// MultiSelectOption -/// -export type MultiSelectOptionProps = SelectOptionProps - -export const MultiSelectOption = SelectOption - - -/// -/// MultiSelectContent -/// -export type MultiSelectContentProps = SelectContentProps - -export const MultiSelectContent = SelectContent - -/// -/// MultiSelectButton -/// -export type MultiSelectButtonProps = SelectButtonProps - -export const MultiSelectButton = SelectButton diff --git a/src/components/user-interaction/select/SelectContext.tsx b/src/components/user-interaction/select/SelectContext.tsx deleted file mode 100644 index b8547a88..00000000 --- a/src/components/user-interaction/select/SelectContext.tsx +++ /dev/null @@ -1,374 +0,0 @@ -import type { Dispatch, PropsWithChildren, ReactNode, SetStateAction } from 'react' -import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react' -import type { FormFieldInteractionStates } from '../../form/FieldLayout' -import type { FormFieldDataHandling } from '../../form/FormField' -import { useControlledState } from '@/src/hooks/useControlledState' - -// -// Context -// -type RegisteredOption = { - value: string, - label: ReactNode, - disabled: boolean, - ref: React.RefObject, - } - -export type HighlightStartPositionBehavior = 'first' | 'last' -export type SelectIconAppearance = 'left' | 'right' | 'none' - -type InternalSelectContextState = { - isOpen: boolean, - options: RegisteredOption[], - highlightedValue?: string, -} - -type SelectContextIds = { - trigger: string, - content: string, -} - -type SelectContextState = InternalSelectContextState & FormFieldInteractionStates & { - value: string[], - selectedOptions: RegisteredOption[], -} - -type SelectConfiguration = { -isMultiSelect: boolean, -iconAppearance: SelectIconAppearance, -} - -type ToggleOpenOptions = { -highlightStartPositionBehavior?: HighlightStartPositionBehavior, -} - -const defaultToggleOpenOptions: ToggleOpenOptions = { - highlightStartPositionBehavior: 'first', -} - - type SelectContextType = { - ids: SelectContextIds, - setIds: Dispatch>, - state: SelectContextState, - config: SelectConfiguration, - item: { - register: (item: RegisteredOption) => void, - unregister: (value: string) => void, - toggleSelection: (value: string, isSelected?: boolean) => void, - highlightItem: (value: string) => void, - moveHighlightedIndex: (delta: number) => void, - }, - trigger: { - ref: React.RefObject, - register: (element: React.RefObject) => void, - unregister: () => void, - toggleOpen: (isOpen?: boolean, options?: ToggleOpenOptions) => void, - }, - } - -export const SelectContext = createContext(null) - -export function useSelectContext() { - const ctx = useContext(SelectContext) - if (!ctx) { - throw new Error('useSelectContext must be used within a SelectRoot or MultiSelectRoot') - } - return ctx -} - - -// -// PrimitiveSelectRoot -// -export type SharedSelectRootProps = Partial & PropsWithChildren & { - id?: string, - initialIsOpen?: boolean, - iconAppearance?: SelectIconAppearance, - onClose?: () => void, - } - -type PrimitiveSelectRootProps = SharedSelectRootProps & { - initialValue?: string, - value?: string, - onValueChange?: (value: string) => void, - initialValues?: string[], - values?: string[], - onValuesChange?: (value: string[]) => void, - isMultiSelect?: boolean, -} - -const PrimitveSelectRoot = ({ - children, - id, - initialValue, - value: controlledValue, - onValueChange, - initialValues, - values: controlledValues, - onValuesChange, - onClose, - initialIsOpen = false, - disabled = false, - readOnly = false, - required = false, - invalid = false, - isMultiSelect = false, - iconAppearance = 'left', -}: PrimitiveSelectRootProps) => { - const [value, setValue] = useControlledState({ - value: controlledValue, - onValueChange, - defaultValue: initialValue, - }) - const [values, setValues] = useControlledState({ - value: controlledValues, - onValueChange: onValuesChange, - defaultValue: initialValues ?? [], - }) - - const triggerRef = useRef(null) - const generatedId = useId() - const [ids, setIds] = useState({ - trigger: id ?? (isMultiSelect ? 'multi-select-' + generatedId : 'select-' + generatedId), - content: isMultiSelect ? 'multi-select-content-' + generatedId : 'select-content-' + generatedId, - }) - - const [internalState, setInternalState] = useState({ - isOpen: initialIsOpen, - options: [], - }) - - const selectedValues = useMemo(() => isMultiSelect ? (values ?? []) : [value].filter(Boolean), - [isMultiSelect, value, values]) - - const selectedOptions = useMemo(() => - selectedValues.map(value => internalState.options.find(option => value === option.value)).filter(Boolean), - [selectedValues, internalState.options]) - - const state: SelectContextState = { - ...internalState, - disabled, - invalid, - readOnly, - required, - value: selectedValues, - selectedOptions, - } - - const config: SelectConfiguration = { - isMultiSelect, - iconAppearance, - } - - const registerItem = useCallback((item: RegisteredOption) => { - setInternalState(prev => { - const updatedOptions = [...prev.options, item] - updatedOptions.sort((a, b) => { - const aEl = a.ref.current - const bEl = b.ref.current - if (!aEl || !bEl) return 0 - return aEl.compareDocumentPosition(bEl) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1 - }) - return { - ...prev, - options: updatedOptions, - } - }) - }, []) - - const unregisterItem = useCallback((value: string) => { - setInternalState(prev => { - const updatedOptions = prev.options.filter(i => i.value !== value) - return { - ...prev, - options: updatedOptions, - } - }) - }, []) - - // Setting isSelected to false only works for multiselects - const toggleSelection = (value: string, isSelected?: boolean) => { - if (disabled) { - return - } - const option = state.options.find(i => i.value === value) - if (!option) { - console.error(`SelectOption with value: ${value} not found`) - return - } - - let newValue: string[] - if (isMultiSelect) { - const isSelectedBefore = state.value.includes(value) - const isSelectedAfter = isSelected ?? !isSelectedBefore - if (!isSelectedAfter) { - newValue = state.value.filter(v => v !== value) - } else { - newValue = [...state.value, value] - } - } else { - newValue = [value] - } - - if (!isMultiSelect) { - setValue(newValue[0]) - } else { - setValues(newValue) - } - - setInternalState(prevState => ({ - ...prevState, - highlightedValue: value, - })) - } - - const highlightItem = (value: string) => { - if (disabled) { - return - } - setInternalState(prevState => ({ - ...prevState, - highlightedValue: value, - })) - } - - const registerTrigger = useCallback((ref: React.RefObject) => { - triggerRef.current = ref.current - }, []) - - const unregisterTrigger = useCallback(() => { - triggerRef.current = null - }, []) - - const toggleOpen = (isOpen?: boolean, toggleOpenOptions?: ToggleOpenOptions) => { - const { highlightStartPositionBehavior } = { ...defaultToggleOpenOptions, ...toggleOpenOptions } - let firstSelectedValue: string | undefined - let firstEnabledValue: string | undefined - for (let i = 0; i < state.options.length; i++) { - const currentOption = state.options[highlightStartPositionBehavior === 'first' ? i : state.options.length - i - 1] - if (!currentOption.disabled) { - if (!firstEnabledValue) { - firstEnabledValue = currentOption.value - } - if (selectedValues.includes(currentOption.value)) { - firstSelectedValue = currentOption.value - break - } - } - } - const newIsOpen = isOpen ?? !internalState.isOpen - setInternalState(prevState => ({ - ...prevState, - isOpen: newIsOpen, - highlightedValue: firstSelectedValue ?? firstEnabledValue - })) - if (!newIsOpen) { - onClose?.() - } - } - - const moveHighlightedIndex = (delta: number) => { - let highlightedIndex = state.options.findIndex(value => value.value === internalState.highlightedValue) - if (highlightedIndex === -1) { - highlightedIndex = 0 - } - const optionLength = state.options.length - const startIndex = (highlightedIndex + (delta % optionLength) + optionLength) % optionLength - const isForward = delta >= 0 - let highlightedValue = state.options[startIndex].value - for (let i = 0; i < state.options.length; i++) { - const index = (startIndex + (isForward ? i : -i) + optionLength) % optionLength - if (!state.options[index].disabled) { - highlightedValue = state.options[index].value - break - } - } - - setInternalState(prevState => ({ - ...prevState, - highlightedValue, - })) - } - - useEffect(() => { - if (!internalState.highlightedValue) return - const highlighted = internalState.options.find(value => value.value === internalState.highlightedValue) - if (highlighted) { - highlighted.ref.current?.scrollIntoView({ behavior: 'instant', block: 'nearest' }) - } else { - console.error(`SelectRoot: Could not find highlighted value (${internalState.highlightedValue})`) - } - }, [internalState.highlightedValue, internalState.options]) - - const contextValue: SelectContextType = { - ids, - setIds, - state, - config, - item: { - register: registerItem, - unregister: unregisterItem, - toggleSelection, - highlightItem, - moveHighlightedIndex, - }, - trigger: { - ref: triggerRef, - register: registerTrigger, - unregister: unregisterTrigger, - toggleOpen, - }, - } - - return ( - - {children} - - ) -} - -// -// SelectRoot -// -export type SelectRootProps = SharedSelectRootProps & Partial> & { - initialValue?: string, -} - -export const SelectRoot = ({ value, onValueChange, onEditComplete, ...props }: SelectRootProps) => { - return ( - { - onValueChange?.(value) - onEditComplete?.(value) - }} - /> - ) -} - -// -// MultiSelectRoot -// -export type MultiSelectRootProps = SharedSelectRootProps & Partial> & { - initialValue?: string[], -} - -export const MultiSelectRoot = ( { value, onValueChange, initialValue, onEditComplete,...props }: MultiSelectRootProps) => { - return ( - { - onValueChange?.(values) - }} - onClose={() => { - onEditComplete?.(value) - props.onClose?.() - }} - /> - ) -} \ No newline at end of file diff --git a/src/components/utils/Polymorphic.tsx b/src/components/utils/Polymorphic.tsx new file mode 100644 index 00000000..892befba --- /dev/null +++ b/src/components/utils/Polymorphic.tsx @@ -0,0 +1,23 @@ +import type { SlotProps } from '@radix-ui/react-slot' +import { Slot } from '@radix-ui/react-slot' +import type { RefObject , Ref } from 'react' +import { forwardRef, type ElementType } from 'react' + +export interface PolymorphicSlotProps extends SlotProps { + asChild?: boolean, + defaultComponent?: ElementType, +} + +export const PolymorphicSlot = forwardRef(function PolymorphicSlot({ + children, + asChild, + defaultComponent = 'div', + ...props +}: PolymorphicSlotProps, ref: RefObject) { + const Component = asChild ? Slot : defaultComponent + return ( + }> + {children} + + ) +}) \ No newline at end of file diff --git a/src/hooks/useControlledState.ts b/src/hooks/useControlledState.ts index ef571c12..7b1b4f99 100644 --- a/src/hooks/useControlledState.ts +++ b/src/hooks/useControlledState.ts @@ -50,7 +50,6 @@ export const useControlledState = ({ const setState: React.Dispatch> = useCallback((action) => { const resolved = resolveSetState(action, lastValue.current) - if(resolved === lastValue.current) return if(!isControlled) { lastValue.current = resolved setInternalValue(resolved) diff --git a/src/hooks/useListNavigation.tsx b/src/hooks/useListNavigation.tsx new file mode 100644 index 00000000..b3a18a9d --- /dev/null +++ b/src/hooks/useListNavigation.tsx @@ -0,0 +1,80 @@ +import { useCallback, useMemo } from 'react' +import { useControlledState } from '@/src/hooks/useControlledState' + +export interface ListNavigationReturn { + highlightedId: string | null, + highlight: (id: string) => void, + first: () => void, + last: () => void, + next: () => void, + previous: () => void, +} + +export interface ListNavigationOptions { + options: ReadonlyArray, + value?: string | null, + onValueChange?: (highlightedId: string | null) => void, + initialValue?: string | null, +} + +export function useListNavigation({ + options, + value, + onValueChange, + initialValue, +}: ListNavigationOptions): ListNavigationReturn { + const [highlightedId, setHighlightedId] = useControlledState({ + value, + onValueChange, + defaultValue: initialValue, + }) + + const resolvedHighlightId = useMemo(() => { + if (options.length === 0) return null + if (highlightedId != null && options.includes(highlightedId)) { + return highlightedId + } + return options[0] ?? null + }, [options, highlightedId]) + + const highlight = useCallback((id: string) => { + if (!options.includes(id)) return + setHighlightedId(id) + }, [options, setHighlightedId]) + + const next = useCallback(() => { + if (options.length <= 1 || resolvedHighlightId === null) return + const idx = options.indexOf(resolvedHighlightId) + const nextIdx = idx < 0 ? 0 : (idx + 1) % options.length + setHighlightedId(options[nextIdx]) + }, [options, resolvedHighlightId, setHighlightedId]) + + const previous = useCallback(() => { + if (options.length <= 1 || resolvedHighlightId === null) return + const idx = options.indexOf(resolvedHighlightId) + const previousIdx = + idx <= 0 + ? options.length - 1 + : (idx - 1 + options.length) % options.length + setHighlightedId(options[previousIdx]) + }, [options, resolvedHighlightId, setHighlightedId]) + + const first = useCallback(() => { + if (options.length <= 1 || resolvedHighlightId === null) return + setHighlightedId(options[0]) + }, [options, resolvedHighlightId, setHighlightedId]) + + const last = useCallback(() => { + if (options.length <= 1 || resolvedHighlightId === null) return + setHighlightedId(options[options.length - 1]) + }, [options, resolvedHighlightId, setHighlightedId]) + + return useMemo((): ListNavigationReturn => ({ + highlightedId: resolvedHighlightId, + highlight, + first, + last, + next, + previous, + }), [resolvedHighlightId, highlight, first, last, next, previous]) +} diff --git a/src/hooks/useMultiSelection.ts b/src/hooks/useMultiSelection.ts new file mode 100644 index 00000000..36af1d57 --- /dev/null +++ b/src/hooks/useMultiSelection.ts @@ -0,0 +1,64 @@ +import { useControlledState } from '@/src/hooks/useControlledState' +import { useCallback, useMemo } from 'react' + +export interface UseMultiSelectionOption { + id: string, + disabled?: boolean, +} + +export interface UseMultiSelectionOptions { + options: ReadonlyArray, + value?: ReadonlyArray, + onSelectionChange?: (selection: ReadonlyArray) => void, + initialSelection?: ReadonlyArray, + isControlled?: boolean, +} + +export interface UseMultiSelectionReturn { + selection: ReadonlyArray, + setSelection: (selection: ReadonlyArray) => void, + toggleSelection: (id: string) => void, + isSelected: (id: string) => boolean, +} + +export function useMultiSelection({ + options: optionsList, + value, + onSelectionChange, + initialSelection = [], + isControlled, +}: UseMultiSelectionOptions): UseMultiSelectionReturn { + const [selection, setSelection] = useControlledState({ + value, + onValueChange: onSelectionChange, + defaultValue: [...initialSelection], + isControlled, + }) + + const isSelected = useCallback((id: string) => selection.includes(id), [selection]) + + const toggleSelection = useCallback( + (id: string) => { + const option = optionsList.find((o) => o.id === id) + if (!option || option.disabled) return + setSelection((prev) => + prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id]) + }, + [optionsList, setSelection] + ) + + const setSelectionValue = useCallback( + (next: ReadonlyArray) => setSelection(Array.from(next)), + [setSelection] + ) + + return useMemo( + () => ({ + selection, + setSelection: setSelectionValue, + toggleSelection, + isSelected, + }), + [selection, setSelectionValue, toggleSelection, isSelected] + ) +} diff --git a/src/hooks/useOverlayRegistry.ts b/src/hooks/useOverlayRegistry.ts index ebb85593..86a880ef 100644 --- a/src/hooks/useOverlayRegistry.ts +++ b/src/hooks/useOverlayRegistry.ts @@ -73,14 +73,10 @@ export class OverlayRegistry { zIndex: startZIndex + index, } for(const tag of item.tags ?? []) { - let position = tagCount[tag] - if(position === undefined) { - position = 0 - } else { - position++ - } - tagCount[tag] = position - itemInformation[id].tagPositions[tag] = position + const count = tagCount[tag] ?? 0 + const nextPosition = count + tagCount[tag] = count + 1 + itemInformation[id].tagPositions[tag] = nextPosition } } for (const callback of this.listeners) { diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts index 5de70765..9047e048 100644 --- a/src/hooks/useSearch.ts +++ b/src/hooks/useSearch.ts @@ -1,72 +1,36 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { MultiSubjectSearchWithMapping } from '@/src/utils/simpleSearch' - -export type UseSearchProps = { - list: T[], - searchMapping: (item: T) => string[], - initialSearch?: string, - additionalSearchTags?: string[], - isSearchInstant?: boolean, - sortingFunction?: (a: T, b: T) => number, - filter?: (item: T) => boolean, - disabled?: boolean, +import { useMemo } from 'react' +import { MultiSearchWithMapping } from '@/src/utils/simpleSearch' +import { useEventCallbackStabilizer } from '@/src/hooks/useEventCallbackStabelizer' + +export interface UseSearchOptions { + items: ReadonlyArray, + searchQuery: string, + toTags?: (value: T) => string[], } -export const useSearch = ({ - list, - initialSearch, - searchMapping, - additionalSearchTags, - isSearchInstant = true, - sortingFunction, - filter, - disabled = false, -}: UseSearchProps) => { - const [search, setSearch] = useState(initialSearch ?? '') - const [result, setResult] = useState(list) - const searchTags = useMemo(() => additionalSearchTags ?? [], [additionalSearchTags]) - - const updateSearch = useCallback((newSearch?: string) => { - const usedSearch = newSearch ?? search - if (newSearch) { - setSearch(search) - } - setResult(MultiSubjectSearchWithMapping([usedSearch, ...searchTags], list, searchMapping)) - }, [searchTags, list, search, searchMapping]) - - useEffect(() => { - if (isSearchInstant) { - setResult(MultiSubjectSearchWithMapping([search, ...searchTags], list, searchMapping)) - } - }, [searchTags, isSearchInstant, list, search, searchMapping, additionalSearchTags]) - - const filteredResult: T[] = useMemo(() => { - if (!filter) { - return result - } - return result.filter(filter) - }, [result, filter]) - - const sortedAndFilteredResult: T[] = useMemo(() => { - if (!sortingFunction) { - return filteredResult - } - return filteredResult.sort(sortingFunction) - }, [filteredResult, sortingFunction]) +export interface UseSearchReturn { + searchResult: ReadonlyArray, +} - const usedResult = useMemo(() => { - if (!disabled) { - return sortedAndFilteredResult - } - return list - }, [disabled, list, sortedAndFilteredResult]) +function defaultToTags(value: T): string[] { + return [String(value)] +} - return { - result: usedResult, - hasResult: usedResult.length > 0, - allItems: list, - updateSearch, - search, - setSearch, - } -} \ No newline at end of file +export function useSearch({ + items, + searchQuery, + toTags, +}: UseSearchOptions): UseSearchReturn { + const toTagsResolved = toTags ?? defaultToTags + const toTagsStable = useEventCallbackStabilizer(toTagsResolved) + + const searchResult = useMemo(() => { + const q = searchQuery.trim().toLowerCase() + if (!q) return items + return MultiSearchWithMapping(searchQuery, [...items], (item) => toTagsStable(item)) + }, [items, searchQuery, toTagsStable]) + + return useMemo((): UseSearchReturn => ({ + searchResult, + }), [searchResult]) +} diff --git a/src/hooks/useSingleSelection.ts b/src/hooks/useSingleSelection.ts new file mode 100644 index 00000000..293dd108 --- /dev/null +++ b/src/hooks/useSingleSelection.ts @@ -0,0 +1,103 @@ +import { useCallback, useMemo } from 'react' +import { useControlledState } from '@/src/hooks/useControlledState' + +export interface SelectionOption { + id: string, + disabled?: boolean, +} + +export interface UseSingleSelectionOptions { + options: ReadonlyArray, + selection?: string | null, + onSelectionChange?: (selection: string | null) => void, + initialSelection?: string | null, + isLooping?: boolean, +} + +export interface SingleSelectionReturn { + selection: string | null, + selectedIndex: number | null, + selectByIndex: (index: number) => void, + selectValue: (value: string | null) => void, + selectFirst: () => void, + selectLast: () => void, + selectNext: () => void, + selectPrevious: () => void, +} + +export function useSingleSelection({ + options: optionsList, + selection: controlledSelection, + onSelectionChange, + initialSelection, + isLooping = true, +}: UseSingleSelectionOptions): SingleSelectionReturn { + const [selection, setSelection] = useControlledState({ + value: controlledSelection, + onValueChange: onSelectionChange, + defaultValue: initialSelection, + }) + + const selectedIndex = useMemo(() => { + return optionsList.findIndex((o) => o.id === selection) + }, [optionsList, selection]) + + const enabledOptions = useMemo(() => optionsList.filter((o) => !o.disabled), [optionsList]) + + const changeSelection = useCallback((next: string | null) => { + const option = enabledOptions.find((o) => o.id === next) + if(!option && next != null) { + console.warn(`Attempted to select an option ${next} that is not valid or disabled`) + return + } + setSelection(option?.id ?? null) + }, [enabledOptions, setSelection]) + + const selectByIndex = useCallback((index: number) => { + const option = optionsList[index] + if(!option || option.disabled || index < 0 || index >= optionsList.length) { + console.warn(`Attempted to select an index ${index} that is not valid or disabled`) + return + } + setSelection(option.id) + }, [optionsList, setSelection]) + + const selectFirst = useCallback(() => { + if(enabledOptions.length === 0) return + const first = enabledOptions.find((o) => !o.disabled) + setSelection(first?.id ?? null) + }, [enabledOptions, setSelection]) + + const selectLast = useCallback(() => { + if(enabledOptions.length === 0) return + const last = [...enabledOptions].reverse().find((o) => !o.disabled) + setSelection(last?.id ?? null) + }, [enabledOptions, setSelection]) + + const selectNext = useCallback(() => { + if(enabledOptions.length === 0) return + let currentIndex = enabledOptions.findIndex((o) => o.id === selection) + if(currentIndex === -1) currentIndex = 0 + const nextIndex = isLooping ? (currentIndex + 1) % enabledOptions.length : Math.min(currentIndex + 1, enabledOptions.length - 1) + setSelection(enabledOptions[nextIndex].id) + }, [enabledOptions, selection, isLooping, setSelection]) + + const selectPrevious = useCallback(() => { + if(enabledOptions.length === 0) return + let currentIndex = enabledOptions.findIndex((o) => o.id === selection) + if(currentIndex === -1) currentIndex = enabledOptions.length + const previousIndex = isLooping ? (currentIndex - 1 + enabledOptions.length) % enabledOptions.length : Math.max(currentIndex - 1, 0) + setSelection(enabledOptions[previousIndex].id) + }, [enabledOptions, selection, isLooping, setSelection]) + + return useMemo(() => ({ + selection, + selectedIndex, + selectByIndex, + selectValue: changeSelection, + selectFirst, + selectLast, + selectNext, + selectPrevious, + }), [selection, selectedIndex, changeSelection, selectFirst, selectLast, selectNext, selectPrevious, selectByIndex]) +} diff --git a/src/hooks/useTypeAheadSearch.ts b/src/hooks/useTypeAheadSearch.ts new file mode 100644 index 00000000..f2f8644f --- /dev/null +++ b/src/hooks/useTypeAheadSearch.ts @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useRef } from 'react' +import { useEventCallbackStabilizer } from '@/src/hooks/useEventCallbackStabelizer' + +export interface UseTypeAheadSearchOptions { + options: ReadonlyArray, + resetTimer: number, + toString?: (value: T) => string, + onResultChange: (value: T | null) => void, +} + +export interface UseTypeAheadSearchReturn { + addToTypeAhead: (str: string) => void, + reset: () => void, +} + +function defaultToString(value: T): string { + return String(value) +} + +export function useTypeAheadSearch({ + options, + resetTimer, + toString: toStringProp, + onResultChange, +}: UseTypeAheadSearchOptions): UseTypeAheadSearchReturn { + const bufferRef = useRef('') + const timeoutRef = useRef | null>(null) + + const toString = toStringProp ?? defaultToString + const toStringStable = useEventCallbackStabilizer(toString) + const onResultChangeStable = useEventCallbackStabilizer(onResultChange) + + const reset = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + bufferRef.current = '' + onResultChangeStable(null) + }, [onResultChangeStable]) + + useEffect(() => () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + }, []) + + const addToTypeAhead = useCallback((str: string) => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + bufferRef.current += str + timeoutRef.current = setTimeout(() => { + timeoutRef.current = null + bufferRef.current = '' + onResultChangeStable(null) + }, resetTimer) + + const buf = bufferRef.current.trim().toLowerCase() + if (!buf) { + onResultChangeStable(null) + return + } + const found = options.find((opt) => { + const s = toStringStable(opt)?.trim().toLowerCase() ?? '' + return s.startsWith(buf) + }) + onResultChangeStable(found ?? null) + }, [options, resetTimer, toStringStable, onResultChangeStable]) + + return { addToTypeAhead, reset } +} diff --git a/src/style/theme/colors/utilities.css b/src/style/theme/colors/utilities.css index ebf81336..25d24037 100644 --- a/src/style/theme/colors/utilities.css +++ b/src/style/theme/colors/utilities.css @@ -3,6 +3,7 @@ --coloring-color: var(--color-primary); --coloring-on-color: var(--color-on-primary); --coloring-hover: var(--color-primary-hover); + --color-focus: var(--color-primary); } @utility secondary { @@ -10,6 +11,7 @@ --coloring-color: var(--color-secondary); --coloring-on-color: var(--color-on-secondary); --coloring-hover: var(--color-secondary-hover); + --color-focus: var(--color-secondary); } @utility positive { @@ -17,6 +19,7 @@ --coloring-color: var(--color-positive); --coloring-on-color: var(--color-on-positive); --coloring-hover: var(--color-positive-hover); + --color-focus: var(--color-positive); } @utility negative { @@ -24,6 +27,7 @@ --coloring-color: var(--color-negative); --coloring-on-color: var(--color-on-negative); --coloring-hover: var(--color-negative-hover); + --color-focus: var(--color-negative); } @utility warning { @@ -31,6 +35,7 @@ --coloring-color: var(--color-warning); --coloring-on-color: var(--color-on-warning); --coloring-hover: var(--color-warning-hover); + --color-focus: var(--color-warning); } @utility neutral { @@ -73,6 +78,7 @@ @apply data-[coloringstyle=text]:coloring-text; @apply data-[coloringstyle=outline]:coloring-outline; @apply data-[coloringstyle=tonal]:coloring-tonal; + @apply data-[coloringstyle=tonal-outline]:coloring-tonal-outline; } @utility coloring-style-hover-detect { @@ -80,6 +86,7 @@ @apply data-[coloringstyle=text]:coloring-text-hover; @apply data-[coloringstyle=outline]:coloring-outline-hover; @apply data-[coloringstyle=tonal]:coloring-tonal-hover; + @apply data-[coloringstyle=tonal-outline]:coloring-tonal-outline-hover; } @utility coloring-color-detect { @@ -92,4 +99,4 @@ @apply data-[color=description]:description; @apply data-[color=surface]:surface; @apply data-[color=disabled]:disabled; -} +} \ No newline at end of file diff --git a/src/style/theme/components/button.css b/src/style/theme/components/button.css index 6991b471..16003f65 100644 --- a/src/style/theme/components/button.css +++ b/src/style/theme/components/button.css @@ -14,7 +14,8 @@ &[data-size="xs"] { @apply gap-x-1 sizing-xs; @apply text-xs min-w-20; - &[data-coloringstyle="outline"] { + &[data-coloringstyle="outline"], + &[data-coloringstyle="tonal-outline"] { padding: calc(var(--spacing-element-padding-xs) - var(--coloring-outline-width)) calc(var(--spacing-element-padding-direction-xs) - var(--coloring-outline-width)); @@ -23,7 +24,8 @@ &[data-size="sm"] { @apply gap-x-1 sizing-sm; @apply min-w-28; - &[data-coloringstyle="outline"] { + &[data-coloringstyle="outline"], + &[data-coloringstyle="tonal-outline"] { padding: calc(var(--spacing-element-padding-sm) - var(--coloring-outline-width)) calc(var(--spacing-element-padding-direction-sm) - var(--coloring-outline-width)); @@ -32,7 +34,8 @@ &[data-size="md"] { @apply gap-x-2 sizing-md; @apply min-w-36; - &[data-coloringstyle="outline"] { + &[data-coloringstyle="outline"], + &[data-coloringstyle="tonal-outline"] { padding: calc(var(--spacing-element-padding-md) - var(--coloring-outline-width)) calc(var(--spacing-element-padding-direction-md) - var(--coloring-outline-width)); @@ -41,7 +44,8 @@ &[data-size="lg"] { @apply gap-x-2 sizing-lg; @apply text-lg min-w-45; - &[data-coloringstyle="outline"] { + &[data-coloringstyle="outline"], + &[data-coloringstyle="tonal-outline"] { padding: calc(var(--spacing-element-padding-lg) - var(--coloring-outline-width)) calc(var(--spacing-element-padding-direction-lg) - var(--coloring-outline-width)); diff --git a/src/style/theme/components/checkbox.css b/src/style/theme/components/checkbox.css index 5132fe60..860cc2d1 100644 --- a/src/style/theme/components/checkbox.css +++ b/src/style/theme/components/checkbox.css @@ -1,6 +1,14 @@ @layer components { [data-name="checkbox"] { @apply flex-col-0 items-center justify-center rounded border-2; + transition: + border-color var(--animation-duration-in, 250ms) ease-in-out, + box-shadow var(--animation-duration-in, 250ms) ease-in-out, + outline-color var(--animation-duration-in, 250ms) ease-in-out, + outline-offset var(--animation-duration-in, 250ms) ease-in-out, + color 100ms ease-in-out, + background-color 100ms ease-in-out; + &:not([data-disabled]) { @apply hover:cursor-pointer; } diff --git a/src/style/theme/components/combobox.css b/src/style/theme/components/combobox.css new file mode 100644 index 00000000..18d8c1d5 --- /dev/null +++ b/src/style/theme/components/combobox.css @@ -0,0 +1,25 @@ +@layer components { + [data-name="combobox-root"] { + @apply flex-col-2; + } + + [data-name="combobox-input"] { + @apply input-element rounded-md; + } + + [data-name="combobox-list"] { + @apply flex-col-1 overflow-y-auto; + } + + [data-name="combobox-option"] { + @apply flex-row-1 items-center px-2 py-1 rounded-md cursor-pointer; + + &[data-highlighted] { + @apply bg-primary/20; + } + } + + [data-name="combobox-list-status"] { + @apply text-description text-sm px-2 py-1 rounded-md; + } +} diff --git a/src/style/theme/components/general.css b/src/style/theme/components/general.css index 400a5ac4..75c1777b 100644 --- a/src/style/theme/components/general.css +++ b/src/style/theme/components/general.css @@ -23,6 +23,6 @@ } * { - @apply focus-style-outline focusable; + @apply focus-style-outline focusable outline-focus; } -} +} \ No newline at end of file diff --git a/src/style/theme/components/icon-button.css b/src/style/theme/components/icon-button.css index 0e7b9aec..bb12921c 100644 --- a/src/style/theme/components/icon-button.css +++ b/src/style/theme/components/icon-button.css @@ -12,25 +12,29 @@ &[data-size="xs"] { @apply sizing-xs; - &[data-coloringstyle="outline"] { + &[data-coloringstyle="outline"], + &[data-coloringstyle="tonal-outline"] { padding: calc(var(--spacing-element-padding-xs) - var(--coloring-outline-width)); } } &[data-size="sm"] { @apply sizing-sm; - &[data-coloringstyle="outline"] { + &[data-coloringstyle="outline"], + &[data-coloringstyle="tonal-outline"] { padding: calc(var(--spacing-element-padding-sm) - var(--coloring-outline-width)); } } &[data-size="md"] { @apply sizing-md; - &[data-coloringstyle="outline"] { + &[data-coloringstyle="outline"], + &[data-coloringstyle="tonal-outline"] { padding: calc(var(--spacing-element-padding-md) - var(--coloring-outline-width)); } } &[data-size="lg"] { @apply sizing-lg; - &[data-coloringstyle="outline"] { + &[data-coloringstyle="outline"], + &[data-coloringstyle="tonal-outline"] { padding: calc(var(--spacing-element-padding-lg) - var(--coloring-outline-width)); } } diff --git a/src/style/theme/components/index.css b/src/style/theme/components/index.css index 98a2fbbd..2b30c1c0 100644 --- a/src/style/theme/components/index.css +++ b/src/style/theme/components/index.css @@ -24,6 +24,7 @@ @import "./property.css"; @import "./pop-up.css"; @import "./select.css"; +@import "./combobox.css"; @import "./general.css"; @import "./icon-button.css"; @import "./date-time-input.css"; \ No newline at end of file diff --git a/src/style/theme/components/input-elements.css b/src/style/theme/components/input-elements.css index 6bbe81e8..dcc8960d 100644 --- a/src/style/theme/components/input-elements.css +++ b/src/style/theme/components/input-elements.css @@ -1,5 +1,13 @@ @utility input-element { @apply border-2 focus-style-none focus-style-border focus-style-shadow; + transition: + border-color var(--animation-duration-in, 250ms) ease-in-out, + box-shadow var(--animation-duration-in, 250ms) ease-in-out, + outline-color var(--animation-duration-in, 250ms) ease-in-out, + outline-offset var(--animation-duration-in, 250ms) ease-in-out, + color var(--animation-duration-in, 250ms) ease-in-out, + background-color var(--animation-duration-in, 250ms) ease-in-out; + &:not([data-disabled]):not([data-invalid]) { @apply bg-input-background hover:border-primary-hover; diff --git a/src/style/theme/components/pop-up.css b/src/style/theme/components/pop-up.css index fc116c00..dec7ebcb 100644 --- a/src/style/theme/components/pop-up.css +++ b/src/style/theme/components/pop-up.css @@ -1,7 +1,14 @@ [data-name="pop-up"] { - @apply surface coloring-solid rounded-md border-2 border-outline-variant shadow-md; + @apply surface coloring-solid rounded-md border-2 border-outline-variant shadow-lg shadow-black/15; @apply focus-within:border-primary; + transition: + top var(--anchored-position-polling-interval, 100ms) linear, + left var(--anchored-position-polling-interval, 100ms) linear, + right var(--anchored-position-polling-interval, 100ms) linear, + bottom var(--anchored-position-polling-interval, 100ms) linear, + border-color var(--animation-duration-in, 250ms) ease-in-out, + box-shadow var(--animation-duration-in, 250ms) ease-in-out; &[data-positioned] { @apply animate-pop-in; } diff --git a/src/style/theme/components/select.css b/src/style/theme/components/select.css index 89e60f00..02734153 100644 --- a/src/style/theme/components/select.css +++ b/src/style/theme/components/select.css @@ -1,19 +1,50 @@ @layer components { - [data-name="select-button"] { - @apply input-element flex-row-2 items-center justify-between rounded-md px-3 py-2; - &:not([data-disabled]) { - @apply hover:cursor-pointer; - } + + [data-name="select-button"], + [data-name="multi-select-button"] { + @apply input-element flex-row-2 items-center justify-between rounded-md px-3 py-2; + + &:not([data-disabled]) { + @apply hover:cursor-pointer; } - - [data-name="select-chip-display"] { - @apply input-element flex flex-wrap gap-2 items-center rounded-md px-2.5 py-2.5; - &:not([data-disabled]) { - @apply hover:cursor-pointer; - } + } + + [data-name="multi-select-chip-display-button"] { + @apply input-element flex flex-wrap gap-2 items-center rounded-md px-2.5 py-2.5; + + &:not([data-disabled]) { + @apply hover:cursor-pointer; + } + } + + [data-name="multi-select-chip-display-chip"] { + @apply flex-row-1 items-center pl-2 pr-1 coloring-solid neutral rounded-md h-9; + } + + [data-name="select-list"], + [data-name="multi-select-list"] { + @apply flex-col-1 overflow-y-auto; + } + + [data-name="select-list-option"], + [data-name="multi-select-list-option"] { + @apply flex-row-1 items-center px-2 py-1 rounded-md; + + &[data-disabled] { + @apply cursor-not-allowed text-disabled; } - [data-name="select-chip-display-chip"] { - @apply flex-row-1 items-center pl-2 pr-1 coloring-solid neutral rounded-md h-9; + &:not([data-disabled]) { + @apply cursor-pointer; + + &[data-highlighted] { + @apply bg-primary/20; + } } - } \ No newline at end of file + } + + [data-name="select-list-status"], + [data-name="multi-select-list-status"] { + @apply text-description px-2 py-1 rounded-md; + } +} \ No newline at end of file diff --git a/src/style/utitlity/coloring.css b/src/style/utitlity/coloring.css index 6e6e1bf6..4629dba3 100644 --- a/src/style/utitlity/coloring.css +++ b/src/style/utitlity/coloring.css @@ -1,4 +1,15 @@ +@utility coloring-transition { + transition: + border-color var(--animation-duration-in, 250ms) ease-in-out, + box-shadow var(--animation-duration-in, 250ms) ease-in-out, + outline-color var(--animation-duration-in, 250ms) ease-in-out, + outline-offset var(--animation-duration-in, 250ms) ease-in-out, + color var(--animation-duration-in, 250ms) ease-in-out, + background-color var(--animation-duration-in, 250ms) ease-in-out; +} + @utility coloring-solid { + @apply coloring-transition; @apply bg-[var(--coloring-solid-color,var(--coloring-color))] text-[var(--coloring-solid-text,var(--coloring-on-color))]; } @@ -9,6 +20,7 @@ } @utility coloring-tonal { + @apply coloring-transition; @apply bg-[var(--coloring-tonal-background,var(--coloring-tonal,var(--coloring-color)))]/20 text-[var(--coloring-tonal-text,var(--coloring-tonal,var(--coloring-color)))]; } @@ -19,6 +31,7 @@ } @utility coloring-text { + @apply coloring-transition; @apply text-[var(--coloring-text,var(--coloring-color))]; } @@ -28,6 +41,7 @@ } @utility coloring-outline { + @apply coloring-transition; border-width: var(--coloring-outline-width, 0.125rem); @apply border-[var(--coloring-border,var(--coloring-outline,var(--coloring-color)))]; @apply text-[var(--coloring-outline,var(--coloring-color))]; @@ -39,6 +53,19 @@ hover:text-[var(--coloring-outline-hover,var(--coloring-hover))]; } +@utility coloring-tonal-outline { + @apply coloring-transition coloring-tonal; + border-width: var(--coloring-outline-width, 0.125rem); + @apply border-[var(--coloring-border,var(--coloring-outline,var(--coloring-color)))]; +} + +@utility coloring-tonal-outline-hover { + @apply coloring-tonal-outline + hover:border-[var(--coloring-border-hover,--coloring-hover)] + hover:text-[var(--coloring-outline-hover,var(--coloring-hover))] + hover:bg-[var(--coloring-tonal-hover,var(--coloring-tonal-background,var(--coloring-tonal,var(--coloring-color))))]/30; +} + @utility coloring-reset-variables { --coloring-color: initial; --coloring-on-color: initial; diff --git a/src/style/utitlity/focus.css b/src/style/utitlity/focus.css index 392248a0..a8f9d904 100644 --- a/src/style/utitlity/focus.css +++ b/src/style/utitlity/focus.css @@ -9,7 +9,7 @@ } @utility focus-style-shadow { - --focus-box-shadow: 0 0 8px 2px color-mix(in srgb, var(--color-focus) 70%, transparent); + --focus-box-shadow: 0 0 calc(var(--spacing) * 1) calc(var(--spacing) * 0.5) color-mix(in srgb, var(--color-focus) 70%, transparent); } @utility focus-style-none { @@ -30,4 +30,4 @@ box-shadow: var(--focus-box-shadow); border-color: var(--focus-border-color); } -} +} \ No newline at end of file diff --git a/src/utils/date.ts b/src/utils/date.ts index 371ee28e..46a07242 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -232,6 +232,28 @@ const toInputString = (date: Date, format: DateTimeFormat, precision: DateTimePr } } +function tryParseDate(dateValue: Date | string | number | undefined | null): Date | null { + if (!dateValue) return null + if (dateValue instanceof Date) return dateValue + if (typeof dateValue === 'string' || typeof dateValue === 'number') { + const parsed = new Date(dateValue) + return isNaN(parsed.getTime()) ? null : parsed + } + return null +} + +function normalizeToDateOnly(date: Date): Date { + const normalized = new Date(date) + normalized.setHours(0, 0, 0, 0) + return normalized +} + +function normalizeDatetime(dateTime: Date): Date { + const normalized = new Date(dateTime) + normalized.setSeconds(0, 0) + return normalized +} + export const DateUtils = { monthsList, weekDayList, @@ -244,4 +266,10 @@ export const DateUtils = { weeksForCalenderMonth, timesInSeconds, toInputString, + tryParseDate, + toOnlyDate: normalizeToDateOnly, + /** + * Normalizes a datetime by removing seconds and milliseconds. + */ + toDateTimeOnly: normalizeDatetime, } \ No newline at end of file diff --git a/src/utils/dom.ts b/src/utils/dom.ts new file mode 100644 index 00000000..07049ec1 --- /dev/null +++ b/src/utils/dom.ts @@ -0,0 +1,10 @@ +function compareDocumentPosition(a: Node | null | undefined, b: Node | null | undefined) { + if (!a && !b) return 0 + if (!a) return 1 + if (!b) return -1 + return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1 +} + +export const DOMUtils = { + compareDocumentPosition, +} \ No newline at end of file diff --git a/src/utils/filter.ts b/src/utils/filter.ts deleted file mode 100644 index 78250a53..00000000 --- a/src/utils/filter.ts +++ /dev/null @@ -1,355 +0,0 @@ -import type { - TextFilterValue, - NumberFilterValue, - DateFilterValue, - DatetimeFilterValue, - BooleanFilterValue, - TagsFilterValue, - TagsSingleFilterValue, - GenericFilterValue -} from '../components/layout/table/TableFilter' -import { TableFilterOperator } from '../components/layout/table/TableFilter' - - - -/** - * Filters a text value based on the provided filter value. - */ -export function filterText(value: unknown, filterValue: TextFilterValue): boolean { - const parameter = filterValue.parameter - const operator = filterValue.operator - const isCaseSensitive = filterValue.parameter.isCaseSensitive ?? false - - if (operator === 'textNotWhitespace') { - return value?.toString().trim().length > 0 - } - - const searchText = isCaseSensitive ? parameter.searchText ?? '' : (parameter.searchText ?? '').toLowerCase() - const cellText = isCaseSensitive ? value?.toString() ?? '' : value?.toString().toLowerCase() ?? '' - - switch (operator) { - case 'textEquals': - return cellText === searchText - case 'textNotEquals': - return cellText !== searchText - case 'textContains': - return cellText.includes(searchText) - case 'textNotContains': - return !cellText.includes(searchText) - case 'textStartsWith': - return cellText.startsWith(searchText) - case 'textEndsWith': - return cellText.endsWith(searchText) - case 'undefined': - return value === undefined || value === null - case 'notUndefined': - return value !== undefined && value !== null - default: - return false - } -} - -/** - * Filters a number value based on the provided filter value. - */ -export function filterNumber(value: unknown, filterValue: NumberFilterValue): boolean { - const parameter = filterValue.parameter - const operator = filterValue.operator - - if (typeof value !== 'number') { - if (operator === 'undefined') { - return value === undefined || value === null - } - if (operator === 'notUndefined') { - return value !== undefined && value !== null - } - return false - } - - switch (operator) { - case 'numberEquals': - return value === parameter.compareValue - case 'numberNotEquals': - return value !== parameter.compareValue - case 'numberGreaterThan': - return value > (parameter.compareValue ?? 0) - case 'numberGreaterThanOrEqual': - return value >= (parameter.compareValue ?? 0) - case 'numberLessThan': - return value < (parameter.compareValue ?? 0) - case 'numberLessThanOrEqual': - return value <= (parameter.compareValue ?? 0) - case 'numberBetween': - return value >= (parameter.min ?? -Infinity) && value <= (parameter.max ?? Infinity) - case 'numberNotBetween': - return value < (parameter.min ?? -Infinity) || value > (parameter.max ?? Infinity) - case 'undefined': - return value === undefined || value === null - case 'notUndefined': - return value !== undefined && value !== null - default: - return false - } -} - -/** - * Parses a date value from various formats. - */ -function parseDate(dateValue: Date | string | number | undefined | null): Date | null { - if (!dateValue) return null - if (dateValue instanceof Date) return dateValue - if (typeof dateValue === 'string' || typeof dateValue === 'number') { - const parsed = new Date(dateValue) - return isNaN(parsed.getTime()) ? null : parsed - } - return null -} - -/** - * Normalizes a date to date-only (removes time component). - */ -function normalizeToDateOnly(date: Date): Date { - const normalized = new Date(date) - normalized.setHours(0, 0, 0, 0) - return normalized -} - -/** - * Filters a date value based on the provided filter value. - * Only compares dates, ignoring time components. - */ -export function filterDate(value: unknown, filterValue: DateFilterValue): boolean { - const parameter = filterValue.parameter - const operator = filterValue.operator - - const date = parseDate(value as Date | string | number | undefined | null) - if (!date && !TableFilterOperator.generic.some(o => o === operator)) return false - - const normalizedDate = date ? normalizeToDateOnly(date) : null - - switch (operator) { - case 'dateEquals': { - const filterDate = parseDate(parameter.compareDate) - if (!filterDate || !normalizedDate) return false - return normalizedDate.getTime() === normalizeToDateOnly(filterDate).getTime() - } - case 'dateNotEquals': { - const filterDate = parseDate(parameter.compareDate) - if (!filterDate || !normalizedDate) return false - return normalizedDate.getTime() !== normalizeToDateOnly(filterDate).getTime() - } - case 'dateGreaterThan': { - const filterDate = parseDate(parameter.compareDate) - if (!filterDate || !normalizedDate) return false - return normalizedDate > normalizeToDateOnly(filterDate) - } - case 'dateGreaterThanOrEqual': { - const filterDate = parseDate(parameter.compareDate) - if (!filterDate || !normalizedDate) return false - return normalizedDate >= normalizeToDateOnly(filterDate) - } - case 'dateLessThan': { - const filterDate = parseDate(parameter.compareDate) - if (!filterDate || !normalizedDate) return false - return normalizedDate < normalizeToDateOnly(filterDate) - } - case 'dateLessThanOrEqual': { - const filterDate = parseDate(parameter.compareDate) - if (!filterDate || !normalizedDate) return false - return normalizedDate <= normalizeToDateOnly(filterDate) - } - case 'dateBetween': { - const minDate = parseDate(parameter.min) - const maxDate = parseDate(parameter.max) - if (!minDate || !maxDate || !normalizedDate) return false - return normalizedDate >= normalizeToDateOnly(minDate) && normalizedDate <= normalizeToDateOnly(maxDate) - } - case 'dateNotBetween': { - const minDate = parseDate(parameter.min) - const maxDate = parseDate(parameter.max) - if (!minDate || !maxDate || !normalizedDate) return false - return normalizedDate < normalizeToDateOnly(minDate) || normalizedDate > normalizeToDateOnly(maxDate) - } - case 'undefined': - return value === undefined || value === null - case 'notUndefined': - return value !== undefined && value !== null - default: - return false - } -} - -/** - * Normalizes a dateTime by removing seconds and milliseconds. - */ -function normalizeDatetime(dateTime: Date): Date { - const normalized = new Date(dateTime) - normalized.setSeconds(0, 0) - return normalized -} - -/** - * Filters a dateTime value based on the provided filter value. - */ -export function filterDatetime(value: unknown, filterValue: DatetimeFilterValue): boolean { - const parameter = filterValue.parameter - const operator = filterValue.operator - - const dateTime = parseDate(value as Date | string | number | undefined | null) - if (!dateTime && !TableFilterOperator.generic.some(o => o === operator)) return false - - const normalizedDatetime = dateTime ? normalizeDatetime(dateTime) : null - - switch (operator) { - case 'dateTimeEquals': { - const filterDatetime = parseDate(parameter.compareDatetime) - if (!filterDatetime || !normalizedDatetime) return false - return normalizedDatetime.getTime() === normalizeDatetime(filterDatetime).getTime() - } - case 'dateTimeNotEquals': { - const filterDatetime = parseDate(parameter.compareDatetime) - if (!filterDatetime || !normalizedDatetime) return false - return normalizedDatetime.getTime() !== normalizeDatetime(filterDatetime).getTime() - } - case 'dateTimeGreaterThan': { - const filterDatetime = parseDate(parameter.compareDatetime) - if (!filterDatetime || !normalizedDatetime) return false - return normalizedDatetime > normalizeDatetime(filterDatetime) - } - case 'dateTimeGreaterThanOrEqual': { - const filterDatetime = parseDate(parameter.compareDatetime) - if (!filterDatetime || !normalizedDatetime) return false - return normalizedDatetime >= normalizeDatetime(filterDatetime) - } - case 'dateTimeLessThan': { - const filterDatetime = parseDate(parameter.compareDatetime) - if (!filterDatetime || !normalizedDatetime) return false - return normalizedDatetime < normalizeDatetime(filterDatetime) - } - case 'dateTimeLessThanOrEqual': { - const filterDatetime = parseDate(parameter.compareDatetime) - if (!filterDatetime || !normalizedDatetime) return false - return normalizedDatetime <= normalizeDatetime(filterDatetime) - } - case 'dateTimeBetween': { - const minDatetime = parseDate(parameter.min) - const maxDatetime = parseDate(parameter.max) - if (!minDatetime || !maxDatetime || !normalizedDatetime) return false - return normalizedDatetime >= normalizeDatetime(minDatetime) && normalizedDatetime <= normalizeDatetime(maxDatetime) - } - case 'dateTimeNotBetween': { - const minDatetime = parseDate(parameter.min) - const maxDatetime = parseDate(parameter.max) - if (!minDatetime || !maxDatetime || !normalizedDatetime) return false - return normalizedDatetime < normalizeDatetime(minDatetime) || normalizedDatetime > normalizeDatetime(maxDatetime) - } - case 'undefined': - return value === undefined || value === null - case 'notUndefined': - return value !== undefined && value !== null - default: - return false - } -} - -/** - * Filters a boolean value based on the provided filter value. - */ -export function filterBoolean(value: unknown, filterValue: BooleanFilterValue): boolean { - const operator = filterValue.operator - - switch (operator) { - case 'booleanIsTrue': - return value === true - case 'booleanIsFalse': - return value === false - case 'undefined': - return value === undefined || value === null - case 'notUndefined': - return value !== undefined && value !== null - default: - return false - } -} - -/** - * Filters a tags array value based on the provided filter value. - */ -export function filterTags(value: unknown, filterValue: TagsFilterValue): boolean { - const parameter = filterValue.parameter - const operator = filterValue.operator - - switch (operator) { - case 'tagsEquals': { - if (!Array.isArray(value) || !Array.isArray(parameter.searchTags)) return false - if (value.length !== parameter.searchTags.length) return false - const valueSet = new Set(value) - const searchTagsSet = new Set(parameter.searchTags) - if (valueSet.size !== searchTagsSet.size) return false - return Array.from(valueSet).every(tag => searchTagsSet.has(tag)) - } - case 'tagsNotEquals': { - if (!Array.isArray(value) || !Array.isArray(parameter.searchTags)) return true - if (value.length !== parameter.searchTags.length) return true - const valueSet = new Set(value) - const searchTagsSet = new Set(parameter.searchTags) - if (valueSet.size !== searchTagsSet.size) return true - return !Array.from(valueSet).every(tag => searchTagsSet.has(tag)) - } - case 'tagsContains': { - if (!Array.isArray(value) || !Array.isArray(parameter.searchTags)) return false - return parameter.searchTags.every(tag => value.includes(tag)) - } - case 'tagsNotContains': { - if (!Array.isArray(value) || !Array.isArray(parameter.searchTags)) return true - return !parameter.searchTags.every(tag => value.includes(tag)) - } - case 'undefined': - return value === undefined || value === null - case 'notUndefined': - return value !== undefined && value !== null - default: - return false - } -} - -/** - * Filters a single tag value based on the provided filter value. - */ -export function filterTagsSingle(value: unknown, filterValue: TagsSingleFilterValue): boolean { - const parameter = filterValue.parameter - const operator = filterValue.operator - - switch (operator) { - case 'tagsSingleEquals': - return value === parameter.searchTag - case 'tagsSingleNotEquals': - return value !== parameter.searchTag - case 'tagsSingleContains': - return parameter.searchTagsContains?.includes(value) ?? false - case 'tagsSingleNotContains': - return !(parameter.searchTagsContains?.includes(value) ?? false) - case 'undefined': - return value === undefined || value === null - case 'notUndefined': - return value !== undefined && value !== null - default: - return false - } -} - -/** - * Filters a generic value based on the provided filter value. - */ -export function filterGeneric(value: unknown, filterValue: GenericFilterValue): boolean { - const operator = filterValue.operator - - switch (operator) { - case 'undefined': - return value === undefined || value === null - case 'notUndefined': - return value !== undefined && value !== null - default: - return false - } -} diff --git a/stories/Layout/Table/AsyncDataExample.stories.tsx b/stories/Layout/Table/AsyncDataExample.stories.tsx index 1d98a4d2..bc13e86c 100644 --- a/stories/Layout/Table/AsyncDataExample.stories.tsx +++ b/stories/Layout/Table/AsyncDataExample.stories.tsx @@ -10,28 +10,7 @@ import { TableColumnSwitcher } from '@/src/components/layout/table/TableColumnSw import { Chip } from '@/src/components/display-and-visualization/Chip' import { Table } from '@/src/components/layout/table/Table' import { Visibility } from '@/src/components/layout/Visibility' -import { - filterText, - filterNumber, - filterDate, - filterDatetime, - filterBoolean, - filterTags, - filterTagsSingle, - filterGeneric -} from '@/src/utils/filter' -import { - TableFilterOperator, - type TextFilterValue, - type NumberFilterValue, - type DateFilterValue, - type DatetimeFilterValue, - type BooleanFilterValue, - type TagsFilterValue, - type TagsSingleFilterValue, - type GenericFilterValue, - type TableFilterValue -} from '@/src/components/layout/table/TableFilter' +import { FilterFunctions, type FilterValue } from '@/src/components/user-interaction/data/filter-function' const relationShipTags = ['Friend', 'Family', 'Work', 'School', 'Other'] as const type RelationShipTag = (typeof relationShipTags)[number] @@ -66,52 +45,6 @@ const createRandomDataType = (): DataType => { const TOTAL_ITEMS = 10000 const allData: DataType[] = range(TOTAL_ITEMS).map(() => createRandomDataType()) -/** - * Determines the filter category based on the operator string. - */ -function getFilterCategory(operator: string): keyof typeof TableFilterOperator | null { - const allOperators = [ - ...TableFilterOperator.generic, - ...TableFilterOperator.text, - ...TableFilterOperator.number, - ...TableFilterOperator.date, - ...TableFilterOperator.dateTime, - ...TableFilterOperator.boolean, - ...TableFilterOperator.multiTags, - ...TableFilterOperator.singleTag, - ] as readonly string[] - - if (!allOperators.includes(operator)) { - return null - } - - if (TableFilterOperator.generic.includes(operator as typeof TableFilterOperator.generic[number])) { - return 'generic' - } - if (TableFilterOperator.text.includes(operator as typeof TableFilterOperator.text[number])) { - return 'text' - } - if (TableFilterOperator.number.includes(operator as typeof TableFilterOperator.number[number])) { - return 'number' - } - if (TableFilterOperator.date.includes(operator as typeof TableFilterOperator.date[number])) { - return 'date' - } - if (TableFilterOperator.dateTime.includes(operator as typeof TableFilterOperator.dateTime[number])) { - return 'dateTime' - } - if (TableFilterOperator.boolean.includes(operator as typeof TableFilterOperator.boolean[number])) { - return 'boolean' - } - if (TableFilterOperator.multiTags.includes(operator as typeof TableFilterOperator.multiTags[number])) { - return 'multiTags' - } - if (TableFilterOperator.singleTag.includes(operator as typeof TableFilterOperator.singleTag[number])) { - return 'singleTag' - } - return null -} - const fetchPaginatedData = async ( pageIndex: number, pageSize: number, @@ -130,30 +63,30 @@ const fetchPaginatedData = async ( const rowValue = row[id as keyof DataType] if (typeof value === 'object' && 'operator' in value && 'parameter' in value) { - const filterValue = value as TableFilterValue - const category = getFilterCategory(filterValue.operator) + const filterValue = value as FilterValue + const dataType = filterValue.dataType - if (!category) { + if (!dataType) { return true } - switch (category) { + switch (dataType) { case 'text': - return filterText(rowValue, filterValue as TextFilterValue) + return FilterFunctions.text(rowValue, filterValue.operator, filterValue.parameter) case 'number': - return filterNumber(rowValue, filterValue as NumberFilterValue) + return FilterFunctions.number(rowValue, filterValue.operator, filterValue.parameter) case 'date': - return filterDate(rowValue, filterValue as DateFilterValue) + return FilterFunctions.date(rowValue, filterValue.operator, filterValue.parameter) case 'dateTime': - return filterDatetime(rowValue, filterValue as DatetimeFilterValue) + return FilterFunctions.dateTime(rowValue, filterValue.operator, filterValue.parameter) case 'boolean': - return filterBoolean(rowValue, filterValue as BooleanFilterValue) - case 'multiTags': - return filterTags(rowValue, filterValue as TagsFilterValue) + return FilterFunctions.boolean(rowValue, filterValue.operator, filterValue.parameter) case 'singleTag': - return filterTagsSingle(rowValue, filterValue as TagsSingleFilterValue) - case 'generic': - return filterGeneric(rowValue, filterValue as GenericFilterValue) + return FilterFunctions.singleTag(rowValue, filterValue.operator, filterValue.parameter) + case 'multiTags': + return FilterFunctions.multiTags(rowValue, filterValue.operator, filterValue.parameter) + case 'unknownType': + return FilterFunctions.unknownType(rowValue, filterValue.operator, filterValue.parameter) default: return true } diff --git a/stories/Layout/Table/FilterListTable.stories.tsx b/stories/Layout/Table/FilterListTable.stories.tsx new file mode 100644 index 00000000..89a131c2 --- /dev/null +++ b/stories/Layout/Table/FilterListTable.stories.tsx @@ -0,0 +1,238 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useId, useMemo, useState } from 'react' +import { faker } from '@faker-js/faker' +import { range } from '@/src/utils/array' +import { Table } from '@/src/components/layout/table/Table' +import { TableColumn } from '@/src/components/layout/table/TableColumn' +import { TableCell } from '@/src/components/layout/table/TableCell' +import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' +import { FilterList } from '@/src/components/user-interaction/data/FilterList' +import type { IdentifierFilterValue, FilterListItem, FilterListPopUpBuilderProps } from '@/src/components/user-interaction/data/FilterList' +import { FilterFunctions } from '@/src/components/user-interaction/data/filter-function' +import type { DataType } from '@/src/components/user-interaction/data/data-types' +import { FilterOperatorUtils } from '@/src/components/user-interaction/data/FilterOperator' +import { FilterBasePopUp } from '@/src/components/user-interaction/data/FilterPopUp' +import { Input } from '@/src/components/user-interaction/input/Input' +import { Select } from '@/src/components/user-interaction/Select/Select' +import { SelectOption } from '@/src/components/user-interaction/Select/SelectOption' +import { Visibility } from '@/src/components/layout/Visibility' + +type Row = { + name: string, + age: number, + entryDate: Date, + hasChildren: boolean, +} + +const createRow = (): Row => ({ + name: faker.person.fullName(), + age: faker.number.int(100), + entryDate: faker.date.past({ years: 20 }), + hasChildren: faker.datatype.boolean(), +}) + +const AgeFilterPopUp = ({ value, onValueChange, onRemove, name }: FilterListPopUpBuilderProps) => { + const translation = useHightideTranslation() + const id = useId() + const ids = { + range: `number-filter-range-${id}`, + compareValue: `number-filter-compare-value-${id}`, + } + + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'between' + if (!FilterOperatorUtils.typeCheck.number(suggestion)) { + return 'between' + } + return suggestion + }, [value]) + + const parameter = value?.parameter ?? {} + + const needsRangeInput = operator === 'between' || operator === 'notBetween' + const needsParameterInput = operator !== 'isUndefined' && operator !== 'isNotUndefined' + + const ageRange = useMemo(() => range(11).map(i => i * 10), []) + + return ( + onValueChange({ dataType: 'number', parameter, operator: newOperator })} + onRemove={onRemove} + allowedOperators={FilterOperatorUtils.operatorsByCategory.number} + hasValue={!!value} + noParameterRequired={!needsParameterInput} + > + +
    + + + buttonProps={{ id: ids.range }} + value={parameter.minNumber !== undefined && parameter.maxNumber !== undefined ? [parameter.minNumber, parameter.maxNumber] : null} + onValueChange={(newRange) => { + onValueChange({ ...value, parameter: { ...parameter, minNumber: newRange[0], maxNumber: newRange[1] } }) + }} + compareFunction={(a, b) => { + if(a === null || b === null) return false + return a[0] === b[0] && a[1] === b[1] + }} + > + {range(ageRange.length - 1).map(i => ( + + {ageRange[i]} - {ageRange[i + 1]} + + ))} + +
    +
    + + { + const num = Number(text) + onValueChange({ + dataType: 'number', + operator, + parameter: { ...parameter, compareValue: isNaN(num) ? undefined : num }, + }) + }} + className="min-w-64" + /> + +
    + ) +} + +const allData: Row[] = range(100).map(() => createRow()) + +const availableItems: FilterListItem[] = [ + { + id: 'name', + label: 'Name', + dataType: 'text', + tags: [], + }, + { + id: 'age', + label: 'Age', + dataType: 'number', + tags: [], + popUpBuilder: (props: FilterListPopUpBuilderProps) => , + }, + { + id: 'entryDate', + label: 'Entry Date', + dataType: 'date', + tags: [], + }, + { + id: 'hasChildren', + label: 'Has Children', + dataType: 'boolean', + tags: [], + }, +] + +function filterData(data: Row[], filters: IdentifierFilterValue[]): Row[] { + if (filters.length === 0) return data + return data.filter(row => { + return filters.every(f => { + const rowValue = row[f.id as keyof Row] + const fn = FilterFunctions[f.dataType as DataType] + if (!fn) return true + return fn(rowValue, f.operator, f.parameter) + }) + }) +} + +const meta: Meta = { + component: Table, +} + +export default meta +type Story = StoryObj + +export const filterListTable: Story = { + args: {}, + render: () => { + const translation = useHightideTranslation() + const [filterValue, setFilterValue] = useState([]) + + const filteredData = useMemo( + () => filterData(allData, filterValue), + [filterValue] + ) + + return ( + + Table with Filter List + + {filteredData.length} of {allData.length} rows + + { + setFilterValue(value) + }} + availableItems={availableItems} + /> + + )} + > + + + ( + + {(cell.getValue() as Date).toLocaleDateString()} + + )} + sortingFn="datetime" + minSize={140} + size={160} + /> + ( + + {cell.getValue() ? translation('yes') : translation('no')} + + )} + sortingFn="basic" + minSize={100} + size={120} + /> +
    + ) + }, +} diff --git a/stories/User Interaction/Combobox.stories.tsx b/stories/User Interaction/Combobox.stories.tsx new file mode 100644 index 00000000..8ee7ca60 --- /dev/null +++ b/stories/User Interaction/Combobox.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { action } from 'storybook/actions' +import { Combobox } from '@/src/components/user-interaction/Combobox/Combobox' +import { ComboboxOption } from '@/src/components/user-interaction/Combobox/ComboboxOption' + +const options = [ + { value: 'apple', label: 'Apple' }, + { value: 'banana', label: 'Banana' }, + { value: 'blueberry', label: 'Blueberry' }, + { value: 'cherry', label: 'Cherry' }, + { value: 'grape', label: 'Grape' }, + { value: 'kiwi', label: 'Kiwi' }, + { value: 'mango', label: 'Mango' }, + { value: 'orange', label: 'Orange' }, + { value: 'papaya', label: 'Papaya' }, + { value: 'pineapple', label: 'Pineapple' }, + { value: 'strawberry', label: 'Strawberry' }, + { value: 'watermelon', label: 'Watermelon' }, +] + +const meta: Meta = { + component: Combobox, +} + +export default meta +type Story = StoryObj; + +export const combobox: Story = { + args: { + id: undefined, + children: options.map(({ value, label }) => ( + + {label} + + )), + onItemClick: action('onItemClick'), + }, + render: (args) => ( +
    + +
    + ), +} diff --git a/stories/User Interaction/Form/Form.stories.tsx b/stories/User Interaction/Form/Form.stories.tsx index 42316c96..dbeea30b 100644 --- a/stories/User Interaction/Form/Form.stories.tsx +++ b/stories/User Interaction/Form/Form.stories.tsx @@ -4,9 +4,9 @@ import type { StorybookHelperSelectType } from '@/src/storybook/helper' import { StorybookHelper } from '@/src/storybook/helper' import { useTranslatedValidators } from '@/src/hooks/useValidators' import { Input } from '@/src/components/user-interaction/input/Input' -import { MultiSelect } from '@/src/components/user-interaction/select/MultiSelect' -import { Select } from '@/src/components/user-interaction/select/Select' -import { SelectOption } from '@/src/components/user-interaction/select/SelectComponents' +import { MultiSelect } from '@/src/components/user-interaction/MultiSelect/MultiSelect' +import { Select } from '@/src/components/user-interaction/Select/Select' +import { SelectOption } from '@/src/components/user-interaction/Select/SelectOption' import { Textarea } from '@/src/components/user-interaction/Textarea' import { Button } from '@/src/components/user-interaction/Button' import { useCreateForm } from '@/src/components/form/useCreateForm' @@ -18,6 +18,7 @@ import type { FormFieldDataHandling } from '@/src/components/form/FormField' import { FormField } from '@/src/components/form/FormField' import { FormProvider } from '@/src/components/form/FormContext' import { DateTimeInput } from '@/src/components/user-interaction/input/DateTimeInput' +import { MultiSelectOption } from '@/src/components/user-interaction/MultiSelect/MultiSelectOption' type FormState = 'editing' | 'sending' | 'submitted' @@ -150,7 +151,7 @@ export const basic: Story = { {({ dataProps, focusableElementProps, interactionStates }) => ( )} @@ -164,9 +165,9 @@ export const basic: Story = { validationBehaviour={validationBehaviour} > {({ dataProps, focusableElementProps, interactionStates }) => ( - } {...focusableElementProps} {...interactionStates}> + {StorybookHelper.selectValues.map(value => ( - + ))} )} @@ -181,7 +182,7 @@ export const basic: Story = { {({ dataProps, focusableElementProps, interactionStates }) => ( } {...focusableElementProps} {...interactionStates}> {StorybookHelper.selectValues.map(value => ( - + ))} )} diff --git a/stories/User Interaction/ListBox.stories.tsx b/stories/User Interaction/ListBox.stories.tsx deleted file mode 100644 index a4e426dc..00000000 --- a/stories/User Interaction/ListBox.stories.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/nextjs' -import { action } from 'storybook/actions' -import { ListBoxItem, ListBox } from '@/src/components/layout/ListBox' - - -const meta = { - component: ListBox, -} satisfies Meta - -export default meta -type Story = StoryObj; - -export const listBox: Story = { - args: { - isSelection: true, - onItemClicked: action('onItemClick'), - onSelectionChanged: action('onSelectionChanged'), - children: [ - { value: 'Apple' }, - { value: 'Banana', disabled: true }, - { value: 'Kiwi' }, - { value: 'Blueberry' }, - { value: 'Strawberry' }, - { value: 'Melon' }, - { value: 'Orange' }, - { value: 'Mango' }, - { value: 'Pineapple', disabled: true }, - { value: 'Papaya' }, - { value: 'Grapes' }, - { value: 'Cherry' }, - { value: 'Peach' }, - { value: 'Plum' }, - { value: 'Pear' }, - { value: 'Fig' }, - { value: 'Lemon' }, - { value: 'Lime' }, - { value: 'Coconut' }, - { value: 'Guava' }, - { value: 'Apricot' }, - { value: 'Pomegranate' }, - { value: 'Raspberry', disabled: true }, - { value: 'Blackberry' }, - { value: 'Tangerine' }, - { value: 'Dragonfruit' } - ].sort((a, b) => a.value.localeCompare(b.value)) - .map((value, index) => ( - - )), - }, -} diff --git a/stories/User Interaction/MultiSelect/MultiSelect.stories.tsx b/stories/User Interaction/MultiSelect/MultiSelect.stories.tsx new file mode 100644 index 00000000..4014a80e --- /dev/null +++ b/stories/User Interaction/MultiSelect/MultiSelect.stories.tsx @@ -0,0 +1,127 @@ +import { action } from 'storybook/actions' +import type { Meta, StoryObj } from '@storybook/nextjs' +import { useEffect, useState } from 'react' +import { MultiSelect } from '@/src/components/user-interaction/MultiSelect/MultiSelect' +import { MultiSelectOption } from '@/src/components/user-interaction/MultiSelect/MultiSelectOption' + +const meta: Meta = { + component: MultiSelect, +} + +export default meta +type Story = StoryObj; + +const fruitOptions = [ + { value: 'Apple', label: 'Apple' }, + { value: 'Banana', label: 'Banana', disabled: true }, + { value: 'Cherry', label: 'Cherry' }, + { value: 'Dragonfruit', label: 'Dragonfruit', className: '!text-red-400' }, + { value: 'Elderberry', label: 'Elderberry' }, + { value: 'Fig', label: 'Fig' }, + { value: 'Grapefruit', label: 'Grapefruit' }, + { value: 'Honeydew', label: 'Honeydew' }, + { value: 'Indianfig', label: 'Indianfig' }, + { value: 'Jackfruit', label: 'Jackfruit' }, + { value: 'Kiwifruit', label: 'Kiwifruit' }, + { value: 'Lemon', label: 'Lemon', disabled: true } +].sort((a, b) => a.value.localeCompare(b.value)) + +export const multiSelect: Story = { + args: { + initialValue: ['Apple', 'Cherry'], + disabled: false, + invalid: false, + showSearch: true, + readOnly: false, + required: false, + onValueChange: action('onValueChange'), + onEditComplete: action('onEditComplete'), + children: fruitOptions.map((item, index) => ( + + )), + }, +} + +export interface User { + uuid: string + name: string + email: string +} + +const users: User[] = [ + { uuid: '1', name: 'Alice Chen', email: 'alice@example.com' }, + { uuid: '2', name: 'Bob Smith', email: 'bob@example.com' }, + { uuid: '3', name: 'Carol Jones', email: 'carol@example.com' }, + { uuid: '4', name: 'David Lee', email: 'david@example.com' }, + { uuid: '5', name: 'Eve Wilson', email: 'eve@example.com' }, +] + +function compareUser(a: User, b: User): boolean { + return a.uuid === b.uuid +} + +export const multiSelectWithUser: Story = { + args: { + value: undefined, + initialValue: undefined, + disabled: false, + invalid: false, + showSearch: true, + readOnly: false, + required: false, + compareFunction: compareUser, + onValueChange: action('onValueChange'), + onEditComplete: action('onEditComplete'), + buttonProps: { + placeholder: 'Select users', + selectedDisplay: (values: User[]) => ( +
    + {values.map((user) => ( +
    + {user.name} + {user.email} +
    + ))} +
    + ), + }, + children: users.map((user) => ( + +
    + {user.name} + {user.email} +
    +
    + )), + }, + render: (args) => { + const [value, setValue] = useState(args.value as User[] | undefined ?? []) + + useEffect(() => { + setValue(args.value as User[] | undefined ?? []) + }, [args.value]) + + const initialValue = args.initialValue as User[] | undefined ?? [] + + return ( + + {...args} + initialValue={initialValue} + value={value} + onValueChange={(v) => { + args.onValueChange?.(v) + setValue(v) + }} + onEditComplete={(v) => { + args.onEditComplete?.(v) + setValue(v) + }} + /> + ) + }, +} diff --git a/stories/User Interaction/MultiSelect/MultiSelectChipDisplay.stories.tsx b/stories/User Interaction/MultiSelect/MultiSelectChipDisplay.stories.tsx new file mode 100644 index 00000000..f5bb583f --- /dev/null +++ b/stories/User Interaction/MultiSelect/MultiSelectChipDisplay.stories.tsx @@ -0,0 +1,41 @@ +import { action } from 'storybook/actions' +import { + MultiSelectChipDisplay +} from '@/src/components/user-interaction/MultiSelect/MultiSelectChipDisplay' +import { MultiSelectOption } from '@/src/components/user-interaction/MultiSelect/MultiSelectOption' +import type { Meta, StoryObj } from '@storybook/nextjs' + +const meta = { + component: MultiSelectChipDisplay, +} satisfies Meta + +export default meta +type Story = StoryObj; + +export const multiSelectChipDisplay: Story = { + args: { + initialValue: ['Apple', 'Cherry'], + disabled: false, + invalid: false, + showSearch: false, + readOnly: false, + required: false, + onValueChange: action('onValueChange'), + onEditComplete: action('onEditComplete'), + children: [ + { value: 'Apple', label: 'Apple' }, + { value: 'Banana', label: 'Banana', disabled: true }, + { value: 'Cherry', label: 'Cherry' }, + { value: 'Dragonfruit', label: 'Dragonfruit', className: '!text-red-400' }, + { value: 'Elderberry', label: 'Elderberry' }, + { value: 'Fig', label: 'Fig' }, + { value: 'Grapefruit', label: 'Grapefruit' }, + { value: 'Honeydew', label: 'Honeydew' }, + { value: 'Indianfig', label: 'Indianfig' }, + { value: 'Jackfruit', label: 'Jackfruit' }, + { value: 'Kiwifruit', label: 'Kiwifruit' }, + { value: 'Lemon', label: 'Lemon', disabled: true } + ].sort((a, b) => a.value.localeCompare(b.value)) + .map((item, index) => ()), + }, +} diff --git a/stories/User Interaction/Properties/MultiSelectProperty.stories.tsx b/stories/User Interaction/Properties/MultiSelectProperty.stories.tsx index 8ff7d97d..26af2f8b 100644 --- a/stories/User Interaction/Properties/MultiSelectProperty.stories.tsx +++ b/stories/User Interaction/Properties/MultiSelectProperty.stories.tsx @@ -4,7 +4,7 @@ import { action } from 'storybook/actions' import clsx from 'clsx' import { MultiSelectProperty } from '@/src/components/user-interaction/properties/MultiSelectProperty' import { StorybookHelper } from '@/src/storybook/helper' -import { MultiSelectOption } from '@/src/components/user-interaction/select/SelectComponents' +import { MultiSelectOption } from '@/src/components/user-interaction/MultiSelect/MultiSelectOption' const options = StorybookHelper.selectValues @@ -22,7 +22,7 @@ export const multiSelectProperty: Story = { value: options.slice(3, 5), readOnly: false, children: options.map(option => ( - + ( - + - -export default meta -type Story = StoryObj; - -export const multiSelect: Story = { - args: { - initialValue: ['Apple', 'Cherry'], - disabled: false, - invalid: false, - onValueChange: action('onValueChange'), - onEditComplete: action('onEditComplete'), - children: [ - { value: 'Apple' }, - { value: 'Banana', disabled: true }, - { value: 'Cherry' }, - { value: 'Dragonfruit', className: '!text-red-400' }, - { value: 'Elderberry' }, - { value: 'Fig' }, - { value: 'Grapefruit' }, - { value: 'Honeydew' }, - { value: 'Indianfig' }, - { value: 'Jackfruit' }, - { value: 'Kiwifruit' }, - { value: 'Lemon', disabled: true } - ].sort((a,b) => a.value.localeCompare(b.value)) - .map((value, index) => ()), - }, -} diff --git a/stories/User Interaction/Select/MultiSelectChipDisplay.stories.tsx b/stories/User Interaction/Select/MultiSelectChipDisplay.stories.tsx deleted file mode 100644 index c4fc003c..00000000 --- a/stories/User Interaction/Select/MultiSelectChipDisplay.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { action } from 'storybook/actions' -import { - MultiSelectChipDisplay -} from '@/src/components/user-interaction/select/MultiSelectChipDisplay' -import { MultiSelectOption } from '@/src/components/user-interaction/select/SelectComponents' -import type { Meta, StoryObj } from '@storybook/nextjs' - -const meta = { - component: MultiSelectChipDisplay, -} satisfies Meta - -export default meta -type Story = StoryObj; - -export const multiSelectChipDisplay: Story = { - args: { - initialValue: ['Apple', 'Cherry'], - disabled: false, - invalid: false, - onValueChange: action('onValueChange'), - onEditComplete: action('onEditComplete'), - children: [ - { value: 'Apple' }, - { value: 'Banana', disabled: true }, - { value: 'Cherry' }, - { value: 'Dragonfruit', className: '!text-red-400' }, - { value: 'Elderberry' }, - { value: 'Fig' }, - { value: 'Grapefruit' }, - { value: 'Honeydew' }, - { value: 'Indianfig' }, - { value: 'Jackfruit' }, - { value: 'Kiwifruit' }, - { value: 'Lemon', disabled: true } - ].sort((a,b) => a.value.localeCompare(b.value)) - .map((value, index) => ()), - }, -} diff --git a/stories/User Interaction/Select/Select.stories.tsx b/stories/User Interaction/Select/Select.stories.tsx index 051b4d39..b7ad05dc 100644 --- a/stories/User Interaction/Select/Select.stories.tsx +++ b/stories/User Interaction/Select/Select.stories.tsx @@ -1,37 +1,124 @@ import type { Meta, StoryObj } from '@storybook/nextjs' import { action } from 'storybook/actions' -import { Select } from '@/src/components/user-interaction/select/Select' -import { SelectOption } from '@/src/components/user-interaction/select/SelectComponents' +import { useEffect, useState } from 'react' +import { Select } from '@/src/components/user-interaction/Select/Select' +import { SelectOption } from '@/src/components/user-interaction/Select/SelectOption' -const meta = { +const meta: Meta = { component: Select, -} satisfies Meta +} export default meta type Story = StoryObj; +const fruitOptions = [ + { value: 'Apple', label: 'Apple' }, + { value: 'Pear', label: 'Pear', disabled: true }, + { value: 'Strawberry', label: 'Strawberry' }, + { value: 'Pineapple', label: 'Pineapple' }, + { value: 'Blackberry', label: 'Blackberry' }, + { value: 'Blueberry', label: 'Blueberry', disabled: true }, + { value: 'Banana', label: 'Banana' }, + { value: 'Kiwi', label: 'Kiwi', disabled: true }, + { value: 'Maracuja', label: 'Maracuja', disabled: true }, + { value: 'Wildberry', label: 'Wildberry', disabled: true }, + { value: 'Watermelon', label: 'Watermelon' }, + { value: 'Honeymelon', label: 'Honeymelon' }, + { value: 'Papja', label: 'Papja' } +].sort((a, b) => a.value.localeCompare(b.value)) + export const select: Story = { args: { initialValue: undefined, disabled: false, invalid: false, + showSearch: false, + readOnly: false, + required: false, onValueChange: action('onValueChange'), onEditComplete: action('onEditComplete'), - children: [ - { value: 'Apple' }, - { value: 'Pear', disabled: true }, - { value: 'Strawberry' }, - { value: 'Pineapple' }, - { value: 'Blackberry' }, - { value: 'Blueberry', disabled: true }, - { value: 'Banana' }, - { value: 'Kiwi', disabled: true }, - { value: 'Maracuja', disabled: true }, - { value: 'Wildberry', disabled: true }, - { value: 'Watermelon' }, - { value: 'Honeymelon' }, - { value: 'Papja' } - ].sort((a,b) => a.value.localeCompare(b.value)) - .map((value, index) => ()), + children: fruitOptions.map((item, index) => ( + + )), + }, +} + +export interface User { + uuid: string + name: string + email: string +} + +const users: User[] = [ + { uuid: '1', name: 'Alice Chen', email: 'alice@example.com' }, + { uuid: '2', name: 'Bob Smith', email: 'bob@example.com' }, + { uuid: '3', name: 'Carol Jones', email: 'carol@example.com' }, + { uuid: '4', name: 'David Lee', email: 'david@example.com' }, + { uuid: '5', name: 'Eve Wilson', email: 'eve@example.com' }, +] + +function compareUser(a: User | null, b: User | null): boolean { + if (a === null || b === null) return a === b + return a.uuid === b.uuid +} + +export const selectWithUser: Story = { + args: { + value: undefined, + initialValue: undefined, + disabled: false, + invalid: false, + showSearch: true, + readOnly: false, + required: false, + compareFunction: compareUser, + onValueChange: action('onValueChange'), + onEditComplete: action('onEditComplete'), + buttonProps: { + placeholder: 'Select a user', + selectedDisplay: (option) => { + if (!option) return null + const user = option.value as User + return ( +
    + {user.name} + {user.email} +
    + ) + }, + }, + children: users.map((user) => ( + +
    + {user.name} + {user.email} +
    +
    + )), + }, + render: (args) => { + const [value, setValue] = useState(args.value ?? null) + useEffect(() => { + setValue(args.value ?? null) + }, [args.value]) + return ( + + {...args} + value={value} + onValueChange={(v) => { + args.onValueChange?.(v) + setValue(v) + }} + onEditComplete={(v) => { + args.onEditComplete?.(v) + setValue(v) + }} + /> + ) }, }