@saasmakers/ui 2.0.5 → 2.0.6

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.
@@ -0,0 +1,138 @@
1
+ <script lang="ts" setup>
2
+ import type { BaseAction } from '../../types/bases'
3
+
4
+ const props = withDefaults(defineProps<BaseAction>(), {
5
+ active: false,
6
+ color: 'black',
7
+ confirmation: false,
8
+ disabled: false,
9
+ icon: undefined,
10
+ loading: false,
11
+ size: 'base',
12
+ text: '',
13
+ variant: 'outline',
14
+ })
15
+
16
+ const emit = defineEmits<{
17
+ click: [event: MouseEvent]
18
+ confirm: [event: MouseEvent]
19
+ }>()
20
+
21
+ const confirming = ref(false)
22
+ const { t } = useI18n()
23
+
24
+ function onClick(event: MouseEvent) {
25
+ if (props.loading || props.disabled) {
26
+ return
27
+ }
28
+
29
+ if (props.confirmation) {
30
+ if (confirming.value) {
31
+ emit('confirm', event)
32
+ }
33
+ else {
34
+ setTimeout(() => (confirming.value = false), 3 * 1000)
35
+ }
36
+
37
+ confirming.value = !confirming.value
38
+ }
39
+ else {
40
+ emit('click', event)
41
+ }
42
+ }
43
+ </script>
44
+
45
+ <template>
46
+ <button
47
+ class="relative flex flex-col items-center rounded-lg text-center transition-colors"
48
+ :aria-busy="loading"
49
+ :class="{
50
+ 'cursor-not-allowed opacity-50': disabled,
51
+ 'cursor-pointer': !disabled && !loading,
52
+ 'gap-1 p-2': size === 'sm',
53
+ 'gap-1.5 p-3': size === 'base',
54
+ 'gap-2 p-4': size === 'lg',
55
+ 'border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900': variant === 'outline' && !active,
56
+ 'border border-gray-300 bg-gray-100 dark:border-gray-700 dark:bg-gray-800': variant === 'outline' && active,
57
+ 'hover:border-gray-300 dark:hover:border-gray-700': variant === 'outline' && !disabled && !loading,
58
+ 'bg-gray-100 dark:bg-gray-800': variant === 'filled',
59
+ }"
60
+ :disabled="disabled || loading"
61
+ type="button"
62
+ @click="onClick"
63
+ >
64
+ <span
65
+ class="flex flex-col items-center"
66
+ :class="{
67
+ 'opacity-0': loading,
68
+ 'gap-1': size === 'sm',
69
+ 'gap-1.5': size === 'base',
70
+ 'gap-2': size === 'lg',
71
+ }"
72
+ >
73
+ <BaseIcon
74
+ :color="color"
75
+ :icon="icon"
76
+ :size="size === 'lg' ? 'xl' : size === 'base' ? 'lg' : 'base'"
77
+ />
78
+
79
+ <BaseText
80
+ bold
81
+ class="tracking-tight uppercase"
82
+ :size="size === 'lg' ? 'xs' : '2xs'"
83
+ :text="confirming ? t('confirm') : text"
84
+ />
85
+ </span>
86
+
87
+ <BaseSpinner
88
+ v-if="loading"
89
+ class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
90
+ :color="color === 'green' || color === 'orange' || color === 'red' ? color : 'black'"
91
+ :size="size === 'sm' ? 'xs' : size === 'lg' ? 'base' : 'sm'"
92
+ />
93
+ </button>
94
+ </template>
95
+
96
+ <i18n lang="json">
97
+ {
98
+ "de": {
99
+ "confirm": "Bestätigen?"
100
+ },
101
+ "en": {
102
+ "confirm": "Confirm?"
103
+ },
104
+ "es": {
105
+ "confirm": "¿Confirmar?"
106
+ },
107
+ "fr": {
108
+ "confirm": "Confirmer ?"
109
+ },
110
+ "id": {
111
+ "confirm": "Konfirmasi?"
112
+ },
113
+ "it": {
114
+ "confirm": "Confermare?"
115
+ },
116
+ "ja": {
117
+ "confirm": "確認しますか?"
118
+ },
119
+ "ko": {
120
+ "confirm": "확인할까요?"
121
+ },
122
+ "nl": {
123
+ "confirm": "Bevestigen?"
124
+ },
125
+ "pl": {
126
+ "confirm": "Potwierdzić?"
127
+ },
128
+ "pt": {
129
+ "confirm": "Confirmar?"
130
+ },
131
+ "pt-BR": {
132
+ "confirm": "Confirmar?"
133
+ },
134
+ "vi": {
135
+ "confirm": "Xác nhận?"
136
+ }
137
+ }
138
+ </i18n>
@@ -118,12 +118,9 @@ function onClick(event: MouseEvent) {
118
118
 
119
119
  <BaseIcon
120
120
  v-else-if="icon"
121
- :class="{
122
- 'text-lg': size === 'base',
123
- 'text-2xl': size === 'lg',
124
- }"
125
121
  :color="color"
