@saasmakers/ui 0.1.60 → 0.1.62

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.
@@ -74,7 +74,7 @@ function onLoad() {
74
74
  @mouseleave="onMouseLeave"
75
75
  >
76
76
  <div
77
- :aria-label="editable ? t('globals.edit') : undefined"
77
+ :aria-label="editable ? t('edit') : undefined"
78
78
  class="relative h-full w-full overflow-hidden"
79
79
  :class="{
80
80
  'rounded-full': circular,
@@ -103,7 +103,7 @@ function onLoad() {
103
103
  v-if="editable && hovered"
104
104
  class="absolute bottom-0 left-0 right-0 w-full bg-gray-900 text-center text-white dark:bg-gray-900 dark:text-white"
105
105
  size="3xs"
106
- :text="t('globals.edit')"
106
+ :text="t('edit')"
107
107
  />
108
108
 
109
109
  <input
@@ -126,13 +126,13 @@ function onLoad() {
126
126
  <i18n lang="json">
127
127
  {
128
128
  "en": {
129
- "habitsCompleted": "Habits completed"
129
+ "edit": "Edit"
130
130
  },
131
131
  "fr": {
132
- "habitsCompleted": "Habitudes accomplies"
132
+ "edit": "Modifier"
133
133
  },
134
134
  "ja": {
135
- "habitsCompleted": "習慣を完了しました"
135
+ "edit": "編集"
136
136
  }
137
137
  }
138
138
  </i18n>
@@ -126,7 +126,7 @@ function onClick(event: MouseEvent) {
126
126
  :light="light"
127
127
  :reverse="reverse"
128
128
  :size="size"
129
- :text="confirming ? t('globals.confirm') : text"
129
+ :text="confirming ? t('confirm') : text"
130
130
  />
131
131
  </span>
132
132
 
@@ -138,3 +138,17 @@ function onClick(event: MouseEvent) {
138
138
  />
139
139
  </component>
140
140
  </template>
141
+
142
+ <i18n lang="json">
143
+ {
144
+ "en": {
145
+ "confirm": "Confirm"
146
+ },
147
+ "fr": {
148
+ "confirm": "Confirmer"
149
+ },
150
+ "ja": {
151
+ "confirm": "確認"
152
+ }
153
+ }
154
+ </i18n>
@@ -132,10 +132,24 @@ function onClick(event: MouseEvent) {
132
132
  no-wrap
133
133
  :reverse="reverse"
134
134
  :size="size"
135
- :text="confirming ? t('globals.confirm') : text"
135
+ :text="confirming ? t('confirm') : text"
136
136
  :truncate="truncate"
137
137
  :underline="underline"
138
138
  :uppercase="uppercase"
139
139
  />
140
140
  </div>
141
141
  </template>
142
+
143
+ <i18n lang="json">
144
+ {
145
+ "en": {
146
+ "confirm": "Confirm"
147
+ },
148
+ "fr": {
149
+ "confirm": "Confirmer"
150
+ },
151
+ "ja": {
152
+ "confirm": "確認"
153
+ }
154
+ }
155
+ </i18n>
@@ -136,7 +136,7 @@ function onClose(event: MouseEvent) {
136
136
 
137
137
  <BaseIcon
138
138
  v-if="hasClose"
139
- v-tooltip="t('globals.close')"
139
+ v-tooltip="t('close')"
140
140
  class="mt-1 self-start text-gray-600 flex-initial dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400"
141
141
  :class="{
142
142
  'ml-2': size === 'xs',
@@ -150,3 +150,17 @@ function onClose(event: MouseEvent) {
150
150
  </div>
151
151
  </div>
152
152
  </template>
153
+
154
+ <i18n lang="json">
155
+ {
156
+ "en": {
157
+ "close": "Close"
158
+ },
159
+ "fr": {
160
+ "close": "Fermer"
161
+ },
162
+ "ja": {
163
+ "close": "閉じる"
164
+ }
165
+ }
166
+ </i18n>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import type { BaseTags } from '../../types/bases'
3
+ import { getIcon } from '../../composables/useIcons'
3
4
 
4
5
  const props = withDefaults(defineProps<BaseTags>(), {
5
6
  active: false,
@@ -146,7 +147,7 @@ function onRemoveTag(event: MouseEvent, tagId?: number | string) {
146
147
  color="indigo"
147
148
  :icon="getIcon('back')"
148
149
  :size="size"
149
- :text="t('globals.cancel')"
150
+ :text="t('cancel')"
150
151
  @click="onGoBack"
151
152
  />
152
153
 
@@ -223,14 +224,17 @@ function onRemoveTag(event: MouseEvent, tagId?: number | string) {
223
224
  <i18n lang="json">
224
225
  {
225
226
  "en": {
227
+ "cancel": "Cancel",
226
228
  "newTag": "New tag",
227
229
  "showMore": "& {value} other | & {value} others"
228
230
  },
229
231
  "fr": {
232
+ "cancel": "Annuler",
230
233
  "newTag": "Nouveau tag",
231
234
  "showMore": "& {value} autre | & {value} autres"
232
235
  },
233
236
  "ja": {
237
+ "cancel": "キャンセル",
234
238
  "newTag": "新しいタグ",
235
239
  "showMore": "& {value} その他 | & {value} その他"
236
240
  }
@@ -0,0 +1,98 @@
1
+ <script lang="ts" setup>
2
+ import type { FieldCheckbox } from '../../types/fields'
3
+
4
+ const props = withDefaults(defineProps<FieldCheckbox>(), {
5
+ description: '',
6
+ disabled: false,
7
+ fullWidth: false,
8
+ hideError: false,
9
+ label: '',
10
+ labelIcon: undefined,
11
+ lineThrough: false,
12
+ loading: false,
13
+ modelValue: false,
14
+ required: false,
15
+ size: 'base',
16
+ truncate: false,
17
+ uppercase: false,
18
+ validation: undefined,
19
+ })
20
+
21
+ const emit = defineEmits<{
22
+ 'update:modelValue': [value: boolean ]
23
+ }>()
24
+
25
+ const input = ref<HTMLInputElement>()
26
+ const uuid = ref(`${Math.floor((1 + Math.random()) * 0x100000)}`)
27
+
28
+ const value = computed({
29
+ get() {
30
+ return props.modelValue
31
+ },
32
+ set(value) {
33
+ emit('update:modelValue', value || false)
34
+ },
35
+ })
36
+
37
+ function onFieldChange(event: Event) {
38
+ const value = (event.target as HTMLInputElement).checked
39
+
40
+ emit('update:modelValue', value)
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <div
46
+ class="relative flex flex-col"
47
+ :class="{ 'w-full': fullWidth }"
48
+ @click.stop
49
+ >
50
+ <div class="flex flex-1 items-center">
51
+ <input
52
+ v-if="!loading"
53
+ :id="uuid"
54
+ ref="input"
55
+ v-model="value"
56
+ class="cursor-pointer border border-gray-300 rounded-lg accent-indigo-700 transition-shadow flex-initial dark:border-gray-700 focus:border-gray-500 hover:border-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500/40 dark:focus:border-gray-500 dark:hover:border-gray-600"
57
+ :class="{
58
+ 'disabled:opacity-50 disabled:cursor-not-allowed': disabled,
59
+
60
+ 'h-3.5 w-3.5': size === 'xs',
61
+ 'h-4 w-4': size === 'sm',
62
+ 'h-4.5 w-4.5': size === 'base',
63
+ 'h-5 w-5': size === 'lg',
64
+ }"
65
+ :disabled="disabled"
66
+ type="checkbox"
67
+ @change="onFieldChange"
68
+ >
69
+
70
+ <BaseSpinner
71
+ v-else
72
+ class="flex-initial"
73
+ :size="size"
74
+ />
75
+
76
+ <FieldLabel
77
+ v-if="label"
78
+ class="flex-1"
79
+ :disabled="disabled"
80
+ :for-field="uuid"
81
+ has-margin-left
82
+ :icon="labelIcon"
83
+ :label="label"
84
+ :line-through="lineThrough && modelValue"
85
+ :required="required"
86
+ :size="size"
87
+ :truncate="truncate"
88
+ />
89
+ </div>
90
+
91
+ <FieldMessage
92
+ :description="description"
93
+ :hide-error="hideError"
94
+ :size="size"
95
+ :validation="validation"
96
+ />
97
+ </div>
98
+ </template>
@@ -0,0 +1,109 @@
1
+ <script lang="ts" setup>
2
+ import type { FieldDays } from '../../types/fields'
3
+
4
+ const props = withDefaults(defineProps<FieldDays>(), { modelValue: () => [] })
5
+
6
+ const emit = defineEmits<{
7
+ 'change': [event: MouseEvent, value: number[]]
8
+ 'update:modelValue': [value: number[]]
9
+ }>()
10
+
11
+ const { t } = useI18n()
12
+
13
+ const daysToDisplay = computed(() => {
14
+ return [
15
+ {
16
+ label: t('days.monday'),
17
+ value: 1,
18
+ },
19
+ {
20
+ label: t('days.tuesday'),
21
+ value: 2,
22
+ },
23
+ {
24
+ label: t('days.wednesday'),
25
+ value: 3,
26
+ },
27
+ {
28
+ label: t('days.thursday'),
29
+ value: 4,
30
+ },
31
+ {
32
+ label: t('days.friday'),
33
+ value: 5,
34
+ },
35
+ {
36
+ label: t('days.saturday'),
37
+ value: 6,
38
+ },
39
+ {
40
+ label: t('days.sunday'),
41
+ value: 0,
42
+ },
43
+ ]
44
+ })
45
+
46
+ function onUpdateDays(event: MouseEvent, day: number) {
47
+ const days = [...props.modelValue]
48
+
49
+ days.includes(day) ? days.splice(days.indexOf(day), 1) : days.push(day)
50
+
51
+ emit('change', event, days)
52
+ emit('update:modelValue', days)
53
+ }
54
+ </script>
55
+
56
+ <template>
57
+ <div class="flex items-center">
58
+ <BaseText
59
+ v-for="day in daysToDisplay"
60
+ :key="day.value"
61
+ class="mr-2 h-7 w-7 flex cursor-pointer items-center justify-center border rounded-full shadow-inner last:mr-0"
62
+ :class="{
63
+ '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),
64
+ '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),
65
+ }"
66
+ size="xs"
67
+ :text="day.label.charAt(0)"
68
+ @click="onUpdateDays($event, day.value)"
69
+ />
70
+ </div>
71
+ </template>
72
+
73
+ <i18n lang="json">
74
+ {
75
+ "en": {
76
+ "days": {
77
+ "monday": "Monday",
78
+ "tuesday": "Tuesday",
79
+ "wednesday": "Wednesday",
80
+ "thursday": "Thursday",
81
+ "friday": "Friday",
82
+ "saturday": "Saturday",
83
+ "sunday": "Sunday"
84
+ }
85
+ },
86
+ "fr": {
87
+ "days": {
88
+ "monday": "Lundi",
89
+ "tuesday": "Mardi",
90
+ "wednesday": "Mercredi",
91
+ "thursday": "Jeudi",
92
+ "friday": "Vendredi",
93
+ "saturday": "Samedi",
94
+ "sunday": "Dimanche"
95
+ }
96
+ },
97
+ "ja": {
98
+ "days": {
99
+ "monday": "月曜日",
100
+ "tuesday": "火曜日",
101
+ "wednesday": "水曜日",
102
+ "thursday": "木曜日",
103
+ "friday": "金曜日",
104
+ "saturday": "土曜日",
105
+ "sunday": "日曜日"
106
+ }
107
+ }
108
+ }
109
+ </i18n>
@@ -0,0 +1,104 @@
1
+ <script lang="ts" setup>
2
+ import type { FieldEmojis } from '../../types/fields'
3
+ import { emojis } from '@saasmakers/shared'
4
+ import { refDebounced } from '@vueuse/core'
5
+
6
+ withDefaults(defineProps<FieldEmojis>(), { modelValue: undefined })
7
+
8
+ const emit = defineEmits<{
9
+ 'click': [event: MouseEvent, emoji: string]
10
+ 'update:modelValue': [value: string]
11
+ }>()
12
+
13
+ const { locale, t } = useI18n()
14
+
15
+ const searchRaw = ref('')
16
+ const searchQuery = refDebounced(searchRaw, 250)
17
+
18
+ const emojisFiltered = computed(() => {
19
+ const searchQueryCleaned = normalizeText(searchQuery.value.trim())
20
+
21
+ if (!searchQueryCleaned) {
22
+ return emojis
23
+ }
24
+
25
+ return emojis
26
+ .map((category) => {
27
+ const filtered = category.emojis.filter((emoji) => {
28
+ const localizedKeywords = (emoji.keywords[locale.value] ?? [])
29
+ const englishKeywords = (emoji.keywords.en ?? [])
30
+ const allKeywords = [...localizedKeywords, ...englishKeywords]
31
+
32
+ return allKeywords.some(keyword => normalizeText(keyword).includes(searchQueryCleaned))
33
+ })
34
+
35
+ return {
36
+ ...category,
37
+ emojis: filtered,
38
+ }
39
+ })
40
+ .filter(category => category.emojis.length > 0)
41
+ })
42
+
43
+ function onEmojiClick(event: MouseEvent, emoji?: string) {
44
+ emit('click', event, emoji || '')
45
+ emit('update:modelValue', emoji || '')
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <div class="border border-gray-200 rounded-lg p-4 shadow-sm dark:border-gray-800">
51
+ <FieldInput
52
+ v-model="searchRaw"
53
+ autofocus
54
+ :placeholder="t('searchEmoji')"
55
+ size="sm"
56
+ />
57
+
58
+ <div class="h-48 overflow-y-auto">
59
+ <template
60
+ v-for="category in emojisFiltered"
61
+ :key="category.category"
62
+ >
63
+ <div
64
+ v-if="category.emojis.length"
65
+ class="mt-4"
66
+ >
67
+ <div class="mb-4">
68
+ <BaseText
69
+ class="mb-2 text-xs text-gray-700 dark:text-gray-300"
70
+ size="xs"
71
+ :text="category.category"
72
+ />
73
+
74
+ <div class="flex flex-wrap">
75
+ <BaseEmoji
76
+ v-for="emoji in category.emojis"
77
+ :key="emoji.id"
78
+ class="rounded-lg p-1.5 hover:bg-gray-200 dark:hover:bg-gray-800"
79
+ clickable
80
+ :emoji="emoji.id"
81
+ size="xl"
82
+ @click="onEmojiClick"
83
+ />
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </template>
88
+ </div>
89
+ </div>
90
+ </template>
91
+
92
+ <i18n lang="json">
93
+ {
94
+ "en": {
95
+ "searchEmoji": "Search an emoji"
96
+ },
97
+ "fr": {
98
+ "searchEmoji": "Rechercher un emoji (en anglais)"
99
+ },
100
+ "ja": {
101
+ "searchEmoji": "絵文字を検索 (英語)"
102
+ }
103
+ }
104
+ </i18n>
@@ -0,0 +1,209 @@
1
+ <script lang="ts" setup>
2
+ import type { FieldInput } from '../../types/fields'
3
+
4
+ const props = withDefaults(defineProps<FieldInput>(), {
5
+ alignment: 'left',
6
+ autocomplete: true,
7
+ autofocus: false,
8
+ background: 'gray',
9
+ border: 'full',
10
+ description: '',
11
+ disabled: false,
12
+ fullWidth: false,
13
+ hideError: false,
14
+ label: '',
15
+ labelIcon: undefined,
16
+ lineThrough: false,
17
+ loading: false,
18
+ lowercaseOnly: false,
19
+ max: undefined,
20
+ min: undefined,
21
+ modelValue: '',
22
+ placeholder: '',
23
+ required: false,
24
+ size: 'base',
25
+ type: 'text',
26
+ uppercase: false,
27
+ validation: undefined,
28
+ })
29
+
30
+ const emit = defineEmits<{
31
+ 'blur': [event: FocusEvent]
32
+ 'focus': [event: FocusEvent]
33
+ 'keyup': [event: KeyboardEvent, value: number | string ]
34
+ 'submit': [event: KeyboardEvent, value: number | string ]
35
+ 'update:modelValue': [value: number | string ]
36
+ }>()
37
+
38
+ const input = ref<HTMLInputElement>()
39
+ const uuid = ref(`${Math.floor((1 + Math.random()) * 0x100000)}`)
40
+
41
+ const { isDesktopBrowser } = useDevice()
42
+
43
+ onMounted(() => {
44
+ if (props.autofocus && input.value && isDesktopBrowser.value) {
45
+ input.value.focus()
46
+ }
47
+ })
48
+
49
+ const value = computed({
50
+ get() {
51
+ return props.modelValue
52
+ },
53
+ set(value) {
54
+ emit('update:modelValue', value || '')
55
+ },
56
+ })
57
+
58
+ function blur() {
59
+ if (input.value) {
60
+ input.value.blur()
61
+ }
62
+ }
63
+
64
+ function focus() {
65
+ if (input.value) {
66
+ input.value.focus()
67
+ }
68
+ }
69
+
70
+ function onFieldBlur(event: FocusEvent) {
71
+ emit('blur', event)
72
+ }
73
+
74
+ function onFieldFocus(event: FocusEvent) {
75
+ emit('focus', event)
76
+ }
77
+
78
+ function onFieldInput(event: Event) {
79
+ let value: number | string = (event.target as HTMLInputElement).value
80
+
81
+ if (props.type === 'number' && props.max && Number.parseInt(value) > props.max) {
82
+ value = props.max
83
+ }
84
+
85
+ if (props.lowercaseOnly) {
86
+ value = value.toString().toLowerCase()
87
+ }
88
+
89
+ emit('update:modelValue', value)
90
+ }
91
+
92
+ function onFieldKeyDown(event: KeyboardEvent) {
93
+ const value = (event.target as HTMLInputElement).value
94
+
95
+ if (['Control', 'Escape'].includes(event.key)) {
96
+ blur()
97
+ }
98
+
99
+ else if (event.key === 'Enter' && props.modelValue) {
100
+ emit('submit', event, value)
101
+ }
102
+
103
+ else if (props.type === 'number' && [
104
+ '+',
105
+ '-',
106
+ ':',
107
+ '^',
108
+ 'e',
109
+ 'E',
110
+ ].includes(event.key)) {
111
+ return event.preventDefault()
112
+ }
113
+
114
+ emit('keyup', event, value)
115
+ }
116
+
117
+ // Expose
118
+ defineExpose({ focus })
119
+ </script>
120
+
121
+ <template>
122
+ <div
123
+ class="relative flex flex-col"
124
+ :class="{ 'w-full': fullWidth }"
125
+ >
126
+ <div
127
+ v-if="label"
128
+ class="flex justify-between"
129
+ >
130
+ <FieldLabel
131
+ :disabled="disabled"
132
+ :for-field="uuid"
133
+ has-margin-bottom
134
+ :icon="labelIcon"
135
+ :label="label"
136
+ :required="required"
137
+ :size="size"
138
+ />
139
+
140
+ <slot name="label" />
141
+ </div>
142
+
143
+ <div
144
+ class="flex items-center"
145
+ :class="{
146
+ 'h-8': size === 'xs',
147
+ 'h-10': size === 'sm',
148
+ 'h-12': size === 'base',
149
+ 'h-14': size === 'lg',
150
+ }"
151
+ >
152
+ <slot name="inputLeft" />
153
+
154
+ <input
155
+ :id="uuid"
156
+ ref="input"
157
+ v-model="value"
158
+ :autocomplete="autocomplete ? 'on' : 'off'"
159
+ class="h-full w-full flex-1 appearance-none items-center rounded-lg outline-none placeholder-gray-600 dark:placeholder-gray-400 focus:placeholder-gray-900 hover:placeholder-gray-900 dark:focus:placeholder-gray-100 dark:hover:placeholder-gray-100"
160
+ :class="{
161
+ 'text-gray-900 dark:text-gray-100': !lineThrough,
162
+ 'text-gray-500 dark:text-gray-500 line-through': lineThrough,
163
+ 'font-medium tracking-tight uppercase': uppercase,
164
+ 'font-normal': !uppercase,
165
+
166
+ 'text-center': alignment === 'center',
167
+ 'text-left': alignment === 'left',
168
+ 'text-right': alignment === 'right',
169
+
170
+ 'bg-gray-100 dark:bg-gray-900': background === 'gray',
171
+ 'bg-white dark:bg-gray-900': background === 'white',
172
+
173
+ 'border shadow-sm border-gray-200 dark:border-gray-800 px-4 hover:border-gray-300 focus:border-gray-400 dark:hover:border-gray-700 dark:focus:border-gray-600': border === 'full',
174
+ 'border-b border-gray-200 dark:border-gray-800 px-10': border === 'bottom',
175
+ 'border-0 p-0': border === 'none',
176
+
177
+ 'text-xs': size === 'xs',
178
+ 'text-sm': size === 'sm',
179
+ 'text-base': size === 'base',
180
+ 'text-lg': size === 'lg',
181
+ }"
182
+ :max="max"
183
+ :min="min"
184
+ :placeholder="placeholder"
185
+ spellcheck="false"
186
+ :type="type"
187
+ @blur="onFieldBlur"
188
+ @focus="onFieldFocus"
189
+ @input="onFieldInput"
190
+ @keydown="onFieldKeyDown"
191
+ >
192
+
193
+ <BaseSpinner
194
+ v-if="loading"
195
+ class="ml-4 flex-initial"
196
+ size="sm"
197
+ />
198
+
199
+ <slot name="inputRight" />
200
+ </div>
201
+
202
+ <FieldMessage
203
+ :description="description"
204
+ :hide-error="hideError"
205
+ :size="size"
206
+ :validation="validation"
207
+ />
208
+ </div>
209
+ </template>