sprintify-ui 0.12.9 → 0.12.11

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,29 @@
1
+ import type { SignatureError } from '@/types';
2
+ type __VLS_Props = {
3
+ modelValue?: string | null;
4
+ width?: number;
5
+ height?: number;
6
+ disabled?: boolean;
7
+ showClearButton?: boolean;
8
+ };
9
+ declare function clear(): void;
10
+ declare function isFilled(): boolean;
11
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {
12
+ clear: typeof clear;
13
+ isFilled: typeof isFilled;
14
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
+ error: (error: SignatureError) => any;
16
+ clear: () => any;
17
+ "update:modelValue": (value: string | null) => any;
18
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
19
+ onError?: ((error: SignatureError) => any) | undefined;
20
+ onClear?: (() => any) | undefined;
21
+ "onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
22
+ }>, {
23
+ width: number;
24
+ height: number;
25
+ disabled: boolean;
26
+ modelValue: string | null;
27
+ showClearButton: boolean;
28
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
29
+ export default _default;
@@ -76,6 +76,7 @@ import BaseRadioGroup from './BaseRadioGroup.vue';
76
76
  import BaseReadMore from './BaseReadMore.vue';
77
77
  import BaseRichText from './BaseRichText.vue';
78
78
  import BaseSelect from './BaseSelect.vue';
79
+ import BaseSignature from './BaseSignature.vue';
79
80
  import BaseShortcut from './BaseShortcut.vue';
80
81
  import BaseSideNavigation from './BaseSideNavigation.vue';
81
82
  import BaseSideNavigationItem from './BaseSideNavigationItem.vue';
@@ -107,4 +108,4 @@ import BaseLayoutStacked from './BaseLayoutStacked.vue';
107
108
  import BaseLayoutStackedConfigurable from './BaseLayoutStackedConfigurable.vue';
108
109
  import BaseLayoutSidebar from './BaseLayoutSidebar.vue';
109
110
  import BaseLayoutSidebarConfigurable from './BaseLayoutSidebarConfigurable.vue';
110
- export { BaseActionButtons, BaseActionItem, BaseAddressForm, BaseAlert, BaseApp, BaseAppDialogs, BaseAppSnackbars, BaseAssign, BaseAutocomplete, BaseAutocompleteFetch, BaseAvatar, BaseAvatarGroup, BaseBadge, BaseBelongsTo, BaseBelongsToFetch, BaseBoolean, BaseBreadcrumbs, BaseButton, BaseButtonGroup, BaseCard, BaseCardRow, BaseCharacterCounter, BaseClipboard, BaseCollapse, BaseColor, BaseContainer, BaseCounter, BaseCropper, BaseCropperModal, BaseDataIterator, BaseDataTable, BaseDatePicker, BaseDateSelect, BaseDraggable, BaseDescriptionList, BaseDescriptionListItem, BaseDialog, BaseDisplayRelativeTime, BaseDropdown, BaseDropdownAutocomplete, BaseEmptyState, BaseField, BaseFieldI18n, BaseFilePicker, BaseFilePickerCrop, BaseFileUploader, BaseForm, BaseGantt, BaseHasMany, BaseHasManyFetch, BaseHeader, BaseIcon, BaseIconPicker, BaseInput, BaseInputLabel, BaseInputPercent, BaseJsonReader, BaseLazy, BaseLoadingCover, BaseMediaItem, BaseMediaLibrary, BaseMediaPreview, BaseMenu, BaseMenuItem, BaseModalCenter, BaseModalSide, BaseNavbar, BaseNavbarItem, BaseNavbarItemContent, BaseNavbarSideItem, BasePagination, BasePanel, BasePassword, BaseProgressCircle, BaseRadioGroup, BaseReadMore, BaseRichText, BaseSelect, BaseShortcut, BaseSideNavigation, BaseSideNavigationItem, BaseSkeleton, BaseStatistic, BaseStepper, BaseStepperItem, BaseSwitch, BaseSystemAlert, BaseTable, BaseTableBody, BaseTableCell, BaseTableHead, BaseTableHeader, BaseTableRow, BaseTabs, BaseTabItem, BaseTagAutocomplete, BaseTagAutocompleteFetch, BaseTableColumn, BaseTextarea, BaseTextareaAutoresize, BaseTimeline, BaseTimelineItem, BaseTimePicker, BaseTooltip, BaseUniqueCode, BaseLayoutStacked, BaseLayoutStackedConfigurable, BaseLayoutSidebar, BaseLayoutSidebarConfigurable, };
111
+ export { BaseActionButtons, BaseActionItem, BaseAddressForm, BaseAlert, BaseApp, BaseAppDialogs, BaseAppSnackbars, BaseAssign, BaseAutocomplete, BaseAutocompleteFetch, BaseAvatar, BaseAvatarGroup, BaseBadge, BaseBelongsTo, BaseBelongsToFetch, BaseBoolean, BaseBreadcrumbs, BaseButton, BaseButtonGroup, BaseCard, BaseCardRow, BaseCharacterCounter, BaseClipboard, BaseCollapse, BaseColor, BaseContainer, BaseCounter, BaseCropper, BaseCropperModal, BaseDataIterator, BaseDataTable, BaseDatePicker, BaseDateSelect, BaseDraggable, BaseDescriptionList, BaseDescriptionListItem, BaseDialog, BaseDisplayRelativeTime, BaseDropdown, BaseDropdownAutocomplete, BaseEmptyState, BaseField, BaseFieldI18n, BaseFilePicker, BaseFilePickerCrop, BaseFileUploader, BaseForm, BaseGantt, BaseHasMany, BaseHasManyFetch, BaseHeader, BaseIcon, BaseIconPicker, BaseInput, BaseInputLabel, BaseInputPercent, BaseJsonReader, BaseLazy, BaseLoadingCover, BaseMediaItem, BaseMediaLibrary, BaseMediaPreview, BaseMenu, BaseMenuItem, BaseModalCenter, BaseModalSide, BaseNavbar, BaseNavbarItem, BaseNavbarItemContent, BaseNavbarSideItem, BasePagination, BasePanel, BasePassword, BaseProgressCircle, BaseRadioGroup, BaseReadMore, BaseRichText, BaseSelect, BaseSignature, BaseShortcut, BaseSideNavigation, BaseSideNavigationItem, BaseSkeleton, BaseStatistic, BaseStepper, BaseStepperItem, BaseSwitch, BaseSystemAlert, BaseTable, BaseTableBody, BaseTableCell, BaseTableHead, BaseTableHeader, BaseTableRow, BaseTabs, BaseTabItem, BaseTagAutocomplete, BaseTagAutocompleteFetch, BaseTableColumn, BaseTextarea, BaseTextareaAutoresize, BaseTimeline, BaseTimelineItem, BaseTimePicker, BaseTooltip, BaseUniqueCode, BaseLayoutStacked, BaseLayoutStackedConfigurable, BaseLayoutSidebar, BaseLayoutSidebarConfigurable, };
@@ -83,6 +83,9 @@ declare const messages: {
83
83
  select_all: string;
84
84
  select_an_item: string;
85
85
  select_an_option: string;
86
+ signature_load_failed: string;
87
+ signature_must_be_png: string;
88
+ signature_pad: string;
86
89
  success: string;
87
90
  the_file_size_must_not_exceed_x: string;
88
91
  the_file_type_is_invalid: string;
@@ -186,6 +189,9 @@ declare const messages: {
186
189
  select_all: string;
187
190
  select_an_item: string;
188
191
  select_an_option: string;
192
+ signature_load_failed: string;
193
+ signature_must_be_png: string;
194
+ signature_pad: string;
189
195
  success: string;
190
196
  the_file_size_must_not_exceed_x: string;
191
197
  the_file_type_is_invalid: string;
@@ -9,6 +9,11 @@ import { ToolbarOption } from './ToolbarOption';
9
9
  export type Locales = {
10
10
  [locale: string]: string;
11
11
  };
12
+ export type SignatureErrorCode = 'invalid_png' | 'load_failed';
13
+ export interface SignatureError {
14
+ code: SignatureErrorCode;
15
+ message: string;
16
+ }
12
17
  export declare enum Method {
13
18
  post = "post",
14
19
  put = "put",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprintify-ui",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "generate-llm-txt": "node scripts/generate-llm-txt.js",
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="min-h-full">
2
+ <div class="min-h-full base-layout-sidebar">
3
3
  <TransitionRoot
4
4
  as="template"
5
5
  :show="showMobileMenu"
@@ -109,7 +109,7 @@
109
109
  </BaseSystemAlert>
110
110
 
111
111
  <div
112
- class="flex bg-white"
112
+ class="flex bg-white base-layout-sidebar-navbar"
113
113
  :style="{ height: navbarHeight + 'px' }"
114
114
  >
115
115
  <button
@@ -0,0 +1,119 @@
1
+ import { Canvas, Controls, Meta, Primary } from '@storybook/addon-docs/blocks';
2
+
3
+ import * as BaseSignatureStories from './BaseSignature.stories';
4
+
5
+ <Meta of={BaseSignatureStories} />
6
+
7
+ # BaseSignature
8
+
9
+ ## Introduction
10
+
11
+ `BaseSignature` is a responsive signature pad for mouse, touch, and pen input. It smooths pointer movement with quadratic curves, varies stroke width according to pointer speed, and uses pen pressure when the device provides it.
12
+
13
+ The component uses a canvas for fluid input and always emits a white-background PNG Data URL. Existing PNG signatures can be loaded through `v-model` and extended with new strokes.
14
+
15
+ <Primary />
16
+
17
+ ## Basic usage
18
+
19
+ ```vue
20
+ <script setup lang="ts">
21
+ import { ref } from 'vue';
22
+ import { BaseSignature } from 'sprintify-ui';
23
+
24
+ const signature = ref<string | null>(null);
25
+ </script>
26
+
27
+ <template>
28
+ <BaseSignature v-model="signature" />
29
+ </template>
30
+ ```
31
+
32
+ `update:modelValue` is emitted after each completed stroke. Clearing the pad emits `update:modelValue` with `null`, followed by the `clear` event.
33
+
34
+ ## Existing signatures and resizing
35
+
36
+ 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.
37
+
38
+ This content-aware approach handles input images that are larger, smaller, wider, or taller than the signature pad without cropping or distortion.
39
+
40
+ <Canvas of={BaseSignatureStories.ExistingSignature} />
41
+
42
+ The same signature also adapts to a smaller pad:
43
+
44
+ <Canvas of={BaseSignatureStories.Compact} />
45
+
46
+ Loading a value only renders it. It does not emit a new `update:modelValue` event. Once another stroke is added, the imported image and new stroke are flattened into a PNG matching the current canvas.
47
+
48
+ ## PNG validation
49
+
50
+ JPEG, WebP, SVG, malformed Data URLs, and invalid PNG payloads are rejected. The component displays a translated error and emits an `error` event with a stable code and translated message.
51
+
52
+ <Canvas of={BaseSignatureStories.InvalidPng} />
53
+
54
+ ## Disabled state
55
+
56
+ The `disabled` prop prevents pointer input and disables the built-in clear button. Exposed methods remain available to the parent component.
57
+
58
+ <Canvas of={BaseSignatureStories.Disabled} />
59
+
60
+ ## Props
61
+
62
+ <Controls />
63
+
64
+ | Prop | Type | Default | Description |
65
+ |------|------|---------|-------------|
66
+ | `modelValue` | `string \| null` | `null` | Complete PNG Data URL containing the current signature. |
67
+ | `width` | `number` | `400` | Maximum visual width of the signature pad in pixels. The pad can shrink with its parent. |
68
+ | `height` | `number` | `180` | Visual height used to determine the pad aspect ratio. |
69
+ | `disabled` | `boolean` | `false` | Prevents drawing and clearing through the built-in button. |
70
+ | `showClearButton` | `boolean` | `true` | Shows or hides the built-in translated clear button. |
71
+
72
+ ## Events
73
+
74
+ | Event | Payload | Description |
75
+ |-------|---------|-------------|
76
+ | `update:modelValue` | `string \| null` | Emits a PNG Data URL after a stroke, or `null` after clearing. |
77
+ | `clear` | — | Emitted after the signature is cleared. |
78
+ | `error` | `SignatureError` | Emitted when the input is not a valid PNG or cannot be loaded. |
79
+
80
+ ```ts
81
+ type SignatureErrorCode = 'invalid_png' | 'load_failed';
82
+
83
+ interface SignatureError {
84
+ code: SignatureErrorCode;
85
+ message: string;
86
+ }
87
+ ```
88
+
89
+ ## Exposed methods
90
+
91
+ | Method | Return type | Description |
92
+ |--------|-------------|-------------|
93
+ | `isFilled()` | `boolean` | Returns whether the pad contains visible signature pixels. A white or transparent PNG is considered empty. |
94
+ | `clear()` | `void` | Clears the pad and emits both `update:modelValue` and `clear`. |
95
+
96
+ ```vue
97
+ <script setup lang="ts">
98
+ import { ref } from 'vue';
99
+ import { BaseSignature } from 'sprintify-ui';
100
+
101
+ const signaturePad = ref<InstanceType<typeof BaseSignature> | null>(null);
102
+
103
+ function validateSignature() {
104
+ return signaturePad.value?.isFilled() ?? false;
105
+ }
106
+
107
+ function clearSignature() {
108
+ signaturePad.value?.clear();
109
+ }
110
+ </script>
111
+
112
+ <template>
113
+ <BaseSignature ref="signaturePad" :show-clear-button="false" />
114
+ </template>
115
+ ```
116
+
117
+ ## Canvas rather than SVG
118
+
119
+ Canvas is used because it offers direct, efficient Pointer Events support and predictable PNG export for mouse, touch, and pressure-sensitive pens. Stroke points are smoothed while drawing, but the public input and output remain raster PNGs. SVG would be preferable only when preserving editable vector paths is a requirement.
@@ -0,0 +1,232 @@
1
+ import { computed, nextTick, onMounted, ref } from "vue";
2
+ import { BaseButton, BaseSignature } from ".";
3
+
4
+ function createDemoSignature(width = 960, height = 220) {
5
+ const canvas = document.createElement("canvas");
6
+ const context = canvas.getContext("2d");
7
+
8
+ canvas.width = width;
9
+ canvas.height = height;
10
+
11
+ context.fillStyle = "#ffffff";
12
+ context.fillRect(0, 0, width, height);
13
+ context.strokeStyle = "#000000";
14
+ context.lineCap = "round";
15
+ context.lineJoin = "round";
16
+ context.lineWidth = 8;
17
+
18
+ context.beginPath();
19
+ context.moveTo(145, 145);
20
+ context.bezierCurveTo(225, 25, 250, 190, 330, 72);
21
+ context.bezierCurveTo(350, 42, 345, 165, 405, 122);
22
+ context.bezierCurveTo(455, 86, 475, 58, 452, 132);
23
+ context.bezierCurveTo(438, 178, 515, 64, 550, 120);
24
+ context.bezierCurveTo(575, 160, 620, 70, 648, 119);
25
+ context.bezierCurveTo(680, 174, 735, 82, 790, 125);
26
+ context.stroke();
27
+
28
+ context.lineWidth = 5;
29
+ context.beginPath();
30
+ context.moveTo(105, 164);
31
+ context.bezierCurveTo(310, 194, 590, 155, 835, 153);
32
+ context.stroke();
33
+
34
+ return canvas.toDataURL("image/png");
35
+ }
36
+
37
+ export default {
38
+ title: "Form/BaseSignature",
39
+ component: BaseSignature,
40
+ args: {
41
+ modelValue: null,
42
+ width: 400,
43
+ height: 180,
44
+ disabled: false,
45
+ showClearButton: true,
46
+ },
47
+ argTypes: {
48
+ modelValue: {
49
+ control: false,
50
+ description: "A PNG Data URL, or null when the signature is empty.",
51
+ },
52
+ width: {
53
+ control: { type: "number", min: 1 },
54
+ description: "Maximum visual width of the signature pad in pixels.",
55
+ },
56
+ height: {
57
+ control: { type: "number", min: 1 },
58
+ description: "Visual height of the signature pad in pixels.",
59
+ },
60
+ disabled: {
61
+ control: "boolean",
62
+ description: "Prevents drawing and clearing from the built-in button.",
63
+ },
64
+ showClearButton: {
65
+ control: "boolean",
66
+ description: "Shows the built-in clear button below the pad.",
67
+ },
68
+ "update:modelValue": {
69
+ action: "update:modelValue",
70
+ table: { category: "Events" },
71
+ },
72
+ clear: {
73
+ action: "clear",
74
+ table: { category: "Events" },
75
+ },
76
+ error: {
77
+ action: "error",
78
+ table: { category: "Events" },
79
+ },
80
+ },
81
+ };
82
+
83
+ const InteractiveTemplate = (args) => ({
84
+ components: { BaseButton, BaseSignature },
85
+ setup() {
86
+ const signature = ref(args.modelValue);
87
+ const signatureComponent = ref(null);
88
+ const filled = ref(false);
89
+ const lastError = ref(null);
90
+ const base64Preview = computed(() => {
91
+ if (!signature.value) return "null";
92
+
93
+ const size = Math.ceil(signature.value.length / 1024);
94
+
95
+ return `${signature.value.slice(0, 54)}… (${size} KiB)`;
96
+ });
97
+
98
+ function updateFilledState() {
99
+ filled.value = signatureComponent.value?.isFilled() ?? false;
100
+ }
101
+
102
+ function updateSignature(value) {
103
+ signature.value = value;
104
+ nextTick(updateFilledState);
105
+ }
106
+
107
+ function clearFromOutside() {
108
+ signatureComponent.value?.clear();
109
+ nextTick(updateFilledState);
110
+ }
111
+
112
+ function onError(error) {
113
+ lastError.value = error;
114
+ }
115
+
116
+ return {
117
+ args,
118
+ base64Preview,
119
+ clearFromOutside,
120
+ filled,
121
+ lastError,
122
+ onError,
123
+ signature,
124
+ signatureComponent,
125
+ updateFilledState,
126
+ updateSignature,
127
+ };
128
+ },
129
+ template: `
130
+ <div class="space-y-5">
131
+ <BaseSignature
132
+ ref="signatureComponent"
133
+ v-bind="args"
134
+ :model-value="signature"
135
+ @update:model-value="updateSignature"
136
+ @clear="updateFilledState"
137
+ @error="onError"
138
+ />
139
+
140
+ <div class="flex flex-wrap items-center gap-3">
141
+ <BaseButton
142
+ type="button"
143
+ size="sm"
144
+ color="secondary-outline"
145
+ @click="clearFromOutside"
146
+ >
147
+ Clear from the parent
148
+ </BaseButton>
149
+ <span class="text-sm text-slate-600">
150
+ isFilled(): <strong>{{ filled }}</strong>
151
+ </span>
152
+ </div>
153
+
154
+ <div class="max-w-2xl rounded-md bg-slate-900 p-3 font-mono text-xs text-slate-100">
155
+ {{ base64Preview }}
156
+ </div>
157
+
158
+ <div
159
+ v-if="lastError"
160
+ class="max-w-2xl rounded-md bg-red-50 p-3 text-sm text-red-700"
161
+ >
162
+ {{ lastError.code }} — {{ lastError.message }}
163
+ </div>
164
+ </div>
165
+ `,
166
+ });
167
+
168
+ export const Demo = InteractiveTemplate.bind({});
169
+
170
+ const ExistingSignatureTemplate = (args) => ({
171
+ components: { BaseSignature },
172
+ setup() {
173
+ const signature = ref(null);
174
+
175
+ onMounted(() => {
176
+ signature.value = createDemoSignature();
177
+ });
178
+
179
+ return { args, signature };
180
+ },
181
+ template: `
182
+ <div class="space-y-3">
183
+ <BaseSignature v-bind="args" v-model="signature" />
184
+ <p class="max-w-lg text-sm text-slate-600">
185
+ This 960 × 220 PNG is cropped to its visible pixels, scaled proportionally,
186
+ centered, and rendered with balanced padding.
187
+ </p>
188
+ </div>
189
+ `,
190
+ });
191
+
192
+ export const ExistingSignature = ExistingSignatureTemplate.bind({});
193
+ ExistingSignature.args = {
194
+ width: 400,
195
+ height: 180,
196
+ };
197
+
198
+ export const Compact = ExistingSignatureTemplate.bind({});
199
+ Compact.args = {
200
+ width: 280,
201
+ height: 120,
202
+ };
203
+
204
+ export const Disabled = ExistingSignatureTemplate.bind({});
205
+ Disabled.args = {
206
+ disabled: true,
207
+ };
208
+
209
+ const InvalidPngTemplate = (args) => ({
210
+ components: { BaseSignature },
211
+ setup() {
212
+ const signature = ref("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2Q==");
213
+ const emittedError = ref(null);
214
+
215
+ return { args, emittedError, signature };
216
+ },
217
+ template: `
218
+ <div class="space-y-3">
219
+ <BaseSignature
220
+ v-bind="args"
221
+ v-model="signature"
222
+ @error="emittedError = $event"
223
+ />
224
+ <pre
225
+ v-if="emittedError"
226
+ class="max-w-lg rounded-md bg-red-50 p-3 text-xs text-red-700"
227
+ >{{ emittedError }}</pre>
228
+ </div>
229
+ `,
230
+ });
231
+
232
+ export const InvalidPng = InvalidPngTemplate.bind({});