@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
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Tangible UI
|
|
2
|
+
|
|
3
|
+
Design system for Tangible WordPress plugins. React components + CSS tokens + utility classes.
|
|
4
|
+
|
|
5
|
+
**Live Storybook:** https://storybook-tangible-ui.pages.dev
|
|
6
|
+
|
|
7
|
+
## Components
|
|
8
|
+
|
|
9
|
+
- **Primitives:** Button, Chip, Icon, IconButton, Progress, Rating, Tooltip
|
|
10
|
+
- **Layout:** Accordion, Card, Modal, Notice, DataTable
|
|
11
|
+
- **Composites:** Chips (multi-select chip group), StepIndicator
|
|
12
|
+
- **Form Inputs:** Text, textarea, select, checkbox, radio, toggle, file (CSS-only)
|
|
13
|
+
|
|
14
|
+
## Getting Started
|
|
15
|
+
|
|
16
|
+
### Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @tangible/ui
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Import styles
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
// In your app entry point
|
|
26
|
+
import '@tangible/ui/styles';
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or in SCSS:
|
|
30
|
+
```scss
|
|
31
|
+
@use '@tangible/ui/styles/scss';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Wrap your app
|
|
35
|
+
|
|
36
|
+
Components require the `.tui-interface` wrapper to access design tokens:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
function App() {
|
|
40
|
+
return (
|
|
41
|
+
<div className="tui-interface">
|
|
42
|
+
{/* Your UI here */}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Use components
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { Button, Card, Tooltip, IconButton } from '@tangible/ui';
|
|
52
|
+
|
|
53
|
+
function Example() {
|
|
54
|
+
return (
|
|
55
|
+
<Card>
|
|
56
|
+
<Card.Body>
|
|
57
|
+
<Button label="Click me" theme="primary" />
|
|
58
|
+
|
|
59
|
+
<IconButton icon="system/settings" label="Settings" showTooltip />
|
|
60
|
+
|
|
61
|
+
<Tooltip>
|
|
62
|
+
<Tooltip.Trigger asChild>
|
|
63
|
+
<Button label="Hover me" variant="outline" />
|
|
64
|
+
</Tooltip.Trigger>
|
|
65
|
+
<Tooltip.Content>Hello!</Tooltip.Content>
|
|
66
|
+
</Tooltip>
|
|
67
|
+
</Card.Body>
|
|
68
|
+
</Card>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
See the [Storybook](https://storybook-tangible-ui.pages.dev) for full component documentation and examples.
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install
|
|
79
|
+
npm run storybook # Dev server on port 6006
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Commands
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm run storybook # Dev server
|
|
86
|
+
npm run build:lib # Build library (outputs to publish/)
|
|
87
|
+
npm run lint # ESLint
|
|
88
|
+
npm run test # Unit tests
|
|
89
|
+
npm run test:storybook # Story tests (requires Playwright)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Documentation
|
|
93
|
+
|
|
94
|
+
- `CLAUDE.md` — Development guide (architecture, patterns, conventions)
|
|
95
|
+
- `CONTEXT.md` — Project background and LMS requirements
|
|
96
|
+
- `TIMELINE.md` — Development roadmap (Jan–Mar 2026)
|
|
97
|
+
|
|
98
|
+
## Status
|
|
99
|
+
|
|
100
|
+
Under active development for Course Viewer (LMS) and Quiz modules. Component APIs are stabilising but may change.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AccordionProps, AccordionItemProps, AccordionTriggerProps, AccordionPanelProps } from './types';
|
|
3
|
+
type AccordionCompound = {
|
|
4
|
+
(props: AccordionProps): React.JSX.Element;
|
|
5
|
+
Item: typeof AccordionItem;
|
|
6
|
+
Trigger: typeof AccordionTrigger;
|
|
7
|
+
Panel: typeof AccordionPanel;
|
|
8
|
+
};
|
|
9
|
+
declare function AccordionItem({ value, disabled, headingLevel, children, className, }: AccordionItemProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
declare namespace AccordionItem {
|
|
11
|
+
var displayName: string;
|
|
12
|
+
}
|
|
13
|
+
declare function AccordionTrigger({ children, className }: AccordionTriggerProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
declare namespace AccordionTrigger {
|
|
15
|
+
var displayName: string;
|
|
16
|
+
}
|
|
17
|
+
declare function AccordionPanel({ children, className }: AccordionPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
declare namespace AccordionPanel {
|
|
19
|
+
var displayName: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const Accordion: AccordionCompound;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useId, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { Icon } from '../Icon/index.js';
|
|
5
|
+
import { AccordionContext, AccordionItemContext, useAccordionContext, useAccordionItemContext } from './AccordionContext.js';
|
|
6
|
+
function AccordionRoot(props) {
|
|
7
|
+
const { type, children, className } = props;
|
|
8
|
+
// Track trigger refs for keyboard navigation
|
|
9
|
+
const triggersRef = useRef(new Map());
|
|
10
|
+
const registerTrigger = useCallback((value, element) => {
|
|
11
|
+
if (element) {
|
|
12
|
+
triggersRef.current.set(value, element);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
triggersRef.current.delete(value);
|
|
16
|
+
}
|
|
17
|
+
}, []);
|
|
18
|
+
const getTriggers = useCallback(() => triggersRef.current, []);
|
|
19
|
+
// State management differs by type
|
|
20
|
+
if (type === 'single') {
|
|
21
|
+
return (_jsx(AccordionSingle, { ...props, registerTrigger: registerTrigger, getTriggers: getTriggers, className: className, children: children }));
|
|
22
|
+
}
|
|
23
|
+
return (_jsx(AccordionMultiple, { ...props, registerTrigger: registerTrigger, getTriggers: getTriggers, className: className, children: children }));
|
|
24
|
+
}
|
|
25
|
+
function AccordionSingle({ value: controlledValue, defaultValue, onValueChange, collapsible = false, children, className, registerTrigger, getTriggers, }) {
|
|
26
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
27
|
+
const isControlled = controlledValue !== undefined;
|
|
28
|
+
const currentValue = isControlled ? controlledValue : internalValue;
|
|
29
|
+
const isOpen = useCallback((itemValue) => currentValue === itemValue, [currentValue]);
|
|
30
|
+
const toggle = useCallback((itemValue) => {
|
|
31
|
+
let newValue;
|
|
32
|
+
if (currentValue === itemValue) {
|
|
33
|
+
// Closing current item
|
|
34
|
+
newValue = collapsible ? undefined : itemValue;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Opening new item
|
|
38
|
+
newValue = itemValue;
|
|
39
|
+
}
|
|
40
|
+
if (!isControlled) {
|
|
41
|
+
setInternalValue(newValue);
|
|
42
|
+
}
|
|
43
|
+
onValueChange?.(newValue);
|
|
44
|
+
}, [currentValue, collapsible, isControlled, onValueChange]);
|
|
45
|
+
const contextValue = useMemo(() => ({
|
|
46
|
+
type: 'single',
|
|
47
|
+
collapsible,
|
|
48
|
+
isOpen,
|
|
49
|
+
toggle,
|
|
50
|
+
registerTrigger,
|
|
51
|
+
getTriggers,
|
|
52
|
+
}), [collapsible, isOpen, toggle, registerTrigger, getTriggers]);
|
|
53
|
+
return (_jsx(AccordionContext.Provider, { value: contextValue, children: _jsx("div", { className: cx('tui-accordion', className), "data-type": "single", children: children }) }));
|
|
54
|
+
}
|
|
55
|
+
function AccordionMultiple({ value: controlledValue, defaultValue, onValueChange, children, className, registerTrigger, getTriggers, }) {
|
|
56
|
+
const [internalValue, setInternalValue] = useState(defaultValue ?? []);
|
|
57
|
+
const isControlled = controlledValue !== undefined;
|
|
58
|
+
const currentValue = isControlled ? controlledValue : internalValue;
|
|
59
|
+
const isOpen = useCallback((itemValue) => currentValue.includes(itemValue), [currentValue]);
|
|
60
|
+
const toggle = useCallback((itemValue) => {
|
|
61
|
+
const newValue = currentValue.includes(itemValue)
|
|
62
|
+
? currentValue.filter((v) => v !== itemValue)
|
|
63
|
+
: [...currentValue, itemValue];
|
|
64
|
+
if (!isControlled) {
|
|
65
|
+
setInternalValue(newValue);
|
|
66
|
+
}
|
|
67
|
+
onValueChange?.(newValue);
|
|
68
|
+
}, [currentValue, isControlled, onValueChange]);
|
|
69
|
+
const contextValue = useMemo(() => ({
|
|
70
|
+
type: 'multiple',
|
|
71
|
+
collapsible: true, // Multiple is always collapsible
|
|
72
|
+
isOpen,
|
|
73
|
+
toggle,
|
|
74
|
+
registerTrigger,
|
|
75
|
+
getTriggers,
|
|
76
|
+
}), [isOpen, toggle, registerTrigger, getTriggers]);
|
|
77
|
+
return (_jsx(AccordionContext.Provider, { value: contextValue, children: _jsx("div", { className: cx('tui-accordion', className), "data-type": "multiple", children: children }) }));
|
|
78
|
+
}
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// Accordion Item
|
|
81
|
+
// =============================================================================
|
|
82
|
+
function AccordionItem({ value, disabled = false, headingLevel, children, className, }) {
|
|
83
|
+
const baseId = useId();
|
|
84
|
+
const triggerId = `${baseId}-trigger`;
|
|
85
|
+
const panelId = `${baseId}-panel`;
|
|
86
|
+
const { isOpen } = useAccordionContext();
|
|
87
|
+
const open = isOpen(value);
|
|
88
|
+
const itemContext = useMemo(() => ({
|
|
89
|
+
value,
|
|
90
|
+
disabled,
|
|
91
|
+
headingLevel,
|
|
92
|
+
triggerId,
|
|
93
|
+
panelId,
|
|
94
|
+
isOpen: open,
|
|
95
|
+
}), [value, disabled, headingLevel, triggerId, panelId, open]);
|
|
96
|
+
const state = open ? 'open' : 'closed';
|
|
97
|
+
return (_jsx(AccordionItemContext.Provider, { value: itemContext, children: _jsx("div", { className: cx('tui-accordion__item', className), "data-state": state, "data-disabled": disabled || undefined, children: children }) }));
|
|
98
|
+
}
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// Accordion Trigger
|
|
101
|
+
// =============================================================================
|
|
102
|
+
function AccordionTrigger({ children, className }) {
|
|
103
|
+
const { toggle, registerTrigger, getTriggers } = useAccordionContext();
|
|
104
|
+
const { value, disabled, headingLevel, triggerId, panelId, isOpen } = useAccordionItemContext();
|
|
105
|
+
const buttonRef = useRef(null);
|
|
106
|
+
// Register trigger for keyboard navigation
|
|
107
|
+
React.useEffect(() => {
|
|
108
|
+
registerTrigger(value, buttonRef.current);
|
|
109
|
+
return () => registerTrigger(value, null);
|
|
110
|
+
}, [value, registerTrigger]);
|
|
111
|
+
const handleClick = () => {
|
|
112
|
+
if (!disabled) {
|
|
113
|
+
toggle(value);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const handleKeyDown = (event) => {
|
|
117
|
+
const triggers = getTriggers();
|
|
118
|
+
// Sort triggers by DOM order (not Map insertion order) to handle
|
|
119
|
+
// conditional rendering, async mounting, or reordered items
|
|
120
|
+
const sortedTriggers = Array.from(triggers.entries()).sort(([, a], [, b]) => {
|
|
121
|
+
if (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
122
|
+
return -1;
|
|
123
|
+
}
|
|
124
|
+
return 1;
|
|
125
|
+
});
|
|
126
|
+
const currentIndex = sortedTriggers.findIndex(([v]) => v === value);
|
|
127
|
+
// Find next/prev non-disabled trigger
|
|
128
|
+
const findNextIndex = (start, direction) => {
|
|
129
|
+
let index = start;
|
|
130
|
+
const len = sortedTriggers.length;
|
|
131
|
+
for (let i = 0; i < len; i++) {
|
|
132
|
+
index = (index + direction + len) % len;
|
|
133
|
+
const [, triggerElement] = sortedTriggers[index];
|
|
134
|
+
if (triggerElement && !triggerElement.disabled) {
|
|
135
|
+
return index;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return start; // No non-disabled trigger found
|
|
139
|
+
};
|
|
140
|
+
let targetIndex = null;
|
|
141
|
+
switch (event.key) {
|
|
142
|
+
case 'ArrowDown':
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
targetIndex = findNextIndex(currentIndex, 1);
|
|
145
|
+
break;
|
|
146
|
+
case 'ArrowUp':
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
targetIndex = findNextIndex(currentIndex, -1);
|
|
149
|
+
break;
|
|
150
|
+
case 'Home':
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
targetIndex = findNextIndex(sortedTriggers.length - 1, 1); // Start from end, go forward
|
|
153
|
+
break;
|
|
154
|
+
case 'End':
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
targetIndex = findNextIndex(0, -1); // Start from beginning, go backward
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
if (targetIndex !== null && targetIndex !== currentIndex) {
|
|
160
|
+
const [, targetElement] = sortedTriggers[targetIndex];
|
|
161
|
+
targetElement?.focus();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const state = isOpen ? 'open' : 'closed';
|
|
165
|
+
const button = (_jsxs("button", { ref: buttonRef, type: "button", id: triggerId, className: cx('tui-accordion__trigger', className), "aria-expanded": isOpen, "aria-controls": panelId, disabled: disabled, "data-state": state, "data-disabled": disabled || undefined, onClick: handleClick, onKeyDown: handleKeyDown, children: [_jsx("span", { className: "tui-accordion__trigger-content", children: children }), _jsx(Icon, { name: "system/chevron-down", size: "sm", className: "tui-accordion__indicator", "aria-hidden": "true" })] }));
|
|
166
|
+
// Wrap in heading if headingLevel is specified
|
|
167
|
+
if (headingLevel) {
|
|
168
|
+
const Heading = `h${headingLevel}`;
|
|
169
|
+
return _jsx(Heading, { className: "tui-accordion__heading", children: button });
|
|
170
|
+
}
|
|
171
|
+
return button;
|
|
172
|
+
}
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// Accordion Panel
|
|
175
|
+
// =============================================================================
|
|
176
|
+
function AccordionPanel({ children, className }) {
|
|
177
|
+
const { triggerId, panelId, isOpen } = useAccordionItemContext();
|
|
178
|
+
const state = isOpen ? 'open' : 'closed';
|
|
179
|
+
return (_jsx("div", { id: panelId, role: "region", "aria-labelledby": triggerId, className: cx('tui-accordion__panel', className), "data-state": state, "aria-hidden": !isOpen,
|
|
180
|
+
// Prevent keyboard focus into collapsed panels
|
|
181
|
+
inert: !isOpen || undefined, children: _jsx("div", { className: "tui-accordion__panel-content", children: children }) }));
|
|
182
|
+
}
|
|
183
|
+
// =============================================================================
|
|
184
|
+
// Export Compound Component
|
|
185
|
+
// =============================================================================
|
|
186
|
+
AccordionItem.displayName = 'Accordion.Item';
|
|
187
|
+
AccordionTrigger.displayName = 'Accordion.Trigger';
|
|
188
|
+
AccordionPanel.displayName = 'Accordion.Panel';
|
|
189
|
+
export const Accordion = AccordionRoot;
|
|
190
|
+
Accordion.Item = AccordionItem;
|
|
191
|
+
Accordion.Trigger = AccordionTrigger;
|
|
192
|
+
Accordion.Panel = AccordionPanel;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AccordionContextValue, AccordionItemContextValue } from './types';
|
|
2
|
+
export declare const AccordionContext: import("react").Context<AccordionContextValue | null>;
|
|
3
|
+
export declare function useAccordionContext(): AccordionContextValue;
|
|
4
|
+
export declare const AccordionItemContext: import("react").Context<AccordionItemContextValue | null>;
|
|
5
|
+
export declare function useAccordionItemContext(): AccordionItemContextValue;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Accordion Root Context
|
|
4
|
+
// =============================================================================
|
|
5
|
+
export const AccordionContext = createContext(null);
|
|
6
|
+
export function useAccordionContext() {
|
|
7
|
+
const context = useContext(AccordionContext);
|
|
8
|
+
if (!context) {
|
|
9
|
+
throw new Error('Accordion compound components must be used within an Accordion');
|
|
10
|
+
}
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Accordion Item Context
|
|
15
|
+
// =============================================================================
|
|
16
|
+
export const AccordionItemContext = createContext(null);
|
|
17
|
+
export function useAccordionItemContext() {
|
|
18
|
+
const context = useContext(AccordionItemContext);
|
|
19
|
+
if (!context) {
|
|
20
|
+
throw new Error('Accordion.Trigger and Accordion.Panel must be used within an Accordion.Item');
|
|
21
|
+
}
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Accordion } from './Accordion.js';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
type AccordionBaseProps = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
export type AccordionSingleProps = AccordionBaseProps & {
|
|
7
|
+
type: 'single';
|
|
8
|
+
/** Controlled: currently open item value */
|
|
9
|
+
value?: string;
|
|
10
|
+
/** Uncontrolled: initially open item */
|
|
11
|
+
defaultValue?: string;
|
|
12
|
+
/** Callback when open item changes (undefined when all closed in collapsible mode) */
|
|
13
|
+
onValueChange?: (value: string | undefined) => void;
|
|
14
|
+
/** Allow closing all items (otherwise one stays open) */
|
|
15
|
+
collapsible?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type AccordionMultipleProps = AccordionBaseProps & {
|
|
18
|
+
type: 'multiple';
|
|
19
|
+
/** Controlled: currently open item values */
|
|
20
|
+
value?: string[];
|
|
21
|
+
/** Uncontrolled: initially open items */
|
|
22
|
+
defaultValue?: string[];
|
|
23
|
+
/** Callback when open items change */
|
|
24
|
+
onValueChange?: (value: string[]) => void;
|
|
25
|
+
};
|
|
26
|
+
export type AccordionProps = AccordionSingleProps | AccordionMultipleProps;
|
|
27
|
+
export type AccordionItemProps = {
|
|
28
|
+
/** Unique identifier for this item (required) */
|
|
29
|
+
value: string;
|
|
30
|
+
/** Prevent interaction */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Wrap trigger in heading element */
|
|
33
|
+
headingLevel?: 2 | 3 | 4 | 5 | 6;
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
className?: string;
|
|
36
|
+
};
|
|
37
|
+
export type AccordionTriggerProps = {
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
className?: string;
|
|
40
|
+
};
|
|
41
|
+
export type AccordionPanelProps = {
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
className?: string;
|
|
44
|
+
};
|
|
45
|
+
export type AccordionContextValue = {
|
|
46
|
+
type: 'single' | 'multiple';
|
|
47
|
+
collapsible: boolean;
|
|
48
|
+
isOpen: (value: string) => boolean;
|
|
49
|
+
toggle: (value: string) => void;
|
|
50
|
+
registerTrigger: (value: string, element: HTMLButtonElement | null) => void;
|
|
51
|
+
getTriggers: () => Map<string, HTMLButtonElement>;
|
|
52
|
+
};
|
|
53
|
+
export type AccordionItemContextValue = {
|
|
54
|
+
value: string;
|
|
55
|
+
disabled: boolean;
|
|
56
|
+
headingLevel?: 2 | 3 | 4 | 5 | 6;
|
|
57
|
+
triggerId: string;
|
|
58
|
+
panelId: string;
|
|
59
|
+
isOpen: boolean;
|
|
60
|
+
};
|
|
61
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AvatarProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Avatar displays a user's profile image, initials, or a placeholder icon.
|
|
5
|
+
*
|
|
6
|
+
* - Loads image from `src` prop
|
|
7
|
+
* - Falls back to initials from `name` if image fails or isn't provided
|
|
8
|
+
* - Shows placeholder icon if neither `src` nor `name` provided
|
|
9
|
+
* - Colors for initials are derived from the name hash for consistency
|
|
10
|
+
*/
|
|
11
|
+
export declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useMemo } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { Icon } from '../Icon/index.js';
|
|
5
|
+
import { AVATAR_COLORS } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Generate initials from a name.
|
|
8
|
+
* "Mary Ghen" → "MG", "Bob" → "B", "Jean-Luc Picard" → "JP"
|
|
9
|
+
*/
|
|
10
|
+
function getInitials(name) {
|
|
11
|
+
const parts = name.trim().split(/[\s-]+/).filter(Boolean);
|
|
12
|
+
if (parts.length === 0)
|
|
13
|
+
return '';
|
|
14
|
+
if (parts.length === 1)
|
|
15
|
+
return parts[0].charAt(0).toUpperCase();
|
|
16
|
+
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Simple hash function for consistent color assignment.
|
|
20
|
+
* Same name always gets the same color.
|
|
21
|
+
*/
|
|
22
|
+
function hashString(str) {
|
|
23
|
+
if (!str)
|
|
24
|
+
return 0;
|
|
25
|
+
let hash = 0;
|
|
26
|
+
for (let i = 0; i < str.length; i++) {
|
|
27
|
+
const char = str.charCodeAt(i);
|
|
28
|
+
hash = ((hash << 5) - hash) + char;
|
|
29
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
30
|
+
}
|
|
31
|
+
return Math.abs(hash);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get a consistent color for a name.
|
|
35
|
+
*/
|
|
36
|
+
function getColorFromName(name, colors) {
|
|
37
|
+
const hash = hashString(name.toLowerCase());
|
|
38
|
+
return colors[hash % colors.length];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Avatar displays a user's profile image, initials, or a placeholder icon.
|
|
42
|
+
*
|
|
43
|
+
* - Loads image from `src` prop
|
|
44
|
+
* - Falls back to initials from `name` if image fails or isn't provided
|
|
45
|
+
* - Shows placeholder icon if neither `src` nor `name` provided
|
|
46
|
+
* - Colors for initials are derived from the name hash for consistency
|
|
47
|
+
*/
|
|
48
|
+
export const Avatar = React.forwardRef(({ src, name, size = 'md', shape = 'circle', color, indicator, indicatorLabel, indicatorPosition = 'bottom-right', className, }, ref) => {
|
|
49
|
+
const [imgError, setImgError] = useState(false);
|
|
50
|
+
// Reset error state when src changes
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
setImgError(false);
|
|
53
|
+
}, [src]);
|
|
54
|
+
const initials = useMemo(() => (name ? getInitials(name) : ''), [name]);
|
|
55
|
+
const derivedColor = useMemo(() => color ?? (name ? getColorFromName(name, AVATAR_COLORS) : 'slate'), [color, name]);
|
|
56
|
+
const showImage = src && !imgError;
|
|
57
|
+
const showInitials = !showImage && initials;
|
|
58
|
+
const showPlaceholder = !showImage && !initials;
|
|
59
|
+
// Placeholder avatars are decorative (no meaningful identity)
|
|
60
|
+
const isDecorative = showPlaceholder;
|
|
61
|
+
return (_jsxs("span", { ref: ref, className: cx('tui-avatar', `is-size-${size}`, `is-shape-${shape}`, !showImage && `is-color-${derivedColor}`, className), ...(isDecorative
|
|
62
|
+
? { 'aria-hidden': true }
|
|
63
|
+
: { role: 'img', 'aria-label': name }), children: [_jsxs("span", { className: "tui-avatar__content", children: [showImage && (_jsx("img", { src: src, alt: "", className: "tui-avatar__image", onError: () => setImgError(true) })), showInitials && (_jsx("span", { className: "tui-avatar__initials", "aria-hidden": "true", children: initials })), showPlaceholder && (_jsx(Icon, { name: "system/user-circle-outline", className: "tui-avatar__placeholder" }))] }), indicator && (_jsx("span", { className: cx('tui-avatar__indicator', `is-position-${indicatorPosition}`), ...(indicatorLabel
|
|
64
|
+
? { role: 'img', 'aria-label': indicatorLabel }
|
|
65
|
+
: { 'aria-hidden': true }), children: indicator }))] }));
|
|
66
|
+
});
|
|
67
|
+
Avatar.displayName = 'Avatar';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AvatarGroupProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* AvatarGroup displays multiple avatars with optional overlap.
|
|
5
|
+
*
|
|
6
|
+
* - Shows up to `max` avatars, with "+N" overflow indicator
|
|
7
|
+
* - Overlap mode (default) uses OverlapStack with avatar-styled overflow
|
|
8
|
+
* - Non-overlap mode uses flex with gap
|
|
9
|
+
* - Override overlap amount via `--tui-avatar-group-overlap` CSS property
|
|
10
|
+
*/
|
|
11
|
+
export declare const AvatarGroup: React.ForwardRefExoticComponent<AvatarGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { Children, isValidElement, cloneElement } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { OverlapStack } from '../OverlapStack/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* AvatarGroup displays multiple avatars with optional overlap.
|
|
7
|
+
*
|
|
8
|
+
* - Shows up to `max` avatars, with "+N" overflow indicator
|
|
9
|
+
* - Overlap mode (default) uses OverlapStack with avatar-styled overflow
|
|
10
|
+
* - Non-overlap mode uses flex with gap
|
|
11
|
+
* - Override overlap amount via `--tui-avatar-group-overlap` CSS property
|
|
12
|
+
*/
|
|
13
|
+
export const AvatarGroup = React.forwardRef(({ max, size, shape, overlap = true, children, className }, ref) => {
|
|
14
|
+
const childArray = Children.toArray(children).filter(isValidElement);
|
|
15
|
+
const total = childArray.length;
|
|
16
|
+
// Clone children to inject size/shape props if provided at group level
|
|
17
|
+
const clonedChildren = childArray.map((child) => {
|
|
18
|
+
if (isValidElement(child)) {
|
|
19
|
+
return cloneElement(child, {
|
|
20
|
+
size: size ?? child.props.size,
|
|
21
|
+
shape: shape ?? child.props.shape,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return child;
|
|
25
|
+
});
|
|
26
|
+
// Descriptive label for the group
|
|
27
|
+
const hasOverflow = max !== undefined && total > max;
|
|
28
|
+
const visibleCount = hasOverflow ? max : total;
|
|
29
|
+
const groupLabel = hasOverflow
|
|
30
|
+
? `${total} users, showing ${visibleCount}`
|
|
31
|
+
: `${total} users`;
|
|
32
|
+
// Non-overlap mode: simple flex layout
|
|
33
|
+
if (!overlap) {
|
|
34
|
+
return (_jsx("div", { ref: ref, className: cx('tui-avatar-group', className), role: "group", "aria-label": groupLabel, children: clonedChildren }));
|
|
35
|
+
}
|
|
36
|
+
// Overlap mode: delegate to OverlapStack
|
|
37
|
+
// Note: We don't use OverlapStack's `frame` prop because its border-radius
|
|
38
|
+
// wouldn't match each avatar's shape. Instead, we apply ring styles via CSS
|
|
39
|
+
// directly to avatars inside .is-overlap groups.
|
|
40
|
+
return (_jsx(OverlapStack, { ref: ref, className: cx('tui-avatar-group', 'is-overlap', className), max: max, renderOverflow: (count) => (_jsx(AvatarOverflow, { count: count, size: size, shape: shape })), overflowLabel: (count) => `${count} more users`, "aria-label": groupLabel, children: clonedChildren }));
|
|
41
|
+
});
|
|
42
|
+
AvatarGroup.displayName = 'AvatarGroup';
|
|
43
|
+
function AvatarOverflow({ count, size, shape }) {
|
|
44
|
+
return (_jsx("span", { className: cx('tui-avatar', 'tui-avatar--overflow', size && `is-size-${size}`, shape && `is-shape-${shape}`), role: "img", "aria-hidden": "true", children: _jsx("span", { className: "tui-avatar__content", children: _jsxs("span", { className: "tui-avatar__initials", children: ["+", count] }) }) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Avatar as AvatarBase } from './Avatar';
|
|
2
|
+
import { AvatarGroup } from './AvatarGroup';
|
|
3
|
+
export type { AvatarProps, AvatarGroupProps, AvatarSize, AvatarShape, AvatarColor, IndicatorPosition, } from './types';
|
|
4
|
+
export { AVATAR_COLORS } from './types';
|
|
5
|
+
type AvatarCompound = typeof AvatarBase & {
|
|
6
|
+
Group: typeof AvatarGroup;
|
|
7
|
+
};
|
|
8
|
+
export declare const Avatar: AvatarCompound;
|
|
9
|
+
export { AvatarGroup };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Avatar as AvatarBase } from './Avatar.js';
|
|
2
|
+
import { AvatarGroup } from './AvatarGroup.js';
|
|
3
|
+
export { AVATAR_COLORS } from './types.js';
|
|
4
|
+
export const Avatar = AvatarBase;
|
|
5
|
+
Avatar.Group = AvatarGroup;
|
|
6
|
+
// Named export for direct import
|
|
7
|
+
export { AvatarGroup };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SizeExtended } from '../../types';
|
|
2
|
+
export type AvatarSize = SizeExtended;
|
|
3
|
+
export type AvatarShape = 'circle' | 'square';
|
|
4
|
+
export type IndicatorPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
5
|
+
/**
|
|
6
|
+
* Avatar palette colors for initials backgrounds.
|
|
7
|
+
* These are not tied to semantic themes - they provide visual variety.
|
|
8
|
+
*/
|
|
9
|
+
export type AvatarColor = 'coral' | 'amber' | 'lime' | 'teal' | 'cyan' | 'blue' | 'violet' | 'pink' | 'slate' | 'emerald';
|
|
10
|
+
export declare const AVATAR_COLORS: AvatarColor[];
|
|
11
|
+
export type AvatarProps = {
|
|
12
|
+
/** Image source URL */
|
|
13
|
+
src?: string;
|
|
14
|
+
/** User's name - used for alt text and generating initials fallback */
|
|
15
|
+
name?: string;
|
|
16
|
+
/** Size of the avatar */
|
|
17
|
+
size?: AvatarSize;
|
|
18
|
+
/** Shape of the avatar */
|
|
19
|
+
shape?: AvatarShape;
|
|
20
|
+
/** Explicit color for initials background (otherwise derived from name) */
|
|
21
|
+
color?: AvatarColor;
|
|
22
|
+
/** Status indicator element (icon, badge, etc.) */
|
|
23
|
+
indicator?: React.ReactNode;
|
|
24
|
+
/** Accessible label for the indicator (e.g., "Online", "Verified", "3 notifications") */
|
|
25
|
+
indicatorLabel?: string;
|
|
26
|
+
/** Position of the indicator */
|
|
27
|
+
indicatorPosition?: IndicatorPosition;
|
|
28
|
+
/** Additional CSS class */
|
|
29
|
+
className?: string;
|
|
30
|
+
};
|
|
31
|
+
export type AvatarGroupProps = {
|
|
32
|
+
/** Maximum avatars to show before "+N" overflow */
|
|
33
|
+
max?: number;
|
|
34
|
+
/** Size for all avatars in the group */
|
|
35
|
+
size?: AvatarSize;
|
|
36
|
+
/** Shape for all avatars in the group */
|
|
37
|
+
shape?: AvatarShape;
|
|
38
|
+
/** Whether avatars overlap (default: true) */
|
|
39
|
+
overlap?: boolean;
|
|
40
|
+
/** Children (Avatar components) */
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
/** Additional CSS class */
|
|
43
|
+
className?: string;
|
|
44
|
+
};
|