@saasmakers/ui 2.0.52 → 2.0.54

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.
@@ -20,6 +20,17 @@ const turnstile = ref<{
20
20
  reset: () => void
21
21
  }>()
22
22
 
23
+ const turnstileConfig = {
24
+ // Cloudflare Turnstile keeps internal requests in-flight (e.g. clearance redemption). When the
25
+ // widget errors we swallow it here so it does not surface as an unhandled rejection.
26
+ options: { 'error-callback': () => true },
27
+
28
+ // The wrapper's default 250s auto-reset re-runs the challenge while the user is idle, which aborts
29
+ // in-flight requests (surfaces as "The operation was cancelled." on WebKit). Disable it and rely
30
+ // on Turnstile's own refresh-expired handling instead.
31
+ resetInterval: 1000 * 60 * 60 * 24,
32
+ }
33
+
23
34
  const { t } = useI18n()
24
35
 
25
36
  const form = reactive({
@@ -87,6 +98,7 @@ async function onLogin() {
87
98
 
88
99
  if (result.isClosed) {
89
100
  emit('accountClosed', result.email ?? '')
101
+ resetTurnstile()
90
102
  }
91
103
  else {
92
104
  v$.value.$reset()
@@ -95,12 +107,10 @@ async function onLogin() {
95
107
  }
96
108
  catch (error) {
97
109
  console.error(error)
110
+
111
+ resetTurnstile()
98
112
  }
99
113
  finally {
100
- form.captchaToken = ''
101
-
102
- turnstile.value?.reset()
103
-
104
114
  loggingWithEmail.value = false
105
115
  }
106
116
  }
@@ -110,6 +120,12 @@ function onUpdateCurrentWindow(newCurrentWindow: AuthEmailConnectCurrentWindow)
110
120
 
111
121
  v$.value.$reset()
112
122
  }
123
+
124
+ function resetTurnstile() {
125
+ form.captchaToken = ''
126
+
127
+ turnstile.value?.reset()
128
+ }
113
129
  </script>
114
130
 
115
131
  <template>
@@ -165,6 +181,8 @@ function onUpdateCurrentWindow(newCurrentWindow: AuthEmailConnectCurrentWindow)
165
181
  <NuxtTurnstile
166
182
  ref="turnstile"
167
183
  v-model="form.captchaToken"
184
+ :options="turnstileConfig.options"
185
+ :reset-interval="turnstileConfig.resetInterval"
168
186
  />
169
187
  </div>
170
188
 
@@ -44,7 +44,7 @@ function onClick(event: MouseEvent) {
44
44
 
45
45
  <template>
46
46
  <button
47
- class="relative flex flex-col items-center rounded-lg text-center transition-colors"
47
+ class="relative flex select-none flex-col items-center rounded-lg text-center transition-colors"
48
48
  :aria-busy="loading"
49
49
  :class="{
50
50
  'cursor-not-allowed opacity-50': disabled,
@@ -1,199 +1,24 @@
1
1
  <script lang="ts" setup>
2
- import { NuxtLinkLocale } from '#components'
3
2
  import type { BaseTag } from '../../types/bases'
4
3
 
5
- const props = withDefaults(defineProps<BaseTag>(), {
6
- active: false,
7
- avatar: '',
8
- circle: '',
9
- clickable: true,
10
- color: 'black',
11
- draggable: false,
12
- editable: false,
13
- icon: undefined,
14
- iconSize: undefined,
15
- id: undefined,
16
- isCreation: false,
17
- light: true,
18
- removable: false,
19
- rounded: false,
4
+ withDefaults(defineProps<BaseTag>(), {
5
+ color: 'gray',
20
6
  size: 'base',
21
7
  text: '',
22
- to: undefined,
23
- truncate: false,
24
8
  })
25
-
26
- const emit = defineEmits<{
27
- click: [event: MouseEvent, id?: number | string]
28
- close: [event: MouseEvent, id?: number | string]
29
- inputBlur: [event: FocusEvent, name: string, id?: number | string]
30
- inputSubmit: [event: KeyboardEvent, name: string, id?: number | string]
31
- remove: [event: MouseEvent, id?: number | string]
32
- }>()
33
-
34
- defineSlots<{
35
- default?: () => VNode[]
36
- }>()
37
-
38
- const hovered = ref(false)
39
- const form = reactive({ name: '' })
40
-
41
- onBeforeMount(() => {
42
- if (!props.isCreation) {
43
- initializeFormForExistingTag()
44
- }
45
- })
46
-
47
- function initializeFormForExistingTag() {
48
- form.name = String(props.text)?.slice(1)
49
- }
50
-
51
- function onBlur() {
52
- hovered.value = false
53
- }
54
-
55
- function onClick(event: MouseEvent) {
56
- if (!props.removable) {
57
- emit('click', event, props.id)
58
- }
59
- }
60
-
61
- function onFocus() {
62
- hovered.value = true
63
- }
64
-
65
- function onInputBlur(event: FocusEvent) {
66
- emit('inputBlur', event, form.name.trim(), props.id)
67
- }
68
-
69
- function onInputSubmit(event: KeyboardEvent) {
70
- emit('inputSubmit', event, form.name.trim(), props.id)
71
- }
72
-
73
- function onKeyDown(event: KeyboardEvent) {
74
- if (props.clickable) {
75
- (event.currentTarget as HTMLElement).click()
76
- }
77
- }
78
-
79
- function onMouseEnter() {
80
- hovered.value = true
81
- }
82
-
83
- function onMouseLeave() {
84
- hovered.value = false
85
- }
86
-
87
- function onRemove(event: MouseEvent) {
88
- emit('remove', event, props.id)
89
- }
90
9
  </script>
91
10
 
92
11
  <template>
93
12
  <span
94
- class="flex select-none"
95
- role="button"
96
- :aria-disabled="!clickable"
97
- tabindex="0"
98
- @click.stop="onClick"
99
- @keydown.enter.stop="onKeyDown"
100
- @keydown.space.prevent.stop="onKeyDown"
101
- @mouseenter="onMouseEnter"
102
- @mouseleave="onMouseLeave"
103
- @focusin="onFocus"
104
- @focusout="onBlur"
13
+ class="pointer-events-none inline-flex rounded font-medium tracking-tight"
14
+ :class="{
15
+ 'px-0.5 py-px text-3xs': size === 'xs',
16
+ 'px-1 py-px text-3xs': size === 'sm',
17
+ 'px-1.5 py-0.5 text-2xs': size === 'base',
18
+ 'bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300': color === 'orange',
19
+ 'bg-gray-200 text-gray-600 dark:bg-gray-800 dark:text-gray-400': color === 'gray',
20
+ }"
105
21
  >
106
- <component
107
- :is="to ? NuxtLinkLocale : 'span'"
108
- class="inline-flex items-center border font-medium"
109
- :class="{
110
- 'cursor-pointer': clickable,
111
- 'rounded-md': !rounded,
112
- 'rounded-full': rounded,
113
- 'border-gray-200 dark:border-gray-800 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200': (color === 'black' && light && !active) || removable,
114
- 'border-gray-900 dark:border-gray-100 bg-gray-900 dark:bg-gray-100 text-white dark:text-black': color === 'black' && (!light || active),
115
- 'border-gray-700 dark:border-gray-300 bg-gray-100 dark:bg-gray-900 text-gray-700 dark:text-gray-300': color === 'gray' && light && !active,
116
- 'border-gray-800 dark:border-gray-200 bg-gray-800 dark:bg-gray-200 text-white dark:text-black': color === 'gray' && (!light || active),
117
- 'border-green-700 dark:border-green-300 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300': color === 'green' && light && !active,
118
- 'border-green-800 dark:border-green-200 bg-green-800 dark:bg-green-200 text-white dark:text-black': color === 'green' && (!light || active),
119
- 'border-indigo-700 dark:border-indigo-300 bg-indigo-100 dark:bg-indigo-900 text-indigo-700 dark:text-indigo-300': color === 'indigo' && light && !active,
120
- 'border-indigo-800 dark:border-indigo-200 bg-indigo-800 dark:bg-indigo-200 text-white dark:text-black': color === 'indigo' && (!light || active),
121
- 'border-orange-700 dark:border-orange-300 bg-white dark:bg-gray-900 text-orange-700 dark:text-orange-300': color === 'orange' && light && !active,
122
- 'border-orange-800 dark:border-orange-200 bg-orange-800 dark:bg-orange-200 text-white dark:text-black': color === 'orange' && (!light || active),
123
- 'border-red-700 dark:border-red-300 bg-white dark:bg-gray-900 text-red-700 dark:text-red-300': color === 'red' && light && !active,
124
- 'border-red-800 dark:border-red-200 bg-red-800 dark:bg-red-200 text-white dark:text-black': color === 'red' && (!light || active),
125
- 'border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200': color === 'white' && light && !active,
126
- 'border-white dark:border-gray-900 text-white dark:text-black': color === 'white' && (!light || active),
127
- 'border-dashed border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200': color === 'white' && light && !active,
128
- 'py-0.5 px-1.5': size === 'xs',
129
- 'py-1.5 px-2.5': size === 'sm',
130
- 'py-2 px-3': size === 'base',
131
- }"
132
- :to="to"
133
- >
134
- <BaseIcon
135
- v-if="draggable"
136
- class="js-drag-handle mr-2 cursor-move"
137
- clickable
138
- color="gray"
139
- :icon="getLayerIcon('drag')"
140
- />
141
-
142
- <span
143
- v-if="avatar || circle || icon"
144
- class="mr-1 flex items-center text-center"
145
- :class="{
146
- 'h-2.5 w-2.5': size === 'xs' || size === 'sm' || iconSize === 2.5,
147
- 'h-3 w-3': size === 'base' || iconSize === 3,
148
- 'h-3.5 w-3.5': iconSize === 3.5,
149
- 'h-4 w-4': iconSize === 4,
150
- }"
151
- >
152
- <BaseAvatar
153
- v-if="avatar"
154
- class="h-full w-full"
155
- :shadow="false"
156
- :src="avatar"
157
- />
158
-
159
- <BaseIcon
160
- v-else-if="icon"
161
- class="h-full w-full"
162
- :icon="icon"
163
- />
164
- </span>
165
-
166
- <span
167
- v-if="$slots.default"
168
- class="mr-1"
169
- >
170
- <slot />
171
- </span>
172
-
173
- <BaseText
174
- v-if="!editable"
175
- class="whitespace-nowrap outline-none"
176
- :class="{ truncate }"
177
- :size="size === 'xs' ? '3xs' : size === 'sm' ? '2xs' : 'xs'"
178
- :text="text"
179
- />
180
-
181
- <FieldInput
182
- v-else
183
- v-model="form.name"
184
- border="none"
185
- class="w-16"
186
- size="sm"
187
- @blur="onInputBlur"
188
- @submit="onInputSubmit"
189
- />
190
-
191
- <BaseIcon
192
- v-if="removable"
193
- class="ml-1.5 text-red-700 dark:text-red-300 hover:text-black dark:hover:text-white"
194
- :icon="getLayerIcon('closeCircle')"
195
- @click.prevent.stop="onRemove"
196
- />
197
- </component>
22
+ <slot>{{ text }}</slot>
198
23
  </span>
199
24
  </template>
@@ -1,289 +1,35 @@
1
1
  <script lang="ts" setup>
2
2
  import type { BaseTags } from '../../types/bases'
3
3
 
4
- const props = withDefaults(defineProps<Omit<BaseTags, 'modelValue'>>(), {
5
- active: false,
6
- clickable: true,
7
- draggable: false,
8
- editable: false,
9
- hasBack: false,
10
- hasCreation: false,
11
- maxTags: Number.POSITIVE_INFINITY,
12
- removable: false,
13
- selectable: false,
14
- selectableUnique: false,
4
+ const props = withDefaults(defineProps<BaseTags>(), {
15
5
  size: 'base',
16
6
  tags: () => [],
17
7
  wrap: false,
18
8
  })
19
9
 
20
- const emit = defineEmits<{
21
- attach: [event: MouseEvent, tagId?: number | string]
22
- back: [event: MouseEvent]
23
- click: [event: MouseEvent]
24
- create: [event: FocusEvent | KeyboardEvent, name: string]
25
- detach: [event: MouseEvent, tagId?: number | string]
26
- remove: [event: MouseEvent, tagId?: number | string]
27
- update: [event: FocusEvent | KeyboardEvent, name: string, tagId?: number | string]
28
- }>()
29
-
30
- const modelValue = defineModel<BaseTags['modelValue']>({ default: () => [] })
31
- const keyForTagCreation = ref(Date.now())
32
- const keyForTagUpdate = ref(Date.now())
33
- const root = ref<HTMLDivElement>()
34
- const showingAllTags = ref(false)
35
- const showingTagCreationField = ref(false)
36
- const { t } = useI18n()
37
-
38
- const sortedTags = computed({
39
- get() {
40
- return [...props.tags]
41
- .filter((_tag, tagIndex) => tagIndex < props.maxTags || showingAllTags.value)
42
- .toSorted((tagA, tagB) => {
43
- return (tagA.order ?? 0) < (tagB.order ?? 0) ? -1 : 0
44
- })
45
- },
46
-
47
- set(tags) {
48
- const suiteForTags = tags
49
- .map((tag, order) => ({
50
- id: tag.id,
51
- order,
52
- }))
53
- .map(tag => ({
54
- id: tag.id,
55
- order: tag.order,
56
- }))
57
-
58
- return suiteForTags
59
- },
10
+ const sortedTags = computed(() => {
11
+ return [...props.tags].toSorted((tagA, tagB) => {
12
+ return (tagA.order ?? 0) - (tagB.order ?? 0)
13
+ })
60
14
  })
61
-
62
- function onCreateTag(event: FocusEvent | KeyboardEvent, name: string) {
63
- showingTagCreationField.value = false
64
- keyForTagCreation.value = Date.now()
65
-
66
- emit('create', event, name)
67
- }
68
-
69
- function onGoBack(event: MouseEvent) {
70
- emit('back', event)
71
- }
72
-
73
- function onRemoveTag(event: MouseEvent, tagId?: number | string) {
74
- emit('remove', event, tagId)
75
- }
76
-
77
- function onShowAllTags() {
78
- showingAllTags.value = true
79
- }
80
-
81
- function onShowTagField() {
82
- if (!showingTagCreationField.value) {
83
- showingTagCreationField.value = true
84
-
85
- nextTick(() => {
86
- const element = root.value?.querySelector<HTMLInputElement>('input[type="text"]')
87
-
88
- element?.focus()
89
- })
90
- }
91
- }
92
-
93
- function onTagClick(event: MouseEvent, tagId?: number | string) {
94
- if (props.selectable && tagId) {
95
- let value = [...modelValue.value]
96
-
97
- if (value.includes(tagId)) {
98
- value.splice(value.indexOf(tagId), 1)
99
- emit('detach', event, tagId)
100
- }
101
- else if (props.selectableUnique) {
102
- value = [tagId]
103
-
104
- emit('attach', event, tagId)
105
- }
106
- else {
107
- value.push(tagId)
108
- emit('attach', event, tagId)
109
- }
110
-
111
- modelValue.value = value
112
- }
113
- }
114
-
115
- function onUpdateTag(event: FocusEvent | KeyboardEvent, name: string, tagId?: number | string) {
116
- if (name) {
117
- emit('update', event, name, tagId)
118
- }
119
-
120
- else {
121
- keyForTagUpdate.value = Date.now()
122
- }
123
- }
124
15
  </script>
125
16
 
126
17
  <template>
127
18
  <div
128
- ref="root"
129
19
  class="flex items-center"
130
20
  :class="{
131
- 'flex-wrap': wrap,
132
- 'w-full overflow-x-auto': !wrap,
133
- '-mt-0.75 -ml-0.75': size === 'sm',
134
- '-mt-1 -ml-1': size === 'base',
21
+ 'flex-wrap gap-1': wrap && size !== 'base',
22
+ 'flex-wrap gap-1.5': wrap && size === 'base',
23
+ 'w-full gap-1 overflow-x-auto': !wrap && size !== 'base',
24
+ 'w-full gap-1.5 overflow-x-auto': !wrap && size === 'base',
135
25
  }"
136
26
  >
137
27
  <BaseTag
138
- v-if="hasBack"
139
- key="BaseTagToGoBack"
140
- :class="{
141
- 'm-0.75': size === 'sm',
142
- 'm-1': size === 'base',
143
- }"
144
- color="indigo"
145
- :icon="getLayerIcon('back')"
146
- :size="size"
147
- :text="t('cancel')"
148
- @click="onGoBack"
149
- />
150
-
151
- <BaseTag
152
- v-if="hasCreation"
153
- :key="keyForTagCreation"
154
- :class="{
155
- 'm-0.75': size === 'sm',
156
- 'm-1': size === 'base',
157
- }"
158
- color="indigo"
159
- :editable="showingTagCreationField"
160
- :icon="showingTagCreationField ? getLayerIcon('tags') : getLayerIcon('plus')"
161
- is-creation
162
- :light="false"
163
- :size="size"
164
- :text="showingTagCreationField ? '' : t('newTag')"
165
- @click="onShowTagField"
166
- @input-blur="onCreateTag"
167
- @input-submit="onCreateTag"
168
- />
169
-
170
- <div class="flex items-center">
171
- <span
172
- v-for="(tag, tagIndex) in sortedTags"
173
- :key="tag.id ? `${tag.id}_${tag.order}_${keyForTagUpdate}` : tagIndex"
174
- class="flex flex-wrap items-center"
175
- :class="{
176
- 'm-0.75': size === 'sm',
177
- 'm-1': size === 'base',
178
- }"
179
- >
180
- <template v-if="tag">
181
- <BaseTag
182
- :id="tag.id || tagIndex"
183
- :key="`${tag.id}_${tagIndex}`"
184
- :active="tag.id && modelValue.includes(tag.id) || tag.active"
185
- :avatar="tag.avatar"
186
- :circle="tag.circle"
187
- :clickable="clickable"
188
- :color="tag.color"
189
- :draggable="draggable"
190
- :editable="editable"
191
- :icon="tag.icon"
192
- :icon-size="tag.iconSize"
193
- :light="tag.light"
194
- :removable="removable"
195
- :size="size"
196
- :text="tag.text"
197
- :to="tag.to"
198
- @click="onTagClick"
199
- @input-blur="onUpdateTag"
200
- @input-submit="onUpdateTag"
201
- @remove="onRemoveTag"
202
- />
203
- </template>
204
- </span>
205
- </div>
206
-
207
- <BaseTag
208
- v-if="tags.length > maxTags && !showingAllTags"
209
- key="showMore"
210
- :class="{
211
- 'm-0.75': size === 'sm',
212
- 'm-1': size === 'base',
213
- }"
214
- :size="size"
215
- :text="t('showMore', { value: tags.length - maxTags })"
216
- @click="onShowAllTags"
28
+ v-for="(tag, tagIndex) in sortedTags"
29
+ :key="tag.id ?? tagIndex"
30
+ :color="tag.color"
31
+ :size="tag.size ?? size"
32
+ :text="tag.text"
217
33
  />
218
34
  </div>
219
35
  </template>
220
-
221
- <i18n lang="json">
222
- {
223
- "de": {
224
- "cancel": "Abbrechen",
225
- "newTag": "Neues Tag",
226
- "showMore": "& {value} weiteres | & {value} weitere"
227
- },
228
- "en": {
229
- "cancel": "Cancel",
230
- "newTag": "New tag",
231
- "showMore": "& {value} other | & {value} others"
232
- },
233
- "es": {
234
- "cancel": "Cancelar",
235
- "newTag": "Nueva etiqueta",
236
- "showMore": "& {value} otro | & {value} otros"
237
- },
238
- "fr": {
239
- "cancel": "Annuler",
240
- "newTag": "Nouveau tag",
241
- "showMore": "& {value} autre | & {value} autres"
242
- },
243
- "id": {
244
- "cancel": "Batal",
245
- "newTag": "Tag baru",
246
- "showMore": "& {value} lainnya | & {value} lainnya"
247
- },
248
- "it": {
249
- "cancel": "Annulla",
250
- "newTag": "Nuovo tag",
251
- "showMore": "& {value} altro | & {value} altri"
252
- },
253
- "ja": {
254
- "cancel": "キャンセル",
255
- "newTag": "新しいタグ",
256
- "showMore": "& {value} その他 | & {value} その他"
257
- },
258
- "ko": {
259
- "cancel": "취소",
260
- "newTag": "새 태그",
261
- "showMore": "& {value}개 더 | & {value}개 더"
262
- },
263
- "nl": {
264
- "cancel": "Annuleren",
265
- "newTag": "Nieuwe tag",
266
- "showMore": "& {value} andere | & {value} anderen"
267
- },
268
- "pl": {
269
- "cancel": "Anuluj",
270
- "newTag": "Nowy tag",
271
- "showMore": "& {value} inny | & {value} innych"
272
- },
273
- "pt": {
274
- "cancel": "Cancelar",
275
- "newTag": "Nova etiqueta",
276
- "showMore": "& {value} outro | & {value} outros"
277
- },
278
- "pt-BR": {
279
- "cancel": "Cancelar",
280
- "newTag": "Nova tag",
281
- "showMore": "& {value} outro | & {value} outros"
282
- },
283
- "vi": {
284
- "cancel": "Hủy",
285
- "newTag": "Thẻ mới",
286
- "showMore": "& {value} mục khác | & {value} mục khác"
287
- }
288
- }
289
- </i18n>
@@ -328,41 +328,21 @@ export interface BaseSpinner {
328
328
  export type BaseStatus = | 'error' | 'info' | 'success' | 'warning'
329
329
 
330
330
  export interface BaseTag {
331
- active?: boolean
332
- avatar?: string
333
- circle?: string
334
- clickable?: boolean
335
- color?: BaseColor
336
- draggable?: boolean
337
- editable?: boolean
338
- icon?: string
339
- iconSize?: number
340
- id?: number
341
- isCreation?: boolean
342
- light?: boolean
343
- order?: number
344
- removable?: boolean
345
- rounded?: boolean
331
+ color?: BaseTagColor
346
332
  size?: BaseTagSize
347
- text: BaseTextText
348
- to?: RouteLocationNamedI18n
349
- truncate?: boolean
333
+ text?: BaseTextText
334
+ }
335
+
336
+ export type BaseTagColor = 'gray' | 'orange'
337
+
338
+ export interface BaseTagItem extends BaseTag {
339
+ id?: number | string
340
+ order?: number
350
341
  }
351
342
 
352
343
  export interface BaseTags {
353
- active?: boolean
354
- clickable?: boolean
355
- draggable?: boolean
356
- editable?: boolean
357
- hasBack?: boolean
358
- hasCreation?: boolean
359
- maxTags?: number
360
- modelValue: (number | string)[]
361
- removable?: boolean
362
- selectable?: boolean
363
- selectableUnique?: boolean
364
344
  size?: BaseTagSize
365
- tags: BaseTag[]
345
+ tags?: BaseTagItem[]
366
346
  wrap?: boolean
367
347
  }
368
348
 
@@ -69,6 +69,8 @@ declare global {
69
69
  type BaseSpinner = import('./bases').BaseSpinner
70
70
  type BaseStatus = import('./bases').BaseStatus
71
71
  type BaseTag = import('./bases').BaseTag
72
+ type BaseTagColor = import('./bases').BaseTagColor
73
+ type BaseTagItem = import('./bases').BaseTagItem
72
74
  type BaseTags = import('./bases').BaseTags
73
75
  type BaseTagSize = import('./bases').BaseTagSize
74
76
  type BaseText = import('./bases').BaseText
package/app/utils/auth.ts CHANGED
@@ -1,3 +1,23 @@
1
+ export function isRequestCancellation(error: unknown): boolean {
2
+ if (!error || typeof error !== 'object') {
3
+ return false
4
+ }
5
+
6
+ const candidate = error as {
7
+ code?: number
8
+ message?: string
9
+ name?: string
10
+ }
11
+
12
+ // WebKit (Safari/iOS/WKWebView) rejects aborted requests with NSURLErrorCancelled (-999) and this
13
+ // exact message when Turnstile's in-flight request is torn down on navigate/unmount after login.
14
+ return (
15
+ candidate.name === 'AbortError'
16
+ || candidate.code === -999
17
+ || candidate.message === 'The operation was cancelled.'
18
+ )
19
+ }
20
+
1
21
  export function isSocialLoginCancellation(error: unknown): boolean {
2
22
  if (!error || typeof error !== 'object') {
3
23
  return false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasmakers/ui",
3
- "version": "2.0.52",
3
+ "version": "2.0.54",
4
4
  "private": false,
5
5
  "description": "Reusable Nuxt UI components for SaaS Makers projects",
6
6
  "license": "MIT",