feat: migrate ToggleSwitch to shadcn-vue Switch
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
382d76c2d7
commit
7a4ae9ff6a
8 changed files with 86 additions and 28 deletions
|
|
@ -4,3 +4,4 @@ export { Button, buttonVariants } from './ui/button'
|
||||||
export type { ButtonVariants } from './ui/button'
|
export type { ButtonVariants } from './ui/button'
|
||||||
export { Input } from './ui/input'
|
export { Input } from './ui/input'
|
||||||
export { Textarea } from './ui/textarea'
|
export { Textarea } from './ui/textarea'
|
||||||
|
export { Switch } from './ui/switch'
|
||||||
|
|
|
||||||
44
packages/library/src/components/ui/switch/Switch.vue
Normal file
44
packages/library/src/components/ui/switch/Switch.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SwitchRootEmits, SwitchRootProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
SwitchRoot,
|
||||||
|
SwitchThumb,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SwitchRootProps & {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}>(), {
|
||||||
|
size: "default",
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<SwitchRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SwitchRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="switch"
|
||||||
|
:data-size="size"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn(
|
||||||
|
'data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-[18.4px] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<SwitchThumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
class="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0 transition-transform"
|
||||||
|
>
|
||||||
|
<slot name="thumb" v-bind="slotProps" />
|
||||||
|
</SwitchThumb>
|
||||||
|
</SwitchRoot>
|
||||||
|
</template>
|
||||||
1
packages/library/src/components/ui/switch/index.ts
Normal file
1
packages/library/src/components/ui/switch/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Switch } from "./Switch.vue"
|
||||||
|
|
@ -16,7 +16,7 @@ export {
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
RadioButton,
|
RadioButton,
|
||||||
RadioButtonGroup,
|
RadioButtonGroup,
|
||||||
ToggleSwitch,
|
Switch,
|
||||||
Select,
|
Select,
|
||||||
Card,
|
Card,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import {
|
||||||
CheckboxGroup as PrimeCheckboxGroup,
|
CheckboxGroup as PrimeCheckboxGroup,
|
||||||
RadioButton as PrimeRadioButton,
|
RadioButton as PrimeRadioButton,
|
||||||
RadioButtonGroup as PrimeRadioButtonGroup,
|
RadioButtonGroup as PrimeRadioButtonGroup,
|
||||||
ToggleSwitch as PrimeToggleSwitch,
|
|
||||||
Select as PrimeSelect,
|
Select as PrimeSelect,
|
||||||
Card as PrimeCard,
|
Card as PrimeCard,
|
||||||
Dialog as PrimeDialog,
|
Dialog as PrimeDialog,
|
||||||
|
|
@ -12,9 +11,9 @@ import {
|
||||||
Tag as PrimeTag,
|
Tag as PrimeTag,
|
||||||
} from 'primevue'
|
} from 'primevue'
|
||||||
import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine'
|
import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine'
|
||||||
import { NychLoadingIcon, Button, buttonVariants, Input, Textarea } from '../components'
|
import { NychLoadingIcon, Button, buttonVariants, Input, Textarea, Switch } from '../components'
|
||||||
|
|
||||||
export { Button, buttonVariants, Input, Textarea }
|
export { Button, buttonVariants, Input, Textarea, Switch }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a Themeable that themes a PrimeVue component through its passthrough (`pt`) API.
|
* Build a Themeable that themes a PrimeVue component through its passthrough (`pt`) API.
|
||||||
|
|
@ -62,7 +61,6 @@ const nychthemeron: ThemeLibrary = {
|
||||||
CheckboxGroup: ptThemeable(PrimeCheckboxGroup, 'checkbox-group'),
|
CheckboxGroup: ptThemeable(PrimeCheckboxGroup, 'checkbox-group'),
|
||||||
RadioButton: ptThemeable(PrimeRadioButton, 'radio', ['box', 'input', 'icon']),
|
RadioButton: ptThemeable(PrimeRadioButton, 'radio', ['box', 'input', 'icon']),
|
||||||
RadioButtonGroup: ptThemeable(PrimeRadioButtonGroup, 'radio-group'),
|
RadioButtonGroup: ptThemeable(PrimeRadioButtonGroup, 'radio-group'),
|
||||||
ToggleSwitch: ptThemeable(PrimeToggleSwitch, 'toggleswitch', ['input', 'slider', 'handle']),
|
|
||||||
Select: ptThemeable(PrimeSelect, 'select', [
|
Select: ptThemeable(PrimeSelect, 'select', [
|
||||||
'label',
|
'label',
|
||||||
'dropdown',
|
'dropdown',
|
||||||
|
|
@ -114,8 +112,6 @@ export const RadioButton: typeof PrimeRadioButton =
|
||||||
_engine.getComponent<typeof PrimeRadioButton>('RadioButton')
|
_engine.getComponent<typeof PrimeRadioButton>('RadioButton')
|
||||||
export const RadioButtonGroup: typeof PrimeRadioButtonGroup =
|
export const RadioButtonGroup: typeof PrimeRadioButtonGroup =
|
||||||
_engine.getComponent<typeof PrimeRadioButtonGroup>('RadioButtonGroup')
|
_engine.getComponent<typeof PrimeRadioButtonGroup>('RadioButtonGroup')
|
||||||
export const ToggleSwitch: typeof PrimeToggleSwitch =
|
|
||||||
_engine.getComponent<typeof PrimeToggleSwitch>('ToggleSwitch')
|
|
||||||
export const Select: typeof PrimeSelect = _engine.getComponent<typeof PrimeSelect>('Select')
|
export const Select: typeof PrimeSelect = _engine.getComponent<typeof PrimeSelect>('Select')
|
||||||
export const Card: typeof PrimeCard = _engine.getComponent<typeof PrimeCard>('Card')
|
export const Card: typeof PrimeCard = _engine.getComponent<typeof PrimeCard>('Card')
|
||||||
export const Dialog: typeof PrimeDialog = _engine.getComponent<typeof PrimeDialog>('Dialog')
|
export const Dialog: typeof PrimeDialog = _engine.getComponent<typeof PrimeDialog>('Dialog')
|
||||||
|
|
@ -131,7 +127,7 @@ export const createNychthemeron = (): {
|
||||||
CheckboxGroup: typeof PrimeCheckboxGroup
|
CheckboxGroup: typeof PrimeCheckboxGroup
|
||||||
RadioButton: typeof PrimeRadioButton
|
RadioButton: typeof PrimeRadioButton
|
||||||
RadioButtonGroup: typeof PrimeRadioButtonGroup
|
RadioButtonGroup: typeof PrimeRadioButtonGroup
|
||||||
ToggleSwitch: typeof PrimeToggleSwitch
|
Switch: typeof Switch
|
||||||
Select: typeof PrimeSelect
|
Select: typeof PrimeSelect
|
||||||
Card: typeof PrimeCard
|
Card: typeof PrimeCard
|
||||||
Dialog: typeof PrimeDialog
|
Dialog: typeof PrimeDialog
|
||||||
|
|
@ -147,7 +143,7 @@ export const createNychthemeron = (): {
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
RadioButton,
|
RadioButton,
|
||||||
RadioButtonGroup,
|
RadioButtonGroup,
|
||||||
ToggleSwitch,
|
Switch,
|
||||||
Select,
|
Select,
|
||||||
Card,
|
Card,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -162,7 +158,7 @@ export const createNychthemeron = (): {
|
||||||
app.component('NychCheckboxGroup', CheckboxGroup)
|
app.component('NychCheckboxGroup', CheckboxGroup)
|
||||||
app.component('NychRadioButton', RadioButton)
|
app.component('NychRadioButton', RadioButton)
|
||||||
app.component('NychRadioButtonGroup', RadioButtonGroup)
|
app.component('NychRadioButtonGroup', RadioButtonGroup)
|
||||||
app.component('NychToggleSwitch', ToggleSwitch)
|
app.component('NychSwitch', Switch)
|
||||||
app.component('NychSelect', Select)
|
app.component('NychSelect', Select)
|
||||||
app.component('NychCard', Card)
|
app.component('NychCard', Card)
|
||||||
app.component('NychDialog', Dialog)
|
app.component('NychDialog', Dialog)
|
||||||
|
|
|
||||||
21
packages/library/tests/Switch.spec.ts
Normal file
21
packages/library/tests/Switch.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { Switch } from '../src/lib'
|
||||||
|
|
||||||
|
describe('Switch', () => {
|
||||||
|
it('renders unchecked by default', () => {
|
||||||
|
const wrapper = mount(Switch)
|
||||||
|
expect(wrapper.get('button').attributes('data-state')).toBe('unchecked')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reflects modelValue as checked', () => {
|
||||||
|
const wrapper = mount(Switch, { props: { modelValue: true } })
|
||||||
|
expect(wrapper.get('button').attributes('data-state')).toBe('checked')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits update:modelValue on click', async () => {
|
||||||
|
const wrapper = mount(Switch, { props: { modelValue: false } })
|
||||||
|
await wrapper.get('button').trigger('click')
|
||||||
|
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([true])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
RadioButton,
|
RadioButton,
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
RadioButtonGroup,
|
RadioButtonGroup,
|
||||||
ToggleSwitch,
|
|
||||||
Select,
|
Select,
|
||||||
Card,
|
Card,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -73,7 +72,6 @@ describe('themed pt components', () => {
|
||||||
{ name: 'CheckboxGroup', component: CheckboxGroup, base: 'nych-checkbox-group' },
|
{ name: 'CheckboxGroup', component: CheckboxGroup, base: 'nych-checkbox-group' },
|
||||||
{ name: 'RadioButton', component: RadioButton, base: 'nych-radio' },
|
{ name: 'RadioButton', component: RadioButton, base: 'nych-radio' },
|
||||||
{ name: 'RadioButtonGroup', component: RadioButtonGroup, base: 'nych-radio-group' },
|
{ name: 'RadioButtonGroup', component: RadioButtonGroup, base: 'nych-radio-group' },
|
||||||
{ name: 'ToggleSwitch', component: ToggleSwitch, base: 'nych-toggleswitch' },
|
|
||||||
{ name: 'Select', component: Select, base: 'nych-select' },
|
{ name: 'Select', component: Select, base: 'nych-select' },
|
||||||
{ name: 'Card', component: Card, base: 'nych-card' },
|
{ name: 'Card', component: Card, base: 'nych-card' },
|
||||||
{ name: 'Dialog', component: Dialog, base: 'nych-dialog' },
|
{ name: 'Dialog', component: Dialog, base: 'nych-dialog' },
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import { ToggleSwitch as NychToggleSwitch } from '@nychthemeron/library'
|
import { Switch as NychSwitch } from '@nychthemeron/library'
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/ToggleSwitch',
|
title: 'Components/Switch',
|
||||||
component: NychToggleSwitch,
|
component: NychSwitch,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
|
|
@ -16,43 +16,40 @@ const meta = {
|
||||||
description: 'Disable the switch',
|
description: 'Disable the switch',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} satisfies Meta<typeof NychToggleSwitch>
|
} satisfies Meta<typeof NychSwitch>
|
||||||
|
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
render: () => ({
|
render: () => ({
|
||||||
components: { NychToggleSwitch },
|
components: { NychSwitch },
|
||||||
setup() {
|
setup() {
|
||||||
const on = ref(false)
|
const on = ref(false)
|
||||||
return { on }
|
return { on }
|
||||||
},
|
},
|
||||||
template: `<NychToggleSwitch v-model="on" />`,
|
template: `<NychSwitch v-model="on" />`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const On: Story = {
|
export const On: Story = {
|
||||||
render: () => ({
|
render: () => ({
|
||||||
components: { NychToggleSwitch },
|
components: { NychSwitch },
|
||||||
setup() {
|
setup() {
|
||||||
const on = ref(true)
|
const on = ref(true)
|
||||||
return { on }
|
return { on }
|
||||||
},
|
},
|
||||||
template: `<NychToggleSwitch v-model="on" />`,
|
template: `<NychSwitch v-model="on" />`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Disabled: Story = {
|
export const Disabled: Story = {
|
||||||
render: () => ({
|
render: () => ({
|
||||||
components: { NychToggleSwitch },
|
components: { NychSwitch },
|
||||||
setup() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
template: `
|
template: `
|
||||||
<div style="display: flex; gap: 1rem;">
|
<div style="display: flex; gap: 1rem;">
|
||||||
<NychToggleSwitch :modelValue="false" disabled />
|
<NychSwitch :model-value="false" disabled />
|
||||||
<NychToggleSwitch :modelValue="true" disabled />
|
<NychSwitch :model-value="true" disabled />
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
|
|
@ -60,14 +57,14 @@ export const Disabled: Story = {
|
||||||
|
|
||||||
export const WithLabel: Story = {
|
export const WithLabel: Story = {
|
||||||
render: () => ({
|
render: () => ({
|
||||||
components: { NychToggleSwitch },
|
components: { NychSwitch },
|
||||||
setup() {
|
setup() {
|
||||||
const nocturnal = ref(true)
|
const nocturnal = ref(true)
|
||||||
return { nocturnal }
|
return { nocturnal }
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<label style="display: flex; align-items: center; gap: 0.75rem; font-family: var(--font-sans); color: var(--text-body); cursor: pointer;">
|
<label style="display: flex; align-items: center; gap: 0.75rem; font-family: var(--font-sans); color: var(--text-body); cursor: pointer;">
|
||||||
<NychToggleSwitch v-model="nocturnal" />
|
<NychSwitch v-model="nocturnal" />
|
||||||
{{ nocturnal ? 'Hades (night)' : 'Apollo (day)' }}
|
{{ nocturnal ? 'Hades (night)' : 'Apollo (day)' }}
|
||||||
</label>
|
</label>
|
||||||
`,
|
`,
|
||||||
Loading…
Reference in a new issue