@proyecto-viviana/solidaria-components 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +23253 -18564
  139. package/dist/index.js.map +1 -1
  140. package/dist/index.jsx +18116 -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 +251 -176
  186. package/src/NumberField.tsx +139 -143
  187. package/src/Popover.tsx +396 -234
  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 +216 -158
  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 +49 -60
  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,117 @@ 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
- children: props.children,
255
+ // Lazy: the region content is gated behind `<Show when={isHydrated() && …}>`
256
+ // (Portal) below, so reading children must not instantiate templates during
257
+ // the component body (would walk getNextElement before the gate the server
258
+ // kept closed → hydration mismatch). See Popover for the full rationale.
259
+ get children() {
260
+ return props.children;
261
+ },
241
262
  class: local.class,
242
263
  style: local.style,
243
- defaultClassName: 'solidaria-ToastRegion',
264
+ defaultClassName: "solidaria-ToastRegion",
244
265
  },
245
- renderValues
266
+ renderValues,
246
267
  );
268
+ const renderedChildren = createMemo(() => renderProps.renderChildren());
247
269
 
248
- // Filter DOM props
249
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
270
+ const domProps = createMemo(() =>
271
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
272
+ );
250
273
 
251
- // Placement styles
252
274
  const placementStyles = createMemo<JSX.CSSProperties>(() => {
253
- const placement = local.placement ?? 'bottom-end';
275
+ const placement = normalizeToastPlacement(local.placement);
276
+ const [edge, align = "center"] = placement.split(" ");
254
277
  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',
278
+ position: "fixed",
279
+ "z-index": 100001,
280
+ display: "flex",
281
+ "flex-direction": edge === "top" ? "column" : "column-reverse",
282
+ gap: "8px",
283
+ padding: "16px",
284
+ "pointer-events": "none",
262
285
  };
263
286
 
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;
287
+ if (edge === "top") {
288
+ if (align === "end") {
289
+ return { ...base, top: "16px", right: "16px" } as JSX.CSSProperties;
290
+ }
291
+ if (align === "start") {
292
+ return { ...base, top: "16px", left: "16px" } as JSX.CSSProperties;
293
+ }
294
+ return {
295
+ ...base,
296
+ top: "16px",
297
+ left: "50%",
298
+ transform: "translateX(-50%)",
299
+ } as JSX.CSSProperties;
300
+ }
301
+
302
+ if (align === "end") {
303
+ return { ...base, bottom: "16px", right: "16px" } as JSX.CSSProperties;
304
+ }
305
+ if (align === "start") {
306
+ return { ...base, bottom: "16px", left: "16px" } as JSX.CSSProperties;
278
307
  }
308
+ return {
309
+ ...base,
310
+ bottom: "16px",
311
+ left: "50%",
312
+ transform: "translateX(-50%)",
313
+ } as JSX.CSSProperties;
279
314
  });
280
315
 
316
+ const normalizedPlacement = () => normalizeToastPlacement(local.placement);
317
+
281
318
  const visibleToasts = () => state()?.visibleToasts() ?? [];
282
319
  const hasToasts = () => visibleToasts().length > 0;
283
320
 
