├── .husky ├── .gitignore └── commit-msg ├── example ├── CNAME ├── entry.ts ├── CurrentValue.vue ├── index.html ├── vite.config.ts ├── ExampleFullBinding.vue ├── DefaultController.vue ├── ExampleTransition.vue ├── ExampleReactiveStyle.vue ├── ExampleDisabledItems.vue ├── ExampleDynamicOptions.vue ├── ExampleDialog.vue ├── ExampleSlot.vue ├── ExampleSensitivity.vue ├── ExampleEvent.vue ├── example.css ├── ExampleMultiple.vue └── App.vue ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode ├── settings.json └── extensions.json ├── commitlint.config.js ├── types └── vue.d.ts ├── renovate.json ├── vitest.config.ts ├── eslint.config.mjs ├── .github └── workflows │ └── ci.yml ├── tsconfig.json ├── src ├── index.ts ├── components │ ├── VueScrollPicker.spec.ts │ └── VueScrollPicker.vue └── style.css ├── scripts └── test-build.mjs ├── vite.config.ts ├── package.json └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /example/CNAME: -------------------------------------------------------------------------------- 1 | vue-scroll-picker.dist.be 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /dist 4 | /example-dist 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !/dist/**/* 3 | !/src/**/* 4 | !README.md 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Optionable" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "dbaeumer.vscode-eslint", 5 | "streetsidesoftware.code-spell-checker" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /types/vue.d.ts: -------------------------------------------------------------------------------- 1 | // declare module '*.vue' { 2 | // import { DefineComponent } from 'vue' 3 | // const component: DefineComponent<{}, {}, any> 4 | // export default component 5 | // } 6 | -------------------------------------------------------------------------------- /example/entry.ts: -------------------------------------------------------------------------------- 1 | import './example.css' 2 | import 'vue-scroll-picker/dist/style.css' 3 | 4 | import { createApp } from 'vue' 5 | 6 | import App from './App.vue' 7 | 8 | const app = createApp(App) 9 | 10 | app.mount('#app') 11 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "docker:disable", 5 | ":dependencyDashboard" 6 | ], 7 | "automerge": true, 8 | "major": { 9 | "automerge": false, 10 | "dependencyDashboardApproval": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/CurrentValue.vue: -------------------------------------------------------------------------------- 1 | 6 | 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig, ViteUserConfig } from 'vitest/config' 2 | import viteConfig from './vite.config' 3 | 4 | export default mergeConfig(viteConfig, { 5 | test: { 6 | include: ['src/**/*.spec.ts'], 7 | setupFiles: ['vitest-browser-vue'], 8 | browser: { 9 | enabled: true, 10 | provider: 'playwright', 11 | instances: [{ browser: 'chromium' }], 12 | }, 13 | }, 14 | } satisfies ViteUserConfig) 15 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' 2 | import eslintPluginVue from 'eslint-plugin-vue' 3 | import { 4 | defineConfigWithVueTs, 5 | vueTsConfigs, 6 | } from '@vue/eslint-config-typescript' 7 | 8 | export default defineConfigWithVueTs( 9 | { 10 | ignores: ['example-dist/*', 'dist/*'], 11 | }, 12 | eslintPluginVue.configs['flat/recommended'], 13 | vueTsConfigs.recommended, 14 | eslintPluginPrettierRecommended, 15 | { 16 | rules: { 17 | 'no-console': 'error', 18 | }, 19 | }, 20 | ) 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [18.x, 20.x, 22.x, 23.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npx playwright install 24 | - run: npm run test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vue Scroll Picker Sample - Default 9 | 10 | 11 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "outDir": "dist", 5 | "baseUrl": "./", 6 | "paths": { 7 | "vue-scroll-picker": ["./src"] 8 | }, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "lib": [ 12 | "DOM", 13 | "ESNEXT" 14 | ], 15 | "emitDeclarationOnly": true, 16 | "declaration": true, 17 | "pretty": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "esModuleInterop": true, 21 | "noImplicitReturns": true, 22 | "skipLibCheck": true, 23 | "types": ["vite/client", "@vue/runtime-dom"] 24 | }, 25 | "include": [ 26 | "example/**/*.ts", 27 | "example/**/*.vue", 28 | "src/**/*.ts", 29 | "src/**/*.vue", 30 | "types/**/*.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { resolve } from 'path' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | resolve: { 9 | alias: { 10 | 'vue-scroll-picker': resolve(__dirname, '..'), 11 | }, 12 | }, 13 | build: { 14 | outDir: resolve(__dirname, '../example-dist'), 15 | emptyOutDir: true, 16 | rollupOptions: { 17 | output: { 18 | manualChunks(id) { 19 | if (!id.includes('node_modules')) { 20 | return 21 | } 22 | return id 23 | .toString() 24 | .split('node_modules/')[1] 25 | .split('/')[0] 26 | .toString() 27 | }, 28 | }, 29 | }, 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { App, Component, Plugin } from 'vue' 2 | 3 | import VueScrollPicker, { 4 | type ScrollPickerValue, 5 | type ScrollPickerOption, 6 | type ScrollPickerOptionable, 7 | } from './components/VueScrollPicker.vue' 8 | 9 | import './style.css' 10 | 11 | export function install(app: App) { 12 | app.component('VueScrollPicker', VueScrollPicker as unknown as Component) 13 | } 14 | 15 | if (typeof window !== 'undefined' && 'Vue' in window) { 16 | install(window.Vue as App) 17 | } 18 | 19 | const plugin: Plugin = { 20 | install, 21 | } 22 | 23 | export default plugin 24 | 25 | export { VueScrollPicker } 26 | 27 | export type VueScrollPickerValue = ScrollPickerValue 28 | export type VueScrollPickerOption = ScrollPickerOption 29 | export type VueScrollPickerOptionable = ScrollPickerOptionable 30 | -------------------------------------------------------------------------------- /example/ExampleFullBinding.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | -------------------------------------------------------------------------------- /src/components/VueScrollPicker.spec.ts: -------------------------------------------------------------------------------- 1 | import '@vitest/browser/matchers.d.ts' 2 | 3 | import { describe, expect, test } from 'vitest' 4 | import { render } from 'vitest-browser-vue' 5 | import VueScrollPicker from './VueScrollPicker.vue' 6 | 7 | import '../style.css' 8 | 9 | describe('Picker', () => { 10 | test('should be able to select option', async () => { 11 | const screen = render(VueScrollPicker, { 12 | props: { 13 | options: [1, 2, 3], 14 | modelValue: 1, 15 | }, 16 | }) 17 | 18 | await expect 19 | .element(screen.getByRole('option', { selected: true })) 20 | .toHaveTextContent('1') 21 | 22 | screen.rerender({ 23 | modelValue: 2, 24 | }) 25 | 26 | await expect 27 | .element(screen.getByRole('option', { selected: true })) 28 | .toHaveTextContent('2') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /example/DefaultController.vue: -------------------------------------------------------------------------------- 1 | 13 | 34 | -------------------------------------------------------------------------------- /scripts/test-build.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { readFile, stat } from 'fs/promises' 4 | 5 | const __dirname = new URL('.', import.meta.url).pathname 6 | 7 | const pkg = JSON.parse(await readFile(`${__dirname}/../package.json`)) 8 | 9 | const files = [pkg.main, pkg.module, pkg.types, pkg.typings, pkg.exports] 10 | 11 | const wrongFiles = [] 12 | async function traverse(obj) { 13 | if (Array.isArray(obj)) { 14 | return Promise.all(obj.map((item) => traverse(item))) 15 | } 16 | 17 | if (typeof obj === 'object' && obj !== null) { 18 | return Promise.all(Object.values(obj).map((item) => traverse(item))) 19 | } 20 | 21 | if (typeof obj === 'string') { 22 | console.log('check', obj) 23 | try { 24 | await stat(`${__dirname}/../${obj}`) 25 | } catch { 26 | console.warn(`file(${file}) not found`) 27 | wrongFiles.push(file) 28 | } 29 | } 30 | } 31 | 32 | traverse(files).then(() => { 33 | if (wrongFiles.length > 0) { 34 | console.error('Build failed') 35 | process.exit(1) 36 | } 37 | 38 | console.log('Build succeeded') 39 | }) 40 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { defineConfig } from 'vite' 3 | import dts from 'vite-plugin-dts' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | esbuild: { 8 | drop: process.env.BUILD_TARGET === 'npm' ? ['console', 'debugger'] : [], 9 | }, 10 | plugins: [ 11 | vue({ 12 | exclude: ['src/**/*.spec.ts'], 13 | }), 14 | dts({ 15 | outDir: 'dist', 16 | include: ['src/**/*.ts', 'src/**/*.vue'], 17 | exclude: ['src/**/*.spec.ts'], 18 | }), 19 | ], 20 | build: { 21 | outDir: 'dist', 22 | lib: { 23 | entry: 'src/index.ts', 24 | name: 'VueScrollPicker', 25 | formats: ['es', 'cjs'], 26 | fileName: (format, entryName) => 27 | format === 'es' 28 | ? `${entryName}.mjs` 29 | : format === 'cjs' 30 | ? `${entryName}.cjs` 31 | : `${entryName}.js`, 32 | }, 33 | rollupOptions: { 34 | external: ['vue'], 35 | output: { 36 | globals: { 37 | vue: 'Vue', 38 | }, 39 | }, 40 | }, 41 | sourcemap: true, 42 | }, 43 | }) 44 | -------------------------------------------------------------------------------- /example/ExampleTransition.vue: -------------------------------------------------------------------------------- 1 | 18 | 37 | 47 | -------------------------------------------------------------------------------- /example/ExampleReactiveStyle.vue: -------------------------------------------------------------------------------- 1 | 19 | 45 | -------------------------------------------------------------------------------- /example/ExampleDisabledItems.vue: -------------------------------------------------------------------------------- 1 | 28 | 37 | -------------------------------------------------------------------------------- /example/ExampleDynamicOptions.vue: -------------------------------------------------------------------------------- 1 | 29 | 44 | -------------------------------------------------------------------------------- /example/ExampleDialog.vue: -------------------------------------------------------------------------------- 1 | 26 | 42 | 75 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .vue-scroll-picker { 2 | position: relative; 3 | width: 100%; 4 | height: 10em; 5 | overflow: hidden; 6 | user-select: none; 7 | } 8 | 9 | .vue-scroll-picker-rotator { 10 | position: absolute; 11 | left: 0; 12 | right: 0; 13 | top: calc(50% - .6em); 14 | } 15 | 16 | .vue-scroll-picker-rotator-transition { 17 | transition: top ease 150ms; 18 | } 19 | 20 | .vue-scroll-picker-item { 21 | text-align: center; 22 | line-height: 1.2em; 23 | color: #333; 24 | } 25 | 26 | .vue-scroll-picker-item[aria-selected='true'] { 27 | color: #007bff; 28 | } 29 | 30 | .vue-scroll-picker-item[data-value=''], 31 | .vue-scroll-picker-item[aria-disabled='true'] { 32 | color: #ccc; 33 | } 34 | 35 | .vue-scroll-picker-item[data-value=''][aria-selected='true'], 36 | .vue-scroll-picker-item[aria-disabled='true'][aria-selected='true'] { 37 | color: #aaa; 38 | } 39 | 40 | .vue-scroll-picker-layer { 41 | position: absolute; 42 | left: 0; 43 | right: 0; 44 | top: 0; 45 | bottom: 0; 46 | } 47 | 48 | .vue-scroll-picker-layer-top, 49 | .vue-scroll-picker-layer-selection, 50 | .vue-scroll-picker-layer-bottom { 51 | position: absolute; 52 | left: 0; 53 | right: 0; 54 | } 55 | 56 | .vue-scroll-picker-layer-top { 57 | box-sizing: border-box; 58 | border-bottom: 1px solid #c8c7cc; 59 | background: linear-gradient(180deg,#fff 10%,rgba(255, 255, 255, .7)); 60 | top: 0; 61 | height: calc(50% - 1em); 62 | cursor: pointer; 63 | } 64 | 65 | .vue-scroll-picker-layer-selection { 66 | top: calc(50% - 1em); 67 | bottom: calc(50% - 1em); 68 | } 69 | 70 | .vue-scroll-picker-layer-bottom { 71 | border-top: 1px solid #c8c7cc; 72 | background: linear-gradient(0deg,#fff 10%,rgba(255, 255, 255, .7)); 73 | bottom: 0; 74 | height: calc(50% - 1em); 75 | cursor: pointer; 76 | } 77 | -------------------------------------------------------------------------------- /example/ExampleSlot.vue: -------------------------------------------------------------------------------- 1 | 28 | 45 | 59 | -------------------------------------------------------------------------------- /example/ExampleSensitivity.vue: -------------------------------------------------------------------------------- 1 | 20 | 75 | 86 | -------------------------------------------------------------------------------- /example/ExampleEvent.vue: -------------------------------------------------------------------------------- 1 | 28 | 56 | 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-scroll-picker", 3 | "version": "2.0.1", 4 | "description": "iOS Style Scroll Picker Component for Vue 3. Support All Gestures of Mouse(also MouseWheel) and Touch.", 5 | "author": "Changwan Jun", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/wan2land/vue-scroll-picker.git" 10 | }, 11 | "scripts": { 12 | "prepack": "npm run build", 13 | "test": "npm run test:eslint && npm run test:vitest && npm run build", 14 | "test:eslint": "eslint", 15 | "test:vitest": "vitest run", 16 | "test:build": "npm run build:production && node scripts/test-build.mjs", 17 | "dev": "concurrently \"npm run dev:example\" \"npm run build:watch\"", 18 | "dev:example": "vite serve example --host 0.0.0.0", 19 | "build": "npm run build:production && node scripts/test-build.mjs", 20 | "build:watch": "vite build --watch", 21 | "build:production": "vite build", 22 | "build:example": "vite build example && cp example/CNAME example-dist/", 23 | "deploy:example": "npm run build:example && gh-pages -d example-dist" 24 | }, 25 | "type": "module", 26 | "types": "dist/index.d.ts", 27 | "typings": "dist/index.d.ts", 28 | "main": "dist/index.mjs", 29 | "module": "dist/index.mjs", 30 | "exports": { 31 | ".": { 32 | "types": "./dist/index.d.ts", 33 | "default": "./dist/index.mjs", 34 | "import": "./dist/index.mjs", 35 | "require": "./dist/index.cjs" 36 | }, 37 | "./style.css": "./dist/style.css", 38 | "./package.json": "./package.json" 39 | }, 40 | "devDependencies": { 41 | "@commitlint/cli": "17.8.1", 42 | "@commitlint/config-conventional": "13.2.0", 43 | "@types/node": "18.19.130", 44 | "@vitejs/plugin-vue": "^4.6.2", 45 | "@vitest/browser": "^3.0.4", 46 | "@vue/compiler-sfc": "3.5.25", 47 | "@vue/eslint-config-typescript": "^14.3.0", 48 | "@vue/test-utils": "^2.4.6", 49 | "concurrently": "^9.1.2", 50 | "date-fns": "^4.1.0", 51 | "eslint": "^9.19.0", 52 | "eslint-config-prettier": "^10.0.1", 53 | "eslint-plugin-prettier": "^5.2.3", 54 | "eslint-plugin-vue": "^9.32.0", 55 | "gh-pages": "5.0.0", 56 | "happy-dom": "^16.7.3", 57 | "husky": "7.0.4", 58 | "playwright": "^1.50.0", 59 | "prettier": "^3.4.2", 60 | "simple-icons": "7.21.0", 61 | "typescript": "^5.7.3", 62 | "vite": "^4.5.2", 63 | "vite-plugin-dts": "^4.5.0", 64 | "vitest": "^3.0.4", 65 | "vitest-browser-vue": "^0.2.0", 66 | "vue": "3.5.25" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | html { 8 | font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, 9 | Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, 10 | Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; 11 | font-weight: 400; 12 | } 13 | 14 | a { 15 | cursor: pointer; 16 | color: inherit; 17 | } 18 | 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 { 25 | font-weight: 400; 26 | line-height: 1.5; 27 | } 28 | 29 | h1 { 30 | font-size: 1.625rem; 31 | } 32 | 33 | h2 { 34 | font-size: 1.5rem; 35 | } 36 | 37 | h3 { 38 | font-size: 1.375rem; 39 | } 40 | 41 | h4 { 42 | font-size: 1.25rem; 43 | } 44 | 45 | h5 { 46 | font-size: 1.125rem; 47 | } 48 | 49 | h1 a, 50 | h2 a, 51 | h3 a, 52 | h4 a, 53 | h5 a, 54 | h6 a { 55 | color: inherit; 56 | text-decoration: none; 57 | } 58 | 59 | h1 a:hover::after, 60 | h2 a:hover::after, 61 | h3 a:hover::after, 62 | h4 a:hover::after, 63 | h5 a:hover::after, 64 | h6 a:hover::after { 65 | /* if hover, appear a link icon */ 66 | content: " 🔗"; 67 | } 68 | 69 | .hero { 70 | position: relative; 71 | padding: 6rem 1rem; 72 | background: linear-gradient(150deg, #281483 15%, #8f6ed5 70%, #d782d9 94%); 73 | display: flex; 74 | flex-flow: column; 75 | align-items: center; 76 | justify-content: center; 77 | color: #fff; 78 | } 79 | 80 | .hero::after { 81 | content: ""; 82 | display: block; 83 | position: absolute; 84 | left: 0; 85 | right: 0; 86 | bottom: 0; 87 | height: 60px; 88 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' preserveAspectRatio='none'%3E%3Cpolygon fill='white' points='16 0 16 16 0 16'/%3E%3C/svg%3E"); 89 | background-repeat: no-repeat; 90 | background-size: 100% 100%; 91 | } 92 | 93 | .hero .github { 94 | position: absolute; 95 | right: 0; 96 | top: 0; 97 | padding: 8px; 98 | color: #ffffff; 99 | text-decoration: none; 100 | } 101 | 102 | .hero .github svg { 103 | width: 32px; 104 | height: 32px; 105 | fill: currentColor; 106 | } 107 | 108 | .section { 109 | padding: 2rem 1rem; 110 | } 111 | 112 | .container { 113 | width: 100%; 114 | margin-left: auto; 115 | margin-right: auto; 116 | } 117 | 118 | @media (min-width: 576px) { 119 | .container { 120 | max-width: 540px; 121 | } 122 | } 123 | 124 | @media (min-width: 768px) { 125 | .container { 126 | max-width: 720px; 127 | } 128 | } 129 | 130 | @media (min-width: 992px) { 131 | .container { 132 | max-width: 960px; 133 | } 134 | } 135 | 136 | @media (min-width: 1200px) { 137 | .container { 138 | max-width: 1140px; 139 | } 140 | } 141 | 142 | .button { 143 | display: inline-block; 144 | padding: 0.25rem 0.5rem; 145 | border: 1px solid #007bff; 146 | color: #007bff; 147 | background-color: transparent; 148 | font-size: 1rem; 149 | outline: none; 150 | cursor: pointer; 151 | } 152 | 153 | .button.active { 154 | background-color: #007bff; 155 | color: #fff; 156 | } 157 | 158 | .button.disabled { 159 | display: inline-block; 160 | padding: 0.25rem 0.5rem; 161 | border: 1px solid #aaaaaa; 162 | color: #aaaaaa; 163 | } 164 | 165 | .button.disabled.active { 166 | background-color: #aaaaaa; 167 | color: #fff; 168 | } 169 | 170 | code { 171 | display: inline-block; 172 | background-color: #f0f0f0; 173 | padding: 0.25rem 0.375rem; 174 | border-radius: 0.25rem; 175 | font-size: 0.875rem; 176 | border: 1px solid #e0e0e0; 177 | } 178 | 179 | .button-group { 180 | display: flex; 181 | flex-wrap: wrap; 182 | gap: 0.5rem; 183 | } 184 | 185 | .controller { 186 | display: flex; 187 | flex-direction: column; 188 | gap: 0.5rem; 189 | user-select: none; 190 | } 191 | -------------------------------------------------------------------------------- /example/ExampleMultiple.vue: -------------------------------------------------------------------------------- 1 | 76 | 121 | 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Scroll Picker 2 | 3 |

4 | Build 5 | Downloads 6 | Version 7 | License 8 | VueJS 3.x 9 | Language Typescript 10 |

11 | 12 | Vue Scroll Picker is an iOS-style scroll picker component for Vue 3. It supports all gestures, including mouse and touch interactions, ensuring a smooth and intuitive user experience. 13 | 14 | If you are using Vue 2, please refer to the [v0.x branch](https://github.com/wan2land/vue-scroll-picker/tree/0.x-vue2). 15 | 16 | [Live Demo](http://vue-scroll-picker.dist.be) ([source](./example)) 17 | 18 | ## Features 19 | 20 | - **TypeScript Support**: Uses generics for strict type checking and improved developer experience. 21 | - **Native-like Behavior**: Mimics `