@code-coaching/vuetiful 0.19.0 → 0.21.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.
Files changed (29) hide show
  1. package/README.md +1 -1
  2. package/dist/style.css +1 -1
  3. package/dist/styles/all.css +232 -0
  4. package/dist/types/components/molecules/VAlert.test.d.ts +1 -0
  5. package/dist/types/components/molecules/VAlert.vue.d.ts +44 -0
  6. package/dist/types/components/molecules/VCard/VCard.test.d.ts +1 -0
  7. package/dist/types/components/molecules/VCard/VCard.vue.d.ts +52 -0
  8. package/dist/types/components/molecules/VCard/VCardBody.test.d.ts +1 -0
  9. package/dist/types/components/molecules/VCard/VCardBody.vue.d.ts +2 -0
  10. package/dist/types/components/molecules/VCard/VCardFooter.test.d.ts +1 -0
  11. package/dist/types/components/molecules/VCard/VCardFooter.vue.d.ts +2 -0
  12. package/dist/types/components/molecules/VCard/VCardHeader.test.d.ts +1 -0
  13. package/dist/types/components/molecules/VCard/VCardHeader.vue.d.ts +2 -0
  14. package/dist/types/components/molecules/index.d.ts +6 -1
  15. package/dist/vuetiful.es.mjs +398 -139
  16. package/dist/vuetiful.umd.js +10 -10
  17. package/package.json +1 -1
  18. package/src/components/molecules/VAlert.test.ts +90 -0
  19. package/src/components/molecules/VAlert.vue +133 -0
  20. package/src/components/molecules/VCard/VCard.test.ts +47 -0
  21. package/src/components/molecules/VCard/VCard.vue +71 -0
  22. package/src/components/molecules/VCard/VCardBody.test.ts +21 -0
  23. package/src/components/molecules/VCard/VCardBody.vue +5 -0
  24. package/src/components/molecules/VCard/VCardFooter.test.ts +45 -0
  25. package/src/components/molecules/VCard/VCardFooter.vue +11 -0
  26. package/src/components/molecules/VCard/VCardHeader.test.ts +68 -0
  27. package/src/components/molecules/VCard/VCardHeader.vue +33 -0
  28. package/src/components/molecules/VListbox/VListbox.vue +3 -1
  29. package/src/components/molecules/index.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-coaching/vuetiful",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "onchange 'src/**/*.vue' 'src/**/*.ts' 'src/**/*.css' -- npm run build",
