@proyecto-viviana/solidaria-components 0.2.2 → 0.2.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.
- package/dist/Color.d.ts +2 -6
- package/dist/Color.d.ts.map +1 -1
- package/dist/ComboBox.d.ts +3 -3
- package/dist/ComboBox.d.ts.map +1 -1
- package/dist/GridList.d.ts +2 -2
- package/dist/GridList.d.ts.map +1 -1
- package/dist/ListBox.d.ts +5 -5
- package/dist/ListBox.d.ts.map +1 -1
- package/dist/Menu.d.ts +3 -3
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Select.d.ts +3 -3
- package/dist/Select.d.ts.map +1 -1
- package/dist/Table.d.ts +2 -2
- package/dist/Table.d.ts.map +1 -1
- package/dist/Tabs.d.ts +1 -1
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/index.js +56 -56
- package/dist/index.js.map +2 -2
- package/dist/index.ssr.js +56 -56
- package/dist/index.ssr.js.map +2 -2
- package/package.json +10 -8
- package/src/Autocomplete.tsx +174 -0
- package/src/Breadcrumbs.tsx +264 -0
- package/src/Button.tsx +238 -0
- package/src/Calendar.tsx +471 -0
- package/src/Checkbox.tsx +387 -0
- package/src/Color.tsx +1370 -0
- package/src/ComboBox.tsx +824 -0
- package/src/DateField.tsx +337 -0
- package/src/DatePicker.tsx +367 -0
- package/src/Dialog.tsx +262 -0
- package/src/Disclosure.tsx +439 -0
- package/src/GridList.tsx +511 -0
- package/src/Landmark.tsx +203 -0
- package/src/Link.tsx +201 -0
- package/src/ListBox.tsx +346 -0
- package/src/Menu.tsx +544 -0
- package/src/Meter.tsx +157 -0
- package/src/Modal.tsx +433 -0
- package/src/NumberField.tsx +542 -0
- package/src/Popover.tsx +540 -0
- package/src/ProgressBar.tsx +162 -0
- package/src/RadioGroup.tsx +356 -0
- package/src/RangeCalendar.tsx +462 -0
- package/src/SearchField.tsx +479 -0
- package/src/Select.tsx +734 -0
- package/src/Separator.tsx +130 -0
- package/src/Slider.tsx +500 -0
- package/src/Switch.tsx +213 -0
- package/src/Table.tsx +857 -0
- package/src/Tabs.tsx +552 -0
- package/src/TagGroup.tsx +421 -0
- package/src/TextField.tsx +271 -0
- package/src/TimeField.tsx +455 -0
- package/src/Toast.tsx +503 -0
- package/src/Toolbar.tsx +160 -0
- package/src/Tooltip.tsx +423 -0
- package/src/Tree.tsx +551 -0
- package/src/VisuallyHidden.tsx +60 -0
- package/src/contexts.ts +74 -0
- package/src/index.ts +620 -0
- package/src/utils.tsx +329 -0
- package/dist/index.jsx +0 -9056
- package/dist/index.jsx.map +0 -7
package/src/Toast.tsx
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast components for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* Toast notifications with auto-dismiss, pause on hover/focus, and accessibility.
|
|
5
|
+
* Port of react-aria-components Toast.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type JSX,
|
|
10
|
+
createContext,
|
|
11
|
+
createMemo,
|
|
12
|
+
splitProps,
|
|
13
|
+
Show,
|
|
14
|
+
useContext,
|
|
15
|
+
} from 'solid-js';
|
|
16
|
+
import { Portal, isServer } from 'solid-js/web';
|
|
17
|
+
import {
|
|
18
|
+
type ToastState,
|
|
19
|
+
type QueuedToast,
|
|
20
|
+
type ToastQueueOptions,
|
|
21
|
+
ToastQueue,
|
|
22
|
+
createToastState,
|
|
23
|
+
} from '@proyecto-viviana/solid-stately';
|
|
24
|
+
import {
|
|
25
|
+
createToast,
|
|
26
|
+
createToastRegion,
|
|
27
|
+
} from '@proyecto-viviana/solidaria';
|
|
28
|
+
import {
|
|
29
|
+
type RenderChildren,
|
|
30
|
+
type ClassNameOrFunction,
|
|
31
|
+
type StyleOrFunction,
|
|
32
|
+
useRenderProps,
|
|
33
|
+
filterDOMProps,
|
|
34
|
+
} from './utils';
|
|
35
|
+
|
|
36
|
+
// ============================================
|
|
37
|
+
// TYPES
|
|
38
|
+
// ============================================
|
|
39
|
+
|
|
40
|
+
export interface ToastContent {
|
|
41
|
+
/** The title of the toast. */
|
|
42
|
+
title?: string;
|
|
43
|
+
/** The description/body of the toast. */
|
|
44
|
+
description?: string;
|
|
45
|
+
/** The type/variant of the toast (info, success, warning, error). */
|
|
46
|
+
type?: 'info' | 'success' | 'warning' | 'error';
|
|
47
|
+
/** Custom action button. */
|
|
48
|
+
action?: {
|
|
49
|
+
label: string;
|
|
50
|
+
onAction: () => void;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ToastRenderProps {
|
|
55
|
+
/** Whether the toast is currently animating in. */
|
|
56
|
+
isEntering: boolean;
|
|
57
|
+
/** Whether the toast is currently animating out. */
|
|
58
|
+
isExiting: boolean;
|
|
59
|
+
/** The animation state (entering, exiting, queued). */
|
|
60
|
+
animation: 'entering' | 'exiting' | 'queued' | undefined;
|
|
61
|
+
/** The toast data. */
|
|
62
|
+
toast: QueuedToast<ToastContent>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ToastRegionRenderProps {
|
|
66
|
+
/** The visible toasts. */
|
|
67
|
+
visibleToasts: QueuedToast<ToastContent>[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ToastRegionProps {
|
|
71
|
+
/** The children of the component - can be JSX or render function. */
|
|
72
|
+
children?: RenderChildren<ToastRegionRenderProps>;
|
|
73
|
+
/** The CSS className for the element. */
|
|
74
|
+
class?: ClassNameOrFunction<ToastRegionRenderProps>;
|
|
75
|
+
/** The inline style for the element. */
|
|
76
|
+
style?: StyleOrFunction<ToastRegionRenderProps>;
|
|
77
|
+
/** The toast state to display. If not provided, uses ToastContext. */
|
|
78
|
+
state?: ToastState<ToastContent>;
|
|
79
|
+
/** Accessible label for the region. */
|
|
80
|
+
'aria-label'?: string;
|
|
81
|
+
/** Whether to render in a portal. */
|
|
82
|
+
portal?: boolean;
|
|
83
|
+
/** Placement of the toast region. */
|
|
84
|
+
placement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ToastProps {
|
|
88
|
+
/** The toast data. */
|
|
89
|
+
toast: QueuedToast<ToastContent>;
|
|
90
|
+
/** The children of the component - can be JSX or render function. */
|
|
91
|
+
children?: RenderChildren<ToastRenderProps>;
|
|
92
|
+
/** The CSS className for the element. */
|
|
93
|
+
class?: ClassNameOrFunction<ToastRenderProps>;
|
|
94
|
+
/** The inline style for the element. */
|
|
95
|
+
style?: StyleOrFunction<ToastRenderProps>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================
|
|
99
|
+
// CONTEXT
|
|
100
|
+
// ============================================
|
|
101
|
+
|
|
102
|
+
export const ToastContext = createContext<ToastState<ToastContent> | null>(null);
|
|
103
|
+
|
|
104
|
+
export function useToastContext(): ToastState<ToastContent> {
|
|
105
|
+
const context = useContext(ToastContext);
|
|
106
|
+
if (!context) {
|
|
107
|
+
throw new Error('Toast components must be used within a ToastProvider');
|
|
108
|
+
}
|
|
109
|
+
return context;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================
|
|
113
|
+
// GLOBAL TOAST QUEUE
|
|
114
|
+
// ============================================
|
|
115
|
+
|
|
116
|
+
/** Default global toast queue that can be used for app-wide toasts. */
|
|
117
|
+
export const globalToastQueue = new ToastQueue<ToastContent>({
|
|
118
|
+
maxVisibleToasts: 5,
|
|
119
|
+
hasExitAnimation: false, // TODO: Enable once exit animation handling is implemented
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Add a toast to the global queue.
|
|
124
|
+
* Convenience function for adding toasts from anywhere in the app.
|
|
125
|
+
*/
|
|
126
|
+
export function addToast(
|
|
127
|
+
content: ToastContent,
|
|
128
|
+
options?: { timeout?: number; priority?: number }
|
|
129
|
+
): string {
|
|
130
|
+
return globalToastQueue.add(content, options);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================
|
|
134
|
+
// TOAST PROVIDER
|
|
135
|
+
// ============================================
|
|
136
|
+
|
|
137
|
+
export interface ToastProviderProps {
|
|
138
|
+
/** The children of the provider. */
|
|
139
|
+
children: JSX.Element;
|
|
140
|
+
/** Custom toast queue options. */
|
|
141
|
+
queueOptions?: ToastQueueOptions;
|
|
142
|
+
/** Use global queue instead of creating a new one. */
|
|
143
|
+
useGlobalQueue?: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* ToastProvider creates a toast queue context for descendant components.
|
|
148
|
+
* Use this to wrap your app or a section that needs toast notifications.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```tsx
|
|
152
|
+
* <ToastProvider>
|
|
153
|
+
* <App />
|
|
154
|
+
* <ToastRegion />
|
|
155
|
+
* </ToastProvider>
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export function ToastProvider(props: ToastProviderProps): JSX.Element {
|
|
159
|
+
const queue = props.useGlobalQueue
|
|
160
|
+
? globalToastQueue
|
|
161
|
+
: new ToastQueue<ToastContent>(props.queueOptions);
|
|
162
|
+
|
|
163
|
+
const state = createToastState({ queue });
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<ToastContext.Provider value={state}>
|
|
167
|
+
{props.children}
|
|
168
|
+
</ToastContext.Provider>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================
|
|
173
|
+
// TOAST REGION COMPONENT
|
|
174
|
+
// ============================================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* ToastRegion is a container that displays all visible toasts.
|
|
178
|
+
* It handles pause on hover/focus and provides the landmark region.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```tsx
|
|
182
|
+
* <ToastRegion placement="bottom-end">
|
|
183
|
+
* {(renderProps) => (
|
|
184
|
+
* <For each={renderProps.visibleToasts}>
|
|
185
|
+
* {(toast) => <Toast toast={toast} />}
|
|
186
|
+
* </For>
|
|
187
|
+
* )}
|
|
188
|
+
* </ToastRegion>
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export function ToastRegion(props: ToastRegionProps): JSX.Element {
|
|
192
|
+
if (isServer) {
|
|
193
|
+
return null as unknown as JSX.Element;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const [local, rest] = splitProps(props, [
|
|
197
|
+
'children',
|
|
198
|
+
'class',
|
|
199
|
+
'style',
|
|
200
|
+
'state',
|
|
201
|
+
'aria-label',
|
|
202
|
+
'portal',
|
|
203
|
+
'placement',
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
// Get state from context if not provided
|
|
207
|
+
const contextState = useContext(ToastContext);
|
|
208
|
+
const state = () => local.state ?? contextState;
|
|
209
|
+
|
|
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
|
+
};
|
|
219
|
+
|
|
220
|
+
// Render props values
|
|
221
|
+
const renderValues = createMemo<ToastRegionRenderProps>(() => ({
|
|
222
|
+
visibleToasts: state()?.visibleToasts() ?? [],
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
// Resolve render props
|
|
226
|
+
const renderProps = useRenderProps(
|
|
227
|
+
{
|
|
228
|
+
children: props.children,
|
|
229
|
+
class: local.class,
|
|
230
|
+
style: local.style,
|
|
231
|
+
defaultClassName: 'solidaria-ToastRegion',
|
|
232
|
+
},
|
|
233
|
+
renderValues
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Filter DOM props
|
|
237
|
+
const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
|
|
238
|
+
|
|
239
|
+
// Placement styles
|
|
240
|
+
const placementStyles = createMemo<JSX.CSSProperties>(() => {
|
|
241
|
+
const placement = local.placement ?? 'bottom-end';
|
|
242
|
+
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',
|
|
250
|
+
};
|
|
251
|
+
|
|
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;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const visibleToasts = () => state()?.visibleToasts() ?? [];
|
|
270
|
+
const hasToasts = () => visibleToasts().length > 0;
|
|
271
|
+
|
|
272
|
+
const regionContent = () => {
|
|
273
|
+
const regionAria = getRegionAria();
|
|
274
|
+
if (!regionAria || !state()) return null;
|
|
275
|
+
|
|
276
|
+
// Merge styles - placement styles are base, renderProps.style() overrides
|
|
277
|
+
const mergedStyle = () => {
|
|
278
|
+
const placement = placementStyles();
|
|
279
|
+
const custom = renderProps.style();
|
|
280
|
+
if (!custom) return placement;
|
|
281
|
+
return { ...placement, ...custom } as JSX.CSSProperties;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Extract ref from regionProps to avoid type conflicts
|
|
285
|
+
const { ref: _ref, ...cleanRegionProps } = regionAria.regionProps as Record<string, unknown>;
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<div
|
|
289
|
+
{...domProps()}
|
|
290
|
+
{...cleanRegionProps}
|
|
291
|
+
class={renderProps.class()}
|
|
292
|
+
style={mergedStyle()}
|
|
293
|
+
data-placement={local.placement ?? 'bottom-end'}
|
|
294
|
+
>
|
|
295
|
+
{renderProps.renderChildren()}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Only render when there are toasts
|
|
301
|
+
return (
|
|
302
|
+
<Show when={hasToasts()}>
|
|
303
|
+
<Show when={local.portal !== false} fallback={regionContent()}>
|
|
304
|
+
<Portal>{regionContent()}</Portal>
|
|
305
|
+
</Show>
|
|
306
|
+
</Show>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ============================================
|
|
311
|
+
// TOAST COMPONENT
|
|
312
|
+
// ============================================
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Toast is an individual notification component.
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```tsx
|
|
319
|
+
* <Toast toast={toast}>
|
|
320
|
+
* {(renderProps) => (
|
|
321
|
+
* <div class={renderProps.isExiting ? 'fade-out' : 'fade-in'}>
|
|
322
|
+
* <h3>{renderProps.toast.content.title}</h3>
|
|
323
|
+
* <p>{renderProps.toast.content.description}</p>
|
|
324
|
+
* </div>
|
|
325
|
+
* )}
|
|
326
|
+
* </Toast>
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
export function Toast(props: ToastProps): JSX.Element {
|
|
330
|
+
const [local, rest] = splitProps(props, [
|
|
331
|
+
'toast',
|
|
332
|
+
'children',
|
|
333
|
+
'class',
|
|
334
|
+
'style',
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
// Get state from context
|
|
338
|
+
const state = useToastContext();
|
|
339
|
+
|
|
340
|
+
// Create toast accessibility props
|
|
341
|
+
const toastAria = createToast({
|
|
342
|
+
toast: local.toast,
|
|
343
|
+
state,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Render props values
|
|
347
|
+
const renderValues = createMemo<ToastRenderProps>(() => ({
|
|
348
|
+
isEntering: local.toast.animation === 'entering',
|
|
349
|
+
isExiting: local.toast.animation === 'exiting',
|
|
350
|
+
animation: local.toast.animation,
|
|
351
|
+
toast: local.toast,
|
|
352
|
+
}));
|
|
353
|
+
|
|
354
|
+
// Resolve render props
|
|
355
|
+
const renderProps = useRenderProps(
|
|
356
|
+
{
|
|
357
|
+
children: props.children,
|
|
358
|
+
class: local.class,
|
|
359
|
+
style: local.style,
|
|
360
|
+
defaultClassName: 'solidaria-Toast',
|
|
361
|
+
},
|
|
362
|
+
renderValues
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Filter DOM props
|
|
366
|
+
const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
|
|
367
|
+
|
|
368
|
+
// Merge styles
|
|
369
|
+
const mergedStyle = () => {
|
|
370
|
+
const custom = renderProps.style();
|
|
371
|
+
if (!custom) return { 'pointer-events': 'auto' as const };
|
|
372
|
+
return { 'pointer-events': 'auto' as const, ...custom } as JSX.CSSProperties;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Extract ref from toastProps to avoid type conflicts
|
|
376
|
+
const { ref: _ref, ...cleanToastProps } = toastAria.toastProps as Record<string, unknown>;
|
|
377
|
+
|
|
378
|
+
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}
|
|
386
|
+
>
|
|
387
|
+
{renderProps.renderChildren()}
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ============================================
|
|
393
|
+
// TOAST SUB-COMPONENTS
|
|
394
|
+
// ============================================
|
|
395
|
+
|
|
396
|
+
export interface ToastTitleProps {
|
|
397
|
+
children: JSX.Element;
|
|
398
|
+
class?: string;
|
|
399
|
+
style?: JSX.CSSProperties;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* ToastTitle renders the toast title with proper accessibility attributes.
|
|
404
|
+
*/
|
|
405
|
+
export function ToastTitle(props: ToastTitleProps): JSX.Element {
|
|
406
|
+
return (
|
|
407
|
+
<div class={props.class} style={props.style}>
|
|
408
|
+
{props.children}
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export interface ToastDescriptionProps {
|
|
414
|
+
children: JSX.Element;
|
|
415
|
+
class?: string;
|
|
416
|
+
style?: JSX.CSSProperties;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* ToastDescription renders the toast description with proper accessibility attributes.
|
|
421
|
+
*/
|
|
422
|
+
export function ToastDescription(props: ToastDescriptionProps): JSX.Element {
|
|
423
|
+
return (
|
|
424
|
+
<div class={props.class} style={props.style}>
|
|
425
|
+
{props.children}
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export interface ToastCloseButtonProps {
|
|
431
|
+
/** The toast to close. */
|
|
432
|
+
toast: QueuedToast<ToastContent>;
|
|
433
|
+
children?: JSX.Element;
|
|
434
|
+
class?: string;
|
|
435
|
+
style?: JSX.CSSProperties;
|
|
436
|
+
'aria-label'?: string;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* ToastCloseButton is a button that closes the toast.
|
|
441
|
+
*/
|
|
442
|
+
export function ToastCloseButton(props: ToastCloseButtonProps): JSX.Element {
|
|
443
|
+
const state = useToastContext();
|
|
444
|
+
|
|
445
|
+
const handleClose = () => {
|
|
446
|
+
state.close(props.toast.key);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<button
|
|
451
|
+
type="button"
|
|
452
|
+
class={props.class}
|
|
453
|
+
style={props.style}
|
|
454
|
+
aria-label={props['aria-label'] ?? 'Close'}
|
|
455
|
+
onClick={handleClose}
|
|
456
|
+
>
|
|
457
|
+
{props.children ?? '×'}
|
|
458
|
+
</button>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ============================================
|
|
463
|
+
// DEFAULT TOAST RENDERING
|
|
464
|
+
// ============================================
|
|
465
|
+
|
|
466
|
+
export interface DefaultToastProps {
|
|
467
|
+
toast: QueuedToast<ToastContent>;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* DefaultToast provides a basic toast layout with title, description, and close button.
|
|
472
|
+
* Use this as a starting point or as-is for simple toast needs.
|
|
473
|
+
*/
|
|
474
|
+
export function DefaultToast(props: DefaultToastProps): JSX.Element {
|
|
475
|
+
const content = () => props.toast.content;
|
|
476
|
+
|
|
477
|
+
return (
|
|
478
|
+
<Toast toast={props.toast}>
|
|
479
|
+
<div style={{ display: 'flex', 'align-items': 'flex-start', gap: '12px' }}>
|
|
480
|
+
<div style={{ flex: 1 }}>
|
|
481
|
+
<Show when={content().title}>
|
|
482
|
+
<ToastTitle style={{ 'font-weight': 'bold', 'margin-bottom': '4px' }}>
|
|
483
|
+
{content().title}
|
|
484
|
+
</ToastTitle>
|
|
485
|
+
</Show>
|
|
486
|
+
<Show when={content().description}>
|
|
487
|
+
<ToastDescription>{content().description}</ToastDescription>
|
|
488
|
+
</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}
|
|
496
|
+
</button>
|
|
497
|
+
</Show>
|
|
498
|
+
</div>
|
|
499
|
+
<ToastCloseButton toast={props.toast} />
|
|
500
|
+
</div>
|
|
501
|
+
</Toast>
|
|
502
|
+
);
|
|
503
|
+
}
|
package/src/Toolbar.tsx
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toolbar component for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* Pre-wired headless toolbar component that combines aria hooks.
|
|
5
|
+
* Port of react-aria-components/src/Toolbar.tsx
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type JSX,
|
|
10
|
+
type ParentProps,
|
|
11
|
+
createContext,
|
|
12
|
+
createMemo,
|
|
13
|
+
splitProps,
|
|
14
|
+
useContext,
|
|
15
|
+
} from 'solid-js'
|
|
16
|
+
import {
|
|
17
|
+
createToolbar,
|
|
18
|
+
type AriaToolbarProps,
|
|
19
|
+
type Orientation,
|
|
20
|
+
} from '@proyecto-viviana/solidaria'
|
|
21
|
+
import {
|
|
22
|
+
type SlotProps,
|
|
23
|
+
filterDOMProps,
|
|
24
|
+
} from './utils'
|
|
25
|
+
|
|
26
|
+
// ============================================
|
|
27
|
+
// TYPES
|
|
28
|
+
// ============================================
|
|
29
|
+
|
|
30
|
+
export interface ToolbarRenderProps {
|
|
31
|
+
/** The orientation of the toolbar. */
|
|
32
|
+
orientation: Orientation
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ToolbarProps
|
|
36
|
+
extends AriaToolbarProps,
|
|
37
|
+
ParentProps,
|
|
38
|
+
SlotProps {
|
|
39
|
+
/** The CSS className for the element. A function may be provided to receive render props. */
|
|
40
|
+
class?: string | ((renderProps: ToolbarRenderProps) => string)
|
|
41
|
+
/** The inline style for the element. A function may be provided to receive render props. */
|
|
42
|
+
style?: JSX.CSSProperties | ((renderProps: ToolbarRenderProps) => JSX.CSSProperties)
|
|
43
|
+
/** Additional data-* attributes. */
|
|
44
|
+
[key: `data-${string}`]: string | undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================
|
|
48
|
+
// CONTEXT
|
|
49
|
+
// ============================================
|
|
50
|
+
|
|
51
|
+
export interface ToolbarContextValue {
|
|
52
|
+
slots?: {
|
|
53
|
+
[name: string]: Record<string, unknown>
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const ToolbarContext = createContext<ToolbarContextValue | null>(null)
|
|
58
|
+
|
|
59
|
+
// ============================================
|
|
60
|
+
// TOOLBAR COMPONENT
|
|
61
|
+
// ============================================
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A toolbar is a container for a set of interactive controls,
|
|
65
|
+
* such as buttons, checkboxes, or links, with arrow key navigation.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* <Toolbar aria-label="Text formatting">
|
|
70
|
+
* <Button>Bold</Button>
|
|
71
|
+
* <Button>Italic</Button>
|
|
72
|
+
* <Button>Underline</Button>
|
|
73
|
+
* </Toolbar>
|
|
74
|
+
*
|
|
75
|
+
* // With render props
|
|
76
|
+
* <Toolbar orientation="vertical">
|
|
77
|
+
* {({ orientation }) => (
|
|
78
|
+
* <div data-orientation={orientation}>
|
|
79
|
+
* <Button>Cut</Button>
|
|
80
|
+
* <Button>Copy</Button>
|
|
81
|
+
* <Button>Paste</Button>
|
|
82
|
+
* </div>
|
|
83
|
+
* )}
|
|
84
|
+
* </Toolbar>
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
88
|
+
const [local, ariaProps, domProps] = splitProps(
|
|
89
|
+
props,
|
|
90
|
+
['class', 'style', 'slot', 'children'],
|
|
91
|
+
['orientation', 'aria-label', 'aria-labelledby']
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// Get slot props from context if available
|
|
95
|
+
const ctx = useContext(ToolbarContext)
|
|
96
|
+
const slotProps = () => {
|
|
97
|
+
if (ctx?.slots && local.slot) {
|
|
98
|
+
return ctx.slots[local.slot] || {}
|
|
99
|
+
}
|
|
100
|
+
return {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Merge slot props with explicit props
|
|
104
|
+
const mergedAriaProps = createMemo(() => ({
|
|
105
|
+
orientation: ariaProps.orientation,
|
|
106
|
+
'aria-label': ariaProps['aria-label'] ?? slotProps()['aria-label'] as string | undefined,
|
|
107
|
+
'aria-labelledby': ariaProps['aria-labelledby'],
|
|
108
|
+
}))
|
|
109
|
+
|
|
110
|
+
// Create toolbar aria props
|
|
111
|
+
const { toolbarProps, orientation } = createToolbar(mergedAriaProps())
|
|
112
|
+
|
|
113
|
+
// Render props values
|
|
114
|
+
const renderValues = createMemo<ToolbarRenderProps>(() => ({
|
|
115
|
+
orientation: orientation(),
|
|
116
|
+
}))
|
|
117
|
+
|
|
118
|
+
// Resolve class
|
|
119
|
+
const resolvedClass = createMemo(() => {
|
|
120
|
+
const cls = local.class
|
|
121
|
+
if (typeof cls === 'function') {
|
|
122
|
+
return cls(renderValues())
|
|
123
|
+
}
|
|
124
|
+
return cls ?? 'solidaria-Toolbar'
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Resolve style
|
|
128
|
+
const resolvedStyle = createMemo(() => {
|
|
129
|
+
const style = local.style
|
|
130
|
+
if (typeof style === 'function') {
|
|
131
|
+
return style(renderValues())
|
|
132
|
+
}
|
|
133
|
+
return style
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Resolve children (support render props)
|
|
137
|
+
const resolvedChildren = createMemo(() => {
|
|
138
|
+
const children = props.children
|
|
139
|
+
if (typeof children === 'function') {
|
|
140
|
+
return (children as (props: ToolbarRenderProps) => JSX.Element)(renderValues())
|
|
141
|
+
}
|
|
142
|
+
return children
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Filter remaining DOM props
|
|
146
|
+
const filteredDOMProps = createMemo(() => filterDOMProps(domProps, { global: true }))
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
{...filteredDOMProps()}
|
|
151
|
+
{...toolbarProps}
|
|
152
|
+
class={resolvedClass()}
|
|
153
|
+
style={resolvedStyle()}
|
|
154
|
+
slot={local.slot}
|
|
155
|
+
data-orientation={orientation()}
|
|
156
|
+
>
|
|
157
|
+
{resolvedChildren()}
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|