@svgrid/grid 2.2.16 → 2.2.17

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 (290) hide show
  1. package/README.md +1 -1
  2. package/dist/GridFooter.svelte +12 -11
  3. package/dist/SvAnchor.svelte +65 -0
  4. package/dist/SvAnchor.svelte.d.ts +24 -0
  5. package/dist/SvAspectRatio.svelte +38 -0
  6. package/dist/SvAspectRatio.svelte.d.ts +16 -0
  7. package/dist/SvAutoComplete.svelte +52 -7
  8. package/dist/SvAutoComplete.svelte.d.ts +4 -1
  9. package/dist/SvBlockquote.svelte +43 -0
  10. package/dist/SvBlockquote.svelte.d.ts +16 -0
  11. package/dist/SvCalendar.svelte +33 -2
  12. package/dist/SvCalendar.svelte.d.ts +8 -1
  13. package/dist/SvCheckBox.svelte +6 -2
  14. package/dist/SvCode.svelte +52 -0
  15. package/dist/SvCode.svelte.d.ts +19 -0
  16. package/dist/SvCollapsible.svelte +75 -0
  17. package/dist/SvCollapsible.svelte.d.ts +27 -0
  18. package/dist/SvColorInput.svelte +11 -17
  19. package/dist/SvColorInput.svelte.d.ts +2 -0
  20. package/dist/SvComboBox.svelte +85 -8
  21. package/dist/SvComboBox.svelte.d.ts +14 -0
  22. package/dist/SvCountryInput.svelte +2 -2
  23. package/dist/SvCountryInput.svelte.d.ts +1 -1
  24. package/dist/SvDropDownList.svelte +154 -51
  25. package/dist/SvDropDownList.svelte.d.ts +17 -3
  26. package/dist/SvDurationInput.svelte +59 -41
  27. package/dist/SvDurationInput.svelte.d.ts +18 -0
  28. package/dist/SvField.svelte +203 -12
  29. package/dist/SvField.svelte.d.ts +45 -8
  30. package/dist/SvForm.svelte +195 -21
  31. package/dist/SvForm.svelte.d.ts +23 -0
  32. package/dist/SvGrid.controller.svelte.d.ts +12 -1
  33. package/dist/SvGrid.controller.svelte.js +87 -7
  34. package/dist/SvGrid.svelte +383 -181
  35. package/dist/SvGrid.types.d.ts +123 -2
  36. package/dist/SvGridChartView.svelte +70 -0
  37. package/dist/SvGridChartView.svelte.d.ts +11 -0
  38. package/dist/SvGridDropdown.svelte +9 -2
  39. package/dist/SvGridSelect.svelte +5 -2
  40. package/dist/SvGridSelect.svelte.d.ts +1 -1
  41. package/dist/SvGroup.svelte +41 -0
  42. package/dist/SvGroup.svelte.d.ts +27 -0
  43. package/dist/SvHoverCard.svelte +70 -0
  44. package/dist/SvHoverCard.svelte.d.ts +41 -0
  45. package/dist/SvKbd.svelte +51 -0
  46. package/dist/SvKbd.svelte.d.ts +18 -0
  47. package/dist/SvList.svelte +55 -0
  48. package/dist/SvList.svelte.d.ts +23 -0
  49. package/dist/SvListBox.svelte +166 -77
  50. package/dist/SvListBox.svelte.d.ts +17 -4
  51. package/dist/SvLoadingOverlay.svelte +53 -0
  52. package/dist/SvLoadingOverlay.svelte.d.ts +28 -0
  53. package/dist/SvMark.svelte +36 -0
  54. package/dist/SvMark.svelte.d.ts +14 -0
  55. package/dist/SvMaskedInput.svelte +50 -35
  56. package/dist/SvMaskedInput.svelte.d.ts +17 -2
  57. package/dist/SvMenuList.svelte +32 -3
  58. package/dist/SvMenubar.svelte +200 -0
  59. package/dist/SvMenubar.svelte.d.ts +18 -0
  60. package/dist/SvMultiSelect.svelte +7 -4
  61. package/dist/SvMultiSelect.svelte.d.ts +1 -1
  62. package/dist/SvNumberInput.svelte +79 -42
  63. package/dist/SvNumberInput.svelte.d.ts +25 -0
  64. package/dist/SvOtpInput.svelte +2 -2
  65. package/dist/SvPasswordInput.svelte +51 -31
  66. package/dist/SvPasswordInput.svelte.d.ts +17 -0
  67. package/dist/SvPhoneInput.svelte +39 -28
  68. package/dist/SvPhoneInput.svelte.d.ts +4 -0
  69. package/dist/SvPopconfirm.svelte +79 -0
  70. package/dist/SvPopconfirm.svelte.d.ts +42 -0
  71. package/dist/SvPopover.svelte +87 -29
  72. package/dist/SvPopover.svelte.d.ts +18 -4
  73. package/dist/SvRadioGroup.svelte +5 -1
  74. package/dist/SvResult.svelte +73 -0
  75. package/dist/SvResult.svelte.d.ts +28 -0
  76. package/dist/SvSegmented.svelte +114 -0
  77. package/dist/SvSegmented.svelte.d.ts +29 -0
  78. package/dist/SvSimpleGrid.svelte +41 -0
  79. package/dist/SvSimpleGrid.svelte.d.ts +25 -0
  80. package/dist/SvSlider.svelte +23 -10
  81. package/dist/SvSpinner.svelte +43 -0
  82. package/dist/SvSpinner.svelte.d.ts +20 -0
  83. package/dist/SvStack.svelte +37 -0
  84. package/dist/SvStack.svelte.d.ts +24 -0
  85. package/dist/SvSwitchButton.svelte +6 -1
  86. package/dist/SvTagsInput.svelte +9 -5
  87. package/dist/SvText.svelte +76 -0
  88. package/dist/SvText.svelte.d.ts +31 -0
  89. package/dist/SvTextInput.svelte +75 -43
  90. package/dist/SvTextInput.svelte.d.ts +28 -1
  91. package/dist/SvTimePicker.svelte +32 -2
  92. package/dist/SvTimePicker.svelte.d.ts +8 -9
  93. package/dist/SvTitle.svelte +57 -0
  94. package/dist/SvTitle.svelte.d.ts +24 -0
  95. package/dist/SvToaster.svelte +41 -6
  96. package/dist/SvToggleButton.svelte +4 -1
  97. package/dist/SvTooltip.svelte +68 -34
  98. package/dist/SvTooltip.svelte.d.ts +14 -2
  99. package/dist/SvTree.svelte +81 -21
  100. package/dist/SvTree.svelte.d.ts +3 -2
  101. package/dist/SvTreeSelect.svelte +5 -2
  102. package/dist/SvTreeSelect.svelte.d.ts +1 -1
  103. package/dist/SvVisuallyHidden.svelte +49 -0
  104. package/dist/SvVisuallyHidden.svelte.d.ts +19 -0
  105. package/dist/ai.d.ts +289 -0
  106. package/dist/ai.js +1013 -0
  107. package/dist/board-view.svelte.d.ts +22 -0
  108. package/dist/board-view.svelte.js +13 -0
  109. package/dist/build-api.js +36 -6
  110. package/dist/cdn/GridMenus-0AK9O7qG.js +420 -0
  111. package/dist/cdn/GridMenus-BjUoCXoD.js +416 -0
  112. package/dist/cdn/SvDateTimePicker-CYC7gRER.js +2176 -0
  113. package/dist/cdn/SvDateTimePicker-DtQKd4Ns.js +2198 -0
  114. package/dist/cdn/SvGridChart-BcLCYvw9.js +1195 -0
  115. package/dist/cdn/SvGridChart-BhnFdLd0.js +1203 -0
  116. package/dist/cdn/SvGridChartPanel-Cmx9DHAS.js +561 -0
  117. package/dist/cdn/SvGridChartPanel-DvjwxLvK.js +577 -0
  118. package/dist/cdn/SvGridChartView-B40DTJSX.js +54 -0
  119. package/dist/cdn/SvGridChartView-PwqI4s02.js +53 -0
  120. package/dist/cdn/SvGridDropdown-DjALc_4f.js +255 -0
  121. package/dist/cdn/SvGridDropdown-hpnHvOGt.js +254 -0
  122. package/dist/cdn/chart-T1usDN0o.js +1212 -0
  123. package/dist/cdn/disclose-version-DoD9AFD_.js +2958 -0
  124. package/dist/cdn/export-format-JT5pWVT8.js +561 -0
  125. package/dist/cdn/popover-P-9P2YYn.js +69 -0
  126. package/dist/cdn/rolldown-runtime-Dy4uBu1J.js +11 -0
  127. package/dist/cdn/src-BLhdLmoN.js +25707 -0
  128. package/dist/cdn/src-BwnUVj3T.js +25623 -0
  129. package/dist/cdn/svgrid.js +9 -32897
  130. package/dist/cdn/svgrid.svelte-external.js +9 -30052
  131. package/dist/cell-formatting.d.ts +2 -2
  132. package/dist/cell-render.js +4 -4
  133. package/dist/chart-view.svelte.d.ts +25 -0
  134. package/dist/chart-view.svelte.js +14 -0
  135. package/dist/createCombobox.svelte.d.ts +5 -0
  136. package/dist/createCombobox.svelte.js +21 -5
  137. package/dist/createDropdownList.svelte.d.ts +3 -0
  138. package/dist/createDropdownList.svelte.js +8 -2
  139. package/dist/createForm.svelte.d.ts +18 -0
  140. package/dist/createForm.svelte.js +51 -6
  141. package/dist/createMenu.svelte.js +27 -0
  142. package/dist/createPopoverSelect.svelte.d.ts +3 -0
  143. package/dist/createPopoverSelect.svelte.js +9 -1
  144. package/dist/createSlider.svelte.d.ts +8 -21
  145. package/dist/createSlider.svelte.js +34 -0
  146. package/dist/createTooltip.svelte.d.ts +12 -0
  147. package/dist/createTooltip.svelte.js +72 -9
  148. package/dist/editor-contract.d.ts +15 -0
  149. package/dist/export-provider.d.ts +27 -0
  150. package/dist/export-provider.js +9 -0
  151. package/dist/filter-operators.d.ts +6 -0
  152. package/dist/filter-operators.js +33 -0
  153. package/dist/form-field.d.ts +45 -2
  154. package/dist/grid-messages.d.ts +61 -0
  155. package/dist/grid-messages.js +72 -0
  156. package/dist/group-display.d.ts +62 -0
  157. package/dist/group-display.js +78 -0
  158. package/dist/index.d.ts +36 -4
  159. package/dist/index.js +43 -2
  160. package/dist/js-scroller.svelte.d.ts +35 -0
  161. package/dist/js-scroller.svelte.js +154 -0
  162. package/dist/list-option.d.ts +46 -0
  163. package/dist/list-option.js +53 -0
  164. package/dist/menus.js +25 -11
  165. package/dist/pivot-view.svelte.d.ts +72 -0
  166. package/dist/pivot-view.svelte.js +13 -0
  167. package/dist/positioning.d.ts +111 -0
  168. package/dist/positioning.js +191 -0
  169. package/dist/scheduler-model.d.ts +3 -0
  170. package/dist/scheduler-model.js +22 -5
  171. package/dist/svgrid-wrapper.types.d.ts +18 -0
  172. package/dist/toast-store.svelte.d.ts +54 -0
  173. package/dist/toast-store.svelte.js +65 -15
  174. package/package.json +9 -1
  175. package/src/GridFooter.svelte +12 -11
  176. package/src/SvAnchor.svelte +65 -0
  177. package/src/SvAspectRatio.svelte +38 -0
  178. package/src/SvAutoComplete.svelte +52 -7
  179. package/src/SvBlockquote.svelte +43 -0
  180. package/src/SvCalendar.svelte +33 -2
  181. package/src/SvCalendar.test.ts +15 -0
  182. package/src/SvCheckBox.svelte +6 -2
  183. package/src/SvCode.svelte +52 -0
  184. package/src/SvCollapsible.svelte +75 -0
  185. package/src/SvColorInput.svelte +11 -17
  186. package/src/SvComboBox.svelte +85 -8
  187. package/src/SvCountryInput.svelte +2 -2
  188. package/src/SvDropDownList.svelte +154 -51
  189. package/src/SvDurationInput.svelte +59 -41
  190. package/src/SvField.svelte +203 -12
  191. package/src/SvForm.svelte +195 -21
  192. package/src/SvForm.test.ts +112 -0
  193. package/src/SvGrid.controller.svelte.ts +88 -7
  194. package/src/SvGrid.svelte +383 -181
  195. package/src/SvGrid.types.ts +125 -1
  196. package/src/SvGridChartView.svelte +70 -0
  197. package/src/SvGridDropdown.svelte +9 -2
  198. package/src/SvGridSelect.svelte +5 -2
  199. package/src/SvGroup.svelte +41 -0
  200. package/src/SvHoverCard.svelte +70 -0
  201. package/src/SvKbd.svelte +51 -0
  202. package/src/SvList.svelte +55 -0
  203. package/src/SvListBox.svelte +166 -77
  204. package/src/SvLoadingOverlay.svelte +53 -0
  205. package/src/SvMark.svelte +36 -0
  206. package/src/SvMaskedInput.svelte +50 -35
  207. package/src/SvMenuList.svelte +32 -3
  208. package/src/SvMenubar.svelte +200 -0
  209. package/src/SvMultiSelect.svelte +7 -4
  210. package/src/SvNumberInput.svelte +79 -42
  211. package/src/SvOtpInput.svelte +2 -2
  212. package/src/SvPasswordInput.svelte +51 -31
  213. package/src/SvPhoneInput.svelte +39 -28
  214. package/src/SvPopconfirm.svelte +79 -0
  215. package/src/SvPopover.svelte +87 -29
  216. package/src/SvRadioGroup.svelte +5 -1
  217. package/src/SvResult.svelte +73 -0
  218. package/src/SvSegmented.svelte +114 -0
  219. package/src/SvSimpleGrid.svelte +41 -0
  220. package/src/SvSlider.svelte +23 -10
  221. package/src/SvSpinner.svelte +43 -0
  222. package/src/SvStack.svelte +37 -0
  223. package/src/SvSwitchButton.svelte +6 -1
  224. package/src/SvTagsInput.svelte +9 -5
  225. package/src/SvText.svelte +76 -0
  226. package/src/SvTextInput.svelte +75 -43
  227. package/src/SvTimePicker.svelte +32 -2
  228. package/src/SvTitle.svelte +57 -0
  229. package/src/SvToaster.svelte +41 -6
  230. package/src/SvToaster.test.ts +32 -0
  231. package/src/SvToggleButton.svelte +4 -1
  232. package/src/SvTooltip.svelte +68 -34
  233. package/src/SvTree.svelte +81 -21
  234. package/src/SvTreeSelect.svelte +5 -2
  235. package/src/SvVisuallyHidden.svelte +49 -0
  236. package/src/ai.test.ts +502 -0
  237. package/src/ai.ts +1391 -0
  238. package/src/board-view.svelte.ts +33 -0
  239. package/src/build-api.coverage.test.ts +101 -0
  240. package/src/build-api.ts +43 -18
  241. package/src/builtin-editors.grid.test.ts +1 -1
  242. package/src/cell-formatting.ts +5 -5
  243. package/src/cell-render.ts +4 -4
  244. package/src/chart-view.svelte.ts +36 -0
  245. package/src/createCombobox.svelte.ts +17 -5
  246. package/src/createDropdownList.svelte.ts +8 -2
  247. package/src/createForm.svelte.ts +59 -6
  248. package/src/createMenu.svelte.ts +23 -0
  249. package/src/createPopoverSelect.svelte.ts +8 -1
  250. package/src/createSlider.svelte.ts +20 -0
  251. package/src/createTooltip.svelte.ts +67 -9
  252. package/src/editor-contract.ts +16 -0
  253. package/src/export-provider.ts +40 -0
  254. package/src/filter-operators.ts +37 -0
  255. package/src/form-field.ts +42 -2
  256. package/src/grid-messages.test.ts +24 -0
  257. package/src/grid-messages.ts +137 -0
  258. package/src/group-display.test.ts +77 -0
  259. package/src/group-display.ts +117 -0
  260. package/src/i18n.grid.test.ts +73 -0
  261. package/src/index.ts +91 -2
  262. package/src/js-scroller.svelte.ts +173 -0
  263. package/src/list-option.test.ts +56 -0
  264. package/src/list-option.ts +81 -0
  265. package/src/menus.test.ts +32 -0
  266. package/src/menus.ts +25 -12
  267. package/src/pivot-view.svelte.ts +90 -0
  268. package/src/pivot-view.test.ts +38 -0
  269. package/src/pivot.grid.test.ts +102 -0
  270. package/src/positioning.test.ts +141 -0
  271. package/src/positioning.ts +266 -0
  272. package/src/scheduler-model.test.ts +16 -0
  273. package/src/scheduler-model.ts +24 -5
  274. package/src/svgrid-wrapper.types.ts +23 -0
  275. package/src/svgrid.charting.test.ts +8 -1
  276. package/src/svgrid.comments-autocomplete.test.ts +7 -2
  277. package/src/svgrid.context-menu.test.ts +8 -3
  278. package/src/toast-store.svelte.ts +98 -0
  279. package/src/toast-store.test.ts +62 -1
  280. package/src/ui-breadth.test.ts +138 -0
  281. package/src/ui-buttons.test.ts +39 -0
  282. package/src/ui-inputs.test.ts +2 -2
  283. package/src/ui-range.test.ts +9 -0
  284. package/src/ui-selection.test.ts +36 -0
  285. package/src/ui-tier1.test.ts +1 -1
  286. package/src/virtual.test.ts +20 -6
  287. package/dist/SvGridBoard.svelte +0 -2339
  288. package/dist/SvGridBoard.svelte.d.ts +0 -31
  289. package/src/SvGridBoard.svelte +0 -2339
  290. package/src/board.test.ts +0 -927
