@astryxdesign/core 0.1.4-canary.39ffd21 → 0.1.4-canary.3d64ef2
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.
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +8 -1
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +31 -11
- package/dist/Calendar/styles.d.ts +2 -2
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +2 -2
- package/dist/Card/Card.d.ts +4 -2
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +15 -7
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +1 -0
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +8 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -1
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +26 -3
- package/dist/CodeBlock/CodeBlock.d.ts +10 -1
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +23 -5
- package/dist/Collapsible/Collapsible.d.ts +9 -3
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +101 -13
- package/dist/Collapsible/CollapsibleGroup.d.ts +39 -7
- package/dist/Collapsible/CollapsibleGroup.d.ts.map +1 -1
- package/dist/Collapsible/CollapsibleGroup.js +70 -9
- package/dist/Collapsible/CollapsibleGroupContext.d.ts +31 -0
- package/dist/Collapsible/CollapsibleGroupContext.d.ts.map +1 -1
- package/dist/Collapsible/CollapsibleGroupContext.js +33 -3
- package/dist/Collapsible/index.d.ts +1 -0
- package/dist/Collapsible/index.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +2 -0
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +2 -0
- package/dist/Grid/Grid.d.ts +4 -3
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +26 -9
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +5 -1
- package/dist/InputGroup/InputGroup.js +2 -2
- package/dist/Item/Item.js +1 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +26 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +32 -16
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +87 -36
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +39 -21
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +2 -1
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +10 -2
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +6 -2
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +34 -7
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +14 -3
- package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
- package/dist/Table/plugins/columnResize/useTableColumnResize.js +5 -2
- package/dist/Table/plugins/pagination/paginateData.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/paginateData.js +5 -1
- package/dist/Table/plugins/selection/useTableSelectionState.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelectionState.js +11 -1
- package/dist/Table/plugins/sortable/useTableSortableState.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortableState.js +8 -4
- package/dist/Thumbnail/Thumbnail.d.ts +3 -2
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +1 -0
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +6 -1
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +11 -3
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +12 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +5 -2
- package/dist/astryx.css +7 -0
- package/dist/astryx.umd.js +48 -48
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/focusableSelector.d.ts +1 -1
- package/dist/hooks/focusableSelector.d.ts.map +1 -1
- package/dist/hooks/focusableSelector.js +1 -1
- package/dist/hooks/useFocusTrap.d.ts +3 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +38 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +10 -3
- package/dist/theme/hct.d.ts +0 -15
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +9 -9
- package/dist/theme/tokens.d.ts +10 -2
- package/dist/theme/tokens.d.ts.map +1 -1
- package/dist/theme/tokens.js +220 -4
- package/dist/utils/color.d.ts +42 -0
- package/dist/utils/color.d.ts.map +1 -0
- package/dist/utils/color.js +150 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +8 -5
- package/package.json +1 -1
- package/src/Avatar/Avatar.doc.mjs +11 -0
- package/src/Banner/Banner.doc.mjs +8 -0
- package/src/Banner/Banner.test.tsx +38 -0
- package/src/Banner/Banner.tsx +8 -1
- package/src/Calendar/Calendar.doc.mjs +1 -1
- package/src/Calendar/Calendar.test.tsx +112 -4
- package/src/Calendar/Calendar.tsx +26 -5
- package/src/Calendar/styles.ts +12 -2
- package/src/Card/Card.tsx +16 -6
- package/src/Carousel/Carousel.doc.mjs +12 -12
- package/src/Carousel/Carousel.test.tsx +13 -0
- package/src/Carousel/Carousel.tsx +1 -0
- package/src/Chat/Chat.doc.mjs +1 -1
- package/src/Chat/ChatToolCalls.test.tsx +55 -0
- package/src/Chat/ChatToolCalls.tsx +10 -12
- package/src/CheckboxInput/CheckboxInput.doc.mjs +3 -3
- package/src/CheckboxInput/CheckboxInput.test.tsx +71 -4
- package/src/CheckboxInput/CheckboxInput.tsx +2 -0
- package/src/Citation/Citation.doc.mjs +5 -0
- package/src/ClickableCard/ClickableCard.tsx +45 -2
- package/src/CodeBlock/CodeBlock.doc.mjs +7 -2
- package/src/CodeBlock/CodeBlock.test.tsx +87 -3
- package/src/CodeBlock/CodeBlock.tsx +43 -3
- package/src/Collapsible/Collapsible.doc.mjs +8 -4
- package/src/Collapsible/Collapsible.tsx +86 -10
- package/src/Collapsible/CollapsibleGroup.doc.mjs +27 -3
- package/src/Collapsible/CollapsibleGroup.test.tsx +288 -0
- package/src/Collapsible/CollapsibleGroup.tsx +107 -9
- package/src/Collapsible/CollapsibleGroupContext.tsx +42 -2
- package/src/Collapsible/index.ts +4 -0
- package/src/CommandPalette/CommandPaletteEmpty.doc.mjs +5 -0
- package/src/CommandPalette/CommandPaletteGroup.doc.mjs +10 -0
- package/src/CommandPalette/CommandPaletteInput.doc.mjs +6 -0
- package/src/CommandPalette/CommandPaletteItem.doc.mjs +6 -0
- package/src/CommandPalette/CommandPaletteList.doc.mjs +9 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +15 -0
- package/src/ContextMenu/ContextMenu.test.tsx +15 -0
- package/src/ContextMenu/ContextMenu.tsx +2 -0
- package/src/ContextMenu/ContextMenuItem.doc.mjs +6 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +70 -0
- package/src/DateTimeInput/DateTimeInput.tsx +2 -0
- package/src/Dialog/DialogHeader.doc.mjs +45 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -1
- package/src/Field/FieldLabel.test.tsx +2 -2
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
- package/src/Grid/Grid.test.tsx +43 -18
- package/src/Grid/Grid.tsx +26 -9
- package/src/Icon/Icon.test.tsx +41 -29
- package/src/Icon/Icon.tsx +6 -2
- package/src/InputGroup/InputGroup.tsx +2 -2
- package/src/Item/Item.tsx +1 -1
- package/src/Layout/LayoutContent.doc.mjs +12 -0
- package/src/Layout/LayoutPanel.doc.mjs +40 -0
- package/src/Lightbox/Lightbox.doc.mjs +5 -0
- package/src/Lightbox/Lightbox.test.tsx +123 -2
- package/src/Lightbox/Lightbox.tsx +29 -1
- package/src/NavMenu/NavMenu.doc.mjs +15 -0
- package/src/NumberInput/NumberInput.test.tsx +48 -3
- package/src/NumberInput/NumberInput.tsx +32 -15
- package/src/Overlay/Overlay.doc.mjs +6 -0
- package/src/Pagination/Pagination.test.tsx +143 -0
- package/src/Pagination/Pagination.tsx +75 -23
- package/src/Popover/Popover.test.tsx +65 -0
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/PowerSearch/PowerSearch.test.tsx +109 -1
- package/src/PowerSearch/PowerSearch.tsx +37 -16
- package/src/ProgressBar/ProgressBar.test.tsx +10 -0
- package/src/ProgressBar/ProgressBar.tsx +2 -1
- package/src/Resizable/ResizeHandle.test.tsx +180 -0
- package/src/Resizable/ResizeHandle.tsx +10 -2
- package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
- package/src/SelectableCard/SelectableCard.doc.mjs +1 -1
- package/src/SideNav/SideNav.doc.mjs +7 -1
- package/src/SideNav/SideNav.test.tsx +20 -0
- package/src/SideNav/SideNav.tsx +6 -2
- package/src/TabList/TabList.test.tsx +185 -1
- package/src/TabList/TabMenu.tsx +45 -9
- package/src/Table/BaseTable.tsx +13 -2
- package/src/Table/Table.test.tsx +51 -0
- package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +13 -0
- package/src/Table/plugins/columnResize/useTableColumnResize.tsx +3 -1
- package/src/Table/plugins/pagination/paginateData.ts +5 -1
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +13 -0
- package/src/Table/plugins/selection/useTableSelectionState.test.tsx +23 -0
- package/src/Table/plugins/selection/useTableSelectionState.tsx +11 -6
- package/src/Table/plugins/sortable/useTableSortableState.test.tsx +35 -1
- package/src/Table/plugins/sortable/useTableSortableState.tsx +8 -4
- package/src/TextArea/TextArea.test.tsx +3 -3
- package/src/TextInput/TextInput.test.tsx +3 -3
- package/src/Thumbnail/Thumbnail.test.tsx +36 -2
- package/src/Thumbnail/Thumbnail.tsx +4 -2
- package/src/TimeInput/TimeInput.test.tsx +37 -0
- package/src/TimeInput/TimeInput.tsx +11 -1
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Toast/Toast.tsx +11 -3
- package/src/Toast/ToastViewport.test.tsx +64 -0
- package/src/Toast/ToastViewport.tsx +12 -0
- package/src/ToggleButton/ToggleButton.doc.mjs +1 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
- package/src/TopNav/TopNav.doc.mjs +1 -1
- package/src/TreeList/TreeList.doc.mjs +2 -2
- package/src/Typeahead/BaseTypeahead.doc.mjs +19 -0
- package/src/Typeahead/BaseTypeahead.tsx +5 -2
- package/src/Typeahead/Typeahead.doc.mjs +1 -1
- package/src/Typeahead/Typeahead.test.tsx +45 -1
- package/src/__tests__/TestIcon.tsx +26 -0
- package/src/docPropReferences.test.ts +178 -0
- package/src/hooks/focusableSelector.ts +1 -1
- package/src/hooks/useFocusTrap.doc.mjs +4 -2
- package/src/hooks/useFocusTrap.test.tsx +149 -1
- package/src/hooks/useFocusTrap.ts +50 -1
- package/src/theme/SyntaxTheme.doc.mjs +4 -4
- package/src/theme/defineTheme.ts +4 -13
- package/src/theme/derivedVarRegistry.test.ts +2 -2
- package/src/theme/expandColorScale.test.ts +29 -0
- package/src/theme/expandColorScale.ts +11 -6
- package/src/theme/hct.ts +9 -17
- package/src/theme/tokens.test.ts +182 -9
- package/src/theme/tokens.ts +239 -4
- package/src/theme/useTheme.test.tsx +20 -0
- package/src/utils/color.test.ts +133 -0
- package/src/utils/color.ts +148 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/timeParser.test.ts +13 -0
- package/src/utils/timeParser.ts +8 -5
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file color.ts
|
|
5
|
+
* @input CSS color strings (hex, rgb()/rgba(), a small set of named colors)
|
|
6
|
+
* @output Shared color parsing/formatting primitives used across the design system
|
|
7
|
+
* @position Package utility; backs theme token resolution, HCT color math, and chart rendering
|
|
8
|
+
*
|
|
9
|
+
* A single home for the color parsers that were previously duplicated across
|
|
10
|
+
* the theme layer, charts, and lab. Consumers get one well-tested definition of
|
|
11
|
+
* "parse this color" rather than several subtly different regexes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A color decomposed into 0-255 RGB channels and a 0-1 alpha. */
|
|
15
|
+
export interface RGBA {
|
|
16
|
+
r: number;
|
|
17
|
+
g: number;
|
|
18
|
+
b: number;
|
|
19
|
+
a: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The named colors the design system relies on in token expressions. */
|
|
23
|
+
const NAMED_COLORS: Record<string, RGBA> = {
|
|
24
|
+
transparent: {r: 0, g: 0, b: 0, a: 0},
|
|
25
|
+
black: {r: 0, g: 0, b: 0, a: 1},
|
|
26
|
+
white: {r: 255, g: 255, b: 255, a: 1},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const clamp = (value: number, min: number, max: number): number =>
|
|
30
|
+
Math.max(min, Math.min(max, value));
|
|
31
|
+
|
|
32
|
+
/** Expand a shorthand hex body (`rgb`/`rgba`) to its full form (`rrggbb`/`rrggbbaa`). */
|
|
33
|
+
function expandShorthand(body: string): string {
|
|
34
|
+
return body
|
|
35
|
+
.split('')
|
|
36
|
+
.map(c => c + c)
|
|
37
|
+
.join('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a hex color into {@link RGBA}. Accepts `#rgb`, `#rgba`, `#rrggbb`, and
|
|
42
|
+
* `#rrggbbaa`, with or without the leading `#`. Returns null for anything else.
|
|
43
|
+
*/
|
|
44
|
+
export function parseHex(hex: string): RGBA | null {
|
|
45
|
+
if (typeof hex !== 'string') {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const body = hex.trim().replace(/^#/, '');
|
|
49
|
+
const normalized =
|
|
50
|
+
body.length === 3 || body.length === 4 ? expandShorthand(body) : body;
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
(normalized.length !== 6 && normalized.length !== 8) ||
|
|
54
|
+
!/^[0-9a-fA-F]+$/.test(normalized)
|
|
55
|
+
) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
r: parseInt(normalized.slice(0, 2), 16),
|
|
61
|
+
g: parseInt(normalized.slice(2, 4), 16),
|
|
62
|
+
b: parseInt(normalized.slice(4, 6), 16),
|
|
63
|
+
a: normalized.length === 8 ? parseInt(normalized.slice(6, 8), 16) / 255 : 1,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse an `rgb()`/`rgba()` color into {@link RGBA}. Accepts comma- or
|
|
69
|
+
* space-separated channels, an optional `/ alpha`, and percentage channels.
|
|
70
|
+
*/
|
|
71
|
+
export function parseRgb(value: string): RGBA | null {
|
|
72
|
+
const open = value.indexOf('(');
|
|
73
|
+
if (open === -1 || !value.trim().endsWith(')')) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const body = value
|
|
77
|
+
.slice(open + 1, value.lastIndexOf(')'))
|
|
78
|
+
.replace(/\//g, ' ');
|
|
79
|
+
const parts = body
|
|
80
|
+
.split(/[\s,]+/)
|
|
81
|
+
.map(p => p.trim())
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
if (parts.length < 3) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const channel = (p: string): number => {
|
|
88
|
+
const n = p.endsWith('%') ? (parseFloat(p) / 100) * 255 : parseFloat(p);
|
|
89
|
+
return clamp(n, 0, 255);
|
|
90
|
+
};
|
|
91
|
+
const r = channel(parts[0]);
|
|
92
|
+
const g = channel(parts[1]);
|
|
93
|
+
const b = channel(parts[2]);
|
|
94
|
+
if ([r, g, b].some(Number.isNaN)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let a = 1;
|
|
99
|
+
if (parts.length >= 4) {
|
|
100
|
+
const raw = parts[3];
|
|
101
|
+
a = raw.endsWith('%') ? parseFloat(raw) / 100 : parseFloat(raw);
|
|
102
|
+
if (Number.isNaN(a)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
a = clamp(a, 0, 1);
|
|
106
|
+
}
|
|
107
|
+
return {r, g, b, a};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse a concrete CSS color string into {@link RGBA}. Supports hex,
|
|
112
|
+
* `rgb()`/`rgba()`, and the named colors used in token expressions. Returns
|
|
113
|
+
* null for anything it can't evaluate (e.g. `var()`, `oklch()`, unknown names)
|
|
114
|
+
* so callers can preserve the original expression rather than guessing.
|
|
115
|
+
*/
|
|
116
|
+
export function parseColor(value: string): RGBA | null {
|
|
117
|
+
const trimmed = value.trim();
|
|
118
|
+
const named = NAMED_COLORS[trimmed.toLowerCase()];
|
|
119
|
+
if (named) {
|
|
120
|
+
return {...named};
|
|
121
|
+
}
|
|
122
|
+
if (trimmed.startsWith('#')) {
|
|
123
|
+
return parseHex(trimmed);
|
|
124
|
+
}
|
|
125
|
+
if (/^rgba?\(/i.test(trimmed)) {
|
|
126
|
+
return parseRgb(trimmed);
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Format RGB channels (0-255) as an uppercase `#RRGGBB` string. */
|
|
132
|
+
export function formatHex(r: number, g: number, b: number): string {
|
|
133
|
+
const channel = (c: number): string =>
|
|
134
|
+
clamp(Math.round(c), 0, 255).toString(16).padStart(2, '0').toUpperCase();
|
|
135
|
+
return `#${channel(r)}${channel(g)}${channel(b)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Serialize {@link RGBA} to a compact, JS-usable CSS color: `#RRGGBB` when
|
|
140
|
+
* fully opaque, otherwise `rgba(r, g, b, a)`.
|
|
141
|
+
*/
|
|
142
|
+
export function formatColor({r, g, b, a}: RGBA): string {
|
|
143
|
+
if (a >= 1) {
|
|
144
|
+
return formatHex(r, g, b);
|
|
145
|
+
}
|
|
146
|
+
const round = (n: number): number => clamp(Math.round(n), 0, 255);
|
|
147
|
+
return `rgba(${round(r)}, ${round(g)}, ${round(b)}, ${parseFloat(a.toFixed(4))})`;
|
|
148
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -81,3 +81,6 @@ export {observeResize, unobserveResize} from './sharedResizeObserver';
|
|
|
81
81
|
export {isRenderable} from './isRenderable';
|
|
82
82
|
export {getInputARIA} from './inputAria';
|
|
83
83
|
export type {InputARIA, InputARIAInputGroup} from './inputAria';
|
|
84
|
+
|
|
85
|
+
export {parseHex, parseRgb, parseColor, formatHex, formatColor} from './color';
|
|
86
|
+
export type {RGBA} from './color';
|
|
@@ -249,6 +249,19 @@ describe('isTimeInRange', () => {
|
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
describe('adjustTime', () => {
|
|
252
|
+
it('returns the input unchanged for non-finite deltas', () => {
|
|
253
|
+
// -Infinity previously spun the wrap-around loop forever;
|
|
254
|
+
// NaN previously produced the corrupt string "NaN:NaN".
|
|
255
|
+
expect(adjustTime('10:00' as ISOTimeString, -Infinity)).toBe('10:00');
|
|
256
|
+
expect(adjustTime('10:00' as ISOTimeString, Infinity)).toBe('10:00');
|
|
257
|
+
expect(adjustTime('10:00' as ISOTimeString, NaN)).toBe('10:00');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('wraps large negative deltas in constant time', () => {
|
|
261
|
+
expect(adjustTime('10:00' as ISOTimeString, -1440 * 1e7)).toBe('10:00');
|
|
262
|
+
expect(adjustTime('10:00' as ISOTimeString, -1440 * 1e7 - 30)).toBe('09:30');
|
|
263
|
+
});
|
|
264
|
+
|
|
252
265
|
it('adds minutes', () => {
|
|
253
266
|
expect(adjustTime('14:30' as ISOTimeString, 15)).toBe('14:45');
|
|
254
267
|
expect(adjustTime('14:30' as ISOTimeString, 30)).toBe('15:00');
|
package/src/utils/timeParser.ts
CHANGED
|
@@ -361,13 +361,16 @@ export function adjustTime(
|
|
|
361
361
|
return time;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
// A non-finite delta would spin the wrap-around forever (-Infinity) or
|
|
365
|
+
// produce "NaN:NaN" (NaN) — return the input unchanged instead.
|
|
366
|
+
if (!Number.isFinite(deltaMinutes)) {
|
|
367
|
+
return time;
|
|
368
|
+
}
|
|
369
|
+
|
|
364
370
|
let totalMinutes = parsed.hour * 60 + parsed.minute + deltaMinutes;
|
|
365
371
|
|
|
366
|
-
// Wrap around midnight
|
|
367
|
-
|
|
368
|
-
totalMinutes += 24 * 60;
|
|
369
|
-
}
|
|
370
|
-
totalMinutes = totalMinutes % (24 * 60);
|
|
372
|
+
// Wrap around midnight (double-modulo handles negatives in O(1))
|
|
373
|
+
totalMinutes = ((totalMinutes % (24 * 60)) + 24 * 60) % (24 * 60);
|
|
371
374
|
|
|
372
375
|
const newHour = Math.floor(totalMinutes / 60);
|
|
373
376
|
const newMinute = totalMinutes % 60;
|