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