@@ -0,0 +1,90 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, test } from "vitest";
3
+ import { VAlert } from "..";
4
+
5
+ describe("VAlert", () => {
6
+ test("types", () => {
7
+ const wrapper = mount({
8
+ template: /*html*/ `
9
+ <v-alert data-test="default"></v-alert>
10
+ <v-alert data-test="info" type="info"></v-alert>
11
+ <v-alert data-test="success" type="success"></v-alert>
12
+ <v-alert data-test="warning" type="warning"></v-alert>
13
+ <v-alert data-test="error" type="error"></v-alert>
14
+ `,
15
+ components: {
16
+ "v-alert": VAlert,
17
+ },
18
+ });
19
+
20
+ const defaultAlert = wrapper.find("[data-test=default]");
21
+ expect(defaultAlert.classes()).toContain("variant-filled-primary");
22
+
23
+ const infoAlert = wrapper.find("[data-test=info]");
24
+ expect(infoAlert.classes()).toContain("variant-filled");
25
+
26
+ const successAlert = wrapper.find("[data-test=success]");
27
+ expect(successAlert.classes()).toContain("variant-filled-success");
28
+
29
+ const warningAlert = wrapper.find("[data-test=warning]");
30
+ expect(warningAlert.classes()).toContain("variant-filled-warning");
31
+ });
32
+
33
+ describe("given close icon is clicked", () => {
34
+ test("should emit close", async () => {
35
+ const wrapper = mount(VAlert);
36
+ await wrapper.find("[data-test=close]").trigger("click");
37
+ expect(wrapper.emitted()["close"][0]).toEqual([]);
38
+
39
+ await wrapper.find("[data-test=close]").trigger("keydown", { key: "Enter" });
40
+ expect(wrapper.emitted()["close"][0]).toEqual([]);
41
+
42
+ await wrapper.find("[data-test=close]").trigger("keydown", { key: " " });
43
+ expect(wrapper.emitted()["close"][0]).toEqual([]);
44
+ });
45
+ });
46
+
47
+ describe("given a pre slot is provided", () => {
48
+ test("should render the pre slot", () => {
49
+ const wrapper = mount(VAlert, {
50
+ slots: {
51
+ pre: "John Duck",
52
+ },
53
+ });
54
+ expect(wrapper.text()).toContain("John Duck");
55
+ });
56
+ })
57
+
58
+ describe("given a actions slot is provided", () => {
59
+ test("should render the actions slot", () => {
60
+ const wrapper = mount(VAlert, {
61
+ slots: {
62
+ actions: "John Duck",
63
+ },
64
+ });
65
+ expect(wrapper.text()).toContain("John Duck");
66
+ });
67
+ })
68
+
69
+ describe("given hide-icon prop is present", () => {
70
+ test("should not render an icon", () => {
71
+ const wrapper = mount(VAlert, {
72
+ props: {
73
+ hideIcon: true,
74
+ },
75
+ });
76
+ expect(wrapper.findAll(".icon").length).toBe(1);
77
+ });
78
+ })
79
+
80
+ describe("given hide-close prop is present", () => {
81
+ test("should not render a close icon", () => {
82
+ const wrapper = mount(VAlert, {
83
+ props: {
84
+ hideClose: true,
85
+ },
86
+ });
87
+ expect(wrapper.find("[data-test=close]").exists()).toBe(false);
88
+ });
89
+ })
90
+ });
@@ -0,0 +1,133 @@
1
+ <script setup lang="ts">
2
+ import { PropType, computed } from "vue";
3
+
4
+ const emit = defineEmits(["close"]);
5
+ const props = defineProps({
6
+ hideIcon: {
7
+ type: Boolean,
8
+ default: false,
9
+ },
10
+ hideClose: {
11
+ type: Boolean,
12
+ default: false,
13
+ },
14
+
15
+ show: {
16
+ type: Boolean,
17
+ default: true,
18
+ },
19
+ type: {
20
+ type: String as PropType<"info" | "success" | "warning" | "error">,
21
+ default: "",
22
+ },
23
+ });
24
+
25
+ const typeBackground = computed(() => {
26
+ switch (props.type) {
27
+ case "info":
28
+ return "variant-filled";
29
+ case "success":
30
+ return "variant-filled-success";
31
+ case "warning":
32
+ return "variant-filled-warning";
33
+ case "error":
34
+ return "variant-filled-error";
35
+ default:
36
+ return "variant-filled-primary";
37
+ }
38
+ });
39
+
40
+ const close = () => emit("close");
41
+ const handleKeydown = (event: KeyboardEvent) => {
42
+ if (event.key === "Enter" || event.key === " ") {
43
+ close();
44
+ }
45
+ };
46
+ </script>
47
+
48
+ <template>
49
+ <aside
50
+ v-if="show"
51
+ :class="`vuetiful-alert flex w-full flex-row items-center gap-4 p-4 border-token rounded-container-token ${typeBackground}`"
52
+ >
53
+ <div v-if="!hideIcon">
54
+ <slot v-if="$slots.pre" name="pre" />
55
+ <template v-if="!$slots.pre">
56
+ <!-- https://fontawesome.com/icons/circle-info?f=classic&s=solid -->
57
+ <svg
58
+ v-if="type === 'info'"
59
+ class="icon"
60
+ xmlns="http://www.w3.org/2000/svg"
61
+ viewBox="0 0 512 512"
62
+ >
63
+ <path
64
+ d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
65
+ />
66
+ </svg>
67
+
68
+ <!-- https://fontawesome.com/icons/circle-check?f=classic&s=solid -->
69
+ <svg
70
+ v-if="type === 'success'"
71
+ class="icon"
72
+ xmlns="http://www.w3.org/2000/svg"
73
+ viewBox="0 0 512 512"
74
+ >
75
+ <path
76
+ d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"
77
+ />
78
+ </svg>
79
+
80
+ <!-- https://fontawesome.com/icons/circle-exclamation?f=classic&s=solid -->
81
+ <svg
82
+ v-if="type === 'warning'"
83
+ class="icon"
84
+ xmlns="http://www.w3.org/2000/svg"
85
+ viewBox="0 0 512 512"
86
+ >
87
+ <path
88
+ d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
89
+ />
90
+ </svg>
91
+
92
+ <!-- https://fontawesome.com/icons/triangle-exclamation?f=classic&s=solid -->
93
+ <svg
94
+ v-if="type === 'error'"
95
+ class="icon"
96
+ xmlns="http://www.w3.org/2000/svg"
97
+ viewBox="0 0 512 512"
98
+ >
99
+ <path
100
+ d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
101
+ />
102
+ </svg>
103
+ </template>
104
+ </div>
105
+
106
+ <div class="vuetiful-alert-message flex-auto">
107
+ <slot />
108
+ </div>
109
+
110
+ <slot name="actions"> </slot>
111
+ <!-- https://fontawesome.com/icons/xmark?f=classic&s=solid -->
112
+ <svg
113
+ data-test="close"
114
+ v-if="!hideClose"
115
+ tabindex="0"
116
+ @keydown="handleKeydown"
117
+ @click="close"
118
+ class="icon hover:cursor-pointer"
119
+ xmlns="http://www.w3.org/2000/svg"
120
+ viewBox="0 0 384 512"
121
+ >
122
+ <path
123
+ d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"
124
+ />
125
+ </svg>
126
+ </aside>
127
+ </template>
128
+
129
+ <style scoped>
130
+ .icon {
131
+ @apply my-1 h-6 min-h-[1.5rem] w-6 min-w-[1.5rem] fill-current;
132
+ }
133
+ </style>
@@ -0,0 +1,47 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, test } from "vitest";
3
+ import { VCard } from "..";
4
+
5
+ describe("VCard", () => {
6
+ test("defaults", async () => {
7
+ const wrapper = mount(VCard);
8
+ expect(wrapper.props()).toEqual({
9
+ background: "bg-surface-200-700-token",
10
+ clickable: false,
11
+ hideSeparator: false,
12
+ horizontal: false,
13
+ text: "text-surface-900-50-token",
14
+ });
15
+ });
16
+
17
+ describe("given the card is clicked", () => {
18
+ describe("given the card is not clickable", () => {
19
+ test("should not emit click", async () => {
20
+ const wrapper = mount(VCard, { props: { clickable: false } });
21
+ expect(wrapper.attributes()['tabindex']).toBeUndefined();
22
+ await wrapper.trigger("click");
23
+ expect(wrapper.emitted()["click"]).toBeUndefined();
24
+
25
+ await wrapper.trigger("keydown", { key: "Enter" });
26
+ expect(wrapper.emitted()["click"]).toBeUndefined();
27
+
28
+ await wrapper.trigger("keydown", { key: " " });
29
+ expect(wrapper.emitted()["click"]).toBeUndefined();
30
+ });
31
+ });
32
+ describe("given the card is clickable", () => {
33
+ test("should emit click", async () => {
34
+ const wrapper = mount(VCard, { props: { clickable: true } });
35
+ expect(wrapper.attributes()['tabindex']).toEqual("0");
36
+ await wrapper.trigger("click");
37
+ expect(wrapper.emitted()["click"].length).toEqual(1);
38
+
39
+ await wrapper.trigger("keydown", { key: "Enter" });
40
+ expect(wrapper.emitted()["click"].length).toEqual(2);
41
+
42
+ await wrapper.trigger("keydown", { key: " " });
43
+ expect(wrapper.emitted()["click"].length).toEqual(3);
44
+ });
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ import { provide } from "vue";
3
+
4
+ const emit = defineEmits(["click"]);
5
+
6
+ const props = defineProps({
7
+ hideSeparator: {
8
+ type: Boolean,
9
+ default: false,
10
+ },
11
+ background: {
12
+ type: String,
13
+ default: "bg-surface-200-700-token",
14
+ },
15
+ text: {
16
+ type: String,
17
+ default: "text-surface-900-50-token",
18
+ },
19
+ horizontal: {
20
+ type: Boolean,
21
+ default: false,
22
+ },
23
+ clickable: {
24
+ type: Boolean,
25
+ default: false,
26
+ },
27
+ });
28
+
29
+ provide("hideSeparator", props.hideSeparator);
30
+
31
+ const onClick = () => {
32
+ if (!props.clickable) return;
33
+ emit("click");
34
+ };
35
+ const onKeydown = (event: KeyboardEvent) => {
36
+ if (!props.clickable) return;
37
+ if (event.key === "Enter") {
38
+ event.preventDefault();
39
+ emit("click");
40
+ }
41
+ if (event.key === " ") {
42
+ event.preventDefault();
43
+ emit("click");
44
+ }
45
+ };
46
+ </script>
47
+
48
+ <template>
49
+ <div
50
+ @click="onClick"
51
+ @keydown="onKeydown"
52
+ :tabindex="clickable ? 0 : undefined"
53
+ :class="`vuetiful-card flex border-token rounded-container-token ring-outline-token ${
54
+ horizontal ? 'flex-row' : 'flex-col'
55
+ } ${background} ${text} ${clickable ? 'card-hover hover:cursor-pointer' : ''}`"
56
+ >
57
+ <slot />
58
+ </div>
59
+ </template>
60
+
61
+ <style>
62
+ .vuetiful-card-header {
63
+ border-top-left-radius: inherit;
64
+ border-top-right-radius: inherit;
65
+ }
66
+
67
+ .vuetiful-card-header > * {
68
+ border-top-left-radius: inherit;
69
+ border-top-right-radius: inherit;
70
+ }
71
+ </style>
@@ -0,0 +1,21 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, test } from "vitest";
3
+ import { VCard, VCardBody } from "..";
4
+
5
+ describe("VCardBody", () => {
6
+ test("defaults", async () => {
7
+ const wrapper = mount({
8
+ template: /*html*/ `
9
+ <v-card>
10
+ <v-card-body>John Duck</v-card-body>
11
+ </v-card>
12
+ `,
13
+ components: {
14
+ "v-card": VCard,
15
+ "v-card-body": VCardBody,
16
+ },
17
+ });
18
+
19
+ expect(wrapper.text()).toEqual("John Duck");
20
+ });
21
+ });
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div data-test="vuetiful-card-body-content" class="vuetiful-card-body p-4">
3
+ <slot />
4
+ </div>
5
+ </template>
@@ -0,0 +1,45 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, test } from "vitest";
3
+ import { VCard, VCardFooter } from "..";
4
+
5
+ describe("VCardFooter", () => {
6
+ test("defaults", async () => {
7
+ const wrapper = mount({
8
+ template: /*html*/ `
9
+ <v-card>
10
+ <v-card-footer>John Duck</v-card-footer>
11
+ </v-card>
12
+ `,
13
+ components: {
14
+ "v-card": VCard,
15
+ "v-card-footer": VCardFooter,
16
+ },
17
+ });
18
+
19
+ const separator = wrapper.find("[data-test='vuetiful-card-footer-separator']");
20
+ const content = wrapper.find("[data-test='vuetiful-card-footer-content']");
21
+ expect(separator.element.tagName).toEqual("HR");
22
+ expect(content.text()).toEqual("John Duck");
23
+ });
24
+
25
+ describe("given hideSeparator is true", () => {
26
+ test("should not show separator", async () => {
27
+ const wrapper = mount({
28
+ template: /*html*/ `
29
+ <v-card hide-separator>
30
+ <v-card-footer>John Duck</v-card-footer>
31
+ </v-card>
32
+ `,
33
+ components: {
34
+ "v-card": VCard,
35
+ "v-card-footer": VCardFooter,
36
+ },
37
+ });
38
+
39
+ const separator = wrapper.find("[data-test='vuetiful-card-footer-separator']");
40
+ const content = wrapper.find("[data-test='vuetiful-card-footer-content']");
41
+ expect(separator.exists()).toEqual(false);
42
+ expect(content.text()).toEqual("John Duck");
43
+ })
44
+ })
45
+ });
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import { inject } from "vue";
3
+ const hideSeparator = inject("hideSeparator", false);
4
+ </script>
5
+
6
+ <template>
7
+ <hr v-if="!hideSeparator" data-test="vuetiful-card-footer-separator" class="opacity-90" />
8
+ <div data-test="vuetiful-card-footer-content" class="vuetiful-card-footer p-4">
9
+ <slot />
10
+ </div>
11
+ </template>
@@ -0,0 +1,68 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, test } from "vitest";
3
+ import { VCard, VCardHeader } from "..";
4
+
5
+ describe("VCardHeader", () => {
6
+ test("defaults", async () => {
7
+ const wrapper = mount({
8
+ template: /*html*/ `
9
+ <v-card>
10
+ <v-card-header>John Duck</v-card-header>
11
+ </v-card>
12
+ `,
13
+ components: {
14
+ "v-card": VCard,
15
+ "v-card-header": VCardHeader,
16
+ },
17
+ });
18
+
19
+ const separator = wrapper.find("[data-test='vuetiful-card-header-separator']");
20
+ const content = wrapper.find("[data-test='vuetiful-card-header-content']");
21
+ expect(separator.element.tagName).toEqual("HR");
22
+ expect(content.text()).toEqual("John Duck");
23
+ expect(content.classes()).toContain("p-4");
24
+ });
25
+
26
+ describe("given hideSeparator is true", () => {
27
+ test("should not show separator", async () => {
28
+ const wrapper = mount({
29
+ template: /*html*/ `
30
+ <v-card hide-separator>
31
+ <v-card-header>John Duck</v-card-header>
32
+ </v-card>
33
+ `,
34
+ components: {
35
+ "v-card": VCard,
36
+ "v-card-header": VCardHeader,
37
+ },
38
+ });
39
+
40
+ const separator = wrapper.find("[data-test='vuetiful-card-header-separator']");
41
+ const content = wrapper.find("[data-test='vuetiful-card-header-content']");
42
+ expect(separator.exists()).toEqual(false);
43
+ expect(content.text()).toEqual("John Duck");
44
+ });
45
+ });
46
+
47
+ describe("given an image is present", () => {
48
+ test("should not have padding", async () => {
49
+ const wrapper = mount({
50
+ template: /*html*/ `
51
+ <v-card>
52
+ <v-card-header>
53
+ <img src="fakeUrl" />
54
+ </v-card-header>
55
+ </v-card>
56
+ `,
57
+ components: {
58
+ "v-card": VCard,
59
+ "v-card-header": VCardHeader,
60
+ },
61
+ });
62
+ await wrapper.vm.$nextTick();
63
+
64
+ const content = wrapper.find("[data-test='vuetiful-card-header-content']");
65
+ expect(content.classes()).not.toContain("p-4");
66
+ });
67
+ });
68
+ });
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { Ref, computed, inject, ref } from 'vue';
3
+
4
+ const headerRef = ref() as Ref<HTMLDivElement>;
5
+
6
+ const hasImageAsChild = computed(() => {
7
+ const children = headerRef.value?.children;
8
+ if (!children) return false;
9
+ const childrenArray = Array.from(children);
10
+ return childrenArray.some((child) => child.tagName === 'IMG');
11
+ });
12
+
13
+ const hideSeparator = inject('hideSeparator', false);
14
+ </script>
15
+
16
+ <template>
17
+ <div ref="headerRef" data-test='vuetiful-card-header-content' :class="`vuetiful-card-header ${hasImageAsChild ? '' : 'p-4'}`">
18
+ <slot />
19
+ </div>
20
+ <hr v-if="!hideSeparator" data-test='vuetiful-card-header-separator' class="opacity-90" />
21
+ </template>
22
+
23
+ <style>
24
+ .vuetiful-card-header {
25
+ border-top-left-radius: inherit;
26
+ border-top-right-radius: inherit;
27
+ }
28
+
29
+ .vuetiful-card-header > * {
30
+ border-top-left-radius: inherit;
31
+ border-top-right-radius: inherit;
32
+ }
33
+ </style>
@@ -101,7 +101,9 @@ const showText = computed(() => {
101
101
  v-model="parentModelValue"
102
102
  >
103
103
  <v-listbox-label v-if="labelText" :class="labelClasses">{{ labelText }}</v-listbox-label>
104
- <v-listbox-button data-test="v-listbox-button">{{ showText }}</v-listbox-button>
104
+ <v-listbox-button data-test="v-listbox-button" :class="`${background} ${text}`">
105
+ {{ showText }}
106
+ </v-listbox-button>
105
107
  <!-- TODO: Add configurable transition -->
106
108
  <transition
107
109
  enter-active-class="transition duration-150 ease-in-out"
@@ -17,9 +17,21 @@ import VTabs from "./VTabs/VTabs.vue";
17
17
  import VAccordion from "./VAccordion/VAccordion.vue";
18
18
  import VAccordionItem from "./VAccordion/VAccordionItem.vue";
19
19
 
20
+ import VAlert from "./VAlert.vue";
21
+
22
+ import VCard from "./VCard/VCard.vue";
23
+ import VCardBody from "./VCard/VCardBody.vue";
24
+ import VCardFooter from "./VCard/VCardFooter.vue";
25
+ import VCardHeader from "./VCard/VCardHeader.vue";
26
+
20
27
  export {
21
28
  VAccordion,
22
29
  VAccordionItem,
30
+ VAlert,
31
+ VCard,
32
+ VCardBody,
33
+ VCardFooter,
34
+ VCardHeader,
23
35
  VDrawer,
24
36
  VListbox,
25
37
  VListboxButton,