@tangible/ui 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/components/Accordion/Accordion.d.ts +22 -0
- package/components/Accordion/Accordion.js +192 -0
- package/components/Accordion/AccordionContext.d.ts +5 -0
- package/components/Accordion/AccordionContext.js +23 -0
- package/components/Accordion/index.d.ts +2 -0
- package/components/Accordion/index.js +1 -0
- package/components/Accordion/types.d.ts +61 -0
- package/components/Accordion/types.js +1 -0
- package/components/Avatar/Avatar.d.ts +11 -0
- package/components/Avatar/Avatar.js +67 -0
- package/components/Avatar/AvatarGroup.d.ts +11 -0
- package/components/Avatar/AvatarGroup.js +45 -0
- package/components/Avatar/index.d.ts +9 -0
- package/components/Avatar/index.js +7 -0
- package/components/Avatar/types.d.ts +44 -0
- package/components/Avatar/types.js +12 -0
- package/components/Button/Button.d.ts +4 -0
- package/components/Button/Button.js +33 -0
- package/components/Button/index.d.ts +2 -0
- package/components/Button/index.js +1 -0
- package/components/Button/types.d.ts +127 -0
- package/components/Button/types.js +1 -0
- package/components/Card/Card.d.ts +29 -0
- package/components/Card/Card.js +47 -0
- package/components/Card/index.d.ts +2 -0
- package/components/Card/index.js +1 -0
- package/components/Chip/Chip.d.ts +24 -0
- package/components/Chip/Chip.js +37 -0
- package/components/Chip/index.d.ts +2 -0
- package/components/Chip/index.js +1 -0
- package/components/Chips/Chips.d.ts +31 -0
- package/components/Chips/Chips.js +21 -0
- package/components/Chips/index.d.ts +2 -0
- package/components/Chips/index.js +1 -0
- package/components/ContentIndicator/ContentIndicator.d.ts +2 -0
- package/components/ContentIndicator/ContentIndicator.js +21 -0
- package/components/ContentIndicator/index.d.ts +2 -0
- package/components/ContentIndicator/index.js +1 -0
- package/components/ContentIndicator/types.d.ts +57 -0
- package/components/ContentIndicator/types.js +1 -0
- package/components/Dropdown/Dropdown.d.ts +31 -0
- package/components/Dropdown/Dropdown.js +219 -0
- package/components/Dropdown/DropdownContext.d.ts +3 -0
- package/components/Dropdown/DropdownContext.js +9 -0
- package/components/Dropdown/index.d.ts +2 -0
- package/components/Dropdown/index.js +1 -0
- package/components/Dropdown/types.d.ts +102 -0
- package/components/Dropdown/types.js +8 -0
- package/components/Icon/Icon.d.ts +22 -0
- package/components/Icon/Icon.js +24 -0
- package/components/Icon/index.d.ts +2 -0
- package/components/Icon/index.js +1 -0
- package/components/IconButton/IconButton.d.ts +2 -0
- package/components/IconButton/IconButton.js +50 -0
- package/components/IconButton/index.d.ts +2 -0
- package/components/IconButton/index.js +1 -0
- package/components/IconButton/types.d.ts +79 -0
- package/components/IconButton/types.js +1 -0
- package/components/Modal/Modal.d.ts +52 -0
- package/components/Modal/Modal.js +133 -0
- package/components/Modal/context.d.ts +6 -0
- package/components/Modal/context.js +9 -0
- package/components/Modal/index.d.ts +2 -0
- package/components/Modal/index.js +1 -0
- package/components/Notice/Notice.d.ts +93 -0
- package/components/Notice/Notice.js +144 -0
- package/components/Notice/index.d.ts +2 -0
- package/components/Notice/index.js +1 -0
- package/components/OverlapStack/OverlapStack.d.ts +44 -0
- package/components/OverlapStack/OverlapStack.js +41 -0
- package/components/OverlapStack/index.d.ts +2 -0
- package/components/OverlapStack/index.js +1 -0
- package/components/Pager/Pager.d.ts +26 -0
- package/components/Pager/Pager.js +151 -0
- package/components/Pager/index.d.ts +2 -0
- package/components/Pager/index.js +1 -0
- package/components/Progress/Progress.d.ts +2 -0
- package/components/Progress/Progress.js +100 -0
- package/components/Progress/index.d.ts +4 -0
- package/components/Progress/index.js +2 -0
- package/components/Progress/types.d.ts +251 -0
- package/components/Progress/types.js +1 -0
- package/components/Progress/useProgressSegments.d.ts +40 -0
- package/components/Progress/useProgressSegments.js +42 -0
- package/components/Rating/Rating.d.ts +32 -0
- package/components/Rating/Rating.js +74 -0
- package/components/Rating/index.d.ts +2 -0
- package/components/Rating/index.js +1 -0
- package/components/SegmentedControl/SegmentedControl.d.ts +10 -0
- package/components/SegmentedControl/SegmentedControl.js +183 -0
- package/components/SegmentedControl/SegmentedControlContext.d.ts +3 -0
- package/components/SegmentedControl/SegmentedControlContext.js +9 -0
- package/components/SegmentedControl/index.d.ts +2 -0
- package/components/SegmentedControl/index.js +1 -0
- package/components/SegmentedControl/types.d.ts +63 -0
- package/components/SegmentedControl/types.js +1 -0
- package/components/Sidebar/Sidebar.d.ts +17 -0
- package/components/Sidebar/Sidebar.js +107 -0
- package/components/Sidebar/index.d.ts +2 -0
- package/components/Sidebar/index.js +1 -0
- package/components/Sidebar/types.d.ts +65 -0
- package/components/Sidebar/types.js +4 -0
- package/components/StepIndicator/StepIndicator.d.ts +2 -0
- package/components/StepIndicator/StepIndicator.js +64 -0
- package/components/StepIndicator/index.d.ts +2 -0
- package/components/StepIndicator/index.js +1 -0
- package/components/StepIndicator/types.d.ts +68 -0
- package/components/StepIndicator/types.js +1 -0
- package/components/StepList/StepList.d.ts +12 -0
- package/components/StepList/StepList.js +59 -0
- package/components/StepList/StepListContext.d.ts +3 -0
- package/components/StepList/StepListContext.js +9 -0
- package/components/StepList/index.d.ts +2 -0
- package/components/StepList/index.js +1 -0
- package/components/StepList/types.d.ts +91 -0
- package/components/StepList/types.js +4 -0
- package/components/Table/BulkActionsBar.d.ts +12 -0
- package/components/Table/BulkActionsBar.js +9 -0
- package/components/Table/DataTable.d.ts +35 -0
- package/components/Table/DataTable.js +184 -0
- package/components/Table/Pagination.d.ts +13 -0
- package/components/Table/Pagination.js +13 -0
- package/components/Table/index.d.ts +2 -0
- package/components/Table/index.js +1 -0
- package/components/Tabs/Tabs.d.ts +23 -0
- package/components/Tabs/Tabs.js +309 -0
- package/components/Tabs/TabsContext.d.ts +3 -0
- package/components/Tabs/TabsContext.js +12 -0
- package/components/Tabs/index.d.ts +2 -0
- package/components/Tabs/index.js +1 -0
- package/components/Tabs/types.d.ts +75 -0
- package/components/Tabs/types.js +1 -0
- package/components/Toolbar/Toolbar.d.ts +18 -0
- package/components/Toolbar/Toolbar.js +241 -0
- package/components/Toolbar/index.d.ts +2 -0
- package/components/Toolbar/index.js +1 -0
- package/components/Toolbar/types.d.ts +28 -0
- package/components/Toolbar/types.js +1 -0
- package/components/Tooltip/Tooltip.d.ts +15 -0
- package/components/Tooltip/Tooltip.js +166 -0
- package/components/Tooltip/TooltipContext.d.ts +15 -0
- package/components/Tooltip/TooltipContext.js +25 -0
- package/components/Tooltip/index.d.ts +2 -0
- package/components/Tooltip/index.js +1 -0
- package/components/Tooltip/types.d.ts +85 -0
- package/components/Tooltip/types.js +8 -0
- package/components/index.d.ts +52 -0
- package/components/index.js +26 -0
- package/constants.d.ts +16 -0
- package/constants.js +16 -0
- package/icons/cred/index.d.ts +31 -0
- package/icons/cred/index.js +136 -0
- package/icons/icons.svg +155 -0
- package/icons/lms/index.d.ts +21 -0
- package/icons/lms/index.js +81 -0
- package/icons/manifest.json +1226 -0
- package/icons/player/index.d.ts +55 -0
- package/icons/player/index.js +268 -0
- package/icons/reaction/index.d.ts +79 -0
- package/icons/reaction/index.js +400 -0
- package/icons/registry.d.ts +316 -0
- package/icons/registry.js +163 -0
- package/icons/system/index.d.ts +155 -0
- package/icons/system/index.js +818 -0
- package/package.json +121 -0
- package/styles/all.css +1 -0
- package/styles/all.expanded.css +4137 -0
- package/styles/all.expanded.unlayered.css +4137 -0
- package/styles/all.unlayered.css +1 -0
- package/styles/components/_bundle.scss +51 -0
- package/styles/components/index.scss +1 -0
- package/styles/components/input/index.scss +248 -0
- package/styles/index.scss +71 -0
- package/styles/system/_constants.scss +12 -0
- package/styles/system/_motion.scss +47 -0
- package/styles/system/_palette-fns.scss +10 -0
- package/styles/system/_palettes.scss +80 -0
- package/styles/system/_tokens.scss +249 -0
- package/styles/system/index.scss +4 -0
- package/styles/utilities/_index.scss +373 -0
- package/tui-manifest.json +1858 -0
- package/types/index.d.ts +2 -0
- package/types/index.js +1 -0
- package/types/index.ts +2 -0
- package/types/sizes.d.ts +17 -0
- package/types/sizes.js +10 -0
- package/types/sizes.ts +21 -0
- package/types/svg.d.ts +5 -0
- package/types/themes.d.ts +14 -0
- package/types/themes.js +9 -0
- package/types/themes.ts +17 -0
- package/utils/color/contrast.d.ts +33 -0
- package/utils/color/contrast.js +88 -0
- package/utils/color-scheme.d.ts +25 -0
- package/utils/color-scheme.js +55 -0
- package/utils/compose-refs.d.ts +17 -0
- package/utils/compose-refs.js +38 -0
- package/utils/cx.d.ts +12 -0
- package/utils/cx.js +14 -0
- package/utils/focus-trap.d.ts +40 -0
- package/utils/focus-trap.js +93 -0
- package/utils/index.d.ts +10 -0
- package/utils/index.js +16 -0
- package/utils/math.d.ts +4 -0
- package/utils/math.js +19 -0
- package/utils/merge-props.d.ts +25 -0
- package/utils/merge-props.js +60 -0
- package/utils/polymorphic.d.ts +28 -0
- package/utils/polymorphic.js +44 -0
- package/utils/portal.d.ts +11 -0
- package/utils/portal.js +105 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { Icon } from '../Icon/index.js';
|
|
5
|
+
export function Rating({ value, defaultValue = 0, max = 5, disabled, readOnly, name, size = 'lg', theme = 'secondary', onChange, allowClear, className, gap, ariaLabel, }) {
|
|
6
|
+
const isControlled = value != null;
|
|
7
|
+
const [internal, setInternal] = React.useState(defaultValue);
|
|
8
|
+
const current = isControlled ? value : internal;
|
|
9
|
+
const generatedId = React.useId();
|
|
10
|
+
const groupName = name ?? generatedId;
|
|
11
|
+
const setValue = (v) => {
|
|
12
|
+
if (disabled || readOnly)
|
|
13
|
+
return;
|
|
14
|
+
if (!isControlled)
|
|
15
|
+
setInternal(v);
|
|
16
|
+
onChange?.(v);
|
|
17
|
+
};
|
|
18
|
+
const handleSelect = (n) => {
|
|
19
|
+
if (allowClear && current === n)
|
|
20
|
+
setValue(0);
|
|
21
|
+
else
|
|
22
|
+
setValue(n);
|
|
23
|
+
};
|
|
24
|
+
// Keyboard navigation per WAI-ARIA APG Radio Group pattern
|
|
25
|
+
// Arrow keys auto-select (not just navigate), Space/Enter for allowClear toggle
|
|
26
|
+
const onKeyDown = (e) => {
|
|
27
|
+
if (disabled || readOnly)
|
|
28
|
+
return;
|
|
29
|
+
switch (e.key) {
|
|
30
|
+
case 'ArrowLeft':
|
|
31
|
+
case 'ArrowDown':
|
|
32
|
+
// Move and select previous, wrap to max or stop at 1 based on allowClear
|
|
33
|
+
setValue(current <= 1 ? (allowClear ? max : 1) : current - 1);
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
break;
|
|
36
|
+
case 'ArrowRight':
|
|
37
|
+
case 'ArrowUp':
|
|
38
|
+
// Move and select next, wrap to 1 or stop at max
|
|
39
|
+
setValue(current >= max ? (allowClear ? 1 : max) : current + 1);
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
break;
|
|
42
|
+
case 'Home':
|
|
43
|
+
setValue(1);
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
break;
|
|
46
|
+
case 'End':
|
|
47
|
+
setValue(max);
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
break;
|
|
50
|
+
case ' ':
|
|
51
|
+
case 'Enter':
|
|
52
|
+
// Toggle clear if allowClear and already have selection
|
|
53
|
+
if (allowClear && current > 0) {
|
|
54
|
+
setValue(0);
|
|
55
|
+
}
|
|
56
|
+
else if (current === 0) {
|
|
57
|
+
setValue(1);
|
|
58
|
+
}
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const defaultAriaLabel = `Rating: ${current} of ${max}`;
|
|
66
|
+
// Roving tabindex: only the selected radio (or first if none) is in tab order
|
|
67
|
+
const focusableIndex = current > 0 ? current : 1;
|
|
68
|
+
return (_jsx("div", { className: cx('tui-rating', `is-size-${size}`, `is-theme-${theme}`, disabled && 'is-disabled', className), style: { gap }, role: readOnly ? 'img' : 'radiogroup', "aria-label": ariaLabel ?? defaultAriaLabel, onKeyDown: readOnly ? undefined : onKeyDown, children: Array.from({ length: max }).map((_, i) => {
|
|
69
|
+
const n = i + 1;
|
|
70
|
+
const checked = current >= n;
|
|
71
|
+
const id = `${groupName}__star-${n}`;
|
|
72
|
+
return (_jsxs("div", { className: "tui-rating__item", children: [!readOnly && (_jsx("input", { className: "tui-visually-hidden", type: "radio", id: id, name: groupName, value: n, checked: current === n, "aria-checked": current === n, tabIndex: n === focusableIndex ? 0 : -1, onChange: () => handleSelect(n), disabled: disabled })), readOnly ? (_jsx("span", { className: cx('tui-rating__star', checked && 'is-active'), children: _jsx(Icon, { name: checked ? 'system/star-fill' : 'system/star-outline' }) })) : (_jsxs("label", { className: cx('tui-rating__star', checked && 'is-active'), htmlFor: id, tabIndex: -1, children: [_jsx(Icon, { name: checked ? 'system/star-fill' : 'system/star-outline' }), _jsx("span", { className: "tui-visually-hidden", children: `${n} of ${max}` })] }))] }, n));
|
|
73
|
+
}) }));
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Rating } from './Rating.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SegmentedControlProps, SegmentedControlItemProps } from './types';
|
|
3
|
+
type SegmentedControlCompound = {
|
|
4
|
+
(props: SegmentedControlProps): React.JSX.Element;
|
|
5
|
+
displayName?: string;
|
|
6
|
+
Item: typeof Item;
|
|
7
|
+
};
|
|
8
|
+
declare function Item({ value, disabled, icon, 'aria-label': ariaLabel, className, children, }: SegmentedControlItemProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare const SegmentedControl: SegmentedControlCompound;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { SegmentedControlContext, useSegmentedControlContext } from './SegmentedControlContext.js';
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// SegmentedControl Root
|
|
7
|
+
// =============================================================================
|
|
8
|
+
function SegmentedControlRoot({ value: controlledValue, defaultValue, onValueChange, variant = 'pill', size = 'md', orientation = 'horizontal', loop = true, disabled = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }) {
|
|
9
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
10
|
+
const [, setRegistryVersion] = useState(0);
|
|
11
|
+
const itemsRef = useRef(new Map());
|
|
12
|
+
const mountCounterRef = useRef(0);
|
|
13
|
+
const isControlled = controlledValue !== undefined;
|
|
14
|
+
const selectedValue = isControlled ? controlledValue : internalValue;
|
|
15
|
+
// Item registration
|
|
16
|
+
const registerItem = useCallback((record) => {
|
|
17
|
+
const existing = itemsRef.current.get(record.value);
|
|
18
|
+
itemsRef.current.set(record.value, {
|
|
19
|
+
...record,
|
|
20
|
+
mountIndex: existing?.mountIndex ?? mountCounterRef.current++,
|
|
21
|
+
});
|
|
22
|
+
setRegistryVersion((v) => v + 1);
|
|
23
|
+
}, []);
|
|
24
|
+
const unregisterItem = useCallback((value) => {
|
|
25
|
+
itemsRef.current.delete(value);
|
|
26
|
+
setRegistryVersion((v) => v + 1);
|
|
27
|
+
}, []);
|
|
28
|
+
// Get items sorted by DOM order
|
|
29
|
+
const getOrderedItems = useCallback(() => {
|
|
30
|
+
const items = Array.from(itemsRef.current.values());
|
|
31
|
+
return items.sort((a, b) => {
|
|
32
|
+
const position = a.element.compareDocumentPosition(b.element);
|
|
33
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
34
|
+
return -1;
|
|
35
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING)
|
|
36
|
+
return 1;
|
|
37
|
+
return a.mountIndex - b.mountIndex;
|
|
38
|
+
});
|
|
39
|
+
}, []);
|
|
40
|
+
// Selection handler
|
|
41
|
+
const onSelect = useCallback((newValue) => {
|
|
42
|
+
if (isControlled) {
|
|
43
|
+
onValueChange?.(newValue);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
setInternalValue(newValue);
|
|
47
|
+
onValueChange?.(newValue);
|
|
48
|
+
}
|
|
49
|
+
}, [isControlled, onValueChange]);
|
|
50
|
+
// Dev-only: Warn if missing accessible name
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
// Safe check for dev mode - import.meta.env may not exist in all bundler contexts
|
|
53
|
+
const isDev = typeof import.meta !== 'undefined' && import.meta.env?.DEV;
|
|
54
|
+
if (isDev && !ariaLabel && !ariaLabelledBy) {
|
|
55
|
+
console.warn('SegmentedControl: Missing accessible name. Provide aria-label or aria-labelledby.');
|
|
56
|
+
}
|
|
57
|
+
}, [ariaLabel, ariaLabelledBy]);
|
|
58
|
+
// Keyboard navigation
|
|
59
|
+
const handleKeyDown = (event) => {
|
|
60
|
+
const items = getOrderedItems().filter((item) => !item.disabled && !disabled);
|
|
61
|
+
if (items.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
let currentIndex = items.findIndex((item) => item.value === selectedValue);
|
|
64
|
+
if (currentIndex === -1)
|
|
65
|
+
currentIndex = 0;
|
|
66
|
+
const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
|
|
67
|
+
const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
|
|
68
|
+
let targetIndex = null;
|
|
69
|
+
switch (event.key) {
|
|
70
|
+
case nextKey:
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
if (loop) {
|
|
73
|
+
targetIndex = (currentIndex + 1) % items.length;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
targetIndex = Math.min(currentIndex + 1, items.length - 1);
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case prevKey:
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
if (loop) {
|
|
82
|
+
targetIndex = (currentIndex - 1 + items.length) % items.length;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
targetIndex = Math.max(currentIndex - 1, 0);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case 'Home':
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
targetIndex = 0;
|
|
91
|
+
break;
|
|
92
|
+
case 'End':
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
targetIndex = items.length - 1;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
if (targetIndex !== null && targetIndex !== currentIndex) {
|
|
98
|
+
const targetItem = items[targetIndex];
|
|
99
|
+
targetItem.element.focus();
|
|
100
|
+
// Radiogroup: arrow keys move focus AND select
|
|
101
|
+
onSelect(targetItem.value);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const contextValue = useMemo(() => ({
|
|
105
|
+
variant,
|
|
106
|
+
size,
|
|
107
|
+
orientation,
|
|
108
|
+
loop,
|
|
109
|
+
selectedValue,
|
|
110
|
+
rootDisabled: disabled,
|
|
111
|
+
registerItem,
|
|
112
|
+
unregisterItem,
|
|
113
|
+
getOrderedItems,
|
|
114
|
+
onSelect,
|
|
115
|
+
itemsRef,
|
|
116
|
+
}), [
|
|
117
|
+
variant,
|
|
118
|
+
size,
|
|
119
|
+
orientation,
|
|
120
|
+
loop,
|
|
121
|
+
selectedValue,
|
|
122
|
+
disabled,
|
|
123
|
+
registerItem,
|
|
124
|
+
unregisterItem,
|
|
125
|
+
getOrderedItems,
|
|
126
|
+
onSelect,
|
|
127
|
+
]);
|
|
128
|
+
return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: _jsx("div", { role: "radiogroup", className: cx('tui-segmented', `is-variant-${variant}`, `is-size-${size}`, orientation === 'vertical' && 'is-vertical', className), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-disabled": disabled || undefined, "aria-orientation": orientation, onKeyDown: handleKeyDown, children: children }) }));
|
|
129
|
+
}
|
|
130
|
+
// =============================================================================
|
|
131
|
+
// SegmentedControl.Item
|
|
132
|
+
// =============================================================================
|
|
133
|
+
function Item({ value, disabled = false, icon, 'aria-label': ariaLabel, className, children, }) {
|
|
134
|
+
const { selectedValue, rootDisabled, registerItem, unregisterItem, onSelect, getOrderedItems, } = useSegmentedControlContext();
|
|
135
|
+
const isSelected = selectedValue === value;
|
|
136
|
+
const isDisabled = rootDisabled || disabled;
|
|
137
|
+
// Determine which item gets tabIndex={0}
|
|
138
|
+
const getIsFocusable = () => {
|
|
139
|
+
if (isDisabled)
|
|
140
|
+
return false;
|
|
141
|
+
if (isSelected)
|
|
142
|
+
return true;
|
|
143
|
+
// If no selection, first enabled item is focusable
|
|
144
|
+
const items = getOrderedItems();
|
|
145
|
+
const enabledItems = items.filter((item) => !item.disabled && !rootDisabled);
|
|
146
|
+
if (enabledItems.length === 0)
|
|
147
|
+
return false;
|
|
148
|
+
if (selectedValue === undefined) {
|
|
149
|
+
return enabledItems[0].value === value;
|
|
150
|
+
}
|
|
151
|
+
// If selected item is disabled, first enabled gets focus
|
|
152
|
+
const selectedItem = items.find((item) => item.value === selectedValue);
|
|
153
|
+
if (selectedItem?.disabled || rootDisabled) {
|
|
154
|
+
return enabledItems[0].value === value;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
};
|
|
158
|
+
const isFocusable = getIsFocusable();
|
|
159
|
+
// Callback ref for registration
|
|
160
|
+
const callbackRef = useCallback((node) => {
|
|
161
|
+
if (node) {
|
|
162
|
+
registerItem({ value, element: node, disabled });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
unregisterItem(value);
|
|
166
|
+
}
|
|
167
|
+
}, [value, disabled, registerItem, unregisterItem]);
|
|
168
|
+
// Click handler
|
|
169
|
+
const handleClick = () => {
|
|
170
|
+
if (isDisabled)
|
|
171
|
+
return;
|
|
172
|
+
onSelect(value);
|
|
173
|
+
};
|
|
174
|
+
const hasIcon = icon !== undefined && icon !== null;
|
|
175
|
+
const hasChildren = children !== undefined && children !== null;
|
|
176
|
+
return (_jsxs("button", { ref: callbackRef, type: "button", role: "radio", className: cx('tui-segmented__item', className), "aria-checked": isSelected, "aria-disabled": isDisabled || undefined, "aria-label": ariaLabel, tabIndex: isDisabled ? -1 : isFocusable ? 0 : -1, onClick: handleClick, children: [hasIcon && _jsx("span", { className: "tui-segmented__item-icon", children: icon }), hasChildren && _jsx("span", { className: "tui-segmented__item-label", children: children })] }));
|
|
177
|
+
}
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// Compound Component Export
|
|
180
|
+
// =============================================================================
|
|
181
|
+
export const SegmentedControl = SegmentedControlRoot;
|
|
182
|
+
SegmentedControl.Item = Item;
|
|
183
|
+
SegmentedControl.displayName = 'SegmentedControl';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const SegmentedControlContext = createContext(null);
|
|
3
|
+
export function useSegmentedControlContext() {
|
|
4
|
+
const context = useContext(SegmentedControlContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error('SegmentedControl compound components must be used within a SegmentedControl');
|
|
7
|
+
}
|
|
8
|
+
return context;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SegmentedControl } from './SegmentedControl.js';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type SegmentedControlValue = string | number;
|
|
3
|
+
export type SegmentedControlVariant = 'pill' | 'outline' | 'underline';
|
|
4
|
+
export type SegmentedControlSize = 'sm' | 'md';
|
|
5
|
+
export type SegmentedControlOrientation = 'horizontal' | 'vertical';
|
|
6
|
+
export type SegmentedControlProps = {
|
|
7
|
+
/** Controlled selected value */
|
|
8
|
+
value?: SegmentedControlValue;
|
|
9
|
+
/** Uncontrolled initial value */
|
|
10
|
+
defaultValue?: SegmentedControlValue;
|
|
11
|
+
/** Callback when selection changes */
|
|
12
|
+
onValueChange?: (value: SegmentedControlValue) => void;
|
|
13
|
+
/** Visual style */
|
|
14
|
+
variant?: SegmentedControlVariant;
|
|
15
|
+
/** Size scale */
|
|
16
|
+
size?: SegmentedControlSize;
|
|
17
|
+
/** Layout direction */
|
|
18
|
+
orientation?: SegmentedControlOrientation;
|
|
19
|
+
/** Whether arrow keys wrap around */
|
|
20
|
+
loop?: boolean;
|
|
21
|
+
/** Disable all items */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Accessible label */
|
|
24
|
+
'aria-label'?: string;
|
|
25
|
+
/** ID of element that labels this control */
|
|
26
|
+
'aria-labelledby'?: string;
|
|
27
|
+
/** Additional classes */
|
|
28
|
+
className?: string;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
};
|
|
31
|
+
export type SegmentedControlItemProps = {
|
|
32
|
+
/** Unique identifier (required) */
|
|
33
|
+
value: SegmentedControlValue;
|
|
34
|
+
/** Disable this item */
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** Icon element */
|
|
37
|
+
icon?: ReactNode;
|
|
38
|
+
/** Accessible label for icon-only items */
|
|
39
|
+
'aria-label'?: string;
|
|
40
|
+
/** Additional classes */
|
|
41
|
+
className?: string;
|
|
42
|
+
/** Label content */
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
};
|
|
45
|
+
export type ItemRecord = {
|
|
46
|
+
value: SegmentedControlValue;
|
|
47
|
+
element: HTMLButtonElement;
|
|
48
|
+
disabled: boolean;
|
|
49
|
+
mountIndex: number;
|
|
50
|
+
};
|
|
51
|
+
export type SegmentedControlContextValue = {
|
|
52
|
+
variant: SegmentedControlVariant;
|
|
53
|
+
size: SegmentedControlSize;
|
|
54
|
+
orientation: SegmentedControlOrientation;
|
|
55
|
+
loop: boolean;
|
|
56
|
+
selectedValue: SegmentedControlValue | undefined;
|
|
57
|
+
rootDisabled: boolean;
|
|
58
|
+
registerItem: (record: Omit<ItemRecord, 'mountIndex'>) => void;
|
|
59
|
+
unregisterItem: (value: SegmentedControlValue) => void;
|
|
60
|
+
getOrderedItems: () => ItemRecord[];
|
|
61
|
+
onSelect: (value: SegmentedControlValue) => void;
|
|
62
|
+
itemsRef: React.RefObject<Map<SegmentedControlValue, ItemRecord>>;
|
|
63
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SidebarProps, SidebarHeaderProps, SidebarNavProps } from './types';
|
|
2
|
+
declare function SidebarHeader(props: SidebarHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
declare namespace SidebarHeader {
|
|
4
|
+
var displayName: string;
|
|
5
|
+
}
|
|
6
|
+
declare function SidebarNav(props: SidebarNavProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
declare namespace SidebarNav {
|
|
8
|
+
var displayName: string;
|
|
9
|
+
}
|
|
10
|
+
type SidebarCompound = {
|
|
11
|
+
(props: SidebarProps): React.JSX.Element | null;
|
|
12
|
+
displayName?: string;
|
|
13
|
+
Header: typeof SidebarHeader;
|
|
14
|
+
Nav: typeof SidebarNav;
|
|
15
|
+
};
|
|
16
|
+
export declare const Sidebar: SidebarCompound;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { useFocusTrap, getInitialFocus } from '../../utils/focus-trap.js';
|
|
4
|
+
const isBrowser = typeof document !== 'undefined';
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// Sidebar (Root)
|
|
7
|
+
// =============================================================================
|
|
8
|
+
function SidebarRoot(props) {
|
|
9
|
+
const { position = 'left', drawer = false, isOpen = false, onClose, ariaLabel, children, className, } = props;
|
|
10
|
+
const sidebarRef = useRef(null);
|
|
11
|
+
const restoreRef = useRef(null);
|
|
12
|
+
const rootClassName = ['tui-sidebar', className].filter(Boolean).join(' ');
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Drawer mode: capture trigger for focus restoration
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!drawer || !isBrowser)
|
|
18
|
+
return;
|
|
19
|
+
if (isOpen) {
|
|
20
|
+
// Capture focus target when opening
|
|
21
|
+
restoreRef.current = document.activeElement;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Restore focus when closing
|
|
25
|
+
const el = restoreRef.current;
|
|
26
|
+
if (el && typeof el.focus === 'function') {
|
|
27
|
+
el.focus();
|
|
28
|
+
}
|
|
29
|
+
restoreRef.current = null;
|
|
30
|
+
}
|
|
31
|
+
}, [drawer, isOpen]);
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Drawer mode: body scroll lock
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!drawer || !isOpen)
|
|
37
|
+
return;
|
|
38
|
+
document.body.classList.add('tui-sidebar-drawer-open');
|
|
39
|
+
return () => {
|
|
40
|
+
document.body.classList.remove('tui-sidebar-drawer-open');
|
|
41
|
+
};
|
|
42
|
+
}, [drawer, isOpen]);
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Drawer mode: focus trap (handles Tab cycling and ESC to close)
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
useFocusTrap(sidebarRef, {
|
|
47
|
+
isActive: drawer && isOpen,
|
|
48
|
+
onEscape: onClose,
|
|
49
|
+
});
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Drawer mode: initial focus
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!drawer || !isOpen)
|
|
55
|
+
return;
|
|
56
|
+
const sidebar = sidebarRef.current;
|
|
57
|
+
if (!sidebar)
|
|
58
|
+
return;
|
|
59
|
+
const target = getInitialFocus(sidebar);
|
|
60
|
+
target.focus({ preventScroll: true });
|
|
61
|
+
}, [drawer, isOpen]);
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Sidebar content (shared between static and drawer modes)
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
const sidebarContent = (_jsx("aside", { ref: sidebarRef, className: rootClassName, "data-position": position, "aria-label": ariaLabel, "aria-modal": drawer && isOpen ? 'true' : undefined, tabIndex: drawer ? -1 : undefined, children: children }));
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Static mode: render directly
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
if (!drawer) {
|
|
70
|
+
return sidebarContent;
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Drawer mode: render inline with fixed positioning (no portal needed)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
if (!isOpen) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const drawerClassName = [
|
|
79
|
+
'tui-sidebar-drawer',
|
|
80
|
+
position === 'right' && 'is-position-right',
|
|
81
|
+
]
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.join(' ');
|
|
84
|
+
return (_jsxs("div", { className: drawerClassName, "data-position": position, "data-state": "open", children: [_jsx("div", { className: "tui-sidebar-drawer__backdrop", onClick: onClose, "aria-hidden": "true" }), _jsx("div", { className: "tui-sidebar-drawer__panel", children: sidebarContent })] }));
|
|
85
|
+
}
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// Sidebar.Header
|
|
88
|
+
// =============================================================================
|
|
89
|
+
function SidebarHeader(props) {
|
|
90
|
+
const { children, className } = props;
|
|
91
|
+
const headerClassName = ['tui-sidebar__header', className].filter(Boolean).join(' ');
|
|
92
|
+
return _jsx("header", { className: headerClassName, children: children });
|
|
93
|
+
}
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// Sidebar.Nav
|
|
96
|
+
// =============================================================================
|
|
97
|
+
function SidebarNav(props) {
|
|
98
|
+
const { ariaLabel, ariaLabelledBy, children, className } = props;
|
|
99
|
+
const navClassName = ['tui-sidebar__nav', className].filter(Boolean).join(' ');
|
|
100
|
+
return (_jsx("nav", { className: navClassName, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: children }));
|
|
101
|
+
}
|
|
102
|
+
SidebarHeader.displayName = 'Sidebar.Header';
|
|
103
|
+
SidebarNav.displayName = 'Sidebar.Nav';
|
|
104
|
+
export const Sidebar = SidebarRoot;
|
|
105
|
+
Sidebar.displayName = 'Sidebar';
|
|
106
|
+
Sidebar.Header = SidebarHeader;
|
|
107
|
+
Sidebar.Nav = SidebarNav;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Sidebar } from './Sidebar.js';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type SidebarProps = {
|
|
3
|
+
/**
|
|
4
|
+
* Sidebar position. Affects border placement and drawer slide direction.
|
|
5
|
+
* @default 'left'
|
|
6
|
+
*/
|
|
7
|
+
position?: 'left' | 'right';
|
|
8
|
+
/**
|
|
9
|
+
* Enable drawer mode (mobile overlay).
|
|
10
|
+
* When true, sidebar renders via portal with backdrop and focus trap.
|
|
11
|
+
* @default false
|
|
12
|
+
*/
|
|
13
|
+
drawer?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Drawer open state. Required when `drawer={true}`.
|
|
16
|
+
*/
|
|
17
|
+
isOpen?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Callback to close the drawer.
|
|
20
|
+
* Called when ESC is pressed or backdrop is clicked.
|
|
21
|
+
*/
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Accessible label for the aside landmark.
|
|
25
|
+
* Typically omit this if using `ariaLabel` on `Sidebar.Nav` instead.
|
|
26
|
+
*/
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Sidebar content (Header, Nav, etc.).
|
|
30
|
+
*/
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Additional CSS class names.
|
|
34
|
+
*/
|
|
35
|
+
className?: string;
|
|
36
|
+
};
|
|
37
|
+
export type SidebarHeaderProps = {
|
|
38
|
+
/**
|
|
39
|
+
* Header content (back button, title, progress, CTAs).
|
|
40
|
+
*/
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* Additional CSS class names.
|
|
44
|
+
*/
|
|
45
|
+
className?: string;
|
|
46
|
+
};
|
|
47
|
+
export type SidebarNavProps = {
|
|
48
|
+
/**
|
|
49
|
+
* Accessible label for the nav landmark.
|
|
50
|
+
* Use this for the primary label since nav is the actionable region.
|
|
51
|
+
*/
|
|
52
|
+
ariaLabel?: string;
|
|
53
|
+
/**
|
|
54
|
+
* ID of a visible heading that labels the navigation.
|
|
55
|
+
*/
|
|
56
|
+
ariaLabelledBy?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Nav content (typically Accordion with StepList).
|
|
59
|
+
*/
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
/**
|
|
62
|
+
* Additional CSS class names.
|
|
63
|
+
*/
|
|
64
|
+
className?: string;
|
|
65
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Progress } from '../Progress/index.js';
|
|
3
|
+
import { Icon } from '../Icon/index.js';
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Icon mapping for static states
|
|
6
|
+
// =============================================================================
|
|
7
|
+
const statusIcons = {
|
|
8
|
+
complete: 'lms/checkmark',
|
|
9
|
+
locked: 'lms/lock',
|
|
10
|
+
};
|
|
11
|
+
const statusLabels = {
|
|
12
|
+
'not-started': 'Not started',
|
|
13
|
+
'in-progress': 'In progress',
|
|
14
|
+
complete: 'Complete',
|
|
15
|
+
locked: 'Locked',
|
|
16
|
+
};
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Status inference
|
|
19
|
+
// =============================================================================
|
|
20
|
+
function inferStatus(value) {
|
|
21
|
+
if (value <= 0)
|
|
22
|
+
return 'not-started';
|
|
23
|
+
if (value >= 100)
|
|
24
|
+
return 'complete';
|
|
25
|
+
return 'in-progress';
|
|
26
|
+
}
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Component
|
|
29
|
+
// =============================================================================
|
|
30
|
+
export function StepIndicator(props) {
|
|
31
|
+
const { value = 0, status: statusOverride, icon: customIcon, size = 'sm', showValue = false, label, className, } = props;
|
|
32
|
+
// Infer status from value, allow override
|
|
33
|
+
const status = statusOverride ?? inferStatus(value);
|
|
34
|
+
// When status is explicit, the visual should match the status, not the value
|
|
35
|
+
// - not-started: always show empty (0%)
|
|
36
|
+
// - complete: always show full (100%)
|
|
37
|
+
// - in-progress: use actual value
|
|
38
|
+
// - locked: shows icon, value doesn't matter visually
|
|
39
|
+
const displayValue = statusOverride === 'not-started' ? 0
|
|
40
|
+
: statusOverride === 'complete' ? 100
|
|
41
|
+
: value;
|
|
42
|
+
// Determine which icon to show:
|
|
43
|
+
// - complete/locked: always use status icons (universal meaning)
|
|
44
|
+
// - not-started/in-progress: use custom icon if provided, otherwise no icon
|
|
45
|
+
const hasStatusIcon = status === 'complete' || status === 'locked';
|
|
46
|
+
const hasCustomIcon = !!customIcon && (status === 'not-started' || status === 'in-progress');
|
|
47
|
+
const hasIcon = hasStatusIcon || hasCustomIcon;
|
|
48
|
+
const iconName = hasStatusIcon ? statusIcons[status] : customIcon;
|
|
49
|
+
// Use solid variant for complete state (filled circle)
|
|
50
|
+
const variant = status === 'complete' ? 'solid' : 'ring';
|
|
51
|
+
// Accessible label
|
|
52
|
+
const ariaLabel = label ?? `Step status: ${statusLabels[status]}`;
|
|
53
|
+
// Build class names
|
|
54
|
+
const rootClassName = [
|
|
55
|
+
'tui-step-indicator',
|
|
56
|
+
`is-size-${size}`,
|
|
57
|
+
`is-status-${status}`,
|
|
58
|
+
hasIcon && 'has-icon',
|
|
59
|
+
className,
|
|
60
|
+
]
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(' ');
|
|
63
|
+
return (_jsx("div", { className: rootClassName, children: _jsxs(Progress, { mode: "circle", variant: variant, size: size, value: displayValue, showLabels: showValue || hasIcon, labelPosition: "inside", ariaLabel: ariaLabel, children: [hasIcon && iconName && (_jsx(Icon, { name: iconName })), showValue && status === 'in-progress' && (_jsxs("span", { className: "tui-step-indicator__value", children: [Math.round(displayValue), "%"] }))] }) }));
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StepIndicator } from './StepIndicator.js';
|