@tangible/ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/README.md +100 -0
  2. package/components/Accordion/Accordion.d.ts +22 -0
  3. package/components/Accordion/Accordion.js +192 -0
  4. package/components/Accordion/AccordionContext.d.ts +5 -0
  5. package/components/Accordion/AccordionContext.js +23 -0
  6. package/components/Accordion/index.d.ts +2 -0
  7. package/components/Accordion/index.js +1 -0
  8. package/components/Accordion/types.d.ts +61 -0
  9. package/components/Accordion/types.js +1 -0
  10. package/components/Avatar/Avatar.d.ts +11 -0
  11. package/components/Avatar/Avatar.js +67 -0
  12. package/components/Avatar/AvatarGroup.d.ts +11 -0
  13. package/components/Avatar/AvatarGroup.js +45 -0
  14. package/components/Avatar/index.d.ts +9 -0
  15. package/components/Avatar/index.js +7 -0
  16. package/components/Avatar/types.d.ts +44 -0
  17. package/components/Avatar/types.js +12 -0
  18. package/components/Button/Button.d.ts +4 -0
  19. package/components/Button/Button.js +33 -0
  20. package/components/Button/index.d.ts +2 -0
  21. package/components/Button/index.js +1 -0
  22. package/components/Button/types.d.ts +127 -0
  23. package/components/Button/types.js +1 -0
  24. package/components/Card/Card.d.ts +29 -0
  25. package/components/Card/Card.js +47 -0
  26. package/components/Card/index.d.ts +2 -0
  27. package/components/Card/index.js +1 -0
  28. package/components/Chip/Chip.d.ts +24 -0
  29. package/components/Chip/Chip.js +37 -0
  30. package/components/Chip/index.d.ts +2 -0
  31. package/components/Chip/index.js +1 -0
  32. package/components/Chips/Chips.d.ts +31 -0
  33. package/components/Chips/Chips.js +21 -0
  34. package/components/Chips/index.d.ts +2 -0
  35. package/components/Chips/index.js +1 -0
  36. package/components/ContentIndicator/ContentIndicator.d.ts +2 -0
  37. package/components/ContentIndicator/ContentIndicator.js +21 -0
  38. package/components/ContentIndicator/index.d.ts +2 -0
  39. package/components/ContentIndicator/index.js +1 -0
  40. package/components/ContentIndicator/types.d.ts +57 -0
  41. package/components/ContentIndicator/types.js +1 -0
  42. package/components/Dropdown/Dropdown.d.ts +31 -0
  43. package/components/Dropdown/Dropdown.js +219 -0
  44. package/components/Dropdown/DropdownContext.d.ts +3 -0
  45. package/components/Dropdown/DropdownContext.js +9 -0
  46. package/components/Dropdown/index.d.ts +2 -0
  47. package/components/Dropdown/index.js +1 -0
  48. package/components/Dropdown/types.d.ts +102 -0
  49. package/components/Dropdown/types.js +8 -0
  50. package/components/Icon/Icon.d.ts +22 -0
  51. package/components/Icon/Icon.js +24 -0
  52. package/components/Icon/index.d.ts +2 -0
  53. package/components/Icon/index.js +1 -0
  54. package/components/IconButton/IconButton.d.ts +2 -0
  55. package/components/IconButton/IconButton.js +50 -0
  56. package/components/IconButton/index.d.ts +2 -0
  57. package/components/IconButton/index.js +1 -0
  58. package/components/IconButton/types.d.ts +79 -0
  59. package/components/IconButton/types.js +1 -0
  60. package/components/Modal/Modal.d.ts +52 -0
  61. package/components/Modal/Modal.js +133 -0
  62. package/components/Modal/context.d.ts +6 -0
  63. package/components/Modal/context.js +9 -0
  64. package/components/Modal/index.d.ts +2 -0
  65. package/components/Modal/index.js +1 -0
  66. package/components/Notice/Notice.d.ts +93 -0
  67. package/components/Notice/Notice.js +144 -0
  68. package/components/Notice/index.d.ts +2 -0
  69. package/components/Notice/index.js +1 -0
  70. package/components/OverlapStack/OverlapStack.d.ts +44 -0
  71. package/components/OverlapStack/OverlapStack.js +41 -0
  72. package/components/OverlapStack/index.d.ts +2 -0
  73. package/components/OverlapStack/index.js +1 -0
  74. package/components/Pager/Pager.d.ts +26 -0
  75. package/components/Pager/Pager.js +151 -0
  76. package/components/Pager/index.d.ts +2 -0
  77. package/components/Pager/index.js +1 -0
  78. package/components/Progress/Progress.d.ts +2 -0
  79. package/components/Progress/Progress.js +100 -0
  80. package/components/Progress/index.d.ts +4 -0
  81. package/components/Progress/index.js +2 -0
  82. package/components/Progress/types.d.ts +251 -0
  83. package/components/Progress/types.js +1 -0
  84. package/components/Progress/useProgressSegments.d.ts +40 -0
  85. package/components/Progress/useProgressSegments.js +42 -0
  86. package/components/Rating/Rating.d.ts +32 -0
  87. package/components/Rating/Rating.js +74 -0
  88. package/components/Rating/index.d.ts +2 -0
  89. package/components/Rating/index.js +1 -0
  90. package/components/SegmentedControl/SegmentedControl.d.ts +10 -0
  91. package/components/SegmentedControl/SegmentedControl.js +183 -0
  92. package/components/SegmentedControl/SegmentedControlContext.d.ts +3 -0
  93. package/components/SegmentedControl/SegmentedControlContext.js +9 -0
  94. package/components/SegmentedControl/index.d.ts +2 -0
  95. package/components/SegmentedControl/index.js +1 -0
  96. package/components/SegmentedControl/types.d.ts +63 -0
  97. package/components/SegmentedControl/types.js +1 -0
  98. package/components/Sidebar/Sidebar.d.ts +17 -0
  99. package/components/Sidebar/Sidebar.js +107 -0
  100. package/components/Sidebar/index.d.ts +2 -0
  101. package/components/Sidebar/index.js +1 -0
  102. package/components/Sidebar/types.d.ts +65 -0
  103. package/components/Sidebar/types.js +4 -0
  104. package/components/StepIndicator/StepIndicator.d.ts +2 -0
  105. package/components/StepIndicator/StepIndicator.js +64 -0
  106. package/components/StepIndicator/index.d.ts +2 -0
  107. package/components/StepIndicator/index.js +1 -0
  108. package/components/StepIndicator/types.d.ts +68 -0
  109. package/components/StepIndicator/types.js +1 -0
  110. package/components/StepList/StepList.d.ts +12 -0
  111. package/components/StepList/StepList.js +59 -0
  112. package/components/StepList/StepListContext.d.ts +3 -0
  113. package/components/StepList/StepListContext.js +9 -0
  114. package/components/StepList/index.d.ts +2 -0
  115. package/components/StepList/index.js +1 -0
  116. package/components/StepList/types.d.ts +91 -0
  117. package/components/StepList/types.js +4 -0
  118. package/components/Table/BulkActionsBar.d.ts +12 -0
  119. package/components/Table/BulkActionsBar.js +9 -0
  120. package/components/Table/DataTable.d.ts +35 -0
  121. package/components/Table/DataTable.js +184 -0
  122. package/components/Table/Pagination.d.ts +13 -0
  123. package/components/Table/Pagination.js +13 -0
  124. package/components/Table/index.d.ts +2 -0
  125. package/components/Table/index.js +1 -0
  126. package/components/Tabs/Tabs.d.ts +23 -0
  127. package/components/Tabs/Tabs.js +309 -0
  128. package/components/Tabs/TabsContext.d.ts +3 -0
  129. package/components/Tabs/TabsContext.js +12 -0
  130. package/components/Tabs/index.d.ts +2 -0
  131. package/components/Tabs/index.js +1 -0
  132. package/components/Tabs/types.d.ts +75 -0
  133. package/components/Tabs/types.js +1 -0
  134. package/components/Toolbar/Toolbar.d.ts +18 -0
  135. package/components/Toolbar/Toolbar.js +241 -0
  136. package/components/Toolbar/index.d.ts +2 -0
  137. package/components/Toolbar/index.js +1 -0
  138. package/components/Toolbar/types.d.ts +28 -0
  139. package/components/Toolbar/types.js +1 -0
  140. package/components/Tooltip/Tooltip.d.ts +15 -0
  141. package/components/Tooltip/Tooltip.js +166 -0
  142. package/components/Tooltip/TooltipContext.d.ts +15 -0
  143. package/components/Tooltip/TooltipContext.js +25 -0
  144. package/components/Tooltip/index.d.ts +2 -0
  145. package/components/Tooltip/index.js +1 -0
  146. package/components/Tooltip/types.d.ts +85 -0
  147. package/components/Tooltip/types.js +8 -0
  148. package/components/index.d.ts +52 -0
  149. package/components/index.js +26 -0
  150. package/constants.d.ts +16 -0
  151. package/constants.js +16 -0
  152. package/icons/cred/index.d.ts +31 -0
  153. package/icons/cred/index.js +136 -0
  154. package/icons/icons.svg +155 -0
  155. package/icons/lms/index.d.ts +21 -0
  156. package/icons/lms/index.js +81 -0
  157. package/icons/manifest.json +1226 -0
  158. package/icons/player/index.d.ts +55 -0
  159. package/icons/player/index.js +268 -0
  160. package/icons/reaction/index.d.ts +79 -0
  161. package/icons/reaction/index.js +400 -0
  162. package/icons/registry.d.ts +316 -0
  163. package/icons/registry.js +163 -0
  164. package/icons/system/index.d.ts +155 -0
  165. package/icons/system/index.js +818 -0
  166. package/package.json +121 -0
  167. package/styles/all.css +1 -0
  168. package/styles/all.expanded.css +4137 -0
  169. package/styles/all.expanded.unlayered.css +4137 -0
  170. package/styles/all.unlayered.css +1 -0
  171. package/styles/components/_bundle.scss +51 -0
  172. package/styles/components/index.scss +1 -0
  173. package/styles/components/input/index.scss +248 -0
  174. package/styles/index.scss +71 -0
  175. package/styles/system/_constants.scss +12 -0
  176. package/styles/system/_motion.scss +47 -0
  177. package/styles/system/_palette-fns.scss +10 -0
  178. package/styles/system/_palettes.scss +80 -0
  179. package/styles/system/_tokens.scss +249 -0
  180. package/styles/system/index.scss +4 -0
  181. package/styles/utilities/_index.scss +373 -0
  182. package/tui-manifest.json +1858 -0
  183. package/types/index.d.ts +2 -0
  184. package/types/index.js +1 -0
  185. package/types/index.ts +2 -0
  186. package/types/sizes.d.ts +17 -0
  187. package/types/sizes.js +10 -0
  188. package/types/sizes.ts +21 -0
  189. package/types/svg.d.ts +5 -0
  190. package/types/themes.d.ts +14 -0
  191. package/types/themes.js +9 -0
  192. package/types/themes.ts +17 -0
  193. package/utils/color/contrast.d.ts +33 -0
  194. package/utils/color/contrast.js +88 -0
  195. package/utils/color-scheme.d.ts +25 -0
  196. package/utils/color-scheme.js +55 -0
  197. package/utils/compose-refs.d.ts +17 -0
  198. package/utils/compose-refs.js +38 -0
  199. package/utils/cx.d.ts +12 -0
  200. package/utils/cx.js +14 -0
  201. package/utils/focus-trap.d.ts +40 -0
  202. package/utils/focus-trap.js +93 -0
  203. package/utils/index.d.ts +10 -0
  204. package/utils/index.js +16 -0
  205. package/utils/math.d.ts +4 -0
  206. package/utils/math.js +19 -0
  207. package/utils/merge-props.d.ts +25 -0
  208. package/utils/merge-props.js +60 -0
  209. package/utils/polymorphic.d.ts +28 -0
  210. package/utils/polymorphic.js +44 -0
  211. package/utils/portal.d.ts +11 -0
  212. package/utils/portal.js +105 -0
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { cx } from '../../utils/cx.js';
5
+ import { PREFIX } from '../../constants.js';
6
+ import { getPortalRootFor } from '../../utils/portal.js';
7
+ import { useFocusTrap, getInitialFocus } from '../../utils/focus-trap.js';
8
+ import { ModalContext, useModalContext } from './context.js';
9
+ import { IconButton } from '../IconButton/index.js';
10
+ const isBrowser = typeof document !== 'undefined';
11
+ function ModalRoot({ isOpen, onClose, size = 'md', stickyHead, stickyFoot, labelledBy, describedBy, initialFocusSelector, container, showCloseButton, closeLabel = 'Close', closeOnBackdropClick = true, closeOnEscape = true, children, }) {
12
+ const dialogRef = useRef(null);
13
+ const restoreRef = useRef(null);
14
+ const [mount, setMount] = useState(null);
15
+ // Capture trigger element and compute portal mount point when modal opens.
16
+ // Uses useLayoutEffect to run before paint — both renders complete before
17
+ // the browser shows anything, so no visible delay.
18
+ //
19
+ // NOTE: This assumes document.activeElement is the trigger at open time.
20
+ // True for click/keyboard handlers. For programmatic opens (timers, effects),
21
+ // the captured element may not be meaningful — focus restore still works,
22
+ // portal scoping may fall back to global interface.
23
+ useLayoutEffect(() => {
24
+ if (!isOpen || !isBrowser) {
25
+ return;
26
+ }
27
+ if (mount)
28
+ return; // Already captured for this open cycle
29
+ const trigger = document.activeElement;
30
+ restoreRef.current = trigger;
31
+ setMount(container ?? getPortalRootFor(trigger));
32
+ }, [isOpen, container, mount]);
33
+ // Reset state when modal closes, restore focus to trigger.
34
+ useEffect(() => {
35
+ if (isOpen)
36
+ return;
37
+ const el = restoreRef.current;
38
+ if (el && typeof el.focus === 'function') {
39
+ el.focus();
40
+ }
41
+ restoreRef.current = null;
42
+ setMount(null);
43
+ }, [isOpen]);
44
+ // Body scroll lock.
45
+ useEffect(() => {
46
+ if (!isOpen)
47
+ return;
48
+ document.body.classList.add('tui-modal-open');
49
+ return () => {
50
+ document.body.classList.remove('tui-modal-open');
51
+ };
52
+ }, [isOpen]);
53
+ // Focus trap (handles Tab cycling and ESC to close).
54
+ useFocusTrap(dialogRef, {
55
+ isActive: isOpen,
56
+ onEscape: onClose,
57
+ escapeDeactivates: closeOnEscape,
58
+ });
59
+ // Make scrollable region focusable; set initial focus.
60
+ useEffect(() => {
61
+ if (!isOpen)
62
+ return;
63
+ const dialog = dialogRef.current;
64
+ if (!dialog)
65
+ return;
66
+ // Ensure scrollable body section is keyboard-focusable for a11y audits.
67
+ // WCAG 4.1.2: focusable elements need accessible names.
68
+ const scrollables = dialog.querySelectorAll(`.${PREFIX}-modal__body-inner, [data-scrollable="true"]`);
69
+ scrollables.forEach((el) => {
70
+ const hasOverflow = el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
71
+ if (hasOverflow && !el.hasAttribute('tabindex')) {
72
+ el.setAttribute('tabindex', '0');
73
+ if (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')) {
74
+ el.setAttribute('aria-label', 'Scrollable content');
75
+ }
76
+ }
77
+ });
78
+ // Initial focus target.
79
+ let target = null;
80
+ if (initialFocusSelector) {
81
+ target = dialog.querySelector(initialFocusSelector);
82
+ if (!target && import.meta.env.DEV) {
83
+ console.warn(`Modal: initialFocusSelector="${initialFocusSelector}" did not match any element. ` +
84
+ `Falling back to first focusable element.`);
85
+ }
86
+ }
87
+ if (!target) {
88
+ target = getInitialFocus(dialog);
89
+ }
90
+ target.focus({ preventScroll: true });
91
+ // Development warning for missing labelledBy target
92
+ if (import.meta.env.DEV && labelledBy) {
93
+ const labelElement = document.getElementById(labelledBy);
94
+ if (!labelElement) {
95
+ console.warn(`Modal: aria-labelledby="${labelledBy}" references a non-existent element. ` +
96
+ `Ensure an element with id="${labelledBy}" exists inside the modal.`);
97
+ }
98
+ }
99
+ }, [isOpen, initialFocusSelector, labelledBy]);
100
+ // Memoize context value to prevent unnecessary re-renders
101
+ const contextValue = useMemo(() => ({ onClose }), [onClose]);
102
+ if (!isOpen || !mount)
103
+ return null;
104
+ return createPortal(_jsx(ModalContext.Provider, { value: contextValue, children: _jsxs("div", { className: "tui-modal", "data-state": "open", children: [_jsx("div", { className: "tui-modal__backdrop", onClick: closeOnBackdropClick ? onClose : undefined }), _jsxs("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-labelledby": labelledBy, "aria-describedby": describedBy, className: cx('tui-modal__dialog', `is-size-${size}`, stickyHead && 'is-sticky-head', stickyFoot && 'is-sticky-foot'), tabIndex: -1, children: [showCloseButton && (_jsx(IconButton, { icon: "system/close", label: closeLabel, variant: "ghost", size: "sm", onClick: onClose, className: "tui-modal__close", showTooltip: true })), children] })] }) }), mount);
105
+ }
106
+ function ModalClose({ label = 'Close', className }) {
107
+ const { onClose } = useModalContext();
108
+ return (_jsx(IconButton, { icon: "system/close", label: label, variant: "ghost", size: "sm", onClick: onClose, className: cx('tui-modal__close', className), showTooltip: true }));
109
+ }
110
+ ModalClose.displayName = 'Modal.Close';
111
+ function ModalHead({ className, children, ...rest }) {
112
+ return (_jsx("div", { className: cx('tui-modal__head', className), ...rest, children: children }));
113
+ }
114
+ ModalHead.displayName = 'Modal.Head';
115
+ // =============================================================================
116
+ // Modal.Body — Scrollable content area with min-height
117
+ // =============================================================================
118
+ function ModalBody({ className, children, ...rest }) {
119
+ return (_jsx("div", { className: cx('tui-modal__body', className), ...rest, children: _jsx("div", { className: "tui-modal__body-inner", children: children }) }));
120
+ }
121
+ ModalBody.displayName = 'Modal.Body';
122
+ // =============================================================================
123
+ // Modal.Foot — Footer slot
124
+ // =============================================================================
125
+ function ModalFoot({ className, children, ...rest }) {
126
+ return (_jsx("div", { className: cx('tui-modal__foot', className), ...rest, children: children }));
127
+ }
128
+ ModalFoot.displayName = 'Modal.Foot';
129
+ export const Modal = ModalRoot;
130
+ Modal.Head = ModalHead;
131
+ Modal.Body = ModalBody;
132
+ Modal.Foot = ModalFoot;
133
+ Modal.Close = ModalClose;
@@ -0,0 +1,6 @@
1
+ type ModalContextValue = {
2
+ onClose: () => void;
3
+ };
4
+ export declare const ModalContext: import("react").Context<ModalContextValue | null>;
5
+ export declare function useModalContext(): ModalContextValue;
6
+ export {};
@@ -0,0 +1,9 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const ModalContext = createContext(null);
3
+ export function useModalContext() {
4
+ const ctx = useContext(ModalContext);
5
+ if (!ctx) {
6
+ throw new Error('Modal.Close must be used within a Modal component');
7
+ }
8
+ return ctx;
9
+ }
@@ -0,0 +1,2 @@
1
+ export { Modal } from './Modal';
2
+ export type { ModalProps } from './Modal';
@@ -0,0 +1 @@
1
+ export { Modal } from './Modal.js';
@@ -0,0 +1,93 @@
1
+ import React from 'react';
2
+ import type { JSX } from 'react';
3
+ import type { ThemeStatus } from '../../types';
4
+ type RootAs = 'section' | 'div';
5
+ type HeadingAs = 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
6
+ type NoticeTheme = ThemeStatus;
7
+ type NoticeAnnounce = 'off' | 'polite' | 'assertive';
8
+ export type NoticeExitAnimation = 'collapse' | 'fade' | 'none';
9
+ type CommonProps = {
10
+ /** Inline layout: icon/title on left, content center, actions right */
11
+ inline?: boolean;
12
+ /** Add box shadow */
13
+ elevated?: boolean;
14
+ /**
15
+ * Enable hover/focus visual states. If combined with onClick,
16
+ * adds role="button" and keyboard handling (Enter/Space).
17
+ */
18
+ interactive?: boolean;
19
+ /** Disable interactions */
20
+ disabled?: boolean;
21
+ /** Show left accent stripe in theme color */
22
+ stripe?: boolean;
23
+ className?: string;
24
+ style?: React.CSSProperties;
25
+ };
26
+ export type NoticeProps<TAs extends RootAs = 'section'> = CommonProps & {
27
+ as?: TAs;
28
+ /** Visual intent */
29
+ theme?: NoticeTheme;
30
+ /**
31
+ * Controls screen-reader announcement behavior.
32
+ * - off: no live region
33
+ * - polite: role=status
34
+ * - assertive: role=alert
35
+ */
36
+ announce?: NoticeAnnounce;
37
+ /**
38
+ * Accessible name when there is no visible title in Notice.Head.
39
+ * Only use when Notice.Head has no title prop - if both exist, the visible title takes precedence.
40
+ */
41
+ ariaLabel?: string;
42
+ /** Renders a dismiss button */
43
+ dismissible?: boolean;
44
+ onDismiss?: () => void;
45
+ dismissLabel?: string;
46
+ /**
47
+ * Exit animation to play when dismissed.
48
+ * - none: onDismiss called immediately (default)
49
+ * - collapse: fade + height collapse (for inline/block notices)
50
+ * - fade: fade out with slight scale (for overlays/toasts)
51
+ */
52
+ exitAnimation?: NoticeExitAnimation;
53
+ /**
54
+ * If true, the notice container will be focusable (tabIndex=-1).
55
+ * Useful for programmatic focus management in toast/notification systems.
56
+ * Not for keyboard navigation - use interactive + onClick for clickable notices.
57
+ */
58
+ focusable?: boolean;
59
+ onClick?: React.MouseEventHandler<HTMLElement>;
60
+ children?: React.ReactNode;
61
+ };
62
+ type SlotProps = React.HTMLAttributes<HTMLDivElement> & {
63
+ className?: string;
64
+ children?: React.ReactNode;
65
+ };
66
+ type HeadSlotProps = SlotProps & {
67
+ /** Icon content */
68
+ icon?: React.ReactNode;
69
+ /** Title text */
70
+ title?: React.ReactNode;
71
+ /** Heading level for title */
72
+ titleAs?: HeadingAs;
73
+ };
74
+ /** Handle exposed via ref for imperative control */
75
+ export type NoticeHandle = {
76
+ /** The underlying DOM element */
77
+ element: HTMLElement | null;
78
+ /**
79
+ * Trigger exit animation programmatically.
80
+ * Returns a promise that resolves when animation completes.
81
+ */
82
+ exit: (animation?: 'collapse' | 'fade') => Promise<void>;
83
+ };
84
+ type NoticeCompound = {
85
+ <TAs extends RootAs = 'section'>(props: NoticeProps<TAs> & Omit<React.ComponentPropsWithoutRef<TAs>, keyof NoticeProps> & {
86
+ ref?: React.Ref<NoticeHandle>;
87
+ }): JSX.Element;
88
+ Head: React.FC<HeadSlotProps>;
89
+ Body: React.FC<SlotProps>;
90
+ Foot: React.FC<SlotProps>;
91
+ };
92
+ export declare const Notice: NoticeCompound;
93
+ export {};
@@ -0,0 +1,144 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { forwardRef, useId, useMemo, useState, useRef, useImperativeHandle, useCallback, useEffect, useContext, createContext } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { IconButton } from '../IconButton/index.js';
5
+ const NoticeContext = createContext(null);
6
+ /** Check if user prefers reduced motion */
7
+ function prefersReducedMotion() {
8
+ if (typeof window === 'undefined')
9
+ return false;
10
+ return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
11
+ }
12
+ export const Notice = forwardRef(function Notice({ as = 'section', inline, elevated, interactive, disabled, stripe, className, style, onClick, theme = 'info', announce = 'off', ariaLabel, dismissible, onDismiss, dismissLabel = 'Dismiss', exitAnimation = 'none', focusable, children, ...rest }, ref) {
13
+ const Tag = as;
14
+ const innerRef = useRef(null);
15
+ const [isExiting, setIsExiting] = useState(false);
16
+ const [exitType, setExitType] = useState(null);
17
+ const exitResolveRef = useRef(null);
18
+ // Track titleId from Notice.Head for aria-labelledby coordination
19
+ const [titleId, setTitleId] = useState(null);
20
+ const contextValue = useMemo(() => ({ setTitleId }), []);
21
+ // Cleanup on unmount - resolve any pending exit promise
22
+ useEffect(() => {
23
+ return () => {
24
+ if (exitResolveRef.current) {
25
+ exitResolveRef.current();
26
+ exitResolveRef.current = null;
27
+ }
28
+ };
29
+ }, []);
30
+ // Listen for animation end to resolve exit promise
31
+ // Also handle prefers-reduced-motion where animation is disabled
32
+ useEffect(() => {
33
+ const element = innerRef.current;
34
+ if (!isExiting || !element)
35
+ return;
36
+ // If user prefers reduced motion, resolve immediately (CSS disables animation)
37
+ if (prefersReducedMotion()) {
38
+ if (exitResolveRef.current) {
39
+ exitResolveRef.current();
40
+ exitResolveRef.current = null;
41
+ }
42
+ return;
43
+ }
44
+ const handleAnimationEnd = (e) => {
45
+ // Only handle our exit animations
46
+ if (e.target !== element)
47
+ return;
48
+ if (exitResolveRef.current) {
49
+ exitResolveRef.current();
50
+ exitResolveRef.current = null;
51
+ }
52
+ };
53
+ element.addEventListener('animationend', handleAnimationEnd);
54
+ return () => element.removeEventListener('animationend', handleAnimationEnd);
55
+ }, [isExiting]);
56
+ // Imperative exit method
57
+ const exit = useCallback((animation) => {
58
+ // If exitAnimation is 'none' and no override provided, resolve immediately
59
+ const animationType = animation ?? (exitAnimation !== 'none' ? exitAnimation : undefined);
60
+ if (!animationType) {
61
+ return Promise.resolve();
62
+ }
63
+ // If user prefers reduced motion, resolve immediately
64
+ if (prefersReducedMotion()) {
65
+ setExitType(animationType);
66
+ setIsExiting(true);
67
+ return Promise.resolve();
68
+ }
69
+ return new Promise((resolve) => {
70
+ exitResolveRef.current = resolve;
71
+ setExitType(animationType);
72
+ setIsExiting(true);
73
+ });
74
+ }, [exitAnimation]);
75
+ // Handle dismiss button click
76
+ const handleDismiss = useCallback(async () => {
77
+ if (disabled)
78
+ return;
79
+ if (exitAnimation !== 'none') {
80
+ await exit(exitAnimation);
81
+ }
82
+ onDismiss?.();
83
+ }, [disabled, exitAnimation, exit, onDismiss]);
84
+ // Expose imperative handle as plain object (not mutating DOM element)
85
+ useImperativeHandle(ref, () => ({
86
+ element: innerRef.current,
87
+ exit,
88
+ }), [exit]);
89
+ // Keyboard handler for interactive notices
90
+ const handleKeyDown = useCallback((e) => {
91
+ if (!interactive || !onClick || disabled)
92
+ return;
93
+ if (e.key === 'Enter' || e.key === ' ') {
94
+ e.preventDefault();
95
+ onClick(e);
96
+ }
97
+ }, [interactive, onClick, disabled]);
98
+ // A11y role mapping
99
+ const liveRole = announce === 'assertive' ? 'alert' : announce === 'polite' ? 'status' : undefined;
100
+ // Use titleId from Notice.Head if available, otherwise check ariaLabel
101
+ const isNamed = Boolean(titleId) || Boolean(ariaLabel);
102
+ const regionRole = !liveRole && isNamed ? 'region' : undefined;
103
+ // If interactive + onClick, behave as button
104
+ const isClickable = interactive && onClick && !disabled;
105
+ const computedRole = isClickable ? 'button' : (liveRole ?? regionRole);
106
+ // aria-labelledby takes precedence over aria-label when titleId exists
107
+ const computedLabelledBy = titleId || undefined;
108
+ const computedAriaLabel = titleId ? undefined : ariaLabel;
109
+ const classes = cx('tui-notice', `is-theme-${theme}`, inline && 'is-layout-inline', elevated && 'is-style-elevated', interactive && 'has-interaction', disabled && 'is-disabled', stripe && 'has-stripe', dismissible && 'is-dismissible', focusable && 'is-focusable', isExiting && exitType && `is-exiting-${exitType}`, className);
110
+ // Dismiss button component
111
+ const dismissButton = dismissible && (_jsx(IconButton, { icon: "system/close", label: dismissLabel, size: "sm", variant: "ghost", className: "tui-notice__dismiss", onClick: (e) => {
112
+ e.stopPropagation();
113
+ handleDismiss();
114
+ }, disabled: disabled || isExiting }));
115
+ // Determine tabIndex: clickable gets 0, focusable gets -1, otherwise none
116
+ const computedTabIndex = isClickable ? 0 : (focusable ? -1 : undefined);
117
+ return (_jsx(NoticeContext.Provider, { value: contextValue, children: _jsxs(Tag, { ref: innerRef, className: classes, style: style, "aria-disabled": disabled || undefined, role: computedRole, "aria-labelledby": computedLabelledBy, "aria-label": computedAriaLabel, "aria-atomic": liveRole ? true : undefined, tabIndex: computedTabIndex, onClick: disabled ? undefined : onClick, onKeyDown: isClickable ? handleKeyDown : undefined, ...rest, children: [_jsx("div", { className: "tui-notice__inner", children: children }), dismissButton] }) }));
118
+ });
119
+ // Notice.Head renders icon and title (consumers can add custom actions via children)
120
+ function NoticeHead({ className, icon, title, titleAs = 'h3', children, ...rest }) {
121
+ const context = useContext(NoticeContext);
122
+ const reactId = useId();
123
+ const titleId = useMemo(() => title ? `tui-notice-title-${reactId}` : null, [title, reactId]);
124
+ // Register titleId with parent Notice for aria-labelledby coordination
125
+ useEffect(() => {
126
+ if (context && titleId) {
127
+ context.setTitleId(titleId);
128
+ return () => context.setTitleId(null);
129
+ }
130
+ }, [context, titleId]);
131
+ return (_jsxs("div", { className: cx('tui-notice__head', className), ...rest, children: [icon && _jsx("div", { className: "tui-notice__icon", children: icon }), title && React.createElement(titleAs, { id: titleId, className: 'tui-notice__title' }, title), children] }));
132
+ }
133
+ NoticeHead.displayName = 'Notice.Head';
134
+ function NoticeBody({ className, children, ...rest }) {
135
+ return (_jsx("div", { className: cx('tui-notice__body', className), ...rest, children: children }));
136
+ }
137
+ NoticeBody.displayName = 'Notice.Body';
138
+ function NoticeFoot({ className, children, ...rest }) {
139
+ return (_jsx("div", { className: cx('tui-notice__foot', className), ...rest, children: children }));
140
+ }
141
+ NoticeFoot.displayName = 'Notice.Foot';
142
+ Notice.Head = NoticeHead;
143
+ Notice.Body = NoticeBody;
144
+ Notice.Foot = NoticeFoot;
@@ -0,0 +1,2 @@
1
+ export { Notice } from './Notice';
2
+ export type { NoticeProps, NoticeHandle, NoticeExitAnimation } from './Notice';
@@ -0,0 +1 @@
1
+ export { Notice } from './Notice.js';
@@ -0,0 +1,44 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface OverlapStackProps {
3
+ /** Overlap amount in pixels (negative) or CSS length string. */
4
+ overlap?: number | string;
5
+ /** Maximum visible items. Overflow becomes +N badge. */
6
+ max?: number;
7
+ /**
8
+ * Enable clipping frame on item wrappers.
9
+ * Adds border-radius and overflow:hidden. Customise via tokens:
10
+ * - `--tui-overlap-stack-item-radius` (default: full)
11
+ * - `--tui-overlap-stack-border-width` (default: 0)
12
+ * - `--tui-overlap-stack-border-color` (default: bg-surface)
13
+ */
14
+ frame?: boolean;
15
+ /** Custom renderer for overflow badge. Receives count of hidden items. */
16
+ renderOverflow?: (count: number) => ReactNode;
17
+ /**
18
+ * Accessible label for the overflow badge. For i18n support.
19
+ * Function receives count and returns label string.
20
+ * @default (count) => `${count} more`
21
+ */
22
+ overflowLabel?: (count: number) => string;
23
+ /** Accessible label for the stack group. Adds role="group" when provided. */
24
+ 'aria-label'?: string;
25
+ /** ID of element labelling the stack. Adds role="group" when provided. */
26
+ 'aria-labelledby'?: string;
27
+ /** Children to stack */
28
+ children: ReactNode;
29
+ /** Additional class name */
30
+ className?: string;
31
+ /** Inline styles */
32
+ style?: React.CSSProperties;
33
+ }
34
+ export interface OverlapStackOverflowProps {
35
+ children: ReactNode;
36
+ className?: string;
37
+ style?: React.CSSProperties;
38
+ /** Accessible label for overflow badge */
39
+ 'aria-label'?: string;
40
+ }
41
+ declare const OverlapStack: import("react").ForwardRefExoticComponent<OverlapStackProps & import("react").RefAttributes<HTMLDivElement>> & {
42
+ Overflow: import("react").ForwardRefExoticComponent<OverlapStackOverflowProps & import("react").RefAttributes<HTMLSpanElement>>;
43
+ };
44
+ export { OverlapStack };
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, forwardRef, isValidElement } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ // =============================================================================
5
+ // Overflow Component
6
+ // =============================================================================
7
+ const OverlapStackOverflow = forwardRef(function OverlapStackOverflow({ children, className, style, 'aria-label': ariaLabel }, ref) {
8
+ return (_jsx("span", { ref: ref, className: cx('tui-overlap-stack__overflow', className), style: style, "aria-label": ariaLabel, children: children }));
9
+ });
10
+ // =============================================================================
11
+ // Main Component
12
+ // =============================================================================
13
+ // Low z-index base — only needs relative ordering among siblings
14
+ const Z_INDEX_BASE = 10;
15
+ const defaultOverflowLabel = (count) => `${count} more`;
16
+ const OverlapStackRoot = forwardRef(function OverlapStack({ overlap, max, frame, renderOverflow, overflowLabel = defaultOverflowLabel, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, children, className, style, }, ref) {
17
+ const childArray = Children.toArray(children).filter(isValidElement);
18
+ const totalCount = childArray.length;
19
+ // Determine visible children and overflow count
20
+ const hasOverflow = max !== undefined && totalCount > max;
21
+ const visibleChildren = hasOverflow ? childArray.slice(0, max) : childArray;
22
+ const overflowCount = hasOverflow ? totalCount - max : 0;
23
+ // Set overlap token when provided (otherwise CSS default applies)
24
+ const cssVars = (overlap !== undefined
25
+ ? { '--tui-overlap-stack-overlap': typeof overlap === 'number' ? `${overlap}px` : overlap }
26
+ : {});
27
+ // Only add role="group" when accessible name is provided
28
+ const hasAccessibleName = ariaLabel !== undefined || ariaLabelledby !== undefined;
29
+ // Default overflow renderer uses overflowLabel for i18n
30
+ const defaultOverflow = (count) => (_jsxs(OverlapStackOverflow, { "aria-label": overflowLabel(count), children: ["+", count] }));
31
+ const overflowRenderer = renderOverflow ?? defaultOverflow;
32
+ return (_jsxs("div", { ref: ref, role: hasAccessibleName ? 'group' : undefined, className: cx('tui-overlap-stack', className), style: { ...cssVars, ...style }, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, children: [visibleChildren.map((child, index) => (_jsx("span", { className: cx('tui-overlap-stack__item', frame && 'is-framed'), style: { zIndex: Z_INDEX_BASE - index }, children: child }, child.key ?? index))), hasOverflow && (_jsx("span", { className: cx('tui-overlap-stack__item', frame && 'is-framed'), style: { zIndex: Z_INDEX_BASE - visibleChildren.length }, children: overflowRenderer(overflowCount) }))] }));
33
+ });
34
+ // Display names for React DevTools
35
+ OverlapStackRoot.displayName = 'OverlapStack';
36
+ OverlapStackOverflow.displayName = 'OverlapStack.Overflow';
37
+ // Compound component pattern
38
+ const OverlapStack = Object.assign(OverlapStackRoot, {
39
+ Overflow: OverlapStackOverflow,
40
+ });
41
+ export { OverlapStack };
@@ -0,0 +1,2 @@
1
+ export { OverlapStack } from './OverlapStack';
2
+ export type { OverlapStackProps, OverlapStackOverflowProps } from './OverlapStack';
@@ -0,0 +1 @@
1
+ export { OverlapStack } from './OverlapStack.js';
@@ -0,0 +1,26 @@
1
+ export type PagerMode = 'simple' | 'ends' | 'full' | 'smart';
2
+ export type PagerProps = {
3
+ /** Current page (1-indexed) */
4
+ currentPage: number;
5
+ /** Total number of pages */
6
+ totalPages: number;
7
+ /** Called when user selects a different page */
8
+ onPageChange: (page: number) => void;
9
+ /** Current page size (for page size selector) */
10
+ pageSize?: number;
11
+ /** Available page size options */
12
+ pageSizeOptions?: number[];
13
+ /** Called when user changes page size */
14
+ onPageSizeChange?: (size: number) => void;
15
+ /** Pagination mode: 'simple' (prev/next only), 'ends', 'full', 'smart' */
16
+ mode?: PagerMode;
17
+ /** Max page number buttons in smart mode */
18
+ maxNumbers?: number;
19
+ /** Style for prev/next: 'text' shows labels, 'icon' shows chevrons */
20
+ navStyle?: 'text' | 'icon';
21
+ /** Hide the entire pager (useful when only 1 page) */
22
+ hidden?: boolean;
23
+ /** Additional class name */
24
+ className?: string;
25
+ };
26
+ export declare function Pager({ currentPage, totalPages, onPageChange, pageSize, pageSizeOptions, onPageSizeChange, mode, maxNumbers, navStyle, hidden, className, }: PagerProps): import("react/jsx-runtime").JSX.Element | null;