@liftkit-vue/core 0.2.0 → 0.2.1

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 (37) hide show
  1. package/package.json +3 -2
  2. package/src/components/LkBadge.vue +64 -0
  3. package/src/components/LkButton.vue +102 -0
  4. package/src/components/LkCard.vue +85 -0
  5. package/src/components/LkColumn.vue +40 -0
  6. package/src/components/LkContainer.vue +26 -0
  7. package/src/components/LkDropdown/LkDropdown.vue +14 -0
  8. package/src/components/LkDropdown/LkDropdownMenu.vue +99 -0
  9. package/src/components/LkDropdown/LkDropdownTrigger.vue +39 -0
  10. package/src/components/LkDropdown/index.ts +3 -0
  11. package/src/components/LkGrid.vue +58 -0
  12. package/src/components/LkHeading.vue +43 -0
  13. package/src/components/LkIcon.vue +56 -0
  14. package/src/components/LkIconButton.vue +78 -0
  15. package/src/components/LkImage.vue +62 -0
  16. package/src/components/LkMaterialLayer.vue +147 -0
  17. package/src/components/LkMenuItem.vue +51 -0
  18. package/src/components/LkNavbar.vue +113 -0
  19. package/src/components/LkPlaceholderBlock.vue +9 -0
  20. package/src/components/LkRow.vue +40 -0
  21. package/src/components/LkSection.vue +45 -0
  22. package/src/components/LkSelect.vue +167 -0
  23. package/src/components/LkSelectMenu.vue +67 -0
  24. package/src/components/LkSelectOption.vue +56 -0
  25. package/src/components/LkSelectTrigger.vue +43 -0
  26. package/src/components/LkSnackbar.vue +127 -0
  27. package/src/components/LkStateLayer.vue +33 -0
  28. package/src/components/LkSticker.vue +42 -0
  29. package/src/components/LkSwitch.vue +90 -0
  30. package/src/components/LkTabContent.vue +33 -0
  31. package/src/components/LkTabLink.vue +50 -0
  32. package/src/components/LkTabMenu.vue +59 -0
  33. package/src/components/LkTabs.vue +65 -0
  34. package/src/components/LkText.vue +37 -0
  35. package/src/components/LkTextInput.vue +105 -0
  36. package/src/components/LkTheme.vue +49 -0
  37. package/src/components/LkThemeController.vue +396 -0
