@svgrid/grid 1.2.21 → 1.2.23

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 (190) hide show
  1. package/dist/GridFooter.svelte +34 -20
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/SvAutoComplete.svelte +100 -0
  4. package/dist/SvAutoComplete.svelte.d.ts +16 -0
  5. package/dist/SvButton.svelte +97 -0
  6. package/dist/SvButton.svelte.d.ts +23 -0
  7. package/dist/SvCalendar.svelte +669 -0
  8. package/dist/SvCalendar.svelte.d.ts +70 -0
  9. package/dist/SvCheckBox.svelte +81 -0
  10. package/dist/SvCheckBox.svelte.d.ts +19 -0
  11. package/dist/SvColorInput.svelte +187 -0
  12. package/dist/SvColorInput.svelte.d.ts +15 -0
  13. package/dist/SvComboBox.svelte +130 -0
  14. package/dist/SvComboBox.svelte.d.ts +15 -0
  15. package/dist/SvCountryInput.svelte +123 -0
  16. package/dist/SvCountryInput.svelte.d.ts +14 -0
  17. package/dist/SvDateTimePicker.svelte +322 -0
  18. package/dist/SvDateTimePicker.svelte.d.ts +37 -0
  19. package/dist/SvDropDownList.svelte +143 -0
  20. package/dist/SvDropDownList.svelte.d.ts +15 -0
  21. package/dist/SvForm.svelte +147 -0
  22. package/dist/SvForm.svelte.d.ts +29 -0
  23. package/dist/SvGauge.svelte +99 -0
  24. package/dist/SvGauge.svelte.d.ts +31 -0
  25. package/dist/SvGrid.controller.svelte.d.ts +16 -2
  26. package/dist/SvGrid.controller.svelte.js +63 -8
  27. package/dist/SvGrid.css +84 -31
  28. package/dist/SvGrid.helpers.d.ts +4 -0
  29. package/dist/SvGrid.helpers.js +40 -12
  30. package/dist/SvGrid.svelte +84 -5
  31. package/dist/SvGrid.types.d.ts +36 -0
  32. package/dist/SvGridDropdown.svelte +16 -30
  33. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  34. package/dist/SvListBox.svelte +122 -0
  35. package/dist/SvListBox.svelte.d.ts +19 -0
  36. package/dist/SvMaskedInput.svelte +0 -0
  37. package/dist/SvMaskedInput.svelte.d.ts +15 -0
  38. package/dist/SvNumberInput.svelte +0 -0
  39. package/dist/SvNumberInput.svelte.d.ts +22 -0
  40. package/dist/SvPasswordInput.svelte +110 -0
  41. package/dist/SvPasswordInput.svelte.d.ts +22 -0
  42. package/dist/SvPhoneInput.svelte +0 -0
  43. package/dist/SvPhoneInput.svelte.d.ts +19 -0
  44. package/dist/SvRadioGroup.svelte +108 -0
  45. package/dist/SvRadioGroup.svelte.d.ts +23 -0
  46. package/dist/SvRating.svelte +111 -0
  47. package/dist/SvRating.svelte.d.ts +20 -0
  48. package/dist/SvRepeatButton.svelte +89 -0
  49. package/dist/SvRepeatButton.svelte.d.ts +21 -0
  50. package/dist/SvSlider.svelte +182 -0
  51. package/dist/SvSlider.svelte.d.ts +25 -0
  52. package/dist/SvSwitchButton.svelte +94 -0
  53. package/dist/SvSwitchButton.svelte.d.ts +19 -0
  54. package/dist/SvTabs.svelte +122 -0
  55. package/dist/SvTabs.svelte.d.ts +26 -0
  56. package/dist/SvTagsInput.svelte +81 -0
  57. package/dist/SvTagsInput.svelte.d.ts +19 -0
  58. package/dist/SvTimePicker.svelte +359 -0
  59. package/dist/SvTimePicker.svelte.d.ts +27 -0
  60. package/dist/SvToggleButton.svelte +57 -0
  61. package/dist/SvToggleButton.svelte.d.ts +16 -0
  62. package/dist/SvTree.svelte +210 -0
  63. package/dist/SvTree.svelte.d.ts +29 -0
  64. package/dist/build-api.js +78 -1
  65. package/dist/cdn/svgrid.js +10022 -5904
  66. package/dist/cdn/svgrid.svelte-external.js +8531 -4429
  67. package/dist/cell-render.d.ts +4 -0
  68. package/dist/cell-render.js +35 -1
  69. package/dist/clipboard.js +19 -1
  70. package/dist/collaboration.d.ts +2 -0
  71. package/dist/collaboration.js +4 -0
  72. package/dist/core.d.ts +22 -1
  73. package/dist/countries.d.ts +15 -0
  74. package/dist/countries.js +61 -0
  75. package/dist/datetime/date-core.d.ts +88 -0
  76. package/dist/datetime/date-core.js +165 -0
  77. package/dist/datetime/date-format.d.ts +39 -0
  78. package/dist/datetime/date-format.js +371 -0
  79. package/dist/datetime/date-restrict.d.ts +23 -0
  80. package/dist/datetime/date-restrict.js +50 -0
  81. package/dist/datetime/date-selection.d.ts +25 -0
  82. package/dist/datetime/date-selection.js +94 -0
  83. package/dist/datetime/mask.d.ts +21 -0
  84. package/dist/datetime/mask.js +90 -0
  85. package/dist/editing.d.ts +1 -1
  86. package/dist/editing.js +7 -0
  87. package/dist/editors/cell-editors.d.ts +2 -2
  88. package/dist/editors/cell-editors.js +3 -1
  89. package/dist/export-format.d.ts +178 -0
  90. package/dist/export-format.js +419 -0
  91. package/dist/hyperformula-adapter.d.ts +4 -0
  92. package/dist/hyperformula-adapter.js +15 -6
  93. package/dist/index.d.ts +36 -0
  94. package/dist/index.js +41 -0
  95. package/dist/list-option.d.ts +10 -0
  96. package/dist/list-option.js +7 -0
  97. package/dist/menus.js +18 -2
  98. package/dist/popover.d.ts +48 -0
  99. package/dist/popover.js +65 -0
  100. package/dist/row-resize.d.ts +4 -4
  101. package/dist/row-resize.js +8 -5
  102. package/dist/selection.d.ts +6 -0
  103. package/dist/selection.js +31 -5
  104. package/dist/server-data-source.d.ts +43 -0
  105. package/dist/server-data-source.js +100 -0
  106. package/dist/sparkline.js +8 -4
  107. package/dist/svgrid-wrapper.types.d.ts +26 -1
  108. package/package.json +6 -1
  109. package/src/GridFooter.svelte +34 -20
  110. package/src/SvAutoComplete.svelte +100 -0
  111. package/src/SvButton.svelte +97 -0
  112. package/src/SvCalendar.svelte +669 -0
  113. package/src/SvCalendar.test.ts +211 -0
  114. package/src/SvCheckBox.svelte +81 -0
  115. package/src/SvColorInput.svelte +187 -0
  116. package/src/SvComboBox.svelte +130 -0
  117. package/src/SvCountryInput.svelte +123 -0
  118. package/src/SvDateTimePicker.svelte +322 -0
  119. package/src/SvDateTimePicker.test.ts +136 -0
  120. package/src/SvDropDownList.svelte +143 -0
  121. package/src/SvForm.svelte +147 -0
  122. package/src/SvGauge.svelte +99 -0
  123. package/src/SvGrid.controller.svelte.ts +74 -8
  124. package/src/SvGrid.css +84 -31
  125. package/src/SvGrid.helpers.ts +41 -12
  126. package/src/SvGrid.svelte +84 -5
  127. package/src/SvGrid.types.ts +33 -0
  128. package/src/SvGridDropdown.svelte +16 -30
  129. package/src/SvListBox.svelte +122 -0
  130. package/src/SvMaskedInput.svelte +0 -0
  131. package/src/SvNumberInput.svelte +0 -0
  132. package/src/SvPasswordInput.svelte +110 -0
  133. package/src/SvPhoneInput.svelte +0 -0
  134. package/src/SvRadioGroup.svelte +108 -0
  135. package/src/SvRating.svelte +111 -0
  136. package/src/SvRepeatButton.svelte +89 -0
  137. package/src/SvSlider.svelte +182 -0
  138. package/src/SvSwitchButton.svelte +94 -0
  139. package/src/SvTabs.svelte +122 -0
  140. package/src/SvTagsInput.svelte +81 -0
  141. package/src/SvTimePicker.svelte +359 -0
  142. package/src/SvTimePicker.test.ts +98 -0
  143. package/src/SvToggleButton.svelte +57 -0
  144. package/src/SvTree.svelte +210 -0
  145. package/src/build-api.ts +115 -0
  146. package/src/cell-render.test.ts +38 -0
  147. package/src/cell-render.ts +43 -0
  148. package/src/clipboard.test.ts +18 -0
  149. package/src/clipboard.ts +24 -1
  150. package/src/collaboration.test.ts +30 -0
  151. package/src/collaboration.ts +6 -0
  152. package/src/core.ts +27 -3
  153. package/src/countries.ts +71 -0
  154. package/src/datetime/date-core.test.ts +217 -0
  155. package/src/datetime/date-core.ts +204 -0
  156. package/src/datetime/date-format.test.ts +121 -0
  157. package/src/datetime/date-format.ts +317 -0
  158. package/src/datetime/date-restrict.ts +60 -0
  159. package/src/datetime/date-selection.test.ts +129 -0
  160. package/src/datetime/date-selection.ts +137 -0
  161. package/src/datetime/mask.test.ts +36 -0
  162. package/src/datetime/mask.ts +84 -0
  163. package/src/editing.test.ts +22 -0
  164. package/src/editing.ts +7 -1
  165. package/src/editors/cell-editors.ts +7 -1
  166. package/src/export-data-api.test.ts +126 -0
  167. package/src/export-format.test.ts +107 -0
  168. package/src/export-format.ts +598 -0
  169. package/src/hyperformula-adapter.test.ts +35 -1
  170. package/src/hyperformula-adapter.ts +18 -6
  171. package/src/index.ts +71 -0
  172. package/src/list-option.ts +15 -0
  173. package/src/menus.ts +21 -2
  174. package/src/popover.ts +79 -0
  175. package/src/row-resize.test.ts +25 -0
  176. package/src/row-resize.ts +8 -5
  177. package/src/selection.test.ts +36 -4
  178. package/src/selection.ts +30 -9
  179. package/src/server-data-source.test.ts +201 -0
  180. package/src/server-data-source.ts +148 -0
  181. package/src/sparkline.test.ts +9 -0
  182. package/src/sparkline.ts +9 -4
  183. package/src/svgrid-wrapper.types.ts +28 -1
  184. package/src/svgrid.behavior.test.ts +27 -0
  185. package/src/svgrid.new-features.wrapper.test.ts +27 -1
  186. package/src/ui-buttons.test.ts +105 -0
  187. package/src/ui-composite.test.ts +89 -0
  188. package/src/ui-inputs.test.ts +92 -0
  189. package/src/ui-range.test.ts +61 -0
  190. package/src/ui-selection.test.ts +106 -0
