@proyecto-viviana/solidaria-components 0.2.9 → 0.3.0

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 (222) hide show
  1. package/README.md +39 -272
  2. package/dist/ActionBar.d.ts +21 -13
  3. package/dist/ActionBar.d.ts.map +1 -1
  4. package/dist/ActionGroup.d.ts +8 -8
  5. package/dist/ActionGroup.d.ts.map +1 -1
  6. package/dist/Alert.d.ts +5 -5
  7. package/dist/Alert.d.ts.map +1 -1
  8. package/dist/Autocomplete.d.ts +5 -5
  9. package/dist/Autocomplete.d.ts.map +1 -1
  10. package/dist/Breadcrumbs.d.ts +18 -7
  11. package/dist/Breadcrumbs.d.ts.map +1 -1
  12. package/dist/Button.d.ts +24 -5
  13. package/dist/Button.d.ts.map +1 -1
  14. package/dist/Calendar.d.ts +38 -7
  15. package/dist/Calendar.d.ts.map +1 -1
  16. package/dist/Checkbox.d.ts +32 -7
  17. package/dist/Checkbox.d.ts.map +1 -1
  18. package/dist/Collection.d.ts +19 -14
  19. package/dist/Collection.d.ts.map +1 -1
  20. package/dist/Color.d.ts +103 -14
  21. package/dist/Color.d.ts.map +1 -1
  22. package/dist/ColorEditor.d.ts +6 -6
  23. package/dist/ColorEditor.d.ts.map +1 -1
  24. package/dist/ComboBox.d.ts +85 -19
  25. package/dist/ComboBox.d.ts.map +1 -1
  26. package/dist/ContextualHelpTrigger.d.ts +2 -2
  27. package/dist/ContextualHelpTrigger.d.ts.map +1 -1
  28. package/dist/DateField.d.ts +8 -6
  29. package/dist/DateField.d.ts.map +1 -1
  30. package/dist/DatePicker.d.ts +53 -22
  31. package/dist/DatePicker.d.ts.map +1 -1
  32. package/dist/DateRangePickerContext.d.ts +30 -0
  33. package/dist/DateRangePickerContext.d.ts.map +1 -0
  34. package/dist/Dialog.d.ts +5 -5
  35. package/dist/Dialog.d.ts.map +1 -1
  36. package/dist/Disclosure.d.ts +23 -5
  37. package/dist/Disclosure.d.ts.map +1 -1
  38. package/dist/DragAndDrop.d.ts +6 -6
  39. package/dist/DragAndDrop.d.ts.map +1 -1
  40. package/dist/DragPreview.d.ts +2 -2
  41. package/dist/DragPreview.d.ts.map +1 -1
  42. package/dist/DropZone.d.ts +4 -4
  43. package/dist/DropZone.d.ts.map +1 -1
  44. package/dist/FieldError.d.ts +9 -5
  45. package/dist/FieldError.d.ts.map +1 -1
  46. package/dist/FileTrigger.d.ts +3 -3
  47. package/dist/FileTrigger.d.ts.map +1 -1
  48. package/dist/Focusable.d.ts +2 -2
  49. package/dist/Focusable.d.ts.map +1 -1
  50. package/dist/Form.d.ts +18 -4
  51. package/dist/Form.d.ts.map +1 -1
  52. package/dist/GridList.d.ts +32 -12
  53. package/dist/GridList.d.ts.map +1 -1
  54. package/dist/HiddenDateInput.d.ts +26 -0
  55. package/dist/HiddenDateInput.d.ts.map +1 -0
  56. package/dist/HiddenTimeInput.d.ts +25 -0
  57. package/dist/HiddenTimeInput.d.ts.map +1 -0
  58. package/dist/Icon.d.ts +5 -5
  59. package/dist/Icon.d.ts.map +1 -1
  60. package/dist/Keyboard.d.ts +1 -1
  61. package/dist/Landmark.d.ts +3 -3
  62. package/dist/Landmark.d.ts.map +1 -1
  63. package/dist/Link.d.ts +10 -4
  64. package/dist/Link.d.ts.map +1 -1
  65. package/dist/ListBox.d.ts +32 -12
  66. package/dist/ListBox.d.ts.map +1 -1
  67. package/dist/ListDropTargetDelegate.d.ts +6 -6
  68. package/dist/ListDropTargetDelegate.d.ts.map +1 -1
  69. package/dist/Menu.d.ts +65 -14
  70. package/dist/Menu.d.ts.map +1 -1
  71. package/dist/Meter.d.ts +3 -3
  72. package/dist/Meter.d.ts.map +1 -1
  73. package/dist/Modal.d.ts +5 -5
  74. package/dist/Modal.d.ts.map +1 -1
  75. package/dist/NumberField.d.ts +8 -12
  76. package/dist/NumberField.d.ts.map +1 -1
  77. package/dist/Popover.d.ts +28 -5
  78. package/dist/Popover.d.ts.map +1 -1
  79. package/dist/Pressable.d.ts +2 -2
  80. package/dist/Pressable.d.ts.map +1 -1
  81. package/dist/ProgressBar.d.ts +5 -3
  82. package/dist/ProgressBar.d.ts.map +1 -1
  83. package/dist/RadioGroup.d.ts +43 -9
  84. package/dist/RadioGroup.d.ts.map +1 -1
  85. package/dist/RangeCalendar.d.ts +34 -7
  86. package/dist/RangeCalendar.d.ts.map +1 -1
  87. package/dist/RouterProvider.d.ts +2 -2
  88. package/dist/RouterProvider.d.ts.map +1 -1
  89. package/dist/SearchField.d.ts +23 -20
  90. package/dist/SearchField.d.ts.map +1 -1
  91. package/dist/Select.d.ts +41 -11
  92. package/dist/Select.d.ts.map +1 -1
  93. package/dist/SelectionIndicator.d.ts +3 -3
  94. package/dist/SelectionIndicator.d.ts.map +1 -1
  95. package/dist/Separator.d.ts +9 -3
  96. package/dist/Separator.d.ts.map +1 -1
  97. package/dist/SharedElementTransition.d.ts +6 -4
  98. package/dist/SharedElementTransition.d.ts.map +1 -1
  99. package/dist/Slider.d.ts +12 -8
  100. package/dist/Slider.d.ts.map +1 -1
  101. package/dist/StepList.d.ts +90 -0
  102. package/dist/StepList.d.ts.map +1 -0
  103. package/dist/Switch.d.ts +11 -5
  104. package/dist/Switch.d.ts.map +1 -1
  105. package/dist/Table.d.ts +187 -23
  106. package/dist/Table.d.ts.map +1 -1
  107. package/dist/Tabs.d.ts +45 -9
  108. package/dist/Tabs.d.ts.map +1 -1
  109. package/dist/TagGroup.d.ts +12 -10
  110. package/dist/TagGroup.d.ts.map +1 -1
  111. package/dist/Text.d.ts +2 -2
  112. package/dist/TextField.d.ts +15 -11
  113. package/dist/TextField.d.ts.map +1 -1
  114. package/dist/TimeField.d.ts +6 -6
  115. package/dist/TimeField.d.ts.map +1 -1
  116. package/dist/Toast.d.ts +29 -14
  117. package/dist/Toast.d.ts.map +1 -1
  118. package/dist/ToggleButton.d.ts +11 -5
  119. package/dist/ToggleButton.d.ts.map +1 -1
  120. package/dist/ToggleButtonGroup.d.ts +7 -7
  121. package/dist/ToggleButtonGroup.d.ts.map +1 -1
  122. package/dist/Toolbar.d.ts +7 -3
  123. package/dist/Toolbar.d.ts.map +1 -1
  124. package/dist/Tooltip.d.ts +50 -8
  125. package/dist/Tooltip.d.ts.map +1 -1
  126. package/dist/Tree.d.ts +66 -17
  127. package/dist/Tree.d.ts.map +1 -1
  128. package/dist/Virtualizer.d.ts +12 -12
  129. package/dist/Virtualizer.d.ts.map +1 -1
  130. package/dist/VirtualizerLayouts.d.ts +2 -2
  131. package/dist/VirtualizerLayouts.d.ts.map +1 -1
  132. package/dist/VisuallyHidden.d.ts +1 -1
  133. package/dist/VisuallyHidden.d.ts.map +1 -1
  134. package/dist/contexts.d.ts +5 -1
  135. package/dist/contexts.d.ts.map +1 -1
  136. package/dist/index.d.ts +73 -71
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +23247 -18564
  139. package/dist/index.js.map +1 -1
  140. package/dist/index.jsx +18110 -0
  141. package/dist/index.jsx.map +1 -0
  142. package/dist/useDragAndDrop.d.ts +13 -13
  143. package/dist/useDragAndDrop.d.ts.map +1 -1
  144. package/dist/utils.d.ts +2 -2
  145. package/dist/utils.d.ts.map +1 -1
  146. package/dist/virtualizer/Layout.d.ts +1 -1
  147. package/dist/virtualizer/Layout.d.ts.map +1 -1
  148. package/package.json +31 -32
  149. package/src/ActionBar.tsx +75 -72
  150. package/src/ActionGroup.tsx +53 -61
  151. package/src/Alert.tsx +17 -42
  152. package/src/Autocomplete.tsx +39 -44
  153. package/src/Breadcrumbs.tsx +149 -80
  154. package/src/Button.tsx +267 -70
  155. package/src/Calendar.tsx +218 -138
  156. package/src/Checkbox.tsx +413 -121
  157. package/src/Collection.tsx +67 -58
  158. package/src/Color.tsx +803 -380
  159. package/src/ColorEditor.tsx +131 -149
  160. package/src/ComboBox.tsx +414 -249
  161. package/src/ContextualHelpTrigger.tsx +86 -74
  162. package/src/DateField.tsx +185 -91
  163. package/src/DatePicker.tsx +524 -213
  164. package/src/DateRangePickerContext.tsx +44 -0
  165. package/src/Dialog.tsx +156 -118
  166. package/src/Disclosure.tsx +127 -80
  167. package/src/DragAndDrop.tsx +60 -54
  168. package/src/DragPreview.tsx +13 -11
  169. package/src/DropZone.tsx +42 -22
  170. package/src/FieldError.tsx +45 -23
  171. package/src/FileTrigger.tsx +19 -19
  172. package/src/Focusable.tsx +21 -24
  173. package/src/Form.tsx +71 -16
  174. package/src/GridList.tsx +273 -197
  175. package/src/HiddenDateInput.tsx +153 -0
  176. package/src/HiddenTimeInput.tsx +133 -0
  177. package/src/Icon.tsx +22 -43
  178. package/src/Keyboard.tsx +3 -3
  179. package/src/Landmark.tsx +37 -63
  180. package/src/Link.tsx +125 -75
  181. package/src/ListBox.tsx +332 -233
  182. package/src/ListDropTargetDelegate.ts +81 -80
  183. package/src/Menu.tsx +1023 -274
  184. package/src/Meter.tsx +38 -56
  185. package/src/Modal.tsx +243 -175
  186. package/src/NumberField.tsx +139 -143
  187. package/src/Popover.tsx +386 -233
  188. package/src/Pressable.tsx +21 -21
  189. package/src/ProgressBar.tsx +48 -57
  190. package/src/RadioGroup.tsx +524 -122
  191. package/src/RangeCalendar.tsx +157 -90
  192. package/src/RouterProvider.tsx +30 -47
  193. package/src/SearchField.tsx +362 -143
  194. package/src/Select.tsx +656 -233
  195. package/src/SelectionIndicator.tsx +18 -15
  196. package/src/Separator.tsx +47 -49
  197. package/src/SharedElementTransition.tsx +103 -97
  198. package/src/Slider.tsx +138 -98
  199. package/src/StepList.tsx +272 -0
  200. package/src/Switch.tsx +93 -46
  201. package/src/Table.tsx +1308 -342
  202. package/src/Tabs.tsx +324 -103
  203. package/src/TagGroup.tsx +139 -126
  204. package/src/Text.tsx +3 -3
  205. package/src/TextField.tsx +389 -79
  206. package/src/TimeField.tsx +136 -76
  207. package/src/Toast.tsx +209 -157
  208. package/src/ToggleButton.tsx +47 -37
  209. package/src/ToggleButtonGroup.tsx +39 -34
  210. package/src/Toolbar.tsx +54 -69
  211. package/src/Tooltip.tsx +387 -119
  212. package/src/Tree.tsx +651 -368
  213. package/src/Virtualizer.tsx +208 -180
  214. package/src/VirtualizerLayouts.ts +45 -30
  215. package/src/VisuallyHidden.tsx +19 -19
  216. package/src/contexts.ts +29 -37
  217. package/src/index.ts +110 -195
  218. package/src/useDragAndDrop.ts +87 -71
  219. package/src/utils.tsx +40 -55
  220. package/src/virtualizer/Layout.ts +14 -22
  221. package/dist/index.ssr.js +0 -16996
  222. package/dist/index.ssr.js.map +0 -1
