@inkra/react 0.1.0 → 0.3.0

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 (177) hide show
  1. package/README.md +186 -1
  2. package/dist/PencilIcon.js +14 -16
  3. package/dist/PencilLine.js +18 -12
  4. package/dist/PencilText.js +192 -59
  5. package/dist/alert-dialog/index.d.ts +64 -0
  6. package/dist/alert-dialog/index.js +111 -0
  7. package/dist/audio-player/index.d.ts +60 -0
  8. package/dist/audio-player/index.js +101 -0
  9. package/dist/carousel/index.d.ts +51 -0
  10. package/dist/carousel/index.js +290 -0
  11. package/dist/choice-grid/index.d.ts +38 -0
  12. package/dist/choice-grid/index.js +85 -0
  13. package/dist/color-picker/color.d.ts +15 -0
  14. package/dist/color-picker/color.js +55 -0
  15. package/dist/color-picker/index.d.ts +31 -0
  16. package/dist/color-picker/index.js +69 -0
  17. package/dist/command/index.d.ts +45 -0
  18. package/dist/command/index.js +39 -0
  19. package/dist/context-menu/index.d.ts +76 -0
  20. package/dist/context-menu/index.js +241 -0
  21. package/dist/data-table/index.d.ts +47 -0
  22. package/dist/data-table/index.js +91 -0
  23. package/dist/date-picker/index.d.ts +50 -0
  24. package/dist/date-picker/index.js +180 -0
  25. package/dist/display/ProgressRing.d.ts +6 -0
  26. package/dist/display/ProgressRing.js +146 -0
  27. package/dist/display/index.d.ts +27 -0
  28. package/dist/display/index.js +18 -2
  29. package/dist/dockable-panel/index.d.ts +41 -0
  30. package/dist/dockable-panel/index.js +253 -0
  31. package/dist/dockable-panel/model.d.ts +21 -0
  32. package/dist/dockable-panel/model.js +61 -0
  33. package/dist/drawer/index.d.ts +63 -0
  34. package/dist/drawer/index.js +88 -0
  35. package/dist/dropdown-menu/index.d.ts +1 -1
  36. package/dist/dropdown-menu/index.js +3 -2
  37. package/dist/editable/index.d.ts +53 -0
  38. package/dist/editable/index.js +162 -0
  39. package/dist/empty/index.d.ts +21 -0
  40. package/dist/empty/index.js +23 -0
  41. package/dist/flip/index.d.ts +49 -0
  42. package/dist/flip/index.js +138 -0
  43. package/dist/form/EditablePencilText.js +188 -143
  44. package/dist/form/Input.js +1 -1
  45. package/dist/form/Textarea.d.ts +12 -0
  46. package/dist/form/Textarea.js +83 -4
  47. package/dist/form/index.d.ts +1 -1
  48. package/dist/heatmap/index.d.ts +59 -0
  49. package/dist/heatmap/index.js +248 -0
  50. package/dist/hotkey/index.d.ts +37 -0
  51. package/dist/hotkey/index.js +130 -0
  52. package/dist/hotkey/model.d.ts +27 -0
  53. package/dist/hotkey/model.js +97 -0
  54. package/dist/hover-card/index.d.ts +33 -0
  55. package/dist/hover-card/index.js +141 -0
  56. package/dist/image-cropper/index.d.ts +82 -0
  57. package/dist/image-cropper/index.js +261 -0
  58. package/dist/image-cropper/model.d.ts +42 -0
  59. package/dist/image-cropper/model.js +94 -0
  60. package/dist/image-viewer/index.d.ts +79 -0
  61. package/dist/image-viewer/index.js +220 -0
  62. package/dist/image-viewer/model.d.ts +9 -0
  63. package/dist/image-viewer/model.js +18 -0
  64. package/dist/index.d.ts +47 -0
  65. package/dist/index.js +47 -0
  66. package/dist/input-number/index.d.ts +6 -0
  67. package/dist/input-number/index.js +126 -15
  68. package/dist/input-number/model.d.ts +13 -0
  69. package/dist/input-number/model.js +50 -0
  70. package/dist/internal/cache.d.ts +1 -1
  71. package/dist/internal/cache.js +1 -1
  72. package/dist/internal/renderTokens.d.ts +10 -0
  73. package/dist/internal/renderTokens.js +13 -0
  74. package/dist/internal/surfaceRenderer.d.ts +14 -6
  75. package/dist/internal/surfaceRenderer.js +15 -12
  76. package/dist/internal/textRenderer.d.ts +1 -0
  77. package/dist/internal/textRenderer.js +33 -7
  78. package/dist/internal/usePencilSurface.js +16 -8
  79. package/dist/internal/useSelectionDrawing.js +15 -11
  80. package/dist/journey-map/MapScale.js +3 -1
  81. package/dist/journey-map/index.js +3 -1
  82. package/dist/keypad/index.d.ts +23 -0
  83. package/dist/keypad/index.js +63 -0
  84. package/dist/keypad/model.d.ts +20 -0
  85. package/dist/keypad/model.js +33 -0
  86. package/dist/layout/index.d.ts +63 -0
  87. package/dist/layout/index.js +62 -0
  88. package/dist/log/index.d.ts +45 -0
  89. package/dist/log/index.js +159 -0
  90. package/dist/log/model.d.ts +16 -0
  91. package/dist/log/model.js +13 -0
  92. package/dist/node-graph/index.d.ts +123 -0
  93. package/dist/node-graph/index.js +304 -0
  94. package/dist/number-ticker/index.d.ts +31 -0
  95. package/dist/number-ticker/index.js +135 -0
  96. package/dist/number-ticker/model.d.ts +41 -0
  97. package/dist/number-ticker/model.js +163 -0
  98. package/dist/number-ticker/precision.d.ts +7 -0
  99. package/dist/number-ticker/precision.js +134 -0
  100. package/dist/overflow-list/index.d.ts +39 -0
  101. package/dist/overflow-list/index.js +230 -0
  102. package/dist/overflow-list/layout.d.ts +19 -0
  103. package/dist/overflow-list/layout.js +44 -0
  104. package/dist/overlay/Overlay.d.ts +1 -1
  105. package/dist/overlay/Overlay.js +4 -2
  106. package/dist/overlay/Popover.d.ts +1 -1
  107. package/dist/overlay/Popover.js +3 -2
  108. package/dist/overlay/themeBridge.d.ts +12 -1
  109. package/dist/overlay/themeBridge.js +27 -1
  110. package/dist/pagination/index.d.ts +23 -0
  111. package/dist/pagination/index.js +41 -0
  112. package/dist/pagination/model.d.ts +4 -0
  113. package/dist/pagination/model.js +42 -0
  114. package/dist/pan-zoom/index.d.ts +71 -0
  115. package/dist/pan-zoom/index.js +175 -0
  116. package/dist/radar/index.d.ts +56 -0
  117. package/dist/radar/index.js +220 -0
  118. package/dist/range-slider/index.d.ts +16 -0
  119. package/dist/range-slider/index.js +22 -0
  120. package/dist/rating/index.d.ts +28 -0
  121. package/dist/rating/index.js +127 -0
  122. package/dist/resizable/index.d.ts +32 -0
  123. package/dist/resizable/index.js +50 -0
  124. package/dist/scroll-area/index.d.ts +36 -0
  125. package/dist/scroll-area/index.js +44 -0
  126. package/dist/segmented-progress/SegmentedProgress.d.ts +17 -0
  127. package/dist/segmented-progress/SegmentedProgress.js +42 -0
  128. package/dist/slider/index.js +1 -1
  129. package/dist/slot/index.d.ts +24 -0
  130. package/dist/slot/index.js +29 -0
  131. package/dist/sortable/index.d.ts +50 -0
  132. package/dist/sortable/index.js +283 -0
  133. package/dist/sortable/model.d.ts +30 -0
  134. package/dist/sortable/model.js +64 -0
  135. package/dist/sparkline/index.d.ts +19 -0
  136. package/dist/sparkline/index.js +240 -0
  137. package/dist/sparkline/model.d.ts +16 -0
  138. package/dist/sparkline/model.js +28 -0
  139. package/dist/steps/index.d.ts +16 -0
  140. package/dist/steps/index.js +37 -0
  141. package/dist/styles.css +4957 -269
  142. package/dist/tag-input/index.d.ts +69 -0
  143. package/dist/tag-input/index.js +325 -0
  144. package/dist/tag-input/model.d.ts +14 -0
  145. package/dist/tag-input/model.js +38 -0
  146. package/dist/time-picker/index.d.ts +15 -0
  147. package/dist/time-picker/index.js +168 -0
  148. package/dist/timeline/index.d.ts +14 -0
  149. package/dist/timeline/index.js +16 -0
  150. package/dist/toast/index.d.ts +27 -0
  151. package/dist/toast/index.js +36 -0
  152. package/dist/toggle-group/index.d.ts +17 -0
  153. package/dist/toggle-group/index.js +26 -0
  154. package/dist/tokens.css +10 -9
  155. package/dist/tooltip/Tooltip.d.ts +1 -1
  156. package/dist/tooltip/Tooltip.js +3 -2
  157. package/dist/transfer/index.d.ts +64 -0
  158. package/dist/transfer/index.js +174 -0
  159. package/dist/transfer/model.d.ts +12 -0
  160. package/dist/transfer/model.js +33 -0
  161. package/dist/tree/index.d.ts +39 -0
  162. package/dist/tree/index.js +36 -0
  163. package/dist/tree-select/index.d.ts +59 -0
  164. package/dist/tree-select/index.js +142 -0
  165. package/dist/tree-select/model.d.ts +31 -0
  166. package/dist/tree-select/model.js +135 -0
  167. package/dist/typewriter/index.d.ts +21 -0
  168. package/dist/typewriter/index.js +101 -0
  169. package/dist/typewriter/model.d.ts +7 -0
  170. package/dist/typewriter/model.js +45 -0
  171. package/dist/upload/index.d.ts +59 -0
  172. package/dist/upload/index.js +230 -0
  173. package/dist/virtual-list/index.d.ts +44 -0
  174. package/dist/virtual-list/index.js +129 -0
  175. package/dist/virtual-list/model.d.ts +7 -0
  176. package/dist/virtual-list/model.js +27 -0
  177. package/package.json +23 -3
