@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.
Files changed (64) hide show
  1. package/dist/Color.d.ts +2 -6
  2. package/dist/Color.d.ts.map +1 -1
  3. package/dist/ComboBox.d.ts +3 -3
  4. package/dist/ComboBox.d.ts.map +1 -1
  5. package/dist/GridList.d.ts +2 -2
  6. package/dist/GridList.d.ts.map +1 -1
  7. package/dist/ListBox.d.ts +5 -5
  8. package/dist/ListBox.d.ts.map +1 -1
  9. package/dist/Menu.d.ts +3 -3
  10. package/dist/Menu.d.ts.map +1 -1
  11. package/dist/Select.d.ts +3 -3
  12. package/dist/Select.d.ts.map +1 -1
  13. package/dist/Table.d.ts +2 -2
  14. package/dist/Table.d.ts.map +1 -1
  15. package/dist/Tabs.d.ts +1 -1
  16. package/dist/Tabs.d.ts.map +1 -1
  17. package/dist/index.js +56 -56
  18. package/dist/index.js.map +2 -2
  19. package/dist/index.ssr.js +56 -56
  20. package/dist/index.ssr.js.map +2 -2
  21. package/package.json +10 -8
  22. package/src/Autocomplete.tsx +174 -0
  23. package/src/Breadcrumbs.tsx +264 -0
  24. package/src/Button.tsx +238 -0
  25. package/src/Calendar.tsx +471 -0
  26. package/src/Checkbox.tsx +387 -0
  27. package/src/Color.tsx +1370 -0
  28. package/src/ComboBox.tsx +824 -0
  29. package/src/DateField.tsx +337 -0
  30. package/src/DatePicker.tsx +367 -0
  31. package/src/Dialog.tsx +262 -0
  32. package/src/Disclosure.tsx +439 -0
  33. package/src/GridList.tsx +511 -0
  34. package/src/Landmark.tsx +203 -0
  35. package/src/Link.tsx +201 -0
  36. package/src/ListBox.tsx +346 -0
  37. package/src/Menu.tsx +544 -0
  38. package/src/Meter.tsx +157 -0
  39. package/src/Modal.tsx +433 -0
  40. package/src/NumberField.tsx +542 -0
  41. package/src/Popover.tsx +540 -0
  42. package/src/ProgressBar.tsx +162 -0
  43. package/src/RadioGroup.tsx +356 -0
  44. package/src/RangeCalendar.tsx +462 -0
  45. package/src/SearchField.tsx +479 -0
  46. package/src/Select.tsx +734 -0
  47. package/src/Separator.tsx +130 -0
  48. package/src/Slider.tsx +500 -0
  49. package/src/Switch.tsx +213 -0
  50. package/src/Table.tsx +857 -0
  51. package/src/Tabs.tsx +552 -0
  52. package/src/TagGroup.tsx +421 -0
  53. package/src/TextField.tsx +271 -0
  54. package/src/TimeField.tsx +455 -0
  55. package/src/Toast.tsx +503 -0
  56. package/src/Toolbar.tsx +160 -0
  57. package/src/Tooltip.tsx +423 -0
  58. package/src/Tree.tsx +551 -0
  59. package/src/VisuallyHidden.tsx +60 -0
  60. package/src/contexts.ts +74 -0
  61. package/src/index.ts +620 -0
  62. package/src/utils.tsx +329 -0
  63. package/dist/index.jsx +0 -9056
  64. package/dist/index.jsx.map +0 -7
