@saasmakers/ui 0.1.59 → 0.1.60

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,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,238 @@
1
+ <script lang="ts" setup>
2
+ import type { BaseTags } from '../../types/bases'
3
+
4
+ const props = withDefaults(defineProps<BaseTags>(), {
5
+ active: false,
6
+ clickable: true,
7
+ draggable: false,
8
+ editable: false,
9
+ hasBack: false,
10
+ hasCreation: false,
11
+ maxTags: Number.POSITIVE_INFINITY,
12
+ removable: false,
13
+ selectable: false,
14
+ selectableUnique: false,
15
+ size: 'base',
16
+ tags: () => [],
17
+ value: () => [],
18
+ wrap: false,
19
+ })
20
+
21
+ const emit = defineEmits<{
22
+ 'attach': [event: MouseEvent, tagId?: number | string]
23
+ 'back': [event: MouseEvent]
24
+ 'click': [event: MouseEvent]
25
+ 'create': [event: MouseEvent, name: string]
26
+ 'detach': [event: MouseEvent, tagId?: number | string]
27
+ 'remove': [event: MouseEvent, tagId?: number | string]
28
+ 'update': [event: MouseEvent, name: string, tagId?: number | string]
29
+ 'update:modelValue': [value: (number | string)[]]
30
+ }>()
31
+
32
+ const keyForTagCreation = ref(Date.now())
33
+ const keyForTagUpdate = ref(Date.now())
34
+ const root = ref<HTMLDivElement>()
35
+ const showingAllTags = ref(false)
36
+ const showingTagCreationField = ref(false)
37
+
38
+ const { t } = useI18n()
39
+
40
+ const sortedTags = computed({
41
+ get() {
42
+ return props.tags
43
+ .slice()
44
+ .filter((_tag, tagIndex) => tagIndex < props.maxTags || showingAllTags.value)
45
+ .sort((tagA, tagB) => (tagA.order! < tagB.order! ? -1 : 0))
46
+ },
47
+
48
+ set(tags) {
49
+ const suiteForTags = tags
50
+ .map((tag, order) => ({
51
+ id: tag.id,
52
+ order,
53
+ }))
54
+ .map(tag => ({
55
+ id: tag.id,
56
+ order: tag.order,
57
+ }))
58
+
59
+ return suiteForTags
60
+ },
61
+ })
62
+
63
+ function onCreateTag(event: MouseEvent, name: string) {
64
+ showingTagCreationField.value = false
65
+ keyForTagCreation.value = Date.now()
66
+
67
+ emit('create', event, name)
68
+ }
69
+
70
+ function onGoBack(event: MouseEvent) {
71
+ emit('back', event)
72
+ }
73
+
74
+ function onShowTagField() {
75
+ if (!showingTagCreationField.value) {
76
+ showingTagCreationField.value = true
77
+
78
+ // Focus contenteditable
79
+ nextTick(() => {
80
+ const element = root.value?.querySelector<HTMLInputElement>('input[type="text"]')
81
+
82
+ element?.focus()
83
+ })
84
+ }
85
+ }
86
+
87
+ function onShowAllTags() {
88
+ showingAllTags.value = true
89
+ }
90
+
91
+ function onTagClick(event: MouseEvent, tagId?: number | string) {
92
+ if (props.selectable && tagId) {
93
+ let value = [...props.value]
94
+
95
+ if (value.includes(tagId)) {
96
+ value.splice(value.indexOf(tagId), 1)
97
+ emit('detach', event, tagId)
98
+ }
99
+ else if (props.selectableUnique) {
100
+ value = [tagId]
101
+ emit('attach', event, tagId)
102
+ }
103
+ else {
104
+ value.push(tagId)
105
+ emit('attach', event, tagId)
106
+ }
107
+
108
+ emit('update:modelValue', value)
109
+ }
110
+ }
111
+
112
+ function onUpdateTag(event: MouseEvent, name: string, tagId?: number | string) {
113
+ if (name) {
114
+ emit('update', event, name, tagId)
115
+ }
116
+
117
+ else {
118
+ keyForTagUpdate.value = Date.now()
119
+ }
120
+ }
121
+
122
+ function onRemoveTag(event: MouseEvent, tagId?: number | string) {
123
+ emit('remove', event, tagId)
124
+ }
125
+ </script>
126
+
127
+ <template>
128
+ <div
129
+ ref="root"
130
+ class="flex items-center"
131
+ :class="{
132
+ 'flex-wrap': wrap,
133
+ 'w-full overflow-x-auto': !wrap,
134
+
135
+ '-mt-0.75 -ml-0.75': size === 'sm',
136
+ '-mt-1 -ml-1': size === 'base',
137
+ }"
138
+ >
139
+ <BaseTag
140
+ v-if="hasBack"
141
+ key="BaseTagToGoBack"
142
+ :class="{
143
+ 'm-0.75': size === 'sm',
144
+ 'm-1': size === 'base',
145
+ }"
146
+ color="indigo"
147
+ :icon="getIcon('back')"
148
+ :size="size"
149
+ :text="t('globals.cancel')"
150
+ @click="onGoBack"
151
+ />
152
+
153
+ <BaseTag
154
+ v-if="hasCreation"
155
+ :key="keyForTagCreation"
156
+ :class="{
157
+ 'm-0.75': size === 'sm',
158
+ 'm-1': size === 'base',
159
+ }"
160
+ color="indigo"
161
+ :editable="showingTagCreationField"
162
+ :icon="showingTagCreationField ? getIcon('tags') : getIcon('plus')"
163
+ is-creation
164
+ :light="false"
165
+ :size="size"
166
+ :text="showingTagCreationField ? '' : t('newTag')"
167
+ @click="onShowTagField"
168
+ @input:blur="onCreateTag"
169
+ @input:submit="onCreateTag"
170
+ />
171
+
172
+ <div class="flex items-center">
173
+ <span
174
+ v-for="(tag, tagIndex) in sortedTags"
175
+ :key="tag.id ? `${tag.id}_${tag.order}_${keyForTagUpdate}` : tagIndex"
176
+ class="flex flex-wrap items-center"
177
+ :class="{
178
+ 'm-0.75': size === 'sm',
179
+ 'm-1': size === 'base',
180
+ }"
181
+ >
182
+ <template v-if="tag">
183
+ <BaseTag
184
+ :id="tag.id || tagIndex"
185
+ :key="`${tag.id}_${tagIndex}`"
186
+ :active="tag.id && value.includes(tag.id) || tag.active"
187
+ :avatar="tag.avatar"
188
+ :circle="tag.circle"
189
+ :clickable="clickable"
190
+ :color="tag.color"
191
+ :draggable="draggable"
192
+ :editable="editable"
193
+ :icon="tag.icon"
194
+ :icon-size="tag.iconSize"
195
+ :light="tag.light"
196
+ :removable="removable"
197
+ :size="size"
198
+ :text="tag.text"
199
+ :to="tag.to"
200
+ @click="onTagClick"
201
+ @input:blur="onUpdateTag"
202
+ @input:submit="onUpdateTag"
203
+ @remove="onRemoveTag"
204
+ />
205
+ </template>
206
+ </span>
207
+ </div>
208
+
209
+ <BaseTag
210
+ v-if="tags.length > maxTags && !showingAllTags"
211
+ key="showMore"
212
+ :class="{
213
+ 'm-0.75': size === 'sm',
214
+ 'm-1': size === 'base',
215
+ }"
216
+ :size="size"
217
+ :text="t('showMore', { value: tags.length - maxTags })"
218
+ @click="onShowAllTags"
219
+ />
220
+ </div>
221
+ </template>
222
+
223
+ <i18n lang="json">
224
+ {
225
+ "en": {
226
+ "newTag": "New tag",
227
+ "showMore": "& {value} other | & {value} others"
228
+ },
229
+ "fr": {
230
+ "newTag": "Nouveau tag",
231
+ "showMore": "& {value} autre | & {value} autres"
232
+ },
233
+ "ja": {
234
+ "newTag": "新しいタグ",
235
+ "showMore": "& {value} その他 | & {value} その他"
236
+ }
237
+ }
238
+ </i18n>
@@ -12,12 +12,16 @@ const icons = {
12
12
  arrowDown: 'solar:alt-arrow-down-bold',
13
13
  arrowRight: 'solar:alt-arrow-right-bold',
14
14
  arrowUp: 'solar:alt-arrow-up-bold',
15
+ back: 'hugeicons:arrow-turn-backward',
15
16
  checkCircle: 'hugeicons:checkmark-circle-02',
16
17
  close: 'hugeicons:cancel-01',
17
18
  closeCircle: 'hugeicons:cancel-circle',
18
19
  default: 'hugeicons:help-circle',
20
+ drag: 'mdi:drag-horizontal-variant',
19
21
  exclamationCircle: 'hugeicons:alert-circle',
20
22
  infoCircle: 'hugeicons:information-circle',
23
+ plus: 'hugeicons:add-01',
24
+ tags: 'hugeicons:tags',
21
25
  } as const
