@tangible/ui 0.0.1 → 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.
Files changed (135) hide show
  1. package/components/Card/Card.d.ts +1 -0
  2. package/components/Card/Card.js +17 -20
  3. package/components/Checkbox/Checkbox.d.ts +9 -0
  4. package/components/Checkbox/Checkbox.js +92 -0
  5. package/components/Checkbox/index.d.ts +2 -0
  6. package/components/Checkbox/index.js +1 -0
  7. package/components/Checkbox/types.d.ts +10 -0
  8. package/components/Checkbox/types.js +1 -0
  9. package/components/Chip/Chip.d.ts +4 -1
  10. package/components/Chip/Chip.js +32 -7
  11. package/components/ChipGroup/ChipGroup.d.ts +5 -0
  12. package/components/ChipGroup/ChipGroup.js +68 -0
  13. package/components/ChipGroup/ChipGroupContext.d.ts +3 -0
  14. package/components/ChipGroup/ChipGroupContext.js +5 -0
  15. package/components/ChipGroup/index.d.ts +3 -0
  16. package/components/ChipGroup/index.js +2 -0
  17. package/components/ChipGroup/types.d.ts +36 -0
  18. package/components/ChipGroup/types.js +1 -0
  19. package/components/Chips/Chips.d.ts +2 -0
  20. package/components/Chips/Chips.js +1 -1
  21. package/components/Combobox/Combobox.d.ts +33 -0
  22. package/components/Combobox/Combobox.js +466 -0
  23. package/components/Combobox/ComboboxContext.d.ts +8 -0
  24. package/components/Combobox/ComboboxContext.js +36 -0
  25. package/components/Combobox/index.d.ts +2 -0
  26. package/components/Combobox/index.js +1 -0
  27. package/components/Combobox/types.d.ts +204 -0
  28. package/components/Combobox/types.js +1 -0
  29. package/components/Dropdown/Dropdown.js +2 -1
  30. package/components/Field/Field.d.ts +39 -0
  31. package/components/Field/Field.js +92 -0
  32. package/components/Field/FieldContext.d.ts +16 -0
  33. package/components/Field/FieldContext.js +10 -0
  34. package/components/Field/index.d.ts +2 -0
  35. package/components/Field/index.js +1 -0
  36. package/components/Modal/Modal.d.ts +4 -4
  37. package/components/Modal/Modal.js +14 -12
  38. package/components/MoveHandle/MoveHandle.d.ts +2 -0
  39. package/components/MoveHandle/MoveHandle.js +84 -0
  40. package/components/MoveHandle/index.d.ts +2 -0
  41. package/components/MoveHandle/index.js +1 -0
  42. package/components/MoveHandle/types.d.ts +43 -0
  43. package/components/MoveHandle/types.js +1 -0
  44. package/components/MultiSelect/MultiSelect.d.ts +39 -0
  45. package/components/MultiSelect/MultiSelect.js +623 -0
  46. package/components/MultiSelect/MultiSelectContext.d.ts +20 -0
  47. package/components/MultiSelect/MultiSelectContext.js +56 -0
  48. package/components/MultiSelect/index.d.ts +2 -0
  49. package/components/MultiSelect/index.js +1 -0
  50. package/components/MultiSelect/types.d.ts +218 -0
  51. package/components/MultiSelect/types.js +3 -0
  52. package/components/Notice/Notice.d.ts +1 -1
  53. package/components/Notice/Notice.js +1 -1
  54. package/components/Progress/Progress.js +1 -1
  55. package/components/Progress/types.d.ts +7 -7
  56. package/components/Radio/Radio.d.ts +2 -0
  57. package/components/Radio/Radio.js +50 -0
  58. package/components/Radio/RadioGroup.d.ts +2 -0
  59. package/components/Radio/RadioGroup.js +54 -0
  60. package/components/Radio/RadioGroupContext.d.ts +3 -0
  61. package/components/Radio/RadioGroupContext.js +9 -0
  62. package/components/Radio/index.d.ts +8 -0
  63. package/components/Radio/index.js +6 -0
  64. package/components/Radio/types.d.ts +32 -0
  65. package/components/Radio/types.js +1 -0
  66. package/components/Rating/Rating.d.ts +5 -5
  67. package/components/Rating/Rating.js +2 -2
  68. package/components/SegmentedControl/SegmentedControl.js +20 -104
  69. package/components/SegmentedControl/types.d.ts +4 -8
  70. package/components/Select/Select.d.ts +39 -0
  71. package/components/Select/Select.js +497 -0
  72. package/components/Select/SelectContext.d.ts +20 -0
  73. package/components/Select/SelectContext.js +56 -0
  74. package/components/Select/index.d.ts +3 -0
  75. package/components/Select/index.js +1 -0
  76. package/components/Select/types.d.ts +216 -0
  77. package/components/Select/types.js +11 -0
  78. package/components/Sidebar/Sidebar.js +12 -12
  79. package/components/Sidebar/types.d.ts +5 -5
  80. package/components/StepIndicator/StepIndicator.js +1 -1
  81. package/components/StepList/StepList.js +2 -2
  82. package/components/StepList/types.d.ts +4 -4
  83. package/components/Switch/Switch.d.ts +9 -0
  84. package/components/Switch/Switch.js +91 -0
  85. package/components/Switch/index.d.ts +2 -0
  86. package/components/Switch/index.js +1 -0
  87. package/components/Switch/types.d.ts +11 -0
  88. package/components/Switch/types.js +1 -0
  89. package/components/TextInput/TextInput.d.ts +8 -0
  90. package/components/TextInput/TextInput.js +25 -0
  91. package/components/TextInput/index.d.ts +2 -0
  92. package/components/TextInput/index.js +1 -0
  93. package/components/TextInput/types.d.ts +32 -0
  94. package/components/TextInput/types.js +1 -0
  95. package/components/Textarea/Textarea.d.ts +6 -0
  96. package/components/Textarea/Textarea.js +49 -0
  97. package/components/Textarea/index.d.ts +2 -0
  98. package/components/Textarea/index.js +1 -0
  99. package/components/Textarea/types.d.ts +25 -0
  100. package/components/Textarea/types.js +1 -0
  101. package/components/index.d.ts +22 -0
  102. package/components/index.js +11 -0
  103. package/icons/icons.svg +2 -0
  104. package/icons/manifest.json +16 -0
  105. package/icons/registry.d.ts +4 -0
  106. package/icons/registry.js +2 -0
  107. package/icons/system/index.d.ts +4 -0
  108. package/icons/system/index.js +22 -0
  109. package/package.json +1 -1
  110. package/styles/all.css +1 -1
  111. package/styles/all.expanded.css +1838 -136
  112. package/styles/all.expanded.unlayered.css +1838 -136
  113. package/styles/all.unlayered.css +1 -1
  114. package/styles/components/_bundle.scss +22 -0
  115. package/styles/components/input/index.scss +5 -20
  116. package/styles/index.scss +21 -0
  117. package/styles/system/_control.scss +49 -0
  118. package/styles/system/_tokens.scss +124 -2
  119. package/styles/system/index.scss +2 -1
  120. package/styles/utilities/_index.scss +50 -0
  121. package/tui-manifest.json +907 -112
  122. package/utils/compose-events.d.ts +15 -0
  123. package/utils/compose-events.js +27 -0
  124. package/utils/hash.d.ts +15 -0
  125. package/utils/hash.js +32 -0
  126. package/utils/index.d.ts +3 -0
  127. package/utils/index.js +6 -0
  128. package/utils/is-dev.d.ts +5 -0
  129. package/utils/is-dev.js +7 -0
  130. package/utils/use-controllable-state.d.ts +19 -0
  131. package/utils/use-controllable-state.js +59 -0
  132. package/utils/use-roving-group.d.ts +33 -0
  133. package/utils/use-roving-group.js +123 -0
  134. package/utils/value-key.d.ts +16 -0
  135. package/utils/value-key.js +14 -0
