diff --git a/docs/app/components/content/examples/color-picker/ColorPickerChooserExample.vue b/docs/app/components/content/examples/color-picker/ColorPickerChooserExample.vue new file mode 100644 index 0000000000..61b0f8bcb3 --- /dev/null +++ b/docs/app/components/content/examples/color-picker/ColorPickerChooserExample.vue @@ -0,0 +1,19 @@ + + + diff --git a/docs/content.config.ts b/docs/content.config.ts index 74c7b33ce0..604bb580d1 100644 --- a/docs/content.config.ts +++ b/docs/content.config.ts @@ -39,7 +39,7 @@ export const collections = { type: 'page', source: [{ include: '**/*' - }, pro!], + }, pro!].filter(Boolean), schema }) } diff --git a/docs/content/3.components/color-picker.md b/docs/content/3.components/color-picker.md new file mode 100644 index 0000000000..8495c96c4e --- /dev/null +++ b/docs/content/3.components/color-picker.md @@ -0,0 +1,150 @@ +--- +title: ColorPicker +description: A component to select a color. +links: + - label: GitHub + icon: i-simple-icons-github + to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/ColorPicker.vue +navigation.badge: New +--- + +## Usage + +Use the `v-model` directive to control the value of the ColorPicker. + +::component-code +--- +ignore: + - modelValue +external: + - modelValue +props: + modelValue: '#00C16A' +--- +:: + +Use the `default-value` prop to set the initial value when you do not need to control its state. + +::component-code +--- +ignore: + - defaultValue +props: + defaultValue: '#00BCD4' +--- +:: + +### RGB Format + +Use the `format` prop to set `rgb` value of the ColorPicker. + +::component-code +--- +ignore: + - modelValue + - format +external: + - modelValue +props: + format: rgb + modelValue: 'rgb(0, 193, 106)' +--- +:: + +### HSL Format + +Use the `format` prop to set `hsl` value of the ColorPicker. + +::component-code +--- +ignore: + - modelValue + - format +external: + - modelValue +props: + format: hsl + modelValue: 'hsl(153, 100%, 37.8%)' +--- +:: + +### HWB Format + +Use the `format` prop to set `hwb` value of the ColorPicker. + +::component-code +--- +ignore: + - modelValue + - format +external: + - modelValue +props: + format: hwb + modelValue: 'hwb(150, 0%, 24%)' +--- +:: + +### Throttle + +Use the `throttle` prop to set the throttle value of the ColorPicker. + +::component-code +--- +ignore: + - modelValue +external: + - modelValue +props: + throttle: 100 + modelValue: '#00C16A' +--- +:: + +### Size + +Use the `size` prop to set the size of the ColorPicker. + +::component-code +--- +props: + size: xl +--- +:: + +### Disabled + +Use the `disabled` prop to disable the ColorPicker. + +::component-code +--- +props: + disabled: true +--- +:: + +## Examples + +### As a Color chooser + +Use a [Button](/components/button) and a [Popover](/components/popover) component to create a color chooser. + +::component-example +--- +name: 'color-picker-chooser-example' +--- +:: + +## API + +### Props + +:component-props + +### Emits + +:component-emits + +## Theme + +:component-theme diff --git a/package.json b/package.json index 3b716c1de1..1ac48e6f4e 100644 --- a/package.json +++ b/package.json @@ -85,9 +85,11 @@ "@tailwindcss/postcss": "4.0.0-beta.5", "@tailwindcss/vite": "4.0.0-beta.5", "@tanstack/vue-table": "^8.20.5", + "@types/color": "^4.2.0", "@unhead/vue": "^1.11.13", "@vueuse/core": "^12.0.0", "@vueuse/integrations": "^12.0.0", + "color": "^4.2.3", "consola": "^3.2.3", "defu": "^6.1.4", "embla-carousel-auto-height": "^8.5.1", diff --git a/playground/app/app.vue b/playground/app/app.vue index cd2ff605d9..1f50516bad 100644 --- a/playground/app/app.vue +++ b/playground/app/app.vue @@ -29,6 +29,7 @@ const components = [ 'checkbox', 'chip', 'collapsible', + 'color-picker', 'context-menu', 'command-palette', 'drawer', diff --git a/playground/app/pages/components/color-picker.vue b/playground/app/pages/components/color-picker.vue new file mode 100644 index 0000000000..b6030162c3 --- /dev/null +++ b/playground/app/pages/components/color-picker.vue @@ -0,0 +1,26 @@ + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd663c3247..6ab35657d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: '@tanstack/vue-table': specifier: ^8.20.5 version: 8.20.5(vue@3.5.13(typescript@5.6.3)) + '@types/color': + specifier: ^4.2.0 + version: 4.2.0 '@unhead/vue': specifier: ^1.11.13 version: 1.11.13(vue@3.5.13(typescript@5.6.3)) @@ -60,6 +63,9 @@ importers: '@vueuse/integrations': specifier: ^12.0.0 version: 12.0.0(change-case@5.4.4)(fuse.js@7.0.0)(typescript@5.6.3) + color: + specifier: ^4.2.3 + version: 4.2.3 consola: specifier: ^3.2.3 version: 3.2.3 @@ -2230,6 +2236,15 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@types/color-convert@2.0.4': + resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==} + + '@types/color-name@1.1.5': + resolution: {integrity: sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg==} + + '@types/color@4.2.0': + resolution: {integrity: sha512-6+xrIRImMtGAL2X3qYkd02Mgs+gFGs+WsK0b7VVMaO4mYRISwyTjcqNrO0mNSmYEoq++rSLDB2F5HDNmqfOe+A==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -9464,6 +9479,16 @@ snapshots: '@trysound/sax@0.2.0': {} + '@types/color-convert@2.0.4': + dependencies: + '@types/color-name': 1.1.5 + + '@types/color-name@1.1.5': {} + + '@types/color@4.2.0': + dependencies: + '@types/color-convert': 2.0.4 + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -10498,7 +10523,6 @@ snapshots: dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 - optional: true color-support@1.1.3: {} @@ -10506,7 +10530,6 @@ snapshots: dependencies: color-convert: 2.0.1 color-string: 1.9.1 - optional: true colord@2.9.3: {} @@ -12129,8 +12152,7 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: - optional: true + is-arrayish@0.3.2: {} is-binary-path@2.1.0: dependencies: @@ -14736,7 +14758,6 @@ snapshots: simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 - optional: true sirv@2.0.4: dependencies: diff --git a/src/runtime/components/ColorPicker.vue b/src/runtime/components/ColorPicker.vue new file mode 100644 index 0000000000..a253b9f9b5 --- /dev/null +++ b/src/runtime/components/ColorPicker.vue @@ -0,0 +1,271 @@ + + + + + + + diff --git a/src/runtime/types/index.ts b/src/runtime/types/index.ts index e89e082426..e407e79d52 100644 --- a/src/runtime/types/index.ts +++ b/src/runtime/types/index.ts @@ -12,6 +12,7 @@ export * from '../components/Carousel.vue' export * from '../components/Checkbox.vue' export * from '../components/Chip.vue' export * from '../components/Collapsible.vue' +export * from '../components/ColorPicker.vue' export * from '../components/CommandPalette.vue' export * from '../components/Container.vue' export * from '../components/ContextMenu.vue' diff --git a/src/theme/color-picker.ts b/src/theme/color-picker.ts new file mode 100644 index 0000000000..00e1638220 --- /dev/null +++ b/src/theme/color-picker.ts @@ -0,0 +1,39 @@ +export default { + slots: { + root: 'data-[disabled]:opacity-75', + picker: 'flex gap-4', + selector: 'rounded-[calc(var(--ui-radius)*1.5)]', + selectorBackground: 'w-full h-full relative rounded-[calc(var(--ui-radius)*1.2)]', + selectorThumb: '-translate-y-1/2 -translate-x-1/2 absolute size-4 ring-2 ring-[var(--color-white)] rounded-full cursor-pointer data-[disabled]:cursor-not-allowed', + track: 'w-[8px] relative rounded-[calc(var(--ui-radius)*1.5)]', + trackThumb: 'absolute transform -translate-y-1/2 -translate-x-[4px] size-4 rounded-full ring-2 ring-[var(--color-white)] cursor-pointer data-[disabled]:cursor-not-allowed' + }, + variants: { + size: { + xs: { + selector: 'w-38 h-38', + track: 'h-38' + }, + sm: { + selector: 'w-40 h-40', + track: 'h-40' + }, + md: { + selector: 'w-42 h-42', + track: 'h-42' + }, + lg: { + selector: 'w-44 h-44', + track: 'h-44' + }, + xl: { + selector: 'w-46 h-46', + track: 'h-46' + } + } + }, + compoundVariants: [], + defaultVariants: { + size: 'md' + } +} diff --git a/src/theme/index.ts b/src/theme/index.ts index c5af513a97..f4c7f753ff 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -12,6 +12,7 @@ export { default as carousel } from './carousel' export { default as checkbox } from './checkbox' export { default as chip } from './chip' export { default as collapsible } from './collapsible' +export { default as colorPicker } from './color-picker' export { default as commandPalette } from './command-palette' export { default as container } from './container' export { default as contextMenu } from './context-menu' diff --git a/test/components/ColorPicker.spec.ts b/test/components/ColorPicker.spec.ts new file mode 100644 index 0000000000..8c706c248b --- /dev/null +++ b/test/components/ColorPicker.spec.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, test } from 'vitest' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import ColorPicker, { type ColorPickerProps } from '../../src/runtime/components/ColorPicker.vue' +import ComponentRender from '../component-render' +import theme from '#build/ui/color-picker' + +describe('ColorPicker', () => { + const sizes = Object.keys(theme.variants.size) as any + const formats = [ + ['hex', '#00C16A'], + ['rgb', 'rgb(0, 193, 106)'], + ['hsl', 'hsl(153, 100%, 37.8%)'], + ['hwb', 'hwb(150, 0%, 24%)'] + ] + + it.each([ + // Props + ['with disabled', { props: { disabled: true } }], + ...sizes.map((size: string) => [`with size ${size}`, { props: { size } }]), + ...formats.map(format => [`with format ${format[0]}`, { props: { format: format[0], defaultValue: format[1] } }]), + ['with as', { props: { as: 'section' } }], + ['with class', { props: { class: 'w-96' } }], + ['with ui', { props: { ui: { picker: 'gap-8' } } }] + ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ColorPickerProps }) => { + const html = await ComponentRender(nameOrHtml, options, ColorPicker) + expect(html).toMatchSnapshot() + }) + + describe('emits', () => { + test('update:modelValue event', async () => { + const wrapper = await mountSuspended(ColorPicker) + await wrapper.setValue('#00C16A') + + expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [['#00C16A']] }) + }) + }) +}) diff --git a/test/components/__snapshots__/ColorPicker-vue.spec.ts.snap b/test/components/__snapshots__/ColorPicker-vue.spec.ts.snap new file mode 100644 index 0000000000..d457a02aab --- /dev/null +++ b/test/components/__snapshots__/ColorPicker-vue.spec.ts.snap @@ -0,0 +1,196 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ColorPicker > renders with as correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with class correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with disabled correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with format hex correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with format hsl correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with format hwb correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with format rgb correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with size lg correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with size md correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with size sm correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with size xl correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with size xs correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with ui correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; diff --git a/test/components/__snapshots__/ColorPicker.spec.ts.snap b/test/components/__snapshots__/ColorPicker.spec.ts.snap new file mode 100644 index 0000000000..ac012573de --- /dev/null +++ b/test/components/__snapshots__/ColorPicker.spec.ts.snap @@ -0,0 +1,196 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ColorPicker > renders with as correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with class correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with disabled correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with format hex correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with format hsl correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with format hwb correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with format rgb correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with size lg correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with size md correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with size sm correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with size xl correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with size xs correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[`ColorPicker > renders with ui correctly 1`] = ` +"
+
+
+
+
+
+
+
+
+
+
+
" +`;