@scttlnd/fintech-design-system 0.2.0 → 0.3.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/AGENTS.md +1 -1
- package/README.md +9 -3
- package/dist/components/Badge.d.mts +9 -0
- package/dist/components/Badge.mjs +18 -0
- package/dist/components/Badge.mjs.map +1 -0
- package/dist/components/Button.d.mts +6 -0
- package/dist/components/Button.mjs +14 -0
- package/dist/components/Button.mjs.map +1 -0
- package/dist/components/Field.d.mts +20 -0
- package/dist/components/Field.mjs +57 -0
- package/dist/components/Field.mjs.map +1 -0
- package/dist/components/Menu.d.mts +18 -0
- package/dist/components/Menu.mjs +109 -0
- package/dist/components/Menu.mjs.map +1 -0
- package/dist/components/Meter.d.mts +39 -0
- package/dist/components/Meter.mjs +82 -0
- package/dist/components/Meter.mjs.map +1 -0
- package/dist/components/Metric.d.mts +17 -0
- package/dist/components/Metric.mjs +34 -0
- package/dist/components/Metric.mjs.map +1 -0
- package/dist/components/Notice.d.mts +16 -0
- package/dist/components/Notice.mjs +48 -0
- package/dist/components/Notice.mjs.map +1 -0
- package/dist/components/Panel.d.mts +13 -0
- package/dist/components/Panel.mjs +42 -0
- package/dist/components/Panel.mjs.map +1 -0
- package/dist/index.d.mts +10 -125
- package/dist/index.mjs +9 -365
- package/dist/lib/cn.d.mts +2 -0
- package/dist/lib/cn.mjs +7 -0
- package/dist/lib/cn.mjs.map +1 -0
- package/package.json +18 -3
- package/styles/components/badge.css +33 -0
- package/styles/components/button.css +45 -0
- package/styles/components/empty-state.css +20 -0
- package/styles/components/eyebrow.css +11 -0
- package/styles/components/field.css +59 -0
- package/styles/components/grain.css +245 -0
- package/styles/components/menu.css +74 -0
- package/styles/components/meter.css +62 -0
- package/styles/components/metric.css +28 -0
- package/styles/components/notice.css +34 -0
- package/styles/components/panel.css +42 -0
- package/styles/components/tone.css +15 -0
- package/styles/components.css +11 -367
- package/styles/index.css +2 -43
- package/styles/themes/funder.css +76 -8
- package/styles/themes/investor.css +76 -8
- package/styles/themes/portare.css +90 -22
- package/styles/tokens.css +88 -23
- package/dist/index.mjs.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -7,7 +7,7 @@ This repository is the **source of truth** for shared visual tokens and primitiv
|
|
|
7
7
|
3. Prefer CSS variables `--fds-*` and exported primitives. Avoid hardcoded colors and duplicate primitives.
|
|
8
8
|
4. Funder is the reference for the dark palette. Portare light theme is a **proposed** accessibility-oriented adaptation, not a verified Funder style.
|
|
9
9
|
5. Preserve keyboard focus, semantic HTML, reduced motion, 44px touch targets where appropriate, WCAG contrast, mobile viewport/safe areas, and 16px mobile form control font sizes.
|
|
10
|
-
6. Changes to public exports/tokens require SemVer, a changeset (`npx changeset`; CHANGELOG.md
|
|
10
|
+
6. Changes to public exports/tokens require SemVer, a changeset (`npx changeset`; CHANGELOG.md and the package version are generated, never edit them by hand), and consumer verification. Any PR that changes `src/` (non-test), `styles/` or package exports needs a described changeset; the `release-policy` CI job enforces it, and tokens, themes and exports need at least a minor. Never silently break an existing theme.
|
|
11
11
|
7. Never place financial arithmetic, API clients, user state, authentication, or business-specific charts in this package.
|
|
12
12
|
8. Run `npm install`, `npm run check`, `npm test`, `npm run build` before proposing a release; review npm tarball contents with `npm pack --dry-run`.
|
|
13
13
|
9. The package must be published to npm before consumer dependencies can resolve. Renovate updates versions only after publication.
|
package/README.md
CHANGED
|
@@ -28,11 +28,13 @@ import "@scttlnd/fintech-design-system/themes/investor.css"; // or portare.css /
|
|
|
28
28
|
import { Button } from "@scttlnd/fintech-design-system";
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Panels have a soft glow and film grain by default (`data-fds-grain="off"` opts out); see [Foundation tokens](docs/TOKENS.md#grain-surfaces-default-opt-out).
|
|
32
|
+
|
|
33
|
+
Read [AGENTS.md](AGENTS.md) and [adoption guide](docs/ADOPTION.md) before integrating. Planned component roadmap: [component plan](docs/COMPONENT-PLAN.md). Original Funder palette is sourced from `nicklaslind/Funder/src/app/globals.css`. Portare's light theme is a proposed product-specific adaptation.
|
|
32
34
|
|
|
33
35
|
## Showcase
|
|
34
36
|
|
|
35
|
-
The
|
|
37
|
+
The component showcase (`npm run showcase:dev` locally, `npm run showcase:build` for a static build) in [`showcase/index.html`](showcase/index.html) is deployed to GitHub Pages by `.github/workflows/pages.yml` when its components, styles, dependencies, or showcase change on `main`: <https://scottlinddk.github.io/fintech-design-system/>. One-time setup: repository **Settings → Pages → Source: GitHub Actions**.
|
|
36
38
|
|
|
37
39
|
## Components
|
|
38
40
|
|
|
@@ -47,7 +49,11 @@ The static design review page in [`showcase/index.html`](showcase/index.html) is
|
|
|
47
49
|
| `Meter`, `SegmentedMeter`, `DistributionList` | Bounded bars, discrete scale, ranked share bars (`role="meter"`) | Funder `EvidenceBar`, `RiskBars`, `ExposureList` |
|
|
48
50
|
| `Menu` | Action menu with Escape/arrow-key support and focus return | Funder `Menu` |
|
|
49
51
|
|
|
50
|
-
Components render only `--fds-*` styled markup. Formatting, locale, currency and calculations stay in the consuming app (pass pre-formatted strings, or `formatValue`). `Menu`, `Field` and `Panel` use
|
|
52
|
+
Components render only `--fds-*` styled markup. Formatting, locale, currency and calculations stay in the consuming app (pass pre-formatted strings, or `formatValue`). The published modules for `Button`, `Menu`, `Field` (including its native controls), and `Panel` carry `"use client"`. The root barrel and static primitives such as `Badge` remain server-compatible. Event handlers, `Menu.items` callbacks and `Field.children` render functions still belong in a consumer client component because functions cannot cross the server/client boundary. See [Next.js verification](docs/verification/next-app-router.md).
|
|
53
|
+
|
|
54
|
+
## Cascade layer
|
|
55
|
+
|
|
56
|
+
Package styles and themes live in `@layer fds`. Normal unlayered consumer CSS overrides them regardless of specificity or import order. This changes cascade behaviour in Phase 0; review consumer resets and focus rules before upgrading. Layered apps can declare `@layer reset, fds, app;`. See [cascade migration notes](docs/CASCADE.md).
|
|
51
57
|
|
|
52
58
|
## npm publishing
|
|
53
59
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Tone } from "./Metric.mjs";
|
|
2
|
+
import { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
4
|
+
tone?: Tone;
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/** Compact status/label chip. Never rely on tone alone: include meaningful text. */
|
|
8
|
+
export declare function Badge({ tone, icon, className, children, ...props }: BadgeProps): import("react").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=Badge.d.mts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.mjs";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
// src/components/Badge.tsx
|
|
4
|
+
/** Compact status/label chip. Never rely on tone alone: include meaningful text. */
|
|
5
|
+
function Badge({ tone = "neutral", icon, className, children, ...props }) {
|
|
6
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
7
|
+
className: cn("fds-badge", `fds-badge--${tone}`, className),
|
|
8
|
+
...props,
|
|
9
|
+
children: [icon ? /* @__PURE__ */ jsx("span", {
|
|
10
|
+
"aria-hidden": "true",
|
|
11
|
+
className: "fds-badge__icon",
|
|
12
|
+
children: icon
|
|
13
|
+
}) : null, children]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export { Badge };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=Badge.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Badge.mjs","names":[],"sources":["../../src/components/Badge.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn.js\";\nimport type { Tone } from \"./Metric.js\";\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n tone?: Tone;\n icon?: ReactNode;\n}\n\n/** Compact status/label chip. Never rely on tone alone: include meaningful text. */\nexport function Badge({ tone = \"neutral\", icon, className, children, ...props }: BadgeProps) {\n return (\n <span className={cn(\"fds-badge\", `fds-badge--${tone}`, className)} {...props}>\n {icon ? (\n <span aria-hidden=\"true\" className=\"fds-badge__icon\">\n {icon}\n </span>\n ) : null}\n {children}\n </span>\n );\n}\n"],"mappings":";;;;AAUA,SAAgB,MAAM,EAAE,OAAO,WAAW,MAAM,WAAW,UAAU,GAAG,SAAqB;CAC3F,OACE,qBAAC,QAAD;EAAM,WAAW,GAAG,aAAa,cAAc,QAAQ,SAAS;EAAG,GAAI;EAAvE,UAAA,CACG,OACC,oBAAC,QAAD;GAAM,eAAY;GAAO,WAAU;GAChC,UAAA;EACG,CAAA,IACJ,MACH,QACG;;AAEV"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ButtonHTMLAttributes } from "react";
|
|
2
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
3
|
+
variant?: "primary" | "secondary" | "ghost";
|
|
4
|
+
}
|
|
5
|
+
export declare function Button({ variant, className, type, ...props }: ButtonProps): import("react").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=Button.d.mts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.mjs";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
// src/components/Button.tsx
|
|
5
|
+
function Button({ variant = "primary", className, type = "button", ...props }) {
|
|
6
|
+
return /* @__PURE__ */ jsx("button", {
|
|
7
|
+
type,
|
|
8
|
+
className: cn("fds-button", `fds-button--${variant}`, className),
|
|
9
|
+
...props
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export { Button };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=Button.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Button.mjs","names":[],"sources":["../../src/components/Button.tsx"],"sourcesContent":["import type { ButtonHTMLAttributes } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: \"primary\" | \"secondary\" | \"ghost\";\n}\n\nexport function Button({ variant = \"primary\", className, type = \"button\", ...props }: ButtonProps) {\n return (\n <button\n type={type}\n className={cn(\"fds-button\", `fds-button--${variant}`, className)}\n {...props}\n />\n );\n}\n"],"mappings":";;;;AAOA,SAAgB,OAAO,EAAE,UAAU,WAAW,WAAW,OAAO,UAAU,GAAG,SAAsB;CACjG,OACE,oBAAC,UAAD;EACQ;EACN,WAAW,GAAG,cAAc,eAAe,WAAW,SAAS;EAC/D,GAAI;CACL,CAAA;AAEL"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { InputHTMLAttributes, ReactNode, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
|
|
2
|
+
export interface FieldControlProps {
|
|
3
|
+
id: string;
|
|
4
|
+
"aria-describedby"?: string;
|
|
5
|
+
"aria-invalid"?: true;
|
|
6
|
+
}
|
|
7
|
+
export interface FieldProps {
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
hint?: ReactNode;
|
|
10
|
+
error?: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Render prop so the control receives the generated id and aria wiring. */
|
|
13
|
+
children: (control: FieldControlProps) => ReactNode;
|
|
14
|
+
}
|
|
15
|
+
/** Label + hint + error wrapper that wires `id`, `aria-describedby` and `aria-invalid`. */
|
|
16
|
+
export declare function Field({ label, hint, error, className, children }: FieldProps): import("react").JSX.Element;
|
|
17
|
+
export declare function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>): import("react").JSX.Element;
|
|
18
|
+
export declare function Select({ className, ...props }: SelectHTMLAttributes<HTMLSelectElement>): import("react").JSX.Element;
|
|
19
|
+
export declare function Textarea({ className, ...props }: TextareaHTMLAttributes<HTMLTextAreaElement>): import("react").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=Field.d.mts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.mjs";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { useId } from "react";
|
|
5
|
+
// src/components/Field.tsx
|
|
6
|
+
/** Label + hint + error wrapper that wires `id`, `aria-describedby` and `aria-invalid`. */
|
|
7
|
+
function Field({ label, hint, error, className, children }) {
|
|
8
|
+
const id = useId();
|
|
9
|
+
const hintId = `${id}-hint`;
|
|
10
|
+
const errorId = `${id}-error`;
|
|
11
|
+
const describedBy = [hint ? hintId : null, error ? errorId : null].filter(Boolean).join(" ");
|
|
12
|
+
const control = { id };
|
|
13
|
+
if (describedBy) control["aria-describedby"] = describedBy;
|
|
14
|
+
if (error) control["aria-invalid"] = true;
|
|
15
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
16
|
+
className: cn("fds-field", className),
|
|
17
|
+
children: [
|
|
18
|
+
/* @__PURE__ */ jsx("label", {
|
|
19
|
+
className: "fds-field__label",
|
|
20
|
+
htmlFor: id,
|
|
21
|
+
children: label
|
|
22
|
+
}),
|
|
23
|
+
children(control),
|
|
24
|
+
hint ? /* @__PURE__ */ jsx("p", {
|
|
25
|
+
className: "fds-field__hint",
|
|
26
|
+
id: hintId,
|
|
27
|
+
children: hint
|
|
28
|
+
}) : null,
|
|
29
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
30
|
+
className: "fds-field__error",
|
|
31
|
+
id: errorId,
|
|
32
|
+
children: error
|
|
33
|
+
}) : null
|
|
34
|
+
]
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function Input({ className, ...props }) {
|
|
38
|
+
return /* @__PURE__ */ jsx("input", {
|
|
39
|
+
className: cn("fds-control", className),
|
|
40
|
+
...props
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function Select({ className, ...props }) {
|
|
44
|
+
return /* @__PURE__ */ jsx("select", {
|
|
45
|
+
className: cn("fds-control", className),
|
|
46
|
+
...props
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function Textarea({ className, ...props }) {
|
|
50
|
+
return /* @__PURE__ */ jsx("textarea", {
|
|
51
|
+
className: cn("fds-control fds-control--multiline", className),
|
|
52
|
+
...props
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export { Field, Input, Select, Textarea };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=Field.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Field.mjs","names":[],"sources":["../../src/components/Field.tsx"],"sourcesContent":["import {\n useId,\n type InputHTMLAttributes,\n type ReactNode,\n type SelectHTMLAttributes,\n type TextareaHTMLAttributes,\n} from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\nexport interface FieldControlProps {\n id: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n}\n\nexport interface FieldProps {\n label: ReactNode;\n hint?: ReactNode;\n error?: ReactNode;\n className?: string;\n /** Render prop so the control receives the generated id and aria wiring. */\n children: (control: FieldControlProps) => ReactNode;\n}\n\n/** Label + hint + error wrapper that wires `id`, `aria-describedby` and `aria-invalid`. */\nexport function Field({ label, hint, error, className, children }: FieldProps) {\n const id = useId();\n const hintId = `${id}-hint`;\n const errorId = `${id}-error`;\n const describedBy = [hint ? hintId : null, error ? errorId : null].filter(Boolean).join(\" \");\n const control: FieldControlProps = { id };\n if (describedBy) control[\"aria-describedby\"] = describedBy;\n if (error) control[\"aria-invalid\"] = true;\n return (\n <div className={cn(\"fds-field\", className)}>\n <label className=\"fds-field__label\" htmlFor={id}>\n {label}\n </label>\n {children(control)}\n {hint ? (\n <p className=\"fds-field__hint\" id={hintId}>\n {hint}\n </p>\n ) : null}\n {error ? (\n <p className=\"fds-field__error\" id={errorId}>\n {error}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {\n return <input className={cn(\"fds-control\", className)} {...props} />;\n}\n\nexport function Select({ className, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {\n return <select className={cn(\"fds-control\", className)} {...props} />;\n}\n\nexport function Textarea({ className, ...props }: TextareaHTMLAttributes<HTMLTextAreaElement>) {\n return <textarea className={cn(\"fds-control fds-control--multiline\", className)} {...props} />;\n}\n"],"mappings":";;;;;;AAyBA,SAAgB,MAAM,EAAE,OAAO,MAAM,OAAO,WAAW,YAAwB;CAC7E,MAAM,KAAK,MAAM;CACjB,MAAM,SAAS,GAAG,GAAG;CACrB,MAAM,UAAU,GAAG,GAAG;CACtB,MAAM,cAAc,CAAC,OAAO,SAAS,MAAM,QAAQ,UAAU,IAAI,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CAC3F,MAAM,UAA6B,EAAE,GAAG;CACxC,IAAI,aAAa,QAAQ,sBAAsB;CAC/C,IAAI,OAAO,QAAQ,kBAAkB;CACrC,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,aAAa,SAAS;EAAzC,UAAA;GACE,oBAAC,SAAD;IAAO,WAAU;IAAmB,SAAS;IAC1C,UAAA;GACI,CAAA;GACN,SAAS,OAAO;GAChB,OACC,oBAAC,KAAD;IAAG,WAAU;IAAkB,IAAI;IAChC,UAAA;GACA,CAAA,IACD;GACH,QACC,oBAAC,KAAD;IAAG,WAAU;IAAmB,IAAI;IACjC,UAAA;GACA,CAAA,IACD;EACD;;AAET;AAEA,SAAgB,MAAM,EAAE,WAAW,GAAG,SAAgD;CACpF,OAAO,oBAAC,SAAD;EAAO,WAAW,GAAG,eAAe,SAAS;EAAG,GAAI;CAAQ,CAAA;AACrE;AAEA,SAAgB,OAAO,EAAE,WAAW,GAAG,SAAkD;CACvF,OAAO,oBAAC,UAAD;EAAQ,WAAW,GAAG,eAAe,SAAS;EAAG,GAAI;CAAQ,CAAA;AACtE;AAEA,SAAgB,SAAS,EAAE,WAAW,GAAG,SAAsD;CAC7F,OAAO,oBAAC,YAAD;EAAU,WAAW,GAAG,sCAAsC,SAAS;EAAG,GAAI;CAAQ,CAAA;AAC/F"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
export interface MenuItem {
|
|
3
|
+
label: string;
|
|
4
|
+
tone?: "danger";
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
onSelect: () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface MenuProps {
|
|
9
|
+
/** Accessible name of the trigger button. */
|
|
10
|
+
ariaLabel: string;
|
|
11
|
+
items: readonly MenuItem[];
|
|
12
|
+
/** Trigger content. Defaults to a vertical-ellipsis glyph. */
|
|
13
|
+
trigger?: ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Action menu: closes on outside press and Escape (returning focus), arrow-key navigation. */
|
|
17
|
+
export declare function Menu({ ariaLabel, items, trigger, className }: MenuProps): import("react").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=Menu.d.mts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.mjs";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
5
|
+
// src/components/Menu.tsx
|
|
6
|
+
/** Action menu: closes on outside press and Escape (returning focus), arrow-key navigation. */
|
|
7
|
+
function Menu({ ariaLabel, items, trigger, className }) {
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
const rootRef = useRef(null);
|
|
10
|
+
const triggerRef = useRef(null);
|
|
11
|
+
const menuId = useId();
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!open) return;
|
|
14
|
+
const onPointerDown = (event) => {
|
|
15
|
+
if (!rootRef.current?.contains(event.target)) setOpen(false);
|
|
16
|
+
};
|
|
17
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
18
|
+
document.addEventListener("touchstart", onPointerDown);
|
|
19
|
+
return () => {
|
|
20
|
+
document.removeEventListener("mousedown", onPointerDown);
|
|
21
|
+
document.removeEventListener("touchstart", onPointerDown);
|
|
22
|
+
};
|
|
23
|
+
}, [open]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (open) rootRef.current?.querySelector("[role=\"menuitem\"]:not(:disabled)")?.focus();
|
|
26
|
+
}, [open]);
|
|
27
|
+
const onKeyDown = (event) => {
|
|
28
|
+
if (!open) return;
|
|
29
|
+
if (event.key === "Escape") {
|
|
30
|
+
event.stopPropagation();
|
|
31
|
+
setOpen(false);
|
|
32
|
+
triggerRef.current?.focus();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const enabled = Array.from(rootRef.current?.querySelectorAll("[role=\"menuitem\"]:not(:disabled)") ?? []);
|
|
36
|
+
if (enabled.length === 0) return;
|
|
37
|
+
const current = enabled.indexOf(document.activeElement);
|
|
38
|
+
const move = (next) => {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
enabled[(next + enabled.length) % enabled.length]?.focus();
|
|
41
|
+
};
|
|
42
|
+
if (event.key === "ArrowDown") move(current + 1);
|
|
43
|
+
else if (event.key === "ArrowUp") move(current < 0 ? enabled.length - 1 : current - 1);
|
|
44
|
+
else if (event.key === "Home") move(0);
|
|
45
|
+
else if (event.key === "End") move(enabled.length - 1);
|
|
46
|
+
else if (event.key === "Tab") setOpen(false);
|
|
47
|
+
};
|
|
48
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
49
|
+
className: cn("fds-menu", className),
|
|
50
|
+
onKeyDown,
|
|
51
|
+
ref: rootRef,
|
|
52
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
53
|
+
"aria-controls": open ? menuId : void 0,
|
|
54
|
+
"aria-expanded": open,
|
|
55
|
+
"aria-haspopup": "menu",
|
|
56
|
+
"aria-label": ariaLabel,
|
|
57
|
+
className: "fds-menu__trigger",
|
|
58
|
+
onClick: () => setOpen((value) => !value),
|
|
59
|
+
ref: triggerRef,
|
|
60
|
+
type: "button",
|
|
61
|
+
children: trigger ?? /* @__PURE__ */ jsxs("svg", {
|
|
62
|
+
"aria-hidden": "true",
|
|
63
|
+
focusable: "false",
|
|
64
|
+
height: "16",
|
|
65
|
+
viewBox: "0 0 16 16",
|
|
66
|
+
width: "16",
|
|
67
|
+
children: [
|
|
68
|
+
/* @__PURE__ */ jsx("circle", {
|
|
69
|
+
cx: "3",
|
|
70
|
+
cy: "8",
|
|
71
|
+
fill: "currentColor",
|
|
72
|
+
r: "1.4"
|
|
73
|
+
}),
|
|
74
|
+
/* @__PURE__ */ jsx("circle", {
|
|
75
|
+
cx: "8",
|
|
76
|
+
cy: "8",
|
|
77
|
+
fill: "currentColor",
|
|
78
|
+
r: "1.4"
|
|
79
|
+
}),
|
|
80
|
+
/* @__PURE__ */ jsx("circle", {
|
|
81
|
+
cx: "13",
|
|
82
|
+
cy: "8",
|
|
83
|
+
fill: "currentColor",
|
|
84
|
+
r: "1.4"
|
|
85
|
+
})
|
|
86
|
+
]
|
|
87
|
+
})
|
|
88
|
+
}), open ? /* @__PURE__ */ jsx("div", {
|
|
89
|
+
className: "fds-menu__list",
|
|
90
|
+
id: menuId,
|
|
91
|
+
role: "menu",
|
|
92
|
+
children: items.map((item) => /* @__PURE__ */ jsx("button", {
|
|
93
|
+
className: cn("fds-menu__item", item.tone === "danger" && "fds-menu__item--danger"),
|
|
94
|
+
disabled: item.disabled,
|
|
95
|
+
onClick: () => {
|
|
96
|
+
setOpen(false);
|
|
97
|
+
triggerRef.current?.focus();
|
|
98
|
+
item.onSelect();
|
|
99
|
+
},
|
|
100
|
+
role: "menuitem",
|
|
101
|
+
type: "button",
|
|
102
|
+
children: item.label
|
|
103
|
+
}, item.label))
|
|
104
|
+
}) : null]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export { Menu };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=Menu.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Menu.mjs","names":[],"sources":["../../src/components/Menu.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\nexport interface MenuItem {\n label: string;\n tone?: \"danger\";\n disabled?: boolean;\n onSelect: () => void;\n}\n\nexport interface MenuProps {\n /** Accessible name of the trigger button. */\n ariaLabel: string;\n items: readonly MenuItem[];\n /** Trigger content. Defaults to a vertical-ellipsis glyph. */\n trigger?: ReactNode;\n className?: string;\n}\n\n/** Action menu: closes on outside press and Escape (returning focus), arrow-key navigation. */\nexport function Menu({ ariaLabel, items, trigger, className }: MenuProps) {\n const [open, setOpen] = useState(false);\n const rootRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const menuId = useId();\n\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (event: MouseEvent | TouchEvent) => {\n if (!rootRef.current?.contains(event.target as Node)) setOpen(false);\n };\n document.addEventListener(\"mousedown\", onPointerDown);\n document.addEventListener(\"touchstart\", onPointerDown);\n return () => {\n document.removeEventListener(\"mousedown\", onPointerDown);\n document.removeEventListener(\"touchstart\", onPointerDown);\n };\n }, [open]);\n\n useEffect(() => {\n if (open)\n rootRef.current?.querySelector<HTMLElement>('[role=\"menuitem\"]:not(:disabled)')?.focus();\n }, [open]);\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!open) return;\n if (event.key === \"Escape\") {\n event.stopPropagation();\n setOpen(false);\n triggerRef.current?.focus();\n return;\n }\n const enabled = Array.from(\n rootRef.current?.querySelectorAll<HTMLElement>('[role=\"menuitem\"]:not(:disabled)') ?? [],\n );\n if (enabled.length === 0) return;\n const current = enabled.indexOf(document.activeElement as HTMLElement);\n const move = (next: number) => {\n event.preventDefault();\n enabled[(next + enabled.length) % enabled.length]?.focus();\n };\n if (event.key === \"ArrowDown\") move(current + 1);\n else if (event.key === \"ArrowUp\") move(current < 0 ? enabled.length - 1 : current - 1);\n else if (event.key === \"Home\") move(0);\n else if (event.key === \"End\") move(enabled.length - 1);\n else if (event.key === \"Tab\") setOpen(false);\n };\n\n return (\n <div className={cn(\"fds-menu\", className)} onKeyDown={onKeyDown} ref={rootRef}>\n <button\n aria-controls={open ? menuId : undefined}\n aria-expanded={open}\n aria-haspopup=\"menu\"\n aria-label={ariaLabel}\n className=\"fds-menu__trigger\"\n onClick={() => setOpen((value) => !value)}\n ref={triggerRef}\n type=\"button\"\n >\n {trigger ?? (\n <svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\">\n <circle cx=\"3\" cy=\"8\" fill=\"currentColor\" r=\"1.4\" />\n <circle cx=\"8\" cy=\"8\" fill=\"currentColor\" r=\"1.4\" />\n <circle cx=\"13\" cy=\"8\" fill=\"currentColor\" r=\"1.4\" />\n </svg>\n )}\n </button>\n {open ? (\n <div className=\"fds-menu__list\" id={menuId} role=\"menu\">\n {items.map((item) => (\n <button\n className={cn(\"fds-menu__item\", item.tone === \"danger\" && \"fds-menu__item--danger\")}\n disabled={item.disabled}\n key={item.label}\n onClick={() => {\n setOpen(false);\n triggerRef.current?.focus();\n item.onSelect();\n }}\n role=\"menuitem\"\n type=\"button\"\n >\n {item.label}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;AAoBA,SAAgB,KAAK,EAAE,WAAW,OAAO,SAAS,aAAwB;CACxE,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CACtC,MAAM,UAAU,OAAuB,IAAI;CAC3C,MAAM,aAAa,OAA0B,IAAI;CACjD,MAAM,SAAS,MAAM;CAErB,gBAAgB;EACd,IAAI,CAAC,MAAM;EACX,MAAM,iBAAiB,UAAmC;GACxD,IAAI,CAAC,QAAQ,SAAS,SAAS,MAAM,MAAc,GAAG,QAAQ,KAAK;EACrE;EACA,SAAS,iBAAiB,aAAa,aAAa;EACpD,SAAS,iBAAiB,cAAc,aAAa;EACrD,aAAa;GACX,SAAS,oBAAoB,aAAa,aAAa;GACvD,SAAS,oBAAoB,cAAc,aAAa;EAC1D;CACF,GAAG,CAAC,IAAI,CAAC;CAET,gBAAgB;EACd,IAAI,MACF,QAAQ,SAAS,cAA2B,oCAAkC,CAAC,EAAE,MAAM;CAC3F,GAAG,CAAC,IAAI,CAAC;CAET,MAAM,aAAa,UAAyC;EAC1D,IAAI,CAAC,MAAM;EACX,IAAI,MAAM,QAAQ,UAAU;GAC1B,MAAM,gBAAgB;GACtB,QAAQ,KAAK;GACb,WAAW,SAAS,MAAM;GAC1B;EACF;EACA,MAAM,UAAU,MAAM,KACpB,QAAQ,SAAS,iBAA8B,oCAAkC,KAAK,CAAC,CACzF;EACA,IAAI,QAAQ,WAAW,GAAG;EAC1B,MAAM,UAAU,QAAQ,QAAQ,SAAS,aAA4B;EACrE,MAAM,QAAQ,SAAiB;GAC7B,MAAM,eAAe;GACrB,SAAS,OAAO,QAAQ,UAAU,QAAQ,OAAO,EAAE,MAAM;EAC3D;EACA,IAAI,MAAM,QAAQ,aAAa,KAAK,UAAU,CAAC;OAC1C,IAAI,MAAM,QAAQ,WAAW,KAAK,UAAU,IAAI,QAAQ,SAAS,IAAI,UAAU,CAAC;OAChF,IAAI,MAAM,QAAQ,QAAQ,KAAK,CAAC;OAChC,IAAI,MAAM,QAAQ,OAAO,KAAK,QAAQ,SAAS,CAAC;OAChD,IAAI,MAAM,QAAQ,OAAO,QAAQ,KAAK;CAC7C;CAEA,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,YAAY,SAAS;EAAc;EAAW,KAAK;EAAtE,UAAA,CACE,oBAAC,UAAD;GACE,iBAAe,OAAO,SAAS,KAAA;GAC/B,iBAAe;GACf,iBAAc;GACd,cAAY;GACZ,WAAU;GACV,eAAe,SAAS,UAAU,CAAC,KAAK;GACxC,KAAK;GACL,MAAK;GAEJ,UAAA,WACC,qBAAC,OAAD;IAAK,eAAY;IAAO,WAAU;IAAQ,QAAO;IAAK,SAAQ;IAAY,OAAM;IAAhF,UAAA;KACE,oBAAC,UAAD;MAAQ,IAAG;MAAI,IAAG;MAAI,MAAK;MAAe,GAAE;KAAO,CAAA;KACnD,oBAAC,UAAD;MAAQ,IAAG;MAAI,IAAG;MAAI,MAAK;MAAe,GAAE;KAAO,CAAA;KACnD,oBAAC,UAAD;MAAQ,IAAG;MAAK,IAAG;MAAI,MAAK;MAAe,GAAE;KAAO,CAAA;IACjD;;EAED,CAAA,GACP,OACC,oBAAC,OAAD;GAAK,WAAU;GAAiB,IAAI;GAAQ,MAAK;GAC9C,UAAA,MAAM,KAAK,SACV,oBAAC,UAAD;IACE,WAAW,GAAG,kBAAkB,KAAK,SAAS,YAAY,wBAAwB;IAClF,UAAU,KAAK;IAEf,eAAe;KACb,QAAQ,KAAK;KACb,WAAW,SAAS,MAAM;KAC1B,KAAK,SAAS;IAChB;IACA,MAAK;IACL,MAAK;IAEJ,UAAA,KAAK;GACA,GAVD,KAAK,KAUJ,CACT;EACE,CAAA,IACH,IACD;;AAET"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface MeterProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
3
|
+
/** Accessible name; required because the bar itself has no text. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Current value within `min`..`max`. Values outside are clamped for display. */
|
|
6
|
+
value: number;
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
/** Optional human-readable value for assistive tech, e.g. "72 of 100". */
|
|
10
|
+
valueText?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Horizontal bar for a bounded score or share. Display only: no domain logic. */
|
|
13
|
+
export declare function Meter({ label, value, min, max, valueText, className, ...props }: MeterProps): import("react").JSX.Element;
|
|
14
|
+
export interface SegmentedMeterProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
15
|
+
label: string;
|
|
16
|
+
/** Number of filled segments. */
|
|
17
|
+
value: number;
|
|
18
|
+
segments?: number;
|
|
19
|
+
valueText?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Discrete scale (for example a 1-7 risk indicator). */
|
|
22
|
+
export declare function SegmentedMeter({ label, value, segments, valueText, className, ...props }: SegmentedMeterProps): import("react").JSX.Element;
|
|
23
|
+
export interface DistributionItem {
|
|
24
|
+
label: ReactNode;
|
|
25
|
+
value: number;
|
|
26
|
+
}
|
|
27
|
+
export interface DistributionListProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
28
|
+
items: readonly DistributionItem[];
|
|
29
|
+
/** Maximum rows shown; the remainder is summarised via `renderOverflow`. */
|
|
30
|
+
limit?: number;
|
|
31
|
+
/** Formats the value text. Consumer owns units and locale. */
|
|
32
|
+
formatValue?: (value: number) => ReactNode;
|
|
33
|
+
/** Shown when there is nothing positive to display. */
|
|
34
|
+
empty?: ReactNode;
|
|
35
|
+
renderOverflow?: (hidden: number) => ReactNode;
|
|
36
|
+
}
|
|
37
|
+
/** Ranked list of share bars, scaled relative to the largest row. */
|
|
38
|
+
export declare function DistributionList({ items, limit, formatValue, empty, renderOverflow, className, ...props }: DistributionListProps): import("react").JSX.Element | null;
|
|
39
|
+
//# sourceMappingURL=Meter.d.mts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.mjs";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
// src/components/Meter.tsx
|
|
4
|
+
const clamp = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
|
|
5
|
+
/** Horizontal bar for a bounded score or share. Display only: no domain logic. */
|
|
6
|
+
function Meter({ label, value, min = 0, max = 100, valueText, className, ...props }) {
|
|
7
|
+
const span = max - min;
|
|
8
|
+
const ratio = span > 0 ? clamp((value - min) / span, 0, 1) : 0;
|
|
9
|
+
return /* @__PURE__ */ jsx("div", {
|
|
10
|
+
role: "meter",
|
|
11
|
+
"aria-label": label,
|
|
12
|
+
"aria-valuemin": min,
|
|
13
|
+
"aria-valuemax": max,
|
|
14
|
+
"aria-valuenow": clamp(value, min, max),
|
|
15
|
+
"aria-valuetext": valueText,
|
|
16
|
+
className: cn("fds-meter", className),
|
|
17
|
+
...props,
|
|
18
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
19
|
+
className: "fds-meter__fill",
|
|
20
|
+
style: { width: `${ratio * 100}%` }
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Discrete scale (for example a 1-7 risk indicator). */
|
|
25
|
+
function SegmentedMeter({ label, value, segments = 7, valueText, className, ...props }) {
|
|
26
|
+
const filled = clamp(Math.round(value), 0, segments);
|
|
27
|
+
return /* @__PURE__ */ jsx("div", {
|
|
28
|
+
role: "meter",
|
|
29
|
+
"aria-label": label,
|
|
30
|
+
"aria-valuemin": 0,
|
|
31
|
+
"aria-valuemax": segments,
|
|
32
|
+
"aria-valuenow": filled,
|
|
33
|
+
"aria-valuetext": valueText,
|
|
34
|
+
className: cn("fds-segmeter", className),
|
|
35
|
+
...props,
|
|
36
|
+
children: Array.from({ length: segments }, (_, index) => /* @__PURE__ */ jsx("span", { className: cn("fds-segmeter__segment", index < filled && "fds-segmeter__segment--on") }, index))
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Ranked list of share bars, scaled relative to the largest row. */
|
|
40
|
+
function DistributionList({ items, limit = 5, formatValue = (v) => String(v), empty, renderOverflow, className, ...props }) {
|
|
41
|
+
const sorted = [...items].sort((a, b) => b.value - a.value);
|
|
42
|
+
const top = sorted.slice(0, limit);
|
|
43
|
+
const max = top[0]?.value ?? 0;
|
|
44
|
+
if (max <= 0) return empty ? /* @__PURE__ */ jsx("p", {
|
|
45
|
+
className: "fds-distribution__empty",
|
|
46
|
+
children: empty
|
|
47
|
+
}) : null;
|
|
48
|
+
const hidden = sorted.length - top.length;
|
|
49
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
50
|
+
className: cn("fds-distribution", className),
|
|
51
|
+
...props,
|
|
52
|
+
children: [/* @__PURE__ */ jsx("ul", {
|
|
53
|
+
className: "fds-distribution__list",
|
|
54
|
+
children: top.map((item, index) => /* @__PURE__ */ jsxs("li", {
|
|
55
|
+
className: "fds-distribution__row",
|
|
56
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
57
|
+
className: "fds-distribution__meta",
|
|
58
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
59
|
+
className: "fds-distribution__label",
|
|
60
|
+
children: item.label
|
|
61
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
62
|
+
className: "fds-distribution__value",
|
|
63
|
+
children: formatValue(item.value)
|
|
64
|
+
})]
|
|
65
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
66
|
+
"aria-hidden": "true",
|
|
67
|
+
className: "fds-meter fds-meter--thin",
|
|
68
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
69
|
+
className: "fds-meter__fill",
|
|
70
|
+
style: { width: `${Math.max(item.value, 0) / max * 100}%` }
|
|
71
|
+
})
|
|
72
|
+
})]
|
|
73
|
+
}, index))
|
|
74
|
+
}), hidden > 0 && renderOverflow ? /* @__PURE__ */ jsx("p", {
|
|
75
|
+
className: "fds-distribution__overflow",
|
|
76
|
+
children: renderOverflow(hidden)
|
|
77
|
+
}) : null]
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export { DistributionList, Meter, SegmentedMeter };
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=Meter.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Meter.mjs","names":[],"sources":["../../src/components/Meter.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\nexport interface MeterProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Accessible name; required because the bar itself has no text. */\n label: string;\n /** Current value within `min`..`max`. Values outside are clamped for display. */\n value: number;\n min?: number;\n max?: number;\n /** Optional human-readable value for assistive tech, e.g. \"72 of 100\". */\n valueText?: string;\n}\n\nconst clamp = (n: number, lo: number, hi: number) => Math.min(Math.max(n, lo), hi);\n\n/** Horizontal bar for a bounded score or share. Display only: no domain logic. */\nexport function Meter({\n label,\n value,\n min = 0,\n max = 100,\n valueText,\n className,\n ...props\n}: MeterProps) {\n const span = max - min;\n const ratio = span > 0 ? clamp((value - min) / span, 0, 1) : 0;\n return (\n <div\n role=\"meter\"\n aria-label={label}\n aria-valuemin={min}\n aria-valuemax={max}\n aria-valuenow={clamp(value, min, max)}\n aria-valuetext={valueText}\n className={cn(\"fds-meter\", className)}\n {...props}\n >\n <div className=\"fds-meter__fill\" style={{ width: `${ratio * 100}%` }} />\n </div>\n );\n}\n\nexport interface SegmentedMeterProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n label: string;\n /** Number of filled segments. */\n value: number;\n segments?: number;\n valueText?: string;\n}\n\n/** Discrete scale (for example a 1-7 risk indicator). */\nexport function SegmentedMeter({\n label,\n value,\n segments = 7,\n valueText,\n className,\n ...props\n}: SegmentedMeterProps) {\n const filled = clamp(Math.round(value), 0, segments);\n return (\n <div\n role=\"meter\"\n aria-label={label}\n aria-valuemin={0}\n aria-valuemax={segments}\n aria-valuenow={filled}\n aria-valuetext={valueText}\n className={cn(\"fds-segmeter\", className)}\n {...props}\n >\n {Array.from({ length: segments }, (_, index) => (\n <span\n key={index}\n className={cn(\"fds-segmeter__segment\", index < filled && \"fds-segmeter__segment--on\")}\n />\n ))}\n </div>\n );\n}\n\nexport interface DistributionItem {\n label: ReactNode;\n value: number;\n}\n\nexport interface DistributionListProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n items: readonly DistributionItem[];\n /** Maximum rows shown; the remainder is summarised via `renderOverflow`. */\n limit?: number;\n /** Formats the value text. Consumer owns units and locale. */\n formatValue?: (value: number) => ReactNode;\n /** Shown when there is nothing positive to display. */\n empty?: ReactNode;\n renderOverflow?: (hidden: number) => ReactNode;\n}\n\n/** Ranked list of share bars, scaled relative to the largest row. */\nexport function DistributionList({\n items,\n limit = 5,\n formatValue = (v) => String(v),\n empty,\n renderOverflow,\n className,\n ...props\n}: DistributionListProps) {\n const sorted = [...items].sort((a, b) => b.value - a.value);\n const top = sorted.slice(0, limit);\n const max = top[0]?.value ?? 0;\n if (max <= 0) return empty ? <p className=\"fds-distribution__empty\">{empty}</p> : null;\n const hidden = sorted.length - top.length;\n return (\n <div className={cn(\"fds-distribution\", className)} {...props}>\n <ul className=\"fds-distribution__list\">\n {top.map((item, index) => (\n <li key={index} className=\"fds-distribution__row\">\n <div className=\"fds-distribution__meta\">\n <span className=\"fds-distribution__label\">{item.label}</span>\n <span className=\"fds-distribution__value\">{formatValue(item.value)}</span>\n </div>\n <div aria-hidden=\"true\" className=\"fds-meter fds-meter--thin\">\n <div\n className=\"fds-meter__fill\"\n style={{ width: `${(Math.max(item.value, 0) / max) * 100}%` }}\n />\n </div>\n </li>\n ))}\n </ul>\n {hidden > 0 && renderOverflow ? (\n <p className=\"fds-distribution__overflow\">{renderOverflow(hidden)}</p>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;AAcA,MAAM,SAAS,GAAW,IAAY,OAAe,KAAK,IAAI,KAAK,IAAI,GAAG,EAAE,GAAG,EAAE;;AAGjF,SAAgB,MAAM,EACpB,OACA,OACA,MAAM,GACN,MAAM,KACN,WACA,WACA,GAAG,SACU;CACb,MAAM,OAAO,MAAM;CACnB,MAAM,QAAQ,OAAO,IAAI,OAAO,QAAQ,OAAO,MAAM,GAAG,CAAC,IAAI;CAC7D,OACE,oBAAC,OAAD;EACE,MAAK;EACL,cAAY;EACZ,iBAAe;EACf,iBAAe;EACf,iBAAe,MAAM,OAAO,KAAK,GAAG;EACpC,kBAAgB;EAChB,WAAW,GAAG,aAAa,SAAS;EACpC,GAAI;EAEJ,UAAA,oBAAC,OAAD;GAAK,WAAU;GAAkB,OAAO,EAAE,OAAO,GAAG,QAAQ,IAAI,GAAG;EAAI,CAAA;CACpE,CAAA;AAET;;AAWA,SAAgB,eAAe,EAC7B,OACA,OACA,WAAW,GACX,WACA,WACA,GAAG,SACmB;CACtB,MAAM,SAAS,MAAM,KAAK,MAAM,KAAK,GAAG,GAAG,QAAQ;CACnD,OACE,oBAAC,OAAD;EACE,MAAK;EACL,cAAY;EACZ,iBAAe;EACf,iBAAe;EACf,iBAAe;EACf,kBAAgB;EAChB,WAAW,GAAG,gBAAgB,SAAS;EACvC,GAAI;EAEH,UAAA,MAAM,KAAK,EAAE,QAAQ,SAAS,IAAI,GAAG,UACpC,oBAAC,QAAD,EAEE,WAAW,GAAG,yBAAyB,QAAQ,UAAU,2BAA2B,EACrF,GAFM,KAEN,CACF;CACE,CAAA;AAET;;AAmBA,SAAgB,iBAAiB,EAC/B,OACA,QAAQ,GACR,eAAe,MAAM,OAAO,CAAC,GAC7B,OACA,gBACA,WACA,GAAG,SACqB;CACxB,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;CAC1D,MAAM,MAAM,OAAO,MAAM,GAAG,KAAK;CACjC,MAAM,MAAM,IAAI,EAAE,EAAE,SAAS;CAC7B,IAAI,OAAO,GAAG,OAAO,QAAQ,oBAAC,KAAD;EAAG,WAAU;EAA2B,UAAA;CAAS,CAAA,IAAI;CAClF,MAAM,SAAS,OAAO,SAAS,IAAI;CACnC,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,oBAAoB,SAAS;EAAG,GAAI;EAAvD,UAAA,CACE,oBAAC,MAAD;GAAI,WAAU;GACX,UAAA,IAAI,KAAK,MAAM,UACd,qBAAC,MAAD;IAAgB,WAAU;IAA1B,UAAA,CACE,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,oBAAC,QAAD;MAAM,WAAU;MAA2B,UAAA,KAAK;KAAY,CAAA,GAC5D,oBAAC,QAAD;MAAM,WAAU;MAA2B,UAAA,YAAY,KAAK,KAAK;KAAQ,CAAA,CACtE;IACL,CAAA,GAAA,oBAAC,OAAD;KAAK,eAAY;KAAO,WAAU;KAChC,UAAA,oBAAC,OAAD;MACE,WAAU;MACV,OAAO,EAAE,OAAO,GAAI,KAAK,IAAI,KAAK,OAAO,CAAC,IAAI,MAAO,IAAI,GAAG;KAC7D,CAAA;IACE,CAAA,CACH;GAXK,GAAA,KAWL,CACL;EACC,CAAA,GACH,SAAS,KAAK,iBACb,oBAAC,KAAD;GAAG,WAAU;GAA8B,UAAA,eAAe,MAAM;EAAK,CAAA,IACnE,IACD;;AAET"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export type Tone = "neutral" | "accent" | "success" | "warn" | "danger";
|
|
3
|
+
export interface MetricProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
/** Pre-formatted value. Formatting and calculation belong to the consuming app. */
|
|
6
|
+
value: ReactNode;
|
|
7
|
+
tone?: Tone;
|
|
8
|
+
}
|
|
9
|
+
/** Label/value tile for pre-formatted figures. Does no number formatting or arithmetic. */
|
|
10
|
+
export declare function Metric({ label, value, tone, className, ...props }: MetricProps): import("react").JSX.Element;
|
|
11
|
+
export interface KeyValueProps extends HTMLAttributes<HTMLDivElement> {
|
|
12
|
+
label: ReactNode;
|
|
13
|
+
value: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
/** Quieter label/value pair for detail views. */
|
|
16
|
+
export declare function KeyValue({ label, value, className, ...props }: KeyValueProps): import("react").JSX.Element;
|
|
17
|
+
//# sourceMappingURL=Metric.d.mts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.mjs";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
// src/components/Metric.tsx
|
|
4
|
+
/** Label/value tile for pre-formatted figures. Does no number formatting or arithmetic. */
|
|
5
|
+
function Metric({ label, value, tone = "neutral", className, ...props }) {
|
|
6
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
7
|
+
className: cn("fds-metric", className),
|
|
8
|
+
...props,
|
|
9
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
10
|
+
className: "fds-metric__label",
|
|
11
|
+
children: label
|
|
12
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
13
|
+
className: cn("fds-metric__value", `fds-tone--${tone}`),
|
|
14
|
+
children: value
|
|
15
|
+
})]
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** Quieter label/value pair for detail views. */
|
|
19
|
+
function KeyValue({ label, value, className, ...props }) {
|
|
20
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
21
|
+
className: cn("fds-keyvalue", className),
|
|
22
|
+
...props,
|
|
23
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
24
|
+
className: "fds-metric__label",
|
|
25
|
+
children: label
|
|
26
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
27
|
+
className: "fds-keyvalue__value",
|
|
28
|
+
children: value
|
|
29
|
+
})]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export { KeyValue, Metric };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=Metric.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Metric.mjs","names":[],"sources":["../../src/components/Metric.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\nexport type Tone = \"neutral\" | \"accent\" | \"success\" | \"warn\" | \"danger\";\n\nexport interface MetricProps extends HTMLAttributes<HTMLDivElement> {\n label: ReactNode;\n /** Pre-formatted value. Formatting and calculation belong to the consuming app. */\n value: ReactNode;\n tone?: Tone;\n}\n\n/** Label/value tile for pre-formatted figures. Does no number formatting or arithmetic. */\nexport function Metric({ label, value, tone = \"neutral\", className, ...props }: MetricProps) {\n return (\n <div className={cn(\"fds-metric\", className)} {...props}>\n <p className=\"fds-metric__label\">{label}</p>\n <p className={cn(\"fds-metric__value\", `fds-tone--${tone}`)}>{value}</p>\n </div>\n );\n}\n\nexport interface KeyValueProps extends HTMLAttributes<HTMLDivElement> {\n label: ReactNode;\n value: ReactNode;\n}\n\n/** Quieter label/value pair for detail views. */\nexport function KeyValue({ label, value, className, ...props }: KeyValueProps) {\n return (\n <div className={cn(\"fds-keyvalue\", className)} {...props}>\n <p className=\"fds-metric__label\">{label}</p>\n <p className=\"fds-keyvalue__value\">{value}</p>\n </div>\n );\n}\n"],"mappings":";;;;AAaA,SAAgB,OAAO,EAAE,OAAO,OAAO,OAAO,WAAW,WAAW,GAAG,SAAsB;CAC3F,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,cAAc,SAAS;EAAG,GAAI;EAAjD,UAAA,CACE,oBAAC,KAAD;GAAG,WAAU;GAAqB,UAAA;EAAS,CAAA,GAC3C,oBAAC,KAAD;GAAG,WAAW,GAAG,qBAAqB,aAAa,MAAM;GAAI,UAAA;EAAS,CAAA,CACnE;;AAET;;AAQA,SAAgB,SAAS,EAAE,OAAO,OAAO,WAAW,GAAG,SAAwB;CAC7E,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,gBAAgB,SAAS;EAAG,GAAI;EAAnD,UAAA,CACE,oBAAC,KAAD;GAAG,WAAU;GAAqB,UAAA;EAAS,CAAA,GAC3C,oBAAC,KAAD;GAAG,WAAU;GAAuB,UAAA;EAAS,CAAA,CAC1C;;AAET"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface NoticeProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
3
|
+
tone?: "info" | "success" | "warn" | "danger";
|
|
4
|
+
title?: ReactNode;
|
|
5
|
+
action?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/** Inline message. `danger` announces as an alert; other tones as polite status. */
|
|
8
|
+
export declare function Notice({ tone, title, action, className, children, role, ...props }: NoticeProps): import("react").JSX.Element;
|
|
9
|
+
export interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
10
|
+
title: ReactNode;
|
|
11
|
+
description?: ReactNode;
|
|
12
|
+
action?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/** Placeholder for lists or panels with no data, with an optional next step. */
|
|
15
|
+
export declare function EmptyState({ title, description, action, className, ...props }: EmptyStateProps): import("react").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=Notice.d.mts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.mjs";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
// src/components/Notice.tsx
|
|
4
|
+
/** Inline message. `danger` announces as an alert; other tones as polite status. */
|
|
5
|
+
function Notice({ tone = "info", title, action, className, children, role, ...props }) {
|
|
6
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
7
|
+
role: role ?? (tone === "danger" ? "alert" : "status"),
|
|
8
|
+
className: cn("fds-notice", `fds-notice--${tone}`, className),
|
|
9
|
+
...props,
|
|
10
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
11
|
+
className: "fds-notice__body",
|
|
12
|
+
children: [title ? /* @__PURE__ */ jsx("p", {
|
|
13
|
+
className: "fds-notice__title",
|
|
14
|
+
children: title
|
|
15
|
+
}) : null, children ? /* @__PURE__ */ jsx("div", {
|
|
16
|
+
className: "fds-notice__text",
|
|
17
|
+
children
|
|
18
|
+
}) : null]
|
|
19
|
+
}), action ? /* @__PURE__ */ jsx("div", {
|
|
20
|
+
className: "fds-notice__action",
|
|
21
|
+
children: action
|
|
22
|
+
}) : null]
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** Placeholder for lists or panels with no data, with an optional next step. */
|
|
26
|
+
function EmptyState({ title, description, action, className, ...props }) {
|
|
27
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
28
|
+
className: cn("fds-empty", className),
|
|
29
|
+
...props,
|
|
30
|
+
children: [
|
|
31
|
+
/* @__PURE__ */ jsx("p", {
|
|
32
|
+
className: "fds-empty__title",
|
|
33
|
+
children: title
|
|
34
|
+
}),
|
|
35
|
+
description ? /* @__PURE__ */ jsx("p", {
|
|
36
|
+
className: "fds-empty__description",
|
|
37
|
+
children: description
|
|
38
|
+
}) : null,
|
|
39
|
+
action ? /* @__PURE__ */ jsx("div", {
|
|
40
|
+
className: "fds-empty__action",
|
|
41
|
+
children: action
|
|
42
|
+
}) : null
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export { EmptyState, Notice };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=Notice.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Notice.mjs","names":[],"sources":["../../src/components/Notice.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\nexport interface NoticeProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n tone?: \"info\" | \"success\" | \"warn\" | \"danger\";\n title?: ReactNode;\n action?: ReactNode;\n}\n\n/** Inline message. `danger` announces as an alert; other tones as polite status. */\nexport function Notice({\n tone = \"info\",\n title,\n action,\n className,\n children,\n role,\n ...props\n}: NoticeProps) {\n return (\n <div\n role={role ?? (tone === \"danger\" ? \"alert\" : \"status\")}\n className={cn(\"fds-notice\", `fds-notice--${tone}`, className)}\n {...props}\n >\n <div className=\"fds-notice__body\">\n {title ? <p className=\"fds-notice__title\">{title}</p> : null}\n {children ? <div className=\"fds-notice__text\">{children}</div> : null}\n </div>\n {action ? <div className=\"fds-notice__action\">{action}</div> : null}\n </div>\n );\n}\n\nexport interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n title: ReactNode;\n description?: ReactNode;\n action?: ReactNode;\n}\n\n/** Placeholder for lists or panels with no data, with an optional next step. */\nexport function EmptyState({ title, description, action, className, ...props }: EmptyStateProps) {\n return (\n <div className={cn(\"fds-empty\", className)} {...props}>\n <p className=\"fds-empty__title\">{title}</p>\n {description ? <p className=\"fds-empty__description\">{description}</p> : null}\n {action ? <div className=\"fds-empty__action\">{action}</div> : null}\n </div>\n );\n}\n"],"mappings":";;;;AAUA,SAAgB,OAAO,EACrB,OAAO,QACP,OACA,QACA,WACA,UACA,MACA,GAAG,SACW;CACd,OACE,qBAAC,OAAD;EACE,MAAM,SAAS,SAAS,WAAW,UAAU;EAC7C,WAAW,GAAG,cAAc,eAAe,QAAQ,SAAS;EAC5D,GAAI;EAHN,UAAA,CAKE,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACG,QAAQ,oBAAC,KAAD;IAAG,WAAU;IAAqB,UAAA;GAAS,CAAA,IAAI,MACvD,WAAW,oBAAC,OAAD;IAAK,WAAU;IAAoB;GAAc,CAAA,IAAI,IAC9D;EACJ,CAAA,GAAA,SAAS,oBAAC,OAAD;GAAK,WAAU;GAAsB,UAAA;EAAY,CAAA,IAAI,IAC5D;;AAET;;AASA,SAAgB,WAAW,EAAE,OAAO,aAAa,QAAQ,WAAW,GAAG,SAA0B;CAC/F,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,aAAa,SAAS;EAAG,GAAI;EAAhD,UAAA;GACE,oBAAC,KAAD;IAAG,WAAU;IAAoB,UAAA;GAAS,CAAA;GACzC,cAAc,oBAAC,KAAD;IAAG,WAAU;IAA0B,UAAA;GAAe,CAAA,IAAI;GACxE,SAAS,oBAAC,OAAD;IAAK,WAAU;IAAqB,UAAA;GAAY,CAAA,IAAI;EAC3D;;AAET"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface PanelProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
3
|
+
title: ReactNode;
|
|
4
|
+
eyebrow?: ReactNode;
|
|
5
|
+
help?: ReactNode;
|
|
6
|
+
/** Optional trailing action area (buttons, menu). */
|
|
7
|
+
action?: ReactNode;
|
|
8
|
+
/** Heading element for the title. Defaults to h2; pick the level that fits the page outline. */
|
|
9
|
+
headingLevel?: 2 | 3 | 4;
|
|
10
|
+
}
|
|
11
|
+
/** Titled surface with optional eyebrow, help text and action. Renders a labelled `<section>`. */
|
|
12
|
+
export declare function Panel({ title, eyebrow, help, action, headingLevel, className, children, ...props }: PanelProps): import("react").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=Panel.d.mts.map
|