@@ -11,6 +11,7 @@ type CommonProps = {
11
11
  };
12
12
  export type CardRootProps<TAs extends RootAs = 'article'> = CommonProps & {
13
13
  as?: TAs;
14
+ /** @deprecated Use Card.Link for interactive cards. onClick without Card.Link has no keyboard/screen-reader semantics. */
14
15
  onClick?: React.MouseEventHandler<HTMLElement>;
15
16
  children?: React.ReactNode;
16
17
  };
@@ -1,29 +1,26 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React, { forwardRef, useCallback } from 'react';
2
+ import React, { forwardRef, useRef, useEffect } from 'react';
3
3
  import { cx } from '../../utils/cx.js';
4
+ import { isDev } from '../../utils/is-dev.js';
4
5
  export const Card = forwardRef(function Card({ as = 'article', inline, elevated, interactive, disabled, className, style, onClick, children, ...rest }, ref) {
5
6
  const Tag = as;
6
- // Determine if this card is clickable (needs button semantics)
7
- const isClickable = (interactive || onClick) && !disabled;
8
- // Keyboard handler for interactive cards
9
- const handleKeyDown = useCallback((e) => {
10
- if (!isClickable || !onClick)
11
- return;
12
- if (e.key === 'Enter' || e.key === ' ') {
13
- e.preventDefault();
14
- onClick(e);
7
+ const hasCardLinkChild = React.Children.toArray(children).some((child) => {
8
+ if (!React.isValidElement(child))
9
+ return false;
10
+ const type = child.type;
11
+ return child.type === CardLink || type.displayName === 'Card.Link';
12
+ });
13
+ // Dev warning: suggest Card.Link for interactive cards
14
+ const warnedRef = useRef(false);
15
+ useEffect(() => {
16
+ if (isDev() && !warnedRef.current && (interactive || onClick) && !hasCardLinkChild) {
17
+ warnedRef.current = true;
18
+ console.warn('[TUI Card] Interactive cards should use <Card.Link> for accessible click targets. ' +
19
+ '`interactive` and `onClick` provide visual hover styles but no keyboard/screen-reader semantics.');
15
20
  }
16
- }, [isClickable, onClick]);
21
+ }, [interactive, onClick, hasCardLinkChild]);
17
22
  const classes = cx('tui-card', inline && 'is-layout-inline', elevated && 'is-style-elevated', interactive && 'has-interaction', className);
18
- // Button semantics for clickable cards
19
- const buttonProps = isClickable
20
- ? {
21
- role: 'button',
22
- tabIndex: 0,
23
- onKeyDown: handleKeyDown,
24
- }
25
- : {};
26
- return (_jsx(Tag, { ref: ref, className: classes, style: style, "aria-disabled": disabled || undefined, onClick: disabled ? undefined : onClick, ...buttonProps, ...rest, children: _jsx("div", { className: "tui-card__inner", children: children }) }));
23
+ return (_jsx(Tag, { ref: ref, className: classes, style: style, "aria-disabled": disabled || undefined, onClick: disabled ? undefined : onClick, ...rest, children: _jsx("div", { className: "tui-card__inner", children: children }) }));
27
24
  });
28
25
  function CardHead({ className, children, ...rest }) {
29
26
  return (_jsx("div", { className: cx('tui-card__head', className), ...rest, children: children }));
@@ -0,0 +1,9 @@
1
+ export declare const Checkbox: import("react").ForwardRefExoticComponent<Omit<import("react").InputHTMLAttributes<HTMLInputElement>, "role" | "type" | "defaultChecked" | "onChange" | "checked"> & {
2
+ checked?: boolean;
3
+ defaultChecked?: boolean;
4
+ onCheckedChange?: (checked: boolean) => void;
5
+ indeterminate?: boolean;
6
+ label?: import("react").ReactNode;
7
+ disabled?: boolean;
8
+ className?: string;
9
+ } & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,92 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useRef } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { composeRefs } from '../../utils/compose-refs.js';
5
+ import { useControllableState } from '../../utils/use-controllable-state.js';
6
+ import { isDev } from '../../utils/is-dev.js';
7
+ // =============================================================================
8
+ // Checkbox Component
9
+ // =============================================================================
10
+ //
11
+ // Native <input type="checkbox"> with accent-color, reusing tui-inline-choice CSS.
12
+ //
13
+ // Bare (no label): returns <input> directly for Field.Control cloneElement.
14
+ // With label: wraps in <label class="tui-inline-choice">.
15
+ //
16
+ // CSS token API (inherited from input styles):
17
+ // --tui-input-accent Accent color for checked state
18
+ //
19
+ // =============================================================================
20
+ // Props that should route to the <input>, not the wrapper label
21
+ const INPUT_PROPS = new Set([
22
+ 'id',
23
+ 'name',
24
+ 'value',
25
+ 'aria-describedby',
26
+ 'aria-invalid',
27
+ 'aria-required',
28
+ 'aria-label',
29
+ 'aria-labelledby',
30
+ 'form',
31
+ 'required',
32
+ 'tabIndex',
33
+ 'autoFocus',
34
+ 'onFocus',
35
+ 'onBlur',
36
+ ]);
37
+ export const Checkbox = forwardRef(function Checkbox({ checked: controlledChecked, defaultChecked = false, onCheckedChange, indeterminate = false, label, disabled, className, ...rest }, externalRef) {
38
+ const internalRef = useRef(null);
39
+ const [checked, setChecked] = useControllableState({
40
+ value: controlledChecked,
41
+ defaultValue: defaultChecked,
42
+ onChange: onCheckedChange,
43
+ });
44
+ // Sync indeterminate DOM property (not available as HTML attribute)
45
+ useEffect(() => {
46
+ if (internalRef.current) {
47
+ internalRef.current.indeterminate = indeterminate;
48
+ }
49
+ }, [indeterminate]);
50
+ // Dev-only: warn if bare checkbox has no accessible name (fire once)
51
+ const hasWarnedRef = useRef(false);
52
+ useEffect(() => {
53
+ if (hasWarnedRef.current)
54
+ return;
55
+ if (isDev() && !label) {
56
+ const ariaLabel = rest['aria-label'];
57
+ const ariaLabelledBy = rest['aria-labelledby'];
58
+ // Check explicit ARIA props, then fall back to native <label> association
59
+ // (e.g. Field.Label htmlFor wiring via Field.Control id injection)
60
+ const hasName = (typeof ariaLabel === 'string' && ariaLabel.trim() !== '') ||
61
+ (typeof ariaLabelledBy === 'string' && ariaLabelledBy.trim() !== '') ||
62
+ (internalRef.current?.labels && internalRef.current.labels.length > 0);
63
+ if (!hasName) {
64
+ console.warn('Checkbox: Missing accessible name. Provide label prop, aria-label, or aria-labelledby.');
65
+ hasWarnedRef.current = true;
66
+ }
67
+ }
68
+ // eslint-disable-next-line react-hooks/exhaustive-deps
69
+ }, []);
70
+ const handleChange = () => {
71
+ // Clicking an indeterminate checkbox produces checked=true (native behaviour)
72
+ setChecked((prev) => !prev);
73
+ };
74
+ const isChecked = checked ?? false;
75
+ // Bare: no label — Field.Control can inject id/aria-* directly
76
+ if (!label) {
77
+ return (_jsx("input", { ref: composeRefs(internalRef, externalRef), type: "checkbox", checked: isChecked, disabled: disabled, className: className, "data-indeterminate": indeterminate || undefined, "aria-checked": indeterminate ? 'mixed' : undefined, onChange: handleChange, ...rest }));
78
+ }
79
+ // Split rest props: some go on input, some on wrapper
80
+ const inputProps = {};
81
+ const wrapperProps = {};
82
+ for (const [key, val] of Object.entries(rest)) {
83
+ if (INPUT_PROPS.has(key)) {
84
+ inputProps[key] = val;
85
+ }
86
+ else {
87
+ wrapperProps[key] = val;
88
+ }
89
+ }
90
+ // With label: wrap in tui-inline-choice
91
+ return (_jsxs("label", { className: cx('tui-inline-choice', disabled && 'is-disabled', className), ...wrapperProps, children: [_jsx("input", { ref: composeRefs(internalRef, externalRef), type: "checkbox", checked: isChecked, disabled: disabled, "data-indeterminate": indeterminate || undefined, "aria-checked": indeterminate ? 'mixed' : undefined, onChange: handleChange, ...inputProps }), _jsx("span", { children: label })] }));
92
+ });
@@ -0,0 +1,2 @@
1
+ export { Checkbox } from './Checkbox';
2
+ export type { CheckboxProps } from './types';
@@ -0,0 +1 @@
1
+ export { Checkbox } from './Checkbox.js';
@@ -0,0 +1,10 @@
1
+ import type { InputHTMLAttributes, ReactNode } from 'react';
2
+ export type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'checked' | 'defaultChecked' | 'onChange' | 'role'> & {
3
+ checked?: boolean;
4
+ defaultChecked?: boolean;
5
+ onCheckedChange?: (checked: boolean) => void;
6
+ indeterminate?: boolean;
7
+ label?: ReactNode;
8
+ disabled?: boolean;
9
+ className?: string;
10
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import type { OptionValue } from '../../utils/value-key';
2
3
  import type { SizeCompact, Theme as ThemeFull } from '../../types';
3
4
  type Size = SizeCompact;
4
5
  type Theme = ThemeFull;
@@ -19,6 +20,8 @@ export type ChipProps = {
19
20
  leftIcon?: React.ReactNode;
20
21
  rightIcon?: React.ReactNode;
21
22
  onClick?: React.MouseEventHandler<HTMLElement>;
23
+ /** When inside ChipGroup, identifies this chip for selection tracking */
24
+ value?: OptionValue;
22
25
  } & Omit<React.HTMLAttributes<HTMLSpanElement>, 'onClick'>;
23
- export declare function Chip({ as, href, target, rel, children, size, theme, variant, selected, disabled, interactive, className, leftIcon, rightIcon, onClick, ...rest }: ChipProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare function Chip({ as, href, target, rel, children, size, theme, variant, selected: selectedProp, disabled: disabledProp, interactive, className, leftIcon, rightIcon, onClick: onClickProp, value, ...rest }: ChipProps): import("react/jsx-runtime").JSX.Element;
24
27
  export {};
@@ -1,11 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
- import { useCallback } from 'react';
3
+ import { useCallback, useEffect } from 'react';
4
4
  import { cx } from '../../utils/cx.js';
5
- export function Chip({ as = 'span', href, target, rel, children, size = 'md', theme = 'secondary', variant = 'default', selected, disabled, interactive, className, leftIcon, rightIcon, onClick, ...rest }) {
5
+ import { toKey } from '../../utils/value-key.js';
6
+ import { isDev } from '../../utils/is-dev.js';
7
+ import { useChipGroupContext } from '../ChipGroup/ChipGroupContext.js';
8
+ export function Chip({ as = 'span', href, target, rel, children, size = 'md', theme = 'secondary', variant = 'default', selected: selectedProp, disabled: disabledProp, interactive, className, leftIcon, rightIcon, onClick: onClickProp, value, ...rest }) {
9
+ const groupContext = useChipGroupContext();
10
+ // Dev warning: inside ChipGroup without value
11
+ useEffect(() => {
12
+ if (isDev() && groupContext && value === undefined) {
13
+ console.warn('Chip: Inside a ChipGroup but missing `value` prop. This chip will not be managed by the group.');
14
+ }
15
+ }, [groupContext, value]);
16
+ // Derive from group context when applicable
17
+ const isManaged = groupContext !== null && value !== undefined;
18
+ const selected = isManaged ? groupContext.selectedValues.has(toKey(value)) : selectedProp;
19
+ const disabled = (isManaged ? groupContext.disabled : false) || disabledProp;
20
+ const managedClick = useCallback(() => {
21
+ if (groupContext && value !== undefined) {
22
+ groupContext.onSelect(value);
23
+ }
24
+ }, [groupContext, value]);
25
+ const onClick = isManaged ? managedClick : onClickProp;
6
26
  const Tag = (as === 'a' ? 'a' : as);
7
27
  // Determine if this chip is clickable (needs button semantics)
8
- const isClickable = (interactive || onClick) && !disabled;
28
+ const isClickable = (interactive || onClick || isManaged) && !disabled;
9
29
  // Keyboard handler for interactive chips
10
30
  const handleKeyDown = useCallback((e) => {
11
31
  if (!isClickable || !onClick)
@@ -15,7 +35,7 @@ export function Chip({ as = 'span', href, target, rel, children, size = 'md', th
15
35
  onClick(e);
16
36
  }
17
37
  }, [isClickable, onClick]);
18
- const classes = cx('tui-chip', size && `is-size-${size}`, theme && `is-theme-${theme}`, variant !== 'default' && `is-style-${variant}`, selected && 'is-selected', interactive && 'is-interactive', className);
38
+ const classes = cx('tui-chip', size && `is-size-${size}`, theme && `is-theme-${theme}`, variant !== 'default' && `is-style-${variant}`, selected && 'is-selected', (interactive || isManaged) && 'is-interactive', className);
19
39
  const anchorProps = as === 'a'
20
40
  ? {
21
41
  href: disabled ? undefined : href ?? '#',
@@ -25,12 +45,17 @@ export function Chip({ as = 'span', href, target, rel, children, size = 'md', th
25
45
  tabIndex: disabled ? -1 : undefined,
26
46
  }
27
47
  : { 'aria-disabled': disabled || undefined };
28
- // Button semantics for non-anchor clickable chips
29
- const buttonProps = as !== 'a' && isClickable
48
+ // Button semantics for clickable chips.
49
+ // Non-anchor clickable chips always need role="button".
50
+ // Managed anchor chips also get role="button" — toggle semantics
51
+ // take priority over link semantics inside a ChipGroup.
52
+ const needsButtonRole = isClickable && (as !== 'a' || isManaged);
53
+ const buttonProps = needsButtonRole
30
54
  ? {
31
55
  role: 'button',
32
- tabIndex: 0,
56
+ tabIndex: as !== 'a' ? 0 : undefined, // anchors are natively focusable
33
57
  onKeyDown: handleKeyDown,
58
+ 'aria-pressed': isManaged ? (selected ?? false) : undefined,
34
59
  }
35
60
  : {};
36
61
  return (_jsxs(Tag, { className: classes, ...anchorProps, ...buttonProps, onClick: disabled ? undefined : onClick, ...rest, children: [leftIcon && leftIcon, _jsx("span", { className: "tui-chip__text", children: children }), rightIcon && rightIcon] }));
@@ -0,0 +1,5 @@
1
+ import type { ChipGroupProps } from './types';
2
+ export declare function ChipGroup(props: ChipGroupProps): import("react/jsx-runtime").JSX.Element;
3
+ export declare namespace ChipGroup {
4
+ var displayName: string;
5
+ }
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { useControllableState } from '../../utils/use-controllable-state.js';
5
+ import { toKey } from '../../utils/value-key.js';
6
+ import { ChipGroupContext } from './ChipGroupContext.js';
7
+ import { isDev } from '../../utils/is-dev.js';
8
+ // =============================================================================
9
+ // ChipGroup Component
10
+ // =============================================================================
11
+ //
12
+ // Selection group for Chip components. Provides context to managed Chips.
13
+ // Chips with a `value` prop derive selected/onClick from context.
14
+ //
15
+ // Single mode: one selection at a time (toggleable — clicking selected deselects)
16
+ // Multiple mode: toggle individual chips on/off
17
+ //
18
+ // =============================================================================
19
+ export function ChipGroup(props) {
20
+ const { multiple = false, disabled = false, density = 'sm', direction = 'inline', alignment, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, } = props;
21
+ // --- Single mode ---
22
+ // isControlled override: 'value' in props detects explicit value={undefined}
23
+ // (deselection) vs prop not passed at all (uncontrolled).
24
+ const [singleValue, setSingleValue] = useControllableState({
25
+ value: !multiple ? props.value : undefined,
26
+ defaultValue: !multiple ? props.defaultValue : undefined,
27
+ onChange: !multiple ? props.onValueChange : undefined,
28
+ isControlled: !multiple && 'value' in props,
29
+ });
30
+ // --- Multiple mode ---
31
+ const [multiValue, setMultiValue] = useControllableState({
32
+ value: multiple ? props.value : undefined,
33
+ defaultValue: multiple ? (props.defaultValue ?? []) : undefined,
34
+ onChange: multiple ? props.onValueChange : undefined,
35
+ isControlled: multiple && 'value' in props,
36
+ });
37
+ // Build Set of toKey'd values for efficient lookup in Chip
38
+ const selectedValues = useMemo(() => {
39
+ if (multiple) {
40
+ return new Set((multiValue ?? []).map(toKey));
41
+ }
42
+ return singleValue !== undefined ? new Set([toKey(singleValue)]) : new Set();
43
+ }, [multiple, singleValue, multiValue]);
44
+ const onSelect = useCallback((value) => {
45
+ if (multiple) {
46
+ setMultiValue((prev) => {
47
+ const current = prev ?? [];
48
+ const key = toKey(value);
49
+ return current.some((v) => toKey(v) === key)
50
+ ? current.filter((v) => toKey(v) !== key)
51
+ : [...current, value];
52
+ });
53
+ }
54
+ else {
55
+ // Toggle: clicking selected chip deselects
56
+ setSingleValue((prev) => prev !== undefined && toKey(prev) === toKey(value) ? undefined : value);
57
+ }
58
+ }, [multiple, setSingleValue, setMultiValue]);
59
+ const contextValue = useMemo(() => ({ selectedValues, multiple, disabled, onSelect }), [selectedValues, multiple, disabled, onSelect]);
60
+ // Dev-only: warn if group has no accessible name
61
+ useEffect(() => {
62
+ if (isDev() && !ariaLabel && !ariaLabelledBy) {
63
+ console.warn('ChipGroup: Missing accessible name. Provide aria-label or aria-labelledby.');
64
+ }
65
+ }, [ariaLabel, ariaLabelledBy]);
66
+ return (_jsx(ChipGroupContext.Provider, { value: contextValue, children: _jsx("div", { role: "group", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: cx('tui-chip-group', `is-density-${density}`, direction === 'stack' && 'is-direction-stack', alignment && `is-align-${alignment}`, className), children: children }) }));
67
+ }
68
+ ChipGroup.displayName = 'ChipGroup';
@@ -0,0 +1,3 @@
1
+ import type { ChipGroupContextValue } from './types';
2
+ export declare const ChipGroupContext: import("react").Context<ChipGroupContextValue | null>;
3
+ export declare function useChipGroupContext(): ChipGroupContextValue | null;
@@ -0,0 +1,5 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const ChipGroupContext = createContext(null);
3
+ export function useChipGroupContext() {
4
+ return useContext(ChipGroupContext);
5
+ }
@@ -0,0 +1,3 @@
1
+ export { ChipGroup } from './ChipGroup';
2
+ export { useChipGroupContext } from './ChipGroupContext';
3
+ export type { ChipGroupProps, ChipGroupSingleProps, ChipGroupMultipleProps } from './types';
@@ -0,0 +1,2 @@
1
+ export { ChipGroup } from './ChipGroup.js';
2
+ export { useChipGroupContext } from './ChipGroupContext.js';
@@ -0,0 +1,36 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { OptionValue } from '../../utils/value-key';
3
+ type ChipGroupBaseProps = {
4
+ disabled?: boolean;
5
+ /** Gap between chips. Default 'sm'. */
6
+ density?: 'xs' | 'sm' | 'md';
7
+ /** Layout direction. Default 'inline' (row wrap). */
8
+ direction?: 'inline' | 'stack';
9
+ /** Alignment along main axis. */
10
+ alignment?: 'start' | 'center' | 'end';
11
+ 'aria-label'?: string;
12
+ 'aria-labelledby'?: string;
13
+ className?: string;
14
+ children: ReactNode;
15
+ };
16
+ export type ChipGroupSingleProps = ChipGroupBaseProps & {
17
+ multiple?: false;
18
+ value?: OptionValue;
19
+ defaultValue?: OptionValue;
20
+ onValueChange?: (value: OptionValue | undefined) => void;
21
+ };
22
+ export type ChipGroupMultipleProps = ChipGroupBaseProps & {
23
+ multiple: true;
24
+ value?: OptionValue[];
25
+ defaultValue?: OptionValue[];
26
+ onValueChange?: (value: OptionValue[]) => void;
27
+ };
28
+ export type ChipGroupProps = ChipGroupSingleProps | ChipGroupMultipleProps;
29
+ export type ChipGroupContextValue = {
30
+ /** Set of toKey'd values for efficient lookup */
31
+ selectedValues: Set<string>;
32
+ multiple: boolean;
33
+ disabled: boolean;
34
+ onSelect: (value: OptionValue) => void;
35
+ };
36
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -4,6 +4,8 @@ type Size = SizeCompact;
4
4
  type Theme = ThemeFull;
5
5
  type Variant = 'default' | 'outline' | 'ghost' | 'solid' | 'flush';
6
6
  export type ChipOption = {
7
+ /** Stable identifier used as React key. Falls back to href, then label string. */
8
+ id?: string;
7
9
  label: React.ReactNode;
8
10
  as?: 'a' | 'span' | 'div';
9
11
  href?: string;
@@ -17,5 +17,5 @@ export function Chips({ name, options, density = 'sm', direction = 'inline', ali
17
17
  alignItems: !isInline && alignment ? alignment : undefined,
18
18
  marginBlockEnd: 'var(--tui-spacing-sm)',
19
19
  };
20
- return (_jsx("div", { role: role, className: className, "data-chips-name": name, style: style, children: options.map((o) => (_jsx(Chip, { as: o.as ?? (o.href ? 'a' : 'span'), href: o.href, target: o.target, rel: o.rel, size: o.size, theme: o.theme, variant: o.variant, disabled: o.disabled, className: o.className, leftIcon: o.leftIcon, rightIcon: o.rightIcon, children: o.label }))) }));
20
+ return (_jsx("div", { role: role, className: className, "data-chips-name": name, style: style, children: options.map((o, index) => (_jsx(Chip, { as: o.as ?? (o.href ? 'a' : 'span'), href: o.href, target: o.target, rel: o.rel, size: o.size, theme: o.theme, variant: o.variant, disabled: o.disabled, className: o.className, leftIcon: o.leftIcon, rightIcon: o.rightIcon, children: o.label }, o.id ?? o.href ?? (typeof o.label === 'string' ? o.label : `chip-${index}`)))) }));
21
21
  }
@@ -0,0 +1,33 @@
1
+ import type { ComboboxProps, ComboboxContentProps, ComboboxOptionProps, ComboboxGroupProps, ComboboxLabelProps } from './types';
2
+ declare function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inputValue: controlledInputValue, onInputChange, open: controlledOpen, defaultOpen, onOpenChange, disabled, placeholder, size, openOnFocus, filterMode, onQueryChange, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, inputClassName, children, }: ComboboxProps): import("react/jsx-runtime").JSX.Element;
3
+ declare namespace ComboboxRoot {
4
+ var displayName: string;
5
+ }
6
+ declare function ComboboxContentComponent({ className, children }: ComboboxContentProps): import("react/jsx-runtime").JSX.Element;
7
+ declare namespace ComboboxContentComponent {
8
+ var displayName: string;
9
+ }
10
+ declare function ComboboxOptionComponent({ value: optionValue, disabled, textValue: explicitTextValue, className, children, }: ComboboxOptionProps): import("react/jsx-runtime").JSX.Element;
11
+ declare namespace ComboboxOptionComponent {
12
+ var displayName: string;
13
+ }
14
+ declare function ComboboxGroupComponent({ className, children }: ComboboxGroupProps): import("react/jsx-runtime").JSX.Element;
15
+ declare namespace ComboboxGroupComponent {
16
+ var displayName: string;
17
+ }
18
+ declare function ComboboxLabelComponent({ className, children }: ComboboxLabelProps): import("react/jsx-runtime").JSX.Element;
19
+ declare namespace ComboboxLabelComponent {
20
+ var displayName: string;
21
+ }
22
+ type ComboboxCompound = typeof ComboboxRoot & {
23
+ Content: typeof ComboboxContentComponent;
24
+ Option: typeof ComboboxOptionComponent;
25
+ Group: typeof ComboboxGroupComponent;
26
+ Label: typeof ComboboxLabelComponent;
27
+ };
28
+ export declare const Combobox: ComboboxCompound;
29
+ export declare const ComboboxContent: typeof ComboboxContentComponent;
30
+ export declare const ComboboxOption: typeof ComboboxOptionComponent;
31
+ export declare const ComboboxGroup: typeof ComboboxGroupComponent;
32
+ export declare const ComboboxLabel: typeof ComboboxLabelComponent;
33
+ export { useComboboxContext as useCombobox } from './ComboboxContext';