@saasmakers/ui 2.0.53 → 2.0.55

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 flex-col select-none items-center rounded-lg text-center transition-colors"
48
48
  :aria-busy="loading"
49
49
  :class="{
50
50
  'cursor-not-allowed opacity-50': disabled,
@@ -8,10 +8,10 @@ withDefaults(defineProps<BaseCircle>(), { color: 'indigo' })
8
8
  <span
9
9
  class="inline-block size-2.5 shrink-0 rounded-full"
10
10
  :class="{
11
- 'bg-red-500': color === 'red',
12
- 'bg-amber-500': color === 'amber',
13
- 'bg-indigo-500': color === 'indigo',
14
- 'bg-green-500': color === 'green',
11
+ 'bg-red-600 dark:bg-red-400': color === 'red',
12
+ 'bg-amber-600 dark:bg-amber-400': color === 'amber',
13
+ 'bg-indigo-600 dark:bg-indigo-400': color === 'indigo',
14
+ 'bg-green-600 dark:bg-green-400': color === 'green',
15
15
  }"
16
16
  />
17
17
  </template>
@@ -15,8 +15,10 @@ withDefaults(defineProps<BaseTag>(), {
15
15
  'px-0.5 py-px text-3xs': size === 'xs',
16
16
  'px-1 py-px text-3xs': size === 'sm',
17
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',
18
+ 'bg-orange-500/15 text-orange-800 dark:text-orange-300': color === 'orange',
19
+ 'bg-indigo-500/15 text-indigo-800 dark:text-indigo-300': color === 'indigo',
20
+ 'bg-green-500/15 text-green-800 dark:text-green-300': color === 'green',
21
+ 'bg-gray-500/20 text-gray-700 dark:text-gray-300': color === 'gray',
20
22
  }"
21
23
  >
22
24
  <slot>{{ text }}</slot>
@@ -0,0 +1,35 @@
1
+ <script lang="ts" setup>
2
+ import type { BaseTags } from '../../types/bases'
3
+
4
+ const props = withDefaults(defineProps<BaseTags>(), {
5
+ size: 'base',
6
+ tags: () => [],
7
+ wrap: false,
8
+ })
9
+
10
+ const sortedTags = computed(() => {
11
+ return [...props.tags].toSorted((tagA, tagB) => {
12
+ return (tagA.order ?? 0) - (tagB.order ?? 0)
13
+ })
14
+ })
15
+ </script>
16
+
17
+ <template>
18
+ <div
19
+ class="flex items-center"
20
+ :class="{
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',
25
+ }"
26
+ >
27
+ <BaseTag
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"
33
+ />
34
+ </div>
35
+ </template>
@@ -127,16 +127,6 @@ export interface BaseCircle {
127
127
 
128
128
  export type BaseCircleColor = 'amber' | 'green' | 'indigo' | 'red'
129
129
 
130
- export interface BaseTag {
131
- color?: BaseTagColor
132
- size?: BaseTagSize
133
- text?: BaseTextText
134
- }
135
-
136
- export type BaseTagColor = 'gray' | 'orange'
137
-
138
- export type BaseTagSize = 'base' | 'sm' | 'xs'
139
-
140
130
  export type BaseColor
141
131
  = | 'black'
142
132
  | 'gray'
@@ -337,6 +327,27 @@ export interface BaseSpinner {
337
327
 
338
328
  export type BaseStatus = | 'error' | 'info' | 'success' | 'warning'
339
329
 
330
+ export interface BaseTag {
331
+ color?: BaseTagColor
332
+ size?: BaseTagSize
333
+ text?: BaseTextText
334
+ }
335
+
336
+ export type BaseTagColor = 'gray' | 'green' | 'indigo' | 'orange'
337
+
338
+ export interface BaseTagItem extends BaseTag {
339
+ id?: number | string
340
+ order?: number
341
+ }
342
+
343
+ export interface BaseTags {
344
+ size?: BaseTagSize
345
+ tags?: BaseTagItem[]
346
+ wrap?: boolean
347
+ }
348
+
349
+ export type BaseTagSize = 'base' | 'sm' | 'xs'
350
+
340
351
  export interface BaseText {
341
352
  alignment?: BaseAlignment
342
353
  background?: BaseBackground
@@ -70,6 +70,8 @@ declare global {
70
70
  type BaseStatus = import('./bases').BaseStatus
71
71
  type BaseTag = import('./bases').BaseTag
72
72
  type BaseTagColor = import('./bases').BaseTagColor
73
+ type BaseTagItem = import('./bases').BaseTagItem
74
+ type BaseTags = import('./bases').BaseTags
73
75
  type BaseTagSize = import('./bases').BaseTagSize
74
76
  type BaseText = import('./bases').BaseText
75
77
  type BaseTextText = import('./bases').BaseTextText
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.53",
3
+ "version": "2.0.55",
4
4
  "private": false,
5
5
  "description": "Reusable Nuxt UI components for SaaS Makers projects",
6
6
  "license": "MIT",