@tangible/ui 0.0.7 → 0.0.8

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 (91) hide show
  1. package/components/Accordion/Accordion.js +11 -3
  2. package/components/Avatar/Avatar.js +4 -3
  3. package/components/Avatar/AvatarGroup.js +7 -5
  4. package/components/Avatar/index.d.ts +2 -2
  5. package/components/Avatar/index.js +1 -1
  6. package/components/Avatar/types.d.ts +27 -0
  7. package/components/Avatar/types.js +8 -0
  8. package/components/Button/Button.js +4 -2
  9. package/components/Button/index.d.ts +2 -1
  10. package/components/Button/index.js +1 -0
  11. package/components/Button/types.d.ts +10 -0
  12. package/components/Button/types.js +3 -1
  13. package/components/Checkbox/Checkbox.js +46 -11
  14. package/components/Checkbox/types.d.ts +9 -0
  15. package/components/Combobox/Combobox.d.ts +1 -1
  16. package/components/Combobox/Combobox.js +28 -7
  17. package/components/Combobox/index.d.ts +2 -1
  18. package/components/Combobox/index.js +1 -0
  19. package/components/Combobox/types.d.ts +9 -0
  20. package/components/Combobox/types.js +3 -1
  21. package/components/Dropdown/Dropdown.js +16 -4
  22. package/components/Field/Field.d.ts +4 -1
  23. package/components/Field/Field.js +35 -14
  24. package/components/Field/FieldContext.d.ts +16 -0
  25. package/components/Field/FieldContext.js +3 -0
  26. package/components/Field/index.d.ts +2 -1
  27. package/components/Field/index.js +1 -0
  28. package/components/MoveHandle/MoveHandle.js +1 -1
  29. package/components/MoveHandle/types.d.ts +1 -1
  30. package/components/MultiSelect/MultiSelect.d.ts +1 -1
  31. package/components/MultiSelect/MultiSelect.js +37 -19
  32. package/components/MultiSelect/index.d.ts +2 -1
  33. package/components/MultiSelect/index.js +1 -0
  34. package/components/MultiSelect/types.d.ts +34 -0
  35. package/components/MultiSelect/types.js +10 -0
  36. package/components/Pager/Pager.d.ts +7 -1
  37. package/components/Pager/Pager.js +7 -5
  38. package/components/Pager/index.d.ts +2 -0
  39. package/components/Pager/index.js +1 -0
  40. package/components/Pager/types.d.ts +37 -0
  41. package/components/Pager/types.js +12 -0
  42. package/components/Rating/Rating.d.ts +2 -32
  43. package/components/Rating/Rating.js +5 -3
  44. package/components/Rating/index.d.ts +2 -1
  45. package/components/Rating/index.js +1 -0
  46. package/components/Rating/types.d.ts +41 -0
  47. package/components/Rating/types.js +4 -0
  48. package/components/SegmentedControl/SegmentedControl.js +6 -5
  49. package/components/SegmentedControl/types.d.ts +17 -5
  50. package/components/Select/Select.d.ts +1 -0
  51. package/components/Select/Select.js +109 -77
  52. package/components/Select/SelectContext.d.ts +4 -16
  53. package/components/Select/SelectContext.js +5 -35
  54. package/components/Select/types.d.ts +19 -19
  55. package/components/Sidebar/Sidebar.js +25 -20
  56. package/components/StepIndicator/StepIndicator.js +11 -8
  57. package/components/StepIndicator/index.d.ts +2 -1
  58. package/components/StepIndicator/index.js +1 -0
  59. package/components/StepIndicator/types.d.ts +18 -0
  60. package/components/StepIndicator/types.js +7 -1
  61. package/components/Table/BulkActionsBar.d.ts +4 -1
  62. package/components/Table/BulkActionsBar.js +5 -4
  63. package/components/Table/DataTable.d.ts +4 -1
  64. package/components/Table/DataTable.js +10 -8
  65. package/components/Table/index.d.ts +3 -0
  66. package/components/Table/index.js +2 -0
  67. package/components/Table/types.d.ts +20 -0
  68. package/components/Table/types.js +11 -0
  69. package/components/Tabs/Tabs.js +11 -4
  70. package/components/TextInput/TextInput.js +2 -1
  71. package/components/TextInput/types.d.ts +7 -1
  72. package/components/Textarea/Textarea.js +3 -2
  73. package/components/Textarea/types.d.ts +6 -1
  74. package/icons/icons.svg +29 -15
  75. package/icons/lms/index.d.ts +8 -0
  76. package/icons/lms/index.js +48 -4
  77. package/icons/manifest.json +112 -0
  78. package/icons/player/index.js +9 -9
  79. package/icons/registry.d.ts +28 -0
  80. package/icons/registry.js +14 -0
  81. package/icons/system/index.d.ts +20 -0
  82. package/icons/system/index.js +112 -2
  83. package/package.json +1 -1
  84. package/styles/all.css +1 -1
  85. package/styles/all.expanded.css +266 -59
  86. package/styles/all.expanded.unlayered.css +266 -59
  87. package/styles/all.unlayered.css +1 -1
  88. package/styles/components/input/index.scss +29 -7
  89. package/styles/system/_constants.scss +1 -1
  90. package/styles/system/_tokens.scss +1 -0
  91. package/tui-manifest.json +73 -44
