@svgrid/grid 1.2.22 → 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
Binary file
@@ -0,0 +1,19 @@
1
+ type Props = {
2
+ value?: string;
3
+ onChange?: (value: string, parts: {
4
+ country: string;
5
+ dial: string;
6
+ national: string;
7
+ }) => void;
8
+ /** Default country ISO code. */
9
+ country?: string;
10
+ disabled?: boolean;
11
+ readonly?: boolean;
12
+ placeholder?: string;
13
+ name?: string;
14
+ size?: 'sm' | 'md' | 'lg';
15
+ ariaLabel?: string;
16
+ };
17
+ declare const SvPhoneInput: import("svelte").Component<Props, {}, "">;
18
+ type SvPhoneInput = ReturnType<typeof SvPhoneInput>;
19
+ export default SvPhoneInput;
@@ -0,0 +1,108 @@
1
+ <script lang="ts" module>
2
+ export type RadioOption = { value: string | number; label: string; disabled?: boolean }
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvRadioGroup - an accessible radio group (WAI-ARIA radiogroup pattern) with
8
+ * roving tabindex + arrow-key navigation. Parity: Smart `smart-radio-button`
9
+ * (as a managed group). Controlled via `value` + `onChange`.
10
+ */
11
+ type Props = {
12
+ options: ReadonlyArray<RadioOption>
13
+ value?: string | number | null
14
+ onChange?: (value: string | number) => void
15
+ name?: string
16
+ disabled?: boolean
17
+ orientation?: 'vertical' | 'horizontal'
18
+ size?: 'sm' | 'md' | 'lg'
19
+ ariaLabel?: string
20
+ }
21
+
22
+ let {
23
+ options,
24
+ value = null,
25
+ onChange,
26
+ name,
27
+ disabled = false,
28
+ orientation = 'vertical',
29
+ size = 'md',
30
+ ariaLabel,
31
+ }: Props = $props()
32
+
33
+ const enabledIdx = $derived(options.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0))
34
+ const selectedIdx = $derived(options.findIndex((o) => o.value === value))
35
+ // The roving-tabindex target: the selected option, else the first enabled.
36
+ const focusIdx = $derived(selectedIdx >= 0 ? selectedIdx : enabledIdx[0] ?? -1)
37
+
38
+ function pick(i: number) {
39
+ const o = options[i]
40
+ if (!o || o.disabled || disabled) return
41
+ onChange?.(o.value)
42
+ }
43
+
44
+ function onKeydown(e: KeyboardEvent, i: number) {
45
+ if (disabled) return
46
+ const pos = enabledIdx.indexOf(i)
47
+ if (pos < 0) return
48
+ let nextPos = pos
49
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight') nextPos = (pos + 1) % enabledIdx.length
50
+ else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') nextPos = (pos - 1 + enabledIdx.length) % enabledIdx.length
51
+ else if (e.key === ' ') { e.preventDefault(); pick(i); return }
52
+ else return
53
+ e.preventDefault()
54
+ const target = enabledIdx[nextPos]!
55
+ pick(target)
56
+ ;(e.currentTarget as HTMLElement).parentElement?.querySelectorAll<HTMLElement>('.sv-radio')[target]?.focus()
57
+ }
58
+ </script>
59
+
60
+ <div
61
+ class="sv-radiogroup sv-radiogroup--{orientation} sv-radiogroup--{size}"
62
+ role="radiogroup"
63
+ aria-label={ariaLabel}
64
+ aria-disabled={disabled}
65
+ >
66
+ {#each options as opt, i (opt.value)}
67
+ <button
68
+ type="button"
69
+ role="radio"
70
+ class="sv-radio"
71
+ class:is-checked={opt.value === value}
72
+ aria-checked={opt.value === value}
73
+ disabled={disabled || opt.disabled}
74
+ tabindex={i === focusIdx ? 0 : -1}
75
+ onclick={() => pick(i)}
76
+ onkeydown={(e) => onKeydown(e, i)}
77
+ >
78
+ <span class="sv-radio__dot"></span>
79
+ <span class="sv-radio__label">{opt.label}</span>
80
+ </button>
81
+ {/each}
82
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
83
+ </div>
84
+
85
+ <style>
86
+ .sv-radiogroup { --_accent: var(--sg-accent, #2563eb); display: flex; gap: 10px; }
87
+ .sv-radiogroup--vertical { flex-direction: column; }
88
+ .sv-radiogroup--horizontal { flex-direction: row; flex-wrap: wrap; }
89
+ .sv-radio {
90
+ display: inline-flex; align-items: center; gap: 8px; background: none; border: 0; padding: 2px;
91
+ font: inherit; color: var(--sg-fg, #0f172a); cursor: pointer; user-select: none;
92
+ }
93
+ .sv-radiogroup--sm .sv-radio { font-size: 12px; }
94
+ .sv-radiogroup--md .sv-radio { font-size: 13px; }
95
+ .sv-radiogroup--lg .sv-radio { font-size: 15px; }
96
+ .sv-radio[disabled] { opacity: 0.5; cursor: not-allowed; }
97
+ .sv-radio__dot {
98
+ width: 17px; height: 17px; flex: none; border-radius: 50%;
99
+ border: 1.5px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff);
100
+ display: grid; place-items: center; transition: border-color 0.12s;
101
+ }
102
+ .sv-radio.is-checked .sv-radio__dot { border-color: var(--_accent); }
103
+ .sv-radio.is-checked .sv-radio__dot::after {
104
+ content: ''; width: 9px; height: 9px; border-radius: 50%; background: var(--_accent);
105
+ }
106
+ .sv-radio:focus-visible { outline: none; }
107
+ .sv-radio:focus-visible .sv-radio__dot { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
108
+ </style>
@@ -0,0 +1,23 @@
1
+ export type RadioOption = {
2
+ value: string | number;
3
+ label: string;
4
+ disabled?: boolean;
5
+ };
6
+ /**
7
+ * SvRadioGroup - an accessible radio group (WAI-ARIA radiogroup pattern) with
8
+ * roving tabindex + arrow-key navigation. Parity: Smart `smart-radio-button`
9
+ * (as a managed group). Controlled via `value` + `onChange`.
10
+ */
11
+ type Props = {
12
+ options: ReadonlyArray<RadioOption>;
13
+ value?: string | number | null;
14
+ onChange?: (value: string | number) => void;
15
+ name?: string;
16
+ disabled?: boolean;
17
+ orientation?: 'vertical' | 'horizontal';
18
+ size?: 'sm' | 'md' | 'lg';
19
+ ariaLabel?: string;
20
+ };
21
+ declare const SvRadioGroup: import("svelte").Component<Props, {}, "">;
22
+ type SvRadioGroup = ReturnType<typeof SvRadioGroup>;
23
+ export default SvRadioGroup;
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvRating - a star rating control (WAI-ARIA slider). Parity: Smart
4
+ * `smart-rating`. Hover preview, keyboard, optional half steps, readOnly.
5
+ * Uses the grid's `--sg-rating-*` tokens so it matches the in-grid editor.
6
+ */
7
+ type Props = {
8
+ value?: number
9
+ onChange?: (value: number) => void
10
+ max?: number
11
+ /** Allow half-star precision. */
12
+ allowHalf?: boolean
13
+ readOnly?: boolean
14
+ disabled?: boolean
15
+ size?: 'sm' | 'md' | 'lg'
16
+ name?: string
17
+ ariaLabel?: string
18
+ }
19
+
20
+ let {
21
+ value = 0,
22
+ onChange,
23
+ max = 5,
24
+ allowHalf = false,
25
+ readOnly = false,
26
+ disabled = false,
27
+ size = 'md',
28
+ name,
29
+ ariaLabel,
30
+ }: Props = $props()
31
+
32
+ let hover = $state<number | null>(null)
33
+ const isInteractive = $derived(!readOnly && !disabled)
34
+ const shown = $derived(hover ?? value)
35
+
36
+ function valueAt(e: MouseEvent, index: number): number {
37
+ if (!allowHalf) return index + 1
38
+ const el = e.currentTarget as HTMLElement
39
+ const rect = el.getBoundingClientRect()
40
+ return e.clientX - rect.left < rect.width / 2 ? index + 0.5 : index + 1
41
+ }
42
+
43
+ function fillOf(index: number): 'full' | 'half' | 'empty' {
44
+ if (shown >= index + 1) return 'full'
45
+ if (shown >= index + 0.5) return 'half'
46
+ return 'empty'
47
+ }
48
+
49
+ function onKeydown(e: KeyboardEvent) {
50
+ if (!isInteractive) return
51
+ const step = allowHalf ? 0.5 : 1
52
+ if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); onChange?.(Math.min(max, value + step)) }
53
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); onChange?.(Math.max(0, value - step)) }
54
+ else if (e.key === 'Home') { e.preventDefault(); onChange?.(0) }
55
+ else if (e.key === 'End') { e.preventDefault(); onChange?.(max) }
56
+ }
57
+ </script>
58
+
59
+ <div
60
+ class="sv-rating sv-rating--{size}"
61
+ class:is-readonly={readOnly}
62
+ class:is-disabled={disabled}
63
+ role="slider"
64
+ tabindex={isInteractive ? 0 : -1}
65
+ aria-label={ariaLabel ?? 'Rating'}
66
+ aria-valuenow={value}
67
+ aria-valuemin="0"
68
+ aria-valuemax={max}
69
+ aria-readonly={readOnly}
70
+ onkeydown={onKeydown}
71
+ onpointerleave={() => (hover = null)}
72
+ >
73
+ {#each Array(max) as _, i (i)}
74
+ <button
75
+ type="button"
76
+ class="sv-rating__star is-{fillOf(i)}"
77
+ tabindex="-1"
78
+ disabled={!isInteractive}
79
+ aria-label={`${i + 1} star${i ? 's' : ''}`}
80
+ onpointermove={(e) => { if (isInteractive) hover = valueAt(e, i) }}
81
+ onclick={(e) => { if (isInteractive) onChange?.(valueAt(e, i)) }}
82
+ >
83
+ <svg viewBox="0 0 24 24" aria-hidden="true">
84
+ <defs>
85
+ <linearGradient id="half-{i}"><stop offset="50%" stop-color="var(--_on)" /><stop offset="50%" stop-color="var(--_off)" /></linearGradient>
86
+ </defs>
87
+ <path d="M12 2.5l2.9 6.1 6.6.9-4.8 4.6 1.2 6.6L12 18.6 6.1 21.7l1.2-6.6L2.5 9.5l6.6-.9z"
88
+ fill={fillOf(i) === 'full' ? 'var(--_on)' : fillOf(i) === 'half' ? `url(#half-${i})` : 'var(--_off)'} />
89
+ </svg>
90
+ </button>
91
+ {/each}
92
+ {#if name}<input type="hidden" {name} value={value} />{/if}
93
+ </div>
94
+
95
+ <style>
96
+ .sv-rating {
97
+ --_on: var(--sg-rating-on, #f59e0b);
98
+ --_off: var(--sg-rating-empty, #d1d5db);
99
+ --_sz: 22px;
100
+ display: inline-flex; gap: 2px; align-items: center;
101
+ }
102
+ .sv-rating--sm { --_sz: 16px; }
103
+ .sv-rating--lg { --_sz: 30px; }
104
+ .sv-rating.is-disabled { opacity: 0.5; }
105
+ .sv-rating__star {
106
+ width: var(--_sz); height: var(--_sz); padding: 0; border: 0; background: none; cursor: pointer; line-height: 0;
107
+ }
108
+ .sv-rating.is-readonly .sv-rating__star, .sv-rating.is-disabled .sv-rating__star { cursor: default; }
109
+ .sv-rating__star svg { width: 100%; height: 100%; }
110
+ .sv-rating:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #2563eb)); outline-offset: 2px; border-radius: 4px; }
111
+ </style>
@@ -0,0 +1,20 @@
1
+ /**
2
+ * SvRating - a star rating control (WAI-ARIA slider). Parity: Smart
3
+ * `smart-rating`. Hover preview, keyboard, optional half steps, readOnly.
4
+ * Uses the grid's `--sg-rating-*` tokens so it matches the in-grid editor.
5
+ */
6
+ type Props = {
7
+ value?: number;
8
+ onChange?: (value: number) => void;
9
+ max?: number;
10
+ /** Allow half-star precision. */
11
+ allowHalf?: boolean;
12
+ readOnly?: boolean;
13
+ disabled?: boolean;
14
+ size?: 'sm' | 'md' | 'lg';
15
+ name?: string;
16
+ ariaLabel?: string;
17
+ };
18
+ declare const SvRating: import("svelte").Component<Props, {}, "">;
19
+ type SvRating = ReturnType<typeof SvRating>;
20
+ export default SvRating;
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvRepeatButton - fires `onclick` repeatedly while held down (after an
4
+ * initial delay, then at a steady interval). Parity: Smart `smart-repeat-button`.
5
+ * Used for spinners / steppers.
6
+ */
7
+ import type { Snippet } from 'svelte'
8
+
9
+ type Props = {
10
+ onclick?: () => void
11
+ /** Delay before repeats begin (ms). */
12
+ delay?: number
13
+ /** Interval between repeats (ms). */
14
+ interval?: number
15
+ disabled?: boolean
16
+ size?: 'sm' | 'md' | 'lg'
17
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
18
+ ariaLabel?: string
19
+ children?: Snippet
20
+ }
21
+
22
+ let {
23
+ onclick,
24
+ delay = 300,
25
+ interval = 60,
26
+ disabled = false,
27
+ size = 'md',
28
+ variant = 'secondary',
29
+ ariaLabel,
30
+ children,
31
+ }: Props = $props()
32
+
33
+ let delayTimer: ReturnType<typeof setTimeout> | null = null
34
+ let repeatTimer: ReturnType<typeof setInterval> | null = null
35
+
36
+ function stop() {
37
+ if (delayTimer) { clearTimeout(delayTimer); delayTimer = null }
38
+ if (repeatTimer) { clearInterval(repeatTimer); repeatTimer = null }
39
+ }
40
+
41
+ function start(e: PointerEvent) {
42
+ if (disabled || e.button !== 0) return
43
+ onclick?.() // fire once immediately
44
+ delayTimer = setTimeout(() => {
45
+ repeatTimer = setInterval(() => onclick?.(), Math.max(16, interval))
46
+ }, Math.max(0, delay))
47
+ }
48
+
49
+ function onKeydown(e: KeyboardEvent) {
50
+ if (disabled) return
51
+ if ((e.key === 'Enter' || e.key === ' ') && !e.repeat) { e.preventDefault(); onclick?.() }
52
+ }
53
+ </script>
54
+
55
+ <svelte:window onpointerup={stop} onpointercancel={stop} />
56
+
57
+ <button
58
+ type="button"
59
+ class="sv-btn sv-btn--{variant} sv-btn--{size} sv-repeat"
60
+ {disabled}
61
+ aria-label={ariaLabel}
62
+ onpointerdown={start}
63
+ onpointerleave={stop}
64
+ onkeydown={onKeydown}
65
+ >
66
+ {#if children}{@render children()}{/if}
67
+ </button>
68
+
69
+ <style>
70
+ /* Reuses the SvButton visual language (kept local so it works standalone). */
71
+ .sv-btn {
72
+ --_accent: var(--sg-accent, #2563eb);
73
+ --_border: var(--sg-border, #cbd5e1);
74
+ --_radius: var(--sg-radius, 8px);
75
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
76
+ font: inherit; font-weight: 600; line-height: 1; border: 1px solid transparent;
77
+ border-radius: var(--_radius); cursor: pointer; user-select: none; touch-action: none;
78
+ }
79
+ .sv-btn--sm { padding: 5px 9px; font-size: 12px; }
80
+ .sv-btn--md { padding: 8px 12px; font-size: 13px; }
81
+ .sv-btn--lg { padding: 11px 16px; font-size: 15px; }
82
+ .sv-btn[disabled] { opacity: 0.55; cursor: not-allowed; }
83
+ .sv-btn--primary { background: var(--_accent); color: var(--sg-on-accent, #fff); }
84
+ .sv-btn--secondary { background: var(--sg-header-bg, #f1f5f9); color: var(--sg-fg, #0f172a); border-color: var(--_border); }
85
+ .sv-btn--outline { background: transparent; color: var(--_accent); border-color: var(--_accent); }
86
+ .sv-btn--ghost { background: transparent; color: var(--sg-fg, #0f172a); }
87
+ .sv-btn:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: 2px; }
88
+ .sv-btn:active { transform: translateY(0.5px); }
89
+ </style>
@@ -0,0 +1,21 @@
1
+ /**
2
+ * SvRepeatButton - fires `onclick` repeatedly while held down (after an
3
+ * initial delay, then at a steady interval). Parity: Smart `smart-repeat-button`.
4
+ * Used for spinners / steppers.
5
+ */
6
+ import type { Snippet } from 'svelte';
7
+ type Props = {
8
+ onclick?: () => void;
9
+ /** Delay before repeats begin (ms). */
10
+ delay?: number;
11
+ /** Interval between repeats (ms). */
12
+ interval?: number;
13
+ disabled?: boolean;
14
+ size?: 'sm' | 'md' | 'lg';
15
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
16
+ ariaLabel?: string;
17
+ children?: Snippet;
18
+ };
19
+ declare const SvRepeatButton: import("svelte").Component<Props, {}, "">;
20
+ type SvRepeatButton = ReturnType<typeof SvRepeatButton>;
21
+ export default SvRepeatButton;
@@ -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,25 @@
1
+ /**
2
+ * SvSlider - a single or range slider (WAI-ARIA slider) with steps, ticks,
3
+ * keyboard + pointer drag. Parity: Smart `smart-slider`. Single emits number;
4
+ * range emits [number, number].
5
+ */
6
+ type Props = {
7
+ value?: number | [number, number];
8
+ onChange?: (value: any) => void;
9
+ min?: number;
10
+ max?: number;
11
+ step?: number;
12
+ range?: boolean;
13
+ /** Show evenly spaced tick marks (count or explicit values). */
14
+ ticks?: number | number[];
15
+ /** Show the value bubble above the thumb. */
16
+ showValue?: boolean;
17
+ disabled?: boolean;
18
+ orientation?: 'horizontal' | 'vertical';
19
+ name?: string;
20
+ ariaLabel?: string;
21
+ formatValue?: (v: number) => string;
22
+ };
23
+ declare const SvSlider: import("svelte").Component<Props, {}, "">;
24
+ type SvSlider = ReturnType<typeof SvSlider>;
25
+ export default SvSlider;