@tangible/ui 0.0.2 → 0.0.3

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.
@@ -0,0 +1,2 @@
1
+ import type { MoveHandleProps } from './types';
2
+ export declare const MoveHandle: import("react").ForwardRefExoticComponent<MoveHandleProps & import("react").RefAttributes<HTMLElement>>;
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useEffect, useId, useRef } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { isDev } from '../../utils/is-dev.js';
5
+ import { Icon } from '../Icon/index.js';
6
+ // =============================================================================
7
+ // MoveHandle Component
8
+ // =============================================================================
9
+ //
10
+ // Compact reorder control for sortable lists. Shows a drag handle with optional
11
+ // up/down chevron buttons and a position index badge.
12
+ //
13
+ // When `index` is provided, shows the number at rest and swaps to the drag
14
+ // handle icon on hover/focus-within (CSS-driven, no JS state).
15
+ //
16
+ // Forwards ref to the root element (div for full, button for handle).
17
+ //
18
+ // Modes:
19
+ // full — Background panel with arrows, index, lock (default)
20
+ // handle — Bare drag icon button, no chrome
21
+ //
22
+ // CSS token API (never defined, read via fallback):
23
+ // --tui-move-handle-size Override container size
24
+ // --tui-move-handle-icon-size Override icon size
25
+ //
26
+ // =============================================================================
27
+ export const MoveHandle = forwardRef(function MoveHandle({ mode = 'full', size = 'md', index, locked = false, onMoveUp, onMoveDown, canMoveUp = true, canMoveDown = true, labels, dragHandleProps, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, }, ref) {
28
+ // All hooks must be called unconditionally (rules of hooks)
29
+ const innerRef = useRef(null);
30
+ const mergedRef = useCallback((node) => {
31
+ innerRef.current = node;
32
+ if (typeof ref === 'function')
33
+ ref(node);
34
+ else if (ref)
35
+ ref.current = node;
36
+ }, [ref]);
37
+ const hasWarnedRef = useRef(false);
38
+ useEffect(() => {
39
+ if (mode === 'handle')
40
+ return;
41
+ if (hasWarnedRef.current)
42
+ return;
43
+ if (isDev() && !ariaLabel && !ariaLabelledBy) {
44
+ console.warn('MoveHandle: Missing accessible name. Provide aria-label or aria-labelledby.');
45
+ hasWarnedRef.current = true;
46
+ }
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ }, []);
49
+ const lockedDescId = useId();
50
+ // Focus recovery: when a move button becomes disabled after a reorder,
51
+ // redirect focus to the opposite button or drag handle
52
+ useEffect(() => {
53
+ if (mode === 'handle')
54
+ return;
55
+ const group = innerRef.current;
56
+ if (!group)
57
+ return;
58
+ const active = document.activeElement;
59
+ if (active instanceof HTMLButtonElement &&
60
+ active.disabled &&
61
+ group.contains(active)) {
62
+ const fallback = group.querySelector('.tui-move-handle__up:not(:disabled), .tui-move-handle__down:not(:disabled)') ??
63
+ group.querySelector('.tui-move-handle__handle');
64
+ fallback?.focus();
65
+ }
66
+ }, [mode, canMoveUp, canMoveDown]);
67
+ // Drag handle label precedence: dragHandleProps > labels.drag > default
68
+ const resolvedDragLabel = dragHandleProps?.['aria-label'] ?? labels?.drag ?? 'Drag to reorder';
69
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
70
+ const { 'aria-label': _dhLabel, ...restDragProps } = dragHandleProps ?? {};
71
+ // ----- Handle mode: just the drag icon button -----
72
+ // Uses ref directly (not mergedRef) — innerRef is unused for this path.
73
+ // Focus recovery and dev warning effects early-return for handle mode.
74
+ if (mode === 'handle') {
75
+ return (_jsx("button", { ref: ref, type: "button", className: cx('tui-move-handle', 'is-handle', className), "aria-label": resolvedDragLabel, ...restDragProps, children: _jsx(Icon, { name: "system/drag" }) }));
76
+ }
77
+ // ----- Full mode -----
78
+ const hasIndex = index != null;
79
+ const hasArrows = !!(onMoveUp || onMoveDown);
80
+ const resolvedLockedDesc = locked
81
+ ? (labels?.locked ?? 'This item is locked and cannot be reordered')
82
+ : 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", "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: locked ? (_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", "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", "aria-label": labels?.moveDown ?? 'Move down', disabled: locked || !canMoveDown, onClick: onMoveDown, children: _jsx(Icon, { name: "system/chevron-down" }) }))] }));
84
+ });
@@ -0,0 +1,2 @@
1
+ export { MoveHandle } from './MoveHandle';
2
+ export type { MoveHandleLabels, MoveHandleProps, MoveHandleSize, MoveHandleMode } from './types';
@@ -0,0 +1 @@
1
+ export { MoveHandle } from './MoveHandle.js';
@@ -0,0 +1,43 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ export type MoveHandleSize = 'sm' | 'md';
3
+ export type MoveHandleMode = 'full' | 'handle';
4
+ export interface MoveHandleLabels {
5
+ /** Label for the move-up button. Include item name for clear AT context, e.g. "Move Introduction up". Default: "Move up" */
6
+ moveUp?: string;
7
+ /** Label for the move-down button. Include item name for clear AT context, e.g. "Move Introduction down". Default: "Move down" */
8
+ moveDown?: string;
9
+ /**
10
+ * Label for the drag handle button. Default: "Drag to reorder".
11
+ * Consumers add position info if needed. dragHandleProps['aria-label'] takes precedence.
12
+ */
13
+ drag?: string;
14
+ /** Descriptive text announced when locked (via aria-describedby). Default: "This item is locked and cannot be reordered". */
15
+ locked?: string;
16
+ }
17
+ export interface MoveHandleProps {
18
+ /** Structural mode. 'full' (default) shows background panel with arrows/index. 'handle' shows only the bare drag icon button. */
19
+ mode?: MoveHandleMode;
20
+ /** Component scale. sm = 32px, md = 40px. Ignored when mode is 'handle'. */
21
+ size?: MoveHandleSize;
22
+ /** Position index. When provided, shows number at rest, drag handle on hover. */
23
+ index?: number;
24
+ /** When true, shows lock icon and disables all interaction. */
25
+ locked?: boolean;
26
+ /** Called when the "move up" button is clicked. Button not rendered when omitted. */
27
+ onMoveUp?: () => void;
28
+ /** Called when the "move down" button is clicked. Button not rendered when omitted. */
29
+ onMoveDown?: () => void;
30
+ /** When false, disables the move-up button without hiding it. Default: true. */
31
+ canMoveUp?: boolean;
32
+ /** When false, disables the move-down button without hiding it. Default: true. */
33
+ canMoveDown?: boolean;
34
+ /** Override internal button labels for i18n. */
35
+ labels?: MoveHandleLabels;
36
+ /** Props to spread on the drag handle button (e.g., from dnd-kit useSortable). */
37
+ dragHandleProps?: HTMLAttributes<HTMLButtonElement>;
38
+ /** Accessible label for the control group. Required if aria-labelledby is not set. */
39
+ 'aria-label'?: string;
40
+ /** ID of element labelling this group. Required if aria-label is not set. */
41
+ 'aria-labelledby'?: string;
42
+ className?: string;
43
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -18,6 +18,7 @@ export { Select, SelectTrigger, SelectContent, SelectOption, SelectGroup, Select
18
18
  export { Icon } from './Icon';
19
19
  export { IconButton } from './IconButton';
20
20
  export { Modal } from './Modal';
21
+ export { MoveHandle } from './MoveHandle';
21
22
  export { Notice } from './Notice';
22
23
  export { OverlapStack } from './OverlapStack';
23
24
  export { Pager } from './Pager';
@@ -53,6 +54,7 @@ export type { SelectProps, SelectTriggerProps, SelectContentProps, SelectOptionP
53
54
  export type { IconProps } from './Icon';
54
55
  export type { IconButtonProps } from './IconButton';
55
56
  export type { ModalProps } from './Modal';
57
+ export type { MoveHandleLabels, MoveHandleMode, MoveHandleProps, MoveHandleSize } from './MoveHandle';
56
58
  export type { NoticeProps } from './Notice';
57
59
  export type { OverlapStackProps, OverlapStackOverflowProps } from './OverlapStack';
58
60
  export type { PagerProps, PagerMode } from './Pager';
@@ -16,6 +16,7 @@ export { Select, SelectTrigger, SelectContent, SelectOption, SelectGroup, Select
16
16
  export { Icon } from './Icon/index.js';
17
17
  export { IconButton } from './IconButton/index.js';
18
18
  export { Modal } from './Modal/index.js';
19
+ export { MoveHandle } from './MoveHandle/index.js';
19
20
  export { Notice } from './Notice/index.js';
20
21
  export { OverlapStack } from './OverlapStack/index.js';
21
22
  export { Pager } from './Pager/index.js';
package/icons/icons.svg CHANGED
@@ -109,6 +109,7 @@
109
109
  <symbol id="tui-system-cog" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M10.29 4.836A1 1 0 0 1 11.275 4h1.306a1 1 0 0 1 .986.836l.244 1.466c.788.26 1.503.679 2.108 1.218l1.393-.522a1 1 0 0 1 1.217.437l.653 1.13a1 1 0 0 1-.23 1.273l-1.148.944a6 6 0 0 1 0 2.435l1.148.946a1 1 0 0 1 .23 1.272l-.652 1.13a1 1 0 0 1-1.217.437l-1.394-.522c-.605.54-1.32.958-2.108 1.218l-.244 1.466a1 1 0 0 1-.986.836h-1.306a1 1 0 0 1-.987-.836l-.244-1.466a6 6 0 0 1-2.108-1.218l-1.394.522a1 1 0 0 1-1.217-.436l-.652-1.131a1 1 0 0 1 .23-1.272l1.148-.946a6 6 0 0 1 0-2.435l-1.147-.944a1 1 0 0 1-.23-1.272l.652-1.131a1 1 0 0 1 1.217-.437l1.393.522a6 6 0 0 1 2.108-1.218zM14.928 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0" clip-rule="evenodd"/></symbol>
110
110
  <symbol id="tui-system-copy" viewBox="0 0 24 24"><path d="M17 10.25h-7v1.5h7zm-7 2.5h7v1.5h-7zm7 2.5h-7v1.5h7z"/><path fill-rule="evenodd" d="M17 6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h1v1a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-1zm1 2.5a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5H9a.5.5 0 0 1-.5-.5V9a.5.5 0 0 1 .5-.5zm-12 7a.5.5 0 0 1-.5-.5V6a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 .5.5v1H9a2 2 0 0 0-2 2v6.5z" clip-rule="evenodd"/></symbol>
111
111
  <symbol id="tui-system-download" viewBox="0 0 24 24"><path d="M5.5 9a.5.5 0 0 1 .5-.5h3V7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8.75A1.75 1.75 0 0 0 18.25 7H15v1.5h3.25a.25.25 0 0 1 .25.25V18a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5z"/><path d="M12 17.004 7.002 12.56l.996-1.122 3.252 2.89V4h1.5v10.33l3.252-2.89.996 1.12z"/></symbol>
112
+ <symbol id="tui-system-drag" viewBox="0 0 24 24"><path d="M10 4.99976H8V6.99976H10V4.99976Z"/><path d="M10 10.9998H8V12.9998H10V10.9998Z"/><path d="M8 16.9998H10V18.9998H8V16.9998Z"/><path d="M16 4.99976H14V6.99976H16V4.99976Z"/><path d="M14 10.9998H16V12.9998H14V10.9998Z"/><path d="M16 16.9998H14V18.9998H16V16.9998Z"/></symbol>
112
113
  <symbol id="tui-system-duplicate" viewBox="0 0 24 24"><path d="M14.25 12.75H17v1.5h-2.75V17h-1.5v-2.75H10v-1.5h2.75V10h1.5z"/><path fill-rule="evenodd" d="M17 7h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2v-1H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2zm-2-1.5H6a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h1V9a2 2 0 0 1 2-2h6.5V6a.5.5 0 0 0-.5-.5m-6 3h9a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5H9a.5.5 0 0 1-.5-.5V9a.5.5 0 0 1 .5-.5" clip-rule="evenodd"/></symbol>
113
114
  <symbol id="tui-system-edit" viewBox="0 0 24 24"><path d="M19.864 4.304a2 2 0 0 0-2.733.732l-.635 1.1 3.464 2 .636-1.1a2 2 0 0 0-.732-2.732m-.746 5.292-3.464-2-4.903 8.49.062 3.893 3.402-1.892zM4 5.25h9v1.5H4zm7 4H4v1.5h7zm-7 4h5v1.5H4zm5 4H4v1.5h5z"/></symbol>
114
115
  <symbol id="tui-system-ellipsis-h" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M5 11h2v2H5zm6 0h2v2h-2zm6 0h2v2h-2z" clip-rule="evenodd"/></symbol>
@@ -879,6 +879,14 @@
879
879
  "viewBox": "0 0 24 24",
880
880
  "sprite": true
881
881
  },
882
+ "system/drag": {
883
+ "id": "tui-system-drag",
884
+ "set": "system",
885
+ "name": "drag",
886
+ "file": "system/drag.svg",
887
+ "viewBox": "0 0 24 24",
888
+ "sprite": true
889
+ },
882
890
  "system/duplicate": {
883
891
  "id": "tui-system-duplicate",
884
892
  "set": "system",
@@ -113,6 +113,7 @@ export type IconName =
113
113
  | 'system/cog'
114
114
  | 'system/copy'
115
115
  | 'system/download'
116
+ | 'system/drag'
116
117
  | 'system/duplicate'
117
118
  | 'system/edit'
118
119
  | 'system/ellipsis-h'
@@ -269,6 +270,7 @@ export declare const iconRegistry: {
269
270
  'system/cog': IconComponent;
270
271
  'system/copy': IconComponent;
271
272
  'system/download': IconComponent;
273
+ 'system/drag': IconComponent;
272
274
  'system/duplicate': IconComponent;
273
275
  'system/edit': IconComponent;
274
276
  'system/ellipsis-h': IconComponent;
package/icons/registry.js CHANGED
@@ -115,6 +115,7 @@ export const iconRegistry = {
115
115
  'system/cog': systemIcons['cog'],
116
116
  'system/copy': systemIcons['copy'],
117
117
  'system/download': systemIcons['download'],
118
+ 'system/drag': systemIcons['drag'],
118
119
  'system/duplicate': systemIcons['duplicate'],
119
120
  'system/edit': systemIcons['edit'],
120
121
  'system/ellipsis-h': systemIcons['ellipsis-h'],
@@ -33,6 +33,7 @@ export declare const Close: IconComponent;
33
33
  export declare const Cog: IconComponent;
34
34
  export declare const Copy: IconComponent;
35
35
  export declare const Download: IconComponent;
36
+ export declare const Drag: IconComponent;
36
37
  export declare const Duplicate: IconComponent;
37
38
  export declare const Edit: IconComponent;
38
39
  export declare const EllipsisH: IconComponent;
@@ -110,6 +111,7 @@ export declare const systemIcons: {
110
111
  'cog': IconComponent;
111
112
  'copy': IconComponent;
112
113
  'download': IconComponent;
114
+ 'drag': IconComponent;
113
115
  'duplicate': IconComponent;
114
116
  'edit': IconComponent;
115
117
  'ellipsis-h': IconComponent;
@@ -310,6 +310,16 @@ export function Download(props) {
310
310
  }, props), [_c("path", {d:"M5.5 9a.5.5 0 0 1 .5-.5h3V7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8.75A1.75 1.75 0 0 0 18.25 7H15v1.5h3.25a.25.25 0 0 1 .25.25V18a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5z"}), _c("path", {d:"M12 17.004 7.002 12.56l.996-1.122 3.252 2.89V4h1.5v10.33l3.252-2.89.996 1.12z"})]);
311
311
  }
312
312
 
313
+ export function Drag(props) {
314
+ return _c("svg", Object.assign({
315
+ xmlns: "http://www.w3.org/2000/svg",
316
+ viewBox: "0 0 24 24",
317
+ width: "1em",
318
+ height: "1em",
319
+ fill: "currentColor"
320
+ }, props), [_c("path", {d:"M10 4.99976H8V6.99976H10V4.99976Z"}), _c("path", {d:"M10 10.9998H8V12.9998H10V10.9998Z"}), _c("path", {d:"M8 16.9998H10V18.9998H8V16.9998Z"}), _c("path", {d:"M16 4.99976H14V6.99976H16V4.99976Z"}), _c("path", {d:"M14 10.9998H16V12.9998H14V10.9998Z"}), _c("path", {d:"M16 16.9998H14V18.9998H16V16.9998Z"})]);
321
+ }
322
+
313
323
  export function Duplicate(props) {
314
324
  return _c("svg", Object.assign({
315
325
  xmlns: "http://www.w3.org/2000/svg",
@@ -782,6 +792,7 @@ export const systemIcons = {
782
792
  'cog': Cog,
783
793
  'copy': Copy,
784
794
  'download': Download,
795
+ 'drag': Drag,
785
796
  'duplicate': Duplicate,
786
797
  'edit': Edit,
787
798
  'ellipsis-h': EllipsisH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangible/ui",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Tangible Design System",
5
5
  "type": "module",
6
6
  "main": "./components/index.js",