@astryxdesign/core 0.2.0-canary.37626ca → 0.2.0-canary.416db07

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/README.md CHANGED
@@ -75,6 +75,8 @@ astryx gap-report # report a missing capability
75
75
 
76
76
  ## Quick Start
77
77
 
78
+ Astryx requires **React 19** or later (`react` and `react-dom` >= 19.0.0 are peer dependencies).
79
+
78
80
  Install Astryx and a theme:
79
81
 
80
82
  ```bash
@@ -5,7 +5,9 @@
5
5
  * @position Horizontal scroll container with fade-edge overflow indication,
6
6
  * optional prev/next buttons on the top layer, scroll-snap, a 1px
7
7
  * visual bleed allowance for child selection indicators, and Shift + wheel
8
- * mapping so mouse users can scroll horizontally. Exposes APG
8
+ * mapping so mouse users can scroll horizontally. Supports optional
9
+ * wrap-around looping and an imperative handle (handleRef) for programmatic
10
+ * scroll control. Exposes APG
9
11
  * carousel semantics: the root is a labelled region with
10
12
  * aria-roledescription="carousel" and each item wrapper is a group with
11
13
  * aria-roledescription="slide" named "Slide N of M".
@@ -18,8 +20,47 @@
18
20
  import { type ReactNode } from 'react';
19
21
  import type { BaseProps } from '../BaseProps';
20
22
  import type { SpacingStep } from '../utils/types';
23
+ /**
24
+ * Imperative control surface for the Carousel, accessed via the `handleRef`
25
+ * prop. Methods drive the same native-scroll machinery as the built-in
26
+ * buttons, so they respect RTL, reduced-motion, and `hasLoop`.
27
+ */
28
+ export interface CarouselHandle {
29
+ /**
30
+ * Scroll forward by roughly one viewport. With `hasLoop`, wraps to the
31
+ * start once the end is reached.
32
+ */
33
+ scrollNext(): void;
34
+ /**
35
+ * Scroll backward by roughly one viewport. With `hasLoop`, wraps to the
36
+ * end once the start is reached.
37
+ */
38
+ scrollPrev(): void;
39
+ /**
40
+ * Scroll the item at the given 0-based index to the start edge. The index
41
+ * is clamped to the item range, and only the carousel scrolls — the page
42
+ * position is left untouched.
43
+ */
44
+ scrollTo(index: number): void;
45
+ /**
46
+ * Whether there is scrollable content past the trailing edge. With
47
+ * `hasLoop`, returns true whenever the content overflows, since wrapping
48
+ * is always available. Reads live state — safe to call in an event handler.
49
+ */
50
+ canScrollNext(): boolean;
51
+ /**
52
+ * Whether there is scrollable content past the leading edge. With
53
+ * `hasLoop`, returns true whenever the content overflows. Reads live state.
54
+ */
55
+ canScrollPrev(): boolean;
56
+ }
21
57
  export interface CarouselProps extends BaseProps<HTMLDivElement> {
22
58
  ref?: React.Ref<HTMLDivElement>;
59
+ /**
60
+ * Imperative handle for programmatic scroll control. Exposes scrollNext,
61
+ * scrollPrev, scrollTo, and the canScrollNext/canScrollPrev queries.
62
+ */
63
+ handleRef?: React.Ref<CarouselHandle>;
23
64
  /** Carousel items — rendered in a horizontal scroll container. */
24
65
  children: ReactNode;
25
66
  /**
@@ -39,6 +80,16 @@ export interface CarouselProps extends BaseProps<HTMLDivElement> {
39
80
  * @default true
40
81
  */
41
82
  hasEdgeFade?: boolean;
83
+ /**
84
+ * Enable wrap-around scrolling. When the content overflows, pressing Next
85
+ * at the end scrolls back to the start, and Prev at the start scrolls to
86
+ * the end — for both the built-in buttons and the imperative handle. The
87
+ * navigation buttons stay visible at both edges instead of hiding, since a
88
+ * scroll is always available. Has no effect when the content fits without
89
+ * overflowing.
90
+ * @default false
91
+ */
92
+ hasLoop?: boolean;
42
93
  /**
43
94
  * Enable scroll-snap on items. Each direct child snaps to the start edge.
44
95
  * @default false
@@ -79,7 +130,7 @@ export interface CarouselProps extends BaseProps<HTMLDivElement> {
79
130
  * </Carousel>
80
131
  * ```
