@groundbrick/svelte-ui 0.1.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.
- package/README.md +125 -0
- package/dist/components/Alert.svelte +335 -0
- package/dist/components/Alert.svelte.d.ts +24 -0
- package/dist/components/AutocompleteInput.svelte +356 -0
- package/dist/components/AutocompleteInput.svelte.d.ts +72 -0
- package/dist/components/Badge.svelte +185 -0
- package/dist/components/Badge.svelte.d.ts +20 -0
- package/dist/components/Button.svelte +415 -0
- package/dist/components/Button.svelte.d.ts +34 -0
- package/dist/components/Card.svelte +181 -0
- package/dist/components/Card.svelte.d.ts +24 -0
- package/dist/components/CardBody.svelte +78 -0
- package/dist/components/CardBody.svelte.d.ts +12 -0
- package/dist/components/CardFooter.svelte +81 -0
- package/dist/components/CardFooter.svelte.d.ts +14 -0
- package/dist/components/CardHeader.svelte +186 -0
- package/dist/components/CardHeader.svelte.d.ts +21 -0
- package/dist/components/Col.svelte +172 -0
- package/dist/components/Col.svelte.d.ts +26 -0
- package/dist/components/Container.svelte +118 -0
- package/dist/components/Container.svelte.d.ts +14 -0
- package/dist/components/Drawer.svelte +233 -0
- package/dist/components/Drawer.svelte.d.ts +13 -0
- package/dist/components/Dropdown.svelte +190 -0
- package/dist/components/Dropdown.svelte.d.ts +26 -0
- package/dist/components/DropdownItem.svelte +103 -0
- package/dist/components/DropdownItem.svelte.d.ts +22 -0
- package/dist/components/DurationInput.svelte +170 -0
- package/dist/components/DurationInput.svelte.d.ts +27 -0
- package/dist/components/EditableTable.svelte +647 -0
- package/dist/components/EditableTable.svelte.d.ts +74 -0
- package/dist/components/EmptyState.svelte +192 -0
- package/dist/components/EmptyState.svelte.d.ts +22 -0
- package/dist/components/FormField.svelte +260 -0
- package/dist/components/FormField.svelte.d.ts +68 -0
- package/dist/components/GridView.svelte +1022 -0
- package/dist/components/GridView.svelte.d.ts +38 -0
- package/dist/components/GridView.types.d.ts +28 -0
- package/dist/components/GridView.types.js +1 -0
- package/dist/components/LoadingSpinner.svelte +253 -0
- package/dist/components/LoadingSpinner.svelte.d.ts +17 -0
- package/dist/components/Modal.svelte +473 -0
- package/dist/components/Modal.svelte.d.ts +42 -0
- package/dist/components/PhoneInput.svelte +406 -0
- package/dist/components/PhoneInput.svelte.d.ts +31 -0
- package/dist/components/PhotoUpload.svelte +529 -0
- package/dist/components/PhotoUpload.svelte.d.ts +46 -0
- package/dist/components/Row.svelte +153 -0
- package/dist/components/Row.svelte.d.ts +18 -0
- package/dist/icons/PawPrintIcon.svelte +41 -0
- package/dist/icons/PawPrintIcon.svelte.d.ts +14 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +49 -0
- package/dist/styles/forms.css +182 -0
- package/dist/styles/tokens.css +243 -0
- package/dist/utils/duration.d.ts +20 -0
- package/dist/utils/duration.js +40 -0
- package/dist/utils/scrollLock.d.ts +7 -0
- package/dist/utils/scrollLock.js +26 -0
- package/package.json +66 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import PawPrintIcon from '../icons/PawPrintIcon.svelte';
|
|
4
|
+
|
|
5
|
+
export interface EmptyStateProps {
|
|
6
|
+
/** Empty state title */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Description or message */
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Alias for description */
|
|
11
|
+
message?: string;
|
|
12
|
+
/** Bootstrap Icons class (e.g. "bi-inbox") */
|
|
13
|
+
icon?: string;
|
|
14
|
+
/** Predefined illustration */
|
|
15
|
+
illustration?: 'inbox' | 'search' | 'pets' | 'appointments' | 'calendar' | 'users' | 'files';
|
|
16
|
+
/** Actions/buttons to display */
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
/** Additional CSS classes */
|
|
19
|
+
class?: string;
|
|
20
|
+
/** Compact mode (reduces padding and sizes) */
|
|
21
|
+
compact?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
message,
|
|
28
|
+
icon,
|
|
29
|
+
illustration,
|
|
30
|
+
children,
|
|
31
|
+
class: additionalClasses = '',
|
|
32
|
+
compact = false
|
|
33
|
+
}: EmptyStateProps = $props();
|
|
34
|
+
|
|
35
|
+
// Support both description and message for flexibility
|
|
36
|
+
const effectiveDescription = $derived(description || message);
|
|
37
|
+
|
|
38
|
+
// Illustration to Bootstrap icon mapping.
|
|
39
|
+
// `pets` is handled separately below (renders PawPrintIcon).
|
|
40
|
+
const illustrations: Record<string, string> = {
|
|
41
|
+
inbox: 'bi-inbox',
|
|
42
|
+
search: 'bi-search',
|
|
43
|
+
appointments: 'bi-calendar-x',
|
|
44
|
+
calendar: 'bi-calendar3',
|
|
45
|
+
users: 'bi-people',
|
|
46
|
+
files: 'bi-file-earmark-text'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const usePawPrint = $derived(icon === 'paw-print' || illustration === 'pets');
|
|
50
|
+
|
|
51
|
+
// Which icon to use (only when not paw-print)
|
|
52
|
+
const iconClass = $derived(icon || (illustration ? illustrations[illustration] : 'bi-inbox'));
|
|
53
|
+
|
|
54
|
+
const emptyStateClass = $derived([
|
|
55
|
+
'empty-state',
|
|
56
|
+
compact ? 'empty-state-compact' : '',
|
|
57
|
+
additionalClasses
|
|
58
|
+
].filter(Boolean).join(' '));
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<div class={emptyStateClass}>
|
|
62
|
+
<div class="empty-state-icon">
|
|
63
|
+
{#if usePawPrint}
|
|
64
|
+
<PawPrintIcon size="4rem" />
|
|
65
|
+
{:else}
|
|
66
|
+
<i class="bi {iconClass}"></i>
|
|
67
|
+
{/if}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<h3 class="empty-state-title">{title}</h3>
|
|
71
|
+
|
|
72
|
+
{#if effectiveDescription}
|
|
73
|
+
<p class="empty-state-description">{effectiveDescription}</p>
|
|
74
|
+
{/if}
|
|
75
|
+
|
|
76
|
+
{#if children}
|
|
77
|
+
<div class="empty-state-actions">
|
|
78
|
+
{@render children()}
|
|
79
|
+
</div>
|
|
80
|
+
{/if}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<style>
|
|
84
|
+
.empty-state {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
align-items: center;
|
|
88
|
+
justify-content: center;
|
|
89
|
+
padding: var(--spacing-2xl) var(--spacing-lg);
|
|
90
|
+
text-align: center;
|
|
91
|
+
min-height: 300px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.empty-state-icon {
|
|
95
|
+
margin-bottom: var(--spacing-lg);
|
|
96
|
+
color: var(--color-gray-400);
|
|
97
|
+
animation: float 3s ease-in-out infinite;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.empty-state-icon i {
|
|
101
|
+
font-size: 4rem;
|
|
102
|
+
display: block;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.empty-state-title {
|
|
106
|
+
font-size: var(--font-size-xl);
|
|
107
|
+
font-weight: var(--font-weight-semibold);
|
|
108
|
+
color: var(--color-gray-700);
|
|
109
|
+
margin-bottom: var(--spacing-sm);
|
|
110
|
+
margin-top: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.empty-state-description {
|
|
114
|
+
font-size: var(--font-size-base);
|
|
115
|
+
color: var(--color-gray-600);
|
|
116
|
+
max-width: 400px;
|
|
117
|
+
margin: 0 auto var(--spacing-lg);
|
|
118
|
+
line-height: var(--line-height-relaxed);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.empty-state-actions {
|
|
122
|
+
display: flex;
|
|
123
|
+
gap: var(--spacing-md);
|
|
124
|
+
flex-wrap: wrap;
|
|
125
|
+
justify-content: center;
|
|
126
|
+
margin-top: var(--spacing-md);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Compact mode */
|
|
130
|
+
.empty-state-compact {
|
|
131
|
+
padding: var(--spacing-lg) var(--spacing-md);
|
|
132
|
+
min-height: 200px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.empty-state-compact .empty-state-icon {
|
|
136
|
+
margin-bottom: var(--spacing-md);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.empty-state-compact .empty-state-icon i {
|
|
140
|
+
font-size: 2.5rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.empty-state-compact .empty-state-title {
|
|
144
|
+
font-size: var(--font-size-lg);
|
|
145
|
+
margin-bottom: var(--spacing-xs);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.empty-state-compact .empty-state-description {
|
|
149
|
+
font-size: var(--font-size-sm);
|
|
150
|
+
margin-bottom: var(--spacing-md);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Smooth floating animation */
|
|
154
|
+
@keyframes float {
|
|
155
|
+
0%, 100% {
|
|
156
|
+
transform: translateY(0);
|
|
157
|
+
}
|
|
158
|
+
50% {
|
|
159
|
+
transform: translateY(-10px);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Responsive */
|
|
164
|
+
@media (max-width: 576px) {
|
|
165
|
+
.empty-state {
|
|
166
|
+
padding: var(--spacing-xl) var(--spacing-md);
|
|
167
|
+
min-height: 250px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.empty-state-icon i {
|
|
171
|
+
font-size: 3rem;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.empty-state-title {
|
|
175
|
+
font-size: var(--font-size-lg);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.empty-state-description {
|
|
179
|
+
font-size: var(--font-size-sm);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.empty-state-actions {
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
width: 100%;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.empty-state-actions :global(button),
|
|
188
|
+
.empty-state-actions :global(a) {
|
|
189
|
+
width: 100%;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
export interface EmptyStateProps {
|
|
3
|
+
/** Empty state title */
|
|
4
|
+
title: string;
|
|
5
|
+
/** Description or message */
|
|
6
|
+
description?: string;
|
|
7
|
+
/** Alias for description */
|
|
8
|
+
message?: string;
|
|
9
|
+
/** Bootstrap Icons class (e.g. "bi-inbox") */
|
|
10
|
+
icon?: string;
|
|
11
|
+
/** Predefined illustration */
|
|
12
|
+
illustration?: 'inbox' | 'search' | 'pets' | 'appointments' | 'calendar' | 'users' | 'files';
|
|
13
|
+
/** Actions/buttons to display */
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
/** Additional CSS classes */
|
|
16
|
+
class?: string;
|
|
17
|
+
/** Compact mode (reduces padding and sizes) */
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare const EmptyState: import("svelte").Component<EmptyStateProps, {}, "">;
|
|
21
|
+
type EmptyState = ReturnType<typeof EmptyState>;
|
|
22
|
+
export default EmptyState;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import "../styles/forms.css";
|
|
4
|
+
|
|
5
|
+
interface FormFieldProps {
|
|
6
|
+
/** ID único do campo */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Label do campo */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Tipo do campo */
|
|
11
|
+
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'date' | 'time' | 'datetime-local' | 'textarea' | 'select' | 'search';
|
|
12
|
+
/** Valor do campo (bindable) */
|
|
13
|
+
value?: string | number;
|
|
14
|
+
/** Placeholder */
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
/** Campo obrigatório */
|
|
17
|
+
required?: boolean;
|
|
18
|
+
/** Campo desabilitado */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Somente leitura */
|
|
21
|
+
readonly?: boolean;
|
|
22
|
+
/** Mensagem de erro */
|
|
23
|
+
error?: string;
|
|
24
|
+
/** Texto de ajuda */
|
|
25
|
+
helpText?: string;
|
|
26
|
+
/** Número de linhas (para textarea) */
|
|
27
|
+
rows?: number;
|
|
28
|
+
/** Opções (para select) */
|
|
29
|
+
options?: Array<{ value: string | number; label: string }>;
|
|
30
|
+
/** Valor mínimo (para number/date) */
|
|
31
|
+
min?: number | string;
|
|
32
|
+
/** Valor máximo (para number/date) */
|
|
33
|
+
max?: number | string;
|
|
34
|
+
/** Step (para number) */
|
|
35
|
+
step?: number;
|
|
36
|
+
/** Pattern (regex) */
|
|
37
|
+
pattern?: string;
|
|
38
|
+
/** Autocomplete */
|
|
39
|
+
autocomplete?: HTMLInputElement['autocomplete'];
|
|
40
|
+
/** Tamanho mínimo (para text/password) */
|
|
41
|
+
minlength?: number;
|
|
42
|
+
/** Tamanho máximo (para text/password) */
|
|
43
|
+
maxlength?: number;
|
|
44
|
+
/** Callback de mudança */
|
|
45
|
+
onchange?: (value: any) => void;
|
|
46
|
+
/** Callback de input */
|
|
47
|
+
oninput?: (value: any) => void;
|
|
48
|
+
/** Callback de blur */
|
|
49
|
+
onblur?: () => void;
|
|
50
|
+
/** Callback de focus */
|
|
51
|
+
onfocus?: () => void;
|
|
52
|
+
/** Callback de keydown */
|
|
53
|
+
onkeydown?: (e: KeyboardEvent) => void;
|
|
54
|
+
/** Ícone do campo (exibido no label) */
|
|
55
|
+
icon?: Snippet;
|
|
56
|
+
/** Ícone dentro do input (à esquerda) */
|
|
57
|
+
inputIcon?: Snippet;
|
|
58
|
+
/** Addon à direita do input (ex: loading spinner) */
|
|
59
|
+
inputAddon?: Snippet;
|
|
60
|
+
/** Classes CSS adicionais */
|
|
61
|
+
class?: string;
|
|
62
|
+
/** Referência ao elemento input */
|
|
63
|
+
inputRef?: HTMLInputElement;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let {
|
|
67
|
+
id,
|
|
68
|
+
label,
|
|
69
|
+
type = 'text',
|
|
70
|
+
value = $bindable(''),
|
|
71
|
+
placeholder,
|
|
72
|
+
required = false,
|
|
73
|
+
disabled = false,
|
|
74
|
+
readonly = false,
|
|
75
|
+
error,
|
|
76
|
+
helpText,
|
|
77
|
+
rows = 3,
|
|
78
|
+
options = [],
|
|
79
|
+
min,
|
|
80
|
+
max,
|
|
81
|
+
step,
|
|
82
|
+
pattern,
|
|
83
|
+
autocomplete,
|
|
84
|
+
minlength,
|
|
85
|
+
maxlength,
|
|
86
|
+
onchange,
|
|
87
|
+
oninput,
|
|
88
|
+
onblur,
|
|
89
|
+
onfocus,
|
|
90
|
+
onkeydown,
|
|
91
|
+
icon,
|
|
92
|
+
inputIcon,
|
|
93
|
+
inputAddon,
|
|
94
|
+
class: additionalClasses = '',
|
|
95
|
+
inputRef = $bindable()
|
|
96
|
+
}: FormFieldProps = $props();
|
|
97
|
+
|
|
98
|
+
// Handler de input
|
|
99
|
+
function handleInput(event: Event) {
|
|
100
|
+
const target = event.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
101
|
+
|
|
102
|
+
if (type === 'number') {
|
|
103
|
+
value = target.value ? Number(target.value) : '';
|
|
104
|
+
} else {
|
|
105
|
+
value = target.value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
oninput?.(value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handler de change
|
|
112
|
+
function handleChange(event: Event) {
|
|
113
|
+
const target = event.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
114
|
+
|
|
115
|
+
if (type === 'number') {
|
|
116
|
+
value = target.value ? Number(target.value) : '';
|
|
117
|
+
} else if (type === 'select') {
|
|
118
|
+
// Preserve the original option value type (e.g., number) instead of the
|
|
119
|
+
// DOM-stringified target.value, so bind:value can match the option correctly.
|
|
120
|
+
const selectedStr = target.value;
|
|
121
|
+
const matchingOption = options.find(opt => String(opt.value) === selectedStr);
|
|
122
|
+
value = matchingOption ? matchingOption.value : selectedStr;
|
|
123
|
+
} else {
|
|
124
|
+
value = target.value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onchange?.(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Handler de blur
|
|
131
|
+
function handleBlur() {
|
|
132
|
+
onblur?.();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Classes dinâmicas
|
|
136
|
+
const fieldClass = $derived([
|
|
137
|
+
'ap-form-control',
|
|
138
|
+
type === 'select' ? 'ap-form-select' : '',
|
|
139
|
+
error ? 'ap-form-control-invalid' : '',
|
|
140
|
+
inputIcon ? 'ap-form-control-with-icon' : ''
|
|
141
|
+
].filter(Boolean).join(' '));
|
|
142
|
+
|
|
143
|
+
const containerClass = $derived([
|
|
144
|
+
'ap-form-field',
|
|
145
|
+
additionalClasses
|
|
146
|
+
].filter(Boolean).join(' '));
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<div class={containerClass}>
|
|
150
|
+
<label for={id} class="ap-form-label">
|
|
151
|
+
{#if icon}
|
|
152
|
+
<span class="ap-form-label-icon">
|
|
153
|
+
{@render icon()}
|
|
154
|
+
</span>
|
|
155
|
+
{/if}
|
|
156
|
+
{label}
|
|
157
|
+
{#if required}
|
|
158
|
+
<span class="ap-form-required" aria-label="obrigatório">*</span>
|
|
159
|
+
{/if}
|
|
160
|
+
</label>
|
|
161
|
+
|
|
162
|
+
<div class="ap-form-input-wrapper" class:has-input-icon={inputIcon} class:has-addon={inputAddon}>
|
|
163
|
+
{#if inputIcon}
|
|
164
|
+
<span class="ap-form-input-icon">
|
|
165
|
+
{@render inputIcon()}
|
|
166
|
+
</span>
|
|
167
|
+
{/if}
|
|
168
|
+
|
|
169
|
+
{#if type === 'textarea'}
|
|
170
|
+
<textarea
|
|
171
|
+
{id}
|
|
172
|
+
class={fieldClass}
|
|
173
|
+
{placeholder}
|
|
174
|
+
{required}
|
|
175
|
+
{disabled}
|
|
176
|
+
{readonly}
|
|
177
|
+
{rows}
|
|
178
|
+
{autocomplete}
|
|
179
|
+
value={value}
|
|
180
|
+
oninput={handleInput}
|
|
181
|
+
onchange={handleChange}
|
|
182
|
+
onblur={handleBlur}
|
|
183
|
+
aria-invalid={error ? 'true' : undefined}
|
|
184
|
+
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
185
|
+
></textarea>
|
|
186
|
+
{:else if type === 'select'}
|
|
187
|
+
<select
|
|
188
|
+
{id}
|
|
189
|
+
class={fieldClass}
|
|
190
|
+
{required}
|
|
191
|
+
{disabled}
|
|
192
|
+
{autocomplete}
|
|
193
|
+
bind:value
|
|
194
|
+
onchange={handleChange}
|
|
195
|
+
onblur={handleBlur}
|
|
196
|
+
aria-invalid={error ? 'true' : undefined}
|
|
197
|
+
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
198
|
+
>
|
|
199
|
+
{#if placeholder}
|
|
200
|
+
<option value="" disabled selected={!value}>
|
|
201
|
+
{placeholder}
|
|
202
|
+
</option>
|
|
203
|
+
{/if}
|
|
204
|
+
{#each options as option}
|
|
205
|
+
<option value={option.value}>
|
|
206
|
+
{option.label}
|
|
207
|
+
</option>
|
|
208
|
+
{/each}
|
|
209
|
+
</select>
|
|
210
|
+
{:else}
|
|
211
|
+
<input
|
|
212
|
+
bind:this={inputRef}
|
|
213
|
+
{id}
|
|
214
|
+
type={type}
|
|
215
|
+
class={fieldClass}
|
|
216
|
+
{placeholder}
|
|
217
|
+
{required}
|
|
218
|
+
{disabled}
|
|
219
|
+
{readonly}
|
|
220
|
+
{min}
|
|
221
|
+
{max}
|
|
222
|
+
{step}
|
|
223
|
+
{pattern}
|
|
224
|
+
{autocomplete}
|
|
225
|
+
{minlength}
|
|
226
|
+
{maxlength}
|
|
227
|
+
value={value}
|
|
228
|
+
oninput={handleInput}
|
|
229
|
+
onchange={handleChange}
|
|
230
|
+
onblur={handleBlur}
|
|
231
|
+
onfocus={onfocus}
|
|
232
|
+
onkeydown={onkeydown}
|
|
233
|
+
aria-invalid={error ? 'true' : undefined}
|
|
234
|
+
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
235
|
+
/>
|
|
236
|
+
{/if}
|
|
237
|
+
|
|
238
|
+
{#if inputAddon}
|
|
239
|
+
<span class="ap-form-input-addon">
|
|
240
|
+
{@render inputAddon()}
|
|
241
|
+
</span>
|
|
242
|
+
{/if}
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{#if error}
|
|
246
|
+
<div id="{id}-error" class="ap-form-error">
|
|
247
|
+
<i class="bi bi-exclamation-circle"></i>
|
|
248
|
+
{error}
|
|
249
|
+
</div>
|
|
250
|
+
{/if}
|
|
251
|
+
|
|
252
|
+
{#if helpText && !error}
|
|
253
|
+
<div id="{id}-help" class="ap-form-help">
|
|
254
|
+
<i class="bi bi-info-circle"></i>
|
|
255
|
+
{helpText}
|
|
256
|
+
</div>
|
|
257
|
+
{/if}
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<!-- Styles are in /src/lib/styles/forms.css -->
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import "../styles/forms.css";
|
|
3
|
+
interface FormFieldProps {
|
|
4
|
+
/** ID único do campo */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Label do campo */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Tipo do campo */
|
|
9
|
+
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'date' | 'time' | 'datetime-local' | 'textarea' | 'select' | 'search';
|
|
10
|
+
/** Valor do campo (bindable) */
|
|
11
|
+
value?: string | number;
|
|
12
|
+
/** Placeholder */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Campo obrigatório */
|
|
15
|
+
required?: boolean;
|
|
16
|
+
/** Campo desabilitado */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** Somente leitura */
|
|
19
|
+
readonly?: boolean;
|
|
20
|
+
/** Mensagem de erro */
|
|
21
|
+
error?: string;
|
|
22
|
+
/** Texto de ajuda */
|
|
23
|
+
helpText?: string;
|
|
24
|
+
/** Número de linhas (para textarea) */
|
|
25
|
+
rows?: number;
|
|
26
|
+
/** Opções (para select) */
|
|
27
|
+
options?: Array<{
|
|
28
|
+
value: string | number;
|
|
29
|
+
label: string;
|
|
30
|
+
}>;
|
|
31
|
+
/** Valor mínimo (para number/date) */
|
|
32
|
+
min?: number | string;
|
|
33
|
+
/** Valor máximo (para number/date) */
|
|
34
|
+
max?: number | string;
|
|
35
|
+
/** Step (para number) */
|
|
36
|
+
step?: number;
|
|
37
|
+
/** Pattern (regex) */
|
|
38
|
+
pattern?: string;
|
|
39
|
+
/** Autocomplete */
|
|
40
|
+
autocomplete?: HTMLInputElement['autocomplete'];
|
|
41
|
+
/** Tamanho mínimo (para text/password) */
|
|
42
|
+
minlength?: number;
|
|
43
|
+
/** Tamanho máximo (para text/password) */
|
|
44
|
+
maxlength?: number;
|
|
45
|
+
/** Callback de mudança */
|
|
46
|
+
onchange?: (value: any) => void;
|
|
47
|
+
/** Callback de input */
|
|
48
|
+
oninput?: (value: any) => void;
|
|
49
|
+
/** Callback de blur */
|
|
50
|
+
onblur?: () => void;
|
|
51
|
+
/** Callback de focus */
|
|
52
|
+
onfocus?: () => void;
|
|
53
|
+
/** Callback de keydown */
|
|
54
|
+
onkeydown?: (e: KeyboardEvent) => void;
|
|
55
|
+
/** Ícone do campo (exibido no label) */
|
|
56
|
+
icon?: Snippet;
|
|
57
|
+
/** Ícone dentro do input (à esquerda) */
|
|
58
|
+
inputIcon?: Snippet;
|
|
59
|
+
/** Addon à direita do input (ex: loading spinner) */
|
|
60
|
+
inputAddon?: Snippet;
|
|
61
|
+
/** Classes CSS adicionais */
|
|
62
|
+
class?: string;
|
|
63
|
+
/** Referência ao elemento input */
|
|
64
|
+
inputRef?: HTMLInputElement;
|
|
65
|
+
}
|
|
66
|
+
declare const FormField: import("svelte").Component<FormFieldProps, {}, "value" | "inputRef">;
|
|
67
|
+
type FormField = ReturnType<typeof FormField>;
|
|
68
|
+
export default FormField;
|