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.
- package/bin/templates/blocks/app/dashboard/page.module.css +4 -3
- package/bin/templates/blocks/app/dashboard/page.tsx +1 -1
- package/bin/templates/blocks/components/blocks/AppShell.module.css +50 -0
- package/bin/templates/blocks/components/blocks/AppShell.tsx +25 -6
- package/chat/ChatThreads.d.ts +3 -1
- package/chat/ChatThreads.js +3 -1
- package/components/AppSidebar/AppSidebar.css +83 -43
- package/components/AppSidebar/AppSidebar.js +1 -0
- package/components/Stat/Stat.css +57 -0
- package/components/Stat/Stat.d.ts +8 -2
- package/components/Stat/Stat.js +4 -2
- package/package.json +1 -1
|
@@ -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
|
|
3
|
-
|
|
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-
|
|
85
|
+
gap: var(--gap-300);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
.panels {
|
|
@@ -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
|
-
<
|
|
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
|
|
108
|
+
<CommandPaletteTrigger className={styles.search} />
|
|
90
109
|
<div className={styles.topBarActions}>
|
|
91
|
-
<CommandPaletteTrigger
|
|
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
|
|
95
|
-
<ThemeToggle
|
|
113
|
+
<AiButton />
|
|
114
|
+
<ThemeToggle showLabels={false} />
|
|
96
115
|
</div>
|
|
97
116
|
</header>
|
|
98
117
|
|
package/chat/ChatThreads.d.ts
CHANGED
|
@@ -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;
|
package/chat/ChatThreads.js
CHANGED
|
@@ -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,
|
|
625
|
-
between
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
|
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
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
667
|
-
|
|
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
|
|
672
|
-
|
|
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
|
-
|
|
678
|
-
|
|
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
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
|
698
|
-
.ds-app-sidebar--panel-mode:not(.ds-app-sidebar--expanded) .ds-app-sidebar__btn--active
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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-
|
|
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
|
|
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",
|
package/components/Stat/Stat.css
CHANGED
|
@@ -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
|
-
/**
|
|
13
|
-
|
|
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. */
|
package/components/Stat/Stat.js
CHANGED
|
@@ -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
|
-
|
|
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