126
122
  :icon="icon"
123
+ :size="size === 'lg' ? '2xl' : 'lg'"
127
124
  />
128
125
 
129
126
  <img
@@ -193,22 +190,17 @@ function onClick(event: MouseEvent) {
193
190
  'mr-1.5 self-center': orientation === 'horizontal',
194
191
  'absolute right-2 top-2': orientation === 'vertical' && size === 'base',
195
192
  'absolute right-2.5 top-2.5': orientation === 'vertical' && size === 'lg',
196
- 'text-xl': size === 'base',
197
- 'text-2xl': size === 'lg',
198
193
  }"
199
194
  color="green"
200
195
  :icon="getLayerIcon('checkCircle')"
196
+ :size="size === 'lg' ? '2xl' : 'xl'"
201
197
  />
202
198
 
203
199
  <BaseIcon
204
200
  v-else-if="orientation === 'horizontal' && (to || hasChevron)"
205
- class="self-center text-gray-500 flex-initial dark:text-gray-500"
206
- :class="{
207
- 'group-hover:text-gray-900 dark:group-hover:text-gray-100': isClickable,
208
- 'text-xl': size === 'base',
209
- 'text-2xl': size === 'lg',
210
- }"
201
+ class="self-center text-gray-500 flex-initial dark:text-gray-500 group-hover:text-gray-900 dark:group-hover:text-gray-100"
211
202
  :icon="getLayerIcon('chevronRight')"
203
+ :size="size === 'lg' ? '2xl' : 'xl'"
212
204
  />
213
205
 
214
206
  <slot name="right" />
@@ -2,6 +2,7 @@
2
2
  import type { BaseEmoji } from '../../types/bases'
3
3
 