22
26
 
23
27
  export function getIcon(attribute: keyof typeof icons) {
@@ -262,6 +262,48 @@ export interface BaseSpinner {
262
262
  uppercase?: boolean
263
263
  }
264
264
 
265
+ export interface BaseTag {
266
+ active?: boolean
267
+ avatar?: string
268
+ circle?: string
269
+ clickable?: boolean
270
+ color?: BaseColor
271
+ draggable?: boolean
272
+ editable?: boolean
273
+ icon?: BaseIconValue
274
+ iconSize?: number
275
+ id?: number
276
+ isCreation?: boolean
277
+ light?: boolean
278
+ order?: number
279
+ removable?: boolean
280
+ rounded?: boolean
281
+ size?: BaseTagSize
282
+ text: BaseTextText
283
+ to?: RouteLocationNamedI18n
284
+ truncate?: boolean
285
+ uppercase?: boolean
286
+ }
287
+
288
+ export type BaseTagSize = 'base' | 'sm'
289
+
290
+ export interface BaseTags {
291
+ active?: boolean
292
+ clickable?: boolean
293
+ draggable?: boolean
294
+ editable?: boolean
295
+ hasBack?: boolean
296
+ hasCreation?: boolean
297
+ maxTags?: number
298
+ removable?: boolean
299
+ selectable?: boolean
300
+ selectableUnique?: boolean
301
+ size?: BaseTagSize
302
+ tags: BaseTag[]
303
+ value?: (number | string)[]
304
+ wrap?: boolean
305
+ }
306
+
265
307
  export interface BaseText {
266
308
  background?: BaseTextBackground
267
309
  bold?: boolean
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saasmakers/ui",
3
3
  "type": "module",
4
- "version": "0.1.59",
4
+ "version": "0.1.60",
5
5
  "private": false,
6
6
  "description": "Reusable Nuxt UI components for SaaS Makers projects",
7
7
  "license": "MIT",