@@ -1,4 +1,4 @@
1
- import type { Placement, FloatingContext, ReferenceType } from '@floating-ui/react';
1
+ import type { Placement, ReferenceType } from '@floating-ui/react';
2
2
  import type { RefObject, CSSProperties, MutableRefObject } from 'react';
3
3
  import type { SizeStandard } from '../../types/sizes';
4
4
  import type { OptionValue } from '../../utils/value-key';
@@ -92,8 +92,14 @@ export type SelectProps = {
92
92
  };
93
93
  export type SelectTriggerProps = {
94
94
  /**
95
- * When true, merges props onto the child element instead of wrapping.
96
- * Child must be a single React element that accepts ref and event handlers.
95
+ * When true, merges props onto the child element instead of rendering
96
+ * the default trigger button. Child must be a single React element that
97
+ * accepts ref, ARIA attributes, and event handlers (onClick, onKeyDown,
98
+ * onBlur, onMouseDown, onPointerDown).
99
+ *
100
+ * Note: `aria-label` and `aria-labelledby` from Select Root are spread
101
+ * onto the child but may be overridden by the child's own props.
102
+ * Ensure the child does not silently replace these if the Root provides them.
97
103
  * @default false
98
104
  */
99
105
  asChild?: boolean;
@@ -156,10 +162,10 @@ export type RegisteredOption = {
156
162
  textValue: string;
157
163
  };
158
164
  /**
159
- * Stable context: config, IDs, refs, and stable callbacks.
160
- * Rarely changes safe to subscribe without rerender concerns.
165
+ * Select context: config, IDs, refs, callbacks, and reactive state.
166
+ * All sub-components subscribe to one context.
161
167
  */
162
- export type SelectActionsContextValue = {
168
+ export type SelectContextValue = {
163
169
  disabled: boolean;
164
170
  placeholder: string;
165
171
  clearable: boolean;
@@ -181,34 +187,28 @@ export type SelectActionsContextValue = {
181
187
  setFloating: (node: HTMLElement | null) => void;
182
188
  };
183
189
  listRef: MutableRefObject<(HTMLElement | null)[]>;
190
+ /** Guard flag — prevents Floating UI from opening dropdown during a clear */
191
+ justClearedRef: MutableRefObject<boolean>;
184
192
  getReferenceProps: (userProps?: React.HTMLProps<Element>) => Record<string, unknown>;
185
193
  getFloatingProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
186
194
  getItemProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
187
- };
188
- /**
189
- * State context: values that change during interaction.
190
- * Subscribe only when you need reactive updates.
191
- */
192
- export type SelectStateContextValue = {
193
195
  open: boolean;
194
196
  value: OptionValue | undefined;
195
- getSelectedTextValue: () => string | undefined;
197
+ displayText: string | undefined;
196
198
  activeIndex: number | null;
197
199
  highlightedValue: OptionValue | null;
198
200
  orderedOptions: RegisteredOption[];
201
+ /** O(1) index lookup: toKey(value) → index in orderedOptions */
202
+ optionIndexMap: Map<string, number>;
199
203
  floatingStyles: CSSProperties;
200
- floatingContext: FloatingContext;
201
204
  };
202
- /**
203
- * Combined context value (for backwards compat and convenience hooks).
204
- * @deprecated Prefer using useSelectActions + useSelectState separately.
205
- */
206
- export type SelectContextValue = SelectActionsContextValue & SelectStateContextValue;
207
205
  export type SelectContentContextValue = {
208
206
  listRef: MutableRefObject<(HTMLElement | null)[]>;
209
207
  activeIndex: number | null;
210
208
  handleSelect: (index: number | null) => void;
211
209
  orderedOptions: RegisteredOption[];
210
+ /** O(1) index lookup: toKey(value) → index in orderedOptions */
211
+ optionIndexMap: Map<string, number>;
212
212
  };
213
213
  /**
214
214
  * Convert side + align to Floating UI placement.
@@ -25,45 +25,50 @@ function SidebarRoot(props) {
25
25
  }, [open]);
26
26
  const isClosing = drawer && !open && visible;
27
27
  const shouldRender = drawer && (open || visible);
28
+ // Dismiss: set visible false and restore focus to the element that opened
29
+ // the drawer. Called from both the reduced-motion layout effect and the
30
+ // animation-end handler — these are the actual unmount points, so focus
31
+ // must be restored here (not in a useEffect, which may not fire before
32
+ // the synchronous re-render unmounts the component).
33
+ const dismiss = useCallback(() => {
34
+ setVisible(false);
35
+ const el = restoreRef.current;
36
+ if (el && typeof el.focus === 'function' && document.contains(el)) {
37
+ el.focus();
38
+ }
39
+ restoreRef.current = null;
40
+ }, []);
28
41
  // When closing, check if animation will actually play (reduced motion).
29
- // If not, unmount immediately to avoid getting stuck.
42
+ // If not, dismiss immediately to avoid getting stuck.
30
43
  useLayoutEffect(() => {
31
44
  if (!isClosing)
32
45
  return;
33
46
  const panel = panelRef.current;
34
47
  if (!panel) {
35
- setVisible(false);
48
+ dismiss();
36
49
  return;
37
50
  }
38
51
  const style = getComputedStyle(panel);
39
52
  if (!style.animationName || style.animationName === 'none') {
40
- setVisible(false);
53
+ dismiss();
41
54
  }
42
- }, [isClosing]);
55
+ }, [isClosing, dismiss]);
43
56
  const handleAnimationEnd = useCallback((e) => {
44
57
  if (e.target === panelRef.current) {
45
- setVisible(false);
58
+ dismiss();
46
59
  }
47
- }, []);
60
+ }, [dismiss]);
48
61
  const rootClassName = ['tui-sidebar', className].filter(Boolean).join(' ');
49
62
  // ---------------------------------------------------------------------------
50
63
  // Drawer mode: capture trigger for focus restoration
51
64
  // ---------------------------------------------------------------------------
52
- useEffect(() => {
53
- if (!drawer || !isBrowser)
65
+ // Capture the element that had focus when the drawer opens.
66
+ // Must be a layout effect so it runs BEFORE the initial focus layout effect
67
+ // moves focus into the drawer. Focus restoration is handled by `dismiss()`.
68
+ useLayoutEffect(() => {
69
+ if (!drawer || !isBrowser || !open)
54
70
  return;
55
- if (open) {
56
- // Capture focus target when opening
57
- restoreRef.current = document.activeElement;
58
- }
59
- else {
60
- // Restore focus when closing (with DOM containment guard)
61
- const el = restoreRef.current;
62
- if (el && typeof el.focus === 'function' && document.contains(el)) {
63
- el.focus();
64
- }
65
- restoreRef.current = null;
66
- }
71
+ restoreRef.current = document.activeElement;
67
72
  }, [drawer, open]);
68
73
  // ---------------------------------------------------------------------------
69
74
  // Drawer mode: body scroll lock
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defaultStepIndicatorLabels } from './types.js';
2
3
  import { Progress } from '../Progress/index.js';
3
4
  import { Icon } from '../Icon/index.js';
4
5
  // =============================================================================
@@ -8,12 +9,6 @@ const statusIcons = {
8
9
  complete: 'lms/checkmark',
9
10
  locked: 'lms/lock',
10
11
  };
11
- const statusLabels = {
12
- 'not-started': 'Not started',
13
- 'in-progress': 'In progress',
14
- complete: 'Complete',
15
- locked: 'Locked',
16
- };
17
12
  // =============================================================================
18
13
  // Status inference
19
14
  // =============================================================================
@@ -28,7 +23,8 @@ function inferStatus(value) {
28
23
  // Component
29
24
  // =============================================================================
30
25
  export function StepIndicator(props) {
31
- const { value = 0, status: statusOverride, icon: customIcon, size = 'sm', showValue = false, label, className, } = props;
26
+ const { value = 0, status: statusOverride, icon: customIcon, size = 'sm', showValue = false, label, className, labels: labelsProp, } = props;
27
+ const labels = { ...defaultStepIndicatorLabels, ...labelsProp };
32
28
  // Infer status from value, allow override
33
29
  const status = statusOverride ?? inferStatus(value);
34
30
  // When status is explicit, the visual should match the status, not the value
@@ -48,8 +44,15 @@ export function StepIndicator(props) {
48
44
  const iconName = hasStatusIcon ? statusIcons[status] : customIcon;
49
45
  // Use solid variant for complete state (filled circle)
50
46
  const variant = status === 'complete' ? 'solid' : 'ring';
47
+ // Resolve status display name from labels
48
+ const statusLabelMap = {
49
+ 'not-started': labels.notStarted,
50
+ 'in-progress': labels.inProgress,
51
+ complete: labels.complete,
52
+ locked: labels.locked,
53
+ };
51
54
  // Accessible label
52
- const ariaLabel = label ?? `Step status: ${statusLabels[status]}`;
55
+ const ariaLabel = label ?? labels.status(statusLabelMap[status]);
53
56
  // Build class names
54
57
  const rootClassName = [
55
58
  'tui-step-indicator',
@@ -1,2 +1,3 @@
1
1
  export { StepIndicator } from './StepIndicator';
2
- export type { StepIndicatorProps, StepStatus } from './types';
2
+ export type { StepIndicatorProps, StepStatus, StepIndicatorLabels } from './types';
3
+ export { defaultStepIndicatorLabels } from './types';
@@ -1 +1,2 @@
1
1
  export { StepIndicator } from './StepIndicator.js';
2
+ export { defaultStepIndicatorLabels } from './types.js';
@@ -9,6 +9,19 @@ export type { Size };
9
9
  * - `locked`: Ring with lock icon (overrides value-based inference)
10
10
  */
11
11
  export type StepStatus = 'not-started' | 'in-progress' | 'complete' | 'locked';
12
+ export type StepIndicatorLabels = {
13
+ /** Label for "Not started" status. */
14
+ notStarted?: string;
15
+ /** Label for "In progress" status. */
16
+ inProgress?: string;
17
+ /** Label for "Complete" status. */
18
+ complete?: string;
19
+ /** Label for "Locked" status. */
20
+ locked?: string;
21
+ /** Full aria-label template. Receives the resolved status label string. */
22
+ status?: (statusLabel: string) => string;
23
+ };
24
+ export declare const defaultStepIndicatorLabels: Required<StepIndicatorLabels>;
12
25
  export type StepIndicatorProps = {
13
26
  /**
14
27
  * Progress value (0-100).
@@ -65,4 +78,9 @@ export type StepIndicatorProps = {
65
78
  * Additional CSS class names.
66
79
  */
67
80
  className?: string;
81
+ /**
82
+ * Override default English strings for i18n.
83
+ * Covers status display names and the aria-label template.
84
+ */
85
+ labels?: StepIndicatorLabels;
68
86
  };
@@ -1 +1,7 @@
1
- export {};
1
+ export const defaultStepIndicatorLabels = {
2
+ notStarted: 'Not started',
3
+ inProgress: 'In progress',
4
+ complete: 'Complete',
5
+ locked: 'Locked',
6
+ status: (statusLabel) => `Step status: ${statusLabel}`,
7
+ };
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import type { BulkActionsBarLabels } from './types';
2
3
  type BulkActionsBarProps = {
3
4
  count: number;
4
5
  onClear?: () => void;
@@ -7,6 +8,8 @@ type BulkActionsBarProps = {
7
8
  className?: string;
8
9
  /** ID of the table element this toolbar controls */
9
10
  'aria-controls'?: string;
11
+ /** Override hardcoded English strings for i18n. */
12
+ labels?: BulkActionsBarLabels;
10
13
  };
11
- export declare function BulkActionsBar({ count, onClear, clearLabel, children, className, 'aria-controls': ariaControls, }: BulkActionsBarProps): import("react/jsx-runtime").JSX.Element | null;
14
+ export declare function BulkActionsBar({ count, onClear, clearLabel, children, className, 'aria-controls': ariaControls, labels: labelsProp, }: BulkActionsBarProps): import("react/jsx-runtime").JSX.Element | null;
12
15
  export {};
@@ -1,9 +1,10 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { cx } from '../../utils/cx.js';
4
- export function BulkActionsBar({ count, onClear, clearLabel = 'Clear', children, className, 'aria-controls': ariaControls, }) {
4
+ import { defaultBulkActionsBarLabels } from './types.js';
5
+ export function BulkActionsBar({ count, onClear, clearLabel = 'Clear', children, className, 'aria-controls': ariaControls, labels: labelsProp, }) {
6
+ const labels = { ...defaultBulkActionsBarLabels, ...labelsProp };
5
7
  if (count === 0)
6
8
  return null;
7
- const label = `Bulk actions for ${count} selected row${count === 1 ? '' : 's'}`;
8
- return (_jsxs("div", { role: "toolbar", "aria-label": label, "aria-controls": ariaControls, className: cx('tui-table__bulkActions', className), children: [_jsxs("span", { className: "tui-table__bulkActions__count", role: "status", children: [count, " selected"] }), _jsxs("div", { className: "tui-table__bulkActions__actions", children: [children, onClear && (_jsx("button", { type: "button", onClick: onClear, className: "tui-table__bulkActions__clear tui-button is-style-outline is-size-xs", children: clearLabel }))] })] }));
9
+ return (_jsxs("div", { role: "toolbar", "aria-label": labels.actions(count), "aria-controls": ariaControls, className: cx('tui-table__bulkActions', className), children: [_jsx("span", { className: "tui-table__bulkActions__count", role: "status", children: labels.selected(count) }), _jsxs("div", { className: "tui-table__bulkActions__actions", children: [children, onClear && (_jsx("button", { type: "button", onClick: onClear, className: "tui-table__bulkActions__clear tui-button is-style-outline is-size-xs", children: clearLabel }))] })] }));
9
10
  }
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import type { ColumnDef, SortingState, Table as TanTable, RowSelectionState, Updater } from '@tanstack/react-table';
3
+ import type { DataTableLabels } from './types';
3
4
  type RowId = string | number;
4
5
  type BulkSelectionRenderArgs<T> = {
5
6
  selectedRows: T[];
@@ -17,6 +18,8 @@ export type DataTableProps<T> = {
17
18
  loading?: boolean;
18
19
  loadingMessage?: React.ReactNode;
19
20
  emptyMessage?: React.ReactNode;
21
+ /** Override hardcoded English strings for i18n. Unset keys use English defaults. */
22
+ labels?: DataTableLabels;
20
23
  renderPagination?: (table: TanTable<T>) => React.ReactNode;
21
24
  paginationMode?: 'simple' | 'ends' | 'full' | 'smart';
22
25
  paginationMaxNumbers?: number;
@@ -31,5 +34,5 @@ export type DataTableProps<T> = {
31
34
  onSelectionChange?: (selectedRows: T[], selectedIds: (string | number)[]) => void;
32
35
  renderBulkBar?: (args: BulkSelectionRenderArgs<T>) => React.ReactNode;
33
36
  };
34
- export declare function DataTable<T>({ data, columns, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, defaultSorting, loading, loadingMessage, emptyMessage, renderPagination, paginationMode, paginationMaxNumbers, paginationNavStyle, pageSizeOptions, initialPageSize, enableRowSelection, getRowId, getRowLabelForSelection, onSelectionChange, renderBulkBar, rowSelection, onRowSelectionChange, }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;
37
+ export declare function DataTable<T>({ data, columns, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, defaultSorting, loading, loadingMessage, emptyMessage, labels: labelsProp, renderPagination, paginationMode, paginationMaxNumbers, paginationNavStyle, pageSizeOptions, initialPageSize, enableRowSelection, getRowId, getRowLabelForSelection, onSelectionChange, renderBulkBar, rowSelection, onRowSelectionChange, }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;
35
38
  export {};
@@ -4,9 +4,11 @@ import { cx } from '../../utils/cx.js';
4
4
  import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, flexRender } from '@tanstack/react-table';
5
5
  import { BulkActionsBar } from './BulkActionsBar.js';
6
6
  import { Pagination } from './Pagination.js';
7
- export function DataTable({ data, columns, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, defaultSorting = [], loading = false, loadingMessage = 'Loading...', emptyMessage = 'No results', renderPagination, paginationMode = 'smart', paginationMaxNumbers = 7, paginationNavStyle = 'text', pageSizeOptions = [10, 20, 50], initialPageSize,
7
+ import { defaultDataTableLabels } from './types.js';
8
+ export function DataTable({ data, columns, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, defaultSorting = [], loading = false, loadingMessage = 'Loading...', emptyMessage = 'No results', labels: labelsProp, renderPagination, paginationMode = 'smart', paginationMaxNumbers = 7, paginationNavStyle = 'text', pageSizeOptions = [10, 20, 50], initialPageSize,
8
9
  // selection
9
10
  enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange, renderBulkBar, rowSelection, onRowSelectionChange, }) {
11
+ const labels = React.useMemo(() => ({ ...defaultDataTableLabels, ...labelsProp }), [labelsProp]);
10
12
  const tableId = React.useId();
11
13
  const [sorting, setSorting] = React.useState(defaultSorting);
12
14
  const [uncontrolledRowSelection, setUncontrolledRowSelection] = React.useState({});
@@ -29,7 +31,7 @@ enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange
29
31
  const isAllSelected = table.getIsAllPageRowsSelected();
30
32
  const isSomeSelected = table.getIsSomePageRowsSelected();
31
33
  const isIndeterminate = isSomeSelected && !isAllSelected;
32
- return (_jsx("input", { type: "checkbox", "aria-label": "Select all rows on this page", "aria-checked": isIndeterminate ? 'mixed' : isAllSelected, checked: isAllSelected, ref: (el) => {
34
+ return (_jsx("input", { type: "checkbox", "aria-label": labels.selectAll, "aria-checked": isIndeterminate ? 'mixed' : isAllSelected, checked: isAllSelected, ref: (el) => {
33
35
  if (el)
34
36
  el.indeterminate = isIndeterminate;
35
37
  }, onChange: table.getToggleAllPageRowsSelectedHandler() }));
@@ -37,7 +39,7 @@ enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange
37
39
  cell: ({ row, table }) => {
38
40
  const idx = row.index;
39
41
  const original = row.original;
40
- const labelText = getRowLabelForSelection?.(original, idx) ?? `Select row ${idx + 1}`;
42
+ const labelText = getRowLabelForSelection?.(original, idx) ?? labels.selectRow(idx);
41
43
  const handleChange = (e) => {
42
44
  const willSelect = e.target.checked;
43
45
  const isShift = e.nativeEvent.shiftKey;
@@ -75,7 +77,7 @@ enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange
75
77
  },
76
78
  enableSorting: false,
77
79
  enableHiding: false,
78
- }), [getRowLabelForSelection]);
80
+ }), [getRowLabelForSelection, labels]);
79
81
  const effectiveColumns = React.useMemo(() => {
80
82
  if (!enableRowSelection)
81
83
  return columns;
@@ -144,17 +146,17 @@ enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange
144
146
  return;
145
147
  }
146
148
  if (prevPageRef.current !== currentPageIndex) {
147
- setAnnouncement(`Page ${currentPageIndex + 1} of ${pageCount}`);
149
+ setAnnouncement(labels.pageStatus(currentPageIndex + 1, pageCount));
148
150
  }
149
151
  else if (prevSortRef.current !== currentSorting && currentSorting.length > 0) {
150
152
  const sort = currentSorting[0];
151
153
  const col = columns.find(c => 'accessorKey' in c && c.accessorKey === sort.id);
152
154
  const colName = col && typeof col.header === 'string' ? col.header : sort.id;
153
- setAnnouncement(`Sorted by ${colName}, ${sort.desc ? 'descending' : 'ascending'}`);
155
+ setAnnouncement(labels.sorted(colName, sort.desc ? 'descending' : 'ascending'));
154
156
  }
155
157
  prevPageRef.current = currentPageIndex;
156
158
  prevSortRef.current = currentSorting;
157
- }, [currentPageIndex, currentSorting, pageCount, columns]);
159
+ }, [currentPageIndex, currentSorting, pageCount, columns, labels]);
158
160
  return (_jsxs("div", { className: cx('tui-table-container', className), children: [_jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "tui-visually-hidden", children: announcement }), showBulkBar && (renderBulkBar ? (renderBulkBar({
159
161
  selectedRows,
160
162
  selectedRowIds,
@@ -173,7 +175,7 @@ enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange
173
175
  : sortDirection === 'desc'
174
176
  ? 'descending'
175
177
  : 'none'
176
- : undefined, children: canSort ? (_jsxs("button", { className: "tui-table__sortButton", onClick: header.column.getToggleSortingHandler(), "aria-label": `Sort by ${headerLabel}`, children: [headerContent, _jsx("span", { className: "tui-table__sortButton__icon", "aria-hidden": "true", children: sortDirection === 'asc'
178
+ : undefined, children: canSort ? (_jsxs("button", { className: "tui-table__sortButton", onClick: header.column.getToggleSortingHandler(), "aria-label": labels.sortBy(headerLabel), children: [headerContent, _jsx("span", { className: "tui-table__sortButton__icon", "aria-hidden": "true", children: sortDirection === 'asc'
177
179
  ? '▲'
178
180
  : sortDirection === 'desc'
179
181
  ? '▼'
@@ -1,2 +1,5 @@
1
1
  export { DataTable } from './DataTable';
2
2
  export type { DataTableProps } from './DataTable';
3
+ export { BulkActionsBar } from './BulkActionsBar';
4
+ export { defaultBulkActionsBarLabels, defaultDataTableLabels } from './types';
5
+ export type { BulkActionsBarLabels, DataTableLabels } from './types';
@@ -1 +1,3 @@
1
1
  export { DataTable } from './DataTable.js';
2
+ export { BulkActionsBar } from './BulkActionsBar.js';
3
+ export { defaultBulkActionsBarLabels, defaultDataTableLabels } from './types.js';
@@ -0,0 +1,20 @@
1
+ export type BulkActionsBarLabels = {
2
+ /** Visible count text, e.g. `"3 selected"`. Default: `` `${count} selected` `` */
3
+ selected?: (count: number) => string;
4
+ /** Toolbar `aria-label`, e.g. `"Bulk actions for 3 selected rows"`. Default: `` `Bulk actions for ${count} selected row(s)` `` */
5
+ actions?: (count: number) => string;
6
+ };
7
+ export declare const defaultBulkActionsBarLabels: Required<BulkActionsBarLabels>;
8
+ export type DataTableLabels = {
9
+ /** Label for the "select all rows" checkbox. Default: `"Select all rows on this page"` */
10
+ selectAll?: string;
11
+ /** Label for individual row checkboxes. Default: `` `Select row ${index + 1}` `` */
12
+ selectRow?: (index: number) => string;
13
+ /** Live-region announcement when page changes. Default: `` `Page ${current} of ${total}` `` */
14
+ pageStatus?: (current: number, total: number) => string;
15
+ /** Label for column sort buttons. Default: `` `Sort by ${column}` `` */
16
+ sortBy?: (column: string) => string;
17
+ /** Live-region announcement after sorting. Default: `` `Sorted by ${column}, ${direction}` `` */
18
+ sorted?: (column: string, direction: 'ascending' | 'descending') => string;
19
+ };
20
+ export declare const defaultDataTableLabels: Required<DataTableLabels>;
@@ -0,0 +1,11 @@
1
+ export const defaultBulkActionsBarLabels = {
2
+ selected: (count) => `${count} selected`,
3
+ actions: (count) => `Bulk actions for ${count} selected row${count === 1 ? '' : 's'}`,
4
+ };
5
+ export const defaultDataTableLabels = {
6
+ selectAll: 'Select all rows on this page',
7
+ selectRow: (index) => `Select row ${index + 1}`,
8
+ pageStatus: (current, total) => `Page ${current} of ${total}`,
9
+ sortBy: (column) => `Sort by ${column}`,
10
+ sorted: (column, direction) => `Sorted by ${column}, ${direction}`,
11
+ };
@@ -291,6 +291,7 @@ function Tab({ value, icon, 'aria-label': ariaLabel, disabled = false, className
291
291
  // =============================================================================
292
292
  function TabPanel({ value, className, children }) {
293
293
  const { activeValue, registerPanel, unregisterPanel, getTabId, getPanelId } = useTabsContext();
294
+ const panelRef = useRef(null);
294
295
  const tabId = getTabId(value);
295
296
  const panelId = getPanelId(value);
296
297
  const isActive = activeValue === value;
@@ -299,11 +300,17 @@ function TabPanel({ value, className, children }) {
299
300
  registerPanel(value);
300
301
  return () => unregisterPanel(value);
301
302
  }, [value, registerPanel, unregisterPanel]);
302
- return (_jsx("div", { role: "tabpanel", id: panelId, className: cx('tui-tabs__panel', className), "data-state": isActive ? 'active' : 'inactive', "aria-labelledby": tabId, "aria-hidden": isActive ? undefined : true,
303
+ // Set inert via DOM property for React 18 compatibility.
304
+ // React 18 doesn't handle `inert` as a boolean prop — setting the DOM
305
+ // property directly works across React 18 and 19.
306
+ useEffect(() => {
307
+ if (panelRef.current) {
308
+ panelRef.current.inert = !isActive;
309
+ }
310
+ }, [isActive]);
311
+ return (_jsx("div", { ref: panelRef, role: "tabpanel", id: panelId, className: cx('tui-tabs__panel', className), "data-state": isActive ? 'active' : 'inactive', "aria-labelledby": tabId, "aria-hidden": isActive ? undefined : true,
303
312
  // tabIndex={0} allows direct focus on panel for screen reader navigation
304
- tabIndex: isActive ? 0 : undefined,
305
- // inert prevents Tab navigation into hidden panel content
306
- inert: !isActive || undefined, children: children }));
313
+ tabIndex: isActive ? 0 : undefined, children: children }));
307
314
  }
308
315
  // =============================================================================
309
316
  // Compound Component Export
@@ -15,6 +15,7 @@ import { cx } from '../../utils/cx.js';
15
15
  // --tui-input-border-focus Focus state border color
16
16
  // --tui-input-border-invalid Invalid state border color
17
17
  // --tui-input-radius Border radius
18
+ // --tui-input-fg-placeholder Placeholder text color
18
19
  //
19
20
  // =============================================================================
20
21
  /** String or number content gets the `.is-text` visual treatment. */
@@ -23,7 +24,7 @@ export const TextInput = forwardRef(function TextInput({ type = 'text', size = '
23
24
  const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
24
25
  const handleGroupClick = useCallback((e) => {
25
26
  const target = e.target;
26
- if (target.closest('button, a, [role="button"], input'))
27
+ if (target.closest('button, a, input, select, textarea, [tabindex]:not([tabindex="-1"]), [role="button"]'))
27
28
  return;
28
29
  e.currentTarget.querySelector('input')?.focus();
29
30
  }, []);
@@ -14,10 +14,15 @@ export type TextInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type'
14
14
  size?: SizeStandard;
15
15
  /**
16
16
  * Content to render before the input (icon, text).
17
+ * @remarks String addons (e.g. "$", "https://") are not part of the input's
18
+ * accessible name. Use `aria-label` on the input to provide full context
19
+ * (e.g. `aria-label="Amount in US dollars"` when prefix is "$").
17
20
  */
18
21
  prefix?: ReactNode;
19
22
  /**
20
23
  * Content to render after the input (icon, button).
24
+ * @remarks String addons (e.g. "USD", ".com") are not part of the input's
25
+ * accessible name. Use `aria-label` on the input to provide full context.
21
26
  */
22
27
  suffix?: ReactNode;
23
28
  /**
@@ -26,7 +31,8 @@ export type TextInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type'
26
31
  className?: string;
27
32
  /**
28
33
  * Class name applied directly to the `<input>` element.
29
- * Use for utilities like `tui-input-reset` that must target the input itself.
34
+ * Use for utilities like `tui-input-reset` (removes browser-native search
35
+ * decoration, clear buttons, and spinners) that must target the input itself.
30
36
  */
31
37
  inputClassName?: string;
32
38
  };
@@ -16,9 +16,10 @@ import { composeRefs } from '../../utils/compose-refs.js';
16
16
  // --tui-input-border-focus Focus state border color
17
17
  // --tui-input-border-invalid Invalid state border color
18
18
  // --tui-input-radius Border radius
19
+ // --tui-input-fg-placeholder Placeholder text color
19
20
  //
20
21
  // =============================================================================
21
- export const Textarea = forwardRef(function Textarea({ size = 'md', resize = 'vertical', autoResize = false, className, onInput, ...rest }, externalRef) {
22
+ export const Textarea = forwardRef(function Textarea({ size = 'md', resize = 'vertical', autoResize = false, className, onInput, style, ...rest }, externalRef) {
22
23
  const internalRef = useRef(null);
23
24
  const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
24
25
  // Auto-resize: adjust height to fit content
@@ -45,5 +46,5 @@ export const Textarea = forwardRef(function Textarea({ size = 'md', resize = 've
45
46
  }
46
47
  onInput?.(e);
47
48
  }, [autoResize, adjustHeight, onInput]);
48
- return (_jsx("textarea", { ref: composeRefs(internalRef, externalRef), className: cx('tui-textarea', sizeClass, autoResize && 'is-autoresize', className), style: !autoResize ? { resize } : undefined, onInput: handleInput, ...rest }));
49
+ return (_jsx("textarea", { ref: composeRefs(internalRef, externalRef), className: cx('tui-textarea', sizeClass, autoResize && 'is-autoresize', className), style: { ...style, ...(!autoResize ? { resize } : {}) }, onInput: handleInput, ...rest }));
49
50
  });
@@ -10,12 +10,17 @@ export type TextareaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 's
10
10
  /**
11
11
  * Resize behaviour.
12
12
  * Ignored when `autoResize` is true (resize is disabled to avoid conflict).
13
+ * Takes precedence over `style.resize` when both are provided.
13
14
  * @default 'vertical'
14
15
  */
15
16
  resize?: TextareaResize;
16
17
  /**
17
18
  * Automatically grow height to fit content.
18
- * Disables manual resize and scrollbar.
19
+ * Disables manual resize and sets `overflow: hidden`.
20
+ *
21
+ * **Warning:** If you apply an external `max-height` via CSS or `style`,
22
+ * content beyond that height will be clipped with no scrollbar.
23
+ * In that case, add `overflow-y: auto` to restore scrolling.
19
24
  */
20
25
  autoResize?: boolean;
21
26
  /**