@proyecto-viviana/ui 0.3.2 → 0.3.4

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 (76) hide show
  1. package/dist/components.css +1077 -1077
  2. package/dist/index.js +236 -249
  3. package/dist/index.js.map +3 -3
  4. package/dist/index.ssr.js +78 -81
  5. package/dist/index.ssr.js.map +3 -3
  6. package/dist/radio/index.d.ts +12 -27
  7. package/dist/radio/index.d.ts.map +1 -1
  8. package/dist/test-utils/index.d.ts +2 -2
  9. package/dist/test-utils/index.d.ts.map +1 -1
  10. package/package.json +13 -12
  11. package/src/alert/index.tsx +48 -0
  12. package/src/assets/favicon.png +0 -0
  13. package/src/assets/fire.gif +0 -0
  14. package/src/autocomplete/index.tsx +313 -0
  15. package/src/avatar/index.tsx +75 -0
  16. package/src/badge/index.tsx +43 -0
  17. package/src/breadcrumbs/index.tsx +207 -0
  18. package/src/button/Button.tsx +74 -0
  19. package/src/button/index.ts +2 -0
  20. package/src/button/types.ts +24 -0
  21. package/src/calendar/DateField.tsx +200 -0
  22. package/src/calendar/DatePicker.tsx +298 -0
  23. package/src/calendar/RangeCalendar.tsx +236 -0
  24. package/src/calendar/TimeField.tsx +196 -0
  25. package/src/calendar/index.tsx +223 -0
  26. package/src/checkbox/index.tsx +257 -0
  27. package/src/color/index.tsx +687 -0
  28. package/src/combobox/index.tsx +383 -0
  29. package/src/components.css +1077 -0
  30. package/src/custom/calendar-card/index.tsx +66 -0
  31. package/src/custom/chip/index.tsx +46 -0
  32. package/src/custom/conversation/index.tsx +105 -0
  33. package/src/custom/event-card/index.tsx +132 -0
  34. package/src/custom/header/index.tsx +33 -0
  35. package/src/custom/lateral-nav/index.tsx +88 -0
  36. package/src/custom/logo/index.tsx +58 -0
  37. package/src/custom/nav-header/index.tsx +42 -0
  38. package/src/custom/page-layout/index.tsx +29 -0
  39. package/src/custom/profile-card/index.tsx +64 -0
  40. package/src/custom/project-card/index.tsx +59 -0
  41. package/src/custom/timeline-item/index.tsx +105 -0
  42. package/src/dialog/Dialog.tsx +260 -0
  43. package/src/dialog/index.tsx +3 -0
  44. package/src/disclosure/index.tsx +307 -0
  45. package/src/gridlist/index.tsx +403 -0
  46. package/src/icon/icons/GitHubIcon.tsx +20 -0
  47. package/src/icon/index.tsx +48 -0
  48. package/src/index.ts +322 -0
  49. package/src/landmark/index.tsx +231 -0
  50. package/src/link/index.tsx +130 -0
  51. package/src/listbox/index.tsx +231 -0
  52. package/src/menu/index.tsx +297 -0
  53. package/src/meter/index.tsx +163 -0
  54. package/src/numberfield/index.tsx +482 -0
  55. package/src/popover/index.tsx +260 -0
  56. package/src/progress-bar/index.tsx +169 -0
  57. package/src/radio/index.tsx +173 -0
  58. package/src/searchfield/index.tsx +453 -0
  59. package/src/select/index.tsx +349 -0
  60. package/src/separator/index.tsx +141 -0
  61. package/src/slider/index.tsx +382 -0
  62. package/src/styles.css +450 -0
  63. package/src/switch/ToggleSwitch.tsx +112 -0
  64. package/src/switch/index.tsx +90 -0
  65. package/src/table/index.tsx +531 -0
  66. package/src/tabs/index.tsx +273 -0
  67. package/src/tag-group/index.tsx +240 -0
  68. package/src/test-utils/index.ts +40 -0
  69. package/src/textfield/index.tsx +211 -0
  70. package/src/theme.css +101 -0
  71. package/src/toast/index.tsx +324 -0
  72. package/src/toolbar/index.tsx +108 -0
  73. package/src/tooltip/index.tsx +197 -0
  74. package/src/tree/index.tsx +494 -0
  75. package/dist/index.jsx +0 -6658
  76. package/dist/index.jsx.map +0 -7