284
321
  const regionContent = () => {
285
- const regionAria = getRegionAria();
286
- if (!regionAria || !state()) return null;
322
+ if (!state()) return null;
287
323
 
288
324
  // Merge styles - placement styles are base, renderProps.style() overrides
289
325
  const mergedStyle = () => {
@@ -293,25 +329,24 @@ export function ToastRegion(props: ToastRegionProps): JSX.Element {
293
329
  return { ...placement, ...custom } as JSX.CSSProperties;
294
330
  };
295
331
 
296
- // Extract ref from regionProps to avoid type conflicts
297
332
  const { ref: _ref, ...cleanRegionProps } = regionAria.regionProps as Record<string, unknown>;
298
333
 
299
334
  return (
300
335
  <div
336
+ ref={setRegionElement}
301
337
  {...domProps()}
302
338
  {...cleanRegionProps}
303
339
  class={renderProps.class()}
304
340
  style={mergedStyle()}
305
- data-placement={local.placement ?? 'bottom-end'}
341
+ data-placement={normalizedPlacement()}
306
342
  >
307
- {renderProps.renderChildren()}
343
+ {renderedChildren()}
308
344
  </div>
309
345
  );
310
346
  };
311
347
 
312
- // Only render when there are toasts
313
348
  return (
314
- <Show when={hasToasts()}>
349
+ <Show when={isHydrated() && hasToasts()}>
315
350
  <Show when={local.portal !== false} fallback={regionContent()}>
316
351
  <Portal mount={portalContainer()}>{regionContent()}</Portal>
317
352
  </Show>
@@ -319,10 +354,6 @@ export function ToastRegion(props: ToastRegionProps): JSX.Element {
319
354
  );
320
355
  }
321
356
 
322
- // ============================================
323
- // TOAST COMPONENT
324
- // ============================================
325
-
326
357
  /**
327
358
  * Toast is an individual notification component.
328
359
  *
@@ -339,53 +370,64 @@ export function ToastRegion(props: ToastRegionProps): JSX.Element {
339
370
  * ```
340
371
  */
341
372
  export function Toast(props: ToastProps): JSX.Element {
342
- const [local, rest] = splitProps(props, [
343
- 'toast',
344
- 'children',
345
- 'class',
346
- 'style',
347
- ]);
373
+ const [local, rest] = splitProps(props, ["toast", "children", "class", "style"]);
348
374
 
349
375
  let toastRef!: HTMLDivElement;
350
376
 
351
- // Get state from context
352
377
  const state = useToastContext();
353
378
 
354
- // Create toast accessibility props
379
+ createEffect(() => {
380
+ const key = local.toast.key;
381
+ toastStateByKey.set(key, state);
382
+ onCleanup(() => {
383
+ if (toastStateByKey.get(key) === state) {
384
+ toastStateByKey.delete(key);
385
+ }
386
+ });
387
+ });
388
+
389
+ const hasTitle = () => !!(local.toast.content.children ?? local.toast.content.title);
355
390
  const toastAria = createToast({
356
391
  toast: local.toast,
357
392
  state,
358
- hasTitle: !!local.toast.content.title,
393
+ hasTitle: hasTitle(),
359
394
  hasDescription: !!local.toast.content.description,
360
395
  });
361
396
 
362
- // Render props values
363
397
  const renderValues = createMemo<ToastRenderProps>(() => ({
364
- isEntering: local.toast.animation === 'entering',
365
- isExiting: local.toast.animation === 'exiting',
398
+ isEntering: local.toast.animation === "entering",
399
+ isExiting: local.toast.animation === "exiting",
366
400
  animation: local.toast.animation,
367
401
  toast: local.toast,
368
402
  }));
369
403
 
370
- // Resolve render props
371
404
  const renderProps = useRenderProps(
372
405
  {
373
406
  children: props.children,
374
407
  class: local.class,
375
408
  style: local.style,
376
- defaultClassName: 'solidaria-Toast',
409
+ defaultClassName: "solidaria-Toast",
377
410
  },
378
- renderValues
411
+ renderValues,
379
412
  );
380
413
 
381
- // Filter DOM props
382
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
414
+ const domProps = createMemo(() =>
415
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
416
+ );
383
417
 
384
- // Merge styles
385
418
  const mergedStyle = () => {
386
419
  const custom = renderProps.style();
387
- if (!custom) return { 'pointer-events': 'auto' as const };
388
- return { 'pointer-events': 'auto' as const, ...custom } as JSX.CSSProperties;
420
+ if (!custom) return { "pointer-events": "auto" as const };
421
+ return { "pointer-events": "auto" as const, ...custom } as JSX.CSSProperties;
422
+ };
423
+
424
+ const handleRootClick = (event: MouseEvent) => {
425
+ const target = event.target;
426
+ if (!(target instanceof Element)) return;
427
+ if (target.closest("[data-solidaria-toast-close-button]")) {
428
+ state.close(local.toast.key);
429
+ state.remove(local.toast.key);
430
+ }
389
431
  };
390
432
 
391
433
  // Exit animation lifecycle:
@@ -395,14 +437,14 @@ export function Toast(props: ToastProps): JSX.Element {
395
437
  // Reduced-motion is handled by CSS (shorter/no animations), so the lifecycle
396
438
  // naturally completes faster when the user prefers reduced motion.
397
439
  createEffect(() => {
398
- if (local.toast.animation !== 'exiting') return;
440
+ if (local.toast.animation !== "exiting") return;
399
441
  if (!toastRef) {
400
442
  state.remove(local.toast.key);
401
443
  return;
402
444
  }
403
445
 
404
446
  // Check if the element supports the Web Animations API
405
- if (!('getAnimations' in toastRef)) {
447
+ if (!("getAnimations" in toastRef)) {
406
448
  state.remove(local.toast.key);
407
449
  return;
408
450
  }
@@ -434,7 +476,6 @@ export function Toast(props: ToastProps): JSX.Element {
434
476
  });
435
477
  });
436
478
 
437
- // Extract ref from toastProps to avoid type conflicts
438
479
  const { ref: _ref, ...cleanToastProps } = toastAria.toastProps as Record<string, unknown>;
439
480
 
440
481
  // Ensure ARIA title/description IDs are present on rendered sub-components,
@@ -443,17 +484,19 @@ export function Toast(props: ToastProps): JSX.Element {
443
484
  if (!toastRef) return;
444
485
 
445
486
  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;
487
+ const descriptionId = (toastAria.descriptionProps as Record<string, unknown>).id as
488
+ | string
489
+ | undefined;
447
490
 
448
491
  if (titleId) {
449
- const titleEl = toastRef.querySelector('[data-solidaria-toast-title]');
492
+ const titleEl = toastRef.querySelector("[data-solidaria-toast-title]");
450
493
  if (titleEl instanceof HTMLElement) {
451
494
  titleEl.id = titleId;
452
495
  }
453
496
  }
454
497
 
455
498
  if (descriptionId) {
456
- const descriptionEl = toastRef.querySelector('[data-solidaria-toast-description]');
499
+ const descriptionEl = toastRef.querySelector("[data-solidaria-toast-description]");
457
500
  if (descriptionEl instanceof HTMLElement) {
458
501
  descriptionEl.id = descriptionId;
459
502
  }
@@ -461,7 +504,9 @@ export function Toast(props: ToastProps): JSX.Element {
461
504
  });
462
505
 
463
506
  return (
464
- <ToastAriaContext.Provider value={{ titleProps: toastAria.titleProps, descriptionProps: toastAria.descriptionProps }}>
507
+ <ToastAriaContext.Provider
508
+ value={{ titleProps: toastAria.titleProps, descriptionProps: toastAria.descriptionProps }}
509
+ >
465
510
  <div
466
511
  ref={toastRef}
467
512
  {...domProps()}
@@ -469,7 +514,9 @@ export function Toast(props: ToastProps): JSX.Element {
469
514
  class={renderProps.class()}
470
515
  style={mergedStyle()}
471
516
  data-animation={local.toast.animation}
472
- data-type={local.toast.content.type}
517
+ data-type={local.toast.content.type ?? local.toast.content.variant}
518
+ data-variant={local.toast.content.variant}
519
+ on:click={handleRootClick}
473
520
  >
474
521
  {renderProps.renderChildren()}
475
522
  </div>
@@ -477,10 +524,6 @@ export function Toast(props: ToastProps): JSX.Element {
477
524
  );
478
525
  }
479
526
 
480
- // ============================================
481
- // TOAST SUB-COMPONENTS
482
- // ============================================
483
-
484
527
  export interface ToastTitleProps {
485
528
  children: JSX.Element;
486
529
  class?: string;
@@ -512,10 +555,18 @@ export interface ToastDescriptionProps {
512
555
  */
513
556
  export function ToastDescription(props: ToastDescriptionProps): JSX.Element {
514
557
  const context = useContext(ToastAriaContext);
515
- const { ref: _ref, ...ariaDescriptionProps } = (context?.descriptionProps ?? {}) as Record<string, unknown>;
558
+ const { ref: _ref, ...ariaDescriptionProps } = (context?.descriptionProps ?? {}) as Record<
559
+ string,
560
+ unknown
561
+ >;
516
562
 
517
563
  return (
518
- <div data-solidaria-toast-description="" {...ariaDescriptionProps} class={props.class} style={props.style}>
564
+ <div
565
+ data-solidaria-toast-description=""
566
+ {...ariaDescriptionProps}
567
+ class={props.class}
568
+ style={props.style}
569
+ >
519
570
  {props.children}
520
571
  </div>
521
572
  );
@@ -527,17 +578,19 @@ export interface ToastCloseButtonProps {
527
578
  children?: JSX.Element;
528
579
  class?: string;
529
580
  style?: JSX.CSSProperties;
530
- 'aria-label'?: string;
581
+ "aria-label"?: string;
531
582
  }
532
583
 
533
584
  /**
534
585
  * ToastCloseButton is a button that closes the toast.
535
586
  */
536
587
  export function ToastCloseButton(props: ToastCloseButtonProps): JSX.Element {
537
- const state = useToastContext();
538
-
588
+ const contextState = useContext(ToastContext);
539
589
  const handleClose = () => {
540
- state.close(props.toast.key);
590
+ const key = props.toast.key;
591
+ const state = contextState ?? toastStateByKey.get(key);
592
+ state?.close(key);
593
+ state?.remove(key);
541
594
  };
542
595
 
543
596
  return (
@@ -545,18 +598,16 @@ export function ToastCloseButton(props: ToastCloseButtonProps): JSX.Element {
545
598
  type="button"
546
599
  class={props.class}
547
600
  style={props.style}
548
- aria-label={props['aria-label'] ?? 'Close'}
601
+ aria-label={props["aria-label"] ?? "Close"}
602
+ data-solidaria-toast-close-button=""
603
+ on:click={handleClose}
549
604
  onClick={handleClose}
550
605
  >
551
- {props.children ?? '×'}
606
+ {props.children ?? "×"}
552
607
  </button>
553
608
  );
554
609
  }
555
610
 
556
- // ============================================
557
- // DEFAULT TOAST RENDERING
558
- // ============================================
559
-
560
611
  export interface DefaultToastProps {
561
612
  toast: QueuedToast<ToastContent>;
562
613
  }
@@ -567,26 +618,33 @@ export interface DefaultToastProps {
567
618
  */
568
619
  export function DefaultToast(props: DefaultToastProps): JSX.Element {
569
620
  const content = () => props.toast.content;
621
+ const title = () => content().children ?? content().title;
622
+ const actionLabel = () => content().actionLabel ?? content().action?.label;
623
+ const actionHandler = () => content().onAction ?? content().action?.onAction;
624
+ const state = useToastContext();
625
+ const handleAction = () => {
626
+ actionHandler()?.();
627
+ if (content().shouldCloseOnAction) {
628
+ state.close(props.toast.key);
629
+ state.remove(props.toast.key);
630
+ }
631
+ };
570
632
 
571
633
  return (
572
634
  <Toast toast={props.toast}>
573
- <div style={{ display: 'flex', 'align-items': 'flex-start', gap: '12px' }}>
635
+ <div style={{ display: "flex", "align-items": "flex-start", gap: "12px" }}>
574
636
  <div style={{ flex: 1 }}>
575
- <Show when={content().title}>
576
- <ToastTitle style={{ 'font-weight': 'bold', 'margin-bottom': '4px' }}>
577
- {content().title}
637
+ <Show when={title()}>
638
+ <ToastTitle style={{ "font-weight": "bold", "margin-bottom": "4px" }}>
639
+ {title()}
578
640
  </ToastTitle>
579
641
  </Show>
580
642
  <Show when={content().description}>
581
643
  <ToastDescription>{content().description}</ToastDescription>
582
644
  </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}
645
+ <Show when={actionLabel()}>
646
+ <button type="button" style={{ "margin-top": "8px" }} onClick={handleAction}>
647
+ {actionLabel()}
590
648
  </button>
591
649
  </Show>
592
650
  </div>