mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Component: Vue Widget Slider (new) (#5516)
* feat: Initial shadcn configuration * component: Add Slider component from shadcn-vue * deps: Add tw-animate-css * component: Align slider with Figma styles * component: Set the step value for the slider, update styles * fix: update component tests to work with Array of values * vite: Don't reload dev server for test changes * component: Swap text for a number input kept in sync with the slider * cleanup: Don't need the override if the input isn't type="number" * test: add step size tests * cleanup: Don't need cn for these * css: Update token names to match new Figma Variables * lint: Fix camelCase vs train-case in passthrough * feat: If the value is deleted, revert to the slider state cc: @PabloWiedemann * feat: Improve cursor styles, grabbable thumb, clickable track * lint: temporarily disable some warnings * feat: Grabbing while sliding (most of the time)
This commit is contained in:
20
components.json
Normal file
20
components.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-vue.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"typescript": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/assets/css/style.css",
|
||||||
|
"baseColor": "stone",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"composables": "@/composables",
|
||||||
|
"utils": "@/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
@@ -64,6 +64,9 @@ export default [
|
|||||||
'vue/no-v-html': 'off',
|
'vue/no-v-html': 'off',
|
||||||
// Enforce dark-theme: instead of dark: prefix
|
// Enforce dark-theme: instead of dark: prefix
|
||||||
'vue/no-restricted-class': ['error', '/^dark:/'],
|
'vue/no-restricted-class': ['error', '/^dark:/'],
|
||||||
|
'vue/multi-word-component-names': 'off', // TODO: fix
|
||||||
|
'vue/no-template-shadow': 'off', // TODO: fix
|
||||||
|
'vue/one-component-per-file': 'off', // TODO: fix
|
||||||
// Restrict deprecated PrimeVue components
|
// Restrict deprecated PrimeVue components
|
||||||
'no-restricted-imports': [
|
'no-restricted-imports': [
|
||||||
'error',
|
'error',
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
"tailwindcss": "^4.1.12",
|
"tailwindcss": "^4.1.12",
|
||||||
"tailwindcss-primeui": "^0.6.1",
|
"tailwindcss-primeui": "^0.6.1",
|
||||||
"tsx": "^4.15.6",
|
"tsx": "^4.15.6",
|
||||||
|
"tw-animate-css": "^1.3.8",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"typescript-eslint": "^8.42.0",
|
"typescript-eslint": "^8.42.0",
|
||||||
"unplugin-icons": "^0.22.0",
|
"unplugin-icons": "^0.22.0",
|
||||||
@@ -140,6 +141,7 @@
|
|||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.2.5",
|
"primevue": "^4.2.5",
|
||||||
|
"reka-ui": "^2.5.0",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"three": "^0.170.0",
|
"three": "^0.170.0",
|
||||||
|
|||||||
150
pnpm-lock.yaml
generated
150
pnpm-lock.yaml
generated
@@ -134,6 +134,9 @@ importers:
|
|||||||
primevue:
|
primevue:
|
||||||
specifier: ^4.2.5
|
specifier: ^4.2.5
|
||||||
version: 4.2.5(vue@3.5.13(typescript@5.9.2))
|
version: 4.2.5(vue@3.5.13(typescript@5.9.2))
|
||||||
|
reka-ui:
|
||||||
|
specifier: ^2.5.0
|
||||||
|
version: 2.5.0(typescript@5.9.2)(vue@3.5.13(typescript@5.9.2))
|
||||||
semver:
|
semver:
|
||||||
specifier: ^7.7.2
|
specifier: ^7.7.2
|
||||||
version: 7.7.2
|
version: 7.7.2
|
||||||
@@ -303,6 +306,9 @@ importers:
|
|||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.15.6
|
specifier: ^4.15.6
|
||||||
version: 4.19.4
|
version: 4.19.4
|
||||||
|
tw-animate-css:
|
||||||
|
specifier: ^1.3.8
|
||||||
|
version: 1.3.8
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.4.5
|
specifier: ^5.4.5
|
||||||
version: 5.9.2
|
version: 5.9.2
|
||||||
@@ -1570,6 +1576,18 @@ packages:
|
|||||||
'@firebase/webchannel-wrapper@1.0.3':
|
'@firebase/webchannel-wrapper@1.0.3':
|
||||||
resolution: {integrity: sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==}
|
resolution: {integrity: sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==}
|
||||||
|
|
||||||
|
'@floating-ui/core@1.7.3':
|
||||||
|
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.7.4':
|
||||||
|
resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.10':
|
||||||
|
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||||
|
|
||||||
|
'@floating-ui/vue@1.1.9':
|
||||||
|
resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
|
||||||
|
|
||||||
'@grpc/grpc-js@1.9.15':
|
'@grpc/grpc-js@1.9.15':
|
||||||
resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==}
|
resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==}
|
||||||
engines: {node: ^8.13.0 || >=10.10.0}
|
engines: {node: ^8.13.0 || >=10.10.0}
|
||||||
@@ -1610,6 +1628,12 @@ packages:
|
|||||||
'@iconify/utils@2.3.0':
|
'@iconify/utils@2.3.0':
|
||||||
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
|
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
|
||||||
|
|
||||||
|
'@internationalized/date@3.9.0':
|
||||||
|
resolution: {integrity: sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg==}
|
||||||
|
|
||||||
|
'@internationalized/number@3.6.5':
|
||||||
|
resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
|
||||||
|
|
||||||
'@intlify/core-base@9.14.3':
|
'@intlify/core-base@9.14.3':
|
||||||
resolution: {integrity: sha512-nbJ7pKTlXFnaXPblyfiH6awAx1C0PWNNuqXAR74yRwgi5A/Re/8/5fErLY0pv4R8+EHj3ZaThMHdnuC/5OBa6g==}
|
resolution: {integrity: sha512-nbJ7pKTlXFnaXPblyfiH6awAx1C0PWNNuqXAR74yRwgi5A/Re/8/5fErLY0pv4R8+EHj3ZaThMHdnuC/5OBa6g==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
@@ -2243,6 +2267,9 @@ packages:
|
|||||||
storybook: ^9.1.1
|
storybook: ^9.1.1
|
||||||
vue: ^3.0.0
|
vue: ^3.0.0
|
||||||
|
|
||||||
|
'@swc/helpers@0.5.17':
|
||||||
|
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.12':
|
'@tailwindcss/node@4.1.12':
|
||||||
resolution: {integrity: sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==}
|
resolution: {integrity: sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==}
|
||||||
|
|
||||||
@@ -2333,6 +2360,14 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^5.2.0 || ^6 || ^7
|
vite: ^5.2.0 || ^6 || ^7
|
||||||
|
|
||||||
|
'@tanstack/virtual-core@3.13.12':
|
||||||
|
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
|
||||||
|
|
||||||
|
'@tanstack/vue-virtual@3.13.12':
|
||||||
|
resolution: {integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^2.7.0 || ^3.0.0
|
||||||
|
|
||||||
'@testing-library/dom@10.4.1':
|
'@testing-library/dom@10.4.1':
|
||||||
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2609,6 +2644,9 @@ packages:
|
|||||||
'@types/web-bluetooth@0.0.20':
|
'@types/web-bluetooth@0.0.20':
|
||||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21':
|
||||||
|
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||||
|
|
||||||
'@types/webxr@0.5.20':
|
'@types/webxr@0.5.20':
|
||||||
resolution: {integrity: sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==}
|
resolution: {integrity: sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==}
|
||||||
|
|
||||||
@@ -2859,12 +2897,21 @@ packages:
|
|||||||
'@vueuse/core@11.0.0':
|
'@vueuse/core@11.0.0':
|
||||||
resolution: {integrity: sha512-shibzNGjmRjZucEm97B8V0NO5J3vPHMCE/mltxQ3vHezbDoFQBMtK11XsfwfPionxSbo+buqPmsCljtYuXIBpw==}
|
resolution: {integrity: sha512-shibzNGjmRjZucEm97B8V0NO5J3vPHMCE/mltxQ3vHezbDoFQBMtK11XsfwfPionxSbo+buqPmsCljtYuXIBpw==}
|
||||||
|
|
||||||
|
'@vueuse/core@12.8.2':
|
||||||
|
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
|
||||||
|
|
||||||
'@vueuse/metadata@11.0.0':
|
'@vueuse/metadata@11.0.0':
|
||||||
resolution: {integrity: sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==}
|
resolution: {integrity: sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==}
|
||||||
|
|
||||||
|
'@vueuse/metadata@12.8.2':
|
||||||
|
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
|
||||||
|
|
||||||
'@vueuse/shared@11.0.0':
|
'@vueuse/shared@11.0.0':
|
||||||
resolution: {integrity: sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==}
|
resolution: {integrity: sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==}
|
||||||
|
|
||||||
|
'@vueuse/shared@12.8.2':
|
||||||
|
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
|
||||||
|
|
||||||
'@webgpu/types@0.1.51':
|
'@webgpu/types@0.1.51':
|
||||||
resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==}
|
resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==}
|
||||||
|
|
||||||
@@ -3018,6 +3065,10 @@ packages:
|
|||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
|
aria-hidden@1.2.6:
|
||||||
|
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
aria-query@5.3.0:
|
aria-query@5.3.0:
|
||||||
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||||
|
|
||||||
@@ -3488,6 +3539,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
defu@6.1.4:
|
||||||
|
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||||
|
|
||||||
delayed-stream@1.0.0:
|
delayed-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@@ -5105,6 +5159,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
|
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
ohash@2.0.11:
|
||||||
|
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
@@ -5555,6 +5612,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==}
|
resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
reka-ui@2.5.0:
|
||||||
|
resolution: {integrity: sha512-81aMAmJeVCy2k0E6x7n1kypDY6aM1ldLis5+zcdV1/JtoAlSDck5OBsyLRJU9CfgbrQp1ImnRnBSmC4fZ2fkZQ==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>= 3.2.0'
|
||||||
|
|
||||||
relateurl@0.2.7:
|
relateurl@0.2.7:
|
||||||
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
|
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -5989,6 +6051,9 @@ packages:
|
|||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
tw-animate-css@1.3.8:
|
||||||
|
resolution: {integrity: sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -7999,6 +8064,26 @@ snapshots:
|
|||||||
|
|
||||||
'@firebase/webchannel-wrapper@1.0.3': {}
|
'@firebase/webchannel-wrapper@1.0.3': {}
|
||||||
|
|
||||||
|
'@floating-ui/core@1.7.3':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/utils': 0.2.10
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.7.4':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 1.7.3
|
||||||
|
'@floating-ui/utils': 0.2.10
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.10': {}
|
||||||
|
|
||||||
|
'@floating-ui/vue@1.1.9(vue@3.5.13(typescript@5.9.2))':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.7.4
|
||||||
|
'@floating-ui/utils': 0.2.10
|
||||||
|
vue-demi: 0.14.10(vue@3.5.13(typescript@5.9.2))
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- vue
|
||||||
|
|
||||||
'@grpc/grpc-js@1.9.15':
|
'@grpc/grpc-js@1.9.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@grpc/proto-loader': 0.7.13
|
'@grpc/proto-loader': 0.7.13
|
||||||
@@ -8050,6 +8135,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@internationalized/date@3.9.0':
|
||||||
|
dependencies:
|
||||||
|
'@swc/helpers': 0.5.17
|
||||||
|
|
||||||
|
'@internationalized/number@3.6.5':
|
||||||
|
dependencies:
|
||||||
|
'@swc/helpers': 0.5.17
|
||||||
|
|
||||||
'@intlify/core-base@9.14.3':
|
'@intlify/core-base@9.14.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/message-compiler': 9.14.3
|
'@intlify/message-compiler': 9.14.3
|
||||||
@@ -8834,6 +8927,10 @@ snapshots:
|
|||||||
vue: 3.5.13(typescript@5.9.2)
|
vue: 3.5.13(typescript@5.9.2)
|
||||||
vue-component-type-helpers: 3.0.6
|
vue-component-type-helpers: 3.0.6
|
||||||
|
|
||||||
|
'@swc/helpers@0.5.17':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.12':
|
'@tailwindcss/node@4.1.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
@@ -8905,6 +9002,13 @@ snapshots:
|
|||||||
tailwindcss: 4.1.12
|
tailwindcss: 4.1.12
|
||||||
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
|
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
|
||||||
|
|
||||||
|
'@tanstack/virtual-core@3.13.12': {}
|
||||||
|
|
||||||
|
'@tanstack/vue-virtual@3.13.12(vue@3.5.13(typescript@5.9.2))':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/virtual-core': 3.13.12
|
||||||
|
vue: 3.5.13(typescript@5.9.2)
|
||||||
|
|
||||||
'@testing-library/dom@10.4.1':
|
'@testing-library/dom@10.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
@@ -9212,6 +9316,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/web-bluetooth@0.0.20': {}
|
'@types/web-bluetooth@0.0.20': {}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21': {}
|
||||||
|
|
||||||
'@types/webxr@0.5.20': {}
|
'@types/webxr@0.5.20': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)':
|
'@typescript-eslint/eslint-plugin@8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)':
|
||||||
@@ -9618,8 +9724,19 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@vueuse/core@12.8.2(typescript@5.9.2)':
|
||||||
|
dependencies:
|
||||||
|
'@types/web-bluetooth': 0.0.21
|
||||||
|
'@vueuse/metadata': 12.8.2
|
||||||
|
'@vueuse/shared': 12.8.2(typescript@5.9.2)
|
||||||
|
vue: 3.5.13(typescript@5.9.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
|
||||||
'@vueuse/metadata@11.0.0': {}
|
'@vueuse/metadata@11.0.0': {}
|
||||||
|
|
||||||
|
'@vueuse/metadata@12.8.2': {}
|
||||||
|
|
||||||
'@vueuse/shared@11.0.0(vue@3.5.13(typescript@5.9.2))':
|
'@vueuse/shared@11.0.0(vue@3.5.13(typescript@5.9.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vue-demi: 0.14.10(vue@3.5.13(typescript@5.9.2))
|
vue-demi: 0.14.10(vue@3.5.13(typescript@5.9.2))
|
||||||
@@ -9627,6 +9744,12 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@vueuse/shared@12.8.2(typescript@5.9.2)':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.13(typescript@5.9.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
|
||||||
'@webgpu/types@0.1.51': {}
|
'@webgpu/types@0.1.51': {}
|
||||||
|
|
||||||
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
|
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
|
||||||
@@ -9775,6 +9898,10 @@ snapshots:
|
|||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
|
aria-hidden@1.2.6:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
aria-query@5.3.0:
|
aria-query@5.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
@@ -10244,6 +10371,8 @@ snapshots:
|
|||||||
|
|
||||||
define-lazy-prop@3.0.0: {}
|
define-lazy-prop@3.0.0: {}
|
||||||
|
|
||||||
|
defu@6.1.4: {}
|
||||||
|
|
||||||
delayed-stream@1.0.0: {}
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
dequal@2.0.3: {}
|
dequal@2.0.3: {}
|
||||||
@@ -12135,6 +12264,8 @@ snapshots:
|
|||||||
|
|
||||||
object-keys@1.1.1: {}
|
object-keys@1.1.1: {}
|
||||||
|
|
||||||
|
ohash@2.0.11: {}
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
@@ -12711,6 +12842,23 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
jsesc: 3.0.2
|
jsesc: 3.0.2
|
||||||
|
|
||||||
|
reka-ui@2.5.0(typescript@5.9.2)(vue@3.5.13(typescript@5.9.2)):
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.7.4
|
||||||
|
'@floating-ui/vue': 1.1.9(vue@3.5.13(typescript@5.9.2))
|
||||||
|
'@internationalized/date': 3.9.0
|
||||||
|
'@internationalized/number': 3.6.5
|
||||||
|
'@tanstack/vue-virtual': 3.13.12(vue@3.5.13(typescript@5.9.2))
|
||||||
|
'@vueuse/core': 12.8.2(typescript@5.9.2)
|
||||||
|
'@vueuse/shared': 12.8.2(typescript@5.9.2)
|
||||||
|
aria-hidden: 1.2.6
|
||||||
|
defu: 6.1.4
|
||||||
|
ohash: 2.0.11
|
||||||
|
vue: 3.5.13(typescript@5.9.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- typescript
|
||||||
|
|
||||||
relateurl@0.2.7: {}
|
relateurl@0.2.7: {}
|
||||||
|
|
||||||
remark-frontmatter@5.0.0:
|
remark-frontmatter@5.0.0:
|
||||||
@@ -13158,6 +13306,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
tw-animate-css@1.3.8: {}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
@import 'tailwindcss/theme' layer(theme);
|
@import 'tailwindcss/theme' layer(theme);
|
||||||
@import 'tailwindcss/utilities' layer(utilities);
|
@import 'tailwindcss/utilities' layer(utilities);
|
||||||
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
@plugin "tailwindcss-primeui";
|
@plugin 'tailwindcss-primeui';
|
||||||
|
|
||||||
@config '../../../tailwind.config.ts';
|
@config '../../../tailwind.config.ts';
|
||||||
|
|
||||||
@@ -114,6 +115,14 @@
|
|||||||
--color-dark-elevation-2: rgba(from white r g b / 0.03);
|
--color-dark-elevation-2: rgba(from white r g b / 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-node-component-surface: var(--color-charcoal-300);
|
||||||
|
--color-node-component-surface-highlight: var(--color-slate-100);
|
||||||
|
--color-node-component-surface-hovered: var(--color-charcoal-500);
|
||||||
|
--color-node-component-surface-selected: var(--color-charcoal-700);
|
||||||
|
--color-node-stroke: var(--color-stone-100);
|
||||||
|
}
|
||||||
|
|
||||||
@custom-variant dark-theme {
|
@custom-variant dark-theme {
|
||||||
.dark-theme & {
|
.dark-theme & {
|
||||||
@slot;
|
@slot;
|
||||||
|
|||||||
78
src/components/ui/slider/Slider.vue
Normal file
78
src/components/ui/slider/Slider.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import type { SliderRootEmits, SliderRootProps } from 'reka-ui'
|
||||||
|
import {
|
||||||
|
SliderRange,
|
||||||
|
SliderRoot,
|
||||||
|
SliderThumb,
|
||||||
|
SliderTrack,
|
||||||
|
useForwardPropsEmits
|
||||||
|
} from 'reka-ui'
|
||||||
|
import { type HTMLAttributes, ref } from 'vue'
|
||||||
|
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
const props = defineProps<
|
||||||
|
SliderRootProps & { class?: HTMLAttributes['class'] }
|
||||||
|
>()
|
||||||
|
|
||||||
|
const pressed = ref(false)
|
||||||
|
const setPressed = (val: boolean) => {
|
||||||
|
pressed.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const emits = defineEmits<SliderRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SliderRoot
|
||||||
|
v-slot="{ modelValue }"
|
||||||
|
data-slot="slider"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50',
|
||||||
|
'data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="forwarded"
|
||||||
|
@slide-start="() => setPressed(true)"
|
||||||
|
@slide-move="() => setPressed(true)"
|
||||||
|
@slide-end="() => setPressed(false)"
|
||||||
|
>
|
||||||
|
<SliderTrack
|
||||||
|
data-slot="slider-track"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'bg-node-stroke relative grow overflow-hidden rounded-full',
|
||||||
|
'cursor-pointer',
|
||||||
|
'data-[orientation=horizontal]:h-0.5 data-[orientation=horizontal]:w-full',
|
||||||
|
'data-[orientation=vertical]:h-full data-[orientation=vertical]:w-0.5'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SliderRange
|
||||||
|
data-slot="slider-range"
|
||||||
|
class="bg-node-component-surface-highlight absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||||
|
/>
|
||||||
|
</SliderTrack>
|
||||||
|
|
||||||
|
<SliderThumb
|
||||||
|
v-for="(_, key) in modelValue"
|
||||||
|
:key="key"
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'bg-node-component-surface-highlight ring-node-component-surface-selected block size-3.5 shrink-0 rounded-full shadow-sm transition-[color,box-shadow]',
|
||||||
|
'cursor-grab',
|
||||||
|
'hover:ring-2 focus-visible:ring-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
{ 'cursor-grabbing': pressed }
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</SliderRoot>
|
||||||
|
</template>
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
import InputText from 'primevue/inputtext'
|
import InputNumber from 'primevue/inputnumber'
|
||||||
import Slider from 'primevue/slider'
|
|
||||||
import type { SliderProps } from 'primevue/slider'
|
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import Slider from '@/components/ui/slider/Slider.vue'
|
||||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
|
|
||||||
import WidgetInputNumberSlider from './WidgetInputNumberSlider.vue'
|
import WidgetInputNumberSlider from './WidgetInputNumberSlider.vue'
|
||||||
|
|
||||||
describe('WidgetInputNumberSlider Value Binding', () => {
|
function createMockWidget(
|
||||||
const createMockWidget = (
|
|
||||||
value: number = 5,
|
value: number = 5,
|
||||||
options: Partial<SliderProps & { precision?: number }> = {},
|
options: SimplifiedWidget['options'] = {},
|
||||||
callback?: (value: number) => void
|
callback?: (value: number) => void
|
||||||
): SimplifiedWidget<number> => ({
|
): SimplifiedWidget<number> {
|
||||||
|
return {
|
||||||
name: 'test_slider',
|
name: 'test_slider',
|
||||||
type: 'float',
|
type: 'float',
|
||||||
value,
|
value,
|
||||||
options: { min: 0, max: 100, step: 1, precision: 0, ...options },
|
options: { min: 0, max: 100, step: 1, precision: 0, ...options },
|
||||||
callback
|
callback
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const mountComponent = (
|
function mountComponent(
|
||||||
widget: SimplifiedWidget<number>,
|
widget: SimplifiedWidget<number>,
|
||||||
modelValue: number,
|
modelValue: number,
|
||||||
readonly = false
|
readonly = false
|
||||||
) => {
|
) {
|
||||||
return mount(WidgetInputNumberSlider, {
|
return mount(WidgetInputNumberSlider, {
|
||||||
global: {
|
global: {
|
||||||
plugins: [PrimeVue],
|
plugins: [PrimeVue],
|
||||||
components: { InputText, Slider }
|
components: { InputNumber, Slider }
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
widget,
|
widget,
|
||||||
@@ -38,25 +38,26 @@ describe('WidgetInputNumberSlider Value Binding', () => {
|
|||||||
readonly
|
readonly
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNumberInput = (wrapper: ReturnType<typeof mount>) => {
|
function getNumberInput(wrapper: ReturnType<typeof mount>) {
|
||||||
const input = wrapper.find('input[type="number"]')
|
const input = wrapper.find('input[inputmode="numeric"]')
|
||||||
if (!(input.element instanceof HTMLInputElement)) {
|
if (!(input.element instanceof HTMLInputElement)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Number input element not found or is not an HTMLInputElement'
|
'Number input element not found or is not an HTMLInputElement'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return { element: input.element }
|
return input.element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('WidgetInputNumberSlider Value Binding', () => {
|
||||||
describe('Props and Values', () => {
|
describe('Props and Values', () => {
|
||||||
it('passes modelValue to slider component', () => {
|
it('passes modelValue to slider component', () => {
|
||||||
const widget = createMockWidget(5)
|
const widget = createMockWidget(5)
|
||||||
const wrapper = mountComponent(widget, 5)
|
const wrapper = mountComponent(widget, 5)
|
||||||
|
|
||||||
const slider = wrapper.findComponent({ name: 'Slider' })
|
const slider = wrapper.findComponent({ name: 'Slider' })
|
||||||
expect(slider.props('modelValue')).toBe(5)
|
expect(slider.props('modelValue')).toEqual([5])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles different initial values', () => {
|
it('handles different initial values', () => {
|
||||||
@@ -67,10 +68,10 @@ describe('WidgetInputNumberSlider Value Binding', () => {
|
|||||||
const wrapper2 = mountComponent(widget2, 10)
|
const wrapper2 = mountComponent(widget2, 10)
|
||||||
|
|
||||||
const slider1 = wrapper1.findComponent({ name: 'Slider' })
|
const slider1 = wrapper1.findComponent({ name: 'Slider' })
|
||||||
expect(slider1.props('modelValue')).toBe(5)
|
expect(slider1.props('modelValue')).toEqual([5])
|
||||||
|
|
||||||
const slider2 = wrapper2.findComponent({ name: 'Slider' })
|
const slider2 = wrapper2.findComponent({ name: 'Slider' })
|
||||||
expect(slider2.props('modelValue')).toBe(10)
|
expect(slider2.props('modelValue')).toEqual([10])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -85,8 +86,9 @@ describe('WidgetInputNumberSlider Value Binding', () => {
|
|||||||
it('renders input field', () => {
|
it('renders input field', () => {
|
||||||
const widget = createMockWidget(5)
|
const widget = createMockWidget(5)
|
||||||
const wrapper = mountComponent(widget, 5)
|
const wrapper = mountComponent(widget, 5)
|
||||||
|
console.log(wrapper.html())
|
||||||
|
|
||||||
expect(wrapper.find('input[type="number"]').exists()).toBe(true)
|
expect(wrapper.find('input[inputmode="numeric"]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays initial value in input field', () => {
|
it('displays initial value in input field', () => {
|
||||||
@@ -94,7 +96,7 @@ describe('WidgetInputNumberSlider Value Binding', () => {
|
|||||||
const wrapper = mountComponent(widget, 42)
|
const wrapper = mountComponent(widget, 42)
|
||||||
|
|
||||||
const input = getNumberInput(wrapper)
|
const input = getNumberInput(wrapper)
|
||||||
expect(input.element.value).toBe('42')
|
expect(input.value).toBe('42')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('disables components in readonly mode', () => {
|
it('disables components in readonly mode', () => {
|
||||||
@@ -105,7 +107,7 @@ describe('WidgetInputNumberSlider Value Binding', () => {
|
|||||||
expect(slider.props('disabled')).toBe(true)
|
expect(slider.props('disabled')).toBe(true)
|
||||||
|
|
||||||
const input = getNumberInput(wrapper)
|
const input = getNumberInput(wrapper)
|
||||||
expect(input.element.disabled).toBe(true)
|
expect(input.disabled).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -127,5 +129,47 @@ describe('WidgetInputNumberSlider Value Binding', () => {
|
|||||||
expect(slider.props('min')).toBe(-100)
|
expect(slider.props('min')).toBe(-100)
|
||||||
expect(slider.props('max')).toBe(100)
|
expect(slider.props('max')).toBe(100)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Step Size', () => {
|
||||||
|
it('should default to 1', () => {
|
||||||
|
const widget = createMockWidget(5)
|
||||||
|
const wrapper = mountComponent(widget, 5)
|
||||||
|
|
||||||
|
const slider = wrapper.findComponent({ name: 'Slider' })
|
||||||
|
expect(slider.props('step')).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get the step2 value if present', () => {
|
||||||
|
const widget = createMockWidget(5, { step2: 0.01 })
|
||||||
|
const wrapper = mountComponent(widget, 5)
|
||||||
|
|
||||||
|
const slider = wrapper.findComponent({ name: 'Slider' })
|
||||||
|
expect(slider.props('step')).toBe(0.01)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be 1 for precision 0', () => {
|
||||||
|
const widget = createMockWidget(5, { precision: 0 })
|
||||||
|
const wrapper = mountComponent(widget, 5)
|
||||||
|
|
||||||
|
const slider = wrapper.findComponent({ name: 'Slider' })
|
||||||
|
expect(slider.props('step')).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be .1 for precision 1', () => {
|
||||||
|
const widget = createMockWidget(5, { precision: 1 })
|
||||||
|
const wrapper = mountComponent(widget, 5)
|
||||||
|
|
||||||
|
const slider = wrapper.findComponent({ name: 'Slider' })
|
||||||
|
expect(slider.props('step')).toBe(0.1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be .00001 for precision 5', () => {
|
||||||
|
const widget = createMockWidget(5, { precision: 5 })
|
||||||
|
const wrapper = mountComponent(widget, 5)
|
||||||
|
|
||||||
|
const slider = wrapper.findComponent({ name: 'Slider' })
|
||||||
|
expect(slider.props('step')).toBe(0.00001)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,31 +6,35 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Slider
|
<Slider
|
||||||
v-model="localValue"
|
:model-value="[localValue]"
|
||||||
v-bind="filteredProps"
|
v-bind="filteredProps"
|
||||||
:disabled="readonly"
|
:disabled="readonly"
|
||||||
class="flex-grow text-xs"
|
class="flex-grow text-xs"
|
||||||
@update:model-value="onChange"
|
|
||||||
/>
|
|
||||||
<InputText
|
|
||||||
v-model="inputDisplayValue"
|
|
||||||
:disabled="readonly"
|
|
||||||
type="number"
|
|
||||||
:step="stepValue"
|
:step="stepValue"
|
||||||
class="w-[4em] text-center text-xs px-0 !border-none !shadow-none !bg-transparent"
|
@update:model-value="updateLocalValue"
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
:key="timesEmptied"
|
||||||
|
:model-value="localValue"
|
||||||
|
v-bind="filteredProps"
|
||||||
|
:disabled="readonly"
|
||||||
|
:step="stepValue"
|
||||||
|
:min-fraction-digits="precision"
|
||||||
|
:max-fraction-digits="precision"
|
||||||
size="small"
|
size="small"
|
||||||
@blur="handleInputBlur"
|
pt:pc-input-text:root="min-w-full bg-transparent border-none text-center"
|
||||||
@keydown="handleInputKeydown"
|
class="w-16"
|
||||||
|
@update:model-value="handleNumberInputUpdate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</WidgetLayoutField>
|
</WidgetLayoutField>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import InputText from 'primevue/inputtext'
|
import InputNumber from 'primevue/inputnumber'
|
||||||
import Slider from 'primevue/slider'
|
import { computed, ref } from 'vue'
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
|
import Slider from '@/components/ui/slider/Slider.vue'
|
||||||
import { useNumberWidgetValue } from '@/composables/graph/useWidgetValue'
|
import { useNumberWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
@@ -42,7 +46,7 @@ import {
|
|||||||
import { WidgetInputBaseClass } from './layout'
|
import { WidgetInputBaseClass } from './layout'
|
||||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const { widget, modelValue, readonly } = defineProps<{
|
||||||
widget: SimplifiedWidget<number>
|
widget: SimplifiedWidget<number>
|
||||||
modelValue: number
|
modelValue: number
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
@@ -53,19 +57,29 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Use the composable for consistent widget value handling
|
// Use the composable for consistent widget value handling
|
||||||
const { localValue, onChange } = useNumberWidgetValue(
|
const { localValue, onChange } = useNumberWidgetValue(widget, modelValue, emit)
|
||||||
props.widget,
|
|
||||||
props.modelValue,
|
const timesEmptied = ref(0)
|
||||||
emit
|
|
||||||
)
|
const updateLocalValue = (newValue: number[] | undefined): void => {
|
||||||
|
onChange(newValue ?? [localValue.value])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNumberInputUpdate = (newValue: number | undefined) => {
|
||||||
|
if (newValue) {
|
||||||
|
updateLocalValue([newValue])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timesEmptied.value += 1
|
||||||
|
}
|
||||||
|
|
||||||
const filteredProps = computed(() =>
|
const filteredProps = computed(() =>
|
||||||
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
|
filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get the precision value for proper number formatting
|
// Get the precision value for proper number formatting
|
||||||
const precision = computed(() => {
|
const precision = computed(() => {
|
||||||
const p = props.widget.options?.precision
|
const p = widget.options?.precision
|
||||||
// Treat negative or non-numeric precision as undefined
|
// Treat negative or non-numeric precision as undefined
|
||||||
return typeof p === 'number' && p >= 0 ? p : undefined
|
return typeof p === 'number' && p >= 0 ? p : undefined
|
||||||
})
|
})
|
||||||
@@ -73,96 +87,21 @@ const precision = computed(() => {
|
|||||||
// Calculate the step value based on precision or widget options
|
// Calculate the step value based on precision or widget options
|
||||||
const stepValue = computed(() => {
|
const stepValue = computed(() => {
|
||||||
// Use step2 (correct input spec value) instead of step (legacy 10x value)
|
// Use step2 (correct input spec value) instead of step (legacy 10x value)
|
||||||
if (props.widget.options?.step2 !== undefined) {
|
if (widget.options?.step2 !== undefined) {
|
||||||
return String(props.widget.options.step2)
|
return widget.options.step2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, derive from precision
|
// Otherwise, derive from precision
|
||||||
if (precision.value !== undefined) {
|
if (precision.value === undefined) {
|
||||||
if (precision.value === 0) {
|
return undefined
|
||||||
return '1'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (precision.value === 0) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
// For precision > 0, step = 1 / (10^precision)
|
// For precision > 0, step = 1 / (10^precision)
|
||||||
// precision 1 → 0.1, precision 2 → 0.01, etc.
|
// precision 1 → 0.1, precision 2 → 0.01, etc.
|
||||||
return (1 / Math.pow(10, precision.value)).toFixed(precision.value)
|
return 1 / Math.pow(10, precision.value)
|
||||||
}
|
|
||||||
// Default to 'any' for unrestricted stepping
|
|
||||||
return 'any'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Format a number according to the widget's precision
|
|
||||||
const formatNumber = (value: number): string => {
|
|
||||||
if (precision.value === undefined) {
|
|
||||||
// No precision specified, return as-is
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
// Use toFixed to ensure correct decimal places
|
|
||||||
return value.toFixed(precision.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply precision-based rounding to a number
|
|
||||||
const applyPrecision = (value: number): number => {
|
|
||||||
if (precision.value === undefined) {
|
|
||||||
// No precision specified, return as-is
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
if (precision.value === 0) {
|
|
||||||
// Integer precision
|
|
||||||
return Math.round(value)
|
|
||||||
}
|
|
||||||
// Round to the specified decimal places
|
|
||||||
const multiplier = Math.pow(10, precision.value)
|
|
||||||
return Math.round(value * multiplier) / multiplier
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep a separate display value for the input field
|
|
||||||
const inputDisplayValue = ref(formatNumber(localValue.value))
|
|
||||||
|
|
||||||
// Update display value when localValue changes from external sources
|
|
||||||
watch(localValue, (newValue) => {
|
|
||||||
inputDisplayValue.value = formatNumber(newValue)
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleInputBlur = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement
|
|
||||||
const value = target.value || '0'
|
|
||||||
const parsed = parseFloat(value)
|
|
||||||
|
|
||||||
if (!isNaN(parsed)) {
|
|
||||||
// Apply precision-based rounding
|
|
||||||
const roundedValue = applyPrecision(parsed)
|
|
||||||
onChange(roundedValue)
|
|
||||||
// Update display value with proper formatting
|
|
||||||
inputDisplayValue.value = formatNumber(roundedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInputKeydown = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
const target = event.target as HTMLInputElement
|
|
||||||
const value = target.value || '0'
|
|
||||||
const parsed = parseFloat(value)
|
|
||||||
|
|
||||||
if (!isNaN(parsed)) {
|
|
||||||
// Apply precision-based rounding
|
|
||||||
const roundedValue = applyPrecision(parsed)
|
|
||||||
onChange(roundedValue)
|
|
||||||
// Update display value with proper formatting
|
|
||||||
inputDisplayValue.value = formatNumber(roundedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Remove number input spinners */
|
|
||||||
:deep(input[type='number']::-webkit-inner-spin-button),
|
|
||||||
:deep(input[type='number']::-webkit-outer-spin-button) {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(input[type='number']) {
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
appearance: textfield;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ defineProps<{
|
|||||||
{{ widget.name }}
|
{{ widget.name }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
class="w-75"
|
class="w-75 cursor-default"
|
||||||
@pointerdown.stop="noop"
|
@pointerdown.stop="noop"
|
||||||
@pointermove.stop="noop"
|
@pointermove.stop="noop"
|
||||||
@pointerup.stop="noop"
|
@pointerup.stop="noop"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export const WidgetInputBaseClass = [
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
export const WidgetInputBaseClass = cn([
|
||||||
// Background
|
// Background
|
||||||
'bg-zinc-500/10',
|
'bg-zinc-500/10',
|
||||||
// Outline
|
// Outline
|
||||||
@@ -11,4 +13,4 @@ export const WidgetInputBaseClass = [
|
|||||||
'!rounded-lg',
|
'!rounded-lg',
|
||||||
// Hover
|
// Hover
|
||||||
'hover:outline-blue-500/80'
|
'hover:outline-blue-500/80'
|
||||||
].join(' ')
|
])
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
host: VITE_REMOTE_DEV ? '0.0.0.0' : undefined,
|
host: VITE_REMOTE_DEV ? '0.0.0.0' : undefined,
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ['**/coverage/**', '**/playwright-report/**']
|
ignored: [
|
||||||
|
'**/coverage/**',
|
||||||
|
'**/playwright-report/**',
|
||||||
|
'**/*.{test,spec}.ts'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/internal': {
|
'/internal': {
|
||||||
|
|||||||
Reference in New Issue
Block a user