package/src/Toast.tsx CHANGED
@@ -7,46 +7,60 @@
7
7
 
8
8
  import {
9
9
  type JSX,
10
+ type Accessor,
10
11
  createContext,
11
12
  createMemo,
12
13
  createEffect,
14
+ createSignal,
13
15
  onCleanup,
14
16
  splitProps,
15
17
  Show,
16
18
  useContext,
17
- } from 'solid-js';
18
- import { Portal, isServer } from 'solid-js/web';
19
+ } from "solid-js";
20
+ import { Portal } from "solid-js/web";
19
21
  import {
20
22
  type ToastState,
21
23
  type QueuedToast,
22
24
  type ToastQueueOptions,
25
+ type ToastOptions,
23
26
  ToastQueue,
24
27
  createToastState,
25
- } from '@proyecto-viviana/solid-stately';
28
+ } from "@proyecto-viviana/solid-stately";
26
29
  import {
27
30
  createToast,
28
31
  createToastRegion,
29
32
  useUNSAFE_PortalContext,
30
- } from '@proyecto-viviana/solidaria';
33
+ } from "@proyecto-viviana/solidaria";
31
34
  import {
32
35
  type RenderChildren,
33
36
  type ClassNameOrFunction,
34
37
  type StyleOrFunction,
35
38
  useRenderProps,
36
39
  filterDOMProps,
37
- } from './utils';
38
-
39
- // ============================================
40
- // TYPES
41
- // ============================================
40
+ useIsHydrated,
41
+ } from "./utils";
42
42
 
