@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.
Files changed (60) hide show
  1. package/README.md +125 -0
  2. package/dist/components/Alert.svelte +335 -0
  3. package/dist/components/Alert.svelte.d.ts +24 -0
  4. package/dist/components/AutocompleteInput.svelte +356 -0
  5. package/dist/components/AutocompleteInput.svelte.d.ts +72 -0
  6. package/dist/components/Badge.svelte +185 -0
  7. package/dist/components/Badge.svelte.d.ts +20 -0
  8. package/dist/components/Button.svelte +415 -0
  9. package/dist/components/Button.svelte.d.ts +34 -0
  10. package/dist/components/Card.svelte +181 -0
  11. package/dist/components/Card.svelte.d.ts +24 -0
  12. package/dist/components/CardBody.svelte +78 -0
  13. package/dist/components/CardBody.svelte.d.ts +12 -0
  14. package/dist/components/CardFooter.svelte +81 -0
  15. package/dist/components/CardFooter.svelte.d.ts +14 -0
  16. package/dist/components/CardHeader.svelte +186 -0
  17. package/dist/components/CardHeader.svelte.d.ts +21 -0
  18. package/dist/components/Col.svelte +172 -0
  19. package/dist/components/Col.svelte.d.ts +26 -0
  20. package/dist/components/Container.svelte +118 -0
  21. package/dist/components/Container.svelte.d.ts +14 -0
  22. package/dist/components/Drawer.svelte +233 -0
  23. package/dist/components/Drawer.svelte.d.ts +13 -0
  24. package/dist/components/Dropdown.svelte +190 -0
  25. package/dist/components/Dropdown.svelte.d.ts +26 -0
  26. package/dist/components/DropdownItem.svelte +103 -0
  27. package/dist/components/DropdownItem.svelte.d.ts +22 -0
  28. package/dist/components/DurationInput.svelte +170 -0
  29. package/dist/components/DurationInput.svelte.d.ts +27 -0
  30. package/dist/components/EditableTable.svelte +647 -0
  31. package/dist/components/EditableTable.svelte.d.ts +74 -0
  32. package/dist/components/EmptyState.svelte +192 -0
  33. package/dist/components/EmptyState.svelte.d.ts +22 -0
  34. package/dist/components/FormField.svelte +260 -0
  35. package/dist/components/FormField.svelte.d.ts +68 -0
  36. package/dist/components/GridView.svelte +1022 -0
  37. package/dist/components/GridView.svelte.d.ts +38 -0
  38. package/dist/components/GridView.types.d.ts +28 -0
  39. package/dist/components/GridView.types.js +1 -0
  40. package/dist/components/LoadingSpinner.svelte +253 -0
  41. package/dist/components/LoadingSpinner.svelte.d.ts +17 -0
  42. package/dist/components/Modal.svelte +473 -0
  43. package/dist/components/Modal.svelte.d.ts +42 -0
  44. package/dist/components/PhoneInput.svelte +406 -0
  45. package/dist/components/PhoneInput.svelte.d.ts +31 -0
  46. package/dist/components/PhotoUpload.svelte +529 -0
  47. package/dist/components/PhotoUpload.svelte.d.ts +46 -0
  48. package/dist/components/Row.svelte +153 -0
  49. package/dist/components/Row.svelte.d.ts +18 -0
  50. package/dist/icons/PawPrintIcon.svelte +41 -0
  51. package/dist/icons/PawPrintIcon.svelte.d.ts +14 -0
  52. package/dist/index.d.ts +41 -0
  53. package/dist/index.js +49 -0
  54. package/dist/styles/forms.css +182 -0
  55. package/dist/styles/tokens.css +243 -0
  56. package/dist/utils/duration.d.ts +20 -0
  57. package/dist/utils/duration.js +40 -0
  58. package/dist/utils/scrollLock.d.ts +7 -0
  59. package/dist/utils/scrollLock.js +26 -0
  60. package/package.json +66 -0