@@ -0,0 +1,511 @@
1
+ /**
2
+ * GridList component for solidaria-components
3
+ *
4
+ * A pre-wired headless grid list that combines state + aria hooks.
5
+ * Based on react-aria-components/src/GridList.tsx
6
+ *
7
+ * GridList is similar to ListBox but supports interactive elements within items
8
+ * and uses grid keyboard navigation.
9
+ */
10
+
11
+ import {
12
+ type JSX,
13
+ createContext,
14
+ createMemo,
15
+ createSignal,
16
+ splitProps,
17
+ useContext,
18
+ For,
19
+ } from 'solid-js';
20
+ import {
21
+ createGridList,
22
+ createGridListItem,
23
+ createGridListSelectionCheckbox,
24
+ createFocusRing,
25
+ createHover,
26
+ type AriaGridListProps,
27
+ } from '@proyecto-viviana/solidaria';
28
+ import {
29
+ createGridState,
30
+ type GridState,
31
+ type GridCollection,
32
+ type GridNode,
33
+ type Key,
34
+ } from '@proyecto-viviana/solid-stately';
35
+ import {
36
+ type RenderChildren,
37
+ type ClassNameOrFunction,
38
+ type StyleOrFunction,
39
+ type SlotProps,
40
+ useRenderProps,
41
+ filterDOMProps,
42
+ } from './utils';
43
+
44
+ // ============================================
45
+ // TYPES
46
+ // ============================================
47
+
48
+ export interface GridListRenderProps {
49
+ /** Whether the grid list has focus. */
50
+ isFocused: boolean;
51
+ /** Whether the grid list has keyboard focus. */
52
+ isFocusVisible: boolean;
53
+ /** Whether the grid list is disabled. */
54
+ isDisabled: boolean;
55
+ /** Whether the grid list is empty. */
56
+ isEmpty: boolean;
57
+ }
58
+
59
+ export interface GridListProps<T extends object> extends Omit<AriaGridListProps, 'children'>, SlotProps {
60
+ /** The items to render in the grid list. */
61
+ items: T[];
62
+ /** Function to get the key from an item. */
63
+ getKey?: (item: T) => Key;
64
+ /** Function to get the text value from an item. */
65
+ getTextValue?: (item: T) => string;
66
+ /** Function to check if an item is disabled. */
67
+ getDisabled?: (item: T) => boolean;
68
+ /** The selection mode. */
69
+ selectionMode?: 'none' | 'single' | 'multiple';
70
+ /** Keys of disabled items. */
71
+ disabledKeys?: Iterable<Key>;
72
+ /** Currently selected keys (controlled). */
73
+ selectedKeys?: 'all' | Iterable<Key>;
74
+ /** Default selected keys (uncontrolled). */
75
+ defaultSelectedKeys?: 'all' | Iterable<Key>;
76
+ /** Handler called when selection changes. */
77
+ onSelectionChange?: (keys: 'all' | Set<Key>) => void;
78
+ /** The children of the component. A function may be provided to render each item. */
79
+ children: (item: T) => JSX.Element;
80
+ /** The CSS className for the element. */
81
+ class?: ClassNameOrFunction<GridListRenderProps>;
82
+ /** The inline style for the element. */
83
+ style?: StyleOrFunction<GridListRenderProps>;
84
+ /** A function to render when the grid list is empty. */
85
+ renderEmptyState?: () => JSX.Element;
86
+ }
87
+
88
+ export interface GridListItemRenderProps {
89
+ /** Whether the item is selected. */
90
+ isSelected: boolean;
91
+ /** Whether the item is focused. */
92
+ isFocused: boolean;
93
+ /** Whether the item has keyboard focus. */
94
+ isFocusVisible: boolean;
95
+ /** Whether the item is pressed. */
96
+ isPressed: boolean;
97
+ /** Whether the item is hovered. */
98
+ isHovered: boolean;
99
+ /** Whether the item is disabled. */
100
+ isDisabled: boolean;
101
+ }
102
+
103
+ export interface GridListItemProps<T extends object> extends SlotProps {
104
+ /** The unique key for the item. */
105
+ id: Key;
106
+ /** The item value. */
107
+ item?: T;
108
+ /** The children of the item. A function may be provided to receive render props. */
109
+ children?: RenderChildren<GridListItemRenderProps>;
110
+ /** The CSS className for the element. */
111
+ class?: ClassNameOrFunction<GridListItemRenderProps>;
112
+ /** The inline style for the element. */
113
+ style?: StyleOrFunction<GridListItemRenderProps>;
114
+ /** The text value of the item (for accessibility). */
115
+ textValue?: string;
116
+ /** Handler called when the item is activated. */
117
+ onAction?: () => void;
118
+ }
119
+
120
+ // ============================================
121
+ // CONTEXT
122
+ // ============================================
123
+
124
+ interface GridListContextValue<T extends object> {
125
+ state: GridState<T, GridCollection<T>>;
126
+ collection: GridCollection<T>;
127
+ isDisabled: boolean;
128
+ }
129
+
130
+ export const GridListContext = createContext<GridListContextValue<object> | null>(null);
131
+ export const GridListStateContext = createContext<GridState<object, GridCollection<object>> | null>(null);
132
+
133
+ // ============================================
134
+ // HELPER: Build GridCollection from items
135
+ // ============================================
136
+
137
+ function buildGridCollection<T extends object>(
138
+ items: T[],
139
+ getKey?: (item: T) => Key,
140
+ getTextValue?: (item: T) => string,
141
+ getDisabled?: (item: T) => boolean
142
+ ): GridCollection<T> {
143
+ const nodes: GridNode<T>[] = items.map((item, index) => {
144
+ const key = getKey?.(item) ?? index;
145
+ return {
146
+ type: 'item' as const,
147
+ key,
148
+ value: item,
149
+ textValue: getTextValue?.(item) ?? String(key),
150
+ level: 0,
151
+ index,
152
+ hasChildNodes: false,
153
+ childNodes: [],
154
+ isDisabled: getDisabled?.(item),
155
+ };
156
+ });
157
+
158
+ const keyMap = new Map<Key, GridNode<T>>();
159
+ nodes.forEach((node) => keyMap.set(node.key, node));
160
+
161
+ return {
162
+ rows: nodes,
163
+ columns: [],
164
+ headerRows: [],
165
+ get rowCount() {
166
+ return nodes.length;
167
+ },
168
+ get columnCount() {
169
+ return 1;
170
+ },
171
+ get size() {
172
+ return nodes.length;
173
+ },
174
+ getKeys() {
175
+ return nodes.map((n) => n.key);
176
+ },
177
+ getItem(key: Key) {
178
+ return keyMap.get(key) ?? null;
179
+ },
180
+ at(index: number) {
181
+ return nodes[index] ?? null;
182
+ },
183
+ getKeyBefore(key: Key) {
184
+ const node = keyMap.get(key);
185
+ if (!node) return null;
186
+ return node.index > 0 ? nodes[node.index - 1].key : null;
187
+ },
188
+ getKeyAfter(key: Key) {
189
+ const node = keyMap.get(key);
190
+ if (!node) return null;
191
+ return node.index < nodes.length - 1 ? nodes[node.index + 1].key : null;
192
+ },
193
+ getFirstKey() {
194
+ return nodes[0]?.key ?? null;
195
+ },
196
+ getLastKey() {
197
+ return nodes[nodes.length - 1]?.key ?? null;
198
+ },
199
+ getChildren(_key: Key) {
200
+ return [];
201
+ },
202
+ getTextValue(key: Key) {
203
+ return keyMap.get(key)?.textValue ?? '';
204
+ },
205
+ getCell(_rowKey: Key, _columnKey: Key) {
206
+ return null;
207
+ },
208
+ [Symbol.iterator]() {
209
+ return nodes[Symbol.iterator]();
210
+ },
211
+ };
212
+ }
213
+
214
+ // ============================================
215
+ // COMPONENTS
216
+ // ============================================
217
+
218
+ /**
219
+ * A grid list displays a list of interactive items, with support for
220
+ * keyboard navigation, single or multiple selection, and row actions.
221
+ */
222
+ export function GridList<T extends object>(props: GridListProps<T>): JSX.Element {
223
+ const [local, stateProps, ariaProps] = splitProps(
224
+ props,
225
+ ['children', 'class', 'style', 'slot', 'renderEmptyState'],
226
+ [
227
+ 'items',
228
+ 'getKey',
229
+ 'getTextValue',
230
+ 'getDisabled',
231
+ 'disabledKeys',
232
+ 'selectionMode',
233
+ 'selectedKeys',
234
+ 'defaultSelectedKeys',
235
+ 'onSelectionChange',
236
+ ]
237
+ );
238
+
239
+ // Create ref signal
240
+ const [ref, setRef] = createSignal<HTMLUListElement | null>(null);
241
+
242
+ // Build collection
243
+ const collection = createMemo(() =>
244
+ buildGridCollection(
245
+ stateProps.items,
246
+ stateProps.getKey,
247
+ stateProps.getTextValue,
248
+ stateProps.getDisabled
249
+ )
250
+ );
251
+
252
+ // Get disabled keys from items + explicit disabledKeys
253
+ const allDisabledKeys = createMemo(() => {
254
+ const keys = new Set<Key>();
255
+
256
+ // Add explicitly disabled keys
257
+ if (stateProps.disabledKeys) {
258
+ for (const key of stateProps.disabledKeys) {
259
+ keys.add(key);
260
+ }
261
+ }
262
+
263
+ // Add keys from items marked as disabled
264
+ for (const node of collection().rows) {
265
+ if (node.isDisabled) {
266
+ keys.add(node.key);
267
+ }
268
+ }
269
+
270
+ return keys;
271
+ });
272
+
273
+ // Create grid state
274
+ const state = createGridState<T, GridCollection<T>>(() => ({
275
+ collection: collection(),
276
+ disabledKeys: allDisabledKeys(),
277
+ selectionMode: stateProps.selectionMode,
278
+ selectedKeys: stateProps.selectedKeys,
279
+ defaultSelectedKeys: stateProps.defaultSelectedKeys,
280
+ onSelectionChange: stateProps.onSelectionChange,
281
+ }));
282
+
283
+ // Create grid list aria props
284
+ const { gridProps } = createGridList<T, GridCollection<T>>(
285
+ () => ({
286
+ id: ariaProps.id,
287
+ 'aria-label': ariaProps['aria-label'],
288
+ 'aria-labelledby': ariaProps['aria-labelledby'],
289
+ 'aria-describedby': ariaProps['aria-describedby'],
290
+ isVirtualized: ariaProps.isVirtualized,
291
+ onAction: ariaProps.onAction,
292
+ isDisabled: ariaProps.isDisabled,
293
+ }),
294
+ () => state,
295
+ ref
296
+ );
297
+
298
+ // Create focus ring
299
+ const { isFocused, isFocusVisible, focusProps } = createFocusRing();
300
+
301
+ // Render props values
302
+ const renderValues = createMemo<GridListRenderProps>(() => ({
303
+ isFocused: state.isFocused || isFocused(),
304
+ isFocusVisible: isFocusVisible(),
305
+ isDisabled: ariaProps.isDisabled ?? false,
306
+ isEmpty: stateProps.items.length === 0,
307
+ }));
308
+
309
+ // Resolve render props
310
+ const renderProps = useRenderProps(
311
+ {
312
+ class: local.class,
313
+ style: local.style,
314
+ defaultClassName: 'solidaria-GridList',
315
+ },
316
+ renderValues
317
+ );
318
+
319
+ // Filter DOM props
320
+ const domProps = createMemo(() => {
321
+ const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
322
+ return filtered;
323
+ });
324
+
325
+ // Remove ref from spread props
326
+ const cleanGridProps = () => {
327
+ const { ref: _ref1, ...rest } = gridProps as Record<string, unknown>;
328
+ return rest;
329
+ };
330
+ const cleanFocusProps = () => {
331
+ const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
332
+ return rest;
333
+ };
334
+
335
+ const isEmpty = () => stateProps.items.length === 0;
336
+
337
+ const contextValue = createMemo<GridListContextValue<T>>(() => ({
338
+ state,
339
+ collection: collection(),
340
+ isDisabled: ariaProps.isDisabled ?? false,
341
+ }));
342
+
343
+ return (
344
+ <GridListContext.Provider value={contextValue() as GridListContextValue<object>}>
345
+ <GridListStateContext.Provider value={state as unknown as GridState<object, GridCollection<object>>}>
346
+ <ul
347
+ ref={setRef}
348
+ {...domProps()}
349
+ {...cleanGridProps()}
350
+ {...cleanFocusProps()}
351
+ class={renderProps.class()}
352
+ style={renderProps.style()}
353
+ data-focused={state.isFocused || undefined}
354
+ data-focus-visible={isFocusVisible() || undefined}
355
+ data-disabled={ariaProps.isDisabled || undefined}
356
+ data-empty={isEmpty() || undefined}
357
+ >
358
+ {isEmpty() && local.renderEmptyState ? (
359
+ local.renderEmptyState()
360
+ ) : (
361
+ <For each={stateProps.items}>{(item) => props.children(item)}</For>
362
+ )}
363
+ </ul>
364
+ </GridListStateContext.Provider>
365
+ </GridListContext.Provider>
366
+ );
367
+ }
368
+
369
+ /**
370
+ * An item in a grid list.
371
+ */
372
+ export function GridListItem<T extends object>(props: GridListItemProps<T>): JSX.Element {
373
+ const [local] = splitProps(props, [
374
+ 'class',
375
+ 'style',
376
+ 'slot',
377
+ 'id',
378
+ 'item',
379
+ 'textValue',
380
+ 'onAction',
381
+ ]);
382
+
383
+ // Get state from context
384
+ const context = useContext(GridListStateContext);
385
+ if (!context) {
386
+ throw new Error('GridListItem must be used within a GridList');
387
+ }
388
+ const state = context as GridState<T, GridCollection<T>>;
389
+
390
+ // Create ref signal
391
+ const [ref, setRef] = createSignal<HTMLLIElement | null>(null);
392
+
393
+ // Find or create the item node
394
+ const itemNode = createMemo(() => {
395
+ const node = state.collection.getItem(local.id);
396
+ if (!node) {
397
+ // Create a simple node for the item
398
+ return {
399
+ type: 'item' as const,
400
+ key: local.id,
401
+ value: local.item ?? null,
402
+ textValue: local.textValue ?? String(local.id),
403
+ level: 0,
404
+ index: 0,
405
+ hasChildNodes: false,
406
+ childNodes: [],
407
+ } as GridNode<T>;
408
+ }
409
+ return node as GridNode<T>;
410
+ });
411
+
412
+ // Create item aria props
413
+ const { rowProps, gridCellProps, isSelected, isDisabled, isPressed } = createGridListItem<T, GridCollection<T>>(
414
+ () => ({
415
+ node: itemNode(),
416
+ onAction: local.onAction,
417
+ }),
418
+ () => state,
419
+ ref
420
+ );
421
+
422
+ // Create hover
423
+ const { isHovered, hoverProps } = createHover({
424
+ get isDisabled() {
425
+ return isDisabled;
426
+ },
427
+ });
428
+
429
+ // Create focus ring
430
+ const { isFocusVisible, focusProps } = createFocusRing();
431
+
432
+ // Check if focused
433
+ const isFocused = createMemo(() => state.focusedKey === local.id);
434
+
435
+ // Render props values
436
+ const renderValues = createMemo<GridListItemRenderProps>(() => ({
437
+ isSelected,
438
+ isFocused: isFocused(),
439
+ isFocusVisible: isFocusVisible() && isFocused(),
440
+ isPressed,
441
+ isHovered: isHovered(),
442
+ isDisabled,
443
+ }));
444
+
445
+ // Resolve render props
446
+ const renderProps = useRenderProps(
447
+ {
448
+ children: props.children,
449
+ class: local.class,
450
+ style: local.style,
451
+ defaultClassName: 'solidaria-GridList-item',
452
+ },
453
+ renderValues
454
+ );
455
+
456
+ // Remove ref from spread props
457
+ const cleanRowProps = () => {
458
+ const { ref: _ref1, ...rest } = rowProps as Record<string, unknown>;
459
+ return rest;
460
+ };
461
+ const cleanHoverProps = () => {
462
+ const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
463
+ return rest;
464
+ };
465
+ const cleanFocusProps = () => {
466
+ const { ref: _ref3, ...rest } = focusProps as Record<string, unknown>;
467
+ return rest;
468
+ };
469
+
470
+ return (
471
+ <li
472
+ ref={setRef}
473
+ {...cleanRowProps()}
474
+ {...cleanHoverProps()}
475
+ {...cleanFocusProps()}
476
+ class={renderProps.class()}
477
+ style={renderProps.style()}
478
+ data-selected={isSelected || undefined}
479
+ data-focused={isFocused() || undefined}
480
+ data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
481
+ data-pressed={isPressed || undefined}
482
+ data-hovered={isHovered() || undefined}
483
+ data-disabled={isDisabled || undefined}
484
+ >
485
+ <div {...gridCellProps}>{renderProps.renderChildren()}</div>
486
+ </li>
487
+ );
488
+ }
489
+
490
+ /**
491
+ * A checkbox for item selection in a grid list.
492
+ */
493
+ export function GridListSelectionCheckbox(props: { itemKey: Key }): JSX.Element {
494
+ const context = useContext(GridListStateContext);
495
+ if (!context) {
496
+ throw new Error('GridListSelectionCheckbox must be used within a GridList');
497
+ }
498
+
499
+ const state = context as GridState<object, GridCollection<object>>;
500
+
501
+ const { checkboxProps } = createGridListSelectionCheckbox<object, GridCollection<object>>(
502
+ () => ({ key: props.itemKey }),
503
+ () => state
504
+ );
505
+
506
+ return <input {...checkboxProps} />;
507
+ }
508
+
509
+ // Attach Item and SelectionCheckbox as static properties
510
+ GridList.Item = GridListItem;
511
+ GridList.SelectionCheckbox = GridListSelectionCheckbox;
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Landmark component for solidaria-components
3
+ *
4
+ * Pre-wired headless landmark component that combines aria hooks.
5
+ * Enables F6 keyboard navigation between major page sections.
6
+ */
7
+
8
+ import {
9
+ type JSX,
10
+ createContext,
11
+ createMemo,
12
+ createSignal,
13
+ splitProps,
14
+ } from 'solid-js';
15
+ import { Dynamic } from 'solid-js/web';
16
+ import {
17
+ createLandmark,
18
+ getLandmarkController,
19
+ type AriaLandmarkProps,
20
+ type AriaLandmarkRole,
21
+ type LandmarkController,
22
+ } from '@proyecto-viviana/solidaria';
23
+ import {
24
+ type SlotProps,
25
+ filterDOMProps,
26
+ } from './utils';
27
+
28
+ // ============================================
29
+ // TYPES
30
+ // ============================================
31
+
32
+ export interface LandmarkRenderProps {
33
+ /** The ARIA landmark role. */
34
+ role: AriaLandmarkRole;
35
+ }
36
+
37
+ export interface LandmarkProps
38
+ extends AriaLandmarkProps,
39
+ SlotProps {
40
+ /**
41
+ * The HTML element type to render.
42
+ * @default 'div' (or semantic element based on role)
43
+ */
44
+ elementType?: keyof JSX.IntrinsicElements;
45
+ /** The CSS className for the element. A function may be provided to receive render props. */
46
+ class?: string | ((renderProps: LandmarkRenderProps) => string);
47
+ /** The inline style for the element. A function may be provided to receive render props. */
48
+ style?: JSX.CSSProperties | ((renderProps: LandmarkRenderProps) => JSX.CSSProperties);
49
+ /** Children content. */
50
+ children?: JSX.Element;
51
+ }
52
+
53
+ // Re-export types
54
+ export type { AriaLandmarkRole, LandmarkController };
55
+
56
+ // ============================================
57
+ // CONTEXT
58
+ // ============================================
59
+
60
+ export const LandmarkContext = createContext<LandmarkProps | null>(null);
61
+
62
+ // ============================================
63
+ // SEMANTIC ELEMENT MAPPING
64
+ // ============================================
65
+
66
+ /**
67
+ * Maps ARIA landmark roles to their semantic HTML elements.
68
+ * Using semantic elements is preferred when possible.
69
+ */
70
+ const roleToSemanticElement: Partial<Record<AriaLandmarkRole, keyof JSX.IntrinsicElements>> = {
71
+ main: 'main',
72
+ navigation: 'nav',
73
+ search: 'search', // HTML5.3 <search> element
74
+ banner: 'header',
75
+ contentinfo: 'footer',
76
+ complementary: 'aside',
77
+ form: 'form',
78
+ region: 'section',
79
+ };
80
+
81
+ // ============================================
82
+ // LANDMARK COMPONENT
83
+ // ============================================
84
+
85
+ /**
86
+ * A landmark is a region of the page that helps screen reader users navigate.
87
+ * Press F6 to cycle through landmarks, or Shift+F6 to go backwards.
88
+ *
89
+ * @example
90
+ * ```tsx
91
+ * // Main content area
92
+ * <Landmark role="main" aria-label="Main content">
93
+ * <h1>Welcome</h1>
94
+ * <p>Page content here...</p>
95
+ * </Landmark>
96
+ *
97
+ * // Navigation
98
+ * <Landmark role="navigation" aria-label="Primary navigation">
99
+ * <nav>...</nav>
100
+ * </Landmark>
101
+ *
102
+ * // Search
103
+ * <Landmark role="search" aria-label="Site search">
104
+ * <form>...</form>
105
+ * </Landmark>
106
+ *
107
+ * // Custom element type
108
+ * <Landmark role="region" aria-label="Featured content" elementType="div">
109
+ * ...
110
+ * </Landmark>
111
+ * ```
112
+ */
113
+ export function Landmark(props: LandmarkProps): JSX.Element {
114
+ const [local, ariaProps] = splitProps(props, [
115
+ 'class',
116
+ 'style',
117
+ 'slot',
118
+ 'children',
119
+ 'elementType',
120
+ ]);
121
+
122
+ // Element ref
123
+ const [ref, setRef] = createSignal<HTMLElement>();
124
+
125
+ // Determine the element type - use semantic element for the role if not specified
126
+ const elementType = createMemo(() => {
127
+ if (local.elementType) {
128
+ return local.elementType;
129
+ }
130
+ return roleToSemanticElement[ariaProps.role] ?? 'div';
131
+ });
132
+
133
+ // Create landmark aria props
134
+ const landmarkAria = createLandmark(
135
+ {
136
+ get role() { return ariaProps.role; },
137
+ get 'aria-label'() { return ariaProps['aria-label']; },
138
+ get 'aria-labelledby'() { return ariaProps['aria-labelledby']; },
139
+ get id() { return ariaProps.id; },
140
+ get focus() { return ariaProps.focus; },
141
+ },
142
+ ref
143
+ );
144
+
145
+ // Render props values
146
+ const renderValues = createMemo<LandmarkRenderProps>(() => ({
147
+ role: ariaProps.role,
148
+ }));
149
+
150
+ // Resolve class
151
+ const resolvedClass = createMemo(() => {
152
+ const cls = local.class;
153
+ if (typeof cls === 'function') {
154
+ return cls(renderValues());
155
+ }
156
+ return cls ?? `solidaria-Landmark solidaria-Landmark--${ariaProps.role}`;
157
+ });
158
+
159
+ // Resolve style
160
+ const resolvedStyle = createMemo(() => {
161
+ const style = local.style;
162
+ if (typeof style === 'function') {
163
+ return style(renderValues());
164
+ }
165
+ return style;
166
+ });
167
+
168
+ // Filter DOM props
169
+ const domProps = createMemo(() => filterDOMProps(ariaProps, { global: true }));
170
+
171
+ return (
172
+ <Dynamic
173
+ component={elementType()}
174
+ ref={setRef}
175
+ {...domProps()}
176
+ {...landmarkAria.landmarkProps}
177
+ class={resolvedClass()}
178
+ style={resolvedStyle()}
179
+ slot={local.slot}
180
+ >
181
+ {props.children}
182
+ </Dynamic>
183
+ );
184
+ }
185
+
186
+ // ============================================
187
+ // LANDMARK CONTROLLER EXPORT
188
+ // ============================================
189
+
190
+ /**
191
+ * Returns a controller for programmatic landmark navigation.
192
+ *
193
+ * @example
194
+ * ```tsx
195
+ * const controller = useLandmarkController();
196
+ *
197
+ * <button onClick={() => controller.focusMain()}>Skip to main content</button>
198
+ * <button onClick={() => controller.focusNext()}>Next landmark</button>
199
+ * ```
200
+ */
201
+ export function useLandmarkController(): LandmarkController {
202
+ return getLandmarkController();
203
+ }