@fiscozen/checkbox 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { FzTooltipProps } from '@fiscozen/tooltip';
1
+ import { FzTooltipProps, FzTooltipStatus } from '@fiscozen/tooltip';
2
2
 
3
3
  /**
4
4
  * Props for the FzCheckbox component.
@@ -136,7 +136,7 @@ export type FzCheckboxGroupProps = {
136
136
  * }
137
137
  * ]
138
138
  */
139
- options: ParentCheckbox[];
139
+ options?: ParentCheckbox[];
140
140
  /**
141
141
  * Applies emphasis styling to all checkboxes in the group.
142
142
  * Icons turn blue when checked.
@@ -190,3 +190,72 @@ export type ParentCheckbox = ChildCheckbox & {
190
190
  * Inherits all FzCheckboxProps except 'size' which is controlled by the parent group.
191
191
  */
192
192
  export type ChildCheckbox = Omit<FzCheckboxProps, "size">;
193
+ /**
194
+ * Shared props for all FzCheckboxCard variants.
195
+ */
196
+ type FzCheckboxCardBaseProps = Omit<FzCheckboxProps, "standalone" | "indeterminate" | "ariaOwns" | "checkboxId" | "value" | "tooltip"> & {
197
+ /**
198
+ * Value associated with the card when used in array v-model.
199
+ * Falls back to label if not provided.
200
+ */
201
+ value?: string | number;
202
+ /**
203
+ * Primary title text displayed in the card.
204
+ */
205
+ title: string;
206
+ /**
207
+ * Optional secondary description text below the title.
208
+ */
209
+ subtitle?: string;
210
+ /**
211
+ * Alt text for the card image.
212
+ */
213
+ imageAlt?: string;
214
+ /**
215
+ * Text to display in the tooltip.
216
+ */
217
+ tooltip?: string;
218
+ /**
219
+ * Status of the tooltip (determines color and icon).
220
+ */
221
+ tooltipStatus?: FzTooltipStatus;
222
+ /**
223
+ * Controls whether the checkbox icon is shown inside the card.
224
+ *
225
+ * @default true
226
+ */
227
+ hasCheckbox?: boolean;
228
+ /**
229
+ * Group name for the checkbox, used for form submission.
230
+ */
231
+ name?: string;
232
+ };
233
+ /**
234
+ * Horizontal card layout: image left, text right (compact).
235
+ * Image is optional.
236
+ *
237
+ * @default variant is 'horizontal' when omitted
238
+ */
239
+ type FzCheckboxCardHorizontal = FzCheckboxCardBaseProps & {
240
+ variant?: "horizontal";
241
+ imageUrl?: string;
242
+ };
243
+ /**
244
+ * Vertical card layout: image top, text bottom (full-width image).
245
+ * Image is required — the vertical layout is designed around the image.
246
+ */
247
+ type FzCheckboxCardVertical = FzCheckboxCardBaseProps & {
248
+ variant: "vertical";
249
+ imageUrl: string;
250
+ };
251
+ /**
252
+ * Props for the FzCheckboxCard component.
253
+ *
254
+ * A card-style checkbox with title, subtitle, optional image and tooltip.
255
+ * Uses a discriminated union on `variant` to enforce that the vertical layout
256
+ * always includes an image (since the layout is designed around it).
257
+ *
258
+ * Uses array v-model for multi-select.
259
+ */
260
+ export type FzCheckboxCardProps = FzCheckboxCardHorizontal | FzCheckboxCardVertical;
261
+ export {};
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .fz-icon-button-wrapper[data-v-b4be112d]>button{gap:0!important;min-width:0!important}.fz-icon-button-wrapper--backoffice[data-v-b4be112d]>button{padding-left:5px;padding-right:5px}.fz-icon-button-wrapper--frontoffice[data-v-b4be112d]>button{padding-left:11px;padding-right:11px}.fz-container[data-v-8c40daeb]{display:flex}.fz-container--vertical[data-v-8c40daeb]{flex-direction:column}.fz-container--horizontal[data-v-8c40daeb]{flex-direction:row;flex-wrap:nowrap}.fz-container.align-items-start[data-v-8c40daeb]{align-items:flex-start}.fz-container.align-items-center[data-v-8c40daeb]{align-items:center}.fz-container.align-items-end[data-v-8c40daeb]{align-items:flex-end}.fz-container.align-items-stretch[data-v-8c40daeb]{align-items:stretch}.fz-container.align-items-baseline[data-v-8c40daeb]{align-items:baseline}.fz-container--horizontal.layout-expand-first[data-v-8c40daeb]>*:first-child{flex-grow:1}.fz-container--horizontal.layout-expand-all[data-v-8c40daeb]>*{flex-grow:1}.fz-container--horizontal.layout-space-between[data-v-8c40daeb]{justify-content:space-between}.fz-container--horizontal.layout-expand-last[data-v-8c40daeb]>*:last-child{flex-grow:1}.fz-container--vertical.gap-main-content-sm[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--main-content-sm, 32px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-main-content-base[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--main-content-base, 48px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-main-content-lg[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--main-content-lg, 64px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-section-content-none[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--section-content-none, 0px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-section-content-xs[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--section-content-xs, 8px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-section-content-sm[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--section-content-sm, 16px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-section-content-base[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--section-content-base, 24px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-section-content-lg[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--section-content-lg, 32px)) + var(--paragraph-gap, 8px))}.fz-container--horizontal[data-v-8c40daeb]>p+p{margin-top:0}.fz-button-group[data-v-79dd8b6f]>*:nth-child(1):nth-last-child(2),.fz-button-group[data-v-79dd8b6f]>*:nth-child(1):nth-last-child(2)~*{flex-basis:50%;flex-grow:0;flex-shrink:1}.fz-button-group[data-v-79dd8b6f]>*:nth-child(1):nth-last-child(3),.fz-button-group[data-v-79dd8b6f]>*:nth-child(1):nth-last-child(3)~*{flex-basis:33.333%;flex-grow:0;flex-shrink:1}.fz-button-group[data-v-79dd8b6f]>.fz-icon-button-wrapper{flex-basis:initial!important;flex-grow:0!important;flex-shrink:0!important}.fz__tooltip__text[data-v-94597a6c]{overflow-wrap:anywhere}.bg-semantic-info-50[data-v-f72b7e4a]{background-color:#f3f7ff}.bg-semantic-error-50[data-v-f72b7e4a]{background-color:#fef3f3}.bg-semantic-warning-50[data-v-f72b7e4a]{background-color:#fff8f3}.bg-semantic-success-50[data-v-f72b7e4a]{background-color:#f2f8f6}.fz-hidden-input[data-v-2b198cc9]{opacity:0;margin:0;height:0;border:0 none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.group[data-error]:hover:not([data-disabled]) .peer~label>div[data-v-2b198cc9]{color:var(--semantic-error-300)!important}.group[data-error]:hover:not([data-disabled]) .peer~label[data-v-2b198cc9]{color:var(--semantic-error-300)!important}.group[data-emphasis]:hover:not([data-disabled]):not([data-error]) .peer~label>div[data-v-2b198cc9]{color:var(--blue-600)!important}.group:hover:not([data-emphasis]):not([data-error]):not([data-disabled]) .peer:checked~label>div[data-v-2b198cc9]{color:var(--core-black)!important}.group:hover:not([data-emphasis]):not([data-error]):not([data-disabled]) .peer:indeterminate~label>div[data-v-2b198cc9]{color:var(--core-black)!important}.group:hover:not([data-emphasis]):not([data-error]):not([data-disabled]) .peer:not(:checked):not(:indeterminate)~label>div[data-v-2b198cc9]{color:var(--blue-600)!important}
1
+ .fz-icon-button-wrapper[data-v-b4be112d]>button{gap:0!important;min-width:0!important}.fz-icon-button-wrapper--backoffice[data-v-b4be112d]>button{padding-left:5px;padding-right:5px}.fz-icon-button-wrapper--frontoffice[data-v-b4be112d]>button{padding-left:11px;padding-right:11px}.fz-container[data-v-8c40daeb]{display:flex}.fz-container--vertical[data-v-8c40daeb]{flex-direction:column}.fz-container--horizontal[data-v-8c40daeb]{flex-direction:row;flex-wrap:nowrap}.fz-container.align-items-start[data-v-8c40daeb]{align-items:flex-start}.fz-container.align-items-center[data-v-8c40daeb]{align-items:center}.fz-container.align-items-end[data-v-8c40daeb]{align-items:flex-end}.fz-container.align-items-stretch[data-v-8c40daeb]{align-items:stretch}.fz-container.align-items-baseline[data-v-8c40daeb]{align-items:baseline}.fz-container--horizontal.layout-expand-first[data-v-8c40daeb]>*:first-child{flex-grow:1}.fz-container--horizontal.layout-expand-all[data-v-8c40daeb]>*{flex-grow:1}.fz-container--horizontal.layout-space-between[data-v-8c40daeb]{justify-content:space-between}.fz-container--horizontal.layout-expand-last[data-v-8c40daeb]>*:last-child{flex-grow:1}.fz-container--vertical.gap-main-content-sm[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--main-content-sm, 32px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-main-content-base[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--main-content-base, 48px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-main-content-lg[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--main-content-lg, 64px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-section-content-none[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--section-content-none, 0px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-section-content-xs[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--section-content-xs, 8px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-section-content-sm[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--section-content-sm, 16px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-section-content-base[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--section-content-base, 24px)) + var(--paragraph-gap, 8px))}.fz-container--vertical.gap-section-content-lg[data-v-8c40daeb]>p+p{margin-top:calc((0px - var(--section-content-lg, 32px)) + var(--paragraph-gap, 8px))}.fz-container--horizontal[data-v-8c40daeb]>p+p{margin-top:0}.fz-button-group[data-v-79dd8b6f]>*:nth-child(1):nth-last-child(2),.fz-button-group[data-v-79dd8b6f]>*:nth-child(1):nth-last-child(2)~*{flex-basis:50%;flex-grow:0;flex-shrink:1}.fz-button-group[data-v-79dd8b6f]>*:nth-child(1):nth-last-child(3),.fz-button-group[data-v-79dd8b6f]>*:nth-child(1):nth-last-child(3)~*{flex-basis:33.333%;flex-grow:0;flex-shrink:1}.fz-button-group[data-v-79dd8b6f]>.fz-icon-button-wrapper{flex-basis:initial!important;flex-grow:0!important;flex-shrink:0!important}.fz__tooltip__text[data-v-94597a6c]{overflow-wrap:anywhere}.bg-semantic-info-50[data-v-f72b7e4a]{background-color:#f3f7ff}.bg-semantic-error-50[data-v-f72b7e4a]{background-color:#fef3f3}.bg-semantic-warning-50[data-v-f72b7e4a]{background-color:#fff8f3}.bg-semantic-success-50[data-v-f72b7e4a]{background-color:#f2f8f6}.fz-hidden-input[data-v-2b198cc9]{opacity:0;margin:0;height:0;border:0 none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.group[data-error]:hover:not([data-disabled]) .peer~label>div[data-v-2b198cc9]{color:var(--semantic-error-300)!important}.group[data-error]:hover:not([data-disabled]) .peer~label[data-v-2b198cc9]{color:var(--semantic-error-300)!important}.group[data-emphasis]:hover:not([data-disabled]):not([data-error]) .peer~label>div[data-v-2b198cc9]{color:var(--blue-600)!important}.group:hover:not([data-emphasis]):not([data-error]):not([data-disabled]) .peer:checked~label>div[data-v-2b198cc9]{color:var(--core-black)!important}.group:hover:not([data-emphasis]):not([data-error]):not([data-disabled]) .peer:indeterminate~label>div[data-v-2b198cc9]{color:var(--core-black)!important}.group:hover:not([data-emphasis]):not([data-error]):not([data-disabled]) .peer:not(:checked):not(:indeterminate)~label>div[data-v-2b198cc9]{color:var(--blue-600)!important}.fz-hidden-input[data-v-d850ec45]{opacity:0;margin:0;height:0;border:0 none;-webkit-appearance:none;-moz-appearance:none;appearance:none}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiscozen/checkbox",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Design System Checkbox and Checkbox Group component",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -9,8 +9,8 @@
9
9
  "dependencies": {
10
10
  "@fiscozen/alert": "1.0.1",
11
11
  "@fiscozen/badge": "1.0.1",
12
- "@fiscozen/composables": "1.0.1",
13
- "@fiscozen/tooltip": "1.0.3"
12
+ "@fiscozen/tooltip": "1.0.3",
13
+ "@fiscozen/composables": "1.0.1"
14
14
  },
15
15
  "peerDependencies": {
16
16
  "tailwindcss": "^3.4.1",
@@ -0,0 +1,209 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from "vue";
3
+ import { FzCheckboxCardProps } from "./types";
4
+ import { generateCheckboxId } from "./utils";
5
+ import { CHECKED_SET_KEY } from "./common";
6
+ import { FzIcon, type IconVariant } from "@fiscozen/icons";
7
+ import { FzTooltip } from "@fiscozen/tooltip";
8
+
9
+ const props = withDefaults(defineProps<FzCheckboxCardProps>(), {
10
+ variant: "horizontal",
11
+ hasCheckbox: true,
12
+ });
13
+
14
+ const computedValue = computed<string | number>(
15
+ () => props.value ?? props.label,
16
+ );
17
+
18
+ const id: string = generateCheckboxId();
19
+
20
+ const model = defineModel<(string | number | boolean)[]>({
21
+ required: true,
22
+ default: [],
23
+ });
24
+
25
+ const injected = inject(CHECKED_SET_KEY, null);
26
+
27
+ const isChecked = computed<boolean>(() => {
28
+ if (model.value == null) return false;
29
+ if (injected && injected.source.value === model.value) {
30
+ return injected.set.value.has(computedValue.value);
31
+ }
32
+ return model.value.includes(computedValue.value);
33
+ });
34
+
35
+ function handleChange() {
36
+ if (props.disabled) return;
37
+ const current = model.value ?? [];
38
+ if (isChecked.value) {
39
+ model.value = current.filter((v) => v !== computedValue.value);
40
+ } else {
41
+ model.value = [...current, computedValue.value];
42
+ }
43
+ }
44
+
45
+ const CHECKBOX_ICONS = Object.freeze({
46
+ CHECKED: "square-check",
47
+ UNCHECKED: "square",
48
+ } as const);
49
+
50
+ const CHECKBOX_ICON_VARIANTS = Object.freeze({
51
+ SOLID: "fas",
52
+ REGULAR: "far",
53
+ } as const);
54
+
55
+ const computedIconName = computed<string>(() =>
56
+ isChecked.value ? CHECKBOX_ICONS.CHECKED : CHECKBOX_ICONS.UNCHECKED,
57
+ );
58
+
59
+ const computedIconVariant = computed<IconVariant>(() =>
60
+ isChecked.value ? CHECKBOX_ICON_VARIANTS.SOLID : CHECKBOX_ICON_VARIANTS.REGULAR,
61
+ );
62
+
63
+ const computedIconColor = computed<string>(() => {
64
+ if (props.disabled) return "text-grey-300";
65
+ if (props.error) return "text-semantic-error-200";
66
+ if (props.emphasis) return "text-blue-500";
67
+ return isChecked.value ? "text-blue-500" : "text-grey-400";
68
+ });
69
+
70
+ const showImage = computed(() => props.variant === "vertical" || !!props.imageUrl);
71
+
72
+ const staticInputClass: string = "w-0 h-0 peer fz-hidden-input";
73
+
74
+ const labelClass = computed(() => ({
75
+ "flex-col": props.variant === "vertical",
76
+ "flex-row": props.variant === "horizontal",
77
+ "pb-20": props.variant === "vertical" && (!isChecked.value || props.disabled),
78
+ "pb-12": props.variant === "horizontal" && (!isChecked.value || props.disabled),
79
+ "pb-[19px]": props.variant === "vertical" && isChecked.value && !props.disabled,
80
+ "pb-[11px]": props.variant === "horizontal" && isChecked.value && !props.disabled,
81
+ "pt-[11px]": isChecked.value && !props.disabled,
82
+ "gap-20": props.variant === "vertical",
83
+ "gap-12": props.variant === "horizontal",
84
+ "border-2 px-[11px] border-blue-500":
85
+ isChecked.value && !props.disabled,
86
+ "border-1 border-grey-300":
87
+ !isChecked.value || props.disabled,
88
+ "hover:bg-[#f9faff] peer-focus:outline peer-focus:bg-[#f9faff] peer-focus:outline-blue-200 peer-focus:outline-2 peer-checked:bg-[#f9faff]":
89
+ !props.disabled,
90
+ }));
91
+ </script>
92
+
93
+ <template>
94
+ <div>
95
+ <input
96
+ type="checkbox"
97
+ :id="id"
98
+ :class="[staticInputClass]"
99
+ :value="computedValue"
100
+ :disabled="disabled"
101
+ :checked="isChecked"
102
+ :name="name"
103
+ :required="required"
104
+ tabindex="0"
105
+ :aria-checked="isChecked"
106
+ :aria-required="required ? 'true' : 'false'"
107
+ :aria-invalid="error ? 'true' : 'false'"
108
+ @change="handleChange"
109
+ />
110
+ <label
111
+ :class="[
112
+ 'relative flex block rounded-lg border-solid pt-12 px-12 cursor-pointer w-full',
113
+ labelClass,
114
+ ]"
115
+ :for="id"
116
+ >
117
+ <FzIcon
118
+ v-if="hasCheckbox"
119
+ :name="computedIconName"
120
+ size="md"
121
+ :class="[
122
+ 'shrink-0',
123
+ computedIconColor,
124
+ {
125
+ 'absolute top-[23px] left-[23px]': variant === 'vertical',
126
+ 'self-start': variant === 'horizontal',
127
+ },
128
+ ]"
129
+ :variant="computedIconVariant"
130
+ aria-hidden="true"
131
+ />
132
+
133
+ <picture
134
+ v-if="showImage"
135
+ :class="[
136
+ 'rounded overflow-hidden',
137
+ {
138
+ 'shrink-0 size-[58px]': variant === 'horizontal',
139
+ 'w-full aspect-[16/9]': variant === 'vertical',
140
+ 'opacity-30': props.disabled,
141
+ },
142
+ ]"
143
+ :title="imageAlt || ''"
144
+ >
145
+ <img
146
+ :src="imageUrl"
147
+ :alt="imageAlt || ''"
148
+ class="object-cover h-full w-full"
149
+ />
150
+ </picture>
151
+
152
+ <div
153
+ class="flex flex-row w-full justify-between min-w-0"
154
+ >
155
+ <div class="justify-center flex flex-col w-full grow-0 min-w-0 gap-4">
156
+ <p
157
+ :class="[
158
+ 'font-medium break-words !m-0 !leading-[20px]',
159
+ { 'text-grey-300': props.disabled },
160
+ ]"
161
+ >
162
+ {{ title }}
163
+ </p>
164
+ <p
165
+ v-if="subtitle"
166
+ :class="[
167
+ 'font-normal text-sm mt-4 break-words !m-0 !leading-[16px]',
168
+ {
169
+ 'text-grey-300': props.disabled,
170
+ 'text-grey-500': !props.disabled,
171
+ },
172
+ ]"
173
+ >
174
+ {{ subtitle }}
175
+ </p>
176
+ </div>
177
+ <FzTooltip
178
+ v-if="tooltip"
179
+ :class="{
180
+ 'self-center': props.variant === 'horizontal',
181
+ 'self-start': props.variant === 'vertical',
182
+ 'ml-8': props.variant === 'vertical',
183
+ 'ml-12': props.variant === 'horizontal',
184
+ }"
185
+ :disabled="props.disabled"
186
+ :text="tooltip"
187
+ :status="tooltipStatus || 'neutral'"
188
+ >
189
+ <FzIcon
190
+ name="circle-info"
191
+ variant="far"
192
+ size="md"
193
+ class="text-semantic-info"
194
+ />
195
+ </FzTooltip>
196
+ </div>
197
+ </label>
198
+ </div>
199
+ </template>
200
+
201
+ <style scoped>
202
+ .fz-hidden-input {
203
+ opacity: 0;
204
+ margin: 0;
205
+ height: 0;
206
+ border: 0 none;
207
+ appearance: none;
208
+ }
209
+ </style>
@@ -35,9 +35,10 @@
35
35
  * ]"
36
36
  * />
37
37
  */
38
- import { computed, useSlots, watch } from "vue";
38
+ import { computed, provide, useSlots, watch } from "vue";
39
39
  import { FzCheckboxGroupProps } from "./types";
40
40
  import { generateGroupId } from "./utils";
41
+ import { CHECKED_SET_KEY } from "./common";
41
42
  import FzCheckboxGroupOption from "./components/FzCheckboxGroupOption.vue";
42
43
  import ErrorAlert from "./components/ErrorAlert.vue";
43
44
 
@@ -80,6 +81,9 @@ const model = defineModel<(string | number | boolean)[]>({
80
81
  default: [],
81
82
  });
82
83
 
84
+ const checkedSet = computed(() => new Set(model.value));
85
+ provide(CHECKED_SET_KEY, { source: model, set: checkedSet });
86
+
83
87
  /** Base layout for the label element */
84
88
  const staticLabeldClass: string = "flex flex-col";
85
89
 
@@ -109,6 +113,22 @@ const computedSlotContainerClass = computed<string[]>(() => [
109
113
  props.horizontal ? "flex-row" : "flex-col",
110
114
  ]);
111
115
 
116
+ /** Whether the group has options to render (options-based mode vs slot-based mode) */
117
+ const hasOptions = computed<boolean>(
118
+ () => Array.isArray(props.options) && props.options.length > 0,
119
+ );
120
+
121
+ /**
122
+ * Props passed down to child components via scoped slot.
123
+ * Used in slot-based mode (e.g., when rendering FzCheckboxCard components).
124
+ */
125
+ const controlledProps = computed(() => ({
126
+ disabled: props.disabled,
127
+ error: props.error,
128
+ emphasis: props.emphasis,
129
+ required: props.required,
130
+ }));
131
+
112
132
  /**
113
133
  * Computes the aria-describedby attribute value for the checkbox group.
114
134
  * Combines help text and error message IDs when present.
@@ -181,15 +201,18 @@ const computedAriaDescribedby = computed<string | undefined>(() => {
181
201
  Supports both simple checkboxes and parent-child hierarchies
182
202
  Key uses value if available, falls back to label for uniqueness
183
203
  -->
184
- <FzCheckboxGroupOption
185
- v-for="option in options"
186
- :key="option.value ? option.value.toString() : option.label"
187
- v-model="model"
188
- :disabled="disabled"
189
- v-bind="option"
190
- :emphasis="emphasis"
191
- :error="error"
192
- />
204
+ <template v-if="hasOptions">
205
+ <FzCheckboxGroupOption
206
+ v-for="option in options"
207
+ :key="option.value ? option.value.toString() : option.label"
208
+ v-model="model"
209
+ :disabled="disabled"
210
+ v-bind="option"
211
+ :emphasis="emphasis"
212
+ :error="error"
213
+ />
214
+ </template>
215
+ <slot v-else :checkboxGroupProps="controlledProps" />
193
216
  </div>
194
217
  <!-- Error message display with accessible ARIA live region -->
195
218
  <ErrorAlert v-if="error && $slots.error" :id="id + '-error'">