@tangible/ui 0.0.5 → 0.0.6
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 +124 -27
- package/components/Accordion/Accordion.js +1 -1
- package/components/Combobox/Combobox.js +2 -1
- package/components/Icon/Icon.js +2 -1
- package/components/Modal/Modal.js +2 -2
- package/components/MoveHandle/MoveHandle.js +13 -2
- package/components/MultiSelect/MultiSelect.js +2 -1
- package/components/Progress/Progress.js +2 -1
- package/components/Select/Select.js +2 -1
- package/components/StepList/StepList.js +2 -1
- package/components/Tabs/Tabs.js +2 -2
- package/components/Toolbar/Toolbar.js +2 -1
- package/components/Tooltip/Tooltip.js +2 -1
- package/package.json +7 -9
- package/styles/all.css +1 -1
- package/styles/all.expanded.css +63 -32
- package/styles/all.expanded.unlayered.css +63 -32
- package/styles/all.unlayered.css +1 -1
- package/styles/components/input/index.scss +2 -2
- package/styles/index.scss +14 -0
- package/styles/system/_control.scss +6 -3
- package/tui-manifest.json +10 -2
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Tangible UI
|
|
2
2
|
|
|
3
|
-
Design system for Tangible WordPress plugins. React components
|
|
3
|
+
Design system for Tangible WordPress plugins. React components, CSS custom property tokens, and CSS-only form elements.
|
|
4
4
|
|
|
5
5
|
**Live Storybook:** https://storybook-tangible-ui.pages.dev
|
|
6
6
|
|
|
7
7
|
## Components
|
|
8
8
|
|
|
9
|
-
- **Primitives:** Button, Chip, ChipGroup, Icon,
|
|
9
|
+
- **Primitives:** Button, IconButton, Chip, ChipGroup, Icon, Progress, Rating, Tooltip
|
|
10
10
|
- **Layout:** Accordion, Card, Modal, Notice, Sidebar, Tabs, Toolbar
|
|
11
11
|
- **Data:** DataTable, StepList, StepIndicator, Pager
|
|
12
|
-
- **Form Controls:** Select, MultiSelect, Combobox, TextInput, Textarea, Checkbox, Switch, Radio
|
|
12
|
+
- **Form Controls:** Select, MultiSelect, Combobox, TextInput, Textarea, Checkbox, Switch, Radio/RadioGroup
|
|
13
13
|
- **Composites:** Avatar, Dropdown, MoveHandle, OverlapStack, SegmentedControl, Field
|
|
14
|
-
- **CSS-only Inputs:** Text, textarea, select, checkbox, radio, toggle, file
|
|
14
|
+
- **CSS-only Inputs:** Text, textarea, select, checkbox, radio, toggle, file (no JS required)
|
|
15
15
|
|
|
16
16
|
## Getting Started
|
|
17
17
|
|
|
@@ -21,21 +21,40 @@ Design system for Tangible WordPress plugins. React components + CSS tokens + ut
|
|
|
21
21
|
npm install @tangible/ui
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
#### Optional peer dependencies
|
|
25
|
+
|
|
26
|
+
Some components require additional packages. Install only what you use:
|
|
27
|
+
|
|
28
|
+
| Package | Required by | Size |
|
|
29
|
+
|---------|-------------|------|
|
|
30
|
+
| `@floating-ui/react` | Select, MultiSelect, Combobox, Dropdown, Tooltip | ~90 KB |
|
|
31
|
+
| `@tanstack/react-table` | DataTable | ~50 KB |
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# If using Select, Dropdown, Tooltip, etc.
|
|
35
|
+
npm install @floating-ui/react
|
|
36
|
+
|
|
37
|
+
# If using DataTable
|
|
38
|
+
npm install @tanstack/react-table
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Components without these dependencies (Button, Card, Accordion, Modal, Tabs, etc.) work with zero additional installs.
|
|
42
|
+
|
|
24
43
|
### Import styles
|
|
25
44
|
|
|
26
45
|
```tsx
|
|
27
|
-
// In your app entry point
|
|
28
46
|
import '@tangible/ui/styles';
|
|
29
47
|
```
|
|
30
48
|
|
|
31
|
-
|
|
49
|
+
For WordPress plugin contexts where CSS layers can lose to unlayered theme styles:
|
|
50
|
+
|
|
32
51
|
```tsx
|
|
33
52
|
import '@tangible/ui/styles/unlayered';
|
|
34
53
|
```
|
|
35
54
|
|
|
36
|
-
###
|
|
55
|
+
### Set up the interface wrapper
|
|
37
56
|
|
|
38
|
-
|
|
57
|
+
All components require the `.tui-interface` wrapper to access design tokens:
|
|
39
58
|
|
|
40
59
|
```tsx
|
|
41
60
|
function App() {
|
|
@@ -47,34 +66,112 @@ function App() {
|
|
|
47
66
|
}
|
|
48
67
|
```
|
|
49
68
|
|
|
50
|
-
Dark mode
|
|
69
|
+
### Dark mode
|
|
70
|
+
|
|
71
|
+
Set `data-theme` on the wrapper:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<div className="tui-interface" data-theme="dark">
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- `"dark"` — force dark mode
|
|
78
|
+
- `"auto"` — follow `prefers-color-scheme`
|
|
79
|
+
- Omit attribute — light mode (inherits host colour)
|
|
51
80
|
|
|
52
81
|
### Use components
|
|
53
82
|
|
|
54
83
|
```tsx
|
|
55
|
-
import { Button, Card,
|
|
84
|
+
import { Button, Card, Select, SelectOption } from '@tangible/ui';
|
|
56
85
|
|
|
57
86
|
function Example() {
|
|
58
87
|
return (
|
|
59
88
|
<Card>
|
|
60
89
|
<Card.Body>
|
|
61
|
-
<Button label="
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
</
|
|
69
|
-
|
|
70
|
-
</Tooltip>
|
|
90
|
+
<Button label="Save" theme="primary" />
|
|
91
|
+
|
|
92
|
+
<Select placeholder="Choose..." onValueChange={(v) => console.log(v)}>
|
|
93
|
+
<Select.Trigger />
|
|
94
|
+
<Select.Content>
|
|
95
|
+
<Select.Option value="a">Option A</Select.Option>
|
|
96
|
+
<Select.Option value="b">Option B</Select.Option>
|
|
97
|
+
</Select.Content>
|
|
98
|
+
</Select>
|
|
71
99
|
</Card.Body>
|
|
72
100
|
</Card>
|
|
73
101
|
);
|
|
74
102
|
}
|
|
75
103
|
```
|
|
76
104
|
|
|
77
|
-
|
|
105
|
+
### Tree-shaking
|
|
106
|
+
|
|
107
|
+
Individual component imports are available if your bundler doesn't tree-shake the barrel export:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { Button } from '@tangible/ui/components/Button';
|
|
111
|
+
import { Tooltip } from '@tangible/ui/components/Tooltip';
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Customisation
|
|
115
|
+
|
|
116
|
+
### Token overrides
|
|
117
|
+
|
|
118
|
+
Components are styled via CSS custom properties. Override them on `.tui-interface` or any ancestor:
|
|
119
|
+
|
|
120
|
+
```css
|
|
121
|
+
/* Global overrides */
|
|
122
|
+
.tui-interface {
|
|
123
|
+
--tui-radius-md: 2px;
|
|
124
|
+
--tui-focus-ring-color: hotpink;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Scoped to a specific context */
|
|
128
|
+
.my-sidebar .tui-interface {
|
|
129
|
+
--tui-button-radius: 0;
|
|
130
|
+
--tui-control-height-md: 32px;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Component API tokens
|
|
135
|
+
|
|
136
|
+
Each component reads its own `--tui-{component}-*` tokens via fallback chains. These are never defined by TUI — only read. Set them from consuming code:
|
|
137
|
+
|
|
138
|
+
```css
|
|
139
|
+
.compact-form {
|
|
140
|
+
--tui-accordion-padding: 8px;
|
|
141
|
+
--tui-select-trigger-radius: 2px;
|
|
142
|
+
--tui-modal-spacing: 24px;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
See each component's `styles.scss` header for its full token API.
|
|
147
|
+
|
|
148
|
+
### Form control sizing
|
|
149
|
+
|
|
150
|
+
All form controls share a unified sizing system:
|
|
151
|
+
|
|
152
|
+
```css
|
|
153
|
+
.my-context .tui-interface {
|
|
154
|
+
--tui-control-height-sm: 28px;
|
|
155
|
+
--tui-control-height-md: 32px;
|
|
156
|
+
--tui-control-height-lg: 40px;
|
|
157
|
+
|
|
158
|
+
/* Optional: decouple font size from size tier */
|
|
159
|
+
--tui-control-font-size-sm: 13px;
|
|
160
|
+
--tui-control-font-size-md: 13px;
|
|
161
|
+
--tui-control-font-size-lg: 14px;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Icons
|
|
166
|
+
|
|
167
|
+
Four icon sets available via the registry: `system`, `cred`, `reaction`, `player`.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { Icon } from '@tangible/ui';
|
|
171
|
+
|
|
172
|
+
<Icon name="system/check" />
|
|
173
|
+
<Icon name="reaction/clap-fill" size="lg" />
|
|
174
|
+
```
|
|
78
175
|
|
|
79
176
|
## Development
|
|
80
177
|
|
|
@@ -83,7 +180,7 @@ npm install
|
|
|
83
180
|
npm run storybook # Dev server on port 6006
|
|
84
181
|
```
|
|
85
182
|
|
|
86
|
-
|
|
183
|
+
### Commands
|
|
87
184
|
|
|
88
185
|
```bash
|
|
89
186
|
npm run storybook # Dev server
|
|
@@ -97,12 +194,12 @@ npm run test:visual:update # Regenerate visual baselines
|
|
|
97
194
|
|
|
98
195
|
## Documentation
|
|
99
196
|
|
|
100
|
-
- `CLAUDE.md` —
|
|
101
|
-
- `CONTEXT.md` — Project background and
|
|
102
|
-
- `TIMELINE.md` — Development roadmap
|
|
197
|
+
- `CLAUDE.md` — Architecture, patterns, conventions, gotchas
|
|
198
|
+
- `CONTEXT.md` — Project background and design philosophy
|
|
199
|
+
- `TIMELINE.md` — Development roadmap
|
|
103
200
|
- `TESTING.md` — Testing strategy and infrastructure
|
|
104
|
-
- `
|
|
201
|
+
- `CHANGELOG.md` — Release history
|
|
105
202
|
|
|
106
203
|
## Status
|
|
107
204
|
|
|
108
|
-
Under active development
|
|
205
|
+
Under active development. Component APIs are stabilising but may change before 1.0.
|
|
@@ -233,7 +233,7 @@ function AccordionTrigger({ asChild = false, 'aria-label': ariaLabel, children,
|
|
|
233
233
|
return element;
|
|
234
234
|
}
|
|
235
235
|
// Default: render as button with built-in chevron indicator
|
|
236
|
-
const button = (_jsxs("button", { ref: triggerRef, type: "button", id: triggerId, className: cx('tui-accordion__trigger', className), "aria-expanded": isOpen, "aria-controls": panelId, "aria-label": ariaLabel, 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: "lg", className: "tui-accordion__indicator", "aria-hidden": "true" })] }));
|
|
236
|
+
const button = (_jsxs("button", { ref: triggerRef, type: "button", id: triggerId, className: cx('tui-accordion__trigger', className), "aria-expanded": isOpen, "aria-controls": panelId, "aria-label": ariaLabel, disabled: disabled, "data-state": state, "data-disabled": disabled || undefined, onClick: handleClick, onKeyDown: handleKeyDown, children: [children && _jsx("span", { className: "tui-accordion__trigger-content", children: children }), _jsx(Icon, { name: "system/chevron-down", size: "lg", className: "tui-accordion__indicator", "aria-hidden": "true" })] }));
|
|
237
237
|
// Wrap in heading if headingLevel is specified
|
|
238
238
|
if (headingLevel) {
|
|
239
239
|
const Heading = `h${headingLevel}`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
3
4
|
import { useFloating, offset, flip, shift, size as sizeMiddleware, autoUpdate, FloatingPortal, useDismiss, useInteractions, useListNavigation, useRole, } from '@floating-ui/react';
|
|
4
5
|
import { cx } from '../../utils/cx.js';
|
|
5
6
|
import { toKey } from '../../utils/value-key.js';
|
|
@@ -396,7 +397,7 @@ function ComboboxOptionComponent({ value: optionValue, disabled = false, textVal
|
|
|
396
397
|
const textValue = explicitTextValue ?? (typeof children === 'string' ? children : '');
|
|
397
398
|
// Warn in dev if textValue couldn't be derived
|
|
398
399
|
useEffect(() => {
|
|
399
|
-
if (
|
|
400
|
+
if (isDev() && !textValue) {
|
|
400
401
|
console.warn(`Combobox.Option with value="${optionValue}" has no textValue. Provide textValue prop when children is not a string.`);
|
|
401
402
|
}
|
|
402
403
|
}, [textValue, optionValue]);
|
package/components/Icon/Icon.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
4
5
|
import { iconRegistry } from '../../icons/registry.js';
|
|
5
6
|
/**
|
|
6
7
|
* Icon component for SVG icons from the registry or emoji characters.
|
|
@@ -12,7 +13,7 @@ import { iconRegistry } from '../../icons/registry.js';
|
|
|
12
13
|
export const Icon = React.forwardRef(({ name, emoji, label, size, className }, ref) => {
|
|
13
14
|
const SvgIcon = name ? iconRegistry[name] : null;
|
|
14
15
|
// Dev warning for invalid icon name
|
|
15
|
-
if (
|
|
16
|
+
if (isDev() && name && !SvgIcon) {
|
|
16
17
|
console.warn(`[Icon] Unknown icon name: "${name}". Check the registry.`);
|
|
17
18
|
}
|
|
18
19
|
// Decorative if no label provided
|
|
@@ -108,7 +108,7 @@ function ModalRoot({ open, onClose, size = 'md', stickyHead, stickyFoot, 'aria-l
|
|
|
108
108
|
let target = null;
|
|
109
109
|
if (initialFocusSelector) {
|
|
110
110
|
target = dialog.querySelector(initialFocusSelector);
|
|
111
|
-
if (!target &&
|
|
111
|
+
if (!target && isDev()) {
|
|
112
112
|
console.warn(`Modal: initialFocusSelector="${initialFocusSelector}" did not match any element. ` +
|
|
113
113
|
`Falling back to first focusable element.`);
|
|
114
114
|
}
|
|
@@ -118,7 +118,7 @@ function ModalRoot({ open, onClose, size = 'md', stickyHead, stickyFoot, 'aria-l
|
|
|
118
118
|
}
|
|
119
119
|
target.focus({ preventScroll: true });
|
|
120
120
|
// Development warning for missing labelledBy target
|
|
121
|
-
if (
|
|
121
|
+
if (isDev() && labelledBy) {
|
|
122
122
|
const labelElement = document.getElementById(labelledBy);
|
|
123
123
|
if (!labelElement) {
|
|
124
124
|
console.warn(`Modal: aria-labelledby="${labelledBy}" references a non-existent element. ` +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useCallback, useEffect, useId, useRef } from 'react';
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
4
|
import { isDev } from '../../utils/is-dev.js';
|
|
5
5
|
import { Icon } from '../Icon/index.js';
|
|
@@ -64,6 +64,17 @@ export const MoveHandle = forwardRef(function MoveHandle({ mode = 'full', size =
|
|
|
64
64
|
fallback?.focus();
|
|
65
65
|
}
|
|
66
66
|
}, [mode, canMoveUp, canMoveDown]);
|
|
67
|
+
// Debounce the lock icon — prevents visual jitter when `locked` flashes
|
|
68
|
+
// briefly (e.g. during a save). Behaviour (disabled buttons) applies
|
|
69
|
+
// immediately; only the icon swap is delayed.
|
|
70
|
+
const [showLockIcon, setShowLockIcon] = useState(locked);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (locked) {
|
|
73
|
+
const id = setTimeout(() => setShowLockIcon(true), 150);
|
|
74
|
+
return () => clearTimeout(id);
|
|
75
|
+
}
|
|
76
|
+
setShowLockIcon(false);
|
|
77
|
+
}, [locked]);
|
|
67
78
|
// Drag handle label precedence: dragHandleProps > labels.drag > default
|
|
68
79
|
const resolvedDragLabel = dragHandleProps?.['aria-label'] ?? labels?.drag ?? 'Drag to reorder';
|
|
69
80
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -80,5 +91,5 @@ export const MoveHandle = forwardRef(function MoveHandle({ mode = 'full', size =
|
|
|
80
91
|
const resolvedLockedDesc = locked
|
|
81
92
|
? (labels?.locked ?? 'This item is locked and cannot be reordered')
|
|
82
93
|
: undefined;
|
|
83
|
-
return (_jsxs("div", { ref: mergedRef, role: "group", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": locked ? lockedDescId : undefined, "aria-disabled": locked || undefined, className: cx('tui-move-handle', `is-size-${size}`, locked && 'is-locked', hasIndex && 'has-index', className), children: [locked && (_jsx("span", { id: lockedDescId, className: "tui-visually-hidden", children: resolvedLockedDesc })), onMoveUp && (_jsx("button", { type: "button", className: "tui-move-handle__up", "data-direction": "up", "aria-label": labels?.moveUp ?? 'Move up', disabled: locked || !canMoveUp, onClick: onMoveUp, children: _jsx(Icon, { name: "system/chevron-up" }) })), _jsx("div", { className: "tui-move-handle__center", children:
|
|
94
|
+
return (_jsxs("div", { ref: mergedRef, role: "group", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": locked ? lockedDescId : undefined, "aria-disabled": locked || undefined, className: cx('tui-move-handle', `is-size-${size}`, locked && 'is-locked', hasIndex && 'has-index', className), children: [locked && (_jsx("span", { id: lockedDescId, className: "tui-visually-hidden", children: resolvedLockedDesc })), onMoveUp && (_jsx("button", { type: "button", className: "tui-move-handle__up", "data-direction": "up", "aria-label": labels?.moveUp ?? 'Move up', disabled: locked || !canMoveUp, onClick: onMoveUp, children: _jsx(Icon, { name: "system/chevron-up" }) })), _jsx("div", { className: "tui-move-handle__center", children: showLockIcon ? (_jsx("span", { className: "tui-move-handle__lock", "aria-hidden": "true", children: _jsx(Icon, { name: "system/lock" }) })) : (_jsxs(_Fragment, { children: [hasIndex && (_jsx("span", { className: "tui-move-handle__index", "aria-hidden": "true", children: index })), _jsx("button", { type: "button", className: "tui-move-handle__handle", "data-role": "drag-handle", "aria-label": resolvedDragLabel, tabIndex: hasArrows ? -1 : 0, ...restDragProps, children: _jsx(Icon, { name: "system/handle-alt" }) })] })) }), onMoveDown && (_jsx("button", { type: "button", className: "tui-move-handle__down", "data-direction": "down", "aria-label": labels?.moveDown ?? 'Move down', disabled: locked || !canMoveDown, onClick: onMoveDown, children: _jsx(Icon, { name: "system/chevron-down" }) }))] }));
|
|
84
95
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, cloneElement, isValidElement, } from 'react';
|
|
3
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
3
4
|
import { useFloating, offset, flip, shift, size as sizeMiddleware, autoUpdate, FloatingPortal, useDismiss, useInteractions, useListNavigation, useTypeahead, useRole, useClick, } from '@floating-ui/react';
|
|
4
5
|
import { cx } from '../../utils/cx.js';
|
|
5
6
|
import { getPortalRootFor } from '../../utils/portal.js';
|
|
@@ -549,7 +550,7 @@ function MultiSelectOptionComponent({ value: optionValue, disabled = false, text
|
|
|
549
550
|
const textValue = explicitTextValue ?? (typeof children === 'string' ? children : '');
|
|
550
551
|
// Warn in dev if textValue couldn't be derived
|
|
551
552
|
useEffect(() => {
|
|
552
|
-
if (
|
|
553
|
+
if (isDev() && !textValue) {
|
|
553
554
|
console.warn(`MultiSelect.Option with value="${optionValue}" has no textValue. Provide textValue prop when children is not a string.`);
|
|
554
555
|
}
|
|
555
556
|
}, [textValue, optionValue]);
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { useProgressSegments } from './useProgressSegments.js';
|
|
4
4
|
import { cx } from '../../utils/cx.js';
|
|
5
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
5
6
|
// =============================================================================
|
|
6
7
|
// COMPONENT
|
|
7
8
|
// =============================================================================
|
|
@@ -23,7 +24,7 @@ export function Progress(props) {
|
|
|
23
24
|
// Calculate percentages for standard mode
|
|
24
25
|
const pct = Math.max(0, Math.min(100, (value / max) * 100));
|
|
25
26
|
// Dev warning: inside position only supports labelStart (or children)
|
|
26
|
-
if (
|
|
27
|
+
if (isDev() && labelPosition === 'inside' && labelStart && labelEnd) {
|
|
27
28
|
console.warn('Progress: labelPosition="inside" only supports a single label. ' +
|
|
28
29
|
'labelEnd will be ignored. Use labelStart or children for inside content.');
|
|
29
30
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, cloneElement, isValidElement, } from 'react';
|
|
3
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
3
4
|
import { useFloating, offset, flip, shift, size as sizeMiddleware, autoUpdate, FloatingPortal, useDismiss, useInteractions, useListNavigation, useTypeahead, useRole, useClick, } from '@floating-ui/react';
|
|
4
5
|
import { cx } from '../../utils/cx.js';
|
|
5
6
|
import { toKey } from '../../utils/value-key.js';
|
|
@@ -435,7 +436,7 @@ function SelectOptionComponent({ value: optionValue, disabled = false, textValue
|
|
|
435
436
|
const textValue = explicitTextValue ?? (typeof children === 'string' ? children : '');
|
|
436
437
|
// Warn in dev if textValue couldn't be derived
|
|
437
438
|
useEffect(() => {
|
|
438
|
-
if (
|
|
439
|
+
if (isDev() && !textValue) {
|
|
439
440
|
console.warn(`Select.Option with value="${optionValue}" has no textValue. Provide textValue prop when children is not a string.`);
|
|
440
441
|
}
|
|
441
442
|
}, [textValue, optionValue]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
4
5
|
import { StepListContext, useStepListContext } from './StepListContext.js';
|
|
5
6
|
import { StepIndicator } from '../StepIndicator/index.js';
|
|
6
7
|
// =============================================================================
|
|
@@ -9,7 +10,7 @@ import { StepIndicator } from '../StepIndicator/index.js';
|
|
|
9
10
|
function StepListRoot(props) {
|
|
10
11
|
const { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ariaCurrent = 'step', current, onSelect, children, className, } = props;
|
|
11
12
|
// Dev warning for missing nav label
|
|
12
|
-
if (
|
|
13
|
+
if (isDev() && !ariaLabel && !ariaLabelledBy) {
|
|
13
14
|
console.warn('StepList: Navigation landmark requires a label. ' +
|
|
14
15
|
'Provide either `aria-label` or `aria-labelledby` prop for screen reader users.');
|
|
15
16
|
}
|
package/components/Tabs/Tabs.js
CHANGED
|
@@ -104,7 +104,7 @@ function TabsRoot({ variant = 'underline', activationMode = 'auto', value: contr
|
|
|
104
104
|
}, [registryVersion, activeValue, focusedValue, isControlled, onValueChange, getOrderedTabs]);
|
|
105
105
|
// Dev-only: Tab-Panel pairing validation
|
|
106
106
|
useEffect(() => {
|
|
107
|
-
if (
|
|
107
|
+
if (isDev()) {
|
|
108
108
|
// Defer validation to next microtask so all tabs/panels have registered
|
|
109
109
|
queueMicrotask(() => {
|
|
110
110
|
const tabValues = new Set(Array.from(tabsRef.current.keys()));
|
|
@@ -167,7 +167,7 @@ function TabsList({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy,
|
|
|
167
167
|
const { orientation, activationMode, activeValue, focusedValue, getOrderedTabs, onSelect, setFocusedValue, tabsRef, } = useTabsContext();
|
|
168
168
|
// Dev-only: Warn if missing accessible name
|
|
169
169
|
useEffect(() => {
|
|
170
|
-
if (
|
|
170
|
+
if (isDev()) {
|
|
171
171
|
if (!ariaLabel && !ariaLabelledBy) {
|
|
172
172
|
console.warn('Tabs.List: Missing accessible name. Provide aria-label or aria-labelledby.');
|
|
173
173
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React, { useCallback, useEffect, useRef, cloneElement, isValidElement, } from 'react';
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
4
5
|
// =============================================================================
|
|
5
6
|
// Toolbar Component
|
|
6
7
|
// =============================================================================
|
|
@@ -66,7 +67,7 @@ function ToolbarRoot({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledB
|
|
|
66
67
|
const originalTabIndexMapRef = useRef(new WeakMap());
|
|
67
68
|
// Dev warning for missing accessible name
|
|
68
69
|
useEffect(() => {
|
|
69
|
-
if (
|
|
70
|
+
if (isDev() && !ariaLabel && !ariaLabelledBy) {
|
|
70
71
|
console.warn('[Toolbar] aria-label or aria-labelledby is required for accessibility.');
|
|
71
72
|
}
|
|
72
73
|
}, [ariaLabel, ariaLabelledBy]);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useCallback, useEffect, useId, useMemo, useRef, useState, cloneElement, isValidElement, } from 'react';
|
|
3
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
3
4
|
import { useFloating, offset, flip, shift, arrow, autoUpdate, FloatingPortal, FloatingArrow, } from '@floating-ui/react';
|
|
4
5
|
import { cx } from '../../utils/cx.js';
|
|
5
6
|
import { getPortalRootFor } from '../../utils/portal.js';
|
|
@@ -165,7 +166,7 @@ function TooltipContentComponent({ side = 'top', align = 'center', sideOffset =
|
|
|
165
166
|
// Dev warning: tooltips should not contain interactive content (WCAG 1.4.13)
|
|
166
167
|
// Use Popover for interactive overlays instead
|
|
167
168
|
useEffect(() => {
|
|
168
|
-
if (
|
|
169
|
+
if (isDev() && open && refs.floating.current) {
|
|
169
170
|
const interactive = refs.floating.current.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
170
171
|
if (interactive.length > 0) {
|
|
171
172
|
console.warn('[Tooltip] Contains interactive elements which violates WCAG 1.4.13. ' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangible/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Tangible Design System",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./components/index.js",
|
|
@@ -79,20 +79,18 @@
|
|
|
79
79
|
"types": "./components/Dropdown/Dropdown.d.ts"
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
|
-
"dependencies": {
|
|
83
|
-
"@floating-ui/react": "^0.27.16",
|
|
84
|
-
"@tanstack/react-table": "^8.21.3"
|
|
85
|
-
},
|
|
86
82
|
"peerDependencies": {
|
|
83
|
+
"@floating-ui/react": ">=0.27.0",
|
|
84
|
+
"@tanstack/react-table": ">=8.0.0",
|
|
87
85
|
"react": ">=18.0.0",
|
|
88
86
|
"react-dom": ">=18.0.0"
|
|
89
87
|
},
|
|
90
88
|
"peerDependenciesMeta": {
|
|
91
|
-
"react": {
|
|
92
|
-
"optional":
|
|
89
|
+
"@floating-ui/react": {
|
|
90
|
+
"optional": true
|
|
93
91
|
},
|
|
94
|
-
"react-
|
|
95
|
-
"optional":
|
|
92
|
+
"@tanstack/react-table": {
|
|
93
|
+
"optional": true
|
|
96
94
|
}
|
|
97
95
|
},
|
|
98
96
|
"homepage": "https://github.com/tangibleinc/tangible-ui",
|