@svgrid/grid 2.2.0 → 2.2.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/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -178
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvColorInput.svelte +187 -187
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvDrawer.svelte +250 -250
- package/dist/SvGrid.svelte +3131 -3131
- package/dist/SvGridBoard.svelte +2339 -2339
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridDropdown.svelte +689 -689
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +117 -117
- package/dist/SvNumberInput.svelte +131 -131
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPopover.svelte +139 -139
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvTooltip.svelte +127 -127
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +377 -377
- package/dist/SvTreeSelect.svelte +230 -230
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/chart-export.js +8 -8
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -178
- package/src/SvCarousel.svelte +141 -141
- package/src/SvColorInput.svelte +187 -187
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvDrawer.svelte +250 -250
- package/src/SvGrid.controller.svelte.ts +3158 -3158
- package/src/SvGrid.svelte +3131 -3131
- package/src/SvGrid.types.ts +1489 -1489
- package/src/SvGridBoard.svelte +2339 -2339
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridDropdown.svelte +689 -689
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +117 -117
- package/src/SvNumberInput.svelte +131 -131
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPopover.svelte +139 -139
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvTooltip.svelte +127 -127
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +377 -377
- package/src/SvTreeSelect.svelte +230 -230
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/build-api.ts +870 -870
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +179 -179
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +162 -162
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createMenu.svelte.ts +202 -202
- package/src/createPopoverSelect.svelte.ts +206 -206
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/flex-render.ts +3 -3
- package/src/index.ts +623 -623
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/menus.ts +639 -639
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +501 -501
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +527 -527
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +159 -159
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
package/dist/SvCarousel.svelte
CHANGED
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SvCarousel - a slideshow: a sliding track with prev/next arrows, dot
|
|
4
|
-
* indicators, optional autoplay (pauses on hover/focus) and swipe. Slides are
|
|
5
|
-
* rendered by the `slide` snippet (index in, your content out). Parity: Smart
|
|
6
|
-
* carousel.
|
|
7
|
-
*
|
|
8
|
-
* ```svelte
|
|
9
|
-
* <SvCarousel count={imgs.length} autoplay={4000}>
|
|
10
|
-
* {#snippet slide(i)}<img src={imgs[i]} alt="" />{/snippet}
|
|
11
|
-
* </SvCarousel>
|
|
12
|
-
* ```
|
|
13
|
-
*/
|
|
14
|
-
import type { Snippet } from 'svelte'
|
|
15
|
-
import { type EditorDir } from './editor-contract'
|
|
16
|
-
import { createCarousel } from './createCarousel.svelte'
|
|
17
|
-
|
|
18
|
-
type Props = {
|
|
19
|
-
count: number
|
|
20
|
-
slide?: Snippet<[number]>
|
|
21
|
-
/** Active slide index (bindable). */
|
|
22
|
-
current?: number
|
|
23
|
-
/** Autoplay interval in ms; 0 = off. */
|
|
24
|
-
autoplay?: number
|
|
25
|
-
loop?: boolean
|
|
26
|
-
arrows?: boolean
|
|
27
|
-
dots?: boolean
|
|
28
|
-
ariaLabel?: string
|
|
29
|
-
/** Text direction (rtl mirrors the prev/next arrows + slide direction). */
|
|
30
|
-
dir?: EditorDir
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let {
|
|
34
|
-
count,
|
|
35
|
-
slide,
|
|
36
|
-
current = $bindable(0),
|
|
37
|
-
autoplay = 0,
|
|
38
|
-
loop = true,
|
|
39
|
-
arrows = true,
|
|
40
|
-
dots = true,
|
|
41
|
-
ariaLabel = 'Carousel',
|
|
42
|
-
dir,
|
|
43
|
-
}: Props = $props()
|
|
44
|
-
|
|
45
|
-
// Navigation, autoplay (pause on hover/focus + WCAG 2.2.2 stop toggle) and the
|
|
46
|
-
// rtl-aware arrow wiring live in the shared headless core; this component keeps
|
|
47
|
-
// only the visual track + swipe. See createCarousel.svelte.ts.
|
|
48
|
-
const car = createCarousel({
|
|
49
|
-
count: () => count,
|
|
50
|
-
current: () => current,
|
|
51
|
-
onChange: (i) => (current = i),
|
|
52
|
-
loop: () => loop,
|
|
53
|
-
autoplay: () => autoplay,
|
|
54
|
-
dir: () => (dir === 'ltr' || dir === 'rtl' ? dir : undefined),
|
|
55
|
-
})
|
|
56
|
-
const rtl = $derived(car.rtl())
|
|
57
|
-
|
|
58
|
-
// Swipe (a DOM gesture the renderer owns; delegates to the core's next/prev).
|
|
59
|
-
let dragging = $state(false)
|
|
60
|
-
let startX = 0
|
|
61
|
-
let dx = $state(0)
|
|
62
|
-
function down(e: PointerEvent) {
|
|
63
|
-
dragging = true
|
|
64
|
-
startX = e.clientX
|
|
65
|
-
;(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId)
|
|
66
|
-
}
|
|
67
|
-
function move(e: PointerEvent) { if (dragging) dx = e.clientX - startX }
|
|
68
|
-
function up() {
|
|
69
|
-
if (!dragging) return
|
|
70
|
-
dragging = false
|
|
71
|
-
if (Math.abs(dx) > 50) (dx < 0 ? car.next() : car.prev())
|
|
72
|
-
dx = 0
|
|
73
|
-
}
|
|
74
|
-
</script>
|
|
75
|
-
|
|
76
|
-
<div class="sv-carousel" {...car.rootProps(ariaLabel)}>
|
|
77
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
78
|
-
<div class="sv-carousel__viewport" onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
|
|
79
|
-
<div class="sv-carousel__track" class:is-dragging={dragging} style:transform={`translateX(calc(${(rtl ? current : -current) * 100}% + ${dx}px))`}>
|
|
80
|
-
{#each Array(count) as _, i (i)}
|
|
81
|
-
<div class="sv-carousel__slide" {...car.slideProps(i)}>
|
|
82
|
-
{@render slide?.(i)}
|
|
83
|
-
</div>
|
|
84
|
-
{/each}
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
{#if arrows && count > 1}
|
|
89
|
-
<button type="button" class="sv-carousel__arrow is-prev" {...car.prevArrowProps()}>{rtl ? '›' : '‹'}</button>
|
|
90
|
-
<button type="button" class="sv-carousel__arrow is-next" {...car.nextArrowProps()}>{rtl ? '‹' : '›'}</button>
|
|
91
|
-
{/if}
|
|
92
|
-
|
|
93
|
-
{#if dots && count > 1}
|
|
94
|
-
<div class="sv-carousel__dots">
|
|
95
|
-
{#each Array(count) as _, i (i)}
|
|
96
|
-
<button type="button" class="sv-carousel__dot" class:is-active={car.isActive(i)} {...car.dotProps(i)}></button>
|
|
97
|
-
{/each}
|
|
98
|
-
</div>
|
|
99
|
-
{/if}
|
|
100
|
-
|
|
101
|
-
{#if autoplay > 0}
|
|
102
|
-
<button type="button" class="sv-carousel__playpause" {...car.playPauseProps()}>{car.userPaused ? '▶' : '⏸'}</button>
|
|
103
|
-
{/if}
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
<style>
|
|
107
|
-
.sv-carousel { position: relative; border-radius: 12px; overflow: hidden; }
|
|
108
|
-
.sv-carousel__viewport { overflow: hidden; touch-action: pan-y; }
|
|
109
|
-
.sv-carousel__track { display: flex; transition: transform 0.32s cubic-bezier(0.16, 1, 0.3, 1); }
|
|
110
|
-
.sv-carousel__track.is-dragging { transition: none; }
|
|
111
|
-
.sv-carousel__slide { flex: 0 0 100%; min-width: 0; }
|
|
112
|
-
.sv-carousel__slide :global(img) { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
113
|
-
|
|
114
|
-
.sv-carousel__arrow {
|
|
115
|
-
position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
|
|
116
|
-
width: 34px; height: 34px; display: grid; place-items: center; border-radius: 50%;
|
|
117
|
-
background: color-mix(in srgb, var(--sg-bg, #fff) 82%, transparent); color: var(--sg-fg, #0f172a);
|
|
118
|
-
border: 1px solid var(--sg-border, #e2e8f0); cursor: pointer; font-size: 20px; line-height: 1;
|
|
119
|
-
box-shadow: 0 4px 12px -4px rgba(15, 23, 42, 0.3);
|
|
120
|
-
}
|
|
121
|
-
.sv-carousel__arrow.is-prev { inset-inline-start: 10px; }
|
|
122
|
-
.sv-carousel__arrow.is-next { inset-inline-end: 10px; }
|
|
123
|
-
.sv-carousel__arrow:hover:not(:disabled) { background: var(--sg-bg, #fff); }
|
|
124
|
-
.sv-carousel__arrow:disabled { opacity: 0.4; cursor: default; }
|
|
125
|
-
|
|
126
|
-
.sv-carousel__dots { position: absolute; inset-inline: 0; bottom: 10px; display: flex; justify-content: center; gap: 6px; z-index: 2; }
|
|
127
|
-
.sv-carousel__dot {
|
|
128
|
-
width: 8px; height: 8px; border-radius: 50%; border: 0; cursor: pointer; padding: 0;
|
|
129
|
-
background: color-mix(in srgb, var(--sg-fg, #0f172a) 30%, var(--sg-bg, #fff)); transition: background 0.15s, width 0.15s;
|
|
130
|
-
}
|
|
131
|
-
.sv-carousel__dot.is-active { width: 20px; border-radius: 4px; background: var(--sg-accent, #2563eb); }
|
|
132
|
-
|
|
133
|
-
.sv-carousel__playpause {
|
|
134
|
-
position: absolute; top: 10px; inset-inline-end: 10px; z-index: 2;
|
|
135
|
-
width: 28px; height: 28px; display: grid; place-items: center; border-radius: 50%;
|
|
136
|
-
background: color-mix(in srgb, var(--sg-bg, #fff) 82%, transparent); color: var(--sg-fg, #0f172a);
|
|
137
|
-
border: 1px solid var(--sg-border, #e2e8f0); cursor: pointer; font-size: 12px; line-height: 1;
|
|
138
|
-
box-shadow: 0 4px 12px -4px rgba(15, 23, 42, 0.3);
|
|
139
|
-
}
|
|
140
|
-
.sv-carousel__playpause:hover { background: var(--sg-bg, #fff); }
|
|
141
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvCarousel - a slideshow: a sliding track with prev/next arrows, dot
|
|
4
|
+
* indicators, optional autoplay (pauses on hover/focus) and swipe. Slides are
|
|
5
|
+
* rendered by the `slide` snippet (index in, your content out). Parity: Smart
|
|
6
|
+
* carousel.
|
|
7
|
+
*
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <SvCarousel count={imgs.length} autoplay={4000}>
|
|
10
|
+
* {#snippet slide(i)}<img src={imgs[i]} alt="" />{/snippet}
|
|
11
|
+
* </SvCarousel>
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import type { Snippet } from 'svelte'
|
|
15
|
+
import { type EditorDir } from './editor-contract'
|
|
16
|
+
import { createCarousel } from './createCarousel.svelte'
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
count: number
|
|
20
|
+
slide?: Snippet<[number]>
|
|
21
|
+
/** Active slide index (bindable). */
|
|
22
|
+
current?: number
|
|
23
|
+
/** Autoplay interval in ms; 0 = off. */
|
|
24
|
+
autoplay?: number
|
|
25
|
+
loop?: boolean
|
|
26
|
+
arrows?: boolean
|
|
27
|
+
dots?: boolean
|
|
28
|
+
ariaLabel?: string
|
|
29
|
+
/** Text direction (rtl mirrors the prev/next arrows + slide direction). */
|
|
30
|
+
dir?: EditorDir
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
count,
|
|
35
|
+
slide,
|
|
36
|
+
current = $bindable(0),
|
|
37
|
+
autoplay = 0,
|
|
38
|
+
loop = true,
|
|
39
|
+
arrows = true,
|
|
40
|
+
dots = true,
|
|
41
|
+
ariaLabel = 'Carousel',
|
|
42
|
+
dir,
|
|
43
|
+
}: Props = $props()
|
|
44
|
+
|
|
45
|
+
// Navigation, autoplay (pause on hover/focus + WCAG 2.2.2 stop toggle) and the
|
|
46
|
+
// rtl-aware arrow wiring live in the shared headless core; this component keeps
|
|
47
|
+
// only the visual track + swipe. See createCarousel.svelte.ts.
|
|
48
|
+
const car = createCarousel({
|
|
49
|
+
count: () => count,
|
|
50
|
+
current: () => current,
|
|
51
|
+
onChange: (i) => (current = i),
|
|
52
|
+
loop: () => loop,
|
|
53
|
+
autoplay: () => autoplay,
|
|
54
|
+
dir: () => (dir === 'ltr' || dir === 'rtl' ? dir : undefined),
|
|
55
|
+
})
|
|
56
|
+
const rtl = $derived(car.rtl())
|
|
57
|
+
|
|
58
|
+
// Swipe (a DOM gesture the renderer owns; delegates to the core's next/prev).
|
|
59
|
+
let dragging = $state(false)
|
|
60
|
+
let startX = 0
|
|
61
|
+
let dx = $state(0)
|
|
62
|
+
function down(e: PointerEvent) {
|
|
63
|
+
dragging = true
|
|
64
|
+
startX = e.clientX
|
|
65
|
+
;(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId)
|
|
66
|
+
}
|
|
67
|
+
function move(e: PointerEvent) { if (dragging) dx = e.clientX - startX }
|
|
68
|
+
function up() {
|
|
69
|
+
if (!dragging) return
|
|
70
|
+
dragging = false
|
|
71
|
+
if (Math.abs(dx) > 50) (dx < 0 ? car.next() : car.prev())
|
|
72
|
+
dx = 0
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<div class="sv-carousel" {...car.rootProps(ariaLabel)}>
|
|
77
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
78
|
+
<div class="sv-carousel__viewport" onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
|
|
79
|
+
<div class="sv-carousel__track" class:is-dragging={dragging} style:transform={`translateX(calc(${(rtl ? current : -current) * 100}% + ${dx}px))`}>
|
|
80
|
+
{#each Array(count) as _, i (i)}
|
|
81
|
+
<div class="sv-carousel__slide" {...car.slideProps(i)}>
|
|
82
|
+
{@render slide?.(i)}
|
|
83
|
+
</div>
|
|
84
|
+
{/each}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{#if arrows && count > 1}
|
|
89
|
+
<button type="button" class="sv-carousel__arrow is-prev" {...car.prevArrowProps()}>{rtl ? '›' : '‹'}</button>
|
|
90
|
+
<button type="button" class="sv-carousel__arrow is-next" {...car.nextArrowProps()}>{rtl ? '‹' : '›'}</button>
|
|
91
|
+
{/if}
|
|
92
|
+
|
|
93
|
+
{#if dots && count > 1}
|
|
94
|
+
<div class="sv-carousel__dots">
|
|
95
|
+
{#each Array(count) as _, i (i)}
|
|
96
|
+
<button type="button" class="sv-carousel__dot" class:is-active={car.isActive(i)} {...car.dotProps(i)}></button>
|
|
97
|
+
{/each}
|
|
98
|
+
</div>
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
{#if autoplay > 0}
|
|
102
|
+
<button type="button" class="sv-carousel__playpause" {...car.playPauseProps()}>{car.userPaused ? '▶' : '⏸'}</button>
|
|
103
|
+
{/if}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<style>
|
|
107
|
+
.sv-carousel { position: relative; border-radius: 12px; overflow: hidden; }
|
|
108
|
+
.sv-carousel__viewport { overflow: hidden; touch-action: pan-y; }
|
|
109
|
+
.sv-carousel__track { display: flex; transition: transform 0.32s cubic-bezier(0.16, 1, 0.3, 1); }
|
|
110
|
+
.sv-carousel__track.is-dragging { transition: none; }
|
|
111
|
+
.sv-carousel__slide { flex: 0 0 100%; min-width: 0; }
|
|
112
|
+
.sv-carousel__slide :global(img) { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
113
|
+
|
|
114
|
+
.sv-carousel__arrow {
|
|
115
|
+
position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
|
|
116
|
+
width: 34px; height: 34px; display: grid; place-items: center; border-radius: 50%;
|
|
117
|
+
background: color-mix(in srgb, var(--sg-bg, #fff) 82%, transparent); color: var(--sg-fg, #0f172a);
|
|
118
|
+
border: 1px solid var(--sg-border, #e2e8f0); cursor: pointer; font-size: 20px; line-height: 1;
|
|
119
|
+
box-shadow: 0 4px 12px -4px rgba(15, 23, 42, 0.3);
|
|
120
|
+
}
|
|
121
|
+
.sv-carousel__arrow.is-prev { inset-inline-start: 10px; }
|
|
122
|
+
.sv-carousel__arrow.is-next { inset-inline-end: 10px; }
|
|
123
|
+
.sv-carousel__arrow:hover:not(:disabled) { background: var(--sg-bg, #fff); }
|
|
124
|
+
.sv-carousel__arrow:disabled { opacity: 0.4; cursor: default; }
|
|
125
|
+
|
|
126
|
+
.sv-carousel__dots { position: absolute; inset-inline: 0; bottom: 10px; display: flex; justify-content: center; gap: 6px; z-index: 2; }
|
|
127
|
+
.sv-carousel__dot {
|
|
128
|
+
width: 8px; height: 8px; border-radius: 50%; border: 0; cursor: pointer; padding: 0;
|
|
129
|
+
background: color-mix(in srgb, var(--sg-fg, #0f172a) 30%, var(--sg-bg, #fff)); transition: background 0.15s, width 0.15s;
|
|
130
|
+
}
|
|
131
|
+
.sv-carousel__dot.is-active { width: 20px; border-radius: 4px; background: var(--sg-accent, #2563eb); }
|
|
132
|
+
|
|
133
|
+
.sv-carousel__playpause {
|
|
134
|
+
position: absolute; top: 10px; inset-inline-end: 10px; z-index: 2;
|
|
135
|
+
width: 28px; height: 28px; display: grid; place-items: center; border-radius: 50%;
|
|
136
|
+
background: color-mix(in srgb, var(--sg-bg, #fff) 82%, transparent); color: var(--sg-fg, #0f172a);
|
|
137
|
+
border: 1px solid var(--sg-border, #e2e8f0); cursor: pointer; font-size: 12px; line-height: 1;
|
|
138
|
+
box-shadow: 0 4px 12px -4px rgba(15, 23, 42, 0.3);
|
|
139
|
+
}
|
|
140
|
+
.sv-carousel__playpause:hover { background: var(--sg-bg, #fff); }
|
|
141
|
+
</style>
|
package/dist/SvColorInput.svelte
CHANGED
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SvColorInput - a color swatch that opens a portalled popover with a hex
|
|
4
|
-
* field, a native picker, and a preset palette. Parity: Smart `smart-color-input`.
|
|
5
|
-
* Emits a hex string. Popover escapes the grid scroll container via popover.ts.
|
|
6
|
-
*
|
|
7
|
-
* The styled renderer over the headless `createColorInput` core: hex
|
|
8
|
-
* normalization, palette, draft field and open/close state come from the core;
|
|
9
|
-
* the portal + anchored measurement stay here (render concerns).
|
|
10
|
-
*/
|
|
11
|
-
import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
|
|
12
|
-
import { createDismissableLayer } from './a11y/dismissable'
|
|
13
|
-
import SvField from './SvField.svelte'
|
|
14
|
-
import { nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
|
|
15
|
-
import { createColorInput } from './createColorInput.svelte'
|
|
16
|
-
|
|
17
|
-
/** User-facing strings for the color popover (localizable via `messages`). */
|
|
18
|
-
type ColorMessages = { dialog: string; picker: string; hex: string }
|
|
19
|
-
const DEFAULT_MESSAGES: ColorMessages = { dialog: 'Choose color', picker: 'Color picker', hex: 'Hex value' }
|
|
20
|
-
|
|
21
|
-
type Props = SvEditorProps & {
|
|
22
|
-
value?: string
|
|
23
|
-
onChange?: (hex: string) => void
|
|
24
|
-
/** Preset swatches. */
|
|
25
|
-
palette?: string[]
|
|
26
|
-
autoOpen?: boolean
|
|
27
|
-
/** Override the built-in popover strings. */
|
|
28
|
-
messages?: Partial<ColorMessages>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let {
|
|
32
|
-
value = $bindable('#3b82f6'),
|
|
33
|
-
onChange,
|
|
34
|
-
disabled = false,
|
|
35
|
-
readonly = false,
|
|
36
|
-
palette,
|
|
37
|
-
name,
|
|
38
|
-
size = 'md',
|
|
39
|
-
ariaLabel,
|
|
40
|
-
invalid = false,
|
|
41
|
-
required = false,
|
|
42
|
-
error,
|
|
43
|
-
label,
|
|
44
|
-
hint,
|
|
45
|
-
dir,
|
|
46
|
-
id,
|
|
47
|
-
autoOpen = false,
|
|
48
|
-
messages,
|
|
49
|
-
}: Props = $props()
|
|
50
|
-
|
|
51
|
-
const autoId = nextEditorId('sv-color')
|
|
52
|
-
const uid = $derived(id ?? autoId)
|
|
53
|
-
const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
|
|
54
|
-
|
|
55
|
-
const col = createColorInput({
|
|
56
|
-
value: () => value,
|
|
57
|
-
// Write `value` (so `bind:value` works) AND fire `onChange` (callback users).
|
|
58
|
-
onChange: (hex) => { value = hex; onChange?.(hex) },
|
|
59
|
-
disabled: () => disabled,
|
|
60
|
-
readonly: () => readonly,
|
|
61
|
-
palette: () => palette as string[],
|
|
62
|
-
id: () => uid,
|
|
63
|
-
invalid: () => invalid,
|
|
64
|
-
required: () => required,
|
|
65
|
-
error: () => error,
|
|
66
|
-
hint: () => hint,
|
|
67
|
-
ariaLabel: () => ariaLabel,
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
71
|
-
let panelEl = $state<HTMLDivElement | null>(null)
|
|
72
|
-
let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
|
|
73
|
-
|
|
74
|
-
function updatePos() {
|
|
75
|
-
if (!triggerEl) return
|
|
76
|
-
panelRect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 210, minWidth: 200 })
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
$effect(() => {
|
|
80
|
-
if (!col.popover.open) return
|
|
81
|
-
updatePos()
|
|
82
|
-
const reposition = () => updatePos()
|
|
83
|
-
window.addEventListener('scroll', reposition, true)
|
|
84
|
-
window.addEventListener('resize', reposition)
|
|
85
|
-
const layer = createDismissableLayer({
|
|
86
|
-
element: () => [triggerEl, panelEl],
|
|
87
|
-
onDismiss: () => col.popover.close(),
|
|
88
|
-
})
|
|
89
|
-
layer.activate()
|
|
90
|
-
return () => {
|
|
91
|
-
window.removeEventListener('scroll', reposition, true)
|
|
92
|
-
window.removeEventListener('resize', reposition)
|
|
93
|
-
layer.release()
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
function focusOpen(node: HTMLButtonElement) {
|
|
98
|
-
if (!autoOpen) return
|
|
99
|
-
node.focus(); col.popover.show()
|
|
100
|
-
}
|
|
101
|
-
</script>
|
|
102
|
-
|
|
103
|
-
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
104
|
-
<button
|
|
105
|
-
bind:this={triggerEl}
|
|
106
|
-
class="sv-color sv-color--{size}"
|
|
107
|
-
class:is-disabled={disabled}
|
|
108
|
-
class:is-invalid={invalid}
|
|
109
|
-
{...col.swatchProps()}
|
|
110
|
-
use:focusOpen
|
|
111
|
-
>
|
|
112
|
-
<span class="sv-color__swatch" style:background={col.normalized}></span>
|
|
113
|
-
<span class="sv-color__hex">{col.normalized}</span>
|
|
114
|
-
</button>
|
|
115
|
-
</SvField>
|
|
116
|
-
|
|
117
|
-
{#if col.popover.open}
|
|
118
|
-
<div
|
|
119
|
-
class="sv-color__panel"
|
|
120
|
-
bind:this={panelEl}
|
|
121
|
-
use:portalToBody
|
|
122
|
-
use:popIn={{ up: panelRect.openUpward }}
|
|
123
|
-
style:position="fixed"
|
|
124
|
-
style:top={`${panelRect.top}px`}
|
|
125
|
-
style:left={`${panelRect.left}px`}
|
|
126
|
-
role="dialog"
|
|
127
|
-
aria-label={M.dialog}
|
|
128
|
-
>
|
|
129
|
-
<div class="sv-color__top">
|
|
130
|
-
<input class="sv-color__native" type="color" value={col.normalized} oninput={(e) => col.pick((e.currentTarget as HTMLInputElement).value)} aria-label={M.picker} />
|
|
131
|
-
<input
|
|
132
|
-
class="sv-color__field"
|
|
133
|
-
type="text"
|
|
134
|
-
bind:value={col.hexDraft}
|
|
135
|
-
spellcheck="false"
|
|
136
|
-
aria-label={M.hex}
|
|
137
|
-
onblur={col.commitHex}
|
|
138
|
-
onkeydown={(e) => { if (e.key === 'Enter') col.commitHex() }}
|
|
139
|
-
/>
|
|
140
|
-
</div>
|
|
141
|
-
<div class="sv-color__palette">
|
|
142
|
-
{#each col.palette as c (c)}
|
|
143
|
-
<button
|
|
144
|
-
class="sv-color__chip"
|
|
145
|
-
class:is-active={col.isActive(c)}
|
|
146
|
-
style:background={c}
|
|
147
|
-
{...col.chipProps(c)}
|
|
148
|
-
></button>
|
|
149
|
-
{/each}
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
{/if}
|
|
153
|
-
{#if name}<input type="hidden" {name} value={col.normalized} />{/if}
|
|
154
|
-
|
|
155
|
-
<style>
|
|
156
|
-
.sv-color {
|
|
157
|
-
--_accent: var(--sg-accent, #2563eb);
|
|
158
|
-
display: inline-flex; align-items: center; gap: 8px;
|
|
159
|
-
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
160
|
-
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
161
|
-
padding: 0 10px; cursor: pointer; font: inherit;
|
|
162
|
-
}
|
|
163
|
-
.sv-color--sm { height: 28px; font-size: 12px; }
|
|
164
|
-
.sv-color--md { height: 34px; font-size: 13px; }
|
|
165
|
-
.sv-color--lg { height: 40px; font-size: 15px; }
|
|
166
|
-
.sv-color.is-disabled { opacity: 0.6; cursor: not-allowed; }
|
|
167
|
-
.sv-color.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
168
|
-
.sv-color.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
|
|
169
|
-
.sv-color:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
170
|
-
.sv-color__swatch { width: 18px; height: 18px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.15); flex: none; }
|
|
171
|
-
.sv-color__hex { font-variant-numeric: tabular-nums; text-transform: lowercase; }
|
|
172
|
-
|
|
173
|
-
:global(.sv-color__panel) {
|
|
174
|
-
z-index: 2147483647; width: 208px; padding: 12px;
|
|
175
|
-
background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
|
|
176
|
-
border-radius: 12px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
|
|
177
|
-
}
|
|
178
|
-
:global(.sv-color__top) { display: flex; gap: 8px; margin-bottom: 10px; }
|
|
179
|
-
:global(.sv-color__native) { width: 36px; height: 34px; padding: 0; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; background: none; cursor: pointer; }
|
|
180
|
-
:global(.sv-color__field) {
|
|
181
|
-
flex: 1; min-width: 0; height: 34px; padding: 0 10px; font: inherit; font-size: 13px;
|
|
182
|
-
border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 8px; background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); outline: none;
|
|
183
|
-
}
|
|
184
|
-
:global(.sv-color__palette) { display: grid; grid-template-columns: repeat(10, 1fr); gap: 4px; }
|
|
185
|
-
:global(.sv-color__chip) { width: 100%; aspect-ratio: 1; border-radius: 5px; border: 1px solid rgba(0,0,0,0.12); cursor: pointer; padding: 0; }
|
|
186
|
-
:global(.sv-color__chip.is-active) { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 1px; }
|
|
187
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvColorInput - a color swatch that opens a portalled popover with a hex
|
|
4
|
+
* field, a native picker, and a preset palette. Parity: Smart `smart-color-input`.
|
|
5
|
+
* Emits a hex string. Popover escapes the grid scroll container via popover.ts.
|
|
6
|
+
*
|
|
7
|
+
* The styled renderer over the headless `createColorInput` core: hex
|
|
8
|
+
* normalization, palette, draft field and open/close state come from the core;
|
|
9
|
+
* the portal + anchored measurement stay here (render concerns).
|
|
10
|
+
*/
|
|
11
|
+
import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
|
|
12
|
+
import { createDismissableLayer } from './a11y/dismissable'
|
|
13
|
+
import SvField from './SvField.svelte'
|
|
14
|
+
import { nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
|
|
15
|
+
import { createColorInput } from './createColorInput.svelte'
|
|
16
|
+
|
|
17
|
+
/** User-facing strings for the color popover (localizable via `messages`). */
|
|
18
|
+
type ColorMessages = { dialog: string; picker: string; hex: string }
|
|
19
|
+
const DEFAULT_MESSAGES: ColorMessages = { dialog: 'Choose color', picker: 'Color picker', hex: 'Hex value' }
|
|
20
|
+
|
|
21
|
+
type Props = SvEditorProps & {
|
|
22
|
+
value?: string
|
|
23
|
+
onChange?: (hex: string) => void
|
|
24
|
+
/** Preset swatches. */
|
|
25
|
+
palette?: string[]
|
|
26
|
+
autoOpen?: boolean
|
|
27
|
+
/** Override the built-in popover strings. */
|
|
28
|
+
messages?: Partial<ColorMessages>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
value = $bindable('#3b82f6'),
|
|
33
|
+
onChange,
|
|
34
|
+
disabled = false,
|
|
35
|
+
readonly = false,
|
|
36
|
+
palette,
|
|
37
|
+
name,
|
|
38
|
+
size = 'md',
|
|
39
|
+
ariaLabel,
|
|
40
|
+
invalid = false,
|
|
41
|
+
required = false,
|
|
42
|
+
error,
|
|
43
|
+
label,
|
|
44
|
+
hint,
|
|
45
|
+
dir,
|
|
46
|
+
id,
|
|
47
|
+
autoOpen = false,
|
|
48
|
+
messages,
|
|
49
|
+
}: Props = $props()
|
|
50
|
+
|
|
51
|
+
const autoId = nextEditorId('sv-color')
|
|
52
|
+
const uid = $derived(id ?? autoId)
|
|
53
|
+
const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
|
|
54
|
+
|
|
55
|
+
const col = createColorInput({
|
|
56
|
+
value: () => value,
|
|
57
|
+
// Write `value` (so `bind:value` works) AND fire `onChange` (callback users).
|
|
58
|
+
onChange: (hex) => { value = hex; onChange?.(hex) },
|
|
59
|
+
disabled: () => disabled,
|
|
60
|
+
readonly: () => readonly,
|
|
61
|
+
palette: () => palette as string[],
|
|
62
|
+
id: () => uid,
|
|
63
|
+
invalid: () => invalid,
|
|
64
|
+
required: () => required,
|
|
65
|
+
error: () => error,
|
|
66
|
+
hint: () => hint,
|
|
67
|
+
ariaLabel: () => ariaLabel,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
71
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
72
|
+
let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
|
|
73
|
+
|
|
74
|
+
function updatePos() {
|
|
75
|
+
if (!triggerEl) return
|
|
76
|
+
panelRect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 210, minWidth: 200 })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
$effect(() => {
|
|
80
|
+
if (!col.popover.open) return
|
|
81
|
+
updatePos()
|
|
82
|
+
const reposition = () => updatePos()
|
|
83
|
+
window.addEventListener('scroll', reposition, true)
|
|
84
|
+
window.addEventListener('resize', reposition)
|
|
85
|
+
const layer = createDismissableLayer({
|
|
86
|
+
element: () => [triggerEl, panelEl],
|
|
87
|
+
onDismiss: () => col.popover.close(),
|
|
88
|
+
})
|
|
89
|
+
layer.activate()
|
|
90
|
+
return () => {
|
|
91
|
+
window.removeEventListener('scroll', reposition, true)
|
|
92
|
+
window.removeEventListener('resize', reposition)
|
|
93
|
+
layer.release()
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
function focusOpen(node: HTMLButtonElement) {
|
|
98
|
+
if (!autoOpen) return
|
|
99
|
+
node.focus(); col.popover.show()
|
|
100
|
+
}
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
104
|
+
<button
|
|
105
|
+
bind:this={triggerEl}
|
|
106
|
+
class="sv-color sv-color--{size}"
|
|
107
|
+
class:is-disabled={disabled}
|
|
108
|
+
class:is-invalid={invalid}
|
|
109
|
+
{...col.swatchProps()}
|
|
110
|
+
use:focusOpen
|
|
111
|
+
>
|
|
112
|
+
<span class="sv-color__swatch" style:background={col.normalized}></span>
|
|
113
|
+
<span class="sv-color__hex">{col.normalized}</span>
|
|
114
|
+
</button>
|
|
115
|
+
</SvField>
|
|
116
|
+
|
|
117
|
+
{#if col.popover.open}
|
|
118
|
+
<div
|
|
119
|
+
class="sv-color__panel"
|
|
120
|
+
bind:this={panelEl}
|
|
121
|
+
use:portalToBody
|
|
122
|
+
use:popIn={{ up: panelRect.openUpward }}
|
|
123
|
+
style:position="fixed"
|
|
124
|
+
style:top={`${panelRect.top}px`}
|
|
125
|
+
style:left={`${panelRect.left}px`}
|
|
126
|
+
role="dialog"
|
|
127
|
+
aria-label={M.dialog}
|
|
128
|
+
>
|
|
129
|
+
<div class="sv-color__top">
|
|
130
|
+
<input class="sv-color__native" type="color" value={col.normalized} oninput={(e) => col.pick((e.currentTarget as HTMLInputElement).value)} aria-label={M.picker} />
|
|
131
|
+
<input
|
|
132
|
+
class="sv-color__field"
|
|
133
|
+
type="text"
|
|
134
|
+
bind:value={col.hexDraft}
|
|
135
|
+
spellcheck="false"
|
|
136
|
+
aria-label={M.hex}
|
|
137
|
+
onblur={col.commitHex}
|
|
138
|
+
onkeydown={(e) => { if (e.key === 'Enter') col.commitHex() }}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="sv-color__palette">
|
|
142
|
+
{#each col.palette as c (c)}
|
|
143
|
+
<button
|
|
144
|
+
class="sv-color__chip"
|
|
145
|
+
class:is-active={col.isActive(c)}
|
|
146
|
+
style:background={c}
|
|
147
|
+
{...col.chipProps(c)}
|
|
148
|
+
></button>
|
|
149
|
+
{/each}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
{/if}
|
|
153
|
+
{#if name}<input type="hidden" {name} value={col.normalized} />{/if}
|
|
154
|
+
|
|
155
|
+
<style>
|
|
156
|
+
.sv-color {
|
|
157
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
158
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
159
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
160
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
161
|
+
padding: 0 10px; cursor: pointer; font: inherit;
|
|
162
|
+
}
|
|
163
|
+
.sv-color--sm { height: 28px; font-size: 12px; }
|
|
164
|
+
.sv-color--md { height: 34px; font-size: 13px; }
|
|
165
|
+
.sv-color--lg { height: 40px; font-size: 15px; }
|
|
166
|
+
.sv-color.is-disabled { opacity: 0.6; cursor: not-allowed; }
|
|
167
|
+
.sv-color.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
168
|
+
.sv-color.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
|
|
169
|
+
.sv-color:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
|
|
170
|
+
.sv-color__swatch { width: 18px; height: 18px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.15); flex: none; }
|
|
171
|
+
.sv-color__hex { font-variant-numeric: tabular-nums; text-transform: lowercase; }
|
|
172
|
+
|
|
173
|
+
:global(.sv-color__panel) {
|
|
174
|
+
z-index: 2147483647; width: 208px; padding: 12px;
|
|
175
|
+
background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
|
|
176
|
+
border-radius: 12px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
|
|
177
|
+
}
|
|
178
|
+
:global(.sv-color__top) { display: flex; gap: 8px; margin-bottom: 10px; }
|
|
179
|
+
:global(.sv-color__native) { width: 36px; height: 34px; padding: 0; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; background: none; cursor: pointer; }
|
|
180
|
+
:global(.sv-color__field) {
|
|
181
|
+
flex: 1; min-width: 0; height: 34px; padding: 0 10px; font: inherit; font-size: 13px;
|
|
182
|
+
border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 8px; background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); outline: none;
|
|
183
|
+
}
|
|
184
|
+
:global(.sv-color__palette) { display: grid; grid-template-columns: repeat(10, 1fr); gap: 4px; }
|
|
185
|
+
:global(.sv-color__chip) { width: 100%; aspect-ratio: 1; border-radius: 5px; border: 1px solid rgba(0,0,0,0.12); cursor: pointer; padding: 0; }
|
|
186
|
+
:global(.sv-color__chip.is-active) { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 1px; }
|
|
187
|
+
</style>
|