sprintify-ui 0.12.16 → 0.12.17

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.
@@ -43,6 +43,7 @@ export default {
43
43
  height: 180,
44
44
  disabled: false,
45
45
  showClearButton: true,
46
+ signatureText: '',
46
47
  },
47
48
  argTypes: {
48
49
  modelValue: {
@@ -65,6 +66,10 @@ export default {
65
66
  control: "boolean",
66
67
  description: "Shows the built-in clear button below the pad.",
67
68
  },
69
+ signatureText: {
70
+ control: "text",
71
+ description: "Text rendered as a Dancing Script PNG when the built-in button is pressed.",
72
+ },
68
73
  "update:modelValue": {
69
74
  action: "update:modelValue",
70
75
  table: { category: "Events" },
@@ -201,6 +206,41 @@ Compact.args = {
201
206
  height: 120,
202
207
  };
203
208
 
209
+ const TextSignatureTemplate = (args) => ({
210
+ components: { BaseSignature },
211
+ setup() {
212
+ const signature = ref(null);
213
+ const signatureText = ref(args.signatureText);
214
+
215
+ return { args, signature, signatureText };
216
+ },
217
+ template: `
218
+ <div class="space-y-3">
219
+ <label class="block max-w-md space-y-1 text-sm font-medium text-slate-700">
220
+ Signature text
221
+ <input
222
+ v-model="signatureText"
223
+ type="text"
224
+ class="block w-full rounded-md border-slate-300 shadow-sm"
225
+ >
226
+ </label>
227
+ <BaseSignature
228
+ v-bind="args"
229
+ v-model="signature"
230
+ :signature-text="signatureText"
231
+ />
232
+ <p class="max-w-lg text-sm text-slate-600">
233
+ Applying the text replaces the current canvas content with a PNG signature.
234
+ </p>
235
+ </div>
236
+ `,
237
+ });
238
+
239
+ export const TextSignature = TextSignatureTemplate.bind({});
240
+ TextSignature.args = {
241
+ signatureText: 'François Lévesque',
242
+ };
243
+
204
244
  export const Disabled = ExistingSignatureTemplate.bind({});
205
245
  Disabled.args = {
206
246
  disabled: true,
@@ -25,7 +25,7 @@
25
25
  </div>
26
26
 
27
27
  <div
28
- v-if="showClearButton || errorMessage"
28
+ v-if="showApplySignatureButton || showClearButton || errorMessage"
29
29
  class="flex items-start justify-between gap-3"
30
30
  >
31
31
  <p
@@ -37,23 +37,38 @@
37
37
  </p>
38
38
  <span v-else />
39
39
 
40
- <BaseButton
41
- v-if="showClearButton"
42
- type="button"
43
- size="sm"
44
- color="secondary-outline"
45
- icon="heroicons:trash"
46
- :disabled="disabled || !canClear"
47
- @click="clear"
48
- >
49
- {{ t('sui.clear') }}
50
- </BaseButton>
40
+ <div class="flex flex-wrap justify-end gap-2">
41
+ <BaseButton
42
+ v-if="showApplySignatureButton"
43
+ type="button"
44
+ size="sm"
45
+ color="secondary-outline"
46
+ :disabled="disabled || applyingTextSignature"
47
+ :loading="applyingTextSignature"
48
+ @click="applyTextSignature"
49
+ >
50
+ {{ t('sui.apply_signature') }}
51
+ </BaseButton>
52
+
53
+ <BaseButton
54
+ v-if="showClearButton"
55
+ type="button"
56
+ size="sm"
57
+ color="secondary-outline"
58
+ icon="heroicons:trash"
59
+ :disabled="disabled || !canClear"
60
+ @click="clear"
61
+ >
62
+ {{ t('sui.clear') }}
63
+ </BaseButton>
64
+ </div>
51
65
  </div>
52
66
  </div>
53
67
  </template>
54
68
 
55
69
  <script lang="ts" setup>
56
70
  import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
71
+ import '@fontsource/dancing-script/400.css';
57
72
  import { BaseButton } from '.';
58
73
  import { t } from '@/i18n';
59
74
  import type { SignatureError, SignatureErrorCode } from '@/types';
@@ -71,12 +86,14 @@ const props = withDefaults(defineProps<{
71
86
  height?: number;
72
87
  disabled?: boolean;
73
88
  showClearButton?: boolean;
89
+ signatureText?: string;
74
90
  }>(), {
75
91
  modelValue: null,
76
92
  width: 400,
77
93
  height: 180,
78
94
  disabled: false,
79
95
  showClearButton: true,
96
+ signatureText: '',
80
97
  });
81
98
 
82
99
  const emit = defineEmits<{
@@ -93,6 +110,7 @@ const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
93
110
  const canvas = ref<HTMLCanvasElement | null>(null);
94
111
  const errorMessage = ref<string | null>(null);
95
112
  const filled = ref(false);
113
+ const applyingTextSignature = ref(false);
96
114
 
97
115
  let context: CanvasRenderingContext2D | null = null;
98
116
  let activePointerId: number | null = null;
@@ -107,6 +125,8 @@ const normalizedHeight = computed(() => Math.max(1, Math.round(props.height)));
107
125
  const canClear = computed(() => {
108
126
  return filled.value || Boolean(props.modelValue) || Boolean(errorMessage.value);
109
127
  });
128
+ const signatureText = computed(() => props.signatureText.trim());
129
+ const showApplySignatureButton = computed(() => Boolean(signatureText.value));
110
130
 
111
131
  const containerStyle = computed(() => ({
112
132
  width: `${normalizedWidth.value}px`,
@@ -189,6 +209,69 @@ function resetCanvas() {
189
209
  filled.value = false;
190
210
  }
191
211
 
212
+ async function applyTextSignature() {
213
+ if (!context || props.disabled || !signatureText.value) return;
214
+
215
+ const text = signatureText.value;
216
+ const requestId = ++loadRequestId;
217
+
218
+ cancelDrawing();
219
+ errorMessage.value = null;
220
+ resetCanvas();
221
+ applyingTextSignature.value = true;
222
+
223
+ try {
224
+ await document.fonts?.load('400 16px "Dancing Script"', text);
225
+
226
+ if (requestId !== loadRequestId || props.disabled) return;
227
+
228
+ drawTextSignature(text);
229
+ emitSignature();
230
+ } finally {
231
+ applyingTextSignature.value = false;
232
+ }
233
+ }
234
+
235
+ function drawTextSignature(text: string) {
236
+ if (!context) return;
237
+
238
+ const availableWidth = normalizedWidth.value * (1 - SIGNATURE_PADDING_RATIO * 2);
239
+ const availableHeight = normalizedHeight.value * (1 - SIGNATURE_PADDING_RATIO * 2);
240
+ const maximumFontSize = Math.max(1, availableHeight * 2);
241
+ let minimumFontSize = 1;
242
+ let maximumFittingFontSize = maximumFontSize;
243
+
244
+ for (let index = 0; index < 20; index += 1) {
245
+ const fontSize = (minimumFontSize + maximumFittingFontSize) / 2;
246
+
247
+ context.font = `400 ${fontSize}px "Dancing Script", cursive`;
248
+
249
+ const metrics = context.measureText(text);
250
+ const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
251
+ const fits = metrics.width <= availableWidth && textHeight <= availableHeight;
252
+
253
+ if (fits) {
254
+ minimumFontSize = fontSize;
255
+ } else {
256
+ maximumFittingFontSize = fontSize;
257
+ }
258
+ }
259
+
260
+ context.font = `400 ${minimumFontSize}px "Dancing Script", cursive`;
261
+
262
+ const metrics = context.measureText(text);
263
+ const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
264
+ const x = (normalizedWidth.value - metrics.width) / 2;
265
+ const y = (normalizedHeight.value - textHeight) / 2 + metrics.actualBoundingBoxAscent;
266
+
267
+ context.save();
268
+ context.fillStyle = '#000000';
269
+ context.textBaseline = 'alphabetic';
270
+ context.fillText(text, x, y);
271
+ context.restore();
272
+ filled.value = true;
273
+ }
274
+
192
275
  async function renderModelValue(modelValue: string | null | undefined) {
193
276
  const requestId = ++loadRequestId;
194
277
  errorMessage.value = null;
@@ -356,6 +439,7 @@ function startDrawing(event: PointerEvent) {
356
439
 
357
440
  event.preventDefault();
358
441
  loadRequestId += 1;
442
+ applyingTextSignature.value = false;
359
443
  activePointerId = event.pointerId;
360
444
  drawing = true;
361
445
  errorMessage.value = null;
package/src/lang/en.json CHANGED
@@ -12,6 +12,7 @@
12
12
  "address_2_description": "Apartment, suite, unit, building",
13
13
  "and": "and",
14
14
  "apply": "Apply",
15
+ "apply_signature": "Apply my signature",
15
16
  "apply_filters": "Apply filters",
16
17
  "authentication_code": "Authentication code",
17
18
  "autocomplete_placeholder": "Search...",
package/src/lang/fr.json CHANGED
@@ -12,6 +12,7 @@
12
12
  "address_2_description": "Appartement, suite, unité, immeuble",
13
13
  "and": "et",
14
14
  "apply": "Appliquer",
15
+ "apply_signature": "Appliquer ma signature",
15
16
  "apply_filters": "Appliquer les filtres",
16
17
  "authentication_code": "Code d'authentification",
17
18
  "autocomplete_placeholder": "Rechercher...",