@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,151 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { Button } from '../Button/index.js';
5
+ import { IconButton } from '../IconButton/index.js';
6
+ // =============================================================================
7
+ // Pager Component
8
+ // =============================================================================
9
+ //
10
+ // Generic pagination control for any paginated content. Not coupled to any
11
+ // specific data source — just needs current page, total pages, and a callback.
12
+ //
13
+ // Use this for card grids, search results, galleries, or any paginated list.
14
+ // For TanStack Table integration, use TablePagination which wraps this.
15
+ //
16
+ // =============================================================================
17
+ const DEFAULT_MAX_SLOTS = 7;
18
+ /**
19
+ * Build a stable-width list of page tokens (numbers + ellipses).
20
+ *
21
+ * Always shows: first, last, current, current±1
22
+ * Single-page gaps are filled in (no "1 ... 3" silliness)
23
+ * Expands symmetrically from current, biased toward center
24
+ */
25
+ function buildNumberTokens(total, current, maxSlots = DEFAULT_MAX_SLOTS) {
26
+ if (total <= 1)
27
+ return [1];
28
+ if (total <= maxSlots)
29
+ return Array.from({ length: total }, (_, i) => i + 1);
30
+ const inRange = (n) => n >= 1 && n <= total;
31
+ // Start with anchors: first, last, current and neighbors
32
+ const chosen = new Set([1, total]);
33
+ if (inRange(current))
34
+ chosen.add(current);
35
+ if (inRange(current - 1))
36
+ chosen.add(current - 1);
37
+ if (inRange(current + 1))
38
+ chosen.add(current + 1);
39
+ /**
40
+ * Convert chosen pages to tokens, inserting ellipses for gaps.
41
+ * Single-page gaps get filled in rather than showing an ellipsis.
42
+ */
43
+ const materialize = () => {
44
+ const pages = [...chosen].sort((a, b) => a - b);
45
+ const tokens = [];
46
+ for (let i = 0; i < pages.length; i++) {
47
+ tokens.push(pages[i]);
48
+ const next = pages[i + 1];
49
+ if (next == null)
50
+ break;
51
+ const gap = next - pages[i];
52
+ if (gap === 2)
53
+ tokens.push(pages[i] + 1);
54
+ else if (gap > 2)
55
+ tokens.push('...');
56
+ }
57
+ return tokens;
58
+ };
59
+ /** Try adding a page. Returns true if it fit within maxSlots. */
60
+ const tryAdd = (p) => {
61
+ if (!inRange(p) || chosen.has(p))
62
+ return false;
63
+ chosen.add(p);
64
+ if (materialize().length > maxSlots) {
65
+ chosen.delete(p);
66
+ return false;
67
+ }
68
+ return true;
69
+ };
70
+ // Expand symmetrically from current until we fill available slots
71
+ let offset = 2;
72
+ while (materialize().length < maxSlots) {
73
+ const left = current - offset;
74
+ const right = current + offset;
75
+ // Bias toward center: if past halfway, try right first
76
+ const preferRight = current > total / 2;
77
+ const [first, second] = preferRight ? [right, left] : [left, right];
78
+ // Try preferred side, then other side
79
+ const added = tryAdd(first) || tryAdd(second);
80
+ // If neither side worked, try both explicitly (handles edge cases)
81
+ if (!added && !tryAdd(left) && !tryAdd(right))
82
+ break;
83
+ offset++;
84
+ }
85
+ return materialize();
86
+ }
87
+ function buildItems(total, current, mode, maxNumbers) {
88
+ const items = [];
89
+ items.push({
90
+ kind: 'prev',
91
+ disabled: current <= 1,
92
+ page: Math.max(1, current - 1),
93
+ });
94
+ if (mode === 'full') {
95
+ for (let p = 1; p <= total; p++) {
96
+ items.push({ kind: 'page', page: p, current: p === current });
97
+ }
98
+ }
99
+ else if (mode === 'ends') {
100
+ const set = new Set([1, current - 1, current, current + 1, total].filter((n) => n >= 1 && n <= total));
101
+ const pages = Array.from(set).sort((a, b) => a - b);
102
+ pages.forEach((p) => items.push({ kind: 'page', page: p, current: p === current }));
103
+ }
104
+ else if (mode === 'smart') {
105
+ const tokens = buildNumberTokens(total, current, maxNumbers);
106
+ tokens.forEach((tok) => {
107
+ if (tok === '...')
108
+ items.push({ kind: 'ellipsis' });
109
+ else
110
+ items.push({ kind: 'page', page: tok, current: tok === current });
111
+ });
112
+ }
113
+ // mode === 'simple' => no page numbers
114
+ items.push({
115
+ kind: 'next',
116
+ disabled: current >= total,
117
+ page: Math.min(total, current + 1),
118
+ });
119
+ return items;
120
+ }
121
+ // -----------------------------------------------------------------------------
122
+ // Component
123
+ // -----------------------------------------------------------------------------
124
+ export function Pager({ currentPage, totalPages, onPageChange, pageSize, pageSizeOptions, onPageSizeChange, mode = 'smart', maxNumbers = DEFAULT_MAX_SLOTS, navStyle = 'text', hidden = false, className, }) {
125
+ // Normalise inputs
126
+ const total = Math.max(1, totalPages);
127
+ const current = Math.min(total, Math.max(1, currentPage));
128
+ const items = React.useMemo(() => buildItems(total, current, mode, maxNumbers), [total, current, mode, maxNumbers]);
129
+ const go = React.useCallback((page) => onPageChange(page), [onPageChange]);
130
+ if (hidden)
131
+ return null;
132
+ const showPageSizeSelector = pageSizeOptions && pageSizeOptions.length > 0 && onPageSizeChange;
133
+ return (_jsxs("div", { className: cx('tui-pager', className), children: [_jsx("nav", { className: "tui-pager__nav", "aria-label": "Pagination", children: items.map((it, i) => {
134
+ if (it.kind === 'ellipsis') {
135
+ return (_jsx("span", { className: "tui-pager__ellipsis", "aria-hidden": true, children: "..." }, `e${i}`));
136
+ }
137
+ if (it.kind === 'prev' || it.kind === 'next') {
138
+ const isPrev = it.kind === 'prev';
139
+ const label = isPrev ? 'Previous page' : 'Next page';
140
+ if (navStyle === 'icon') {
141
+ return (_jsx(IconButton, { icon: isPrev ? 'system/chevron-left' : 'system/chevron-right', label: label, showTooltip: true, size: "sm", theme: "secondary", variant: "outline", disabled: it.disabled, onClick: () => go(it.page), className: "tui-pager__item" }, it.kind));
142
+ }
143
+ return (_jsx(Button, { size: "sm", theme: "secondary", variant: "outline", disabled: it.disabled, onClick: () => go(it.page), "aria-label": label, className: "tui-pager__item", children: isPrev ? 'Prev' : 'Next' }, it.kind));
144
+ }
145
+ // Page number
146
+ if (it.kind === 'page') {
147
+ return (_jsx(Button, { size: "sm", theme: it.current ? 'primary' : 'secondary', variant: it.current ? 'solid' : 'outline', "aria-current": it.current ? 'page' : undefined, "aria-label": `Page ${it.page}`, onClick: () => go(it.page), className: "tui-pager__item", children: it.page }, it.page));
148
+ }
149
+ return null;
150
+ }) }), _jsxs("div", { className: "tui-pager__info", children: [_jsxs("span", { children: ["Page ", _jsx("strong", { children: current }), " of ", total] }), showPageSizeSelector && (_jsxs("label", { className: "tui-pager__page-size-label", children: [_jsx("span", { className: "tui-visually-hidden", children: "Items per page" }), _jsx("select", { className: "tui-input", value: pageSize, onChange: (e) => onPageSizeChange(Number(e.target.value)), children: pageSizeOptions.map((s) => (_jsxs("option", { value: s, children: [s, " / page"] }, s))) })] }))] })] }));
151
+ }
@@ -0,0 +1,2 @@
1
+ export { Pager } from './Pager';
2
+ export type { PagerProps, PagerMode } from './Pager';
@@ -0,0 +1 @@
1
+ export { Pager } from './Pager.js';
@@ -0,0 +1,2 @@
1
+ import type { ProgressProps } from './types';
2
+ export declare function Progress(props: ProgressProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,100 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { useProgressSegments } from './useProgressSegments.js';
4
+ import { cx } from '../../utils/cx.js';
5
+ // =============================================================================
6
+ // COMPONENT
7
+ // =============================================================================
8
+ export function Progress(props) {
9
+ const { children, mode = 'line', size = 'md', max = 100, showLabels = true, labelledBy, ariaLabel, defaultLabel, className, } = props;
10
+ // Determine mode
11
+ const isSegmented = 'segments' in props && Array.isArray(props.segments);
12
+ // Standard mode props
13
+ const value = !isSegmented ? (props.value ?? 0) : 0;
14
+ const indeterminate = !isSegmented ? (props.indeterminate ?? false) : false;
15
+ const labelPosition = !isSegmented ? (props.labelPosition ?? 'inline') : 'inline';
16
+ const labelStart = !isSegmented ? props.labelStart : undefined;
17
+ const labelEnd = !isSegmented ? props.labelEnd : undefined;
18
+ const variant = !isSegmented && 'variant' in props ? (props.variant ?? 'ring') : 'ring';
19
+ // Segments (for segmented mode) — fallback ensures type safety
20
+ const segments = ('segments' in props && props.segments) || [];
21
+ // Process segments via hook
22
+ const { processedSegments, ariaValueText, primaryValue, primaryPct, } = useProgressSegments({ segments, max });
23
+ // Calculate percentages for standard mode
24
+ const pct = Math.max(0, Math.min(100, (value / max) * 100));
25
+ // Dev warning: inside position only supports labelStart (or children)
26
+ if (import.meta.env.DEV && labelPosition === 'inside' && labelStart && labelEnd) {
27
+ console.warn('Progress: labelPosition="inside" only supports a single label. ' +
28
+ 'labelEnd will be ignored. Use labelStart or children for inside content.');
29
+ }
30
+ // Check if we have any label content to show
31
+ const hasLabels = !!(labelStart || labelEnd || children);
32
+ // Determine accessible name props for progressbar
33
+ const ariaProps = {};
34
+ if (labelledBy) {
35
+ ariaProps['aria-labelledby'] = labelledBy;
36
+ }
37
+ else if (ariaLabel) {
38
+ ariaProps['aria-label'] = ariaLabel;
39
+ }
40
+ else if (defaultLabel) {
41
+ ariaProps['aria-label'] = defaultLabel;
42
+ }
43
+ // Add valuetext for segmented mode
44
+ if (isSegmented && ariaValueText) {
45
+ ariaProps['aria-valuetext'] = ariaValueText;
46
+ }
47
+ // aria-valuenow, aria-valuemin, aria-valuemax
48
+ const ariaValueProps = indeterminate
49
+ ? {
50
+ 'aria-valuenow': undefined,
51
+ 'aria-valuemin': undefined,
52
+ 'aria-valuemax': undefined,
53
+ 'aria-busy': true,
54
+ }
55
+ : {
56
+ 'aria-valuenow': Math.round(isSegmented ? primaryValue : value),
57
+ 'aria-valuemin': 0,
58
+ 'aria-valuemax': max,
59
+ 'aria-busy': undefined,
60
+ };
61
+ // Build CSS custom properties
62
+ const styleVars = {
63
+ '--tui-progress-value': String(isSegmented ? primaryValue : value),
64
+ '--tui-progress-max': String(max),
65
+ '--tui-progress-pct': String(isSegmented ? primaryPct : pct),
66
+ };
67
+ // Add segment-specific vars
68
+ processedSegments.forEach((seg, i) => {
69
+ styleVars[`--tui-progress-z${i + 1}-pct`] = String(seg.pct);
70
+ styleVars[`--tui-progress-z${i + 1}-width`] = String(seg.width);
71
+ });
72
+ const rootProps = {
73
+ role: 'progressbar',
74
+ ...ariaProps,
75
+ ...ariaValueProps,
76
+ className: cx('tui-progress', `is-mode-${mode}`, `is-size-${size}`, !isSegmented && showLabels && hasLabels && `is-label-${labelPosition}`, indeterminate && 'is-indeterminate', isSegmented && 'is-segmented', mode === 'circle' && `is-variant-${variant}`, className),
77
+ style: styleVars,
78
+ };
79
+ // Inside label content (for line integrated or circle center)
80
+ const insideContent = children ?? labelStart;
81
+ // Content for line variant
82
+ const lineContent = isSegmented ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "tui-progress__track", children: processedSegments.map((seg, i) => (_jsx("div", { className: cx('tui-progress__bar', `is-z${seg.zIndex}`), style: { '--tui-segment-width': `${seg.width}%` } }, i))) }), showLabels && (children ? (_jsx("div", { className: "tui-progress__segment-labels", children: children })) : (_jsx("div", { className: "tui-progress__segment-labels", children: processedSegments.map((seg, i) => (seg.visible && (_jsxs("span", { className: cx('tui-progress__segment-label', `is-z${seg.zIndex}`), children: [seg.label && _jsx("span", { className: "tui-progress__segment-label-text", children: seg.label }), _jsxs("span", { className: "tui-progress__segment-label-value", children: [seg.roundedPct, "%"] })] }, i)))) })))] })) : (_jsxs("div", { className: "tui-progress__track", children: [_jsx("div", { className: "tui-progress__bar" }), !indeterminate && showLabels && labelPosition === 'inside' && insideContent && (_jsx("div", { className: "tui-progress__label-wrapper", children: _jsx("div", { className: "tui-progress__label is-inside", children: insideContent }) }))] }));
83
+ // Content for circle variant (segmented not supported — enforced by TypeScript)
84
+ const circleContent = (_jsxs("div", { className: "tui-progress__circle", children: [_jsx("div", { className: "tui-progress__circle__bar" }), !indeterminate && showLabels && labelPosition === 'inside' && insideContent && (_jsx("div", { className: "tui-progress__label is-inside", children: insideContent }))] }));
85
+ const trackContent = mode === 'line' ? lineContent : circleContent;
86
+ // ==========================================================================
87
+ // Render based on label position
88
+ // ==========================================================================
89
+ // No labels to show
90
+ if (!showLabels || !hasLabels || isSegmented) {
91
+ return (_jsxs("div", { ...rootProps, children: [trackContent, !labelledBy && !ariaLabel && defaultLabel && (_jsx("span", { className: "visually-hidden", children: defaultLabel }))] }));
92
+ }
93
+ // Inside position: labels are rendered within trackContent above
94
+ if (labelPosition === 'inside') {
95
+ return (_jsxs("div", { ...rootProps, children: [trackContent, !labelledBy && !ariaLabel && defaultLabel && (_jsx("span", { className: "visually-hidden", children: defaultLabel }))] }));
96
+ }
97
+ // Above/below/inline positions: render labels outside the track
98
+ const labelRow = (_jsxs("div", { className: "tui-progress__labels", children: [labelStart && _jsx("span", { className: "tui-progress__label is-start", children: labelStart }), labelEnd && _jsx("span", { className: "tui-progress__label is-end", children: labelEnd })] }));
99
+ return (_jsxs("div", { ...rootProps, children: [labelPosition === 'above' && labelRow, labelPosition === 'inline' ? (_jsxs("div", { className: "tui-progress__inline", children: [labelStart && _jsx("span", { className: "tui-progress__label is-start", children: labelStart }), trackContent, labelEnd && _jsx("span", { className: "tui-progress__label is-end", children: labelEnd })] })) : (trackContent), labelPosition === 'below' && labelRow, !labelledBy && !ariaLabel && defaultLabel && (_jsx("span", { className: "visually-hidden", children: defaultLabel }))] }));
100
+ }
@@ -0,0 +1,4 @@
1
+ export { Progress } from './Progress';
2
+ export { useProgressSegments } from './useProgressSegments';
3
+ export type { ProgressProps, ProgressSegment, SegmentedProgressProps, StandardProgressProps, Mode, Size, LabelPos, } from './types';
4
+ export type { ProcessedSegment } from './useProgressSegments';
@@ -0,0 +1,2 @@
1
+ export { Progress } from './Progress.js';
2
+ export { useProgressSegments } from './useProgressSegments.js';
@@ -0,0 +1,251 @@
1
+ import type React from 'react';
2
+ import type { Size } from '../../types';
3
+ export type { Size };
4
+ export type Mode = 'line' | 'circle';
5
+ export type LabelPos = 'above' | 'inline' | 'below' | 'inside';
6
+ /**
7
+ * A single segment in segmented progress mode.
8
+ *
9
+ * Segments are rendered front-to-back (z1, z2, z3...) with each segment
10
+ * showing the portion from the previous segment's value to its own value.
11
+ *
12
+ * @example
13
+ * // Student is at 24%, should be at 38%
14
+ * { value: 24, label: 'Progress' } // z1: 0-24%
15
+ * { value: 38, label: 'Expected' } // z2: 24-38%
16
+ */
17
+ export type ProgressSegment = {
18
+ /**
19
+ * Cumulative value for this segment (0–100, or use with `max` prop).
20
+ * Each segment's visual width = its value minus the previous segment's value.
21
+ */
22
+ value: number;
23
+ /**
24
+ * Label text shown before the percentage in the label below the bar.
25
+ * If omitted, only the percentage is shown.
26
+ */
27
+ label?: string;
28
+ };
29
+ /**
30
+ * Base props shared by all Progress variants.
31
+ */
32
+ export type BaseProgressProps = {
33
+ /**
34
+ * Custom content to render inside the progress indicator.
35
+ * - Line mode with `inside`: Replaces default content inside the bar
36
+ * - Circle mode with `inside`: Replaces centered content
37
+ * - Segmented mode: Replaces the segment labels area below the bar
38
+ */
39
+ children?: React.ReactNode;
40
+ /**
41
+ * Size of the progress bar, affecting height and font size.
42
+ * - `'xs'`: 12px font, thinnest bar (for dense step indicators)
43
+ * - `'sm'`: 14px font, thinner bar
44
+ * - `'md'`: 16px font, standard bar (default)
45
+ * - `'lg'`: 20px font, thicker bar
46
+ * @default 'md'
47
+ */
48
+ size?: Size;
49
+ /**
50
+ * Maximum value for the progress calculation.
51
+ * Progress percentage = (value / max) × 100
52
+ * @default 100
53
+ */
54
+ max?: number;
55
+ /**
56
+ * Whether to display labels.
57
+ * When false, hides all label content regardless of other props.
58
+ * @default true
59
+ */
60
+ showLabels?: boolean;
61
+ /**
62
+ * ARIA: ID of an element that labels this progress bar.
63
+ * Takes precedence over `ariaLabel` and `defaultLabel`.
64
+ */
65
+ labelledBy?: string;
66
+ /**
67
+ * ARIA: Accessible name for the progress bar.
68
+ * Use when no visible label exists. Takes precedence over `defaultLabel`.
69
+ */
70
+ ariaLabel?: string;
71
+ /**
72
+ * Fallback accessible name, rendered as visually-hidden text.
73
+ * Only used if neither `labelledBy` nor `ariaLabel` is provided.
74
+ */
75
+ defaultLabel?: string;
76
+ /**
77
+ * Additional CSS class names to apply to the root element.
78
+ */
79
+ className?: string;
80
+ };
81
+ /**
82
+ * Props for segmented mode — displays multiple stacked segments (z1, z2, z3...).
83
+ *
84
+ * Each segment shows the portion from the previous segment's value to its value.
85
+ * Useful for showing actual vs expected progress, or multi-phase completion.
86
+ *
87
+ * **Restricted in segmented mode:**
88
+ * - `variant` — only `'line'` is supported (enforced by TypeScript)
89
+ * - `labelPosition`, `labelStart`, `labelEnd` — use segment-level labels instead
90
+ * - `indeterminate` — not supported
91
+ * - `value` — derived from segments array
92
+ *
93
+ * @example
94
+ * ```tsx
95
+ * <Progress
96
+ * segments={[
97
+ * { value: 24, label: 'Progress' },
98
+ * { value: 38, label: 'Expected' },
99
+ * ]}
100
+ * ariaLabel="Student progress"
101
+ * />
102
+ * ```
103
+ */
104
+ export type SegmentedProgressProps = BaseProgressProps & {
105
+ /**
106
+ * Display mode. Segmented progress only supports line mode.
107
+ * @default 'line'
108
+ */
109
+ mode?: 'line';
110
+ /**
111
+ * Segments to display, ordered front-to-back (z1, z2, z3...).
112
+ * Maximum 5 segments have default colors; beyond that, override via CSS.
113
+ */
114
+ segments: ProgressSegment[];
115
+ /** Not available in segmented mode. */
116
+ value?: never;
117
+ /** Not supported in segmented mode. */
118
+ indeterminate?: never;
119
+ /** Not available in segmented mode. Labels appear below the bar. */
120
+ labelPosition?: never;
121
+ /** Not available in segmented mode. */
122
+ labelStart?: never;
123
+ /** Not available in segmented mode. */
124
+ labelEnd?: never;
125
+ };
126
+ /**
127
+ * Props for standard single-value progress mode.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * // Basic bar with no label
132
+ * <Progress value={42} ariaLabel="Upload progress" />
133
+ *
134
+ * // With labels above the bar (split left/right)
135
+ * <Progress
136
+ * value={33}
137
+ * labelPosition="above"
138
+ * labelStart="33% Complete"
139
+ * labelEnd="3/9 Steps"
140
+ * />
141
+ *
142
+ * // Indeterminate (loading)
143
+ * <Progress indeterminate ariaLabel="Loading" />
144
+ * ```
145
+ */
146
+ export type StandardProgressProps = BaseProgressProps & {
147
+ /**
148
+ * Display mode of the progress indicator.
149
+ * - `'line'`: Horizontal bar (default)
150
+ * - `'circle'`: Circular/ring indicator
151
+ * @default 'line'
152
+ */
153
+ mode?: Mode;
154
+ /**
155
+ * Visual variant for circle mode.
156
+ * - `'ring'`: Hollow centre (default) — shows progress as a ring/donut
157
+ * - `'solid'`: Filled centre — shows as a solid filled circle
158
+ *
159
+ * Only applies when `mode="circle"`. Ignored in line mode.
160
+ * @default 'ring'
161
+ */
162
+ variant?: 'ring' | 'solid';
163
+ /**
164
+ * Current progress value (0–100, or use with `max` prop).
165
+ * @default 0
166
+ */
167
+ value?: number;
168
+ /**
169
+ * When true, shows an animated indeterminate state.
170
+ * Use for operations where progress percentage is unknown.
171
+ * Ignores `value` when enabled.
172
+ * @default false
173
+ */
174
+ indeterminate?: boolean;
175
+ /**
176
+ * Where to display the labels relative to the progress bar.
177
+ * - `'above'`: Labels in a row above the bar
178
+ * - `'inline'`: Labels beside the bar horizontally (default)
179
+ * - `'below'`: Labels in a row below the bar
180
+ * - `'inside'`: Label content inside/overlapping the bar (or centered in circle)
181
+ *
182
+ * When both `labelStart` and `labelEnd` are provided with `above`, `inline`,
183
+ * or `below`, they appear on opposite sides (space-between).
184
+ *
185
+ * @default 'inline'
186
+ */
187
+ labelPosition?: LabelPos;
188
+ /**
189
+ * Label content for the start (left) position.
190
+ * Use with `labelPosition` to control placement.
191
+ * @example labelStart="33% Complete"
192
+ */
193
+ labelStart?: React.ReactNode;
194
+ /**
195
+ * Label content for the end (right) position.
196
+ * Use with `labelPosition` to control placement.
197
+ * @example labelEnd="3/9 Steps"
198
+ */
199
+ labelEnd?: React.ReactNode;
200
+ /** Not available in standard mode. Use segmented mode instead. */
201
+ segments?: never;
202
+ };
203
+ /**
204
+ * Progress bar component with support for single-value and segmented modes.
205
+ *
206
+ * ## Modes
207
+ *
208
+ * **Standard mode** — Single progress value with optional labels:
209
+ * ```tsx
210
+ * <Progress
211
+ * value={42}
212
+ * labelPosition="above"
213
+ * labelStart="42%"
214
+ * labelEnd="Progress"
215
+ * />
216
+ * ```
217
+ *
218
+ * **Segmented mode** — Multiple stacked segments for comparing values:
219
+ * ```tsx
220
+ * <Progress
221
+ * segments={[
222
+ * { value: 24, label: 'Actual' },
223
+ * { value: 38, label: 'Expected' },
224
+ * ]}
225
+ * />
226
+ * ```
227
+ *
228
+ * ## Styling
229
+ *
230
+ * Customise via CSS custom properties on the component or a parent:
231
+ *
232
+ * | Property | Description | Default |
233
+ * |----------|-------------|---------|
234
+ * | `--tui-progress-fill` | Bar fill color (standard mode) | Primary theme color |
235
+ * | `--tui-progress-track` | Track background color | Border color |
236
+ * | `--tui-progress-radius` | Border radius | Medium radius |
237
+ * | `--tui-progress-z1-fill` | Segment 1 color | Primary |
238
+ * | `--tui-progress-z2-fill` | Segment 2 color | Warning (amber) |
239
+ * | `--tui-progress-z3-fill` | Segment 3 color | Success (green) |
240
+ * | `--tui-progress-z4-fill` | Segment 4 color | Danger (red) |
241
+ * | `--tui-progress-z5-fill` | Segment 5 color | Info (blue) |
242
+ *
243
+ * ## Accessibility
244
+ *
245
+ * - Uses `role="progressbar"` with proper ARIA attributes
246
+ * - `aria-valuenow`, `aria-valuemin`, `aria-valuemax` for screen readers
247
+ * - `aria-valuetext` in segmented mode announces all segment values
248
+ * - `aria-busy` set during indeterminate state
249
+ * - Respects `prefers-reduced-motion` for animations
250
+ */
251
+ export type ProgressProps = SegmentedProgressProps | StandardProgressProps;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import type { ProgressSegment } from './types';
2
+ export type { ProgressSegment };
3
+ export type ProcessedSegment = ProgressSegment & {
4
+ /** Percentage value (0-100) */
5
+ pct: number;
6
+ /** Rounded percentage for display */
7
+ roundedPct: number;
8
+ /** Visual width as percentage (this segment's portion) */
9
+ width: number;
10
+ /** Whether segment has visible width (width > 0) */
11
+ visible: boolean;
12
+ /** Z-index identifier (1, 2, 3...) */
13
+ zIndex: number;
14
+ };
15
+ type UseProgressSegmentsOptions = {
16
+ segments: ProgressSegment[];
17
+ max: number;
18
+ };
19
+ type UseProgressSegmentsResult = {
20
+ /** Processed segments with calculated widths and visibility */
21
+ processedSegments: ProcessedSegment[];
22
+ /** Accessible description of all segments for screen readers */
23
+ ariaValueText: string;
24
+ /** Primary value (first segment) for aria-valuenow */
25
+ primaryValue: number;
26
+ /** Primary percentage (first segment) */
27
+ primaryPct: number;
28
+ };
29
+ /**
30
+ * Hook to process progress segments into renderable data.
31
+ *
32
+ * Calculates each segment's visual width based on cumulative values:
33
+ * - Segment 1 (z1): width = segment[0].value
34
+ * - Segment 2 (z2): width = segment[1].value - segment[0].value
35
+ * - etc.
36
+ *
37
+ * Segments with zero or negative width (when a later segment has lower value)
38
+ * are marked as not visible.
39
+ */
40
+ export declare function useProgressSegments({ segments, max, }: UseProgressSegmentsOptions): UseProgressSegmentsResult;
@@ -0,0 +1,42 @@
1
+ import { useMemo } from 'react';
2
+ /**
3
+ * Hook to process progress segments into renderable data.
4
+ *
5
+ * Calculates each segment's visual width based on cumulative values:
6
+ * - Segment 1 (z1): width = segment[0].value
7
+ * - Segment 2 (z2): width = segment[1].value - segment[0].value
8
+ * - etc.
9
+ *
10
+ * Segments with zero or negative width (when a later segment has lower value)
11
+ * are marked as not visible.
12
+ */
13
+ export function useProgressSegments({ segments, max, }) {
14
+ return useMemo(() => {
15
+ const processedSegments = segments.map((seg, i) => {
16
+ const segPct = Math.max(0, Math.min(100, (seg.value / max) * 100));
17
+ const prevPct = i === 0
18
+ ? 0
19
+ : Math.max(0, Math.min(100, (segments[i - 1].value / max) * 100));
20
+ const width = Math.max(0, segPct - prevPct);
21
+ return {
22
+ ...seg,
23
+ pct: segPct,
24
+ roundedPct: Math.round(segPct),
25
+ width,
26
+ visible: width > 0,
27
+ zIndex: i + 1,
28
+ };
29
+ });
30
+ const ariaValueText = processedSegments
31
+ .map(seg => `${seg.label ?? `Segment ${seg.zIndex}`} ${seg.roundedPct}%`)
32
+ .join(', ');
33
+ const primaryValue = segments[0]?.value ?? 0;
34
+ const primaryPct = processedSegments[0]?.pct ?? 0;
35
+ return {
36
+ processedSegments,
37
+ ariaValueText,
38
+ primaryValue,
39
+ primaryPct,
40
+ };
41
+ }, [segments, max]);
42
+ }
@@ -0,0 +1,32 @@
1
+ import type { SizeStandard, Theme as ThemeFull } from '../../types';
2
+ type Size = SizeStandard;
3
+ type Theme = ThemeFull;
4
+ export type RatingProps = {
5
+ /** Controlled value (1..max). Use with onChange */
6
+ value?: number;
7
+ /** Uncontrolled initial value */
8
+ defaultValue?: number;
9
+ /** Maximum icons shown */
10
+ max?: number;
11
+ /** Disable interaction (keeps semantics) */
12
+ disabled?: boolean;
13
+ /** Presentational readOnly (no form semantics) */
14
+ readOnly?: boolean;
15
+ /** Name for the radio group (if you care about form posts) */
16
+ name?: string;
17
+ /** Size maps to icon + spacing */
18
+ size?: Size;
19
+ /** Theme feeds foreground color tokens */
20
+ theme?: Theme;
21
+ /** Called on change (controlled or uncontrolled) */
22
+ onChange?: (value: number) => void;
23
+ /** Allow clicking the current selection to clear back to 0 */
24
+ allowClear?: boolean;
25
+ className?: string;
26
+ /** Gap override (e.g. '0.25rem') – otherwise uses density utilities */
27
+ gap?: string;
28
+ /** Accessible label for the rating group. Defaults to "Rating: X of Y" */
29
+ ariaLabel?: string;
30
+ };
31
+ export declare function Rating({ value, defaultValue, max, disabled, readOnly, name, size, theme, onChange, allowClear, className, gap, ariaLabel, }: RatingProps): import("react/jsx-runtime").JSX.Element;
32
+ export {};