@@ -24,6 +24,10 @@
24
24
  placeholder?: string
25
25
  /** Override the built-in strings (e.g. the country selector aria-label). */
26
26
  messages?: Partial<PhoneMessages>
27
+ /** Stretch to the container width. */
28
+ block?: boolean
29
+ /** Control width in px (ignored when `block`). Default 250. */
30
+ width?: number
27
31
  }
28
32
 
29
33
  let {
@@ -43,7 +47,10 @@
43
47
  hint,
44
48
  dir,
45
49
  id,
50
+ loading = false,
46
51
  messages,
52
+ block = false,
53
+ width = 250,
47
54
  }: Props = $props()
48
55
 
49
56
  const autoId = nextEditorId('sv-phone')
@@ -68,53 +75,57 @@
68
75
  })
69
76
  </script>
70
77
 
71
- <SvField id={uid} {label} {hint} {error} {required} {dir}>
72
- <div class="sv-phone sv-phone--{size}" class:is-disabled={disabled} class:is-invalid={invalid}>
73
- <div class="sv-phone__country">
78
+ {#snippet countryChip()}
79
+ <span class="sv-phone__country">
74
80
  <span class="sv-phone__flag" aria-hidden="true">{ph.flag}</span>
75
81
  <span class="sv-phone__dial">{ph.dial}</span>
82
+ <svg class="sv-phone__chev" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
76
83
  <select class="sv-phone__select" {...ph.selectProps()}>
77
84
  {#each COUNTRIES as c (c.code)}
78
85
  <option value={c.code}>{flagEmoji(c.code)} {c.name} ({c.dial})</option>
79
86
  {/each}
80
87
  </select>
81
- <svg class="sv-phone__chev" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
82
- </div>
88
+ </span>
89
+ {/snippet}
90
+
91
+ <SvField
92
+ frame
93
+ id={uid}
94
+ {label}
95
+ {hint}
96
+ {error}
97
+ {required}
98
+ {dir}
99
+ {size}
100
+ {invalid}
101
+ {disabled}
102
+ {readonly}
103
+ {loading}
104
+ {block}
105
+ {width}
106
+ leading={countryChip}
107
+ >
83
108
  <input class="sv-phone__number" {...ph.inputProps()} />
84
109
  {#if name}<input type="hidden" {name} value={ph.value} />{/if}
85
- </div>
86
110
  </SvField>
87
111
 
88
112
  <style>
89
- .sv-phone {
90
- --_accent: var(--sg-accent, #2563eb);
91
- display: inline-flex; align-items: stretch; width: 250px;
92
- background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
93
- border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
94
- overflow: hidden;
95
- }
96
- .sv-phone:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
97
- .sv-phone.is-invalid { border-color: var(--sg-danger, #dc2626); }
98
- .sv-phone.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
99
- .sv-phone.is-disabled { opacity: 0.6; }
113
+ /* A compact country chip in the leading slot: flag + dial + chevron, with the
114
+ native <select> as an invisible overlay covering it. */
100
115
  .sv-phone__country {
101
116
  position: relative; display: inline-flex; align-items: center; gap: 4px;
102
- padding-block: 0; padding-inline: 10px 6px; border-inline-end: 1px solid var(--sg-border, #e2e8f0);
103
- background: var(--sg-header-bg, #f8fafc); cursor: pointer;
117
+ padding-inline-end: 8px; border-inline-end: 1px solid var(--sg-border, #e2e8f0); cursor: pointer;
104
118
  }
105
119
  .sv-phone__flag { font-size: 15px; }
106
120
  .sv-phone__dial { font-size: 13px; font-weight: 600; }
107
121
  .sv-phone__chev { color: var(--sg-muted, #64748b); }
108
122
  /* The native <select> is an invisible overlay, but its option popup is drawn by
109
- the OS using the control's own color/background - so theme them explicitly or
110
- dark themes get light text on the default white popup. */
111
- .sv-phone__select {
112
- position: absolute; inset: 0; opacity: 0; cursor: pointer; font-size: 13px;
123
+ the OS using the control's own color/background - so theme them explicitly (a
124
+ 2-class selector, so it beats SvField frame's generic `select` reset) or dark
125
+ themes get light text on the default white popup. */
126
+ .sv-phone__country .sv-phone__select {
127
+ position: absolute; inset: 0; opacity: 0; cursor: pointer; font-size: 13px; padding: 0;
113
128
  color: var(--sg-fg, #0f172a); background: var(--sg-bg, #fff);
114
129
  }
115
- .sv-phone__select option { color: var(--sg-fg, #0f172a); background: var(--sg-bg, #fff); }
116
- .sv-phone__number { flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit; padding: 0 10px; }
117
- .sv-phone--sm { height: 28px; font-size: 12px; }
118
- .sv-phone--md { height: 34px; font-size: 13px; }
119
- .sv-phone--lg { height: 40px; font-size: 15px; }
130
+ .sv-phone__country .sv-phone__select option { color: var(--sg-fg, #0f172a); background: var(--sg-bg, #fff); }
120
131
  </style>
@@ -0,0 +1,79 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvPopconfirm - a lightweight confirm step in a popover anchored to its
4
+ * trigger, for a low-stakes destructive action ("Delete this row?") where a
5
+ * full modal dialog would be too heavy. Click the anchor to reveal a short
6
+ * message plus Cancel / Confirm buttons; confirming runs `onConfirm` and closes.
7
+ * Built on SvPopover, so it is portalled, positioned by the shared engine, and
8
+ * dismisses on Escape / outside-click.
9
+ *
10
+ * ```svelte
11
+ * <SvPopconfirm title="Delete this row?" confirmVariant="danger"
12
+ * onConfirm={() => remove(row)}>
13
+ * {#snippet anchor()}<SvButton variant="danger" size="sm">Delete</SvButton>{/snippet}
14
+ * </SvPopconfirm>
15
+ * ```
16
+ */
17
+ import type { Snippet } from 'svelte'
18
+ import SvPopover from './SvPopover.svelte'
19
+ import SvButton from './SvButton.svelte'
20
+ import type { Placement } from './positioning'
21
+
22
+ type Props = {
23
+ /** Controlled open state (bindable). */
24
+ open?: boolean
25
+ onOpenChange?: (open: boolean) => void
26
+ /** Bold prompt, e.g. "Delete this row?". */
27
+ title?: string
28
+ /** Optional longer explanation under the title. */
29
+ description?: string
30
+ confirmLabel?: string
31
+ cancelLabel?: string
32
+ /** Variant for the confirm button (use `danger` for destructive actions). */
33
+ confirmVariant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
34
+ /** Runs when the user confirms; the popover then closes. */
35
+ onConfirm?: () => void
36
+ /** Runs when the user cancels (or dismisses); the popover then closes. */
37
+ onCancel?: () => void
38
+ /** Preferred placement; flips when there is no room. Default `top`. */
39
+ placement?: Placement
40
+ ariaLabel?: string
41
+ /** The trigger that opens the confirm. */
42
+ anchor?: Snippet
43
+ }
44
+
45
+ let {
46
+ open = $bindable(false),
47
+ onOpenChange,
48
+ title,
49
+ description,
50
+ confirmLabel = 'Confirm',
51
+ cancelLabel = 'Cancel',
52
+ confirmVariant = 'primary',
53
+ onConfirm,
54
+ onCancel,
55
+ placement = 'top',
56
+ ariaLabel,
57
+ anchor,
58
+ }: Props = $props()
59
+
60
+ function confirm() { onConfirm?.(); open = false }
61
+ function cancel() { onCancel?.(); open = false }
62
+ </script>
63
+
64
+ <SvPopover bind:open {onOpenChange} trigger="click" {placement} minWidth={230} ariaLabel={ariaLabel ?? title} {anchor}>
65
+ <div class="sv-popconfirm">
66
+ {#if title}<div class="sv-popconfirm__title">{title}</div>{/if}
67
+ {#if description}<div class="sv-popconfirm__desc">{description}</div>{/if}
68
+ <div class="sv-popconfirm__actions">
69
+ <SvButton size="sm" variant="ghost" onclick={cancel}>{cancelLabel}</SvButton>
70
+ <SvButton size="sm" variant={confirmVariant} onclick={confirm}>{confirmLabel}</SvButton>
71
+ </div>
72
+ </div>
73
+ </SvPopover>
74
+
75
+ <style>
76
+ .sv-popconfirm__title { font-weight: 650; font-size: 13px; color: var(--sg-fg, #0f172a); }
77
+ .sv-popconfirm__desc { margin-top: 4px; font-size: 12.5px; color: var(--sg-muted, #64748b); line-height: 1.45; }
78
+ .sv-popconfirm__actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
79
+ </style>
@@ -2,18 +2,22 @@
2
2
  /**
3
3
  * SvPopover - a floating panel anchored to a trigger, portalled to <body> so it
4
4
  * escapes any `overflow:hidden` ancestor. Open on click (default), hover, or
5
- * manually via `open`. Flips above the trigger when there's no room below,
6
- * closes on outside-click / Escape, and animates in. Parity: Smart popover.
5
+ * manually via `open`. Positioned by the shared engine (`computePosition`): full
6
+ * placement matrix + flip + shift + an optional arrow, kept in sync with
7
+ * `autoUpdate`. Closes on outside-click / Escape via the dismissable layer
8
+ * stack, and animates in. The panel is hoverable (interactive) under the hover
9
+ * trigger. Parity: Smart popover.
7
10
  *
8
11
  * ```svelte
9
- * <SvPopover>
12
+ * <SvPopover placement="bottom" arrow>
10
13
  * {#snippet anchor()}<button>Open</button>{/snippet}
11
14
  * <p>Panel content</p>
12
15
  * </SvPopover>
13
16
  * ```
14
17
  */
15
18
  import type { Snippet } from 'svelte'
16
- import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
19
+ import { portalToBody, popIn } from './popover'
20
+ import { computePosition, autoUpdate, type Placement, type ComputePositionResult } from './positioning'
17
21
  import { createDismissableLayer } from './a11y/dismissable'
18
22
 
19
23
  type Props = {
@@ -22,7 +26,17 @@
22
26
  onOpenChange?: (open: boolean) => void
23
27
  /** How the anchor opens the panel. `manual` = only `open` drives it. */
24
28
  trigger?: 'click' | 'hover' | 'manual'
25
- /** Estimated panel height for flip decisions. */
29
+ /** Preferred placement (flips when there is no room). Default `bottom-start`. */
30
+ placement?: Placement
31
+ /** Main-axis gap between trigger and panel. Default 8 (room for the arrow). */
32
+ offset?: number
33
+ /** Render a pointer arrow toward the trigger. Default true. */
34
+ arrow?: boolean
35
+ /** Hover trigger: delay (ms) before opening. Default 0. */
36
+ openDelay?: number
37
+ /** Hover trigger: delay (ms) before closing after leave. Default 120. */
38
+ closeDelay?: number
39
+ /** Estimated panel height, used before the panel has been measured. */
26
40
  estimatedHeight?: number
27
41
  /** Force a minimum panel width (else it hugs its content). */
28
42
  minWidth?: number
@@ -39,6 +53,11 @@
39
53
  open = $bindable(false),
40
54
  onOpenChange,
41
55
  trigger = 'click',
56
+ placement = 'bottom-start',
57
+ offset = 8,
58
+ arrow = true,
59
+ openDelay = 0,
60
+ closeDelay = 120,
42
61
  estimatedHeight = 220,
43
62
  minWidth,
44
63
  closeOnOutsideClick = true,
@@ -47,43 +66,60 @@
47
66
  children,
48
67
  }: Props = $props()
49
68
 
69
+ const ARROW = 10
70
+
50
71
  let anchorEl = $state<HTMLSpanElement | null>(null)
51
72
  let panelEl = $state<HTMLDivElement | null>(null)
52
- let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
73
+ let pos = $state<ComputePositionResult>({
74
+ x: 0, y: 0, placement, side: 'bottom', align: 'start', maxWidth: 0, maxHeight: 0,
75
+ })
53
76
 
54
77
  function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
55
78
  function toggle() { setOpen(!open) }
56
79
 
57
80
  function updatePos() {
58
81
  if (!anchorEl) return
59
- rect = anchoredRect(anchorEl.getBoundingClientRect(), { estimatedHeight, minWidth })
82
+ const r = anchorEl.getBoundingClientRect()
83
+ const f = panelEl?.getBoundingClientRect()
84
+ const floating = {
85
+ width: f?.width || minWidth || r.width,
86
+ height: f?.height || estimatedHeight,
87
+ }
88
+ pos = computePosition(
89
+ { x: r.left, y: r.top, width: r.width, height: r.height },
90
+ floating,
91
+ { placement, offset, padding: 8, minMainAxis: 96, arrow: arrow ? { size: ARROW } : undefined },
92
+ )
60
93
  }
61
94
 
95
+ // Reposition whenever the trigger/panel resize or an ancestor scrolls, and wire
96
+ // Escape + outside-click dismissal via the shared layer stack (so a popover
97
+ // nested in a dialog closes top-first). Re-runs once `panelEl` mounts.
62
98
  $effect(() => {
63
- if (!open) return
64
- updatePos()
65
- const rp = () => updatePos()
66
- window.addEventListener('scroll', rp, true)
67
- window.addEventListener('resize', rp)
68
- // Escape + outside-click dismissal via the shared layer stack (so a popover
69
- // nested in a dialog closes top-first). The trigger is passed alongside the
70
- // panel so clicking it toggles rather than dismiss-then-reopen.
99
+ if (!open || !anchorEl || !panelEl) return
100
+ const stop = autoUpdate(anchorEl, panelEl, updatePos)
71
101
  const layer = createDismissableLayer({
72
102
  element: () => [anchorEl, panelEl],
73
103
  onDismiss: () => setOpen(false),
74
104
  closeOnOutside: closeOnOutsideClick,
75
105
  })
76
106
  layer.activate()
77
- return () => {
78
- window.removeEventListener('scroll', rp, true)
79
- window.removeEventListener('resize', rp)
80
- layer.release()
81
- }
107
+ return () => { stop(); layer.release() }
82
108
  })
83
109
 
84
- let hoverTimer: ReturnType<typeof setTimeout> | undefined
85
- function onEnter() { if (trigger === 'hover') { clearTimeout(hoverTimer); setOpen(true) } }
86
- function onLeave() { if (trigger === 'hover') { hoverTimer = setTimeout(() => setOpen(false), 120) } }
110
+ let openTimer: ReturnType<typeof setTimeout> | undefined
111
+ let closeTimer: ReturnType<typeof setTimeout> | undefined
112
+ function onEnter() {
113
+ if (trigger !== 'hover') return
114
+ clearTimeout(closeTimer)
115
+ if (open) return
116
+ openTimer = setTimeout(() => setOpen(true), openDelay)
117
+ }
118
+ function onLeave() {
119
+ if (trigger !== 'hover') return
120
+ clearTimeout(openTimer)
121
+ closeTimer = setTimeout(() => setOpen(false), closeDelay)
122
+ }
87
123
 
88
124
  /** Fallback accessible name for the panel when `ariaLabel` isn't given: the
89
125
  * anchor's own aria-label, else its trimmed text content. */
@@ -112,11 +148,11 @@
112
148
  bind:this={panelEl}
113
149
  class="sv-pop__panel"
114
150
  use:portalToBody
115
- use:popIn={{ up: rect.openUpward }}
151
+ use:popIn={{ up: pos.side === 'top' }}
116
152
  style:position="fixed"
117
- style:top={`${rect.top}px`}
118
- style:left={`${rect.left}px`}
119
- style:min-width={minWidth ? `${rect.width}px` : undefined}
153
+ style:top={`${pos.y}px`}
154
+ style:left={`${pos.x}px`}
155
+ style:min-width={minWidth ? `${minWidth}px` : undefined}
120
156
  role="dialog"
121
157
  aria-label={ariaLabel ?? anchorFallbackLabel()}
122
158
  onpointerenter={onEnter}
@@ -124,16 +160,38 @@
124
160
  onfocusin={onEnter}
125
161
  onfocusout={onLeave}
126
162
  >
127
- {@render children?.()}
163
+ {#if arrow}
164
+ <span
165
+ class="sv-pop__arrow sv-pop__arrow--{pos.side}"
166
+ style:left={pos.arrow?.x != null ? `${pos.arrow.x}px` : undefined}
167
+ style:top={pos.arrow?.y != null ? `${pos.arrow.y}px` : undefined}
168
+ ></span>
169
+ {/if}
170
+ <div class="sv-pop__content" style:max-height={pos.maxHeight ? `${pos.maxHeight}px` : undefined}>
171
+ {@render children?.()}
172
+ </div>
128
173
  </div>
129
174
  {/if}
130
175
 
131
176
  <style>
132
177
  .sv-pop__anchor { display: inline-flex; }
133
178
  :global(.sv-pop__panel) {
134
- z-index: 2147483646; max-width: min(92vw, 360px); padding: 12px 14px;
179
+ z-index: 2147483646; max-width: min(92vw, 360px);
135
180
  background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
136
181
  border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
137
182
  box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px; line-height: 1.55;
138
183
  }
184
+ :global(.sv-pop__content) { padding: 12px 14px; overflow: auto; }
185
+ /* Arrow: a rotated square straddling the panel edge, two borders showing so it
186
+ reads as a continuation of the panel outline. Not clipped (sibling of the
187
+ scrollable content, panel itself has no overflow). */
188
+ :global(.sv-pop__arrow) {
189
+ position: absolute; width: 10px; height: 10px;
190
+ background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
191
+ transform: rotate(45deg);
192
+ }
193
+ :global(.sv-pop__arrow--bottom) { top: -6px; border-right: 0; border-bottom: 0; }
194
+ :global(.sv-pop__arrow--top) { bottom: -6px; border-left: 0; border-top: 0; }
195
+ :global(.sv-pop__arrow--right) { left: -6px; border-top: 0; border-right: 0; }
196
+ :global(.sv-pop__arrow--left) { right: -6px; border-bottom: 0; border-left: 0; }
139
197
  </style>
@@ -25,6 +25,7 @@
25
25
  onChange,
26
26
  name,
27
27
  disabled = false,
28
+ readonly = false,
28
29
  orientation = 'vertical',
29
30
  size = 'md',
30
31
  ariaLabel,
@@ -44,7 +45,7 @@
44
45
  const rg = createRadioGroup({
45
46
  options: () => options,
46
47
  value: () => value,
47
- onChange: (v) => onChange?.(v),
48
+ onChange: (v) => { if (readonly) return; onChange?.(v) },
48
49
  disabled: () => disabled,
49
50
  // A visible field `label` also names the group for assistive tech.
50
51
  ariaLabel: () => ariaLabel ?? label,
@@ -60,7 +61,9 @@
60
61
  <div
61
62
  class="sv-radiogroup sv-radiogroup--{orientation} sv-radiogroup--{size}"
62
63
  class:is-invalid={invalid}
64
+ class:is-readonly={readonly}
63
65
  {...rg.groupProps()}
66
+ aria-readonly={readonly ? 'true' : undefined}
64
67
  >
65
68
  {#each options as opt (opt.value)}
66
69
  <button
@@ -88,6 +91,7 @@
88
91
  .sv-radiogroup--md .sv-radio { font-size: 13px; }
89
92
  .sv-radiogroup--lg .sv-radio { font-size: 15px; }
90
93
  .sv-radio[disabled] { opacity: 0.5; cursor: not-allowed; }
94
+ .sv-radiogroup.is-readonly .sv-radio { cursor: default; }
91
95
  .sv-radio__dot {
92
96
  width: 17px; height: 17px; flex: none; border-radius: 50%;
93
97
  border: 1.5px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff);
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvResult - a centered result / status page for the outcome of an operation or
4
+ * a route state: success, error, warning, info, or not-found. An icon (built-in
5
+ * per status, or your own), a title, an optional description, and an actions
6
+ * slot. For empty data tables prefer SvEmptyState; use this for full-page
7
+ * outcomes ("Payment complete", "Something went wrong", "404").
8
+ *
9
+ * ```svelte
10
+ * <SvResult status="success" title="Payment complete" description="A receipt is on its way.">
11
+ * {#snippet actions()}<SvButton>Back to dashboard</SvButton>{/snippet}
12
+ * </SvResult>
13
+ * ```
14
+ */
15
+ import type { Snippet } from 'svelte'
16
+
17
+ type Status = 'success' | 'error' | 'warning' | 'info' | 'notfound'
18
+
19
+ type Props = {
20
+ status?: Status
21
+ title: string
22
+ description?: string
23
+ /** Replace the built-in status icon. */
24
+ icon?: Snippet
25
+ /** Action buttons row under the text. */
26
+ actions?: Snippet
27
+ children?: Snippet
28
+ }
29
+
30
+ let { status = 'info', title, description, icon, actions, children }: Props = $props()
31
+ </script>
32
+
33
+ <div class="sv-result sv-result--{status}" role="status">
34
+ <div class="sv-result__icon" aria-hidden="true">
35
+ {#if icon}{@render icon()}
36
+ {:else if status === 'success'}
37
+ <svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
38
+ {:else if status === 'error'}
39
+ <svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12" /></svg>
40
+ {:else if status === 'warning'}
41
+ <svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" /></svg>
42
+ {:else if status === 'notfound'}
43
+ <span class="sv-result__code">404</span>
44
+ {:else}
45
+ <svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 8h.01M11 12h1v4h1" /></svg>
46
+ {/if}
47
+ </div>
48
+ <h2 class="sv-result__title">{title}</h2>
49
+ {#if description}<p class="sv-result__desc">{description}</p>{/if}
50
+ {#if children}<div class="sv-result__body">{@render children()}</div>{/if}
51
+ {#if actions}<div class="sv-result__actions">{@render actions()}</div>{/if}
52
+ </div>
53
+
54
+ <style>
55
+ .sv-result {
56
+ --_c: var(--sg-accent, #2563eb);
57
+ display: flex; flex-direction: column; align-items: center; text-align: center; gap: 8px;
58
+ padding: 40px 24px; max-width: 460px; margin: 0 auto;
59
+ }
60
+ .sv-result--success { --_c: var(--sg-success, #16a34a); }
61
+ .sv-result--error { --_c: var(--sg-danger, #dc2626); }
62
+ .sv-result--warning { --_c: var(--sg-warning, #f59e0b); }
63
+ .sv-result--notfound { --_c: var(--sg-muted, #64748b); }
64
+ .sv-result__icon {
65
+ display: grid; place-items: center; width: 60px; height: 60px; border-radius: 50%; margin-bottom: 4px;
66
+ color: var(--_c); background: color-mix(in srgb, var(--_c) 12%, transparent);
67
+ }
68
+ .sv-result__code { font-size: 22px; font-weight: 800; letter-spacing: 0.02em; }
69
+ .sv-result__title { margin: 0; font-size: 19px; font-weight: 700; color: var(--sg-fg, #0f172a); }
70
+ .sv-result__desc { margin: 0; font-size: 14px; line-height: 1.5; color: var(--sg-muted, #64748b); }
71
+ .sv-result__body { margin-top: 6px; }
72
+ .sv-result__actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; justify-content: center; }
73
+ </style>
@@ -0,0 +1,114 @@
1
+ <script lang="ts" module>
2
+ export type { RadioOption as SegmentedOption } from './createRadioGroup.svelte'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvSegmented - a segmented control: a compact row of mutually-exclusive
8
+ * options in a shared track (the modern alternative to a small radio group or a
9
+ * tab strip for a setting). Single-select, keyboard-accessible (it reuses the
10
+ * radio-group core: arrow keys roam, Space/Enter select), and themed from the
11
+ * shared `--sg-*` tokens.
12
+ *
13
+ * ```svelte
14
+ * <SvSegmented options={[{value:'list',label:'List'},{value:'grid',label:'Grid'}]} bind:value />
15
+ * ```
16
+ */
17
+ import type { Snippet } from 'svelte'
18
+ import SvField from './SvField.svelte'
19
+ import { createRadioGroup, type RadioOption } from './createRadioGroup.svelte'
20
+ import { nextEditorId, type SvEditorProps } from './editor-contract'
21
+
22
+ type SegOption = RadioOption & { icon?: Snippet }
23
+
24
+ type Props = Pick<
25
+ SvEditorProps,
26
+ 'disabled' | 'size' | 'dir' | 'id' | 'ariaLabel' | 'label' | 'hint' | 'error' | 'required' | 'invalid'
27
+ > & {
28
+ options: ReadonlyArray<SegOption>
29
+ value?: string | number | null
30
+ onChange?: (value: string | number) => void
31
+ /** Stretch the control (and split options evenly) to the container width. */
32
+ block?: boolean
33
+ name?: string
34
+ }
35
+
36
+ let {
37
+ options,
38
+ value = $bindable<string | number | null>(null),
39
+ onChange,
40
+ disabled = false,
41
+ size = 'md',
42
+ dir,
43
+ id,
44
+ ariaLabel,
45
+ label,
46
+ hint,
47
+ error,
48
+ required = false,
49
+ invalid = false,
50
+ block = false,
51
+ name,
52
+ }: Props = $props()
53
+
54
+ const autoId = nextEditorId('sv-seg')
55
+ const uid = $derived(id ?? autoId)
56
+
57
+ const rg = createRadioGroup({
58
+ options: () => options,
59
+ value: () => value,
60
+ onChange: (v) => { value = v; onChange?.(v) },
61
+ disabled: () => disabled,
62
+ ariaLabel: () => ariaLabel ?? label,
63
+ id: () => uid,
64
+ invalid: () => invalid,
65
+ required: () => required,
66
+ error: () => error,
67
+ })
68
+ </script>
69
+
70
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
71
+ <div class="sv-seg sv-seg--{size}" class:is-block={block} class:is-disabled={disabled} class:is-invalid={invalid} {...rg.groupProps()}>
72
+ {#each options as opt (opt.value)}
73
+ <button
74
+ class="sv-seg__opt"
75
+ class:is-selected={rg.isChecked(opt)}
76
+ {...rg.radioProps(opt)}
77
+ >
78
+ {#if opt.icon}<span class="sv-seg__icon">{@render opt.icon()}</span>{/if}
79
+ <span class="sv-seg__label">{opt.label}</span>
80
+ </button>
81
+ {/each}
82
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
83
+ </div>
84
+ </SvField>
85
+
86
+ <style>
87
+ .sv-seg {
88
+ --_accent: var(--sg-accent, #2563eb);
89
+ display: inline-flex; gap: 2px; padding: 2px; box-sizing: border-box;
90
+ background: var(--sg-row-hover-bg, #f1f5f9); border-radius: calc(var(--sg-radius, 8px) + 2px);
91
+ border: 1px solid var(--sg-border, #e2e8f0);
92
+ }
93
+ .sv-seg.is-block { display: flex; width: 100%; }
94
+ .sv-seg.is-disabled { opacity: 0.6; }
95
+ .sv-seg__opt {
96
+ flex: 1 1 auto; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
97
+ padding: 0 12px; border: 0; border-radius: var(--sg-radius, 8px); background: none; cursor: pointer;
98
+ font: inherit; font-weight: 550; color: var(--sg-muted, #64748b); white-space: nowrap;
99
+ transition: background 0.14s, color 0.14s, box-shadow 0.14s;
100
+ }
101
+ .sv-seg--sm .sv-seg__opt { height: 26px; font-size: 12px; }
102
+ .sv-seg--md .sv-seg__opt { height: 32px; font-size: 13px; }
103
+ .sv-seg--lg .sv-seg__opt { height: 38px; font-size: 14.5px; }
104
+ .sv-seg__opt:hover:not(.is-selected):not([disabled]) { color: var(--sg-fg, #0f172a); }
105
+ .sv-seg__opt.is-selected {
106
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
107
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12);
108
+ }
109
+ .sv-seg__opt[disabled] { opacity: 0.5; cursor: default; }
110
+ .sv-seg__opt:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: -2px; }
111
+ .sv-seg.is-invalid { border-color: var(--sg-danger, #dc2626); }
112
+ .sv-seg__icon { display: inline-flex; }
113
+ @media (prefers-reduced-motion: reduce) { .sv-seg__opt { transition: none; } }
114
+ </style>
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvSimpleGrid - a responsive grid primitive: lays children into `cols` equal
4
+ * columns that auto-fit down to a `minChildWidth` before wrapping, with a
5
+ * consistent `gap`. Use it for card galleries, stat tiles, and dashboards.
6
+ *
7
+ * ```svelte
8
+ * <SvSimpleGrid minChildWidth={220} gap={12}>{#each tiles as t}<Card {t} />{/each}</SvSimpleGrid>
9
+ * ```
10
+ */
11
+ import type { Snippet } from 'svelte'
12
+
13
+ type Props = {
14
+ /**
15
+ * Fixed column count. When omitted, columns auto-fit based on
16
+ * `minChildWidth` (responsive by default).
17
+ */
18
+ cols?: number
19
+ /** Minimum child width (px) before wrapping, for the auto-fit layout. Default 200. */
20
+ minChildWidth?: number
21
+ /** Gap between cells (px, or any CSS length). Default 12. */
22
+ gap?: number | string
23
+ children?: Snippet
24
+ }
25
+
26
+ let { cols, minChildWidth = 200, gap = 12, children }: Props = $props()
27
+ const g = $derived(typeof gap === 'number' ? `${gap}px` : gap)
28
+ const template = $derived(
29
+ cols != null
30
+ ? `repeat(${cols}, minmax(0, 1fr))`
31
+ : `repeat(auto-fit, minmax(min(${minChildWidth}px, 100%), 1fr))`,
32
+ )
33
+ </script>
34
+
35
+ <div class="sv-simplegrid" style:gap={g} style:grid-template-columns={template}>
36
+ {@render children?.()}
37
+ </div>
38
+
39
+ <style>
40
+ .sv-simplegrid { display: grid; min-width: 0; }
41
+ </style>