polymorph-ui-components 0.4.0 → 0.6.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.
@@ -22,6 +22,12 @@
22
22
  classes
23
23
  }: ButtonProperties = $props();
24
24
 
25
+ export function getButtonRef(): HTMLButtonElement | null {
26
+ return buttonElement;
27
+ }
28
+
29
+ let buttonElement: HTMLButtonElement | null = $state(null);
30
+
25
31
  let isDisabled = $derived(!enable || disabled || showLoader);
26
32
 
27
33
  function handleButtonClick(event: MouseEvent): void {
@@ -40,6 +46,7 @@
40
46
  <div class="button-progress-bar"></div>
41
47
  {/if}
42
48
  <button
49
+ bind:this={buttonElement}
43
50
  class:disabled={isDisabled}
44
51
  onclick={handleButtonClick}
45
52
  {onkeyup}
@@ -95,6 +102,8 @@
95
102
  gap: var(--button-content-gap, 16px);
96
103
  visibility: var(--button-visibility, visible);
97
104
  box-shadow: var(--button-box-shadow, none);
105
+ text-decoration: var(--button-text-decoration, none);
106
+ line-height: var(--button-line-height, normal);
98
107
  }
99
108
 
100
109
  .disabled {
@@ -105,6 +114,7 @@
105
114
  font-weight: var(--button-disabled-font-weight);
106
115
  border: var(--button-disabled-border);
107
116
  background: var(--button-disabled-background-color, var(--button-color, #18181b));
117
+ text-decoration: var(--button-disabled-text-decoration, var(--button-text-decoration, none));
108
118
  }
109
119
 
110
120
  .button-loader {
@@ -126,6 +136,7 @@
126
136
  color: var(--button-hover-text-color, var(--button-text-color, #ffffff));
127
137
  border: var(--button-hover-border, var(--button-border, none));
128
138
  transform: var(--button-hover-transform);
139
+ opacity: var(--button-hover-opacity, var(--button-opacity, 1));
129
140
  }
130
141
 
131
142
  button:active {
@@ -1,4 +1,6 @@
1
1
  import type { ButtonProperties } from './properties';
2
- declare const Button: import("svelte").Component<ButtonProperties, {}, "showProgressBar">;
2
+ declare const Button: import("svelte").Component<ButtonProperties, {
3
+ getButtonRef: () => HTMLButtonElement | null;
4
+ }, "showProgressBar">;
3
5
  type Button = ReturnType<typeof Button>;
4
6
  export default Button;
@@ -1,10 +1,12 @@
1
1
  <script lang="ts">
2
2
  import type { ChoiceboxProperties } from './properties';
3
+ import checkmarkSvg from '../assets/checkmark.svg?raw';
3
4
 
4
5
  let {
5
6
  children,
6
7
  selected = $bindable(false),
7
8
  mode = 'radio',
9
+ showIndicator = false,
8
10
  disabled = false,
9
11
  testId,
10
12
  onclick,
@@ -43,8 +45,18 @@
43
45
  onkeydown={handleKeyDown}
44
46
  data-pw={testId}
45
47
  >
46
- {#if typeof children === 'function'}
47
- {@render children()}
48
+ <div class="body">
49
+ {#if typeof children === 'function'}
50
+ {@render children()}
51
+ {/if}
52
+ </div>
53
+ {#if showIndicator}
54
+ <span class="indicator {mode}" class:selected aria-hidden="true">
55
+ {#if mode === 'checkbox' && selected}
56
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
57
+ <span class="indicator-icon">{@html checkmarkSvg}</span>
58
+ {/if}
59
+ </span>
48
60
  {/if}
49
61
  </div>
50
62
 
@@ -63,6 +75,14 @@
63
75
  -webkit-tap-highlight-color: transparent;
64
76
  }
65
77
 
78
+ .body {
79
+ flex: var(--choicebox-body-flex, 1);
80
+ min-width: var(--choicebox-body-min-width, 0);
81
+ display: var(--choicebox-body-display, flex);
82
+ align-items: var(--choicebox-body-align-items, var(--choicebox-align-items, center));
83
+ gap: var(--choicebox-body-gap, var(--choicebox-gap, 12px));
84
+ }
85
+
66
86
  .choicebox:focus-visible {
67
87
  outline: none;
68
88
  box-shadow: var(--choicebox-focus-ring, 0 0 0 3px currentColor);
@@ -82,4 +102,46 @@
82
102
  opacity: var(--choicebox-disabled-opacity, 0.4);
83
103
  cursor: var(--choicebox-disabled-cursor, not-allowed);
84
104
  }
105
+
106
+ .indicator {
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ box-sizing: border-box;
111
+ width: var(--choicebox-indicator-size, 20px);
112
+ height: var(--choicebox-indicator-size, 20px);
113
+ border: var(--choicebox-indicator-border, 2px solid #757575);
114
+ background: var(--choicebox-indicator-background, transparent);
115
+ transition: var(--choicebox-indicator-transition, background 0.2s, border-color 0.2s);
116
+ }
117
+
118
+ .indicator.radio {
119
+ border-radius: 50%;
120
+ }
121
+
122
+ .indicator.checkbox {
123
+ border-radius: var(--choicebox-indicator-border-radius, var(--radius, 4px));
124
+ }
125
+
126
+ .indicator.selected {
127
+ border: var(--choicebox-indicator-selected-border, 2px solid #2196f3);
128
+ background: var(--choicebox-indicator-selected-background, #2196f3);
129
+ }
130
+
131
+ .indicator.radio.selected {
132
+ box-shadow: inset 0 0 0 var(--choicebox-indicator-dot-inset, 4px)
133
+ var(--choicebox-indicator-dot-color, #ffffff);
134
+ }
135
+
136
+ .indicator-icon {
137
+ display: flex;
138
+ width: var(--choicebox-indicator-icon-size, 14px);
139
+ height: var(--choicebox-indicator-icon-size, 14px);
140
+ color: var(--choicebox-indicator-icon-color, #ffffff);
141
+ }
142
+
143
+ .indicator-icon :global(svg) {
144
+ width: 100%;
145
+ height: 100%;
146
+ }
85
147
  </style>
@@ -6,6 +6,7 @@ export type OptionalChoiceboxProperties = {
6
6
  selected?: boolean;
7
7
  mode?: ChoiceboxMode;
8
8
  disabled?: boolean;
9
+ showIndicator?: boolean;
9
10
  testId?: string;
10
11
  classes?: string;
11
12
  };
@@ -0,0 +1,549 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import type { Snippet } from 'svelte';
4
+ import { fade } from 'svelte/transition';
5
+ import type { GalleryImage, GalleryProperties } from './properties';
6
+ import Img from '../Img/Img.svelte';
7
+ import Button from '../Button/Button.svelte';
8
+ import Icon from '../Icon/Icon.svelte';
9
+ import closeSvg from '../assets/close.svg?raw';
10
+ import chevronLeftSvg from '../assets/chevron-left-lg.svg?raw';
11
+ import chevronRightSvg from '../assets/chevron-right-lg.svg?raw';
12
+ import editSvg from '../assets/edit.svg?raw';
13
+ import deleteSvg from '../assets/delete.svg?raw';
14
+
15
+ let {
16
+ images,
17
+ view = 'grid',
18
+ open = $bindable(false),
19
+ activeIndex = $bindable(0),
20
+ enableLightbox = true,
21
+ loop = false,
22
+ showCounter = true,
23
+ showCaption = true,
24
+ previousIcon,
25
+ nextIcon,
26
+ closeIcon,
27
+ editIcon,
28
+ deleteIcon,
29
+ testId,
30
+ onimageclick,
31
+ oneditclick,
32
+ ondeleteclick,
33
+ onopen,
34
+ onclose,
35
+ onchange,
36
+ onkeydown,
37
+ classes
38
+ }: GalleryProperties = $props();
39
+
40
+ let lightboxDiv: HTMLDivElement | null = $state(null);
41
+ let closeButtonRef: ReturnType<typeof Button> | null = $state(null);
42
+ let previousButtonRef: ReturnType<typeof Button> | null = $state(null);
43
+ let nextButtonRef: ReturnType<typeof Button> | null = $state(null);
44
+ let openerElement: HTMLElement | null = null;
45
+
46
+ let activeImage = $derived(images.at(activeIndex));
47
+ let hasPrevious = $derived(loop ? images.length > 1 : activeIndex > 0);
48
+ let hasNext = $derived(loop ? images.length > 1 : activeIndex < images.length - 1);
49
+ let itemsAreInteractive = $derived(enableLightbox || typeof onimageclick === 'function');
50
+ let showEditButton = $derived(typeof oneditclick === 'function');
51
+ let showDeleteButton = $derived(typeof ondeleteclick === 'function');
52
+ let showItemActions = $derived(showEditButton || showDeleteButton);
53
+
54
+ function lightboxAction(node: HTMLElement) {
55
+ if (openerElement === null && document.activeElement instanceof HTMLElement) {
56
+ openerElement = document.activeElement;
57
+ }
58
+ document.body.style.overflow = 'hidden';
59
+ tick().then(() => {
60
+ node.focus();
61
+ });
62
+ return {
63
+ destroy() {
64
+ document.body.style.overflow = '';
65
+ if (openerElement !== null) {
66
+ openerElement.focus();
67
+ openerElement = null;
68
+ }
69
+ }
70
+ };
71
+ }
72
+
73
+ function openLightbox(index: number, opener: EventTarget | null): void {
74
+ if (opener instanceof HTMLElement) {
75
+ openerElement = opener;
76
+ }
77
+ activeIndex = index;
78
+ open = true;
79
+ onopen?.(index);
80
+ }
81
+
82
+ function closeLightbox(): void {
83
+ open = false;
84
+ onclose?.();
85
+ }
86
+
87
+ async function goToIndex(index: number): Promise<void> {
88
+ if (index < 0 || index >= images.length) {
89
+ return;
90
+ }
91
+ activeIndex = index;
92
+ onchange?.(activeIndex);
93
+ await tick();
94
+ if (lightboxDiv !== null && !lightboxDiv.contains(document.activeElement)) {
95
+ lightboxDiv.focus();
96
+ }
97
+ }
98
+
99
+ function showPrevious(): void {
100
+ if (activeIndex > 0) {
101
+ goToIndex(activeIndex - 1);
102
+ } else if (loop && images.length > 1) {
103
+ goToIndex(images.length - 1);
104
+ }
105
+ }
106
+
107
+ function showNext(): void {
108
+ if (activeIndex < images.length - 1) {
109
+ goToIndex(activeIndex + 1);
110
+ } else if (loop && images.length > 1) {
111
+ goToIndex(0);
112
+ }
113
+ }
114
+
115
+ function handleImageClick(index: number, event: MouseEvent): void {
116
+ onimageclick?.(index, event);
117
+ if (enableLightbox) {
118
+ openLightbox(index, event.currentTarget);
119
+ }
120
+ }
121
+
122
+ function handleEditClick(index: number, event: MouseEvent): void {
123
+ oneditclick?.(index, event);
124
+ }
125
+
126
+ function handleDeleteClick(index: number, event: MouseEvent): void {
127
+ ondeleteclick?.(index, event);
128
+ }
129
+
130
+ function trapFocus(event: KeyboardEvent): void {
131
+ const first = closeButtonRef?.getButtonRef() ?? null;
132
+ const last = (nextButtonRef ?? previousButtonRef ?? closeButtonRef)?.getButtonRef() ?? null;
133
+ if (first === null || last === null) {
134
+ return;
135
+ }
136
+ if (
137
+ event.shiftKey &&
138
+ (document.activeElement === first || document.activeElement === lightboxDiv)
139
+ ) {
140
+ event.preventDefault();
141
+ last.focus();
142
+ } else if (!event.shiftKey && document.activeElement === last) {
143
+ event.preventDefault();
144
+ first.focus();
145
+ }
146
+ }
147
+
148
+ function handleLightboxKeydown(event: KeyboardEvent): void {
149
+ onkeydown?.(event);
150
+ if (event.key === 'Escape') {
151
+ closeLightbox();
152
+ } else if (event.key === 'ArrowLeft') {
153
+ event.preventDefault();
154
+ showPrevious();
155
+ } else if (event.key === 'ArrowRight') {
156
+ event.preventDefault();
157
+ showNext();
158
+ } else if (event.key === 'Home') {
159
+ event.preventDefault();
160
+ goToIndex(0);
161
+ } else if (event.key === 'End') {
162
+ event.preventDefault();
163
+ goToIndex(images.length - 1);
164
+ } else if (event.key === 'Tab') {
165
+ trapFocus(event);
166
+ }
167
+ }
168
+
169
+ function handleBackdropClick(event: MouseEvent): void {
170
+ if (event.target === lightboxDiv) {
171
+ closeLightbox();
172
+ }
173
+ }
174
+ </script>
175
+
176
+ {#snippet controlIcon(fallbackMarkup: string, custom?: Snippet)}
177
+ {#if typeof custom === 'function'}
178
+ {@render custom()}
179
+ {:else}
180
+ <Icon svg={fallbackMarkup} />
181
+ {/if}
182
+ {/snippet}
183
+
184
+ {#snippet itemContent(image: GalleryImage)}
185
+ <Img src={image.thumbnail ?? image.src} alt={image.alt} fallback={image.fallback} />
186
+ {#if view === 'list'}
187
+ <span class="list-text">
188
+ <span class="list-title">{image.alt}</span>
189
+ {#if typeof image.caption === 'string' && image.caption.length > 0}
190
+ <span class="list-caption">{image.caption}</span>
191
+ {/if}
192
+ </span>
193
+ {/if}
194
+ {/snippet}
195
+
196
+ <div class="gallery {view} {classes ?? ''}" data-pw={testId} role="list">
197
+ {#each images as image, index (index)}
198
+ <div class="gallery-item" role="listitem">
199
+ {#if itemsAreInteractive}
200
+ <button
201
+ class="gallery-item-content"
202
+ onclick={(event) => handleImageClick(index, event)}
203
+ aria-label={enableLightbox
204
+ ? `View image ${index + 1} of ${images.length}: ${image.alt}`
205
+ : image.alt}
206
+ >
207
+ {@render itemContent(image)}
208
+ </button>
209
+ {:else}
210
+ <div class="gallery-item-content">
211
+ {@render itemContent(image)}
212
+ </div>
213
+ {/if}
214
+ {#if showItemActions}
215
+ <div class="gallery-item-actions">
216
+ {#if showEditButton}
217
+ <div class="gallery-item-action">
218
+ <Button
219
+ ariaLabel={`Edit image ${index + 1}: ${image.alt}`}
220
+ onclick={(event) => handleEditClick(index, event)}
221
+ >
222
+ {@render controlIcon(editSvg, editIcon)}
223
+ </Button>
224
+ </div>
225
+ {/if}
226
+ {#if showDeleteButton}
227
+ <div class="gallery-item-action">
228
+ <Button
229
+ ariaLabel={`Delete image ${index + 1}: ${image.alt}`}
230
+ onclick={(event) => handleDeleteClick(index, event)}
231
+ >
232
+ {@render controlIcon(deleteSvg, deleteIcon)}
233
+ </Button>
234
+ </div>
235
+ {/if}
236
+ </div>
237
+ {/if}
238
+ </div>
239
+ {/each}
240
+ </div>
241
+
242
+ {#if open}
243
+ <div
244
+ class="lightbox {classes ?? ''}"
245
+ role="dialog"
246
+ aria-modal="true"
247
+ aria-label={typeof activeImage === 'object' ? activeImage.alt : 'Image gallery'}
248
+ tabindex="-1"
249
+ bind:this={lightboxDiv}
250
+ use:lightboxAction
251
+ onclick={handleBackdropClick}
252
+ onkeydown={handleLightboxKeydown}
253
+ transition:fade={{ duration: 200 }}
254
+ >
255
+ <div class="lightbox-close">
256
+ <Button ariaLabel="Close gallery" onclick={closeLightbox} bind:this={closeButtonRef}>
257
+ {@render controlIcon(closeSvg, closeIcon)}
258
+ </Button>
259
+ </div>
260
+ {#if hasPrevious}
261
+ <div class="lightbox-previous">
262
+ <Button ariaLabel="Previous image" onclick={showPrevious} bind:this={previousButtonRef}>
263
+ {@render controlIcon(chevronLeftSvg, previousIcon)}
264
+ </Button>
265
+ </div>
266
+ {/if}
267
+ {#if typeof activeImage === 'object'}
268
+ <figure class="lightbox-figure">
269
+ <Img src={activeImage.src} alt={activeImage.alt} fallback={activeImage.fallback} />
270
+ {#if showCaption && typeof activeImage.caption === 'string' && activeImage.caption.length > 0}
271
+ <figcaption class="lightbox-caption">{activeImage.caption}</figcaption>
272
+ {/if}
273
+ </figure>
274
+ {/if}
275
+ {#if hasNext}
276
+ <div class="lightbox-next">
277
+ <Button ariaLabel="Next image" onclick={showNext} bind:this={nextButtonRef}>
278
+ {@render controlIcon(chevronRightSvg, nextIcon)}
279
+ </Button>
280
+ </div>
281
+ {/if}
282
+ {#if showCounter && typeof activeImage === 'object'}
283
+ <div class="lightbox-counter" aria-live="polite">
284
+ {activeIndex + 1} / {images.length}
285
+ </div>
286
+ {/if}
287
+ </div>
288
+ {/if}
289
+
290
+ <style>
291
+ .gallery {
292
+ width: var(--gallery-width, 100%);
293
+ padding: var(--gallery-padding, 0px);
294
+ margin: var(--gallery-margin, 0px);
295
+ background: var(--gallery-background, transparent);
296
+ }
297
+
298
+ .gallery.grid {
299
+ display: grid;
300
+ grid-template-columns: repeat(var(--gallery-columns, 3), 1fr);
301
+ gap: var(--gallery-gap, 8px);
302
+ }
303
+
304
+ .gallery.list {
305
+ display: flex;
306
+ flex-direction: column;
307
+ gap: var(--gallery-gap, 8px);
308
+ }
309
+
310
+ .gallery-item {
311
+ position: relative;
312
+ }
313
+
314
+ .grid .gallery-item {
315
+ aspect-ratio: var(--gallery-item-aspect-ratio, 1);
316
+ border-radius: var(--gallery-item-border-radius, 0px);
317
+ overflow: hidden;
318
+ --image-width: 100%;
319
+ --image-height: 100%;
320
+ --image-object-fit: var(--gallery-item-image-fit, cover);
321
+ --image-border-radius: var(--gallery-item-border-radius, 0px);
322
+ --image-transition: var(--gallery-item-image-transition);
323
+ }
324
+
325
+ .list .gallery-item {
326
+ display: flex;
327
+ align-items: center;
328
+ border-radius: var(--gallery-list-item-border-radius, 0px);
329
+ background: var(--gallery-list-item-background, transparent);
330
+ --image-width: var(--gallery-list-thumbnail-width, 56px);
331
+ --image-height: var(--gallery-list-thumbnail-height, 56px);
332
+ --image-object-fit: var(--gallery-list-thumbnail-fit, cover);
333
+ --image-border-radius: var(--gallery-list-thumbnail-border-radius, 0px);
334
+ --image-transition: var(--gallery-item-image-transition);
335
+ --image-flex-shrink: 0;
336
+ }
337
+
338
+ .gallery-item-content {
339
+ font: inherit;
340
+ color: inherit;
341
+ border: var(--gallery-item-border, none);
342
+ background: transparent;
343
+ }
344
+
345
+ button.gallery-item-content {
346
+ cursor: var(--gallery-item-cursor, pointer);
347
+ transition: var(--gallery-item-transition);
348
+ }
349
+
350
+ .grid .gallery-item-content {
351
+ display: block;
352
+ width: 100%;
353
+ height: 100%;
354
+ padding: 0;
355
+ border-radius: var(--gallery-item-border-radius, 0px);
356
+ }
357
+
358
+ .list .gallery-item-content {
359
+ display: flex;
360
+ align-items: center;
361
+ text-align: left;
362
+ flex: 1;
363
+ min-width: 0;
364
+ gap: var(--gallery-list-item-gap, 12px);
365
+ padding: var(--gallery-list-item-padding, 8px);
366
+ }
367
+
368
+ .grid button.gallery-item-content:hover {
369
+ opacity: var(--gallery-item-hover-opacity, 1);
370
+ transform: var(--gallery-item-hover-transform);
371
+ }
372
+
373
+ .grid button.gallery-item-content:focus-visible {
374
+ outline: var(--gallery-item-focus-outline, 2px solid currentColor);
375
+ outline-offset: var(--gallery-item-focus-outline-offset, 2px);
376
+ }
377
+
378
+ .list button.gallery-item-content:focus-visible {
379
+ outline: none;
380
+ }
381
+
382
+ .list .gallery-item:has(button.gallery-item-content):hover {
383
+ background: var(--gallery-list-item-hover-background, transparent);
384
+ }
385
+
386
+ .list .gallery-item:has(button.gallery-item-content:focus-visible) {
387
+ outline: var(--gallery-item-focus-outline, 2px solid currentColor);
388
+ outline-offset: var(--gallery-item-focus-outline-offset, 2px);
389
+ }
390
+
391
+ .list-text {
392
+ display: flex;
393
+ flex-direction: column;
394
+ gap: var(--gallery-list-text-gap, 2px);
395
+ min-width: 0;
396
+ }
397
+
398
+ .list-title,
399
+ .list-caption {
400
+ overflow: hidden;
401
+ text-overflow: ellipsis;
402
+ white-space: nowrap;
403
+ }
404
+
405
+ .list-title {
406
+ color: var(--gallery-list-title-color, inherit);
407
+ font-size: var(--gallery-list-title-font-size, 14px);
408
+ font-weight: var(--gallery-list-title-font-weight, 500);
409
+ font-family: var(--gallery-list-title-font-family);
410
+ }
411
+
412
+ .list-caption {
413
+ color: var(--gallery-list-caption-color, inherit);
414
+ font-size: var(--gallery-list-caption-font-size, 12px);
415
+ font-family: var(--gallery-list-caption-font-family);
416
+ }
417
+
418
+ .gallery-item-actions {
419
+ display: flex;
420
+ align-items: center;
421
+ gap: var(--gallery-item-actions-gap, 4px);
422
+ }
423
+
424
+ .grid .gallery-item-actions {
425
+ position: absolute;
426
+ top: var(--gallery-item-actions-top, 8px);
427
+ right: var(--gallery-item-actions-right, 8px);
428
+ }
429
+
430
+ .list .gallery-item-actions {
431
+ flex-shrink: 0;
432
+ padding-right: var(--gallery-item-actions-right, 8px);
433
+ }
434
+
435
+ .gallery-item-action {
436
+ border-radius: var(--gallery-item-action-border-radius, 8px);
437
+ --button-color: transparent;
438
+ --button-hover-color: transparent;
439
+ --button-padding: var(--gallery-item-action-padding, 6px);
440
+ --button-border-radius: var(--gallery-item-action-border-radius, 8px);
441
+ --icon-container-padding: 0px;
442
+ --icon-padding: 0px;
443
+ --icon-width: var(--gallery-item-action-icon-size, 16px);
444
+ --icon-height: var(--gallery-item-action-icon-size, 16px);
445
+ }
446
+
447
+ .grid .gallery-item-action {
448
+ background: var(--gallery-item-action-background, #00000066);
449
+ -webkit-backdrop-filter: var(--gallery-item-action-backdrop-filter, blur(8px));
450
+ backdrop-filter: var(--gallery-item-action-backdrop-filter, blur(8px));
451
+ --button-text-color: var(--gallery-item-action-color, #ffffff);
452
+ }
453
+
454
+ .grid .gallery-item-action:hover {
455
+ background: var(--gallery-item-action-hover-background, #00000099);
456
+ }
457
+
458
+ .list .gallery-item-action {
459
+ background: var(--gallery-item-action-background, transparent);
460
+ --button-text-color: var(--gallery-item-action-color, currentColor);
461
+ }
462
+
463
+ .list .gallery-item-action:hover {
464
+ background: var(--gallery-item-action-hover-background, #80808026);
465
+ }
466
+
467
+ .lightbox {
468
+ position: fixed;
469
+ top: 0;
470
+ bottom: 0;
471
+ left: 0;
472
+ right: 0;
473
+ z-index: var(--gallery-lightbox-z-index, 15);
474
+ display: flex;
475
+ justify-content: center;
476
+ align-items: center;
477
+ background: var(--gallery-lightbox-background, #000000e6);
478
+ -webkit-tap-highlight-color: transparent;
479
+ }
480
+
481
+ .lightbox:focus {
482
+ outline: none;
483
+ }
484
+
485
+ .lightbox-figure {
486
+ display: flex;
487
+ flex-direction: column;
488
+ align-items: center;
489
+ gap: var(--gallery-lightbox-caption-gap, 12px);
490
+ margin: 0;
491
+ pointer-events: none;
492
+ --image-width: var(--gallery-lightbox-image-width, 85vw);
493
+ --image-height: var(--gallery-lightbox-image-height, 75vh);
494
+ --image-object-fit: var(--gallery-lightbox-image-fit, contain);
495
+ --image-border-radius: var(--gallery-lightbox-image-border-radius, 0px);
496
+ }
497
+
498
+ .lightbox-caption {
499
+ color: var(--gallery-lightbox-caption-color, #ffffff);
500
+ font-size: var(--gallery-lightbox-caption-font-size, 14px);
501
+ font-family: var(--gallery-lightbox-caption-font-family);
502
+ text-align: center;
503
+ max-width: var(--gallery-lightbox-image-width, 85vw);
504
+ }
505
+
506
+ .lightbox-counter {
507
+ position: absolute;
508
+ bottom: var(--gallery-lightbox-counter-bottom, 16px);
509
+ left: 50%;
510
+ transform: translateX(-50%);
511
+ color: var(--gallery-lightbox-counter-color, #ffffff);
512
+ font-size: var(--gallery-lightbox-counter-font-size, 13px);
513
+ font-family: var(--gallery-lightbox-counter-font-family);
514
+ }
515
+
516
+ .lightbox-close,
517
+ .lightbox-previous,
518
+ .lightbox-next {
519
+ position: absolute;
520
+ --button-color: var(--gallery-lightbox-control-background, transparent);
521
+ --button-text-color: var(--gallery-lightbox-control-color, #ffffff);
522
+ --button-hover-color: var(--gallery-lightbox-control-hover-background, #ffffff1f);
523
+ --button-padding: var(--gallery-lightbox-control-padding, 8px);
524
+ --button-border-radius: var(--gallery-lightbox-control-border-radius, 50%);
525
+ --icon-container-padding: 0px;
526
+ --icon-padding: 0px;
527
+ --icon-width: var(--gallery-lightbox-control-icon-size, 24px);
528
+ --icon-height: var(--gallery-lightbox-control-icon-size, 24px);
529
+ }
530
+
531
+ .lightbox-close {
532
+ top: var(--gallery-lightbox-close-top, 16px);
533
+ right: var(--gallery-lightbox-close-right, 16px);
534
+ }
535
+
536
+ .lightbox-previous,
537
+ .lightbox-next {
538
+ top: 50%;
539
+ transform: translateY(-50%);
540
+ }
541
+
542
+ .lightbox-previous {
543
+ left: var(--gallery-lightbox-nav-inset, 16px);
544
+ }
545
+
546
+ .lightbox-next {
547
+ right: var(--gallery-lightbox-nav-inset, 16px);
548
+ }
549
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { GalleryProperties } from './properties';
2
+ declare const Gallery: import("svelte").Component<GalleryProperties, {}, "activeIndex" | "open">;
3
+ type Gallery = ReturnType<typeof Gallery>;
4
+ export default Gallery;
@@ -0,0 +1,38 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type GalleryView = 'grid' | 'list';
3
+ export type GalleryImage = {
4
+ src: string;
5
+ alt: string;
6
+ thumbnail?: string;
7
+ fallback?: string;
8
+ caption?: string;
9
+ };
10
+ export type GalleryProperties = MandatoryGalleryProperties & OptionalGalleryProperties & GalleryEventProperties;
11
+ export type MandatoryGalleryProperties = {
12
+ images: GalleryImage[];
13
+ };
14
+ export type OptionalGalleryProperties = {
15
+ view?: GalleryView;
16
+ open?: boolean;
17
+ activeIndex?: number;
18
+ enableLightbox?: boolean;
19
+ loop?: boolean;
20
+ showCounter?: boolean;
21
+ showCaption?: boolean;
22
+ previousIcon?: Snippet;
23
+ nextIcon?: Snippet;
24
+ closeIcon?: Snippet;
25
+ editIcon?: Snippet;
26
+ deleteIcon?: Snippet;
27
+ testId?: string;
28
+ classes?: string;
29
+ };
30
+ export type GalleryEventProperties = {
31
+ onimageclick?: (index: number, event: MouseEvent) => void;
32
+ oneditclick?: (index: number, event: MouseEvent) => void;
33
+ ondeleteclick?: (index: number, event: MouseEvent) => void;
34
+ onopen?: (index: number) => void;
35
+ onclose?: () => void;
36
+ onchange?: (index: number) => void;
37
+ onkeydown?: (event: KeyboardEvent) => void;
38
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -2,14 +2,28 @@
2
2
  import type { IconProperties } from './properties';
3
3
 
4
4
  let { icon, svg, text, onclick, onkeydown, testId, classes }: IconProperties = $props();
5
+
6
+ let interactive = $derived(typeof onclick === 'function');
7
+
8
+ function handleKeydown(event: KeyboardEvent): void {
9
+ if (event.key === 'Enter' || event.key === ' ') {
10
+ event.preventDefault();
11
+ if (event.currentTarget instanceof HTMLElement) {
12
+ event.currentTarget.click();
13
+ }
14
+ }
15
+ onkeydown?.(event);
16
+ }
5
17
  </script>
6
18
 
19
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
7
20
  <div
8
21
  class="icon-container {classes ?? ''}"
9
- {onclick}
10
- {onkeydown}
11
- role="button"
12
- tabindex="0"
22
+ class:interactive
23
+ onclick={interactive ? onclick : null}
24
+ onkeydown={interactive ? handleKeydown : onkeydown}
25
+ role={interactive ? 'button' : null}
26
+ tabindex={interactive ? 0 : null}
13
27
  data-pw={testId}
14
28
  >
15
29
  {#if typeof svg === 'string' && svg.length > 0}
@@ -29,8 +43,12 @@
29
43
  padding: var(--icon-container-padding, 4px);
30
44
  flex-direction: var(--icon-container-direction, column);
31
45
  align-items: center;
32
- cursor: pointer;
33
46
  }
47
+
48
+ .icon-container.interactive {
49
+ cursor: var(--icon-cursor, pointer);
50
+ }
51
+
34
52
  .icon-container img {
35
53
  height: var(--icon-height, 20px);
36
54
  width: var(--icon-width, 20px);
@@ -25,6 +25,7 @@
25
25
  border-radius: var(--image-border-radius, 0px);
26
26
  margin: var(--image-margin, 0px);
27
27
  filter: var(--image-filter, none);
28
+ flex-shrink: var(--image-flex-shrink);
28
29
  background: var(--image-background);
29
30
  border: var(--image-border);
30
31
  transition: var(--image-transition);
@@ -8,11 +8,14 @@
8
8
 
9
9
  let overlayDiv: HTMLDivElement | null = $state(null);
10
10
  let backPressed = false;
11
+ let dismissTimer: ReturnType<typeof setTimeout> | null = null;
11
12
 
12
13
  let {
13
14
  size = 'fit-content',
14
15
  align = 'center',
15
16
  showOverlay = true,
17
+ lockScroll = true,
18
+ autoDismissAfter = null,
16
19
  supportHardwareBackPress = false,
17
20
  enableTransition = true,
18
21
  transitionType = 'ALL',
@@ -75,7 +78,12 @@
75
78
  }
76
79
 
77
80
  onMount(() => {
78
- document.body.style.overflow = 'hidden';
81
+ if (lockScroll) {
82
+ document.body.style.overflow = 'hidden';
83
+ }
84
+ if (typeof autoDismissAfter === 'number') {
85
+ dismissTimer = setTimeout(() => onclose?.(), autoDismissAfter);
86
+ }
79
87
  if (supportHardwareBackPress) {
80
88
  history.pushState(null, '', window.location.href);
81
89
  window.addEventListener('popstate', handlePopstate);
@@ -83,8 +91,13 @@
83
91
  });
84
92
 
85
93
  onDestroy(() => {
94
+ if (dismissTimer !== null) {
95
+ clearTimeout(dismissTimer);
96
+ }
86
97
  if (typeof window !== 'undefined') {
87
- document.body.style.overflow = '';
98
+ if (lockScroll) {
99
+ document.body.style.overflow = '';
100
+ }
88
101
  if (supportHardwareBackPress) {
89
102
  if (!backPressed) {
90
103
  history.back();
@@ -172,7 +185,7 @@
172
185
 
173
186
  <style>
174
187
  .modal {
175
- position: fixed;
188
+ position: var(--modal-position, fixed);
176
189
  top: 0;
177
190
  bottom: 0;
178
191
  left: 0;
@@ -7,6 +7,8 @@ export type ModalProperties = ModalEventProperties & {
7
7
  size?: ModalSize;
8
8
  align?: ModalAlign;
9
9
  showOverlay?: boolean;
10
+ lockScroll?: boolean;
11
+ autoDismissAfter?: number | null;
10
12
  supportHardwareBackPress?: boolean;
11
13
  enableTransition?: boolean;
12
14
  transitionType?: ModalTransition;
@@ -0,0 +1,7 @@
1
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M3 6h18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
3
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <line x1="10" y1="11" x2="10" y2="17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
6
+ <line x1="14" y1="11" x2="14" y2="17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
7
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
3
+ <path d="m15 5 4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
package/dist/index.d.ts CHANGED
@@ -64,6 +64,7 @@ export { default as ChatSuggestions } from './ChatSuggestions/ChatSuggestions.sv
64
64
  export { default as Resizable } from './Resizable/Resizable.svelte';
65
65
  export { default as Draggable } from './Draggable/Draggable.svelte';
66
66
  export { default as ChatBubble } from './ChatBubble/ChatBubble.svelte';
67
+ export { default as Gallery } from './Gallery/Gallery.svelte';
67
68
  export { ChatController } from './Chat/controller.svelte';
68
69
  export { partyOf } from './Chat/roles';
69
70
  export type * from './Button/properties';
@@ -127,4 +128,5 @@ export type * from './ChatSuggestions/properties';
127
128
  export type * from './Resizable/properties';
128
129
  export type * from './Draggable/properties';
129
130
  export type * from './ChatBubble/properties';
131
+ export type * from './Gallery/properties';
130
132
  export { validateInput } from './utils';
package/dist/index.js CHANGED
@@ -64,6 +64,7 @@ export { default as ChatSuggestions } from './ChatSuggestions/ChatSuggestions.sv
64
64
  export { default as Resizable } from './Resizable/Resizable.svelte';
65
65
  export { default as Draggable } from './Draggable/Draggable.svelte';
66
66
  export { default as ChatBubble } from './ChatBubble/ChatBubble.svelte';
67
+ export { default as Gallery } from './Gallery/Gallery.svelte';
67
68
  export { ChatController } from './Chat/controller.svelte';
68
69
  export { partyOf } from './Chat/roles';
69
70
  export { validateInput } from './utils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polymorph-ui-components",
3
- "version": "0.4.0",
3
+ "version": "0.6.1",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",
@@ -14,6 +14,15 @@
14
14
  "svelte-components"
15
15
  ],
16
16
  "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/sinha-sahil/polymorph-ui-components.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/sinha-sahil/polymorph-ui-components/issues"
23
+ },
24
+ "homepage": "https://github.com/sinha-sahil/polymorph-ui-components#readme",
25
+ "author": "Sahil Sinha",
17
26
  "scripts": {
18
27
  "prepare": "husky",
19
28
  "dev": "vite dev --host",