@saasmakers/ui 1.4.51 → 1.4.53
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.
- package/app/components/bases/BaseBordered.vue +0 -1
- package/app/components/bases/BaseCard.vue +59 -18
- package/app/components/bases/BaseDivider.vue +0 -2
- package/app/components/bases/BaseIcon.vue +0 -2
- package/app/components/bases/BaseSpinner.vue +0 -2
- package/app/components/bases/BaseText.vue +11 -13
- package/app/components/bases/BaseToast.vue +2 -4
- package/app/components/fields/FieldLabel.vue +15 -2
- package/app/components/fields/FieldMessage.vue +14 -1
- package/app/components/layout/LayoutBottomSheet.vue +3 -141
- package/app/components/layout/LayoutModal.vue +65 -0
- package/app/composables/useDialog.ts +144 -0
- package/app/types/bases.d.ts +4 -3
- package/app/types/global.d.ts +2 -0
- package/app/types/layout.d.ts +4 -0
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ const props = withDefaults(defineProps<BaseCard>(), {
|
|
|
10
10
|
details: '',
|
|
11
11
|
detailsIcon: undefined,
|
|
12
12
|
direction: 'horizontal',
|
|
13
|
+
disabled: false,
|
|
13
14
|
emoji: undefined,
|
|
14
15
|
hasBackground: true,
|
|
15
16
|
hasChevron: false,
|
|
@@ -17,6 +18,7 @@ const props = withDefaults(defineProps<BaseCard>(), {
|
|
|
17
18
|
id: undefined,
|
|
18
19
|
image: '',
|
|
19
20
|
isSelected: false,
|
|
21
|
+
size: 'base',
|
|
20
22
|
title: undefined,
|
|
21
23
|
titleIcon: undefined,
|
|
22
24
|
to: undefined,
|
|
@@ -43,25 +45,32 @@ const hasAvatarBox = computed<boolean>(() => {
|
|
|
43
45
|
})
|
|
44
46
|
|
|
45
47
|
const isClickable = computed(() => {
|
|
46
|
-
return props.to || props.clickable
|
|
48
|
+
return !props.disabled && (props.to || props.clickable)
|
|
47
49
|
})
|
|
48
50
|
|
|
49
51
|
function onClick(event: MouseEvent) {
|
|
52
|
+
if (props.disabled) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
emit('click', event, props.id)
|
|
51
57
|
}
|
|
52
58
|
</script>
|
|
53
59
|
|
|
54
60
|
<template>
|
|
55
61
|
<component
|
|
56
|
-
:is="to ? NuxtLinkLocale : 'div'"
|
|
62
|
+
:is="to && !disabled ? NuxtLinkLocale : 'div'"
|
|
57
63
|
class="group flex flex-col"
|
|
58
64
|
:class="{
|
|
59
65
|
'cursor-pointer': isClickable,
|
|
60
|
-
'
|
|
66
|
+
'pointer-events-none': disabled,
|
|
67
|
+
'opacity-50': disabled,
|
|
68
|
+
'border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm p-1.5 pr-2.5': hasBackground && size === 'base',
|
|
69
|
+
'border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm p-2.5 pr-4': hasBackground && size === 'lg',
|
|
61
70
|
'hover:border-gray-300 dark:hover:border-gray-700': hasBackground && isClickable,
|
|
62
71
|
'rounded-xl': hasBackground,
|
|
63
72
|
}"
|
|
64
|
-
:to="to"
|
|
73
|
+
:to="disabled ? undefined : to"
|
|
65
74
|
@click="onClick"
|
|
66
75
|
>
|
|
67
76
|
<div
|
|
@@ -76,20 +85,28 @@ function onClick(event: MouseEvent) {
|
|
|
76
85
|
|
|
77
86
|
<span
|
|
78
87
|
v-if="hasAvatarBox"
|
|
79
|
-
class="relative z-10
|
|
88
|
+
class="relative z-10 flex items-center justify-center flex-initial"
|
|
80
89
|
:class="{
|
|
81
90
|
'border shadow-inner': !avatar && !image,
|
|
82
91
|
'rounded-lg': !avatar && !image,
|
|
83
92
|
'border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-900': !avatar && !image,
|
|
84
|
-
'mr-2': direction === 'horizontal',
|
|
85
|
-
'
|
|
93
|
+
'mr-2': direction === 'horizontal' && size === 'base',
|
|
94
|
+
'mr-3': direction === 'horizontal' && size === 'lg',
|
|
95
|
+
'mb-1': direction === 'vertical' && size === 'base',
|
|
96
|
+
'mb-1.5': direction === 'vertical' && size === 'lg',
|
|
97
|
+
'h-9 w-9': size === 'base',
|
|
98
|
+
'h-11 w-11': size === 'lg',
|
|
86
99
|
}"
|
|
87
100
|
>
|
|
88
101
|
<slot name="innerBoxLeft" />
|
|
89
102
|
|
|
90
103
|
<BaseAvatar
|
|
91
104
|
v-if="avatar"
|
|
92
|
-
class="
|
|
105
|
+
class="rounded-lg flex-initial"
|
|
106
|
+
:class="{
|
|
107
|
+
'h-10 w-10': size === 'base',
|
|
108
|
+
'h-12 w-12': size === 'lg',
|
|
109
|
+
}"
|
|
93
110
|
:src="avatar"
|
|
94
111
|
/>
|
|
95
112
|
|
|
@@ -98,18 +115,26 @@ function onClick(event: MouseEvent) {
|
|
|
98
115
|
class="m-0.5"
|
|
99
116
|
:emoji="emoji"
|
|
100
117
|
:has-box="false"
|
|
118
|
+
:size="size === 'lg' ? 'lg' : 'base'"
|
|
101
119
|
/>
|
|
102
120
|
|
|
103
121
|
<BaseIcon
|
|
104
122
|
v-else-if="icon"
|
|
105
|
-
class="
|
|
123
|
+
:class="{
|
|
124
|
+
'text-lg': size === 'base',
|
|
125
|
+
'text-2xl': size === 'lg',
|
|
126
|
+
}"
|
|
106
127
|
:color="color"
|
|
107
128
|
:icon="icon"
|
|
108
129
|
/>
|
|
109
130
|
|
|
110
131
|
<img
|
|
111
132
|
v-else-if="image"
|
|
112
|
-
class="
|
|
133
|
+
class="rounded-lg object-cover shadow-sm drag-none flex-initial"
|
|
134
|
+
:class="{
|
|
135
|
+
'h-9 w-9': size === 'base',
|
|
136
|
+
'h-11 w-11': size === 'lg',
|
|
137
|
+
}"
|
|
113
138
|
:alt="title"
|
|
114
139
|
loading="lazy"
|
|
115
140
|
:src="image"
|
|
@@ -124,17 +149,24 @@ function onClick(event: MouseEvent) {
|
|
|
124
149
|
<div
|
|
125
150
|
class="min-w-0 flex flex-1 flex-col justify-center leading-snug"
|
|
126
151
|
:class="{
|
|
127
|
-
'mr-4': direction === 'horizontal',
|
|
152
|
+
'mr-4': direction === 'horizontal' && size === 'base',
|
|
153
|
+
'mr-5': direction === 'horizontal' && size === 'lg',
|
|
128
154
|
'items-center': direction === 'vertical',
|
|
129
155
|
}"
|
|
130
156
|
>
|
|
131
|
-
<div
|
|
157
|
+
<div
|
|
158
|
+
class="w-full flex flex-col"
|
|
159
|
+
:class="{
|
|
160
|
+
'gap-0': size === 'base',
|
|
161
|
+
'gap-0.5': size === 'lg',
|
|
162
|
+
}"
|
|
163
|
+
>
|
|
132
164
|
<BaseIcon
|
|
133
165
|
v-if="title"
|
|
134
166
|
class="w-full"
|
|
135
167
|
:class="{ 'self-center': !hasAvatarBox }"
|
|
136
168
|
:icon="titleIcon"
|
|
137
|
-
size="base"
|
|
169
|
+
:size="size === 'lg' ? 'base' : 'sm'"
|
|
138
170
|
:text="title"
|
|
139
171
|
truncate
|
|
140
172
|
/>
|
|
@@ -142,8 +174,8 @@ function onClick(event: MouseEvent) {
|
|
|
142
174
|
<BaseText
|
|
143
175
|
v-if="description"
|
|
144
176
|
block
|
|
145
|
-
class="text-gray-600
|
|
146
|
-
size="2xs"
|
|
177
|
+
class="text-gray-600 dark:text-gray-400"
|
|
178
|
+
:size="size === 'lg' ? 'xs' : '2xs'"
|
|
147
179
|
:text="description"
|
|
148
180
|
/>
|
|
149
181
|
</div>
|
|
@@ -155,7 +187,7 @@ function onClick(event: MouseEvent) {
|
|
|
155
187
|
v-if="details"
|
|
156
188
|
class="mt-0.5 whitespace-nowrap text-gray-700 font-semibold tracking-tighter dark:text-gray-300"
|
|
157
189
|
:icon="detailsIcon"
|
|
158
|
-
size="2xs"
|
|
190
|
+
:size="size === 'lg' ? 'xs' : '2xs'"
|
|
159
191
|
:text="details"
|
|
160
192
|
/>
|
|
161
193
|
</div>
|
|
@@ -163,14 +195,23 @@ function onClick(event: MouseEvent) {
|
|
|
163
195
|
|
|
164
196
|
<BaseIcon
|
|
165
197
|
v-if="isSelected"
|
|
166
|
-
class="mr-1.5 self-center
|
|
198
|
+
class="mr-1.5 self-center flex-initial"
|
|
199
|
+
:class="{
|
|
200
|
+
'text-2xl': size === 'base',
|
|
201
|
+
'text-3xl': size === 'lg',
|
|
202
|
+
}"
|
|
167
203
|
color="green"
|
|
168
204
|
:icon="getIcon('checkCircle')"
|
|
169
205
|
/>
|
|
170
206
|
|
|
171
207
|
<BaseIcon
|
|
172
208
|
v-else-if="to || hasChevron"
|
|
173
|
-
class="self-center text-
|
|
209
|
+
class="self-center text-gray-500 flex-initial dark:text-gray-500"
|
|
210
|
+
:class="{
|
|
211
|
+
'group-hover:text-gray-900 dark:group-hover:text-gray-100': isClickable,
|
|
212
|
+
'text-xl': size === 'base',
|
|
213
|
+
'text-2xl': size === 'lg',
|
|
214
|
+
}"
|
|
174
215
|
:icon="getIcon('chevronRight')"
|
|
175
216
|
/>
|
|
176
217
|
|
|
@@ -63,7 +63,6 @@ function onNavigate(event: MouseEvent, direction: BaseDividerNavigateDirection)
|
|
|
63
63
|
:icon="getIcon('chevronLeft')"
|
|
64
64
|
size="xs"
|
|
65
65
|
:text="t('previous')"
|
|
66
|
-
:uppercase="false"
|
|
67
66
|
@click="onNavigate($event, 'previous')"
|
|
68
67
|
/>
|
|
69
68
|
|
|
@@ -102,7 +101,6 @@ function onNavigate(event: MouseEvent, direction: BaseDividerNavigateDirection)
|
|
|
102
101
|
:icon="getIcon('chevronRight')"
|
|
103
102
|
size="xs"
|
|
104
103
|
:text="t('next')"
|
|
105
|
-
:uppercase="false"
|
|
106
104
|
@click="onNavigate($event, 'next')"
|
|
107
105
|
/>
|
|
108
106
|
</div>
|
|
@@ -16,7 +16,6 @@ const props = withDefaults(defineProps<BaseIcon>(), {
|
|
|
16
16
|
to: undefined,
|
|
17
17
|
truncate: false,
|
|
18
18
|
underline: false,
|
|
19
|
-
uppercase: true,
|
|
20
19
|
})
|
|
21
20
|
|
|
22
21
|
const emit = defineEmits<{
|
|
@@ -154,7 +153,6 @@ function onKeyDown(event: KeyboardEvent) {
|
|
|
154
153
|
:text="confirming ? t('confirm') : text"
|
|
155
154
|
:truncate="truncate"
|
|
156
155
|
:underline="underline"
|
|
157
|
-
:uppercase="uppercase"
|
|
158
156
|
/>
|
|
159
157
|
</div>
|
|
160
158
|
</template>
|
|
@@ -8,7 +8,6 @@ const props = withDefaults(defineProps<BaseSpinner>(), {
|
|
|
8
8
|
reverse: false,
|
|
9
9
|
size: 'base',
|
|
10
10
|
text: '',
|
|
11
|
-
uppercase: true,
|
|
12
11
|
})
|
|
13
12
|
|
|
14
13
|
const emit = defineEmits<{
|
|
@@ -80,7 +79,6 @@ function onKeyDown(event: KeyboardEvent) {
|
|
|
80
79
|
:reverse="reverse"
|
|
81
80
|
:size="size"
|
|
82
81
|
:text="text"
|
|
83
|
-
:uppercase="uppercase"
|
|
84
82
|
/>
|
|
85
83
|
</div>
|
|
86
84
|
</template>
|
|
@@ -16,7 +16,6 @@ const props = withDefaults(defineProps<BaseText>(), {
|
|
|
16
16
|
to: undefined,
|
|
17
17
|
truncate: false,
|
|
18
18
|
underline: false,
|
|
19
|
-
uppercase: false,
|
|
20
19
|
})
|
|
21
20
|
|
|
22
21
|
const emit = defineEmits<{
|
|
@@ -81,7 +80,7 @@ function onShowMore() {
|
|
|
81
80
|
'bg-white dark:bg-gray-900': background === 'white',
|
|
82
81
|
'block': block,
|
|
83
82
|
'font-bold': bold,
|
|
84
|
-
'font-medium':
|
|
83
|
+
'font-medium': !bold,
|
|
85
84
|
'ml-0.5': ['3xs'].includes(size) && !reverse && hasMargin,
|
|
86
85
|
'ml-1.5': ['2xs', 'xs', 'sm'].includes(size) && !reverse && hasMargin,
|
|
87
86
|
'ml-2.5': ['xl', '2xl'].includes(size) && !reverse && hasMargin,
|
|
@@ -92,23 +91,22 @@ function onShowMore() {
|
|
|
92
91
|
'mr-2.5': ['xl', '2xl'].includes(size) && reverse && hasMargin,
|
|
93
92
|
'mr-2': ['base', 'lg'].includes(size) && reverse && hasMargin,
|
|
94
93
|
'mr-3': ['3xl', '4xl'].includes(size) && reverse && hasMargin,
|
|
95
|
-
'text-2xl':
|
|
96
|
-
'text-2xs':
|
|
97
|
-
'text-3xl':
|
|
98
|
-
'text-3xs':
|
|
99
|
-
'text-4xl': size === '4xl'
|
|
100
|
-
'text-base':
|
|
94
|
+
'text-2xl': size === '2xl',
|
|
95
|
+
'text-2xs': size === '2xs',
|
|
96
|
+
'text-3xl': size === '3xl',
|
|
97
|
+
'text-3xs': size === '3xs',
|
|
98
|
+
'text-4xl': size === '4xl',
|
|
99
|
+
'text-base': size === 'base',
|
|
101
100
|
'text-center': alignment === 'center',
|
|
102
101
|
'text-left': alignment === 'left',
|
|
103
|
-
'text-lg':
|
|
102
|
+
'text-lg': size === 'lg',
|
|
104
103
|
'text-right': alignment === 'right',
|
|
105
|
-
'text-sm':
|
|
106
|
-
'text-xl':
|
|
107
|
-
'text-xs':
|
|
104
|
+
'text-sm': size === 'sm',
|
|
105
|
+
'text-xl': size === 'xl',
|
|
106
|
+
'text-xs': size === 'xs',
|
|
108
107
|
'truncate': truncate,
|
|
109
108
|
'underline text-indigo-700 dark:text-indigo-300': !!to,
|
|
110
109
|
'underline': underline,
|
|
111
|
-
'uppercase': uppercase,
|
|
112
110
|
'whitespace-nowrap': noWrap,
|
|
113
111
|
}"
|
|
114
112
|
:to="to"
|
|
@@ -31,7 +31,7 @@ function onClose(event: KeyboardEvent | MouseEvent) {
|
|
|
31
31
|
|
|
32
32
|
<template>
|
|
33
33
|
<div
|
|
34
|
-
class="group flex select-none items-center border border-gray-200 rounded-full bg-white px-3 py-2 text-base text-gray-800 font-normal shadow-sm transition-colors dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
|
34
|
+
class="group flex select-none items-center border border-gray-200 rounded-full bg-white px-3 py-2 text-base text-gray-800 font-normal uppercase shadow-sm transition-colors dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
|
35
35
|
:class="{ 'cursor-pointer': hasClose }"
|
|
36
36
|
role="button"
|
|
37
37
|
tabindex="0"
|
|
@@ -47,7 +47,7 @@ function onClose(event: KeyboardEvent | MouseEvent) {
|
|
|
47
47
|
|
|
48
48
|
<button
|
|
49
49
|
v-if="action"
|
|
50
|
-
class="ml-
|
|
50
|
+
class="ml-2 min-h-6 inline-flex items-center justify-center gap-1.25 rounded-md px-2 py-0.5 uppercase transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-800"
|
|
51
51
|
:class="{
|
|
52
52
|
'bg-red-100 text-red-700 hover:bg-red-200 focus-visible:ring-red-500 dark:bg-red-400/15 dark:text-red-400 dark:hover:bg-red-400/25': status === 'error',
|
|
53
53
|
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 focus-visible:ring-indigo-500 dark:bg-indigo-400/15 dark:text-indigo-400 dark:hover:bg-indigo-400/25': status === 'info',
|
|
@@ -58,12 +58,10 @@ function onClose(event: KeyboardEvent | MouseEvent) {
|
|
|
58
58
|
@click.stop="onAction"
|
|
59
59
|
>
|
|
60
60
|
<BaseText
|
|
61
|
-
bold
|
|
62
61
|
class="pointer-events-none leading-none"
|
|
63
62
|
no-wrap
|
|
64
63
|
size="sm"
|
|
65
64
|
:text="action.label"
|
|
66
|
-
uppercase
|
|
67
65
|
/>
|
|
68
66
|
|
|
69
67
|
<BaseShortcut
|
|
@@ -23,6 +23,19 @@ const isClickable = computed(() => {
|
|
|
23
23
|
return !props.loading && !props.disabled
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
+
const textSize = computed<BaseSize>(() => {
|
|
27
|
+
switch (props.size) {
|
|
28
|
+
case 'lg':
|
|
29
|
+
return 'base'
|
|
30
|
+
case 'sm':
|
|
31
|
+
return 'xs'
|
|
32
|
+
case 'xs':
|
|
33
|
+
return '2xs'
|
|
34
|
+
default:
|
|
35
|
+
return 'sm'
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
26
39
|
function onClick(event: MouseEvent) {
|
|
27
40
|
emit('click', event)
|
|
28
41
|
}
|
|
@@ -71,14 +84,14 @@ function onKeyDown(event: KeyboardEvent) {
|
|
|
71
84
|
>
|
|
72
85
|
<BaseIcon
|
|
73
86
|
:icon="icon"
|
|
74
|
-
:size="
|
|
87
|
+
:size="textSize"
|
|
75
88
|
:text="label"
|
|
76
89
|
/>
|
|
77
90
|
|
|
78
91
|
<BaseText
|
|
79
92
|
v-if="required"
|
|
80
93
|
class="ml-1 text-red-700 dark:text-red-300"
|
|
81
|
-
:size="
|
|
94
|
+
:size="textSize"
|
|
82
95
|
text="*"
|
|
83
96
|
/>
|
|
84
97
|
</component>
|
|
@@ -120,6 +120,19 @@ function ruleIsInvalid(rule: unknown) {
|
|
|
120
120
|
|
|
121
121
|
return false
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
const textSize = computed<BaseSize>(() => {
|
|
125
|
+
switch (props.size) {
|
|
126
|
+
case 'lg':
|
|
127
|
+
return 'sm'
|
|
128
|
+
case 'sm':
|
|
129
|
+
return 'xs'
|
|
130
|
+
case 'xs':
|
|
131
|
+
return '2xs'
|
|
132
|
+
default:
|
|
133
|
+
return 'xs'
|
|
134
|
+
}
|
|
135
|
+
})
|
|
123
136
|
</script>
|
|
124
137
|
|
|
125
138
|
<template>
|
|
@@ -134,7 +147,7 @@ function ruleIsInvalid(rule: unknown) {
|
|
|
134
147
|
'mt-3.5': size === 'lg',
|
|
135
148
|
}"
|
|
136
149
|
:color="status === 'error' ? 'red' : 'gray'"
|
|
137
|
-
size="
|
|
150
|
+
:size="textSize"
|
|
138
151
|
:text="text"
|
|
139
152
|
/>
|
|
140
153
|
</template>
|
|
@@ -9,11 +9,10 @@ defineSlots<{
|
|
|
9
9
|
|
|
10
10
|
const closeThresholdRatio = 0.25
|
|
11
11
|
const dragMoveThreshold = 8
|
|
12
|
-
const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
13
12
|
const visible = defineModel<boolean>({ default: false })
|
|
14
13
|
const contentRef = ref<HTMLElement>()
|
|
15
|
-
const focusTrigger = ref<HTMLElement>()
|
|
16
14
|
const panelRef = ref<HTMLElement>()
|
|
15
|
+
const { release: onAfterLeave } = useDialog(visible, panelRef, { ignoreClass: 'layout-bottom-sheet' })
|
|
17
16
|
|
|
18
17
|
const drag = reactive({
|
|
19
18
|
closing: false,
|
|
@@ -24,56 +23,10 @@ const drag = reactive({
|
|
|
24
23
|
startY: 0,
|
|
25
24
|
})
|
|
26
25
|
|
|
27
|
-
let savedBodyOverflow = ''
|
|
28
|
-
let savedBodyPaddingRight = ''
|
|
29
|
-
|
|
30
|
-
const fixedElementPadding = new Map<HTMLElement, string>()
|
|
31
|
-
|
|
32
26
|
const closeThreshold = computed(() => {
|
|
33
27
|
return (panelRef.value?.offsetHeight ?? 320) * closeThresholdRatio
|
|
34
28
|
})
|
|
35
29
|
|
|
36
|
-
function getFocusableElements(container: HTMLElement) {
|
|
37
|
-
return Array.from(container.querySelectorAll<HTMLElement>(focusableSelector))
|
|
38
|
-
.filter((element) => {
|
|
39
|
-
return element.offsetParent !== null || getComputedStyle(element).position === 'fixed'
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function lockBodyScroll() {
|
|
44
|
-
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
|
|
45
|
-
|
|
46
|
-
savedBodyOverflow = document.body.style.overflow
|
|
47
|
-
savedBodyPaddingRight = document.body.style.paddingRight
|
|
48
|
-
document.body.style.overflow = 'hidden'
|
|
49
|
-
|
|
50
|
-
if (scrollbarWidth > 0) {
|
|
51
|
-
document.body.style.paddingRight = `${scrollbarWidth}px`
|
|
52
|
-
|
|
53
|
-
for (const element of document.querySelectorAll('body *')) {
|
|
54
|
-
if (!(element instanceof HTMLElement)) {
|
|
55
|
-
continue
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (element.classList.contains('layout-bottom-sheet')) {
|
|
59
|
-
continue
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const { position } = getComputedStyle(element)
|
|
63
|
-
|
|
64
|
-
if (position !== 'fixed' && position !== 'sticky') {
|
|
65
|
-
continue
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
fixedElementPadding.set(element, element.style.paddingRight)
|
|
69
|
-
|
|
70
|
-
const currentPadding = Number.parseFloat(getComputedStyle(element).paddingRight) || 0
|
|
71
|
-
|
|
72
|
-
element.style.paddingRight = `${currentPadding + scrollbarWidth}px`
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
30
|
function onClose() {
|
|
78
31
|
visible.value = false
|
|
79
32
|
}
|
|
@@ -92,34 +45,6 @@ function onContentPointerDown(event: PointerEvent) {
|
|
|
92
45
|
drag.startY = event.clientY
|
|
93
46
|
}
|
|
94
47
|
|
|
95
|
-
function onFocusTrap(event: KeyboardEvent) {
|
|
96
|
-
if (event.key !== 'Tab' || !panelRef.value) {
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const focusableElements = getFocusableElements(panelRef.value)
|
|
101
|
-
|
|
102
|
-
if (focusableElements.length === 0) {
|
|
103
|
-
event.preventDefault()
|
|
104
|
-
panelRef.value.focus()
|
|
105
|
-
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const firstElement = focusableElements[0]
|
|
110
|
-
const lastElement = focusableElements[focusableElements.length - 1]
|
|
111
|
-
const activeElement = document.activeElement
|
|
112
|
-
|
|
113
|
-
if (event.shiftKey && activeElement === firstElement) {
|
|
114
|
-
event.preventDefault()
|
|
115
|
-
lastElement.focus()
|
|
116
|
-
}
|
|
117
|
-
else if (!event.shiftKey && activeElement === lastElement) {
|
|
118
|
-
event.preventDefault()
|
|
119
|
-
firstElement.focus()
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
48
|
function onHeaderPointerDown(event: PointerEvent) {
|
|
124
49
|
if (drag.closing) {
|
|
125
50
|
return
|
|
@@ -136,26 +61,6 @@ function onHeaderPointerDown(event: PointerEvent) {
|
|
|
136
61
|
handle.setPointerCapture(event.pointerId)
|
|
137
62
|
}
|
|
138
63
|
|
|
139
|
-
function onKeydown(event: KeyboardEvent) {
|
|
140
|
-
if (!visible.value) {
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (event.key === 'Escape') {
|
|
145
|
-
onClose()
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function onPanelAfterLeave() {
|
|
150
|
-
if (import.meta.client) {
|
|
151
|
-
unlockBodyScroll()
|
|
152
|
-
window.removeEventListener('keydown', onFocusTrap)
|
|
153
|
-
focusTrigger.value?.focus()
|
|
154
|
-
|
|
155
|
-
focusTrigger.value = undefined
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
64
|
function onPanelTransitionEnd(event: TransitionEvent) {
|
|
160
65
|
if (event.propertyName !== 'transform' || !drag.closing) {
|
|
161
66
|
return
|
|
@@ -262,53 +167,10 @@ function resetPendingDrag() {
|
|
|
262
167
|
drag.startedFromContent = false
|
|
263
168
|
}
|
|
264
169
|
|
|
265
|
-
|
|
266
|
-
document.body.style.overflow = savedBodyOverflow
|
|
267
|
-
document.body.style.paddingRight = savedBodyPaddingRight
|
|
268
|
-
|
|
269
|
-
for (const [element, paddingRight] of fixedElementPadding) {
|
|
270
|
-
element.style.paddingRight = paddingRight
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
fixedElementPadding.clear()
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
watch(visible, async (isVisible) => {
|
|
277
|
-
if (import.meta.client && isVisible) {
|
|
278
|
-
focusTrigger.value = document.activeElement instanceof HTMLElement ? document.activeElement : undefined
|
|
279
|
-
|
|
280
|
-
lockBodyScroll()
|
|
281
|
-
|
|
282
|
-
await nextTick()
|
|
283
|
-
|
|
284
|
-
if (panelRef.value) {
|
|
285
|
-
const focusableElements = getFocusableElements(panelRef.value)
|
|
286
|
-
|
|
287
|
-
if (focusableElements.length > 0) {
|
|
288
|
-
focusableElements[0].focus()
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
panelRef.value.focus()
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
window.addEventListener('keydown', onFocusTrap)
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
170
|
+
watch(visible, (isVisible) => {
|
|
298
171
|
if (!isVisible) {
|
|
299
172
|
resetDragState()
|
|
300
173
|
}
|
|
301
|
-
}, { immediate: true })
|
|
302
|
-
|
|
303
|
-
onMounted(() => window.addEventListener('keydown', onKeydown))
|
|
304
|
-
|
|
305
|
-
onUnmounted(() => {
|
|
306
|
-
window.removeEventListener('keydown', onKeydown)
|
|
307
|
-
window.removeEventListener('keydown', onFocusTrap)
|
|
308
|
-
|
|
309
|
-
if (import.meta.client) {
|
|
310
|
-
unlockBodyScroll()
|
|
311
|
-
}
|
|
312
174
|
})
|
|
313
175
|
</script>
|
|
314
176
|
|
|
@@ -345,7 +207,7 @@ onUnmounted(() => {
|
|
|
345
207
|
leave-active-class="transition-transform duration-300 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
346
208
|
leave-from-class="translate-y-0"
|
|
347
209
|
leave-to-class="translate-y-full motion-reduce:translate-y-0"
|
|
348
|
-
@after-leave="
|
|
210
|
+
@after-leave="onAfterLeave"
|
|
349
211
|
>
|
|
350
212
|
<div
|
|
351
213
|
v-if="visible"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { LayoutModal } from '../../types/layout'
|
|
3
|
+
|
|
4
|
+
withDefaults(defineProps<LayoutModal>(), { title: undefined })
|
|
5
|
+
|
|
6
|
+
defineSlots<{
|
|
7
|
+
default?: () => VNode[]
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const visible = defineModel<boolean>({ default: false })
|
|
11
|
+
const panelRef = ref<HTMLElement>()
|
|
12
|
+
const { release: onAfterLeave } = useDialog(visible, panelRef, { ignoreClass: 'layout-modal' })
|
|
13
|
+
|
|
14
|
+
function onClose() {
|
|
15
|
+
visible.value = false
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<ClientOnly>
|
|
21
|
+
<Teleport to="body">
|
|
22
|
+
<Transition
|
|
23
|
+
enter-active-class="transition-opacity duration-300 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
24
|
+
enter-from-class="opacity-0 motion-reduce:opacity-100"
|
|
25
|
+
enter-to-class="opacity-100"
|
|
26
|
+
leave-active-class="transition-opacity duration-300 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
27
|
+
leave-from-class="opacity-100"
|
|
28
|
+
leave-to-class="opacity-0 motion-reduce:opacity-100"
|
|
29
|
+
>
|
|
30
|
+
<div
|
|
31
|
+
v-if="visible"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
class="layout-modal fixed inset-0 z-[60] bg-black/50"
|
|
34
|
+
@click="onClose"
|
|
35
|
+
/>
|
|
36
|
+
</Transition>
|
|
37
|
+
|
|
38
|
+
<Transition
|
|
39
|
+
enter-active-class="transition duration-200 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
40
|
+
enter-from-class="opacity-0 scale-95 motion-reduce:scale-100 motion-reduce:opacity-100"
|
|
41
|
+
enter-to-class="opacity-100 scale-100"
|
|
42
|
+
leave-active-class="transition duration-200 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
43
|
+
leave-from-class="opacity-100 scale-100"
|
|
44
|
+
leave-to-class="opacity-0 scale-95 motion-reduce:scale-100 motion-reduce:opacity-100"
|
|
45
|
+
@after-leave="onAfterLeave"
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
v-if="visible"
|
|
49
|
+
class="layout-modal pointer-events-none fixed inset-0 z-[60] flex items-center justify-center p-4"
|
|
50
|
+
>
|
|
51
|
+
<div
|
|
52
|
+
ref="panelRef"
|
|
53
|
+
:aria-label="title"
|
|
54
|
+
aria-modal="true"
|
|
55
|
+
class="pointer-events-auto max-h-[85dvh] max-w-md w-full overflow-y-auto rounded-2xl bg-white p-5 text-gray-900 shadow-lg dark:bg-gray-900 dark:text-gray-100"
|
|
56
|
+
role="dialog"
|
|
57
|
+
tabindex="-1"
|
|
58
|
+
>
|
|
59
|
+
<slot />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</Transition>
|
|
63
|
+
</Teleport>
|
|
64
|
+
</ClientOnly>
|
|
65
|
+
</template>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
interface UseDialogOptions {
|
|
2
|
+
ignoreClass: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export default function useDialog(
|
|
6
|
+
visible: Ref<boolean>,
|
|
7
|
+
panelRef: Ref<HTMLElement | undefined>,
|
|
8
|
+
options: UseDialogOptions,
|
|
9
|
+
) {
|
|
10
|
+
const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
11
|
+
const fixedElementPadding = new Map<HTMLElement, string>()
|
|
12
|
+
|
|
13
|
+
let savedBodyOverflow = ''
|
|
14
|
+
let savedBodyPaddingRight = ''
|
|
15
|
+
|
|
16
|
+
function getFocusableElements(container: HTMLElement) {
|
|
17
|
+
return Array.from(container.querySelectorAll<HTMLElement>(focusableSelector))
|
|
18
|
+
.filter((element) => {
|
|
19
|
+
return element.offsetParent !== null || getComputedStyle(element).position === 'fixed'
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function lockBodyScroll() {
|
|
24
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
|
|
25
|
+
|
|
26
|
+
savedBodyOverflow = document.body.style.overflow
|
|
27
|
+
savedBodyPaddingRight = document.body.style.paddingRight
|
|
28
|
+
document.body.style.overflow = 'hidden'
|
|
29
|
+
|
|
30
|
+
if (scrollbarWidth > 0) {
|
|
31
|
+
document.body.style.paddingRight = `${scrollbarWidth}px`
|
|
32
|
+
|
|
33
|
+
for (const element of document.querySelectorAll('body *')) {
|
|
34
|
+
if (!(element instanceof HTMLElement)) {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Skip the dialog's own teleported elements so they are not padded.
|
|
39
|
+
if (element.classList.contains(options.ignoreClass)) {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { position } = getComputedStyle(element)
|
|
44
|
+
|
|
45
|
+
if (position !== 'fixed' && position !== 'sticky') {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fixedElementPadding.set(element, element.style.paddingRight)
|
|
50
|
+
|
|
51
|
+
const currentPadding = Number.parseFloat(getComputedStyle(element).paddingRight) || 0
|
|
52
|
+
|
|
53
|
+
element.style.paddingRight = `${currentPadding + scrollbarWidth}px`
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onFocusTrap(event: KeyboardEvent) {
|
|
59
|
+
if (event.key !== 'Tab' || !panelRef.value) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const focusableElements = getFocusableElements(panelRef.value)
|
|
64
|
+
|
|
65
|
+
if (focusableElements.length === 0) {
|
|
66
|
+
event.preventDefault()
|
|
67
|
+
panelRef.value.focus()
|
|
68
|
+
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const firstElement = focusableElements[0]
|
|
73
|
+
const lastElement = focusableElements[focusableElements.length - 1]
|
|
74
|
+
const activeElement = document.activeElement
|
|
75
|
+
|
|
76
|
+
if (event.shiftKey && activeElement === firstElement) {
|
|
77
|
+
event.preventDefault()
|
|
78
|
+
lastElement.focus()
|
|
79
|
+
}
|
|
80
|
+
else if (!event.shiftKey && activeElement === lastElement) {
|
|
81
|
+
event.preventDefault()
|
|
82
|
+
firstElement.focus()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onKeydown(event: KeyboardEvent) {
|
|
87
|
+
if (visible.value && event.key === 'Escape') {
|
|
88
|
+
visible.value = false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function unlockBodyScroll() {
|
|
93
|
+
document.body.style.overflow = savedBodyOverflow
|
|
94
|
+
document.body.style.paddingRight = savedBodyPaddingRight
|
|
95
|
+
|
|
96
|
+
for (const [element, paddingRight] of fixedElementPadding) {
|
|
97
|
+
element.style.paddingRight = paddingRight
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fixedElementPadding.clear()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Restores scroll lock and focus trap once the leave transition has finished.
|
|
104
|
+
function release() {
|
|
105
|
+
if (import.meta.client) {
|
|
106
|
+
unlockBodyScroll()
|
|
107
|
+
window.removeEventListener('keydown', onFocusTrap)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
watch(visible, async (isVisible) => {
|
|
112
|
+
if (import.meta.client && isVisible) {
|
|
113
|
+
lockBodyScroll()
|
|
114
|
+
|
|
115
|
+
await nextTick()
|
|
116
|
+
|
|
117
|
+
if (panelRef.value) {
|
|
118
|
+
const focusableElements = getFocusableElements(panelRef.value)
|
|
119
|
+
|
|
120
|
+
if (focusableElements.length > 0) {
|
|
121
|
+
focusableElements[0].focus()
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
panelRef.value.focus()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
window.addEventListener('keydown', onFocusTrap)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, { immediate: true })
|
|
131
|
+
|
|
132
|
+
onMounted(() => window.addEventListener('keydown', onKeydown))
|
|
133
|
+
|
|
134
|
+
onUnmounted(() => {
|
|
135
|
+
window.removeEventListener('keydown', onKeydown)
|
|
136
|
+
window.removeEventListener('keydown', onFocusTrap)
|
|
137
|
+
|
|
138
|
+
if (import.meta.client) {
|
|
139
|
+
unlockBodyScroll()
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return { release }
|
|
144
|
+
}
|
package/app/types/bases.d.ts
CHANGED
|
@@ -80,6 +80,7 @@ export interface BaseCard {
|
|
|
80
80
|
details?: string
|
|
81
81
|
detailsIcon?: string
|
|
82
82
|
direction?: BaseCardDirection
|
|
83
|
+
disabled?: boolean
|
|
83
84
|
emoji?: string
|
|
84
85
|
hasBackground?: boolean
|
|
85
86
|
hasChevron?: boolean
|
|
@@ -87,6 +88,7 @@ export interface BaseCard {
|
|
|
87
88
|
id?: number | string
|
|
88
89
|
image?: string
|
|
89
90
|
isSelected?: boolean
|
|
91
|
+
size?: BaseCardSize
|
|
90
92
|
title?: string
|
|
91
93
|
titleIcon?: string
|
|
92
94
|
to?: RouteLocationNamedI18n
|
|
@@ -94,6 +96,8 @@ export interface BaseCard {
|
|
|
94
96
|
|
|
95
97
|
export type BaseCardDirection = 'horizontal' | 'vertical'
|
|
96
98
|
|
|
99
|
+
export type BaseCardSize = 'base' | 'lg'
|
|
100
|
+
|
|
97
101
|
export interface BaseCharacter {
|
|
98
102
|
character?: BaseCharacterCharacter
|
|
99
103
|
size?: BaseCharacterSize
|
|
@@ -187,7 +191,6 @@ export interface BaseIcon {
|
|
|
187
191
|
to?: RouteLocationNamedI18n
|
|
188
192
|
truncate?: boolean
|
|
189
193
|
underline?: boolean
|
|
190
|
-
uppercase?: boolean
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
export interface BaseMessage {
|
|
@@ -321,7 +324,6 @@ export interface BaseSpinner {
|
|
|
321
324
|
reverse?: boolean
|
|
322
325
|
size?: BaseSize
|
|
323
326
|
text?: BaseTextText
|
|
324
|
-
uppercase?: boolean
|
|
325
327
|
}
|
|
326
328
|
|
|
327
329
|
export type BaseStatus = | 'error' | 'info' | 'success' | 'warning'
|
|
@@ -382,7 +384,6 @@ export interface BaseText {
|
|
|
382
384
|
to?: RouteLocationNamedI18n
|
|
383
385
|
truncate?: boolean
|
|
384
386
|
underline?: boolean
|
|
385
|
-
uppercase?: boolean
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
export type BaseTextText = string | {
|
package/app/types/global.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ declare global {
|
|
|
14
14
|
type BaseButtonType = import('./bases').BaseButtonType
|
|
15
15
|
type BaseCard = import('./bases').BaseCard
|
|
16
16
|
type BaseCardDirection = import('./bases').BaseCardDirection
|
|
17
|
+
type BaseCardSize = import('./bases').BaseCardSize
|
|
17
18
|
type BaseCharacter = import('./bases').BaseCharacter
|
|
18
19
|
type BaseCharacterCharacter = import('./bases').BaseCharacterCharacter
|
|
19
20
|
type BaseCharacterSize = import('./bases').BaseCharacterSize
|
|
@@ -94,6 +95,7 @@ declare global {
|
|
|
94
95
|
|
|
95
96
|
// Layout
|
|
96
97
|
type LayoutBottomSheet = import('./layout').LayoutBottomSheet
|
|
98
|
+
type LayoutModal = import('./layout').LayoutModal
|
|
97
99
|
|
|
98
100
|
// Project
|
|
99
101
|
type LayerIconIcon = import('../composables/useLayerIcons').LayerIconIcon
|
package/app/types/layout.d.ts
CHANGED