gbs-add-block 1.2.16 → 1.2.17
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 +2 -2
- package/index.cjs +5 -0
- package/package.json +1 -1
- package/source/beta-components/card/README.md +60 -0
- package/source/beta-components/card/__tests__/core.test.ts +57 -0
- package/source/beta-components/card/core/index.ts +4 -0
- package/source/beta-components/card/core/trend.ts +32 -0
- package/source/beta-components/card/core/types.ts +5 -0
- package/source/beta-components/card/index.ts +5 -0
- package/source/beta-components/card/react/Card.tsx +133 -0
- package/source/beta-components/card/react/Stat.tsx +127 -0
- package/source/beta-components/card/react/props.ts +4 -0
- package/source/beta-components/card/styles.css +231 -0
- package/source/beta-components/menu/README.md +68 -0
- package/source/beta-components/menu/__tests__/core.test.ts +120 -0
- package/source/beta-components/menu/core/index.ts +11 -0
- package/source/beta-components/menu/core/navigation.ts +84 -0
- package/source/beta-components/menu/core/types.ts +32 -0
- package/source/beta-components/menu/index.ts +19 -0
- package/source/beta-components/menu/react/Menu.tsx +371 -0
- package/source/beta-components/menu/react/context.ts +31 -0
- package/source/beta-components/menu/react/items.tsx +268 -0
- package/source/beta-components/menu/react/locale.ts +6 -0
- package/source/beta-components/menu/react/props.ts +4 -0
- package/source/beta-components/menu/react/useMenuKeys.ts +124 -0
- package/source/beta-components/menu/styles.css +222 -0
- package/source/beta-components/popover/README.md +44 -0
- package/source/beta-components/popover/core/index.ts +2 -0
- package/source/beta-components/popover/core/types.ts +12 -0
- package/source/beta-components/popover/index.ts +4 -0
- package/source/beta-components/popover/react/Popover.tsx +202 -0
- package/source/beta-components/popover/react/locale.ts +5 -0
- package/source/beta-components/popover/react/props.ts +4 -0
- package/source/beta-components/popover/styles.css +146 -0
- package/source/beta-components/shared/__tests__/position.test.ts +143 -0
- package/source/beta-components/shared/core/position.ts +124 -0
- package/source/beta-components/shared/index.ts +8 -7
- package/source/beta-components/shared/react/Popover.tsx +189 -147
- package/source/beta-components/shared/version.json +4 -4
- package/source/beta-components/shared/version.ts +6 -6
- package/source/beta-components/skeleton/README.md +58 -0
- package/source/beta-components/skeleton/__tests__/core.test.ts +41 -0
- package/source/beta-components/skeleton/core/index.ts +3 -0
- package/source/beta-components/skeleton/core/lines.ts +25 -0
- package/source/beta-components/skeleton/core/types.ts +5 -0
- package/source/beta-components/skeleton/index.ts +5 -0
- package/source/beta-components/skeleton/react/Empty.tsx +51 -0
- package/source/beta-components/skeleton/react/Skeleton.tsx +92 -0
- package/source/beta-components/skeleton/react/props.ts +4 -0
- package/source/beta-components/skeleton/styles.css +186 -0
- package/source/beta-components/spinner/styles.css +174 -167
- package/source/beta-components/tabs/react/Tabs.tsx +65 -17
- package/source/beta-components/tabs/styles.css +264 -257
- package/source/beta-components/tooltip/README.md +43 -0
- package/source/beta-components/tooltip/__tests__/core.test.ts +42 -0
- package/source/beta-components/tooltip/core/index.ts +4 -0
- package/source/beta-components/tooltip/core/types.ts +4 -0
- package/source/beta-components/tooltip/core/visibility.ts +34 -0
- package/source/beta-components/tooltip/index.ts +3 -0
- package/source/beta-components/tooltip/react/Tooltip.tsx +196 -0
- package/source/beta-components/tooltip/react/props.ts +4 -0
- package/source/beta-components/tooltip/styles.css +72 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# GBS Building Blocks 2.0 (v1.2.
|
|
1
|
+
# GBS Building Blocks 2.0 (v1.2.17)
|
|
2
2
|
|
|
3
3
|
Latest and upgraded version of GBS building blocks with headless UI and removed dependencies.
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ For detailed documentation on usage and props, Please visit: [Building Block Doc
|
|
|
10
10
|
|
|
11
11
|
New Beta Components are avilable for testing and feedback, Please visit: [Building Block Documentation v2.0.0 beta](https://gramprokit.vercel.app/2.0.0-beta)
|
|
12
12
|
|
|
13
|
-
## What's New 🎉 (Ver 1.2.
|
|
13
|
+
## What's New 🎉 (Ver 1.2.17)
|
|
14
14
|
|
|
15
15
|
- Update Candidate for next major change 2.0.0
|
|
16
16
|
|
package/index.cjs
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Card
|
|
2
|
+
|
|
3
|
+
A surface that groups related content, plus `Stat` for a single figure. For
|
|
4
|
+
React 19, with no runtime dependencies besides React.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { Card, CardHeader, CardBody, CardFooter, Stat, trendDirection } from "@/components/card";
|
|
8
|
+
import "@/components/card/styles.css";
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<Card>
|
|
13
|
+
<CardHeader title={<h3>Revenue</h3>} actions={<Menu trigger={<Button variant="ghost">…</Button>}>…</Menu>} />
|
|
14
|
+
<CardBody>
|
|
15
|
+
<Stat
|
|
16
|
+
label="This month"
|
|
17
|
+
value="£48,120"
|
|
18
|
+
trend={{ direction: trendDirection(12.4), label: "12.4%", description: "vs last month" }}
|
|
19
|
+
/>
|
|
20
|
+
</CardBody>
|
|
21
|
+
</Card>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Card
|
|
25
|
+
|
|
26
|
+
| Prop | Default | Description |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `variant` | `outlined` | `outlined`, `elevated` or `plain`. |
|
|
29
|
+
| `padding` | `md` | `none`, `sm`, `md` or `lg`. |
|
|
30
|
+
| `href` / `target` / `rel` | — | Makes the whole card a link. |
|
|
31
|
+
| `interactive` | `false` | Hover and focus styles for a card clickable another way. |
|
|
32
|
+
| `className`, `classNames`, `style` | — | Slots: `root`, `header`, `title`, `description`, `actions`, `body`, `footer`. |
|
|
33
|
+
|
|
34
|
+
`CardHeader` takes `title`, `description` and `actions`. Pass your own heading
|
|
35
|
+
element as the title — `<h3>Revenue</h3>` — so the page decides the level; a card
|
|
36
|
+
is not always at the same depth in the outline.
|
|
37
|
+
|
|
38
|
+
When `href` is set the card renders as an `<a>`, so keep other links and buttons
|
|
39
|
+
out of it: nesting interactive elements is not valid HTML and traps the
|
|
40
|
+
keyboard. Use `interactive` with your own handler instead.
|
|
41
|
+
|
|
42
|
+
## Stat
|
|
43
|
+
|
|
44
|
+
| Prop | Description |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `label` / `value` | Always both: a number on its own means nothing. |
|
|
47
|
+
| `trend` | `{ direction, label, invert?, description? }`. |
|
|
48
|
+
| `help` | A note under the figure. |
|
|
49
|
+
| `icon` | Drawn beside the label. |
|
|
50
|
+
| `loading` | Placeholder bars, announced as "Loading". |
|
|
51
|
+
|
|
52
|
+
`trend.invert` is for figures where down is the good outcome, such as churn or
|
|
53
|
+
error rates: it changes the colour, and the arrow still points the way the
|
|
54
|
+
number actually moved. The direction is also written out for screen readers, so
|
|
55
|
+
the meaning never rests on colour alone.
|
|
56
|
+
|
|
57
|
+
`trendDirection(change, threshold?)`, `isPositive(direction, invert?)` and
|
|
58
|
+
`percentChange(from, to)` are framework-free helpers in `core/trend.ts`.
|
|
59
|
+
`percentChange` returns `null` when the starting value is zero, because growth
|
|
60
|
+
from nothing is not a percentage.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isPositive, percentChange, trendDirection } from "../core/trend";
|
|
3
|
+
|
|
4
|
+
describe("trendDirection", () => {
|
|
5
|
+
it("reads the sign of the change", () => {
|
|
6
|
+
expect(trendDirection(4.2)).toBe("up");
|
|
7
|
+
expect(trendDirection(-0.3)).toBe("down");
|
|
8
|
+
expect(trendDirection(0)).toBe("flat");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("treats movement under the threshold as flat", () => {
|
|
12
|
+
expect(trendDirection(0.4, 0.5)).toBe("flat");
|
|
13
|
+
expect(trendDirection(-0.4, 0.5)).toBe("flat");
|
|
14
|
+
expect(trendDirection(0.6, 0.5)).toBe("up");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("is flat for a value that is not a number", () => {
|
|
18
|
+
expect(trendDirection(Number.NaN)).toBe("flat");
|
|
19
|
+
expect(trendDirection(Number.POSITIVE_INFINITY)).toBe("flat");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("isPositive", () => {
|
|
24
|
+
it("treats up as good by default", () => {
|
|
25
|
+
expect(isPositive("up")).toBe(true);
|
|
26
|
+
expect(isPositive("down")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("flips for figures where down is the good outcome, such as churn", () => {
|
|
30
|
+
expect(isPositive("up", true)).toBe(false);
|
|
31
|
+
expect(isPositive("down", true)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("is null when nothing moved, so no colour is applied", () => {
|
|
35
|
+
expect(isPositive("flat")).toBeNull();
|
|
36
|
+
expect(isPositive("flat", true)).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("percentChange", () => {
|
|
41
|
+
it("is the change as a share of where it started", () => {
|
|
42
|
+
expect(percentChange(200, 250)).toBe(25);
|
|
43
|
+
expect(percentChange(200, 150)).toBe(-25);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("measures against the size of a negative start", () => {
|
|
47
|
+
expect(percentChange(-200, -150)).toBe(25);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("is null from zero, which has no percentage", () => {
|
|
51
|
+
expect(percentChange(0, 10)).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("is null for values that are not numbers", () => {
|
|
55
|
+
expect(percentChange(Number.NaN, 10)).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type TrendDirection = "up" | "down" | "flat";
|
|
2
|
+
|
|
3
|
+
/** Whether a change counts as a rise, a fall, or no real movement. */
|
|
4
|
+
export function trendDirection(change: number, threshold = 0): TrendDirection {
|
|
5
|
+
if (!Number.isFinite(change)) return "flat";
|
|
6
|
+
if (change > threshold) return "up";
|
|
7
|
+
if (change < -threshold) return "down";
|
|
8
|
+
return "flat";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether a direction should be drawn as good news.
|
|
13
|
+
*
|
|
14
|
+
* Up is usually good, but not for churn, refunds or error rates, so `invert`
|
|
15
|
+
* flips it. Colour alone never carries the meaning: the arrow and the label do
|
|
16
|
+
* too.
|
|
17
|
+
*/
|
|
18
|
+
export function isPositive(direction: TrendDirection, invert = false): boolean | null {
|
|
19
|
+
if (direction === "flat") return null;
|
|
20
|
+
return invert ? direction === "down" : direction === "up";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A change as a percentage of where it started.
|
|
25
|
+
*
|
|
26
|
+
* Returns null when there is nothing to compare against: growth from zero is
|
|
27
|
+
* not "infinite percent", it is simply not a percentage.
|
|
28
|
+
*/
|
|
29
|
+
export function percentChange(from: number, to: number): number | null {
|
|
30
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || from === 0) return null;
|
|
31
|
+
return ((to - from) / Math.abs(from)) * 100;
|
|
32
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type CardVariant = "outlined" | "elevated" | "plain";
|
|
2
|
+
export type CardPadding = "none" | "sm" | "md" | "lg";
|
|
3
|
+
|
|
4
|
+
export type CardSlot = "root" | "header" | "title" | "description" | "actions" | "body" | "footer";
|
|
5
|
+
export type StatSlot = "root" | "label" | "value" | "trend" | "help" | "icon";
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { AnchorHTMLAttributes, CSSProperties, HTMLAttributes, ReactNode, Ref } from "react";
|
|
4
|
+
import type { CardPadding, CardSlot, CardVariant } from "../core/types";
|
|
5
|
+
import { cx } from "./props";
|
|
6
|
+
|
|
7
|
+
export interface CardProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
/** Default `outlined`. */
|
|
10
|
+
variant?: CardVariant;
|
|
11
|
+
/** Padding applied to the header, body and footer. Default `md`. */
|
|
12
|
+
padding?: CardPadding;
|
|
13
|
+
/**
|
|
14
|
+
* Makes the whole card a link. The card becomes an `<a>`, so keep any other
|
|
15
|
+
* interactive content out of it — nested links and buttons are not valid.
|
|
16
|
+
*/
|
|
17
|
+
href?: string;
|
|
18
|
+
/** Passed through when `href` is set. */
|
|
19
|
+
target?: AnchorHTMLAttributes<HTMLAnchorElement>["target"];
|
|
20
|
+
rel?: string;
|
|
21
|
+
/** Lifts the card on hover, for a card that is clickable by other means. */
|
|
22
|
+
interactive?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
classNames?: Partial<Record<CardSlot, string>>;
|
|
25
|
+
style?: CSSProperties;
|
|
26
|
+
ref?: Ref<HTMLElement>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A surface that groups related content, with optional header, body and footer.
|
|
31
|
+
*
|
|
32
|
+
* Give it a heading through `CardHeader` rather than a bare `title` prop, so the
|
|
33
|
+
* page decides the heading level: a card is not always at the same depth.
|
|
34
|
+
*/
|
|
35
|
+
export function Card(props: CardProps) {
|
|
36
|
+
const {
|
|
37
|
+
ref,
|
|
38
|
+
children,
|
|
39
|
+
variant = "outlined",
|
|
40
|
+
padding = "md",
|
|
41
|
+
href,
|
|
42
|
+
target,
|
|
43
|
+
rel,
|
|
44
|
+
interactive = false,
|
|
45
|
+
className,
|
|
46
|
+
classNames,
|
|
47
|
+
style,
|
|
48
|
+
...rest
|
|
49
|
+
} = props;
|
|
50
|
+
|
|
51
|
+
const shared = {
|
|
52
|
+
className: cx("cd-root", classNames?.root, className),
|
|
53
|
+
style,
|
|
54
|
+
"data-variant": variant,
|
|
55
|
+
"data-padding": padding,
|
|
56
|
+
"data-interactive": (interactive || href !== undefined) || undefined,
|
|
57
|
+
...rest,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (href !== undefined) {
|
|
61
|
+
return (
|
|
62
|
+
<a
|
|
63
|
+
{...shared}
|
|
64
|
+
ref={ref as Ref<HTMLAnchorElement>}
|
|
65
|
+
href={href}
|
|
66
|
+
target={target}
|
|
67
|
+
// Anything opening a new tab gets the safe default unless told otherwise.
|
|
68
|
+
rel={rel ?? (target === "_blank" ? "noreferrer" : undefined)}
|
|
69
|
+
>
|
|
70
|
+
{children}
|
|
71
|
+
</a>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div {...shared} ref={ref as Ref<HTMLDivElement>}>
|
|
77
|
+
{children}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface CardHeaderProps {
|
|
83
|
+
/** The heading. Wrap it in your own `h2`/`h3` to set the level. */
|
|
84
|
+
title?: ReactNode;
|
|
85
|
+
description?: ReactNode;
|
|
86
|
+
/** Buttons or a menu, aligned to the end of the header. */
|
|
87
|
+
actions?: ReactNode;
|
|
88
|
+
children?: ReactNode;
|
|
89
|
+
className?: string;
|
|
90
|
+
classNames?: Partial<Record<CardSlot, string>>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function CardHeader({
|
|
94
|
+
title,
|
|
95
|
+
description,
|
|
96
|
+
actions,
|
|
97
|
+
children,
|
|
98
|
+
className,
|
|
99
|
+
classNames,
|
|
100
|
+
}: CardHeaderProps) {
|
|
101
|
+
return (
|
|
102
|
+
<div className={cx("cd-header", classNames?.header, className)}>
|
|
103
|
+
<div className="cd-heading">
|
|
104
|
+
{title && <div className={cx("cd-title", classNames?.title)}>{title}</div>}
|
|
105
|
+
{description && (
|
|
106
|
+
<div className={cx("cd-description", classNames?.description)}>{description}</div>
|
|
107
|
+
)}
|
|
108
|
+
{children}
|
|
109
|
+
</div>
|
|
110
|
+
{actions && <div className={cx("cd-actions", classNames?.actions)}>{actions}</div>}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function CardBody({
|
|
116
|
+
children,
|
|
117
|
+
className,
|
|
118
|
+
}: {
|
|
119
|
+
children?: ReactNode;
|
|
120
|
+
className?: string;
|
|
121
|
+
}) {
|
|
122
|
+
return <div className={cx("cd-body", className)}>{children}</div>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function CardFooter({
|
|
126
|
+
children,
|
|
127
|
+
className,
|
|
128
|
+
}: {
|
|
129
|
+
children?: ReactNode;
|
|
130
|
+
className?: string;
|
|
131
|
+
}) {
|
|
132
|
+
return <div className={cx("cd-footer", className)}>{children}</div>;
|
|
133
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, type CSSProperties, type ReactNode } from "react";
|
|
4
|
+
import { isPositive, type TrendDirection } from "../core/trend";
|
|
5
|
+
import type { StatSlot } from "../core/types";
|
|
6
|
+
import { cx } from "./props";
|
|
7
|
+
|
|
8
|
+
export interface StatTrend {
|
|
9
|
+
/** `up`, `down` or `flat`. `trendDirection(change)` works this out for you. */
|
|
10
|
+
direction: TrendDirection;
|
|
11
|
+
/** What is shown beside the arrow, already formatted, e.g. "12.4%". */
|
|
12
|
+
label: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Down is the good outcome for this figure, as with churn or error rates.
|
|
15
|
+
* Only changes the colour; the arrow still points the way the number moved.
|
|
16
|
+
*/
|
|
17
|
+
invert?: boolean;
|
|
18
|
+
/** Read out after the change, e.g. "since last month". */
|
|
19
|
+
description?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StatProps {
|
|
23
|
+
/** What the figure is. Always visible — a number alone means nothing. */
|
|
24
|
+
label: ReactNode;
|
|
25
|
+
value: ReactNode;
|
|
26
|
+
trend?: StatTrend;
|
|
27
|
+
/** A note under the figure. */
|
|
28
|
+
help?: ReactNode;
|
|
29
|
+
icon?: ReactNode;
|
|
30
|
+
/** Shows placeholder bars instead of the figure. */
|
|
31
|
+
loading?: boolean;
|
|
32
|
+
className?: string;
|
|
33
|
+
classNames?: Partial<Record<StatSlot, string>>;
|
|
34
|
+
style?: CSSProperties;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ARROWS: Record<TrendDirection, string> = {
|
|
38
|
+
up: "M12 19V5M5 12l7-7 7 7",
|
|
39
|
+
down: "M12 5v14M19 12l-7 7-7-7",
|
|
40
|
+
flat: "M5 12h14",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** The word behind the arrow, so the direction is not carried by colour alone. */
|
|
44
|
+
const DIRECTION_TEXT: Record<TrendDirection, string> = {
|
|
45
|
+
up: "Up",
|
|
46
|
+
down: "Down",
|
|
47
|
+
flat: "No change",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A single figure with its label and, optionally, how it has changed.
|
|
52
|
+
*
|
|
53
|
+
* Put it inside a `Card` for a dashboard tile, or use it on its own in a row of
|
|
54
|
+
* summary numbers.
|
|
55
|
+
*/
|
|
56
|
+
export function Stat(props: StatProps) {
|
|
57
|
+
const { label, value, trend, help, icon, loading = false, className, classNames, style } = props;
|
|
58
|
+
const id = useId();
|
|
59
|
+
const positive = trend ? isPositive(trend.direction, trend.invert) : null;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className={cx("st-root", classNames?.root, className)}
|
|
64
|
+
style={style}
|
|
65
|
+
data-loading={loading || undefined}
|
|
66
|
+
>
|
|
67
|
+
<div className="st-head">
|
|
68
|
+
<span id={`${id}-label`} className={cx("st-label", classNames?.label)}>
|
|
69
|
+
{label}
|
|
70
|
+
</span>
|
|
71
|
+
{icon && (
|
|
72
|
+
<span className={cx("st-icon", classNames?.icon)} aria-hidden="true">
|
|
73
|
+
{icon}
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{loading ? (
|
|
79
|
+
<>
|
|
80
|
+
<div className="st-placeholder st-placeholder-value" />
|
|
81
|
+
<div className="st-placeholder st-placeholder-help" />
|
|
82
|
+
<span className="st-sr-only">Loading</span>
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
<>
|
|
86
|
+
<div
|
|
87
|
+
className={cx("st-value", classNames?.value)}
|
|
88
|
+
aria-labelledby={`${id}-label`}
|
|
89
|
+
role="figure"
|
|
90
|
+
>
|
|
91
|
+
{value}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{trend && (
|
|
95
|
+
<div
|
|
96
|
+
className={cx("st-trend", classNames?.trend)}
|
|
97
|
+
data-direction={trend.direction}
|
|
98
|
+
data-positive={positive === null ? undefined : positive}
|
|
99
|
+
>
|
|
100
|
+
<svg
|
|
101
|
+
width="14"
|
|
102
|
+
height="14"
|
|
103
|
+
viewBox="0 0 24 24"
|
|
104
|
+
fill="none"
|
|
105
|
+
stroke="currentColor"
|
|
106
|
+
strokeWidth="2.5"
|
|
107
|
+
strokeLinecap="round"
|
|
108
|
+
strokeLinejoin="round"
|
|
109
|
+
aria-hidden="true"
|
|
110
|
+
focusable="false"
|
|
111
|
+
>
|
|
112
|
+
<path d={ARROWS[trend.direction]} />
|
|
113
|
+
</svg>
|
|
114
|
+
<span className="st-sr-only">{DIRECTION_TEXT[trend.direction]}</span>
|
|
115
|
+
<span className="st-trend-label">{trend.label}</span>
|
|
116
|
+
{trend.description && (
|
|
117
|
+
<span className="st-trend-description">{trend.description}</span>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{help && <div className={cx("st-help", classNames?.help)}>{help}</div>}
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|