@@ -0,0 +1,182 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvSlider - a single or range slider (WAI-ARIA slider) with steps, ticks,
4
+ * keyboard + pointer drag. Parity: Smart `smart-slider`. Single emits number;
5
+ * range emits [number, number].
6
+ */
7
+ type Props = {
8
+ value?: number | [number, number]
9
+ onChange?: (value: any) => void
10
+ min?: number
11
+ max?: number
12
+ step?: number
13
+ range?: boolean
14
+ /** Show evenly spaced tick marks (count or explicit values). */
15
+ ticks?: number | number[]
16
+ /** Show the value bubble above the thumb. */
17
+ showValue?: boolean
18
+ disabled?: boolean
19
+ orientation?: 'horizontal' | 'vertical'
20
+ name?: string
21
+ ariaLabel?: string
22
+ formatValue?: (v: number) => string
23
+ }
24
+
25
+ let {
26
+ value = 0,
27
+ onChange,
28
+ min = 0,
29
+ max = 100,
30
+ step = 1,
31
+ range = false,
32
+ ticks,
33
+ showValue = false,
34
+ disabled = false,
35
+ orientation = 'horizontal',
36
+ name,
37
+ ariaLabel,
38
+ formatValue,
39
+ }: Props = $props()
40
+
41
+ const isVert = $derived(orientation === 'vertical')
42
+ const lo = $derived(range ? (value as [number, number])[0] : min)
43
+ const hi = $derived(range ? (value as [number, number])[1] : (value as number))
44
+
45
+ function clampStep(v: number): number {
46
+ const stepped = Math.round((v - min) / step) * step + min
47
+ return Math.min(max, Math.max(min, Number(stepped.toFixed(10))))
48
+ }
49
+ const pct = (v: number) => ((v - min) / (max - min)) * 100
50
+
51
+ const tickValues = $derived.by<number[]>(() => {
52
+ if (!ticks) return []
53
+ if (Array.isArray(ticks)) return ticks
54
+ return Array.from({ length: ticks }, (_, i) => min + ((max - min) / (ticks - 1)) * i)
55
+ })
56
+
57
+ let trackEl: HTMLDivElement | null = null
58
+ let dragging = $state<null | 'lo' | 'hi' | 'single'>(null)
59
+
60
+ function valueFromPointer(e: PointerEvent): number {
61
+ const rect = trackEl!.getBoundingClientRect()
62
+ const frac = isVert
63
+ ? 1 - (e.clientY - rect.top) / rect.height
64
+ : (e.clientX - rect.left) / rect.width
65
+ return clampStep(min + Math.min(1, Math.max(0, frac)) * (max - min))
66
+ }
67
+
68
+ function emitSingle(v: number) { onChange?.(v) }
69
+ function emitRange(nlo: number, nhi: number) { onChange?.([Math.min(nlo, nhi), Math.max(nlo, nhi)]) }
70
+
71
+ function onTrackDown(e: PointerEvent) {
72
+ if (disabled) return
73
+ const v = valueFromPointer(e)
74
+ if (range) {
75
+ const which = Math.abs(v - lo) <= Math.abs(v - hi) ? 'lo' : 'hi'
76
+ dragging = which
77
+ which === 'lo' ? emitRange(v, hi) : emitRange(lo, v)
78
+ } else {
79
+ dragging = 'single'
80
+ emitSingle(v)
81
+ }
82
+ trackEl?.setPointerCapture(e.pointerId)
83
+ }
84
+ function onMove(e: PointerEvent) {
85
+ if (!dragging) return
86
+ const v = valueFromPointer(e)
87
+ if (dragging === 'single') emitSingle(v)
88
+ else if (dragging === 'lo') emitRange(v, hi)
89
+ else emitRange(lo, v)
90
+ }
91
+ function onUp(e: PointerEvent) { if (dragging) { dragging = null; trackEl?.releasePointerCapture(e.pointerId) } }
92
+
93
+ function onThumbKey(e: KeyboardEvent, which: 'lo' | 'hi' | 'single') {
94
+ if (disabled) return
95
+ const cur = which === 'lo' ? lo : hi
96
+ let next = cur
97
+ if (e.key === 'ArrowRight' || e.key === 'ArrowUp') next = clampStep(cur + step)
98
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = clampStep(cur - step)
99
+ else if (e.key === 'Home') next = min
100
+ else if (e.key === 'End') next = max
101
+ else if (e.key === 'PageUp') next = clampStep(cur + step * 10)
102
+ else if (e.key === 'PageDown') next = clampStep(cur - step * 10)
103
+ else return
104
+ e.preventDefault()
105
+ if (which === 'single') emitSingle(next)
106
+ else if (which === 'lo') emitRange(next, hi)
107
+ else emitRange(lo, next)
108
+ }
109
+
110
+ const fmt = (v: number) => (formatValue ? formatValue(v) : String(v))
111
+ </script>
112
+
113
+ <div
114
+ class="sv-slider sv-slider--{orientation}"
115
+ class:is-disabled={disabled}
116
+ aria-label={ariaLabel}
117
+ >
118
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
119
+ <div
120
+ bind:this={trackEl}
121
+ class="sv-slider__track"
122
+ onpointerdown={onTrackDown}
123
+ onpointermove={onMove}
124
+ onpointerup={onUp}
125
+ onpointercancel={onUp}
126
+ >
127
+ <div
128
+ class="sv-slider__fill"
129
+ style:--a={`${range ? pct(lo) : 0}%`}
130
+ style:--b={`${range ? pct(hi) : pct(hi)}%`}
131
+ ></div>
132
+ {#each tickValues as t (t)}
133
+ <span class="sv-slider__tick" style:--p={`${pct(t)}%`}></span>
134
+ {/each}
135
+
136
+ {#if range}
137
+ <div class="sv-slider__thumb" style:--p={`${pct(lo)}%`} role="slider" tabindex={disabled ? -1 : 0} aria-valuemin={min} aria-valuemax={hi} aria-valuenow={lo} aria-label="Minimum" onkeydown={(e) => onThumbKey(e, 'lo')}>
138
+ {#if showValue}<span class="sv-slider__bubble">{fmt(lo)}</span>{/if}
139
+ </div>
140
+ <div class="sv-slider__thumb" style:--p={`${pct(hi)}%`} role="slider" tabindex={disabled ? -1 : 0} aria-valuemin={lo} aria-valuemax={max} aria-valuenow={hi} aria-label="Maximum" onkeydown={(e) => onThumbKey(e, 'hi')}>
141
+ {#if showValue}<span class="sv-slider__bubble">{fmt(hi)}</span>{/if}
142
+ </div>
143
+ {:else}
144
+ <div class="sv-slider__thumb" style:--p={`${pct(hi)}%`} role="slider" tabindex={disabled ? -1 : 0} aria-valuemin={min} aria-valuemax={max} aria-valuenow={hi} aria-label={ariaLabel ?? 'Value'} onkeydown={(e) => onThumbKey(e, 'single')}>
145
+ {#if showValue}<span class="sv-slider__bubble">{fmt(hi)}</span>{/if}
146
+ </div>
147
+ {/if}
148
+ </div>
149
+ {#if name}<input type="hidden" {name} value={range ? `${lo},${hi}` : hi} />{/if}
150
+ </div>
151
+
152
+ <style>
153
+ .sv-slider { --_accent: var(--sg-accent, #2563eb); --_track: var(--sg-border, #e2e8f0); padding: 10px; }
154
+ .sv-slider--horizontal { width: 240px; }
155
+ .sv-slider--vertical { height: 200px; width: 40px; }
156
+ .sv-slider.is-disabled { opacity: 0.5; pointer-events: none; }
157
+ .sv-slider__track {
158
+ position: relative; background: var(--_track); border-radius: 999px; cursor: pointer; touch-action: none;
159
+ }
160
+ .sv-slider--horizontal .sv-slider__track { height: 6px; }
161
+ .sv-slider--vertical .sv-slider__track { width: 6px; height: 100%; margin: 0 auto; }
162
+ .sv-slider__fill { position: absolute; background: var(--_accent); border-radius: 999px; }
163
+ .sv-slider--horizontal .sv-slider__fill { left: var(--a); right: calc(100% - var(--b)); top: 0; bottom: 0; }
164
+ .sv-slider--vertical .sv-slider__fill { bottom: var(--a); top: calc(100% - var(--b)); left: 0; right: 0; }
165
+ .sv-slider__thumb {
166
+ position: absolute; width: 16px; height: 16px; border-radius: 50%;
167
+ background: #fff; border: 2px solid var(--_accent); box-shadow: 0 1px 3px rgba(0,0,0,0.25);
168
+ cursor: grab; touch-action: none;
169
+ }
170
+ .sv-slider--horizontal .sv-slider__thumb { left: var(--p); top: 50%; transform: translate(-50%, -50%); }
171
+ .sv-slider--vertical .sv-slider__thumb { bottom: var(--p); left: 50%; transform: translate(-50%, 50%); }
172
+ .sv-slider__thumb:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
173
+ .sv-slider__thumb:active { cursor: grabbing; }
174
+ .sv-slider__bubble {
175
+ position: absolute; bottom: 130%; left: 50%; transform: translateX(-50%);
176
+ background: var(--_accent); color: var(--sg-on-accent, #fff); font-size: 11px; font-weight: 600;
177
+ padding: 2px 6px; border-radius: 5px; white-space: nowrap;
178
+ }
179
+ .sv-slider__tick { position: absolute; width: 2px; height: 2px; border-radius: 50%; background: var(--sg-muted, #94a3b8); }
180
+ .sv-slider--horizontal .sv-slider__tick { left: var(--p); top: 50%; transform: translate(-50%, -50%); }
181
+ .sv-slider--vertical .sv-slider__tick { bottom: var(--p); left: 50%; transform: translate(-50%, 50%); }
182
+ </style>
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvSwitchButton - an on/off sliding switch (ARIA `switch`). Parity: Smart
4
+ * `smart-switch-button`. Controlled via `checked` + `onChange`; keyboard
5
+ * togglable; optional inline on/off labels.
6
+ */
7
+ type Props = {
8
+ checked?: boolean
9
+ onChange?: (checked: boolean) => void
10
+ disabled?: boolean
11
+ size?: 'sm' | 'md' | 'lg'
12
+ name?: string
13
+ ariaLabel?: string
14
+ /** Optional labels rendered inside the track. */
15
+ onLabel?: string
16
+ offLabel?: string
17
+ }
18
+
19
+ let {
20
+ checked = false,
21
+ onChange,
22
+ disabled = false,
23
+ size = 'md',
24
+ name,
25
+ ariaLabel,
26
+ onLabel,
27
+ offLabel,
28
+ }: Props = $props()
29
+
30
+ function toggle() {
31
+ if (disabled) return
32
+ onChange?.(!checked)
33
+ }
34
+ function onKeydown(e: KeyboardEvent) {
35
+ if (disabled) return
36
+ if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); toggle() }
37
+ else if (e.key === 'ArrowRight') { if (!checked) onChange?.(true) }
38
+ else if (e.key === 'ArrowLeft') { if (checked) onChange?.(false) }
39
+ }
40
+ </script>
41
+
42
+ <button
43
+ type="button"
44
+ role="switch"
45
+ class="sv-switch sv-switch--{size}"
46
+ class:is-on={checked}
47
+ aria-checked={checked}
48
+ aria-label={ariaLabel}
49
+ {disabled}
50
+ onclick={toggle}
51
+ onkeydown={onKeydown}
52
+ >
53
+ <span class="sv-switch__track">
54
+ {#if onLabel || offLabel}
55
+ <span class="sv-switch__txt sv-switch__txt--on">{onLabel ?? ''}</span>
56
+ <span class="sv-switch__txt sv-switch__txt--off">{offLabel ?? ''}</span>
57
+ {/if}
58
+ <span class="sv-switch__thumb"></span>
59
+ </span>
60
+ {#if name}<input type="hidden" {name} value={checked ? 'true' : 'false'} />{/if}
61
+ </button>
62
+
63
+ <style>
64
+ .sv-switch {
65
+ --_accent: var(--sg-accent, #2563eb);
66
+ --_h: 22px; --_w: 40px;
67
+ display: inline-flex; align-items: center; background: none; border: 0; padding: 2px; cursor: pointer;
68
+ }
69
+ .sv-switch--sm { --_h: 18px; --_w: 32px; }
70
+ .sv-switch--lg { --_h: 28px; --_w: 52px; }
71
+ .sv-switch[disabled] { opacity: 0.5; cursor: not-allowed; }
72
+ .sv-switch__track {
73
+ position: relative; width: var(--_w); height: var(--_h); border-radius: 999px;
74
+ background: var(--sg-border, #cbd5e1); transition: background 0.16s;
75
+ display: inline-flex; align-items: center;
76
+ }
77
+ .sv-switch.is-on .sv-switch__track { background: var(--_accent); }
78
+ .sv-switch__thumb {
79
+ position: absolute; left: 2px; top: 50%; transform: translateY(-50%);
80
+ width: calc(var(--_h) - 4px); height: calc(var(--_h) - 4px); border-radius: 50%;
81
+ background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.3); transition: left 0.16s;
82
+ }
83
+ .sv-switch.is-on .sv-switch__thumb { left: calc(var(--_w) - var(--_h) + 2px); }
84
+ .sv-switch__txt {
85
+ position: absolute; font-size: 9px; font-weight: 700; color: #fff; text-transform: uppercase;
86
+ top: 50%; transform: translateY(-50%);
87
+ }
88
+ .sv-switch__txt--on { left: 6px; opacity: 0; }
89
+ .sv-switch__txt--off { right: 5px; color: var(--sg-muted, #64748b); }
90
+ .sv-switch.is-on .sv-switch__txt--on { opacity: 1; }
91
+ .sv-switch.is-on .sv-switch__txt--off { opacity: 0; }
92
+ .sv-switch:focus-visible { outline: none; }
93
+ .sv-switch:focus-visible .sv-switch__track { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
94
+ </style>
@@ -0,0 +1,122 @@
1
+ <script lang="ts" module>
2
+ export type TabItem = { id: string; label: string; disabled?: boolean }
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvTabs - a WAI-ARIA tabs widget with roving tabindex + arrow-key navigation.
8
+ * Parity: Smart `smart-tabs`. Controlled via `value` + `onChange`; the active
9
+ * panel is rendered through the `panel` snippet (receives the active id).
10
+ */
11
+ import type { Snippet } from 'svelte'
12
+
13
+ type Props = {
14
+ tabs: ReadonlyArray<TabItem>
15
+ value?: string
16
+ onChange?: (id: string) => void
17
+ orientation?: 'horizontal' | 'vertical'
18
+ /** 'line' underline (default) or 'pill' segmented look. */
19
+ variant?: 'line' | 'pill'
20
+ /** Activate on focus (automatic) vs. on Enter/Space (manual). Default automatic. */
21
+ activation?: 'automatic' | 'manual'
22
+ panel?: Snippet<[string]>
23
+ ariaLabel?: string
24
+ }
25
+
26
+ let {
27
+ tabs,
28
+ value = $bindable(),
29
+ onChange,
30
+ orientation = 'horizontal',
31
+ variant = 'line',
32
+ activation = 'automatic',
33
+ panel,
34
+ ariaLabel,
35
+ }: Props = $props()
36
+
37
+ const active = $derived(value ?? tabs.find((t) => !t.disabled)?.id ?? tabs[0]?.id ?? '')
38
+ const enabled = $derived(tabs.filter((t) => !t.disabled))
39
+ let tablistEl: HTMLDivElement | null = null
40
+
41
+ function select(id: string) {
42
+ const t = tabs.find((x) => x.id === id)
43
+ if (!t || t.disabled) return
44
+ value = id
45
+ onChange?.(id)
46
+ }
47
+ function focusTab(id: string) {
48
+ queueMicrotask(() => tablistEl?.querySelector<HTMLElement>(`[data-tab="${id}"]`)?.focus())
49
+ }
50
+ function onKeydown(e: KeyboardEvent) {
51
+ const idx = enabled.findIndex((t) => t.id === active)
52
+ let next: string | null = null
53
+ const fwd = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'
54
+ const back = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'
55
+ if (e.key === fwd) next = enabled[(idx + 1) % enabled.length]?.id ?? null
56
+ else if (e.key === back) next = enabled[(idx - 1 + enabled.length) % enabled.length]?.id ?? null
57
+ else if (e.key === 'Home') next = enabled[0]?.id ?? null
58
+ else if (e.key === 'End') next = enabled.at(-1)?.id ?? null
59
+ else if ((e.key === 'Enter' || e.key === ' ') && activation === 'manual') { e.preventDefault(); select(active); return }
60
+ else return
61
+ if (next) {
62
+ e.preventDefault()
63
+ focusTab(next)
64
+ if (activation === 'automatic') select(next)
65
+ }
66
+ }
67
+ </script>
68
+
69
+ <div class="sv-tabs sv-tabs--{orientation} sv-tabs--{variant}">
70
+ <div bind:this={tablistEl} class="sv-tabs__list" role="tablist" aria-orientation={orientation} aria-label={ariaLabel}>
71
+ {#each tabs as tab (tab.id)}
72
+ <button
73
+ type="button"
74
+ class="sv-tabs__tab"
75
+ class:is-active={tab.id === active}
76
+ role="tab"
77
+ data-tab={tab.id}
78
+ aria-selected={tab.id === active}
79
+ aria-controls={`panel-${tab.id}`}
80
+ tabindex={tab.id === active ? 0 : -1}
81
+ disabled={tab.disabled}
82
+ onclick={() => select(tab.id)}
83
+ onkeydown={onKeydown}
84
+ >{tab.label}</button>
85
+ {/each}
86
+ </div>
87
+ {#if panel}
88
+ <div class="sv-tabs__panel" role="tabpanel" id={`panel-${active}`} tabindex="0">
89
+ {@render panel(active)}
90
+ </div>
91
+ {/if}
92
+ </div>
93
+
94
+ <style>
95
+ .sv-tabs { --_accent: var(--sg-accent, #2563eb); display: flex; gap: 0; }
96
+ .sv-tabs--horizontal { flex-direction: column; }
97
+ .sv-tabs--vertical { flex-direction: row; }
98
+ .sv-tabs__list { display: flex; gap: 2px; }
99
+ .sv-tabs--horizontal .sv-tabs__list { border-bottom: 1px solid var(--sg-border, #e2e8f0); }
100
+ .sv-tabs--vertical .sv-tabs__list { flex-direction: column; border-right: 1px solid var(--sg-border, #e2e8f0); }
101
+ .sv-tabs--pill .sv-tabs__list { border: 0; background: var(--sg-header-bg, #f1f5f9); padding: 3px; border-radius: 9px; gap: 3px; }
102
+
103
+ .sv-tabs__tab {
104
+ background: none; border: 0; font: inherit; font-size: 13px; font-weight: 600; color: var(--sg-muted, #64748b);
105
+ padding: 9px 14px; cursor: pointer; white-space: nowrap; position: relative;
106
+ }
107
+ .sv-tabs__tab:hover:not(:disabled):not(.is-active) { color: var(--sg-fg, #0f172a); }
108
+ .sv-tabs__tab:disabled { opacity: 0.45; cursor: not-allowed; }
109
+ .sv-tabs__tab:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: -2px; border-radius: 6px; }
110
+
111
+ .sv-tabs--line .sv-tabs__tab.is-active { color: var(--_accent); }
112
+ .sv-tabs--line .sv-tabs__tab.is-active::after {
113
+ content: ''; position: absolute; left: 8px; right: 8px; height: 2px; background: var(--_accent);
114
+ }
115
+ .sv-tabs--horizontal.sv-tabs--line .sv-tabs__tab.is-active::after { bottom: -1px; }
116
+ .sv-tabs--vertical.sv-tabs--line .sv-tabs__tab.is-active::after { top: 8px; bottom: 8px; left: auto; right: -1px; width: 2px; height: auto; }
117
+
118
+ .sv-tabs--pill .sv-tabs__tab { border-radius: 7px; padding: 7px 14px; }
119
+ .sv-tabs--pill .sv-tabs__tab.is-active { background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); box-shadow: 0 1px 2px rgba(0,0,0,0.08); }
120
+
121
+ .sv-tabs__panel { padding: 16px 2px; outline: none; }
122
+ </style>
@@ -0,0 +1,81 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvTagsInput - editable token/chips input. Type + Enter/comma to add,
4
+ * Backspace to remove the last, click × to remove a chip. Parity: Smart
5
+ * `smart-tags` / `smart-input` token mode. Emits string[].
6
+ */
7
+ type Props = {
8
+ value?: string[]
9
+ onChange?: (tags: string[]) => void
10
+ placeholder?: string
11
+ disabled?: boolean
12
+ /** Reject duplicate tags. Default true. */
13
+ unique?: boolean
14
+ max?: number
15
+ name?: string
16
+ ariaLabel?: string
17
+ }
18
+
19
+ let { value = [], onChange, placeholder = 'Add tag…', disabled = false, unique = true, max = Infinity, name, ariaLabel }: Props = $props()
20
+
21
+ let draft = $state('')
22
+
23
+ function add(raw: string) {
24
+ const tag = raw.trim()
25
+ if (!tag || disabled) return
26
+ if (value.length >= max) return
27
+ if (unique && value.includes(tag)) { draft = ''; return }
28
+ onChange?.([...value, tag])
29
+ draft = ''
30
+ }
31
+ function removeAt(i: number) {
32
+ if (disabled) return
33
+ onChange?.(value.filter((_, idx) => idx !== i))
34
+ }
35
+ function onKeydown(e: KeyboardEvent) {
36
+ if (disabled) return
37
+ if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); add(draft) }
38
+ else if (e.key === 'Backspace' && draft === '' && value.length) { removeAt(value.length - 1) }
39
+ }
40
+ </script>
41
+
42
+ <div class="sv-tags" class:is-disabled={disabled} role="group" aria-label={ariaLabel}>
43
+ {#each value as tag, i (tag + i)}
44
+ <span class="sv-tags__chip">
45
+ <span class="sv-tags__label">{tag}</span>
46
+ {#if !disabled}
47
+ <button type="button" class="sv-tags__x" aria-label={`Remove ${tag}`} onclick={() => removeAt(i)}>&times;</button>
48
+ {/if}
49
+ </span>
50
+ {/each}
51
+ <input
52
+ class="sv-tags__input"
53
+ type="text"
54
+ bind:value={draft}
55
+ placeholder={value.length ? '' : placeholder}
56
+ {disabled}
57
+ aria-label={ariaLabel ?? 'Add tag'}
58
+ onkeydown={onKeydown}
59
+ onblur={() => add(draft)}
60
+ />
61
+ {#if name}<input type="hidden" {name} value={value.join(',')} />{/if}
62
+ </div>
63
+
64
+ <style>
65
+ .sv-tags {
66
+ --_accent: var(--sg-accent, #2563eb);
67
+ display: flex; flex-wrap: wrap; gap: 5px; align-items: center; width: 280px; min-height: 34px; padding: 4px 6px;
68
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
69
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
70
+ }
71
+ .sv-tags:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
72
+ .sv-tags.is-disabled { opacity: 0.6; }
73
+ .sv-tags__chip {
74
+ display: inline-flex; align-items: center; gap: 4px; height: 22px; padding: 0 4px 0 8px;
75
+ background: color-mix(in srgb, var(--_accent) 14%, transparent); color: var(--_accent);
76
+ border-radius: 5px; font-size: 12px; font-weight: 600;
77
+ }
78
+ .sv-tags__x { background: none; border: 0; color: inherit; cursor: pointer; font-size: 15px; line-height: 1; padding: 0 2px; opacity: 0.7; }
79
+ .sv-tags__x:hover { opacity: 1; }
80
+ .sv-tags__input { flex: 1; min-width: 80px; border: 0; background: none; outline: none; color: inherit; font: inherit; font-size: 13px; height: 24px; }
81
+ </style>