sprintify-ui 0.12.0 → 0.12.1

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.
@@ -22,6 +22,10 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
22
22
  default: string;
23
23
  type: PropType<Placement>;
24
24
  };
25
+ twButton: {
26
+ default: string;
27
+ type: StringConstructor;
28
+ };
25
29
  }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
26
30
  animated: {
27
31
  default: boolean;
@@ -35,8 +39,13 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
35
39
  default: string;
36
40
  type: PropType<Placement>;
37
41
  };
42
+ twButton: {
43
+ default: string;
44
+ type: StringConstructor;
45
+ };
38
46
  }>> & Readonly<{}>, {
39
47
  placement: Placement;
48
+ twButton: string;
40
49
  animated: boolean;
41
50
  keepAlive: boolean;
42
51
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -68,6 +68,7 @@ declare const messages: {
68
68
  page: string;
69
69
  pagination_detail_1: string;
70
70
  pagination_detail_2: string;
71
+ paste_link: string;
71
72
  postal_code_zip_code: string;
72
73
  previous: string;
73
74
  previous_month: string;
@@ -170,6 +171,7 @@ declare const messages: {
170
171
  page: string;
171
172
  pagination_detail_1: string;
172
173
  pagination_detail_2: string;
174
+ paste_link: string;
173
175
  postal_code_zip_code: string;
174
176
  previous: string;
175
177
  previous_month: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprintify-ui",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "generate-llm-txt": "node scripts/generate-llm-txt.js",
@@ -38,6 +38,8 @@
38
38
  "dependencies": {
39
39
  "@floating-ui/vue": "^1.0.6",
40
40
  "@headlessui/vue": "^1.7.19",
41
+ "@tiptap/extension-highlight": "^3.22.4",
42
+ "@tiptap/extension-image": "^3.22.4",
41
43
  "@tiptap/extension-subscript": "^3.20.4",
42
44
  "@tiptap/extension-superscript": "^3.20.4",
43
45
  "color2k": "^2.0.3",
@@ -1,6 +1,8 @@
1
1
  <template>
2
2
  <Popover v-slot="{ open }">
3
- <PopoverButton class="outline-none">
3
+ <PopoverButton
4
+ :class="twButtonInternal"
5
+ >
4
6
  <div ref="buttonRef">
5
7
  <slot name="button" />
6
8
  </div>
@@ -43,6 +45,7 @@
43
45
  import { autoUpdate, offset, flip, shift, useFloating, Placement } from '@floating-ui/vue';
44
46
  import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue';
45
47
  import { unrefElement } from '@vueuse/core';
48
+ import { twMerge } from 'tailwind-merge';
46
49
  import { PropType } from 'vue';
47
50
 
48
51
  const buttonRef = ref<InstanceType<typeof PopoverButton> | null>(null);
@@ -62,7 +65,11 @@ const props = defineProps({
62
65
  placement: {
63
66
  default: 'bottom-end',
64
67
  type: String as PropType<Placement>,
65
- }
68
+ },
69
+ twButton: {
70
+ default: 'outline-none',
71
+ type: String,
72
+ },
66
73
  });
67
74
 
68
75
  const { floatingStyles } = useFloating(buttonRefEl, dropdownRef, {
@@ -71,4 +78,11 @@ const { floatingStyles } = useFloating(buttonRefEl, dropdownRef, {
71
78
  whileElementsMounted: autoUpdate,
72
79
  });
73
80
 
81
+ const twButtonInternal = computed(() => {
82
+ return twMerge(
83
+ 'outline-none',
84
+ props.twButton,
85
+ );
86
+ });
87
+
74
88
  </script>
@@ -6,20 +6,122 @@
6
6
  noMargin ? 'tip-tap-no-margin' : ''
7
7
  ]"
8
8
  >
9
- <div class="divide-x px-1 py-1 border-b flex">
10
- <button
9
+ <div class="px-1 py-1 border-b flex items-center">
10
+ <template
11
11
  v-for="action in filteredActions"
12
12
  :key="action.name"
13
- :class="[buttonClass, editor.isActive(action.name) ? 'bg-slate-200' : '']"
14
- type="button"
15
- :disabled="disabled"
16
- @click="action.command()"
17
13
  >
18
- <BaseIcon
19
- class="h-5 w-5"
20
- :icon="action.icon"
21
- />
22
- </button>
14
+ <BaseDropdown
15
+ v-if="action.name === 'highlight'"
16
+ placement="bottom"
17
+ tw-button="block"
18
+ >
19
+ <template #button>
20
+ <div :class="[buttonClass, editor.isActive('highlight') ? 'bg-slate-200' : '']">
21
+ <BaseIcon
22
+ class="h-4 w-4 text-slate-600"
23
+ :icon="action.icon"
24
+ />
25
+ </div>
26
+ </template>
27
+ <template #dropdown="{ close }">
28
+ <div class="bg-white rounded-full shadow-lg border border-slate-200 px-4 py-3 flex items-center gap-2">
29
+ <button
30
+ v-for="color in highlightColors"
31
+ :key="color"
32
+ type="button"
33
+ class="w-5 h-5 rounded-full ring-inset ring-1 ring-black/20"
34
+ :style="{ backgroundColor: color }"
35
+ @click="setHighlight(color); close()"
36
+ />
37
+ <div class="w-px h-5 bg-slate-200" />
38
+ <button
39
+ type="button"
40
+ class="w-5 h-5 flex items-center justify-center bg-white "
41
+ @click="unsetHighlight(); close()"
42
+ >
43
+ <BaseIcon
44
+ class="h-5 w-5 text-slate-600"
45
+ icon="lucide:ban"
46
+ />
47
+ </button>
48
+ </div>
49
+ </template>
50
+ </BaseDropdown>
51
+ <BaseDropdown
52
+ v-else-if="action.name === 'link'"
53
+ placement="bottom"
54
+ tw-button="block"
55
+ >
56
+ <template #button>
57
+ <div
58
+ :class="[buttonClass, editor.isActive('link') ? 'bg-slate-200' : '']"
59
+ @click="openLinkDropdown"
60
+ >
61
+ <BaseIcon
62
+ class="h-4 w-4 text-slate-600"
63
+ :icon="action.icon"
64
+ />
65
+ </div>
66
+ </template>
67
+ <template #dropdown="{ close }">
68
+ <div class="bg-white rounded-full shadow-lg border border-slate-200 pl-4 pr-3 py-2 flex items-center gap-2 focus:outline-none outline-none">
69
+ <input
70
+ :ref="focusLinkInput"
71
+ v-model="linkUrl"
72
+ type="text"
73
+ class="bg-transparent border-none border-0 border-transparent shadow-none focus:border-none focus:outline-none focus:ring-0 focus:shadow-none outline-none ring-0 text-sm text-slate-700 placeholder-slate-400 w-56 py-1 px-2"
74
+ :placeholder="t('sui.paste_link')"
75
+ @keydown.enter.prevent="applyLink(); close()"
76
+ >
77
+ <button
78
+ type="button"
79
+ class="w-7 h-7 flex items-center justify-center rounded-full hover:bg-slate-100"
80
+ @click="applyLink(); close()"
81
+ >
82
+ <BaseIcon
83
+ class="h-4 w-4 text-slate-600"
84
+ icon="lucide:corner-down-left"
85
+ />
86
+ </button>
87
+ <div class="w-px h-5 bg-slate-200" />
88
+ <button
89
+ type="button"
90
+ class="w-7 h-7 flex items-center justify-center rounded-full hover:bg-slate-100 disabled:opacity-40 disabled:hover:bg-transparent"
91
+ :disabled="!linkUrl"
92
+ @click="openLink(linkUrl)"
93
+ >
94
+ <BaseIcon
95
+ class="h-4 w-4 text-slate-600"
96
+ icon="lucide:external-link"
97
+ />
98
+ </button>
99
+ <button
100
+ type="button"
101
+ class="w-7 h-7 flex items-center justify-center rounded-full hover:bg-slate-100"
102
+ @click="removeLink(); close()"
103
+ >
104
+ <BaseIcon
105
+ class="h-4 w-4 text-slate-600"
106
+ icon="lucide:trash-2"
107
+ />
108
+ </button>
109
+ </div>
110
+ </template>
111
+ </BaseDropdown>
112
+ <button
113
+ v-else
114
+ :class="[buttonClass, editor.isActive(action.name) ? 'bg-slate-200' : '']"
115
+ type="button"
116
+ :disabled="disabled"
117
+ @click="action.command()"
118
+ >
119
+ <BaseIcon
120
+ class="h-4 w-4 text-slate-600"
121
+ :icon="action.icon"
122
+ />
123
+ </button>
124
+ </template>
23
125
  </div>
24
126
 
25
127
  <div
@@ -31,6 +133,14 @@
31
133
  :editor="editor"
32
134
  />
33
135
  </div>
136
+
137
+ <input
138
+ ref="imageInput"
139
+ type="file"
140
+ accept="image/*"
141
+ class="hidden"
142
+ @change="onImageSelected"
143
+ >
34
144
  </div>
35
145
  </template>
36
146
 
@@ -41,9 +151,14 @@ import Link from '@tiptap/extension-link';
41
151
  import Superscript from '@tiptap/extension-superscript';
42
152
  import Subscript from '@tiptap/extension-subscript';
43
153
  import Strike from '@tiptap/extension-strike'
154
+ import Image from '@tiptap/extension-image';
155
+ import Highlight from '@tiptap/extension-highlight';
44
156
  import { Placeholder } from '@tiptap/extensions'
45
157
  import { Footnotes, FootnoteReference, Footnote } from "tiptap-footnotes";
46
158
  import StarterKit from '@tiptap/starter-kit';
159
+ import resizeImageForUpload from '@/utils/resizeImageForUpload';
160
+ import BaseDropdown from './BaseDropdown.vue';
161
+ import { t } from '@/i18n';
47
162
 
48
163
  const emit = defineEmits(['update:modelValue']);
49
164
 
@@ -78,13 +193,20 @@ const editor = new Editor({
78
193
  Strike,
79
194
  Superscript,
80
195
  Subscript,
196
+ Image.configure({
197
+ inline: false,
198
+ allowBase64: true,
199
+ }),
200
+ Highlight.configure({
201
+ multicolor: true,
202
+ }),
81
203
  Footnotes,
82
204
  Footnote,
83
205
  FootnoteReference,
84
206
  ],
85
207
  editorProps: {
86
208
  attributes: {
87
- class: 'prose prose-sm mx-auto max-w-full focus:outline-none',
209
+ class: 'prose prose-sm focus:outline-none',
88
210
  },
89
211
  },
90
212
  content: props.modelValue,
@@ -143,37 +265,117 @@ onBeforeUnmount(() => {
143
265
  }
144
266
  });
145
267
 
146
- function setLink() {
147
- const previousUrl = editor.getAttributes('link').href;
148
- const url = window.prompt('URL', previousUrl);
268
+ const imageInput = ref<HTMLInputElement | null>(null);
269
+
270
+ function pickImage() {
271
+ imageInput.value?.click();
272
+ }
273
+
274
+ const highlightColors = [
275
+ '#BBF7D0',
276
+ '#BFDBFE',
277
+ '#FECACA',
278
+ '#E9D5FF',
279
+ '#FEF08A',
280
+ ];
281
+
282
+ function setHighlight(color: string) {
283
+ editor.chain().focus().setHighlight({ color }).run();
284
+ }
285
+
286
+ function unsetHighlight() {
287
+ editor.chain().focus().unsetHighlight().run();
288
+ }
149
289
 
150
- // cancelled
151
- if (url === null) {
290
+ const MAX_IMAGE_BYTES = 1024 * 1024;
291
+
292
+ async function onImageSelected(event: Event) {
293
+ const target = event.target as HTMLInputElement;
294
+ const file = target.files?.[0];
295
+ target.value = '';
296
+
297
+ if (!file) {
152
298
  return;
153
299
  }
154
300
 
155
- // empty
301
+ let blob: Blob = file;
302
+
303
+ try {
304
+ blob = await resizeImageForUpload(file, { maxBytes: MAX_IMAGE_BYTES });
305
+ } catch (error) {
306
+ console.error(error);
307
+ }
308
+
309
+ const reader = new FileReader();
310
+
311
+ reader.onload = () => {
312
+ const src = reader.result;
313
+
314
+ if (typeof src === 'string') {
315
+ editor.chain().focus().setImage({ src }).run();
316
+ }
317
+ };
318
+
319
+ reader.readAsDataURL(blob);
320
+ }
321
+
322
+ const linkUrl = ref('');
323
+
324
+ function openLinkDropdown() {
325
+ linkUrl.value = editor.getAttributes('link').href ?? '';
326
+ }
327
+
328
+ let focusedLinkInput: HTMLInputElement | null = null;
329
+
330
+ function focusLinkInput(el: unknown) {
331
+ if (el instanceof HTMLInputElement) {
332
+ if (focusedLinkInput === el) {
333
+ return;
334
+ }
335
+
336
+ focusedLinkInput = el;
337
+ el.focus();
338
+ el.select();
339
+ } else {
340
+ focusedLinkInput = null;
341
+ }
342
+ }
343
+
344
+ function applyLink() {
345
+ const url = linkUrl.value.trim();
346
+
156
347
  if (url === '') {
157
348
  editor.chain().focus().extendMarkRange('link').unsetLink().run();
158
349
 
159
350
  return;
160
351
  }
161
352
 
162
- // update link
163
353
  editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
164
354
  }
165
355
 
356
+ function removeLink() {
357
+ editor.chain().focus().extendMarkRange('link').unsetLink().run();
358
+ }
359
+
360
+ function openLink(url: string) {
361
+ if (!url) {
362
+ return;
363
+ }
364
+
365
+ window.open(url, '_blank', 'noopener,noreferrer');
366
+ }
367
+
166
368
  const buttonClass = computed(() => {
167
369
  const sizeClass = {
168
370
  xs: 'p-1',
169
371
  sm: 'p-1.5',
170
- md: 'p-2',
171
- lg: 'p-2.5',
172
- xl: 'p-3',
372
+ md: 'px-2 py-1.5',
373
+ lg: 'px-2.5 py-2',
374
+ xl: 'px-3 py-2.5',
173
375
  }
174
376
 
175
377
  return [
176
- 'hover:bg-slate-100',
378
+ 'block hover:bg-slate-100 rounded-lg',
177
379
  'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white',
178
380
  sizeClass[props.size || 'md'] || sizeClass['md'],
179
381
  ];
@@ -189,87 +391,92 @@ const actions = computed<Action[]>(() => {
189
391
  return [
190
392
  {
191
393
  name: 'undo',
192
- icon: 'mdi:undo-variant',
394
+ icon: 'lucide:undo-2',
193
395
  command: () => editor.chain().focus().undo().run(),
194
396
  },
195
397
  {
196
398
  name: 'redo',
197
- icon: 'mdi:redo-variant',
399
+ icon: 'lucide:redo-2',
198
400
  command: () => editor.chain().focus().redo().run(),
199
401
  },
200
402
  {
201
403
  name: 'paragraph',
202
- icon: 'material-symbols:format-paragraph',
404
+ icon: 'lucide:pilcrow',
203
405
  command: () => editor.chain().focus().setParagraph().run(),
204
406
  },
205
407
  {
206
408
  name: 'heading-2',
207
- icon: 'material-symbols:format-h2',
409
+ icon: 'lucide:heading-2',
208
410
  command: () => editor.chain().focus().setHeading({ level: 2 }).run(),
209
411
  },
210
412
  {
211
413
  name: 'heading-3',
212
- icon: 'material-symbols:format-h3',
414
+ icon: 'lucide:heading-3',
213
415
  command: () => editor.chain().focus().setHeading({ level: 3 }).run(),
214
416
  },
215
417
  {
216
418
  name: 'bold',
217
- icon: 'material-symbols:format-bold',
419
+ icon: 'lucide:bold',
218
420
  command: () => editor.chain().focus().toggleBold().run(),
219
421
  },
220
422
  {
221
423
  name: 'italic',
222
- icon: 'material-symbols:format-italic',
424
+ icon: 'lucide:italic',
223
425
  command: () => editor.chain().focus().toggleItalic().run(),
224
426
  },
225
427
  {
226
428
  name: 'underline',
227
- icon: 'material-symbols:format-underlined',
429
+ icon: 'lucide:underline',
228
430
  command: () => editor.chain().focus().toggleUnderline().run(),
229
431
  },
230
432
  {
231
433
  name: 'strikethrough',
232
- icon: 'material-symbols:strikethrough-s',
434
+ icon: 'lucide:strikethrough',
233
435
  command: () => editor.chain().focus().toggleStrike().run(),
234
436
  },
235
437
  {
236
438
  name: 'superscript',
237
- icon: 'material-symbols:superscript',
439
+ icon: 'lucide:superscript',
238
440
  command: () => editor.chain().focus().toggleSuperscript().run(),
239
441
  },
240
442
  {
241
443
  name: 'subscript',
242
- icon: 'material-symbols:subscript',
444
+ icon: 'lucide:subscript',
243
445
  command: () => editor.chain().focus().toggleSubscript().run(),
244
446
  },
447
+ {
448
+ name: 'highlight',
449
+ icon: 'lucide:highlighter',
450
+ command: () => { },
451
+ },
245
452
  {
246
453
  name: 'link',
247
- icon: 'material-symbols:link',
248
- command: () => setLink(),
454
+ icon: 'lucide:link',
455
+ command: () => { },
249
456
  },
250
457
  {
251
- name: 'unlink',
252
- icon: 'material-symbols:link-off',
253
- command: () => editor.chain().focus().unsetLink().run(),
458
+ name: 'image',
459
+ icon: 'lucide:image',
460
+ command: () => pickImage(),
254
461
  },
255
462
  {
256
463
  name: 'bulletList',
257
- icon: 'material-symbols:format-list-bulleted',
464
+ icon: 'lucide:list',
258
465
  command: () => editor.chain().focus().toggleBulletList().run(),
259
466
  },
260
467
  {
261
468
  name: 'orderedList',
262
- icon: 'material-symbols:format-list-numbered',
469
+ icon: 'lucide:list-ordered',
263
470
  command: () => editor.chain().focus().toggleOrderedList().run(),
264
471
  },
265
472
  {
266
473
  name: 'footnote',
267
- icon: 'material-symbols:edit-note-outline',
474
+ icon: 'lucide:asterisk',
268
475
  command: () => editor?.commands.addFootnote(),
269
476
  },
270
477
  {
271
478
  name: 'nonBreakingSpace',
272
- icon: 'material-symbols:space-bar',
479
+ icon: 'lucide:space',
273
480
  command: () => editor.chain().focus().insertContent('\u00A0').run(),
274
481
  },
275
482
 
@@ -289,8 +496,10 @@ const toolbarTemplates: Record<string, string[]> = {
289
496
  'underline',
290
497
  'superscript',
291
498
  'subscript',
499
+ 'highlight',
292
500
  'link',
293
501
  'unlink',
502
+ 'image',
294
503
  'bulletList',
295
504
  'orderedList',
296
505
  'footnote',
package/src/lang/en.json CHANGED
@@ -60,6 +60,7 @@
60
60
  "page": "Page",
61
61
  "pagination_detail_1": "Viewing",
62
62
  "pagination_detail_2": "of",
63
+ "paste_link": "Paste link...",
63
64
  "postal_code_zip_code": "Postal Code / Zip Code",
64
65
  "previous": "Previous",
65
66
  "previous_month": "Previous month",
package/src/lang/fr.json CHANGED
@@ -60,6 +60,7 @@
60
60
  "page": "Page",
61
61
  "pagination_detail_1": "Affichage de",
62
62
  "pagination_detail_2": "de",
63
+ "paste_link": "Coller le lien...",
63
64
  "postal_code_zip_code": "Code postal",
64
65
  "previous": "Précédent",
65
66
  "previous_month": "Mois précédent",