@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,219 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useEffect, useId, useMemo, useRef, useState, cloneElement, isValidElement, Children, } from 'react';
|
|
3
|
+
import { useFloating, offset, flip, shift, autoUpdate, FloatingPortal, useDismiss, useInteractions, useListNavigation, useRole, FloatingFocusManager, } from '@floating-ui/react';
|
|
4
|
+
import { cx } from '../../utils/cx.js';
|
|
5
|
+
import { getPortalRootFor } from '../../utils/portal.js';
|
|
6
|
+
import { Button } from '../Button/index.js';
|
|
7
|
+
import { DropdownContext, useDropdownContext } from './DropdownContext.js';
|
|
8
|
+
import { toPlacement } from './types.js';
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Dropdown Root
|
|
11
|
+
// =============================================================================
|
|
12
|
+
function DropdownRoot({ open: controlledOpen, onOpenChange, defaultOpen = false, children, }) {
|
|
13
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
14
|
+
const [activeIndex, setActiveIndex] = useState(null);
|
|
15
|
+
const isControlled = controlledOpen !== undefined;
|
|
16
|
+
const open = isControlled ? controlledOpen : uncontrolledOpen;
|
|
17
|
+
const setOpen = useCallback((nextOpen) => {
|
|
18
|
+
if (!isControlled) {
|
|
19
|
+
setUncontrolledOpen(nextOpen);
|
|
20
|
+
}
|
|
21
|
+
onOpenChange?.(nextOpen);
|
|
22
|
+
if (!nextOpen) {
|
|
23
|
+
setActiveIndex(null);
|
|
24
|
+
}
|
|
25
|
+
}, [isControlled, onOpenChange]);
|
|
26
|
+
const triggerRef = useRef(null);
|
|
27
|
+
const contentId = useId();
|
|
28
|
+
// Focus restoration: track if we need to restore focus on close
|
|
29
|
+
const shouldRestoreFocus = useRef(false);
|
|
30
|
+
const prevOpen = useRef(open);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// When opening, mark that we should restore focus on close
|
|
33
|
+
if (open && !prevOpen.current) {
|
|
34
|
+
shouldRestoreFocus.current = true;
|
|
35
|
+
}
|
|
36
|
+
// When closing, restore focus to trigger
|
|
37
|
+
if (!open && prevOpen.current && shouldRestoreFocus.current) {
|
|
38
|
+
triggerRef.current?.focus();
|
|
39
|
+
shouldRestoreFocus.current = false;
|
|
40
|
+
}
|
|
41
|
+
prevOpen.current = open;
|
|
42
|
+
}, [open]);
|
|
43
|
+
const contextValue = useMemo(() => ({
|
|
44
|
+
open,
|
|
45
|
+
setOpen,
|
|
46
|
+
triggerRef,
|
|
47
|
+
contentId,
|
|
48
|
+
activeIndex,
|
|
49
|
+
setActiveIndex,
|
|
50
|
+
}), [open, setOpen, contentId, activeIndex]);
|
|
51
|
+
return (_jsx(DropdownContext.Provider, { value: contextValue, children: children }));
|
|
52
|
+
}
|
|
53
|
+
DropdownRoot.displayName = 'Dropdown';
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// DropdownTrigger
|
|
56
|
+
// =============================================================================
|
|
57
|
+
function DropdownTriggerComponent({ asChild = false, children }) {
|
|
58
|
+
const { open, setOpen, triggerRef, contentId } = useDropdownContext();
|
|
59
|
+
const handleClick = useCallback(() => {
|
|
60
|
+
setOpen(!open);
|
|
61
|
+
}, [open, setOpen]);
|
|
62
|
+
const handleKeyDown = useCallback((e) => {
|
|
63
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
setOpen(!open);
|
|
66
|
+
}
|
|
67
|
+
else if (e.key === 'ArrowDown') {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
setOpen(true);
|
|
70
|
+
}
|
|
71
|
+
else if (e.key === 'ArrowUp') {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
setOpen(true);
|
|
74
|
+
}
|
|
75
|
+
}, [open, setOpen]);
|
|
76
|
+
// Merge child ref with our triggerRef
|
|
77
|
+
const setRefs = useCallback((node) => {
|
|
78
|
+
triggerRef.current = node;
|
|
79
|
+
if (asChild && isValidElement(children)) {
|
|
80
|
+
const childRef = children.ref;
|
|
81
|
+
if (typeof childRef === 'function') {
|
|
82
|
+
childRef(node);
|
|
83
|
+
}
|
|
84
|
+
else if (childRef && typeof childRef === 'object') {
|
|
85
|
+
childRef.current = node;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}, [asChild, children, triggerRef]);
|
|
89
|
+
const triggerProps = {
|
|
90
|
+
onClick: handleClick,
|
|
91
|
+
onKeyDown: handleKeyDown,
|
|
92
|
+
'aria-haspopup': 'menu',
|
|
93
|
+
'aria-expanded': open,
|
|
94
|
+
'aria-controls': open ? contentId : undefined,
|
|
95
|
+
'data-dropdown-open': open || undefined,
|
|
96
|
+
};
|
|
97
|
+
if (asChild && isValidElement(children)) {
|
|
98
|
+
return cloneElement(children, {
|
|
99
|
+
...triggerProps,
|
|
100
|
+
ref: setRefs,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Non-asChild wrapper needs proper button semantics
|
|
104
|
+
return (_jsx("span", { ref: setRefs, role: "button", tabIndex: 0, ...triggerProps, style: { display: 'inline-flex' }, children: children }));
|
|
105
|
+
}
|
|
106
|
+
DropdownTriggerComponent.displayName = 'Dropdown.Trigger';
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// DropdownContent
|
|
109
|
+
// =============================================================================
|
|
110
|
+
function DropdownContentComponent({ side = 'bottom', align = 'start', sideOffset = 4, className, style, children, }) {
|
|
111
|
+
const { open, setOpen, triggerRef, contentId, activeIndex, setActiveIndex } = useDropdownContext();
|
|
112
|
+
const listRef = useRef([]);
|
|
113
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
114
|
+
placement: toPlacement(side, align),
|
|
115
|
+
open,
|
|
116
|
+
onOpenChange: setOpen,
|
|
117
|
+
middleware: [offset(sideOffset), flip(), shift({ padding: 8 })],
|
|
118
|
+
whileElementsMounted: autoUpdate,
|
|
119
|
+
});
|
|
120
|
+
// Sync reference element
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (triggerRef.current) {
|
|
123
|
+
refs.setReference(triggerRef.current);
|
|
124
|
+
}
|
|
125
|
+
}, [triggerRef, refs]);
|
|
126
|
+
// Collect disabled indices from children for keyboard navigation
|
|
127
|
+
const disabledIndices = useMemo(() => {
|
|
128
|
+
const indices = [];
|
|
129
|
+
Children.forEach(children, (child, index) => {
|
|
130
|
+
if (isValidElement(child)) {
|
|
131
|
+
const props = child.props;
|
|
132
|
+
if (props.disabled) {
|
|
133
|
+
indices.push(index);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return indices;
|
|
138
|
+
}, [children]);
|
|
139
|
+
const dismiss = useDismiss(context);
|
|
140
|
+
const role = useRole(context, { role: 'menu' });
|
|
141
|
+
const listNavigation = useListNavigation(context, {
|
|
142
|
+
listRef,
|
|
143
|
+
activeIndex,
|
|
144
|
+
onNavigate: setActiveIndex,
|
|
145
|
+
loop: true,
|
|
146
|
+
disabledIndices,
|
|
147
|
+
focusItemOnOpen: true,
|
|
148
|
+
});
|
|
149
|
+
const { getFloatingProps, getItemProps } = useInteractions([
|
|
150
|
+
dismiss,
|
|
151
|
+
role,
|
|
152
|
+
listNavigation,
|
|
153
|
+
]);
|
|
154
|
+
// Get portal root inside .tui-interface
|
|
155
|
+
const portalRoot = getPortalRootFor(triggerRef.current);
|
|
156
|
+
if (!open)
|
|
157
|
+
return null;
|
|
158
|
+
// Clone children to inject item props and role
|
|
159
|
+
const items = Children.map(children, (child, index) => {
|
|
160
|
+
if (!isValidElement(child))
|
|
161
|
+
return child;
|
|
162
|
+
const childProps = child.props;
|
|
163
|
+
const isDisabled = childProps.disabled === true;
|
|
164
|
+
const existingRole = childProps.role;
|
|
165
|
+
return cloneElement(child, {
|
|
166
|
+
...getItemProps({
|
|
167
|
+
ref: (node) => {
|
|
168
|
+
listRef.current[index] = node;
|
|
169
|
+
},
|
|
170
|
+
// Disabled items get tabIndex -1 always
|
|
171
|
+
tabIndex: isDisabled ? -1 : (activeIndex === index ? 0 : -1),
|
|
172
|
+
}),
|
|
173
|
+
// Add menuitem role if not already specified
|
|
174
|
+
role: existingRole || 'menuitem',
|
|
175
|
+
'aria-disabled': isDisabled || undefined,
|
|
176
|
+
'data-active': activeIndex === index || undefined,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
return (_jsx(FloatingPortal, { root: portalRoot, children: _jsx(FloatingFocusManager, { context: context, modal: false, initialFocus: -1, children: _jsx("div", { ref: refs.setFloating, id: contentId, className: cx('tui-dropdown', className), style: {
|
|
180
|
+
...floatingStyles,
|
|
181
|
+
...style,
|
|
182
|
+
}, ...getFloatingProps(), children: items }) }) }));
|
|
183
|
+
}
|
|
184
|
+
DropdownContentComponent.displayName = 'Dropdown.Content';
|
|
185
|
+
// =============================================================================
|
|
186
|
+
// DropdownItem
|
|
187
|
+
// =============================================================================
|
|
188
|
+
/**
|
|
189
|
+
* Dropdown menu item. Renders a Button internally with ghost variant.
|
|
190
|
+
* When href is provided, renders as an anchor.
|
|
191
|
+
*/
|
|
192
|
+
function DropdownItemComponent({ onSelect, href, target = '_self', disabled = false, keepOpen = false, className, children, ...props }) {
|
|
193
|
+
const { setOpen, triggerRef } = useDropdownContext();
|
|
194
|
+
const handleClick = useCallback(() => {
|
|
195
|
+
if (disabled)
|
|
196
|
+
return;
|
|
197
|
+
onSelect?.();
|
|
198
|
+
if (!keepOpen) {
|
|
199
|
+
setOpen(false);
|
|
200
|
+
// Explicitly restore focus after programmatic close
|
|
201
|
+
// Small delay to let the dropdown unmount first
|
|
202
|
+
requestAnimationFrame(() => {
|
|
203
|
+
triggerRef.current?.focus();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}, [disabled, onSelect, keepOpen, setOpen, triggerRef]);
|
|
207
|
+
return (_jsx(Button, { variant: "ghost", href: disabled ? undefined : href, target: href ? target : undefined, disabled: disabled, onClick: handleClick, className: cx('tui-dropdown__item', className), ...props, children: children }));
|
|
208
|
+
}
|
|
209
|
+
DropdownItemComponent.displayName = 'Dropdown.Item';
|
|
210
|
+
export const Dropdown = DropdownRoot;
|
|
211
|
+
Dropdown.Trigger = DropdownTriggerComponent;
|
|
212
|
+
Dropdown.Content = DropdownContentComponent;
|
|
213
|
+
Dropdown.Item = DropdownItemComponent;
|
|
214
|
+
// Named exports for direct imports
|
|
215
|
+
export const DropdownTrigger = DropdownTriggerComponent;
|
|
216
|
+
export const DropdownContent = DropdownContentComponent;
|
|
217
|
+
export const DropdownItem = DropdownItemComponent;
|
|
218
|
+
// Hook for advanced use cases (custom items that need to close the dropdown)
|
|
219
|
+
export { useDropdownContext as useDropdown } from './DropdownContext.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const DropdownContext = createContext(null);
|
|
3
|
+
export function useDropdownContext() {
|
|
4
|
+
const context = useContext(DropdownContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error('Dropdown components must be used within a Dropdown');
|
|
7
|
+
}
|
|
8
|
+
return context;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Dropdown, DropdownTrigger, DropdownContent, DropdownItem, useDropdown, } from './Dropdown.js';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Placement } from '@floating-ui/react';
|
|
2
|
+
/**
|
|
3
|
+
* Dropdown placement sides.
|
|
4
|
+
*/
|
|
5
|
+
export type Side = 'top' | 'right' | 'bottom' | 'left';
|
|
6
|
+
/**
|
|
7
|
+
* Dropdown alignment relative to trigger.
|
|
8
|
+
*/
|
|
9
|
+
export type Align = 'start' | 'center' | 'end';
|
|
10
|
+
export type DropdownProps = {
|
|
11
|
+
/**
|
|
12
|
+
* Controlled open state.
|
|
13
|
+
*/
|
|
14
|
+
open?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Callback when open state changes.
|
|
17
|
+
*/
|
|
18
|
+
onOpenChange?: (open: boolean) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Default open state for uncontrolled usage.
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
defaultOpen?: boolean;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
};
|
|
26
|
+
export type DropdownTriggerProps = {
|
|
27
|
+
/**
|
|
28
|
+
* When true, merges props onto the child element instead of wrapping in a span.
|
|
29
|
+
* Child must be a single React element that accepts ref and event handlers.
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
asChild?: boolean;
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
};
|
|
35
|
+
export type DropdownContentProps = {
|
|
36
|
+
/**
|
|
37
|
+
* Preferred side of the trigger to place the dropdown.
|
|
38
|
+
* @default 'bottom'
|
|
39
|
+
*/
|
|
40
|
+
side?: Side;
|
|
41
|
+
/**
|
|
42
|
+
* Alignment along the side.
|
|
43
|
+
* @default 'start'
|
|
44
|
+
*/
|
|
45
|
+
align?: Align;
|
|
46
|
+
/**
|
|
47
|
+
* Distance from the trigger in pixels.
|
|
48
|
+
* @default 4
|
|
49
|
+
*/
|
|
50
|
+
sideOffset?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Additional CSS class names.
|
|
53
|
+
*/
|
|
54
|
+
className?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Inline styles (use sparingly, prefer className).
|
|
57
|
+
*/
|
|
58
|
+
style?: React.CSSProperties;
|
|
59
|
+
children: React.ReactNode;
|
|
60
|
+
};
|
|
61
|
+
export type DropdownItemProps = {
|
|
62
|
+
/**
|
|
63
|
+
* Click handler for the item.
|
|
64
|
+
*/
|
|
65
|
+
onSelect?: () => void;
|
|
66
|
+
/**
|
|
67
|
+
* URL to navigate to. When provided, renders as an anchor.
|
|
68
|
+
*/
|
|
69
|
+
href?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Link target (only applies when href is provided).
|
|
72
|
+
* @default '_self'
|
|
73
|
+
*/
|
|
74
|
+
target?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Whether the item is disabled.
|
|
77
|
+
* @default false
|
|
78
|
+
*/
|
|
79
|
+
disabled?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* When true, keeps the dropdown open after selection.
|
|
82
|
+
* @default false
|
|
83
|
+
*/
|
|
84
|
+
keepOpen?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Additional CSS class names.
|
|
87
|
+
*/
|
|
88
|
+
className?: string;
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
};
|
|
91
|
+
export type DropdownContextValue = {
|
|
92
|
+
open: boolean;
|
|
93
|
+
setOpen: (open: boolean) => void;
|
|
94
|
+
triggerRef: React.RefObject<HTMLElement | null>;
|
|
95
|
+
contentId: string;
|
|
96
|
+
activeIndex: number | null;
|
|
97
|
+
setActiveIndex: (index: number | null) => void;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Convert side + align to Floating UI placement.
|
|
101
|
+
*/
|
|
102
|
+
export declare function toPlacement(side: Side, align: Align): Placement;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IconName } from '../../icons/registry';
|
|
3
|
+
import type { SizeExtended } from '../../types';
|
|
4
|
+
export interface IconProps {
|
|
5
|
+
/** Icon name from registry (e.g., "system/check") */
|
|
6
|
+
name?: IconName;
|
|
7
|
+
/** Emoji or unicode character (alternative to name) */
|
|
8
|
+
emoji?: string;
|
|
9
|
+
/** Accessible label for informative icons. Required if icon conveys meaning. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Icon size. Defaults to inherit from parent font-size. */
|
|
12
|
+
size?: SizeExtended;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Icon component for SVG icons from the registry or emoji characters.
|
|
17
|
+
*
|
|
18
|
+
* Accessibility:
|
|
19
|
+
* - Decorative icons (no label): automatically hidden from screen readers
|
|
20
|
+
* - Informative icons: provide a `label` prop for screen reader announcement
|
|
21
|
+
*/
|
|
22
|
+
export declare const Icon: React.ForwardRefExoticComponent<IconProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { iconRegistry } from '../../icons/registry.js';
|
|
5
|
+
/**
|
|
6
|
+
* Icon component for SVG icons from the registry or emoji characters.
|
|
7
|
+
*
|
|
8
|
+
* Accessibility:
|
|
9
|
+
* - Decorative icons (no label): automatically hidden from screen readers
|
|
10
|
+
* - Informative icons: provide a `label` prop for screen reader announcement
|
|
11
|
+
*/
|
|
12
|
+
export const Icon = React.forwardRef(({ name, emoji, label, size, className }, ref) => {
|
|
13
|
+
const SvgIcon = name ? iconRegistry[name] : null;
|
|
14
|
+
// Dev warning for invalid icon name
|
|
15
|
+
if (import.meta.env.DEV && name && !SvgIcon) {
|
|
16
|
+
console.warn(`[Icon] Unknown icon name: "${name}". Check the registry.`);
|
|
17
|
+
}
|
|
18
|
+
// Decorative if no label provided
|
|
19
|
+
const isDecorative = !label;
|
|
20
|
+
return (_jsxs("span", { ref: ref, className: cx('tui-icon', size && `is-size-${size}`, className), ...(isDecorative
|
|
21
|
+
? { 'aria-hidden': true }
|
|
22
|
+
: { role: 'img', 'aria-label': label }), children: [SvgIcon && _jsx(SvgIcon, { "aria-hidden": "true", focusable: "false" }), !SvgIcon && emoji] }));
|
|
23
|
+
});
|
|
24
|
+
Icon.displayName = 'Icon';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Icon } from './Icon.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { Icon } from '../Icon/index.js';
|
|
5
|
+
import { Tooltip } from '../Tooltip/index.js';
|
|
6
|
+
import { getSafeRel } from '../../utils/polymorphic.js';
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// IconButton Component
|
|
9
|
+
// =============================================================================
|
|
10
|
+
//
|
|
11
|
+
// Square button containing only an icon. Requires an accessible label since
|
|
12
|
+
// there's no visible text. Use for toolbar actions, close buttons, navigation
|
|
13
|
+
// controls, etc.
|
|
14
|
+
//
|
|
15
|
+
// Renders as <button> by default, or <a> when href is provided.
|
|
16
|
+
// Set showTooltip to display the label on hover for sighted users.
|
|
17
|
+
//
|
|
18
|
+
// =============================================================================
|
|
19
|
+
export const IconButton = forwardRef((props, ref) => {
|
|
20
|
+
const { icon, label, size = 'sm', theme = 'secondary', variant = 'ghost', disabled = false, loading = false, pressed, showTooltip = false, tooltipSide = 'top', className, ...rest } = props;
|
|
21
|
+
const isLink = typeof rest.href === 'string';
|
|
22
|
+
const isDisabled = disabled || loading;
|
|
23
|
+
const classes = cx('tui-icon-button', `is-size-${size}`, `is-theme-${theme}`, `is-style-${variant}`, isDisabled && 'is-disabled', className);
|
|
24
|
+
const iconContent = (_jsxs(_Fragment, { children: [_jsx(Icon, { name: icon }), loading && _jsx("span", { className: "tui-icon-button__spinner", "aria-hidden": "true" })] }));
|
|
25
|
+
// Render the base element (button or anchor)
|
|
26
|
+
let element;
|
|
27
|
+
if (isLink) {
|
|
28
|
+
const { href, target, rel, tabIndex, onClick, ...anchorRest } = rest;
|
|
29
|
+
const safeRel = getSafeRel(target, rel);
|
|
30
|
+
const handleClick = (e) => {
|
|
31
|
+
if (isDisabled) {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
onClick?.(e);
|
|
37
|
+
};
|
|
38
|
+
element = (_jsx("a", { ref: ref, href: isDisabled ? undefined : href, className: classes, "aria-label": label, "aria-disabled": isDisabled || undefined, "aria-busy": loading || undefined, tabIndex: isDisabled ? -1 : tabIndex, onClick: handleClick, target: target, rel: safeRel, ...anchorRest, children: iconContent }));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const buttonRest = rest;
|
|
42
|
+
element = (_jsx("button", { ref: ref, type: buttonRest.type ?? 'button', className: classes, "aria-label": label, disabled: isDisabled, "aria-busy": loading || undefined, "aria-pressed": pressed, ...buttonRest, children: iconContent }));
|
|
43
|
+
}
|
|
44
|
+
// Wrap in tooltip if requested
|
|
45
|
+
if (showTooltip) {
|
|
46
|
+
return (_jsxs(Tooltip, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: element }), _jsx(Tooltip.Content, { side: tooltipSide, children: label })] }));
|
|
47
|
+
}
|
|
48
|
+
return element;
|
|
49
|
+
});
|
|
50
|
+
IconButton.displayName = 'IconButton';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { IconButton } from './IconButton.js';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { IconName } from '../../icons/registry';
|
|
2
|
+
import type { Size, ThemeIntent } from '../../types';
|
|
3
|
+
export type { Size };
|
|
4
|
+
export type Theme = ThemeIntent;
|
|
5
|
+
export type Variant = 'solid' | 'outline' | 'ghost';
|
|
6
|
+
type IconButtonBaseProps = {
|
|
7
|
+
/**
|
|
8
|
+
* Icon to display. Required — this is the button's visual content.
|
|
9
|
+
* @example icon="system/close"
|
|
10
|
+
*/
|
|
11
|
+
icon: IconName;
|
|
12
|
+
/**
|
|
13
|
+
* Accessible label. Required — icon-only buttons must have a label
|
|
14
|
+
* for screen readers since there's no visible text.
|
|
15
|
+
* @example label="Close modal"
|
|
16
|
+
*/
|
|
17
|
+
label: string;
|
|
18
|
+
/**
|
|
19
|
+
* Size of the button.
|
|
20
|
+
* - `'xs'`: 24px
|
|
21
|
+
* - `'sm'`: 32px (default)
|
|
22
|
+
* - `'md'`: 40px
|
|
23
|
+
* - `'lg'`: 48px
|
|
24
|
+
* @default 'sm'
|
|
25
|
+
*/
|
|
26
|
+
size?: Size;
|
|
27
|
+
/**
|
|
28
|
+
* Semantic color theme.
|
|
29
|
+
* Note: Defaults to 'secondary' (unlike Button which defaults to 'primary')
|
|
30
|
+
* because IconButtons are typically used for toolbar/utility actions.
|
|
31
|
+
* @default 'secondary'
|
|
32
|
+
*/
|
|
33
|
+
theme?: Theme;
|
|
34
|
+
/**
|
|
35
|
+
* Visual style variant.
|
|
36
|
+
* - `'solid'`: Filled background
|
|
37
|
+
* - `'outline'`: Border only, transparent background
|
|
38
|
+
* - `'ghost'`: No border or background, icon only
|
|
39
|
+
* @default 'ghost'
|
|
40
|
+
*/
|
|
41
|
+
variant?: Variant;
|
|
42
|
+
/**
|
|
43
|
+
* Disabled state.
|
|
44
|
+
*/
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Loading state — shows spinner, disables interaction.
|
|
48
|
+
*/
|
|
49
|
+
loading?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Pressed state for toggle buttons.
|
|
52
|
+
* When true, applies aria-pressed="true" and active styling.
|
|
53
|
+
* @example pressed={isMuted}
|
|
54
|
+
*/
|
|
55
|
+
pressed?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Show the label as a tooltip on hover.
|
|
58
|
+
* Since icon-only buttons have no visible text, this helps sighted
|
|
59
|
+
* users discover what the button does.
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
showTooltip?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Tooltip placement side.
|
|
65
|
+
* @default 'top'
|
|
66
|
+
*/
|
|
67
|
+
tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
68
|
+
/**
|
|
69
|
+
* Additional CSS class names.
|
|
70
|
+
*/
|
|
71
|
+
className?: string;
|
|
72
|
+
};
|
|
73
|
+
export type IconButtonAsButton = IconButtonBaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'aria-label'> & {
|
|
74
|
+
href?: undefined;
|
|
75
|
+
};
|
|
76
|
+
export type IconButtonAsAnchor = IconButtonBaseProps & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children' | 'aria-label'> & {
|
|
77
|
+
href: string;
|
|
78
|
+
};
|
|
79
|
+
export type IconButtonProps = IconButtonAsButton | IconButtonAsAnchor;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SizeStandard } from '../../types';
|
|
3
|
+
type Size = SizeStandard;
|
|
4
|
+
export type ModalProps = {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
size?: Size;
|
|
8
|
+
stickyHead?: boolean;
|
|
9
|
+
stickyFoot?: boolean;
|
|
10
|
+
labelledBy?: string;
|
|
11
|
+
describedBy?: string;
|
|
12
|
+
initialFocusSelector?: string;
|
|
13
|
+
container?: Element | null;
|
|
14
|
+
showCloseButton?: boolean;
|
|
15
|
+
closeLabel?: string;
|
|
16
|
+
closeOnBackdropClick?: boolean;
|
|
17
|
+
closeOnEscape?: boolean;
|
|
18
|
+
children?: React.ReactNode;
|
|
19
|
+
};
|
|
20
|
+
declare function ModalRoot({ isOpen, onClose, size, stickyHead, stickyFoot, labelledBy, describedBy, initialFocusSelector, container, showCloseButton, closeLabel, closeOnBackdropClick, closeOnEscape, children, }: ModalProps): React.ReactPortal | null;
|
|
21
|
+
type ModalCloseProps = {
|
|
22
|
+
label?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
};
|
|
25
|
+
declare function ModalClose({ label, className }: ModalCloseProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
declare namespace ModalClose {
|
|
27
|
+
var displayName: string;
|
|
28
|
+
}
|
|
29
|
+
type ModalSlotProps = {
|
|
30
|
+
className?: string;
|
|
31
|
+
children?: React.ReactNode;
|
|
32
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
33
|
+
declare function ModalHead({ className, children, ...rest }: ModalSlotProps): import("react/jsx-runtime").JSX.Element;
|
|
34
|
+
declare namespace ModalHead {
|
|
35
|
+
var displayName: string;
|
|
36
|
+
}
|
|
37
|
+
declare function ModalBody({ className, children, ...rest }: ModalSlotProps): import("react/jsx-runtime").JSX.Element;
|
|
38
|
+
declare namespace ModalBody {
|
|
39
|
+
var displayName: string;
|
|
40
|
+
}
|
|
41
|
+
declare function ModalFoot({ className, children, ...rest }: ModalSlotProps): import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
declare namespace ModalFoot {
|
|
43
|
+
var displayName: string;
|
|
44
|
+
}
|
|
45
|
+
type ModalCompound = typeof ModalRoot & {
|
|
46
|
+
Head: typeof ModalHead;
|
|
47
|
+
Body: typeof ModalBody;
|
|
48
|
+
Foot: typeof ModalFoot;
|
|
49
|
+
Close: typeof ModalClose;
|
|
50
|
+
};
|
|
51
|
+
export declare const Modal: ModalCompound;
|
|
52
|
+
export {};
|