@@ -0,0 +1,167 @@
1
+ <script lang="ts">
2
+ import type { InjectionKey, Ref } from 'vue'
3
+
4
+ export interface LkSelectOption {
5
+ label: string
6
+ value: string
7
+ }
8
+
9
+ export interface LkSelectContext {
10
+ open: Ref<boolean>
11
+ setOpen: (value: boolean) => void
12
+ toggle: () => void
13
+ triggerRef: Ref<HTMLElement | null>
14
+ contentRef: Ref<HTMLDivElement | null>
15
+ selectedValue: Ref<string>
16
+ setSelectedValue: (value: string) => void
17
+ options: Ref<LkSelectOption[]>
18
+ name: string | undefined
19
+ }
20
+
21
+ export const SELECT_INJECTION_KEY: InjectionKey<LkSelectContext> = Symbol('liftkit-select')
22
+
23
+ export interface LkSelectProps {
24
+ modelValue?: string
25
+ options?: LkSelectOption[]
26
+ name?: string
27
+ label?: string
28
+ labelPosition?: 'default' | 'on-input'
29
+ helpText?: string
30
+ placeholderText?: string
31
+ }
32
+ </script>
33
+
34
+ <script setup lang="ts">
35
+ import { ref, computed, watch, provide, onMounted, onBeforeUnmount } from 'vue'
36
+
37
+ const props = withDefaults(defineProps<LkSelectProps>(), {
38
+ modelValue: '',
39
+ options: () => [],
40
+ labelPosition: 'default',
41
+ })
42
+
43
+ const emit = defineEmits<{
44
+ 'update:modelValue': [value: string]
45
+ }>()
46
+
47
+ const open = ref(false)
48
+ const triggerRef = ref<HTMLElement | null>(null)
49
+ const contentRef = ref<HTMLDivElement | null>(null)
50
+ const hiddenSelectRef = ref<HTMLSelectElement | null>(null)
51
+ const selectedValue = ref(props.modelValue)
52
+
53
+ // Sync with v-model
54
+ watch(
55
+ () => props.modelValue,
56
+ (newVal) => {
57
+ selectedValue.value = newVal ?? ''
58
+ },
59
+ )
60
+
61
+ function setOpen(value: boolean) {
62
+ open.value = value
63
+ }
64
+
65
+ function toggle() {
66
+ setOpen(!open.value)
67
+ }
68
+
69
+ function setSelectedValue(value: string) {
70
+ selectedValue.value = value
71
+ emit('update:modelValue', value)
72
+ setOpen(false)
73
+ }
74
+
75
+ // Singleton registry for ensuring only one select open at a time
76
+ let registryInstance: { close: () => void } | null = null
77
+
78
+ watch(open, (isOpen) => {
79
+ if (isOpen) {
80
+ registryInstance = { close: () => setOpen(false) }
81
+ // Close other selects via a global event
82
+ window.dispatchEvent(new CustomEvent('lk-select-opened', { detail: registryInstance }))
83
+ }
84
+ })
85
+
86
+ function handleOtherSelectOpened(e: Event) {
87
+ const customEvent = e as CustomEvent
88
+ if (registryInstance && customEvent.detail !== registryInstance && open.value) {
89
+ setOpen(false)
90
+ }
91
+ }
92
+
93
+ // Click outside detection
94
+ function handleClickOutside(e: MouseEvent) {
95
+ if (
96
+ contentRef.value &&
97
+ !contentRef.value.contains(e.target as Node) &&
98
+ triggerRef.value &&
99
+ !triggerRef.value.contains(e.target as Node)
100
+ ) {
101
+ setOpen(false)
102
+ }
103
+ }
104
+
105
+ // Keyboard navigation
106
+ function handleKeyDown(e: KeyboardEvent) {
107
+ if (e.key === 'Escape' && open.value) {
108
+ setOpen(false)
109
+ triggerRef.value?.focus()
110
+ }
111
+ }
112
+
113
+ onMounted(() => {
114
+ document.addEventListener('mousedown', handleClickOutside)
115
+ document.addEventListener('keydown', handleKeyDown)
116
+ window.addEventListener('lk-select-opened', handleOtherSelectOpened)
117
+ })
118
+
119
+ onBeforeUnmount(() => {
120
+ document.removeEventListener('mousedown', handleClickOutside)
121
+ document.removeEventListener('keydown', handleKeyDown)
122
+ window.removeEventListener('lk-select-opened', handleOtherSelectOpened)
123
+ })
124
+
125
+ const optionsRef = computed(() => props.options)
126
+
127
+ provide(SELECT_INJECTION_KEY, {
128
+ open,
129
+ setOpen,
130
+ toggle,
131
+ triggerRef,
132
+ contentRef,
133
+ selectedValue,
134
+ setSelectedValue,
135
+ options: optionsRef,
136
+ name: props.name,
137
+ })
138
+ </script>
139
+
140
+ <template>
141
+ <div data-lk-component="select" v-bind="$attrs">
142
+ <!-- Hidden native select for form compatibility -->
143
+ <select
144
+ ref="hiddenSelectRef"
145
+ :name="name"
146
+ :value="selectedValue"
147
+ tabindex="-1"
148
+ aria-hidden="true"
149
+ style="position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
150
+ >
151
+ <option value="" disabled />
152
+ <option
153
+ v-for="option in options"
154
+ :key="option.value"
155
+ :value="option.value"
156
+ >
157
+ {{ option.label }}
158
+ </option>
159
+ </select>
160
+ <slot
161
+ :is-open="open"
162
+ :toggle="toggle"
163
+ :selected-value="selectedValue"
164
+ :set-selected-value="setSelectedValue"
165
+ />
166
+ </div>
167
+ </template>
@@ -0,0 +1,67 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, onMounted, onBeforeUnmount, inject } from 'vue'
3
+ import { SELECT_INJECTION_KEY, type LkSelectContext } from './LkSelect.vue'
4
+ import LkCard from './LkCard.vue'
5
+
6
+ export interface LkSelectMenuProps {
7
+ scaleFactor?: LkFontClass
8
+ }
9
+
10
+ const props = withDefaults(defineProps<LkSelectMenuProps>(), {
11
+ scaleFactor: 'body',
12
+ })
13
+
14
+ const ctx = inject<LkSelectContext>(SELECT_INJECTION_KEY)
15
+ if (!ctx) {
16
+ throw new Error('LkSelectMenu must be used inside an LkSelect component')
17
+ }
18
+
19
+ const { open, triggerRef, contentRef } = ctx
20
+ const localContentRef = ref<HTMLDivElement | null>(null)
21
+ const positionStyle = ref<Record<string, string>>({})
22
+
23
+ function calculatePosition() {
24
+ if (!triggerRef.value) return
25
+
26
+ const rect = triggerRef.value.getBoundingClientRect()
27
+ positionStyle.value = {
28
+ top: `${rect.bottom + window.scrollY}px`,
29
+ left: `${rect.left + window.scrollX}px`,
30
+ }
31
+ }
32
+
33
+ watch(open, (isOpen) => {
34
+ if (isOpen) {
35
+ calculatePosition()
36
+ }
37
+ })
38
+
39
+ onMounted(() => {
40
+ if (localContentRef.value) {
41
+ contentRef.value = localContentRef.value
42
+ }
43
+ })
44
+
45
+ onBeforeUnmount(() => {
46
+ contentRef.value = null
47
+ })
48
+ </script>
49
+
50
+ <template>
51
+ <Teleport to="body">
52
+ <div
53
+ v-if="open && triggerRef"
54
+ ref="localContentRef"
55
+ :style="positionStyle"
56
+ role="menu"
57
+ data-lk-component="select-menu"
58
+ :data-isactive="open"
59
+ >
60
+ <LkCard class="shadow-xl" :scale-factor="scaleFactor">
61
+ <div data-lk-component="column" data-lk-column-gap="none" :class="scaleFactor">
62
+ <slot />
63
+ </div>
64
+ </LkCard>
65
+ </div>
66
+ </Teleport>
67
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from 'vue'
3
+ import { SELECT_INJECTION_KEY, type LkSelectContext } from './LkSelect.vue'
4
+
5
+ export interface LkSelectOptionProps {
6
+ value: string
7
+ }
8
+
9
+ const props = defineProps<LkSelectOptionProps>()
10
+
11
+ const ctx = inject<LkSelectContext>(SELECT_INJECTION_KEY)
12
+ if (!ctx) {
13
+ throw new Error('LkSelectOption must be used inside an LkSelect component')
14
+ }
15
+
16
+ const { selectedValue, setSelectedValue } = ctx
17
+
18
+ const isSelected = computed(() => selectedValue.value === props.value)
19
+
20
+ function handleClick() {
21
+ setSelectedValue(props.value)
22
+ }
23
+
24
+ function handleKeyDown(e: KeyboardEvent) {
25
+ if (e.key === 'Enter' || e.key === ' ') {
26
+ e.preventDefault()
27
+ handleClick()
28
+ }
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div
34
+ role="menuitem"
35
+ data-lk-component="menu-item"
36
+ :tabindex="0"
37
+ :data-selected="isSelected"
38
+ :style="{
39
+ cursor: 'pointer',
40
+ fontWeight: isSelected ? 'bold' : 'normal',
41
+ }"
42
+ class="select-option"
43
+ @click="handleClick"
44
+ @keydown="handleKeyDown"
45
+ >
46
+ <slot name="startIcon" />
47
+ <p data-lk-menu-item-element="content-wrap">
48
+ <slot />
49
+ </p>
50
+ <slot name="endIcon" />
51
+ <div
52
+ data-lk-component="state-layer"
53
+ :data-lk-state-layer-forced-state="isSelected ? 'active' : undefined"
54
+ />
55
+ </div>
56
+ </template>
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ import { inject, ref, onMounted, onBeforeUnmount } from 'vue'
3
+ import { SELECT_INJECTION_KEY, type LkSelectContext } from './LkSelect.vue'
4
+
5
+ const ctx = inject<LkSelectContext>(SELECT_INJECTION_KEY)
6
+ if (!ctx) {
7
+ throw new Error('LkSelectTrigger must be used inside an LkSelect component')
8
+ }
9
+
10
+ const { open, toggle, triggerRef } = ctx
11
+ const localRef = ref<HTMLElement | null>(null)
12
+
13
+ onMounted(() => {
14
+ if (localRef.value) {
15
+ const firstChild = localRef.value.firstElementChild as HTMLElement | null
16
+ if (firstChild) {
17
+ triggerRef.value = firstChild
18
+ } else {
19
+ triggerRef.value = localRef.value
20
+ }
21
+ }
22
+ })
23
+
24
+ onBeforeUnmount(() => {
25
+ triggerRef.value = null
26
+ })
27
+
28
+ function handleClick() {
29
+ toggle()
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <div
35
+ ref="localRef"
36
+ data-lk-component="select-trigger"
37
+ :aria-expanded="open"
38
+ aria-haspopup="menu"
39
+ @click="handleClick"
40
+ >
41
+ <slot />
42
+ </div>
43
+ </template>
@@ -0,0 +1,127 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch, onMounted, onUnmounted, useAttrs } from 'vue'
3
+ import { propsToDataAttrs } from '../utils/utilities'
4
+ import { getOnToken } from '../utils/colorUtils'
5
+ import LkCard from './LkCard.vue'
6
+
7
+ export interface LkSnackbarProps {
8
+ globalColor?: LkColorWithOnToken
9
+ message?: string
10
+ modelValue?: boolean
11
+ autoDismiss?: number
12
+ }
13
+
14
+ const props = withDefaults(defineProps<LkSnackbarProps>(), {
15
+ message: 'Notification text goes here.',
16
+ modelValue: true,
17
+ autoDismiss: 0,
18
+ })
19
+
20
+ const emit = defineEmits<{
21
+ 'update:modelValue': [value: boolean]
22
+ }>()
23
+
24
+ defineOptions({
25
+ inheritAttrs: false,
26
+ })
27
+
28
+ const attrs = useAttrs()
29
+
30
+ const isVisible = ref(props.modelValue)
31
+ let dismissTimer: ReturnType<typeof setTimeout> | null = null
32
+
33
+ watch(
34
+ () => props.modelValue,
35
+ (newVal) => {
36
+ isVisible.value = newVal
37
+ if (newVal && props.autoDismiss > 0) {
38
+ startDismissTimer()
39
+ }
40
+ },
41
+ )
42
+
43
+ function startDismissTimer() {
44
+ clearDismissTimer()
45
+ if (props.autoDismiss > 0) {
46
+ dismissTimer = setTimeout(() => {
47
+ isVisible.value = false
48
+ emit('update:modelValue', false)
49
+ }, props.autoDismiss)
50
+ }
51
+ }
52
+
53
+ function clearDismissTimer() {
54
+ if (dismissTimer) {
55
+ clearTimeout(dismissTimer)
56
+ dismissTimer = null
57
+ }
58
+ }
59
+
60
+ onMounted(() => {
61
+ if (isVisible.value && props.autoDismiss > 0) {
62
+ startDismissTimer()
63
+ }
64
+ })
65
+
66
+ onUnmounted(() => {
67
+ clearDismissTimer()
68
+ })
69
+
70
+ const dataAttrs = computed(() =>
71
+ propsToDataAttrs(
72
+ {
73
+ globalColor: props.globalColor,
74
+ message: props.message,
75
+ },
76
+ 'snackbar',
77
+ ),
78
+ )
79
+
80
+ const onColorToken = computed(() => {
81
+ if (!props.globalColor) return undefined
82
+ return getOnToken(props.globalColor as LkColor)
83
+ })
84
+ </script>
85
+
86
+ <template>
87
+ <Teleport to="body">
88
+ <div
89
+ v-if="isVisible"
90
+ data-lk-component="snackbar"
91
+ v-bind="{ ...dataAttrs, ...attrs }"
92
+ >
93
+ <LkCard
94
+ :scale-factor="$slots.icon ? 'subheading' : 'body'"
95
+ :bg-color="globalColor"
96
+ :optical-correction="$slots.icon ? 'none' : 'y'"
97
+ class="shadow-sm"
98
+ >
99
+ <div data-lk-component="row" data-lk-row-align-items="center">
100
+ <!-- Icon slot -->
101
+ <div v-if="$slots.icon" data-lk-slot="snackbar-icon">
102
+ <slot name="icon" :on-color="onColorToken" />
103
+ </div>
104
+
105
+ <!-- Message / default content slot -->
106
+ <div data-lk-slot="snackbar-text">
107
+ <slot :on-color="onColorToken" :global-color="globalColor">
108
+ <p>{{ message }}</p>
109
+ </slot>
110
+ </div>
111
+
112
+ <div data-lk-component="row">
113
+ <!-- Action buttons slot -->
114
+ <div v-if="$slots.action" data-lk-slot="snackbar-actions">
115
+ <slot name="action" :on-color="onColorToken" :global-color="globalColor" />
116
+ </div>
117
+
118
+ <!-- Icon action buttons (dismiss etc.) slot -->
119
+ <div v-if="$slots.iconAction" data-lk-slot="snackbar-icon-actions">
120
+ <slot name="iconAction" :on-color="onColorToken" :global-color="globalColor" />
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </LkCard>
125
+ </div>
126
+ </Teleport>
127
+ </template>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ interface LkStateLayerProps {
5
+ bgColor?: LkColor | 'currentColor'
6
+ forcedState?: 'hover' | 'active' | 'focus'
7
+ }
8
+
9
+ const props = withDefaults(defineProps<LkStateLayerProps>(), {
10
+ bgColor: 'currentColor',
11
+ })
12
+
13
+ defineOptions({
14
+ inheritAttrs: false,
15
+ })
16
+
17
+ const layerClass = computed(() =>
18
+ props.bgColor !== 'currentColor' ? `bg-${props.bgColor}` : '',
19
+ )
20
+
21
+ const layerStyle = computed(() =>
22
+ props.bgColor === 'currentColor' ? { backgroundColor: 'currentColor' } : {},
23
+ )
24
+ </script>
25
+
26
+ <template>
27
+ <div
28
+ data-lk-component="state-layer"
29
+ :class="layerClass"
30
+ :style="layerStyle"
31
+ v-bind="props.forcedState ? { 'data-lk-forced-state': props.forcedState } : {}"
32
+ ></div>
33
+ </template>
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { getOnToken } from '../utils/colorUtils'
4
+
5
+ interface LkStickerProps {
6
+ fontClass?: LkFontClass
7
+ bgColor?: LkColor
8
+ class?: string
9
+ }
10
+
11
+ const props = withDefaults(defineProps<LkStickerProps>(), {
12
+ fontClass: 'label',
13
+ bgColor: 'primarycontainer',
14
+ })
15
+
16
+ defineOptions({
17
+ inheritAttrs: false,
18
+ })
19
+
20
+ const onColor = computed(() => getOnToken(props.bgColor))
21
+
22
+ const stickerClass = computed(() => {
23
+ const classes: string[] = [
24
+ `bg-${props.bgColor}`,
25
+ `color-${onColor.value}`,
26
+ ]
27
+ if (props.class) classes.push(props.class)
28
+ return classes.join(' ')
29
+ })
30
+ </script>
31
+
32
+ <template>
33
+ <div
34
+ data-lk-component="sticker"
35
+ :class="stickerClass"
36
+ v-bind="$attrs"
37
+ >
38
+ <LkText :font-class="props.fontClass">
39
+ <slot>Sticker</slot>
40
+ </LkText>
41
+ </div>
42
+ </template>
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { getOnToken } from '../utils/colorUtils'
4
+
5
+ export interface LkSwitchProps {
6
+ modelValue?: boolean
7
+ offColor?: LkColorWithOnToken
8
+ onColor?: LkColorWithOnToken
9
+ }
10
+
11
+ const props = withDefaults(defineProps<LkSwitchProps>(), {
12
+ modelValue: false,
13
+ offColor: 'surfacevariant',
14
+ onColor: 'primary',
15
+ })
16
+
17
+ const emit = defineEmits<{
18
+ 'update:modelValue': [value: boolean]
19
+ }>()
20
+
21
+ defineOptions({
22
+ inheritAttrs: false,
23
+ })
24
+
25
+ const switchThumbOffColor = computed(() => getOnToken(props.offColor))
26
+ const switchThumbOnColor = computed(() => getOnToken(props.onColor))
27
+
28
+ function handleClick() {
29
+ emit('update:modelValue', !props.modelValue)
30
+ }
31
+
32
+ const switchState = computed(() => (props.modelValue ? 'on' : 'off'))
33
+
34
+ const switchStyles = computed(() => ({
35
+ '--switch-off-color': `var(--lk-${props.offColor})`,
36
+ '--switch-on-color': `var(--lk-${props.onColor})`,
37
+ '--switch-thumb-off-color': `var(--lk-${switchThumbOffColor.value})`,
38
+ '--switch-thumb-on-color': `var(--lk-${switchThumbOnColor.value})`,
39
+ }))
40
+ </script>
41
+
42
+ <template>
43
+ <div
44
+ data-lk-component="switch"
45
+ v-bind="{ ...$attrs, style: switchStyles }"
46
+ :data-lk-switch-state="switchState"
47
+ @click="handleClick"
48
+ >
49
+ <div
50
+ data-lk-component="switch-thumb"
51
+ :data-lk-switch-state="switchState"
52
+ />
53
+ </div>
54
+ </template>
55
+
56
+ <style scoped>
57
+ [data-lk-component="switch"] {
58
+ position: relative;
59
+ display: block;
60
+ align-items: center;
61
+ justify-content: start;
62
+ width: var(--lk-size-xl);
63
+ height: calc(var(--lk-size-md) + calc(var(--lk-size-2xs) * 2));
64
+ background-color: var(--switch-off-color);
65
+ border-radius: 100em;
66
+ cursor: pointer;
67
+ }
68
+
69
+ [data-lk-component="switch"][data-lk-switch-state="on"] {
70
+ background-color: var(--switch-on-color);
71
+ }
72
+
73
+ [data-lk-component="switch-thumb"] {
74
+ position: absolute;
75
+ top: 50%;
76
+ left: var(--lk-size-2xs);
77
+ right: auto;
78
+ width: var(--lk-size-md);
79
+ height: var(--lk-size-md);
80
+ border-radius: 50%;
81
+ background-color: var(--switch-thumb-off-color);
82
+ transform: translateY(-50%);
83
+ transition: left 0.1s ease-out;
84
+ }
85
+
86
+ [data-lk-component="switch-thumb"][data-lk-switch-state="on"] {
87
+ left: calc(100% - calc(var(--lk-size-md) + var(--lk-size-2xs)));
88
+ background-color: var(--switch-thumb-on-color);
89
+ }
90
+ </style>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from 'vue'
3
+ import type { Ref } from 'vue'
4
+
5
+ export interface LkTabContentProps {
6
+ index: number
7
+ }
8
+
9
+ const props = defineProps<LkTabContentProps>()
10
+
11
+ defineOptions({
12
+ inheritAttrs: false,
13
+ })
14
+
15
+ const tabsContext = inject<{ activeTab: Ref<number> } | null>('lk-tabs', null)
16
+
17
+ const isVisible = computed(() => {
18
+ if (tabsContext) {
19
+ return tabsContext.activeTab.value === props.index
20
+ }
21
+ return true
22
+ })
23
+ </script>
24
+
25
+ <template>
26
+ <div
27
+ v-show="isVisible"
28
+ data-lk-component="tab-content"
29
+ v-bind="$attrs"
30
+ >
31
+ <slot />
32
+ </div>
33
+ </template>