43
43
  export interface ToastContent {
44
+ /** DOM id for the toast root when content is queued through Spectrum ToastQueue. */
45
+ id?: string;
46
+ /** Data attributes for the toast root when content is queued through Spectrum ToastQueue. */
47
+ [dataAttribute: `data-${string}`]: string | number | boolean | undefined;
48
+ /** Spectrum Toast message content. */
49
+ children?: string;
44
50
  /** The title of the toast. */
45
51
  title?: string;
46
52
  /** The description/body of the toast. */
47
53
  description?: string;
48
- /** The type/variant of the toast (info, success, warning, error). */
49
- type?: 'info' | 'success' | 'warning' | 'error';
54
+ /** Spectrum variant. */
55
+ variant?: "info" | "positive" | "negative" | "neutral";
56
+ /** Backward-compatible type/variant of the toast. */
57
+ type?: "info" | "success" | "warning" | "error" | "positive" | "negative" | "neutral";
58
+ /** Spectrum action button label. */
59
+ actionLabel?: string;
60
+ /** Handler called when the Spectrum action button is pressed. */
61
+ onAction?: () => void;
62
+ /** Whether the toast should close when the Spectrum action is performed. */
63
+ shouldCloseOnAction?: boolean;
50
64
  /** Custom action button. */
51
65
  action?: {
52
66
  label: string;
@@ -60,14 +74,14 @@ export interface ToastRenderProps {
60
74
  /** Whether the toast is currently animating out. */
61
75
  isExiting: boolean;
62
76
  /** The animation state (entering, exiting, queued). */
63
- animation: 'entering' | 'exiting' | 'queued' | undefined;
77
+ animation: "entering" | "exiting" | "queued" | undefined;
64
78
  /** The toast data. */
65
79
  toast: QueuedToast<ToastContent>;
66
80
  }
67
81
 
68
82
  export interface ToastRegionRenderProps {
69
83
  /** The visible toasts. */
70
- visibleToasts: QueuedToast<ToastContent>[];
84
+ visibleToasts: Accessor<QueuedToast<ToastContent>[]>;
71
85
  }
72
86
 
73
87
  export interface ToastRegionProps {
@@ -80,14 +94,28 @@ export interface ToastRegionProps {
80
94
  /** The toast state to display. If not provided, uses ToastContext. */
81
95
  state?: ToastState<ToastContent>;
82
96
  /** Accessible label for the region. */
83
- 'aria-label'?: string;
97
+ "aria-label"?: string;
84
98
  /** Whether to render in a portal. */
85
99
  portal?: boolean;
86
100
  /** Placement of the toast region. */
87
- placement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end';
101
+ placement?:
102
+ | "top"
103
+ | "top start"
104
+ | "top end"
105
+ | "top-start"
106
+ | "top-end"
107
+ | "bottom"
108
+ | "bottom start"
109
+ | "bottom end"
110
+ | "bottom-start"
111
+ | "bottom-end";
88
112
  }
89
113
 
90
114
  export interface ToastProps {
115
+ /** DOM id for the toast root. */
116
+ id?: string;
117
+ /** Data attributes for the toast root. */
118
+ [dataAttribute: `data-${string}`]: string | number | boolean | undefined;
91
119
  /** The toast data. */
92
120
  toast: QueuedToast<ToastContent>;
93
121
  /** The children of the component - can be JSX or render function. */
@@ -98,10 +126,6 @@ export interface ToastProps {
98
126
  style?: StyleOrFunction<ToastRenderProps>;
99
127
  }
100
128
 
101
- // ============================================
102
- // CONTEXT
103
- // ============================================
104
-
105
129
  export const ToastContext = createContext<ToastState<ToastContent> | null>(null);
106
130
 
107
131
  interface ToastAriaContextValue {
@@ -110,22 +134,19 @@ interface ToastAriaContextValue {
110
134
  }
111
135
 
112
136
  const ToastAriaContext = createContext<ToastAriaContextValue | null>(null);
137
+ const toastStateByKey = new Map<string, ToastState<ToastContent>>();
113
138
 
114
139
  export function useToastContext(): ToastState<ToastContent> {
115
140
  const context = useContext(ToastContext);
116
141
  if (!context) {
117
- throw new Error('Toast components must be used within a ToastProvider');
142
+ throw new Error("Toast components must be used within a ToastProvider");
118
143
  }
119
144
  return context;
120
145
  }
121
146
 
122
- // ============================================
123
- // GLOBAL TOAST QUEUE
124
- // ============================================
125
-
126
147
  /** Default global toast queue that can be used for app-wide toasts. */
127
148
  export const globalToastQueue = new ToastQueue<ToastContent>({
128
- maxVisibleToasts: 5,
149
+ maxVisibleToasts: Infinity,
129
150
  hasExitAnimation: true,
130
151
  });
131
152
 
@@ -133,16 +154,13 @@ export const globalToastQueue = new ToastQueue<ToastContent>({
133
154
  * Add a toast to the global queue.
134
155
  * Convenience function for adding toasts from anywhere in the app.
135
156
  */
136
- export function addToast(
137
- content: ToastContent,
138
- options?: { timeout?: number; priority?: number }
139
- ): string {
157
+ export function addToast(content: ToastContent, options?: ToastOptions): string {
140
158
  return globalToastQueue.add(content, options);
141
159
  }
142
160
 
143
- // ============================================
144
- // TOAST PROVIDER
145
- // ============================================
161
+ function normalizeToastPlacement(placement?: ToastRegionProps["placement"]) {
162
+ return (placement ?? "bottom").replace("-", " ") as NonNullable<ToastRegionProps["placement"]>;
163
+ }
146
164
 
147
165
  export interface ToastProviderProps {
148
166
  /** The children of the provider. */
@@ -172,17 +190,9 @@ export function ToastProvider(props: ToastProviderProps): JSX.Element {
172
190
 
173
191
  const state = createToastState({ queue });
174
192
 
175
- return (
176
- <ToastContext.Provider value={state}>
177
- {props.children}
178
- </ToastContext.Provider>
179
- );
193
+ return <ToastContext.Provider value={state}>{props.children}</ToastContext.Provider>;
180
194
  }
181
195
 
182
- // ============================================
183
- // TOAST REGION COMPONENT
184
- // ============================================
185
-
186
196
  /**
187
197
  * ToastRegion is a container that displays all visible toasts.
188
198
  * It handles pause on hover/focus and provides the landmark region.
@@ -199,91 +209,111 @@ export function ToastProvider(props: ToastProviderProps): JSX.Element {
199
209
  * ```
200
210
  */
201
211
  export function ToastRegion(props: ToastRegionProps): JSX.Element {
202
- if (isServer) {
203
- return null as unknown as JSX.Element;
204
- }
212
+ // Do NOT early-return on the server: returning null on the server and a <Show>
213
+ // on the client desyncs hydration when the region is in the SSR tree. Render the
214
+ // same structure on both and gate the Portal on useIsHydrated() (see Popover).
215
+ const isHydrated = useIsHydrated();
205
216
 
206
217
  const [local, rest] = splitProps(props, [
207
- 'children',
208
- 'class',
209
- 'style',
210
- 'state',
211
- 'aria-label',
212
- 'portal',
213
- 'placement',
218
+ "children",
219
+ "class",
220
+ "style",
221
+ "state",
222
+ "aria-label",
223
+ "portal",
224
+ "placement",
214
225
  ]);
215
226
  const portalContext = useUNSAFE_PortalContext();
216
227
  const portalContainer = () => portalContext.getContainer?.() ?? undefined;
228
+ const [regionElement, setRegionElement] = createSignal<HTMLElement>();
217
229
 
218
- // Get state from context if not provided
219
230
  const contextState = useContext(ToastContext);
220
231
  const state = () => local.state ?? contextState;
221
232
 
222
- // Create region accessibility props
223
- const getRegionAria = () => {
224
- const s = state();
225
- if (!s) return null;
226
- return createToastRegion({
227
- state: s,
228
- 'aria-label': local['aria-label'],
229
- });
230
- };
233
+ const regionAria = createToastRegion<ToastContent>({
234
+ state: {
235
+ visibleToasts: () => state()?.visibleToasts() ?? [],
236
+ add: (content, options) => state()?.add(content, options) ?? "",
237
+ close: (key) => state()?.close(key),
238
+ remove: (key) => state()?.remove(key),
239
+ clear: () => state()?.clear(),
240
+ pauseAll: () => state()?.pauseAll(),
241
+ resumeAll: () => state()?.resumeAll(),
242
+ },
243
+ ref: regionElement,
244
+ get "aria-label"() {
245
+ return local["aria-label"];
246
+ },
247
+ });
231
248
 
232
- // Render props values
233
249
  const renderValues = createMemo<ToastRegionRenderProps>(() => ({
234
- visibleToasts: state()?.visibleToasts() ?? [],
250
+ visibleToasts: () => state()?.visibleToasts() ?? [],
235
251
  }));
236
252
 
237
- // Resolve render props
238
253
  const renderProps = useRenderProps(
239
254
  {
240
255
  children: props.children,
241
256
  class: local.class,
242
257
  style: local.style,
243
- defaultClassName: 'solidaria-ToastRegion',
258
+ defaultClassName: "solidaria-ToastRegion",
244
259
  },
245
- renderValues
260
+ renderValues,
246
261
  );
262
+ const renderedChildren = createMemo(() => renderProps.renderChildren());
247
263
 
248
- // Filter DOM props
249
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
264
+ const domProps = createMemo(() =>
265
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
266
+ );
250
267
 
251
- // Placement styles
252
268
  const placementStyles = createMemo<JSX.CSSProperties>(() => {
253
- const placement = local.placement ?? 'bottom-end';
269
+ const placement = normalizeToastPlacement(local.placement);
270
+ const [edge, align = "center"] = placement.split(" ");
254
271
  const base: JSX.CSSProperties = {
255
- position: 'fixed',
256
- 'z-index': 100001,
257
- display: 'flex',
258
- 'flex-direction': 'column',
259
- gap: '8px',
260
- padding: '16px',
261
- 'pointer-events': 'none',
272
+ position: "fixed",
273
+ "z-index": 100001,
274
+ display: "flex",
275
+ "flex-direction": edge === "top" ? "column" : "column-reverse",
276
+ gap: "8px",
277
+ padding: "16px",
278
+ "pointer-events": "none",
262
279
  };
263
280
 
264
- switch (placement) {
265
- case 'top':
266
- return { ...base, top: 0, left: '50%', transform: 'translateX(-50%)' } as JSX.CSSProperties;
267
- case 'top-start':
268
- return { ...base, top: 0, left: 0 } as JSX.CSSProperties;
269
- case 'top-end':
270
- return { ...base, top: 0, right: 0 } as JSX.CSSProperties;
271
- case 'bottom':
272
- return { ...base, bottom: 0, left: '50%', transform: 'translateX(-50%)' } as JSX.CSSProperties;
273
- case 'bottom-start':
274
- return { ...base, bottom: 0, left: 0 } as JSX.CSSProperties;
275
- case 'bottom-end':
276
- default:
277
- return { ...base, bottom: 0, right: 0 } as JSX.CSSProperties;
281
+ if (edge === "top") {
282
+ if (align === "end") {
283
+ return { ...base, top: "16px", right: "16px" } as JSX.CSSProperties;
284
+ }
285
+ if (align === "start") {
286
+ return { ...base, top: "16px", left: "16px" } as JSX.CSSProperties;
287
+ }
288
+ return {
289
+ ...base,
290
+ top: "16px",
291
+ left: "50%",
292
+ transform: "translateX(-50%)",
293
+ } as JSX.CSSProperties;
294
+ }
295
+
296
+ if (align === "end") {
297
+ return { ...base, bottom: "16px", right: "16px" } as JSX.CSSProperties;
298
+ }
299
+ if (align === "start") {
300
+ return { ...base, bottom: "16px", left: "16px" } as JSX.CSSProperties;
278
301
  }
302
+ return {
303
+ ...base,
304
+ bottom: "16px",
305
+ left: "50%",
306
+ transform: "translateX(-50%)",
307
+ } as JSX.CSSProperties;
279
308
  });
280
309
 
310
+ const normalizedPlacement = () => normalizeToastPlacement(local.placement);
311
+
281
312
  const visibleToasts = () => state()?.visibleToasts() ?? [];
282
313
  const hasToasts = () => visibleToasts().length > 0;
283
314
 
284
315
  const regionContent = () => {
285
- const regionAria = getRegionAria();
286
- if (!regionAria || !state()) return null;
316
+ if (!state()) return null;
287
317
 
288
318
  // Merge styles - placement styles are base, renderProps.style() overrides
289
319
  const mergedStyle = () => {
@@ -293,25 +323,24 @@ export function ToastRegion(props: ToastRegionProps): JSX.Element {
293
323
  return { ...placement, ...custom } as JSX.CSSProperties;
294
324
  };
295
325
 
296
- // Extract ref from regionProps to avoid type conflicts
297
326
  const { ref: _ref, ...cleanRegionProps } = regionAria.regionProps as Record<string, unknown>;
298
327
 
299
328
  return (
300
329
  <div
330
+ ref={setRegionElement}
301
331
  {...domProps()}
302
332
  {...cleanRegionProps}
303
333
  class={renderProps.class()}
304
334
  style={mergedStyle()}
305
- data-placement={local.placement ?? 'bottom-end'}
335
+ data-placement={normalizedPlacement()}
306
336
  >
307
- {renderProps.renderChildren()}
337
+ {renderedChildren()}
308
338
  </div>
309
339
  );
310
340
  };
311
341
 
312
- // Only render when there are toasts
313
342
  return (
314
- <Show when={hasToasts()}>
343
+ <Show when={isHydrated() && hasToasts()}>
315
344
  <Show when={local.portal !== false} fallback={regionContent()}>
316
345
  <Portal mount={portalContainer()}>{regionContent()}</Portal>
317
346
  </Show>
@@ -319,10 +348,6 @@ export function ToastRegion(props: ToastRegionProps): JSX.Element {
319
348
  );
320
349
  }
321
350
 
322
- // ============================================
323
- // TOAST COMPONENT
324
- // ============================================
325
-
326
351
  /**
327
352
  * Toast is an individual notification component.
328
353
  *
@@ -339,53 +364,64 @@ export function ToastRegion(props: ToastRegionProps): JSX.Element {
339
364
  * ```
340
365
  */
341
366
  export function Toast(props: ToastProps): JSX.Element {
342
- const [local, rest] = splitProps(props, [
343
- 'toast',
344
- 'children',
345
- 'class',
346
- 'style',
347
- ]);
367
+ const [local, rest] = splitProps(props, ["toast", "children", "class", "style"]);
348
368
 
349
369
  let toastRef!: HTMLDivElement;
350
370
 
351
- // Get state from context
352
371
  const state = useToastContext();
353
372
 
354
- // Create toast accessibility props
373
+ createEffect(() => {
374
+ const key = local.toast.key;
375
+ toastStateByKey.set(key, state);
376
+ onCleanup(() => {
377
+ if (toastStateByKey.get(key) === state) {
378
+ toastStateByKey.delete(key);
379
+ }
380
+ });
381
+ });
382
+
383
+ const hasTitle = () => !!(local.toast.content.children ?? local.toast.content.title);
355
384
  const toastAria = createToast({
356
385
  toast: local.toast,
357
386
  state,
358
- hasTitle: !!local.toast.content.title,
387
+ hasTitle: hasTitle(),
359
388
  hasDescription: !!local.toast.content.description,
360
389
  });
361
390
 
362
- // Render props values
363
391
  const renderValues = createMemo<ToastRenderProps>(() => ({
364
- isEntering: local.toast.animation === 'entering',
365
- isExiting: local.toast.animation === 'exiting',
392
+ isEntering: local.toast.animation === "entering",
393
+ isExiting: local.toast.animation === "exiting",
366
394
  animation: local.toast.animation,
367
395
  toast: local.toast,
368
396
  }));
369
397
 
370
- // Resolve render props
371
398
  const renderProps = useRenderProps(
372
399
  {
373
400
  children: props.children,
374
401
  class: local.class,
375
402
  style: local.style,
376
- defaultClassName: 'solidaria-Toast',
403
+ defaultClassName: "solidaria-Toast",
377
404
  },
378
- renderValues
405
+ renderValues,
379
406
  );
380
407
 
381
- // Filter DOM props
382
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
408
+ const domProps = createMemo(() =>
409
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
410
+ );
383
411
 
384
- // Merge styles
385
412
  const mergedStyle = () => {
386
413
  const custom = renderProps.style();
387
- if (!custom) return { 'pointer-events': 'auto' as const };
388
- return { 'pointer-events': 'auto' as const, ...custom } as JSX.CSSProperties;
414
+ if (!custom) return { "pointer-events": "auto" as const };
415
+ return { "pointer-events": "auto" as const, ...custom } as JSX.CSSProperties;
416
+ };
417
+
418
+ const handleRootClick = (event: MouseEvent) => {
419
+ const target = event.target;
420
+ if (!(target instanceof Element)) return;
421
+ if (target.closest("[data-solidaria-toast-close-button]")) {
422
+ state.close(local.toast.key);
423
+ state.remove(local.toast.key);
424
+ }
389
425
  };
390
426
 
391
427
  // Exit animation lifecycle:
@@ -395,14 +431,14 @@ export function Toast(props: ToastProps): JSX.Element {
395
431
  // Reduced-motion is handled by CSS (shorter/no animations), so the lifecycle
396
432
  // naturally completes faster when the user prefers reduced motion.
397
433
  createEffect(() => {
398
- if (local.toast.animation !== 'exiting') return;
434
+ if (local.toast.animation !== "exiting") return;
399
435
  if (!toastRef) {
400
436
  state.remove(local.toast.key);
401
437
  return;
402
438
  }
403
439
 
404
440
  // Check if the element supports the Web Animations API
405
- if (!('getAnimations' in toastRef)) {
441
+ if (!("getAnimations" in toastRef)) {
406
442
  state.remove(local.toast.key);
407
443
  return;
408
444
  }
@@ -434,7 +470,6 @@ export function Toast(props: ToastProps): JSX.Element {
434
470
  });
435
471
  });
436
472
 
437
- // Extract ref from toastProps to avoid type conflicts
438
473
  const { ref: _ref, ...cleanToastProps } = toastAria.toastProps as Record<string, unknown>;
439
474
 
440
475
  // Ensure ARIA title/description IDs are present on rendered sub-components,
@@ -443,17 +478,19 @@ export function Toast(props: ToastProps): JSX.Element {
443
478
  if (!toastRef) return;
444
479
 
445
480
  const titleId = (toastAria.titleProps as Record<string, unknown>).id as string | undefined;
446
- const descriptionId = (toastAria.descriptionProps as Record<string, unknown>).id as string | undefined;
481
+ const descriptionId = (toastAria.descriptionProps as Record<string, unknown>).id as
482
+ | string
483
+ | undefined;
447
484
 
448
485
  if (titleId) {
449
- const titleEl = toastRef.querySelector('[data-solidaria-toast-title]');
486
+ const titleEl = toastRef.querySelector("[data-solidaria-toast-title]");
450
487
  if (titleEl instanceof HTMLElement) {
451
488
  titleEl.id = titleId;
452
489
  }
453
490
  }
454
491
 
455
492
  if (descriptionId) {
456
- const descriptionEl = toastRef.querySelector('[data-solidaria-toast-description]');
493
+ const descriptionEl = toastRef.querySelector("[data-solidaria-toast-description]");
457
494
  if (descriptionEl instanceof HTMLElement) {
458
495
  descriptionEl.id = descriptionId;
459
496
  }
@@ -461,7 +498,9 @@ export function Toast(props: ToastProps): JSX.Element {
461
498
  });
462
499
 
463
500
  return (
464
- <ToastAriaContext.Provider value={{ titleProps: toastAria.titleProps, descriptionProps: toastAria.descriptionProps }}>
501
+ <ToastAriaContext.Provider
502
+ value={{ titleProps: toastAria.titleProps, descriptionProps: toastAria.descriptionProps }}
503
+ >
465
504
  <div
466
505
  ref={toastRef}
467
506
  {...domProps()}
@@ -469,7 +508,9 @@ export function Toast(props: ToastProps): JSX.Element {
469
508
  class={renderProps.class()}
470
509
  style={mergedStyle()}
471
510
  data-animation={local.toast.animation}
472
- data-type={local.toast.content.type}
511
+ data-type={local.toast.content.type ?? local.toast.content.variant}
512
+ data-variant={local.toast.content.variant}
513
+ on:click={handleRootClick}
473
514
  >
474
515
  {renderProps.renderChildren()}
475
516
  </div>
@@ -477,10 +518,6 @@ export function Toast(props: ToastProps): JSX.Element {
477
518
  );
478
519
  }
479
520
 
480
- // ============================================
481
- // TOAST SUB-COMPONENTS
482
- // ============================================
483
-
484
521
  export interface ToastTitleProps {
485
522
  children: JSX.Element;
486
523
  class?: string;
@@ -512,10 +549,18 @@ export interface ToastDescriptionProps {
512
549
  */
513
550
  export function ToastDescription(props: ToastDescriptionProps): JSX.Element {
514
551
  const context = useContext(ToastAriaContext);
515
- const { ref: _ref, ...ariaDescriptionProps } = (context?.descriptionProps ?? {}) as Record<string, unknown>;
552
+ const { ref: _ref, ...ariaDescriptionProps } = (context?.descriptionProps ?? {}) as Record<
553
+ string,
554
+ unknown
555
+ >;
516
556
 
517
557
  return (
518
- <div data-solidaria-toast-description="" {...ariaDescriptionProps} class={props.class} style={props.style}>
558
+ <div
559
+ data-solidaria-toast-description=""
560
+ {...ariaDescriptionProps}
561
+ class={props.class}
562
+ style={props.style}
563
+ >
519
564
  {props.children}
520
565
  </div>
521
566
  );
@@ -527,17 +572,19 @@ export interface ToastCloseButtonProps {
527
572
  children?: JSX.Element;
528
573
  class?: string;
529
574
  style?: JSX.CSSProperties;
530
- 'aria-label'?: string;
575
+ "aria-label"?: string;
531
576
  }
532
577
 
533
578
  /**
534
579
  * ToastCloseButton is a button that closes the toast.
535
580
  */
536
581
  export function ToastCloseButton(props: ToastCloseButtonProps): JSX.Element {
537
- const state = useToastContext();
538
-
582
+ const contextState = useContext(ToastContext);
539
583
  const handleClose = () => {
540
- state.close(props.toast.key);
584
+ const key = props.toast.key;
585
+ const state = contextState ?? toastStateByKey.get(key);
586
+ state?.close(key);
587
+ state?.remove(key);
541
588
  };
542
589
 
543
590
  return (
@@ -545,18 +592,16 @@ export function ToastCloseButton(props: ToastCloseButtonProps): JSX.Element {
545
592
  type="button"
546
593
  class={props.class}
547
594
  style={props.style}
548
- aria-label={props['aria-label'] ?? 'Close'}
595
+ aria-label={props["aria-label"] ?? "Close"}
596
+ data-solidaria-toast-close-button=""
597
+ on:click={handleClose}
549
598
  onClick={handleClose}
550
599
  >
551
- {props.children ?? '×'}
600
+ {props.children ?? "×"}
552
601
  </button>
553
602
  );
554
603
  }
555
604
 
556
- // ============================================
557
- // DEFAULT TOAST RENDERING
558
- // ============================================
559
-
560
605
  export interface DefaultToastProps {
561
606
  toast: QueuedToast<ToastContent>;
562
607
  }
@@ -567,26 +612,33 @@ export interface DefaultToastProps {
567
612
  */
568
613
  export function DefaultToast(props: DefaultToastProps): JSX.Element {
569
614
  const content = () => props.toast.content;
615
+ const title = () => content().children ?? content().title;
616
+ const actionLabel = () => content().actionLabel ?? content().action?.label;
617
+ const actionHandler = () => content().onAction ?? content().action?.onAction;
618
+ const state = useToastContext();
619
+ const handleAction = () => {
620
+ actionHandler()?.();
621
+ if (content().shouldCloseOnAction) {
622
+ state.close(props.toast.key);
623
+ state.remove(props.toast.key);
624
+ }
625
+ };
570
626
 
571
627
  return (
572
628
  <Toast toast={props.toast}>
573
- <div style={{ display: 'flex', 'align-items': 'flex-start', gap: '12px' }}>
629
+ <div style={{ display: "flex", "align-items": "flex-start", gap: "12px" }}>
574
630
  <div style={{ flex: 1 }}>
575
- <Show when={content().title}>
576
- <ToastTitle style={{ 'font-weight': 'bold', 'margin-bottom': '4px' }}>
577
- {content().title}
631
+ <Show when={title()}>
632
+ <ToastTitle style={{ "font-weight": "bold", "margin-bottom": "4px" }}>
633
+ {title()}
578
634
  </ToastTitle>
579
635
  </Show>
580
636
  <Show when={content().description}>
581
637
  <ToastDescription>{content().description}</ToastDescription>
582
638
  </Show>
583
- <Show when={content().action}>
584
- <button
585
- type="button"
586
- style={{ 'margin-top': '8px' }}
587
- onClick={content().action?.onAction}
588
- >
589
- {content().action?.label}
639
+ <Show when={actionLabel()}>
640
+ <button type="button" style={{ "margin-top": "8px" }} onClick={handleAction}>
641
+ {actionLabel()}
590
642
  </button>
591
643
  </Show>
592
644
  </div>