rift-ds 1.17.0 → 1.18.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.
@@ -1,6 +1,7 @@
1
1
  /* The dashboard's grid. Four stats across on a desktop, two from the
2
- laptop width down to a phone; the two panels sit side by side until the
3
- tablet width, then stack. */
2
+ laptop width down to a phone, where each stat drops its delta under its
3
+ label once its tile is narrow (deltaPlacement="auto"); the two panels sit
4
+ side by side until the tablet width, then stack. */
4
5
 
5
6
  .stats {
6
7
  display: grid;
@@ -81,7 +82,7 @@
81
82
 
82
83
  @media (max-width: 600px) {
83
84
  .stats {
84
- gap: var(--gap-400);
85
+ gap: var(--gap-300);
85
86
  }
86
87
 
87
88
  .panels {
@@ -85,7 +85,7 @@ export default function DashboardPage() {
85
85
  value={stat.value}
86
86
  delta={stat.delta}
87
87
  trend={stat.trend}
88
- deltaPlacement="inline"
88
+ deltaPlacement="auto"
89
89
  />
90
90
  </Panel>
91
91
  ))}
@@ -29,6 +29,7 @@
29
29
  }
30
30
 
31
31
  .content {
32
+ position: relative;
32
33
  width: 100%;
33
34
  max-width: 1440px;
34
35
  min-width: 0;
@@ -40,12 +41,61 @@
40
41
 
41
42
  /* ---------------------------------------------------------- top bar */
42
43
 
44
+ /* The bar holds one inset off the top of the page and the content runs
45
+ under it. Once the page scrolls (data-stuck), the page colour fills the
46
+ strip above it, fading out across the bar, and the blur behind it
47
+ dissolves a few rows below. */
43
48
  .topBar {
49
+ position: sticky;
50
+ top: var(--shell-inset);
51
+ z-index: var(--layer-sticky);
44
52
  display: flex;
45
53
  align-items: center;
46
54
  justify-content: space-between;
47
55
  gap: var(--gap-200);
48
56
  min-height: var(--control-height-default);
57
+ /* Room between the bar and the page header under it. */
58
+ margin-bottom: var(--gap-400);
59
+ }
60
+
61
+ .topBar::before {
62
+ content: '';
63
+ position: absolute;
64
+ top: calc(-1 * var(--shell-inset));
65
+ bottom: 0;
66
+ inset-inline: calc(-1 * var(--padding-400));
67
+ z-index: -1;
68
+ background: linear-gradient(to bottom, var(--color-bg-page-primary) var(--shell-inset), transparent);
69
+ pointer-events: none;
70
+ opacity: 0;
71
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
72
+ }
73
+
74
+ .topBar[data-stuck]::before {
75
+ opacity: 1;
76
+ }
77
+
78
+ .topBarBlur:global(.ds-progressive-blur) {
79
+ position: absolute;
80
+ top: calc(-1 * var(--shell-inset));
81
+ bottom: calc(-1 * var(--gap-1000));
82
+ inset-inline: calc(-1 * var(--padding-400));
83
+ height: auto;
84
+ z-index: -1;
85
+ opacity: 0;
86
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
87
+ }
88
+
89
+ .topBarBlur:global(.ds-progressive-blur--tinted) {
90
+ opacity: 1;
91
+ }
92
+
93
+ /* A one-pixel marker at the bar's resting place. Absolute, so the
94
+ column's gap skips it; it keeps its static place at the column's top. */
95
+ .topBarMarker {
96
+ position: absolute;
97
+ width: 1px;
98
+ height: 1px;
49
99
  }
50
100
 
51
101
  .search {
@@ -7,13 +7,17 @@
7
7
  * set. This file is yours: rename the product, change the navigation, and
8
8
  * grow it with your app.
9
9
  *
10
+ * The top bar sticks to the top of the page and the content scrolls under
11
+ * it: bare while the page is at the top, then a fill above it and the
12
+ * library's ProgressiveBlur behind it fade in once the page scrolls.
13
+ *
10
14
  * Responsive by construction: above 768px the sidebar floats beside the
11
15
  * page and the content moves over by its width; below it the sidebar
12
16
  * becomes a drawer behind its own menu button, the search field folds to
13
17
  * its icon, and the content runs edge to edge at a tighter gutter.
14
18
  */
15
19
 
16
- import { useState, type ReactNode } from 'react';
20
+ import { useEffect, useRef, useState, type ReactNode } from 'react';
17
21
  import { AiButton } from 'rift-ds/components/AiButton/AiButton';
18
22
  import {
19
23
  AppSidebar,
@@ -22,6 +26,7 @@ import {
22
26
  } from 'rift-ds/components/AppSidebar/AppSidebar';
23
27
  import { Breadcrumb, type BreadcrumbItem } from 'rift-ds/components/Breadcrumb/Breadcrumb';
24
28
  import { CommandPaletteTrigger } from 'rift-ds/components/CommandPaletteTrigger/CommandPaletteTrigger';
29
+ import { ProgressiveBlur } from 'rift-ds/components/ProgressiveBlur/ProgressiveBlur';
25
30
  import { ThemeToggle } from 'rift-ds/components/ThemeToggle/ThemeToggle';
26
31
  import styles from './AppShell.module.css';
27
32
 
@@ -69,6 +74,18 @@ export interface AppShellProps {
69
74
  export function AppShell({ activeKey, breadcrumb, title, actions, children }: AppShellProps) {
70
75
  const [expanded, setExpanded] = useState(true);
71
76
 
77
+ // The top bar is stuck once the marker at its resting place scrolls out
78
+ // of view; the fill and the blur behind the bar follow.
79
+ const markerRef = useRef<HTMLDivElement>(null);
80
+ const [stuck, setStuck] = useState(false);
81
+ useEffect(() => {
82
+ const marker = markerRef.current;
83
+ if (!marker) return;
84
+ const observer = new IntersectionObserver(([entry]) => setStuck(!entry.isIntersecting));
85
+ observer.observe(marker);
86
+ return () => observer.disconnect();
87
+ }, []);
88
+
72
89
  return (
73
90
  <div className={styles.shell}>
74
91
  <AppSidebar
@@ -83,16 +100,18 @@ export function AppShell({ activeKey, breadcrumb, title, actions, children }: Ap
83
100
 
84
101
  <div className={`${styles.main} ${expanded ? styles.mainExpanded : ''}`}>
85
102
  <div className={styles.content}>
86
- <header className={styles.topBar}>
103
+ <div ref={markerRef} className={styles.topBarMarker} aria-hidden="true" />
104
+ <header className={styles.topBar} data-stuck={stuck ? '' : undefined}>
105
+ <ProgressiveBlur className={styles.topBarBlur} tinted={stuck} />
87
106
  {/* Wire both to your command palette: CommandPalette from
88
107
  rift-ds, opened from here and from Cmd+K. */}
89
- <CommandPaletteTrigger size="compact" className={styles.search} />
108
+ <CommandPaletteTrigger className={styles.search} />
90
109
  <div className={styles.topBarActions}>
91
- <CommandPaletteTrigger size="compact" iconOnly className={styles.searchIcon} />
110
+ <CommandPaletteTrigger iconOnly className={styles.searchIcon} />
92
111
  {/* The AI entry. Point it at your chat: `npx rift-ds init chat`
93
112
  scaffolds one, and ChatLauncher opens it. */}
94
- <AiButton size="compact" />
95
- <ThemeToggle size="compact" showLabels={false} />
113
+ <AiButton />
114
+ <ThemeToggle showLabels={false} />
96
115
  </div>
97
116
  </header>
98
117
 
@@ -11,6 +11,8 @@ export interface ChatThreadsProps {
11
11
  logoText?: string;
12
12
  /** The signed-in person, shown on the inline rail. */
13
13
  profile?: ThreadPanelProfile;
14
+ /** Rendered in the rail's footer in both seats (above the profile row on the inline rail; the sheet has none): a theme toggle, a link out. Fades out while the inline rail is collapsed. */
15
+ footerSlot?: ReactNode;
14
16
  /** Whether the inline rail can collapse to its icon strip. */
15
17
  collapsible?: boolean;
16
18
  /**
@@ -38,4 +40,4 @@ export interface ChatThreadsProps {
38
40
  * thread lifecycle (`useThreads`). The widget seats it inline on a wide
39
41
  * host and as a slide-over sheet on a narrow one, and tells it which.
40
42
  */
41
- export declare function ChatThreads({ overlay, close, logo, logoText, profile, collapsible, defaultExpanded, historyLabel, threadPanelLabels, actionLabels, }: ChatThreadsProps): import("react").JSX.Element;
43
+ export declare function ChatThreads({ overlay, close, logo, logoText, profile, footerSlot, collapsible, defaultExpanded, historyLabel, threadPanelLabels, actionLabels, }: ChatThreadsProps): import("react").JSX.Element;
@@ -9,6 +9,7 @@ function ChatThreads({
9
9
  logo,
10
10
  logoText,
11
11
  profile,
12
+ footerSlot,
12
13
  collapsible = true,
13
14
  defaultExpanded = false,
14
15
  historyLabel = "Chat history",
@@ -65,7 +66,8 @@ function ChatThreads({
65
66
  logoText: overlay ? logoText : void 0,
66
67
  expanded: overlay || !collapsible ? true : expanded,
67
68
  onExpandedChange: overlay || !collapsible ? void 0 : setExpanded,
68
- profile: overlay ? void 0 : profile
69
+ profile: overlay ? void 0 : profile,
70
+ footerSlot
69
71
  }
70
72
  );
71
73
  }
@@ -621,13 +621,12 @@
621
621
  inset-inline-start: calc(var(--ds-sidebar-float-inset, 20px) + 280px + var(--gap-300));
622
622
  }
623
623
 
624
- /* Inset: the side panel joins the rail on the shell plane, with no borders
625
- between them, so rail and panel read as one column of navigation and the
626
- inset content surface starts beyond both (AppLayout reserves the panel's
627
- width while it shows). */
624
+ /* Inset: the side panel joins the rail on the shell plane, one hairline
625
+ divider between the two levels, and the inset content surface starts
626
+ beyond both (AppLayout reserves the panel's width while it shows). The
627
+ trailing edge needs no border: the surface's own edge ends the panel. */
628
628
  .ds-app-sidebar__panel--inset {
629
629
  background-color: var(--color-bg-app-shell);
630
- border-inline-start-color: transparent;
631
630
  border-inline-end-color: transparent;
632
631
  }
633
632
 
@@ -638,85 +637,126 @@
638
637
  widens to 80px and names every destination:
639
638
  a pill indicator behind the icon, the label
640
639
  beneath (the navigation-rail pattern).
640
+
641
+ Expand/collapse holds one seat, as ThreadPanel
642
+ does: the logo, every icon and the avatar sit
643
+ at the same x and y in both states, so only the
644
+ rail's width, the rows' explicit heights and
645
+ the labels' opacity move. The caption under the
646
+ icon fades out as the inline label fades in.
641
647
  ============================================ */
642
648
  .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) {
643
649
  width: 80px;
644
650
  }
645
651
 
646
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__logo {
647
- /* Centres the 24px mark in the 80px rail: (80 - 24) / 2. */
648
- padding: 0 calc(var(--padding-500) + var(--padding-200));
652
+ /* The seat: 12px row margin + 16px row padding puts every 24px icon's
653
+ centre on the 80px rail's centre line (28 + 12 = 40), and the logo,
654
+ the category labels and the profile row share that leading edge. */
655
+ .ds-app-sidebar--panel-mode .ds-app-sidebar__logo {
656
+ padding-inline-start: calc(var(--padding-500) + var(--padding-200));
657
+ }
658
+
659
+ .ds-app-sidebar--panel-mode .ds-app-sidebar__category {
660
+ padding-inline-start: calc(var(--padding-500) + var(--padding-200));
649
661
  }
650
662
 
651
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__nav {
663
+ .ds-app-sidebar--panel-mode .ds-app-sidebar__nav {
652
664
  gap: 0;
665
+ transition: gap var(--motion-duration-slow) var(--motion-ease-emphasized);
653
666
  }
654
667
 
655
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn {
656
- height: auto;
657
- margin: 0 var(--padding-100);
658
- /* Room between the content and the focus ring; the rows' spacing lives
659
- here rather than in the nav gap, so the rhythm is unchanged. */
660
- padding: var(--padding-100) 0;
661
- justify-content: center;
662
- border-radius: var(--radius-300);
668
+ .ds-app-sidebar--panel-mode.ds-app-sidebar--expanded .ds-app-sidebar__nav {
669
+ gap: var(--gap-100);
670
+ }
671
+
672
+ /* The icon is pinned to the row's top in both states, so it holds its
673
+ y while the row's height glides between its two explicit values. */
674
+ .ds-app-sidebar--panel-mode .ds-app-sidebar__btn {
675
+ position: relative;
676
+ isolation: isolate;
677
+ align-items: flex-start;
678
+ /* Collapsed: the 32px indicator, the caption beneath it, a 4px beat
679
+ above, between and below. */
680
+ height: calc(32px + var(--font-caption-line-height) + var(--padding-100) * 3);
681
+ margin: 0 var(--padding-300);
682
+ padding: var(--padding-200) var(--padding-200) var(--padding-200) var(--padding-400);
663
683
  overflow: visible;
684
+ transition:
685
+ background-color var(--motion-duration-fast) var(--motion-ease-standard),
686
+ height var(--motion-duration-slow) var(--motion-ease-emphasized);
664
687
  }
665
688
 
666
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn:hover,
667
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn--active {
668
- background-color: transparent;
689
+ .ds-app-sidebar--panel-mode.ds-app-sidebar--expanded .ds-app-sidebar__btn {
690
+ height: 40px;
669
691
  }
670
692
 
671
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn-left {
672
- flex-direction: column;
673
- gap: var(--gap-100);
674
- width: 100%;
693
+ .ds-app-sidebar--panel-mode .ds-app-sidebar__btn-left {
694
+ height: 24px;
675
695
  }
676
696
 
677
- /* The trailing chevron and count pill are faded out on any collapsed
678
- rail, but faded still takes width: on this centred column it pushed
679
- the icon and label off centre. Here they leave the layout outright. */
680
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn-chevron,
681
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn-badge {
682
- display: none;
697
+ .ds-app-sidebar--panel-mode .ds-app-sidebar__btn-chevron {
698
+ flex-shrink: 0;
683
699
  }
684
700
 
685
701
  /* The indicator: a 56 x 32 pill behind the icon carries hover and the
686
- selection, so the label stays legible on the page ground. */
687
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn-icon {
688
- display: flex;
689
- align-items: center;
690
- justify-content: center;
702
+ selection while collapsed, so the caption stays legible on the page
703
+ ground; expanded, the row's own pill takes over and the two crossfade. */
704
+ .ds-app-sidebar--panel-mode .ds-app-sidebar__btn::before {
705
+ content: '';
706
+ position: absolute;
707
+ top: var(--padding-100);
708
+ inset-inline-start: 0;
709
+ z-index: -1;
691
710
  width: 56px;
692
711
  height: 32px;
693
712
  border-radius: var(--radius-pill);
713
+ background-color: transparent;
694
714
  transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
695
715
  }
696
716
 
697
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn:hover .ds-app-sidebar__btn-icon,
698
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn--active .ds-app-sidebar__btn-icon {
717
+ .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn:hover,
718
+ .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn--active {
719
+ background-color: transparent;
720
+ }
721
+
722
+ .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn:hover::before,
723
+ .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn--active::before {
699
724
  background-color: var(--color-action-passive-bg-hover);
700
725
  }
701
726
 
702
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn-label {
703
- opacity: 1;
704
- max-width: 100%;
727
+ /* The caption: centred under the indicator, the rail's 72px wide
728
+ (80 less a 4px inset either side), fading out as the rail opens. */
729
+ .ds-app-sidebar__btn-caption {
730
+ position: absolute;
731
+ top: calc(32px + var(--padding-100) * 2);
732
+ inset-inline-start: calc(var(--padding-200) * -1);
733
+ width: 72px;
705
734
  font-family: var(--font-caption-family);
706
735
  font-size: var(--font-caption-size);
707
736
  font-weight: var(--font-caption-weight);
708
737
  line-height: var(--font-caption-line-height);
709
738
  letter-spacing: var(--font-caption-letter-spacing);
739
+ color: var(--color-text-primary);
710
740
  text-align: center;
741
+ white-space: nowrap;
742
+ overflow: hidden;
711
743
  text-overflow: ellipsis;
744
+ pointer-events: none;
745
+ opacity: 1;
746
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
747
+ }
748
+
749
+ .ds-app-sidebar--expanded .ds-app-sidebar__btn-caption {
750
+ opacity: 0;
751
+ transition-duration: var(--motion-duration-fast);
712
752
  }
713
753
 
714
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn--active .ds-app-sidebar__btn-label {
754
+ .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn--active .ds-app-sidebar__btn-caption {
715
755
  font-weight: var(--font-paragraph-sm-emphasis-weight);
716
756
  }
717
757
 
718
- .ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__profile {
719
- /* Centres the 40px avatar in the 80px rail. */
758
+ .ds-app-sidebar--panel-mode .ds-app-sidebar__profile {
759
+ /* Centres the 40px avatar in the 80px rail, and holds it there open. */
720
760
  padding: 0 var(--padding-500);
721
761
  }
722
762
 
@@ -324,6 +324,7 @@ const AppSidebar = React.forwardRef(function AppSidebar2({
324
324
  ),
325
325
  /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-label`, children: item.label })
326
326
  ] }),
327
+ isPanelMode && /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-caption`, "aria-hidden": "true", children: item.label }),
327
328
  item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__btn-badge`, children: item.badge }),
328
329
  hasChildren && /* @__PURE__ */ jsx(
329
330
  "span",
@@ -111,6 +111,63 @@
111
111
  margin-bottom: var(--gap-100);
112
112
  }
113
113
 
114
+ /* ============================================
115
+ AUTO DELTA — inline while the stat has room,
116
+ stacked under the label once it is narrow.
117
+ The root fills its parent and is the size
118
+ container; the body inside it re-templates.
119
+ ============================================ */
120
+
121
+ .ds-stat--delta-auto {
122
+ display: block;
123
+ width: 100%;
124
+ min-width: 0;
125
+ container: ds-stat / inline-size;
126
+ }
127
+
128
+ .ds-stat__body {
129
+ display: grid;
130
+ grid-template-columns: auto 1fr;
131
+ grid-template-areas:
132
+ 'value delta'
133
+ 'label label';
134
+ justify-items: start;
135
+ column-gap: var(--gap-200);
136
+ row-gap: var(--gap-050);
137
+ }
138
+
139
+ .ds-stat__body .ds-stat__value {
140
+ grid-area: value;
141
+ }
142
+
143
+ .ds-stat__body .ds-stat__label {
144
+ grid-area: label;
145
+ }
146
+
147
+ .ds-stat__body .ds-stat__delta {
148
+ grid-area: delta;
149
+ align-self: end;
150
+ /* The inline placement's optical baseline. */
151
+ margin-bottom: var(--gap-100);
152
+ }
153
+
154
+ /* Too narrow for the value and its delta side by side: the delta drops
155
+ under the label, the stacked placement's order. */
156
+ @container ds-stat (max-width: 180px) {
157
+ .ds-stat__body {
158
+ grid-template-columns: minmax(0, 1fr);
159
+ grid-template-areas:
160
+ 'value'
161
+ 'label'
162
+ 'delta';
163
+ }
164
+
165
+ .ds-stat__body .ds-stat__delta {
166
+ align-self: start;
167
+ margin-bottom: 0;
168
+ }
169
+ }
170
+
114
171
  /* Icons take the colour of the text around them. Explicit, not just
115
172
  inherited by default: a consumer stylesheet that sets a colour on
116
173
  .material-symbols-rounded itself (a common global default) would
@@ -9,8 +9,14 @@ type StatOwnProps = {
9
9
  delta?: string;
10
10
  /** Direction of the delta — colours it and adds an arrow */
11
11
  trend?: 'up' | 'down' | 'neutral';
12
- /** Where the delta sits: stacked below the label, or inline to the right of the value, bottom-aligned */
13
- deltaPlacement?: 'stacked' | 'inline';
12
+ /**
13
+ * Where the delta sits: stacked below the label, inline to the right of the
14
+ * value (bottom-aligned), or `auto`: inline while the stat has room and
15
+ * stacked once it is narrow, such as a KPI tile in a two-up phone grid.
16
+ * `auto` fills its parent's width and reads its own, so it restacks
17
+ * wherever it sits, whatever the viewport.
18
+ */
19
+ deltaPlacement?: 'stacked' | 'inline' | 'auto';
14
20
  /** Stat size */
15
21
  size?: 'default' | 'large';
16
22
  /** The BCP 47 locale a number `value` is written in, such as `ar-EG`. Defaults to `en-US`. This component can render from a Server Component, so it takes the prop alone and reads no LocaleProvider. */
@@ -1,4 +1,4 @@
1
- import { jsxs, jsx } from "react/jsx-runtime";
1
+ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import "./Stat.css";
4
4
  import "../../fonts/material-symbols.css";
@@ -25,9 +25,10 @@ const Stat = React.forwardRef(
25
25
  baseClass,
26
26
  `${baseClass}--${size}`,
27
27
  deltaPlacement === "inline" ? `${baseClass}--delta-inline` : "",
28
+ deltaPlacement === "auto" ? `${baseClass}--delta-auto` : "",
28
29
  className
29
30
  ].filter(Boolean).join(" ");
30
- return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
31
+ const parts = /* @__PURE__ */ jsxs(Fragment, { children: [
31
32
  /* @__PURE__ */ jsx("span", { className: `${baseClass}__value`, children: typeof value === "number" ? formatNumber(value, locale) : value }),
32
33
  /* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: label }),
33
34
  delta && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__delta ${baseClass}__delta--${trend}`, children: [
@@ -35,6 +36,7 @@ const Stat = React.forwardRef(
35
36
  delta
36
37
  ] })
37
38
  ] });
39
+ return /* @__PURE__ */ jsx("div", { ...rest, ref, className: classes, children: deltaPlacement === "auto" ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__body`, children: parts }) : parts });
38
40
  }
39
41
  );
40
42
  Stat.displayName = "Stat";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rift-ds",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "An AI-ready React design system: accessible components on composable tokens, light/dark theming, and CSS-variable overrides.",
5
5
  "author": {
6
6
  "name": "Robert Ritacca",