@scttlnd/fintech-design-system 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +9 -3
  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 -125
  28. package/dist/index.mjs +9 -365
  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 +18 -3
  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 -367
  46. package/styles/index.css +2 -43
  47. package/styles/themes/funder.css +76 -8
  48. package/styles/themes/investor.css +76 -8
  49. package/styles/themes/portare.css +90 -22
  50. package/styles/tokens.css +88 -23
  51. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,42 @@
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/Panel.tsx
6
+ /** Titled surface with optional eyebrow, help text and action. Renders a labelled `<section>`. */
7
+ function Panel({ title, eyebrow, help, action, headingLevel = 2, className, children, ...props }) {
8
+ const headingId = useId();
9
+ const Heading = `h${headingLevel}`;
10
+ return /* @__PURE__ */ jsxs("section", {
11
+ "aria-labelledby": headingId,
12
+ className: cn("fds-panel", className),
13
+ ...props,
14
+ children: [/* @__PURE__ */ jsxs("header", {
15
+ className: "fds-panel__header",
16
+ children: [/* @__PURE__ */ jsxs("div", {
17
+ className: "fds-panel__heading",
18
+ children: [
19
+ eyebrow ? /* @__PURE__ */ jsx("p", {
20
+ className: "fds-eyebrow",
21
+ children: eyebrow
22
+ }) : null,
23
+ /* @__PURE__ */ jsx(Heading, {
24
+ id: headingId,
25
+ className: "fds-panel__title",
26
+ children: title
27
+ }),
28
+ help ? /* @__PURE__ */ jsx("p", {
29
+ className: "fds-panel__help",
30
+ children: help
31
+ }) : null
32
+ ]
33
+ }), action ? /* @__PURE__ */ jsx("div", {
34
+ className: "fds-panel__action",
35
+ children: action
36
+ }) : null]
37
+ }), children]
38
+ });
39
+ }
40
+ export { Panel };
41
+
42
+ //# sourceMappingURL=Panel.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Panel.mjs","names":[],"sources":["../../src/components/Panel.tsx"],"sourcesContent":["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"],"mappings":";;;;;;AAcA,SAAgB,MAAM,EACpB,OACA,SACA,MACA,QACA,eAAe,GACf,WACA,UACA,GAAG,SACU;CACb,MAAM,YAAY,MAAM;CACxB,MAAM,UAAU,IAAI;CACpB,OACE,qBAAC,WAAD;EAAS,mBAAiB;EAAW,WAAW,GAAG,aAAa,SAAS;EAAG,GAAI;EAAhF,UAAA,CACE,qBAAC,UAAD;GAAQ,WAAU;GAAlB,UAAA,CACE,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACG,UAAU,oBAAC,KAAD;MAAG,WAAU;MAAe,UAAA;KAAW,CAAA,IAAI;KACtD,oBAAC,SAAD;MAAS,IAAI;MAAW,WAAU;MAC/B,UAAA;KACM,CAAA;KACR,OAAO,oBAAC,KAAD;MAAG,WAAU;MAAmB,UAAA;KAAQ,CAAA,IAAI;IACjD;GACJ,CAAA,GAAA,SAAS,oBAAC,OAAD;IAAK,WAAU;IAAqB,UAAA;GAAY,CAAA,IAAI,IACxD;EACP,CAAA,GAAA,QACM;;AAEb"}
package/dist/index.d.mts CHANGED
@@ -1,125 +1,10 @@
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
1
+ import { cn } from "./lib/cn.mjs";
2
+ import { Button, ButtonProps } from "./components/Button.mjs";
3
+ import { Panel, PanelProps } from "./components/Panel.mjs";
4
+ import { KeyValue, KeyValueProps, Metric, MetricProps, Tone } from "./components/Metric.mjs";
5
+ import { Badge, BadgeProps } from "./components/Badge.mjs";
6
+ import { EmptyState, EmptyStateProps, Notice, NoticeProps } from "./components/Notice.mjs";
7
+ import { Field, FieldControlProps, FieldProps, Input, Select, Textarea } from "./components/Field.mjs";
8
+ import { DistributionItem, DistributionList, DistributionListProps, Meter, MeterProps, SegmentedMeter, SegmentedMeterProps } from "./components/Meter.mjs";
9
+ import { Menu, MenuItem, MenuProps } from "./components/Menu.mjs";
10
+ export { Badge, type BadgeProps, Button, type ButtonProps, type DistributionItem, DistributionList, type DistributionListProps, EmptyState, type EmptyStateProps, Field, type FieldControlProps, type FieldProps, Input, KeyValue, type KeyValueProps, Menu, type MenuItem, type MenuProps, Meter, type MeterProps, Metric, type MetricProps, Notice, type NoticeProps, Panel, type PanelProps, SegmentedMeter, type SegmentedMeterProps, Select, Textarea, type Tone, cn };
package/dist/index.mjs CHANGED
@@ -1,366 +1,10 @@
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
- }
1
+ import { cn } from "./lib/cn.mjs";
2
+ import { Button } from "./components/Button.mjs";
3
+ import { Panel } from "./components/Panel.mjs";
4
+ import { KeyValue, Metric } from "./components/Metric.mjs";
5
+ import { Badge } from "./components/Badge.mjs";
6
+ import { EmptyState, Notice } from "./components/Notice.mjs";
7
+ import { Field, Input, Select, Textarea } from "./components/Field.mjs";
8
+ import { DistributionList, Meter, SegmentedMeter } from "./components/Meter.mjs";
9
+ import { Menu } from "./components/Menu.mjs";
364
10
  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,2 @@
