@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
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @groundbrick/svelte-ui
2
+
3
+ Reusable **Svelte 5** UI components (design system). Themeable via CSS variables,
4
+ extensible via props, `class` passthrough and snippets.
5
+
6
+ These components were extracted to be reused across projects. They have sensible
7
+ default styling and behavior, but every visual aspect can be re-themed by
8
+ overriding CSS custom properties.
9
+
10
+ ## Install (workspace)
11
+
12
+ ```jsonc
13
+ // consumer app package.json
14
+ {
15
+ "dependencies": {
16
+ "@groundbrick/svelte-ui": "workspace:*"
17
+ }
18
+ }
19
+ ```
20
+
21
+ Build the package before the consuming app (the monorepo build runs packages
22
+ first):
23
+
24
+ ```bash
25
+ pnpm -F @groundbrick/svelte-ui build
26
+ ```
27
+
28
+ ## Peer dependencies
29
+
30
+ | Dependency | Required | Notes |
31
+ | ---------------- | -------- | ------------------------------------------------------------------ |
32
+ | `svelte` | yes | `^5.25.0` (runes). |
33
+ | `@200systems/shared` | yes | Used by `PhoneInput` (phone parsing/formatting utilities). |
34
+ | `bootstrap` | optional | Components use Bootstrap 5 classes (`.row`, `.dropdown`, etc.). Load its CSS/JS in the app. |
35
+ | `bootstrap-icons`| optional | Some built-in icons use `bi bi-*` classes. |
36
+
37
+ The consuming app is responsible for loading Bootstrap and Bootstrap Icons (as
38
+ the AgendaPet app already does in its root layout).
39
+
40
+ ## Theming
41
+
42
+ Import the default tokens once (e.g. in your root layout), then override any CSS
43
+ variable to re-theme. Components reference tokens exclusively, so changing one
44
+ token re-skins every component that uses it.
45
+
46
+ ```svelte
47
+ <script>
48
+ import '@groundbrick/svelte-ui/styles/tokens.css';
49
+ </script>
50
+
51
+ <!-- Re-theme a subtree (e.g. per-tenant branding) by overriding variables -->
52
+ <div style="--color-primary: #0aa; --radius-full: 8px;">
53
+ <Button>Themed button</Button>
54
+ </div>
55
+ ```
56
+
57
+ You can also skip the bundled tokens entirely and define your own
58
+ `--color-primary`, `--spacing-*`, `--radius-*`, `--shadow-*`, etc.
59
+
60
+ ## Usage
61
+
62
+ ```svelte
63
+ <script lang="ts">
64
+ import { Button, Card, CardBody, Alert, Badge, EmptyState } from '@groundbrick/svelte-ui';
65
+ import '@groundbrick/svelte-ui/styles/tokens.css';
66
+
67
+ let saving = $state(false);
68
+ </script>
69
+
70
+ <Card variant="elevated">
71
+ <CardBody>
72
+ <Alert variant="info">Welcome back!</Alert>
73
+ <Badge variant="success">Active</Badge>
74
+ <Button variant="primary" loading={saving} onclick={() => (saving = true)}>
75
+ Save
76
+ </Button>
77
+ </CardBody>
78
+ </Card>
79
+ ```
80
+
81
+ ## Components
82
+
83
+ | Component | Purpose |
84
+ | ------------------------------------------ | --------------------------------------------------- |
85
+ | `Button` | Button/link with variants, sizes and loading state. |
86
+ | `Dropdown`, `DropdownItem` | Bootstrap-based dropdown menu. |
87
+ | `Card`, `CardHeader`, `CardBody`, `CardFooter` | Card primitives. |
88
+ | `FormField` | Labelled input/textarea/select field. |
89
+ | `PhoneInput` | E.164 phone input with country selector. |
90
+ | `DurationInput` | Hours + minutes duration input. |
91
+ | `AutocompleteInput` | Async/debounced autocomplete field. |
92
+ | `Badge` | Pill/solid badges with variants. |
93
+ | `EmptyState` | Empty-state placeholder with illustration. |
94
+ | `LoadingSpinner` | Centered/full-screen spinner. |
95
+ | `Container`, `Row`, `Col` | Lightweight flex layout helpers. |
96
+ | `Modal`, `Drawer` | Overlays (with body scroll lock). |
97
+ | `GridView` | Responsive data grid: search, sort, pagination, drag-reorder, row actions. |
98
+ | `EditableTable` | Inline-editable table with add/remove rows. |
99
+ | `PhotoUpload` | Headless photo uploader (upload/remove/compress via callbacks). |
100
+ | `PawPrintIcon` | Inline SVG paw icon. |
101
+
102
+ ### Dependency-injection props (decoupled components)
103
+
104
+ Some components delegate app-specific concerns through props instead of importing
105
+ app code:
106
+
107
+ - **`GridView`**: `resolveImageUrl(value, folder)` (image cells), `formatDate(value)`
108
+ (date cells, defaults to pt-PT), `hasPermission(code)` (row-action gating,
109
+ defaults to allow-all).
110
+ - **`PhotoUpload`** (headless): `onUpload(file, { folder })` returns the uploaded
111
+ URL, `onRemove()` deletes, optional `compress(file, opts)`, plus
112
+ `onError`/`onFileSelected`/`onUploaded`/`onRemoved` notifications.
113
+
114
+ ### Utilities
115
+
116
+ - `lockBodyScroll()`, `unlockBodyScroll()` - reference-counted body scroll lock.
117
+ - `minutesToHoursAndMinutes()`, `hoursAndMinutesToMinutes()` - duration helpers.
118
+
119
+ ## Notes
120
+
121
+ - Components are decoupled from any app: permission/feature gating present in the
122
+ original AgendaPet `Button`/`DropdownItem` was intentionally removed here, and
123
+ app utilities used by `GridView`/`PhotoUpload` are injected via props.
124
+ - `GridView` keeps the original Svelte 4 (`export let`) syntax; Svelte 5 runs it
125
+ in legacy mode. All other components use runes.
@@ -0,0 +1,335 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+
4
+ interface AlertProps {
5
+ /** Variante semântica do alerta */
6
+ variant?: 'success' | 'info' | 'warning' | 'danger' | 'primary' | 'secondary';
7
+ /** Título do alerta (opcional) */
8
+ title?: string;
9
+ /** Permite fechar o alerta */
10
+ dismissible?: boolean;
11
+ /** Ícone customizado (Bootstrap Icons class) */
12
+ icon?: string;
13
+ /** Mostrar inline (sem margem inferior) */
14
+ inline?: boolean;
15
+ /** Estilo compacto (pill shape, sem título) */
16
+ compact?: boolean;
17
+ /** Classes CSS adicionais */
18
+ class?: string;
19
+ /** Callback ao fechar */
20
+ ondismiss?: () => void;
21
+ /** Conteúdo do alerta */
22
+ children?: Snippet;
23
+ }
24
+
25
+ let {
26
+ variant = 'info',
27
+ title,
28
+ dismissible = false,
29
+ icon,
30
+ inline = false,
31
+ compact = false,
32
+ class: additionalClasses = '',
33
+ ondismiss,
34
+ children
35
+ }: AlertProps = $props();
36
+
37
+ let visible = $state(true);
38
+
39
+ // Ícones padrão por variante
40
+ const defaultIcons: Record<string, string> = {
41
+ success: 'bi-check-circle-fill',
42
+ info: 'bi-info-circle-fill',
43
+ warning: 'bi-exclamation-triangle-fill',
44
+ danger: 'bi-x-circle-fill',
45
+ primary: 'bi-bell-fill',
46
+ secondary: 'bi-chat-fill'
47
+ };
48
+
49
+ const alertIcon = $derived(icon ?? defaultIcons[variant]);
50
+
51
+ function handleDismiss() {
52
+ visible = false;
53
+ ondismiss?.();
54
+ }
55
+ </script>
56
+
57
+ {#if visible}
58
+ {#if compact}
59
+ <!-- Compact/Inline Alert (pill shape) -->
60
+ <div
61
+ class="ap-alert-compact ap-alert-compact-{variant} {additionalClasses}"
62
+ class:ap-alert-inline={inline}
63
+ role="alert"
64
+ >
65
+ {#if alertIcon}
66
+ <i class="{alertIcon} ap-alert-compact-icon"></i>
67
+ {/if}
68
+ {#if children}
69
+ {@render children()}
70
+ {/if}
71
+ </div>
72
+ {:else}
73
+ <!-- Standard Alert with icon badge -->
74
+ <div
75
+ class="ap-alert ap-alert-{variant} {additionalClasses}"
76
+ class:ap-alert-inline={inline}
77
+ role="alert"
78
+ >
79
+ <div class="ap-alert-icon">
80
+ <i class={alertIcon}></i>
81
+ </div>
82
+ <div class="ap-alert-content">
83
+ {#if title}
84
+ <div class="ap-alert-title">{title}</div>
85
+ {/if}
86
+ {#if children}
87
+ <div class="ap-alert-message">
88
+ {@render children()}
89
+ </div>
90
+ {/if}
91
+ </div>
92
+ {#if dismissible}
93
+ <button
94
+ type="button"
95
+ class="ap-alert-close"
96
+ onclick={handleDismiss}
97
+ aria-label="Fechar"
98
+ >
99
+ <i class="bi-x-lg"></i>
100
+ </button>
101
+ {/if}
102
+ </div>
103
+ {/if}
104
+ {/if}
105
+
106
+ <style>
107
+ /* ============================================
108
+ ALERT - AgendaPet Design System
109
+ ============================================ */
110
+
111
+ /* Standard Alert */
112
+ .ap-alert {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 0.875rem;
116
+ padding: 1rem 1.25rem;
117
+ border-radius: var(--radius-lg);
118
+ margin-bottom: var(--spacing-md);
119
+ border: 1px solid;
120
+ position: relative;
121
+ }
122
+
123
+ .ap-alert-inline {
124
+ margin-bottom: 0;
125
+ }
126
+
127
+ /* Icon Badge */
128
+ .ap-alert-icon {
129
+ flex-shrink: 0;
130
+ width: 32px;
131
+ height: 32px;
132
+ border-radius: 8px;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ font-size: 1rem;
137
+ }
138
+
139
+ /* Content */
140
+ .ap-alert-content {
141
+ flex: 1;
142
+ min-width: 0;
143
+ }
144
+
145
+ .ap-alert-title {
146
+ font-size: var(--font-size-sm);
147
+ font-weight: var(--font-weight-medium);
148
+ margin-bottom: 0.25rem;
149
+ line-height: 1.4;
150
+ }
151
+
152
+ .ap-alert-message {
153
+ font-size: var(--font-size-xs);
154
+ line-height: 1.4;
155
+ opacity: 0.85;
156
+ }
157
+
158
+ .ap-alert-message :global(p:last-child) {
159
+ margin-bottom: 0;
160
+ }
161
+
162
+ /* Close Button */
163
+ .ap-alert-close {
164
+ flex-shrink: 0;
165
+ background: transparent;
166
+ border: none;
167
+ padding: 0.25rem;
168
+ cursor: pointer;
169
+ opacity: 0.5;
170
+ transition: opacity var(--transition-fast);
171
+ border-radius: var(--radius-sm);
172
+ line-height: 1;
173
+ }
174
+
175
+ .ap-alert-close:hover {
176
+ opacity: 1;
177
+ }
178
+
179
+ .ap-alert-close:focus-visible {
180
+ outline: 2px solid currentColor;
181
+ outline-offset: 2px;
182
+ }
183
+
184
+ /* ===== STANDARD VARIANTS ===== */
185
+
186
+ /* Success */
187
+ .ap-alert-success {
188
+ background-color: var(--color-success-bg);
189
+ border-color: var(--color-success-border) !important;
190
+ color: var(--color-success-text);
191
+ }
192
+
193
+ .ap-alert-success .ap-alert-icon {
194
+ background-color: rgba(24, 183, 126, 0.15);
195
+ color: var(--color-success);
196
+ }
197
+
198
+ /* Info */
199
+ .ap-alert-info {
200
+ background-color: var(--color-info-bg);
201
+ border-color: var(--color-info-border) !important;
202
+ color: var(--color-info-text);
203
+ }
204
+
205
+ .ap-alert-info .ap-alert-icon {
206
+ background-color: rgba(24, 167, 214, 0.15);
207
+ color: var(--color-info);
208
+ }
209
+
210
+ /* Warning */
211
+ .ap-alert-warning {
212
+ background-color: var(--color-warning-bg);
213
+ border-color: var(--color-warning-border) !important;
214
+ color: var(--color-warning-text);
215
+ }
216
+
217
+ .ap-alert-warning .ap-alert-icon {
218
+ background-color: rgba(242, 183, 5, 0.15);
219
+ color: #D99E00;
220
+ }
221
+
222
+ /* Danger */
223
+ .ap-alert-danger {
224
+ background-color: var(--color-danger-bg);
225
+ border-color: var(--color-danger-border) !important;
226
+ color: var(--color-danger-text);
227
+ }
228
+
229
+ .ap-alert-danger .ap-alert-icon {
230
+ background-color: rgba(228, 71, 90, 0.15);
231
+ color: var(--color-danger);
232
+ }
233
+
234
+ /* Primary */
235
+ .ap-alert-primary {
236
+ background-color: var(--color-primary-light);
237
+ border-color: var(--color-primary) !important;
238
+ color: var(--color-primary-dark);
239
+ }
240
+
241
+ .ap-alert-primary .ap-alert-icon {
242
+ background-color: rgba(123, 63, 242, 0.15);
243
+ color: var(--color-primary);
244
+ }
245
+
246
+ /* Secondary */
247
+ .ap-alert-secondary {
248
+ background-color: var(--color-gray-100);
249
+ border-color: var(--color-gray-300) !important;
250
+ color: var(--color-gray-700);
251
+ }
252
+
253
+ .ap-alert-secondary .ap-alert-icon {
254
+ background-color: rgba(108, 117, 125, 0.15);
255
+ color: var(--color-secondary);
256
+ }
257
+
258
+ /* ============================================
259
+ COMPACT ALERT (Pill shape)
260
+ ============================================ */
261
+ .ap-alert-compact {
262
+ display: inline-flex;
263
+ align-items: center;
264
+ gap: 0.5rem;
265
+ padding: 0.5rem 0.875rem;
266
+ border-radius: var(--radius-full);
267
+ font-size: var(--font-size-xs);
268
+ font-weight: var(--font-weight-medium);
269
+ margin-bottom: var(--spacing-sm);
270
+ }
271
+
272
+ .ap-alert-compact.ap-alert-inline {
273
+ margin-bottom: 0;
274
+ }
275
+
276
+ .ap-alert-compact-icon {
277
+ flex-shrink: 0;
278
+ font-size: 0.875rem;
279
+ }
280
+
281
+ /* Compact Variants */
282
+ .ap-alert-compact-success {
283
+ background-color: var(--color-success-bg);
284
+ color: var(--color-success-text);
285
+ }
286
+
287
+ .ap-alert-compact-success .ap-alert-compact-icon {
288
+ color: var(--color-success);
289
+ }
290
+
291
+ .ap-alert-compact-info {
292
+ background-color: var(--color-info-bg);
293
+ color: var(--color-info-text);
294
+ }
295
+
296
+ .ap-alert-compact-info .ap-alert-compact-icon {
297
+ color: var(--color-info);
298
+ }
299
+
300
+ .ap-alert-compact-warning {
301
+ background-color: var(--color-warning-bg);
302
+ color: var(--color-warning-text);
303
+ }
304
+
305
+ .ap-alert-compact-warning .ap-alert-compact-icon {
306
+ color: #D99E00;
307
+ }
308
+
309
+ .ap-alert-compact-danger {
310
+ background-color: var(--color-danger-bg);
311
+ color: var(--color-danger-text);
312
+ }
313
+
314
+ .ap-alert-compact-danger .ap-alert-compact-icon {
315
+ color: var(--color-danger);
316
+ }
317
+
318
+ .ap-alert-compact-primary {
319
+ background-color: var(--color-primary-light);
320
+ color: var(--color-primary-dark);
321
+ }
322
+
323
+ .ap-alert-compact-primary .ap-alert-compact-icon {
324
+ color: var(--color-primary);
325
+ }
326
+
327
+ .ap-alert-compact-secondary {
328
+ background-color: var(--color-gray-100);
329
+ color: var(--color-gray-700);
330
+ }
331
+
332
+ .ap-alert-compact-secondary .ap-alert-compact-icon {
333
+ color: var(--color-secondary);
334
+ }
335
+ </style>
@@ -0,0 +1,24 @@
1
+ import type { Snippet } from "svelte";
2
+ interface AlertProps {
3
+ /** Variante semântica do alerta */
4
+ variant?: 'success' | 'info' | 'warning' | 'danger' | 'primary' | 'secondary';
5
+ /** Título do alerta (opcional) */
6
+ title?: string;
7
+ /** Permite fechar o alerta */
8
+ dismissible?: boolean;
9
+ /** Ícone customizado (Bootstrap Icons class) */
10
+ icon?: string;
11
+ /** Mostrar inline (sem margem inferior) */
12
+ inline?: boolean;
13
+ /** Estilo compacto (pill shape, sem título) */
14
+ compact?: boolean;
15
+ /** Classes CSS adicionais */
16
+ class?: string;
17
+ /** Callback ao fechar */
18
+ ondismiss?: () => void;
19
+ /** Conteúdo do alerta */
20
+ children?: Snippet;
21
+ }
22
+ declare const Alert: import("svelte").Component<AlertProps, {}, "">;
23
+ type Alert = ReturnType<typeof Alert>;
24
+ export default Alert;