81
132
  */
82
- export declare function Carousel({ ref, children, gap, hasButtons, hasEdgeFade, hasSnap, padding, 'aria-label': ariaLabelFromProps, xstyle, className, style, 'data-testid': testId, ...htmlProps }: CarouselProps): import("react").JSX.Element;
133
+ export declare function Carousel({ ref, handleRef, children, gap, hasButtons, hasEdgeFade, hasLoop, hasSnap, padding, 'aria-label': ariaLabelFromProps, xstyle, className, style, 'data-testid': testId, ...htmlProps }: CarouselProps): import("react").JSX.Element;
83
134
  export declare namespace Carousel {
84
135
  var displayName: string;
85
136
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/Carousel/Carousel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACL,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAIhD,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA+JD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,QAAQ,EACR,GAAO,EACP,UAAiB,EACjB,WAAkB,EAClB,OAAe,EACf,OAAO,EACP,YAAY,EAAE,kBAAkB,EAChC,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,SAAS,EACb,EAAE,aAAa,+BA6Mf;yBA3Ne,QAAQ"}
1
+ {"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/Carousel/Carousel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACL,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAIhD;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;;OAIG;IACH,aAAa,IAAI,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACtC,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA+JD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,SAAS,EACT,QAAQ,EACR,GAAO,EACP,UAAiB,EACjB,WAAkB,EAClB,OAAe,EACf,OAAe,EACf,OAAO,EACP,YAAY,EAAE,kBAAkB,EAChC,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,SAAS,EACb,EAAE,aAAa,+BAiRf;yBAjSe,QAAQ"}
@@ -9,7 +9,9 @@
9
9
  * @position Horizontal scroll container with fade-edge overflow indication,
10
10
  * optional prev/next buttons on the top layer, scroll-snap, a 1px
11
11
  * visual bleed allowance for child selection indicators, and Shift + wheel
12
- * mapping so mouse users can scroll horizontally. Exposes APG
12
+ * mapping so mouse users can scroll horizontally. Supports optional
13
+ * wrap-around looping and an imperative handle (handleRef) for programmatic
14
+ * scroll control. Exposes APG
13
15
  * carousel semantics: the root is a labelled region with
14
16
  * aria-roledescription="carousel" and each item wrapper is a group with
15
17
  * aria-roledescription="slide" named "Slide N of M".
@@ -19,7 +21,7 @@
19
21
  * - /apps/storybook/stories/Carousel.stories.tsx
20
22
  * - /packages/cli/assets/templates/blocks/components/Carousel/ (showcase blocks)
21
23
  */
22
- import { useRef, useCallback, useEffect, Children, isValidElement } from 'react';
24
+ import { useRef, useCallback, useEffect, useImperativeHandle, Children, isValidElement } from 'react';
23
25
  import * as stylex from '@stylexjs/stylex';
24
26
  import "../theme/tokens.stylex.js";
25
27
  import { spacingVars, colorVars, shadowVars, radiusVars, durationVars, easeVars } from "../theme/tokens.stylex.js";
@@ -31,6 +33,12 @@ import { isRtlElement } from "../hooks/isRtlElement.js";
31
33
  import { mergeProps, mergeRefs, rtlStyles } from "../utils/index.js";
32
34
  import { themeProps } from "../utils/themeProps.js";
33
35
  import { useTranslator } from "../i18n/index.js";
36
+
37
+ /**
38
+ * Imperative control surface for the Carousel, accessed via the `handleRef`
39
+ * prop. Methods drive the same native-scroll machinery as the built-in
40
+ * buttons, so they respect RTL, reduced-motion, and `hasLoop`.
41
+ */
34
42
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
35
43
  // =============================================================================
36
44
  // Styles
@@ -203,10 +211,12 @@ const paddingStyles = {
203
211
  */
204
212
  export function Carousel({
205
213
  ref,
214
+ handleRef,
206
215
  children,
207
216
  gap = 1,
208
217
  hasButtons = true,
209
218
  hasEdgeFade = true,
219
+ hasLoop = false,
210
220
  hasSnap = false,
211
221
  padding,
212
222
  'aria-label': ariaLabelFromProps,
@@ -222,7 +232,8 @@ export function Carousel({
222
232
  const {
223
233
  scrollRef,
224
234
  overflowStart,
225
- overflowEnd
235
+ overflowEnd,
236
+ hasOverflow
226
237
  } = useScrollOverflow();
227
238
 
228
239
  // Children.toArray drops null/undefined/boolean children and assigns
@@ -277,22 +288,78 @@ export function Carousel({
277
288
  if (!el) {
278
289
  return;
279
290
  }
280
- const firstChild = el.firstElementChild;
281
- const itemWidth = firstChild ? firstChild.offsetWidth : 0;
282
- const amount = el.clientWidth - itemWidth * 0.5;
283
291
  // Respect the user's reduced-motion preference — mirrors the CSS
284
292
  // scroll-behavior override so button-driven scrolling doesn't animate
285
293
  // for users who opted out of motion.
286
294
  const prefersReducedMotion = typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
295
+ const behavior = prefersReducedMotion ? 'auto' : 'smooth';
296
+ // Under RTL the scroll axis is inverted (start is scrollLeft 0, the end
297
+ // is negative), so flip the physical delta sign.
298
+ const rtlSign = isRtlElement(el) ? -1 : 1;
299
+
300
+ // Wrap-around: when looping over overflowing content, a press that would
301
+ // run past an edge jumps to the opposite edge instead. Overshoot by the
302
+ // full scroll width in the opposite logical direction and let the browser
303
+ // clamp to the far edge — this reuses the same RTL sign convention as a
304
+ // normal scroll, so it stays direction-correct without special-casing.
305
+ if (hasLoop && hasOverflow) {
306
+ const atEnd = direction === 1 && !overflowEnd;
307
+ const atStart = direction === -1 && !overflowStart;
308
+ if (atEnd || atStart) {
309
+ el.scrollBy({
310
+ left: rtlSign * -direction * el.scrollWidth,
311
+ behavior
312
+ });
313
+ return;
314
+ }
315
+ }
316
+ const firstChild = el.firstElementChild;
317
+ const itemWidth = firstChild ? firstChild.offsetWidth : 0;
318
+ const amount = el.clientWidth - itemWidth * 0.5;
287
319
  el.scrollBy({
288
320
  // `direction` is the logical intent (-1 = toward content start, +1 =
289
- // toward content end). Under RTL the scroll axis is inverted (start is
290
- // scrollLeft 0, the end is negative), so flip the physical delta sign.
291
- left: (isRtlElement(el) ? -1 : 1) * direction * Math.max(amount, itemWidth),
321
+ // toward content end).
322
+ left: rtlSign * direction * Math.max(amount, itemWidth),
323
+ behavior
324
+ });
325
+ }, [hasLoop, hasOverflow, overflowStart, overflowEnd]);
326
+ const scrollToIndex = useCallback(index => {
327
+ const el = scrollElRef.current;
328
+ const items = el?.children;
329
+ if (!el || !items || items.length === 0) {
330
+ return;
331
+ }
332
+ const clamped = Math.max(0, Math.min(index, items.length - 1));
333
+ const target = items[clamped];
334
+ const prefersReducedMotion = typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
335
+ // Bring the item to the start edge by scrolling the container by the
336
+ // measured gap between the item and the container edge. Using scrollBy
337
+ // (not scrollIntoView) keeps the scroll contained to the carousel and
338
+ // never moves ancestors or the page. In RTL the start edge is the right
339
+ // edge, so align the trailing edges instead of the leading ones.
340
+ const containerRect = el.getBoundingClientRect();
341
+ const itemRect = target.getBoundingClientRect();
342
+ const delta = isRtlElement(el) ? itemRect.right - containerRect.right : itemRect.left - containerRect.left;
343
+ el.scrollBy({
344
+ left: delta,
292
345
  behavior: prefersReducedMotion ? 'auto' : 'smooth'
293
346
  });
294
347
  }, []);
295
- const fadeStyle = hasEdgeFade ? overflowStart && overflowEnd ? styles.fadeBoth : overflowStart ? styles.fadeStart : overflowEnd ? styles.fadeEnd : null : null;
348
+ useImperativeHandle(handleRef, () => ({
349
+ scrollNext: () => scrollBy(1),
350
+ scrollPrev: () => scrollBy(-1),
351
+ scrollTo: index => scrollToIndex(index),
352
+ // With loop, either direction is reachable whenever the content
353
+ // overflows; otherwise reflect the live per-edge overflow state.
354
+ canScrollNext: () => hasLoop ? hasOverflow : overflowEnd,
355
+ canScrollPrev: () => hasLoop ? hasOverflow : overflowStart
356
+ }), [scrollBy, scrollToIndex, hasLoop, hasOverflow, overflowStart, overflowEnd]);
357
+
358
+ // With loop, the buttons stay reachable at both edges as long as there's
359
+ // something to scroll; without loop they follow the per-edge overflow state.
360
+ const canScrollStart = hasLoop && hasOverflow ? true : overflowStart;
361
+ const canScrollEnd = hasLoop && hasOverflow ? true : overflowEnd;
362
+ const fadeStyle = hasEdgeFade ? hasLoop && hasOverflow || overflowStart && overflowEnd ? styles.fadeBoth : overflowStart ? styles.fadeStart : overflowEnd ? styles.fadeEnd : null : null;
296
363
 
297
364
  // Self-authored position styles (positioning: 'custom' below): a cover
298
365
  // centered on the anchor, sized to it — direction-neutral by construction.
@@ -341,7 +408,7 @@ export function Carousel({
341
408
  1: {
342
409
  className: "x78zum5 x6s0dn4 xl56j7k x1prclbq xjspbzw x14hfi27 x19991ni xuedmi6 xlr8y92 xuuh30 x1nyx83j xg01cxk x47corl"
343
410
  }
344
- }[!!!overflowStart << 0],
411
+ }[!!!canScrollStart << 0],
345
412
  children: /*#__PURE__*/_jsx(Button, {
346
413
  icon: /*#__PURE__*/_jsx("span", {
347
414
  ...stylex.props(rtlStyles.mirror),
@@ -359,7 +426,7 @@ export function Carousel({
359
426
  // tab order and a11y tree while it's visually hidden, so
360
427
  // keyboard users don't land on an invisible control.
361
428
  ,
362
- isDisabled: !overflowStart,
429
+ isDisabled: !canScrollStart,
363
430
  onClick: () => scrollBy(-1),
364
431
  xstyle: styles.buttonRadiusOverride
365
432
  })
@@ -371,7 +438,7 @@ export function Carousel({
371
438
  1: {
372
439
  className: "x78zum5 x6s0dn4 xl56j7k x1prclbq xjspbzw x14hfi27 x19991ni xuedmi6 xlr8y92 xitnhlw x36dvrd xg01cxk x47corl"
373
440
  }
374
- }[!!!overflowEnd << 0],
441
+ }[!!!canScrollEnd << 0],
375
442
  children: /*#__PURE__*/_jsx(Button, {
376
443
  icon: /*#__PURE__*/_jsx("span", {
377
444
  ...stylex.props(rtlStyles.mirror),
@@ -387,7 +454,7 @@ export function Carousel({
387
454
  // See "Scroll left" — disabled while visually hidden so the
388
455
  // button stays mounted but out of the tab order / a11y tree.
389
456
  ,
390
- isDisabled: !overflowEnd,
457
+ isDisabled: !canScrollEnd,
391
458
  onClick: () => scrollBy(1),
392
459
  xstyle: styles.buttonRadiusOverride
393
460
  })
@@ -5,5 +5,5 @@
5
5
  * @position Public entry point for the Carousel module
6
6
  */
7
7
  export { Carousel } from './Carousel';
8
- export type { CarouselProps } from './Carousel';
8
+ export type { CarouselProps, CarouselHandle } from './Carousel';
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Carousel/index.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAEH,OAAO,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AACpC,YAAY,EAAC,aAAa,EAAC,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Carousel/index.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAEH,OAAO,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AACpC,YAAY,EAAC,aAAa,EAAE,cAAc,EAAC,MAAM,YAAY,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import type { TextType, TextSize, TextColor, TextWeight } from '../theme/types';
2
2
  import type { BaseProps } from '../BaseProps';
3
3
  import type { TimestampTooltipEntry } from './tooltipEntries';
4
- export type TimestampFormat = 'relative' | 'auto' | 'date' | 'date_long' | 'date_weekday' | 'date_time' | 'time' | 'system_date' | 'system_date_time' | 'system_time';
4
+ export type TimestampFormat = 'relative' | 'relative_short' | 'auto' | 'date' | 'date_long' | 'date_weekday' | 'date_time' | 'time' | 'system_date' | 'system_date_time' | 'system_time';
5
5
  export interface TimestampProps extends BaseProps<HTMLTimeElement> {
6
6
  /** Ref forwarded to the root `<time>` element. */
7
7
  ref?: React.Ref<HTMLTimeElement>;
@@ -10,6 +10,9 @@ export interface TimestampProps extends BaseProps<HTMLTimeElement> {
10
10
  /**
11
11
  * Display format.
12
12
  * - `'relative'`: "2 hours ago", "yesterday", "now"
13
+ * - `'relative_short'`: "2h ago", "1d ago", "now" — the same tiers as
14
+ * `'relative'` with abbreviated units (s/m/h/d/mo/y), for compact,
15
+ * space-constrained surfaces
13
16
  * - `'auto'`: Relative for recent times, `date_time` for older
14
17
  * - `'date'`: "Mar 21, 2025"
15
18
  * - `'date_long'`: "March 21, 2025"
@@ -1 +1 @@
1
- {"version":3,"file":"Timestamp.d.ts","sourceRoot":"","sources":["../../src/Timestamp/Timestamp.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAC,MAAM,gBAAgB,CAAC;AAG9E,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,kBAAkB,CAAC;AAU5D,MAAM,MAAM,eAAe,GACvB,UAAU,GACV,MAAM,GACN,MAAM,GACN,WAAW,GACX,cAAc,GACd,WAAW,GACX,MAAM,GACN,aAAa,GACb,kBAAkB,GAClB,aAAa,CAAC;AAElB,MAAM,WAAW,cAAe,SAAQ,SAAS,CAAC,eAAe,CAAC;IAChE,kDAAkD;IAClD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACjC,uFAAuF;IACvF,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IACtD;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB;;OAEG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,sCAAsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA2LD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,MAAe,EACf,aAAsC,EACtC,UAAiB,EACjB,cAAc,EACd,eAAuB,EACvB,MAAc,EACd,IAAmB,EACnB,IAAI,EACJ,KAAmB,EACnB,MAAM,EACN,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,aAAa,EAAE,MAAM,GACtB,EAAE,cAAc,sCA4IhB;yBA7Je,SAAS"}
1
+ {"version":3,"file":"Timestamp.d.ts","sourceRoot":"","sources":["../../src/Timestamp/Timestamp.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAC,MAAM,gBAAgB,CAAC;AAG9E,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,kBAAkB,CAAC;AAU5D,MAAM,MAAM,eAAe,GACvB,UAAU,GACV,gBAAgB,GAChB,MAAM,GACN,MAAM,GACN,WAAW,GACX,cAAc,GACd,WAAW,GACX,MAAM,GACN,aAAa,GACb,kBAAkB,GAClB,aAAa,CAAC;AAElB,MAAM,WAAW,cAAe,SAAQ,SAAS,CAAC,eAAe,CAAC;IAChE,kDAAkD;IAClD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACjC,uFAAuF;IACvF,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IACtD;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB;;OAEG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,sCAAsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAwQD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,MAAe,EACf,aAAsC,EACtC,UAAiB,EACjB,cAAc,EACd,eAAuB,EACvB,MAAc,EACd,IAAmB,EACnB,IAAI,EACJ,KAAmB,EACnB,MAAM,EACN,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,aAAa,EAAE,MAAM,GACtB,EAAE,cAAc,sCA8IhB;yBA/Je,SAAS"}
@@ -139,6 +139,67 @@ function getRelativeTimeString(date, now) {
139
139
  return `${years} ${years === 1 ? 'year' : 'years'} ago`;
140
140
  }
141
141
 
142
+ /**
143
+ * The compact sibling of `getRelativeTimeString`: the same tier boundaries and
144
+ * present/future-skew handling, rendered with abbreviated units for
145
+ * space-constrained surfaces (chat metadata, dense tables, chips).
146
+ *
147
+ * Units follow the common compact convention (and the Microsoft Style Guide):
148
+ * `s` seconds, `m` minutes, `h` hours, `d` days, `mo` months, `y` years.
149
+ * Months use `mo` — not `m` — because `m` already means minutes; a bare `m`
150
+ * for months would be ambiguous. The value is always numeric (no "yesterday"
151
+ * idiom, which belongs to the long form) so the short form stays predictable
152
+ * and easy to scan. The `ago` / `in` affixes are kept so direction stays
153
+ * unambiguous at a glance.
154
+ */
155
+ function getRelativeTimeShortString(date, now) {
156
+ const diffSeconds = Math.round((now.getTime() - date.getTime()) / 1000);
157
+
158
+ // Present clamp — identical to the long form (see getRelativeTimeString).
159
+ if (Math.abs(diffSeconds) < 10) {
160
+ return 'now';
161
+ }
162
+ if (diffSeconds < 0) {
163
+ // Future dates.
164
+ const absDiff = Math.abs(diffSeconds);
165
+ if (absDiff <= FUTURE_SKEW_TOLERANCE) {
166
+ return 'now';
167
+ }
168
+ if (absDiff < MINUTE) {
169
+ return `in ${absDiff}s`;
170
+ }
171
+ if (absDiff < HOUR) {
172
+ return `in ${Math.floor(absDiff / MINUTE)}m`;
173
+ }
174
+ if (absDiff < DAY) {
175
+ return `in ${Math.floor(absDiff / HOUR)}h`;
176
+ }
177
+ if (absDiff < MONTH) {
178
+ return `in ${Math.floor(absDiff / DAY)}d`;
179
+ }
180
+ if (absDiff < YEAR) {
181
+ return `in ${Math.floor(absDiff / MONTH)}mo`;
182
+ }
183
+ return `in ${Math.floor(absDiff / YEAR)}y`;
184
+ }
185
+ if (diffSeconds < MINUTE) {
186
+ return `${diffSeconds}s ago`;
187
+ }
188
+ if (diffSeconds < HOUR) {
189
+ return `${Math.floor(diffSeconds / MINUTE)}m ago`;
190
+ }
191
+ if (diffSeconds < DAY) {
192
+ return `${Math.floor(diffSeconds / HOUR)}h ago`;
193
+ }
194
+ if (diffSeconds < MONTH) {
195
+ return `${Math.floor(diffSeconds / DAY)}d ago`;
196
+ }
197
+ if (diffSeconds < YEAR) {
198
+ return `${Math.floor(diffSeconds / MONTH)}mo ago`;
199
+ }
200
+ return `${Math.floor(diffSeconds / YEAR)}y ago`;
201
+ }
202
+
142
203
  /** Returns the interval (in ms) at which a relative timestamp should update. */
143
204
  function getLiveInterval(diffSeconds) {
144
205
  const absDiff = Math.abs(diffSeconds);
@@ -156,7 +217,17 @@ function getLiveInterval(diffSeconds) {
156
217
 
157
218
  /** Whether a format is non-relative (i.e. shows a fixed date/time). */
158
219
  function isAbsoluteFormat(format) {
159
- return format !== 'relative' && format !== 'auto';
220
+ return format !== 'relative' && format !== 'relative_short' && format !== 'auto';
221
+ }
222
+
223
+ /**
224
+ * Whether a format renders a relative phrase ("2 hours ago" / "2h ago") rather
225
+ * than a fixed instant. Both the long and short relative forms share the same
226
+ * treatment: they get the accessible full-date name, the hover tooltip, and
227
+ * live updates.
228
+ */
229
+ function isRelativeFormat(format) {
230
+ return format === 'relative' || format === 'relative_short';
160
231
  }
161
232
 
162
233
  // =============================================================================
@@ -213,7 +284,7 @@ export function Timestamp({
213
284
 
214
285
  // Format the display text. No time zone is passed: the visible text always
215
286
  // reads in the viewer's own zone, and only the tooltip names others.
216
- const displayText = !isValidDate ? '' : effectiveFormat === 'relative' ? getRelativeTimeString(date, now) : isAbsoluteFormat(effectiveFormat) ? formatInstant(date, effectiveFormat, {
287
+ const displayText = !isValidDate ? '' : effectiveFormat === 'relative' ? getRelativeTimeString(date, now) : effectiveFormat === 'relative_short' ? getRelativeTimeShortString(date, now) : isAbsoluteFormat(effectiveFormat) ? formatInstant(date, effectiveFormat, {
217
288
  isTimezoneShown
218
289
  }) : '';
219
290
 
@@ -222,7 +293,7 @@ export function Timestamp({
222
293
 
223
294
  // Live updates
224
295
  useEffect(() => {
225
- if (!isLive || !isValidDate || effectiveFormat !== 'relative') {
296
+ if (!isLive || !isValidDate || !isRelativeFormat(effectiveFormat)) {
226
297
  return;
227
298
  }
228
299
  const interval = getLiveInterval(diffSeconds);
@@ -246,7 +317,7 @@ export function Timestamp({
246
317
  // when a consumer has explicitly configured tooltip lines would let `format`
247
318
  // silently suppress another prop's output, so entry presence opens it too.
248
319
  // With no entries this reduces to the original condition exactly.
249
- const showTooltip = hasTooltip && (effectiveFormat === 'relative' || entries !== undefined);
320
+ const showTooltip = hasTooltip && (isRelativeFormat(effectiveFormat) || entries !== undefined);
250
321
  const timestampProps = mergeProps(themeProps('timestamp', {
251
322
  format: effectiveFormat
252
323
  }), {
@@ -263,7 +334,7 @@ export function Timestamp({
263
334
  children: /*#__PURE__*/_jsx("time", {
264
335
  ref: mergeRefs(ref, timeRef),
265
336
  dateTime: isoString,
266
- "aria-label": effectiveFormat === 'relative' ? fullAbsoluteText : undefined
337
+ "aria-label": isRelativeFormat(effectiveFormat) ? fullAbsoluteText : undefined
267
338
  // The absolute-time tooltip is anchored here with the default 'auto'
268
339
  // focus trigger, which only activates on focusable anchors. A bare
269
340
  // <time> is not focusable, so without a tab stop sighted keyboard
@@ -4,10 +4,10 @@ import type { TimestampFormat } from './Timestamp';
4
4
  * style ("February 19, 2026 at 5:00:00 PM UTC") that backs a relative
5
5
  * timestamp's accessible name and the tooltip's default line.
6
6
  *
7
- * `'relative'` and `'auto'` are excluded: a relative phrase names no instant,
8
- * so a zone could not change what it says.
7
+ * `'relative'`, `'relative_short'`, and `'auto'` are excluded: a relative
8
+ * phrase names no instant, so a zone could not change what it says.
9
9
  */
10
- export type InstantFormat = Exclude<TimestampFormat, 'relative' | 'auto'> | 'full';
10
+ export type InstantFormat = Exclude<TimestampFormat, 'relative' | 'relative_short' | 'auto'> | 'full';
11
11
  export interface FormatInstantOptions {
12
12
  /**
13
13
  * IANA time zone identifier. Omit for the viewer's own zone — which is what
@@ -1 +1 @@
1
- {"version":3,"file":"formatInstant.d.ts","sourceRoot":"","sources":["../../src/Timestamp/formatInstant.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,aAAa,CAAC;AAMjD;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GACrB,OAAO,CAAC,eAAe,EAAE,UAAU,GAAG,MAAM,CAAC,GAC7C,MAAM,CAAC;AAEX,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AA6ED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,aAAa,EACrB,EAAC,QAAQ,EAAE,eAAuB,EAAC,GAAE,oBAAyB,GAC7D,MAAM,CA0DR"}
1
+ {"version":3,"file":"formatInstant.d.ts","sourceRoot":"","sources":["../../src/Timestamp/formatInstant.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,aAAa,CAAC;AAMjD;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GACvB,OAAO,CAAC,eAAe,EAAE,UAAU,GAAG,gBAAgB,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC;AAE5E,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AA6ED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,aAAa,EACrB,EAAC,QAAQ,EAAE,eAAuB,EAAC,GAAE,oBAAyB,GAC7D,MAAM,CA0DR"}
@@ -40,8 +40,8 @@ import { SHARED_DATE_FORMAT_OPTIONS, getTimeZoneParts } from "../utils/plainDate
40
40
  * style ("February 19, 2026 at 5:00:00 PM UTC") that backs a relative
41
41
  * timestamp's accessible name and the tooltip's default line.
42
42
  *
43
- * `'relative'` and `'auto'` are excluded: a relative phrase names no instant,
44
- * so a zone could not change what it says.
43
+ * `'relative'`, `'relative_short'`, and `'auto'` are excluded: a relative
44
+ * phrase names no instant, so a zone could not change what it says.
45
45
  */
46
46
 
47
47
  // =============================================================================
@@ -4,8 +4,9 @@ import type { InstantFormat } from './formatInstant';
4
4
  *
5
5
  * Every `TimestampFormat` that names a fixed instant, plus `'full'` — the long
6
6
  * absolute style the tooltip has always shown ("February 19, 2026 at 5:00:00 PM
7
- * UTC"). `'relative'` and `'auto'` are excluded: a relative phrase ignores any
8
- * zone, which would make `timezoneID` silently inert on that line.
7
+ * UTC"). `'relative'`, `'relative_short'`, and `'auto'` are excluded: a
8
+ * relative phrase ignores any zone, which would make `timezoneID` silently
9
+ * inert on that line.
9
10
  *
10
11
  * `'full'` lives only in this vocabulary, never in `TimestampFormat` — nothing
11
12
  * asked for it as a visible display format, and keeping it out means new
@@ -1 +1 @@
1
- {"version":3,"file":"tooltipEntries.d.ts","sourceRoot":"","sources":["../../src/Timestamp/tooltipEntries.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAMnD;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC;AAEnD,yCAAyC;AACzC,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,sBAAsB,CAAC;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,+BAA+B;AAC/B,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAqGD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,aAAa,CAAC,qBAAqB,CAAC,GAC5C,aAAa,CAAC,oBAAoB,CAAC,CAoBrC"}
1
+ {"version":3,"file":"tooltipEntries.d.ts","sourceRoot":"","sources":["../../src/Timestamp/tooltipEntries.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAMnD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC;AAEnD,yCAAyC;AACzC,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,sBAAsB,CAAC;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,+BAA+B;AAC/B,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAqGD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,aAAa,CAAC,qBAAqB,CAAC,GAC5C,aAAa,CAAC,oBAAoB,CAAC,CAoBrC"}
@@ -33,8 +33,9 @@ import { formatInstant } from "./formatInstant.js";
33
33
  *
34
34
  * Every `TimestampFormat` that names a fixed instant, plus `'full'` — the long
35
35
  * absolute style the tooltip has always shown ("February 19, 2026 at 5:00:00 PM
36
- * UTC"). `'relative'` and `'auto'` are excluded: a relative phrase ignores any
37
- * zone, which would make `timezoneID` silently inert on that line.
36
+ * UTC"). `'relative'`, `'relative_short'`, and `'auto'` are excluded: a
37
+ * relative phrase ignores any zone, which would make `timezoneID` silently
38
+ * inert on that line.
38
39
  *
39
40
  * `'full'` lives only in this vocabulary, never in `TimestampFormat` — nothing
40
41
  * asked for it as a visible display format, and keeping it out means new