feat: 新增 FaCheckboxGrou 组件,并重构 FaCheckbox API

This commit is contained in:
Hooray
2026-04-29 16:15:10 +08:00
parent c0bc31f7f4
commit 5ba6d08df3
21 changed files with 704 additions and 65 deletions
@@ -53,6 +53,14 @@ const routes: RouteRecordRaw = {
title: '复选框',
},
},
{
path: 'checkbox_group',
name: 'componentExampleCheckboxGroup',
component: () => import('@/views/component_example/checkbox_group/index.vue'),
meta: {
title: '复选框组',
},
},
{
path: 'collapsible',
name: 'componentExampleCollapsible',
+2
View File
@@ -24,6 +24,7 @@ declare module 'vue' {
FaButtonGroup: typeof import('@fantastic-admin/components')['FaButtonGroup']
FaCard: typeof import('@fantastic-admin/components')['FaCard']
FaCheckbox: typeof import('@fantastic-admin/components')['FaCheckbox']
FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
@@ -76,6 +77,7 @@ declare global {
const FaButtonGroup: typeof import('@fantastic-admin/components')['FaButtonGroup']
const FaCard: typeof import('@fantastic-admin/components')['FaCard']
const FaCheckbox: typeof import('@fantastic-admin/components')['FaCheckbox']
const FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
const FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
const FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
const FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
+1 -1
View File
@@ -5,7 +5,7 @@ const checked = ref(false)
<template>
<div class="flex gap-4">
<FaCheckbox v-model="checked">
Checkbox
同意协议
</FaCheckbox>
</div>
</template>
+1 -1
View File
@@ -4,6 +4,6 @@ const checked = ref<'indeterminate' | boolean>('indeterminate')
<template>
<FaCheckbox v-model="checked">
Checkbox
部分选中
</FaCheckbox>
</template>
View File
@@ -0,0 +1,26 @@
<script setup lang="ts">
const value = ref(['music'])
const options = [
{ label: '阅读', value: 'reading' },
{ label: '音乐', value: 'music' },
{ label: '运动', value: 'sports' },
]
const currentText = computed(() =>
options
.filter(option => value.value.includes(option.value))
.map(option => option.label)
.join('、'),
)
</script>
<template>
<div class="gap-4 grid">
<FaCheckboxGroup v-model="value" :options="options" />
<FaCheckboxGroup v-model="value" :options="options" class="flex" />
<div class="text-sm text-muted-foreground">
当前值:{{ currentText }}
</div>
</div>
</template>
@@ -0,0 +1,38 @@
<script setup lang="ts">
const value = ref(['dashboard', 'report'])
const options = [
{
label: '看板订阅',
value: 'dashboard',
description: '每天 8:00 推送核心经营指标。',
},
{
label: '周报摘要',
value: 'report',
description: '每周一汇总关键数据变化。',
},
{
label: '系统告警',
value: 'alarm',
description: '异常波动时立即通知处理人。',
disabled: true,
},
]
const currentText = computed(() =>
options
.filter(option => value.value.includes(option.value))
.map(option => option.label)
.join('、'),
)
</script>
<template>
<div class="gap-4 grid">
<FaCheckboxGroup v-model="value" :options="options" />
<div class="text-sm text-muted-foreground">
当前值:{{ currentText }}
</div>
</div>
</template>
@@ -0,0 +1,68 @@
<script setup lang="ts">
const value = ref(['focus', 'balanced'])
const options = [
{
label: '专注模式',
value: 'focus',
description: '突出主任务,弱化辅助信息,适合录入和审批场景。',
},
{
label: '平衡模式',
value: 'balanced',
description: '信息密度与可读性保持平衡,适合作为默认配置。',
},
{
label: '高密度模式',
value: 'dense',
description: '在大屏中同时承载更多信息,适合运营与监控看板。',
},
]
const currentText = computed(() =>
options
.filter(option => value.value.includes(option.value))
.map(option => option.label)
.join('、'),
)
</script>
<template>
<FaCheckboxGroup
v-model="value"
:options="options"
class="gap-2 md:grid-cols-3"
option-class="rounded-xl border border-transparent px-1 py-1"
>
<template #option="{ option, checked, disabled }">
<div
class="px-4 py-3 border rounded-xl flex gap-3 w-full transition-colors items-start justify-between" :class="[
checked ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/40',
disabled && 'opacity-60',
]"
>
<div class="gap-1 grid min-w-0">
<div class="text-sm font-medium truncate">
{{ option.label }}
</div>
<div
v-if="option.description"
class="text-xs text-muted-foreground leading-5"
>
{{ option.description }}
</div>
</div>
<span
class="text-xs font-medium shrink-0" :class="[
checked ? 'text-primary' : 'text-muted-foreground',
]"
>
{{ checked ? '已选中' : '可选择' }}
</span>
</div>
</template>
</FaCheckboxGroup>
<div class="text-sm text-muted-foreground">
当前模式:{{ currentText }}
</div>
</template>
@@ -0,0 +1,33 @@
<script setup lang="ts">
const value = ref(['dashboard'])
const options = [
{ label: '看板订阅', value: 'dashboard' },
{ label: '周报摘要', value: 'report' },
{ label: '系统告警', value: 'alarm' },
]
const currentText = computed(() =>
options
.filter(option => value.value.includes(option.value))
.map(option => option.label)
.join('、'),
)
</script>
<template>
<div class="gap-4 grid">
<FaCheckboxGroup
v-model="value"
:options="options"
:min="1"
:max="2"
/>
<div class="text-sm text-muted-foreground">
当前值:{{ currentText }}
</div>
<div class="text-xs text-muted-foreground leading-5">
已选数量小于等于 1 时,已选项会被禁用;已选数量大于等于 2 时,未选项会被禁用。
</div>
</div>
</template>
@@ -0,0 +1,24 @@
<script setup lang="ts">
import Demo1 from './_demo1.vue'
import Demo2 from './_demo2.vue'
import Demo3 from './_demo3.vue'
import Demo4 from './_demo4.vue'
</script>
<template>
<div>
<FaPageHeader title="复选框组" description="FaCheckboxGroup" />
<FaPageMain main-class="p-4">
<Demo1 />
</FaPageMain>
<FaPageMain title="带描述和禁用态" main-class="p-4">
<Demo2 />
</FaPageMain>
<FaPageMain title="自定义选项内容" main-class="p-4">
<Demo3 />
</FaPageMain>
<FaPageMain title="最少 / 最多选择" main-class="p-4">
<Demo4 />
</FaPageMain>
</div>
</template>
@@ -11,6 +11,7 @@ const options = [
<template>
<div class="gap-4 grid">
<FaRadioGroup v-model="value" :options="options" />
<FaRadioGroup v-model="value" :options="options" class="flex" />
<div class="text-sm text-muted-foreground">
当前值:{{ value }}
</div>
@@ -32,7 +32,6 @@ const currentLabel = computed(() =>
:options="options"
class="gap-4 md:grid-cols-3"
option-class="rounded-xl border border-transparent px-1 py-1"
item-class="mt-1"
>
<template #option="{ option, checked, disabled }">
<div
+1
View File
@@ -5,6 +5,7 @@ declare module 'vue' {
FaButtonGroup: typeof import('./index')['FaButtonGroup']
FaCard: typeof import('./index')['FaCard']
FaCheckbox: typeof import('./index')['FaCheckbox']
FaCheckboxGroup: typeof import('./index')['FaCheckboxGroup']
FaCollapsible: typeof import('./index')['FaCollapsible']
FaContextMenu: typeof import('./index')['FaContextMenu']
FaDivider: typeof import('./index')['FaDivider']
+1
View File
@@ -2,6 +2,7 @@ export { default as FaAvatar } from './src/avatar/index.vue'
export { default as FaButtonGroup } from './src/button-group/index.vue'
export { default as FaButton } from './src/button/index.vue'
export { default as FaCard } from './src/card/index.vue'
export { default as FaCheckboxGroup } from './src/checkbox-group/index.vue'
export { default as FaCheckbox } from './src/checkbox/index.vue'
export { default as FaCollapsible } from './src/collapsible/index.vue'
export { default as FaContextMenu } from './src/context-menu/index.vue'
+1
View File
@@ -10,6 +10,7 @@ const COMPONENT_NAMES = [
'FaButtonGroup',
'FaCard',
'FaCheckbox',
'FaCheckboxGroup',
'FaCollapsible',
'FaContextMenu',
'FaDivider',
@@ -0,0 +1,227 @@
# FaCheckboxGroup 复选框组
基于选项数组的复选框组组件,支持描述文案、自定义布局样式和选项插槽扩展。
## 使用场景
- 表单中的多选配置项
- 多标签偏好选择
- 权限点批量选择
- 带说明文案的多选列表
- 需要卡片式多选交互的场景
## Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `options` | `CheckboxGroupOption[]` | **必需** | 复选框组选项数据 |
| `disabled` | `boolean` | `false` | 是否整体禁用 |
| `min` | `number` | - | 最少选中数量,达到后已选项会被禁用 |
| `max` | `number` | - | 最多选中数量,达到后未选项会被禁用 |
| `dir` | `'ltr' \| 'rtl'` | - | 文本方向 |
| `class` | `HTMLAttributes['class']` | - | 组容器类名 |
| `optionClass` | `HTMLAttributes['class']` | - | 每个选项外层容器类名 |
| `itemClass` | `HTMLAttributes['class']` | - | 复选框本体类名 |
| `labelClass` | `HTMLAttributes['class']` | - | 标签内容类名 |
### CheckboxGroupOption 接口
```ts
interface CheckboxGroupOption {
label: string
value: AcceptableValue
description?: string
disabled?: boolean
id?: string
}
```
## Slots
| 名称 | 说明 |
|------|------|
| `option` | 自定义选项内容,slot props: `{ option, checked, disabled, id }` |
## Events
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| `change` | 选中值变化时触发 | `value: AcceptableValue[]` |
通过 `v-model` 实现双向绑定。
## 示例
### 基础用法
```vue
<script setup lang="ts">
const value = ref(['music'])
const options = [
{ label: '阅读', value: 'reading' },
{ label: '音乐', value: 'music' },
{ label: '运动', value: 'sports' },
]
</script>
<template>
<FaCheckboxGroup v-model="value" :options="options" />
</template>
```
### 带描述文案
```vue
<script setup lang="ts">
const value = ref(['dashboard', 'report'])
const options = [
{
label: '看板订阅',
value: 'dashboard',
description: '每天 8:00 推送核心经营指标。',
},
{
label: '周报摘要',
value: 'report',
description: '每周一汇总关键数据变化。',
},
{
label: '系统告警',
value: 'alarm',
description: '异常波动时立即通知处理人。',
},
]
</script>
<template>
<FaCheckboxGroup v-model="value" :options="options" />
</template>
```
### 禁用选项
```vue
<script setup lang="ts">
const value = ref(['basic'])
const options = [
{ label: '基础能力', value: 'basic' },
{ label: '审批流', value: 'approval' },
{ label: '审计日志', value: 'audit', disabled: true },
]
</script>
<template>
<FaCheckboxGroup v-model="value" :options="options" />
</template>
```
### 多列排列
```vue
<script setup lang="ts">
const value = ref(['desktop', 'tablet'])
const options = [
{ label: '桌面端', value: 'desktop' },
{ label: '平板端', value: 'tablet' },
{ label: '移动端', value: 'mobile' },
]
</script>
<template>
<FaCheckboxGroup
v-model="value"
:options="options"
class="gap-4 md:grid-cols-3"
/>
</template>
```
### 最少 / 最多选择
```vue
<script setup lang="ts">
const value = ref(['dashboard'])
const options = [
{ label: '看板订阅', value: 'dashboard' },
{ label: '周报摘要', value: 'report' },
{ label: '系统告警', value: 'alarm' },
]
</script>
<template>
<FaCheckboxGroup
v-model="value"
:options="options"
:min="1"
:max="2"
/>
</template>
```
### 自定义选项内容
```vue
<script setup lang="ts">
const value = ref(['focus', 'balanced'])
const options = [
{
label: '专注模式',
value: 'focus',
description: '突出主任务,弱化辅助信息。',
},
{
label: '平衡模式',
value: 'balanced',
description: '信息密度与可读性保持平衡。',
},
{
label: '高密度模式',
value: 'dense',
description: '适合大屏运营与监控看板。',
},
]
</script>
<template>
<FaCheckboxGroup
v-model="value"
:options="options"
class="gap-4 md:grid-cols-3"
>
<template #option="{ option, checked }">
<div
:class="[
'flex w-full items-start justify-between gap-3 rounded-xl border px-4 py-3',
checked ? 'border-primary bg-primary/5' : 'border-border',
]"
>
<div class="min-w-0">
<div class="truncate font-medium">{{ option.label }}</div>
<div v-if="option.description" class="text-xs text-muted-foreground leading-5">
{{ option.description }}
</div>
</div>
<span class="text-xs" :class="checked ? 'text-primary' : 'text-muted-foreground'">
{{ checked ? '已选中' : '可选择' }}
</span>
</div>
</template>
</FaCheckboxGroup>
</template>
```
## 注意事项
1. **数据驱动**:组选项必须通过 `options` 传入,不再使用多个 `FaCheckbox` 手工拼接。
2. **v-model 类型**:组件通过 `v-model` 管理选中值数组。
3. **禁用优先级**:组件整体 `disabled` 为 `true` 时,会覆盖单项可用状态。
4. **数量限制**:当选中数量小于等于 `min` 时,已选项会被禁用;当选中数量大于等于 `max` 时,未选项会被禁用。
5. **布局控制**:通过 `class` 和 `optionClass` 自定义多列或卡片式排列,不再提供 `orientation`。
6. **描述文案布局**:存在 `description` 时,选项会自动切换为顶部对齐布局。
7. **自定义插槽**:使用 `option` 插槽后,默认复选框本体会隐藏,需要由插槽内容自行承担完整视觉表现。
@@ -0,0 +1,201 @@
<script setup lang="ts">
import type { AcceptableValue } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '../../utils'
import { Checkbox } from '../checkbox/checkbox'
import { Label } from '../label/label'
interface CheckboxGroupOption {
label: string
value: AcceptableValue
description?: string
disabled?: boolean
id?: string
}
defineOptions({
name: 'FaCheckboxGroup',
})
const props = defineProps<{
options: CheckboxGroupOption[]
disabled?: boolean
min?: number
max?: number
dir?: 'ltr' | 'rtl'
class?: HTMLAttributes['class']
optionClass?: HTMLAttributes['class']
itemClass?: HTMLAttributes['class']
labelClass?: HTMLAttributes['class']
}>()
const emit = defineEmits<{
change: [value: AcceptableValue[]]
}>()
const slots = defineSlots<{
option?: (props: {
option: CheckboxGroupOption
checked: boolean
disabled: boolean
id: string
}) => any
}>()
const value = defineModel<AcceptableValue[]>({
default: () => [],
})
const baseId = useId()
const checkedCount = computed(() => value.value.length)
watch(value, (newValue) => {
emit('change', newValue)
})
function getOptionId(option: CheckboxGroupOption, index: number) {
return option.id || `${baseId}-${index}`
}
function getOptionKey(option: CheckboxGroupOption, index: number) {
if (option.id) {
return option.id
}
return typeof option.value === 'string' || typeof option.value === 'number'
? option.value
: index
}
function isOptionDisabled(option: CheckboxGroupOption) {
if (props.disabled || option.disabled) {
return true
}
const checked = isOptionChecked(option)
if (checked && props.min !== undefined && checkedCount.value <= props.min) {
return true
}
if (!checked && props.max !== undefined && checkedCount.value >= props.max) {
return true
}
return false
}
function findOptionIndex(option: CheckboxGroupOption) {
return value.value.findIndex(item => Object.is(item, option.value))
}
function isOptionChecked(option: CheckboxGroupOption) {
return findOptionIndex(option) > -1
}
function updateOptionChecked(option: CheckboxGroupOption, checked: boolean) {
const nextValue = [...value.value]
const optionIndex = nextValue.findIndex(item => Object.is(item, option.value))
if (checked) {
if (optionIndex === -1) {
nextValue.push(option.value)
}
}
else if (optionIndex > -1) {
nextValue.splice(optionIndex, 1)
}
value.value = nextValue
}
function handleOptionModelValueChange(option: CheckboxGroupOption, checked: boolean | 'indeterminate') {
if (isOptionDisabled(option)) {
return
}
updateOptionChecked(option, checked === true)
}
function handleCustomOptionClick(option: CheckboxGroupOption) {
if (!slots.option || isOptionDisabled(option)) {
return
}
updateOptionChecked(option, !isOptionChecked(option))
}
function handleCustomOptionKeydown(option: CheckboxGroupOption) {
if (!slots.option || isOptionDisabled(option)) {
return
}
updateOptionChecked(option, !isOptionChecked(option))
}
</script>
<template>
<div
role="group"
:dir="dir"
:class="cn('grid gap-3', props.class)"
>
<div
v-for="(option, index) in props.options"
:key="getOptionKey(option, index)"
:tabindex="slots.option && !isOptionDisabled(option) ? 0 : undefined"
:role="slots.option ? 'checkbox' : undefined"
:aria-checked="slots.option ? `${isOptionChecked(option)}` : undefined"
:aria-disabled="slots.option ? `${isOptionDisabled(option)}` : undefined"
:class="
cn(
'flex gap-2',
option.description ? 'items-start' : 'items-center',
slots.option && 'outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] rounded-xl',
props.optionClass,
)
"
@click="handleCustomOptionClick(option)"
@keydown.enter.prevent="handleCustomOptionKeydown(option)"
@keydown.space.prevent="handleCustomOptionKeydown(option)"
>
<Checkbox
v-show="!slots.option"
:id="getOptionId(option, index)"
:model-value="isOptionChecked(option)"
:disabled="isOptionDisabled(option)"
:class="props.itemClass"
@update:model-value="handleOptionModelValueChange(option, $event)"
/>
<Label
:for="slots.option ? undefined : getOptionId(option, index)"
:class="
cn(
'min-w-0 flex-1 cursor-pointer gap-0',
option.description ? 'items-start' : 'items-center',
isOptionDisabled(option) && 'cursor-not-allowed opacity-60',
props.labelClass,
)
"
>
<slot
:id="getOptionId(option, index)"
name="option"
:option="option"
:checked="isOptionChecked(option)"
:disabled="isOptionDisabled(option)"
>
<div class="gap-1 grid min-w-0">
<span class="truncate">{{ option.label }}</span>
<span
v-if="option.description"
class="text-xs text-muted-foreground leading-5 font-normal"
>
{{ option.description }}
</span>
</div>
</slot>
</Label>
</div>
</div>
</template>
+46 -58
View File
@@ -1,20 +1,23 @@
# FaCheckbox 复选框
简洁的复选框组件,支持双向绑定和标签内容插槽。
单个复选框组件,支持普通选中、禁用和半选状态。
## 使用场景
- 表单中的同意协议选项
- 多选过滤条件
- 设置选项开关
- 列表项批量选择
- 单个布尔开关配置
- 树形结构中的半选状态展示
- 单项功能启用/禁用
## Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `id` | `string` | 自动生成 | 复选框 id |
| `disabled` | `boolean` | `false` | 是否禁用 |
| `class` | `HTMLAttributes['class']` | - | 自定义 CSS 类 |
| `class` | `HTMLAttributes['class']` | - | 外层容器类名 |
| `itemClass` | `HTMLAttributes['class']` | - | 复选框本体类名 |
| `labelClass` | `HTMLAttributes['class']` | - | 标签内容类名 |
## Slots
@@ -24,7 +27,11 @@
## Events
通过 `v-model` 双向绑定,无需单独事件
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| `change` | 选中值变化时触发 | `value: boolean \| 'indeterminate' \| undefined` |
通过 `v-model` 实现双向绑定。
## 示例
@@ -36,43 +43,41 @@ const checked = ref(false)
</script>
<template>
<FaCheckbox v-model="checked">同意协议</FaCheckbox>
<FaCheckbox v-model="checked">
我已阅读并同意服务条款
</FaCheckbox>
</template>
```
### 基础复选框
### 半选状态
```vue
<script setup lang="ts">
const checked = ref(false)
const checked = ref<boolean | 'indeterminate'>('indeterminate')
</script>
<template>
<FaCheckbox v-model="checked">选项</FaCheckbox>
<FaCheckbox v-model="checked">
部分选中
</FaCheckbox>
</template>
```
### 禁用状态
```vue
<template>
<FaCheckbox disabled>禁用选项</FaCheckbox>
<FaCheckbox v-model="checked" disabled>禁用选中</FaCheckbox>
</template>
```
### 复选框组
```vue
<script setup lang="ts">
const hobbies = ref(['reading', 'music'])
const checked = ref(true)
</script>
<template>
<div class="flex flex-col gap-2">
<FaCheckbox v-model="hobbies" value="reading">阅读</FaCheckbox>
<FaCheckbox v-model="hobbies" value="music">音乐</FaCheckbox>
<FaCheckbox v-model="hobbies" value="sports">运动</FaCheckbox>
<FaCheckbox disabled>
禁用未选中
</FaCheckbox>
<FaCheckbox v-model="checked" disabled>
禁用已选中
</FaCheckbox>
</div>
</template>
```
@@ -80,6 +85,10 @@ const hobbies = ref(['reading', 'music'])
### 无标签复选框
```vue
<script setup lang="ts">
const checked = ref(false)
</script>
<template>
<FaCheckbox v-model="checked" />
</template>
@@ -87,48 +96,27 @@ const hobbies = ref(['reading', 'music'])
### 自定义样式
```vue
<template>
<FaCheckbox v-model="checked" class="gap-4">
<span class="text-primary font-medium">自定义样式</span>
</FaCheckbox>
</template>
```
### 表单中的使用
```vue
<script setup lang="ts">
const form = reactive({
agreeTerms: false,
subscribe: true,
})
function handleSubmit() {
if (!form.agreeTerms) {
return
}
console.log('提交表单')
}
const checked = ref(false)
</script>
<template>
<form @submit.prevent="handleSubmit">
<FaCheckbox v-model="form.agreeTerms">
我已阅读并同意服务条款
</FaCheckbox>
<FaCheckbox v-model="form.subscribe">
订阅我们的新闻通讯
</FaCheckbox>
<FaButton type="submit" :disabled="!form.agreeTerms">提交</FaButton>
</form>
<FaCheckbox
v-model="checked"
class="gap-3"
item-class="size-5 rounded-md"
label-class="text-primary font-medium"
>
自定义样式
</FaCheckbox>
</template>
```
## 注意事项
1. **v-model 绑定**:支持单个 boolean 值和数组(多选)两种模式
2. **标签点击**:点击标签文字也能触发复选框状态切换
3. **空标签隐藏**:当不提供默认插槽内容时,标签元素会自动隐藏
4. **禁用状态**:禁用状态下复选框和标签都无法点击
5. **无障碍支持**:原生支持键盘操作(空格键切换状态)
1. **单个使用**:`FaCheckbox` 只负责单个复选框,不承担组选项逻辑。
2. **v-model 类型**:支持 `boolean` 和 `'indeterminate'` 两种状态值。
3. **标签点击**:点击标签文字也能触发复选框状态切换。
4. **空标签隐藏**:当不提供默认插槽内容时,标签元素会自动隐藏。
5. **禁用状态**:禁用状态下复选框和标签都无法点击。
View File
View File
+25 -4
View File
@@ -2,6 +2,7 @@
import type { CheckboxRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '../../utils'
import { Label } from '../label/label'
import { Checkbox } from './checkbox'
defineOptions({
@@ -9,20 +10,40 @@ defineOptions({
})
const props = defineProps<{
id?: string
disabled?: boolean
class?: HTMLAttributes['class']
itemClass?: HTMLAttributes['class']
labelClass?: HTMLAttributes['class']
}>()
const emit = defineEmits<{
change: [value: CheckboxRootProps['modelValue'] | undefined]
}>()
const value = defineModel<CheckboxRootProps['modelValue']>()
const generatedId = useId()
const checkboxId = computed(() => props.id || generatedId)
const id = useId()
watch(value, (newValue) => {
emit('change', newValue)
})
</script>
<template>
<div :class="cn('flex-center-start gap-2', props.class)">
<Checkbox :id v-model="value" :disabled />
<label :for="id" class="text-sm cursor-pointer empty:hidden">
<Checkbox :id="checkboxId" v-model="value" :disabled="disabled" :class="props.itemClass" />
<Label
:for="checkboxId"
:class="
cn(
'text-sm cursor-pointer empty:hidden',
props.disabled && 'cursor-not-allowed opacity-60',
props.labelClass,
)
"
>
<slot />
</label>
</Label>
</div>
</template>