4
4
  const props = withDefaults(defineProps<BaseEmoji>(), {
5
+ background: 'gray',
5
6
  clickable: true,
6
7
  emoji: undefined,
7
8
  hasBox: false,
@@ -33,8 +34,11 @@ function onKeyDown(event: KeyboardEvent) {
33
34
  <span
34
35
  class="flex items-center justify-center transition"
35
36
  :class="{
36
- 'border border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-900 shadow-inner': hasBox,
37
- 'cursor-pointer hover:border-gray-400 dark:hover:border-gray-600': clickable,
37
+ 'border border-gray-200 dark:border-gray-800': hasBox,
38
+ 'bg-gray-100 dark:bg-gray-900 shadow-inner': hasBox && background === 'gray',
39
+ 'bg-white dark:bg-gray-900 shadow-sm': hasBox && background === 'white',
40
+ 'cursor-pointer hover:border-gray-400 dark:hover:border-gray-600': clickable && (!hasBox || background === 'gray'),
41
+ 'cursor-pointer hover:border-gray-300 focus-visible:border-gray-400 dark:hover:border-gray-700 dark:focus-visible:border-gray-600': clickable && hasBox && background === 'white',
38
42
  'rounded': ['xs'].includes(size) && hasBox,
39
43
  'rounded-md': ['sm', 'base'].includes(size) && hasBox,
40
44
  'rounded-lg': ['lg', 'xl'].includes(size) && hasBox,
@@ -78,6 +78,10 @@ const finalColor = computed(() => {
78
78
  return statusColor.value || props.color
79
79
  })
80
80
 
81
+ const shouldSizeIcon = computed(() => {
82
+ return !!props.text || props.size !== 'base'
83
+ })
84
+
81
85
  function onClick(event: MouseEvent) {
82
86
  if (props.confirmation) {
83
87
  if (confirming.value) {
@@ -122,13 +126,23 @@ function onKeyDown(event: KeyboardEvent) {
122
126
  v-if="finalIcon && !loading"
123
127
  class="flex items-center justify-center"
124
128
  :class="{
129
+ 'text-2xl': shouldSizeIcon && size === '2xl',
130
+ 'text-2xs': shouldSizeIcon && size === '2xs',
131
+ 'text-3xl': shouldSizeIcon && size === '3xl',
132
+ 'text-3xs': shouldSizeIcon && size === '3xs',
133
+ 'text-4xl': shouldSizeIcon && size === '4xl',
134
+ 'text-base': shouldSizeIcon && size === 'base',
135
+ 'text-lg': shouldSizeIcon && size === 'lg',
136
+ 'text-sm': shouldSizeIcon && size === 'sm',
137
+ 'text-xl': shouldSizeIcon && size === 'xl',
138
+ 'text-xs': shouldSizeIcon && size === 'xs',
139
+ 'text-gray-100 dark:text-gray-900': finalColor === 'white',
140
+ 'text-gray-600 dark:text-gray-400': finalColor === 'gray',
125
141
  'text-gray-900 dark:text-gray-100': finalColor === 'black',
126
142
  'text-green-600 dark:text-green-400': finalColor === 'green',
127
- 'text-gray-600 dark:text-gray-400': finalColor === 'gray',
128
143
  'text-indigo-600 dark:text-indigo-400': finalColor === 'indigo',
129
144
  'text-orange-600 dark:text-orange-400': finalColor === 'orange',
130
145
  'text-red-600 dark:text-red-400': finalColor === 'red',
131
- 'text-gray-100 dark:text-gray-900': finalColor === 'white',
132
146
  }"
133
147
  :to="to"
134
148
  >
@@ -38,7 +38,8 @@ function onClose(event: KeyboardEvent | MouseEvent) {
38
38
  @keydown.space.prevent="onClose"
39
39
  >
40
40
  <BaseIcon
41
- class="pointer-events-none text-lg flex-initial"
41
+ class="pointer-events-none flex-initial"
42
+ size="lg"
42
43
  :status="status"
43
44
  :text="text"
44
45
  />
@@ -73,7 +74,7 @@ function onClose(event: KeyboardEvent | MouseEvent) {
73
74
  <div class="ml-2 mr-1.5 h-5 border-l border-gray-200 flex-initial dark:border-gray-700" />
74
75
 
75
76
  <BaseIcon
76
- class="text-lg text-gray-400 transition-colors flex-initial dark:text-gray-500"
77
+ class="text-gray-400 transition-colors flex-initial dark:text-gray-500"
77
78
  :class="{
78
79
  'group-hover:text-red-600 dark:group-hover:text-red-400': status === 'error',
79
80
  'group-hover:text-indigo-600 dark:group-hover:text-indigo-400': status === 'info',
@@ -81,6 +82,7 @@ function onClose(event: KeyboardEvent | MouseEvent) {
81
82
  'group-hover:text-orange-600 dark:group-hover:text-orange-400': status === 'warning',
82
83
  }"
83
84
  :icon="getLayerIcon('close')"
85
+ size="lg"
84
86
  />
85
87
  </template>
86
88
  </div>
@@ -67,10 +67,12 @@ function onUpdateDays(event: MouseEvent, day: number) {
67
67
  <BaseText
68
68
  v-for="day in daysToDisplay"
69
69
  :key="day.value"
70
- class="mr-2 h-7 w-7 flex cursor-pointer items-center justify-center border rounded-full shadow-inner last:mr-0"
70
+ class="mr-2 h-7 w-7 flex items-center justify-center border rounded-full shadow-inner transition-colors last:mr-0"
71
71
  :class="{
72
72
  'border-indigo-700 dark:border-indigo-300 bg-gray-100 dark:bg-gray-900 text-indigo-700 dark:text-indigo-300': modelValue.includes(day.value),
73
73
  'border-red-700 dark:border-red-300 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300': !modelValue.includes(day.value),
74
+ 'cursor-pointer hover:border-indigo-800 hover:text-indigo-800 dark:hover:border-indigo-200 dark:hover:text-indigo-200': modelValue.includes(day.value) && !disabled,
75
+ 'cursor-pointer hover:border-red-800 hover:text-red-800 dark:hover:border-red-200 dark:hover:text-red-200': !modelValue.includes(day.value) && !disabled,
74
76
  }"
75
77
  size="xs"
76
78
  :text="day.label.charAt(0)"
@@ -24,6 +24,7 @@ const props = withDefaults(defineProps<Omit<FieldInput, 'modelValue'>>(), {
24
24
  slugOnly: false,
25
25
  trim: false,
26
26
  type: 'text',
27
+ uppercase: true,
27
28
  validation: undefined,
28
29
  })
29
30
 
@@ -178,8 +179,9 @@ defineExpose({ focus })
178
179
  :id="id"
179
180
  ref="input"
180
181
  :autocomplete="autocomplete ? 'on' : 'off'"
181
- class="h-full w-full appearance-none items-center rounded-lg font-medium tracking-tight uppercase outline-none placeholder-gray-600 dark:placeholder-gray-400"
182
+ class="h-full w-full appearance-none items-center rounded-lg font-medium tracking-tight outline-none placeholder-gray-600 dark:placeholder-gray-400"
182
183
  :class="{
184
+ 'uppercase': uppercase,
183
185
  'field-disabled': disabled,
184
186
  'focus:placeholder-gray-900 hover:placeholder-gray-900 dark:focus:placeholder-gray-100 dark:hover:placeholder-gray-100': !disabled,
185
187
  'text-gray-900 dark:text-gray-100': !lineThrough,
@@ -18,6 +18,7 @@ const props = withDefaults(defineProps<Omit<FieldTextarea, 'modelValue'>>(), {
18
18
  resize: false,
19
19
  rows: 3,
20
20
  size: 'base',
21
+ uppercase: true,
21
22
  validation: undefined,
22
23
  })
23
24
 
@@ -67,8 +68,9 @@ function onFieldClick(event: MouseEvent) {
67
68
  ref="textarea"
68
69
  v-model="modelValue"
69
70
  v-bind="{ 'data-enable-grammarly': 'false' }"
70
- class="w-full flex-1 appearance-none rounded-lg text-gray-900 font-medium tracking-tight uppercase outline-none dark:text-gray-100 placeholder-gray-600 dark:placeholder-gray-400"
71
+ class="w-full flex-1 appearance-none rounded-lg text-gray-900 font-medium tracking-tight outline-none dark:text-gray-100 placeholder-gray-600 dark:placeholder-gray-400"
71
72
  :class="{
73
+ 'uppercase': uppercase,
72
74
  'field-disabled': disabled,
73
75
  'focus:placeholder-gray-900 hover:placeholder-gray-900 dark:focus:placeholder-gray-100 dark:hover:placeholder-gray-100': !disabled,
74
76
  'p-2.5': padding,
@@ -242,7 +242,7 @@ watch(visible, (isVisible) => {
242
242
 
243
243
  <div
244
244
  ref="contentRef"
245
- class="min-h-0 flex-1 overflow-y-auto px-6 md:px-8"
245
+ class="scroll-y-stable min-h-0 flex-1 px-6 pt-2 md:px-8"
246
246
  :class="{
247
247
  'pb-4': $slots.footer,
248
248
  'pb-6': !($slots.footer),
@@ -52,7 +52,7 @@ function onClose() {
52
52
  ref="panelRef"
53
53
  :aria-label="title"
54
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 app-text shadow-lg outline-none dark:bg-gray-900 dark:text-gray-100"
55
+ class="scroll-y-stable pointer-events-auto max-h-[85dvh] max-w-md w-full rounded-2xl bg-white p-5 text-gray-900 app-text shadow-lg outline-none dark:bg-gray-900 dark:text-gray-100"
56
56
  role="dialog"
57
57
  tabindex="-1"
58
58
  >
@@ -1,4 +1,18 @@
1
1
  // Components
2
+ export interface BaseAction {
3
+ active?: boolean
4
+ color?: BaseColor
5
+ confirmation?: boolean
6
+ disabled?: boolean
7
+ icon?: string
8
+ loading?: boolean
9
+ size?: BaseSize
10
+ text?: BaseTextText
11
+ variant?: BaseActionVariant
12
+ }
13
+
14
+ export type BaseActionVariant = 'filled' | 'outline'
15
+
2
16
  export interface BaseAlert {
3
17
  closingId?: string
4
18
  isClosable?: boolean
@@ -151,6 +165,7 @@ export type BaseDividerNavigateDirection = 'next' | 'previous'
151
165
  export type BaseDividerSize = 'base' | 'sm'
152
166
 
153
167
  export interface BaseEmoji {
168
+ background?: BaseBackground
154
169
  clickable?: boolean
155
170
  emoji?: string
156
171
  hasBox?: boolean
@@ -71,6 +71,7 @@ export interface FieldInput {
71
71
  slugOnly?: boolean
72
72
  trim?: boolean
73
73
  type?: FieldInputType
74
+ uppercase?: boolean
74
75
  validation?: VuelidateValidation
75
76
  }
76
77
 
@@ -204,6 +205,7 @@ export interface FieldTextarea {
204
205
  resize?: boolean
205
206
  rows?: number
206
207
  size?: FieldSize
208
+ uppercase?: boolean
207
209
  validation?: VuelidateValidation
208
210
  }
209
211
 
@@ -9,6 +9,8 @@ declare global {
9
9
  type AuthSocialConnectSuccess = import('./auth').AuthSocialConnectSuccess
10
10
 
11
11
  // Bases
12
+ type BaseAction = import('./bases').BaseAction
13
+ type BaseActionVariant = import('./bases').BaseActionVariant
12
14
  type BaseAlert = import('./bases').BaseAlert
13
15
  type BaseAlignment = import('./bases').BaseAlignment
14
16
  type BaseAvatar = import('./bases').BaseAvatar
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasmakers/ui",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "private": false,
5
5
  "description": "Reusable Nuxt UI components for SaaS Makers projects",
6
6
  "license": "MIT",
package/uno.config.ts CHANGED
@@ -27,6 +27,7 @@ export const rules: [string, Record<string, string>][] = [
27
27
  export const shortcuts: Record<string, string> = {
28
28
  'app-text': 'font-bold leading-relaxed tracking-tight uppercase',
29
29
  'field-disabled': 'opacity-50 cursor-not-allowed',
30
+ 'scroll-y-stable': 'overflow-y-auto [scrollbar-gutter:stable]',
30
31
  }
31
32
 
32
33
  export const theme = {