@fiscozen/checkbox 0.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.
@@ -0,0 +1,76 @@
1
+ export type FzCheckboxProps = {
2
+ /**
3
+ * The label of the checkbox
4
+ */
5
+ label: string;
6
+ /**
7
+ * The value of the checkbox. If not provided, the value will be the label
8
+ */
9
+ value?: string;
10
+ /**
11
+ * The size of the checkbox
12
+ */
13
+ size: "sm" | "md";
14
+ /**
15
+ * If the checkbox is checked
16
+ */
17
+ checked?: boolean;
18
+ /**
19
+ * if the checkbox is indeterminate
20
+ */
21
+ indeterminate?: boolean;
22
+ /**
23
+ * If true, the checkbox will be emphasized
24
+ */
25
+ emphasis?: boolean;
26
+ /**
27
+ * If true, the checkbox will be disabled
28
+ */
29
+ disabled?: boolean;
30
+ /**
31
+ * If true, the checkbox will be in an error state
32
+ */
33
+ error?: boolean;
34
+ /**
35
+ * If the checkbox is required
36
+ */
37
+ required?: boolean;
38
+ /**
39
+ * If the checkbox is standalone
40
+ */
41
+ standalone?: boolean;
42
+ };
43
+ export type FzCheckboxGroupProps = {
44
+ /**
45
+ * The label of the checkbox group
46
+ */
47
+ label: string;
48
+ /**
49
+ * The size of the checkbox
50
+ */
51
+ size: "sm" | "md";
52
+ /**
53
+ * The checkbox to render
54
+ */
55
+ options: ParentCheckbox[];
56
+ /**
57
+ * If true, the checkbox will be emphasized
58
+ */
59
+ emphasis?: boolean;
60
+ /**
61
+ * If true, the checkbox will be disabled
62
+ */
63
+ disabled?: boolean;
64
+ /**
65
+ * If true, the checkbox will be in an error state
66
+ */
67
+ error?: boolean;
68
+ /**
69
+ * If the checkbox group is required
70
+ */
71
+ required?: boolean;
72
+ };
73
+ export type ParentCheckbox = ChildCheckbox & {
74
+ children?: ChildCheckbox[];
75
+ };
76
+ export type ChildCheckbox = Omit<FzCheckboxProps, "size">;
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ input[type=checkbox][data-v-7bafffef]{width:0;height:0}input[type=checkbox]+label[data-v-7bafffef]{cursor:pointer}input[type=checkbox]+label[data-v-7bafffef]:hover{cursor:pointer}input[type=checkbox]:focus+label div[data-v-7bafffef]:after{border-radius:3px;content:"";border-style:solid;border-width:1px;@apply border-blue-500;top:0;left:0;right:0;bottom:0;position:absolute}.checkbox--small:focus+label div[data-v-7bafffef]:after{transform:translate(-.2px,.4px)}.checkbox-group--child-container__small[data-v-c563830e]{margin-top:5px;padding-left:24px}.checkbox-group--child-container__medium[data-v-c563830e]{margin-top:6px;padding-left:24px}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@fiscozen/checkbox",
3
+ "version": "0.1.0",
4
+ "description": "Design System Checkbox and Checkbox Group component",
5
+ "main": "src/index.ts",
6
+ "type": "module",
7
+ "keywords": [],
8
+ "author": "Cristian Barraco",
9
+ "dependencies": {
10
+ "@fiscozen/badge": "^0.1.0",
11
+ "@fiscozen/composables": "^0.1.8"
12
+ },
13
+ "peerDependencies": {
14
+ "tailwindcss": "^3.4.1",
15
+ "vue": "^3.4.13",
16
+ "@fiscozen/icons": "^0.1.3"
17
+ },
18
+ "devDependencies": {
19
+ "@rushstack/eslint-patch": "^1.3.3",
20
+ "@types/jsdom": "^21.1.6",
21
+ "@types/node": "^18.19.3",
22
+ "@vitejs/plugin-vue": "^4.5.2",
23
+ "@vitest/coverage-v8": "^1.2.1",
24
+ "@vue/test-utils": "^2.4.3",
25
+ "@vue/tsconfig": "^0.5.0",
26
+ "vite-plugin-dts": "^3.8.3",
27
+ "eslint": "^8.49.0",
28
+ "jsdom": "^23.0.1",
29
+ "prettier": "^3.0.3",
30
+ "typescript": "~5.3.0",
31
+ "vite": "^5.0.10",
32
+ "vitest": "^1.2.0",
33
+ "vue-tsc": "^1.8.25",
34
+ "@awesome.me/kit-8137893ad3": "^1.0.65",
35
+ "@fortawesome/fontawesome-svg-core": "^6.5.1",
36
+ "@fortawesome/vue-fontawesome": "^3.0.6",
37
+ "@fiscozen/eslint-config": "^0.1.0",
38
+ "@fiscozen/prettier-config": "^0.1.0",
39
+ "@fiscozen/tsconfig": "^0.1.0",
40
+ "@fiscozen/icons": "^0.1.3"
41
+ },
42
+ "license": "MIT",
43
+ "scripts": {
44
+ "coverage": "vitest run --coverage",
45
+ "format": "prettier --write src/",
46
+ "test:unit": "vitest run",
47
+ "build": "vue-tsc && vite build"
48
+ }
49
+ }
@@ -0,0 +1,139 @@
1
+ <template>
2
+ <div class="flex justify-center flex-col w-fit">
3
+ <input
4
+ type="checkbox"
5
+ :id="value.replace(/ /g, '-').toLowerCase()"
6
+ :disabled="disabled"
7
+ :checked="checked"
8
+ :class="staticInputClass"
9
+ :required="required"
10
+ :value="value"
11
+ @change="emit('change', $event)"
12
+ v-model="model"
13
+ :indeterminate="indeterminate"
14
+ ref="refCheckbox"
15
+ />
16
+ <label
17
+ :for="value.replace(/ /g, '-').toLowerCase()"
18
+ :class="[staticLabelClass, computedLabelClass]"
19
+ >
20
+ <FzIcon
21
+ :name="computedName"
22
+ :size="size"
23
+ :class="[staticIconClass, computedIconClasses]"
24
+ :variant="computedVariant"
25
+ />
26
+ <span class="w-fit">{{ standalone ? "" : label }}</span>
27
+ </label>
28
+ <FzRadioErrorText :size="size" v-if="error && $slots.error">
29
+ <slot name="error" />
30
+ </FzRadioErrorText>
31
+ <slot name="children" />
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { computed, onMounted, ref } from "vue";
37
+ import { FzCheckboxProps } from "./types";
38
+ import { mapSizeToClasses } from "./common";
39
+ import FzRadioErrorText from "./components/FzCheckboxErrorText.vue";
40
+ import { FzIcon } from "@fiscozen/icons";
41
+
42
+ const props = withDefaults(defineProps<FzCheckboxProps>(), {
43
+ size: "md",
44
+ indeterminate: false,
45
+ });
46
+
47
+ const value = props.value || props.label;
48
+
49
+ const model = defineModel<boolean | string[]>({
50
+ required: true,
51
+ });
52
+
53
+ const emit = defineEmits(["change"]);
54
+ const refCheckbox = ref<HTMLInputElement | null>(null);
55
+
56
+ const staticInputClass = "w-0 h-0 peer";
57
+ const staticLabelClass = `
58
+ flex items-start gap-4 hover:cursor-pointer
59
+ peer-focus:[&_div]:after:border-1
60
+ peer-focus:[&_div]:after:border-solid
61
+ peer-focus:[&_div]:after:rounded-[3px]
62
+ peer-focus:[&_div]:after:border-blue-500
63
+ peer-focus:[&_div]:after:content-['']
64
+ peer-focus:[&_div]:after:top-0
65
+ peer-focus:[&_div]:after:left-0
66
+ peer-focus:[&_div]:after:right-0
67
+ peer-focus:[&_div]:after:bottom-0
68
+ peer-focus:[&_div]:after:absolute
69
+ `;
70
+ const staticIconClass = "relative";
71
+
72
+ const computedLabelClass = computed(() => [
73
+ mapSizeToClasses[props.size],
74
+ getTextClassForLabel(),
75
+ ]);
76
+
77
+ const computedIconClasses = computed(() => [
78
+ props.size === "sm" ? "mt-1" : "",
79
+ props.size === "md" ? "mt-2" : "",
80
+ getTextClassForIcon(),
81
+ ]);
82
+
83
+ const computedName = computed(() => {
84
+ if (props.indeterminate) {
85
+ return "square-minus";
86
+ }
87
+
88
+ return checkValueIsInModel() ? "square-check" : "square";
89
+ });
90
+
91
+ const computedVariant = computed(() => {
92
+ return !props.indeterminate && !checkValueIsInModel() ? "far" : "fas";
93
+ });
94
+
95
+ const checkValueIsInModel = () => {
96
+ if (typeof model.value === "boolean") {
97
+ return model.value;
98
+ } else {
99
+ return model.value.includes(value);
100
+ }
101
+ };
102
+
103
+ const getTextClassForLabel = () => {
104
+ switch (true) {
105
+ case props.disabled:
106
+ return "text-grey-300";
107
+ case props.error:
108
+ return "text-semantic-error";
109
+ case props.emphasis:
110
+ return "text-grey-500 peer-checked:[&_div]:text-blue-500 peer-indeterminate:[&_div]:text-blue-500";
111
+ default:
112
+ return "text-grey-500";
113
+ }
114
+ };
115
+
116
+ const getTextClassForIcon = () => {
117
+ switch (true) {
118
+ case props.disabled:
119
+ return "text-grey-300";
120
+ case props.error:
121
+ return "text-semantic-error";
122
+ case props.emphasis:
123
+ default:
124
+ return "text-grey-500";
125
+ }
126
+ };
127
+
128
+ onMounted(() => {
129
+ if (typeof model.value === "boolean") {
130
+ if (model.value) refCheckbox.value?.dispatchEvent(new Event("change"));
131
+ else if (props.checked !== undefined) model.value = props.checked;
132
+ } else {
133
+ if (model.value.includes(value))
134
+ refCheckbox.value?.dispatchEvent(new Event("change"));
135
+ else if (props.checked) model.value.push(value);
136
+ }
137
+ });
138
+ </script>
139
+ <style scoped></style>
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <div :class="[staticContainerClass, computedContainerClass]" :id="id">
3
+ <label :for="id" :class="[staticLabeldClass, computedLabelClass]">
4
+ <span>{{ label }}<span v-if="required"> *</span></span>
5
+ <p :class="computedHelpTextClass" v-if="$slots.help">
6
+ <slot name="help" />
7
+ </p>
8
+ </label>
9
+ <div :class="[staticSlotContainerClass, computedSlotContainerClass]">
10
+ <FzCheckboxGroupOption
11
+ v-for="option in options"
12
+ :key="option.value"
13
+ v-model="model"
14
+ v-bind="option"
15
+ :disabled="disabled"
16
+ :emphasis="emphasis"
17
+ :error="error"
18
+ :size="size"
19
+ />
20
+ </div>
21
+ <FzCheckboxErrorText :size="size" v-if="error && $slots.error">
22
+ <slot name="error" />
23
+ </FzCheckboxErrorText>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import { computed } from "vue";
29
+ import { FzCheckboxGroupProps, ParentCheckbox } from "./types";
30
+ import FzCheckboxErrorText from "./components/FzCheckboxErrorText.vue";
31
+ import { mapSizeToClasses } from "./common";
32
+ import FzCheckboxGroupOption from "./components/FzCheckboxGroupOption.vue";
33
+
34
+ const props = defineProps<FzCheckboxGroupProps>();
35
+ const id = `fz-checkbox-group-${generateRandomId()}`;
36
+
37
+ const computedHelpTextClass = computed(() => [
38
+ props.size === "sm" ? "text-xs" : "text-sm",
39
+ props.disabled ? "text-grey-400" : "text-grey-500",
40
+ ]);
41
+
42
+ const model = defineModel<string[]>({
43
+ required: true,
44
+ default: [],
45
+ });
46
+
47
+ const staticLabeldClass = "flex flex-col";
48
+ const staticContainerClass = "flex flex-col";
49
+ const staticSlotContainerClass = "flex flex-col";
50
+
51
+ const computedLabelClass = computed(() => [
52
+ mapSizeToClasses[props.size],
53
+ props.size === "sm" ? "gap-4" : "",
54
+ props.size === "md" ? "gap-6" : "",
55
+ props.disabled ? "text-grey-400" : "",
56
+ !props.disabled ? "text-grey-500" : "",
57
+ ]);
58
+
59
+ const computedContainerClass = computed(() => [
60
+ mapSizeToClasses[props.size],
61
+ props.size === "sm" ? "gap-10" : "",
62
+ props.size === "md" ? "gap-12" : "",
63
+ ]);
64
+
65
+ const computedSlotContainerClass = computed(() => [
66
+ mapSizeToClasses[props.size],
67
+ props.size === "sm" ? "gap-6" : "",
68
+ props.size === "md" ? "gap-8" : "",
69
+ ]);
70
+
71
+ function generateRandomId() {
72
+ return Math.random().toString(36).slice(2, 9);
73
+ }
74
+ </script>
@@ -0,0 +1,88 @@
1
+ // FzCheckbox.spec.ts
2
+ import { mount } from "@vue/test-utils";
3
+ import { describe, it, expect } from "vitest";
4
+ import FzCheckbox from "../FzCheckbox.vue";
5
+
6
+ describe("FzCheckbox", () => {
7
+ it("renders correctly", async () => {
8
+ const wrapper = mount(FzCheckbox, {
9
+ props: {
10
+ label: "Test Checkbox",
11
+ value: "test",
12
+ size: "md",
13
+ modelValue: false,
14
+ },
15
+ });
16
+
17
+ await wrapper.vm.$nextTick();
18
+ expect(wrapper.html()).toContain("Test Checkbox");
19
+ expect(wrapper.html()).toMatchSnapshot();
20
+ });
21
+
22
+ it("emits an update event when clicked", async () => {
23
+ const wrapper = mount(FzCheckbox, {
24
+ props: {
25
+ label: "Test Checkbox",
26
+ value: "test",
27
+ modelValue: false,
28
+ },
29
+ });
30
+ await wrapper.find("input").trigger("click");
31
+ expect(wrapper.emitted()).toHaveProperty("update:modelValue");
32
+ });
33
+
34
+ it("is checked when v-model is true", async () => {
35
+ const wrapper = mount(FzCheckbox, {
36
+ props: {
37
+ label: "Test Checkbox",
38
+ value: "test",
39
+ modelValue: true,
40
+ },
41
+ });
42
+ await wrapper.vm.$nextTick();
43
+ expect(wrapper.find("input").element.checked).toBe(true);
44
+ });
45
+
46
+ it("is unchecked when v-model is false", async () => {
47
+ const wrapper = mount(FzCheckbox, {
48
+ props: {
49
+ label: "Test Checkbox",
50
+ value: "test",
51
+ modelValue: false,
52
+ },
53
+ });
54
+
55
+ await wrapper.vm.$nextTick();
56
+ expect(wrapper.find("input").element.checked).toBe(false);
57
+ });
58
+
59
+ it("should be emphasized", async () => {
60
+ const wrapper = mount(FzCheckbox, {
61
+ props: {
62
+ label: "Test Checkbox",
63
+ value: "test",
64
+ size: "md",
65
+ modelValue: false,
66
+ emphasis: true,
67
+ },
68
+ });
69
+ await wrapper.vm.$nextTick();
70
+ expect(wrapper.find("label").classes()).toContain(
71
+ "peer-checked:[&_div]:text-blue-500",
72
+ );
73
+ });
74
+
75
+ it("should be disabled", async () => {
76
+ const wrapper = mount(FzCheckbox, {
77
+ props: {
78
+ label: "Test Checkbox",
79
+ value: "test",
80
+ size: "md",
81
+ modelValue: false,
82
+ disabled: true,
83
+ },
84
+ });
85
+ await wrapper.vm.$nextTick();
86
+ expect(wrapper.find("input").element.disabled).toBe(true);
87
+ });
88
+ });
@@ -0,0 +1,100 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, it, expect } from "vitest";
3
+ import FzCheckboxGroup from "../FzCheckboxGroup.vue";
4
+ import FzCheckbox from "../FzCheckbox.vue";
5
+
6
+ describe("FzCheckboxGroup", () => {
7
+ it("renders correctly", async () => {
8
+ const wrapper = mount(FzCheckboxGroup, {
9
+ props: {
10
+ label: "Test Checkbox Group",
11
+ size: "md",
12
+ modelValue: [],
13
+ options: [
14
+ { label: "Option 1", value: "option1" },
15
+ { label: "Option 2", value: "option2" },
16
+ ],
17
+ },
18
+ });
19
+
20
+ await wrapper.vm.$nextTick();
21
+ expect(wrapper.html()).toContain("Test Checkbox Group");
22
+ expect(wrapper.findAllComponents(FzCheckbox).length).toBe(2);
23
+ });
24
+
25
+ it("displays error text when error prop is true", async () => {
26
+ const wrapper = mount(FzCheckboxGroup, {
27
+ props: {
28
+ label: "Test Checkbox Group",
29
+ size: "md",
30
+ modelValue: [],
31
+ options: [
32
+ { label: "Option 1", value: "option1" },
33
+ { label: "Option 2", value: "option2" },
34
+ ],
35
+ error: true,
36
+ },
37
+ slots: {
38
+ error: "Test error message",
39
+ },
40
+ });
41
+
42
+ await wrapper.vm.$nextTick();
43
+ expect(wrapper.html()).toContain("Test error message");
44
+ expect(wrapper.findAllComponents(FzCheckbox).length).toBe(2);
45
+ expect(wrapper.findAllComponents(FzCheckbox).at(0)!.props("error")).toBe(
46
+ true,
47
+ );
48
+ expect(wrapper.findAllComponents(FzCheckbox).at(1)!.props("error")).toBe(
49
+ true,
50
+ );
51
+ });
52
+
53
+ it("has disabled checkboxes when disabled prop is true", async () => {
54
+ const wrapper = mount(FzCheckboxGroup, {
55
+ props: {
56
+ label: "Test Checkbox Group",
57
+ size: "md",
58
+ modelValue: [],
59
+ options: [
60
+ { label: "Option 1", value: "option1" },
61
+ { label: "Option 2", value: "option2" },
62
+ ],
63
+ disabled: true,
64
+ },
65
+ });
66
+
67
+ await wrapper.vm.$nextTick();
68
+ expect(wrapper.findAllComponents(FzCheckbox).length).toBe(2);
69
+ expect(wrapper.findAllComponents(FzCheckbox).at(0)!.props("disabled")).toBe(
70
+ true,
71
+ );
72
+ expect(wrapper.findAllComponents(FzCheckbox).at(1)!.props("disabled")).toBe(
73
+ true,
74
+ );
75
+ });
76
+
77
+ it("display emphasized checkboxes when emphasis prop is true", async () => {
78
+ const wrapper = mount(FzCheckboxGroup, {
79
+ props: {
80
+ label: "Test Checkbox Group",
81
+ size: "md",
82
+ modelValue: [],
83
+ options: [
84
+ { label: "Option 1", value: "option1" },
85
+ { label: "Option 2", value: "option2" },
86
+ ],
87
+ emphasis: true,
88
+ },
89
+ });
90
+
91
+ await wrapper.vm.$nextTick();
92
+ expect(wrapper.findAllComponents(FzCheckbox).length).toBe(2);
93
+ expect(wrapper.findAllComponents(FzCheckbox).at(0)!.props("emphasis")).toBe(
94
+ true,
95
+ );
96
+ expect(wrapper.findAllComponents(FzCheckbox).at(1)!.props("emphasis")).toBe(
97
+ true,
98
+ );
99
+ });
100
+ });
@@ -0,0 +1,21 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`FzCheckbox > renders correctly 1`] = `
4
+ "<div class="flex justify-center flex-col w-fit"><input type="checkbox" id="test" class="w-0 h-0 peer" value="test"><label for="test" class="flex items-start gap-4 hover:cursor-pointer
5
+ peer-focus:[&amp;_div]:after:border-1
6
+ peer-focus:[&amp;_div]:after:border-solid
7
+ peer-focus:[&amp;_div]:after:rounded-[3px]
8
+ peer-focus:[&amp;_div]:after:border-blue-500
9
+ peer-focus:[&amp;_div]:after:content-['']
10
+ peer-focus:[&amp;_div]:after:top-0
11
+ peer-focus:[&amp;_div]:after:left-0
12
+ peer-focus:[&amp;_div]:after:right-0
13
+ peer-focus:[&amp;_div]:after:bottom-0
14
+ peer-focus:[&amp;_div]:after:absolute text-md text-grey-500">
15
+ <div class="flex items-center justify-center w-[20px] h-[20px] relative mt-2 text-grey-500"><svg class="svg-inline--fa fa-square h-[16px]" aria-hidden="true" focusable="false" data-prefix="far" data-icon="square" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
16
+ <path class="" fill="currentColor" d="M384 80c8.8 0 16 7.2 16 16V416c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V96c0-8.8 7.2-16 16-16H384zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"></path>
17
+ </svg></div><span class="w-fit">Test Checkbox</span>
18
+ </label>
19
+ <!--v-if-->
20
+ </div>"
21
+ `;
package/src/common.ts ADDED
@@ -0,0 +1,4 @@
1
+ export const mapSizeToClasses = {
2
+ sm: "text-sm",
3
+ md: "text-md",
4
+ };
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <p :class="[staticErrorTextClass, computedErrorTextClass]">
3
+ <FzIcon
4
+ class="text-semantic-error"
5
+ name="triangle-exclamation"
6
+ :size="size"
7
+ />
8
+ <slot />
9
+ </p>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { computed } from "vue";
14
+ import { mapSizeToClasses } from "../common";
15
+ import { FzIcon } from "@fiscozen/icons";
16
+
17
+ const props = defineProps<{ errorText?: string; size: "sm" | "md" }>();
18
+ const staticErrorTextClass = "flex w-full gap-4 items-center";
19
+ const computedErrorTextClass = computed(() => [mapSizeToClasses[props.size]]);
20
+ </script>
@@ -0,0 +1,96 @@
1
+ <template>
2
+ <FzCheckbox
3
+ v-model="model"
4
+ :value="props.value"
5
+ :label="props.label"
6
+ :disabled="disabled"
7
+ :emphasis="emphasis"
8
+ :error="error"
9
+ :size="size"
10
+ :indeterminate="isIndeterminate"
11
+ @change="onCheckboxParentChange"
12
+ >
13
+ <template #children v-if="children?.length">
14
+ <div :class="[staticChildContainerClass, computedChildContainerClasses]">
15
+ <FzCheckbox
16
+ v-for="child in children"
17
+ :key="child.value"
18
+ v-model="model"
19
+ v-bind="child"
20
+ :disabled="disabled"
21
+ :emphasis="emphasis"
22
+ :error="error"
23
+ :size="size"
24
+ @change="handleCheckboxParentChange"
25
+ />
26
+ </div>
27
+ </template>
28
+ </FzCheckbox>
29
+ </template>
30
+ <script setup lang="ts">
31
+ import { computed } from "vue";
32
+ import FzCheckbox from "../FzCheckbox.vue";
33
+ import { ParentCheckbox } from "../types";
34
+
35
+ const props = defineProps<ParentCheckbox & { size: "sm" | "md" }>();
36
+
37
+ const model = defineModel<string[]>({
38
+ required: true,
39
+ default: [],
40
+ });
41
+
42
+ const staticChildContainerClass = "flex flex-col justify-center pl-24";
43
+
44
+ const computedChildContainerClasses = computed(() => [
45
+ props.size === "sm" ? "gap-6 mt-6" : "",
46
+ props.size === "md" ? "gap-8 mt-8" : "",
47
+ ]);
48
+
49
+ const isIndeterminate = computed(() => {
50
+ if (!props.children) return false;
51
+
52
+ const numChecked = props.children.filter((child) =>
53
+ model.value.includes(child.value || child.label),
54
+ ).length;
55
+ return numChecked > 0 && numChecked < props.children.length;
56
+ });
57
+
58
+ function handleCheckboxParentChange() {
59
+ if (!props.children) return;
60
+ const numChecked = props.children.filter((child) =>
61
+ model.value.includes(child.value || child.label),
62
+ ).length;
63
+
64
+ if (numChecked === props.children.length) {
65
+ // push parent value to model (using concat to force reactivity, push seems to not work)
66
+ model.value = model.value.concat(props.value || props.label);
67
+ } else {
68
+ // remove parent value from model if it exists
69
+ if (model.value.includes(props.value || props.label))
70
+ model.value = model.value.filter(
71
+ (value) => value !== (props.value || props.label),
72
+ );
73
+ }
74
+ }
75
+
76
+ function onCheckboxParentChange() {
77
+ if (!props.children) return;
78
+ if (model.value.includes(props.value || props.label)) {
79
+ // push all children values to model that are not already in model
80
+ model.value = model.value.concat(
81
+ props.children
82
+ ?.map((child) => child.value || child.label)
83
+ .filter((value) => !model.value.includes(value)),
84
+ );
85
+ } else {
86
+ // remove all children values from model
87
+ model.value = model.value.filter(
88
+ (value) =>
89
+ !props.children
90
+ ?.map((child) => child.value || child.label)
91
+ .includes(value),
92
+ );
93
+ }
94
+ }
95
+ </script>
96
+ <style scoped></style>
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { default as FzCheckbox } from "./FzCheckbox.vue";
2
+ export { default as FzCheckboxGroup } from "./FzCheckboxGroup.vue";
3
+ export type { FzCheckboxProps, FzCheckboxGroupProps } from "./types";