@@ -0,0 +1,163 @@
1
+ import { ExactDecimal, formatExactParts, tickerDecimal } from './precision.js';
2
+ export { subtractTickerValues, tickerValueKey } from './precision.js';
3
+ export const numberTickerUnits = {
4
+ en: ['K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'Dc']
5
+ .map((label, index) => ({ label, factor: Number(`1e${(index + 1) * 3}`) })),
6
+ zh: [{ label: '千', factor: 1e3 }, ...['万', '亿', '兆', '京', '垓', '秭', '穰', '沟', '涧', '正', '载', '极', '恒河沙', '阿僧祇', '那由他', '不可思议', '无量大数']
7
+ .map((label, index) => ({ label, factor: Number(`1e${(index + 1) * 4}`) }))],
8
+ };
9
+ export function createNumberFormatter(locale = 'en-US', options) {
10
+ const formatter = new Intl.NumberFormat(locale, options);
11
+ if (options?.roundingMode && options.roundingMode !== 'halfExpand' && !('roundingMode' in formatter.resolvedOptions())) {
12
+ throw new RangeError('NumberTicker roundingMode requires Intl.NumberFormat rounding support');
13
+ }
14
+ return formatter;
15
+ }
16
+ export function createTickerFormatter(locale = 'en-US', options, compact = false, compactOptions = {}) {
17
+ if (!compact || options?.notation === 'scientific' || options?.notation === 'engineering') {
18
+ const native = createNumberFormatter(locale, options);
19
+ const formatToParts = (value) => formatExactParts(native, value);
20
+ return { formatToParts, formatDecimal: value => formatExactParts(native, value), format: value => formatToParts(value).map(part => part.value).join('') };
21
+ }
22
+ const precision = {
23
+ minimumFractionDigits: 0,
24
+ maximumFractionDigits: Math.max(1, options?.minimumFractionDigits ?? 0),
25
+ ...options,
26
+ notation: 'standard',
27
+ };
28
+ const formatter = createNumberFormatter(locale, precision);
29
+ const language = compact === true
30
+ ? (formatter.resolvedOptions().locale.startsWith('zh') ? 'zh' : 'en')
31
+ : compact;
32
+ const multiplier = tickerDecimal(compactOptions.promotionThreshold ?? 1);
33
+ if (!multiplier.gt(0) || !multiplier.isFinite())
34
+ throw new RangeError('promotionThreshold must be positive and finite');
35
+ const supplied = [...(compactOptions.units ?? numberTickerUnits[language])]
36
+ .map(unit => ({ ...unit, factor: tickerDecimal(unit.factor), threshold: unit.threshold === undefined ? undefined : tickerDecimal(unit.threshold) }))
37
+ .sort((a, b) => a.factor.cmp(b.factor));
38
+ const labels = new Set();
39
+ let previousFactor = tickerDecimal(1);
40
+ let previousThreshold = tickerDecimal(0);
41
+ for (const unit of supplied) {
42
+ const threshold = unit.threshold ?? unit.factor.times(multiplier);
43
+ if (!unit.label.trim() || labels.has(unit.label) || !unit.factor.isFinite() || unit.factor.lte(previousFactor)
44
+ || !threshold.isFinite() || threshold.lte(previousThreshold)) {
45
+ throw new RangeError('Compact units need unique nonempty labels, increasing finite factors > 1 and positive increasing thresholds');
46
+ }
47
+ labels.add(unit.label);
48
+ previousFactor = unit.factor;
49
+ previousThreshold = threshold;
50
+ }
51
+ const units = [{ factor: tickerDecimal(1), label: '', threshold: undefined }, ...supplied];
52
+ const indexFor = (label, fallback) => {
53
+ if (label === undefined)
54
+ return fallback;
55
+ const index = units.findIndex(unit => unit.label === label);
56
+ if (index < 0)
57
+ throw new RangeError(`Unknown compact unit: ${label}`);
58
+ return index;
59
+ };
60
+ const minIndex = indexFor(compactOptions.minUnit, 0);
61
+ const maxIndex = indexFor(compactOptions.maxUnit, units.length - 1);
62
+ if (minIndex > maxIndex)
63
+ throw new RangeError('minUnit must not exceed maxUnit');
64
+ const thresholdFor = (index) => units[index].threshold ?? units[index].factor.times(multiplier);
65
+ const last = units[maxIndex];
66
+ const overflowThreshold = maxIndex + 1 < units.length ? thresholdFor(maxIndex + 1)
67
+ : thresholdFor(maxIndex).times(maxIndex > 0 ? last.factor.div(units[maxIndex - 1].factor) : 1000);
68
+ const overflow = compactOptions.overflow ?? (compactOptions.maxUnit !== undefined ? 'unit' : 'scientific');
69
+ const scientific = createNumberFormatter(locale, { ...precision, notation: 'scientific' });
70
+ const rounding = createNumberFormatter('en-US', {
71
+ ...precision, style: 'decimal', numberingSystem: 'latn', useGrouping: false,
72
+ });
73
+ const percentScale = options?.style === 'percent' ? 100 : 1;
74
+ const formatDecimal = (exact) => {
75
+ if (!exact.isFinite())
76
+ return formatExactParts(formatter, exact);
77
+ let index = minIndex;
78
+ const reaches = (threshold) => {
79
+ if (compactOptions.promoteAfterRounding === false)
80
+ return exact.abs().times(percentScale).gte(threshold);
81
+ const scaled = exact.div(units[index].factor).times(percentScale);
82
+ const rounded = formatExactParts(rounding, scaled).map(part => part.value).join('');
83
+ return new ExactDecimal(rounded).abs().times(units[index].factor).gte(threshold);
84
+ };
85
+ while (index < maxIndex) {
86
+ if (!reaches(thresholdFor(index + 1)))
87
+ break;
88
+ index++;
89
+ }
90
+ if (index === maxIndex && overflow === 'scientific' && reaches(overflowThreshold))
91
+ return formatExactParts(scientific, exact);
92
+ const { factor, label } = units[index];
93
+ const parts = formatExactParts(formatter, exact.div(factor));
94
+ if (label) {
95
+ let insertion = parts.length;
96
+ while (insertion > 0 && !['integer', 'fraction'].includes(parts[insertion - 1].type))
97
+ insertion--;
98
+ const separator = compactOptions.separator ?? ' ';
99
+ parts.splice(insertion, 0, ...(separator ? [{ type: 'literal', value: separator }] : []), { type: 'compact', value: label });
100
+ }
101
+ return parts;
102
+ };
103
+ const formatToParts = (value) => formatDecimal(tickerDecimal(value));
104
+ return { formatToParts, formatDecimal, format: value => formatToParts(value).map(part => part.value).join('') };
105
+ }
106
+ export function formatTickerValue(formatter, value) {
107
+ return formatter instanceof Intl.NumberFormat ? formatExactParts(formatter, value).map(part => part.value).join('') : formatter.format(value);
108
+ }
109
+ export function formatTickerDelta(formatter, delta) {
110
+ const exact = tickerDecimal(delta);
111
+ return formatDecimalDelta(formatter, exact);
112
+ }
113
+ // Deltas are derived from validated inputs and can legitimately exceed input bounds.
114
+ export function formatDerivedTickerDelta(formatter, delta) {
115
+ return formatDecimalDelta(formatter, new ExactDecimal(String(delta)));
116
+ }
117
+ function formatDecimalDelta(formatter, exact) {
118
+ const sign = exact.gt(0) ? '+' : exact.lt(0) ? '\u2212' : '';
119
+ const magnitude = (formatter instanceof Intl.NumberFormat ? formatExactParts(formatter, exact) : formatter.formatDecimal(exact))
120
+ .filter(part => part.type !== 'plusSign' && part.type !== 'minusSign')
121
+ .filter(part => part.type !== 'literal' || !/^[()]$/.test(part.value))
122
+ .map(part => part.value)
123
+ .join('');
124
+ return `${sign}${magnitude}`;
125
+ }
126
+ export function interpolateTickerValue(from, to, progress) {
127
+ if (!tickerDecimal(from).isFinite() || !tickerDecimal(to).isFinite())
128
+ return to;
129
+ const boundedProgress = Number.isFinite(progress) ? Math.min(1, Math.max(0, progress)) : 1;
130
+ if (boundedProgress === 0)
131
+ return from;
132
+ if (boundedProgress === 1)
133
+ return to;
134
+ if (typeof from === 'number' && typeof to === 'number') {
135
+ const distance = to - from;
136
+ return Number.isFinite(distance) ? from + distance * boundedProgress
137
+ : from * (1 - boundedProgress) + to * boundedProgress;
138
+ }
139
+ const start = tickerDecimal(from);
140
+ const next = start.plus(tickerDecimal(to).minus(start).times(String(boundedProgress)));
141
+ return typeof from === 'bigint' && typeof to === 'bigint' ? BigInt(next.trunc().toFixed(0)) : next.toString();
142
+ }
143
+ export function tickerValueChanged(previous, next) {
144
+ if (Object.is(previous, next))
145
+ return false;
146
+ const before = tickerDecimal(previous);
147
+ const after = tickerDecimal(next);
148
+ return !before.eq(after) || (before.isZero() && before.isNegative() !== after.isNegative());
149
+ }
150
+ export function canAnimateTickerValue(from, to, duration) {
151
+ const start = tickerDecimal(from);
152
+ const end = tickerDecimal(to);
153
+ return start.isFinite() && end.isFinite()
154
+ && typeof from === typeof to
155
+ && (typeof to === 'number' || (Math.max(start.sd(), end.sd(), Math.abs(start.e - end.e)) <= 512))
156
+ && Number.isFinite(duration)
157
+ && duration > 0
158
+ && tickerValueChanged(from, to);
159
+ }
160
+ export function easeOutCubic(progress) {
161
+ const boundedProgress = Math.min(1, Math.max(0, progress));
162
+ return 1 - (1 - boundedProgress) ** 3;
163
+ }
@@ -0,0 +1,7 @@
1
+ import { Decimal } from 'decimal.js';
2
+ export type NumberTickerValue = number | bigint | string;
3
+ export declare const ExactDecimal: typeof Decimal;
4
+ export declare function tickerDecimal(value: NumberTickerValue): Decimal;
5
+ export declare function tickerValueKey(value: unknown): string;
6
+ export declare function formatExactParts(formatter: Intl.NumberFormat, value: NumberTickerValue | Decimal): Intl.NumberFormatPart[];
7
+ export declare function subtractTickerValues(next: NumberTickerValue, previous: NumberTickerValue): NumberTickerValue | null;
@@ -0,0 +1,134 @@
1
+ import { Decimal } from 'decimal.js';
2
+ // Isolated arithmetic context; never change the application's Decimal defaults.
3
+ // Covers the full span of the bounded input exponents and significant digits.
4
+ export const ExactDecimal = Decimal.clone({ precision: 30050, toExpNeg: -7, toExpPos: 21 });
5
+ const decimalPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
6
+ const maxDigits = 10000;
7
+ export function tickerDecimal(value) {
8
+ const source = typeof value === 'number' && Object.is(value, -0) ? '-0' : String(value).trim();
9
+ if (typeof value !== 'number') {
10
+ const [coefficient, exponent = '0'] = source.split(/[eE]/);
11
+ if (!decimalPattern.test(source) || coefficient.replace(/\D/g, '').length > maxDigits
12
+ || exponent.length > 7 || Math.abs(Number(exponent)) > maxDigits) {
13
+ throw new RangeError('NumberTicker requires a decimal value with at most 10000 digits and an exponent between -10000 and 10000');
14
+ }
15
+ }
16
+ const decimal = new ExactDecimal(source);
17
+ if (decimal.isFinite() && Math.abs(decimal.e) > maxDigits) {
18
+ throw new RangeError('NumberTicker decimal magnitude exceeds the supported exponent range');
19
+ }
20
+ return decimal;
21
+ }
22
+ export function tickerValueKey(value) {
23
+ return JSON.stringify(value, (_key, item) => typeof item === 'bigint' ? { $bigint: String(item) } : item);
24
+ }
25
+ const roundingModes = {
26
+ ceil: Decimal.ROUND_CEIL, floor: Decimal.ROUND_FLOOR,
27
+ expand: Decimal.ROUND_UP, trunc: Decimal.ROUND_DOWN,
28
+ halfCeil: Decimal.ROUND_HALF_CEIL, halfFloor: Decimal.ROUND_HALF_FLOOR,
29
+ halfExpand: Decimal.ROUND_HALF_UP, halfTrunc: Decimal.ROUND_HALF_DOWN,
30
+ halfEven: Decimal.ROUND_HALF_EVEN,
31
+ };
32
+ function roundedDecimal(value, options) {
33
+ const extra = options;
34
+ const mode = roundingModes[extra.roundingMode ?? 'halfExpand'];
35
+ const maxFraction = options.maximumFractionDigits ?? 0;
36
+ const increment = new ExactDecimal(extra.roundingIncrement ?? 1).times(new ExactDecimal(10).pow(-maxFraction));
37
+ const fraction = value.div(increment).toDecimalPlaces(0, mode).times(increment);
38
+ const significant = options.maximumSignificantDigits === undefined ? null : value.toSignificantDigits(options.maximumSignificantDigits, mode);
39
+ const significantPlace = value.e - (options.maximumSignificantDigits ?? 1) + 1;
40
+ const useSignificant = significant !== null && (extra.roundingPriority === 'morePrecision'
41
+ ? significantPlace < -maxFraction : extra.roundingPriority === 'lessPrecision'
42
+ ? significantPlace >= -maxFraction : true);
43
+ const rounded = useSignificant ? significant : fraction;
44
+ const decimals = extra.trailingZeroDisplay === 'stripIfInteger' && rounded.isInteger() ? 0
45
+ : useSignificant ? Math.max(0, (options.minimumSignificantDigits ?? 1) - rounded.e - 1)
46
+ : options.minimumFractionDigits ?? 0;
47
+ return { value: rounded, decimals };
48
+ }
49
+ // Intl accepts exact decimal strings in modern engines, but some engines still
50
+ // turn strings beyond Number.MAX_VALUE into Infinity. BigInt bypasses that path.
51
+ export function formatExactParts(formatter, value) {
52
+ if (typeof value === 'number')
53
+ return formatter.formatToParts(value);
54
+ const decimal = Decimal.isDecimal(value) ? value : tickerDecimal(value);
55
+ if (!decimal.isFinite())
56
+ return formatter.formatToParts(decimal.toNumber());
57
+ if (decimal.isZero())
58
+ return formatter.formatToParts(decimal.isNegative() ? -0 : 0);
59
+ if (decimal.isInteger())
60
+ return formatter.formatToParts(BigInt(decimal.toFixed(0)));
61
+ if (decimal.e < 308) {
62
+ const exactFormat = formatter.formatToParts;
63
+ const probe = new Intl.NumberFormat('en-US', { useGrouping: false });
64
+ if (probe.format('9007199254740993') !== '9007199254740993') {
65
+ throw new RangeError('NumberTicker decimal strings require exact Intl.NumberFormat string support');
66
+ }
67
+ return exactFormat.call(formatter, decimal.toString());
68
+ }
69
+ const options = formatter.resolvedOptions();
70
+ // Directed rounding still observes tiny tails at huge magnitudes. Round the
71
+ // mantissa before converting to BigInt, then let Intl supply locale notation.
72
+ if (options.notation === 'scientific' || options.notation === 'engineering') {
73
+ const display = options.style === 'percent' ? decimal.times(100) : decimal;
74
+ const exponent = options.notation === 'engineering' ? Math.floor(display.e / 3) * 3 : display.e;
75
+ const factor = new ExactDecimal(10).pow(exponent);
76
+ const rounded = roundedDecimal(display.div(factor), options).value.times(factor);
77
+ const originalScale = options.style === 'percent' ? rounded.div(100) : rounded;
78
+ return formatter.formatToParts(BigInt(originalScale.toFixed(0)));
79
+ }
80
+ let scaled = options.style === 'percent' ? decimal.times(100) : decimal;
81
+ if (options.notation === 'compact') {
82
+ const probe = new Intl.NumberFormat(options.locale, { ...options, style: 'decimal', useGrouping: false });
83
+ const digits = probe.formatToParts(BigInt(new ExactDecimal(10).pow(scaled.e).toFixed(0)))
84
+ .filter(part => part.type === 'integer').map(part => Array.from(part.value).length).reduce((sum, length) => sum + length, 0);
85
+ scaled = scaled.div(new ExactDecimal(10).pow(scaled.e - digits + 1));
86
+ }
87
+ const rounded = roundedDecimal(scaled, options);
88
+ const [integer, fraction = ''] = rounded.value.abs().toFixed().split('.');
89
+ const fractionalDigits = fraction.padEnd(rounded.decimals, '0');
90
+ const plain = new Intl.NumberFormat(options.locale, {
91
+ numberingSystem: options.numberingSystem, minimumIntegerDigits: options.minimumIntegerDigits,
92
+ useGrouping: options.useGrouping, maximumFractionDigits: 0,
93
+ });
94
+ const numericParts = plain.formatToParts(BigInt(integer));
95
+ if (fractionalDigits) {
96
+ const digits = new Intl.NumberFormat(options.locale, { numberingSystem: options.numberingSystem, useGrouping: false });
97
+ const symbols = Array.from({ length: 10 }, (_, digit) => digits.format(digit));
98
+ numericParts.push({ type: 'decimal', value: digits.formatToParts(1.1).find(part => part.type === 'decimal').value }, { type: 'fraction', value: fractionalDigits.replace(/\d/g, digit => symbols[Number(digit)]) });
99
+ }
100
+ const pattern = formatter.formatToParts(BigInt(decimal.trunc().toFixed(0)));
101
+ // Long unit/currency names depend on the displayed fraction as well as the
102
+ // integer ending. Preserve plural operands without passing an overflowing
103
+ // decimal string to Intl (CLDR cardinal rules inspect at most six end digits).
104
+ const pluralParts = options.style === 'unit' || (options.style === 'currency' && options.currencyDisplay === 'name')
105
+ ? formatExactParts(new Intl.NumberFormat(options.locale, {
106
+ numberingSystem: options.numberingSystem, style: options.style,
107
+ unit: options.unit, unitDisplay: options.unitDisplay,
108
+ currency: options.currency, currencyDisplay: options.currencyDisplay,
109
+ minimumFractionDigits: fractionalDigits.length, maximumFractionDigits: fractionalDigits.length,
110
+ }), `${BigInt(integer) > 1000000n ? 1000000n + BigInt(integer) % 1000000n : BigInt(integer)}${fractionalDigits ? `.${fractionalDigits}` : ''}`)
111
+ : undefined;
112
+ let inserted = false;
113
+ return pattern.flatMap(part => {
114
+ if (part.type === 'unit' || part.type === 'currency') {
115
+ return [pluralParts?.find(candidate => candidate.type === part.type) ?? part];
116
+ }
117
+ if (!['integer', 'group', 'decimal', 'fraction'].includes(part.type))
118
+ return [part];
119
+ if (inserted)
120
+ return [];
121
+ inserted = true;
122
+ return numericParts;
123
+ });
124
+ }
125
+ export function subtractTickerValues(next, previous) {
126
+ const to = tickerDecimal(next);
127
+ const from = tickerDecimal(previous);
128
+ if (!to.isFinite() || !from.isFinite())
129
+ return null;
130
+ if (typeof next === 'number' && typeof previous === 'number')
131
+ return next - previous;
132
+ const difference = to.minus(from);
133
+ return typeof next === 'bigint' && typeof previous === 'bigint' ? BigInt(difference.toFixed(0)) : difference.toString();
134
+ }
@@ -0,0 +1,39 @@
1
+ import type { CSSProperties, HTMLAttributes, ReactElement, ReactNode, RefAttributes } from 'react';
2
+ import type { ButtonProps } from '../Button.js';
3
+ import type { OverflowListStrategy } from './layout.js';
4
+ export type { OverflowListStrategy } from './layout.js';
5
+ export interface OverflowListItem {
6
+ id: string | number;
7
+ label: ReactNode;
8
+ disabled?: boolean;
9
+ priority?: number;
10
+ measureKey?: string | number;
11
+ }
12
+ export interface OverflowListRenderContext {
13
+ id: string | number;
14
+ index: number;
15
+ }
16
+ export interface OverflowListOverflowContext<Item extends OverflowListItem> {
17
+ count: number;
18
+ items: readonly Item[];
19
+ }
20
+ export interface OverflowListChange<Item extends OverflowListItem> {
21
+ visibleItems: readonly Item[];
22
+ overflowItems: readonly Item[];
23
+ }
24
+ type OverflowButtonProps = Omit<ButtonProps, 'aria-label' | 'children' | 'ref'>;
25
+ export interface OverflowListProps<Item extends OverflowListItem = OverflowListItem> extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
26
+ items: readonly Item[];
27
+ strategy?: OverflowListStrategy;
28
+ measurementKey?: string | number;
29
+ gap?: CSSProperties['gap'];
30
+ renderItem?: (item: Item, context: OverflowListRenderContext) => ReactNode;
31
+ renderOverflowItem?: (item: Item, context: OverflowListRenderContext) => ReactNode;
32
+ renderOverflow?: (context: OverflowListOverflowContext<Item>) => ReactNode;
33
+ getOverflowItemIcon?: (item: Item, context: OverflowListRenderContext) => ReactNode;
34
+ overflowLabel?: string | ((count: number) => string);
35
+ overflowButtonProps?: OverflowButtonProps;
36
+ onOverflowItemSelect?: (item: Item, event: Event) => void;
37
+ onOverflowChange?: (change: OverflowListChange<Item>) => void;
38
+ }
39
+ export declare const OverflowList: <Item extends OverflowListItem = OverflowListItem>(props: OverflowListProps<Item> & RefAttributes<HTMLDivElement>) => ReactElement;
@@ -0,0 +1,230 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react';
3
+ import { Button } from '../Button.js';
4
+ import { DropdownMenu } from '../dropdown-menu/index.js';
5
+ import { PencilIcon } from '../PencilIcon.js';
6
+ import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
7
+ import { calculateOverflowListLayout } from './layout.js';
8
+ const ellipsisPaths = [{ d: 'M5 12h.01M12 12h.01M19 12h.01' }];
9
+ function itemKey(id) {
10
+ return `${typeof id}:${String(id)}`;
11
+ }
12
+ function finitePriority(value) {
13
+ if (value === Number.POSITIVE_INFINITY)
14
+ return Number.MAX_SAFE_INTEGER;
15
+ if (value === Number.NEGATIVE_INFINITY)
16
+ return Number.MIN_SAFE_INTEGER;
17
+ return Number.isFinite(value) ? value : 0;
18
+ }
19
+ function sameKeys(current, next) {
20
+ if (current.size !== next.length)
21
+ return false;
22
+ return next.every(key => current.has(key));
23
+ }
24
+ function assignRef(ref, value) {
25
+ if (typeof ref === 'function')
26
+ ref(value);
27
+ else if (ref)
28
+ ref.current = value;
29
+ }
30
+ function defaultOverflowLabel(count) {
31
+ return `More items (${count})`;
32
+ }
33
+ function OverflowListInner({ 'aria-label': ariaLabel, className, gap, getOverflowItemIcon, items, measurementKey = '', onOverflowChange, onOverflowItemSelect, overflowButtonProps, overflowLabel = defaultOverflowLabel, renderItem, renderOverflow, renderOverflowItem, role, strategy = 'end', style, ...props }, forwardedRef) {
34
+ const generatedId = useId();
35
+ const rootRef = useRef(null);
36
+ const triggerRef = useRef(null);
37
+ const itemRefs = useRef(new Map());
38
+ const widthCache = useRef(new Map());
39
+ const frameRef = useRef(null);
40
+ const focusOverflowRef = useRef(false);
41
+ const lastChangeRef = useRef('');
42
+ const onOverflowChangeRef = useRef(onOverflowChange);
43
+ onOverflowChangeRef.current = onOverflowChange;
44
+ const entries = useMemo(() => {
45
+ const nextEntries = items.map((item, index) => ({
46
+ item,
47
+ index,
48
+ key: itemKey(item.id),
49
+ priority: finitePriority(item.priority),
50
+ }));
51
+ const duplicateCheck = new Set();
52
+ for (const entry of nextEntries) {
53
+ if (duplicateCheck.has(entry.key)) {
54
+ throw new Error(`OverflowList item ids must be unique. Received duplicate id: ${String(entry.item.id)}`);
55
+ }
56
+ duplicateCheck.add(entry.key);
57
+ }
58
+ return nextEntries;
59
+ }, [items]);
60
+ const signature = useMemo(() => JSON.stringify([
61
+ measurementKey,
62
+ strategy,
63
+ entries.map(entry => [entry.key, entry.item.measureKey ?? entry.key, entry.priority]),
64
+ ]), [entries, measurementKey, strategy]);
65
+ const allKeys = useMemo(() => entries.map(entry => entry.key), [entries]);
66
+ const [layout, setLayout] = useState(() => ({
67
+ signature,
68
+ visibleKeys: new Set(allKeys),
69
+ needsOverflow: false,
70
+ measuring: true,
71
+ }));
72
+ const effectiveLayout = layout.signature === signature
73
+ ? layout
74
+ : { signature, visibleKeys: new Set(allKeys), needsOverflow: false, measuring: true };
75
+ const visibleEntries = useMemo(() => effectiveLayout.measuring
76
+ ? entries
77
+ : entries.filter(entry => effectiveLayout.visibleKeys.has(entry.key)), [effectiveLayout.measuring, effectiveLayout.visibleKeys, entries]);
78
+ const overflowEntries = useMemo(() => effectiveLayout.measuring
79
+ ? []
80
+ : entries.filter(entry => !effectiveLayout.visibleKeys.has(entry.key)), [effectiveLayout.measuring, effectiveLayout.visibleKeys, entries]);
81
+ const setRootRef = useCallback((node) => {
82
+ rootRef.current = node;
83
+ assignRef(forwardedRef, node);
84
+ }, [forwardedRef]);
85
+ const setItemRef = useCallback((key, node) => {
86
+ if (node)
87
+ itemRefs.current.set(key, node);
88
+ else
89
+ itemRefs.current.delete(key);
90
+ }, []);
91
+ const updateLayout = useCallback(() => {
92
+ const root = rootRef.current;
93
+ if (!root)
94
+ return;
95
+ const containerWidth = root.getBoundingClientRect().width;
96
+ if (containerWidth <= 0)
97
+ return;
98
+ const computedGap = Number.parseFloat(getComputedStyle(root).columnGap) || 0;
99
+ for (const entry of entries) {
100
+ const node = itemRefs.current.get(entry.key);
101
+ if (!node)
102
+ continue;
103
+ const width = node.getBoundingClientRect().width;
104
+ if (width > 0)
105
+ widthCache.current.set(entry.key, width);
106
+ }
107
+ if (entries.some(entry => !widthCache.current.has(entry.key)))
108
+ return;
109
+ const triggerWidth = triggerRef.current?.getBoundingClientRect().width;
110
+ const result = calculateOverflowListLayout({
111
+ containerWidth,
112
+ gap: computedGap,
113
+ items: entries.map(entry => ({
114
+ key: entry.key,
115
+ priority: entry.priority,
116
+ width: widthCache.current.get(entry.key) ?? 0,
117
+ })),
118
+ strategy,
119
+ triggerWidth,
120
+ });
121
+ if (result.needsTriggerMeasurement) {
122
+ setLayout(current => current.signature === signature
123
+ && current.needsOverflow
124
+ && current.measuring
125
+ && sameKeys(current.visibleKeys, allKeys)
126
+ ? current
127
+ : { signature, visibleKeys: new Set(allKeys), needsOverflow: true, measuring: true });
128
+ return;
129
+ }
130
+ const activeElement = typeof document === 'undefined' ? null : document.activeElement;
131
+ if (activeElement && result.overflowKeys.some(key => itemRefs.current.get(key)?.contains(activeElement))) {
132
+ focusOverflowRef.current = true;
133
+ }
134
+ setLayout(current => current.signature === signature
135
+ && !current.measuring
136
+ && current.needsOverflow === (result.overflowKeys.length > 0)
137
+ && sameKeys(current.visibleKeys, result.visibleKeys)
138
+ ? current
139
+ : {
140
+ signature,
141
+ visibleKeys: new Set(result.visibleKeys),
142
+ needsOverflow: result.overflowKeys.length > 0,
143
+ measuring: false,
144
+ });
145
+ }, [allKeys, entries, signature, strategy]);
146
+ const scheduleLayout = useCallback(() => {
147
+ if (frameRef.current !== null || typeof window === 'undefined')
148
+ return;
149
+ frameRef.current = window.requestAnimationFrame(() => {
150
+ frameRef.current = null;
151
+ updateLayout();
152
+ });
153
+ }, [updateLayout]);
154
+ useBrowserLayoutEffect(() => {
155
+ if (layout.signature === signature)
156
+ return;
157
+ widthCache.current.clear();
158
+ setLayout({ signature, visibleKeys: new Set(allKeys), needsOverflow: false, measuring: true });
159
+ }, [allKeys, layout.signature, signature]);
160
+ useEffect(() => {
161
+ const root = rootRef.current;
162
+ if (!root)
163
+ return undefined;
164
+ const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(scheduleLayout);
165
+ observer?.observe(root);
166
+ itemRefs.current.forEach(node => observer?.observe(node));
167
+ if (triggerRef.current)
168
+ observer?.observe(triggerRef.current);
169
+ if (!observer)
170
+ window.addEventListener('resize', scheduleLayout);
171
+ scheduleLayout();
172
+ return () => {
173
+ observer?.disconnect();
174
+ window.removeEventListener('resize', scheduleLayout);
175
+ if (frameRef.current !== null) {
176
+ window.cancelAnimationFrame(frameRef.current);
177
+ frameRef.current = null;
178
+ }
179
+ };
180
+ }, [effectiveLayout.measuring, effectiveLayout.needsOverflow, scheduleLayout, signature]);
181
+ useEffect(() => {
182
+ const fonts = typeof document === 'undefined' ? undefined : document.fonts;
183
+ if (!fonts?.addEventListener)
184
+ return undefined;
185
+ const remeasure = () => {
186
+ widthCache.current.clear();
187
+ setLayout({ signature, visibleKeys: new Set(allKeys), needsOverflow: effectiveLayout.needsOverflow, measuring: true });
188
+ };
189
+ fonts.addEventListener('loadingdone', remeasure);
190
+ return () => fonts.removeEventListener('loadingdone', remeasure);
191
+ }, [allKeys, effectiveLayout.needsOverflow, signature]);
192
+ useBrowserLayoutEffect(() => {
193
+ if (!focusOverflowRef.current || !effectiveLayout.needsOverflow || effectiveLayout.measuring)
194
+ return;
195
+ focusOverflowRef.current = false;
196
+ triggerRef.current?.focus();
197
+ }, [effectiveLayout.measuring, effectiveLayout.needsOverflow, effectiveLayout.visibleKeys]);
198
+ useEffect(() => {
199
+ if (effectiveLayout.measuring)
200
+ return;
201
+ const changeKey = `${signature}:${visibleEntries.map(entry => entry.key).join('|')}`;
202
+ if (lastChangeRef.current === changeKey)
203
+ return;
204
+ lastChangeRef.current = changeKey;
205
+ onOverflowChangeRef.current?.({
206
+ visibleItems: visibleEntries.map(entry => entry.item),
207
+ overflowItems: overflowEntries.map(entry => entry.item),
208
+ });
209
+ }, [effectiveLayout.measuring, overflowEntries, signature, visibleEntries]);
210
+ const resolvedLabel = typeof overflowLabel === 'function'
211
+ ? overflowLabel(overflowEntries.length)
212
+ : overflowLabel;
213
+ const overflowContext = useMemo(() => ({
214
+ count: overflowEntries.length,
215
+ items: overflowEntries.map(entry => entry.item),
216
+ }), [overflowEntries]);
217
+ const { className: overflowButtonClassName, size: overflowButtonSize, title: overflowButtonTitle, variant: overflowButtonVariant, ...overflowButtonRest } = overflowButtonProps ?? {};
218
+ const resolvedStyle = {
219
+ ...style,
220
+ ...(gap !== undefined ? { '--inkra-overflow-list-gap': typeof gap === 'number' ? `${gap}px` : String(gap) } : {}),
221
+ };
222
+ return _jsxs("div", { ...props, "aria-label": ariaLabel, className: ['inkra-overflow-list', className].filter(Boolean).join(' '), "data-measuring": effectiveLayout.measuring || undefined, ref: setRootRef, role: role, style: resolvedStyle, children: [visibleEntries.map(({ item, index, key }) => _jsx("div", { className: "inkra-overflow-list__item", "data-overflow-list-item": String(item.id), ref: node => setItemRef(key, node), children: renderItem
223
+ ? renderItem(item, { id: item.id, index })
224
+ : _jsx("span", { className: "inkra-overflow-list__default-item", children: item.label }) }, key)), effectiveLayout.needsOverflow ? _jsxs(DropdownMenu.Root, { children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsx(Button, { ...overflowButtonRest, "aria-label": resolvedLabel, className: ['inkra-overflow-list__trigger', overflowButtonClassName].filter(Boolean).join(' '), ref: triggerRef, size: overflowButtonSize ?? (renderOverflow ? 'sm' : 'icon-sm'), title: overflowButtonTitle ?? resolvedLabel, variant: overflowButtonVariant ?? 'outline', children: renderOverflow
225
+ ? renderOverflow(overflowContext)
226
+ : _jsx(PencilIcon, { paths: ellipsisPaths, seed: `${generatedId}-overflow`, size: 16, sourceSize: 24 }) }) }), _jsx(DropdownMenu.Portal, { children: _jsx(DropdownMenu.Content, { align: "end", "aria-label": resolvedLabel, children: overflowEntries.map(({ item, index, key }) => _jsx(DropdownMenu.Item, { disabled: item.disabled, icon: getOverflowItemIcon?.(item, { id: item.id, index }), onSelect: event => onOverflowItemSelect?.(item, event), children: renderOverflowItem
227
+ ? renderOverflowItem(item, { id: item.id, index })
228
+ : item.label }, key)) }) })] }) : null] });
229
+ }
230
+ export const OverflowList = forwardRef(OverflowListInner);
@@ -0,0 +1,19 @@
1
+ export type OverflowListStrategy = 'end' | 'start' | 'priority';
2
+ export interface OverflowListLayoutItem {
3
+ key: string;
4
+ priority: number;
5
+ width: number;
6
+ }
7
+ export interface OverflowListLayoutInput {
8
+ items: readonly OverflowListLayoutItem[];
9
+ containerWidth: number;
10
+ gap: number;
11
+ strategy: OverflowListStrategy;
12
+ triggerWidth?: number;
13
+ }
14
+ export interface OverflowListLayoutResult {
15
+ visibleKeys: string[];
16
+ overflowKeys: string[];
17
+ needsTriggerMeasurement: boolean;
18
+ }
19
+ export declare function calculateOverflowListLayout({ containerWidth, gap, items, strategy, triggerWidth, }: OverflowListLayoutInput): OverflowListLayoutResult;
@@ -0,0 +1,44 @@
1
+ const finiteSize = (value) => Number.isFinite(value) ? Math.max(0, value) : 0;
2
+ function itemsWidth(items, gap) {
3
+ if (items.length === 0)
4
+ return 0;
5
+ return items.reduce((total, item) => total + finiteSize(item.width), 0)
6
+ + finiteSize(gap) * (items.length - 1);
7
+ }
8
+ function candidateOrder(items, strategy) {
9
+ const indexed = items.map((item, index) => ({ item, index }));
10
+ if (strategy === 'start')
11
+ return indexed.reverse();
12
+ if (strategy === 'priority') {
13
+ return indexed.sort((left, right) => right.item.priority - left.item.priority || left.index - right.index);
14
+ }
15
+ return indexed;
16
+ }
17
+ export function calculateOverflowListLayout({ containerWidth, gap, items, strategy, triggerWidth, }) {
18
+ const availableWidth = finiteSize(containerWidth);
19
+ const resolvedGap = finiteSize(gap);
20
+ const allKeys = items.map(item => item.key);
21
+ if (itemsWidth(items, resolvedGap) <= availableWidth + 0.5) {
22
+ return { visibleKeys: allKeys, overflowKeys: [], needsTriggerMeasurement: false };
23
+ }
24
+ if (triggerWidth === undefined || !Number.isFinite(triggerWidth) || triggerWidth <= 0) {
25
+ return { visibleKeys: allKeys, overflowKeys: [], needsTriggerMeasurement: true };
26
+ }
27
+ const visible = new Set();
28
+ let occupied = finiteSize(triggerWidth);
29
+ for (const { item } of candidateOrder(items, strategy)) {
30
+ const nextWidth = occupied + resolvedGap + finiteSize(item.width);
31
+ if (nextWidth > availableWidth + 0.5) {
32
+ if (strategy === 'priority')
33
+ continue;
34
+ break;
35
+ }
36
+ visible.add(item.key);
37
+ occupied = nextWidth;
38
+ }
39
+ return {
40
+ visibleKeys: allKeys.filter(key => visible.has(key)),
41
+ overflowKeys: allKeys.filter(key => !visible.has(key)),
42
+ needsTriggerMeasurement: false,
43
+ };
44
+ }
@@ -7,7 +7,7 @@ export declare function OverlayRoot(props: OverlayRootProps): import("react").JS
7
7
  export type OverlayTriggerProps = DialogPrimitive.DialogTriggerProps;
8
8
  export declare const OverlayTrigger: import("react").ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
9
9
  export type OverlayPortalProps = DialogPrimitive.DialogPortalProps;
10
- export declare function OverlayPortal({ children, ...props }: OverlayPortalProps): import("react").JSX.Element;
10
+ export declare function OverlayPortal({ children, ...props }: OverlayPortalProps): ReactNode;
11
11
  export type OverlayBackdropProps = DialogPrimitive.DialogOverlayProps;
12
12
  export declare const OverlayBackdrop: import("react").ForwardRefExoticComponent<DialogPrimitive.DialogOverlayProps & import("react").RefAttributes<HTMLDivElement>>;
13
13
  export interface OverlayContentProps extends DialogPrimitive.DialogContentProps {