@scttlnd/fintech-design-system 0.0.0-stage → 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.
Files changed (50) hide show
  1. package/AGENTS.md +15 -0
  2. package/README.md +59 -2
  3. package/dist/components/Badge.d.mts +9 -0
  4. package/dist/components/Badge.mjs +18 -0
  5. package/dist/components/Badge.mjs.map +1 -0
  6. package/dist/components/Button.d.mts +6 -0
  7. package/dist/components/Button.mjs +14 -0
  8. package/dist/components/Button.mjs.map +1 -0
  9. package/dist/components/Field.d.mts +20 -0
  10. package/dist/components/Field.mjs +57 -0
  11. package/dist/components/Field.mjs.map +1 -0
  12. package/dist/components/Menu.d.mts +18 -0
  13. package/dist/components/Menu.mjs +109 -0
  14. package/dist/components/Menu.mjs.map +1 -0
  15. package/dist/components/Meter.d.mts +39 -0
  16. package/dist/components/Meter.mjs +82 -0
  17. package/dist/components/Meter.mjs.map +1 -0
  18. package/dist/components/Metric.d.mts +17 -0
  19. package/dist/components/Metric.mjs +34 -0
  20. package/dist/components/Metric.mjs.map +1 -0
  21. package/dist/components/Notice.d.mts +16 -0
  22. package/dist/components/Notice.mjs +48 -0
  23. package/dist/components/Notice.mjs.map +1 -0
  24. package/dist/components/Panel.d.mts +13 -0
  25. package/dist/components/Panel.mjs +42 -0
  26. package/dist/components/Panel.mjs.map +1 -0
  27. package/dist/index.d.mts +10 -0
  28. package/dist/index.mjs +10 -0
  29. package/dist/lib/cn.d.mts +2 -0
  30. package/dist/lib/cn.mjs +7 -0
  31. package/dist/lib/cn.mjs.map +1 -0
  32. package/package.json +72 -4
  33. package/styles/components/badge.css +33 -0
  34. package/styles/components/button.css +45 -0
  35. package/styles/components/empty-state.css +20 -0
  36. package/styles/components/eyebrow.css +11 -0
  37. package/styles/components/field.css +59 -0
  38. package/styles/components/grain.css +245 -0
  39. package/styles/components/menu.css +74 -0
  40. package/styles/components/meter.css +62 -0
  41. package/styles/components/metric.css +28 -0
  42. package/styles/components/notice.css +34 -0
  43. package/styles/components/panel.css +42 -0
  44. package/styles/components/tone.css +15 -0
  45. package/styles/components.css +11 -0
  46. package/styles/index.css +4 -0
  47. package/styles/themes/funder.css +77 -0
  48. package/styles/themes/investor.css +77 -0
  49. package/styles/themes/portare.css +92 -0
  50. package/styles/tokens.css +90 -0
package/AGENTS.md ADDED
@@ -0,0 +1,15 @@
1
+ # Agent implementation contract
2
+
3
+ This repository is the **source of truth** for shared visual tokens and primitive components used by Funder, Portare and Investor. Read this file and README.md before UI changes.
4
+
5
+ 1. Consume the published npm package. Do not copy components or token values into consumer repositories.
6
+ 2. Import `@scttlnd/fintech-design-system/styles.css` and exactly one theme CSS at the app root. Keep product-specific composition, navigation and domain components in the consuming app.
7
+ 3. Prefer CSS variables `--fds-*` and exported primitives. Avoid hardcoded colors and duplicate primitives.
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
+ 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 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
+ 7. Never place financial arithmetic, API clients, user state, authentication, or business-specific charts in this package.
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
+ 9. The package must be published to npm before consumer dependencies can resolve. Renovate updates versions only after publication.
14
+
15
+ Consumer integration: Portare and Investor should document this package in their AGENTS.md and renovate.json. Use one version per app, pin through package.json and lockfile, and review Renovate PRs with visual checks.
package/README.md CHANGED
@@ -1,3 +1,60 @@
1
- # Temporary Holding Version
1
+ # Fintech Design System
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared Funder-derived design foundations for Portare, Investor and Funder. Each brand and product remains independent.
4
+
5
+ ## Develop with Vite+
6
+
7
+ ```sh
8
+ npm ci
9
+ npm run check
10
+ npm test
11
+ npm run build
12
+ npm pack --dry-run
13
+ ```
14
+
15
+ The project-local `vite-plus` package provides `vp pack`, `vp check` and `vp test`. Global `vp` is optional.
16
+
17
+ ## Use in frontend (after npm release)
18
+
19
+ ```sh
20
+ npm install @scttlnd/fintech-design-system
21
+ ```
22
+
23
+ At the app root import base styles then exactly one theme:
24
+
25
+ ```tsx
26
+ import "@scttlnd/fintech-design-system/styles.css";
27
+ import "@scttlnd/fintech-design-system/themes/investor.css"; // or portare.css / funder.css
28
+ import { Button } from "@scttlnd/fintech-design-system";
29
+ ```
30
+
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.
34
+
35
+ ## Showcase
36
+
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**.
38
+
39
+ ## Components
40
+
41
+ | Component | Purpose | Origin |
42
+ | --------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------- |
43
+ | `Button` | Primary / secondary / ghost actions | Shared |
44
+ | `Panel` | Titled surface with eyebrow, help text and action; labelled `<section>` | Funder `Panel`, Investor `.panel` |
45
+ | `Metric`, `KeyValue` | Label/value tiles for **pre-formatted** figures | Funder `Metric` / `KeyValue`, Investor `.metrics` |
46
+ | `Badge` | Status chip (`neutral`, `accent`, `success`, `warn`, `danger`) | Funder `Pill`, Investor `.status-badge` |
47
+ | `Notice`, `EmptyState` | Inline messages (`danger` = `role="alert"`) and empty placeholders | Investor `.notice`, `.empty-state` |
48
+ | `Field`, `Input`, `Select`, `Textarea` | Label/hint/error wiring with 44px touch targets and 16px mobile font | Investor `.form-grid`/`.field-hint`, Funder form sizing standard |
49
+ | `Meter`, `SegmentedMeter`, `DistributionList` | Bounded bars, discrete scale, ranked share bars (`role="meter"`) | Funder `EvidenceBar`, `RiskBars`, `ExposureList` |
50
+ | `Menu` | Action menu with Escape/arrow-key support and focus return | Funder `Menu` |
51
+
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).
57
+
58
+ ## npm publishing
59
+
60
+ Releases are automated by `.github/workflows/release.yml` (Changesets) on every push to `main`, using npm trusted publishing (OIDC). **Requires** the npm user/scope `@scttlnd` (npm username `scttlnd`; the GitHub owner `scottlinddk` is a different name), a one-time bootstrap publication because npm can only attach a Trusted Publisher to an existing package, and then a Trusted Publisher configured on npmjs.com for GitHub `scottlinddk/fintech-design-system`, workflow filename `release.yml`. No package has been published yet.
@@ -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