sprintify-ui 0.12.16 → 0.12.18

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,
@@ -24,36 +24,48 @@
24
24
  />
25
25
  </div>
26
26
 
27
- <div
28
- v-if="showClearButton || errorMessage"
29
- class="flex items-start justify-between gap-3"
30
- >
31
- <p
32
- v-if="errorMessage"
33
- role="alert"
34
- class="pt-1 text-sm leading-tight text-red-600"
27
+ <div v-if="showApplySignatureButton || showClearButton" class="btn-group w-full">
28
+ <BaseButton
29
+ v-if="showApplySignatureButton"
30
+ type="button"
31
+ size="sm"
32
+ class="w-full"
33
+ color="slate-100-outline"
34
+ icon="ph:signature-light"
35
+ :disabled="disabled || applyingTextSignature"
36
+ :loading="applyingTextSignature"
37
+ @click="applyTextSignature"
35
38
  >
36
- {{ errorMessage }}
37
- </p>
38
- <span v-else />
39
+ {{ t('sui.apply_signature') }}
40
+ </BaseButton>
39
41
 
40
42
  <BaseButton
41
43
  v-if="showClearButton"
42
44
  type="button"
43
45
  size="sm"
44
- color="secondary-outline"
45
- icon="heroicons:trash"
46
+ class="w-full"
47
+ icon="mdi:remove"
48
+ color="slate-100-outline"
46
49
  :disabled="disabled || !canClear"
47
50
  @click="clear"
48
51
  >
49
52
  {{ t('sui.clear') }}
50
53
  </BaseButton>
51
54
  </div>
55
+
56
+ <p
57
+ v-if="errorMessage"
58
+ role="alert"
59
+ class="text-sm mt-1 leading-tight text-red-600"
60
+ >
61
+ {{ errorMessage }}
62
+ </p>
52
63
  </div>
53
64
  </template>
54
65
 
55
66
  <script lang="ts" setup>
56
67
  import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
68
+ import '@fontsource/dancing-script/400.css';
57
69
  import { BaseButton } from '.';
58
70
  import { t } from '@/i18n';
59
71
  import type { SignatureError, SignatureErrorCode } from '@/types';
@@ -71,12 +83,14 @@ const props = withDefaults(defineProps<{
71
83
  height?: number;
72
84
  disabled?: boolean;
73
85
  showClearButton?: boolean;
86
+ signatureText?: string;
74
87
  }>(), {
75
88
  modelValue: null,
76
89
  width: 400,
77
90
  height: 180,
78
91
  disabled: false,
79
92
  showClearButton: true,
93
+ signatureText: '',
80
94
  });
81
95
 
82
96
  const emit = defineEmits<{
@@ -93,6 +107,7 @@ const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
93
107
  const canvas = ref<HTMLCanvasElement | null>(null);
94
108
  const errorMessage = ref<string | null>(null);
95
109
  const filled = ref(false);
110
+ const applyingTextSignature = ref(false);
96
111
 
97
112
  let context: CanvasRenderingContext2D | null = null;
98
113
  let activePointerId: number | null = null;
@@ -107,6 +122,8 @@ const normalizedHeight = computed(() => Math.max(1, Math.round(props.height)));
107
122
  const canClear = computed(() => {
108
123
  return filled.value || Boolean(props.modelValue) || Boolean(errorMessage.value);
109
124
  });
125
+ const signatureText = computed(() => props.signatureText.trim());
126
+ const showApplySignatureButton = computed(() => Boolean(signatureText.value));
110
127
 
111
128
  const containerStyle = computed(() => ({
112
129
  width: `${normalizedWidth.value}px`,
@@ -189,6 +206,69 @@ function resetCanvas() {
189
206
  filled.value = false;
190
207
  }
191
208
 
209
+ async function applyTextSignature() {
210
+ if (!context || props.disabled || !signatureText.value) return;
211
+
212
+ const text = signatureText.value;
213
+ const requestId = ++loadRequestId;
214
+
215
+ cancelDrawing();
216
+ errorMessage.value = null;
217
+ resetCanvas();
218
+ applyingTextSignature.value = true;
219
+
220
+ try {
221
+ await document.fonts?.load('400 16px "Dancing Script"', text);
222
+
223
+ if (requestId !== loadRequestId || props.disabled) return;
224
+
225
+ drawTextSignature(text);
226
+ emitSignature();
227
+ } finally {
228
+ applyingTextSignature.value = false;
229
+ }
230
+ }
231
+
232
+ function drawTextSignature(text: string) {
233
+ if (!context) return;
234
+
235
+ const availableWidth = normalizedWidth.value * (1 - SIGNATURE_PADDING_RATIO * 2);
236
+ const availableHeight = normalizedHeight.value * (1 - SIGNATURE_PADDING_RATIO * 2);
237
+ const maximumFontSize = Math.max(1, availableHeight * 2);
238
+ let minimumFontSize = 1;
239
+ let maximumFittingFontSize = maximumFontSize;
240
+
241
+ for (let index = 0; index < 20; index += 1) {
242
+ const fontSize = (minimumFontSize + maximumFittingFontSize) / 2;
243
+
244
+ context.font = `400 ${fontSize}px "Dancing Script", cursive`;
245
+
246
+ const metrics = context.measureText(text);
247
+ const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
248
+ const fits = metrics.width <= availableWidth && textHeight <= availableHeight;
249
+
250
+ if (fits) {
251
+ minimumFontSize = fontSize;
252
+ } else {
253
+ maximumFittingFontSize = fontSize;
254
+ }
255
+ }
256
+
257
+ context.font = `400 ${minimumFontSize}px "Dancing Script", cursive`;
258
+
259
+ const metrics = context.measureText(text);
260
+ const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
261
+ const x = (normalizedWidth.value - metrics.width) / 2;
262
+ const y = (normalizedHeight.value - textHeight) / 2 + metrics.actualBoundingBoxAscent;
263
+
264
+ context.save();
265
+ context.fillStyle = '#000000';
266
+ context.textBaseline = 'alphabetic';
267
+ context.fillText(text, x, y);
268
+ context.restore();
269
+ filled.value = true;
270
+ }
271
+
192
272
  async function renderModelValue(modelValue: string | null | undefined) {
193
273
  const requestId = ++loadRequestId;
194
274
  errorMessage.value = null;
@@ -356,6 +436,7 @@ function startDrawing(event: PointerEvent) {
356
436
 
357
437
  event.preventDefault();
358
438
  loadRequestId += 1;
439
+ applyingTextSignature.value = false;
359
440
  activePointerId = event.pointerId;
360
441
  drawing = true;
361
442
  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...",