@saasmakers/ui 1.3.0 → 1.3.2

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.
@@ -40,7 +40,7 @@ function onAvatarSelected(event: Event) {
40
40
 
41
41
  if (file) {
42
42
  if (props.maxSizeMb && file.size > props.maxSizeMb * 1024 * 1024) {
43
- return createToast('fileSizeTooLarge', 'error', { maxSizeMb: props.maxSizeMb })
43
+ return createToast(t('fileSizeTooLarge', { maxSizeMb: props.maxSizeMb }), 'error')
44
44
  }
45
45
 
46
46
  emit('avatarSelected', event, file)
@@ -117,9 +117,9 @@ function onMouseLeave() {
117
117
  :class="{
118
118
  'rounded-full': circular,
119
119
  'border-white dark:border-white': bordered,
120
- 'border-2': borderWidth === 2,
121
- 'border-3': borderWidth === 3,
122
- 'border-4': borderWidth === 4,
120
+ 'border-2': bordered && borderWidth === 2,
121
+ 'border-3': bordered && borderWidth === 3,
122
+ 'border-4': bordered && borderWidth === 4,
123
123
  }"
124
124
  :alt="editable ? t('edit') : ''"
125
125
  loading="lazy"
@@ -156,43 +156,56 @@ function onMouseLeave() {
156
156
  <i18n lang="json">
157
157
  {
158
158
  "de": {
159
- "edit": "Bearbeiten"
159
+ "edit": "Bearbeiten",
160
+ "fileSizeTooLarge": "Die Datei ist zu groß (max.: {maxSizeMb}MB)"
160
161
  },
161
162
  "en": {
162
- "edit": "Edit"
163
+ "edit": "Edit",
164
+ "fileSizeTooLarge": "The file is too large (max: {maxSizeMb}MB)"
163
165
  },
164
166
  "es": {
165
- "edit": "Editar"
167
+ "edit": "Editar",
168
+ "fileSizeTooLarge": "El archivo es demasiado grande (máx.: {maxSizeMb}MB)"
166
169
  },
167
170
  "fr": {
168
- "edit": "Modifier"
171
+ "edit": "Modifier",
172
+ "fileSizeTooLarge": "Le fichier est trop grand (max: {maxSizeMb}MB)"
169
173
  },
170
174
  "it": {
171
- "edit": "Modifica"
175
+ "edit": "Modifica",
176
+ "fileSizeTooLarge": "Il file è troppo grande (max: {maxSizeMb}MB)"
172
177
  },
173
178
  "ja": {
174
- "edit": "編集"
179
+ "edit": "編集",
180
+ "fileSizeTooLarge": "ファイルが大きすぎます (最大: {maxSizeMb}MB)"
175
181
  },
176
182
  "ko": {
177
- "edit": "편집"
183
+ "edit": "편집",
184
+ "fileSizeTooLarge": "파일이 너무 큽니다 (최대: {maxSizeMb}MB)"
178
185
  },
179
186
  "nl": {
180
- "edit": "Bewerken"
187
+ "edit": "Bewerken",
188
+ "fileSizeTooLarge": "Het bestand is te groot (max.: {maxSizeMb}MB)"
181
189
  },
182
190
  "pl": {
183
- "edit": "Edytuj"
191
+ "edit": "Edytuj",
192
+ "fileSizeTooLarge": "Plik jest za duży (maks.: {maxSizeMb}MB)"
184
193
  },
185
194
  "pt": {
186
- "edit": "Editar"
195
+ "edit": "Editar",
196
+ "fileSizeTooLarge": "O ficheiro é demasiado grande (máx.: {maxSizeMb}MB)"
187
197
  },
188
198
  "pt-BR": {
189
- "edit": "Editar"
199
+ "edit": "Editar",
200
+ "fileSizeTooLarge": "O arquivo é muito grande (máx.: {maxSizeMb}MB)"
190
201
  },
191
202
  "id": {
192
- "edit": "Edit"
203
+ "edit": "Edit",
204
+ "fileSizeTooLarge": "File terlalu besar (maks.: {maxSizeMb}MB)"
193
205
  },
194
206
  "vi": {
195
- "edit": "Chỉnh sửa"
207
+ "edit": "Chỉnh sửa",
208
+ "fileSizeTooLarge": "Tệp quá lớn (tối đa: {maxSizeMb}MB)"
196
209
  }
197
210
  }
198
211
  </i18n>
@@ -0,0 +1,37 @@
1
+ import FieldAvatar from './FieldAvatar.vue'
2
+
3
+ export default {
4
+ component: FieldAvatar,
5
+ title: 'Fields/FieldAvatar',
6
+
7
+ argTypes: {
8
+ action: { control: 'text' },
9
+ background: {
10
+ control: 'select',
11
+ options: ['gray', 'white'] satisfies FieldBackground[],
12
+ },
13
+ border: {
14
+ control: 'select',
15
+ options: [
16
+ 'bottom',
17
+ 'full',
18
+ 'none',
19
+ ] satisfies FieldInputBorder[],
20
+ },
21
+ disabled: { control: 'boolean' },
22
+ fullWidth: { control: 'boolean' },
23
+ label: { control: 'text' },
24
+ loading: { control: 'boolean' },
25
+ maxSizeMb: { control: 'number' },
26
+ src: { control: 'text' },
27
+ },
28
+ } satisfies Meta<typeof FieldAvatar>
29
+
30
+ export const Default: StoryObj<typeof FieldAvatar> = { args: { src: 'https://via.placeholder.com/100' } satisfies Partial<FieldAvatar> }
31
+
32
+ export const Loading: StoryObj<typeof FieldAvatar> = {
33
+ args: {
34
+ loading: true,
35
+ src: 'https://via.placeholder.com/100',
36
+ } satisfies Partial<FieldAvatar>,
37
+ }
@@ -0,0 +1,227 @@
1
+ <script lang="ts" setup>
2
+ import type { FieldAvatar } from '../../types/fields'
3
+
4
+ const props = withDefaults(defineProps<FieldAvatar>(), {
5
+ accept: 'image/*',
6
+ action: undefined,
7
+ background: 'white',
8
+ border: 'full',
9
+ description: '',
10
+ disabled: false,
11
+ fullWidth: false,
12
+ hideError: false,
13
+ label: undefined,
14
+ labelIcon: undefined,
15
+ loading: false,
16
+ maxSizeMb: undefined,
17
+ required: false,
18
+ size: 'base',
19
+ src: undefined,
20
+ validation: undefined,
21
+ })
22
+
23
+ const emit = defineEmits<{
24
+ change: [event: Event, file: File]
25
+ }>()
26
+
27
+ const { createToast } = useToasts()
28
+ const { t } = useI18n()
29
+
30
+ const fileInput = ref<HTMLInputElement>()
31
+ const id = useId()
32
+
33
+ function onAvatarChange(event: Event) {
34
+ const input = event.target as HTMLInputElement
35
+ const file = input.files?.[0]
36
+
37
+ input.value = ''
38
+
39
+ if (!file) {
40
+ return
41
+ }
42
+
43
+ if (props.maxSizeMb && file.size > props.maxSizeMb * 1024 * 1024) {
44
+ createToast(t('fileSizeTooLarge', { maxSizeMb: props.maxSizeMb }), 'error')
45
+ return
46
+ }
47
+
48
+ emit('change', event, file)
49
+ }
50
+
51
+ function onOpenFilePicker() {
52
+ if (!props.disabled && !props.loading) {
53
+ fileInput.value?.click()
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <template>
59
+ <div
60
+ class="flex flex-col"
61
+ :class="{ 'w-full': fullWidth }"
62
+ >
63
+ <FieldLabel
64
+ :disabled="disabled"
65
+ :for-field="id"
66
+ has-margin-bottom
67
+ :icon="labelIcon"
68
+ :label="props.label || t('label')"
69
+ :required="required"
70
+ :size="size"
71
+ />
72
+
73
+ <div
74
+ :aria-label="loading ? t('actionLoading') : (typeof props.action === 'string' ? props.action : (props.action?.base || t('action')))"
75
+ class="w-full flex items-center gap-2 px-3 normal-case outline-none"
76
+ :class="{
77
+ 'cursor-pointer': !disabled && !loading,
78
+ 'field-disabled': disabled,
79
+ 'pointer-events-none opacity-50': disabled || loading,
80
+ 'hover:border-gray-300 focus:border-gray-400 dark:hover:border-gray-700 dark:focus:border-gray-600': border === 'full' && !disabled && !loading,
81
+ 'bg-gray-100 dark:bg-gray-900': background === 'gray',
82
+ 'bg-white dark:bg-gray-900': background === 'white',
83
+ 'rounded-lg border border-gray-200 shadow-sm dark:border-gray-800': border === 'full',
84
+ 'border-b border-gray-200 dark:border-gray-800': border === 'bottom',
85
+ 'border-0': border === 'none',
86
+ 'text-2xs h-8': size === 'xs',
87
+ 'text-xs h-10': size === 'sm',
88
+ 'text-sm h-12': size === 'base',
89
+ 'text-base h-14': size === 'lg',
90
+ }"
91
+ role="button"
92
+ :aria-disabled="disabled || loading"
93
+ tabindex="0"
94
+ @click="onOpenFilePicker"
95
+ @keydown.enter="onOpenFilePicker"
96
+ @keydown.space.prevent="onOpenFilePicker"
97
+ >
98
+ <div class="pointer-events-none min-w-0 flex flex-1 items-center gap-2">
99
+ <BaseSpinner
100
+ v-if="loading"
101
+ class="shrink-0"
102
+ :size="size === 'xs' ? 'xl' : size === 'sm' ? '2xl' : size === 'base' ? '3xl' : '3xl'"
103
+ />
104
+
105
+ <BaseAvatar
106
+ v-else
107
+ :border-width="2"
108
+ class="shrink-0"
109
+ :class="{
110
+ 'h-7 w-7': size === 'xs',
111
+ 'h-8 w-8': size === 'sm',
112
+ 'h-9 w-9': size === 'base',
113
+ 'h-10 w-10': size === 'lg',
114
+ }"
115
+ :shadow="false"
116
+ :src="src"
117
+ />
118
+
119
+ <BaseText
120
+ class="min-w-0 flex-1 text-gray-900 dark:text-gray-100"
121
+ :text="loading ? t('actionLoading') : (props.action || t('action'))"
122
+ />
123
+ </div>
124
+
125
+ <input
126
+ :id="id"
127
+ ref="fileInput"
128
+ :accept="accept"
129
+ class="hidden"
130
+ :disabled="disabled"
131
+ type="file"
132
+ @change="onAvatarChange"
133
+ >
134
+ </div>
135
+
136
+ <FieldMessage
137
+ :description="description"
138
+ :disabled="disabled"
139
+ :hide-error="hideError"
140
+ :size="size"
141
+ :validation="validation"
142
+ />
143
+ </div>
144
+ </template>
145
+
146
+ <i18n lang="json">
147
+ {
148
+ "de": {
149
+ "fileSizeTooLarge": "Die Datei ist zu groß (max.: {maxSizeMb}MB)",
150
+ "action": "Foto ändern",
151
+ "actionLoading": "Foto wird aktualisiert...",
152
+ "label": "Profilbild"
153
+ },
154
+ "en": {
155
+ "fileSizeTooLarge": "The file is too large (max: {maxSizeMb}MB)",
156
+ "action": "Change photo",
157
+ "actionLoading": "Updating photo...",
158
+ "label": "Profile picture"
159
+ },
160
+ "es": {
161
+ "fileSizeTooLarge": "El archivo es demasiado grande (máx.: {maxSizeMb}MB)",
162
+ "action": "Cambiar foto",
163
+ "actionLoading": "Actualizando foto...",
164
+ "label": "Foto de perfil"
165
+ },
166
+ "fr": {
167
+ "fileSizeTooLarge": "Le fichier est trop grand (max: {maxSizeMb}MB)",
168
+ "action": "Changer la photo",
169
+ "actionLoading": "Mise a jour de la photo...",
170
+ "label": "Photo de profil"
171
+ },
172
+ "it": {
173
+ "fileSizeTooLarge": "Il file è troppo grande (max: {maxSizeMb}MB)",
174
+ "action": "Cambia foto",
175
+ "actionLoading": "Aggiornamento foto...",
176
+ "label": "Foto del profilo"
177
+ },
178
+ "ja": {
179
+ "fileSizeTooLarge": "ファイルが大きすぎます (最大: {maxSizeMb}MB)",
180
+ "action": "写真を変更",
181
+ "actionLoading": "写真を更新中...",
182
+ "label": "プロフィール写真"
183
+ },
184
+ "ko": {
185
+ "fileSizeTooLarge": "파일이 너무 큽니다 (최대: {maxSizeMb}MB)",
186
+ "action": "사진 변경",
187
+ "actionLoading": "사진 업데이트 중...",
188
+ "label": "프로필 사진"
189
+ },
190
+ "nl": {
191
+ "fileSizeTooLarge": "Het bestand is te groot (max.: {maxSizeMb}MB)",
192
+ "action": "Foto wijzigen",
193
+ "actionLoading": "Foto wordt bijgewerkt...",
194
+ "label": "Profielfoto"
195
+ },
196
+ "pl": {
197
+ "fileSizeTooLarge": "Plik jest za duży (maks.: {maxSizeMb}MB)",
198
+ "action": "Zmień zdjęcie",
199
+ "actionLoading": "Aktualizowanie zdjęcia...",
200
+ "label": "Zdjęcie profilowe"
201
+ },
202
+ "pt": {
203
+ "fileSizeTooLarge": "O ficheiro é demasiado grande (máx.: {maxSizeMb}MB)",
204
+ "action": "Alterar foto",
205
+ "actionLoading": "A atualizar foto...",
206
+ "label": "Foto de perfil"
207
+ },
208
+ "pt-BR": {
209
+ "fileSizeTooLarge": "O arquivo é muito grande (máx.: {maxSizeMb}MB)",
210
+ "action": "Alterar foto",
211
+ "actionLoading": "Atualizando foto...",
212
+ "label": "Foto de perfil"
213
+ },
214
+ "id": {
215
+ "fileSizeTooLarge": "File terlalu besar (maks.: {maxSizeMb}MB)",
216
+ "action": "Ganti foto",
217
+ "actionLoading": "Memperbarui foto...",
218
+ "label": "Foto profil"
219
+ },
220
+ "vi": {
221
+ "fileSizeTooLarge": "Tệp quá lớn (tối đa: {maxSizeMb}MB)",
222
+ "action": "Đổi ảnh",
223
+ "actionLoading": "Dang cap nhat anh...",
224
+ "label": "Ảnh đại diện"
225
+ }
226
+ }
227
+ </i18n>
@@ -262,8 +262,8 @@ function selectOption(event: MouseEvent, value: string) {
262
262
  :key="option.value"
263
263
  class="group flex cursor-pointer items-center outline-none"
264
264
  :class="{
265
- 'border-b border-gray-200 dark:border-gray-800 p-3 hover:bg-gray-100 dark:hover:bg-gray-900 last:border-b-0': computedColumns.length < 2,
266
- 'px-2 py-1 last:mb-2': computedColumns.length >= 2,
265
+ 'border-b border-gray-200 dark:border-gray-800 p-3 hover:bg-gray-100 dark:hover:bg-gray-800 last:border-b-0': computedColumns.length < 2,
266
+ 'px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800 last:mb-2': computedColumns.length >= 2,
267
267
  'font-medium underline': selectedOption && option.value === selectedOption.value,
268
268
  'text-gray-900 dark:text-gray-100': selectedOption && option.value === selectedOption.value,
269
269
  'bg-white dark:bg-gray-900': selectedOption && option.value !== selectedOption.value,
@@ -2,6 +2,25 @@
2
2
  export type FieldBackground = 'gray' | 'white'
3
3
 
4
4
  // Components
5
+ export interface FieldAvatar {
6
+ accept?: string
7
+ action?: BaseTextText
8
+ background?: FieldBackground
9
+ border?: FieldInputBorder
10
+ description?: BaseTextText
11
+ disabled?: boolean
12
+ fullWidth?: boolean
13
+ hideError?: boolean
14
+ label?: BaseTextText
15
+ labelIcon?: string
16
+ loading?: boolean
17
+ maxSizeMb?: number
18
+ required?: boolean
19
+ size?: FieldSize
20
+ src?: string
21
+ validation?: VuelidateValidation
22
+ }
23
+
5
24
  export interface FieldCheckbox {
6
25
  description?: BaseTextText
7
26
  disabled?: boolean
@@ -64,6 +64,7 @@ declare global {
64
64
  type ChartistOptions = import('chartist').Options
65
65
  type ChartistPieChartData = import('chartist').PieChartData
66
66
  // Fields
67
+ type FieldAvatar = Fields.FieldAvatar
67
68
  type FieldBackground = Fields.FieldBackground
68
69
  type FieldCheckbox = Fields.FieldCheckbox
69
70
  type FieldDays = Fields.FieldDays
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasmakers/ui",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "private": false,
5
5
  "description": "Reusable Nuxt UI components for SaaS Makers projects",
6
6
  "license": "MIT",