1
+ export declare function cn(...classes: Array<string | false | null | undefined>): string;
2
+ //# sourceMappingURL=cn.d.mts.map
@@ -0,0 +1,7 @@
1
+ // src/lib/cn.ts
2
+ function cn(...classes) {
3
+ return classes.filter(Boolean).join(" ");
4
+ }
5
+ export { cn };
6
+
7
+ //# sourceMappingURL=cn.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cn.mjs","names":[],"sources":["../../src/lib/cn.ts"],"sourcesContent":["export function cn(...classes: Array<string | false | null | undefined>): string {\n return classes.filter(Boolean).join(\" \");\n}\n"],"mappings":";AAAA,SAAgB,GAAG,GAAG,SAA2D;CAC/E,OAAO,QAAQ,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;AACzC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scttlnd/fintech-design-system",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared design foundations for Funder, Portare and Investor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,20 +35,35 @@
35
35
  "build": "vp pack",
36
36
  "check": "vp check",
37
37
  "test": "vp test --run",
38
+ "typecheck": "tsc --noEmit",
38
39
  "changeset": "changeset",
39
40
  "version": "changeset version && vp check --fix",
40
41
  "release": "changeset publish",
41
- "prepublishOnly": "npm run build"
42
+ "prepublishOnly": "npm run build",
43
+ "showcase:dev": "vp dev --config showcase/vite.config.ts",
44
+ "showcase:build": "vp build --config showcase/vite.config.ts",
45
+ "test:browser": "playwright test",
46
+ "test:visual": "FDS_VISUAL=1 playwright test",
47
+ "test:visual:update": "FDS_VISUAL=1 playwright test --update-snapshots",
48
+ "test:package": "node scripts/check-build.mjs && node scripts/check-size.mjs",
49
+ "smoke:next": "node scripts/smoke-next.mjs",
50
+ "smoke:next:browser": "node scripts/verify-next.mjs"
42
51
  },
43
52
  "devDependencies": {
44
53
  "@changesets/changelog-github": "^1.0.1",
45
54
  "@changesets/cli": "^3.0.3",
55
+ "@playwright/test": "^1.65.0",
56
+ "@testing-library/react": "^16.3.3",
57
+ "@testing-library/user-event": "^14.6.8",
46
58
  "@types/react": "^18.3.18",
47
59
  "@types/react-dom": "^18.3.7",
60
+ "axe-core": "^4.14.0",
61
+ "happy-dom": "^21.0.4",
48
62
  "react": "^18.3.1",
49
63
  "react-dom": "^18.3.1",
50
64
  "typescript": "^5.7.3",
51
- "vite-plus": "^1.5.0"
65
+ "vite-plus": "^1.5.0",
66
+ "vitest-axe": "^0.1.0"
52
67
  },
53
68
  "peerDependencies": {
54
69
  "react": "^18.3.0 || ^19.0.0"
@@ -0,0 +1,33 @@
1
+ @layer fds {
2
+ /* Badge */
3
+ .fds-badge {
4
+ --fds-badge-color: var(--fds-muted-strong, var(--fds-foreground));
5
+ display: inline-flex;
6
+ align-items: center;
7
+ gap: 0.375rem;
8
+ min-height: 1.75rem;
9
+ padding: 0 0.5rem;
10
+ border: 1px solid color-mix(in srgb, var(--fds-badge-color) 30%, transparent);
11
+ border-radius: var(--fds-radius-sm);
12
+ background: color-mix(in srgb, var(--fds-badge-color) 12%, transparent);
13
+ color: var(--fds-badge-color);
14
+ font-size: 0.75rem;
15
+ font-weight: 500;
16
+ white-space: nowrap;
17
+ }
18
+ .fds-badge--accent {
19
+ --fds-badge-color: var(--fds-accent);
20
+ }
21
+ .fds-badge--success {
22
+ --fds-badge-color: var(--fds-success);
23
+ }
24
+ .fds-badge--warn {
25
+ --fds-badge-color: var(--fds-warn);
26
+ }
27
+ .fds-badge--danger {
28
+ --fds-badge-color: var(--fds-danger);
29
+ }
30
+ .fds-badge__icon {
31
+ display: inline-flex;
32
+ }
33
+ }