@scttlnd/fintech-design-system 0.0.0-stage → 0.2.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 +15 -0
- package/README.md +53 -2
- package/dist/index.d.mts +125 -0
- package/dist/index.mjs +366 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +57 -4
- package/styles/components.css +367 -0
- package/styles/index.css +45 -0
- package/styles/themes/funder.css +9 -0
- package/styles/themes/investor.css +9 -0
- package/styles/themes/portare.css +24 -0
- package/styles/tokens.css +25 -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 is generated), and consumer verification. 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,54 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Fintech Design System
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
Read [AGENTS.md](AGENTS.md) and [adoption guide](docs/ADOPTION.md) before integrating. Original Funder palette is sourced from `nicklaslind/Funder/src/app/globals.css`. Portare's light theme is a proposed product-specific adaptation.
|
|
32
|
+
|
|
33
|
+
## Showcase
|
|
34
|
+
|
|
35
|
+
The static design review page in [`showcase/index.html`](showcase/index.html) is deployed to GitHub Pages by `.github/workflows/pages.yml` on every push to `main` that touches it: <https://scottlinddk.github.io/fintech-design-system/>. One-time setup: repository **Settings → Pages → Source: GitHub Actions**.
|
|
36
|
+
|
|
37
|
+
## Components
|
|
38
|
+
|
|
39
|
+
| Component | Purpose | Origin |
|
|
40
|
+
| --------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
41
|
+
| `Button` | Primary / secondary / ghost actions | Shared |
|
|
42
|
+
| `Panel` | Titled surface with eyebrow, help text and action; labelled `<section>` | Funder `Panel`, Investor `.panel` |
|
|
43
|
+
| `Metric`, `KeyValue` | Label/value tiles for **pre-formatted** figures | Funder `Metric` / `KeyValue`, Investor `.metrics` |
|
|
44
|
+
| `Badge` | Status chip (`neutral`, `accent`, `success`, `warn`, `danger`) | Funder `Pill`, Investor `.status-badge` |
|
|
45
|
+
| `Notice`, `EmptyState` | Inline messages (`danger` = `role="alert"`) and empty placeholders | Investor `.notice`, `.empty-state` |
|
|
46
|
+
| `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 |
|
|
47
|
+
| `Meter`, `SegmentedMeter`, `DistributionList` | Bounded bars, discrete scale, ranked share bars (`role="meter"`) | Funder `EvidenceBar`, `RiskBars`, `ExposureList` |
|
|
48
|
+
| `Menu` | Action menu with Escape/arrow-key support and focus return | Funder `Menu` |
|
|
49
|
+
|
|
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 React hooks: in Next.js import them from a file marked `"use client"`.
|
|
51
|
+
|
|
52
|
+
## npm publishing
|
|
53
|
+
|
|
54
|
+
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.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ButtonHTMLAttributes, HTMLAttributes, InputHTMLAttributes, ReactNode, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
|
|
2
|
+
export declare function cn(...classes: Array<string | false | null | undefined>): string;
|
|
3
|
+
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4
|
+
variant?: "primary" | "secondary" | "ghost";
|
|
5
|
+
}
|
|
6
|
+
export declare function Button({ variant, className, type, ...props }: ButtonProps): import("react").JSX.Element;
|
|
7
|
+
interface PanelProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
eyebrow?: ReactNode;
|
|
10
|
+
help?: ReactNode;
|
|
11
|
+
/** Optional trailing action area (buttons, menu). */
|
|
12
|
+
action?: ReactNode;
|
|
13
|
+
/** Heading element for the title. Defaults to h2; pick the level that fits the page outline. */
|
|
14
|
+
headingLevel?: 2 | 3 | 4;
|
|
15
|
+
}
|
|
16
|
+
/** Titled surface with optional eyebrow, help text and action. Renders a labelled `<section>`. */
|
|
17
|
+
export declare function Panel({ title, eyebrow, help, action, headingLevel, className, children, ...props }: PanelProps): import("react").JSX.Element;
|
|
18
|
+
type Tone = "neutral" | "accent" | "success" | "warn" | "danger";
|
|
19
|
+
interface MetricProps extends HTMLAttributes<HTMLDivElement> {
|
|
20
|
+
label: ReactNode;
|
|
21
|
+
/** Pre-formatted value. Formatting and calculation belong to the consuming app. */
|
|
22
|
+
value: ReactNode;
|
|
23
|
+
tone?: Tone;
|
|
24
|
+
}
|
|
25
|
+
/** Label/value tile for pre-formatted figures. Does no number formatting or arithmetic. */
|
|
26
|
+
export declare function Metric({ label, value, tone, className, ...props }: MetricProps): import("react").JSX.Element;
|
|
27
|
+
interface KeyValueProps extends HTMLAttributes<HTMLDivElement> {
|
|
28
|
+
label: ReactNode;
|
|
29
|
+
value: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
/** Quieter label/value pair for detail views. */
|
|
32
|
+
export declare function KeyValue({ label, value, className, ...props }: KeyValueProps): import("react").JSX.Element;
|
|
33
|
+
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
34
|
+
tone?: Tone;
|
|
35
|
+
icon?: ReactNode;
|
|
36
|
+
}
|
|
37
|
+
/** Compact status/label chip. Never rely on tone alone: include meaningful text. */
|
|
38
|
+
export declare function Badge({ tone, icon, className, children, ...props }: BadgeProps): import("react").JSX.Element;
|
|
39
|
+
interface NoticeProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
40
|
+
tone?: "info" | "success" | "warn" | "danger";
|
|
41
|
+
title?: ReactNode;
|
|
42
|
+
action?: ReactNode;
|
|
43
|
+
}
|
|
44
|
+
/** Inline message. `danger` announces as an alert; other tones as polite status. */
|
|
45
|
+
export declare function Notice({ tone, title, action, className, children, role, ...props }: NoticeProps): import("react").JSX.Element;
|
|
46
|
+
interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
47
|
+
title: ReactNode;
|
|
48
|
+
description?: ReactNode;
|
|
49
|
+
action?: ReactNode;
|
|
50
|
+
}
|
|
51
|
+
/** Placeholder for lists or panels with no data, with an optional next step. */
|
|
52
|
+
export declare function EmptyState({ title, description, action, className, ...props }: EmptyStateProps): import("react").JSX.Element;
|
|
53
|
+
interface FieldControlProps {
|
|
54
|
+
id: string;
|
|
55
|
+
"aria-describedby"?: string;
|
|
56
|
+
"aria-invalid"?: true;
|
|
57
|
+
}
|
|
58
|
+
interface FieldProps {
|
|
59
|
+
label: ReactNode;
|
|
60
|
+
hint?: ReactNode;
|
|
61
|
+
error?: ReactNode;
|
|
62
|
+
className?: string;
|
|
63
|
+
/** Render prop so the control receives the generated id and aria wiring. */
|
|
64
|
+
children: (control: FieldControlProps) => ReactNode;
|
|
65
|
+
}
|
|
66
|
+
/** Label + hint + error wrapper that wires `id`, `aria-describedby` and `aria-invalid`. */
|
|
67
|
+
export declare function Field({ label, hint, error, className, children }: FieldProps): import("react").JSX.Element;
|
|
68
|
+
export declare function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>): import("react").JSX.Element;
|
|
69
|
+
export declare function Select({ className, ...props }: SelectHTMLAttributes<HTMLSelectElement>): import("react").JSX.Element;
|
|
70
|
+
export declare function Textarea({ className, ...props }: TextareaHTMLAttributes<HTMLTextAreaElement>): import("react").JSX.Element;
|
|
71
|
+
interface MeterProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
72
|
+
/** Accessible name; required because the bar itself has no text. */
|
|
73
|
+
label: string;
|
|
74
|
+
/** Current value within `min`..`max`. Values outside are clamped for display. */
|
|
75
|
+
value: number;
|
|
76
|
+
min?: number;
|
|
77
|
+
max?: number;
|
|
78
|
+
/** Optional human-readable value for assistive tech, e.g. "72 of 100". */
|
|
79
|
+
valueText?: string;
|
|
80
|
+
}
|
|
81
|
+
/** Horizontal bar for a bounded score or share. Display only: no domain logic. */
|
|
82
|
+
export declare function Meter({ label, value, min, max, valueText, className, ...props }: MeterProps): import("react").JSX.Element;
|
|
83
|
+
interface SegmentedMeterProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
84
|
+
label: string;
|
|
85
|
+
/** Number of filled segments. */
|
|
86
|
+
value: number;
|
|
87
|
+
segments?: number;
|
|
88
|
+
valueText?: string;
|
|
89
|
+
}
|
|
90
|
+
/** Discrete scale (for example a 1-7 risk indicator). */
|
|
91
|
+
export declare function SegmentedMeter({ label, value, segments, valueText, className, ...props }: SegmentedMeterProps): import("react").JSX.Element;
|
|
92
|
+
interface DistributionItem {
|
|
93
|
+
label: ReactNode;
|
|
94
|
+
value: number;
|
|
95
|
+
}
|
|
96
|
+
interface DistributionListProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
97
|
+
items: readonly DistributionItem[];
|
|
98
|
+
/** Maximum rows shown; the remainder is summarised via `renderOverflow`. */
|
|
99
|
+
limit?: number;
|
|
100
|
+
/** Formats the value text. Consumer owns units and locale. */
|
|
101
|
+
formatValue?: (value: number) => ReactNode;
|
|
102
|
+
/** Shown when there is nothing positive to display. */
|
|
103
|
+
empty?: ReactNode;
|
|
104
|
+
renderOverflow?: (hidden: number) => ReactNode;
|
|
105
|
+
}
|
|
106
|
+
/** Ranked list of share bars, scaled relative to the largest row. */
|
|
107
|
+
export declare function DistributionList({ items, limit, formatValue, empty, renderOverflow, className, ...props }: DistributionListProps): import("react").JSX.Element | null;
|
|
108
|
+
interface MenuItem {
|
|
109
|
+
label: string;
|
|
110
|
+
tone?: "danger";
|
|
111
|
+
disabled?: boolean;
|
|
112
|
+
onSelect: () => void;
|
|
113
|
+
}
|
|
114
|
+
interface MenuProps {
|
|
115
|
+
/** Accessible name of the trigger button. */
|
|
116
|
+
ariaLabel: string;
|
|
117
|
+
items: readonly MenuItem[];
|
|
118
|
+
/** Trigger content. Defaults to a vertical-ellipsis glyph. */
|
|
119
|
+
trigger?: ReactNode;
|
|
120
|
+
className?: string;
|
|
121
|
+
}
|
|
122
|
+
/** Action menu: closes on outside press and Escape (returning focus), arrow-key navigation. */
|
|
123
|
+
export declare function Menu({ ariaLabel, items, trigger, className }: MenuProps): import("react").JSX.Element;
|
|
124
|
+
export type { BadgeProps, ButtonProps, DistributionItem, DistributionListProps, EmptyStateProps, FieldControlProps, FieldProps, KeyValueProps, MenuItem, MenuProps, MeterProps, MetricProps, NoticeProps, PanelProps, SegmentedMeterProps, Tone };
|
|
125
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
// src/lib/cn.ts
|
|
4
|
+
function cn(...classes) {
|
|
5
|
+
return classes.filter(Boolean).join(" ");
|
|
6
|
+
}
|
|
7
|
+
// src/components/Button.tsx
|
|
8
|
+
function Button({ variant = "primary", className, type = "button", ...props }) {
|
|
9
|
+
return /* @__PURE__ */ jsx("button", {
|
|
10
|
+
type,
|
|
11
|
+
className: cn("fds-button", `fds-button--${variant}`, className),
|
|
12
|
+
...props
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
// src/components/Panel.tsx
|
|
16
|
+
/** Titled surface with optional eyebrow, help text and action. Renders a labelled `<section>`. */
|
|
17
|
+
function Panel({ title, eyebrow, help, action, headingLevel = 2, className, children, ...props }) {
|
|
18
|
+
const headingId = useId();
|
|
19
|
+
const Heading = `h${headingLevel}`;
|
|
20
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
21
|
+
"aria-labelledby": headingId,
|
|
22
|
+
className: cn("fds-panel", className),
|
|
23
|
+
...props,
|
|
24
|
+
children: [/* @__PURE__ */ jsxs("header", {
|
|
25
|
+
className: "fds-panel__header",
|
|
26
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
27
|
+
className: "fds-panel__heading",
|
|
28
|
+
children: [
|
|
29
|
+
eyebrow ? /* @__PURE__ */ jsx("p", {
|
|
30
|
+
className: "fds-eyebrow",
|
|
31
|
+
children: eyebrow
|
|
32
|
+
}) : null,
|
|
33
|
+
/* @__PURE__ */ jsx(Heading, {
|
|
34
|
+
id: headingId,
|
|
35
|
+
className: "fds-panel__title",
|
|
36
|
+
children: title
|
|
37
|
+
}),
|
|
38
|
+
help ? /* @__PURE__ */ jsx("p", {
|
|
39
|
+
className: "fds-panel__help",
|
|
40
|
+
children: help
|
|
41
|
+
}) : null
|
|
42
|
+
]
|
|
43
|
+
}), action ? /* @__PURE__ */ jsx("div", {
|
|
44
|
+
className: "fds-panel__action",
|
|
45
|
+
children: action
|
|
46
|
+
}) : null]
|
|
47
|
+
}), children]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// src/components/Metric.tsx
|
|
51
|
+
/** Label/value tile for pre-formatted figures. Does no number formatting or arithmetic. */
|
|
52
|
+
function Metric({ label, value, tone = "neutral", className, ...props }) {
|
|
53
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
54
|
+
className: cn("fds-metric", className),
|
|
55
|
+
...props,
|
|
56
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
57
|
+
className: "fds-metric__label",
|
|
58
|
+
children: label
|
|
59
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
60
|
+
className: cn("fds-metric__value", `fds-tone--${tone}`),
|
|
61
|
+
children: value
|
|
62
|
+
})]
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Quieter label/value pair for detail views. */
|
|
66
|
+
function KeyValue({ label, value, className, ...props }) {
|
|
67
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
68
|
+
className: cn("fds-keyvalue", className),
|
|
69
|
+
...props,
|
|
70
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
71
|
+
className: "fds-metric__label",
|
|
72
|
+
children: label
|
|
73
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
74
|
+
className: "fds-keyvalue__value",
|
|
75
|
+
children: value
|
|
76
|
+
})]
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// src/components/Badge.tsx
|
|
80
|
+
/** Compact status/label chip. Never rely on tone alone: include meaningful text. */
|
|
81
|
+
function Badge({ tone = "neutral", icon, className, children, ...props }) {
|
|
82
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
83
|
+
className: cn("fds-badge", `fds-badge--${tone}`, className),
|
|
84
|
+
...props,
|
|
85
|
+
children: [icon ? /* @__PURE__ */ jsx("span", {
|
|
86
|
+
"aria-hidden": "true",
|
|
87
|
+
className: "fds-badge__icon",
|
|
88
|
+
children: icon
|
|
89
|
+
}) : null, children]
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// src/components/Notice.tsx
|
|
93
|
+
/** Inline message. `danger` announces as an alert; other tones as polite status. */
|
|
94
|
+
function Notice({ tone = "info", title, action, className, children, role, ...props }) {
|
|
95
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
96
|
+
role: role ?? (tone === "danger" ? "alert" : "status"),
|
|
97
|
+
className: cn("fds-notice", `fds-notice--${tone}`, className),
|
|
98
|
+
...props,
|
|
99
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
100
|
+
className: "fds-notice__body",
|
|
101
|
+
children: [title ? /* @__PURE__ */ jsx("p", {
|
|
102
|
+
className: "fds-notice__title",
|
|
103
|
+
children: title
|
|
104
|
+
}) : null, children ? /* @__PURE__ */ jsx("div", {
|
|
105
|
+
className: "fds-notice__text",
|
|
106
|
+
children
|
|
107
|
+
}) : null]
|
|
108
|
+
}), action ? /* @__PURE__ */ jsx("div", {
|
|
109
|
+
className: "fds-notice__action",
|
|
110
|
+
children: action
|
|
111
|
+
}) : null]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** Placeholder for lists or panels with no data, with an optional next step. */
|
|
115
|
+
function EmptyState({ title, description, action, className, ...props }) {
|
|
116
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
117
|
+
className: cn("fds-empty", className),
|
|
118
|
+
...props,
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ jsx("p", {
|
|
121
|
+
className: "fds-empty__title",
|
|
122
|
+
children: title
|
|
123
|
+
}),
|
|
124
|
+
description ? /* @__PURE__ */ jsx("p", {
|
|
125
|
+
className: "fds-empty__description",
|
|
126
|
+
children: description
|
|
127
|
+
}) : null,
|
|
128
|
+
action ? /* @__PURE__ */ jsx("div", {
|
|
129
|
+
className: "fds-empty__action",
|
|
130
|
+
children: action
|
|
131
|
+
}) : null
|
|
132
|
+
]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// src/components/Field.tsx
|
|
136
|
+
/** Label + hint + error wrapper that wires `id`, `aria-describedby` and `aria-invalid`. */
|
|
137
|
+
function Field({ label, hint, error, className, children }) {
|
|
138
|
+
const id = useId();
|
|
139
|
+
const hintId = `${id}-hint`;
|
|
140
|
+
const errorId = `${id}-error`;
|
|
141
|
+
const describedBy = [hint ? hintId : null, error ? errorId : null].filter(Boolean).join(" ");
|
|
142
|
+
const control = { id };
|
|
143
|
+
if (describedBy) control["aria-describedby"] = describedBy;
|
|
144
|
+
if (error) control["aria-invalid"] = true;
|
|
145
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
146
|
+
className: cn("fds-field", className),
|
|
147
|
+
children: [
|
|
148
|
+
/* @__PURE__ */ jsx("label", {
|
|
149
|
+
className: "fds-field__label",
|
|
150
|
+
htmlFor: id,
|
|
151
|
+
children: label
|
|
152
|
+
}),
|
|
153
|
+
children(control),
|
|
154
|
+
hint ? /* @__PURE__ */ jsx("p", {
|
|
155
|
+
className: "fds-field__hint",
|
|
156
|
+
id: hintId,
|
|
157
|
+
children: hint
|
|
158
|
+
}) : null,
|
|
159
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
160
|
+
className: "fds-field__error",
|
|
161
|
+
id: errorId,
|
|
162
|
+
children: error
|
|
163
|
+
}) : null
|
|
164
|
+
]
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function Input({ className, ...props }) {
|
|
168
|
+
return /* @__PURE__ */ jsx("input", {
|
|
169
|
+
className: cn("fds-control", className),
|
|
170
|
+
...props
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function Select({ className, ...props }) {
|
|
174
|
+
return /* @__PURE__ */ jsx("select", {
|
|
175
|
+
className: cn("fds-control", className),
|
|
176
|
+
...props
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function Textarea({ className, ...props }) {
|
|
180
|
+
return /* @__PURE__ */ jsx("textarea", {
|
|
181
|
+
className: cn("fds-control fds-control--multiline", className),
|
|
182
|
+
...props
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// src/components/Meter.tsx
|
|
186
|
+
const clamp = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
|
|
187
|
+
/** Horizontal bar for a bounded score or share. Display only: no domain logic. */
|
|
188
|
+
function Meter({ label, value, min = 0, max = 100, valueText, className, ...props }) {
|
|
189
|
+
const span = max - min;
|
|
190
|
+
const ratio = span > 0 ? clamp((value - min) / span, 0, 1) : 0;
|
|
191
|
+
return /* @__PURE__ */ jsx("div", {
|
|
192
|
+
role: "meter",
|
|
193
|
+
"aria-label": label,
|
|
194
|
+
"aria-valuemin": min,
|
|
195
|
+
"aria-valuemax": max,
|
|
196
|
+
"aria-valuenow": clamp(value, min, max),
|
|
197
|
+
"aria-valuetext": valueText,
|
|
198
|
+
className: cn("fds-meter", className),
|
|
199
|
+
...props,
|
|
200
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
201
|
+
className: "fds-meter__fill",
|
|
202
|
+
style: { width: `${ratio * 100}%` }
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/** Discrete scale (for example a 1-7 risk indicator). */
|
|
207
|
+
function SegmentedMeter({ label, value, segments = 7, valueText, className, ...props }) {
|
|
208
|
+
const filled = clamp(Math.round(value), 0, segments);
|
|
209
|
+
return /* @__PURE__ */ jsx("div", {
|
|
210
|
+
role: "meter",
|
|
211
|
+
"aria-label": label,
|
|
212
|
+
"aria-valuemin": 0,
|
|
213
|
+
"aria-valuemax": segments,
|
|
214
|
+
"aria-valuenow": filled,
|
|
215
|
+
"aria-valuetext": valueText,
|
|
216
|
+
className: cn("fds-segmeter", className),
|
|
217
|
+
...props,
|
|
218
|
+
children: Array.from({ length: segments }, (_, index) => /* @__PURE__ */ jsx("span", { className: cn("fds-segmeter__segment", index < filled && "fds-segmeter__segment--on") }, index))
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/** Ranked list of share bars, scaled relative to the largest row. */
|
|
222
|
+
function DistributionList({ items, limit = 5, formatValue = (v) => String(v), empty, renderOverflow, className, ...props }) {
|
|
223
|
+
const sorted = [...items].sort((a, b) => b.value - a.value);
|
|
224
|
+
const top = sorted.slice(0, limit);
|
|
225
|
+
const max = top[0]?.value ?? 0;
|
|
226
|
+
if (max <= 0) return empty ? /* @__PURE__ */ jsx("p", {
|
|
227
|
+
className: "fds-distribution__empty",
|
|
228
|
+
children: empty
|
|
229
|
+
}) : null;
|
|
230
|
+
const hidden = sorted.length - top.length;
|
|
231
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
232
|
+
className: cn("fds-distribution", className),
|
|
233
|
+
...props,
|
|
234
|
+
children: [/* @__PURE__ */ jsx("ul", {
|
|
235
|
+
className: "fds-distribution__list",
|
|
236
|
+
children: top.map((item, index) => /* @__PURE__ */ jsxs("li", {
|
|
237
|
+
className: "fds-distribution__row",
|
|
238
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
239
|
+
className: "fds-distribution__meta",
|
|
240
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
241
|
+
className: "fds-distribution__label",
|
|
242
|
+
children: item.label
|
|
243
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
244
|
+
className: "fds-distribution__value",
|
|
245
|
+
children: formatValue(item.value)
|
|
246
|
+
})]
|
|
247
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
248
|
+
"aria-hidden": "true",
|
|
249
|
+
className: "fds-meter fds-meter--thin",
|
|
250
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
251
|
+
className: "fds-meter__fill",
|
|
252
|
+
style: { width: `${Math.max(item.value, 0) / max * 100}%` }
|
|
253
|
+
})
|
|
254
|
+
})]
|
|
255
|
+
}, index))
|
|
256
|
+
}), hidden > 0 && renderOverflow ? /* @__PURE__ */ jsx("p", {
|
|
257
|
+
className: "fds-distribution__overflow",
|
|
258
|
+
children: renderOverflow(hidden)
|
|
259
|
+
}) : null]
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
// src/components/Menu.tsx
|
|
263
|
+
/** Action menu: closes on outside press and Escape (returning focus), arrow-key navigation. */
|
|
264
|
+
function Menu({ ariaLabel, items, trigger, className }) {
|
|
265
|
+
const [open, setOpen] = useState(false);
|
|
266
|
+
const rootRef = useRef(null);
|
|
267
|
+
const triggerRef = useRef(null);
|
|
268
|
+
const menuId = useId();
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (!open) return;
|
|
271
|
+
const onPointerDown = (event) => {
|
|
272
|
+
if (!rootRef.current?.contains(event.target)) setOpen(false);
|
|
273
|
+
};
|
|
274
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
275
|
+
document.addEventListener("touchstart", onPointerDown);
|
|
276
|
+
return () => {
|
|
277
|
+
document.removeEventListener("mousedown", onPointerDown);
|
|
278
|
+
document.removeEventListener("touchstart", onPointerDown);
|
|
279
|
+
};
|
|
280
|
+
}, [open]);
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (open) rootRef.current?.querySelector("[role=\"menuitem\"]:not(:disabled)")?.focus();
|
|
283
|
+
}, [open]);
|
|
284
|
+
const onKeyDown = (event) => {
|
|
285
|
+
if (!open) return;
|
|
286
|
+
if (event.key === "Escape") {
|
|
287
|
+
event.stopPropagation();
|
|
288
|
+
setOpen(false);
|
|
289
|
+
triggerRef.current?.focus();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const enabled = Array.from(rootRef.current?.querySelectorAll("[role=\"menuitem\"]:not(:disabled)") ?? []);
|
|
293
|
+
if (enabled.length === 0) return;
|
|
294
|
+
const current = enabled.indexOf(document.activeElement);
|
|
295
|
+
const move = (next) => {
|
|
296
|
+
event.preventDefault();
|
|
297
|
+
enabled[(next + enabled.length) % enabled.length]?.focus();
|
|
298
|
+
};
|
|
299
|
+
if (event.key === "ArrowDown") move(current + 1);
|
|
300
|
+
else if (event.key === "ArrowUp") move(current < 0 ? enabled.length - 1 : current - 1);
|
|
301
|
+
else if (event.key === "Home") move(0);
|
|
302
|
+
else if (event.key === "End") move(enabled.length - 1);
|
|
303
|
+
else if (event.key === "Tab") setOpen(false);
|
|
304
|
+
};
|
|
305
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
306
|
+
className: cn("fds-menu", className),
|
|
307
|
+
onKeyDown,
|
|
308
|
+
ref: rootRef,
|
|
309
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
310
|
+
"aria-controls": open ? menuId : void 0,
|
|
311
|
+
"aria-expanded": open,
|
|
312
|
+
"aria-haspopup": "menu",
|
|
313
|
+
"aria-label": ariaLabel,
|
|
314
|
+
className: "fds-menu__trigger",
|
|
315
|
+
onClick: () => setOpen((value) => !value),
|
|
316
|
+
ref: triggerRef,
|
|
317
|
+
type: "button",
|
|
318
|
+
children: trigger ?? /* @__PURE__ */ jsxs("svg", {
|
|
319
|
+
"aria-hidden": "true",
|
|
320
|
+
focusable: "false",
|
|
321
|
+
height: "16",
|
|
322
|
+
viewBox: "0 0 16 16",
|
|
323
|
+
width: "16",
|
|
324
|
+
children: [
|
|
325
|
+
/* @__PURE__ */ jsx("circle", {
|
|
326
|
+
cx: "3",
|
|
327
|
+
cy: "8",
|
|
328
|
+
fill: "currentColor",
|
|
329
|
+
r: "1.4"
|
|
330
|
+
}),
|
|
331
|
+
/* @__PURE__ */ jsx("circle", {
|
|
332
|
+
cx: "8",
|
|
333
|
+
cy: "8",
|
|
334
|
+
fill: "currentColor",
|
|
335
|
+
r: "1.4"
|
|
336
|
+
}),
|
|
337
|
+
/* @__PURE__ */ jsx("circle", {
|
|
338
|
+
cx: "13",
|
|
339
|
+
cy: "8",
|
|
340
|
+
fill: "currentColor",
|
|
341
|
+
r: "1.4"
|
|
342
|
+
})
|
|
343
|
+
]
|
|
344
|
+
})
|
|
345
|
+
}), open ? /* @__PURE__ */ jsx("div", {
|
|
346
|
+
className: "fds-menu__list",
|
|
347
|
+
id: menuId,
|
|
348
|
+
role: "menu",
|
|
349
|
+
children: items.map((item) => /* @__PURE__ */ jsx("button", {
|
|
350
|
+
className: cn("fds-menu__item", item.tone === "danger" && "fds-menu__item--danger"),
|
|
351
|
+
disabled: item.disabled,
|
|
352
|
+
onClick: () => {
|
|
353
|
+
setOpen(false);
|
|
354
|
+
triggerRef.current?.focus();
|
|
355
|
+
item.onSelect();
|
|
356
|
+
},
|
|
357
|
+
role: "menuitem",
|
|
358
|
+
type: "button",
|
|
359
|
+
children: item.label
|
|
360
|
+
}, item.label))
|
|
361
|
+
}) : null]
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
export { Badge, Button, DistributionList, EmptyState, Field, Input, KeyValue, Menu, Meter, Metric, Notice, Panel, SegmentedMeter, Select, Textarea, cn };
|
|
365
|
+
|
|
366
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/cn.ts","../src/components/Button.tsx","../src/components/Panel.tsx","../src/components/Metric.tsx","../src/components/Badge.tsx","../src/components/Notice.tsx","../src/components/Field.tsx","../src/components/Meter.tsx","../src/components/Menu.tsx"],"sourcesContent":["export function cn(...classes: Array<string | false | null | undefined>): string {\n return classes.filter(Boolean).join(\" \");\n}\n","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","import { useId, type ElementType, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\nexport interface PanelProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n title: ReactNode;\n eyebrow?: ReactNode;\n help?: ReactNode;\n /** Optional trailing action area (buttons, menu). */\n action?: ReactNode;\n /** Heading element for the title. Defaults to h2; pick the level that fits the page outline. */\n headingLevel?: 2 | 3 | 4;\n}\n\n/** Titled surface with optional eyebrow, help text and action. Renders a labelled `<section>`. */\nexport function Panel({\n title,\n eyebrow,\n help,\n action,\n headingLevel = 2,\n className,\n children,\n ...props\n}: PanelProps) {\n const headingId = useId();\n const Heading = `h${headingLevel}` as ElementType;\n return (\n <section aria-labelledby={headingId} className={cn(\"fds-panel\", className)} {...props}>\n <header className=\"fds-panel__header\">\n <div className=\"fds-panel__heading\">\n {eyebrow ? <p className=\"fds-eyebrow\">{eyebrow}</p> : null}\n <Heading id={headingId} className=\"fds-panel__title\">\n {title}\n </Heading>\n {help ? <p className=\"fds-panel__help\">{help}</p> : null}\n </div>\n {action ? <div className=\"fds-panel__action\">{action}</div> : null}\n </header>\n {children}\n </section>\n );\n}\n","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","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","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","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","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","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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scttlnd/fintech-design-system",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Shared design foundations for Funder, Portare and Investor",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/scottlinddk/fintech-design-system.git"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"styles",
|
|
13
|
+
"AGENTS.md",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"**/*.css"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.mts",
|
|
23
|
+
"import": "./dist/index.mjs"
|
|
24
|
+
},
|
|
25
|
+
"./styles.css": "./styles/index.css",
|
|
26
|
+
"./themes/funder.css": "./styles/themes/funder.css",
|
|
27
|
+
"./themes/portare.css": "./styles/themes/portare.css",
|
|
28
|
+
"./themes/investor.css": "./styles/themes/investor.css"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"registry": "https://registry.npmjs.org/"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "vp pack",
|
|
36
|
+
"check": "vp check",
|
|
37
|
+
"test": "vp test --run",
|
|
38
|
+
"changeset": "changeset",
|
|
39
|
+
"version": "changeset version && vp check --fix",
|
|
40
|
+
"release": "changeset publish",
|
|
41
|
+
"prepublishOnly": "npm run build"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@changesets/changelog-github": "^1.0.1",
|
|
45
|
+
"@changesets/cli": "^3.0.3",
|
|
46
|
+
"@types/react": "^18.3.18",
|
|
47
|
+
"@types/react-dom": "^18.3.7",
|
|
48
|
+
"react": "^18.3.1",
|
|
49
|
+
"react-dom": "^18.3.1",
|
|
50
|
+
"typescript": "^5.7.3",
|
|
51
|
+
"vite-plus": "^1.5.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"react": "^18.3.0 || ^19.0.0"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=22.14.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/* Shared primitives. Only --fds-* tokens; tones are derived with color-mix so every theme works. */
|
|
2
|
+
|
|
3
|
+
.fds-eyebrow {
|
|
4
|
+
margin: 0;
|
|
5
|
+
font-size: 0.75rem;
|
|
6
|
+
letter-spacing: 0.06em;
|
|
7
|
+
text-transform: uppercase;
|
|
8
|
+
color: var(--fds-muted);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Panel */
|
|
12
|
+
.fds-panel {
|
|
13
|
+
padding: 1rem;
|
|
14
|
+
border: 1px solid var(--fds-line);
|
|
15
|
+
border-radius: var(--fds-radius-lg);
|
|
16
|
+
background: var(--fds-panel);
|
|
17
|
+
color: var(--fds-foreground);
|
|
18
|
+
}
|
|
19
|
+
.fds-panel__header {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 0.75rem;
|
|
23
|
+
margin-bottom: 1rem;
|
|
24
|
+
}
|
|
25
|
+
.fds-panel__heading {
|
|
26
|
+
min-width: 0;
|
|
27
|
+
}
|
|
28
|
+
.fds-panel__title {
|
|
29
|
+
margin: 0.25rem 0 0;
|
|
30
|
+
font-size: 1.125rem;
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
}
|
|
33
|
+
.fds-panel__help {
|
|
34
|
+
margin: 0.375rem 0 0;
|
|
35
|
+
max-width: 65ch;
|
|
36
|
+
font-size: 0.75rem;
|
|
37
|
+
line-height: 1.5;
|
|
38
|
+
color: var(--fds-muted);
|
|
39
|
+
}
|
|
40
|
+
@media (min-width: 640px) {
|
|
41
|
+
.fds-panel {
|
|
42
|
+
padding: 1.25rem;
|
|
43
|
+
}
|
|
44
|
+
.fds-panel__header {
|
|
45
|
+
flex-direction: row;
|
|
46
|
+
align-items: flex-start;
|
|
47
|
+
justify-content: space-between;
|
|
48
|
+
gap: 1rem;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Tones */
|
|
53
|
+
.fds-tone--accent {
|
|
54
|
+
color: var(--fds-accent);
|
|
55
|
+
}
|
|
56
|
+
.fds-tone--success {
|
|
57
|
+
color: var(--fds-success);
|
|
58
|
+
}
|
|
59
|
+
.fds-tone--warn {
|
|
60
|
+
color: var(--fds-warn);
|
|
61
|
+
}
|
|
62
|
+
.fds-tone--danger {
|
|
63
|
+
color: var(--fds-danger);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Metric / KeyValue */
|
|
67
|
+
.fds-metric,
|
|
68
|
+
.fds-keyvalue {
|
|
69
|
+
padding: 0.75rem;
|
|
70
|
+
border: 1px solid var(--fds-line);
|
|
71
|
+
border-radius: var(--fds-radius-md);
|
|
72
|
+
background: var(--fds-panel-soft, var(--fds-panel));
|
|
73
|
+
}
|
|
74
|
+
.fds-keyvalue {
|
|
75
|
+
border-color: var(--fds-line-soft, var(--fds-line));
|
|
76
|
+
}
|
|
77
|
+
.fds-metric__label {
|
|
78
|
+
margin: 0;
|
|
79
|
+
font-size: 0.75rem;
|
|
80
|
+
color: var(--fds-muted);
|
|
81
|
+
}
|
|
82
|
+
.fds-metric__value {
|
|
83
|
+
margin: 0.5rem 0 0;
|
|
84
|
+
font-size: 1.125rem;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
font-variant-numeric: tabular-nums;
|
|
87
|
+
}
|
|
88
|
+
.fds-keyvalue__value {
|
|
89
|
+
margin: 0.25rem 0 0;
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Badge */
|
|
94
|
+
.fds-badge {
|
|
95
|
+
--fds-badge-color: var(--fds-muted-strong, var(--fds-foreground));
|
|
96
|
+
display: inline-flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: 0.375rem;
|
|
99
|
+
min-height: 1.75rem;
|
|
100
|
+
padding: 0 0.5rem;
|
|
101
|
+
border: 1px solid color-mix(in srgb, var(--fds-badge-color) 30%, transparent);
|
|
102
|
+
border-radius: var(--fds-radius-sm);
|
|
103
|
+
background: color-mix(in srgb, var(--fds-badge-color) 12%, transparent);
|
|
104
|
+
color: var(--fds-badge-color);
|
|
105
|
+
font-size: 0.75rem;
|
|
106
|
+
font-weight: 500;
|
|
107
|
+
white-space: nowrap;
|
|
108
|
+
}
|
|
109
|
+
.fds-badge--accent {
|
|
110
|
+
--fds-badge-color: var(--fds-accent);
|
|
111
|
+
}
|
|
112
|
+
.fds-badge--success {
|
|
113
|
+
--fds-badge-color: var(--fds-success);
|
|
114
|
+
}
|
|
115
|
+
.fds-badge--warn {
|
|
116
|
+
--fds-badge-color: var(--fds-warn);
|
|
117
|
+
}
|
|
118
|
+
.fds-badge--danger {
|
|
119
|
+
--fds-badge-color: var(--fds-danger);
|
|
120
|
+
}
|
|
121
|
+
.fds-badge__icon {
|
|
122
|
+
display: inline-flex;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Notice */
|
|
126
|
+
.fds-notice {
|
|
127
|
+
--fds-notice-color: var(--fds-accent);
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-wrap: wrap;
|
|
130
|
+
align-items: center;
|
|
131
|
+
justify-content: space-between;
|
|
132
|
+
gap: 0.75rem;
|
|
133
|
+
padding: 0.75rem 1rem;
|
|
134
|
+
border: 1px solid color-mix(in srgb, var(--fds-notice-color) 35%, transparent);
|
|
135
|
+
border-left-width: 4px;
|
|
136
|
+
border-radius: var(--fds-radius-md);
|
|
137
|
+
background: color-mix(in srgb, var(--fds-notice-color) 10%, transparent);
|
|
138
|
+
color: var(--fds-foreground);
|
|
139
|
+
}
|
|
140
|
+
.fds-notice--success {
|
|
141
|
+
--fds-notice-color: var(--fds-success);
|
|
142
|
+
}
|
|
143
|
+
.fds-notice--warn {
|
|
144
|
+
--fds-notice-color: var(--fds-warn);
|
|
145
|
+
}
|
|
146
|
+
.fds-notice--danger {
|
|
147
|
+
--fds-notice-color: var(--fds-danger);
|
|
148
|
+
}
|
|
149
|
+
.fds-notice__title {
|
|
150
|
+
margin: 0;
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
}
|
|
153
|
+
.fds-notice__text {
|
|
154
|
+
font-size: 0.875rem;
|
|
155
|
+
color: var(--fds-muted-strong, var(--fds-foreground));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Empty state */
|
|
159
|
+
.fds-empty {
|
|
160
|
+
padding: 2rem 1rem;
|
|
161
|
+
text-align: center;
|
|
162
|
+
}
|
|
163
|
+
.fds-empty__title {
|
|
164
|
+
margin: 0;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
}
|
|
167
|
+
.fds-empty__description {
|
|
168
|
+
margin: 0.5rem auto 0;
|
|
169
|
+
max-width: 45ch;
|
|
170
|
+
font-size: 0.875rem;
|
|
171
|
+
color: var(--fds-muted);
|
|
172
|
+
}
|
|
173
|
+
.fds-empty__action {
|
|
174
|
+
margin-top: 1rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Form controls: 44px targets on touch / small screens, 16px font to stop iOS zoom. */
|
|
178
|
+
.fds-field {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: 0.375rem;
|
|
182
|
+
}
|
|
183
|
+
.fds-field__label {
|
|
184
|
+
font-size: 0.875rem;
|
|
185
|
+
font-weight: 500;
|
|
186
|
+
}
|
|
187
|
+
.fds-field__hint,
|
|
188
|
+
.fds-field__error {
|
|
189
|
+
margin: 0;
|
|
190
|
+
font-size: 0.8125rem;
|
|
191
|
+
}
|
|
192
|
+
.fds-field__hint {
|
|
193
|
+
color: var(--fds-muted);
|
|
194
|
+
}
|
|
195
|
+
.fds-field__error {
|
|
196
|
+
color: var(--fds-danger);
|
|
197
|
+
}
|
|
198
|
+
.fds-control {
|
|
199
|
+
width: 100%;
|
|
200
|
+
min-height: 2.75rem;
|
|
201
|
+
padding: 0.5rem 0.75rem;
|
|
202
|
+
border: 1px solid var(--fds-line);
|
|
203
|
+
border-radius: var(--fds-radius-md);
|
|
204
|
+
background: var(--fds-panel-soft, var(--fds-panel));
|
|
205
|
+
color: var(--fds-foreground);
|
|
206
|
+
font: inherit;
|
|
207
|
+
font-size: 1rem;
|
|
208
|
+
}
|
|
209
|
+
.fds-control--multiline {
|
|
210
|
+
min-height: 6rem;
|
|
211
|
+
resize: vertical;
|
|
212
|
+
}
|
|
213
|
+
.fds-control:focus-visible {
|
|
214
|
+
outline: 2px solid var(--fds-accent);
|
|
215
|
+
outline-offset: 2px;
|
|
216
|
+
border-color: var(--fds-accent);
|
|
217
|
+
}
|
|
218
|
+
.fds-control[aria-invalid="true"] {
|
|
219
|
+
border-color: var(--fds-danger);
|
|
220
|
+
}
|
|
221
|
+
.fds-control:disabled {
|
|
222
|
+
opacity: 0.5;
|
|
223
|
+
cursor: not-allowed;
|
|
224
|
+
}
|
|
225
|
+
@media (min-width: 640px) and (pointer: fine) {
|
|
226
|
+
.fds-control {
|
|
227
|
+
min-height: 2.25rem;
|
|
228
|
+
font-size: 0.875rem;
|
|
229
|
+
}
|
|
230
|
+
.fds-control--multiline {
|
|
231
|
+
min-height: 6rem;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Meters */
|
|
236
|
+
.fds-meter {
|
|
237
|
+
height: 0.5rem;
|
|
238
|
+
overflow: hidden;
|
|
239
|
+
border-radius: 999px;
|
|
240
|
+
background: var(--fds-line);
|
|
241
|
+
}
|
|
242
|
+
.fds-meter--thin {
|
|
243
|
+
height: 0.375rem;
|
|
244
|
+
}
|
|
245
|
+
.fds-meter__fill {
|
|
246
|
+
height: 100%;
|
|
247
|
+
border-radius: inherit;
|
|
248
|
+
background: var(--fds-accent);
|
|
249
|
+
}
|
|
250
|
+
.fds-segmeter {
|
|
251
|
+
display: flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
gap: 0.25rem;
|
|
254
|
+
}
|
|
255
|
+
.fds-segmeter__segment {
|
|
256
|
+
width: 0.5rem;
|
|
257
|
+
height: 1rem;
|
|
258
|
+
border-radius: 2px;
|
|
259
|
+
background: var(--fds-line);
|
|
260
|
+
}
|
|
261
|
+
.fds-segmeter__segment--on {
|
|
262
|
+
background: var(--fds-accent);
|
|
263
|
+
}
|
|
264
|
+
.fds-distribution__list {
|
|
265
|
+
display: flex;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
gap: 0.5rem;
|
|
268
|
+
margin: 0;
|
|
269
|
+
padding: 0;
|
|
270
|
+
list-style: none;
|
|
271
|
+
}
|
|
272
|
+
.fds-distribution__meta {
|
|
273
|
+
display: flex;
|
|
274
|
+
justify-content: space-between;
|
|
275
|
+
gap: 0.75rem;
|
|
276
|
+
margin-bottom: 0.25rem;
|
|
277
|
+
font-size: 0.75rem;
|
|
278
|
+
}
|
|
279
|
+
.fds-distribution__label {
|
|
280
|
+
color: var(--fds-muted-strong, var(--fds-foreground));
|
|
281
|
+
}
|
|
282
|
+
.fds-distribution__value {
|
|
283
|
+
font-weight: 500;
|
|
284
|
+
font-variant-numeric: tabular-nums;
|
|
285
|
+
}
|
|
286
|
+
.fds-distribution__empty,
|
|
287
|
+
.fds-distribution__overflow {
|
|
288
|
+
margin: 0;
|
|
289
|
+
font-size: 0.8125rem;
|
|
290
|
+
color: var(--fds-muted);
|
|
291
|
+
}
|
|
292
|
+
.fds-distribution__overflow {
|
|
293
|
+
margin-top: 0.5rem;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* Menu */
|
|
297
|
+
.fds-menu {
|
|
298
|
+
position: relative;
|
|
299
|
+
display: inline-block;
|
|
300
|
+
}
|
|
301
|
+
.fds-menu__trigger {
|
|
302
|
+
display: inline-flex;
|
|
303
|
+
align-items: center;
|
|
304
|
+
justify-content: center;
|
|
305
|
+
width: 2.75rem;
|
|
306
|
+
height: 2.75rem;
|
|
307
|
+
border: 1px solid var(--fds-line);
|
|
308
|
+
border-radius: var(--fds-radius-md);
|
|
309
|
+
background: var(--fds-panel-soft, var(--fds-panel));
|
|
310
|
+
color: var(--fds-muted);
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
}
|
|
313
|
+
.fds-menu__trigger:hover {
|
|
314
|
+
border-color: var(--fds-accent);
|
|
315
|
+
color: var(--fds-foreground);
|
|
316
|
+
}
|
|
317
|
+
.fds-menu__trigger:focus-visible,
|
|
318
|
+
.fds-menu__item:focus-visible {
|
|
319
|
+
outline: 2px solid var(--fds-accent);
|
|
320
|
+
outline-offset: 2px;
|
|
321
|
+
}
|
|
322
|
+
.fds-menu__list {
|
|
323
|
+
position: absolute;
|
|
324
|
+
top: 100%;
|
|
325
|
+
right: 0;
|
|
326
|
+
z-index: 30;
|
|
327
|
+
min-width: 11rem;
|
|
328
|
+
margin-top: 0.25rem;
|
|
329
|
+
padding: 0.25rem 0;
|
|
330
|
+
overflow: hidden;
|
|
331
|
+
border: 1px solid var(--fds-line);
|
|
332
|
+
border-radius: var(--fds-radius-md);
|
|
333
|
+
background: var(--fds-panel);
|
|
334
|
+
box-shadow: 0 10px 30px rgb(0 0 0 / 0.25);
|
|
335
|
+
}
|
|
336
|
+
.fds-menu__item {
|
|
337
|
+
display: block;
|
|
338
|
+
width: 100%;
|
|
339
|
+
min-height: 2.75rem;
|
|
340
|
+
padding: 0.5rem 0.75rem;
|
|
341
|
+
border: 0;
|
|
342
|
+
background: transparent;
|
|
343
|
+
color: var(--fds-foreground);
|
|
344
|
+
font: inherit;
|
|
345
|
+
text-align: left;
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
}
|
|
348
|
+
.fds-menu__item:hover,
|
|
349
|
+
.fds-menu__item:focus-visible {
|
|
350
|
+
background: var(--fds-panel-raised);
|
|
351
|
+
}
|
|
352
|
+
.fds-menu__item--danger {
|
|
353
|
+
color: var(--fds-danger);
|
|
354
|
+
}
|
|
355
|
+
.fds-menu__item:disabled {
|
|
356
|
+
opacity: 0.5;
|
|
357
|
+
cursor: not-allowed;
|
|
358
|
+
}
|
|
359
|
+
@media (min-width: 640px) and (pointer: fine) {
|
|
360
|
+
.fds-menu__trigger {
|
|
361
|
+
width: 2.25rem;
|
|
362
|
+
height: 2.25rem;
|
|
363
|
+
}
|
|
364
|
+
.fds-menu__item {
|
|
365
|
+
min-height: 2.25rem;
|
|
366
|
+
}
|
|
367
|
+
}
|
package/styles/index.css
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
@import "./tokens.css";
|
|
2
|
+
@import "./components.css";
|
|
3
|
+
.fds-button {
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
justify-content: center;
|
|
7
|
+
gap: 0.5rem;
|
|
8
|
+
min-height: 2.5rem;
|
|
9
|
+
padding: 0.55rem 1rem;
|
|
10
|
+
border-radius: var(--fds-radius-md);
|
|
11
|
+
font: inherit;
|
|
12
|
+
font-weight: 600;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
transition:
|
|
15
|
+
background-color 0.15s,
|
|
16
|
+
border-color 0.15s;
|
|
17
|
+
}
|
|
18
|
+
.fds-button:focus-visible {
|
|
19
|
+
outline: 2px solid var(--fds-accent);
|
|
20
|
+
outline-offset: 2px;
|
|
21
|
+
}
|
|
22
|
+
.fds-button:disabled {
|
|
23
|
+
opacity: 0.5;
|
|
24
|
+
cursor: not-allowed;
|
|
25
|
+
}
|
|
26
|
+
.fds-button--primary {
|
|
27
|
+
background: var(--fds-accent);
|
|
28
|
+
color: var(--fds-on-accent);
|
|
29
|
+
border: 1px solid transparent;
|
|
30
|
+
}
|
|
31
|
+
.fds-button--secondary {
|
|
32
|
+
background: var(--fds-panel-raised);
|
|
33
|
+
color: var(--fds-foreground);
|
|
34
|
+
border: 1px solid var(--fds-line);
|
|
35
|
+
}
|
|
36
|
+
.fds-button--ghost {
|
|
37
|
+
background: transparent;
|
|
38
|
+
color: var(--fds-foreground);
|
|
39
|
+
border: 1px solid transparent;
|
|
40
|
+
}
|
|
41
|
+
@media (prefers-reduced-motion: reduce) {
|
|
42
|
+
.fds-button {
|
|
43
|
+
transition: none;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
:root,
|
|
2
|
+
[data-fds-theme="portare"] {
|
|
3
|
+
color-scheme: light;
|
|
4
|
+
--fds-background: #fafbfc;
|
|
5
|
+
--fds-foreground: #111827;
|
|
6
|
+
--fds-muted: #525b6b;
|
|
7
|
+
--fds-panel: #fff;
|
|
8
|
+
--fds-panel-raised: #f3f5f8;
|
|
9
|
+
--fds-line: #d8dee7;
|
|
10
|
+
--fds-accent: #075e7c;
|
|
11
|
+
--fds-accent-strong: #064c66;
|
|
12
|
+
--fds-on-accent: #fff;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Semantic and secondary surface tokens: the base (dark) values fail contrast on a light surface. */
|
|
16
|
+
:root,
|
|
17
|
+
[data-fds-theme="portare"] {
|
|
18
|
+
--fds-muted-strong: #374151;
|
|
19
|
+
--fds-line-soft: #e5e9f0;
|
|
20
|
+
--fds-panel-soft: #f8fafc;
|
|
21
|
+
--fds-success: #1f6f4a;
|
|
22
|
+
--fds-warn: #8a5a00;
|
|
23
|
+
--fds-danger: #b3261e;
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--fds-background: #08090b;
|
|
3
|
+
--fds-foreground: #f4f4f5;
|
|
4
|
+
--fds-muted: #a1a1aa;
|
|
5
|
+
--fds-muted-strong: #d4d4d8;
|
|
6
|
+
--fds-line: #27272a;
|
|
7
|
+
--fds-line-soft: #1b1b1f;
|
|
8
|
+
--fds-panel: #101114;
|
|
9
|
+
--fds-panel-raised: #15161a;
|
|
10
|
+
--fds-panel-soft: #0c0d10;
|
|
11
|
+
--fds-accent: #7dd3fc;
|
|
12
|
+
--fds-accent-strong: #38bdf8;
|
|
13
|
+
--fds-on-accent: #08090b;
|
|
14
|
+
--fds-success: #86efac;
|
|
15
|
+
--fds-warn: #fbbf24;
|
|
16
|
+
--fds-danger: #fda4af;
|
|
17
|
+
--fds-radius-md: 0.625rem;
|
|
18
|
+
--fds-font-sans:
|
|
19
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
:root {
|
|
23
|
+
--fds-radius-lg: 0.75rem;
|
|
24
|
+
--fds-radius-sm: 0.375rem;
|
|
25
|
+
}
|