@@ -0,0 +1,406 @@
1
+ <script lang="ts">
2
+ import "../styles/forms.css";
3
+ import {
4
+ getCountryOptions,
5
+ getPhoneCountry,
6
+ getNationalNumber,
7
+ normalizePhone,
8
+ formatNationalAsYouType,
9
+ DEFAULT_COUNTRY,
10
+ type CountryOption,
11
+ type CountryCode,
12
+ } from "@200systems/shared";
13
+
14
+ interface Props {
15
+ /** ID único do campo */
16
+ id: string;
17
+ /** Label do campo */
18
+ label: string;
19
+ /** Valor em E.164 (bindable), ex.: "+351912345678" */
20
+ value?: string;
21
+ /** Placeholder do número nacional */
22
+ placeholder?: string;
23
+ /** Campo obrigatório */
24
+ required?: boolean;
25
+ /** Campo desabilitado */
26
+ disabled?: boolean;
27
+ /** Mensagem de erro */
28
+ error?: string;
29
+ /** Texto de ajuda */
30
+ helpText?: string;
31
+ /** País assumido por omissão */
32
+ defaultCountry?: CountryCode;
33
+ /** Callback de input (recebe o valor E.164) */
34
+ oninput?: (value: string) => void;
35
+ /** Callback de blur */
36
+ onblur?: () => void;
37
+ /** Classes CSS adicionais */
38
+ class?: string;
39
+ }
40
+
41
+ let {
42
+ id,
43
+ label,
44
+ value = $bindable(""),
45
+ placeholder = "912 345 678",
46
+ required = false,
47
+ disabled = false,
48
+ error,
49
+ helpText,
50
+ defaultCountry = DEFAULT_COUNTRY,
51
+ oninput,
52
+ onblur,
53
+ class: additionalClasses = "",
54
+ }: Props = $props();
55
+
56
+ const countries: CountryOption[] = getCountryOptions("pt");
57
+
58
+ // Estado interno: país selecionado + parte nacional digitada.
59
+ let selectedCountry = $state<CountryCode>(
60
+ getPhoneCountry(value, defaultCountry) ?? defaultCountry
61
+ );
62
+ let national = $state<string>(
63
+ formatNationalAsYouType(getNationalNumber(value, defaultCountry), selectedCountry)
64
+ );
65
+
66
+ let open = $state(false);
67
+ let search = $state("");
68
+ let inputEl = $state<HTMLInputElement | null>(null);
69
+
70
+ const current = $derived(
71
+ countries.find((c) => c.code === selectedCountry) ?? countries[0]
72
+ );
73
+
74
+ const filtered = $derived.by(() => {
75
+ const q = search.trim().toLowerCase();
76
+ if (!q) return countries;
77
+ const digitsQ = q.replace(/\D/g, "");
78
+ return countries.filter(
79
+ (c) =>
80
+ c.name.toLowerCase().includes(q) ||
81
+ c.code.toLowerCase().includes(q) ||
82
+ (digitsQ.length > 0 && c.callingCode.includes(digitsQ))
83
+ );
84
+ });
85
+
86
+ // Reconstrói o valor E.164 a partir do país + número nacional e propaga.
87
+ function emit() {
88
+ const digits = national.replace(/\D/g, "");
89
+ if (!digits) {
90
+ value = "";
91
+ } else {
92
+ const normalized = normalizePhone(`+${current.callingCode}${digits}`, selectedCountry);
93
+ // Enquanto o número está incompleto guarda-se o melhor esforço (a validação
94
+ // do formulário rejeita números inválidos na submissão).
95
+ value = normalized ?? `+${current.callingCode}${digits}`;
96
+ }
97
+ oninput?.(value);
98
+ }
99
+
100
+ function handleNationalInput(event: Event) {
101
+ const raw = (event.target as HTMLInputElement).value;
102
+
103
+ // Se colarem um número internacional completo, deteta o país e separa a parte nacional.
104
+ if (/^(\+|00)/.test(raw.trim())) {
105
+ const detected = getPhoneCountry(raw, defaultCountry);
106
+ if (detected) {
107
+ selectedCountry = detected;
108
+ national = formatNationalAsYouType(getNationalNumber(raw, defaultCountry), detected);
109
+ emit();
110
+ return;
111
+ }
112
+ }
113
+
114
+ // Aceita apenas dígitos e formata-os progressivamente (ex.: "912 345 678").
115
+ national = formatNationalAsYouType(raw, selectedCountry);
116
+ // Força a sincronização do DOM mesmo quando o valor formatado não muda
117
+ // (ex.: ao digitar uma letra que é removida), evitando caracteres inválidos presos.
118
+ if (inputEl && inputEl.value !== national) inputEl.value = national;
119
+ emit();
120
+ }
121
+
122
+ function selectCountry(code: CountryCode) {
123
+ selectedCountry = code;
124
+ open = false;
125
+ search = "";
126
+ // Reagrupa o número já digitado segundo o plano do novo país.
127
+ national = formatNationalAsYouType(national, code);
128
+ emit();
129
+ }
130
+
131
+ function toggle() {
132
+ if (disabled) return;
133
+ open = !open;
134
+ search = "";
135
+ }
136
+
137
+ // Fecha o dropdown ao clicar fora.
138
+ function handleWindowClick(event: MouseEvent) {
139
+ if (!open) return;
140
+ const target = event.target as HTMLElement;
141
+ if (!target.closest(`#${id}-phone-wrapper`)) {
142
+ open = false;
143
+ }
144
+ }
145
+ </script>
146
+
147
+ <svelte:window onclick={handleWindowClick} />
148
+
149
+ <div class="ap-form-field {additionalClasses}">
150
+ <label for="{id}" class="ap-form-label">
151
+ <span class="ap-form-label-icon"><i class="bi bi-telephone"></i></span>
152
+ {label}
153
+ {#if required}
154
+ <span class="ap-form-required" aria-label="obrigatório">*</span>
155
+ {/if}
156
+ </label>
157
+
158
+ <div class="phone-input" id="{id}-phone-wrapper" class:is-invalid={!!error}>
159
+ <button
160
+ type="button"
161
+ class="phone-country"
162
+ onclick={toggle}
163
+ {disabled}
164
+ aria-haspopup="listbox"
165
+ aria-expanded={open}
166
+ aria-label="Selecionar país (indicativo {current.callingCode})"
167
+ >
168
+ <span class="phone-flag">{current.flag}</span>
169
+ <span class="phone-dial">+{current.callingCode}</span>
170
+ <i class="bi bi-chevron-down phone-caret"></i>
171
+ </button>
172
+
173
+ <input
174
+ {id}
175
+ bind:this={inputEl}
176
+ type="tel"
177
+ inputmode="tel"
178
+ class="ap-form-control phone-national"
179
+ {placeholder}
180
+ {required}
181
+ {disabled}
182
+ autocomplete="tel-national"
183
+ value={national}
184
+ oninput={handleNationalInput}
185
+ {onblur}
186
+ aria-invalid={error ? "true" : undefined}
187
+ aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
188
+ />
189
+
190
+ {#if open}
191
+ <div class="phone-menu" role="listbox">
192
+ <div class="phone-search">
193
+ <i class="bi bi-search"></i>
194
+ <!-- svelte-ignore a11y_autofocus -->
195
+ <input
196
+ type="text"
197
+ placeholder="Procurar país ou indicativo"
198
+ bind:value={search}
199
+ autofocus
200
+ aria-label="Procurar país"
201
+ />
202
+ </div>
203
+ <ul class="phone-options">
204
+ {#each filtered as country (country.code)}
205
+ <li>
206
+ <button
207
+ type="button"
208
+ class="phone-option"
209
+ class:selected={country.code === selectedCountry}
210
+ role="option"
211
+ aria-selected={country.code === selectedCountry}
212
+ onclick={() => selectCountry(country.code)}
213
+ >
214
+ <span class="phone-flag">{country.flag}</span>
215
+ <span class="phone-option-name">{country.name}</span>
216
+ <span class="phone-option-dial">+{country.callingCode}</span>
217
+ </button>
218
+ </li>
219
+ {:else}
220
+ <li class="phone-empty">Sem resultados</li>
221
+ {/each}
222
+ </ul>
223
+ </div>
224
+ {/if}
225
+ </div>
226
+
227
+ {#if error}
228
+ <div id="{id}-error" class="ap-form-error">
229
+ <i class="bi bi-exclamation-circle"></i>
230
+ {error}
231
+ </div>
232
+ {/if}
233
+
234
+ {#if helpText && !error}
235
+ <div id="{id}-help" class="ap-form-help">
236
+ <i class="bi bi-info-circle"></i>
237
+ {helpText}
238
+ </div>
239
+ {/if}
240
+ </div>
241
+
242
+ <style>
243
+ /* Contentor único com a borda/raio; botão e input ficam sem borda lá dentro. */
244
+ .phone-input {
245
+ position: relative;
246
+ display: flex;
247
+ align-items: stretch;
248
+ border: 1px solid var(--color-border-strong);
249
+ border-radius: var(--radius-lg);
250
+ background: var(--color-bg-surface);
251
+ transition: all var(--transition-base);
252
+ }
253
+
254
+ .phone-input:focus-within {
255
+ border-color: var(--color-primary);
256
+ box-shadow: 0 0 0 0.25rem var(--focus-ring-color);
257
+ }
258
+
259
+ .phone-input.is-invalid {
260
+ border-color: var(--color-danger, #d92d20);
261
+ border-width: 2px;
262
+ }
263
+
264
+ /* Neutraliza o ícone de erro global (.is-invalid::before), que entraria como item
265
+ flex à esquerda e desalinhava o campo. O erro é mostrado abaixo (.ap-form-error). */
266
+ .phone-input.is-invalid::before {
267
+ display: none;
268
+ content: none;
269
+ }
270
+
271
+ .phone-country {
272
+ display: inline-flex;
273
+ align-items: center;
274
+ gap: var(--spacing-xs, 0.25rem);
275
+ padding: 0.72rem 0.85rem;
276
+ border: 0;
277
+ border-right: 1px solid var(--color-border);
278
+ border-radius: var(--radius-lg) 0 0 var(--radius-lg);
279
+ background: transparent;
280
+ color: var(--color-text, #1d2939);
281
+ font-size: var(--font-size-sm, 0.875rem);
282
+ cursor: pointer;
283
+ white-space: nowrap;
284
+ }
285
+
286
+ .phone-country:hover:not(:disabled) {
287
+ background: var(--color-gray-50);
288
+ }
289
+
290
+ .phone-country:disabled {
291
+ cursor: not-allowed;
292
+ opacity: 0.6;
293
+ }
294
+
295
+ .phone-flag {
296
+ font-size: 1.1rem;
297
+ line-height: 1;
298
+ }
299
+
300
+ .phone-dial {
301
+ font-weight: var(--font-weight-medium, 500);
302
+ }
303
+
304
+ .phone-caret {
305
+ font-size: 0.65rem;
306
+ opacity: 0.6;
307
+ margin-left: 0.125rem;
308
+ transition: transform 0.15s ease;
309
+ }
310
+
311
+ .phone-country[aria-expanded="true"] .phone-caret {
312
+ transform: rotate(180deg);
313
+ }
314
+
315
+ /* O input herda padding/tipografia de .ap-form-control mas sem borda/raio/fundo. */
316
+ .phone-national {
317
+ flex: 1 1 auto;
318
+ min-width: 0;
319
+ border: 0;
320
+ border-radius: 0;
321
+ background: transparent;
322
+ }
323
+
324
+ .phone-national:focus {
325
+ outline: none;
326
+ box-shadow: none;
327
+ border: 0;
328
+ }
329
+
330
+ .phone-national:disabled {
331
+ background: transparent;
332
+ }
333
+
334
+ .phone-menu {
335
+ position: absolute;
336
+ top: calc(100% + 4px);
337
+ left: 0;
338
+ z-index: 1050;
339
+ width: min(20rem, 90vw);
340
+ background: var(--color-surface, #fff);
341
+ border: 1px solid var(--color-border, #d0d5dd);
342
+ border-radius: var(--radius-md, 0.5rem);
343
+ box-shadow: var(--shadow-lg, 0 8px 24px rgba(16, 24, 40, 0.12));
344
+ overflow: hidden;
345
+ }
346
+
347
+ .phone-search {
348
+ display: flex;
349
+ align-items: center;
350
+ gap: 0.5rem;
351
+ padding: 0.5rem 0.75rem;
352
+ border-bottom: 1px solid var(--color-border, #eaecf0);
353
+ color: var(--color-text-muted, #667085);
354
+ }
355
+
356
+ .phone-search input {
357
+ flex: 1 1 auto;
358
+ border: 0;
359
+ outline: none;
360
+ font-size: var(--font-size-sm, 0.875rem);
361
+ background: transparent;
362
+ color: var(--color-text, #1d2939);
363
+ }
364
+
365
+ .phone-options {
366
+ list-style: none;
367
+ margin: 0;
368
+ padding: 0;
369
+ max-height: 16rem;
370
+ overflow-y: auto;
371
+ }
372
+
373
+ .phone-option {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 0.625rem;
377
+ width: 100%;
378
+ padding: 0.5rem 0.75rem;
379
+ border: 0;
380
+ background: transparent;
381
+ text-align: left;
382
+ cursor: pointer;
383
+ font-size: var(--font-size-sm, 0.875rem);
384
+ color: var(--color-text, #1d2939);
385
+ }
386
+
387
+ .phone-option:hover,
388
+ .phone-option.selected {
389
+ background: var(--color-primary-soft, #f4eefe);
390
+ }
391
+
392
+ .phone-option-name {
393
+ flex: 1 1 auto;
394
+ }
395
+
396
+ .phone-option-dial {
397
+ color: var(--color-text-muted, #667085);
398
+ }
399
+
400
+ .phone-empty {
401
+ padding: 0.75rem;
402
+ text-align: center;
403
+ color: var(--color-text-muted, #667085);
404
+ font-size: var(--font-size-sm, 0.875rem);
405
+ }
406
+ </style>
@@ -0,0 +1,31 @@
1
+ import "../styles/forms.css";
2
+ import { type CountryCode } from "@200systems/shared";
3
+ interface Props {
4
+ /** ID único do campo */
5
+ id: string;
6
+ /** Label do campo */
7
+ label: string;
8
+ /** Valor em E.164 (bindable), ex.: "+351912345678" */
9
+ value?: string;
10
+ /** Placeholder do número nacional */
11
+ placeholder?: string;
12
+ /** Campo obrigatório */
13
+ required?: boolean;
14
+ /** Campo desabilitado */
15
+ disabled?: boolean;
16
+ /** Mensagem de erro */
17
+ error?: string;
18
+ /** Texto de ajuda */
19
+ helpText?: string;
20
+ /** País assumido por omissão */
21
+ defaultCountry?: CountryCode;
22
+ /** Callback de input (recebe o valor E.164) */
23
+ oninput?: (value: string) => void;
24
+ /** Callback de blur */
25
+ onblur?: () => void;
26
+ /** Classes CSS adicionais */
27
+ class?: string;
28
+ }
29
+ declare const PhoneInput: import("svelte").Component<Props, {}, "value">;
30
+ type PhoneInput = ReturnType<typeof PhoneInput>;
31
+ export default PhoneInput;