@saasmakers/ui 0.1.59 → 0.1.61

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>
@@ -0,0 +1,179 @@
1
+ <script lang="ts" setup>
2
+ import type { BaseTag } from '../../types/bases'
3
+ import { NuxtLinkLocale } from '#components'
4
+ import { getIcon } from '../../composables/useIcons'
5
+
6
+ const props = withDefaults(defineProps<BaseTag>(), {
7
+ active: false,
8
+ avatar: '',
9
+ circle: '',
10
+ clickable: true,
11
+ color: 'black',
12
+ draggable: false,
13
+ editable: false,
14
+ icon: undefined,
15
+ iconSize: undefined,
16
+ id: undefined,
17
+ isCreation: false,
18
+ light: true,
19
+ removable: false,
20
+ rounded: false,
21
+ size: 'base',
22
+ text: '',
23
+ to: undefined,
24
+ truncate: false,
25
+ uppercase: true,
26
+ })
27
+
28
+ const emit = defineEmits<{
29
+ click: [event: MouseEvent, id?: number | string]
30
+ close: [event: MouseEvent, id?: number | string]
31
+ inputBlur: [event: FocusEvent, name: string, id?: number | string]
32
+ inputSubmit: [event: KeyboardEvent, name: string, id?: number | string]
33
+ remove: [event: MouseEvent, id?: number | string]
34
+ }>()
35
+
36
+ const hovered = ref(false)
37
+
38
+ const form = reactive({ name: '' })
39
+
40
+ onBeforeMount(() => {
41
+ if (!props.isCreation) {
42
+ initializeFormForExistingTag()
43
+ }
44
+ })
45
+
46
+ function initializeFormForExistingTag() {
47
+ form.name = String(props.text)?.substring(1)
48
+ }
49
+
50
+ function onClick(event: MouseEvent) {
51
+ if (!props.removable) {
52
+ emit('click', event, props.id)
53
+ }
54
+ }
55
+
56
+ function onInputBlur(event: FocusEvent) {
57
+ emit('inputBlur', event, form.name.trim(), props.id)
58
+ }
59
+
60
+ function onInputSubmit(event: KeyboardEvent) {
61
+ emit('inputSubmit', event, form.name.trim(), props.id)
62
+ }
63
+
64
+ function onMouseEnter() {
65
+ hovered.value = true
66
+ }
67
+
68
+ function onMouseLeave() {
69
+ hovered.value = false
70
+ }
71
+
72
+ function onRemove(event: MouseEvent) {
73
+ emit('remove', event, props.id)
74
+ }
75
+ </script>
76
+
77
+ <template>
78
+ <span
79
+ class="flex select-none"
80
+ @click.stop="onClick"
81
+ @mouseenter="onMouseEnter"
82
+ @mouseleave="onMouseLeave"
83
+ >
84
+ <component
85
+ :is="to ? NuxtLinkLocale : 'span'"
86
+ class="inline-flex items-center border font-medium"
87
+ :class="{
88
+ 'cursor-pointer': clickable,
89
+ 'rounded-md': !rounded,
90
+ 'rounded-full': rounded,
91
+ 'uppercase': uppercase,
92
+
93
+ '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,
94
+ 'border-gray-900 dark:border-gray-100 bg-gray-900 dark:bg-gray-100 text-white dark:text-black': color === 'black' && (!light || active),
95
+ '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,
96
+ 'border-gray-800 dark:border-gray-200 bg-gray-800 dark:bg-gray-200 text-white dark:text-black': color === 'gray' && (!light || active),
97
+ '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,
98
+ 'border-green-800 dark:border-green-200 bg-green-800 dark:bg-green-200 text-white dark:text-black': color === 'green' && (!light || active),
99
+ '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,
100
+ 'border-indigo-800 dark:border-indigo-200 bg-indigo-800 dark:bg-indigo-200 text-white dark:text-black': color === 'indigo' && (!light || active),
101
+ 'border-orange-700 dark:border-orange-300 bg-white dark:bg-gray-900 text-orange-700 dark:text-orange-300': color === 'orange' && light && !active,
102
+ 'border-orange-800 dark:border-orange-200 bg-orange-800 dark:bg-orange-200 text-white dark:text-black': color === 'orange' && (!light || active),
103
+ 'border-red-700 dark:border-red-300 bg-white dark:bg-gray-900 text-red-700 dark:text-red-300': color === 'red' && light && !active,
104
+ 'border-red-800 dark:border-red-200 bg-red-800 dark:bg-red-200 text-white dark:text-black': color === 'red' && (!light || active),
105
+ 'border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200': color === 'white' && light && !active,
106
+ 'border-white dark:border-gray-900 text-white dark:text-black': color === 'white' && (!light || active),
107
+ '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,
108
+
109
+ 'py-1.5 px-2.5 text-2xs': size === 'sm',
110
+ 'py-2 px-3 text-xs': size === 'base',
111
+ }"
112
+ :to="to"
113
+ >
114
+ <BaseIcon
115
+ v-if="draggable"
116
+ class="js-drag-handle mr-2 cursor-move"
117
+ clickable
118
+ color="gray"
119
+ :icon="getIcon('drag')"
120
+ />
121
+
122
+ <span
123
+ v-if="avatar || circle || icon"
124
+ class="mr-1 flex items-center text-center"
125
+ :class="{
126
+ 'h-2.5 w-2.5': size === 'sm' || iconSize === 2.5,
127
+ 'h-3 w-3': size === 'base' || iconSize === 3,
128
+ 'h-3.5 w-3.5': iconSize === 3.5,
129
+ 'h-4 w-4': iconSize === 4,
130
+ }"
131
+ >
132
+ <BaseAvatar
133
+ v-if="avatar"
134
+ class="h-full w-full"
135
+ rounded="sm"
136
+ :shadow="false"
137
+ :src="avatar"
138
+ />
139
+
140
+ <BaseIcon
141
+ v-else-if="icon"
142
+ class="h-full w-full"
143
+ :icon="icon"
144
+ />
145
+ </span>
146
+
147
+ <span
148
+ v-if="$slots.left"
149
+ class="mr-1"
150
+ >
151
+ <slot name="left" />
152
+ </span>
153
+
154
+ <BaseText
155
+ v-if="!editable"
156
+ class="whitespace-nowrap outline-none"
157
+ :class="{ truncate }"
158
+ :text="text"
159
+ />
160
+
161
+ <FieldInput
162
+ v-else
163
+ v-model="form.name"
164
+ border="none"
165
+ class="w-16"
166
+ size="sm"
167
+ @blur="onInputBlur"
168
+ @submit="onInputSubmit"
169
+ />
170
+
171
+ <BaseIcon
172
+ v-if="removable"
173
+ class="ml-1.5 text-red-700 dark:text-red-300 hover:text-black dark:hover:text-white"
174
+ :icon="getIcon('closeCircle')"
175
+ @click.prevent.stop="onRemove"
176
+ />
177
+ </component>
178
+ </span>
179
+ </template>
@@ -0,0 +1,242 @@
1
+ <script lang="ts" setup>
2
+ import type { BaseTags } from '../../types/bases'
3
+ import { getIcon } from '../../composables/useIcons'
4
+
5
+ const props = withDefaults(defineProps<BaseTags>(), {
6
+ active: false,
7
+ clickable: true,
8
+ draggable: false,
9
+ editable: false,
10
+ hasBack: false,
11
+ hasCreation: false,
12
+ maxTags: Number.POSITIVE_INFINITY,
13
+ removable: false,
14
+ selectable: false,
15
+ selectableUnique: false,
16
+ size: 'base',
17
+ tags: () => [],
18
+ value: () => [],
19
+ wrap: false,
20
+ })
21
+
22
+ const emit = defineEmits<{
23
+ 'attach': [event: MouseEvent, tagId?: number | string]
24
+ 'back': [event: MouseEvent]
25
+ 'click': [event: MouseEvent]
26
+ 'create': [event: MouseEvent, name: string]
27
+ 'detach': [event: MouseEvent, tagId?: number | string]
28
+ 'remove': [event: MouseEvent, tagId?: number | string]
29
+ 'update': [event: MouseEvent, name: string, tagId?: number | string]
30
+ 'update:modelValue': [value: (number | string)[]]
31
+ }>()
32
+
33
+ const keyForTagCreation = ref(Date.now())
34
+ const keyForTagUpdate = ref(Date.now())
35
+ const root = ref<HTMLDivElement>()
36
+ const showingAllTags = ref(false)
37
+ const showingTagCreationField = ref(false)
38
+
39
+ const { t } = useI18n()
40
+
41
+ const sortedTags = computed({
42
+ get() {
43
+ return props.tags
44
+ .slice()
45
+ .filter((_tag, tagIndex) => tagIndex < props.maxTags || showingAllTags.value)
46
+ .sort((tagA, tagB) => (tagA.order! < tagB.order! ? -1 : 0))
47
+ },
48
+
49
+ set(tags) {
50
+ const suiteForTags = tags
51
+ .map((tag, order) => ({
52
+ id: tag.id,
53
+ order,
54
+ }))
55
+ .map(tag => ({
56
+ id: tag.id,
57
+ order: tag.order,
58
+ }))
59
+
60
+ return suiteForTags
61
+ },
62
+ })
63
+
64
+ function onCreateTag(event: MouseEvent, name: string) {
65
+ showingTagCreationField.value = false
66
+ keyForTagCreation.value = Date.now()
67
+
68
+ emit('create', event, name)
69
+ }
70
+
71
+ function onGoBack(event: MouseEvent) {
72
+ emit('back', event)
73
+ }
74
+
75
+ function onShowTagField() {
76
+ if (!showingTagCreationField.value) {
77
+ showingTagCreationField.value = true
78
+
79
+ // Focus contenteditable
80
+ nextTick(() => {
81
+ const element = root.value?.querySelector<HTMLInputElement>('input[type="text"]')
82
+
83
+ element?.focus()
84
+ })
85
+ }
86
+ }
87
+
88
+ function onShowAllTags() {
89
+ showingAllTags.value = true
90
+ }
91
+
92
+ function onTagClick(event: MouseEvent, tagId?: number | string) {
93
+ if (props.selectable && tagId) {
94
+ let value = [...props.value]
95
+
96
+ if (value.includes(tagId)) {
97
+ value.splice(value.indexOf(tagId), 1)
98
+ emit('detach', event, tagId)
99
+ }
100
+ else if (props.selectableUnique) {
101
+ value = [tagId]
102
+ emit('attach', event, tagId)
103
+ }
104
+ else {
105
+ value.push(tagId)
106
+ emit('attach', event, tagId)
107
+ }
108
+
109
+ emit('update:modelValue', value)
110
+ }
111
+ }
112
+
113
+ function onUpdateTag(event: MouseEvent, name: string, tagId?: number | string) {
114
+ if (name) {
115
+ emit('update', event, name, tagId)
116
+ }
117
+
118
+ else {
119
+ keyForTagUpdate.value = Date.now()
120
+ }
121
+ }
122
+
123
+ function onRemoveTag(event: MouseEvent, tagId?: number | string) {
124
+ emit('remove', event, tagId)
125
+ }
126
+ </script>
127
+
128
+ <template>
129
+ <div
130
+ ref="root"
131
+ class="flex items-center"
132
+ :class="{
133
+ 'flex-wrap': wrap,
134
+ 'w-full overflow-x-auto': !wrap,
135
+
136
+ '-mt-0.75 -ml-0.75': size === 'sm',
137
+ '-mt-1 -ml-1': size === 'base',
138
+ }"
139
+ >
140
+ <BaseTag
141
+ v-if="hasBack"
142
+ key="BaseTagToGoBack"
143
+ :class="{
144
+ 'm-0.75': size === 'sm',
145
+ 'm-1': size === 'base',
146
+ }"
147
+ color="indigo"
148
+ :icon="getIcon('back')"
149
+ :size="size"
150
+ :text="t('cancel')"
151
+ @click="onGoBack"
152
+ />
153
+
154
+ <BaseTag
155
+ v-if="hasCreation"
156
+ :key="keyForTagCreation"
157
+ :class="{
158
+ 'm-0.75': size === 'sm',
159
+ 'm-1': size === 'base',
160
+ }"
161
+ color="indigo"
162
+ :editable="showingTagCreationField"
163
+ :icon="showingTagCreationField ? getIcon('tags') : getIcon('plus')"
164
+ is-creation
165
+ :light="false"
166
+ :size="size"
167
+ :text="showingTagCreationField ? '' : t('newTag')"
168
+ @click="onShowTagField"
169
+ @input:blur="onCreateTag"
170
+ @input:submit="onCreateTag"
171
+ />
172
+
173
+ <div class="flex items-center">
174
+ <span
175
+ v-for="(tag, tagIndex) in sortedTags"
176
+ :key="tag.id ? `${tag.id}_${tag.order}_${keyForTagUpdate}` : tagIndex"
177
+ class="flex flex-wrap items-center"
178
+ :class="{
179
+ 'm-0.75': size === 'sm',
180
+ 'm-1': size === 'base',
181
+ }"
182
+ >
183
+ <template v-if="tag">
184
+ <BaseTag
185
+ :id="tag.id || tagIndex"
186
+ :key="`${tag.id}_${tagIndex}`"
187
+ :active="tag.id && value.includes(tag.id) || tag.active"
188
+ :avatar="tag.avatar"
189
+ :circle="tag.circle"
190
+ :clickable="clickable"
191
+ :color="tag.color"
192
+ :draggable="draggable"
193
+ :editable="editable"
194
+ :icon="tag.icon"
195
+ :icon-size="tag.iconSize"
196
+ :light="tag.light"
197
+ :removable="removable"
198
+ :size="size"
199
+ :text="tag.text"
200
+ :to="tag.to"
201
+ @click="onTagClick"
202
+ @input:blur="onUpdateTag"
203
+ @input:submit="onUpdateTag"
204
+ @remove="onRemoveTag"
205
+ />
206
+ </template>
207
+ </span>
208
+ </div>
209
+
210
+ <BaseTag
211
+ v-if="tags.length > maxTags && !showingAllTags"
212
+ key="showMore"
213
+ :class="{
214
+ 'm-0.75': size === 'sm',
215
+ 'm-1': size === 'base',
216
+ }"
217
+ :size="size"
218
+ :text="t('showMore', { value: tags.length - maxTags })"
219
+ @click="onShowAllTags"
220
+ />
221
+ </div>
222
+ </template>
223
+
224
+ <i18n lang="json">
225
+ {
226
+ "en": {
227
+ "cancel": "Cancel",
228
+ "newTag": "New tag",
229
+ "showMore": "& {value} other | & {value} others"
230
+ },
231
+ "fr": {
232
+ "cancel": "Annuler",
233
+ "newTag": "Nouveau tag",
234
+ "showMore": "& {value} autre | & {value} autres"
235
+ },
236
+ "ja": {
237
+ "cancel": "キャンセル",
238
+ "newTag": "新しいタグ",
239
+ "showMore": "& {value} その他 | & {value} その他"
240
+ }
241
+ }
242
+ </i18n>
@@ -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>