sprintify-ui 0.12.15 → 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.
- package/dist/sprintify-ui.es.js +6823 -6768
- package/dist/style.css +1 -1
- package/dist/types/components/BaseDataTable.vue.d.ts +4 -0
- package/dist/types/components/BaseSignature.vue.d.ts +3 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +2 -1
- package/src/components/BaseDataIterator.mdx +5 -0
- package/src/components/BaseDataTable.vue +5 -1
- package/src/components/BaseSignature.mdx +26 -2
- package/src/components/BaseSignature.stories.js +40 -0
- package/src/components/BaseSignature.vue +96 -12
- package/src/lang/en.json +1 -0
- package/src/lang/fr.json +1 -0
|
@@ -31,6 +31,29 @@ const signature = ref<string | null>(null);
|
|
|
31
31
|
|
|
32
32
|
`update:modelValue` is emitted after each completed stroke. Clearing the pad emits `update:modelValue` with `null`, followed by the `clear` event.
|
|
33
33
|
|
|
34
|
+
## Text signatures
|
|
35
|
+
|
|
36
|
+
Pass `signature-text` when the signer’s name is entered elsewhere in the form. The component then shows a translated **Apply my signature** button. Clicking it clears the canvas internally and renders the name with the bundled Dancing Script font, before emitting the resulting white-background PNG through `v-model`. It does not emit `clear` because the signature is replaced, not removed.
|
|
37
|
+
|
|
38
|
+
```vue
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { ref } from 'vue';
|
|
41
|
+
import { BaseInput, BaseSignature } from 'sprintify-ui';
|
|
42
|
+
|
|
43
|
+
const name = ref('François Lévesque');
|
|
44
|
+
const signature = ref<string | null>(null);
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<BaseInput v-model="name" label="Full name" />
|
|
49
|
+
<BaseSignature v-model="signature" :signature-text="name" />
|
|
50
|
+
</template>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The text action is optional: drawing with a mouse, touch, or pen remains available. Blank text (including spaces only) hides the button. Text is rendered on one line and scales down to fit the available canvas space.
|
|
54
|
+
|
|
55
|
+
<Canvas of={BaseSignatureStories.TextSignature} />
|
|
56
|
+
|
|
34
57
|
## Existing signatures and resizing
|
|
35
58
|
|
|
36
59
|
Only complete PNG Data URLs are accepted, for example `data:image/png;base64,...`. The image is inspected to find its visible, non-white pixels. That content is then scaled proportionally, centered, and rendered with 8% horizontal and vertical padding.
|
|
@@ -53,7 +76,7 @@ JPEG, WebP, SVG, malformed Data URLs, and invalid PNG payloads are rejected. The
|
|
|
53
76
|
|
|
54
77
|
## Disabled state
|
|
55
78
|
|
|
56
|
-
The `disabled` prop prevents pointer input and disables
|
|
79
|
+
The `disabled` prop prevents pointer input and disables all built-in actions. Exposed methods remain available to the parent component.
|
|
57
80
|
|
|
58
81
|
<Canvas of={BaseSignatureStories.Disabled} />
|
|
59
82
|
|
|
@@ -66,8 +89,9 @@ The `disabled` prop prevents pointer input and disables the built-in clear butto
|
|
|
66
89
|
| `modelValue` | `string \| null` | `null` | Complete PNG Data URL containing the current signature. |
|
|
67
90
|
| `width` | `number` | `400` | Maximum visual width of the signature pad in pixels. The pad can shrink with its parent. |
|
|
68
91
|
| `height` | `number` | `180` | Visual height used to determine the pad aspect ratio. |
|
|
69
|
-
| `disabled` | `boolean` | `false` | Prevents drawing and
|
|
92
|
+
| `disabled` | `boolean` | `false` | Prevents drawing and disables built-in actions. |
|
|
70
93
|
| `showClearButton` | `boolean` | `true` | Shows or hides the built-in translated clear button. |
|
|
94
|
+
| `signatureText` | `string` | `''` | When non-blank, shows an action that renders the text as a Dancing Script PNG signature. |
|
|
71
95
|
|
|
72
96
|
## Events
|
|
73
97
|
|
|
@@ -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
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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...",
|