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.
Files changed (62) hide show
  1. package/README.md +2 -2
  2. package/index.cjs +5 -0
  3. package/package.json +1 -1
  4. package/source/beta-components/card/README.md +60 -0
  5. package/source/beta-components/card/__tests__/core.test.ts +57 -0
  6. package/source/beta-components/card/core/index.ts +4 -0
  7. package/source/beta-components/card/core/trend.ts +32 -0
  8. package/source/beta-components/card/core/types.ts +5 -0
  9. package/source/beta-components/card/index.ts +5 -0
  10. package/source/beta-components/card/react/Card.tsx +133 -0
  11. package/source/beta-components/card/react/Stat.tsx +127 -0
  12. package/source/beta-components/card/react/props.ts +4 -0
  13. package/source/beta-components/card/styles.css +231 -0
  14. package/source/beta-components/menu/README.md +68 -0
  15. package/source/beta-components/menu/__tests__/core.test.ts +120 -0
  16. package/source/beta-components/menu/core/index.ts +11 -0
  17. package/source/beta-components/menu/core/navigation.ts +84 -0
  18. package/source/beta-components/menu/core/types.ts +32 -0
  19. package/source/beta-components/menu/index.ts +19 -0
  20. package/source/beta-components/menu/react/Menu.tsx +371 -0
  21. package/source/beta-components/menu/react/context.ts +31 -0
  22. package/source/beta-components/menu/react/items.tsx +268 -0
  23. package/source/beta-components/menu/react/locale.ts +6 -0
  24. package/source/beta-components/menu/react/props.ts +4 -0
  25. package/source/beta-components/menu/react/useMenuKeys.ts +124 -0
  26. package/source/beta-components/menu/styles.css +222 -0
  27. package/source/beta-components/popover/README.md +44 -0
  28. package/source/beta-components/popover/core/index.ts +2 -0
  29. package/source/beta-components/popover/core/types.ts +12 -0
  30. package/source/beta-components/popover/index.ts +4 -0
  31. package/source/beta-components/popover/react/Popover.tsx +202 -0
  32. package/source/beta-components/popover/react/locale.ts +5 -0
  33. package/source/beta-components/popover/react/props.ts +4 -0
  34. package/source/beta-components/popover/styles.css +146 -0
  35. package/source/beta-components/shared/__tests__/position.test.ts +143 -0
  36. package/source/beta-components/shared/core/position.ts +124 -0
  37. package/source/beta-components/shared/index.ts +8 -7
  38. package/source/beta-components/shared/react/Popover.tsx +189 -147
  39. package/source/beta-components/shared/version.json +4 -4
  40. package/source/beta-components/shared/version.ts +6 -6
  41. package/source/beta-components/skeleton/README.md +58 -0
  42. package/source/beta-components/skeleton/__tests__/core.test.ts +41 -0
  43. package/source/beta-components/skeleton/core/index.ts +3 -0
  44. package/source/beta-components/skeleton/core/lines.ts +25 -0
  45. package/source/beta-components/skeleton/core/types.ts +5 -0
  46. package/source/beta-components/skeleton/index.ts +5 -0
  47. package/source/beta-components/skeleton/react/Empty.tsx +51 -0
  48. package/source/beta-components/skeleton/react/Skeleton.tsx +92 -0
  49. package/source/beta-components/skeleton/react/props.ts +4 -0
  50. package/source/beta-components/skeleton/styles.css +186 -0
  51. package/source/beta-components/spinner/styles.css +174 -167
  52. package/source/beta-components/tabs/react/Tabs.tsx +65 -17
  53. package/source/beta-components/tabs/styles.css +264 -257
  54. package/source/beta-components/tooltip/README.md +43 -0
  55. package/source/beta-components/tooltip/__tests__/core.test.ts +42 -0
  56. package/source/beta-components/tooltip/core/index.ts +4 -0
  57. package/source/beta-components/tooltip/core/types.ts +4 -0
  58. package/source/beta-components/tooltip/core/visibility.ts +34 -0
  59. package/source/beta-components/tooltip/index.ts +3 -0
  60. package/source/beta-components/tooltip/react/Tooltip.tsx +196 -0
  61. package/source/beta-components/tooltip/react/props.ts +4 -0
  62. 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.16)
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.16)
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
@@ -51,6 +51,11 @@ const CONFIG = {
51
51
  "Checkbox",
52
52
  "Tabs",
53
53
  "Spinner",
54
+ "Menu",
55
+ "Tooltip",
56
+ "Popover",
57
+ "Card",
58
+ "Skeleton",
54
59
  ],
55
60
  // Beta folder names that are not simply the lowercased component name.
56
61
  betaFolders: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gbs-add-block",
3
- "version": "1.2.16",
3
+ "version": "1.2.17",
4
4
  "description": "React Component Library",
5
5
  "type": "module",
6
6
  "files": [
@@ -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,4 @@
1
+ // Framework-free: what counts as a rise or a fall.
2
+ export { isPositive, percentChange, trendDirection } from "./trend";
3
+ export type { TrendDirection } from "./trend";
4
+ export type * from "./types";
@@ -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,5 @@
1
+ export { Card, CardBody, CardFooter, CardHeader } from "./react/Card";
2
+ export type { CardHeaderProps, CardProps } from "./react/Card";
3
+ export { Stat } from "./react/Stat";
4
+ export type { StatProps, StatTrend } from "./react/Stat";
5
+ export * from "./core";
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export type { CardSlot, StatSlot } from "../core/types";
2
+
3
+ // Shared with every other component, so class handling cannot drift.
4
+ export { cx } from "../../shared/core/cx";