@@ -0,0 +1,211 @@
1
+ /**
2
+ * TextField component for proyecto-viviana-ui
3
+ *
4
+ * A styled text field component built on solidaria hooks directly.
5
+ * This bypasses solidaria-components for now due to context timing issues.
6
+ */
7
+
8
+ import { type JSX, splitProps, mergeProps as solidMergeProps, Show } from 'solid-js'
9
+ import {
10
+ createTextField,
11
+ createFocusRing,
12
+ type AriaTextFieldProps,
13
+ } from '@proyecto-viviana/solidaria'
14
+ import {
15
+ createTextFieldState,
16
+ } from '@proyecto-viviana/solid-stately'
17
+
18
+ // ============================================
19
+ // TYPES
20
+ // ============================================
21
+
22
+ export type TextFieldSize = 'sm' | 'md' | 'lg'
23
+ export type TextFieldVariant = 'outline' | 'filled'
24
+
25
+ export interface TextFieldProps extends Omit<AriaTextFieldProps, 'children'> {
26
+ /** The size of the text field. */
27
+ size?: TextFieldSize
28
+ /** The visual variant of the text field. */
29
+ variant?: TextFieldVariant
30
+ /** Additional CSS class name. */
31
+ class?: string
32
+ /** Label text for the input. */
33
+ label?: string
34
+ /** Description text shown below the input. */
35
+ description?: string
36
+ /** Error message shown when invalid. */
37
+ errorMessage?: string
38
+ }
39
+
40
+ // ============================================
41
+ // STYLES
42
+ // ============================================
43
+
44
+ const sizeStyles = {
45
+ sm: {
46
+ input: 'h-8 px-2 text-sm',
47
+ label: 'text-sm',
48
+ description: 'text-xs',
49
+ },
50
+ md: {
51
+ input: 'h-10 px-3 text-base',
52
+ label: 'text-sm',
53
+ description: 'text-sm',
54
+ },
55
+ lg: {
56
+ input: 'h-12 px-4 text-lg',
57
+ label: 'text-base',
58
+ description: 'text-sm',
59
+ },
60
+ }
61
+
62
+ // ============================================
63
+ // COMPONENT
64
+ // ============================================
65
+
66
+ /**
67
+ * A text field allows users to enter a plain text value with a keyboard.
68
+ *
69
+ * Built directly on solidaria hooks for full accessibility support.
70
+ */
71
+ export function TextField(props: TextFieldProps): JSX.Element {
72
+ const defaultProps: Partial<TextFieldProps> = {
73
+ size: 'md',
74
+ variant: 'outline',
75
+ }
76
+
77
+ const merged = solidMergeProps(defaultProps, props)
78
+
79
+ const [local, ariaProps] = splitProps(merged, [
80
+ 'size',
81
+ 'variant',
82
+ 'class',
83
+ 'label',
84
+ 'description',
85
+ 'errorMessage',
86
+ ])
87
+
88
+ const size = () => sizeStyles[local.size!]
89
+
90
+ // Create text field state
91
+ const state = createTextFieldState(() => ({
92
+ value: ariaProps.value,
93
+ defaultValue: ariaProps.defaultValue,
94
+ onChange: ariaProps.onChange,
95
+ }))
96
+
97
+ // Create text field aria props
98
+ const textFieldAria = createTextField(() => ({
99
+ ...ariaProps,
100
+ value: state.value(),
101
+ onChange: state.setValue,
102
+ }))
103
+
104
+ // Create focus ring
105
+ const { isFocused, isFocusVisible, focusProps } = createFocusRing()
106
+
107
+ // Compute classes
108
+ const containerClasses = () => {
109
+ const base = 'flex flex-col'
110
+ const disabledClass = ariaProps.isDisabled ? 'opacity-60' : ''
111
+ const custom = local.class || ''
112
+ return [base, disabledClass, custom].filter(Boolean).join(' ')
113
+ }
114
+
115
+ const inputClasses = () => {
116
+ const base = 'w-full rounded-md transition-all duration-200 outline-none'
117
+ const sizeClass = size().input
118
+
119
+ let variantClass: string
120
+ if (local.variant === 'filled') {
121
+ variantClass = 'bg-bg-200 border border-transparent'
122
+ } else {
123
+ variantClass = 'bg-transparent border border-bg-400'
124
+ }
125
+
126
+ let stateClass: string
127
+ if (ariaProps.isDisabled) {
128
+ stateClass = 'bg-bg-200 text-primary-500 cursor-not-allowed'
129
+ } else if (textFieldAria.isInvalid) {
130
+ stateClass = 'border-danger-500 focus:border-danger-400 focus:ring-2 focus:ring-danger-400/20'
131
+ } else {
132
+ stateClass = 'text-primary-100 placeholder:text-primary-500 focus:border-accent focus:ring-2 focus:ring-accent/20'
133
+ }
134
+
135
+ const hoverClass = ariaProps.isDisabled ? '' : 'hover:border-accent-300'
136
+
137
+ return [base, sizeClass, variantClass, stateClass, hoverClass].filter(Boolean).join(' ')
138
+ }
139
+
140
+ const labelClasses = () => {
141
+ const base = 'block font-medium text-primary-200 mb-1'
142
+ const sizeClass = size().label
143
+ return [base, sizeClass].filter(Boolean).join(' ')
144
+ }
145
+
146
+ const descriptionClasses = () => {
147
+ const base = 'mt-1 text-primary-400'
148
+ const sizeClass = size().description
149
+ return [base, sizeClass].filter(Boolean).join(' ')
150
+ }
151
+
152
+ const errorClasses = () => {
153
+ const base = 'mt-1 text-danger-400'
154
+ const sizeClass = size().description
155
+ return [base, sizeClass].filter(Boolean).join(' ')
156
+ }
157
+
158
+ // Clean props - remove ref to avoid type conflicts
159
+ const cleanLabelProps = () => {
160
+ const { ref: _ref, ...rest } = textFieldAria.labelProps as Record<string, unknown>
161
+ return rest
162
+ }
163
+ const cleanInputProps = () => {
164
+ const { ref: _ref1, ...rest } = textFieldAria.inputProps as Record<string, unknown>
165
+ const { ref: _ref2, ...focusRest } = focusProps as Record<string, unknown>
166
+ return { ...rest, ...focusRest }
167
+ }
168
+ const cleanDescriptionProps = () => {
169
+ const { ref: _ref, ...rest } = textFieldAria.descriptionProps as Record<string, unknown>
170
+ return rest
171
+ }
172
+ const cleanErrorMessageProps = () => {
173
+ const { ref: _ref, ...rest } = textFieldAria.errorMessageProps as Record<string, unknown>
174
+ return rest
175
+ }
176
+
177
+ return (
178
+ <div
179
+ class={containerClasses()}
180
+ data-disabled={ariaProps.isDisabled || undefined}
181
+ data-invalid={textFieldAria.isInvalid || undefined}
182
+ data-readonly={ariaProps.isReadOnly || undefined}
183
+ data-required={ariaProps.isRequired || undefined}
184
+ data-focused={isFocused() || undefined}
185
+ data-focus-visible={isFocusVisible() || undefined}
186
+ >
187
+ <Show when={local.label}>
188
+ <label {...cleanLabelProps()} class={labelClasses()}>
189
+ {local.label}
190
+ <Show when={ariaProps.isRequired}>
191
+ <span class="text-danger-400 ml-0.5">*</span>
192
+ </Show>
193
+ </label>
194
+ </Show>
195
+
196
+ <input {...cleanInputProps()} class={inputClasses()} />
197
+
198
+ <Show when={local.description && !textFieldAria.isInvalid}>
199
+ <p {...cleanDescriptionProps()} class={descriptionClasses()}>
200
+ {local.description}
201
+ </p>
202
+ </Show>
203
+
204
+ <Show when={local.errorMessage && textFieldAria.isInvalid}>
205
+ <p {...cleanErrorMessageProps()} class={errorClasses()}>
206
+ {local.errorMessage}
207
+ </p>
208
+ </Show>
209
+ </div>
210
+ )
211
+ }
package/src/theme.css ADDED
@@ -0,0 +1,101 @@
1
+ /*
2
+ * Viviana UI Theme - Tailwind v4
3
+ *
4
+ * IMPORTANT: Due to Tailwind v4's @theme processing, the @theme block
5
+ * should be COPIED into your app's CSS, not imported via @import.
6
+ *
7
+ * Usage:
8
+ *
9
+ * 1. Add fonts in HTML <head>:
10
+ * <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ * <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ * <link href="https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100..900;1,100..900&family=Sen:wght@400..800&display=swap" rel="stylesheet">
13
+ *
14
+ * 2. In your app's CSS:
15
+ * @import "tailwindcss";
16
+ *
17
+ * @theme {
18
+ * // Copy the @theme block below into your CSS
19
+ * }
20
+ *
21
+ * @import "proyecto-viviana-ui/components.css";
22
+ */
23
+
24
+ @theme {
25
+ /* Breakpoints */
26
+ --breakpoint-xxs: 320px;
27
+ --breakpoint-xs: 475px;
28
+
29
+ /* Background - Dark blue-gray */
30
+ --color-bg-100: #3B5260;
31
+ --color-bg-200: #293E4B;
32
+ --color-bg-300: #24313a;
33
+ --color-bg-400: #1D272E;
34
+ --color-bg-light: #515151;
35
+
36
+ /* Accent - Pink */
37
+ --color-accent: #DF5C9A;
38
+ --color-accent-200: #FFB2D7;
39
+ --color-accent-300: #FF88C0;
40
+ --color-accent-500: #DF5C9A;
41
+ --color-accent-highlight: #e2a2be;
42
+
43
+ /* Primary - Cyan/Blue */
44
+ --color-primary-100: #D9F2FF;
45
+ --color-primary-200: #D0E9F5;
46
+ --color-primary-300: #ADCCDC;
47
+ --color-primary-400: #B8DFFF;
48
+ --color-primary-500: #75ABC7;
49
+ --color-primary-600: #58748B;
50
+ --color-primary-700: #4F6D85;
51
+ --color-primary-800: #4C6477;
52
+
53
+ /* Success - Green */
54
+ --color-success-100: #E5FFF3;
55
+ --color-success-400: #C2FFC8;
56
+ --color-success-600: #559D87;
57
+
58
+ /* Warning - Yellow */
59
+ --color-warning-100: #FFFBE5;
60
+ --color-warning-400: #FFDD87;
61
+ --color-warning-600: #9D8D55;
62
+
63
+ /* Danger - Red */
64
+ --color-danger-100: #FFE5E5;
65
+ --color-danger-400: #FF8787;
66
+ --color-danger-600: #9D5555;
67
+
68
+ /* Cards */
69
+ --color-cards-bg: #373737;
70
+ --color-cards-bg-load: #484848;
71
+ --color-correct: #83f1a7;
72
+ --color-incorrect: #e74c3c;
73
+
74
+ /* Typography */
75
+ --font-sen: 'Sen', sans-serif;
76
+ --font-jost: 'Jost', sans-serif;
77
+ --font-sans: 'Sen', sans-serif;
78
+ --font-mono: 'JetBrains Mono', monospace;
79
+ }
80
+
81
+ /* Font utility classes */
82
+ .font-sen {
83
+ font-family: 'Sen', sans-serif;
84
+ }
85
+
86
+ .font-jost {
87
+ font-family: 'Jost', sans-serif;
88
+ }
89
+
90
+ /* Custom drop shadows */
91
+ .drop-shadow-title-card {
92
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
93
+ }
94
+
95
+ .drop-shadow-logo {
96
+ filter: drop-shadow(0 0 10px rgba(223, 92, 154, 0.8));
97
+ }
98
+
99
+ .shadow-primary-chip {
100
+ box-shadow: 0 2px 8px rgba(79, 109, 133, 0.4);
101
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Toast components for proyecto-viviana-ui
3
+ *
4
+ * Toast notifications with auto-dismiss, animations, and variants.
5
+ * Built on top of solidaria-components for accessibility.
6
+ */
7
+
8
+ import { type JSX, splitProps, For, Show } from 'solid-js';
9
+ import {
10
+ Toast as HeadlessToast,
11
+ ToastRegion as HeadlessToastRegion,
12
+ ToastProvider as HeadlessToastProvider,
13
+ ToastContext,
14
+ ToastCloseButton as HeadlessToastCloseButton,
15
+ globalToastQueue,
16
+ addToast as headlessAddToast,
17
+ useToastContext,
18
+ type ToastContent,
19
+ type ToastProps as HeadlessToastProps,
20
+ type ToastRegionProps as HeadlessToastRegionProps,
21
+ type ToastProviderProps as HeadlessToastProviderProps,
22
+ type ToastRenderProps,
23
+ type ToastRegionRenderProps,
24
+ } from '@proyecto-viviana/solidaria-components';
25
+ import { type QueuedToast, type ToastOptions } from '@proyecto-viviana/solid-stately';
26
+
27
+ // ============================================
28
+ // TYPES
29
+ // ============================================
30
+
31
+ export type ToastPlacement = 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end';
32
+ export type ToastVariant = 'info' | 'success' | 'warning' | 'error' | 'neutral';
33
+
34
+ export interface ToastProviderProps extends HeadlessToastProviderProps {}
35
+
36
+ export interface ToastRegionProps extends Omit<HeadlessToastRegionProps, 'class' | 'style' | 'children'> {
37
+ /** The placement of the toast region. */
38
+ placement?: ToastPlacement;
39
+ /** Additional CSS class name. */
40
+ class?: string;
41
+ }
42
+
43
+ export interface ToastProps extends Omit<HeadlessToastProps, 'class' | 'style'> {
44
+ /** Additional CSS class name. */
45
+ class?: string;
46
+ }
47
+
48
+ // ============================================
49
+ // STYLES
50
+ // ============================================
51
+
52
+ const regionStyles = [
53
+ 'flex flex-col gap-3',
54
+ 'p-4',
55
+ ].join(' ');
56
+
57
+ const toastBaseStyles = [
58
+ 'flex items-start gap-3',
59
+ 'px-4 py-3',
60
+ 'rounded-lg shadow-lg',
61
+ 'min-w-[300px] max-w-[400px]',
62
+ 'border',
63
+ // Animations
64
+ 'data-[animation=entering]:animate-in data-[animation=entering]:fade-in-0 data-[animation=entering]:slide-in-from-right-5',
65
+ 'data-[animation=exiting]:animate-out data-[animation=exiting]:fade-out-0 data-[animation=exiting]:slide-out-to-right-5',
66
+ ].join(' ');
67
+
68
+ const variantStyles: Record<ToastVariant, string> = {
69
+ info: 'bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-950 dark:border-blue-800 dark:text-blue-200',
70
+ success: 'bg-green-50 border-green-200 text-green-800 dark:bg-green-950 dark:border-green-800 dark:text-green-200',
71
+ warning: 'bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-950 dark:border-yellow-800 dark:text-yellow-200',
72
+ error: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-950 dark:border-red-800 dark:text-red-200',
73
+ neutral: 'bg-neutral-50 border-neutral-200 text-neutral-800 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-200',
74
+ };
75
+
76
+ const iconStyles: Record<ToastVariant, string> = {
77
+ info: 'text-blue-500 dark:text-blue-400',
78
+ success: 'text-green-500 dark:text-green-400',
79
+ warning: 'text-yellow-500 dark:text-yellow-400',
80
+ error: 'text-red-500 dark:text-red-400',
81
+ neutral: 'text-neutral-500 dark:text-neutral-400',
82
+ };
83
+
84
+ const closeButtonStyles = [
85
+ 'ml-auto -mr-1 -mt-1',
86
+ 'p-1 rounded-md',
87
+ 'text-current opacity-60 hover:opacity-100',
88
+ 'transition-opacity',
89
+ 'focus:outline-none focus:ring-2 focus:ring-offset-2',
90
+ ].join(' ');
91
+
92
+ // ============================================
93
+ // ICONS
94
+ // ============================================
95
+
96
+ const InfoIcon = () => (
97
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
98
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
99
+ </svg>
100
+ );
101
+
102
+ const SuccessIcon = () => (
103
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
104
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
105
+ </svg>
106
+ );
107
+
108
+ const WarningIcon = () => (
109
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
110
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
111
+ </svg>
112
+ );
113
+
114
+ const ErrorIcon = () => (
115
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
116
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
117
+ </svg>
118
+ );
119
+
120
+ const CloseIcon = () => (
121
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
122
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
123
+ </svg>
124
+ );
125
+
126
+ const getVariantIcon = (variant: ToastVariant) => {
127
+ switch (variant) {
128
+ case 'success': return <SuccessIcon />;
129
+ case 'warning': return <WarningIcon />;
130
+ case 'error': return <ErrorIcon />;
131
+ case 'info':
132
+ case 'neutral':
133
+ default: return <InfoIcon />;
134
+ }
135
+ };
136
+
137
+ // ============================================
138
+ // COMPONENTS
139
+ // ============================================
140
+
141
+ /**
142
+ * ToastProvider creates a toast queue context for descendant components.
143
+ * Wrap your app or a section that needs toast notifications.
144
+ *
145
+ * @example
146
+ * ```tsx
147
+ * <ToastProvider>
148
+ * <App />
149
+ * <ToastRegion placement="bottom-end" />
150
+ * </ToastProvider>
151
+ * ```
152
+ */
153
+ export function ToastProvider(props: ToastProviderProps): JSX.Element {
154
+ return <HeadlessToastProvider {...props} />;
155
+ }
156
+
157
+ /**
158
+ * ToastRegion displays all visible toasts in a fixed position.
159
+ *
160
+ * @example
161
+ * ```tsx
162
+ * <ToastRegion placement="bottom-end" />
163
+ * ```
164
+ */
165
+ export function ToastRegion(props: ToastRegionProps): JSX.Element {
166
+ const [local, rest] = splitProps(props, ['placement', 'class']);
167
+
168
+ return (
169
+ <HeadlessToastRegion
170
+ {...rest}
171
+ placement={local.placement ?? 'bottom-end'}
172
+ class={(_renderProps: ToastRegionRenderProps) => {
173
+ return [regionStyles, local.class ?? ''].filter(Boolean).join(' ');
174
+ }}
175
+ >
176
+ {(regionProps: ToastRegionRenderProps) => (
177
+ <For each={regionProps.visibleToasts}>
178
+ {(toast) => <Toast toast={toast} />}
179
+ </For>
180
+ )}
181
+ </HeadlessToastRegion>
182
+ );
183
+ }
184
+
185
+ /**
186
+ * Toast displays an individual notification with icon, content, and close button.
187
+ *
188
+ * Usually you don't need to use this directly - ToastRegion renders toasts automatically.
189
+ */
190
+ export function Toast(props: ToastProps): JSX.Element {
191
+ const [local, rest] = splitProps(props, ['toast', 'class']);
192
+
193
+ const content = () => local.toast.content;
194
+ const variant = (): ToastVariant => content().type ?? 'neutral';
195
+
196
+ return (
197
+ <HeadlessToast
198
+ {...rest}
199
+ toast={local.toast}
200
+ class={(_renderProps: ToastRenderProps) => {
201
+ return [
202
+ toastBaseStyles,
203
+ variantStyles[variant()],
204
+ local.class ?? '',
205
+ ].filter(Boolean).join(' ');
206
+ }}
207
+ >
208
+ {/* Icon */}
209
+ <div class={`flex-shrink-0 ${iconStyles[variant()]}`}>
210
+ {getVariantIcon(variant())}
211
+ </div>
212
+
213
+ {/* Content */}
214
+ <div class="flex-1 min-w-0">
215
+ <Show when={content().title}>
216
+ <p class="font-semibold text-sm">{content().title}</p>
217
+ </Show>
218
+ <Show when={content().description}>
219
+ <p class="text-sm opacity-90 mt-1">{content().description}</p>
220
+ </Show>
221
+ <Show when={content().action}>
222
+ <button
223
+ type="button"
224
+ class="mt-2 text-sm font-medium underline hover:no-underline"
225
+ onClick={content().action?.onAction}
226
+ >
227
+ {content().action?.label}
228
+ </button>
229
+ </Show>
230
+ </div>
231
+
232
+ {/* Close Button */}
233
+ <HeadlessToastCloseButton
234
+ toast={local.toast}
235
+ class={closeButtonStyles}
236
+ aria-label="Dismiss"
237
+ >
238
+ <CloseIcon />
239
+ </HeadlessToastCloseButton>
240
+ </HeadlessToast>
241
+ );
242
+ }
243
+
244
+ // ============================================
245
+ // GLOBAL TOAST API
246
+ // ============================================
247
+
248
+ /**
249
+ * Add a toast to the global queue.
250
+ * Use this to show toasts from anywhere in your app.
251
+ *
252
+ * @example
253
+ * ```tsx
254
+ * // Show a success toast
255
+ * addToast({
256
+ * title: 'Success!',
257
+ * description: 'Your changes have been saved.',
258
+ * type: 'success',
259
+ * });
260
+ *
261
+ * // Show an error toast with auto-dismiss
262
+ * addToast({
263
+ * title: 'Error',
264
+ * description: 'Something went wrong.',
265
+ * type: 'error',
266
+ * }, { timeout: 5000 });
267
+ *
268
+ * // Show a toast with action
269
+ * addToast({
270
+ * title: 'File deleted',
271
+ * type: 'info',
272
+ * action: {
273
+ * label: 'Undo',
274
+ * onAction: () => restoreFile(),
275
+ * },
276
+ * }, { timeout: 10000 });
277
+ * ```
278
+ */
279
+ export function addToast(
280
+ content: ToastContent,
281
+ options?: ToastOptions
282
+ ): string {
283
+ return headlessAddToast(content, options);
284
+ }
285
+
286
+ /**
287
+ * Convenience function to show a success toast.
288
+ */
289
+ export function toastSuccess(message: string, options?: Omit<ToastOptions, 'priority'>): string {
290
+ return addToast({ title: message, type: 'success' }, { timeout: 5000, ...options });
291
+ }
292
+
293
+ /**
294
+ * Convenience function to show an error toast.
295
+ */
296
+ export function toastError(message: string, options?: Omit<ToastOptions, 'priority'>): string {
297
+ return addToast({ title: message, type: 'error' }, { timeout: 8000, ...options });
298
+ }
299
+
300
+ /**
301
+ * Convenience function to show a warning toast.
302
+ */
303
+ export function toastWarning(message: string, options?: Omit<ToastOptions, 'priority'>): string {
304
+ return addToast({ title: message, type: 'warning' }, { timeout: 6000, ...options });
305
+ }
306
+
307
+ /**
308
+ * Convenience function to show an info toast.
309
+ */
310
+ export function toastInfo(message: string, options?: Omit<ToastOptions, 'priority'>): string {
311
+ return addToast({ title: message, type: 'info' }, { timeout: 5000, ...options });
312
+ }
313
+
314
+ // Re-exports
315
+ export {
316
+ ToastContext,
317
+ globalToastQueue,
318
+ useToastContext,
319
+ type ToastContent,
320
+ type ToastRenderProps,
321
+ type ToastRegionRenderProps,
322
+ type QueuedToast,
323
+ type ToastOptions,
324
+ };